camstack 1.2.84 → 1.2.86
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.
|
@@ -23633,9 +23633,9 @@ var require_zod = __commonJS({
|
|
|
23633
23633
|
}
|
|
23634
23634
|
});
|
|
23635
23635
|
|
|
23636
|
-
// ../system/dist/dist-
|
|
23637
|
-
var
|
|
23638
|
-
"../system/dist/dist-
|
|
23636
|
+
// ../system/dist/dist-D8VIyu_0.js
|
|
23637
|
+
var require_dist_D8VIyu_0 = __commonJS({
|
|
23638
|
+
"../system/dist/dist-D8VIyu_0.js"(exports) {
|
|
23639
23639
|
"use strict";
|
|
23640
23640
|
var zod = require_zod();
|
|
23641
23641
|
var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
|
|
@@ -31077,7 +31077,26 @@ var require_dist_TEwQZL3g = __commonJS({
|
|
|
31077
31077
|
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
31078
31078
|
* flows live through the cap STATUS SLICE after adoption.
|
|
31079
31079
|
*/
|
|
31080
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
31080
|
+
sourceInfo: SourceInfoSchema.optional(),
|
|
31081
|
+
/**
|
|
31082
|
+
* Set when this candidate is a device the provider ALREADY owns.
|
|
31083
|
+
*
|
|
31084
|
+
* A scan cannot generally produce the identity a device was onboarded under
|
|
31085
|
+
* (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
|
|
31086
|
+
* comparison never matches and an owned device looks addable. Re-adopting one
|
|
31087
|
+
* overwrites its config with scan-derived values — that is how a Home Hub's
|
|
31088
|
+
* Baichuan port was overwritten with its ONVIF port, taking the hub and its
|
|
31089
|
+
* three child cameras offline for four hours.
|
|
31090
|
+
*
|
|
31091
|
+
* A provider that can recognise its own devices says so here. Absent means
|
|
31092
|
+
* "not recognised", which is not the same as "known to be new" — a provider
|
|
31093
|
+
* that cannot tell simply never sets it.
|
|
31094
|
+
*/
|
|
31095
|
+
alreadyOnboarded: zod.z.boolean().optional(),
|
|
31096
|
+
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
31097
|
+
onboardedDeviceId: zod.z.number().optional(),
|
|
31098
|
+
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
31099
|
+
onboardedName: zod.z.string().optional()
|
|
31081
31100
|
});
|
|
31082
31101
|
var DeviceSummarySchema = zod.z.object({
|
|
31083
31102
|
id: zod.z.number(),
|
|
@@ -31114,6 +31133,35 @@ var require_dist_TEwQZL3g = __commonJS({
|
|
|
31114
31133
|
name: zod.z.string(),
|
|
31115
31134
|
type: zod.z.string()
|
|
31116
31135
|
}))),
|
|
31136
|
+
/**
|
|
31137
|
+
* Tear down and reconstruct ONE device in place from its persisted rows —
|
|
31138
|
+
* touching no other device this provider owns.
|
|
31139
|
+
*
|
|
31140
|
+
* The primitive `deviceManager.migrateDevice` uses to flush the two
|
|
31141
|
+
* migrated numbers: after `swapIds` the runner's live instance still
|
|
31142
|
+
* carries the PRE-swap numeric id (baked into the object, its native-cap
|
|
31143
|
+
* registrations and its log tags), and a live object cannot be renumbered.
|
|
31144
|
+
* Before this method the only flush was restarting the whole owning addon
|
|
31145
|
+
* — which took every camera the provider owns down with it (28 devices
|
|
31146
|
+
* for one migrated camera, measured 2026-09-04, and the morning of the
|
|
31147
|
+
* same day ~27 devices' native caps did not come back on their own).
|
|
31148
|
+
*
|
|
31149
|
+
* Keyed by `stableId`, deliberately: the numeric id is exactly the thing
|
|
31150
|
+
* that changes. The reply carries the id the device answers on NOW.
|
|
31151
|
+
* Implemented once in `BaseDeviceProvider` — decommission the live
|
|
31152
|
+
* instance (if any), then re-create from the persisted row: the same
|
|
31153
|
+
* teardown/rehydrate pair every graceful shutdown + boot already uses.
|
|
31154
|
+
* An RPC, never an event: a dropped event would leave the runner writing
|
|
31155
|
+
* against the wrong camera (D8).
|
|
31156
|
+
*
|
|
31157
|
+
* Construction can dial hardware, and the migrated source is
|
|
31158
|
+
* characteristically dead — the timeout covers a full activate window
|
|
31159
|
+
* rather than the 60 s default.
|
|
31160
|
+
*/
|
|
31161
|
+
reloadDevice: method(zod.z.object({ stableId: zod.z.string() }), zod.z.object({ deviceId: zod.z.number() }), {
|
|
31162
|
+
kind: "mutation",
|
|
31163
|
+
timeoutMs: 3 * 6e4
|
|
31164
|
+
}),
|
|
31117
31165
|
supportsDiscovery: method(zod.z.object({}), zod.z.boolean()),
|
|
31118
31166
|
/**
|
|
31119
31167
|
* Run a network scan. `params` carries optional provider-specific scan
|
|
@@ -31434,13 +31482,21 @@ var require_dist_TEwQZL3g = __commonJS({
|
|
|
31434
31482
|
* `sourceStillLive` names what stayed live; empty is the only value that
|
|
31435
31483
|
* means the old hardware is quiet. The migration proceeds either way —
|
|
31436
31484
|
* refusing would refuse the case this exists for.
|
|
31485
|
+
*
|
|
31486
|
+
* **Safe to retry.** A duplicate call while a migration involving either
|
|
31487
|
+
* device is in flight is REFUSED, never queued; a repeat of a swap that
|
|
31488
|
+
* already completed (matched against the `addonId`/`stableId` fingerprint
|
|
31489
|
+
* the swap left on the rows, within 15 min) is a no-op returning the
|
|
31490
|
+
* original report. A blind re-run can no longer silently undo the
|
|
31491
|
+
* migration it retried.
|
|
31437
31492
|
*/
|
|
31438
31493
|
migrateDevice: method(zod.z.object({
|
|
31439
31494
|
sourceId: zod.z.number(),
|
|
31440
31495
|
targetId: zod.z.number()
|
|
31441
31496
|
}), MigrateDeviceResultSchema, {
|
|
31442
31497
|
kind: "mutation",
|
|
31443
|
-
auth: "admin"
|
|
31498
|
+
auth: "admin",
|
|
31499
|
+
timeoutMs: 12 * 6e4
|
|
31444
31500
|
}),
|
|
31445
31501
|
/** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
|
|
31446
31502
|
registerDevice: method(DeviceRegisterPayloadSchema, zod.z.void(), { kind: "mutation" }),
|
|
@@ -52508,6 +52564,12 @@ var require_dist_TEwQZL3g = __commonJS({
|
|
|
52508
52564
|
addonId: null,
|
|
52509
52565
|
access: "view"
|
|
52510
52566
|
},
|
|
52567
|
+
"deviceProvider.reloadDevice": {
|
|
52568
|
+
capName: "device-provider",
|
|
52569
|
+
capScope: "system",
|
|
52570
|
+
addonId: null,
|
|
52571
|
+
access: "create"
|
|
52572
|
+
},
|
|
52511
52573
|
"deviceProvider.start": {
|
|
52512
52574
|
capName: "device-provider",
|
|
52513
52575
|
capScope: "system",
|
|
@@ -59999,7 +60061,7 @@ var require_alerts_addon = __commonJS({
|
|
|
59999
60061
|
[Symbol.toStringTag]: { value: "Module" }
|
|
60000
60062
|
});
|
|
60001
60063
|
require_chunk_Cek0wNdY();
|
|
60002
|
-
var require_dist10 =
|
|
60064
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
60003
60065
|
function selectExpired(alerts, cutoffMs) {
|
|
60004
60066
|
return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
|
|
60005
60067
|
}
|
|
@@ -60818,7 +60880,7 @@ var require_console_logging = __commonJS({
|
|
|
60818
60880
|
[Symbol.toStringTag]: { value: "Module" }
|
|
60819
60881
|
});
|
|
60820
60882
|
require_chunk_Cek0wNdY();
|
|
60821
|
-
var require_dist10 =
|
|
60883
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
60822
60884
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
60823
60885
|
var LEVEL_RANK = {
|
|
60824
60886
|
debug: 0,
|
|
@@ -60912,7 +60974,7 @@ var require_core_blocks_addon = __commonJS({
|
|
|
60912
60974
|
"use strict";
|
|
60913
60975
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
60914
60976
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
60915
|
-
var require_dist10 =
|
|
60977
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
60916
60978
|
var node_crypto = __require("crypto");
|
|
60917
60979
|
var node_fs_promises = __require("fs/promises");
|
|
60918
60980
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -61809,11 +61871,11 @@ var require_core_blocks = __commonJS({
|
|
|
61809
61871
|
}
|
|
61810
61872
|
});
|
|
61811
61873
|
|
|
61812
|
-
// ../system/dist/retired-settings-keys-
|
|
61813
|
-
var
|
|
61814
|
-
"../system/dist/retired-settings-keys-
|
|
61874
|
+
// ../system/dist/retired-settings-keys-wakiINPT.js
|
|
61875
|
+
var require_retired_settings_keys_wakiINPT = __commonJS({
|
|
61876
|
+
"../system/dist/retired-settings-keys-wakiINPT.js"(exports) {
|
|
61815
61877
|
"use strict";
|
|
61816
|
-
var require_dist10 =
|
|
61878
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
61817
61879
|
function settingsStoreIsAuthoritativeHere(env) {
|
|
61818
61880
|
const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
|
|
61819
61881
|
return raw === "" || raw === "hub";
|
|
@@ -64027,8 +64089,8 @@ var require_device_manager_addon = __commonJS({
|
|
|
64027
64089
|
[Symbol.toStringTag]: { value: "Module" }
|
|
64028
64090
|
});
|
|
64029
64091
|
require_chunk_Cek0wNdY();
|
|
64030
|
-
var require_dist10 =
|
|
64031
|
-
var require_retired_settings_keys =
|
|
64092
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
64093
|
+
var require_retired_settings_keys = require_retired_settings_keys_wakiINPT();
|
|
64032
64094
|
var node_crypto = __require("crypto");
|
|
64033
64095
|
var _camstack_types_node = require_node();
|
|
64034
64096
|
var JOB_HISTORY = 20;
|
|
@@ -65504,7 +65566,10 @@ var require_device_manager_addon = __commonJS({
|
|
|
65504
65566
|
stableId: d.stableId,
|
|
65505
65567
|
type: d.type,
|
|
65506
65568
|
suggestedName: d.suggestedName,
|
|
65507
|
-
prefilledConfig: d.prefilledConfig
|
|
65569
|
+
prefilledConfig: d.prefilledConfig,
|
|
65570
|
+
...d.alreadyOnboarded === true ? { alreadyOnboarded: true } : {},
|
|
65571
|
+
...d.onboardedDeviceId !== void 0 ? { onboardedDeviceId: d.onboardedDeviceId } : {},
|
|
65572
|
+
...d.onboardedName !== void 0 ? { onboardedName: d.onboardedName } : {}
|
|
65508
65573
|
}));
|
|
65509
65574
|
}
|
|
65510
65575
|
async function adoptDevice(pctx, input) {
|
|
@@ -66527,10 +66592,26 @@ var require_device_manager_addon = __commonJS({
|
|
|
66527
66592
|
detail
|
|
66528
66593
|
};
|
|
66529
66594
|
}
|
|
66595
|
+
async function setSwitchBounded(deps, deviceId, switchId, enabled) {
|
|
66596
|
+
const budget = deps.switchTimeoutMs ?? 15e3;
|
|
66597
|
+
const write = deps.setSwitch(deviceId, switchId, enabled);
|
|
66598
|
+
write.catch(() => {
|
|
66599
|
+
});
|
|
66600
|
+
let timer;
|
|
66601
|
+
try {
|
|
66602
|
+
await Promise.race([write, new Promise((_resolve, reject) => {
|
|
66603
|
+
timer = setTimeout(() => {
|
|
66604
|
+
reject(/* @__PURE__ */ new Error(`switch write did not answer within ${budget}ms \u2014 reported unreachable; if it lands late, a duplicate write of the same value is a no-op`));
|
|
66605
|
+
}, budget);
|
|
66606
|
+
})]);
|
|
66607
|
+
} finally {
|
|
66608
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
66609
|
+
}
|
|
66610
|
+
}
|
|
66530
66611
|
async function applyAll(deps, deviceId, enabled) {
|
|
66531
66612
|
const out = [];
|
|
66532
66613
|
for (const switchId of require_dist10.CAMERA_SWITCH_ORDER) try {
|
|
66533
|
-
await deps
|
|
66614
|
+
await setSwitchBounded(deps, deviceId, switchId, enabled);
|
|
66534
66615
|
out.push({
|
|
66535
66616
|
deviceId,
|
|
66536
66617
|
switchId,
|
|
@@ -66571,7 +66652,6 @@ var require_device_manager_addon = __commonJS({
|
|
|
66571
66652
|
});
|
|
66572
66653
|
}
|
|
66573
66654
|
const up = await applyAll(deps, sourceId, true);
|
|
66574
|
-
await deps.respawnRunners([sourceId, targetId]);
|
|
66575
66655
|
deps.log("camera migrated", {
|
|
66576
66656
|
sourceId,
|
|
66577
66657
|
targetId,
|
|
@@ -66586,6 +66666,50 @@ var require_device_manager_addon = __commonJS({
|
|
|
66586
66666
|
swapped: true
|
|
66587
66667
|
};
|
|
66588
66668
|
}
|
|
66669
|
+
async function reloadMigratedDevices(pctx, targets) {
|
|
66670
|
+
const ctx = pctx.host.ctx;
|
|
66671
|
+
for (const target of targets) {
|
|
66672
|
+
pctx.metaStore.idToAddonId.set(target.deviceId, target.addonId);
|
|
66673
|
+
pctx.host.remoteNativeCaps.delete(target.deviceId);
|
|
66674
|
+
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(target.deviceId);
|
|
66675
|
+
}
|
|
66676
|
+
const fallbackAddons = /* @__PURE__ */ new Map();
|
|
66677
|
+
await Promise.all(targets.map(async (target) => {
|
|
66678
|
+
try {
|
|
66679
|
+
const reloaded = await (await pctx.host.requireDeviceProvider(target.addonId)).reloadDevice({ stableId: target.stableId });
|
|
66680
|
+
ctx.logger.info("migration: device reloaded in place \u2014 no other device disturbed", {
|
|
66681
|
+
tags: { deviceId: target.deviceId },
|
|
66682
|
+
meta: {
|
|
66683
|
+
addonId: target.addonId,
|
|
66684
|
+
stableId: target.stableId,
|
|
66685
|
+
reloadedAs: reloaded.deviceId
|
|
66686
|
+
}
|
|
66687
|
+
});
|
|
66688
|
+
} catch (err) {
|
|
66689
|
+
ctx.logger.error("migration: in-place reload FAILED \u2014 falling back to a provider restart", {
|
|
66690
|
+
tags: { deviceId: target.deviceId },
|
|
66691
|
+
meta: {
|
|
66692
|
+
addonId: target.addonId,
|
|
66693
|
+
stableId: target.stableId,
|
|
66694
|
+
error: err instanceof Error ? err.message : String(err)
|
|
66695
|
+
}
|
|
66696
|
+
});
|
|
66697
|
+
fallbackAddons.set(target.addonId, target.deviceId);
|
|
66698
|
+
}
|
|
66699
|
+
}));
|
|
66700
|
+
if (fallbackAddons.size === 0) return;
|
|
66701
|
+
await Promise.all([...fallbackAddons.entries()].map(async ([addonId, migratedDeviceId]) => {
|
|
66702
|
+
const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
|
|
66703
|
+
ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
|
|
66704
|
+
tags: { deviceId: migratedDeviceId },
|
|
66705
|
+
meta: {
|
|
66706
|
+
addonId,
|
|
66707
|
+
devicesTakenDown: owned
|
|
66708
|
+
}
|
|
66709
|
+
});
|
|
66710
|
+
await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
66711
|
+
}));
|
|
66712
|
+
}
|
|
66589
66713
|
var PROFILE_MAP_CONFIG_KEY = "_profileMap";
|
|
66590
66714
|
var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
|
|
66591
66715
|
function splitConfig(config) {
|
|
@@ -66674,68 +66798,110 @@ var require_device_manager_addon = __commonJS({
|
|
|
66674
66798
|
async function migrateDevice(pctx, input) {
|
|
66675
66799
|
const { sourceId, targetId } = input;
|
|
66676
66800
|
const ctx = pctx.host.ctx;
|
|
66677
|
-
|
|
66678
|
-
|
|
66679
|
-
const
|
|
66680
|
-
|
|
66681
|
-
|
|
66682
|
-
|
|
66683
|
-
|
|
66684
|
-
|
|
66685
|
-
|
|
66686
|
-
|
|
66687
|
-
},
|
|
66688
|
-
|
|
66689
|
-
|
|
66690
|
-
}
|
|
66691
|
-
|
|
66692
|
-
|
|
66693
|
-
|
|
66694
|
-
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
66695
|
-
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
66696
|
-
tags: { deviceId: a },
|
|
66801
|
+
pctx.migrationGuard.begin(sourceId, targetId);
|
|
66802
|
+
try {
|
|
66803
|
+
const outcome = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
66804
|
+
const sourceRow = await pctx.metaStore.rows.get(sourceId);
|
|
66805
|
+
if (sourceRow === null) throw new Error(`[device-manager] migrateDevice: device ${sourceId} does not exist`);
|
|
66806
|
+
const targetRow = await pctx.metaStore.rows.get(targetId);
|
|
66807
|
+
if (targetRow === null) throw new Error(`[device-manager] migrateDevice: device ${targetId} does not exist`);
|
|
66808
|
+
const replay = pctx.migrationGuard.matchReplay(sourceId, targetId, {
|
|
66809
|
+
addonId: sourceRow.meta.addonId,
|
|
66810
|
+
stableId: sourceRow.meta.stableId
|
|
66811
|
+
}, {
|
|
66812
|
+
addonId: targetRow.meta.addonId,
|
|
66813
|
+
stableId: targetRow.meta.stableId
|
|
66814
|
+
});
|
|
66815
|
+
if (replay !== null) {
|
|
66816
|
+
ctx.logger.info("migrateDevice: this swap already completed \u2014 replaying the original report, NOTHING re-run", {
|
|
66817
|
+
tags: { deviceId: sourceId },
|
|
66697
66818
|
meta: {
|
|
66698
|
-
sourceId
|
|
66699
|
-
targetId
|
|
66700
|
-
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
66701
|
-
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
66702
|
-
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
66703
|
-
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
66819
|
+
sourceId,
|
|
66820
|
+
targetId
|
|
66704
66821
|
}
|
|
66705
66822
|
});
|
|
66706
|
-
|
|
66707
|
-
|
|
66708
|
-
|
|
66709
|
-
|
|
66710
|
-
|
|
66711
|
-
|
|
66712
|
-
|
|
66713
|
-
|
|
66714
|
-
if (
|
|
66715
|
-
|
|
66716
|
-
|
|
66717
|
-
|
|
66718
|
-
|
|
66719
|
-
|
|
66823
|
+
return {
|
|
66824
|
+
result: replay,
|
|
66825
|
+
reloadTargets: null
|
|
66826
|
+
};
|
|
66827
|
+
}
|
|
66828
|
+
const tempId = await pctx.metaStore.allocateNextDeviceId();
|
|
66829
|
+
const result = await migrateDevice$1({
|
|
66830
|
+
setSwitch: async (deviceId, switchId, enabled) => {
|
|
66831
|
+
if (switchId === "stream-broker") {
|
|
66832
|
+
const moved = await writeDisabledFlagUnderLock(pctx, deviceId, !enabled);
|
|
66833
|
+
emitDisabledEvents(pctx, deviceId, !enabled, moved);
|
|
66834
|
+
return;
|
|
66835
|
+
}
|
|
66836
|
+
await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
|
|
66837
|
+
deviceId,
|
|
66838
|
+
switchId,
|
|
66839
|
+
enabled
|
|
66840
|
+
});
|
|
66841
|
+
},
|
|
66842
|
+
swapIds: async (a, b) => {
|
|
66843
|
+
await pctx.metaStore.rows.swapIds(a, b, tempId);
|
|
66844
|
+
},
|
|
66845
|
+
swapHardwareState: async (a, b) => {
|
|
66846
|
+
const swapped = await swapHardwareState(pctx.settings, a, b);
|
|
66847
|
+
pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
|
|
66848
|
+
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
66849
|
+
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
66850
|
+
tags: { deviceId: a },
|
|
66720
66851
|
meta: {
|
|
66721
|
-
|
|
66722
|
-
|
|
66852
|
+
sourceId: a,
|
|
66853
|
+
targetId: b,
|
|
66854
|
+
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
66855
|
+
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
66856
|
+
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
66857
|
+
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
66723
66858
|
}
|
|
66724
66859
|
});
|
|
66725
|
-
|
|
66860
|
+
},
|
|
66861
|
+
forgetStreamState: async (deviceId) => {
|
|
66862
|
+
await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
|
|
66863
|
+
},
|
|
66864
|
+
log: (message, meta) => {
|
|
66865
|
+
ctx.logger.info(message, {
|
|
66866
|
+
tags: { deviceId: sourceId },
|
|
66867
|
+
meta
|
|
66868
|
+
});
|
|
66726
66869
|
}
|
|
66727
|
-
},
|
|
66728
|
-
|
|
66729
|
-
|
|
66730
|
-
|
|
66731
|
-
|
|
66732
|
-
|
|
66733
|
-
|
|
66734
|
-
|
|
66735
|
-
|
|
66736
|
-
|
|
66870
|
+
}, {
|
|
66871
|
+
sourceId,
|
|
66872
|
+
targetId
|
|
66873
|
+
});
|
|
66874
|
+
pctx.migrationGuard.recordCompleted({
|
|
66875
|
+
sourceId,
|
|
66876
|
+
targetId,
|
|
66877
|
+
identityAtSource: {
|
|
66878
|
+
addonId: targetRow.meta.addonId,
|
|
66879
|
+
stableId: targetRow.meta.stableId
|
|
66880
|
+
},
|
|
66881
|
+
identityAtTarget: {
|
|
66882
|
+
addonId: sourceRow.meta.addonId,
|
|
66883
|
+
stableId: sourceRow.meta.stableId
|
|
66884
|
+
},
|
|
66885
|
+
result
|
|
66886
|
+
});
|
|
66887
|
+
return {
|
|
66888
|
+
result,
|
|
66889
|
+
reloadTargets: [{
|
|
66890
|
+
deviceId: sourceId,
|
|
66891
|
+
addonId: targetRow.meta.addonId,
|
|
66892
|
+
stableId: targetRow.meta.stableId
|
|
66893
|
+
}, {
|
|
66894
|
+
deviceId: targetId,
|
|
66895
|
+
addonId: sourceRow.meta.addonId,
|
|
66896
|
+
stableId: sourceRow.meta.stableId
|
|
66897
|
+
}]
|
|
66898
|
+
};
|
|
66737
66899
|
});
|
|
66738
|
-
|
|
66900
|
+
if (outcome.reloadTargets !== null) await reloadMigratedDevices(pctx, outcome.reloadTargets);
|
|
66901
|
+
return outcome.result;
|
|
66902
|
+
} finally {
|
|
66903
|
+
pctx.migrationGuard.end(sourceId, targetId);
|
|
66904
|
+
}
|
|
66739
66905
|
}
|
|
66740
66906
|
async function registerDevice(pctx, input) {
|
|
66741
66907
|
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
@@ -67256,18 +67422,17 @@ var require_device_manager_addon = __commonJS({
|
|
|
67256
67422
|
}
|
|
67257
67423
|
});
|
|
67258
67424
|
}
|
|
67259
|
-
async function
|
|
67260
|
-
const
|
|
67261
|
-
|
|
67262
|
-
|
|
67263
|
-
|
|
67264
|
-
|
|
67265
|
-
|
|
67266
|
-
|
|
67267
|
-
|
|
67268
|
-
|
|
67269
|
-
|
|
67270
|
-
});
|
|
67425
|
+
async function writeDisabledFlagUnderLock(pctx, deviceId, disabled) {
|
|
67426
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
67427
|
+
if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
|
|
67428
|
+
const { meta: m } = persisted;
|
|
67429
|
+
await pctx.metaStore.rows.patch(deviceId, { disabled });
|
|
67430
|
+
return {
|
|
67431
|
+
changed: (m.disabled ?? false) !== disabled,
|
|
67432
|
+
integrationId: m.integrationId ?? ""
|
|
67433
|
+
};
|
|
67434
|
+
}
|
|
67435
|
+
function emitDisabledEvents(pctx, deviceId, disabled, moved) {
|
|
67271
67436
|
pctx.host.ctx.eventBus.emit({
|
|
67272
67437
|
id: (0, node_crypto.randomUUID)(),
|
|
67273
67438
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -67297,6 +67462,10 @@ var require_device_manager_addon = __commonJS({
|
|
|
67297
67462
|
}
|
|
67298
67463
|
});
|
|
67299
67464
|
}
|
|
67465
|
+
async function setDisabled(pctx, input) {
|
|
67466
|
+
const { deviceId, disabled } = input;
|
|
67467
|
+
emitDisabledEvents(pctx, deviceId, disabled, await pctx.metaStore.withMetaWriteLock(() => writeDisabledFlagUnderLock(pctx, deviceId, disabled)));
|
|
67468
|
+
}
|
|
67300
67469
|
async function loadRuntimeState(pctx, input) {
|
|
67301
67470
|
const { deviceId } = input;
|
|
67302
67471
|
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
@@ -68677,6 +68846,42 @@ var require_device_manager_addon = __commonJS({
|
|
|
68677
68846
|
};
|
|
68678
68847
|
return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
|
|
68679
68848
|
}
|
|
68849
|
+
function sameIdentity(a, b) {
|
|
68850
|
+
return a.addonId === b.addonId && a.stableId === b.stableId;
|
|
68851
|
+
}
|
|
68852
|
+
function createMigrationGuard(now = Date.now) {
|
|
68853
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
68854
|
+
const completed = /* @__PURE__ */ new Map();
|
|
68855
|
+
const pairKey = (sourceId, targetId) => `${sourceId}->${targetId}`;
|
|
68856
|
+
return {
|
|
68857
|
+
begin(sourceId, targetId) {
|
|
68858
|
+
for (const id of [sourceId, targetId]) if (inFlight.has(id)) throw new Error(`[device-manager] migrateDevice: a migration involving device ${id} is already in flight \u2014 this call was refused, not queued. The running migration's report stands; re-read the device rows before retrying.`);
|
|
68859
|
+
inFlight.add(sourceId);
|
|
68860
|
+
inFlight.add(targetId);
|
|
68861
|
+
},
|
|
68862
|
+
end(sourceId, targetId) {
|
|
68863
|
+
inFlight.delete(sourceId);
|
|
68864
|
+
inFlight.delete(targetId);
|
|
68865
|
+
},
|
|
68866
|
+
recordCompleted(record) {
|
|
68867
|
+
completed.set(pairKey(record.sourceId, record.targetId), {
|
|
68868
|
+
...record,
|
|
68869
|
+
completedAt: now()
|
|
68870
|
+
});
|
|
68871
|
+
},
|
|
68872
|
+
matchReplay(sourceId, targetId, atSource, atTarget) {
|
|
68873
|
+
const key = pairKey(sourceId, targetId);
|
|
68874
|
+
const record = completed.get(key);
|
|
68875
|
+
if (record === void 0) return null;
|
|
68876
|
+
if (now() - record.completedAt > 9e5) {
|
|
68877
|
+
completed.delete(key);
|
|
68878
|
+
return null;
|
|
68879
|
+
}
|
|
68880
|
+
if (!sameIdentity(record.identityAtSource, atSource) || !sameIdentity(record.identityAtTarget, atTarget)) return null;
|
|
68881
|
+
return record.result;
|
|
68882
|
+
}
|
|
68883
|
+
};
|
|
68884
|
+
}
|
|
68680
68885
|
var DeviceManagerAddon = class extends require_dist10.BaseAddon {
|
|
68681
68886
|
constructor() {
|
|
68682
68887
|
super({});
|
|
@@ -68915,6 +69120,7 @@ var require_device_manager_addon = __commonJS({
|
|
|
68915
69120
|
settings,
|
|
68916
69121
|
registry,
|
|
68917
69122
|
bindingsDeps: this.bindingsDeps,
|
|
69123
|
+
migrationGuard: createMigrationGuard(),
|
|
68918
69124
|
requireDeviceOps,
|
|
68919
69125
|
stampIntegrationId: stampIntegrationId$1,
|
|
68920
69126
|
get provider() {
|
|
@@ -69156,7 +69362,7 @@ var require_hub_forwarder = __commonJS({
|
|
|
69156
69362
|
[Symbol.toStringTag]: { value: "Module" }
|
|
69157
69363
|
});
|
|
69158
69364
|
require_chunk_Cek0wNdY();
|
|
69159
|
-
var require_dist10 =
|
|
69365
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
69160
69366
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
69161
69367
|
var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
|
|
69162
69368
|
var HubForwarderDestination = class {
|
|
@@ -69293,7 +69499,7 @@ var require_liveness_monitor_addon = __commonJS({
|
|
|
69293
69499
|
"use strict";
|
|
69294
69500
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
69295
69501
|
require_chunk_Cek0wNdY();
|
|
69296
|
-
var require_dist10 =
|
|
69502
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
69297
69503
|
var NO_DEVICES = "liveness:no-devices";
|
|
69298
69504
|
var ALL_OFFLINE = "liveness:all-devices-offline";
|
|
69299
69505
|
var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
|
|
@@ -69483,7 +69689,7 @@ var require_local_auth_addon = __commonJS({
|
|
|
69483
69689
|
[Symbol.toStringTag]: { value: "Module" }
|
|
69484
69690
|
});
|
|
69485
69691
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
69486
|
-
var require_dist10 =
|
|
69692
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
69487
69693
|
var node_crypto = __require("crypto");
|
|
69488
69694
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
69489
69695
|
var crypto$1 = __require("crypto");
|
|
@@ -77296,7 +77502,7 @@ var require_loki_logging = __commonJS({
|
|
|
77296
77502
|
[Symbol.toStringTag]: { value: "Module" }
|
|
77297
77503
|
});
|
|
77298
77504
|
require_chunk_Cek0wNdY();
|
|
77299
|
-
var require_dist10 =
|
|
77505
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
77300
77506
|
function sanitizeLabelName(raw) {
|
|
77301
77507
|
const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
|
|
77302
77508
|
return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
|
|
@@ -77861,7 +78067,7 @@ var require_native_metrics_addon = __commonJS({
|
|
|
77861
78067
|
[Symbol.toStringTag]: { value: "Module" }
|
|
77862
78068
|
});
|
|
77863
78069
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
77864
|
-
var require_dist10 =
|
|
78070
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
77865
78071
|
var node_fs_promises = __require("fs/promises");
|
|
77866
78072
|
var node_child_process = __require("child_process");
|
|
77867
78073
|
var node_util = __require("util");
|
|
@@ -80445,7 +80651,7 @@ var require_filesystem_storage_addon = __commonJS({
|
|
|
80445
80651
|
[Symbol.toStringTag]: { value: "Module" }
|
|
80446
80652
|
});
|
|
80447
80653
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
80448
|
-
var require_dist10 =
|
|
80654
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
80449
80655
|
var node_crypto = __require("crypto");
|
|
80450
80656
|
var node_fs_promises = __require("fs/promises");
|
|
80451
80657
|
var node_path = __require("path");
|
|
@@ -81561,8 +81767,8 @@ var require_sqlite_settings_addon = __commonJS({
|
|
|
81561
81767
|
[Symbol.toStringTag]: { value: "Module" }
|
|
81562
81768
|
});
|
|
81563
81769
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
81564
|
-
var require_dist10 =
|
|
81565
|
-
var require_retired_settings_keys =
|
|
81770
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
81771
|
+
var require_retired_settings_keys = require_retired_settings_keys_wakiINPT();
|
|
81566
81772
|
var node_crypto = __require("crypto");
|
|
81567
81773
|
var node_fs = __require("fs");
|
|
81568
81774
|
var node_module = __require("module");
|
|
@@ -84052,7 +84258,7 @@ var require_storage_orchestrator_addon = __commonJS({
|
|
|
84052
84258
|
[Symbol.toStringTag]: { value: "Module" }
|
|
84053
84259
|
});
|
|
84054
84260
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
84055
|
-
var require_dist10 =
|
|
84261
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
84056
84262
|
var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
|
|
84057
84263
|
var zod = require_zod();
|
|
84058
84264
|
var node_crypto = __require("crypto");
|
|
@@ -87003,7 +87209,7 @@ var require_system_config_addon = __commonJS({
|
|
|
87003
87209
|
[Symbol.toStringTag]: { value: "Module" }
|
|
87004
87210
|
});
|
|
87005
87211
|
require_chunk_Cek0wNdY();
|
|
87006
|
-
var require_dist10 =
|
|
87212
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
87007
87213
|
var SECTION_TITLES = {
|
|
87008
87214
|
server: "Server",
|
|
87009
87215
|
auth: "Authentication"
|
|
@@ -105064,7 +105270,7 @@ var require_winston_logging = __commonJS({
|
|
|
105064
105270
|
[Symbol.toStringTag]: { value: "Module" }
|
|
105065
105271
|
});
|
|
105066
105272
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
105067
|
-
var require_dist10 =
|
|
105273
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
105068
105274
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
105069
105275
|
var node_path = __require("path");
|
|
105070
105276
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -107007,9 +107213,9 @@ var require_event_category_BaEgqJNv = __commonJS({
|
|
|
107007
107213
|
}
|
|
107008
107214
|
});
|
|
107009
107215
|
|
|
107010
|
-
// ../types/dist/sleep-
|
|
107011
|
-
var
|
|
107012
|
-
"../types/dist/sleep-
|
|
107216
|
+
// ../types/dist/sleep-jdpPltQH.js
|
|
107217
|
+
var require_sleep_jdpPltQH = __commonJS({
|
|
107218
|
+
"../types/dist/sleep-jdpPltQH.js"(exports) {
|
|
107013
107219
|
"use strict";
|
|
107014
107220
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
107015
107221
|
var zod = require_zod();
|
|
@@ -109933,6 +110139,7 @@ var require_sleep_BDmIj1HV = __commonJS({
|
|
|
109933
110139
|
getDeviceLiveContribution: (input) => dispatchSystem("deviceManager", "getDeviceLiveContribution", "query", input),
|
|
109934
110140
|
applyDeviceSettingsPatch: (input) => dispatchSystem("deviceManager", "applyDeviceSettingsPatch", "mutation", input)
|
|
109935
110141
|
},
|
|
110142
|
+
deviceProvider: { reloadDevice: (input) => dispatchSystem("deviceProvider", "reloadDevice", "mutation", input) },
|
|
109936
110143
|
deviceState: {
|
|
109937
110144
|
getSnapshot: (input) => dispatchSystem("deviceState", "getSnapshot", "query", input),
|
|
109938
110145
|
getCapSlice: (input) => dispatchSystem("deviceState", "getCapSlice", "query", input),
|
|
@@ -110682,7 +110889,7 @@ var require_addon = __commonJS({
|
|
|
110682
110889
|
"use strict";
|
|
110683
110890
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
110684
110891
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
110685
|
-
var require_sleep =
|
|
110892
|
+
var require_sleep = require_sleep_jdpPltQH();
|
|
110686
110893
|
var require_err_msg = require_err_msg_COpsHMw2();
|
|
110687
110894
|
var CAP_INPUT_DEFAULTS = Object.freeze({
|
|
110688
110895
|
"addons": { "getLogs": { "limit": 100 } },
|
|
@@ -117565,12 +117772,12 @@ var require_dist2 = __commonJS({
|
|
|
117565
117772
|
}
|
|
117566
117773
|
});
|
|
117567
117774
|
|
|
117568
|
-
// ../system/dist/manifest-system-deps-
|
|
117569
|
-
var
|
|
117570
|
-
"../system/dist/manifest-system-deps-
|
|
117775
|
+
// ../system/dist/manifest-system-deps-BbP419V1.js
|
|
117776
|
+
var require_manifest_system_deps_BbP419V1 = __commonJS({
|
|
117777
|
+
"../system/dist/manifest-system-deps-BbP419V1.js"(exports) {
|
|
117571
117778
|
"use strict";
|
|
117572
117779
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
117573
|
-
|
|
117780
|
+
require_dist_D8VIyu_0();
|
|
117574
117781
|
require_hub_cap_forward_DmHNjbB0();
|
|
117575
117782
|
var node_crypto = __require("crypto");
|
|
117576
117783
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
@@ -129637,7 +129844,7 @@ var require_dist3 = __commonJS({
|
|
|
129637
129844
|
"use strict";
|
|
129638
129845
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
129639
129846
|
var require_chunk = require_chunk_Cek0wNdY();
|
|
129640
|
-
var require_dist10 =
|
|
129847
|
+
var require_dist10 = require_dist_D8VIyu_0();
|
|
129641
129848
|
var require_builtins_alerts_alerts_addon = require_alerts_addon();
|
|
129642
129849
|
require_alerts();
|
|
129643
129850
|
var require_formatter = require_formatter_DqAKDlvN();
|
|
@@ -129664,7 +129871,7 @@ var require_dist3 = __commonJS({
|
|
|
129664
129871
|
var require_builtins_winston_logging_index = require_winston_logging();
|
|
129665
129872
|
var require_file_data_plane = require_file_data_plane_DO8KbxCe();
|
|
129666
129873
|
var require_tls$1 = require_tls_BxQlomxd();
|
|
129667
|
-
var require_manifest_system_deps =
|
|
129874
|
+
var require_manifest_system_deps = require_manifest_system_deps_BbP419V1();
|
|
129668
129875
|
var require_resource_monitor = require_resource_monitor_CdnzxBLP();
|
|
129669
129876
|
var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
|
|
129670
129877
|
var zod = require_zod();
|
|
@@ -210754,7 +210961,7 @@ var require_dist4 = __commonJS({
|
|
|
210754
210961
|
"use strict";
|
|
210755
210962
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
210756
210963
|
var require_event_category = require_event_category_BaEgqJNv();
|
|
210757
|
-
var require_sleep =
|
|
210964
|
+
var require_sleep = require_sleep_jdpPltQH();
|
|
210758
210965
|
var require_canonical_hash = require_canonical_hash_DNV8S5ET();
|
|
210759
210966
|
var require_enums2 = require_enums();
|
|
210760
210967
|
var require_err_msg = require_err_msg_COpsHMw2();
|
|
@@ -218062,7 +218269,26 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
218062
218269
|
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
218063
218270
|
* flows live through the cap STATUS SLICE after adoption.
|
|
218064
218271
|
*/
|
|
218065
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
218272
|
+
sourceInfo: SourceInfoSchema.optional(),
|
|
218273
|
+
/**
|
|
218274
|
+
* Set when this candidate is a device the provider ALREADY owns.
|
|
218275
|
+
*
|
|
218276
|
+
* A scan cannot generally produce the identity a device was onboarded under
|
|
218277
|
+
* (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
|
|
218278
|
+
* comparison never matches and an owned device looks addable. Re-adopting one
|
|
218279
|
+
* overwrites its config with scan-derived values — that is how a Home Hub's
|
|
218280
|
+
* Baichuan port was overwritten with its ONVIF port, taking the hub and its
|
|
218281
|
+
* three child cameras offline for four hours.
|
|
218282
|
+
*
|
|
218283
|
+
* A provider that can recognise its own devices says so here. Absent means
|
|
218284
|
+
* "not recognised", which is not the same as "known to be new" — a provider
|
|
218285
|
+
* that cannot tell simply never sets it.
|
|
218286
|
+
*/
|
|
218287
|
+
alreadyOnboarded: zod.z.boolean().optional(),
|
|
218288
|
+
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
218289
|
+
onboardedDeviceId: zod.z.number().optional(),
|
|
218290
|
+
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
218291
|
+
onboardedName: zod.z.string().optional()
|
|
218066
218292
|
});
|
|
218067
218293
|
var DeviceSummarySchema = zod.z.object({
|
|
218068
218294
|
id: zod.z.number(),
|
|
@@ -218099,6 +218325,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
218099
218325
|
name: zod.z.string(),
|
|
218100
218326
|
type: zod.z.string()
|
|
218101
218327
|
}))),
|
|
218328
|
+
/**
|
|
218329
|
+
* Tear down and reconstruct ONE device in place from its persisted rows —
|
|
218330
|
+
* touching no other device this provider owns.
|
|
218331
|
+
*
|
|
218332
|
+
* The primitive `deviceManager.migrateDevice` uses to flush the two
|
|
218333
|
+
* migrated numbers: after `swapIds` the runner's live instance still
|
|
218334
|
+
* carries the PRE-swap numeric id (baked into the object, its native-cap
|
|
218335
|
+
* registrations and its log tags), and a live object cannot be renumbered.
|
|
218336
|
+
* Before this method the only flush was restarting the whole owning addon
|
|
218337
|
+
* — which took every camera the provider owns down with it (28 devices
|
|
218338
|
+
* for one migrated camera, measured 2026-09-04, and the morning of the
|
|
218339
|
+
* same day ~27 devices' native caps did not come back on their own).
|
|
218340
|
+
*
|
|
218341
|
+
* Keyed by `stableId`, deliberately: the numeric id is exactly the thing
|
|
218342
|
+
* that changes. The reply carries the id the device answers on NOW.
|
|
218343
|
+
* Implemented once in `BaseDeviceProvider` — decommission the live
|
|
218344
|
+
* instance (if any), then re-create from the persisted row: the same
|
|
218345
|
+
* teardown/rehydrate pair every graceful shutdown + boot already uses.
|
|
218346
|
+
* An RPC, never an event: a dropped event would leave the runner writing
|
|
218347
|
+
* against the wrong camera (D8).
|
|
218348
|
+
*
|
|
218349
|
+
* Construction can dial hardware, and the migrated source is
|
|
218350
|
+
* characteristically dead — the timeout covers a full activate window
|
|
218351
|
+
* rather than the 60 s default.
|
|
218352
|
+
*/
|
|
218353
|
+
reloadDevice: require_sleep.method(zod.z.object({ stableId: zod.z.string() }), zod.z.object({ deviceId: zod.z.number() }), {
|
|
218354
|
+
kind: "mutation",
|
|
218355
|
+
timeoutMs: 3 * 6e4
|
|
218356
|
+
}),
|
|
218102
218357
|
supportsDiscovery: require_sleep.method(zod.z.object({}), zod.z.boolean()),
|
|
218103
218358
|
/**
|
|
218104
218359
|
* Run a network scan. `params` carries optional provider-specific scan
|
|
@@ -218420,13 +218675,21 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
218420
218675
|
* `sourceStillLive` names what stayed live; empty is the only value that
|
|
218421
218676
|
* means the old hardware is quiet. The migration proceeds either way —
|
|
218422
218677
|
* refusing would refuse the case this exists for.
|
|
218678
|
+
*
|
|
218679
|
+
* **Safe to retry.** A duplicate call while a migration involving either
|
|
218680
|
+
* device is in flight is REFUSED, never queued; a repeat of a swap that
|
|
218681
|
+
* already completed (matched against the `addonId`/`stableId` fingerprint
|
|
218682
|
+
* the swap left on the rows, within 15 min) is a no-op returning the
|
|
218683
|
+
* original report. A blind re-run can no longer silently undo the
|
|
218684
|
+
* migration it retried.
|
|
218423
218685
|
*/
|
|
218424
218686
|
migrateDevice: require_sleep.method(zod.z.object({
|
|
218425
218687
|
sourceId: zod.z.number(),
|
|
218426
218688
|
targetId: zod.z.number()
|
|
218427
218689
|
}), MigrateDeviceResultSchema, {
|
|
218428
218690
|
kind: "mutation",
|
|
218429
|
-
auth: "admin"
|
|
218691
|
+
auth: "admin",
|
|
218692
|
+
timeoutMs: 12 * 6e4
|
|
218430
218693
|
}),
|
|
218431
218694
|
/** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
|
|
218432
218695
|
registerDevice: require_sleep.method(DeviceRegisterPayloadSchema, zod.z.void(), { kind: "mutation" }),
|
|
@@ -239111,6 +239374,138 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239111
239374
|
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
239112
239375
|
}
|
|
239113
239376
|
};
|
|
239377
|
+
var DEVICE_RESTORE_RETRY_DELAYS_MS = [
|
|
239378
|
+
1e4,
|
|
239379
|
+
3e4,
|
|
239380
|
+
9e4
|
|
239381
|
+
];
|
|
239382
|
+
var DEVICE_RESTORE_RETRY_CONCURRENCY = 4;
|
|
239383
|
+
function sleep$1(ms, signal) {
|
|
239384
|
+
return new Promise((resolve) => {
|
|
239385
|
+
if (signal.aborted) {
|
|
239386
|
+
resolve();
|
|
239387
|
+
return;
|
|
239388
|
+
}
|
|
239389
|
+
const onAbort = () => {
|
|
239390
|
+
clearTimeout(timer);
|
|
239391
|
+
resolve();
|
|
239392
|
+
};
|
|
239393
|
+
const timer = setTimeout(() => {
|
|
239394
|
+
signal.removeEventListener("abort", onAbort);
|
|
239395
|
+
resolve();
|
|
239396
|
+
}, ms);
|
|
239397
|
+
timer.unref?.();
|
|
239398
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
239399
|
+
});
|
|
239400
|
+
}
|
|
239401
|
+
async function runWithConcurrency(items, width, fn) {
|
|
239402
|
+
const queue = [...items];
|
|
239403
|
+
const laneCount = Math.max(1, Math.min(width, queue.length));
|
|
239404
|
+
const lane = async () => {
|
|
239405
|
+
for (; ; ) {
|
|
239406
|
+
const item = queue.shift();
|
|
239407
|
+
if (item === void 0) return;
|
|
239408
|
+
await fn(item);
|
|
239409
|
+
}
|
|
239410
|
+
};
|
|
239411
|
+
await Promise.all(Array.from({ length: laneCount }, lane));
|
|
239412
|
+
}
|
|
239413
|
+
var DeviceRestoreRetryScheduler = class {
|
|
239414
|
+
#logger;
|
|
239415
|
+
#attempt;
|
|
239416
|
+
#onPermanentFailure;
|
|
239417
|
+
#delaysMs;
|
|
239418
|
+
#concurrency;
|
|
239419
|
+
#now;
|
|
239420
|
+
#abort = new AbortController();
|
|
239421
|
+
constructor(options) {
|
|
239422
|
+
this.#logger = options.logger;
|
|
239423
|
+
this.#attempt = options.attempt;
|
|
239424
|
+
this.#onPermanentFailure = options.onPermanentFailure;
|
|
239425
|
+
this.#delaysMs = options.delaysMs ?? DEVICE_RESTORE_RETRY_DELAYS_MS;
|
|
239426
|
+
this.#concurrency = options.concurrency ?? 4;
|
|
239427
|
+
this.#now = options.now ?? Date.now;
|
|
239428
|
+
}
|
|
239429
|
+
/** Stop retrying (shutdown). Pending entries are NOT marked
|
|
239430
|
+
* permanently failed — the next boot restores them from disk. */
|
|
239431
|
+
cancel() {
|
|
239432
|
+
this.#abort.abort();
|
|
239433
|
+
}
|
|
239434
|
+
/**
|
|
239435
|
+
* Run the bounded retry rounds. Resolves when every entry has either
|
|
239436
|
+
* restored, been marked permanently failed, or the scheduler was
|
|
239437
|
+
* cancelled. Never rejects.
|
|
239438
|
+
*/
|
|
239439
|
+
async run(initialFailures) {
|
|
239440
|
+
let pending = initialFailures.map((failure) => ({
|
|
239441
|
+
saved: failure.saved,
|
|
239442
|
+
lastError: failure.error,
|
|
239443
|
+
attempts: 1
|
|
239444
|
+
}));
|
|
239445
|
+
for (let round = 0; round < this.#delaysMs.length; round += 1) {
|
|
239446
|
+
if (pending.length === 0 || this.#abort.signal.aborted) break;
|
|
239447
|
+
await sleep$1(this.#delaysMs[round] ?? 0, this.#abort.signal);
|
|
239448
|
+
if (this.#abort.signal.aborted) break;
|
|
239449
|
+
pending = await this.#runRound(pending, round);
|
|
239450
|
+
}
|
|
239451
|
+
if (this.#abort.signal.aborted) return [];
|
|
239452
|
+
const terminal = pending.map((entry) => ({
|
|
239453
|
+
deviceId: entry.saved.id,
|
|
239454
|
+
stableId: entry.saved.stableId,
|
|
239455
|
+
type: String(entry.saved.type),
|
|
239456
|
+
attempts: entry.attempts,
|
|
239457
|
+
lastError: entry.lastError,
|
|
239458
|
+
failedAt: this.#now()
|
|
239459
|
+
}));
|
|
239460
|
+
for (const failure of terminal) this.#onPermanentFailure(failure);
|
|
239461
|
+
return terminal;
|
|
239462
|
+
}
|
|
239463
|
+
/** One retry round: parents first (phase 0), then hub-adopted
|
|
239464
|
+
* children (phase 1) — a child's attempt depends on its parent
|
|
239465
|
+
* having landed, exactly like the initial two-pass restore. */
|
|
239466
|
+
async #runRound(pending, round) {
|
|
239467
|
+
const next = [];
|
|
239468
|
+
const parents = pending.filter((entry) => entry.saved.parentDeviceId === null);
|
|
239469
|
+
const children = pending.filter((entry) => entry.saved.parentDeviceId !== null);
|
|
239470
|
+
for (const phase of [parents, children]) await runWithConcurrency(phase, this.#concurrency, async (entry) => {
|
|
239471
|
+
if (this.#abort.signal.aborted) {
|
|
239472
|
+
next.push(entry);
|
|
239473
|
+
return;
|
|
239474
|
+
}
|
|
239475
|
+
const attemptNo = entry.attempts + 1;
|
|
239476
|
+
try {
|
|
239477
|
+
await this.#attempt(entry.saved);
|
|
239478
|
+
this.#logger.info("Device restored on retry", {
|
|
239479
|
+
tags: {
|
|
239480
|
+
deviceId: entry.saved.id,
|
|
239481
|
+
stableId: entry.saved.stableId
|
|
239482
|
+
},
|
|
239483
|
+
meta: { attempt: attemptNo }
|
|
239484
|
+
});
|
|
239485
|
+
} catch (err) {
|
|
239486
|
+
const lastError = err instanceof Error ? err.message : String(err);
|
|
239487
|
+
const remainingRetries = this.#delaysMs.length - (round + 1);
|
|
239488
|
+
this.#logger.warn("Device restore retry failed", {
|
|
239489
|
+
tags: {
|
|
239490
|
+
deviceId: entry.saved.id,
|
|
239491
|
+
stableId: entry.saved.stableId
|
|
239492
|
+
},
|
|
239493
|
+
meta: {
|
|
239494
|
+
attempt: attemptNo,
|
|
239495
|
+
remainingRetries,
|
|
239496
|
+
error: lastError
|
|
239497
|
+
}
|
|
239498
|
+
});
|
|
239499
|
+
next.push({
|
|
239500
|
+
saved: entry.saved,
|
|
239501
|
+
lastError,
|
|
239502
|
+
attempts: attemptNo
|
|
239503
|
+
});
|
|
239504
|
+
}
|
|
239505
|
+
});
|
|
239506
|
+
return next;
|
|
239507
|
+
}
|
|
239508
|
+
};
|
|
239114
239509
|
function toDeviceSummary(device2, addonId) {
|
|
239115
239510
|
const config = {};
|
|
239116
239511
|
for (const entry of device2.config.entries()) config[entry.key] = entry.value;
|
|
@@ -239136,6 +239531,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239136
239531
|
}];
|
|
239137
239532
|
}
|
|
239138
239533
|
async onShutdown() {
|
|
239534
|
+
this.cancelRestoreRetries();
|
|
239139
239535
|
const devices = await this.ctx.kernel.devices?.getAll() ?? [];
|
|
239140
239536
|
for (const device2 of devices) try {
|
|
239141
239537
|
await this.ctx.kernel.devices?.decommission(device2.id);
|
|
@@ -239155,9 +239551,16 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239155
239551
|
async stop() {
|
|
239156
239552
|
}
|
|
239157
239553
|
async getStatus() {
|
|
239554
|
+
const all = await this.ctx.kernel.devices?.getAll() ?? [];
|
|
239555
|
+
const summary = this.restoreFailureSummary();
|
|
239556
|
+
if (summary === null) return {
|
|
239557
|
+
connected: true,
|
|
239558
|
+
deviceCount: all.length
|
|
239559
|
+
};
|
|
239158
239560
|
return {
|
|
239159
239561
|
connected: true,
|
|
239160
|
-
deviceCount:
|
|
239562
|
+
deviceCount: all.length,
|
|
239563
|
+
error: summary
|
|
239161
239564
|
};
|
|
239162
239565
|
}
|
|
239163
239566
|
async getDevices() {
|
|
@@ -239247,8 +239650,137 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239247
239650
|
};
|
|
239248
239651
|
}
|
|
239249
239652
|
async restoreDevices(savedDevices) {
|
|
239250
|
-
await this.onRestoreDevices(savedDevices);
|
|
239251
|
-
if (savedDevices.length
|
|
239653
|
+
const report = await this.onRestoreDevices(savedDevices);
|
|
239654
|
+
if (savedDevices.length === 0) return;
|
|
239655
|
+
if (report && report.failedCount > 0) {
|
|
239656
|
+
this.ctx.logger.warn(`Restored ${report.restoredCount}/${savedDevices.length} ${this.providerName} device(s) \u2014 ${report.failedCount} failed, bounded retry scheduled`);
|
|
239657
|
+
return;
|
|
239658
|
+
}
|
|
239659
|
+
const restoredCount = report ? report.restoredCount : savedDevices.length;
|
|
239660
|
+
this.ctx.logger.info(`Restored ${restoredCount} ${this.providerName} device(s)`);
|
|
239661
|
+
}
|
|
239662
|
+
/** Retry schedule. Overridable (tests use millisecond delays). */
|
|
239663
|
+
restoreRetryDelaysMs = DEVICE_RESTORE_RETRY_DELAYS_MS;
|
|
239664
|
+
/** Retry lane width. See `device-restore-retry.ts` for why retries
|
|
239665
|
+
* never re-stampede full-width while the initial pass does (D167). */
|
|
239666
|
+
restoreRetryConcurrency = 4;
|
|
239667
|
+
_restoreRetryScheduler = null;
|
|
239668
|
+
_restoreRetryCompletion = null;
|
|
239669
|
+
_permanentRestoreFailures = /* @__PURE__ */ new Map();
|
|
239670
|
+
/** Settles when the background retry rounds finish (or `null` when
|
|
239671
|
+
* nothing failed). Exposed for tests and subclass diagnostics —
|
|
239672
|
+
* boot NEVER awaits this: the runner's post-init handshake goes out
|
|
239673
|
+
* with the devices that restored, and a late success is announced
|
|
239674
|
+
* through the `native-cap-change` → `updateCaps` path. */
|
|
239675
|
+
get restoreRetryCompletion() {
|
|
239676
|
+
return this._restoreRetryCompletion;
|
|
239677
|
+
}
|
|
239678
|
+
/** Devices that exhausted the retry bound this process lifetime. */
|
|
239679
|
+
get permanentRestoreFailures() {
|
|
239680
|
+
return [...this._permanentRestoreFailures.values()];
|
|
239681
|
+
}
|
|
239682
|
+
/** One-line operator-facing summary for `getStatus().error`, or
|
|
239683
|
+
* `null` when every device restored. */
|
|
239684
|
+
restoreFailureSummary() {
|
|
239685
|
+
if (this._permanentRestoreFailures.size === 0) return null;
|
|
239686
|
+
const ids = [...this._permanentRestoreFailures.keys()].join(", ");
|
|
239687
|
+
return `${this._permanentRestoreFailures.size} device(s) permanently failed restore (deviceIds: ${ids}) \u2014 restart the ${this.providerName} provider to retry`;
|
|
239688
|
+
}
|
|
239689
|
+
cancelRestoreRetries() {
|
|
239690
|
+
this._restoreRetryScheduler?.cancel();
|
|
239691
|
+
this._restoreRetryScheduler = null;
|
|
239692
|
+
}
|
|
239693
|
+
recordPermanentRestoreFailure(failure) {
|
|
239694
|
+
this._permanentRestoreFailures.set(failure.deviceId, failure);
|
|
239695
|
+
this.ctx.logger.error("Device restore permanently failed \u2014 its capabilities will not register until the provider restarts", {
|
|
239696
|
+
tags: {
|
|
239697
|
+
deviceId: failure.deviceId,
|
|
239698
|
+
stableId: failure.stableId
|
|
239699
|
+
},
|
|
239700
|
+
meta: {
|
|
239701
|
+
type: failure.type,
|
|
239702
|
+
attempts: failure.attempts,
|
|
239703
|
+
error: failure.lastError
|
|
239704
|
+
}
|
|
239705
|
+
});
|
|
239706
|
+
}
|
|
239707
|
+
scheduleRestoreRetries(failures, attempt) {
|
|
239708
|
+
const scheduler2 = new DeviceRestoreRetryScheduler({
|
|
239709
|
+
logger: this.ctx.logger,
|
|
239710
|
+
delaysMs: this.restoreRetryDelaysMs,
|
|
239711
|
+
concurrency: this.restoreRetryConcurrency,
|
|
239712
|
+
attempt,
|
|
239713
|
+
onPermanentFailure: (failure) => this.recordPermanentRestoreFailure(failure)
|
|
239714
|
+
});
|
|
239715
|
+
this._restoreRetryScheduler = scheduler2;
|
|
239716
|
+
this._restoreRetryCompletion = scheduler2.run(failures).then(() => void 0).catch((err) => {
|
|
239717
|
+
this.ctx.logger.error("Restore retry scheduler crashed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
239718
|
+
});
|
|
239719
|
+
}
|
|
239720
|
+
/**
|
|
239721
|
+
* Tear down and reconstruct ONE device from its persisted rows — the
|
|
239722
|
+
* `deviceProvider.reloadDevice` cap method. Persistence is never touched,
|
|
239723
|
+
* and no other device this provider owns is disturbed.
|
|
239724
|
+
*
|
|
239725
|
+
* Keyed by `stableId` because the caller's whole reason to be here is that
|
|
239726
|
+
* the NUMERIC id changed (`deviceManager.migrateDevice` swapped it): the
|
|
239727
|
+
* fresh instance resolves its id through `allocateDeviceId`, which returns
|
|
239728
|
+
* whatever number the row carries NOW. The teardown is `decommission` —
|
|
239729
|
+
* exactly what a graceful shutdown runs per device (fires `removeDevice()`,
|
|
239730
|
+
* unregisters native caps, drops the registry entry) — and the rebuild is
|
|
239731
|
+
* the boot restore's own `create()` path, including its pass 2: first-class
|
|
239732
|
+
* children (hub-adopted cameras under an NVR) are decommissioned with the
|
|
239733
|
+
* parent by the cascade and must be re-created explicitly, because only
|
|
239734
|
+
* accessory children come back through `getAccessoryChildren()`.
|
|
239735
|
+
*
|
|
239736
|
+
* Reloading an accessory child directly is refused (no device class) —
|
|
239737
|
+
* reload its parent instead.
|
|
239738
|
+
*/
|
|
239739
|
+
async reloadDevice(input) {
|
|
239740
|
+
const { stableId } = input;
|
|
239741
|
+
const devices = this.ctx.kernel.devices;
|
|
239742
|
+
if (!devices) throw new Error(`${this.providerName}: kernel.devices unavailable \u2014 cannot reload`);
|
|
239743
|
+
const live = (await devices.getAll()).find((d) => d.stableId === stableId);
|
|
239744
|
+
if (live) await devices.decommission(live.id);
|
|
239745
|
+
const { id } = await this.ctx.api.deviceManager.allocateDeviceId.mutate({
|
|
239746
|
+
addonId: this.addonId,
|
|
239747
|
+
stableId
|
|
239748
|
+
});
|
|
239749
|
+
const meta = await this.ctx.api.deviceManager.loadMeta.query({ deviceId: id });
|
|
239750
|
+
if (meta === null) throw new Error(`${this.providerName}: no persisted meta for "${stableId}" (id ${id}) \u2014 cannot reload`);
|
|
239751
|
+
const deviceType = Object.values(require_sleep.DeviceType).find((t) => t === meta.type);
|
|
239752
|
+
const Class = deviceType !== void 0 ? this.deviceClasses[deviceType] : void 0;
|
|
239753
|
+
if (!Class) throw new Error(`${this.providerName}: no device class for type "${meta.type}" \u2014 "${stableId}" is an accessory child; reload its parent instead`);
|
|
239754
|
+
await devices.create(stableId, Class, {}, meta.parentDeviceId ?? null);
|
|
239755
|
+
const rows = await this.ctx.api.deviceManager.listPersistedByAddon.query({ addonId: this.addonId });
|
|
239756
|
+
for (const row of rows) {
|
|
239757
|
+
if (row.parentDeviceId !== id) continue;
|
|
239758
|
+
const childType = Object.values(require_sleep.DeviceType).find((t) => t === row.type);
|
|
239759
|
+
const ChildClass = childType !== void 0 ? this.deviceClasses[childType] : void 0;
|
|
239760
|
+
if (!ChildClass) continue;
|
|
239761
|
+
try {
|
|
239762
|
+
await devices.create(row.stableId, ChildClass, {}, id);
|
|
239763
|
+
} catch (err) {
|
|
239764
|
+
this.ctx.logger.warn("reloadDevice: failed to re-create first-class child", {
|
|
239765
|
+
tags: {
|
|
239766
|
+
deviceId: row.id,
|
|
239767
|
+
stableId: row.stableId
|
|
239768
|
+
},
|
|
239769
|
+
meta: {
|
|
239770
|
+
parentDeviceId: id,
|
|
239771
|
+
error: err instanceof Error ? err.message : String(err)
|
|
239772
|
+
}
|
|
239773
|
+
});
|
|
239774
|
+
}
|
|
239775
|
+
}
|
|
239776
|
+
this.ctx.logger.info("device reloaded in place from persisted rows", {
|
|
239777
|
+
tags: { deviceId: id },
|
|
239778
|
+
meta: {
|
|
239779
|
+
stableId,
|
|
239780
|
+
type: meta.type
|
|
239781
|
+
}
|
|
239782
|
+
});
|
|
239783
|
+
return { deviceId: id };
|
|
239252
239784
|
}
|
|
239253
239785
|
/**
|
|
239254
239786
|
* Restore devices from persisted state. Two-pass:
|
|
@@ -239274,55 +239806,126 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
239274
239806
|
* accessory-spawn flow handles via the parent's
|
|
239275
239807
|
* `getAccessoryChildren()`. Override only when the default doesn't
|
|
239276
239808
|
* fit.
|
|
239809
|
+
*
|
|
239810
|
+
* A row that fails either pass is NOT terminal (D347): it is handed
|
|
239811
|
+
* to a bounded background retry (`DeviceRestoreRetryScheduler`).
|
|
239812
|
+
* Only after the bound is exhausted is the device marked permanently
|
|
239813
|
+
* failed — logged at ERROR with `tags.deviceId` and surfaced via
|
|
239814
|
+
* `getStatus().error`.
|
|
239815
|
+
*/
|
|
239816
|
+
/**
|
|
239817
|
+
* Repair a row's PERSISTED config blob immediately before it is restored.
|
|
239818
|
+
* Default: no-op — most providers have nothing to heal.
|
|
239819
|
+
*
|
|
239820
|
+
* This exists because a restored device self-hydrates from the DB: `create()`
|
|
239821
|
+
* passes `{}` and `BaseDevice` parses the stored blob against the device
|
|
239822
|
+
* schema. A blob that lost a REQUIRED field therefore fails restore forever,
|
|
239823
|
+
* and no later pass revisits it — a hub-adopted Reolink camera whose blob had
|
|
239824
|
+
* been emptied failed all four bounded attempts against fields
|
|
239825
|
+
* (`host`, `password`) it inherits from its parent and never dials itself.
|
|
239826
|
+
*
|
|
239827
|
+
* Implementations get every saved row, so a child can read its parent's blob.
|
|
239828
|
+
* A heal that throws is treated like any other restore failure: retried under
|
|
239829
|
+
* the bound, then reported — never swallowed.
|
|
239277
239830
|
*/
|
|
239831
|
+
async healSavedConfig(_saved, _allSaved) {
|
|
239832
|
+
}
|
|
239278
239833
|
async onRestoreDevices(savedDevices) {
|
|
239279
239834
|
const restored = /* @__PURE__ */ new Set();
|
|
239835
|
+
const failures = [];
|
|
239836
|
+
const attemptRestore = async (saved) => {
|
|
239837
|
+
if (restored.has(saved.id)) return;
|
|
239838
|
+
const Class = this.deviceClasses[saved.type];
|
|
239839
|
+
if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
|
|
239840
|
+
if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
|
|
239841
|
+
await this.healSavedConfig(saved, savedDevices);
|
|
239842
|
+
await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
|
|
239843
|
+
restored.add(saved.id);
|
|
239844
|
+
};
|
|
239280
239845
|
const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
|
|
239281
239846
|
const restoreOne = async (saved) => {
|
|
239282
|
-
|
|
239283
|
-
if (!Class) {
|
|
239847
|
+
if (!this.deviceClasses[saved.type]) {
|
|
239284
239848
|
this.ctx.logger.warn("No device class registered for restored type \u2014 skipping", {
|
|
239285
|
-
tags: {
|
|
239849
|
+
tags: {
|
|
239850
|
+
deviceId: saved.id,
|
|
239851
|
+
stableId: saved.stableId
|
|
239852
|
+
},
|
|
239286
239853
|
meta: { type: saved.type }
|
|
239287
239854
|
});
|
|
239288
239855
|
return;
|
|
239289
239856
|
}
|
|
239290
239857
|
try {
|
|
239291
|
-
await
|
|
239292
|
-
restored.add(saved.id);
|
|
239858
|
+
await attemptRestore(saved);
|
|
239293
239859
|
} catch (err) {
|
|
239294
|
-
|
|
239295
|
-
|
|
239860
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
239861
|
+
this.ctx.logger.warn("Failed to restore device \u2014 bounded retry scheduled", {
|
|
239862
|
+
tags: {
|
|
239863
|
+
deviceId: saved.id,
|
|
239864
|
+
stableId: saved.stableId
|
|
239865
|
+
},
|
|
239296
239866
|
meta: {
|
|
239297
239867
|
type: saved.type,
|
|
239298
|
-
|
|
239868
|
+
attempt: 1,
|
|
239869
|
+
error
|
|
239299
239870
|
}
|
|
239300
239871
|
});
|
|
239872
|
+
failures.push({
|
|
239873
|
+
saved,
|
|
239874
|
+
error
|
|
239875
|
+
});
|
|
239301
239876
|
}
|
|
239302
239877
|
};
|
|
239303
239878
|
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
239879
|
+
const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
|
|
239304
239880
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
239305
239881
|
for (const saved of childRows) {
|
|
239306
|
-
|
|
239307
|
-
if (!Class) continue;
|
|
239882
|
+
if (!this.deviceClasses[saved.type]) continue;
|
|
239308
239883
|
if (saved.parentDeviceId === null) continue;
|
|
239309
|
-
if (
|
|
239310
|
-
|
|
239311
|
-
|
|
239312
|
-
|
|
239313
|
-
|
|
239314
|
-
|
|
239884
|
+
if (restored.has(saved.parentDeviceId)) {
|
|
239885
|
+
try {
|
|
239886
|
+
await attemptRestore(saved);
|
|
239887
|
+
} catch (err) {
|
|
239888
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
239889
|
+
this.ctx.logger.warn("Failed to restore hub-adopted child \u2014 bounded retry scheduled", {
|
|
239890
|
+
tags: {
|
|
239891
|
+
deviceId: saved.id,
|
|
239892
|
+
stableId: saved.stableId,
|
|
239893
|
+
parentDeviceId: saved.parentDeviceId
|
|
239894
|
+
},
|
|
239895
|
+
meta: {
|
|
239896
|
+
type: saved.type,
|
|
239897
|
+
attempt: 1,
|
|
239898
|
+
error
|
|
239899
|
+
}
|
|
239900
|
+
});
|
|
239901
|
+
failures.push({
|
|
239902
|
+
saved,
|
|
239903
|
+
error
|
|
239904
|
+
});
|
|
239905
|
+
}
|
|
239906
|
+
continue;
|
|
239907
|
+
}
|
|
239908
|
+
if (failedTopLevelIds.has(saved.parentDeviceId)) {
|
|
239909
|
+
this.ctx.logger.warn("Hub-adopted child deferred \u2014 parent failed initial restore", {
|
|
239315
239910
|
tags: {
|
|
239911
|
+
deviceId: saved.id,
|
|
239316
239912
|
stableId: saved.stableId,
|
|
239317
239913
|
parentDeviceId: saved.parentDeviceId
|
|
239318
239914
|
},
|
|
239319
|
-
meta: {
|
|
239320
|
-
type: saved.type,
|
|
239321
|
-
error: err instanceof Error ? err.message : String(err)
|
|
239322
|
-
}
|
|
239915
|
+
meta: { type: saved.type }
|
|
239323
239916
|
});
|
|
239917
|
+
failures.push({
|
|
239918
|
+
saved,
|
|
239919
|
+
error: `parent device ${saved.parentDeviceId} not restored`
|
|
239920
|
+
});
|
|
239921
|
+
continue;
|
|
239324
239922
|
}
|
|
239325
239923
|
}
|
|
239924
|
+
if (failures.length > 0) this.scheduleRestoreRetries(failures, attemptRestore);
|
|
239925
|
+
return {
|
|
239926
|
+
restoredCount: restored.size,
|
|
239927
|
+
failedCount: failures.length
|
|
239928
|
+
};
|
|
239326
239929
|
}
|
|
239327
239930
|
/** Convert an IDevice to the flat DeviceSummary for the cap router. */
|
|
239328
239931
|
toSummary(device2) {
|
|
@@ -243418,6 +244021,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
243418
244021
|
addonId: null,
|
|
243419
244022
|
access: "view"
|
|
243420
244023
|
},
|
|
244024
|
+
"deviceProvider.reloadDevice": {
|
|
244025
|
+
capName: "device-provider",
|
|
244026
|
+
capScope: "system",
|
|
244027
|
+
addonId: null,
|
|
244028
|
+
access: "create"
|
|
244029
|
+
},
|
|
243421
244030
|
"deviceProvider.start": {
|
|
243422
244031
|
capName: "device-provider",
|
|
243423
244032
|
capScope: "system",
|
|
@@ -253822,6 +254431,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
253822
254431
|
exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
|
|
253823
254432
|
exports.DEVICE_CHILDREN_BATCH_MAX = DEVICE_CHILDREN_BATCH_MAX;
|
|
253824
254433
|
exports.DEVICE_PROFILES = DEVICE_PROFILES;
|
|
254434
|
+
exports.DEVICE_RESTORE_RETRY_CONCURRENCY = DEVICE_RESTORE_RETRY_CONCURRENCY;
|
|
254435
|
+
exports.DEVICE_RESTORE_RETRY_DELAYS_MS = DEVICE_RESTORE_RETRY_DELAYS_MS;
|
|
253825
254436
|
exports.DEVICE_SCOPED_CAPS = require_sleep.DEVICE_SCOPED_CAPS;
|
|
253826
254437
|
exports.DEVICE_SETTINGS_CONTRIBUTION_METHODS = require_sleep.DEVICE_SETTINGS_CONTRIBUTION_METHODS;
|
|
253827
254438
|
exports.DEVICE_STATE_READERS = DEVICE_STATE_READERS;
|
|
@@ -253851,6 +254462,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
|
|
|
253851
254462
|
exports.DeviceFeature = require_sleep.DeviceFeature;
|
|
253852
254463
|
exports.DeviceInfoSchema = DeviceInfoSchema;
|
|
253853
254464
|
exports.DeviceNetworkStatsSchema = DeviceNetworkStatsSchema;
|
|
254465
|
+
exports.DeviceRestoreRetryScheduler = DeviceRestoreRetryScheduler;
|
|
253854
254466
|
exports.DeviceRole = require_sleep.DeviceRole;
|
|
253855
254467
|
exports.DeviceRuntimeState = DeviceRuntimeState;
|
|
253856
254468
|
exports.DeviceSelectorSchema = DeviceSelectorSchema;
|