@camstack/system 1.2.172 → 1.2.174

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.
Files changed (58) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +200 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +200 -2
  17. package/dist/builtins/device-manager/device-meta-actions.d.ts +13 -0
  18. package/dist/builtins/device-manager/device-row-store.d.ts +41 -0
  19. package/dist/builtins/device-manager/migrate-device.d.ts +44 -0
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  22. package/dist/builtins/hub-forwarder/index.js +1 -1
  23. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  28. package/dist/builtins/local-network/local-network.addon.js +1 -1
  29. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  30. package/dist/builtins/loki-logging/index.js +1 -1
  31. package/dist/builtins/loki-logging/index.mjs +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  34. package/dist/builtins/platform-probe/index.js +1 -1
  35. package/dist/builtins/platform-probe/index.mjs +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/snapshot/index.js +1 -1
  39. package/dist/builtins/snapshot/index.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-1DEQFmTq.mjs → dist-B25PDvaS.mjs} +85 -2
  51. package/dist/{dist-DsoVfJSR.js → dist-BzPNEPHo.js} +90 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.mjs +2 -2
  54. package/dist/{manifest-system-deps-ir_XTNpe.js → manifest-system-deps-B0mzJPmb.js} +1 -1
  55. package/dist/{manifest-system-deps-Da6sme9C.mjs → manifest-system-deps-Pw8iaY5X.mjs} +1 -1
  56. package/dist/{retired-settings-keys-DTE5k4lU.js → retired-settings-keys-DZmeJ8ja.js} +1 -1
  57. package/dist/{retired-settings-keys-ChS1Q2Kv.mjs → retired-settings-keys-tWdiQpwa.mjs} +1 -1
  58. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_system_deps = require("./manifest-system-deps-ir_XTNpe.js");
