homebridge-nest-accfactory 0.3.0 → 0.3.2

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.
@@ -1,41 +1,42 @@
1
1
  // HomeKitDevice class
2
2
  //
3
- // This is the base class for all HomeKit accessories we code for in Homebridge/HAP-NodeJS
3
+ // Base class for all HomeKit accessories using Homebridge or HAP-NodeJS.
4
4
  //
5
- // The deviceData structure should at a minimum contain the following elements:
5
+ // Provides internal device tracking, metadata validation, lifecycle management,
6
+ // centralized message dispatch, and optional EveHome-compatible history logging.
6
7
  //
7
- // Homebridge Plugin:
8
+ // The `deviceData` object must include:
9
+ // serialNumber, softwareVersion, description, manufacturer, model
8
10
  //
9
- // serialNumber
10
- // softwareVersion
11
- // description
12
- // manufacturer
13
- // model
11
+ // For enabling EveHome history support, include in the `deviceData`:
12
+ // eveHistory
14
13
  //
15
- // HAP-NodeJS Library Accessory:
14
+ // For HAP-NodeJS standalone mode, also required:
15
+ // hkUsername, hkPairingCode
16
16
  //
17
- // serialNumber
18
- // softwareVersion
19
- // description
20
- // manufacturer
21
- // model
22
- // hkUsername
23
- // hkPairingCode
17
+ // The following static constants should be defined in subclasses:
18
+ // HomeKitDevice.PLUGIN_NAME // Required (string)
19
+ // HomeKitDevice.PLATFORM_NAME // Required (string)
20
+ // HomeKitDevice.TYPE // Optional (device type string)
21
+ // HomeKitDevice.VERSION // Optional (device code version)
22
+ // HomeKitDevice.EVEHOME // Optional (EveHome-compatible history module)
24
23
  //
25
- // Following constants should be overridden in the module loading this class file
24
+ // The following instance methods can be optionally implemented by subclasses:
25
+ // async onAdd(message, ...args) // Called when HomeKitDevice.ADD is received
26
+ // async onUpdate(deviceData, ...args) // Called when HomeKitDevice.UPDATE is received
27
+ // async onRemove(message, ...args) // Called when HomeKitDevice.REMOVE is received
28
+ // async onHistory(target, entry) // Called after a history entry is logged
29
+ // async onGet(message, ...args) // Called when HomeKitDevice.GET is received
30
+ // async onSet(message, ...args) // Called when HomeKitDevice.SET is received
31
+ // async onMessage(type, message) // Called for unhandled or custom message types
26
32
  //
27
- // HomeKitDevice.HOMEKITHISTORY
28
- // HomeKitDevice.PLUGIN_NAME
29
- // HomeKitDevice.PLATFORM_NAME
30
- // HomeKitDevice.TYPE
31
- // HomeKitDevice.VERSION
33
+ // Messages should be sent via:
34
+ // await device.message(type, message, ...args);
32
35
  //
33
- // The following functions should be overriden in your class which extends this
36
+ // All internal lifecycle events (`add`, `update`, `remove`, `history`, `get`, `set`) and external
37
+ // interactions should use the `message()` dispatch system for consistency.
34
38
  //
35
- // HomeKitDevice.setupDevice()
36
- // HomeKitDevice.removeDevice()
37
- // HomeKitDevice.updateDevice(deviceData)
38
- // HomeKitDevice.messageDevice(type, message)
39
+ // See README.md for usage examples and detailed documentation.
39
40
  //
40
41
  // Mark Hulskamp
41
42
  'use strict';
@@ -45,106 +46,116 @@ import crypto from 'crypto';
45
46
  import EventEmitter from 'node:events';
46
47
 
47
48
  // Define constants
