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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-homebridge-automation",
3
- "version": "0.1.12-beta.34",
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.12"
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?.register(this);
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
- handleError(warning, statusText) {
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: 'red',
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;
@@ -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
- }, 5000);
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
- this.hbDevices = await this.hapClient.getAllServices();
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
- debug('Queue stats:', this.reqisterQueue.getStats());
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', 'Doorbell',
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.includes(service.humanType))
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 seenFullNames = new Set();
97
- const seenUniqueIds = new Set();
113
+ const seen = new Map();
98
114
 
99
115
  list.forEach(endpoint => {
100
- if (!seenFullNames.add(endpoint.fullName)) {
101
- console.warn('WARNING: Duplicate device name', endpoint.fullName);
102
- }
116
+ const { fullName, uniqueId } = endpoint;
103
117
 
104
- if (!seenUniqueIds.add(endpoint.uniqueId)) {
105
- console.error('ERROR: Duplicate uniqueId detected.', endpoint.fullName);
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
- register(clientNode) {
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 _register(clientNodes, cb) {
118
- for (const clientNode of clientNodes) {
119
- debug('_Register: %s type: %s', clientNode.type, clientNode.name, clientNode.instance);
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
- debug('monitorDevices', Object.keys(this.monitorNodes).length);
144
- if (Object.keys(this.monitorNodes).length) {
145
- this.monitor = await this.hapClient.monitorCharacteristics(Object.values(this.monitorNodes));
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
- if (!this.hapClient.this.discoveryInProgress) {
157
- this.monitor.finish();
158
- this.hapClient.resetInstancePool();
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();
@@ -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.handleError('HB not initialized');
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(`Unhandled error: ${error.message}`);
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
  }
@@ -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.handleError('HB not initialized');
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.handleError(
23
+ this.handleWarning(
24
24
  `Invalid payload. Expected: {"On": false, "Brightness": 0}. Valid values: ${validNames}`,
25
25
  'Invalid payload'
26
26
  );
@@ -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.handleError('HB not initialized');
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