2
+ const require_manifest_system_deps = require("./manifest-system-deps-B0mzJPmb.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { $t as emitHeapDiagnosticReport, A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, j as LocalChildClient, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-Da6sme9C.mjs";
1
+ import { $t as emitHeapDiagnosticReport, A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, j as LocalChildClient, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-Pw8iaY5X.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DsoVfJSR.js");
6
+ const require_dist = require("../../dist-BzPNEPHo.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Mt as errMsg, Nt as BaseAddon, T as addonPagesCapability, an as EventCategory } from "../../dist-1DEQFmTq.mjs";
1
+ import { E as addonPagesCapability, Nt as errMsg, Pt as BaseAddon, on as EventCategory } from "../../dist-B25PDvaS.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DsoVfJSR.js");
6
+ const require_dist = require("../../dist-BzPNEPHo.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { D as addonWidgetsCapability, Mt as errMsg, Nt as BaseAddon, an as EventCategory } from "../../dist-1DEQFmTq.mjs";
1
+ import { Nt as errMsg, O as addonWidgetsCapability, Pt as BaseAddon, on as EventCategory } from "../../dist-B25PDvaS.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DsoVfJSR.js");
6
+ const require_dist = require("../../dist-BzPNEPHo.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Kt as createEvent, Mt as errMsg, Nt as BaseAddon, O as alertsCapability, an as EventCategory, n as AlertSchema } from "../../dist-1DEQFmTq.mjs";
1
+ import { Nt as errMsg, Pt as BaseAddon, k as alertsCapability, n as AlertSchema, on as EventCategory, qt as createEvent } from "../../dist-B25PDvaS.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DsoVfJSR.js");
6
+ const require_dist = require("../../dist-BzPNEPHo.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { A as backupCapability, Nt as BaseAddon, an as EventCategory } from "../../dist-1DEQFmTq.mjs";
1
+ import { Pt as BaseAddon, j as backupCapability, on as EventCategory } from "../../dist-B25PDvaS.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DsoVfJSR.js");
6
+ const require_dist = require("../../dist-BzPNEPHo.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Nt as BaseAddon, st as logDestinationCapability } from "../../dist-1DEQFmTq.mjs";
1
+ import { Pt as BaseAddon, ct as logDestinationCapability } from "../../dist-B25PDvaS.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-DsoVfJSR.js");
3
+ const require_dist = require("../../dist-BzPNEPHo.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { F as coreBlockAddonId, I as coreBlockIdFromAddonId, L as coreBlocksCapability, Mt as errMsg, Nt as BaseAddon, c as CoreBlockSchema, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, u as DeclaredDevices } from "../../dist-1DEQFmTq.mjs";
1
+ import { I as coreBlockAddonId, L as coreBlockIdFromAddonId, Nt as errMsg, Pt as BaseAddon, R as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-B25PDvaS.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DsoVfJSR.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-DTE5k4lU.js");
6
+ const require_dist = require("../../dist-BzPNEPHo.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-DZmeJ8ja.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -3271,6 +3271,75 @@ var DeviceEventPropagator = class {
3271
3271
  }
3272
3272
  };
3273
3273
  //#endregion
3274
+ //#region src/builtins/device-manager/migrate-device.ts
3275
+ /** A refusal that names a switch the camera does not offer, rather than one it
3276
+ * could not be asked about. The cap documents rejecting the former; anything
3277
+ * else is treated as the camera being out of reach, which is the safe reading:
3278
+ * it keeps the switch in `sourceStillLive` instead of quietly clearing it. */
3279
+ function classify(err) {
3280
+ const detail = err instanceof Error ? err.message : String(err);
3281
+ return {
3282
+ outcome: /does not offer|not available|unsupported/i.test(detail) ? "not-offered" : "unreachable",
3283
+ detail
3284
+ };
3285
+ }
3286
+ async function applyAll(deps, deviceId, enabled) {
3287
+ const out = [];
3288
+ for (const switchId of require_dist.CAMERA_SWITCH_ORDER) try {
3289
+ await deps.setSwitch(deviceId, switchId, enabled);
3290
+ out.push({
3291
+ deviceId,
3292
+ switchId,
3293
+ outcome: enabled ? "on" : "off"
3294
+ });
3295
+ } catch (err) {
3296
+ const { outcome, detail } = classify(err);
3297
+ out.push({
3298
+ deviceId,
3299
+ switchId,
3300
+ outcome,
3301
+ detail
3302
+ });
3303
+ deps.log("camera switch refused", {
3304
+ deviceId,
3305
+ switchId,
3306
+ enabled,
3307
+ outcome,
3308
+ detail
3309
+ });
3310
+ }
3311
+ return out;
3312
+ }
3313
+ /**
3314
+ * Put both cameras to sleep, exchange their identities, wake the target.
3315
+ *
3316
+ * Throws only if the swap itself fails — a switch that could not be written is
3317
+ * reported, not raised, because refusing to migrate a camera whose old hardware
3318
+ * is unreachable would refuse the exact case this exists for.
3319
+ */
3320
+ async function migrateDevice$1(deps, input) {
3321
+ const { sourceId, targetId } = input;
3322
+ if (sourceId === targetId) throw new Error(`[device-manager] migrateDevice: ${sourceId} is both source and target`);
3323
+ const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
3324
+ const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
3325
+ await deps.swapIds(sourceId, targetId);
3326
+ const up = await applyAll(deps, sourceId, true);
3327
+ await deps.respawnRunners([sourceId, targetId]);
3328
+ deps.log("camera migrated", {
3329
+ sourceId,
3330
+ targetId,
3331
+ sourceStillLive,
3332
+ refusals: [...down, ...up].filter((r) => r.outcome === "unreachable").length
3333
+ });
3334
+ return {
3335
+ sourceId,
3336
+ targetId,
3337
+ switches: [...down, ...up],
3338
+ sourceStillLive,
3339
+ swapped: true
3340
+ };
3341
+ }
3342
+ //#endregion
3274
3343
  //#region src/builtins/device-manager/device-meta-actions.ts
3275
3344
  /**
3276
3345
  * Device meta-mutation + persistence actions for the device-manager addon —
@@ -3344,6 +3413,54 @@ async function allocateDeviceId(pctx, input) {
3344
3413
  return { id };
3345
3414
  });
3346
3415
  }
3416
+ /**
3417
+ * Replace a camera: the new hardware inherits the number the system knows.
3418
+ *
3419
+ * The composition lives in `migrate-device.ts`, deliberately free of this
3420
+ * file's context so the decisions it carries — above all "never report a switch
3421
+ * as off when it could not be written" — are testable without a hub. This is
3422
+ * the wiring: the four real dependencies, and the write lock.
3423
+ *
3424
+ * The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
3425
+ * value a crash could park a row on was never used and will never be reused.
3426
+ * `swapIds` therefore runs INSIDE the same lock the id was allocated under.
3427
+ */
3428
+ async function migrateDevice(pctx, input) {
3429
+ const { sourceId, targetId } = input;
3430
+ const ctx = pctx.host.ctx;
3431
+ return await pctx.metaStore.withMetaWriteLock(async () => {
3432
+ const tempId = await pctx.metaStore.allocateNextDeviceId();
3433
+ return await migrateDevice$1({
3434
+ setSwitch: async (deviceId, switchId, enabled) => {
3435
+ await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
3436
+ deviceId,
3437
+ switchId,
3438
+ enabled
3439
+ });
3440
+ },
3441
+ swapIds: async (a, b) => {
3442
+ await pctx.metaStore.rows.swapIds(a, b, tempId);
3443
+ },
3444
+ respawnRunners: async (deviceIds) => {
3445
+ const owners = /* @__PURE__ */ new Set();
3446
+ for (const deviceId of deviceIds) {
3447
+ const addonId = pctx.metaStore.idToAddonId.get(deviceId);
3448
+ if (addonId !== void 0) owners.add(addonId);
3449
+ }
3450
+ for (const addonId of owners) await ctx.api.addons.restartAddon.mutate({ addonId });
3451
+ },
3452
+ log: (message, meta) => {
3453
+ ctx.logger.info(message, {
3454
+ tags: { deviceId: sourceId },
3455
+ meta
3456
+ });
3457
+ }
3458
+ }, {
3459
+ sourceId,
3460
+ targetId
3461
+ });
3462
+ });
3463
+ }
3347
3464
  async function registerDevice(pctx, input) {
3348
3465
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
3349
3466
  const featuresArr = Array.isArray(features) ? [...features] : [];
@@ -5151,6 +5268,86 @@ var DeviceRowStore = class {
5151
5268
  });
5152
5269
  }