48
- const HK_PIN_3_2_3 = /^\d{3}-\d{2}-\d{3}$/;
49
- const HK_PIN_4_4 = /^\d{4}-\d{4}$/;
50
- const MAC_ADDR = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
51
- const LOGLEVELS = {
52
- info: 'info',
53
- success: 'success',
54
- warn: 'warn',
55
- error: 'error',
56
- debug: 'debug',
49
+ const LOG_LEVELS = {
50
+ INFO: 'info',
51
+ SUCCESS: 'success',
52
+ WARN: 'warn',
53
+ ERROR: 'error',
54
+ DEBUG: 'debug',
57
55
  };
58
56
 
59
57
  // Define our HomeKit device class
60
- class HomeKitDevice {
61
- static UPDATE = 'HomeKitDevice.update'; // Device update message
62
- static REMOVE = 'HomeKitDevice.remove'; // Device remove message
63
- static SET = 'HomeKitDevice.set'; // Device set property message
64
- static GET = 'HomeKitDevice.get'; // Device get property message
58
+ export default class HomeKitDevice extends EventEmitter {
59
+ // Device messages
60
+ static ADD = 'HomeKitDevice.onAdd';
61
+ static UPDATE = 'HomeKitDevice.onUpdate';
62
+ static REMOVE = 'HomeKitDevice.onRemove';
63
+ static HISTORY = 'HomeKitDevice.onHistory';
64
+ static SET = 'HomeKitDevice.onSet';
65
+ static GET = 'HomeKitDevice.onGet';
66
+ static MESSAGE = 'HomeKitDevice.onMessage';
67
+ static SHUTDOWN = 'HomeKitDevice.onShutdown';
68
+
69
+ // HomeKit pin format and MAC address regex's
70
+ static HK_PIN_3_2_3 = /^\d{3}-\d{2}-\d{3}$/;
71
+ static HK_PIN_4_4 = /^\d{4}-\d{4}$/;
72
+ static MAC_ADDR = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
65
73
 
66
74
  // Override this in the class which extends
67
75
  static PLUGIN_NAME = undefined; // Homebridge plugin name
68
76
  static PLATFORM_NAME = undefined; // Homebridge platform name
69
- static HISTORY = undefined; // HomeKit History object
77
+ static EVEHOME = undefined; // HomeKit History object
70
78
  static TYPE = 'base'; // String naming type of device
71
- static VERSION = '2025.06.12'; // Code version
79
+ static VERSION = '2025.07.29'; // Code version
80
+
81
+ // Backend types
82
+ static HOMEBRIDGE = 'homebridge';
83
+ static HAP_NODEJS = 'hap-nodejs';
84
+
85
+ // Internal device and listener registry
86
+ static #listeners = {};
87
+ static #deviceRegistry = new Map();
72
88
 
73
89
  deviceData = {}; // The devices data we store
74
90
  historyService = undefined; // HomeKit history service
75
91
  accessory = undefined; // HomeKit accessory service for this device
76
92
  hap = undefined; // HomeKit Accessory Protocol (HAP) API stub
77
93
  log = undefined; // Logging function object
78
- uuid = undefined; // UUID for this instance
94
+ backend = undefined; // Backend library type
79
95
 
80
96
  // Internal data only for this class
81
- #platform = undefined; // Homebridge platform api
82
- #eventEmitter = undefined; // Event emitter to use for comms
97
+ #uuid = undefined; // UUID for this instance
98
+ #platform = undefined; // Homebridge platform API
83
99
  #postSetupDetails = []; // Use for extra output details once a device has been setup
84
100
 
85
- constructor(accessory, api, log, eventEmitter, deviceData) {
101
+ constructor(accessory = undefined, api = undefined, log = undefined, deviceData = {}) {
102
+ super(); // Setup event emitter for our class ONLY
103
+
86
104
  // Validate the passed in logging object. We are expecting certain functions to be present
87
- if (Object.keys(LOGLEVELS).every((fn) => typeof log?.[fn] === 'function')) {
105
+ if (Object.values(LOG_LEVELS).every((fn) => typeof log?.[fn] === 'function')) {
88
106
  this.log = log;
89
107
  }
90
108
 
91
- // Workout if we're running under Homebridge or HAP-NodeJS library
92
- if (isNaN(api?.version) === false && typeof api?.hap === 'object' && api?.HAPLibraryVersion === undefined) {
93
- // We have the Homebridge version number and hap API object
109
+ // Determine runtime environment (Homebridge vs HAP-NodeJS)
110
+ if (typeof api?.hap === 'object' && isNaN(api?.version) === false && typeof api?.HAPLibraryVersion === 'undefined') {
94
111
  this.hap = api.hap;
95
112
  this.#platform = api;
96
-
97
- this.postSetupDetail('Homebridge backend', LOGLEVELS.debug);
113
+ this.backend = HomeKitDevice.HOMEBRIDGE;
114
+ this.postSetupDetail('Homebridge backend', LOG_LEVELS.DEBUG);
98
115
  }
99
116
 
100
- if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
101
- // As we're missing the Homebridge entry points but have the HAP library version
117
+ if (typeof api?.hap === 'undefined' && isNaN(api?.version) === true && typeof api?.HAPLibraryVersion === 'function') {
102
118
  this.hap = api;
103
-
104
- this.postSetupDetail('HAP-NodeJS library', LOGLEVELS.debug);
119
+ this.backend = HomeKitDevice.HAP_NODEJS;
120
+ this.postSetupDetail('HAP-NodeJS library', LOG_LEVELS.DEBUG);
105
121
  }
106
122
 
107
123
  // Generate UUID for this device instance
108
124
  // Will either be a random generated one or HAP generated one
109
125
  // HAP is based upon defined plugin name and devices serial number
110
- this.uuid = HomeKitDevice.generateUUID(HomeKitDevice.PLUGIN_NAME, api, deviceData.serialNumber);
126
+ this.#uuid = HomeKitDevice.generateUUID(HomeKitDevice.PLUGIN_NAME, api, deviceData.serialNumber);
127
+
128
+ // Register this device instance in the static device registry
129
+ HomeKitDevice.#deviceRegistry.set(this.#uuid, this);
111
130
 
112
131
  // See if we were passed in an existing accessory object or array of accessory objects
113
132
  // Mainly used to restore a Homebridge cached accessory
114
- if (typeof accessory === 'object' && this.#platform !== undefined) {
133
+ if (typeof accessory === 'object' && this.backend === HomeKitDevice.HOMEBRIDGE) {
115
134
  if (Array.isArray(accessory) === true) {
116
- this.accessory = accessory.find((accessory) => this?.uuid !== undefined && accessory?.UUID === this.uuid);
135
+ this.accessory = accessory.find((accessory) => this?.uuid !== undefined && accessory?.UUID === this.#uuid);
117
136
  }
118
- if (Array.isArray(accessory) === false && accessory?.UUID === this.uuid) {
137
+ if (Array.isArray(accessory) === false && accessory?.UUID === this.#uuid) {
119
138
  this.accessory = accessory;
120
139
  }
121
140
  }
122
141
 
123
- // Validate if eventEmitter object passed to us is an instance of EventEmitter
124
- // If valid, setup an event listener for messages to this device using our generated uuid
125
- if (eventEmitter instanceof EventEmitter === true) {
126
- this.#eventEmitter = eventEmitter;
127
- this.#eventEmitter.addListener(this.uuid, this.#message.bind(this));
128
- }
129
-
130
142
  // Make a clone of current data and store in this object
131
143
  // Important that we done have a 'linked' copy of the object data
132
- // eslint-disable-next-line no-undef
133
144
  this.deviceData = structuredClone(deviceData);
134
145
  }
135
146
 
136
147
  // Class functions
137
- async add(accessoryName, accessoryCategory, useHistoryService) {
148
+ async add(hapAccessoryName, hapCategory, enableHistory = false) {
138
149
  if (
139
150
  this.hap === undefined ||
140
151
  typeof HomeKitDevice.PLUGIN_NAME !== 'string' ||
141
152
  HomeKitDevice.PLUGIN_NAME === '' ||
142
153
  typeof HomeKitDevice.PLATFORM_NAME !== 'string' ||
143
154
  HomeKitDevice.PLATFORM_NAME === '' ||
144
- typeof accessoryName !== 'string' ||
145
- accessoryName === '' ||
146
- typeof this.hap.Categories[accessoryCategory] === 'undefined' ||
147
- typeof useHistoryService !== 'boolean' ||
155
+ typeof hapAccessoryName !== 'string' ||
156
+ hapAccessoryName === '' ||
157
+ typeof this.hap.Categories[hapCategory] === 'undefined' ||
158
+ typeof enableHistory !== 'boolean' ||
148
159
  typeof this.deviceData !== 'object' ||
149
160
  typeof this.deviceData?.serialNumber !== 'string' ||
150
161
  this.deviceData.serialNumber === '' ||
@@ -157,32 +168,42 @@ class HomeKitDevice {
157
168
  this.deviceData.manufacturer === '' ||
158
169
  (this.#platform === undefined &&
159
170
  (typeof this.deviceData?.hkPairingCode !== 'string' ||
160
- (new RegExp(HK_PIN_3_2_3).test(this.deviceData.hkPairingCode) === false &&
161
- new RegExp(HK_PIN_4_4).test(this.deviceData.hkPairingCode) === false) ||
171
+ (HomeKitDevice.HK_PIN_3_2_3.test(this.deviceData.hkPairingCode) === false &&
172
+ HomeKitDevice.HK_PIN_4_4.test(this.deviceData.hkPairingCode) === false) ||
162
173
  typeof this.deviceData?.hkUsername !== 'string' ||
163
- new RegExp(MAC_ADDR).test(this.deviceData.hkUsername) === false))
174
+ HomeKitDevice.MAC_ADDR.test(this.deviceData.hkUsername) === false))
164
175
  ) {
165
176
  return;
166
177
  }
167
178
 
168
179
  // If we do not have an existing accessory object, create a new one
169
- if (this.accessory === undefined && this.#platform !== undefined) {
180
+ if (
181
+ this.accessory === undefined &&
182
+ typeof this.#platform?.platformAccessory === 'function' &&
183
+ typeof this.#platform?.registerPlatformAccessories === 'function' &&
184
+ this.backend === HomeKitDevice.HOMEBRIDGE
185
+ ) {
170
186
  // Create Homebridge platform accessory
171
- this.accessory = new this.#platform.platformAccessory(this.deviceData.description, this.uuid);
187
+ this.accessory = new this.#platform.platformAccessory(this.deviceData.description, this.#uuid);
172
188
  this.#platform.registerPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
173
189
  }
174
190
 
175
- if (this.accessory === undefined && this.#platform === undefined) {
191
+ if (this.accessory === undefined && this.backend === HomeKitDevice.HAP_NODEJS) {
176
192
  // Create HAP-NodeJS libray accessory
177
- this.accessory = new this.hap.Accessory(accessoryName, this.uuid);
193
+ this.accessory = new this.hap.Accessory(hapAccessoryName, this.#uuid);
178
194
 
179
195
  this.accessory.username = this.deviceData.hkUsername;
180
196
  this.accessory.pincode = this.deviceData.hkPairingCode;
181
- this.accessory.category = accessoryCategory;
197
+ this.accessory.category = hapCategory;
182
198
  }
183
199
 
184
200
  // Setup accessory information
185
201
  let informationService = this.accessory.getService(this.hap.Service.AccessoryInformation);
202
+ if (informationService === undefined) {
203
+ this?.log?.error?.('AccessoryInformation service not found on accessory for "%s"', this.deviceData.description);
204
+ return;
205
+ }
206
+
186
207
  if (informationService !== undefined) {
187
208
  informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, this.deviceData.manufacturer);
188
209
  informationService.updateCharacteristic(this.hap.Characteristic.Model, this.deviceData.model);
@@ -192,248 +213,359 @@ class HomeKitDevice {
192
213
  }
193
214
 
194
215
  // Setup our history service if module has been defined and requested to be active for this device
195
- if (typeof HomeKitDevice?.HISTORY === 'function' && this.historyService === undefined && useHistoryService === true) {
196
- this.historyService = new HomeKitDevice.HISTORY(this.accessory, this.log, this.hap, {});
216
+ if (typeof HomeKitDevice?.EVEHOME === 'function' && this.historyService === undefined && enableHistory === true) {
217
+ this.historyService = new HomeKitDevice.EVEHOME(this.accessory, this.hap, this.log, {});
197
218
  }
198
219
 
199
- if (typeof this?.setupDevice === 'function') {
200
- try {
201
- this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber, LOGLEVELS.debug);
220
+ this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber, LOG_LEVELS.DEBUG);
202
221
 
203
- await this.setupDevice();
204
-
205
- if (this.historyService?.EveHome !== undefined) {
206
- this.postSetupDetail('EveHome support as "%s"', this.historyService.EveHome.evetype);
207
- }
222
+ // Trigger registered handlers (onAdd + listeners)
223
+ await this.message(HomeKitDevice.ADD);
208
224
 
209
- this?.log?.info?.('Setup %s %s as "%s"', this.deviceData.manufacturer, this.deviceData.model, this.deviceData.description);
225
+ if (this.historyService?.EveHome !== undefined) {
226
+ this.postSetupDetail('EveHome support as "%s"', this.historyService.EveHome.evetype);
227
+ }
210
228
 
211
- this.#postSetupDetails.forEach((entry) => {
212
- if (typeof entry === 'string') {
213
- this?.log?.[LOGLEVELS.info]?.(' += %s', entry);
214
- } else if (typeof entry?.message === 'string') {
215
- let level =
216
- Object.hasOwn(LOGLEVELS, entry?.level) && typeof this?.log?.[entry?.level] === 'function' ? entry.level : LOGLEVELS.info;
217
- this?.log?.[level]?.(' += ' + entry.message, ...(Array.isArray(entry?.args) ? entry.args : []));
218
- }
219
- });
220
- } catch (error) {
221
- this?.log?.error('setupDevice call for device "%s" failed. Error was', this.deviceData.description, error);
229
+ this?.log?.success?.('Setup %s %s as "%s"', this.deviceData.manufacturer, this.deviceData.model, this.deviceData.description);
230
+ this.#postSetupDetails.forEach((entry) => {
231
+ if (typeof entry === 'string') {
232
+ this?.log?.[LOG_LEVELS.INFO]?.(' += %s', entry);
233
+ } else if (typeof entry?.message === 'string') {
234
+ let level =
235
+ Object.hasOwn(LOG_LEVELS, entry?.level?.toUpperCase?.()) &&
236
+ typeof this?.log?.[LOG_LEVELS[entry.level.toUpperCase()]] === 'function'
237
+ ? LOG_LEVELS[entry.level.toUpperCase()]
238
+ : LOG_LEVELS.INFO;
239
+
240
+ this?.log?.[level]?.(' += ' + entry.message, ...(Array.isArray(entry?.args) ? entry.args : []));
222
241
  }
223
- }
242
+ });
224
243
 
225
- // Perform an initial update using current data
226
- this.update(this.deviceData, true);
244
+ // Trigger registered handlers (onUpdate + listeners) for initial device data updates
245
+ await this.message(HomeKitDevice.UPDATE, this.deviceData, { force: true });
227
246
 
228
247
  // If using HAP-NodeJS library, publish accessory on local network
229
- if (this.#platform === undefined && this.accessory !== undefined) {
248
+ if (this.accessory !== undefined && this.backend === HomeKitDevice.HAP_NODEJS) {
230
249
  this.accessory.publish({
231
250
  username: this.accessory.username,
232
251
  pincode: this.accessory.pincode,
233
252
  category: this.accessory.category,
234
253
  });
235
254
 
236
- this?.log?.info(' += Advertising as "%s"', this.accessory.displayName);
237
- this?.log?.info(' += Pairing code is "%s"', this.accessory.pincode);
255
+ this?.log?.info?.(' += Advertising as "%s"', this.accessory.displayName);
256
+ this?.log?.info?.(' += Pairing code is "%s"', this.accessory.pincode);
238
257
  }
239
- this.#postSetupDetails = []; // Dont' need these anymore
258
+ this.#postSetupDetails = []; // Don't need these anymore
240
259
  return this.accessory; // Return our HomeKit accessory
241
260
  }
242
261
 
243
- remove() {
244
- this?.log?.warn?.('Device "%s" has been removed', this.deviceData.description);
262
+ async remove() {
263
+ // Trigger registered handlers (onRemove + listeners)
264
+ await this.message(HomeKitDevice.REMOVE);
265
+ }
245
266
 
246
- if (this.#eventEmitter !== undefined) {
247
- // Remove listener for 'messages'
248
- this.#eventEmitter.removeAllListeners(this.uuid);
267
+ async update(deviceData, ...args) {
268
+ if (typeof deviceData !== 'object') {
269
+ return;
249
270
  }
250
271
 
251
- if (typeof this?.removeDevice === 'function') {
252
- try {
253
- this.removeDevice();
254
- } catch (error) {
255
- this?.log?.error('removeDevice call for device "%s" failed. Error was', this.deviceData.description, error);
256
- }
257
- }
272
+ // Trigger registered handlers (onUpdate + listeners)
273
+ await this.message(HomeKitDevice.UPDATE, deviceData, ...args);
274
+ }
258
275
 
259
- if (this.accessory !== undefined && this.#platform !== undefined) {
260
- // Unregister the accessory from Homebridge platform
261
- this.#platform.unregisterPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
276
+ async history(target, entry, options = {}) {
277
+ if (
278
+ typeof this.historyService !== 'object' ||
279
+ typeof this.historyService.addHistory !== 'function' ||
280
+ typeof entry !== 'object' ||
281
+ typeof target !== 'object' ||
282
+ typeof target.UUID !== 'string'
283
+ ) {
284
+ return;
262
285
  }
263
286
 
264
- if (this.accessory !== undefined && this.#platform === undefined) {
265
- // Unpublish the accessory from HAP-NodeJS library
266
- this.accessory.unpublish();
287
+ // Trigger registered handlers (onHistory + listeners)
288
+ await this.message(HomeKitDevice.HISTORY, target, entry, options);
289
+ }
290
+
291
+ async set(values, ...args) {
292
+ if (typeof values !== 'object' || values === null) {
293
+ return;
267
294
  }
268
295
 
269
- this.deviceData = {};
270
- this.accessory = undefined;
271
- this.historyService = undefined;
272
- this.hap = undefined;
273
- this.log = undefined;
274
- this.uuid = undefined;
275
- this.#platform = undefined;
276
- this.#eventEmitter = undefined;
296
+ // Trigger registered handlers (onSet + listeners)
297
+ await this.message(HomeKitDevice.SET, values, ...args);
298
+ }
277
299
 
278
- // Do we destroy this object??
279
- // this = null;
280
- // delete this;
300
+ async get(values, ...args) {
301
+ // Trigger registered handlers (onGet + listeners)
302
+ return await this.message(HomeKitDevice.GET, values, ...args);
281
303
  }
282
304
 
283
- update(deviceData, forceUpdate) {
284
- if (typeof deviceData !== 'object' || typeof forceUpdate !== 'boolean') {
305
+ static async message(uuid, type, message = undefined, ...args) {
306
+ if (typeof uuid !== 'string' || uuid === '' || typeof type !== 'string' || type === '') {
285
307
  return;
286
308
  }
287
309
 
288
- // Updated data may only contain selected fields, so we'll handle that here by taking our internally stored data
289
- // and merge with the updates to ensure we have a complete data object
290
- Object.entries(this.deviceData).forEach(([key, value]) => {
291
- if (typeof deviceData[key] === 'undefined') {
292
- // Updated data doesn't have this key, so add it to our internally stored data
293
- deviceData[key] = value;
310
+ if (typeof message === 'function' || (typeof message === 'object' && message !== null && message?.constructor !== Object)) {
311
+ if (this.#listeners?.[uuid] === undefined) {
312
+ this.#listeners[uuid] = {};
313
+ }
314
+ if (Array.isArray(this.#listeners[uuid][type]) === false) {
315
+ this.#listeners[uuid][type] = [];
294
316
  }
295
- });
296
317
 
297
- // Check updated device data with our internally stored data. Flag if changes between the two
298
- let changedData = false;
299
- Object.keys(deviceData).forEach((key) => {
300
- if (JSON.stringify(deviceData[key]) !== JSON.stringify(this.deviceData[key])) {
301
- changedData = true;
318
+ let handler, context;
319
+
320
+ if (typeof message === 'function' || typeof message === 'string') {
321
+ handler = message;
322
+ context = undefined;
323
+ } else {
324
+ context = message;
325
+ handler = typeof type === 'string' ? type.match(/\.?(on[A-Z][a-zA-Z0-9]*)$/)?.[1] : undefined;
302
326
  }
303
- });
304
327
 
305
- // If we have any changed data OR we've been requested to force an update, do so here
306
- if ((changedData === true || forceUpdate === true) && this.accessory !== undefined) {
307
- let informationService = this.accessory.getService(this.hap.Service.AccessoryInformation);
308
- if (informationService !== undefined) {
309
- // Update details associated with the accessory
310
- // ie: Name, Manufacturer, Model, Serial # and firmware version
311
- if (typeof deviceData?.description === 'string' && deviceData.description !== this.deviceData.description) {
312
- // Update serial number on the HomeKit accessory
313
- informationService.updateCharacteristic(this.hap.Characteristic.Name, this.deviceData.description);
328
+ if (handler !== undefined) {
329
+ if (this.#listeners?.[uuid]?.[type]?.find?.((h) => h.handler === handler && h.context === context) === undefined) {
330
+ this.#listeners[uuid][type].push({ handler, context });
314
331
  }
332
+ }
315
333
 
316
- if (
317
- typeof deviceData?.manufacturer === 'string' &&
318
- deviceData.manufacturer !== '' &&
319
- deviceData.manufacturer !== this.deviceData.manufacturer
320
- ) {
321
- // Update manufacturer number on the HomeKit accessory
322
- informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, deviceData.manufacturer);
323
- }
334
+ return;
335
+ }
324
336
 
325
- if (typeof deviceData?.model === 'string' && deviceData.model !== '' && deviceData.model !== this.deviceData.model) {
326
- // Update model on the HomeKit accessory
327
- informationService.updateCharacteristic(this.hap.Characteristic.Model, deviceData.model);
328
- }
337
+ // Handle message delivery
338
+ return await this.#deviceRegistry.get(uuid)?.message?.(type, message, ...args);
339
+ }
329
340
 
330
- if (
331
- typeof deviceData?.softwareVersion === 'string' &&
332
- deviceData.softwareVersion !== '' &&
333
- deviceData.softwareVersion !== this.deviceData.softwareVersion
334
- ) {
335
- // Update software version on the HomeKit accessory
336
- informationService.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceData.softwareVersion);
337
- }
341
+ async message(type, message, ...args) {
342
+ if (typeof type !== 'string' || type === '') {
343
+ return;
344
+ }
338
345
 
339
- // Check for devices serial number changing. Really shouldn't occur, but handle case anyway
340
- if (
341
- typeof deviceData?.serialNumber === 'string' &&
342
- deviceData.serialNumber !== '' &&
343
- deviceData.serialNumber.toUpperCase() !== this.deviceData.serialNumber.toUpperCase()
344
- ) {
345
- this?.log?.warn?.('Serial number on "%s" has changed', deviceData.description);
346
- this?.log?.warn?.('This may cause the device to become unresponsive in HomeKit');
346
+ let result = { call: undefined, handler: undefined };
347
+ let handled = false;
348
+ let handler =
349
+ Array.isArray(HomeKitDevice.#listeners?.[this.#uuid]?.[type]) === true
350
+ ? HomeKitDevice.#listeners[this.#uuid][type]
351
+ : HomeKitDevice.#listeners?.[this.#uuid]?.[type] !== undefined
352
+ ? [HomeKitDevice.#listeners[this.#uuid][type]]
353
+ : [];
354
+
355
+ // Dynamically extract the handler method name from the type string (e.g., "HomeKitDevice.onAdd" becomes "onAdd")
356
+ // This allows consistent routing to instance methods like onAdd, onSet, onUpdate, etc.
357
+ let methodName = typeof type === 'string' ? type.match(/\.?(on[A-Z][a-zA-Z0-9]*)$/)?.[1] : undefined;
358
+
359
+ // Internal helper to call handlers with error trapping. Will also walk up the prototype chain
360
+ const callLifecycleHook = async (labelOrFn, ...params) => {
361
+ let results = [];
362
+ let called = new Set(); // track calls using context + function identity
363
+
364
+ const callMethodWithProtoChain = async (obj, method, contextLabel) => {
365
+ let current = obj;
366
+ let seen = new Set();
367
+
368
+ while (current && typeof current === 'object' && seen.has(current) === false) {
369
+ seen.add(current);
370
+
371
+ let fn = current?.[method];
372
+ if (typeof fn === 'function') {
373
+ let key = fn + '@' + obj;
374
+ if (called.has(key) === false) {
375
+ called.add(key);
376
+ try {
377
+ results.push(await fn.apply(obj, params));
378
+ } catch (error) {
379
+ this?.log?.warn?.('Error in %s.%s(): %s', contextLabel, method, String(error?.stack || error));
380
+ }
381
+ }
382
+ }
347
383
 
348
- // Update software version on the HomeKit accessory
349
- informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, deviceData.serialNumber);
384
+ current = Object.getPrototypeOf(current);
350
385
  }
351
- }
352
-
353
- if (typeof deviceData?.online === 'boolean' && deviceData.online !== this.deviceData.online) {
354
- // Output device online/offline status
355
- if (deviceData.online === false) {
356
- this?.log?.warn?.('Device "%s" is offline', deviceData.description);
386
+ };
387
+
388
+ if (typeof labelOrFn === 'string') {
389
+ await callMethodWithProtoChain(this, labelOrFn, this?.constructor?.name ?? 'this');
390
+ } else if (typeof labelOrFn === 'function') {
391
+ let key = labelOrFn + '@' + this;
392
+ if (called.has(key) === false) {
393
+ called.add(key);
394
+ try {
395
+ results.push(await labelOrFn(...params));
396
+ } catch (error) {
397
+ this?.log?.warn?.('Error in inline function handler: %s', String(error?.stack || error));
398
+ }
357
399
  }
358
-
359
- if (deviceData.online === true) {
360
- this?.log?.success?.('Device "%s" is online', deviceData.description);
400
+ } else if (Array.isArray(labelOrFn) === true) {
401
+ let [label, list] = labelOrFn;
402
+
403
+ for (let item of list || []) {
404
+ let fn = item?.handler;
405
+ let context = item?.context ?? this;
406
+ let key = fn + '@' + context;
407
+
408
+ if (typeof fn === 'function') {
409
+ if (called.has(key) === false) {
410
+ called.add(key);
411
+ try {
412
+ results.push(await fn.call(context, ...params));
413
+ } catch (error) {
414
+ this?.log?.warn?.('Error in registered %s(): %s', label, String(error?.stack || error));
415
+ }
416
+ }
417
+ } else if (typeof fn === 'string' && context) {
418
+ await callMethodWithProtoChain(context, fn, context?.constructor?.name ?? 'handler');
419
+ }
361
420
  }
362
421
  }
363
422
 
364
- if (typeof this?.updateDevice === 'function') {
365
- try {
366
- this.updateDevice(deviceData); // Pass updated data on for accessory to process as it needs
367
- } catch (error) {
368
- this?.log?.error('updateDevice call for device "%s" failed. Error was', deviceData.description, error);
423
+ return results.length === 1 ? results[0] : results;
424
+ };
425
+
426
+ // Handle built-in types with special behavior
427
+ if (type === HomeKitDevice.ADD || type === HomeKitDevice.REMOVE || type === HomeKitDevice.SET) {
428
+ // Call the dynamic on<Type> method (ie. onAdd, onRemove, onSet) and after
429
+ // Any static handler registered via HomeKitDevice.message(uuid, type, handler)
430
+ await callLifecycleHook(methodName, message, ...args);
431
+ await callLifecycleHook(['handler for ' + type, handler], message, ...args);
432
+ handled = true;
433
+
434
+ // Special setup for ADD
435
+ if (type === HomeKitDevice.ADD) {
436
+ // After the accessory is initialized and onAdd has run, link any EveHome services that requested it
437
+ if (this.deviceData?.eveHistory === true && typeof this.historyService?.linkToEveHome === 'function') {
438
+ for (let service of this.accessory?.services || []) {
439
+ let options = service?.[HomeKitDevice?.EVEHOME?.EVE_OPTIONS];
440
+ if (options !== undefined) {
441
+ delete service[HomeKitDevice?.EVEHOME?.EVE_OPTIONS];
442
+ this.historyService.linkToEveHome(service, options);
443
+ }
444
+ }
369
445
  }
370
446
  }
371
447
 
372
- // Finally, update our internally stored data with the new data
373
- // eslint-disable-next-line no-undef
374
- this.deviceData = structuredClone(deviceData);
375
- }
376
- }
448
+ // Special teardown for REMOVE
449
+ if (type === HomeKitDevice.REMOVE) {
450
+ this?.log?.warn?.('Device "%s" has been removed', this.deviceData.description);
451
+ this?.removeAllListeners?.();
452
+ HomeKitDevice.#deviceRegistry.delete(this.#uuid);
453
+ delete HomeKitDevice.#listeners[this.#uuid];
377
454
 
378
- async set(values) {
379
- if (typeof values !== 'object' || this.#eventEmitter === undefined) {
380
- return;
381
- }
455
+ if (this.accessory !== undefined && typeof this.#platform?.unregisterPlatformAccessories === 'function') {
456
+ this.#platform.unregisterPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
457
+ }
382
458
 
383
- // Send event with data to set
384
- this.#eventEmitter.emit(HomeKitDevice.SET, this.uuid, values);
459
+ if (this.accessory !== undefined && this.#platform === undefined) {
460
+ this.accessory.unpublish();
461
+ }
385
462
 
386
- // Update the internal data for the set values, as could take sometime once we emit the event
387
- Object.entries(values).forEach(([key, value]) => {
388
- if (this.deviceData[key] !== undefined) {
389
- this.deviceData[key] = value;
463
+ this.deviceData = {};
464
+ this.accessory = undefined;
465
+ this.historyService = undefined;
466
+ this.hap = undefined;
467
+ this.log = undefined;
468
+ this.#uuid = undefined;
469
+ this.#platform = undefined;
390
470
  }
391
- });
392
- }
393
-
394
- async get(values) {
395
- if (typeof values !== 'object' || this.#eventEmitter === undefined) {
396
- return;
397
- }
398
-
399
- // Send event with data to get
400
- // Once get has completed, we'll get an event back with the requested data
401
- this.#eventEmitter.emit(HomeKitDevice.GET, this.uuid, values);
402
471
 
403
- // This should always return, but we probably should put in a timeout?
404
- let results = await EventEmitter.once(this.#eventEmitter, HomeKitDevice.GET + '->' + this.uuid);
405
- return results?.[0];
406
- }
407
-
408
- #message(type, message) {
409
- switch (type) {
410
- case HomeKitDevice.UPDATE: {
411
- // Got some device data, so process any updates
412
- this.update(message, false);
413
- break;
472
+ // Update the internal data for the set values, as could take some time once we emit the event
473
+ if (type === HomeKitDevice.SET) {
474
+ if (typeof message === 'object' && message !== null) {
475
+ Object.entries(message).forEach(([key, value]) => {
476
+ if (this.deviceData?.[key] !== undefined) {
477
+ this.deviceData[key] = value;
478
+ }
479
+ });
480
+ }
414
481
  }
482
+ } else if (type === HomeKitDevice.UPDATE) {
483
+ if (typeof message === 'object' && message !== null) {
484
+ let { merged, changed } = this.#mergeDeviceData(message);
485
+ this.#updateAccessoryInformation(merged);
486
+
487
+ if (changed === true || (typeof args?.[0] === 'object' && args?.[0]?.force === true)) {
488
+ // Call the onUpdate method and after any static handler registered via HomeKitDevice.message(uuid, type, handler)
489
+ await callLifecycleHook('onUpdate', merged, ...args);
490
+ await callLifecycleHook(['handler for UPDATE', handler], merged, ...args);
491
+ }
415
492
 
416
- case HomeKitDevice.REMOVE: {
417
- // Got message for device removal
418
- this.remove();
419
- break;
493
+ // Finally, update our internally stored data with the new data
494
+ this.deviceData = structuredClone(merged);
420
495
  }
496
+ handled = true;
497
+ } else if (type === HomeKitDevice.HISTORY) {
498
+ let [target, entry, options = {}] = [message, args[0], args[1]];
499
+ let skipHistory = false;
500
+
501
+ if (
502
+ typeof this.historyService === 'object' &&
503
+ typeof this.historyService?.addHistory === 'function' &&
504
+ typeof entry === 'object' &&
505
+ typeof target === 'object' &&
506
+ typeof target.UUID === 'string'
507
+ ) {
508
+ if (isNaN(entry?.time) === true) {
509
+ entry.time = Math.floor(Date.now() / 1000);
510
+ }
421
511
 
422
- default: {
423
- // This is not a message we know about, so pass onto accessory for it to perform any processing
424
- if (typeof this?.messageDevice === 'function') {
425
- try {
426
- this.messageDevice(type, message);
427
- } catch (error) {
428
- this?.log?.error('messageDevice call for device "%s" failed. Error was', this.deviceData.description, error);
512
+ if (options?.force !== true && typeof this.historyService?.lastHistory === 'function') {
513
+ let last = this.historyService.lastHistory(target);
514
+ if (typeof last === 'object') {
515
+ let changed = Object.keys(entry).some((key) => {
516
+ if (key === 'time') {
517
+ return false;
518
+ }
519
+ let v = entry[key];
520
+ let lv = last[key];
521
+ return typeof v === 'object' ? JSON.stringify(v) !== JSON.stringify(lv) : v !== lv;
522
+ });
523
+ if (changed === false) {
524
+ skipHistory = true;
525
+ }
429
526
  }
430
527
  }
431
- break;
528
+
529
+ if (skipHistory === false) {
530
+ this.historyService.addHistory(target, entry, isNaN(options?.timegap) === false ? options.timegap : undefined);
531
+ }
432
532
  }
533
+
534
+ // Call the onHistory method and after any static handler registered via HomeKitDevice.message(uuid, type, handler)
535
+ await callLifecycleHook('onHistory', target, entry, options);
536
+ await callLifecycleHook(['handler for HISTORY', handler], target, entry, options);
537
+
538
+ handled = true;
539
+ }
540
+
541
+ // Dynamically handle any remaining on<Type> method (e.g., onGet etc that we haven’t handled yet)
542
+ // Any static handler registered via HomeKitDevice.message(uuid, type, handler)
543
+ if (handled === false && (typeof this?.[methodName] === 'function' || (Array.isArray(handler) === true && handler.length > 0))) {
544
+ // Use string method name so we get inheritance merging;
545
+ result.call = await callLifecycleHook(methodName, message, ...args);
546
+ result.handler = await callLifecycleHook(['handler for ' + type, handler], message, ...args);
547
+ handled = true;
548
+ }
549
+
550
+ // Call generic handler if present and we haven't handled the message yet
551
+ if (handled === false) {
552
+ result.call = await callLifecycleHook('onMessage', type, message, ...args);
553
+ handled = true;
554
+ }
555
+
556
+ // No handler at all — not even onMessage()
557
+ if (handled === false && (Array.isArray(handler) === false || handler.length === 0) && typeof this?.[methodName] !== 'function') {
558
+ this?.log?.warn?.('Unhandled message type "%s" for device "%s"', type, this.deviceData.description);
559
+ }
560
+
561
+ if (typeof result.call === 'object' && typeof result.handler === 'object') {
562
+ return Object.assign({}, result.call, result.handler);
433
563
  }
564
+
565
+ return result.call !== undefined ? result.call : result.handler;
434
566
  }
435
567
 
436
- addHKService(hkServiceType, name = '', subType = undefined) {
568
+ addHKService(hkServiceType, name = '', subType = undefined, eveOptions = undefined) {
437
569
  let service = undefined;
438
570
 
439
571
  if (
@@ -451,12 +583,22 @@ class HomeKitDevice {
451
583
  if (service === undefined) {
452
584
  service = this.accessory.addService(hkServiceType, name, subType);
453
585
  }
586
+
587
+ // Setup for EveHome history if enabled. The actual linkage will be done in .add() after returning from .onAdd()
588
+ if (
589
+ service !== undefined &&
590
+ typeof eveOptions === 'object' &&
591
+ this.deviceData?.eveHistory === true &&
592
+ typeof this.historyService?.linkToEveHome === 'function'
593
+ ) {
594
+ service[HomeKitDevice?.EVEHOME?.EVE_OPTIONS] = eveOptions;
595
+ }
454
596
  }
455
597
 
456
598
  return service;
457
599
  }
458
600
 
459
- addHKCharacteristic(hkService, hkCharacteristicType, { props, onSet, onGet } = {}) {
601
+ addHKCharacteristic(hkService, hkCharacteristicType, { props, onSet, onGet, initialValue } = {}) {
460
602
  let characteristic = undefined;
461
603
 
462
604
  if (
@@ -468,9 +610,8 @@ class HomeKitDevice {
468
610
  ) {
469
611
  if (hkService.testCharacteristic(hkCharacteristicType) === false) {
470
612
  if (
471
- Array.isArray(hkService?.optionalCharacteristics) &&
472
- hkService.optionalCharacteristics.includes(hkCharacteristicType) &&
473
- typeof hkService?.addOptionalCharacteristic === 'function'
613
+ Array.isArray(hkService?.optionalCharacteristics) === true &&
614
+ hkService.optionalCharacteristics.includes(hkCharacteristicType) === true
474
615
  ) {
475
616
  hkService.addOptionalCharacteristic(hkCharacteristicType);
476
617
  } else {
@@ -490,7 +631,13 @@ class HomeKitDevice {
490
631
  if (typeof props === 'object' && typeof characteristic.setProps === 'function') {
491
632
  characteristic.setProps(props);
492
633
  }
634
+
635
+ // Set initial value if provided
636
+ if (typeof initialValue !== 'undefined' && typeof hkService?.updateCharacteristic === 'function') {
637
+ hkService.updateCharacteristic(hkCharacteristicType, initialValue);
638
+ }
493
639
  }
640
+
494
641
  return characteristic;
495
642
  }
496
643
 
@@ -499,36 +646,29 @@ class HomeKitDevice {
499
646
  return;
500
647
  }
501
648
 
502
- let level = 'info';
503
- let availableLevel = Object.keys(LOGLEVELS).find((lvl) => typeof this.log?.[lvl] === 'function') || 'info';
649
+ let levelKey = 'INFO';
504
650
  let lastArg = args.at(-1);
505
651
 
506
- if (typeof lastArg === 'string' && Object.hasOwn(LOGLEVELS, lastArg)) {
507
- level = lastArg;
652
+ if (typeof lastArg === 'string' && Object.hasOwn(LOG_LEVELS, lastArg.toUpperCase())) {
653
+ levelKey = lastArg.toUpperCase();
508
654
  args = args.slice(0, -1);
509
- } else {
510
- level = availableLevel;
511
655
  }
512
656
 
513
657
  this.#postSetupDetails.push({
514
- level,
658
+ level: LOG_LEVELS[levelKey], // 'info', 'debug', etc.
515
659
  message,
516
660
  args: args.length > 0 ? args : undefined,
517
661
  });
518
662
  }
519
663
 
520
664
  static generateUUID(PLUGIN_NAME, api, serialNumber) {
521
- let hap = undefined;
665
+ let hap;
522
666
  let uuid = crypto.randomUUID();
523
667
 
524
- // Workout if we're running under Homebridge or HAP-NodeJS library
525
- if (isNaN(api?.version) === false && typeof api?.hap === 'object' && api?.HAPLibraryVersion === undefined) {
526
- // We have the Homebridge version number and hap API object
668
+ // Determine runtime environment (Homebridge vs HAP-NodeJS)
669
+ if (typeof api?.hap === 'object' && isNaN(api?.version) === false && typeof api?.HAPLibraryVersion === 'undefined') {
527
670
  hap = api.hap;
528
- }
529
-
530
- if (typeof api?.HAPLibraryVersion === 'function' && api?.version === undefined && api?.hap === undefined) {
531
- // As we're missing the Homebridge entry points but have the HAP library version
671
+ } else if (typeof api?.HAPLibraryVersion === 'function' && typeof api?.version === 'undefined' && typeof api?.hap === 'undefined') {
532
672
  hap = api;
533
673
  }
534
674
 
@@ -544,8 +684,110 @@ class HomeKitDevice {
544
684
 
545
685
  return uuid;
546
686
  }
547
- }
548
687
 
549
- // Define exports
550
- export { HK_PIN_3_2_3, HK_PIN_4_4, MAC_ADDR, HomeKitDevice };
551
- export default HomeKitDevice;
688
+ static makeValidHKName(name) {
689
+ // Strip invalid characters to meet HomeKit naming requirements
690
+ // Ensure only letters or numbers are at the beginning AND/OR end of string
691
+ // Matches against uni-code characters
692
+ return typeof name === 'string'
693
+ ? name
694
+ .replace(/[^\p{L}\p{N}\p{Z}\u2019.,-]/gu, '')
695
+ .replace(/^[^\p{L}\p{N}]*/gu, '')
696
+ .replace(/[^\p{L}\p{N}]+$/gu, '')
697
+ : name;
698
+ }
699
+
700
+ get uuid() {
701
+ return this.#uuid;
702
+ }
703
+
704
+ #mergeDeviceData(deviceDataUpdates = {}) {
705
+ let merged = { ...deviceDataUpdates };
706
+
707
+ // Updated data may only contain selected fields, so we'll handle that here by taking our internally stored data
708
+ // and merge with the updates to ensure we have a complete data object
709
+ Object.entries(this.deviceData).forEach(([key, value]) => {
710
+ if (typeof merged[key] === 'undefined') {
711
+ // Updated data doesn't have this key, so add it to our internally stored data
712
+ merged[key] = value;
713
+ }
714
+ });
715
+
716
+ // Check updated device data with our internally stored data. Flag if changes between the two
717
+ let changed = false;
718
+ Object.keys(merged).forEach((key) => {
719
+ if (JSON.stringify(merged[key]) !== JSON.stringify(this.deviceData[key])) {
720
+ changed = true;
721
+ }
722
+ });
723
+
724
+ return { merged, changed };
725
+ }
726
+
727
+ #updateAccessoryInformation(deviceData) {
728
+ // Always update accessory information if we have changed data
729
+ if (this.accessory === undefined) {
730
+ return;
731
+ }
732
+
733
+ let informationService = this.accessory.getService(this.hap.Service.AccessoryInformation);
734
+ if (informationService === undefined) {
735
+ this?.log?.error?.('AccessoryInformation service not found on accessory for "%s"', this.deviceData.description);
736
+ return;
737
+ }
738
+
739
+ // Update details associated with the accessory
740
+ // ie: Name, Manufacturer, Model, Serial # and firmware version
741
+ if (typeof deviceData?.description === 'string' && deviceData.description !== this.deviceData.description) {
742
+ // Update devices description on the HomeKit accessory
743
+ informationService.updateCharacteristic(this.hap.Characteristic.Name, deviceData.description);
744
+ }
745
+
746
+ if (
747
+ typeof deviceData?.manufacturer === 'string' &&
748
+ deviceData.manufacturer !== '' &&
749
+ deviceData.manufacturer !== this.deviceData.manufacturer
750
+ ) {
751
+ // Update manufacturer number on the HomeKit accessory
752
+ informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, deviceData.manufacturer);
753
+ }
754
+
755
+ if (typeof deviceData?.model === 'string' && deviceData.model !== '' && deviceData.model !== this.deviceData.model) {
756
+ // Update model on the HomeKit accessory
757
+ informationService.updateCharacteristic(this.hap.Characteristic.Model, deviceData.model);
758
+ }
759
+
760
+ if (
761
+ typeof deviceData?.softwareVersion === 'string' &&
762
+ deviceData.softwareVersion !== '' &&
763
+ deviceData.softwareVersion !== this.deviceData.softwareVersion
764
+ ) {
765
+ // Update software version on the HomeKit accessory
766
+ informationService.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceData.softwareVersion);
767
+ }
768
+
769
+ // Check for devices serial number changing. Really shouldn't occur, but handle case anyway
770
+ if (
771
+ typeof deviceData?.serialNumber === 'string' &&
772
+ deviceData.serialNumber !== '' &&
773
+ deviceData.serialNumber.toUpperCase() !== this.deviceData.serialNumber?.toUpperCase()
774
+ ) {
775
+ this?.log?.warn?.('Serial number on "%s" has changed', deviceData.description);
776
+ this?.log?.warn?.('This may cause the device to become unresponsive in HomeKit');
777
+
778
+ // Update serial number on the HomeKit accessory
779
+ informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, deviceData.serialNumber);
780
+ }
781
+
782
+ if (typeof deviceData?.online === 'boolean' && deviceData.online !== this.deviceData.online) {
783
+ // Output device online/offline status
784
+ if (deviceData.online === false) {
785
+ this?.log?.warn?.('Device "%s" is offline', deviceData.description);
786
+ }
787
+
788
+ if (deviceData.online === true) {
789
+ this?.log?.success?.('Device "%s" is online', deviceData.description);
790
+ }
791
+ }
792
+ }
793
+ }