homebridge-nest-accfactory 0.2.11 → 0.3.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +14 -7
  3. package/config.schema.json +118 -0
  4. package/dist/HomeKitDevice.js +203 -77
  5. package/dist/HomeKitHistory.js +1 -1
  6. package/dist/config.js +207 -0
  7. package/dist/devices.js +118 -0
  8. package/dist/index.js +2 -1
  9. package/dist/nexustalk.js +46 -48
  10. package/dist/{camera.js → plugins/camera.js} +216 -241
  11. package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
  12. package/dist/plugins/floodlight.js +91 -0
  13. package/dist/plugins/heatlink.js +17 -0
  14. package/dist/{protect.js → plugins/protect.js} +26 -43
  15. package/dist/{tempsensor.js → plugins/tempsensor.js} +15 -19
  16. package/dist/{thermostat.js → plugins/thermostat.js} +426 -383
  17. package/dist/{weather.js → plugins/weather.js} +26 -60
  18. package/dist/protobuf/nest/services/apigateway.proto +31 -1
  19. package/dist/protobuf/nest/trait/firmware.proto +207 -89
  20. package/dist/protobuf/nest/trait/hvac.proto +1052 -312
  21. package/dist/protobuf/nest/trait/located.proto +51 -8
  22. package/dist/protobuf/nest/trait/network.proto +366 -36
  23. package/dist/protobuf/nest/trait/occupancy.proto +145 -17
  24. package/dist/protobuf/nest/trait/product/protect.proto +57 -43
  25. package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
  26. package/dist/protobuf/nest/trait/sensor.proto +7 -1
  27. package/dist/protobuf/nest/trait/service.proto +3 -1
  28. package/dist/protobuf/nest/trait/structure.proto +60 -14
  29. package/dist/protobuf/nest/trait/ui.proto +41 -1
  30. package/dist/protobuf/nest/trait/user.proto +6 -1
  31. package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
  32. package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
  33. package/dist/protobuf/root.proto +1 -0
  34. package/dist/protobuf/wdl.proto +18 -2
  35. package/dist/protobuf/weave/common.proto +2 -1
  36. package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
  37. package/dist/protobuf/weave/trait/power.proto +1 -0
  38. package/dist/protobuf/weave/trait/security.proto +10 -1
  39. package/dist/streamer.js +74 -78
  40. package/dist/system.js +1213 -1264
  41. package/dist/webrtc.js +39 -34
  42. package/package.json +11 -11
  43. package/dist/floodlight.js +0 -97
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
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
+ ## v0.3.1 (2025/06/16)
6
+
7
+ - Minor stability improvements affecting standalone docker version
8
+
9
+ ## v0.3.0 (2025/06/14)
10
+
11
+ - General code cleanup and stability improvements
12
+ - Introduced plugin-style architecture for Nest/Google devices
13
+ - Updated README.md to reflect changes to the devices section in configuration
14
+ - Prevent excluded devices from being restored from Homebridge cache
15
+ - Added internal support for selecting active temperature sensor (not yet exposed to HomeKit)
16
+ - Fixed loss of custom devices section when using the plugin config UI
17
+ - Fixed motion services being recreated when restored from Homebridge cache
18
+ - Fixed missing devices for Nest FieldTest accounts
19
+ - Added hot water heating boost (on/off) support for compatible EU/UK Thermostats
20
+ - Added support for Nest Heat Link devices as room temperature sensors
21
+
22
+ ### Deprecation Notice
23
+ - Support for the standalone Docker version of this plugin is planned to be deprecated in an upcoming release. While it currently remains functional, future updates may no longer include Docker-specific build support. Users are encouraged to transition to standard Homebridge installations where possible.
24
+
25
+ ### Known Issues
26
+
27
+ - Audio from newer Nest/Google cameras and doorbells is currently silent (blank audio output)
28
+ - The ip npm package has a known security advisory [GHSA-2p57-rm9w-gvfp](https://github.com/advisories/GHSA-2p57-rm9w-gvfp); this is used indirectly via the werift library
29
+
30
+ ### Thanks
31
+ Special thanks to [@Daniel](https://github.com/no1knows) and [@Brad](https://github.com/bcullman) for testing and feedback on this release!
32
+
5
33
  ## v0.2.11 (2025/04/17)
6
34
 
7
35
  - General code cleanup and bug fixes
package/README.md CHANGED
@@ -23,8 +23,12 @@ The following Nest devices are known to be supported
23
23
  * Nest Temp Sensors (1st gen)
24
24
  * Nest Cameras (Cam Indoor, IQ Indoor, Outdoor, IQ Outdoor, Cam with Floodlight)
25
25
  * Nest Doorbells (wired 1st gen)
26
+ * Nest HeatLink
26
27
 
27
- The accessory supports connection to Nest using a Nest account OR a Google (migrated Nest account) account.
28
+ **Note:** Google has announced it will discontinue support for 1st and 2nd generation Nest Thermostats as of **October 25, 2025**.
29
+ Based on their stated intentions, these models are expected to stop functioning with this Homebridge plugin after that date.
30
+
31
+ The accessory supports connection to Nest using a Nest account AND/OR a Google (migrated Nest account) account.
28
32
 
29
33
  ## Configuration
30
34
 
@@ -79,14 +83,15 @@ Sample config.json entries below
79
83
  "elevation": 600,
80
84
  "hksv": false
81
85
  },
82
- "devices": {
83
- "XXXXXXXX": {
86
+ "devices": [
87
+ {
88
+ "serialNumber": "XXXXXXXX",
84
89
  "exclude": false
85
90
  },
86
- "YYYYYYYY" : {
91
+ "serialNumber": "YYYYYYYY",
87
92
  "hksv" : true
88
93
  }
89
- },
94
+ ],
90
95
  "platform": "NestAccfactory"
91
96
  }
92
97
  ```
@@ -99,15 +104,15 @@ The following options are available in the config.json options object. These app
99
104
  |-------------------|-----------------------------------------------------------------------------------------------|----------------|
100
105
  | elevation | Height above sea level for the weather station | 0 |
101
106
  | eveHistory | Provide history in EveHome application where applicable | true |
107
+ | exclude | Exclude ALL devices | false |
102
108
  | ffmegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
103
109
  | ffmegPath | Path to an ffmpeg binary for us to use | /usr/local/bin |
104
110
  | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
105
- | maxStreams | Maximum number of concurrent video streams in HomeKit for supported camera(s) and doorbell(s) | 2 |
106
111
  | weather | Virtual weather station for each Nest/Google home we discover | false |
107
112
 
108
113
  #### devices
109
114
 
110
- 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)
115
+ The following options are available on a per-device level in the `config.json` `devices` array. Each device is specified as a JSON object, and the device is identified using the `"serialNumber"` key with the value of its serial number (in uppercase).
111
116
 
112
117
  | Name | Description | Default |
113
118
  |-------------------|-----------------------------------------------------------------------------------------------|----------------|
@@ -118,10 +123,12 @@ The following options are available on a per-device level in the config.json dev
118
123
  | exclude | Exclude the device | false |
119
124
  | ffmegDebug | Turns on specific debugging output for when ffmpeg is envoked | false |
120
125
  | hksv | Enable HomeKit Secure Video for supported camera(s) and doorbell(s) | false |
126
+ | hotWaterBoostTime | Time in seconds for hotwater boost heating | 1800 |
121
127
  | humiditySensor | Create a seperate humidity sensor for supported thermostat(s) | false |
122
128
  | localAccess | Use direct access to supported camera(s) and doorbell(s) for video streaming and recording | false |
123
129
  | motionCooldown | Time in seconds between detected motion events | 60 |
124
130
  | personCooldown | Time in seconds between detected person events | 120 |
131
+ | serialNumber | Device serial number to which these settings belong too | |
125
132
 
126
133
  ## ffmpeg
127
134
 
@@ -105,6 +105,84 @@
105
105
  "type": "string",
106
106
  "placeholder": "Path to an ffmpeg binary",
107
107
  "default": "/usr/local/bin/ffmpeg"
108
+ },
109
+ "ffmegDebug": {
110
+ "type": "boolean"
111
+ },
112
+ "maxStreams": {
113
+ "type": "integer"
114
+ },
115
+ "exclude": {
116
+ "type": "boolean"
117
+ },
118
+ "useNestAPI": {
119
+ "type": "boolean"
120
+ },
121
+ "useGoogleAPI": {
122
+ "type": "boolean"
123
+ }
124
+ }
125
+ },
126
+ "devices": {
127
+ "title": "Per device configuration options",
128
+ "type": "array",
129
+ "items": {
130
+ "title": "Device",
131
+ "type": "object",
132
+ "properties": {
133
+ "serialNumber": {
134
+ "title": "Device Serial Number",
135
+ "type": "string",
136
+ "required": true
137
+ },
138
+ "chimeSwitch": {
139
+ "type": "boolean"
140
+ },
141
+ "doorbellCooldown": {
142
+ "type": "number"
143
+ },
144
+ "elevation": {
145
+ "type": "number"
146
+ },
147
+ "eveHistory": {
148
+ "type": "boolean"
149
+ },
150
+ "exclude": {
151
+ "type": "boolean"
152
+ },
153
+ "externalCool": {
154
+ "type": "string"
155
+ },
156
+ "externalDehumidifier": {
157
+ "type": "string"
158
+ },
159
+ "externalFan": {
160
+ "type": "string"
161
+ },
162
+ "externalHeat": {
163
+ "type": "string"
164
+ },
165
+ "ffmegDebug": {
166
+ "type": "boolean"
167
+ },
168
+ "hkPairingCode": {
169
+ "type": "string"
170
+ },
171
+ "hksv": {
172
+ "type": "boolean"
173
+ },
174
+ "humiditySensor": {
175
+ "type": "boolean"
176
+ },
177
+ "localAccess": {
178
+ "type": "boolean"
179
+ },
180
+ "motionCooldown": {
181
+ "type": "number"
182
+ },
183
+ "personCooldown": {
184
+ "type": "number"
185
+ }
108
186
  }
109
187
  }
110
188
  }
@@ -117,5 +195,45 @@
117
195
  {
118
196
  "required": ["google"]
119
197
  }
198
+ ],
199
+ "layout": [
200
+ {
201
+ "type": "fieldset",
202
+ "title": "Nest Account",
203
+ "expandable": true,
204
+ "expanded": {
205
+ "functionBody": "return model.nest && model.nest.access_token"
206
+ },
207
+ "items": [
208
+ "nest.access_token",
209
+ "nest.fieldTest"
210
+ ]
211
+ },
212
+ {
213
+ "type": "fieldset",
214
+ "title": "Google Account",
215
+ "expandable": true,
216
+ "expanded": {
217
+ "functionBody": "return model.google && model.google.issuetoken && model.google.cookie"
218
+ },
219
+ "items": [
220
+ "google.issuetoken",
221
+ "google.cookie",
222
+ "google.fieldTest"
223
+ ]
224
+ },
225
+ {
226
+ "type": "fieldset",
227
+ "title": "Options",
228
+ "expandable": true,
229
+ "expanded": true,
230
+ "items": [
231
+ "options.eveHistory",
232
+ "options.weather",
233
+ "options.elevation",
234
+ "options.hksv",
235
+ "options.ffmpegPath"
236
+ ]
237
+ }
120
238
  ]
121
239
  }
@@ -27,15 +27,16 @@
27
27
  // HomeKitDevice.HOMEKITHISTORY
28
28
  // HomeKitDevice.PLUGIN_NAME
29
29
  // HomeKitDevice.PLATFORM_NAME
30
+ // HomeKitDevice.TYPE
31
+ // HomeKitDevice.VERSION
30
32
  //
31
33
  // The following functions should be overriden in your class which extends this
32
34
  //
33
- // HomeKitDevice.addServices()
34
- // HomeKitDevice.removeServices()
35
- // HomeKitDevice.updateServices(deviceData)
36
- // HomeKitDevice.messageServices(type, message)
35
+ // HomeKitDevice.setupDevice()
36
+ // HomeKitDevice.removeDevice()
37
+ // HomeKitDevice.updateDevice(deviceData)
38
+ // HomeKitDevice.messageDevice(type, message)
37
39
  //
38
- // Code version 8/10/2024
39
40
  // Mark Hulskamp
40
41
  'use strict';
41
42
 
@@ -43,75 +44,77 @@
43
44
  import crypto from 'crypto';
44
45
  import EventEmitter from 'node:events';
45
46
 
47
+ // Define constants
48
+ const LOG_LEVELS = {
49
+ INFO: 'info',
50
+ SUCCESS: 'success',
51
+ WARN: 'warn',
52
+ ERROR: 'error',
53
+ DEBUG: 'debug',
54
+ };
55
+
46
56
  // Define our HomeKit device class
47
57
  export default class HomeKitDevice {
48
- static ADD = 'HomeKitDevice.add'; // Device add message
49
58
  static UPDATE = 'HomeKitDevice.update'; // Device update message
50
59
  static REMOVE = 'HomeKitDevice.remove'; // Device remove message
51
60
  static SET = 'HomeKitDevice.set'; // Device set property message
52
61
  static GET = 'HomeKitDevice.get'; // Device get property message
53
- static PLUGIN_NAME = undefined; // Homebridge plugin name (override)
54
- static PLATFORM_NAME = undefined; // Homebridge platform name (override)
55
- static HISTORY = undefined; // HomeKit History object (override)
62
+
63
+ static HK_PIN_3_2_3 = /^\d{3}-\d{2}-\d{3}$/;
64
+ static HK_PIN_4_4 = /^\d{4}-\d{4}$/;
65
+ static MAC_ADDR = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
66
+
67
+ // Override this in the class which extends
68
+ static PLUGIN_NAME = undefined; // Homebridge plugin name
69
+ static PLATFORM_NAME = undefined; // Homebridge platform name
70
+ static HISTORY = undefined; // HomeKit History object
71
+ static TYPE = 'base'; // String naming type of device
72
+ static VERSION = '2025.06.15'; // Code version
56
73
 
57
74
  deviceData = {}; // The devices data we store
58
75
  historyService = undefined; // HomeKit history service
59
- accessory = undefined; // Accessory service for this device
60
- hap = undefined; // HomeKit Accessory Protocol API stub
76
+ accessory = undefined; // HomeKit accessory service for this device
77
+ hap = undefined; // HomeKit Accessory Protocol (HAP) API stub
61
78
  log = undefined; // Logging function object
62
79
  uuid = undefined; // UUID for this instance
63
80
 
64
81
  // Internal data only for this class
65
82
  #platform = undefined; // Homebridge platform api
66
83
  #eventEmitter = undefined; // Event emitter to use for comms
84
+ #postSetupDetails = []; // Use for extra output details once a device has been setup
67
85
 
68
- constructor(accessory, api, log, eventEmitter, deviceData) {
86
+ constructor(accessory = undefined, api = undefined, log = undefined, eventEmitter = undefined, deviceData = {}) {
69
87
  // Validate the passed in logging object. We are expecting certain functions to be present
70
- if (
71
- typeof log?.info === 'function' &&
72
- typeof log?.success === 'function' &&
73
- typeof log?.warn === 'function' &&
74
- typeof log?.error === 'function' &&
75
- typeof log?.debug === 'function'
76
- ) {
88
+ if (Object.values(LOG_LEVELS).every((fn) => typeof log?.[fn] === 'function')) {
77
89
  this.log = log;
78
90
  }
79
91
 
80
- // Workout if we're running under HomeBridge or HAP-NodeJS library
92
+ // Workout if we're running under Homebridge or HAP-NodeJS library
81
93
  if (isNaN(api?.version) === false && typeof api?.hap === 'object' && api?.HAPLibraryVersion === undefined) {
82
- // We have the HomeBridge version number and hap API object
94
+ // We have the Homebridge version number and hap API object
83
95
  this.hap = api.hap;
84
96
  this.#platform = api;
85
97
 
86
- this?.log?.debug && this.log.debug('HomeKitDevice module using Homebridge backend for "%s"', deviceData?.description);
98
+ this.postSetupDetail('Homebridge backend', LOG_LEVELS.DEBUG);
87
99
  }
88
100
 
89
101
  if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
90
- // As we're missing the HomeBridge entry points but have the HAP library version
102
+ // As we're missing the Homebridge entry points but have the HAP library version
91
103
  this.hap = api;
92
104
 
93
- this?.log?.debug && this.log.debug('HomeKitDevice module using HAP-NodeJS library for "%s"', deviceData?.description);
105
+ this.postSetupDetail('HAP-NodeJS library', LOG_LEVELS.DEBUG);
94
106
  }
95
107
 
96
108
  // Generate UUID for this device instance
97
109
  // Will either be a random generated one or HAP generated one
98
110
  // 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());
108
- }
111
+ this.uuid = HomeKitDevice.generateUUID(HomeKitDevice.PLUGIN_NAME, api, deviceData.serialNumber);
109
112
 
110
113
  // See if we were passed in an existing accessory object or array of accessory objects
111
- // Mainly used to restore a HomeBridge cached accessory
114
+ // Mainly used to restore a Homebridge cached accessory
112
115
  if (typeof accessory === 'object' && this.#platform !== undefined) {
113
116
  if (Array.isArray(accessory) === true) {
114
- this.accessory = accessory.find((accessory) => accessory?.UUID === this.uuid);
117
+ this.accessory = accessory.find((accessory) => this?.uuid !== undefined && accessory?.UUID === this.uuid);
115
118
  }
116
119
  if (Array.isArray(accessory) === false && accessory?.UUID === this.uuid) {
117
120
  this.accessory = accessory;
@@ -155,17 +158,17 @@ export default class HomeKitDevice {
155
158
  this.deviceData.manufacturer === '' ||
156
159
  (this.#platform === undefined &&
157
160
  (typeof this.deviceData?.hkPairingCode !== 'string' ||
158
- (new RegExp(/^([0-9]{3}-[0-9]{2}-[0-9]{3})$/).test(this.deviceData.hkPairingCode) === false &&
159
- new RegExp(/^([0-9]{4}-[0-9]{4})$/).test(this.deviceData.hkPairingCode) === false) ||
161
+ (HomeKitDevice.HK_PIN_3_2_3.test(this.deviceData.hkPairingCode) === false &&
162
+ HomeKitDevice.HK_PIN_4_4.test(this.deviceData.hkPairingCode) === false) ||
160
163
  typeof this.deviceData?.hkUsername !== 'string' ||
161
- new RegExp(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/).test(this.deviceData.hkUsername) === false))
164
+ HomeKitDevice.MAC_ADDR.test(this.deviceData.hkUsername) === false))
162
165
  ) {
163
166
  return;
164
167
  }
165
168
 
166
169
  // If we do not have an existing accessory object, create a new one
167
170
  if (this.accessory === undefined && this.#platform !== undefined) {
168
- // Create HomeBridge platform accessory
171
+ // Create Homebridge platform accessory
169
172
  this.accessory = new this.#platform.platformAccessory(this.deviceData.description, this.uuid);
170
173
  this.#platform.registerPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
171
174
  }
@@ -194,21 +197,32 @@ export default class HomeKitDevice {
194
197
  this.historyService = new HomeKitDevice.HISTORY(this.accessory, this.log, this.hap, {});
195
198
  }
196
199
 
197
- if (typeof this.addServices === 'function') {
200
+ if (typeof this?.setupDevice === 'function') {
198
201
  try {
199
- let postSetupDetails = await this.addServices();
200
- this?.log?.info &&
201
- this.log.info('Setup %s %s as "%s"', this.deviceData.manufacturer, this.deviceData.model, this.deviceData.description);
202
+ this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber, LOG_LEVELS.DEBUG);
203
+
204
+ await this.setupDevice();
205
+
202
206
  if (this.historyService?.EveHome !== undefined) {
203
- this?.log?.info && this.log.info(' += EveHome support as "%s"', this.historyService.EveHome.evetype);
204
- }
205
- if (typeof postSetupDetails === 'object') {
206
- postSetupDetails.forEach((output) => {
207
- this?.log?.info && this.log.info(' += %s', output);
208
- });
207
+ this.postSetupDetail('EveHome support as "%s"', this.historyService.EveHome.evetype);
209
208
  }
209
+
210
+ this?.log?.info?.('Setup %s %s as "%s"', this.deviceData.manufacturer, this.deviceData.model, this.deviceData.description);
211
+ this.#postSetupDetails.forEach((entry) => {
212
+ if (typeof entry === 'string') {
213
+ this?.log?.[LOG_LEVELS.INFO]?.(' += %s', entry);
214
+ } else if (typeof entry?.message === 'string') {
215
+ let level =
216
+ Object.hasOwn(LOG_LEVELS, entry?.level?.toUpperCase?.()) &&
217
+ typeof this?.log?.[LOG_LEVELS[entry.level.toUpperCase()]] === 'function'
218
+ ? LOG_LEVELS[entry.level.toUpperCase()]
219
+ : LOG_LEVELS.INFO;
220
+
221
+ this?.log?.[level]?.(' += ' + entry.message, ...(Array.isArray(entry?.args) ? entry.args : []));
222
+ }
223
+ });
210
224
  } catch (error) {
211
- this?.log?.error && this.log.error('addServices call for device "%s" failed. Error was', this.deviceData.description, error);
225
+ this?.log?.error('setupDevice call for device "%s" failed. Error was', this.deviceData.description, error);
212
226
  }
213
227
  }
214
228
 
@@ -223,26 +237,26 @@ export default class HomeKitDevice {
223
237
  category: this.accessory.category,
224
238
  });
225
239
 
226
- this?.log?.info && this.log.info(' += Advertising as "%s"', this.accessory.displayName);
227
- this?.log?.info && this.log.info(' += Pairing code is "%s"', this.accessory.pincode);
240
+ this?.log?.info(' += Advertising as "%s"', this.accessory.displayName);
241
+ this?.log?.info(' += Pairing code is "%s"', this.accessory.pincode);
228
242
  }
229
-
243
+ this.#postSetupDetails = []; // Don't need these anymore
230
244
  return this.accessory; // Return our HomeKit accessory
231
245
  }
232
246
 
233
247
  remove() {
234
- this?.log?.warn && this.log.warn('Device "%s" has been removed', this.deviceData.description);
248
+ this?.log?.warn?.('Device "%s" has been removed', this.deviceData.description);
235
249
 
236
250
  if (this.#eventEmitter !== undefined) {
237
251
  // Remove listener for 'messages'
238
252
  this.#eventEmitter.removeAllListeners(this.uuid);
239
253
  }
240
254
 
241
- if (typeof this.removeServices === 'function') {
255
+ if (typeof this?.removeDevice === 'function') {
242
256
  try {
243
- this.removeServices();
257
+ this.removeDevice();
244
258
  } catch (error) {
245
- this?.log?.error && this.log.error('removeServices call for device "%s" failed. Error was', this.deviceData.description, error);
259
+ this?.log?.error('removeDevice call for device "%s" failed. Error was', this.deviceData.description, error);
246
260
  }
247
261
  }
248
262
 
@@ -332,8 +346,8 @@ export default class HomeKitDevice {
332
346
  deviceData.serialNumber !== '' &&
333
347
  deviceData.serialNumber.toUpperCase() !== this.deviceData.serialNumber.toUpperCase()
334
348
  ) {
335
- this?.log?.warn && this.log.warn('Serial number on "%s" has changed', deviceData.description);
336
- this?.log?.warn && this.log.warn('This may cause the device to become unresponsive in HomeKit');
349
+ this?.log?.warn?.('Serial number on "%s" has changed', deviceData.description);
350
+ this?.log?.warn?.('This may cause the device to become unresponsive in HomeKit');
337
351
 
338
352
  // Update software version on the HomeKit accessory
339
353
  informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, deviceData.serialNumber);
@@ -343,19 +357,19 @@ export default class HomeKitDevice {
343
357
  if (typeof deviceData?.online === 'boolean' && deviceData.online !== this.deviceData.online) {
344
358
  // Output device online/offline status
345
359
  if (deviceData.online === false) {
346
- this?.log?.warn && this.log.warn('Device "%s" is offline', deviceData.description);
360
+ this?.log?.warn?.('Device "%s" is offline', deviceData.description);
347
361
  }
348
362
 
349
363
  if (deviceData.online === true) {
350
- this?.log?.success && this.log.success('Device "%s" is online', deviceData.description);
364
+ this?.log?.success?.('Device "%s" is online', deviceData.description);
351
365
  }
352
366
  }
353
367
 
354
- if (typeof this.updateServices === 'function') {
368
+ if (typeof this?.updateDevice === 'function') {
355
369
  try {
356
- this.updateServices(deviceData); // Pass updated data on for accessory to process as it needs
370
+ this.updateDevice(deviceData); // Pass updated data on for accessory to process as it needs
357
371
  } catch (error) {
358
- this?.log?.error && this.log.error('updateServices call for device "%s" failed. Error was', deviceData.description, error);
372
+ this?.log?.error('updateDevice call for device "%s" failed. Error was', deviceData.description, error);
359
373
  }
360
374
  }
361
375
 
@@ -397,14 +411,6 @@ export default class HomeKitDevice {
397
411
 
398
412
  #message(type, message) {
399
413
  switch (type) {
400
- case HomeKitDevice.ADD: {
401
- // Got message for device add
402
- if (typeof message?.name === 'string' && isNaN(message?.category) === false && typeof message?.history === 'boolean') {
403
- this.add(message.name, Number(message.category), message.history);
404
- }
405
- break;
406
- }
407
-
408
414
  case HomeKitDevice.UPDATE: {
409
415
  // Got some device data, so process any updates
410
416
  this.update(message, false);
@@ -419,16 +425,136 @@ export default class HomeKitDevice {
419
425
 
420
426
  default: {
421
427
  // This is not a message we know about, so pass onto accessory for it to perform any processing
422
- if (typeof this.messageServices === 'function') {
428
+ if (typeof this?.messageDevice === 'function') {
423
429
  try {
424
- this.messageServices(type, message);
430
+ this.messageDevice(type, message);
425
431
  } catch (error) {
426
- this?.log?.error &&
427
- this.log.error('messageServices call for device "%s" failed. Error was', this.deviceData.description, error);
432
+ this?.log?.error('messageDevice call for device "%s" failed. Error was', this.deviceData.description, error);
428
433
  }
429
434
  }
430
435
  break;
431
436
  }
432
437
  }
433
438
  }
439
+
440
+ addHKService(hkServiceType, name = '', subType = undefined) {
441
+ let service = undefined;
442
+
443
+ if (
444
+ hkServiceType !== undefined &&
445
+ typeof this?.accessory?.getService === 'function' &&
446
+ typeof this?.accessory?.getServiceById === 'function' &&
447
+ typeof this?.accessory?.addService === 'function'
448
+ ) {
449
+ if (subType !== undefined) {
450
+ service = this.accessory.getServiceById(hkServiceType, subType);
451
+ } else {
452
+ service = this.accessory.getService(hkServiceType);
453
+ }
454
+
455
+ if (service === undefined) {
456
+ service = this.accessory.addService(hkServiceType, name, subType);
457
+ }
458
+ }
459
+
460
+ return service;
461
+ }
462
+
463
+ addHKCharacteristic(hkService, hkCharacteristicType, { props, onSet, onGet } = {}) {
464
+ let characteristic = undefined;
465
+
466
+ if (
467
+ hkCharacteristicType !== undefined &&
468
+ typeof hkService?.getCharacteristic === 'function' &&
469
+ typeof hkService?.testCharacteristic === 'function' &&
470
+ typeof hkService?.addCharacteristic === 'function' &&
471
+ typeof hkService?.addOptionalCharacteristic === 'function'
472
+ ) {
473
+ if (hkService.testCharacteristic(hkCharacteristicType) === false) {
474
+ if (
475
+ Array.isArray(hkService?.optionalCharacteristics) &&
476
+ hkService.optionalCharacteristics.includes(hkCharacteristicType) &&
477
+ typeof hkService?.addOptionalCharacteristic === 'function'
478
+ ) {
479
+ hkService.addOptionalCharacteristic(hkCharacteristicType);
480
+ } else {
481
+ hkService.addCharacteristic(hkCharacteristicType);
482
+ }
483
+ }
484
+
485
+ characteristic = hkService.getCharacteristic(hkCharacteristicType);
486
+
487
+ // Apply optional config
488
+ if (typeof onSet === 'function') {
489
+ characteristic.onSet(onSet);
490
+ }
491
+ if (typeof onGet === 'function') {
492
+ characteristic.onGet(onGet);
493
+ }
494
+ if (typeof props === 'object' && typeof characteristic.setProps === 'function') {
495
+ characteristic.setProps(props);
496
+ }
497
+ }
498
+ return characteristic;
499
+ }
500
+
501
+ postSetupDetail(message, ...args) {
502
+ if (typeof message !== 'string' || message === '') {
503
+ return;
504
+ }
505
+
506
+ let levelKey = 'INFO';
507
+ let lastArg = args.at(-1);
508
+
509
+ if (typeof lastArg === 'string' && Object.hasOwn(LOG_LEVELS, lastArg.toUpperCase())) {
510
+ levelKey = lastArg.toUpperCase();
511
+ args = args.slice(0, -1);
512
+ }
513
+
514
+ this.#postSetupDetails.push({
515
+ level: LOG_LEVELS[levelKey], // 'info', 'debug', etc.
516
+ message,
517
+ args: args.length > 0 ? args : undefined,
518
+ });
519
+ }
520
+
521
+ static generateUUID(PLUGIN_NAME, api, serialNumber) {
522
+ let hap = undefined;
523
+ let uuid = crypto.randomUUID();
524
+
525
+ // Workout if we're running under Homebridge or HAP-NodeJS library
526
+ if (isNaN(api?.version) === false && typeof api?.hap === 'object' && api?.HAPLibraryVersion === undefined) {
527
+ // We have the Homebridge version number and hap API object
528
+ hap = api.hap;
529
+ }
530
+
531
+ if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
532
+ // As we're missing the Homebridge entry points but have the HAP library version
533
+ hap = api;
534
+ }
535
+
536
+ if (
537
+ typeof PLUGIN_NAME === 'string' &&
538
+ PLUGIN_NAME !== '' &&
539
+ typeof serialNumber === 'string' &&
540
+ serialNumber !== '' &&
541
+ typeof hap?.uuid?.generate === 'function'
542
+ ) {
543
+ uuid = hap.uuid.generate(PLUGIN_NAME + '_' + serialNumber.toUpperCase());
544
+ }
545
+
546
+ return uuid;
547
+ }
548
+
549
+ static makeHomeKitName(name) {
550
+ // Strip invalid characters to meet HomeKit naming requirements
551
+ // Ensure only letters or numbers are at the beginning AND/OR end of string
552
+ // Matches against uni-code characters
553
+ return typeof name === 'string'
554
+ ? name
555
+ .replace(/[^\p{L}\p{N}\p{Z}\u2019.,-]/gu, '')
556
+ .replace(/^[^\p{L}\p{N}]*/gu, '')
557
+ .replace(/[^\p{L}\p{N}]+$/gu, '')
558
+ : name;
559
+ }
434
560
  }
@@ -10,7 +10,7 @@
10
10
  //
11
11
  // Credit to https://github.com/simont77/fakegato-history for the work on starting the EveHome comms protocol decoding
12
12
  //
13
- // Version 2025/18/01
13
+ // Version 2025/01/18
14
14
  // Mark Hulskamp
15
15
 
16
16
  // Define nodejs module requirements