5153
5270
  /** Drop the device's row. Idempotent. */
5271
+ /**
5272
+ * Exchange the ids of two devices, carrying their children with them.
5273
+ *
5274
+ * ## Why this exists as a primitive
5275
+ *
5276
+ * Replacing a camera means the new hardware has to become the device the rest
5277
+ * of the system already knows. Everything general about a camera is keyed on
5278
+ * the NUMERIC id and on nothing else — the recording path on disk is
5279
+ * `<deviceId>/profile/…`, the hour ledger, the media store and zone ownership
5280
+ * carry `deviceId` and not one `stableId` between them, and all four
5281
+ * ecosystem exports (Home Assistant, Alexa, HomeKit, Google) key on it too.
5282
+ * Moving that work is the expensive, lossy operation. Moving the NUMBER is
5283
+ * two rows.
5284
+ *
5285
+ * ## Children are re-pointed, not renumbered
5286
+ *
5287
+ * A child keeps its own id and follows its parent: every row whose
5288
+ * `parentDeviceId` was `a` now names `b`, and the reverse. Renumbering the
5289
+ * children as well would move THEIR general work for no reason — the
5290
+ * accessory's own recordings and rules are keyed on the child id, which is
5291
+ * not the id being inherited.
5292
+ *
5293
+ * ## There are no transactions here, so the ORDER is the safety
5294
+ *
5295
+ * The backing store is a collection API (`set` / `delete` / `updateWhere`);
5296
+ * there is no rollback to lean on. So the sequence is chosen to make every
5297
+ * crash point LOUD rather than plausible: `a` first moves to a freshly
5298
+ * allocated id, which the monotonic counter guarantees was never used and
5299
+ * will never be reused. A crash before the end therefore leaves a device
5300
+ * missing from its number and a stray at an unfamiliar one — visible in the
5301
+ * device list at a glance. What it can never leave is two rows on one number,
5302
+ * or a row that looks correct while its children point elsewhere.
5303
+ *
5304
+ * The caller holds {@link DeviceMetaStore.withMetaWriteLock}. This method does
5305
+ * not take it: taking it here would deadlock the migration that has to disable
5306
+ * functions on both devices in the same critical section.
5307
+ */
5308
+ async swapIds(a, b, tempId) {
5309
+ if (a === b) throw new Error(`[device-manager] swapIds: ${a} and ${b} are the same device`);
5310
+ await this.declare();
5311
+ const rowA = await this.get(a);
5312
+ const rowB = await this.get(b);
5313
+ if (rowA === null) throw new Error(`[device-manager] swapIds: device ${a} does not exist`);
5314
+ if (rowB === null) throw new Error(`[device-manager] swapIds: device ${b} does not exist`);
5315
+ if (await this.get(tempId) !== null) throw new Error(`[device-manager] swapIds: temp id ${tempId} is taken`);
5316
+ const childrenOfA = (await this.listByParent(a)).map((r) => r.meta.id);
5317
+ const childrenOfB = (await this.listByParent(b)).map((r) => r.meta.id);
5318
+ await this.moveRow(rowA, tempId);
5319
+ await this.moveRow(rowB, a);
5320
+ await this.moveRow({
5321
+ ...rowA,
5322
+ meta: {
5323
+ ...rowA.meta,
5324
+ id: tempId
5325
+ }
5326
+ }, b);
5327
+ for (const childId of childrenOfA) await this.patch(childId, { parentDeviceId: b });
5328
+ for (const childId of childrenOfB) await this.patch(childId, { parentDeviceId: a });
5329
+ this.logger.info("device ids swapped", {
5330
+ tags: { deviceId: a },
5331
+ meta: {
5332
+ a,
5333
+ b,
5334
+ childrenOfA,
5335
+ childrenOfB
5336
+ }
5337
+ });
5338
+ }
5339
+ /** Rewrite a row under a new id and drop the old key. Not exported: the only
5340
+ * legitimate reason to move a row is {@link swapIds}. */
5341
+ async moveRow(row, toId) {
5342
+ await this.upsert({
5343
+ ...row.meta,
5344
+ id: toId
5345
+ }, {
5346
+ registered: row.registered,
5347
+ metadata: row.metadata
5348
+ });
5349
+ await this.remove(row.meta.id);
5350
+ }
5154
5351
  async remove(deviceId) {
5155
5352
  await this.declare();
5156
5353
  await this.backend.delete({
@@ -5871,6 +6068,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
5871
6068
  resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
5872
6069
  resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),
5873
6070
  allocateDeviceId: (input) => allocateDeviceId(pctx, input),
6071
+ migrateDevice: (input) => migrateDevice(pctx, input),
5874
6072
  registerDevice: (input) => registerDevice(pctx, input),
5875
6073
  removeDevice: (input) => removeDevice(pctx, input),
5876
6074
  persistConfig: (input) => persistConfig(pctx, input),
@@ -1,5 +1,5 @@
1
- import { H as deviceStateCapability, Ht as WELL_KNOWN_TAB_MAP, K as enumerateItemArrayFields, Lt as DeviceFeature, Mt as errMsg, N as buildStreamParamsConfigSchema, Nt as BaseAddon, Rt as DeviceRole, U as deviceStatusCapability, V as deviceManagerCapability, Zt as isDeviceConfigCap, a as CAP_NAMES_WITH_STATUS, an as EventCategory, d as DeviceStatusSchema, dt as normalizeUnit, in as sleep, pt as parseStreamParamsFormPatch, q as enumerateSchemaFields, t as ALL_CAPABILITY_DEFINITIONS, y as STREAM_PROFILE_META, yt as runtimeStatePolicyFor, zt as DeviceType } from "../../dist-1DEQFmTq.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-ChS1Q2Kv.mjs";
1
+ import { Bt as DeviceType, H as deviceManagerCapability, J as enumerateSchemaFields, Nt as errMsg, P as buildStreamParamsConfigSchema, Pt as BaseAddon, Qt as isDeviceConfigCap, Rt as DeviceFeature, U as deviceStateCapability, Ut as WELL_KNOWN_TAB_MAP, W as deviceStatusCapability, a as CAMERA_SWITCH_ORDER, an as sleep, b as STREAM_PROFILE_META, bt as runtimeStatePolicyFor, f as DeviceStatusSchema, ft as normalizeUnit, mt as parseStreamParamsFormPatch, o as CAP_NAMES_WITH_STATUS, on as EventCategory, q as enumerateItemArrayFields, t as ALL_CAPABILITY_DEFINITIONS, zt as DeviceRole } from "../../dist-B25PDvaS.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-tWdiQpwa.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -3266,6 +3266,75 @@ var DeviceEventPropagator = class {
3266
3266
  }
3267
3267
  };
