node-red-contrib-homebridge-automation 0.1.12-beta.19 → 0.1.12-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/hbBaseNode.js +63 -328
- package/src/hbConfigNode.js +44 -130
- package/src/hbControlNode.js +37 -58
- package/test/node-red/flows.json +17 -0
package/package.json
CHANGED
package/src/hbBaseNode.js
CHANGED
|
@@ -2,425 +2,160 @@ const debug = require('debug')('hapNodeRed:hbBaseNode');
|
|
|
2
2
|
|
|
3
3
|
class HbBaseNode {
|
|
4
4
|
constructor(config, RED) {
|
|
5
|
-
debug("
|
|
6
|
-
// RED.nodes.createNode(this, config);
|
|
5
|
+
debug("Constructor:", config);
|
|
7
6
|
RED.nodes.createNode(this, config);
|
|
7
|
+
|
|
8
8
|
if (!config.conf) {
|
|
9
|
-
|
|
9
|
+
this.error(`Warning: ${config.type} @ (${config.x}, ${config.y}) not connected to a HB Configuration Node`);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
// console.log("HbBaseNode - conf", this.conf);
|
|
11
|
+
|
|
13
12
|
this.config = config;
|
|
13
|
+
this.hbConfigNode = RED.nodes.getNode(config.conf);
|
|
14
14
|
this.confId = config.conf;
|
|
15
15
|
this.device = config.device;
|
|
16
16
|
this.service = config.Service;
|
|
17
17
|
this.name = config.name;
|
|
18
18
|
this.fullName = `${config.name} - ${config.Service}`;
|
|
19
|
-
|
|
20
19
|
this.hbDevice = null;
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
this.hbConfigNode?.register(this);
|
|
22
|
+
|
|
22
23
|
if (this.handleInput) {
|
|
23
24
|
this.on('input', this.handleInput.bind(this));
|
|
24
25
|
}
|
|
25
26
|
this.on('close', this.handleClose.bind(this));
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
* Common logic for registering the node
|
|
30
|
-
*/
|
|
31
29
|
registerNode() {
|
|
32
|
-
debug("
|
|
33
|
-
|
|
30
|
+
debug("Registering node:", this.fullName);
|
|
34
31
|
this.hbDevice = hbDevices.findDevice(this.device);
|
|
35
32
|
|
|
36
|
-
if (this.hbDevice) {
|
|
37
|
-
this.
|
|
33
|
+
if (!this.hbDevice) {
|
|
34
|
+
this.error(`Device not found: ${this.device}`);
|
|
38
35
|
} else {
|
|
39
|
-
this.
|
|
36
|
+
this.deviceType = this.hbDevice.deviceType;
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
/**
|
|
44
|
-
* Common logic for handling the close event
|
|
45
|
-
* @param {Function} callback - Callback to be executed on close
|
|
46
|
-
*/
|
|
47
40
|
handleClose(callback) {
|
|
48
41
|
callback();
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* @param {*} hbMessage
|
|
54
|
-
* @param {*} node
|
|
55
|
-
* @returns
|
|
56
|
-
*/
|
|
57
|
-
_convertHBcharactericToNode(hbMessage, node) {
|
|
58
|
-
// debug("_convertHBcharactericToNode", node.device);
|
|
59
|
-
var payload = {};
|
|
44
|
+
_convertHBcharacteristicToNode(hbMessage, node) {
|
|
45
|
+
let payload = {};
|
|
60
46
|
if (!hbMessage.payload) {
|
|
61
|
-
|
|
62
|
-
// debug("Device", device);
|
|
63
|
-
|
|
64
|
-
// characteristics = Object.assign(characteristics, characteristic.characteristic);
|
|
47
|
+
const device = hbDevices.findDevice(node.device);
|
|
65
48
|
if (device) {
|
|
66
|
-
hbMessage.forEach(
|
|
67
|
-
|
|
68
|
-
if (device.characteristics[
|
|
69
|
-
payload =
|
|
70
|
-
[device.characteristics[characteristic.aid + '.' + characteristic.iid].characteristic]: characteristic.value
|
|
71
|
-
});
|
|
49
|
+
hbMessage.forEach(characteristic => {
|
|
50
|
+
const charKey = `${characteristic.aid}.${characteristic.iid}`;
|
|
51
|
+
if (device.characteristics[charKey]) {
|
|
52
|
+
payload[device.characteristics[charKey].characteristic] = characteristic.value;
|
|
72
53
|
}
|
|
73
54
|
});
|
|
74
55
|
}
|
|
75
56
|
} else {
|
|
76
57
|
payload = hbMessage.payload;
|
|
77
58
|
}
|
|
78
|
-
|
|
79
|
-
return (payload);
|
|
59
|
+
return payload;
|
|
80
60
|
}
|
|
81
61
|
|
|
82
|
-
/**
|
|
83
|
-
* Create a HB ontrol message for a device
|
|
84
|
-
* @param {*} payload
|
|
85
|
-
* @param {*} node
|
|
86
|
-
* @param {*} device
|
|
87
|
-
* @returns
|
|
88
|
-
*/
|
|
89
62
|
_createControlMessage(payload, node, device) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (var key in payload) {
|
|
95
|
-
// debug("IID", key, _getKey(device.characteristics, key));
|
|
96
|
-
if (this._getKey(device.characteristics, key)) {
|
|
63
|
+
const response = [];
|
|
64
|
+
for (const key in payload) {
|
|
65
|
+
const characteristic = this._getKey(device.characteristics, key);
|
|
66
|
+
if (characteristic) {
|
|
97
67
|
response.push({
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
68
|
+
aid: device.aid,
|
|
69
|
+
iid: characteristic.iid,
|
|
70
|
+
value: payload[key],
|
|
101
71
|
});
|
|
102
72
|
} else {
|
|
103
|
-
this.warn(
|
|
104
|
-
node.status({
|
|
105
|
-
text: 'warn - Invalid Characteristic ' + key,
|
|
106
|
-
shape: 'ring',
|
|
107
|
-
fill: 'yellow'
|
|
108
|
-
});
|
|
73
|
+
this.warn(`Invalid characteristic: '${key}'. Available: ${device.descriptions}`);
|
|
74
|
+
node.status({ text: `Invalid characteristic: ${key}`, shape: 'ring', fill: 'yellow' });
|
|
109
75
|
}
|
|
110
76
|
}
|
|
111
|
-
return
|
|
112
|
-
"characteristics": response
|
|
113
|
-
});
|
|
77
|
+
return { characteristics: response };
|
|
114
78
|
}
|
|
115
79
|
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Return the status of a device
|
|
119
|
-
* @param {*} nrDevice
|
|
120
|
-
* @param {*} node
|
|
121
|
-
* @param {*} perms
|
|
122
|
-
* @returns
|
|
123
|
-
*/
|
|
124
80
|
async _status(nrDevice, node, perms) {
|
|
125
|
-
debug("_status", nrDevice);
|
|
126
|
-
let error;
|
|
127
81
|
try {
|
|
128
|
-
if (!this.hbDevices) {
|
|
129
|
-
throw new Error('_status hbDevices not initialized');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
82
|
const device = hbDevices.findDevice(node.device, perms);
|
|
133
|
-
if (device) {
|
|
134
|
-
let status, message;
|
|
135
|
-
switch (device.type) {
|
|
136
|
-
case "00000110": // Camera RTPStream Management
|
|
137
|
-
case "00000111": // Camera Control
|
|
138
|
-
message = {
|
|
139
|
-
"resource-type": "image",
|
|
140
|
-
"image-width": 1920,
|
|
141
|
-
"image-height": 1080
|
|
142
|
-
};
|
|
143
|
-
debug("_status Control %s -> %s", device.id, JSON.stringify(message));
|
|
144
|
-
|
|
145
|
-
// Await the result of HAPresourceByDeviceIDAsync
|
|
146
|
-
status = await this.HAPresourceByDeviceIDAsync(device.id, JSON.stringify(message));
|
|
147
|
-
|
|
148
|
-
debug("_status Controlled %s:%s ->", device.host, device.port);
|
|
149
|
-
node.status({
|
|
150
|
-
text: 'sent',
|
|
151
|
-
shape: 'dot',
|
|
152
|
-
fill: 'green'
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
clearTimeout(node.timeout);
|
|
156
|
-
node.timeout = setTimeout(() => {
|
|
157
|
-
node.status({});
|
|
158
|
-
}, 30 * 1000);
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
characteristics: {
|
|
162
|
-
payload: btoa(status)
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
default:
|
|
167
|
-
message = '?id=' + device.getCharacteristics;
|
|
168
|
-
debug("_status request: %s -> %s:%s ->", node.fullName, device.id, message);
|
|
169
|
-
|
|
170
|
-
// Await the result of HAPstatusByDeviceIDAsync
|
|
171
|
-
status = await this.HAPstatusByDeviceIDAsync(device.id, message);
|
|
83
|
+
if (!device) throw new Error(`Device not found: ${nrDevice}`);
|
|
172
84
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
fill: 'green'
|
|
177
|
-
});
|
|
85
|
+
const message = device.type === "00000110" || device.type === "00000111"
|
|
86
|
+
? { "resource-type": "image", "image-width": 1920, "image-height": 1080 }
|
|
87
|
+
: `?id=${device.getCharacteristics}`;
|
|
178
88
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}, 30 * 1000);
|
|
89
|
+
const status = device.type === "00000110" || device.type === "00000111"
|
|
90
|
+
? await this.HAPresourceByDeviceIDAsync(device.id, JSON.stringify(message))
|
|
91
|
+
: await this.HAPstatusByDeviceIDAsync(device.id, message);
|
|
183
92
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
node.status({
|
|
189
|
-
text: 'Device not found',
|
|
190
|
-
shape: 'ring',
|
|
191
|
-
fill: 'red'
|
|
192
|
-
});
|
|
193
|
-
throw new Error(error);
|
|
194
|
-
}
|
|
93
|
+
node.status({ text: 'Success', shape: 'dot', fill: 'green' });
|
|
94
|
+
return device.type === "00000110" || device.type === "00000111"
|
|
95
|
+
? { characteristics: { payload: this.btoa(status) } }
|
|
96
|
+
: status;
|
|
195
97
|
} catch (err) {
|
|
196
98
|
debug("Error in _status:", err);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
text: error,
|
|
200
|
-
shape: 'ring',
|
|
201
|
-
fill: 'red'
|
|
202
|
-
});
|
|
203
|
-
// throw new Error(error);
|
|
99
|
+
node.status({ text: 'Error retrieving status', shape: 'ring', fill: 'red' });
|
|
100
|
+
throw err;
|
|
204
101
|
}
|
|
205
102
|
}
|
|
206
103
|
|
|
207
|
-
/**
|
|
208
|
-
* Control a HB Device
|
|
209
|
-
* @param {*} node
|
|
210
|
-
* @param {*} payload
|
|
211
|
-
* @returns
|
|
212
|
-
*/
|
|
213
104
|
async _control(node, payload) {
|
|
214
|
-
debug("_control", node, payload);
|
|
215
105
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const device = hbDevices.findDevice(node.device, {
|
|
221
|
-
perms: 'pw'
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
if (device) {
|
|
225
|
-
let message;
|
|
226
|
-
switch (device.type) {
|
|
227
|
-
case "00000110": // Camera RTPStream Management
|
|
228
|
-
case "00000111": // Camera Control
|
|
229
|
-
{
|
|
230
|
-
message = {
|
|
231
|
-
"resource-type": "image",
|
|
232
|
-
"image-width": 1920,
|
|
233
|
-
"image-height": 1080,
|
|
234
|
-
"aid": node.hbDevice.aid
|
|
235
|
-
};
|
|
236
|
-
debug("Control %s ->", device.id, node.fullName, JSON.stringify(message));
|
|
106
|
+
const device = hbDevices.findDevice(node.device, { perms: 'pw' });
|
|
107
|
+
if (!device) throw new Error('Device not available');
|
|
237
108
|
|
|
238
|
-
|
|
239
|
-
|
|
109
|
+
const message = typeof payload === "object"
|
|
110
|
+
? this._createControlMessage(payload, node, device)
|
|
111
|
+
: null;
|
|
240
112
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
clearTimeout(node.timeout);
|
|
248
|
-
node.timeout = setTimeout(() => {
|
|
249
|
-
node.status({});
|
|
250
|
-
}, 30 * 1000);
|
|
251
|
-
|
|
252
|
-
return status;
|
|
253
|
-
}
|
|
254
|
-
default:
|
|
255
|
-
if (typeof payload === "object") {
|
|
256
|
-
message = this._createControlMessage.call(this, payload, node, device);
|
|
257
|
-
debug("Control %s ->", device.id, JSON.stringify(message));
|
|
258
|
-
|
|
259
|
-
if (message.characteristics.length > 0) {
|
|
260
|
-
// Await the result of HAPcontrolByDeviceIDAsync
|
|
261
|
-
const status = await this.HAPcontrolByDeviceIDAsync(device.id, JSON.stringify(message));
|
|
262
|
-
|
|
263
|
-
if (status && status.characteristics[0].status === 0) {
|
|
264
|
-
debug("Controlled %s ->", device.id, JSON.stringify(status));
|
|
265
|
-
node.status({
|
|
266
|
-
text: JSON.stringify(payload).slice(0, 30) + '...',
|
|
267
|
-
shape: 'dot',
|
|
268
|
-
fill: 'green'
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
clearTimeout(node.timeout);
|
|
272
|
-
node.timeout = setTimeout(() => {
|
|
273
|
-
node.status({});
|
|
274
|
-
}, 10 * 1000);
|
|
275
|
-
|
|
276
|
-
return; // Successful control, no error
|
|
277
|
-
} else {
|
|
278
|
-
debug("Controlled %s ->", device.id, payload);
|
|
279
|
-
node.status({
|
|
280
|
-
text: JSON.stringify(payload).slice(0, 30) + '...',
|
|
281
|
-
shape: 'dot',
|
|
282
|
-
fill: 'green'
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
clearTimeout(node.timeout);
|
|
286
|
-
node.timeout = setTimeout(() => {
|
|
287
|
-
node.status({});
|
|
288
|
-
}, 10 * 1000);
|
|
289
|
-
|
|
290
|
-
return; // Status controlled, no error
|
|
291
|
-
}
|
|
292
|
-
} else {
|
|
293
|
-
throw new Error('Invalid payload');
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
throw new Error("Payload should be a JSON object containing device characteristics and values.");
|
|
297
|
-
}
|
|
298
|
-
}
|
|
113
|
+
if (message && message.characteristics.length > 0) {
|
|
114
|
+
const status = await this.HAPcontrolByDeviceIDAsync(device.id, JSON.stringify(message));
|
|
115
|
+
node.status({ text: 'Controlled', shape: 'dot', fill: 'green' });
|
|
116
|
+
return status;
|
|
299
117
|
} else {
|
|
300
|
-
throw new Error('
|
|
118
|
+
throw new Error('Invalid payload');
|
|
301
119
|
}
|
|
302
120
|
} catch (err) {
|
|
303
121
|
debug("Error in _control:", err);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
text: error,
|
|
307
|
-
shape: 'ring',
|
|
308
|
-
fill: 'red'
|
|
309
|
-
});
|
|
310
|
-
// throw new Error(error);
|
|
122
|
+
node.status({ text: 'Control error', shape: 'ring', fill: 'red' });
|
|
123
|
+
throw err;
|
|
311
124
|
}
|
|
312
125
|
}
|
|
313
126
|
|
|
314
127
|
async _register(node) {
|
|
315
|
-
debug("_register", node.device);
|
|
316
128
|
try {
|
|
317
|
-
debug("_register", node.device);
|
|
318
129
|
const device = hbDevices.findDevice(node.device, { perms: 'ev' });
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"characteristics": device.eventRegisters
|
|
323
|
-
};
|
|
324
|
-
debug("_register", node.fullName, device.id, message);
|
|
325
|
-
|
|
326
|
-
// Use the shared async function here
|
|
327
|
-
const status = await hapEventByDeviceIDAsync(device.id, JSON.stringify(message));
|
|
328
|
-
|
|
329
|
-
// Check the result of the operation
|
|
330
|
-
if (status === null) {
|
|
331
|
-
debug("%s registered: %s -> %s", node.type, node.fullName, device.id);
|
|
332
|
-
} else {
|
|
333
|
-
debug("%s registered: %s -> %s", node.type, node.fullName, device.id, JSON.stringify(status));
|
|
334
|
-
}
|
|
130
|
+
if (device) {
|
|
131
|
+
const message = { characteristics: device.eventRegisters };
|
|
132
|
+
await hapEventByDeviceIDAsync(device.id, JSON.stringify(message));
|
|
335
133
|
}
|
|
336
134
|
} catch (err) {
|
|
337
|
-
// Handle errors that occur in the async function
|
|
338
135
|
debug("Error in _register:", err);
|
|
339
|
-
|
|
340
|
-
node.status({
|
|
341
|
-
text: 'error',
|
|
342
|
-
shape: 'ring',
|
|
343
|
-
fill: 'red'
|
|
344
|
-
});
|
|
136
|
+
node.status({ text: 'Register error', shape: 'ring', fill: 'red' });
|
|
345
137
|
}
|
|
346
138
|
}
|
|
347
139
|
|
|
348
|
-
_getObjectDiff(obj1, obj2) {
|
|
349
|
-
const diff = Object.keys(obj1).reduce((result, key) => {
|
|
350
|
-
if (!obj2.hasOwnProperty(key)) {
|
|
351
|
-
result.push(key);
|
|
352
|
-
} else if (obj1[key] === obj2[key]) {
|
|
353
|
-
const resultKeyIndex = result.indexOf(key);
|
|
354
|
-
result.splice(resultKeyIndex, 1);
|
|
355
|
-
}
|
|
356
|
-
return result;
|
|
357
|
-
}, Object.keys(obj2));
|
|
358
|
-
|
|
359
|
-
return diff;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
140
|
_getKey(obj, value) {
|
|
363
|
-
|
|
364
|
-
// debug("%s === %s", obj[key].characteristic, value);
|
|
365
|
-
// debug("%s === %s", obj[key].characteristic.toLowerCase(), value.toLowerCase());
|
|
366
|
-
if (obj[key].characteristic.toLowerCase() === value.toLowerCase()) {
|
|
367
|
-
return obj[key];
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return null;
|
|
141
|
+
return Object.values(obj).find(char => char.characteristic.toLowerCase() === value.toLowerCase()) || null;
|
|
371
142
|
}
|
|
372
143
|
|
|
373
144
|
btoa(str) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (str instanceof Buffer) {
|
|
377
|
-
buffer = str;
|
|
378
|
-
} else {
|
|
379
|
-
buffer = Buffer.from(str.toString(), 'binary');
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return buffer.toString('base64');
|
|
145
|
+
return Buffer.from(str.toString(), 'binary').toString('base64');
|
|
383
146
|
}
|
|
384
147
|
|
|
385
|
-
|
|
386
|
-
// Helper function to promisify HAPresourceByDeviceID
|
|
387
148
|
async HAPresourceByDeviceIDAsync(deviceId, message) {
|
|
388
149
|
return new Promise((resolve, reject) => {
|
|
389
|
-
homebridge.HAPresourceByDeviceID(deviceId, message, (err, status) =>
|
|
390
|
-
if (err) {
|
|
391
|
-
reject(err);
|
|
392
|
-
} else {
|
|
393
|
-
resolve(status);
|
|
394
|
-
}
|
|
395
|
-
});
|
|
150
|
+
homebridge.HAPresourceByDeviceID(deviceId, message, (err, status) => err ? reject(err) : resolve(status));
|
|
396
151
|
});
|
|
397
152
|
}
|
|
398
153
|
|
|
399
|
-
// Helper function to promisify HAPstatusByDeviceID
|
|
400
154
|
async HAPstatusByDeviceIDAsync(deviceId, message) {
|
|
401
155
|
return new Promise((resolve, reject) => {
|
|
402
|
-
homebridge.HAPstatusByDeviceID(deviceId, message, (err, status) =>
|
|
403
|
-
if (err) {
|
|
404
|
-
reject(err);
|
|
405
|
-
} else {
|
|
406
|
-
resolve(status);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
156
|
+
homebridge.HAPstatusByDeviceID(deviceId, message, (err, status) => err ? reject(err) : resolve(status));
|
|
409
157
|
});
|
|
410
158
|
}
|
|
411
|
-
|
|
412
|
-
async hapEventByDeviceIDAsync(deviceId, message) {
|
|
413
|
-
return new Promise((resolve, reject) => {
|
|
414
|
-
homebridge.HAPeventByDeviceID(deviceId, message, (err, status) => {
|
|
415
|
-
if (err) {
|
|
416
|
-
reject(err);
|
|
417
|
-
} else {
|
|
418
|
-
resolve(status);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
159
|
}
|
|
425
160
|
|
|
426
161
|
module.exports = HbBaseNode;
|
package/src/hbConfigNode.js
CHANGED
|
@@ -1,47 +1,32 @@
|
|
|
1
1
|
const hbBaseNode = require('./hbBaseNode.js');
|
|
2
|
-
// const HAPNodeJSClient = require('hap-node-client').HAPNodeJSClient;
|
|
3
2
|
const { HapClient } = require('@homebridge/hap-client');
|
|
4
3
|
const debug = require('debug')('hapNodeRed:hbConfigNode');
|
|
5
4
|
const { Homebridges } = require('./lib/Homebridges.js');
|
|
6
5
|
const { Log } = require('./lib/logger.js');
|
|
7
|
-
|
|
8
|
-
const { manualSync } = require('rimraf');
|
|
6
|
+
const Queue = require('better-queue');
|
|
9
7
|
|
|
10
8
|
class HBConfigNode {
|
|
11
9
|
constructor(config, RED) {
|
|
12
10
|
if (!config.jest) {
|
|
13
11
|
RED.nodes.createNode(this, config);
|
|
14
|
-
this.username = config.username;
|
|
15
|
-
this.macAddress = config.macAddress || '';
|
|
16
|
-
this.password = this.password;
|
|
17
|
-
this.on('close', function () {
|
|
18
|
-
this.hbConf.close(); // Close any open connections
|
|
19
|
-
});
|
|
20
12
|
|
|
21
|
-
// console.log('HBConfNode', config);
|
|
22
13
|
this.username = config.username;
|
|
23
14
|
this.macAddress = config.macAddress || '';
|
|
24
|
-
// this.password = config.credentials.password;
|
|
25
15
|
this.users = {};
|
|
26
16
|
this.homebridge = null;
|
|
27
17
|
this.evDevices = [];
|
|
28
18
|
this.ctDevices = [];
|
|
29
19
|
this.hbDevices = [];
|
|
30
|
-
|
|
31
|
-
this.clientNodes = []; // An array of client nodes attached
|
|
32
|
-
|
|
20
|
+
this.clientNodes = [];
|
|
33
21
|
this.log = new Log(console, true);
|
|
34
22
|
|
|
35
|
-
this.reqisterQueue = new Queue((
|
|
36
|
-
// debug('Queue execute', clientNode);
|
|
37
|
-
this._register(clientNode, cb);
|
|
38
|
-
}, {
|
|
23
|
+
this.reqisterQueue = new Queue(this._register.bind(this), {
|
|
39
24
|
concurrent: 1,
|
|
40
25
|
autoResume: false,
|
|
41
26
|
maxRetries: 1000,
|
|
42
27
|
retryDelay: 30000,
|
|
43
28
|
batchDelay: 2000,
|
|
44
|
-
batchSize: 150
|
|
29
|
+
batchSize: 150,
|
|
45
30
|
});
|
|
46
31
|
this.reqisterQueue.pause();
|
|
47
32
|
|
|
@@ -52,33 +37,28 @@ class HBConfigNode {
|
|
|
52
37
|
});
|
|
53
38
|
|
|
54
39
|
this.waitForNoMoreDiscoveries();
|
|
55
|
-
this.hapClient.on('instance-discovered',
|
|
40
|
+
this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
41
|
+
|
|
42
|
+
this.on('close', () => {
|
|
43
|
+
this.close();
|
|
44
|
+
});
|
|
56
45
|
}
|
|
57
46
|
}
|
|
58
47
|
|
|
59
48
|
waitForNoMoreDiscoveries = () => {
|
|
60
|
-
// Clear any existing timeout
|
|
61
49
|
if (this.discoveryTimeout) {
|
|
62
50
|
clearTimeout(this.discoveryTimeout);
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
// Set up the timeout
|
|
66
53
|
this.discoveryTimeout = setTimeout(() => {
|
|
67
54
|
this.log.debug('No more instances discovered, publishing services');
|
|
68
55
|
this.hapClient.removeListener('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
69
|
-
// debug('waitfornomore', this);
|
|
70
56
|
this.handleReady();
|
|
71
|
-
// this.requestSync();
|
|
72
|
-
// this.hapClient.on('instance-discovered', this.requestSync.bind(this)); // Request sync on new instance discovery
|
|
73
57
|
}, 5000);
|
|
74
58
|
};
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.hbDevices = await this.hapClient.getAllServices(accessories);
|
|
79
|
-
// debug('handleReady', JSON.stringify(this.hbDevices, null, 2));
|
|
80
|
-
debug('Discovered %s new evDevices', this.toList({ perms: 'ev' }).length);
|
|
81
|
-
|
|
60
|
+
async handleReady() {
|
|
61
|
+
this.hbDevices = await this.hapClient.getAllServices();
|
|
82
62
|
this.evDevices = this.toList({ perms: 'ev' });
|
|
83
63
|
this.ctDevices = this.toList({ perms: 'pw' });
|
|
84
64
|
this.handleDuplicates(this.evDevices);
|
|
@@ -86,75 +66,32 @@ class HBConfigNode {
|
|
|
86
66
|
this.reqisterQueue.resume();
|
|
87
67
|
}
|
|
88
68
|
|
|
89
|
-
|
|
90
|
-
|
|
91
69
|
toList(perms) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
case 'Television':
|
|
116
|
-
case 'Temperature Sensor':
|
|
117
|
-
case 'Thermostat':
|
|
118
|
-
case 'Contact Sensor':
|
|
119
|
-
return true;
|
|
120
|
-
case 'Camera Operating Mode':
|
|
121
|
-
case 'Camera Rtp Stream Management':
|
|
122
|
-
case 'Protocol Information':
|
|
123
|
-
|
|
124
|
-
return false;
|
|
125
|
-
default:
|
|
126
|
-
debug('Unsupport HomeKit Service Type \'%s\':', service.humanType);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// debug('toList', this.hbDevices);
|
|
131
|
-
return this.hbDevices.filter(service => supportedServiceType(service)).map(service => ({
|
|
132
|
-
name: `${service.serviceName}`,
|
|
133
|
-
fullName: `${service.serviceName} - ${service.type}`,
|
|
134
|
-
sortName: `${service.serviceName}:${service.type}`,
|
|
135
|
-
uniqueId: `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`, homebridge: `${service.instance.name}`,
|
|
136
|
-
service: `${service.type}`,
|
|
137
|
-
manufacturer: `${service.accessoryInformation.Manufacturer}`
|
|
138
|
-
})).sort((a, b) => (a.sortName > b.sortName) ? 1 : ((b.sortName > a.sortName) ? -1 : 0));;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Start processing
|
|
142
|
-
*/
|
|
143
|
-
async start() {
|
|
144
|
-
this.services = await this.loadAccessories();
|
|
145
|
-
this.log.info(`Discovered ${this.services.length} accessories`);
|
|
146
|
-
this.ready = true;
|
|
147
|
-
await this.buildSyncResponse();
|
|
148
|
-
const evServices = this.services.filter(x => this.evTypes.some(uuid => x.serviceCharacteristics.find(c => c.uuid === uuid)));
|
|
149
|
-
this.log.debug(`Monitoring ${evServices.length} services for changes`);
|
|
150
|
-
|
|
151
|
-
const monitor = await this.hapClient.monitorCharacteristics(evServices);
|
|
152
|
-
monitor.on('service-update', (services) => {
|
|
153
|
-
this.reportStateSubject.next(services[0].uniqueId);
|
|
154
|
-
});
|
|
70
|
+
const supportedServiceType = (service) => {
|
|
71
|
+
const supportedTypes = [
|
|
72
|
+
'Battery', 'Carbon Dioxide Sensor', 'Carbon Monoxide Sensor', 'Doorbell',
|
|
73
|
+
'Fan', 'Fanv2', 'Garage Door Opener', 'Humidity Sensor', 'Input Source',
|
|
74
|
+
'Leak Sensor', 'Lightbulb', 'Lock Mechanism', 'Motion Sensor', 'Occupancy Sensor',
|
|
75
|
+
'Outlet', 'Smoke Sensor', 'Speaker', 'Stateless Programmable Switch', 'Switch',
|
|
76
|
+
'Television', 'Temperature Sensor', 'Thermostat', 'Contact Sensor',
|
|
77
|
+
];
|
|
78
|
+
return supportedTypes.includes(service.humanType);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return this.hbDevices
|
|
82
|
+
.filter(service => supportedServiceType(service))
|
|
83
|
+
.map(service => ({
|
|
84
|
+
name: service.serviceName,
|
|
85
|
+
fullName: `${service.serviceName} - ${service.type}`,
|
|
86
|
+
sortName: `${service.serviceName}:${service.type}`,
|
|
87
|
+
uniqueId: `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`,
|
|
88
|
+
homebridge: service.instance.name,
|
|
89
|
+
service: service.type,
|
|
90
|
+
manufacturer: service.accessoryInformation.Manufacturer,
|
|
91
|
+
}))
|
|
92
|
+
.sort((a, b) => a.sortName.localeCompare(b.sortName));
|
|
155
93
|
}
|
|
156
94
|
|
|
157
|
-
// Handle duplicate devices
|
|
158
95
|
handleDuplicates(list) {
|
|
159
96
|
const seenFullNames = new Set();
|
|
160
97
|
const seenUniqueIds = new Set();
|
|
@@ -165,9 +102,7 @@ class HBConfigNode {
|
|
|
165
102
|
} else {
|
|
166
103
|
seenFullNames.add(endpoint.fullName);
|
|
167
104
|
}
|
|
168
|
-
}
|
|
169
105
|
|
|
170
|
-
for (const endpoint of list) {
|
|
171
106
|
if (seenUniqueIds.has(endpoint.uniqueId)) {
|
|
172
107
|
console.error('ERROR: Parsing failed, duplicate uniqueID.', endpoint.fullName);
|
|
173
108
|
} else {
|
|
@@ -176,62 +111,41 @@ class HBConfigNode {
|
|
|
176
111
|
}
|
|
177
112
|
}
|
|
178
113
|
|
|
179
|
-
// Register a device node
|
|
180
114
|
register(clientNode) {
|
|
181
|
-
// debug('hbConf.register', clientNode);
|
|
182
115
|
debug('Register %s -> %s', clientNode.type, clientNode.name);
|
|
183
116
|
this.clientNodes[clientNode.id] = clientNode;
|
|
184
|
-
this.reqisterQueue.push(
|
|
185
|
-
|
|
186
|
-
);
|
|
117
|
+
this.reqisterQueue.push(clientNode);
|
|
118
|
+
clientNode.status({ fill: 'yellow', shape: 'ring', text: 'connecting' });
|
|
187
119
|
}
|
|
188
120
|
|
|
189
|
-
/**
|
|
190
|
-
* Process batched event registration messages
|
|
191
|
-
*/
|
|
192
121
|
async _register(clientNodes, cb) {
|
|
193
|
-
// debug('_register', clientNodes);
|
|
194
|
-
|
|
195
|
-
// debug('clientNodes', this.clientNodes);
|
|
196
|
-
|
|
197
122
|
for (const clientNode of clientNodes) {
|
|
198
123
|
debug('_Register %s -> %s', clientNode.type, clientNode.name);
|
|
199
124
|
clientNode.hbDevice = this.hbDevices.find(service => {
|
|
200
|
-
|
|
201
|
-
// console.log('clientNodeDevice', clientNode);
|
|
202
|
-
// debug('Testing:', { clientNodeDevice: clientNode.device, serviceName: service });
|
|
203
125
|
const testValue = `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`;
|
|
204
|
-
|
|
126
|
+
clientNode.status({ fill: 'green', shape: 'dot', text: 'connected' });
|
|
205
127
|
return clientNode.device === testValue;
|
|
206
128
|
});
|
|
207
|
-
|
|
129
|
+
|
|
208
130
|
if (!clientNode.hbDevice) {
|
|
209
|
-
console.
|
|
131
|
+
console.error('ERROR: _register - HB Device Missing', clientNode.name);
|
|
210
132
|
}
|
|
211
133
|
}
|
|
212
|
-
// const monitor = await this.hapClient.monitorCharacteristics(clientNodes);
|
|
213
|
-
// monitor.on('service-update', (services) => {
|
|
214
|
-
// debug('service-update', services);
|
|
215
|
-
// });
|
|
216
|
-
|
|
217
134
|
cb(null);
|
|
218
135
|
}
|
|
219
136
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
deviceNode.status({
|
|
137
|
+
deregister(clientNode) {
|
|
138
|
+
clientNode.status({
|
|
223
139
|
text: 'disconnected',
|
|
224
140
|
shape: 'ring',
|
|
225
141
|
fill: 'red',
|
|
226
142
|
});
|
|
227
|
-
|
|
228
|
-
this.clientNodes[clientNode.id] = {};
|
|
143
|
+
delete this.clientNodes[clientNode.id];
|
|
229
144
|
}
|
|
230
145
|
|
|
231
|
-
// Clean up resources
|
|
232
146
|
close() {
|
|
233
|
-
if (this.
|
|
234
|
-
this.
|
|
147
|
+
if (this.hapClient) {
|
|
148
|
+
this.hapClient.close();
|
|
235
149
|
}
|
|
236
150
|
}
|
|
237
151
|
}
|
package/src/hbControlNode.js
CHANGED
|
@@ -4,75 +4,54 @@ const debug = require('debug')('hapNodeRed:hbControlNode');
|
|
|
4
4
|
class HbControlNode extends hbBaseNode {
|
|
5
5
|
constructor(config, RED) {
|
|
6
6
|
super(config, RED);
|
|
7
|
-
|
|
8
|
-
// Register the node-specific input and close handlers
|
|
9
|
-
// this.on('input', this.handleInput.bind(this));
|
|
10
|
-
// Register the node with the configuration
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
// Handle input messages
|
|
14
9
|
async handleInput(message) {
|
|
15
|
-
debug('handleInput', message, this.
|
|
16
|
-
if (this.hbDevice) {
|
|
17
|
-
var results = [];
|
|
18
|
-
if (typeof message.payload === "object") {
|
|
19
|
-
var fill = 'green';
|
|
20
|
-
for (const key of Object.keys(message.payload)) {
|
|
21
|
-
const characteristic = this.hbDevice.serviceCharacteristics.find(
|
|
22
|
-
c => c.type === key
|
|
23
|
-
);
|
|
10
|
+
debug('handleInput', message.payload, this.name);
|
|
24
11
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.error('Invalid Characteristic \'' + key + '\' found in the message ' + JSON.stringify(message));
|
|
31
|
-
results.push({ 'Invalid Key': key });
|
|
32
|
-
fill = 'red';
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
this.status({
|
|
36
|
-
text: JSON.stringify(Object.assign({}, ...results)),
|
|
37
|
-
shape: 'dot',
|
|
38
|
-
fill: fill
|
|
39
|
-
});
|
|
12
|
+
if (!this.hbDevice) {
|
|
13
|
+
this.error('HB not initialized');
|
|
14
|
+
this.status({ text: 'HB not initialized', shape: 'ring', fill: 'red' });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
40
17
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
shape: 'ring',
|
|
50
|
-
fill: 'red'
|
|
51
|
-
});
|
|
18
|
+
if (typeof message.payload !== 'object') {
|
|
19
|
+
const validNames = Object.keys(this.hbDevice.values)
|
|
20
|
+
.filter(key => key !== 'ConfiguredName')
|
|
21
|
+
.join(', ');
|
|
22
|
+
this.error(`Payload should be a JSON object containing device characteristics and values, e.g. {"On":false, "Brightness":0}. Valid values: ${validNames}`);
|
|
23
|
+
this.status({ text: 'Invalid payload', shape: 'ring', fill: 'red' });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
52
26
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.error("HB not initialized");
|
|
56
|
-
this.status({
|
|
57
|
-
text: 'HB not initialized',
|
|
58
|
-
shape: 'ring',
|
|
59
|
-
fill: 'red',
|
|
60
|
-
});
|
|
27
|
+
const results = [];
|
|
28
|
+
let fill = 'green';
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
30
|
+
for (const key of Object.keys(message.payload)) {
|
|
31
|
+
const characteristic = this.hbDevice.serviceCharacteristics.find(c => c.type === key);
|
|
32
|
+
|
|
33
|
+
if (characteristic) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await characteristic.setValue(message.payload[key]);
|
|
36
|
+
results.push({ [result.type]: result.value });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
this.error(`Failed to set value for ${key}: ${error.message}`);
|
|
39
|
+
fill = 'red';
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
this.error(`Invalid characteristic '${key}' found in the message: ${JSON.stringify(message.payload)}`);
|
|
43
|
+
results.push({ 'Invalid Key': key });
|
|
44
|
+
fill = 'red';
|
|
70
45
|
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.status({
|
|
49
|
+
text: JSON.stringify(Object.assign({}, ...results)),
|
|
50
|
+
shape: 'dot',
|
|
51
|
+
fill,
|
|
71
52
|
});
|
|
72
|
-
*/
|
|
73
53
|
}
|
|
74
54
|
|
|
75
|
-
// Handle node closure
|
|
76
55
|
handleClose(callback) {
|
|
77
56
|
callback();
|
|
78
57
|
}
|
package/test/node-red/flows.json
CHANGED
|
@@ -402,5 +402,22 @@
|
|
|
402
402
|
"6703815a8874b156"
|
|
403
403
|
]
|
|
404
404
|
]
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
"id": "ab3d4add7178f1f1",
|
|
408
|
+
"type": "hb-event",
|
|
409
|
+
"z": "caef1e7b5b399e80",
|
|
410
|
+
"name": "",
|
|
411
|
+
"Homebridge": "",
|
|
412
|
+
"Manufacturer": "",
|
|
413
|
+
"Service": "",
|
|
414
|
+
"device": "",
|
|
415
|
+
"conf": "",
|
|
416
|
+
"sendInitialState": false,
|
|
417
|
+
"x": 890,
|
|
418
|
+
"y": 800,
|
|
419
|
+
"wires": [
|
|
420
|
+
[]
|
|
421
|
+
]
|
|
405
422
|
}
|
|
406
423
|
]
|