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