3268
3268
  //#endregion
3269
+ //#region src/builtins/device-manager/migrate-device.ts
3270
+ /** A refusal that names a switch the camera does not offer, rather than one it
3271
+ * could not be asked about. The cap documents rejecting the former; anything
3272
+ * else is treated as the camera being out of reach, which is the safe reading:
3273
+ * it keeps the switch in `sourceStillLive` instead of quietly clearing it. */
3274
+ function classify(err) {
3275
+ const detail = err instanceof Error ? err.message : String(err);
3276
+ return {
3277
+ outcome: /does not offer|not available|unsupported/i.test(detail) ? "not-offered" : "unreachable",
3278
+ detail
3279
+ };
3280
+ }
3281
+ async function applyAll(deps, deviceId, enabled) {
3282
+ const out = [];
3283
+ for (const switchId of CAMERA_SWITCH_ORDER) try {
3284
+ await deps.setSwitch(deviceId, switchId, enabled);
3285
+ out.push({
3286
+ deviceId,
3287
+ switchId,
3288
+ outcome: enabled ? "on" : "off"
3289
+ });
3290
+ } catch (err) {
3291
+ const { outcome, detail } = classify(err);
3292
+ out.push({
3293
+ deviceId,
3294
+ switchId,
3295
+ outcome,
3296
+ detail
3297
+ });
3298
+ deps.log("camera switch refused", {
3299
+ deviceId,
3300
+ switchId,
3301
+ enabled,
3302
+ outcome,
3303
+ detail
3304
+ });
3305
+ }
3306
+ return out;
3307
+ }
3308
+ /**
3309
+ * Put both cameras to sleep, exchange their identities, wake the target.
3310
+ *
3311
+ * Throws only if the swap itself fails — a switch that could not be written is
3312
+ * reported, not raised, because refusing to migrate a camera whose old hardware
3313
+ * is unreachable would refuse the exact case this exists for.
3314
+ */
3315
+ async function migrateDevice$1(deps, input) {
3316
+ const { sourceId, targetId } = input;
3317
+ if (sourceId === targetId) throw new Error(`[device-manager] migrateDevice: ${sourceId} is both source and target`);
3318
+ const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
3319
+ const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
3320
+ await deps.swapIds(sourceId, targetId);
3321
+ const up = await applyAll(deps, sourceId, true);
3322
+ await deps.respawnRunners([sourceId, targetId]);
3323
+ deps.log("camera migrated", {
3324
+ sourceId,
3325
+ targetId,
3326
+ sourceStillLive,
3327
+ refusals: [...down, ...up].filter((r) => r.outcome === "unreachable").length
3328
+ });
3329
+ return {
3330
+ sourceId,
3331
+ targetId,
3332
+ switches: [...down, ...up],
3333
+ sourceStillLive,
3334
+ swapped: true
3335
+ };
3336
+ }
3337
+ //#endregion
3269
3338
  //#region src/builtins/device-manager/device-meta-actions.ts
