/
air-conditioner-api.js
188 lines (142 loc) · 6.1 KB
/
air-conditioner-api.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
const events = require('events');
const util = require('util');
const tls = require('tls');
const carrier = require('carrier');
const fs = require('fs');
const path = require('path');
const shortid = require('shortid');
const keepAlive = require('net-keepalive');
const port = 2878;
function AirConditionerApi(ipAddress, duid, token, log, logSocketActivity, keepAliveConfig) {
this.ipAddress = ipAddress;
this.duid = duid;
this.token = token;
this.log = log;
this.logSocketActivity = logSocketActivity;
const defaultKeepAliveConfig = {
"enabled": true,
"initial_delay": 10000,
"interval": 10000,
"probes": 10
}
this.keepAliveConfig = Object.assign({}, defaultKeepAliveConfig, keepAliveConfig);
log('Keep alive config:', this.keepAliveConfig);
this.authenticated = false;
};
AirConditionerApi.prototype = {
connect: function () {
this.log('Connecting...');
this.controlCallbacks = {};
const pfxPath = path.join(__dirname, '../res/ac14k_m.pfx')
const options = {
pfx: fs.readFileSync(pfxPath),
port: port,
host: this.ipAddress,
rejectUnauthorized: false,
ciphers: 'HIGH:!DH:!aNULL'
};
this.socket = tls.connect(options, function () {
this.log('Connected');
this.socket.setKeepAlive(this.keepAliveConfig.enabled, this.keepAliveConfig.initial_delay);
keepAlive.setKeepAliveInterval(this.socket, this.keepAliveConfig.interval);
keepAlive.setKeepAliveProbes(this.socket, this.keepAliveConfig.probes);
// All responses from AC are received here as lines
carrier.carry(this.socket, this._readLine.bind(this));
}.bind(this));
this.socket
.on('end', this._connectionEnded.bind(this))
.on('close', this._connectionClosed.bind(this))
.on('error', this._errorOccured.bind(this));
},
deviceControl: function (key, value, callback) {
if (!this.authenticated) {
callback(new Error('Connection not established'));
return;
}
// Create id for callback. It will be passed to request and returned by AC in response
// It allows us to match callbacks to responses received in `carrier.carry` callback above
const id = shortid.generate()
if (!!callback) {
this.controlCallbacks[id] = callback;
}
this._send(
'<Request Type="DeviceControl"><Control CommandID="' + id + '" DUID="' + this.duid + '"><Attr ID="' + key + '" Value="' + value + '" /></Control></Request>'
);
},
_send: function (line) {
if (this.logSocketActivity) { this.log('Write:', line); }
this.socket.write(line + "\r\n");
},
_readLine: function (line) {
if (this.logSocketActivity) { this.log('Read:', line); }
if (line.match(/Update Type="InvalidateAccount"/)) { // Returned in the beginning of connection. We need to send auth request with token.
this._handleInvalidateAccount();
} else if (line.match(/Response Type="AuthToken" Status="Okay"/)) { // Auth success
this._handleAuthSuccessResponse();
} else if (line.match(/Update Type="Status"/)) { // Status update received - AC sends them when some setting is changed via remote.
this._handleDeviceStatusUpdate(line);
} else if (line.match(/Response Type="DeviceState" Status="Okay"/)) { // Status response received - AC sends it after receiving request for status
this._handleDeviceStateResponse(line);
} else if (line.match(/Response Type="DeviceControl" Status="Okay"/)) { // Control confirmation received - AC sends it to confirm control success
this._handleDeviceControlResponse(line);
};
},
_handleInvalidateAccount: function() {
this.log("Auth request received - Authenticating...");
this._send('<Request Type="AuthToken"><User Token="' + this.token + '"/></Request>');
},
_handleAuthSuccessResponse: function() {
this.log("Authentication succeeded");
this.authenticated = true;
this.log("Requesting full state summary...");
// Request full state summary;
this._send('<Request Type="DeviceState" DUID="' + this.duid + '"></Request>');
},
_handleDeviceStateResponse: function (line) {
this.log("Full state summary received");
const attributes = line.split("><");
const state = {};
attributes.forEach(function (attr) {
if ((matches = attr.match(/Attr ID="(.*)" Type=".*" Value="(.*)"/))) {
const id = matches[1];
state[id] = matches[2];
}
}.bind(this));
this.emit('stateUpdate', state);
},
_handleDeviceControlResponse: function (line) {
if ((matches = line.match(/CommandID="(.*)"/))) {
id = matches[1];
if (!this.controlCallbacks[id]) return;
callback = this.controlCallbacks[id];
delete (this.controlCallbacks[id]);
callback(null);
}
},
_handleDeviceStatusUpdate: function(line) {
if ((matches = line.match(/Attr ID="(.*)" Value="(.*)"/))) {
const state = {};
state[matches[1]] = matches[2];
this.emit('stateUpdate', state);
}
},
_errorOccured: function(error) {
this.log('Error occured:', error.message);
// Error all callbacks
Object.keys(this.controlCallbacks).forEach(function(id) {
this.controlCallbacks[id](error);
}.bind(this));
this.controlCallbacks = {};
},
_connectionEnded: function () {
this.log('Connection ended');
},
_connectionClosed: function (hadError) {
this.authenticated = false;
this.log('Connection closed' + (hadError ? ' because error occured' : ''));
this.log('Trying to reconnect in 5s...');
setTimeout(this.connect.bind(this), 5000);
}
};
util.inherits(AirConditionerApi, events.EventEmitter);
module.exports = AirConditionerApi;