homebridge-nest-accfactory 0.0.5 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -2,11 +2,49 @@
2
2
 
3
3
  All notable changes to `homebridge-nest-accfactory` will be documented in this file. This project tries to adhere to [Semantic Versioning](http://semver.org/).
4
4
 
5
- ### alpha
5
+ ## Breaking changes v0.2.0+
6
6
 
7
- Currently all releases are considered 'alpha' status, where things may or may not be working. Use at your own risk :-)
7
+ Unfornunately, from version **0.2.0**, I've made some breaking changes in the code to help move forward with the project.
8
+ So, what does this mean for you, the end user.
9
+ 1) You'll need to remove all previously discovered devices from HomeKit before adding them back in after upgrading to this version
10
+ 2) If using Homebridge version, remove any cached acccesory data associated with this plug-in
11
+ 3) If using docker/standalone version, remove the 'persist' folder
12
+ 4) Re-add devices to HomeKit once version upgraded
13
+ 5) Any HomeKit Secure Video recordings will be lost
14
+ 6) Will need to re-configure camera streaming and notification options
15
+ 6) Any automations will need to be re-created in HomeKit
16
+ 7) History in EveHome app will be lost
8
17
 
9
- ## v0.0.5 (2024/09/13)
18
+ Appologies for this change, as I can understand what an inconvience and frustration it will be :-(
19
+
20
+ ## Known Issues
21
+
22
+ - Audio from newer Nest/Google camera/doorbell devices is still blank
23
+ - npm package [ip](https://github.com/advisories/GHSA-2p57-rm9w-gvfp) has severity issue. This is being used in external library (werift)
24
+
25
+ ## v0.2.0 (2024/10/04)
26
+
27
+ - General code cleanup and bug fixes
28
+ - Common configuration between Homebridge plug-in and docker/standalone versions
29
+ - Seemlessly allow Nest/Google devices to be migrated between Nest <-> Google Home apps
30
+
31
+ ## v0.1.9 (alpha)
32
+
33
+ - General code cleanup and bug fixes
34
+ - Aligned version numbering to old Nest_accfactory repo
35
+ - Audio talkback support for newer Nest/Google camera/doorbell devices
36
+
37
+ ## v0.0.7 (alpha)
38
+
39
+ - General code cleanup and bug fixes
40
+ - Updated streaming/recording support for newer Nest/Google camera/doorbell devices
41
+ - No incoming audio, just video stream
42
+
43
+ ## v0.0.6 (2024-09-14)
44
+
45
+ - Fix for two/way audio starting on non-enabled HKSV camera/doorbells
46
+
47
+ ## v0.0.5 (2024-09-13)
10
48
 
11
49
  - General code cleanup and bug fixes
12
50
  - External dependancy reductions, dropped pbf and axios libraries
@@ -18,7 +56,7 @@ Currently all releases are considered 'alpha' status, where things may or may no
18
56
  - Camera/Doorbell support for snapshots and live video re-introduced
19
57
  - HomeKit Secure Video recording support re-introduced
20
58
  - Should support Nest Thermostat 4th Gen (untested)
21
- - *might* have finally resolved audio sync issues for both live and recording
59
+ - *might* have finally resolved audio sync issues with live video and recording
22
60
  - Depending on the libraries present in ffmpeg binary, we limit functionality for camera/doorbells
23
61
  - missing libspeex = no two-way audio
24
62
  - missing libfdk_aac = no audio
package/README.md CHANGED
@@ -82,6 +82,9 @@ Sample config.json entries below
82
82
  "XXXXXXXX": {
83
83
  "exclude": false
84
84
  },
85
+ "YYYYYYYY" : {
86
+ "hksv" : true
87
+ }
85
88
  },
86
89
  "platform": "NestAccfactory"
87
90
  }
@@ -91,30 +94,45 @@ Sample config.json entries below
91
94
 
92
95
  The following options are available in the config.json options object. These apply to all discovered devices.
93
96
 