3270
3339
  /**
3271
3340
  * Device meta-mutation + persistence actions for the device-manager addon —
@@ -3339,6 +3408,54 @@ async function allocateDeviceId(pctx, input) {
3339
3408
  return { id };
3340
3409
  });
3341
3410
  }
3411
+ /**
3412
+ * Replace a camera: the new hardware inherits the number the system knows.
3413
+ *
3414
+ * The composition lives in `migrate-device.ts`, deliberately free of this
3415
+ * file's context so the decisions it carries — above all "never report a switch
3416
+ * as off when it could not be written" — are testable without a hub. This is
3417
+ * the wiring: the four real dependencies, and the write lock.
3418
+ *
3419
+ * The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
3420
+ * value a crash could park a row on was never used and will never be reused.
3421
+ * `swapIds` therefore runs INSIDE the same lock the id was allocated under.
3422
+ */
3423
+ async function migrateDevice(pctx, input) {
3424
+ const { sourceId, targetId } = input;
3425
+ const ctx = pctx.host.ctx;
3426
+ return await pctx.metaStore.withMetaWriteLock(async () => {
3427
+ const tempId = await pctx.metaStore.allocateNextDeviceId();
3428
+ return await migrateDevice$1({
3429
+ setSwitch: async (deviceId, switchId, enabled) => {
3430
+ await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
3431
+ deviceId,
3432
+ switchId,
3433
+ enabled
3434
+ });
3435
+ },
3436
+ swapIds: async (a, b) => {
3437
+ await pctx.metaStore.rows.swapIds(a, b, tempId);
3438
+ },
3439
+ respawnRunners: async (deviceIds) => {
3440
+ const owners = /* @__PURE__ */ new Set();
3441
+ for (const deviceId of deviceIds) {
3442
+ const addonId = pctx.metaStore.idToAddonId.get(deviceId);
3443
+ if (addonId !== void 0) owners.add(addonId);
3444
+ }
3445
+ for (const addonId of owners) await ctx.api.addons.restartAddon.mutate({ addonId });
3446
+ },
3447
+ log: (message, meta) => {
3448
+ ctx.logger.info(message, {
3449
+ tags: { deviceId: sourceId },
3450
+ meta
3451
+ });
3452
+ }
3453
+ }, {
3454
+ sourceId,
3455
+ targetId
3456
+ });
3457
+ });
3458
+ }
3342
3459
  async function registerDevice(pctx, input) {
3343
3460
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
3344
3461
  const featuresArr = Array.isArray(features) ? [...features] : [];
@@ -5146,6 +5263,86 @@ var DeviceRowStore = class {
5146
5263
  });
