@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.35 → 5.0.0-beta.37
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/CONTRIBUTING.md +9 -17
- package/config.schema.json +6 -0
- package/dist/account/ownership.js +16 -16
- package/dist/account/ownership.js.map +1 -1
- package/dist/account/persistence.js +24 -74
- package/dist/account/persistence.js.map +1 -1
- package/dist/account/temporary-authentication.js +2 -2
- package/dist/account/temporary-authentication.js.map +1 -1
- package/dist/configuration.js +18 -25
- package/dist/configuration.js.map +1 -1
- package/dist/device/member-evidence.js +1 -1
- package/dist/device/member-evidence.js.map +1 -1
- package/dist/device/member-trust.js +14 -32
- package/dist/device/member-trust.js.map +1 -1
- package/dist/device/snapshot.js +5 -17
- package/dist/device/snapshot.js.map +1 -1
- package/dist/diagnostics.js +268 -308
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapter.js +4 -1
- package/dist/homekit/adapter.js.map +1 -1
- package/dist/homekit/adapters/battery.js +3 -3
- package/dist/homekit/adapters/battery.js.map +1 -1
- package/dist/homekit/adapters/camera-controls.js +15 -37
- package/dist/homekit/adapters/camera-controls.js.map +1 -1
- package/dist/homekit/adapters/camera-streaming.js +9 -19
- package/dist/homekit/adapters/camera-streaming.js.map +1 -1
- package/dist/homekit/adapters/contact.js +5 -5
- package/dist/homekit/adapters/contact.js.map +1 -1
- package/dist/homekit/adapters/doorbell.js +3 -15
- package/dist/homekit/adapters/doorbell.js.map +1 -1
- package/dist/homekit/adapters/lock.js +15 -214
- package/dist/homekit/adapters/lock.js.map +1 -1
- package/dist/homekit/adapters/motion.js +2 -2
- package/dist/homekit/adapters/motion.js.map +1 -1
- package/dist/homekit/adapters/package-sensor.js +2 -2
- package/dist/homekit/adapters/package-sensor.js.map +1 -1
- package/dist/homekit/adapters/security-system.js +11 -211
- package/dist/homekit/adapters/security-system.js.map +1 -1
- package/dist/homekit/adapters/siren.js +5 -33
- package/dist/homekit/adapters/siren.js.map +1 -1
- package/dist/homekit/adapters/smart-light.js +19 -217
- package/dist/homekit/adapters/smart-light.js.map +1 -1
- package/dist/homekit/coverage-matrix.js +8 -65
- package/dist/homekit/coverage-matrix.js.map +1 -1
- package/dist/homekit/device-control.js +249 -13
- package/dist/homekit/device-control.js.map +1 -1
- package/dist/homekit/reconciler.js +5 -5
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/last-successful-image.js +3 -20
- package/dist/media/last-successful-image.js.map +1 -1
- package/dist/media/live-preview.js +163 -42
- package/dist/media/live-preview.js.map +1 -1
- package/dist/media/live-stream.js +126 -72
- package/dist/media/live-stream.js.map +1 -1
- package/dist/media/recording.js +37 -120
- package/dist/media/recording.js.map +1 -1
- package/dist/media/snapshot.js +1 -9
- package/dist/media/snapshot.js.map +1 -1
- package/dist/media/station-live-sessions.js +0 -15
- package/dist/media/station-live-sessions.js.map +1 -1
- package/dist/platform.js +10 -10
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +91 -69
- package/dist/runtime/channel.js.map +1 -1
- package/dist/runtime/owner.js +6 -6
- package/dist/runtime/owner.js.map +1 -1
- package/dist/runtime/sdk-client.js +3 -14
- package/dist/runtime/sdk-client.js.map +1 -1
- package/dist/runtime/tracker.js +8 -38
- package/dist/runtime/tracker.js.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/storage.js.map +1 -1
- package/dist/ui/runtime-channel-client.js +145 -282
- package/dist/ui/runtime-channel-client.js.map +1 -1
- package/dist/ui/server.js +39 -53
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +150 -78
- package/homebridge-ui/public/i18n/en.json +15 -2
- package/homebridge-ui/public/i18n/fr.json +15 -2
- package/homebridge-ui/public/index.html +59 -10
- package/homebridge-ui/public/js/app.js +206 -42
- package/homebridge-ui/public/js/dashboard.js +4 -19
- package/i18n/runtime/en.json +1 -0
- package/package.json +3 -7
- package/homebridge-ui/public/js/legacy-settings.js +0 -37
- package/homebridge-ui/public/js/profile-wizard.js +0 -31
- package/homebridge-ui/public/js/setup-wizard.js +0 -52
package/dist/runtime/tracker.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { chmodSync, closeSync, mkdirSync, openSync, readSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
1
|
import { dirname } from 'node:path';
|
|
4
|
-
import { reapAbandonedTemporaryFiles } from '../account/
|
|
2
|
+
import { reapAbandonedTemporaryFiles } from '../account/ownership.js';
|
|
3
|
+
import { readJsonBounded, writeJsonAtomically } from '../account/persistence.js';
|
|
5
4
|
import { parseCompleteDeviceSnapshot } from '../device/snapshot.js';
|
|
6
5
|
const MAX_TRACKER_BYTES = 1024 * 1024;
|
|
6
|
+
/** How long a published active record stands as evidence of a live runtime. */
|
|
7
|
+
const FRESH_RUNTIME_MS = 90_000;
|
|
7
8
|
const ACTIVE_RUNTIME_STATES = new Set([
|
|
8
9
|
'acquiring-ownership',
|
|
9
10
|
'starting',
|
|
@@ -50,14 +51,12 @@ export function runtimeStatusFor(state) {
|
|
|
50
51
|
/** Reads advisory runtime liveness without granting or overriding SDK ownership. */
|
|
51
52
|
export class RuntimeTracker {
|
|
52
53
|
path;
|
|
53
|
-
freshThresholdMs;
|
|
54
54
|
now;
|
|
55
55
|
onError;
|
|
56
56
|
heartbeat;
|
|
57
57
|
current;
|
|
58
|
-
constructor(path,
|
|
58
|
+
constructor(path, now = Date.now, onError = () => undefined) {
|
|
59
59
|
this.path = path;
|
|
60
|
-
this.freshThresholdMs = freshThresholdMs;
|
|
61
60
|
this.now = now;
|
|
62
61
|
this.onError = onError;
|
|
63
62
|
}
|
|
@@ -76,7 +75,7 @@ export class RuntimeTracker {
|
|
|
76
75
|
}
|
|
77
76
|
const updatedAt = Date.parse(value.updatedAt);
|
|
78
77
|
const age = this.now() - updatedAt;
|
|
79
|
-
if (!Number.isFinite(updatedAt) || age < -5_000 || age >
|
|
78
|
+
if (!Number.isFinite(updatedAt) || age < -5_000 || age > FRESH_RUNTIME_MS) {
|
|
80
79
|
return null;
|
|
81
80
|
}
|
|
82
81
|
return { state: value.state, updatedAt: value.updatedAt };
|
|
@@ -163,21 +162,7 @@ export class RuntimeTracker {
|
|
|
163
162
|
this.update('stopped');
|
|
164
163
|
}
|
|
165
164
|
readValue() {
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
descriptor = openSync(this.path, 'r');
|
|
169
|
-
const buffer = Buffer.allocUnsafe(MAX_TRACKER_BYTES + 1);
|
|
170
|
-
const bytesRead = readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
171
|
-
if (bytesRead > MAX_TRACKER_BYTES) {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
return JSON.parse(buffer.toString('utf8', 0, bytesRead));
|
|
175
|
-
}
|
|
176
|
-
finally {
|
|
177
|
-
if (descriptor !== undefined) {
|
|
178
|
-
closeSync(descriptor);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
165
|
+
return readJsonBounded(this.path, MAX_TRACKER_BYTES);
|
|
181
166
|
}
|
|
182
167
|
publishSafely(record) {
|
|
183
168
|
try {
|
|
@@ -190,22 +175,7 @@ export class RuntimeTracker {
|
|
|
190
175
|
}
|
|
191
176
|
}
|
|
192
177
|
publish(record) {
|
|
193
|
-
|
|
194
|
-
mkdirSync(directory, { mode: 0o700, recursive: true });
|
|
195
|
-
chmodSync(directory, 0o700);
|
|
196
|
-
const temporaryPath = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
|
|
197
|
-
try {
|
|
198
|
-
const serialized = JSON.stringify({ ...record, updatedAt: new Date(this.now()).toISOString() });
|
|
199
|
-
if (Buffer.byteLength(serialized) > MAX_TRACKER_BYTES) {
|
|
200
|
-
throw new Error(`runtime tracker exceeds ${MAX_TRACKER_BYTES} bytes`);
|
|
201
|
-
}
|
|
202
|
-
writeFileSync(temporaryPath, serialized, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
203
|
-
renameSync(temporaryPath, this.path);
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
rmSync(temporaryPath, { force: true });
|
|
207
|
-
throw error;
|
|
208
|
-
}
|
|
178
|
+
writeJsonAtomically(this.path, { ...record, updatedAt: new Date(this.now()).toISOString() }, 'runtime tracker', MAX_TRACKER_BYTES);
|
|
209
179
|
}
|
|
210
180
|
}
|
|
211
181
|
//# sourceMappingURL=tracker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/runtime/tracker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/runtime/tracker.ts"],"names":[],"mappings":"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"}
|
package/dist/storage.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, renameSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isOwnerProcessAlive } from './account/ownership.js';
|
|
4
|
-
|
|
4
|
+
const STORAGE_DIRECTORY = 'homebridge-eufy';
|
|
5
5
|
const LEGACY_V5_STORAGE_DIRECTORY = 'eufy-security';
|
|
6
6
|
function hasLiveOwner(root) {
|
|
7
7
|
const ownershipRoot = join(root, 'ownership');
|
package/dist/storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAC5C,MAAM,2BAA2B,GAAG,eAAe,CAAC;AAEpD,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,aAAa,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aACvD,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;aACtC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACd,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAG5F,CAAC;gBACF,IACE,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC;oBAC5B,MAAM,CAAC,GAAc,IAAI,CAAC;oBAC3B,CAAC,MAAM,CAAC,eAAe,KAAK,SAAS,IAAI,OAAO,MAAM,CAAC,eAAe,KAAK,QAAQ,CAAC,EACpF,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,OAAO,mBAAmB,CAAC;oBACzB,GAAG,EAAE,MAAM,CAAC,GAAa;oBACzB,eAAe,EAAE,MAAM,CAAC,eAAqC;iBAC9D,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;IACP,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,kBAAkB,CAAC,qBAA6B;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,qBAAqB,EAAE,iBAAiB,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG,IAAI,CAAC,qBAAqB,EAAE,2BAA2B,CAAC,CAAC;IAC5E,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,QAAQ,iBAAiB,QAAQ,2BAA2B,4BAA4B,CAAC,CAAC;IAC5G,CAAC;IACD,IAAI,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7B,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,IAAI,CAAC;QACH,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5E,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createConnection } from 'node:net';
|
|
2
|
-
import { answeredDevice, answeredDeviceSetting, answeredStatus, areRuntimeChannelDevices, areRuntimeChannelDeviceSettings, FrameReader, isRuntimeChannelStatus, ownedSocket, RUNTIME_CHANNEL_CONNECT_TIMEOUT_MS, RUNTIME_CHANNEL_PROTOCOL, RUNTIME_CHANNEL_RESPONSE_TIMEOUT_MS, RUNTIME_CHANNEL_STAND_DOWN_TIMEOUT_MS, RUNTIME_CHANNEL_WRITE_TIMEOUT_MS, RUNTIME_DEVICE_LIVE_PATH, RUNTIME_DEVICE_SETTING_WRITE_PATH, RUNTIME_DEVICE_SETTINGS_PATH, RUNTIME_DEVICES_PATH, RUNTIME_DIAGNOSTICS_PATH, RUNTIME_RESTART_PATH, RUNTIME_STAND_DOWN_PATH, RUNTIME_STATUS_PATH, } from '../runtime/channel.js';
|
|
2
|
+
import { answeredDevice, answeredDeviceSetting, answeredStatus, areRuntimeChannelDevices, areRuntimeChannelDeviceSettings, FrameReader, isRuntimeChannelStatus, LIVE_UNPLAYABLE, ownedSocket, RUNTIME_CHANNEL_CONNECT_TIMEOUT_MS, RUNTIME_CHANNEL_PROTOCOL, RUNTIME_DEVICE_LIVE_ADAPT_PATH, RUNTIME_CHANNEL_RESPONSE_TIMEOUT_MS, RUNTIME_CHANNEL_STAND_DOWN_TIMEOUT_MS, RUNTIME_CHANNEL_WRITE_TIMEOUT_MS, RUNTIME_DEVICE_LIVE_PATH, RUNTIME_DEVICE_SETTING_WRITE_PATH, RUNTIME_DEVICE_SETTINGS_PATH, RUNTIME_DEVICES_PATH, RUNTIME_DIAGNOSTICS_PATH, RUNTIME_RESTART_PATH, RUNTIME_STAND_DOWN_PATH, RUNTIME_STATUS_PATH, } from '../runtime/channel.js';
|
|
3
3
|
const STATUS_REQUEST = 1;
|
|
4
4
|
const DEVICES_REQUEST = 2;
|
|
5
5
|
const DIAGNOSTICS_REQUEST = 3;
|
|
@@ -30,20 +30,95 @@ export class RuntimeChannelClient {
|
|
|
30
30
|
this.standDownTimeoutMs = options.standDownTimeoutMs ?? RUNTIME_CHANNEL_STAND_DOWN_TIMEOUT_MS;
|
|
31
31
|
this.writeTimeoutMs = options.writeTimeoutMs ?? RUNTIME_CHANNEL_WRITE_TIMEOUT_MS;
|
|
32
32
|
}
|
|
33
|
+
/** A device's eufy settings, or undefined where no runtime answers them. */
|
|
33
34
|
async readDeviceSettings(serial) {
|
|
34
|
-
return this.over(undefined, (connection) => this.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
return this.over(undefined, (connection) => this.converse(connection, () => frame(SETTINGS_REQUEST, RUNTIME_DEVICE_SETTINGS_PATH, { serial }), (response, conclude) => {
|
|
36
|
+
if (response?.id !== SETTINGS_REQUEST)
|
|
37
|
+
return;
|
|
38
|
+
conclude(response.ok && areRuntimeChannelDeviceSettings(response.data)
|
|
39
|
+
? response.data.map(answeredDeviceSetting)
|
|
40
|
+
: undefined);
|
|
41
|
+
}, () => undefined, this.responseTimeoutMs));
|
|
37
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Writes one device setting, true where the runtime delivered it. Delivery is not the device changing: the next
|
|
45
|
+
* read is what states that. An absent runtime refuses every write.
|
|
46
|
+
*/
|
|
38
47
|
async writeDeviceSetting(write) {
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
return this.over(false, (connection) => this.converse(connection, () => frame(SETTINGS_REQUEST, RUNTIME_DEVICE_SETTING_WRITE_PATH, write), answered, () => false, this.writeTimeoutMs));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Streams one camera's live preview as base64 JPEG pictures, each with its side and the live encoder and rate
|
|
52
|
+
* of the HomeKit side, until it ends, the runtime is gone, or `signal` aborts. Settles once it has stopped, true
|
|
53
|
+
* where the last HomeKit run yielded no picture; a runtime without the path, or with no preview to give, settles
|
|
54
|
+
* at once.
|
|
55
|
+
*/
|
|
56
|
+
async liveDevice(query, onPicture, signal) {
|
|
57
|
+
return this.over(false, (connection) => {
|
|
58
|
+
if (signal.aborted)
|
|
59
|
+
connection.destroy();
|
|
60
|
+
else
|
|
61
|
+
signal.addEventListener('abort', () => connection.destroy(), { once: true });
|
|
62
|
+
return this.converse(connection, () => frame(SETTINGS_REQUEST, RUNTIME_DEVICE_LIVE_PATH, query), (response, conclude) => {
|
|
63
|
+
if (response?.id !== SETTINGS_REQUEST)
|
|
64
|
+
return;
|
|
65
|
+
if (!response.ok)
|
|
66
|
+
return conclude(response.data === LIVE_UNPLAYABLE);
|
|
67
|
+
const { side, encoder, kbps } = response.coding ?? {};
|
|
68
|
+
if (typeof response.data !== 'string' || typeof encoder !== 'string')
|
|
69
|
+
return conclude(false);
|
|
70
|
+
if (side !== 'raw' && side !== 'homekit')
|
|
71
|
+
return conclude(false);
|
|
72
|
+
onPicture(response.data, typeof kbps === 'number' ? { side, encoder, kbps } : { side, encoder });
|
|
73
|
+
}, () => false);
|
|
74
|
+
});
|
|
41
75
|
}
|
|
42
|
-
|
|
43
|
-
|
|
76
|
+
/** Restarts the HomeKit side of the camera's running preview with other settings; false where none runs. */
|
|
77
|
+
async adaptLive(query) {
|
|
78
|
+
return this.over(false, (connection) => this.converse(connection, () => frame(SETTINGS_REQUEST, RUNTIME_DEVICE_LIVE_ADAPT_PATH, query), answered, () => false, this.responseTimeoutMs));
|
|
44
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Reads the greeting, the status, and the observations.
|
|
82
|
+
*
|
|
83
|
+
* The status is one answer and the observations are another, so a runtime that answers the first and then says
|
|
84
|
+
* nothing, closes, or overruns the frame bound concludes with the status alone. Only a read with no status at
|
|
85
|
+
* all, an unreadable frame, or an answer this client cannot use is absence.
|
|
86
|
+
*/
|
|
45
87
|
async read() {
|
|
46
|
-
|
|
88
|
+
let greeting;
|
|
89
|
+
let status;
|
|
90
|
+
let devices;
|
|
91
|
+
let devicesSettled = false;
|
|
92
|
+
const reading = () => status && {
|
|
93
|
+
ready: greeting.ready,
|
|
94
|
+
status,
|
|
95
|
+
...(devices === undefined ? {} : { devices }),
|
|
96
|
+
...(greeting.version === undefined ? {} : { version: greeting.version }),
|
|
97
|
+
...(greeting.build === undefined ? {} : { build: greeting.build }),
|
|
98
|
+
...(greeting.restartable === undefined ? {} : { restartable: greeting.restartable }),
|
|
99
|
+
};
|
|
100
|
+
return this.over(undefined, (connection) => this.converse(connection, (opened) => {
|
|
101
|
+
if (typeof opened.ready !== 'boolean')
|
|
102
|
+
return undefined;
|
|
103
|
+
greeting = opened;
|
|
104
|
+
return frame(STATUS_REQUEST, RUNTIME_STATUS_PATH) + frame(DEVICES_REQUEST, RUNTIME_DEVICES_PATH);
|
|
105
|
+
}, (response, conclude) => {
|
|
106
|
+
if (response?.id === STATUS_REQUEST) {
|
|
107
|
+
if (!response.ok || !isRuntimeChannelStatus(response.data))
|
|
108
|
+
return conclude(undefined);
|
|
109
|
+
status = answeredStatus(response.data);
|
|
110
|
+
}
|
|
111
|
+
else if (response?.id === DEVICES_REQUEST) {
|
|
112
|
+
devicesSettled = true;
|
|
113
|
+
if (response.ok) {
|
|
114
|
+
if (!areRuntimeChannelDevices(response.data))
|
|
115
|
+
return conclude(undefined);
|
|
116
|
+
devices = response.data.map(answeredDevice);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (status && devicesSettled)
|
|
120
|
+
conclude(reading());
|
|
121
|
+
}, (ending) => (ending === 'fault' ? undefined : reading()), this.responseTimeoutMs));
|
|
47
122
|
}
|
|
48
123
|
/**
|
|
49
124
|
* Tells the runtime which authorized evidence window was written, so it reads the file now.
|
|
@@ -52,7 +127,10 @@ export class RuntimeChannelClient {
|
|
|
52
127
|
* way: the file the runtime reads is the authority, and it decided before this was sent.
|
|
53
128
|
*/
|
|
54
129
|
async notifyAuthorization(supportCaseId) {
|
|
55
|
-
return this.over(undefined, (connection) => this.
|
|
130
|
+
return this.over(undefined, (connection) => this.converse(connection, () => frame(DIAGNOSTICS_REQUEST, RUNTIME_DIAGNOSTICS_PATH, { supportCaseId }), (response, conclude) => {
|
|
131
|
+
if (response?.id === DIAGNOSTICS_REQUEST)
|
|
132
|
+
conclude();
|
|
133
|
+
}, () => undefined, this.responseTimeoutMs));
|
|
56
134
|
}
|
|
57
135
|
/**
|
|
58
136
|
* Asks the runtime to release the account session, reporting whether it stood down.
|
|
@@ -64,16 +142,30 @@ export class RuntimeChannelClient {
|
|
|
64
142
|
* answer, which is that the session was not freed.
|
|
65
143
|
*/
|
|
66
144
|
async requestStandDown() {
|
|
67
|
-
return this.over(false, (connection) => this.
|
|
145
|
+
return this.over(false, (connection) => this.departed(connection, RUNTIME_STAND_DOWN_PATH));
|
|
68
146
|
}
|
|
69
147
|
/**
|
|
70
148
|
* Asks the runtime to end itself so Homebridge brings up the installed build, reporting whether it accepted.
|
|
71
149
|
*
|
|
72
150
|
* A runtime that does not serve the path, or refuses because ending it would not replace it, is a false rather
|
|
73
|
-
* than a failure, so a caller never has to tell an old runtime apart from an unwilling one.
|
|
151
|
+
* than a failure, so a caller never has to tell an old runtime apart from an unwilling one. Whether the
|
|
152
|
+
* replacement came up is read from the next greeting.
|
|
74
153
|
*/
|
|
75
154
|
async requestRestart() {
|
|
76
|
-
return this.over(false, (connection) => this.
|
|
155
|
+
return this.over(false, (connection) => this.departed(connection, RUNTIME_RESTART_PATH));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Sends one departure request and reports whether the endpoint then closed without refusing it.
|
|
159
|
+
*
|
|
160
|
+
* A runtime that releases its session or ends for replacement closes its endpoint at the end of its own bounded
|
|
161
|
+
* shutdown, so the closing is the completion this waits for and an explicit refusal is the one answer that ends
|
|
162
|
+
* it early. Acceptance is not waited for, because a runtime that closes the connection may not have flushed it.
|
|
163
|
+
*/
|
|
164
|
+
departed(connection, path) {
|
|
165
|
+
return this.converse(connection, () => frame(DEPARTURE_REQUEST, path), (response, conclude) => {
|
|
166
|
+
if (response?.id === DEPARTURE_REQUEST && !response.ok)
|
|
167
|
+
conclude(false);
|
|
168
|
+
}, (ending) => ending === 'closed', this.standDownTimeoutMs);
|
|
77
169
|
}
|
|
78
170
|
/**
|
|
79
171
|
* Runs one operation over its own connection, answering `absent` wherever the endpoint could not be used.
|
|
@@ -117,297 +209,68 @@ export class RuntimeChannelClient {
|
|
|
117
209
|
});
|
|
118
210
|
}
|
|
119
211
|
/**
|
|
120
|
-
*
|
|
212
|
+
* Holds one conversation: writes what `request` makes of the greeting, then hands each response to `onResponse`
|
|
213
|
+
* until it concludes, and answers every other ending with `unanswered`.
|
|
121
214
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
215
|
+
* Everything before the request is written is a `fault`: a greeting naming another protocol or one `request`
|
|
216
|
+
* refuses is never sent the request, so a runtime running older code is never handed a frame it would read as
|
|
217
|
+
* something else. An unreadable frame is a `fault` too. After the request, the endpoint closing is `closed`, and
|
|
218
|
+
* `timeoutMs` passing or a frame beyond the frame bound is `lapsed`.
|
|
124
219
|
*/
|
|
125
|
-
|
|
220
|
+
converse(connection, request, onResponse, unanswered, timeoutMs) {
|
|
126
221
|
return new Promise((resolve) => {
|
|
127
|
-
const settle = setTimeout(resolve, this.responseTimeoutMs);
|
|
128
|
-
settle.unref();
|
|
129
|
-
const conclude = () => {
|
|
130
|
-
clearTimeout(settle);
|
|
131
|
-
resolve();
|
|
132
|
-
};
|
|
133
|
-
const reader = new FrameReader();
|
|
134
|
-
let greeted = false;
|
|
135
|
-
connection.once('error', conclude);
|
|
136
|
-
connection.once('close', conclude);
|
|
137
|
-
connection.on('data', (chunk) => {
|
|
138
|
-
const frames = reader.accept(chunk);
|
|
139
|
-
if (!frames) {
|
|
140
|
-
conclude();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
for (const frame of frames) {
|
|
144
|
-
let parsed;
|
|
145
|
-
try {
|
|
146
|
-
parsed = JSON.parse(frame);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
conclude();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (greeted) {
|
|
153
|
-
if (parsed?.id === DIAGNOSTICS_REQUEST) {
|
|
154
|
-
conclude();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (parsed?.protocol !== RUNTIME_CHANNEL_PROTOCOL) {
|
|
160
|
-
conclude();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
greeted = true;
|
|
164
|
-
connection.write(`${JSON.stringify({
|
|
165
|
-
v: RUNTIME_CHANNEL_PROTOCOL,
|
|
166
|
-
id: DIAGNOSTICS_REQUEST,
|
|
167
|
-
path: RUNTIME_DIAGNOSTICS_PATH,
|
|
168
|
-
body: { supportCaseId },
|
|
169
|
-
})}\n`);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Sends one departure request after the greeting and reports what became of it.
|
|
176
|
-
*
|
|
177
|
-
* A runtime that releases its session or ends for replacement closes its endpoint at the end of its own bounded
|
|
178
|
-
* shutdown, so the closing is the completion this waits for and an explicit refusal is the one answer that ends
|
|
179
|
-
* it early. Acceptance is not waited for, because a runtime that closes the connection may not have flushed it.
|
|
180
|
-
* A protocol this client does not speak is never sent the request, so a frame an older runtime reads as
|
|
181
|
-
* something else cannot take a session down that nothing asked for.
|
|
182
|
-
*/
|
|
183
|
-
accepted(connection, path) {
|
|
184
|
-
return new Promise((resolve) => {
|
|
185
|
-
const settle = setTimeout(() => resolve(false), this.standDownTimeoutMs);
|
|
186
|
-
settle.unref();
|
|
187
|
-
const conclude = (accepted) => {
|
|
188
|
-
clearTimeout(settle);
|
|
189
|
-
resolve(accepted);
|
|
190
|
-
};
|
|
191
|
-
const reader = new FrameReader();
|
|
192
222
|
let requested = false;
|
|
193
|
-
|
|
194
|
-
connection.once('close', () => conclude(requested));
|
|
195
|
-
connection.on('data', (chunk) => {
|
|
196
|
-
const frames = reader.accept(chunk);
|
|
197
|
-
if (!frames) {
|
|
198
|
-
conclude(false);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
for (const frame of frames) {
|
|
202
|
-
let parsed;
|
|
203
|
-
try {
|
|
204
|
-
parsed = JSON.parse(frame);
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
conclude(false);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (!requested) {
|
|
211
|
-
if (parsed?.protocol !== RUNTIME_CHANNEL_PROTOCOL) {
|
|
212
|
-
conclude(false);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
requested = true;
|
|
216
|
-
connection.write(`${JSON.stringify({ v: RUNTIME_CHANNEL_PROTOCOL, id: DEPARTURE_REQUEST, path })}\n`);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
const response = parsed;
|
|
220
|
-
if (response?.id === DEPARTURE_REQUEST && !response.ok) {
|
|
221
|
-
conclude(false);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Sends one request after the greeting and answers what `accept` makes of its response.
|
|
230
|
-
*
|
|
231
|
-
* Undefined is every other outcome: a protocol this client does not speak, which is never sent the request, a
|
|
232
|
-
* refusal, an answer `accept` does not take, and no answer within `timeoutMs`.
|
|
233
|
-
*/
|
|
234
|
-
requested(connection, path, body, timeoutMs, accept) {
|
|
235
|
-
return new Promise((resolve) => {
|
|
236
|
-
const settle = setTimeout(() => resolve(undefined), timeoutMs);
|
|
237
|
-
settle.unref();
|
|
223
|
+
let done = false;
|
|
238
224
|
const conclude = (value) => {
|
|
225
|
+
done = true;
|
|
239
226
|
clearTimeout(settle);
|
|
240
227
|
resolve(value);
|
|
241
228
|
};
|
|
229
|
+
const end = (ending) => {
|
|
230
|
+
if (!done)
|
|
231
|
+
conclude(unanswered(requested ? ending : 'fault'));
|
|
232
|
+
};
|
|
233
|
+
const settle = timeoutMs === undefined ? undefined : setTimeout(() => end('lapsed'), timeoutMs);
|
|
234
|
+
settle?.unref();
|
|
242
235
|
const reader = new FrameReader();
|
|
243
|
-
|
|
244
|
-
connection.once('
|
|
245
|
-
connection.once('close', () => conclude(undefined));
|
|
246
|
-
connection.on('data', (chunk) => {
|
|
247
|
-
const frames = reader.accept(chunk);
|
|
248
|
-
if (!frames) {
|
|
249
|
-
conclude(undefined);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
for (const frame of frames) {
|
|
253
|
-
let parsed;
|
|
254
|
-
try {
|
|
255
|
-
parsed = JSON.parse(frame);
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
conclude(undefined);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (!requested) {
|
|
262
|
-
if (parsed?.protocol !== RUNTIME_CHANNEL_PROTOCOL) {
|
|
263
|
-
conclude(undefined);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
requested = true;
|
|
267
|
-
connection.write(`${JSON.stringify({ v: RUNTIME_CHANNEL_PROTOCOL, id: SETTINGS_REQUEST, path, body })}\n`);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
const response = parsed;
|
|
271
|
-
if (response?.id === SETTINGS_REQUEST) {
|
|
272
|
-
conclude(accept(response));
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
/** Sends one live request after the greeting and hands on each picture, settling once the stream ends. */
|
|
280
|
-
streamed(connection, serial, onPicture, signal) {
|
|
281
|
-
return new Promise((resolve) => {
|
|
282
|
-
const conclude = () => resolve(undefined);
|
|
283
|
-
if (signal.aborted)
|
|
284
|
-
return conclude();
|
|
285
|
-
signal.addEventListener('abort', conclude, { once: true });
|
|
286
|
-
const reader = new FrameReader();
|
|
287
|
-
let requested = false;
|
|
288
|
-
connection.once('error', conclude);
|
|
289
|
-
connection.once('close', conclude);
|
|
236
|
+
connection.once('error', () => end('closed'));
|
|
237
|
+
connection.once('close', () => end('closed'));
|
|
290
238
|
connection.on('data', (chunk) => {
|
|
291
239
|
const frames = reader.accept(chunk);
|
|
292
240
|
if (!frames)
|
|
293
|
-
return
|
|
294
|
-
for (const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
parsed = JSON.parse(frame);
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
return conclude();
|
|
301
|
-
}
|
|
302
|
-
if (!requested) {
|
|
303
|
-
if (parsed?.protocol !== RUNTIME_CHANNEL_PROTOCOL)
|
|
304
|
-
return conclude();
|
|
305
|
-
requested = true;
|
|
306
|
-
const request = { v: RUNTIME_CHANNEL_PROTOCOL, id: SETTINGS_REQUEST, path: RUNTIME_DEVICE_LIVE_PATH };
|
|
307
|
-
connection.write(`${JSON.stringify({ ...request, body: { serial } })}\n`);
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const response = parsed;
|
|
311
|
-
if (response?.id !== SETTINGS_REQUEST)
|
|
312
|
-
continue;
|
|
313
|
-
if (!response.ok || typeof response.data !== 'string')
|
|
314
|
-
return conclude();
|
|
315
|
-
onPicture(response.data);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
exchange(connection) {
|
|
321
|
-
return new Promise((resolve, reject) => {
|
|
322
|
-
const settle = setTimeout(() => concludeOrFail('runtime channel response timed out'), this.responseTimeoutMs);
|
|
323
|
-
settle.unref();
|
|
324
|
-
const reader = new FrameReader();
|
|
325
|
-
let greeting;
|
|
326
|
-
const fail = (message) => {
|
|
327
|
-
clearTimeout(settle);
|
|
328
|
-
reject(new Error(message));
|
|
329
|
-
};
|
|
330
|
-
/**
|
|
331
|
-
* Concludes with the status alone where the observations never settled.
|
|
332
|
-
*
|
|
333
|
-
* The status is one answer and the observations are another, so a runtime that answers the first and
|
|
334
|
-
* then says nothing must not cost both. Only a read with no status at all is a failure.
|
|
335
|
-
*/
|
|
336
|
-
const concludeOrFail = (message) => {
|
|
337
|
-
if (greeting && answeredStatusValue) {
|
|
338
|
-
devicesSettled = true;
|
|
339
|
-
settleIfComplete();
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
fail(message);
|
|
343
|
-
};
|
|
344
|
-
connection.once('error', () => concludeOrFail('runtime channel faulted'));
|
|
345
|
-
connection.once('close', () => concludeOrFail('runtime channel closed before answering'));
|
|
346
|
-
let answeredStatusValue;
|
|
347
|
-
let answeredDevices;
|
|
348
|
-
let devicesSettled = false;
|
|
349
|
-
const settleIfComplete = () => {
|
|
350
|
-
if (!greeting || !answeredStatusValue || !devicesSettled) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
clearTimeout(settle);
|
|
354
|
-
resolve({
|
|
355
|
-
ready: greeting.ready,
|
|
356
|
-
status: answeredStatusValue,
|
|
357
|
-
...(answeredDevices === undefined ? {} : { devices: answeredDevices }),
|
|
358
|
-
...(greeting.version === undefined ? {} : { version: greeting.version }),
|
|
359
|
-
...(greeting.build === undefined ? {} : { build: greeting.build }),
|
|
360
|
-
...(greeting.restartable === undefined ? {} : { restartable: greeting.restartable }),
|
|
361
|
-
});
|
|
362
|
-
};
|
|
363
|
-
connection.on('data', (chunk) => {
|
|
364
|
-
const frames = reader.accept(chunk);
|
|
365
|
-
if (!frames) {
|
|
366
|
-
connection.destroy();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
for (const frame of frames) {
|
|
241
|
+
return end('lapsed');
|
|
242
|
+
for (const text of frames) {
|
|
243
|
+
if (done)
|
|
244
|
+
return;
|
|
370
245
|
let parsed;
|
|
371
246
|
try {
|
|
372
|
-
parsed = JSON.parse(
|
|
247
|
+
parsed = JSON.parse(text);
|
|
373
248
|
}
|
|
374
249
|
catch {
|
|
375
|
-
|
|
376
|
-
return;
|
|
250
|
+
return end('fault');
|
|
377
251
|
}
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
if (opened?.protocol !== RUNTIME_CHANNEL_PROTOCOL || typeof opened.ready !== 'boolean') {
|
|
381
|
-
fail('runtime channel speaks another protocol');
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
greeting = opened;
|
|
385
|
-
connection.write(`${JSON.stringify({ v: RUNTIME_CHANNEL_PROTOCOL, id: STATUS_REQUEST, path: RUNTIME_STATUS_PATH })}\n` +
|
|
386
|
-
`${JSON.stringify({ v: RUNTIME_CHANNEL_PROTOCOL, id: DEVICES_REQUEST, path: RUNTIME_DEVICES_PATH })}\n`);
|
|
252
|
+
if (requested) {
|
|
253
|
+
onResponse(parsed, conclude);
|
|
387
254
|
continue;
|
|
388
255
|
}
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
answeredStatusValue = answeredStatus(response.data);
|
|
396
|
-
}
|
|
397
|
-
else if (response?.id === DEVICES_REQUEST) {
|
|
398
|
-
devicesSettled = true;
|
|
399
|
-
if (response.ok) {
|
|
400
|
-
if (!areRuntimeChannelDevices(response.data)) {
|
|
401
|
-
fail('runtime channel answered no observations this client can use');
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
answeredDevices = response.data.map(answeredDevice);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
settleIfComplete();
|
|
256
|
+
const greeting = parsed;
|
|
257
|
+
const written = greeting?.protocol === RUNTIME_CHANNEL_PROTOCOL ? request(greeting) : undefined;
|
|
258
|
+
if (written === undefined)
|
|
259
|
+
return end('fault');
|
|
260
|
+
requested = true;
|
|
261
|
+
connection.write(written);
|
|
408
262
|
}
|
|
409
263
|
});
|
|
410
264
|
});
|
|
411
265
|
}
|
|
412
266
|
}
|
|
267
|
+
/** One request frame of this protocol. */
|
|
268
|
+
function frame(id, path, body) {
|
|
269
|
+
return `${JSON.stringify({ v: RUNTIME_CHANNEL_PROTOCOL, id, path, ...(body === undefined ? {} : { body }) })}\n`;
|
|
270
|
+
}
|
|
271
|
+
/** Concludes on the settings request's answer: true where accepted, false where refused. */
|
|
272
|
+
function answered(response, conclude) {
|
|
273
|
+
if (response?.id === SETTINGS_REQUEST)
|
|
274
|
+
conclude(!!response.ok);
|
|
275
|
+
}
|
|
413
276
|
//# sourceMappingURL=runtime-channel-client.js.map
|