homebridge-nest-accfactory 0.3.1 → 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.
- package/CHANGELOG.md +27 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +495 -262
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +67 -85
- package/dist/consts.js +160 -0
- package/dist/devices.js +35 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +157 -124
- package/dist/plugins/camera.js +1153 -924
- package/dist/plugins/doorbell.js +26 -32
- package/dist/plugins/floodlight.js +11 -24
- package/dist/plugins/heatlink.js +411 -5
- package/dist/plugins/lock.js +309 -0
- package/dist/plugins/protect.js +239 -70
- package/dist/plugins/tempsensor.js +158 -34
- package/dist/plugins/thermostat.js +890 -454
- package/dist/plugins/weather.js +128 -33
- package/dist/protobuf/nest/services/apigateway.proto +1 -1
- package/dist/protobuf/nestlabs/gateway/v2.proto +1 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/rtpmuxer.js +186 -0
- package/dist/streamer.js +486 -244
- package/dist/system.js +1739 -2852
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +354 -225
- package/package.json +19 -16
package/dist/HomeKitDevice.js
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
// HomeKitDevice class
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Base class for all HomeKit accessories using Homebridge or HAP-NodeJS.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Provides internal device tracking, metadata validation, lifecycle management,
|
|
6
|
+
// centralized message dispatch, and optional EveHome-compatible history logging.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
+
// The `deviceData` object must include:
|
|
9
|
+
// serialNumber, softwareVersion, description, manufacturer, model
|
|
8
10
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// description
|
|
12
|
-
// manufacturer
|
|
13
|
-
// model
|
|
11
|
+
// For enabling EveHome history support, include in the `deviceData`:
|
|
12
|
+
// eveHistory
|
|
14
13
|
//
|
|
15
|
-
// HAP-NodeJS
|
|
14
|
+
// For HAP-NodeJS standalone mode, also required:
|
|
15
|
+
// hkUsername, hkPairingCode
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
28
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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';
|
|
@@ -54,12 +55,18 @@ const LOG_LEVELS = {
|
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
// Define our HomeKit device class
|
|
57
|
-
export default class HomeKitDevice {
|
|
58
|
-
|
|
59
|
-
static
|
|
60
|
-
static
|
|
61
|
-
static
|
|
62
|
-
|
|
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
|
|
63
70
|
static HK_PIN_3_2_3 = /^\d{3}-\d{2}-\d{3}$/;
|
|
64
71
|
static HK_PIN_4_4 = /^\d{4}-\d{4}$/;
|
|
65
72
|
static MAC_ADDR = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
|
|
@@ -67,85 +74,88 @@ export default class HomeKitDevice {
|
|
|
67
74
|
// Override this in the class which extends
|
|
68
75
|
static PLUGIN_NAME = undefined; // Homebridge plugin name
|
|
69
76
|
static PLATFORM_NAME = undefined; // Homebridge platform name
|
|
70
|
-
static
|
|
77
|
+
static EVEHOME = undefined; // HomeKit History object
|
|
71
78
|
static TYPE = 'base'; // String naming type of device
|
|
72
|
-
static VERSION = '2025.
|
|
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();
|
|
73
88
|
|
|
74
89
|
deviceData = {}; // The devices data we store
|
|
75
90
|
historyService = undefined; // HomeKit history service
|
|
76
91
|
accessory = undefined; // HomeKit accessory service for this device
|
|
77
92
|
hap = undefined; // HomeKit Accessory Protocol (HAP) API stub
|
|
78
93
|
log = undefined; // Logging function object
|
|
79
|
-
|
|
94
|
+
backend = undefined; // Backend library type
|
|
80
95
|
|
|
81
96
|
// Internal data only for this class
|
|
82
|
-
#
|
|
83
|
-
#
|
|
97
|
+
#uuid = undefined; // UUID for this instance
|
|
98
|
+
#platform = undefined; // Homebridge platform API
|
|
84
99
|
#postSetupDetails = []; // Use for extra output details once a device has been setup
|
|
85
100
|
|
|
86
|
-
constructor(accessory = undefined, api = undefined, log = undefined,
|
|
101
|
+
constructor(accessory = undefined, api = undefined, log = undefined, deviceData = {}) {
|
|
102
|
+
super(); // Setup event emitter for our class ONLY
|
|
103
|
+
|
|
87
104
|
// Validate the passed in logging object. We are expecting certain functions to be present
|
|
88
105
|
if (Object.values(LOG_LEVELS).every((fn) => typeof log?.[fn] === 'function')) {
|
|
89
106
|
this.log = log;
|
|
90
107
|
}
|
|
91
108
|
|
|
92
|
-
//
|
|
93
|
-
if (
|
|
94
|
-
// 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') {
|
|
95
111
|
this.hap = api.hap;
|
|
96
112
|
this.#platform = api;
|
|
97
|
-
|
|
113
|
+
this.backend = HomeKitDevice.HOMEBRIDGE;
|
|
98
114
|
this.postSetupDetail('Homebridge backend', LOG_LEVELS.DEBUG);
|
|
99
115
|
}
|
|
100
116
|
|
|
101
|
-
if (typeof api?.
|
|
102
|
-
// 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') {
|
|
103
118
|
this.hap = api;
|
|
104
|
-
|
|
119
|
+
this.backend = HomeKitDevice.HAP_NODEJS;
|
|
105
120
|
this.postSetupDetail('HAP-NodeJS library', LOG_LEVELS.DEBUG);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
// Generate UUID for this device instance
|
|
109
124
|
// Will either be a random generated one or HAP generated one
|
|
110
125
|
// HAP is based upon defined plugin name and devices serial number
|
|
111
|
-
this
|
|
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);
|
|
112
130
|
|
|
113
131
|
// See if we were passed in an existing accessory object or array of accessory objects
|
|
114
132
|
// Mainly used to restore a Homebridge cached accessory
|
|
115
|
-
if (typeof accessory === 'object' && this
|
|
133
|
+
if (typeof accessory === 'object' && this.backend === HomeKitDevice.HOMEBRIDGE) {
|
|
116
134
|
if (Array.isArray(accessory) === true) {
|
|
117
|
-
this.accessory = accessory.find((accessory) => this?.uuid !== undefined && accessory?.UUID === this
|
|
135
|
+
this.accessory = accessory.find((accessory) => this?.uuid !== undefined && accessory?.UUID === this.#uuid);
|
|
118
136
|
}
|
|
119
|
-
if (Array.isArray(accessory) === false && accessory?.UUID === this
|
|
137
|
+
if (Array.isArray(accessory) === false && accessory?.UUID === this.#uuid) {
|
|
120
138
|
this.accessory = accessory;
|
|
121
139
|
}
|
|
122
140
|
}
|
|
123
141
|
|
|
124
|
-
// Validate if eventEmitter object passed to us is an instance of EventEmitter
|
|
125
|
-
// If valid, setup an event listener for messages to this device using our generated uuid
|
|
126
|
-
if (eventEmitter instanceof EventEmitter === true) {
|
|
127
|
-
this.#eventEmitter = eventEmitter;
|
|
128
|
-
this.#eventEmitter.addListener(this.uuid, this.#message.bind(this));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
142
|
// Make a clone of current data and store in this object
|
|
132
143
|
// Important that we done have a 'linked' copy of the object data
|
|
133
|
-
// eslint-disable-next-line no-undef
|
|
134
144
|
this.deviceData = structuredClone(deviceData);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
// Class functions
|
|
138
|
-
async add(
|
|
148
|
+
async add(hapAccessoryName, hapCategory, enableHistory = false) {
|
|
139
149
|
if (
|
|
140
150
|
this.hap === undefined ||
|
|
141
151
|
typeof HomeKitDevice.PLUGIN_NAME !== 'string' ||
|
|
142
152
|
HomeKitDevice.PLUGIN_NAME === '' ||
|
|
143
153
|
typeof HomeKitDevice.PLATFORM_NAME !== 'string' ||
|
|
144
154
|
HomeKitDevice.PLATFORM_NAME === '' ||
|
|
145
|
-
typeof
|
|
146
|
-
|
|
147
|
-
typeof this.hap.Categories[
|
|
148
|
-
typeof
|
|
155
|
+
typeof hapAccessoryName !== 'string' ||
|
|
156
|
+
hapAccessoryName === '' ||
|
|
157
|
+
typeof this.hap.Categories[hapCategory] === 'undefined' ||
|
|
158
|
+
typeof enableHistory !== 'boolean' ||
|
|
149
159
|
typeof this.deviceData !== 'object' ||
|
|
150
160
|
typeof this.deviceData?.serialNumber !== 'string' ||
|
|
151
161
|
this.deviceData.serialNumber === '' ||
|
|
@@ -167,23 +177,33 @@ export default class HomeKitDevice {
|
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
// If we do not have an existing accessory object, create a new one
|
|
170
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
this.accessory === undefined &&
|
|
182
|
+
typeof this.#platform?.platformAccessory === 'function' &&
|
|
183
|
+
typeof this.#platform?.registerPlatformAccessories === 'function' &&
|
|
184
|
+
this.backend === HomeKitDevice.HOMEBRIDGE
|
|
185
|
+
) {
|
|
171
186
|
// Create Homebridge platform accessory
|
|
172
|
-
this.accessory = new this.#platform.platformAccessory(this.deviceData.description, this
|
|
187
|
+
this.accessory = new this.#platform.platformAccessory(this.deviceData.description, this.#uuid);
|
|
173
188
|
this.#platform.registerPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
|
|
174
189
|
}
|
|
175
190
|
|
|
176
|
-
if (this.accessory === undefined && this
|
|
191
|
+
if (this.accessory === undefined && this.backend === HomeKitDevice.HAP_NODEJS) {
|
|
177
192
|
// Create HAP-NodeJS libray accessory
|
|
178
|
-
this.accessory = new this.hap.Accessory(
|
|
193
|
+
this.accessory = new this.hap.Accessory(hapAccessoryName, this.#uuid);
|
|
179
194
|
|
|
180
195
|
this.accessory.username = this.deviceData.hkUsername;
|
|
181
196
|
this.accessory.pincode = this.deviceData.hkPairingCode;
|
|
182
|
-
this.accessory.category =
|
|
197
|
+
this.accessory.category = hapCategory;
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
// Setup accessory information
|
|
186
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
|
+
|
|
187
207
|
if (informationService !== undefined) {
|
|
188
208
|
informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, this.deviceData.manufacturer);
|
|
189
209
|
informationService.updateCharacteristic(this.hap.Characteristic.Model, this.deviceData.model);
|
|
@@ -193,251 +213,359 @@ export default class HomeKitDevice {
|
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
// Setup our history service if module has been defined and requested to be active for this device
|
|
196
|
-
if (typeof HomeKitDevice?.
|
|
197
|
-
this.historyService = new HomeKitDevice.
|
|
216
|
+
if (typeof HomeKitDevice?.EVEHOME === 'function' && this.historyService === undefined && enableHistory === true) {
|
|
217
|
+
this.historyService = new HomeKitDevice.EVEHOME(this.accessory, this.hap, this.log, {});
|
|
198
218
|
}
|
|
199
219
|
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber, LOG_LEVELS.DEBUG);
|
|
220
|
+
this.postSetupDetail('Serial number "%s"', this.deviceData.serialNumber, LOG_LEVELS.DEBUG);
|
|
203
221
|
|
|
204
|
-
|
|
222
|
+
// Trigger registered handlers (onAdd + listeners)
|
|
223
|
+
await this.message(HomeKitDevice.ADD);
|
|
205
224
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
225
|
+
if (this.historyService?.EveHome !== undefined) {
|
|
226
|
+
this.postSetupDetail('EveHome support as "%s"', this.historyService.EveHome.evetype);
|
|
227
|
+
}
|
|
209
228
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
} catch (error) {
|
|
225
|
-
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 : []));
|
|
226
241
|
}
|
|
227
|
-
}
|
|
242
|
+
});
|
|
228
243
|
|
|
229
|
-
//
|
|
230
|
-
this.
|
|
244
|
+
// Trigger registered handlers (onUpdate + listeners) for initial device data updates
|
|
245
|
+
await this.message(HomeKitDevice.UPDATE, this.deviceData, { force: true });
|
|
231
246
|
|
|
232
247
|
// If using HAP-NodeJS library, publish accessory on local network
|
|
233
|
-
if (this
|
|
248
|
+
if (this.accessory !== undefined && this.backend === HomeKitDevice.HAP_NODEJS) {
|
|
234
249
|
this.accessory.publish({
|
|
235
250
|
username: this.accessory.username,
|
|
236
251
|
pincode: this.accessory.pincode,
|
|
237
252
|
category: this.accessory.category,
|
|
238
253
|
});
|
|
239
254
|
|
|
240
|
-
this?.log?.info(' += Advertising as "%s"', this.accessory.displayName);
|
|
241
|
-
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);
|
|
242
257
|
}
|
|
243
258
|
this.#postSetupDetails = []; // Don't need these anymore
|
|
244
259
|
return this.accessory; // Return our HomeKit accessory
|
|
245
260
|
}
|
|
246
261
|
|
|
247
|
-
remove() {
|
|
248
|
-
|
|
262
|
+
async remove() {
|
|
263
|
+
// Trigger registered handlers (onRemove + listeners)
|
|
264
|
+
await this.message(HomeKitDevice.REMOVE);
|
|
265
|
+
}
|
|
249
266
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
async update(deviceData, ...args) {
|
|
268
|
+
if (typeof deviceData !== 'object') {
|
|
269
|
+
return;
|
|
253
270
|
}
|
|
254
271
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} catch (error) {
|
|
259
|
-
this?.log?.error('removeDevice call for device "%s" failed. Error was', this.deviceData.description, error);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
272
|
+
// Trigger registered handlers (onUpdate + listeners)
|
|
273
|
+
await this.message(HomeKitDevice.UPDATE, deviceData, ...args);
|
|
274
|
+
}
|
|
262
275
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
this
|
|
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;
|
|
266
285
|
}
|
|
267
286
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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;
|
|
271
294
|
}
|
|
272
295
|
|
|
273
|
-
|
|
274
|
-
this.
|
|
275
|
-
|
|
276
|
-
this.hap = undefined;
|
|
277
|
-
this.log = undefined;
|
|
278
|
-
this.uuid = undefined;
|
|
279
|
-
this.#platform = undefined;
|
|
280
|
-
this.#eventEmitter = undefined;
|
|
296
|
+
// Trigger registered handlers (onSet + listeners)
|
|
297
|
+
await this.message(HomeKitDevice.SET, values, ...args);
|
|
298
|
+
}
|
|
281
299
|
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
300
|
+
async get(values, ...args) {
|
|
301
|
+
// Trigger registered handlers (onGet + listeners)
|
|
302
|
+
return await this.message(HomeKitDevice.GET, values, ...args);
|
|
285
303
|
}
|
|
286
304
|
|
|
287
|
-
|
|
288
|
-
if (typeof
|
|
305
|
+
static async message(uuid, type, message = undefined, ...args) {
|
|
306
|
+
if (typeof uuid !== 'string' || uuid === '' || typeof type !== 'string' || type === '') {
|
|
289
307
|
return;
|
|
290
308
|
}
|
|
291
309
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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] = [];
|
|
298
316
|
}
|
|
299
|
-
});
|
|
300
317
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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;
|
|
306
326
|
}
|
|
307
|
-
});
|
|
308
327
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (informationService !== undefined) {
|
|
313
|
-
// Update details associated with the accessory
|
|
314
|
-
// ie: Name, Manufacturer, Model, Serial # and firmware version
|
|
315
|
-
if (typeof deviceData?.description === 'string' && deviceData.description !== this.deviceData.description) {
|
|
316
|
-
// Update serial number on the HomeKit accessory
|
|
317
|
-
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 });
|
|
318
331
|
}
|
|
332
|
+
}
|
|
319
333
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
deviceData.manufacturer !== '' &&
|
|
323
|
-
deviceData.manufacturer !== this.deviceData.manufacturer
|
|
324
|
-
) {
|
|
325
|
-
// Update manufacturer number on the HomeKit accessory
|
|
326
|
-
informationService.updateCharacteristic(this.hap.Characteristic.Manufacturer, deviceData.manufacturer);
|
|
327
|
-
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
328
336
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
337
|
+
// Handle message delivery
|
|
338
|
+
return await this.#deviceRegistry.get(uuid)?.message?.(type, message, ...args);
|
|
339
|
+
}
|
|
333
340
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
) {
|
|
339
|
-
// Update software version on the HomeKit accessory
|
|
340
|
-
informationService.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, deviceData.softwareVersion);
|
|
341
|
-
}
|
|
341
|
+
async message(type, message, ...args) {
|
|
342
|
+
if (typeof type !== 'string' || type === '') {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
342
345
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
this
|
|
350
|
-
|
|
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
|
+
}
|
|
351
383
|
|
|
352
|
-
|
|
353
|
-
informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, deviceData.serialNumber);
|
|
384
|
+
current = Object.getPrototypeOf(current);
|
|
354
385
|
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (typeof
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
399
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
420
|
}
|
|
366
421
|
}
|
|
367
422
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
}
|
|
373
445
|
}
|
|
374
446
|
}
|
|
375
447
|
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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];
|
|
381
454
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
455
|
+
if (this.accessory !== undefined && typeof this.#platform?.unregisterPlatformAccessories === 'function') {
|
|
456
|
+
this.#platform.unregisterPlatformAccessories(HomeKitDevice.PLUGIN_NAME, HomeKitDevice.PLATFORM_NAME, [this.accessory]);
|
|
457
|
+
}
|
|
386
458
|
|
|
387
|
-
|
|
388
|
-
|
|
459
|
+
if (this.accessory !== undefined && this.#platform === undefined) {
|
|
460
|
+
this.accessory.unpublish();
|
|
461
|
+
}
|
|
389
462
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.
|
|
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;
|
|
394
470
|
}
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async get(values) {
|
|
399
|
-
if (typeof values !== 'object' || this.#eventEmitter === undefined) {
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Send event with data to get
|
|
404
|
-
// Once get has completed, we'll get an event back with the requested data
|
|
405
|
-
this.#eventEmitter.emit(HomeKitDevice.GET, this.uuid, values);
|
|
406
471
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
this.update(message, false);
|
|
417
|
-
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
|
+
}
|
|
418
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
|
+
}
|
|
419
492
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this.remove();
|
|
423
|
-
break;
|
|
493
|
+
// Finally, update our internally stored data with the new data
|
|
494
|
+
this.deviceData = structuredClone(merged);
|
|
424
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
|
+
}
|
|
425
511
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
}
|
|
433
526
|
}
|
|
434
527
|
}
|
|
435
|
-
|
|
528
|
+
|
|
529
|
+
if (skipHistory === false) {
|
|
530
|
+
this.historyService.addHistory(target, entry, isNaN(options?.timegap) === false ? options.timegap : undefined);
|
|
531
|
+
}
|
|
436
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);
|
|
437
559
|
}
|
|
560
|
+
|
|
561
|
+
if (typeof result.call === 'object' && typeof result.handler === 'object') {
|
|
562
|
+
return Object.assign({}, result.call, result.handler);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return result.call !== undefined ? result.call : result.handler;
|
|
438
566
|
}
|
|
439
567
|
|
|
440
|
-
addHKService(hkServiceType, name = '', subType = undefined) {
|
|
568
|
+
addHKService(hkServiceType, name = '', subType = undefined, eveOptions = undefined) {
|
|
441
569
|
let service = undefined;
|
|
442
570
|
|
|
443
571
|
if (
|
|
@@ -455,12 +583,22 @@ export default class HomeKitDevice {
|
|
|
455
583
|
if (service === undefined) {
|
|
456
584
|
service = this.accessory.addService(hkServiceType, name, subType);
|
|
457
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
|
+
}
|
|
458
596
|
}
|
|
459
597
|
|
|
460
598
|
return service;
|
|
461
599
|
}
|
|
462
600
|
|
|
463
|
-
addHKCharacteristic(hkService, hkCharacteristicType, { props, onSet, onGet } = {}) {
|
|
601
|
+
addHKCharacteristic(hkService, hkCharacteristicType, { props, onSet, onGet, initialValue } = {}) {
|
|
464
602
|
let characteristic = undefined;
|
|
465
603
|
|
|
466
604
|
if (
|
|
@@ -472,9 +610,8 @@ export default class HomeKitDevice {
|
|
|
472
610
|
) {
|
|
473
611
|
if (hkService.testCharacteristic(hkCharacteristicType) === false) {
|
|
474
612
|
if (
|
|
475
|
-
Array.isArray(hkService?.optionalCharacteristics) &&
|
|
476
|
-
hkService.optionalCharacteristics.includes(hkCharacteristicType)
|
|
477
|
-
typeof hkService?.addOptionalCharacteristic === 'function'
|
|
613
|
+
Array.isArray(hkService?.optionalCharacteristics) === true &&
|
|
614
|
+
hkService.optionalCharacteristics.includes(hkCharacteristicType) === true
|
|
478
615
|
) {
|
|
479
616
|
hkService.addOptionalCharacteristic(hkCharacteristicType);
|
|
480
617
|
} else {
|
|
@@ -494,7 +631,13 @@ export default class HomeKitDevice {
|
|
|
494
631
|
if (typeof props === 'object' && typeof characteristic.setProps === 'function') {
|
|
495
632
|
characteristic.setProps(props);
|
|
496
633
|
}
|
|
634
|
+
|
|
635
|
+
// Set initial value if provided
|
|
636
|
+
if (typeof initialValue !== 'undefined' && typeof hkService?.updateCharacteristic === 'function') {
|
|
637
|
+
hkService.updateCharacteristic(hkCharacteristicType, initialValue);
|
|
638
|
+
}
|
|
497
639
|
}
|
|
640
|
+
|
|
498
641
|
return characteristic;
|
|
499
642
|
}
|
|
500
643
|
|
|
@@ -519,17 +662,13 @@ export default class HomeKitDevice {
|
|
|
519
662
|
}
|
|
520
663
|
|
|
521
664
|
static generateUUID(PLUGIN_NAME, api, serialNumber) {
|
|
522
|
-
let hap
|
|
665
|
+
let hap;
|
|
523
666
|
let uuid = crypto.randomUUID();
|
|
524
667
|
|
|
525
|
-
//
|
|
526
|
-
if (
|
|
527
|
-
// 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') {
|
|
528
670
|
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
|
|
671
|
+
} else if (typeof api?.HAPLibraryVersion === 'function' && typeof api?.version === 'undefined' && typeof api?.hap === 'undefined') {
|
|
533
672
|
hap = api;
|
|
534
673
|
}
|
|
535
674
|
|
|
@@ -546,15 +685,109 @@ export default class HomeKitDevice {
|
|
|
546
685
|
return uuid;
|
|
547
686
|
}
|
|
548
687
|
|
|
549
|
-
static
|
|
688
|
+
static makeValidHKName(name) {
|
|
550
689
|
// Strip invalid characters to meet HomeKit naming requirements
|
|
551
690
|
// Ensure only letters or numbers are at the beginning AND/OR end of string
|
|
552
691
|
// Matches against uni-code characters
|
|
553
692
|
return typeof name === 'string'
|
|
554
693
|
? name
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
694
|
+
.replace(/[^\p{L}\p{N}\p{Z}\u2019.,-]/gu, '')
|
|
695
|
+
.replace(/^[^\p{L}\p{N}]*/gu, '')
|
|
696
|
+
.replace(/[^\p{L}\p{N}]+$/gu, '')
|
|
558
697
|
: name;
|
|
559
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
|
+
}
|
|
560
793
|
}
|