5147
5264
  }
5148
5265
  /** Drop the device's row. Idempotent. */
5266
+ /**
5267
+ * Exchange the ids of two devices, carrying their children with them.
5268
+ *
5269
+ * ## Why this exists as a primitive
5270
+ *
5271
+ * Replacing a camera means the new hardware has to become the device the rest
5272
+ * of the system already knows. Everything general about a camera is keyed on
5273
+ * the NUMERIC id and on nothing else — the recording path on disk is
5274
+ * `<deviceId>/profile/…`, the hour ledger, the media store and zone ownership
5275
+ * carry `deviceId` and not one `stableId` between them, and all four
5276
+ * ecosystem exports (Home Assistant, Alexa, HomeKit, Google) key on it too.
5277
+ * Moving that work is the expensive, lossy operation. Moving the NUMBER is
5278
+ * two rows.
5279
+ *
5280
+ * ## Children are re-pointed, not renumbered
5281
+ *
5282
+ * A child keeps its own id and follows its parent: every row whose
5283
+ * `parentDeviceId` was `a` now names `b`, and the reverse. Renumbering the
5284
+ * children as well would move THEIR general work for no reason — the
5285
+ * accessory's own recordings and rules are keyed on the child id, which is
5286
+ * not the id being inherited.
5287
+ *
5288
+ * ## There are no transactions here, so the ORDER is the safety
5289
+ *
5290
+ * The backing store is a collection API (`set` / `delete` / `updateWhere`);
5291
+ * there is no rollback to lean on. So the sequence is chosen to make every
5292
+ * crash point LOUD rather than plausible: `a` first moves to a freshly
5293
+ * allocated id, which the monotonic counter guarantees was never used and
5294
+ * will never be reused. A crash before the end therefore leaves a device
5295
+ * missing from its number and a stray at an unfamiliar one — visible in the
5296
+ * device list at a glance. What it can never leave is two rows on one number,
5297
+ * or a row that looks correct while its children point elsewhere.
5298
+ *
5299
+ * The caller holds {@link DeviceMetaStore.withMetaWriteLock}. This method does
5300
+ * not take it: taking it here would deadlock the migration that has to disable
5301
+ * functions on both devices in the same critical section.
5302
+ */
5303
+ async swapIds(a, b, tempId) {
5304
+ if (a === b) throw new Error(`[device-manager] swapIds: ${a} and ${b} are the same device`);
5305
+ await this.declare();
5306
+ const rowA = await this.get(a);
5307
+ const rowB = await this.get(b);
5308
+ if (rowA === null) throw new Error(`[device-manager] swapIds: device ${a} does not exist`);
5309
+ if (rowB === null) throw new Error(`[device-manager] swapIds: device ${b} does not exist`);
5310
+ if (await this.get(tempId) !== null) throw new Error(`[device-manager] swapIds: temp id ${tempId} is taken`);
5311
+ const childrenOfA = (await this.listByParent(a)).map((r) => r.meta.id);
5312
+ const childrenOfB = (await this.listByParent(b)).map((r) => r.meta.id);
5313
+ await this.moveRow(rowA, tempId);
5314
+ await this.moveRow(rowB, a);
5315
+ await this.moveRow({
5316
+ ...rowA,
5317
+ meta: {
5318
+ ...rowA.meta,
5319
+ id: tempId
5320
+ }
5321
+ }, b);
5322
+ for (const childId of childrenOfA) await this.patch(childId, { parentDeviceId: b });
5323
+ for (const childId of childrenOfB) await this.patch(childId, { parentDeviceId: a });
5324
+ this.logger.info("device ids swapped", {
5325
+ tags: { deviceId: a },
5326
+ meta: {
5327
+ a,
5328
+ b,
5329
+ childrenOfA,
5330
+ childrenOfB
5331
+ }
5332
+ });
5333
+ }
5334
+ /** Rewrite a row under a new id and drop the old key. Not exported: the only
5335
+ * legitimate reason to move a row is {@link swapIds}. */
5336
+ async moveRow(row, toId) {
5337
+ await this.upsert({
5338
+ ...row.meta,
5339
+ id: toId
5340
+ }, {
5341
+ registered: row.registered,
5342
+ metadata: row.metadata
5343
+ });
5344
+ await this.remove(row.meta.id);
5345
+ }
5149
5346
  async remove(deviceId) {
5150
5347
  await this.declare();
5151
5348
  await this.backend.delete({
@@ -5866,6 +6063,7 @@ var DeviceManagerAddon = class extends BaseAddon {
5866
6063
  resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
5867
6064
  resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),
5868
6065
  allocateDeviceId: (input) => allocateDeviceId(pctx, input),
6066
+ migrateDevice: (input) => migrateDevice(pctx, input),
5869
6067
  registerDevice: (input) => registerDevice(pctx, input),
5870
6068
  removeDevice: (input) => removeDevice(pctx, input),
5871
6069
  persistConfig: (input) => persistConfig(pctx, input),
@@ -13,6 +13,19 @@ type IDeviceManagerProvider = InferProvider<typeof deviceManagerCapability>;
13
13
  */
14
14
  export declare function stampIntegrationId(metaStore: DeviceMetaStore, ctx: AddonContext, deviceId: number, integrationId: string): Promise<void>;
15
15
  export declare function allocateDeviceId(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['allocateDeviceId']>[0]): ReturnType<IDeviceManagerProvider['allocateDeviceId']>;
16
+ /**
17
+ * Replace a camera: the new hardware inherits the number the system knows.
18
+ *
19
+ * The composition lives in `migrate-device.ts`, deliberately free of this
20
+ * file's context so the decisions it carries — above all "never report a switch
21
+ * as off when it could not be written" — are testable without a hub. This is
22
+ * the wiring: the four real dependencies, and the write lock.
23
+ *
24
+ * The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
25
+ * value a crash could park a row on was never used and will never be reused.
26
+ * `swapIds` therefore runs INSIDE the same lock the id was allocated under.
27
+ */
28
+ export declare function migrateDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['migrateDevice']>[0]): ReturnType<IDeviceManagerProvider['migrateDevice']>;
16
29
  export declare function registerDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['registerDevice']>[0]): ReturnType<IDeviceManagerProvider['registerDevice']>;
17
30
  export declare function removeDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['removeDevice']>[0]): ReturnType<IDeviceManagerProvider['removeDevice']>;