94
- | Name | Description | Default |
95
- |-------------------|-----------------------------------------------------------------------------------------------|-----------|
96
- | elevation | Height above sea level for the weather station | 0 |
97
- | eveHistory | Provide history in EveHome application where applicable | true |
98
- | ffmegPath | Path to an ffmpeg binary for us to use. Will look in current directory by default | |
99
- | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
100
- | maxStreams | Maximum number of concurrent video streams in HomeKit for supported camera(s) and doorbell(s) | 2 |
101
- | weather | Virtual weather station for each Nest/Google home we discover | false |
97
+ | Name | Description | Default |
98
+ |-------------------|-----------------------------------------------------------------------------------------------|------------|
99
+ | elevation | Height above sea level for the weather station | 0 |
100
+ | eveHistory | Provide history in EveHome application where applicable | true |
101
+ | ffmegPath | Path to an ffmpeg binary for us to use. Will look in current directory by default | |
102
+ | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
103
+ | maxStreams | Maximum number of concurrent video streams in HomeKit for supported camera(s) and doorbell(s) | 2 |
104
+ | weather | Virtual weather station for each Nest/Google home we discover | false |
102
105
 
103
106
  #### devices
104
107
 
105
108
  The following options are available on a per-device level in the config.json devices object. The device is specified by using its serial number (in uppercase)
106
109
 
