node-red-contrib-homebridge-automation 0.1.12-beta.33 → 0.1.12-beta.35

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.33",
3
+ "version": "0.1.12-beta.35",
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,31 +29,59 @@ 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.log.debug('Monitor reported homebridge stability issues, refreshing devices');
44
+ this.hapClient.on('instance-discovered', this.waitForNoMoreDiscoveries);
45
+ this.hapClient.resetInstancePool();
46
+ this.waitForNoMoreDiscoveries();
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Wait for no more instance discoveries to be made before publishing services
52
+ */
48
53
  waitForNoMoreDiscoveries = () => {
49
54
  if (!this.discoveryTimeout) {
50
55
  clearTimeout(this.discoveryTimeout);
51
56
  this.discoveryTimeout = setTimeout(() => {
52
57
  this.log.debug('No more instances discovered, publishing services');
53
58
  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
59
  this.handleReady();
57
60
  this.discoveryTimeout = null;
58
- }, 5000);
61
+ this.refreshInProcess = false;
62
+ }, 20000); // resetInstancePool() triggers a discovery after 6 seconds. Need to wait for it to finish.
59
63
  }
60
64
  };
61
65
 
66
+ /**
67
+ * Populate the list of devices and handle duplicates
68
+ */
62
69
  async handleReady() {
63
- this.hbDevices = await this.hapClient.getAllServices();
70
+ const updatedDevices = await this.hapClient.getAllServices();
71
+ updatedDevices.forEach((updatedService, index) => {
72
+ if (this.hbDevices.find(service => service.uniqueId === updatedService.uniqueId)) {
73
+ const update = this.hbDevices.find(service => service.uniqueId === updatedService.uniqueId);
74
+ update.instance = updatedService.instance;
75
+ } else {
76
+ this.hbDevices.push(updatedService);
77
+ }
78
+ });
79
+
64
80
  this.evDevices = this.toList({ perms: 'ev' });
65
81
  this.ctDevices = this.toList({ perms: 'pw' });
66
82
  this.log.info(`Devices initialized: evDevices: ${this.evDevices.length}, ctDevices: ${this.ctDevices.length}`);
67
83
  this.handleDuplicates(this.evDevices);
68
- debug('Queue stats:', this.reqisterQueue.getStats());
69
- this.reqisterQueue.resume();
84
+ this.connectClientNodes();
70
85
  }
71
86
 
72
87
  toList(perms) {
@@ -107,16 +122,17 @@ class HBConfigNode {
107
122
  });
108
123
  }
109
124
 
110
- register(clientNode) {
125
+ registerClientNode(clientNode) {
111
126
  debug('Register: %s type: %s', clientNode.type, clientNode.name);
112
127
  this.clientNodes[clientNode.id] = clientNode;
113
- this.reqisterQueue.push(clientNode);
114
128
  clientNode.status({ fill: 'yellow', shape: 'ring', text: 'connecting' });
115
129
  }
116
130
 
117
- async _register(clientNodes, cb) {
118
- for (const clientNode of clientNodes) {
119
- debug('_Register: %s type: %s', clientNode.type, clientNode.name, clientNode.instance);
131
+ async connectClientNodes() {
132
+ debug('connect %s nodes', Object.keys(this.clientNodes).length);
133
+ console.log(this.clientNodes);
134
+ for (const [key, clientNode] of Object.entries(this.clientNodes)) {
135
+ // debug('_Register: %s type: %s', clientNode.type, clientNode.name, clientNode.instance);
120
136
  const matchedDevice = this.hbDevices.find(service =>
121
137
  clientNode.device === `${service.instance.name}${service.instance.username}${service.accessoryInformation.Manufacturer}${service.serviceName}${service.uuid.slice(0, 8)}`
122
138
  );
@@ -126,23 +142,19 @@ class HBConfigNode {
126
142
  clientNode.status({ fill: 'green', shape: 'dot', text: 'connected' });
127
143
  clientNode.emit('hbReady', matchedDevice);
128
144
  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
145
  } else {
133
146
  console.error('ERROR: Device registration failed', clientNode.name);
134
147
  }
135
- }
148
+ };
136
149
 
137
150
  await this.monitorDevices();
138
-
139
- cb(null);
140
151
  }
141
152
 
142
153
  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));
154
+ debug('monitorDevices', Object.keys(this.clientNodes).length);
155
+ if (Object.keys(this.clientNodes).length) {
156
+ const monitorNodes = this.clientNodes.filter(clientNode => clientNode.config.type === 'hb-status' || clientNode.config.type === 'hb-event').hbDevice;
157
+ this.monitor = await this.hapClient.monitorCharacteristics(monitorNodes);
146
158
  this.monitor.on('service-update', (services) => {
147
159
  services.forEach(service => {
148
160
  const eventNodes = Object.values(this.clientNodes).filter(clientNode =>
@@ -151,15 +163,29 @@ class HBConfigNode {
151
163
  eventNodes.forEach(eventNode => eventNode.emit('hbEvent', service));
152
164
  });
153
165
  });
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
- }
166
+ this.monitor.on('monitor-close', (instance, hadError) => {
167
+ debug('monitor-close', instance.name, instance.ipAddress, instance.port, hadError)
168
+ this.disconnectClientNodes(instance);
169
+ this.refreshDevices();
170
+ })
171
+ this.monitor.on('monitor-error', (instance, hadError) => {
172
+ debug('monitor-error', instance, hadError)
160
173
  })
161
174
  }
162
175
  }
176
+
177
+ disconnectClientNodes(instance) {
178
+ debug('disconnectClientNodes', `${instance.ipAddress}:${instance.port}`);
179
+ const clientNodes = Object.values(this.clientNodes).filter(clientNode => {
180
+ return `${clientNode.hbDevice.instance.ipAddress}:${clientNode.hbDevice.instance.port}` === `${instance.ipAddress}:${instance.port}`;
181
+ });
182
+
183
+ clientNodes.forEach(clientNode => {
184
+ clientNode.status({ fill: 'red', shape: 'ring', text: 'disconnected' });
185
+ clientNode.emit('hbDisconnected', instance);
186
+ });
187
+ }
188
+
163
189
  close() {
164
190
  debug('hb-config: close');
165
191
  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