18
31
  export declare function persistConfig(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['persistConfig']>[0]): ReturnType<IDeviceManagerProvider['persistConfig']>;
@@ -340,6 +340,47 @@ export declare class DeviceRowStore {
340
340
  */
341
341
  upsertRegistration(row: DeviceRegistrationRow): Promise<void>;
342
342
  /** Drop the device's row. Idempotent. */
343
+ /**
344
+ * Exchange the ids of two devices, carrying their children with them.
345
+ *
346
+ * ## Why this exists as a primitive
347
+ *
348
+ * Replacing a camera means the new hardware has to become the device the rest
349
+ * of the system already knows. Everything general about a camera is keyed on
350
+ * the NUMERIC id and on nothing else — the recording path on disk is
351
+ * `<deviceId>/profile/…`, the hour ledger, the media store and zone ownership
352
+ * carry `deviceId` and not one `stableId` between them, and all four
353
+ * ecosystem exports (Home Assistant, Alexa, HomeKit, Google) key on it too.
354
+ * Moving that work is the expensive, lossy operation. Moving the NUMBER is
355
+ * two rows.
356
+ *
357
+ * ## Children are re-pointed, not renumbered
358
+ *
359
+ * A child keeps its own id and follows its parent: every row whose
360
+ * `parentDeviceId` was `a` now names `b`, and the reverse. Renumbering the
361
+ * children as well would move THEIR general work for no reason — the
362
+ * accessory's own recordings and rules are keyed on the child id, which is
363
+ * not the id being inherited.
364
+ *
365
+ * ## There are no transactions here, so the ORDER is the safety
366
+ *
367
+ * The backing store is a collection API (`set` / `delete` / `updateWhere`);
368
+ * there is no rollback to lean on. So the sequence is chosen to make every
369
+ * crash point LOUD rather than plausible: `a` first moves to a freshly
370
+ * allocated id, which the monotonic counter guarantees was never used and
371
+ * will never be reused. A crash before the end therefore leaves a device
372
+ * missing from its number and a stray at an unfamiliar one — visible in the
373
+ * device list at a glance. What it can never leave is two rows on one number,
374
+ * or a row that looks correct while its children point elsewhere.
375
+ *
376
+ * The caller holds {@link DeviceMetaStore.withMetaWriteLock}. This method does
377
+ * not take it: taking it here would deadlock the migration that has to disable
378
+ * functions on both devices in the same critical section.
379
+ */
380
+ swapIds(a: number, b: number, tempId: number): Promise<void>;
381
+ /** Rewrite a row under a new id and drop the old key. Not exported: the only
382
+ * legitimate reason to move a row is {@link swapIds}. */
383
+ private moveRow;
343
384
  remove(deviceId: number): Promise<void>;
344
385
  /**
345
386
  * The retirement door for the three blobs this collection replaced.