107
- | Name | Description | Default |
108
- |-------------------|-----------------------------------------------------------------------------------------------|-----------|
109
- | chimeSwitch | Create a switch for supported doorbell(s) which allows the indoor chime to be turned on/off | false |
110
- | doorbellCooldown | Time in seconds between doorbell press events | 60 |
111
- | elevation | Height above sea level for the specific weather station | 0 |
112
- | eveHistory | Provide history in EveHome application where applicable for the specific device | true |
113
- | exclude | Exclude the device | false |
114
- | humiditySensor | Create a seperate humidity sensor for supported thermostat(s) | false |
115
- | localAccess | Enable local video streaming for supported supported camera(s) and doorbell(s) | false |
116
- | motionCooldown | Time in seconds between detected motion events | 60 |
117
- | personCooldown | Time in seconds between detected person events | 120 |
110
+ | Name | Description | Default |
111
+ |-------------------|-----------------------------------------------------------------------------------------------|------------|
112
+ | chimeSwitch | Create a switch for supported doorbell(s) which allows the indoor chime to be turned on/off | false |
113
+ | doorbellCooldown | Time in seconds between doorbell press events | 60 |
114
+ | elevation | Height above sea level for the specific weather station | 0 |
115
+ | eveHistory | Provide history in EveHome application where applicable for the specific device | true |
116
+ | exclude | Exclude the device | false |
117
+ | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
118
+ | humiditySensor | Create a seperate humidity sensor for supported thermostat(s) | false |
119
+ | localAccess | Use direct access to supported camera(s) and doorbell(s) for video streaming and recording | false |
120
+ | motionCooldown | Time in seconds between detected motion events | 60 |
121
+ | personCooldown | Time in seconds between detected person events | 120 |
122
+
123
+ ## ffmpeg
124
+
125
+ **As of 3/10/2024, the [Homebridge Docker Image](https://hub.docker.com/r/homebridge/homebridge) includes an ffmpeg binary meeting our requirements. Its located in /usr/local/bin/ffmpeg**
126
+
127
+ To support streaming and recording from cameras, an ffmpeg binary needs to be present. We have specific requirements, which are:
128
+ - version 6.0 or later
129
+ - compiled with:
130
+ - libx264
131
+ - libfdk-aac
132
+ - libspeex
133
+ - libopus
134
+
135
+ By default, we look in the current directory where the plug-in excutes for an ffmpeg binary, however, you can specify a specific ffmpeg binary to use va the configuration option 'ffmpegPath'
118
136
 
119
137
  ## Caveats
120
138
 
@@ -6,22 +6,19 @@
6
6
  //
7
7
  // Homebridge Plugin:
8
8
  //
9
- // uuid
10
- // serial_number
11
- // software_version
9
+ // serialNumber
10
+ // softwareVersion
12
11
  // description
13
12
  // manufacturer
14
13
  // model
15
14
  //
16
15
  // HAP-NodeJS Library Accessory:
17
16
  //
18
- // uuid
19
- // serial_number
20
- // software_version
17
+ // serialNumber
18
+ // softwareVersion
21
19
  // description
22
20
  // manufacturer
23
21
  // model
24
- // hkUsername
25
22
  // hkPairingCode
26
23
  //
27
24
  // Following constants should be overridden in the module loading this class file
@@ -37,12 +34,14 @@
37
34
  // HomeKitDevice.updateServices(deviceData)
38
35
  // HomeKitDevice.messageServices(type, message)
39
36
  //
40
- // Code version 13/9/2024
37
+ // Code version 28/9/2024
41
38
  // Mark Hulskamp
42
39
  'use strict';
43
40
 
44
41
  // Define nodejs module requirements
42
+ import crypto from 'crypto';
45
43
  import EventEmitter from 'node:events';
44
+ import { Buffer } from 'node:buffer';
46
45
 
47
46
  // Define our HomeKit device class
48
47
  export default class HomeKitDevice {
@@ -60,6 +59,7 @@ export default class HomeKitDevice {
60
59
  accessory = undefined; // Accessory service for this device
61
60
  hap = undefined; // HomeKit Accessory Protocol API stub
62
61
  log = undefined; // Logging function object
62
+ uuid = undefined; // UUID for this instance
63
63
 
64
64
  // Internal data only for this class
65
65
  #platform = undefined; // Homebridge platform api
@@ -78,7 +78,7 @@ export default class HomeKitDevice {
78
78
  }
79
79
 
80
80
  // Workout if we're running under HomeBridge or HAP-NodeJS library
81
- if (typeof api?.version === 'number' && typeof api?.hap === 'object' && typeof api?.HAPLibraryVersion === 'undefined') {
81
+ if (isNaN(api?.version) === false && typeof api?.hap === 'object' && api?.HAPLibraryVersion === undefined) {
82
82
  // We have the HomeBridge version number and hap API object
83
83
  this.hap = api.hap;
84
84
  this.#platform = api;
@@ -86,65 +86,68 @@ export default class HomeKitDevice {
86
86
  this?.log?.debug && this.log.debug('HomeKitDevice module using Homebridge backend for "%s"', deviceData?.description);
87
87
  }
88
88
 
89
- if (typeof api?.HAPLibraryVersion === 'function' && typeof api?.version === 'undefined' && typeof api?.hap === 'undefined') {
89
+ if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
90
90
  // As we're missing the HomeBridge entry points but have the HAP library version
91
91
  this.hap = api;
92
92
 
93
93
  this?.log?.debug && this.log.debug('HomeKitDevice module using HAP-NodeJS library for "%s"', deviceData?.description);
94
94
  }
95
95
 
96
- // Validate if eventEmitter object passed to us is an instance of EventEmitter
97
- if (eventEmitter instanceof EventEmitter === true) {
98
- this.#eventEmitter = eventEmitter;
99
- }
100
-
101
- // If we have a valid EventEmitter and a device uuid
102
- // Setup a listener for messages to this device
103
- if (this.#eventEmitter !== undefined && typeof this.deviceData?.uuid === 'string' && this.deviceData.uuid !== '') {
104
- this.#eventEmitter.addListener(this.deviceData.uuid, this.#message.bind(this));
96
+ // Generate UUID for this device instance
97
+ // Will either be a random generated one or HAP generated one
98
+ // HAP is based upon defined plugin name and devices serial number
99
+ this.uuid = crypto.randomUUID();
100
+ if (
101
+ typeof HomeKitDevice.PLUGIN_NAME === 'string' &&
102
+ HomeKitDevice.PLUGIN_NAME !== '' &&
103
+ typeof deviceData.serialNumber === 'string' &&
104
+ deviceData.serialNumber !== '' &&
105
+ typeof this?.hap?.uuid?.generate === 'function'
106
+ ) {
107
+ this.uuid = this.hap.uuid.generate(HomeKitDevice.PLUGIN_NAME + '_' + deviceData.serialNumber.toUpperCase());
105
108
  }
106
109
 
107
- // Make a clone of current data and store in this object
108
- // Important that we done have a 'linked' cope of the object data
109
- // eslint-disable-next-line no-undef
110
- this.deviceData = structuredClone(deviceData);
111
-
112
110
  // See if we were passed in an existing accessory object or array of accessory objects
113
111
  // Mainly used to restore a HomeBridge cached accessory
114
- if (
115
- typeof accessory === 'object' &&
116
- typeof this?.hap?.uuid?.generate === 'function' &&
117
- typeof deviceData.uuid === 'string' &&
118
- this.#platform !== undefined
119
- ) {
120
- let uuid = this.hap.uuid.generate(HomeKitDevice.PLUGIN_NAME + '_' + deviceData.uuid);
121
-
112
+ if (typeof accessory === 'object' && this.#platform !== undefined) {
122
113
  if (Array.isArray(accessory) === true) {
123
- this.accessory = accessory.find((accessory) => accessory.UUID === uuid);
114
+ this.accessory = accessory.find((accessory) => accessory?.UUID === this.uuid);
124
115
  }
125
- if (Array.isArray(accessory) === false && typeof accessory?.UUID === 'string' && accessory.UUID === uuid) {
116
+ if (Array.isArray(accessory) === false && accessory?.UUID === this.uuid) {
126
117
  this.accessory = accessory;
127
118
  }
128
119
  }
120
+
121
+ // Validate if eventEmitter object passed to us is an instance of EventEmitter
122
+ // If valid, setup an event listener for messages to this device using our generated uuid
123
+ if (eventEmitter instanceof EventEmitter === true) {
124
+ this.#eventEmitter = eventEmitter;
125
+ this.#eventEmitter.addListener(this.uuid, this.#message.bind(this));
126
+ }
127
+
128
+ // Make a clone of current data and store in this object
129
+ // Important that we done have a 'linked' copy of the object data
130
+ // eslint-disable-next-line no-undef
131
+ this.deviceData = structuredClone(deviceData);
129
132
  }
130
133
 
131
134
  // Class functions
132
135
  async add(accessoryName, accessoryCategory, useHistoryService) {
133
136
  if (
134
137
  this.hap === undefined ||
135
- HomeKitDevice.PLUGIN_NAME === undefined ||
136
- HomeKitDevice.PLATFORM_NAME === undefined ||
138
+ typeof HomeKitDevice.PLUGIN_NAME !== 'string' ||
139
+ HomeKitDevice.PLUGIN_NAME === '' ||
140
+ typeof HomeKitDevice.PLATFORM_NAME !== 'string' ||
141
+ HomeKitDevice.PLATFORM_NAME === '' ||
137
142
  typeof accessoryName !== 'string' ||
138
143
  accessoryName === '' ||
139
144
  typeof this.hap.Categories[accessoryCategory] === 'undefined' ||
140
145
  typeof useHistoryService !== 'boolean' ||
141
146
  typeof this.deviceData !== 'object' ||
142
- typeof this.deviceData?.uuid !== 'string' ||
143
- this.deviceData.uuid === '' ||
144
- typeof this.deviceData?.serial_number !== 'string' ||
145
- this.deviceData.serial_number === '' ||
146
- typeof this.deviceData?.software_version !== 'string' ||
147
- this.deviceData.software_version === '' ||
147
+ typeof this.deviceData?.serialNumber !== 'string' ||
148
+ this.deviceData.serialNumber === '' ||
149
+ typeof this.deviceData?.softwareVersion !== 'string' ||
150
+ this.deviceData.softwareVersion === '' ||
148
151
  (typeof this.deviceData?.description !== 'string' && this.deviceData.description === '') ||
149
152
  typeof this.deviceData?.model !== 'string' ||
150
153
  this.deviceData.model === '' ||
@@ -153,10 +156,7 @@ export default class HomeKitDevice {
153
156
  (this.#platform === undefined &&
154
157
  typeof this.deviceData?.hkPairingCode !== 'string' &&
155
158
  (new RegExp(/^([0-9]{3}-[0-9]{2}-[0-9]{3})$/).test(this.deviceData.hkPairingCode) === true ||
156
- new RegExp(/^([0-9]{4}-[0-9]{4})$/).test(this.deviceData.hkPairingCode) === true)) ||
157
- (this.#platform === undefined &&
158
- typeof this.deviceData?.hkUsername !== 'string' &&
159
- new RegExp(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/).test(this.deviceData.hkUsername) === false)
159
+ new RegExp(/^([0-9]{4}-[0-9]{4})$/).test(this.deviceData.hkPairingCode) === true))
160
160
  ) {
161
161
  return;
162
162
  }
@@ -164,23 +164,21 @@ export default class HomeKitDevice {
164
164
  // If we do not have an existing accessory object, create a new one
165
165
  if (this.accessory === undefined && this.#platform !== undefined) {
166
166
  // Create HomeBridge platform accessory
167
- this.accessory = new this.#platform.platformAccessory(
168
- this.deviceData.description,
169
- this.hap.uuid.generate(HomeKitDevice.PLUGIN_NAME + '_' + this.deviceData.uuid),
170
- );
167
+ this.accessory = new this.#platform.platformAccessory(this.deviceData.description, this.uuid);
171
168
  this.#platform.registerPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
172
169
  }
173
170
 
174
171
  if (this.accessory === undefined && this.#platform === undefined) {
175
172
  // Create HAP-NodeJS libray accessory
176
- // We're using our previous parameters to generate the uuid, rather than the 'new' way we do for Homebridge
177
- this.accessory = new this.hap.Accessory(
178
- accessoryName,
179
- this.hap.uuid.generate(
180
- 'hap-nodejs:accessories:' + this.deviceData.manufacturer.toLowerCase() + '_' + this.deviceData.serial_number,
181
- ),
182
- );
183
- this.accessory.username = this.deviceData.hkUsername;
173
+ this.accessory = new this.hap.Accessory(accessoryName, this.uuid);
174
+
175
+ // Create a HomeKit username for the device in format of xx:xx:xx:xx:xx:xx
176
+ // Use a Nest Labs prefix for first 6 digits, followed by a CRC24 based off serial number for last 6 digits.
177
+ this.accessory.username = ('18B430' + crc24(this.deviceData.serialNumber.toUpperCase()))
178
+ .toString('hex')
179
+ .split(/(..)/)
180
+ .filter((s) => s)
181
+ .join(':');
184
182
  this.accessory.pincode = this.deviceData.hkPairingCode;
185
183
  this.accessory.category = accessoryCategory;
186
184
  }
@@ -190,10 +188,10 @@ export default class HomeKitDevice {
190
188
  if (informationService !== undefined) {
191
189
  informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, this.deviceData.manufacturer);
192
190
  informationService.updateCharacteristic(this.hap.Characteristic.Model, this.deviceData.model);
193
- informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, this.deviceData.serial_number);
194
- informationService.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, this.deviceData.software_version);
191
+ informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, this.deviceData.serialNumber);
192
+ informationService.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, this.deviceData.softwareVersion);
193
+ informationService.updateCharacteristic(this.hap.Characteristic.Name, this.deviceData.description);
195
194
  }
196
- informationService.updateCharacteristic(this.hap.Characteristic.Name, this.deviceData.description);
197
195
 
198
196
  // Setup our history service if module has been defined and requested to be active for this device
199
197
  if (typeof HomeKitDevice?.HISTORY === 'function' && this.historyService === undefined && useHistoryService === true) {
@@ -221,34 +219,31 @@ export default class HomeKitDevice {
221
219
  }
222
220
  }
223
221
 
224
- // if we have a valid EventEmitter and we have not previously setup an message event handler, do so now
225
- if (this.#eventEmitter !== undefined && this.#eventEmitter.listenerCount(this.deviceData.uuid) === 0) {
226
- this.#eventEmitter.addListener(this.deviceData.uuid, this.#message.bind(this));
227
- }
228
-
229
222
  // Perform an initial update using current data
230
223
  this.update(this.deviceData, true);
231
224
 
232
225
  // If using HAP-NodeJS library, publish accessory on local network
233
226
  if (this.#platform === undefined && this.accessory !== undefined) {
234
- if (this?.log?.info) {
235
- this.log.info(' += Advertising as "%s"', accessoryName);
236
- this.log.info(' += Pairing code is "%s"', this.accessory.pincode);
237
- }
238
227
  this.accessory.publish({
239
228
  username: this.accessory.username,
240
229
  pincode: this.accessory.pincode,
241
230
  category: this.accessory.category,
242
231
  });
232
+ if (this?.log?.info) {
233
+ this.log.info(' += Advertising as "%s"', this.accessory.displayName);
234
+ this.log.info(' += Pairing code is "%s"', this.accessory.pincode);
235
+ }
243
236
  }
237
+
238
+ return this.accessory; // Return our HomeKit accessory
244
239
  }
245
240
 
246
241
  remove() {
247
242
  this?.log?.warn && this.log.warn('Device "%s" has been removed', this.deviceData.description);
248
243
 
249
- if (this.#eventEmitter === undefined && typeof this.deviceData?.uuid === 'string' && this.deviceData.uuid !== '') {
244
+ if (this.#eventEmitter !== undefined) {
250
245
  // Remove listener for 'messages'
251
- this.#eventEmitter.removeAllListeners(this.deviceData.uuid);
246
+ this.#eventEmitter.removeAllListeners(this.uuid);
252
247
  }
253
248
 
254
249
  if (typeof this.removeServices === 'function') {
@@ -260,12 +255,12 @@ export default class HomeKitDevice {
260
255
  }
261
256
 
262
257
  if (this.accessory !== undefined && this.#platform !== undefined) {
263
- // Unregister the accessory from Homebridge
258
+ // Unregister the accessory from Homebridge platform
264
259
  this.#platform.unregisterPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
265
260
  }
266
261
 
267
262
  if (this.accessory !== undefined && this.#platform === undefined) {
268
- // Unpublish the accessory from HAP
263
+ // Unpublish the accessory from HAP-NodeJS library
269
264
  this.accessory.unpublish();
270
265
  }
271
266
 
@@ -274,6 +269,7 @@ export default class HomeKitDevice {
274
269
  this.historyService = undefined;
275
270
  this.hap = undefined;
276
271
  this.log = undefined;
272
+ this.uuid = undefined;
277
273
  this.#platform = undefined;
278
274
  this.#eventEmitter = undefined;
279
275
 
@@ -320,42 +316,46 @@ export default class HomeKitDevice {
320
316
  deviceData.manufacturer !== '' &&
321
317
  deviceData.manufacturer !== this.deviceData.manufacturer
322
318
  ) {
323
- // Update serial number on the HomeKit accessory
324
- informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, this.deviceData.manufacturer);
319
+ // Update manufacturer number on the HomeKit accessory
320
+ informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, deviceData.manufacturer);
325
321
  }
326
322
 
327
323
  if (typeof deviceData?.model === 'string' && deviceData.model !== '' && deviceData.model !== this.deviceData.model) {
328
- // Update serial number on the HomeKit accessory
329
- informationService.updateCharacteristic(this.hap.Characteristic.Model, this.deviceData.model);
324
+ // Update model on the HomeKit accessory
325
+ informationService.updateCharacteristic(this.hap.Characteristic.Model, deviceData.model);
330
326
  }
331
327
 
332
328
  if (
333
- typeof deviceData?.serial_number === 'string' &&
334
- deviceData.serial_number !== '' &&
335
- deviceData.serial_number !== this.deviceData.serial_number
329
+ typeof deviceData?.softwareVersion === 'string' &&
330
+ deviceData.softwareVersion !== '' &&
331
+ deviceData.softwareVersion !== this.deviceData.softwareVersion
336
332
  ) {
337
- // Update serial number on the HomeKit accessory
338
- informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, this.deviceData.serial_number);
333
+ // Update software version on the HomeKit accessory
334
+ informationService.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceData.softwareVersion);
339
335
  }
340
336
 
337
+ // Check for devices serial number changing. Really shouldn't occur, but handle case anyway
341
338
  if (
342
- typeof deviceData?.software_version === 'string' &&
343
- deviceData.software_version !== '' &&
344
- deviceData.software_version !== this.deviceData.software_version
339
+ typeof deviceData?.serialNumber === 'string' &&
340
+ deviceData.serialNumber !== '' &&
341
+ deviceData.serialNumber.toUpperCase() !== this.deviceData.serialNumber.toUpperCase()
345
342
  ) {
343
+ this?.log?.warn && this.log.warn('Serial number on "%s" has changed', deviceData.description);
344
+ this?.log?.warn && this.log.warn('This may cause the device to become unresponsive in HomeKit');
345
+
346
346
  // Update software version on the HomeKit accessory
347
- informationService.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, this.deviceData.software_version);
347
+ informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, deviceData.serialNumber);
348
348
  }
