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/utils.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// General helper functions
|
|
2
|
+
// Part of homebridge-nest-accfactory
|
|
3
|
+
//
|
|
4
|
+
// Code version 2025.08.02
|
|
5
|
+
// Mark Hulskamp
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Define nodejs module requirements
|
|
9
|
+
import { Buffer } from 'node:buffer';
|
|
10
|
+
import { setTimeout } from 'node:timers';
|
|
11
|
+
|
|
12
|
+
// Define our modules
|
|
13
|
+
import HomeKitDevice from './HomeKitDevice.js';
|
|
14
|
+
|
|
15
|
+
// Define external library requirements
|
|
16
|
+
import { Agent } from 'undici';
|
|
17
|
+
|
|
18
|
+
// Define constants
|
|
19
|
+
const defaultFetchAgent = new Agent(); // shared across all requests
|
|
20
|
+
|
|
21
|
+
function adjustTemperature(temperature, currentTemperatureUnit, targetTemperatureUnit, round) {
|
|
22
|
+
currentTemperatureUnit = currentTemperatureUnit?.toUpperCase?.();
|
|
23
|
+
targetTemperatureUnit = targetTemperatureUnit?.toUpperCase?.();
|
|
24
|
+
|
|
25
|
+
if (currentTemperatureUnit === 'F' && targetTemperatureUnit === 'C') {
|
|
26
|
+
temperature = ((temperature - 32) * 5) / 9;
|
|
27
|
+
if (round === true) {
|
|
28
|
+
temperature = Math.round(temperature * 2) / 2; // round to nearest 0.5°C
|
|
29
|
+
}
|
|
30
|
+
} else if (currentTemperatureUnit === 'C' && targetTemperatureUnit === 'F') {
|
|
31
|
+
temperature = (temperature * 9) / 5 + 32;
|
|
32
|
+
if (round === true) {
|
|
33
|
+
temperature = Math.round(temperature); // round to nearest 1°F
|
|
34
|
+
}
|
|
35
|
+
} else if (round === true) {
|
|
36
|
+
// No conversion, just rounding
|
|
37
|
+
temperature = targetTemperatureUnit === 'C' ? Math.round(temperature * 2) / 2 : Math.round(temperature);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return temperature;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function crc24(valueToHash) {
|
|
44
|
+
const crc24HashTable = [
|
|
45
|
+
0x000000, 0x864cfb, 0x8ad50d, 0x0c99f6, 0x93e6e1, 0x15aa1a, 0x1933ec, 0x9f7f17, 0xa18139, 0x27cdc2, 0x2b5434, 0xad18cf, 0x3267d8,
|
|
46
|
+
0xb42b23, 0xb8b2d5, 0x3efe2e, 0xc54e89, 0x430272, 0x4f9b84, 0xc9d77f, 0x56a868, 0xd0e493, 0xdc7d65, 0x5a319e, 0x64cfb0, 0xe2834b,
|
|
47
|
+
0xee1abd, 0x685646, 0xf72951, 0x7165aa, 0x7dfc5c, 0xfbb0a7, 0x0cd1e9, 0x8a9d12, 0x8604e4, 0x00481f, 0x9f3708, 0x197bf3, 0x15e205,
|
|
48
|
+
0x93aefe, 0xad50d0, 0x2b1c2b, 0x2785dd, 0xa1c926, 0x3eb631, 0xb8faca, 0xb4633c, 0x322fc7, 0xc99f60, 0x4fd39b, 0x434a6d, 0xc50696,
|
|
49
|
+
0x5a7981, 0xdc357a, 0xd0ac8c, 0x56e077, 0x681e59, 0xee52a2, 0xe2cb54, 0x6487af, 0xfbf8b8, 0x7db443, 0x712db5, 0xf7614e, 0x19a3d2,
|
|
50
|
+
0x9fef29, 0x9376df, 0x153a24, 0x8a4533, 0x0c09c8, 0x00903e, 0x86dcc5, 0xb822eb, 0x3e6e10, 0x32f7e6, 0xb4bb1d, 0x2bc40a, 0xad88f1,
|
|
51
|
+
0xa11107, 0x275dfc, 0xdced5b, 0x5aa1a0, 0x563856, 0xd074ad, 0x4f0bba, 0xc94741, 0xc5deb7, 0x43924c, 0x7d6c62, 0xfb2099, 0xf7b96f,
|
|
52
|
+
0x71f594, 0xee8a83, 0x68c678, 0x645f8e, 0xe21375, 0x15723b, 0x933ec0, 0x9fa736, 0x19ebcd, 0x8694da, 0x00d821, 0x0c41d7, 0x8a0d2c,
|
|
53
|
+
0xb4f302, 0x32bff9, 0x3e260f, 0xb86af4, 0x2715e3, 0xa15918, 0xadc0ee, 0x2b8c15, 0xd03cb2, 0x567049, 0x5ae9bf, 0xdca544, 0x43da53,
|
|
54
|
+
0xc596a8, 0xc90f5e, 0x4f43a5, 0x71bd8b, 0xf7f170, 0xfb6886, 0x7d247d, 0xe25b6a, 0x641791, 0x688e67, 0xeec29c, 0x3347a4, 0xb50b5f,
|
|
55
|
+
0xb992a9, 0x3fde52, 0xa0a145, 0x26edbe, 0x2a7448, 0xac38b3, 0x92c69d, 0x148a66, 0x181390, 0x9e5f6b, 0x01207c, 0x876c87, 0x8bf571,
|
|
56
|
+
0x0db98a, 0xf6092d, 0x7045d6, 0x7cdc20, 0xfa90db, 0x65efcc, 0xe3a337, 0xef3ac1, 0x69763a, 0x578814, 0xd1c4ef, 0xdd5d19, 0x5b11e2,
|
|
57
|
+
0xc46ef5, 0x42220e, 0x4ebbf8, 0xc8f703, 0x3f964d, 0xb9dab6, 0xb54340, 0x330fbb, 0xac70ac, 0x2a3c57, 0x26a5a1, 0xa0e95a, 0x9e1774,
|
|
58
|
+
0x185b8f, 0x14c279, 0x928e82, 0x0df195, 0x8bbd6e, 0x872498, 0x016863, 0xfad8c4, 0x7c943f, 0x700dc9, 0xf64132, 0x693e25, 0xef72de,
|
|
59
|
+
0xe3eb28, 0x65a7d3, 0x5b59fd, 0xdd1506, 0xd18cf0, 0x57c00b, 0xc8bf1c, 0x4ef3e7, 0x426a11, 0xc426ea, 0x2ae476, 0xaca88d, 0xa0317b,
|
|
60
|
+
0x267d80, 0xb90297, 0x3f4e6c, 0x33d79a, 0xb59b61, 0x8b654f, 0x0d29b4, 0x01b042, 0x87fcb9, 0x1883ae, 0x9ecf55, 0x9256a3, 0x141a58,
|
|
61
|
+
0xefaaff, 0x69e604, 0x657ff2, 0xe33309, 0x7c4c1e, 0xfa00e5, 0xf69913, 0x70d5e8, 0x4e2bc6, 0xc8673d, 0xc4fecb, 0x42b230, 0xddcd27,
|
|
62
|
+
0x5b81dc, 0x57182a, 0xd154d1, 0x26359f, 0xa07964, 0xace092, 0x2aac69, 0xb5d37e, 0x339f85, 0x3f0673, 0xb94a88, 0x87b4a6, 0x01f85d,
|
|
63
|
+
0x0d61ab, 0x8b2d50, 0x145247, 0x921ebc, 0x9e874a, 0x18cbb1, 0xe37b16, 0x6537ed, 0x69ae1b, 0xefe2e0, 0x709df7, 0xf6d10c, 0xfa48fa,
|
|
64
|
+
0x7c0401, 0x42fa2f, 0xc4b6d4, 0xc82f22, 0x4e63d9, 0xd11cce, 0x575035, 0x5bc9c3, 0xdd8538,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
let crc = 0xb704ce;
|
|
68
|
+
|
|
69
|
+
let buffer = Buffer.from(valueToHash);
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
72
|
+
let index = ((crc >> 16) ^ buffer[i]) & 0xff;
|
|
73
|
+
crc = (crc24HashTable[index] ^ (crc << 8)) & 0xffffff;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return crc.toString(16).padStart(6, '0'); // ensures 6-digit hex
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function scaleValue(value, sourceMin, sourceMax, targetMin, targetMax) {
|
|
80
|
+
if (sourceMax === sourceMin) {
|
|
81
|
+
return targetMin;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
value = Math.max(sourceMin, Math.min(sourceMax, value));
|
|
85
|
+
|
|
86
|
+
return ((value - sourceMin) * (targetMax - targetMin)) / (sourceMax - sourceMin) + targetMin;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchWrapper(method, url, options, data) {
|
|
90
|
+
if ((method !== 'get' && method !== 'post') || typeof url !== 'string' || url === '' || typeof options !== 'object') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isNaN(options?.timeout) === false && Number(options.timeout) > 0) {
|
|
95
|
+
// eslint-disable-next-line no-undef
|
|
96
|
+
options.signal = AbortSignal.timeout(Number(options.timeout));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isNaN(options.retry) || options.retry < 1) {
|
|
100
|
+
options.retry = 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isNaN(options._retryCount)) {
|
|
104
|
+
options._retryCount = 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
options.method = method;
|
|
108
|
+
|
|
109
|
+
if (method === 'post' && data !== undefined) {
|
|
110
|
+
if (typeof data === 'object' && data !== null && data.constructor === Object) {
|
|
111
|
+
options.body = JSON.stringify(data);
|
|
112
|
+
|
|
113
|
+
// Set Content-Type header only if not already set
|
|
114
|
+
options.headers = options.headers || {};
|
|
115
|
+
if (options.headers['Content-Type'] === undefined) {
|
|
116
|
+
options.headers['Content-Type'] = 'application/json';
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
options.body = data;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// eslint-disable-next-line no-undef
|
|
125
|
+
let response = await fetch(url, {
|
|
126
|
+
...options,
|
|
127
|
+
dispatcher: options?.dispatcher ?? defaultFetchAgent, // Always use a secure default agent unless explicitly overridden
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (response?.ok === false) {
|
|
131
|
+
if (options.retry > 1) {
|
|
132
|
+
options.retry--;
|
|
133
|
+
options._retryCount++;
|
|
134
|
+
|
|
135
|
+
let delay = 500 * Math.pow(2, options._retryCount - 1);
|
|
136
|
+
await new Promise((resolve) => {
|
|
137
|
+
resolve = resolve;
|
|
138
|
+
setTimeout(resolve, delay);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return fetchWrapper(method, url, options, data);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Optionally get response body
|
|
145
|
+
let body;
|
|
146
|
+
try {
|
|
147
|
+
body = await response.text();
|
|
148
|
+
// eslint-disable-next-line no-unused-vars
|
|
149
|
+
} catch (error) {
|
|
150
|
+
body = '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw Object.assign(
|
|
154
|
+
new Error('HTTP ' + response.status + ' on ' + method.toUpperCase() + ' ' + url + ': ' + (response.statusText || 'Unknown error')),
|
|
155
|
+
{ code: response.status, status: response.status, body },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return response;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (
|
|
162
|
+
options.retry > 1 &&
|
|
163
|
+
(error?.cause?.code === 'UND_ERR_HEADERS_TIMEOUT' || error?.name === 'AbortError' || error?.name === 'TypeError')
|
|
164
|
+
) {
|
|
165
|
+
options.retry--;
|
|
166
|
+
options._retryCount++;
|
|
167
|
+
|
|
168
|
+
let delay = 500 * Math.pow(2, options._retryCount - 1);
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new Error(
|
|
173
|
+
method.toUpperCase() +
|
|
174
|
+
' ' +
|
|
175
|
+
url +
|
|
176
|
+
' failed after ' +
|
|
177
|
+
(options._retryCount + 1) +
|
|
178
|
+
' attempt' +
|
|
179
|
+
(options._retryCount + 1 > 1 ? 's' : '') +
|
|
180
|
+
': ' +
|
|
181
|
+
(error?.message || String(error)) +
|
|
182
|
+
(error?.cause?.code ? ' (' + error.cause.code + ')' : ''),
|
|
183
|
+
{ cause: error },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseDurationToSeconds(inputDuration, { defaultValue = null, min = 0, max = Infinity } = {}) {
|
|
189
|
+
let normalisedSeconds = defaultValue;
|
|
190
|
+
|
|
191
|
+
if (inputDuration !== undefined && inputDuration !== null && inputDuration !== '') {
|
|
192
|
+
inputDuration = String(inputDuration).trim().toLowerCase();
|
|
193
|
+
|
|
194
|
+
// Case: plain numeric seconds (e.g. "30")
|
|
195
|
+
if (/^\d+$/.test(inputDuration) === true) {
|
|
196
|
+
normalisedSeconds = Number(inputDuration);
|
|
197
|
+
} else {
|
|
198
|
+
// Process input into normalised units. We'll convert in standard h (hours), m (minutes), s (seconds)
|
|
199
|
+
inputDuration = inputDuration
|
|
200
|
+
.replace(/hrs?|hours?/g, 'h')
|
|
201
|
+
.replace(/mins?|minutes?/g, 'm')
|
|
202
|
+
.replace(/secs?|s\b/g, 's')
|
|
203
|
+
.replace(/ +/g, '');
|
|
204
|
+
|
|
205
|
+
// Match duration format like "1h30m15s"
|
|
206
|
+
let match = inputDuration.match(/^((\d+)h)?((\d+)m)?((\d+)s?)?$/);
|
|
207
|
+
|
|
208
|
+
if (Array.isArray(match) === true) {
|
|
209
|
+
let total = Number(match[2] || 0) * 3600 + Number(match[4] || 0) * 60 + Number(match[6] || 0);
|
|
210
|
+
normalisedSeconds = Math.floor(total / 3600) * 3600 + Math.floor((total % 3600) / 60) * 60 + (total % 60);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (normalisedSeconds === null || isNaN(normalisedSeconds) === true) {
|
|
215
|
+
normalisedSeconds = defaultValue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (isNaN(min) === false && normalisedSeconds < min) {
|
|
219
|
+
normalisedSeconds = min;
|
|
220
|
+
}
|
|
221
|
+
if (isNaN(max) === false && normalisedSeconds > max) {
|
|
222
|
+
normalisedSeconds = max;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return normalisedSeconds;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function processCommonData(deviceUUID, data, config) {
|
|
230
|
+
if (
|
|
231
|
+
typeof deviceUUID !== 'string' ||
|
|
232
|
+
deviceUUID === '' ||
|
|
233
|
+
data === null ||
|
|
234
|
+
typeof data !== 'object' ||
|
|
235
|
+
data?.constructor !== Object ||
|
|
236
|
+
typeof config !== 'object' ||
|
|
237
|
+
config?.constructor !== Object
|
|
238
|
+
) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Process common data for all devices
|
|
242
|
+
|
|
243
|
+
// Process software version strings and return as x.x.x
|
|
244
|
+
// handles things like:
|
|
245
|
+
// 1.0a17 -> 1.0.17
|
|
246
|
+
// 3.6rc8 -> 3.6.8
|
|
247
|
+
// rquartz-user 1 OPENMASTER 507800056 test-keys stable-channel stable-channel -> 507800056
|
|
248
|
+
// nq-user 1.73 OPENMASTER 422270 release-keys stable-channel stable-channel -> 422270
|
|
249
|
+
const process_software_version = (versionString) => {
|
|
250
|
+
let version = '0.0.0';
|
|
251
|
+
if (typeof versionString === 'string') {
|
|
252
|
+
let normalised = versionString.replace(/[-_]/g, '.');
|
|
253
|
+
let tokens = normalised.split(/\s+/);
|
|
254
|
+
let candidate = tokens[3] || normalised;
|
|
255
|
+
let match = candidate.match(/\d+(?:\.\d+)*[a-zA-Z]*\d*/) || normalised.match(/\d+(?:\.\d+)*[a-zA-Z]*\d*/);
|
|
256
|
+
|
|
257
|
+
if (Array.isArray(match) === true) {
|
|
258
|
+
let raw = match[0];
|
|
259
|
+
if (raw.includes('.') === false) {
|
|
260
|
+
return raw; // Return single-number version like "422270" as-is
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let parts = raw.split('.').flatMap((part) => {
|
|
264
|
+
let [, n1, , n2] = part.match(/^(\d+)([a-zA-Z]+)?(\d+)?$/) || [];
|
|
265
|
+
return [n1, n2].filter(Boolean).map(Number);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
while (parts.length < 3) {
|
|
269
|
+
parts.push(0);
|
|
270
|
+
}
|
|
271
|
+
version = parts.slice(0, 3).join('.');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return version;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
let processed = {};
|
|
279
|
+
try {
|
|
280
|
+
// Fix up data we need to
|
|
281
|
+
let deviceOptions = config?.devices?.find((device) => device?.serialNumber?.toUpperCase?.() === data?.serialNumber?.toUpperCase?.());
|
|
282
|
+
data.nest_google_uuid = deviceUUID;
|
|
283
|
+
data.serialNumber = data.serialNumber.toUpperCase(); // ensure serial numbers are in upper case
|
|
284
|
+
data.excluded = this?.config?.options?.exclude === true ? deviceOptions?.exclude !== false : deviceOptions?.exclude === true;
|
|
285
|
+
data.manufacturer = typeof data?.manufacturer === 'string' && data.manufacturer !== '' ? data.manufacturer : 'Nest';
|
|
286
|
+
data.softwareVersion = process_software_version(data.softwareVersion);
|
|
287
|
+
let description = typeof data?.description === 'string' ? data.description : '';
|
|
288
|
+
let location = typeof data?.location === 'string' ? data.location : '';
|
|
289
|
+
if (description === '' && location !== '') {
|
|
290
|
+
description = location;
|
|
291
|
+
location = '';
|
|
292
|
+
}
|
|
293
|
+
if (description === '' && location === '') {
|
|
294
|
+
description = 'unknown description';
|
|
295
|
+
}
|
|
296
|
+
data.description = HomeKitDevice.makeValidHKName(location === '' ? description : description + ' - ' + location);
|
|
297
|
+
delete data.location;
|
|
298
|
+
|
|
299
|
+
processed = data;
|
|
300
|
+
// eslint-disable-next-line no-unused-vars
|
|
301
|
+
} catch (error) {
|
|
302
|
+
// Empty
|
|
303
|
+
}
|
|
304
|
+
return processed;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function logJSONObject(log, object) {
|
|
308
|
+
if (typeof object !== 'object' || object.constructor !== Object) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
Object.entries(object).forEach(([key, value]) => {
|
|
313
|
+
if (typeof value === 'object' && value !== null) {
|
|
314
|
+
log?.debug?.(' %s:', key);
|
|
315
|
+
String(JSON.stringify(value, null, 2))
|
|
316
|
+
.split('\n')
|
|
317
|
+
.forEach((line) => {
|
|
318
|
+
log?.debug?.(' %s', line);
|
|
319
|
+
});
|
|
320
|
+
} else {
|
|
321
|
+
log?.debug?.(' %s: %j', key, value);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Define exports
|
|
327
|
+
export { processCommonData, adjustTemperature, crc24, scaleValue, fetchWrapper, parseDurationToSeconds, logJSONObject };
|