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.
- package/CHANGELOG.md +31 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +523 -281
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +69 -87
- package/dist/consts.js +160 -0
- package/dist/devices.js +40 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +182 -149
- package/dist/plugins/camera.js +1164 -933
- 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 +240 -71
- package/dist/plugins/tempsensor.js +159 -35
- package/dist/plugins/thermostat.js +891 -455
- 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 +490 -248
- package/dist/system.js +1741 -2868
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +358 -229
- 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';
|
|
@@ -45,106 +46,116 @@ import crypto from 'crypto';
|
|
|
45
46
|
import EventEmitter from 'node:events';
|
|
46
47
|
|
|
47
48
|
// Define constants
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
62
|
-
static
|
|
63
|
-
static
|
|
64
|
-
static
|
|
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
|
|
77
|
+
static EVEHOME = undefined; // HomeKit History object
|
|
70
78
|
static TYPE = 'base'; // String naming type of device
|
|
71
|
-
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();
|
|
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
|
-
|
|
94
|
+
backend = undefined; // Backend library type
|
|
79
95
|
|
|
80
96
|
// Internal data only for this class
|
|
81
|
-
#
|
|
82
|
-
#
|
|
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
|
|
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.
|
|
105
|
+
if (Object.values(LOG_LEVELS).every((fn) => typeof log?.[fn] === 'function')) {
|
|
88
106
|
this.log = log;
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
//
|
|
92
|
-
if (
|
|
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',
|
|
113
|
+
this.backend = HomeKitDevice.HOMEBRIDGE;
|
|
114
|
+
this.postSetupDetail('Homebridge backend', LOG_LEVELS.DEBUG);
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
if (typeof api?.
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
135
|
+
this.accessory = accessory.find((accessory) => this?.uuid !== undefined && accessory?.UUID === this.#uuid);
|
|
117
136
|
}
|
|
118
|
-
if (Array.isArray(accessory) === false && accessory?.UUID === this
|
|
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(
|
|
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
|
|
145
|
-
|
|
146
|
-
typeof this.hap.Categories[
|
|
147
|
-
typeof
|
|
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
|
-
(
|
|
161
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
191
|
+
if (this.accessory === undefined && this.backend === HomeKitDevice.HAP_NODEJS) {
|
|
176
192
|
// Create HAP-NodeJS libray accessory
|
|
177
|
-
this.accessory = new this.hap.Accessory(
|
|
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 =
|
|
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?.
|
|
196
|
-
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, {});
|
|
197
218
|
}
|
|
198
219
|
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
+
if (this.historyService?.EveHome !== undefined) {
|
|
226
|
+
this.postSetupDetail('EveHome support as "%s"', this.historyService.EveHome.evetype);
|
|
227
|
+
}
|
|
210
228
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
//
|
|
226
|
-
this.
|
|
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
|
|
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 = []; //
|
|
258
|
+
this.#postSetupDetails = []; // Don't need these anymore
|
|
240
259
|
return this.accessory; // Return our HomeKit accessory
|
|
241
260
|
}
|
|
242
261
|
|
|
243
|
-
remove() {
|
|
244
|
-
|
|
262
|
+
async remove() {
|
|
263
|
+
// Trigger registered handlers (onRemove + listeners)
|
|
264
|
+
await this.message(HomeKitDevice.REMOVE);
|
|
265
|
+
}
|
|
245
266
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
267
|
+
async update(deviceData, ...args) {
|
|
268
|
+
if (typeof deviceData !== 'object') {
|
|
269
|
+
return;
|
|
249
270
|
}
|
|
250
271
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
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;
|
|
262
285
|
}
|
|
263
286
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
this.
|
|
271
|
-
|
|
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
|
-
|
|
279
|
-
//
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
if (typeof
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
337
|
+
// Handle message delivery
|
|
338
|
+
return await this.#deviceRegistry.get(uuid)?.message?.(type, message, ...args);
|
|
339
|
+
}
|
|
329
340
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
this
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
informationService.updateCharacteristic(this.hap.Characteristic.SerialNumber, deviceData.serialNumber);
|
|
384
|
+
current = Object.getPrototypeOf(current);
|
|
350
385
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (typeof
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
459
|
+
if (this.accessory !== undefined && this.#platform === undefined) {
|
|
460
|
+
this.accessory.unpublish();
|
|
461
|
+
}
|
|
385
462
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
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;
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
507
|
-
|
|
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
|
|
665
|
+
let hap;
|
|
522
666
|
let uuid = crypto.randomUUID();
|
|
523
667
|
|
|
524
|
-
//
|
|
525
|
-
if (
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
+
}
|