349
349
  }
350
350
 
351
351
  if (typeof deviceData?.online === 'boolean' && deviceData.online !== this.deviceData.online) {
352
352
  // Output device online/offline status
353
- if (deviceData.online === false && this?.log?.warn) {
354
- this.log.warn('Device "%s" is offline', this.deviceData.description);
353
+ if (deviceData.online === false) {
354
+ this?.log?.warn && this.log.warn('Device "%s" is offline', deviceData.description);
355
355
  }
356
356
 
357
- if (deviceData.online === true && this?.log?.success) {
358
- this.log.success('Device "%s" is online', this.deviceData.description);
357
+ if (deviceData.online === true) {
358
+ this?.log?.success && this.log.success('Device "%s" is online', deviceData.description);
359
359
  }
360
360
  }
361
361
 
@@ -363,7 +363,7 @@ export default class HomeKitDevice {
363
363
  try {
364
364
  this.updateServices(deviceData); // Pass updated data on for accessory to process as it needs
365
365
  } catch (error) {
366
- this?.log?.error && this.log.error('updateServices call for device "%s" failed. Error was', this.deviceData.description, error);
366
+ this?.log?.error && this.log.error('updateServices call for device "%s" failed. Error was', deviceData.description, error);
367
367
  }
368
368
  }
369
369
 
@@ -374,17 +374,12 @@ export default class HomeKitDevice {
374
374
  }
375
375
 
376
376
  async set(values) {
377
- if (
378
- typeof values !== 'object' ||
379
- this.#eventEmitter === undefined ||
380
- typeof this.deviceData?.uuid !== 'string' ||
381
- this.deviceData.uuid === ''
382
- ) {
377
+ if (typeof values !== 'object' || this.#eventEmitter === undefined) {
383
378
  return;
384
379
  }
385
380
 
386
381
  // Send event with data to set
387
- this.#eventEmitter.emit(HomeKitDevice.SET, this.deviceData.uuid, values);
382
+ this.#eventEmitter.emit(HomeKitDevice.SET, this.uuid, values);
388
383
 
389
384
  // Update the internal data for the set values, as could take sometime once we emit the event
390
385
  Object.entries(values).forEach(([key, value]) => {
@@ -395,21 +390,16 @@ export default class HomeKitDevice {
395
390
  }
396
391
 
397
392
  async get(values) {
398
- if (
399
- typeof values !== 'object' ||
400
- this.#eventEmitter === undefined ||
401
- typeof this.deviceData?.uuid !== 'string' ||
402
- this.deviceData.uuid === ''
403
- ) {
393
+ if (typeof values !== 'object' || this.#eventEmitter === undefined) {
404
394
  return;
405
395
  }
406
396
 
407
397
  // Send event with data to get
408
398
  // Once get has completed, we'll get an event back with the requested data
409
- this.#eventEmitter.emit(HomeKitDevice.GET, this.deviceData.uuid, values);
399
+ this.#eventEmitter.emit(HomeKitDevice.GET, this.uuid, values);
410
400
 
411
401
  // This should always return, but we probably should put in a timeout?
412
- let results = await EventEmitter.once(this.#eventEmitter, HomeKitDevice.GET + '->' + this.deviceData.uuid);
402
+ let results = await EventEmitter.once(this.#eventEmitter, HomeKitDevice.GET + '->' + this.uuid);
413
403
  return results?.[0];
414
404
  }
415
405
 
@@ -417,8 +407,8 @@ export default class HomeKitDevice {
417
407
  switch (type) {
418
408
  case HomeKitDevice.ADD: {
419
409
  // Got message for device add
420
- if (typeof message?.name === 'string' && typeof message?.category === 'number' && typeof message?.history === 'boolean') {
421
- this.add(message.name, message.category, message.history);
410
+ if (typeof message?.name === 'string' && isNaN(message?.category) === false && typeof message?.history === 'boolean') {
411
+ this.add(message.name, Number(message.category), message.history);
422
412
  }
423
413
  break;
424
414
  }
@@ -450,3 +440,36 @@ export default class HomeKitDevice {
450
440
  }
451
441
  }
452
442
  }
443
+
444
+ // General helper functions which don't need to be part of an object class
445
+ function crc24(valueToHash) {
446
+ const crc24HashTable = [
447
+ 0x000000, 0x864cfb, 0x8ad50d, 0x0c99f6, 0x93e6e1, 0x15aa1a, 0x1933ec, 0x9f7f17, 0xa18139, 0x27cdc2, 0x2b5434, 0xad18cf, 0x3267d8,
448
+ 0xb42b23, 0xb8b2d5, 0x3efe2e, 0xc54e89, 0x430272, 0x4f9b84, 0xc9d77f, 0x56a868, 0xd0e493, 0xdc7d65, 0x5a319e, 0x64cfb0, 0xe2834b,
449
+ 0xee1abd, 0x685646, 0xf72951, 0x7165aa, 0x7dfc5c, 0xfbb0a7, 0x0cd1e9, 0x8a9d12, 0x8604e4, 0x00481f, 0x9f3708, 0x197bf3, 0x15e205,
450
+ 0x93aefe, 0xad50d0, 0x2b1c2b, 0x2785dd, 0xa1c926, 0x3eb631, 0xb8faca, 0xb4633c, 0x322fc7, 0xc99f60, 0x4fd39b, 0x434a6d, 0xc50696,
451
+ 0x5a7981, 0xdc357a, 0xd0ac8c, 0x56e077, 0x681e59, 0xee52a2, 0xe2cb54, 0x6487af, 0xfbf8b8, 0x7db443, 0x712db5, 0xf7614e, 0x19a3d2,
452
+ 0x9fef29, 0x9376df, 0x153a24, 0x8a4533, 0x0c09c8, 0x00903e, 0x86dcc5, 0xb822eb, 0x3e6e10, 0x32f7e6, 0xb4bb1d, 0x2bc40a, 0xad88f1,
453
+ 0xa11107, 0x275dfc, 0xdced5b, 0x5aa1a0, 0x563856, 0xd074ad, 0x4f0bba, 0xc94741, 0xc5deb7, 0x43924c, 0x7d6c62, 0xfb2099, 0xf7b96f,
454
+ 0x71f594, 0xee8a83, 0x68c678, 0x645f8e, 0xe21375, 0x15723b, 0x933ec0, 0x9fa736, 0x19ebcd, 0x8694da, 0x00d821, 0x0c41d7, 0x8a0d2c,
455
+ 0xb4f302, 0x32bff9, 0x3e260f, 0xb86af4, 0x2715e3, 0xa15918, 0xadc0ee, 0x2b8c15, 0xd03cb2, 0x567049, 0x5ae9bf, 0xdca544, 0x43da53,
456
+ 0xc596a8, 0xc90f5e, 0x4f43a5, 0x71bd8b, 0xf7f170, 0xfb6886, 0x7d247d, 0xe25b6a, 0x641791, 0x688e67, 0xeec29c, 0x3347a4, 0xb50b5f,
457
+ 0xb992a9, 0x3fde52, 0xa0a145, 0x26edbe, 0x2a7448, 0xac38b3, 0x92c69d, 0x148a66, 0x181390, 0x9e5f6b, 0x01207c, 0x876c87, 0x8bf571,
458
+ 0x0db98a, 0xf6092d, 0x7045d6, 0x7cdc20, 0xfa90db, 0x65efcc, 0xe3a337, 0xef3ac1, 0x69763a, 0x578814, 0xd1c4ef, 0xdd5d19, 0x5b11e2,
459
+ 0xc46ef5, 0x42220e, 0x4ebbf8, 0xc8f703, 0x3f964d, 0xb9dab6, 0xb54340, 0x330fbb, 0xac70ac, 0x2a3c57, 0x26a5a1, 0xa0e95a, 0x9e1774,
460
+ 0x185b8f, 0x14c279, 0x928e82, 0x0df195, 0x8bbd6e, 0x872498, 0x016863, 0xfad8c4, 0x7c943f, 0x700dc9, 0xf64132, 0x693e25, 0xef72de,
461
+ 0xe3eb28, 0x65a7d3, 0x5b59fd, 0xdd1506, 0xd18cf0, 0x57c00b, 0xc8bf1c, 0x4ef3e7, 0x426a11, 0xc426ea, 0x2ae476, 0xaca88d, 0xa0317b,
462
+ 0x267d80, 0xb90297, 0x3f4e6c, 0x33d79a, 0xb59b61, 0x8b654f, 0x0d29b4, 0x01b042, 0x87fcb9, 0x1883ae, 0x9ecf55, 0x9256a3, 0x141a58,
463
+ 0xefaaff, 0x69e604, 0x657ff2, 0xe33309, 0x7c4c1e, 0xfa00e5, 0xf69913, 0x70d5e8, 0x4e2bc6, 0xc8673d, 0xc4fecb, 0x42b230, 0xddcd27,
464
+ 0x5b81dc, 0x57182a, 0xd154d1, 0x26359f, 0xa07964, 0xace092, 0x2aac69, 0xb5d37e, 0x339f85, 0x3f0673, 0xb94a88, 0x87b4a6, 0x01f85d,
465
+ 0x0d61ab, 0x8b2d50, 0x145247, 0x921ebc, 0x9e874a, 0x18cbb1, 0xe37b16, 0x6537ed, 0x69ae1b, 0xefe2e0, 0x709df7, 0xf6d10c, 0xfa48fa,
466
+ 0x7c0401, 0x42fa2f, 0xc4b6d4, 0xc82f22, 0x4e63d9, 0xd11cce, 0x575035, 0x5bc9c3, 0xdd8538,
467
+ ];
468
+
469
+ let crc24 = 0xb704ce; // init crc24 hash;
470
+ valueToHash = Buffer.from(valueToHash); // convert value into buffer for processing
471
+ for (let index = 0; index < valueToHash.length; index++) {
472
+ crc24 = (crc24HashTable[((crc24 >> 16) ^ valueToHash[index]) & 0xff] ^ (crc24 << 8)) & 0xffffff;
473
+ }
474
+ return crc24.toString(16); // return crc24 as hex string
475
+ }