node-red-contrib-homebridge-automation 0.1.12-beta.34 → 0.1.12-beta.36
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 +2 -2
- package/src/hbBaseNode.js +17 -3
- package/src/hbConfigNode.js +94 -51
- package/src/hbControlNode.js +3 -3
- package/src/hbResumeNode.js +2 -2
- package/src/hbStatusNode.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-homebridge-automation",
|
|
3
|
-
"version": "0.1.12-beta.
|
|
3
|
+
"version": "0.1.12-beta.36",
|
|
4
4
|
"description": "NodeRED Automation for HomeBridge",
|
|
5
5
|
"main": "src/HAP-NodeRed.js",
|
|
6
6
|
"scripts": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"better-queue": ">=3.8.12",
|
|
48
48
|
"debug": "^4.3.5",
|
|
49
|
-
"@homebridge/hap-client": "2.0.5-beta.
|
|
49
|
+
"@homebridge/hap-client": "2.0.5-beta.18"
|
|
50
50
|
},
|
|
51
51
|
"author": "NorthernMan54",
|
|
52
52
|
"license": "ISC",
|
package/src/hbBaseNode.js
CHANGED
|
@@ -18,7 +18,7 @@ class HbBaseNode {
|
|
|
18
18
|
this.fullName = `${config.name} - ${config.Service}`;
|
|
19
19
|
this.hbDevice = null;
|
|
20
20
|
|
|
21
|
-
this.hbConfigNode?.
|
|
21
|
+
this.hbConfigNode?.registerClientNode(this);
|
|
22
22
|
|
|
23
23
|
if (this.handleInput) {
|
|
24
24
|
this.on('input', this.handleInput.bind(this));
|
|
@@ -61,20 +61,34 @@ class HbBaseNode {
|
|
|
61
61
|
statusText(message) {
|
|
62
62
|
return message.slice(0, 20)
|
|
63
63
|
}
|
|
64
|
+
|
|
64
65
|
/**
|
|
65
66
|
*
|
|
66
67
|
* @param {*} warning - Message to log and display in debug panel
|
|
67
68
|
* @param {*} statusText - Message to display under Node ( If not present, uses warning message text)
|
|
68
69
|
*/
|
|
69
|
-
|
|
70
|
+
handleWarning(warning, statusText) {
|
|
70
71
|
this.warn(warning);
|
|
71
72
|
this.status({
|
|
72
73
|
text: (statusText ? statusText : warning).slice(0, 20),
|
|
73
74
|
shape: 'ring',
|
|
74
|
-
fill: '
|
|
75
|
+
fill: 'yellow',
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
*
|
|
81
|
+
* @param {*} warning - Message to log and display in debug panel
|
|
82
|
+
* @param {*} statusText - Message to display under Node ( If not present, uses warning message text)
|
|
83
|
+
*/
|
|
84
|
+
handleError(error, statusText) {
|
|
85
|
+
this.error(error);
|
|
86
|
+
this.status({
|
|
87
|
+
text: (statusText ? statusText : error).slice(0, 20),
|
|
88
|
+
shape: 'ring',
|
|
89
|
+
fill: 'red',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
module.exports = HbBaseNode;
|
package/src/hbConfigNode.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const { HapClient } = require('@homebridge/hap-client');
|
|
2
2
|
const debug = require('debug')('hapNodeRed:hbConfigNode');
|
|
3
3
|
const { Log } = require('./lib/logger.js');
|
|
4
|
-
const Queue = require('better-queue');
|
|
5
4
|
|
|
6
5
|
class HBConfigNode {
|
|
7
6
|
constructor(config, RED) {
|
|
@@ -17,21 +16,9 @@ class HBConfigNode {
|
|
|
17
16
|
this.ctDevices = [];
|
|
18
17
|
this.hbDevices = [];
|
|
19
18
|
this.clientNodes = [];
|
|
20
|
-
this.monitorNodes = [];
|
|
21
19
|
this.log = new Log(console, true);
|
|
22
20
|
this.discoveryTimeout = null;
|
|
23
21
|
|
|
24
|
-
// Initialize queue
|
|
25
|
-
this.reqisterQueue = new Queue(this._register.bind(this), {
|
|
26
|
-
concurrent: 1,
|
|
27
|
-
autoResume: false,
|
|
28
|
-
maxRetries: 1000,
|
|
29
|
-
retryDelay: 30000,
|
|
30
|
-
batchDelay: 2000,
|
|
31
|
-
batchSize: 150,
|
|
32
|
-
});
|
|
33
|
-
this.reqisterQueue.pause();
|
|
34
|
-
|
|
35
22
|
// Initialize HAP client
|
|
36
23
|
this.hapClient = new HapClient({
|
|
37
24
|
config: { debug: true },
|
|
@@ -42,44 +29,73 @@ class HBConfigNode {
|
|
|
42
29
|
this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
43
30
|
this.waitForNoMoreDiscoveries();
|
|
44
31
|
this.on('close', this.close.bind(this));
|
|
32
|
+
this.refreshInProcess = true; // Prevents multiple refreshes, hapClient kicks of a discovery on start
|
|
45
33
|
}
|
|
46
34
|
}
|
|
47
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Start device discovery after monitor reports issues
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
refreshDevices = () => {
|
|
41
|
+
if (!this.refreshInProcess) {
|
|
42
|
+
|
|
43
|
+
this.monitor.finish();
|
|
44
|
+
this.log.debug('Monitor reported homebridge stability issues, refreshing devices');
|
|
45
|
+
this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
46
|
+
this.hapClient.resetInstancePool();
|
|
47
|
+
this.waitForNoMoreDiscoveries();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wait for no more instance discoveries to be made before publishing services
|
|
53
|
+
*/
|
|
48
54
|
waitForNoMoreDiscoveries = () => {
|
|
49
55
|
if (!this.discoveryTimeout) {
|
|
50
56
|
clearTimeout(this.discoveryTimeout);
|
|
51
57
|
this.discoveryTimeout = setTimeout(() => {
|
|
52
58
|
this.log.debug('No more instances discovered, publishing services');
|
|
53
59
|
this.hapClient.removeListener('instance-discovered', this.waitForNoMoreDiscoveries);
|
|
54
|
-
this.hapClient.on('instance-discovered', async (instance) => { debug('instance-discovered', instance); await this.monitorDevices(); });
|
|
55
|
-
this.hapClient.on('discovery-ended', async () => { debug('discovery-ended'); });
|
|
56
60
|
this.handleReady();
|
|
57
61
|
this.discoveryTimeout = null;
|
|
58
|
-
|
|
62
|
+
this.refreshInProcess = false;
|
|
63
|
+
}, 20000); // resetInstancePool() triggers a discovery after 6 seconds. Need to wait for it to finish.
|
|
59
64
|
}
|
|
60
65
|
};
|
|
61
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Populate the list of devices and handle duplicates
|
|
69
|
+
*/
|
|
62
70
|
async handleReady() {
|
|
63
|
-
|
|
71
|
+
const updatedDevices = await this.hapClient.getAllServices();
|
|
72
|
+
updatedDevices.forEach((updatedService, index) => {
|
|
73
|
+
if (this.hbDevices.find(service => service.uniqueId === updatedService.uniqueId)) {
|
|
74
|
+
const update = this.hbDevices.find(service => service.uniqueId === updatedService.uniqueId);
|
|
75
|
+
update.instance = updatedService.instance;
|
|
76
|
+
} else {
|
|
77
|
+
this.hbDevices.push(updatedService);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
64
81
|
this.evDevices = this.toList({ perms: 'ev' });
|
|
65
82
|
this.ctDevices = this.toList({ perms: 'pw' });
|
|
66
83
|
this.log.info(`Devices initialized: evDevices: ${this.evDevices.length}, ctDevices: ${this.ctDevices.length}`);
|
|
67
84
|
this.handleDuplicates(this.evDevices);
|
|
68
|
-
|
|
69
|
-
this.reqisterQueue.resume();
|
|
85
|
+
this.connectClientNodes();
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
toList(perms) {
|
|
73
|
-
const supportedTypes = [
|
|
74
|
-
'Battery', 'Carbon Dioxide Sensor', 'Carbon Monoxide Sensor', 'Camera Rtp Stream Management',
|
|
75
|
-
'Fan', 'Fanv2', 'Garage Door Opener', 'Humidity Sensor', 'Input Source',
|
|
89
|
+
const supportedTypes = new Set([
|
|
90
|
+
'Battery', 'Carbon Dioxide Sensor', 'Carbon Monoxide Sensor', 'Camera Rtp Stream Management',
|
|
91
|
+
'Doorbell', 'Fan', 'Fanv2', 'Garage Door Opener', 'Humidity Sensor', 'Input Source',
|
|
76
92
|
'Leak Sensor', 'Lightbulb', 'Lock Mechanism', 'Motion Sensor', 'Occupancy Sensor',
|
|
77
93
|
'Outlet', 'Smoke Sensor', 'Speaker', 'Stateless Programmable Switch', 'Switch',
|
|
78
94
|
'Television', 'Temperature Sensor', 'Thermostat', 'Contact Sensor',
|
|
79
|
-
];
|
|
95
|
+
]);
|
|
80
96
|
|
|
81
97
|
return filterUnique(this.hbDevices)
|
|
82
|
-
.filter(service => supportedTypes.
|
|
98
|
+
.filter(service => supportedTypes.has(service.humanType))
|
|
83
99
|
.map(service => ({
|
|
84
100
|
name: service.serviceName,
|
|
85
101
|
fullName: `${service.serviceName} - ${service.type}`,
|
|
@@ -92,31 +108,35 @@ class HBConfigNode {
|
|
|
92
108
|
.sort((a, b) => a.sortName.localeCompare(b.sortName));
|
|
93
109
|
}
|
|
94
110
|
|
|
111
|
+
|
|
95
112
|
handleDuplicates(list) {
|
|
96
|
-
const
|
|
97
|
-
const seenUniqueIds = new Set();
|
|
113
|
+
const seen = new Map();
|
|
98
114
|
|
|
99
115
|
list.forEach(endpoint => {
|
|
100
|
-
|
|
101
|
-
console.warn('WARNING: Duplicate device name', endpoint.fullName);
|
|
102
|
-
}
|
|
116
|
+
const { fullName, uniqueId } = endpoint;
|
|
103
117
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
118
|
+
if (seen.has(fullName)) {
|
|
119
|
+
this.log.warn(`Duplicate device name detected: ${fullName}`);
|
|
106
120
|
}
|
|
121
|
+
if (seen.has(uniqueId)) {
|
|
122
|
+
this.log.error(`Duplicate uniqueId detected: ${uniqueId}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
seen.set(fullName, true);
|
|
126
|
+
seen.set(uniqueId, true);
|
|
107
127
|
});
|
|
108
128
|
}
|
|
109
129
|
|
|
110
|
-
|
|
130
|
+
registerClientNode(clientNode) {
|
|
111
131
|
debug('Register: %s type: %s', clientNode.type, clientNode.name);
|
|
112
132
|
this.clientNodes[clientNode.id] = clientNode;
|
|
113
|
-
this.reqisterQueue.push(clientNode);
|
|
114
133
|
clientNode.status({ fill: 'yellow', shape: 'ring', text: 'connecting' });
|
|
115
134
|
}
|
|
116
135
|
|
|
117
|
-
async
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
async connectClientNodes() {
|
|
137
|
+
debug('connect %s nodes', Object.keys(this.clientNodes).length);
|
|
138
|
+
for (const [key, clientNode] of Object.entries(this.clientNodes)) {
|
|
139
|
+
// debug('_Register: %s type: %s', clientNode.type, clientNode.name, clientNode.instance);
|
|
120
140
|
const matchedDevice = this.hbDevices.find(service =>
|
|
121
141
|
clientNode.device === `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`
|
|
122
142
|
);
|
|
@@ -126,23 +146,32 @@ class HBConfigNode {
|
|
|
126
146
|
clientNode.status({ fill: 'green', shape: 'dot', text: 'connected' });
|
|
127
147
|
clientNode.emit('hbReady', matchedDevice);
|
|
128
148
|
debug('_Registered: %s type: %s', matchedDevice.type, matchedDevice.serviceName, matchedDevice.instance);
|
|
129
|
-
if (clientNode.config.type === 'hb-status' || clientNode.config.type === 'hb-event') {
|
|
130
|
-
this.monitorNodes[clientNode.device] = matchedDevice;
|
|
131
|
-
}
|
|
132
149
|
} else {
|
|
133
150
|
console.error('ERROR: Device registration failed', clientNode.name);
|
|
134
151
|
}
|
|
135
|
-
}
|
|
152
|
+
};
|
|
136
153
|
|
|
137
154
|
await this.monitorDevices();
|
|
138
|
-
|
|
139
|
-
cb(null);
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
async monitorDevices() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
if (Object.keys(this.clientNodes).length) {
|
|
159
|
+
const uniqueDevices = new Set();
|
|
160
|
+
|
|
161
|
+
const monitorNodes = Object.values(this.clientNodes)
|
|
162
|
+
.filter(node => ['hb-status', 'hb-control'].includes(node.type)) // Filter by type
|
|
163
|
+
.filter(node => {
|
|
164
|
+
if (uniqueDevices.has(node.device)) {
|
|
165
|
+
return false; // Exclude duplicates
|
|
166
|
+
}
|
|
167
|
+
uniqueDevices.add(node.device);
|
|
168
|
+
return true; // Include unique devices
|
|
169
|
+
})
|
|
170
|
+
.map(node => node.hbDevice) // Map to hbDevice property
|
|
171
|
+
.filter(Boolean); // Remove any undefined or null values, if present;
|
|
172
|
+
debug('monitorNodes', Object.keys(monitorNodes).length);
|
|
173
|
+
// console.log('monitorNodes', monitorNodes);
|
|
174
|
+
this.monitor = await this.hapClient.monitorCharacteristics(monitorNodes);
|
|
146
175
|
this.monitor.on('service-update', (services) => {
|
|
147
176
|
services.forEach(service => {
|
|
148
177
|
const eventNodes = Object.values(this.clientNodes).filter(clientNode =>
|
|
@@ -151,15 +180,29 @@ class HBConfigNode {
|
|
|
151
180
|
eventNodes.forEach(eventNode => eventNode.emit('hbEvent', service));
|
|
152
181
|
});
|
|
153
182
|
});
|
|
154
|
-
this.monitor.on('monitor-close', (hadError) => {
|
|
155
|
-
debug('monitor-close', hadError)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
this.monitor.on('monitor-close', (instance, hadError) => {
|
|
184
|
+
debug('monitor-close', instance.name, instance.ipAddress, instance.port, hadError)
|
|
185
|
+
this.disconnectClientNodes(instance);
|
|
186
|
+
this.refreshDevices();
|
|
187
|
+
})
|
|
188
|
+
this.monitor.on('monitor-error', (instance, hadError) => {
|
|
189
|
+
debug('monitor-error', instance, hadError)
|
|
160
190
|
})
|
|
161
191
|
}
|
|
162
192
|
}
|
|
193
|
+
|
|
194
|
+
disconnectClientNodes(instance) {
|
|
195
|
+
debug('disconnectClientNodes', `${instance.ipAddress}:${instance.port}`);
|
|
196
|
+
const clientNodes = Object.values(this.clientNodes).filter(clientNode => {
|
|
197
|
+
return `${clientNode.hbDevice.instance.ipAddress}:${clientNode.hbDevice.instance.port}` === `${instance.ipAddress}:${instance.port}`;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
clientNodes.forEach(clientNode => {
|
|
201
|
+
clientNode.status({ fill: 'red', shape: 'ring', text: 'disconnected' });
|
|
202
|
+
clientNode.emit('hbDisconnected', instance);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
163
206
|
close() {
|
|
164
207
|
debug('hb-config: close');
|
|
165
208
|
this.hapClient?.destroy();
|
package/src/hbControlNode.js
CHANGED
|
@@ -10,7 +10,7 @@ class HbControlNode extends hbBaseNode {
|
|
|
10
10
|
debug('handleInput', message.payload, this.name);
|
|
11
11
|
|
|
12
12
|
if (!this.hbDevice) {
|
|
13
|
-
this.
|
|
13
|
+
this.handleWarning('HB not initialized');
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -58,6 +58,7 @@ class HbControlNode extends hbBaseNode {
|
|
|
58
58
|
this.error(`Failed to set value for "${key}": ${error.message}`);
|
|
59
59
|
results.push({ [key]: `Error: ${error.message}` });
|
|
60
60
|
fill = 'red';
|
|
61
|
+
this.hbConfigNode.disconnectClientNodes(this.hbDevice.instance);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -67,8 +68,7 @@ class HbControlNode extends hbBaseNode {
|
|
|
67
68
|
this.status({ text: statusText, shape: 'dot', fill });
|
|
68
69
|
done
|
|
69
70
|
} catch (error) {
|
|
70
|
-
this.error
|
|
71
|
-
this.status({ text: 'Unhandled error', shape: 'dot', fill: 'red' });
|
|
71
|
+
this.handleError(error, 'Unhandled error');
|
|
72
72
|
done(`Unhandled error: ${error.message}`);
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/hbResumeNode.js
CHANGED
|
@@ -12,7 +12,7 @@ class HbResumeNode extends HbBaseNode {
|
|
|
12
12
|
debug('handleInput', message.payload, this.name);
|
|
13
13
|
|
|
14
14
|
if (!this.hbDevice) {
|
|
15
|
-
this.
|
|
15
|
+
this.handleWarning('HB not initialized');
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ class HbResumeNode extends HbBaseNode {
|
|
|
20
20
|
const validNames = Object.keys(this.hbDevice.values)
|
|
21
21
|
.filter(key => key !== 'ConfiguredName')
|
|
22
22
|
.join(', ');
|
|
23
|
-
this.
|
|
23
|
+
this.handleWarning(
|
|
24
24
|
`Invalid payload. Expected: {"On": false, "Brightness": 0}. Valid values: ${validNames}`,
|
|
25
25
|
'Invalid payload'
|
|
26
26
|
);
|
package/src/hbStatusNode.js
CHANGED
|
@@ -10,7 +10,7 @@ class HbStatusNode extends HbBaseNode {
|
|
|
10
10
|
debug('handleInput', message.payload, this.name);
|
|
11
11
|
|
|
12
12
|
if (!this.hbDevice) {
|
|
13
|
-
this.
|
|
13
|
+
this.handleWarning('HB not initialized');
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -27,6 +27,7 @@ class HbStatusNode extends HbBaseNode {
|
|
|
27
27
|
} else {
|
|
28
28
|
this.status({ fill: "red", shape: "ring", text: "disconnected" });
|
|
29
29
|
this.error("No response from device", this.name);
|
|
30
|
+
this.hbConfigNode.disconnectClientNodes(this.hbDevice.instance);
|
|
30
31
|
done("No response from device");
|
|
31
32
|
}
|
|
32
33
|
|