@camstack/system 1.1.53 → 1.1.55

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 (54) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  3. package/dist/addon-utils.d.ts +1 -0
  4. package/dist/addon-utils.js +2 -0
  5. package/dist/addon-utils.mjs +2 -2
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/console-logging/index.js +1 -1
  15. package/dist/builtins/console-logging/index.mjs +1 -1
  16. package/dist/builtins/device-manager/device-manager.addon.js +46 -37
  17. package/dist/builtins/device-manager/device-manager.addon.mjs +46 -37
  18. package/dist/builtins/device-manager/device-store-sections.d.ts +26 -0
  19. package/dist/builtins/doorbell/doorbell-settings.d.ts +58 -0
  20. package/dist/builtins/doorbell/trigger-engine.d.ts +32 -30
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +39 -22
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -191
  23. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -191
  24. package/dist/builtins/hub-forwarder/index.js +1 -1
  25. package/dist/builtins/hub-forwarder/index.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/native-metrics/native-metrics.addon.js +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  32. package/dist/builtins/platform-probe/index.js +1 -1
  33. package/dist/builtins/platform-probe/index.mjs +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  36. package/dist/builtins/snapshot/index.js +1 -1
  37. package/dist/builtins/snapshot/index.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/system-config/system-config.addon.js +1 -1
  45. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  46. package/dist/builtins/winston-logging/index.js +1 -1
  47. package/dist/builtins/winston-logging/index.mjs +1 -1
  48. package/dist/{dist-BNro5CRu.js → dist-CD5HJkAg.js} +384 -9
  49. package/dist/{dist-D7_69kDt.mjs → dist-RlzjLezj.mjs} +384 -9
  50. package/dist/index.js +2 -2
  51. package/dist/index.mjs +2 -2
  52. package/dist/{manifest-python-deps-EM2H-jjS.mjs → manifest-python-deps-BMKlyabv.mjs} +1 -1
  53. package/dist/{manifest-python-deps-DszE7bYn.js → manifest-python-deps-BpDOYWC2.js} +1 -1
  54. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_dist = require("./dist-BNro5CRu.js");
3
- const require_manifest_python_deps = require("./manifest-python-deps-DszE7bYn.js");
2
+ const require_dist = require("./dist-CD5HJkAg.js");
3
+ const require_manifest_python_deps = require("./manifest-python-deps-BpDOYWC2.js");
4
4
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
5
5
  let node_fs = require("node:fs");
6
6
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,5 +1,5 @@
1
- import { j as isCollectionArrayMethod, t as ALL_CAPABILITY_DEFINITIONS } from "./dist-D7_69kDt.mjs";
2
- import { F as createUdsLoggerWithControl, I as LocalChildClient, bt as resolveAddonClass, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps } from "./manifest-python-deps-EM2H-jjS.mjs";
1
+ import { j as isCollectionArrayMethod, t as ALL_CAPABILITY_DEFINITIONS } from "./dist-RlzjLezj.mjs";
2
+ import { F as createUdsLoggerWithControl, I as LocalChildClient, bt as resolveAddonClass, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps } from "./manifest-python-deps-BMKlyabv.mjs";
3
3
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
4
4
  import { register } from "node:module";
5
5
  import * as fs from "node:fs";
@@ -17,4 +17,5 @@
17
17
  export { downloadFile, ensureModel, isModelDownloaded, deleteModelFromDisk, collectModelFiles, getModelFilePath, } from './download/model-downloader.js';
18
18
  export { ModelDownloadService } from './download/model-download-service.js';
19
19
  export { createFileDataPlaneHandler } from './http/file-data-plane.js';
20
+ export { parseRangeHeader, contentTypeFor, type ByteRange, } from './http/authenticated-file-server.js';
20
21
  export { CustomActionRegistry } from './kernel/custom-action-registry.js';
@@ -5,9 +5,11 @@ const require_custom_action_registry = require("./custom-action-registry-vLYEFTt
5
5
  exports.CustomActionRegistry = require_custom_action_registry.CustomActionRegistry;
6
6
  exports.ModelDownloadService = require_model_download_service.ModelDownloadService;
7
7
  exports.collectModelFiles = require_model_download_service.collectModelFiles;
8
+ exports.contentTypeFor = require_model_download_service.contentTypeFor;
8
9
  exports.createFileDataPlaneHandler = require_model_download_service.createFileDataPlaneHandler;
9
10
  exports.deleteModelFromDisk = require_model_download_service.deleteModelFromDisk;
10
11
  exports.downloadFile = require_model_download_service.downloadFile;
11
12
  exports.ensureModel = require_model_download_service.ensureModel;
12
13
  exports.getModelFilePath = require_model_download_service.getModelFilePath;
13
14
  exports.isModelDownloaded = require_model_download_service.isModelDownloaded;
15
+ exports.parseRangeHeader = require_model_download_service.parseRangeHeader;
@@ -1,3 +1,3 @@
1
- import { c as getModelFilePath, i as downloadFile, l as isModelDownloaded, n as collectModelFiles, o as ensureModel, r as deleteModelFromDisk, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-Cp9f4dk6.mjs";
1
+ import { c as getModelFilePath, d as contentTypeFor, i as downloadFile, l as isModelDownloaded, n as collectModelFiles, o as ensureModel, p as parseRangeHeader, r as deleteModelFromDisk, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-Cp9f4dk6.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
- export { CustomActionRegistry, ModelDownloadService, collectModelFiles, createFileDataPlaneHandler, deleteModelFromDisk, downloadFile, ensureModel, getModelFilePath, isModelDownloaded };
3
+ export { CustomActionRegistry, ModelDownloadService, collectModelFiles, contentTypeFor, createFileDataPlaneHandler, deleteModelFromDisk, downloadFile, ensureModel, getModelFilePath, isModelDownloaded, parseRangeHeader };
@@ -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-BNro5CRu.js");
6
+ const require_dist = require("../../dist-CD5HJkAg.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { f as addonPagesCapability, it as BaseAddon, kt as EventCategory, rt as errMsg } from "../../dist-D7_69kDt.mjs";
1
+ import { f as addonPagesCapability, it as BaseAddon, kt as EventCategory, rt as errMsg } from "../../dist-RlzjLezj.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
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-BNro5CRu.js");
6
+ const require_dist = require("../../dist-CD5HJkAg.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { it as BaseAddon, kt as EventCategory, m as addonWidgetsCapability, rt as errMsg } from "../../dist-D7_69kDt.mjs";
1
+ import { it as BaseAddon, kt as EventCategory, m as addonWidgetsCapability, rt as errMsg } from "../../dist-RlzjLezj.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
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-BNro5CRu.js");
6
+ const require_dist = require("../../dist-CD5HJkAg.js");
7
7
  //#region src/builtins/alerts/alerts.addon.ts
8
8
  var CATEGORY_MAPPINGS = {
9
9
  "addon.crashed": {
@@ -1,4 +1,4 @@
1
- import { _t as createEvent, h as alertsCapability, it as BaseAddon, kt as EventCategory, n as AlertSchema, rt as errMsg } from "../../dist-D7_69kDt.mjs";
1
+ import { _t as createEvent, h as alertsCapability, it as BaseAddon, kt as EventCategory, n as AlertSchema, rt as errMsg } from "../../dist-RlzjLezj.mjs";
2
2
  //#region src/builtins/alerts/alerts.addon.ts
3
3
  var CATEGORY_MAPPINGS = {
4
4
  "addon.crashed": {
@@ -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-BNro5CRu.js");
6
+ const require_dist = require("../../dist-CD5HJkAg.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_fs$1 = require_chunk.__toESM(node_fs, 1);
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { it as BaseAddon, kt as EventCategory, v as backupCapability } from "../../dist-D7_69kDt.mjs";
1
+ import { it as BaseAddon, kt as EventCategory, v as backupCapability } from "../../dist-RlzjLezj.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import Kt from "node:fs";
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-BNro5CRu.js");
6
+ const require_dist = require("../../dist-CD5HJkAg.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 { L as logDestinationCapability, it as BaseAddon } from "../../dist-D7_69kDt.mjs";
1
+ import { L as logDestinationCapability, it as BaseAddon } from "../../dist-RlzjLezj.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 = {
@@ -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-BNro5CRu.js");
6
+ const require_dist = require("../../dist-CD5HJkAg.js");
7
7
  let _camstack_types_node = require("@camstack/types/node");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1333,6 +1333,39 @@ async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
1333
1333
  return { success: true };
1334
1334
  }
1335
1335
  //#endregion
1336
+ //#region src/builtins/device-manager/device-store-sections.ts
1337
+ /** The device-manager's device-store-backed sections. Add an entry to extend. */
1338
+ var DEVICE_STORE_SECTIONS = [{
1339
+ keys: [LINKED_MODE_CONFIG_KEY, LINKED_IDS_CONFIG_KEY],
1340
+ apply: (pctx, deviceId, patch) => applyLinkedDevicesPatch(pctx, deviceId, patch)
1341
+ }];
1342
+ /**
1343
+ * Split a `writerCapName: 'device-manager'` patch into device-store slices
1344
+ * (one per matching registered section) and the leftover driver patch.
1345
+ * Generic — consults {@link DEVICE_STORE_SECTIONS}, no hardcoded key list.
1346
+ */
1347
+ function splitDeviceStoreKeys(patch) {
1348
+ const claimed = /* @__PURE__ */ new Set();
1349
+ const storeGroups = [];
1350
+ for (const section of DEVICE_STORE_SECTIONS) {
1351
+ const slice = {};
1352
+ for (const key of section.keys) if (key in patch) {
1353
+ slice[key] = patch[key];
1354
+ claimed.add(key);
1355
+ }
1356
+ if (Object.keys(slice).length > 0) storeGroups.push({
1357
+ section,
1358
+ patch: slice
1359
+ });
1360
+ }
1361
+ const driverPatch = {};
1362
+ for (const [key, value] of Object.entries(patch)) if (!claimed.has(key)) driverPatch[key] = value;
1363
+ return {
1364
+ storeGroups,
1365
+ driverPatch
1366
+ };
1367
+ }
1368
+ //#endregion
1336
1369
  //#region src/builtins/device-manager/device-bindings-store.ts
1337
1370
  /**
1338
1371
  * Persisted wrapper-activation + per-device binding resolution for the
@@ -2031,40 +2064,9 @@ async function dispatchDerivedFormSave(builderId, deviceId, patch, dcProvider, c
2031
2064
  throw new Error(`[device-manager] device-config cap "${capName}" provider exposes neither setSettings nor setProfile`);
2032
2065
  }
2033
2066
  async function updateDeviceField(deps, input) {
2034
- if (input.writerCapName === "device-manager") {
2035
- if (input.key === "_linkedDevicesMode" || input.key === "_linkedDeviceIds") {
2036
- const settings = deps.ctx.settings;
2037
- if (!settings) throw new Error("[device-manager] linked-devices save requires the settings surface");
2038
- await applyLinkedDevicesPatch({ settings }, input.deviceId, { [input.key]: input.value });
2039
- return { success: true };
2040
- }
2041
- const hubRegistry = deps.ctx.kernel?.deviceRegistry;
2042
- const found = hubRegistry ? resolveDeviceById(hubRegistry, input.deviceId) : null;
2043
- if (found) {
2044
- await found.device.applySettingsPatch({ [input.key]: input.value });
2045
- return { success: true };
2046
- }
2047
- const ops = deps.capabilityRegistry?.getNativeProvider("device-ops", input.deviceId);
2048
- if (!ops) throw new Error(`[device-manager] device "${input.deviceId}" not found (no hub-local entry, no device-ops native provider)`);
2049
- await ops.setConfig({
2050
- deviceId: input.deviceId,
2051
- values: { [input.key]: input.value }
2052
- });
2053
- return { success: true };
2054
- }
2055
- const registry = deps.capabilityRegistry;
2056
- if (!registry) throw new Error("[device-manager] updateDeviceField requires capability registry — unavailable on this node");
2057
- const def = registry.getDefinition(input.writerCapName);
2058
- if (require_dist.isDeviceConfigCap(def)) {
2059
- if (def.deviceConfig.ui.kind === "widget") return { success: true };
2060
- const dcProvider = registry.getProviderForDevice(input.writerCapName, input.deviceId);
2061
- if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${input.writerCapName}" on device ${input.deviceId}`);
2062
- await dispatchDerivedFormSave(def.deviceConfig.ui.builderId, input.deviceId, { [input.key]: input.value }, dcProvider, input.writerCapName);
2063
- return { success: true };
2064
- }
2065
- if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
2066
- await resolveContributionProvider(registry, def, input.writerCapName, input.writerAddonId, input.deviceId).applyDeviceSettingsPatch({
2067
- deviceId: input.deviceId,
2067
+ await applyGroupPatch(deps, input.deviceId, {
2068
+ writerCapName: input.writerCapName,
2069
+ writerAddonId: input.writerAddonId,
2068
2070
  patch: { [input.key]: input.value }
2069
2071
  });
2070
2072
  return { success: true };
@@ -2157,17 +2159,24 @@ async function updateDeviceFieldsBatch(deps, input) {
2157
2159
  * registry lookup). Used by `updateDeviceFieldsBatch`. */
2158
2160
  async function applyGroupPatch(deps, deviceId, group) {
2159
2161
  if (group.writerCapName === "device-manager") {
2162
+ const { storeGroups, driverPatch } = splitDeviceStoreKeys(group.patch);
2163
+ if (storeGroups.length > 0) {
2164
+ const settings = deps.ctx.settings;
2165
+ if (!settings) throw new Error("[device-manager] device-store save requires the settings surface");
2166
+ for (const { section, patch } of storeGroups) await section.apply({ settings }, deviceId, patch);
2167
+ }
2168
+ if (Object.keys(driverPatch).length === 0) return;
2160
2169
  const hubRegistry = deps.ctx.kernel?.deviceRegistry;
2161
2170
  const found = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
2162
2171
  if (found) {
2163
- await found.device.applySettingsPatch(group.patch);
2172
+ await found.device.applySettingsPatch(driverPatch);
2164
2173
  return;
2165
2174
  }
2166
2175
  const ops = deps.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
2167
2176
  if (!ops) throw new Error(`[device-manager] device "${deviceId}" not found (no hub-local entry, no device-ops native provider)`);
2168
2177
  await ops.setConfig({
2169
2178
  deviceId,
2170
- values: group.patch
2179
+ values: driverPatch
2171
2180
  });
2172
2181
  return;
2173
2182
  }
@@ -1,4 +1,4 @@
1
- import { E as evaluateLinkExpression, J as setByPath, Ot as sleep, S as deviceStatusCapability, St as isDeviceConfigCap, T as enumerateSchemaFields, U as parseStreamParamsFormPatch, V as normalizeUnit, a as DeviceStatusSchema, b as deviceManagerCapability, c as STREAM_PROFILE_META, ct as DeviceFeature, et as toExpressionValue, g as applyTransform, i as CAP_NAMES_WITH_STATUS, it as BaseAddon, k as getByPath, kt as EventCategory, lt as DeviceRole, nt as validateExpressionSource, pt as WELL_KNOWN_TAB_MAP, rt as errMsg, t as ALL_CAPABILITY_DEFINITIONS, ut as DeviceType, w as enumerateItemArrayFields, x as deviceStateCapability, y as buildStreamParamsConfigSchema } from "../../dist-D7_69kDt.mjs";
1
+ import { E as evaluateLinkExpression, J as setByPath, Ot as sleep, S as deviceStatusCapability, St as isDeviceConfigCap, T as enumerateSchemaFields, U as parseStreamParamsFormPatch, V as normalizeUnit, a as DeviceStatusSchema, b as deviceManagerCapability, c as STREAM_PROFILE_META, ct as DeviceFeature, et as toExpressionValue, g as applyTransform, i as CAP_NAMES_WITH_STATUS, it as BaseAddon, k as getByPath, kt as EventCategory, lt as DeviceRole, nt as validateExpressionSource, pt as WELL_KNOWN_TAB_MAP, rt as errMsg, t as ALL_CAPABILITY_DEFINITIONS, ut as DeviceType, w as enumerateItemArrayFields, x as deviceStateCapability, y as buildStreamParamsConfigSchema } from "../../dist-RlzjLezj.mjs";
2
2
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -1328,6 +1328,39 @@ async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
1328
1328
  return { success: true };
1329
1329
  }
1330
1330
  //#endregion
1331
+ //#region src/builtins/device-manager/device-store-sections.ts
1332
+ /** The device-manager's device-store-backed sections. Add an entry to extend. */
1333
+ var DEVICE_STORE_SECTIONS = [{
1334
+ keys: [LINKED_MODE_CONFIG_KEY, LINKED_IDS_CONFIG_KEY],
1335
+ apply: (pctx, deviceId, patch) => applyLinkedDevicesPatch(pctx, deviceId, patch)
1336
+ }];
1337
+ /**
1338
+ * Split a `writerCapName: 'device-manager'` patch into device-store slices
1339
+ * (one per matching registered section) and the leftover driver patch.
1340
+ * Generic — consults {@link DEVICE_STORE_SECTIONS}, no hardcoded key list.
1341
+ */
1342
+ function splitDeviceStoreKeys(patch) {
1343
+ const claimed = /* @__PURE__ */ new Set();
1344
+ const storeGroups = [];
1345
+ for (const section of DEVICE_STORE_SECTIONS) {
1346
+ const slice = {};
1347
+ for (const key of section.keys) if (key in patch) {
1348
+ slice[key] = patch[key];
1349
+ claimed.add(key);
1350
+ }
1351
+ if (Object.keys(slice).length > 0) storeGroups.push({
1352
+ section,
1353
+ patch: slice
1354
+ });
1355
+ }
1356
+ const driverPatch = {};
1357
+ for (const [key, value] of Object.entries(patch)) if (!claimed.has(key)) driverPatch[key] = value;
1358
+ return {
1359
+ storeGroups,
1360
+ driverPatch
1361
+ };
1362
+ }
1363
+ //#endregion
1331
1364
  //#region src/builtins/device-manager/device-bindings-store.ts
1332
1365
  /**
1333
1366
  * Persisted wrapper-activation + per-device binding resolution for the
@@ -2026,40 +2059,9 @@ async function dispatchDerivedFormSave(builderId, deviceId, patch, dcProvider, c
2026
2059
  throw new Error(`[device-manager] device-config cap "${capName}" provider exposes neither setSettings nor setProfile`);
2027
2060
  }
2028
2061
  async function updateDeviceField(deps, input) {
2029
- if (input.writerCapName === "device-manager") {
2030
- if (input.key === "_linkedDevicesMode" || input.key === "_linkedDeviceIds") {
2031
- const settings = deps.ctx.settings;
2032
- if (!settings) throw new Error("[device-manager] linked-devices save requires the settings surface");
2033
- await applyLinkedDevicesPatch({ settings }, input.deviceId, { [input.key]: input.value });
2034
- return { success: true };
2035
- }
2036
- const hubRegistry = deps.ctx.kernel?.deviceRegistry;
2037
- const found = hubRegistry ? resolveDeviceById(hubRegistry, input.deviceId) : null;
2038
- if (found) {
2039
- await found.device.applySettingsPatch({ [input.key]: input.value });
2040
- return { success: true };
2041
- }
2042
- const ops = deps.capabilityRegistry?.getNativeProvider("device-ops", input.deviceId);
2043
- if (!ops) throw new Error(`[device-manager] device "${input.deviceId}" not found (no hub-local entry, no device-ops native provider)`);
2044
- await ops.setConfig({
2045
- deviceId: input.deviceId,
2046
- values: { [input.key]: input.value }
2047
- });
2048
- return { success: true };
2049
- }
2050
- const registry = deps.capabilityRegistry;
2051
- if (!registry) throw new Error("[device-manager] updateDeviceField requires capability registry — unavailable on this node");
2052
- const def = registry.getDefinition(input.writerCapName);
2053
- if (isDeviceConfigCap(def)) {
2054
- if (def.deviceConfig.ui.kind === "widget") return { success: true };
2055
- const dcProvider = registry.getProviderForDevice(input.writerCapName, input.deviceId);
2056
- if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${input.writerCapName}" on device ${input.deviceId}`);
2057
- await dispatchDerivedFormSave(def.deviceConfig.ui.builderId, input.deviceId, { [input.key]: input.value }, dcProvider, input.writerCapName);
2058
- return { success: true };
2059
- }
2060
- if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
2061
- await resolveContributionProvider(registry, def, input.writerCapName, input.writerAddonId, input.deviceId).applyDeviceSettingsPatch({
2062
- deviceId: input.deviceId,
2062
+ await applyGroupPatch(deps, input.deviceId, {
2063
+ writerCapName: input.writerCapName,
2064
+ writerAddonId: input.writerAddonId,
2063
2065
  patch: { [input.key]: input.value }
2064
2066
  });
2065
2067
  return { success: true };
@@ -2152,17 +2154,24 @@ async function updateDeviceFieldsBatch(deps, input) {
2152
2154
  * registry lookup). Used by `updateDeviceFieldsBatch`. */
2153
2155
  async function applyGroupPatch(deps, deviceId, group) {
2154
2156
  if (group.writerCapName === "device-manager") {
2157
+ const { storeGroups, driverPatch } = splitDeviceStoreKeys(group.patch);
2158
+ if (storeGroups.length > 0) {
2159
+ const settings = deps.ctx.settings;
2160
+ if (!settings) throw new Error("[device-manager] device-store save requires the settings surface");
2161
+ for (const { section, patch } of storeGroups) await section.apply({ settings }, deviceId, patch);
2162
+ }
2163
+ if (Object.keys(driverPatch).length === 0) return;
2155
2164
  const hubRegistry = deps.ctx.kernel?.deviceRegistry;
2156
2165
  const found = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
2157
2166
  if (found) {
2158
- await found.device.applySettingsPatch(group.patch);
2167
+ await found.device.applySettingsPatch(driverPatch);
2159
2168
  return;
2160
2169
  }
2161
2170
  const ops = deps.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
2162
2171
  if (!ops) throw new Error(`[device-manager] device "${deviceId}" not found (no hub-local entry, no device-ops native provider)`);
2163
2172
  await ops.setConfig({
2164
2173
  deviceId,
2165
- values: group.patch
2174
+ values: driverPatch
2166
2175
  });
2167
2176
  return;
2168
2177
  }
@@ -0,0 +1,26 @@
1
+ import { ProviderContext } from './device-provider-context.js';
2
+ /** One device-store-backed section: the keys it owns + how to persist them. */
3
+ export interface DeviceStoreSection {
4
+ /** Reserved config keys this section owns (must not reach the driver). */
5
+ readonly keys: readonly string[];
6
+ /** Persist this section's slice of a save into the device-store blob. */
7
+ apply(pctx: Pick<ProviderContext, 'settings'>, deviceId: number, patch: Record<string, unknown>): Promise<unknown>;
8
+ }
9
+ /** The device-manager's device-store-backed sections. Add an entry to extend. */
10
+ export declare const DEVICE_STORE_SECTIONS: readonly DeviceStoreSection[];
11
+ /** Result of splitting a `device-manager`-writer patch by persistence target. */
12
+ export interface DeviceStoreSplit {
13
+ /** Per-section slices that persist to the device-store blob. */
14
+ readonly storeGroups: ReadonlyArray<{
15
+ section: DeviceStoreSection;
16
+ patch: Record<string, unknown>;
17
+ }>;
18
+ /** The remaining keys, destined for the device driver's own config. */
19
+ readonly driverPatch: Record<string, unknown>;
20
+ }
21
+ /**
22
+ * Split a `writerCapName: 'device-manager'` patch into device-store slices
23
+ * (one per matching registered section) and the leftover driver patch.
24
+ * Generic — consults {@link DEVICE_STORE_SECTIONS}, no hardcoded key list.
25
+ */
26
+ export declare function splitDeviceStoreKeys(patch: Record<string, unknown>): DeviceStoreSplit;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Pure per-camera settings parsing/normalization for the virtual-doorbell
3
+ * builtin. Kept separate from the addon class (and the runtime trigger
4
+ * engine) so the persisted-shape handling — including tolerant migration of
5
+ * the legacy single-source blob — is unit-testable in isolation.
6
+ *
7
+ * PERSISTED SHAPE (extensible by design):
8
+ * doorbellSources: Array<{ deviceId: number }>
9
+ *
10
+ * The source is an OBJECT rather than a bare id so future per-source options
11
+ * (a custom trigger override, an event-type match, a per-source debounce)
12
+ * can be added without another migration. Today only `deviceId` is stored;
13
+ * the reaction is DERIVED at runtime from the source device's cap (see
14
+ * `trigger-engine.ts`).
15
+ */
16
+ /** Device-store key holding the source list. */
17
+ export declare const KEY_SOURCES = "doorbellSources";
18
+ /**
19
+ * Legacy device-store keys from the pre-simplification shape (single source
20
+ * device + explicit trigger + event type). Retained ONLY for tolerant
21
+ * migration on read and cleanup on write — never written afresh.
22
+ */
23
+ export declare const LEGACY_KEY_SOURCE = "doorbellSourceDeviceId";
24
+ export declare const LEGACY_KEY_TRIGGER = "doorbellTrigger";
25
+ export declare const LEGACY_KEY_EVENT_TYPE = "doorbellEventType";
26
+ /**
27
+ * One source device driving a camera's virtual doorbell. Intentionally an
28
+ * object so it can grow (custom trigger, event type) without a migration.
29
+ */
30
+ export interface DoorbellSource {
31
+ readonly deviceId: number;
32
+ }
33
+ /** Coerce a persisted / form-supplied id (number or numeric string) to a
34
+ * positive integer device id, or null when it is not a usable id. */
35
+ export declare function coerceDeviceId(value: unknown): number | null;
36
+ /**
37
+ * Parse the persisted doorbell sources for one camera. Tolerant of BOTH:
38
+ * - the new `doorbellSources: [{deviceId}]` shape (ids may arrive as
39
+ * numbers, numeric strings, or `{deviceId}` records); and
40
+ * - the legacy single `doorbellSourceDeviceId` scalar, mapped onto one
41
+ * source (the old trigger/eventType are dropped — the reaction is now
42
+ * derived at runtime).
43
+ *
44
+ * Never throws on malformed data — unknown entries are skipped.
45
+ */
46
+ export declare function parseDoorbellSources(blob: Record<string, unknown>): readonly DoorbellSource[];
47
+ /**
48
+ * Normalize a settings-patch value for the `doorbellSources` multiselect
49
+ * (a `string[]` of device ids from the form, or an already-shaped
50
+ * `{deviceId}[]` / `number[]`) into the persisted extensible shape.
51
+ */
52
+ export declare function normalizeDoorbellSources(value: unknown): DoorbellSource[];
53
+ /**
54
+ * Produce the next device-store blob after applying a doorbell-sources
55
+ * patch: writes the normalized new shape and strips every legacy key so a
56
+ * migrated camera never carries stale single-source fields.
57
+ */
58
+ export declare function applyDoorbellSourcesPatch(current: Record<string, unknown>, patchValue: unknown): Record<string, unknown>;
@@ -1,26 +1,35 @@
1
1
  /**
2
- * Pure trigger logic for the virtual-doorbell builtin — edge detection
3
- * over runtime-state slices, event-emitter matching, and per-camera
4
- * debounce. Extracted from the addon class (mirrors the
5
- * `snapshot-coalescing.ts` split) so the behaviour is unit-testable
6
- * without instantiating the addon or mocking the event bus.
2
+ * Pure trigger logic for the virtual-doorbell builtin — edge detection over
3
+ * runtime-state slices with per-camera debounce. Extracted from the addon
4
+ * class so the behaviour is unit-testable without instantiating the addon or
5
+ * mocking the event bus.
6
+ *
7
+ * REACTION MODEL (simplified — operator picks a source, the system reacts
8
+ * with a sensible default): a source device rings a camera's doorbell when a
9
+ * known binary / switch cap on it RISES to its active state (false→true) — a
10
+ * contact opening, a switch turning on, an alarm sensor firing. There is no
11
+ * per-source trigger configuration today; the shape stays extensible for a
12
+ * future custom-trigger override (see `doorbell-settings.ts`).
13
+ */
14
+ /**
15
+ * Known binary / switch source caps → the boolean slice field whose
16
+ * false→true rise rings the doorbell. Every entry is "ring on active".
17
+ * Sensors whose "active" reading is not a plain boolean (presence's string
18
+ * state, connectivity's connected flag) are deliberately excluded — a
19
+ * reconnect is not a doorbell press.
7
20
  */
8
- /** Trigger kinds an operator can configure per camera. */
9
- export type DoorbellTrigger = 'contact-open' | 'contact-close' | 'switch-on' | 'event';
10
- export declare const DOORBELL_TRIGGERS: readonly DoorbellTrigger[];
11
- /** Type guard for {@link DoorbellTrigger}. */
12
- export declare function isDoorbellTrigger(value: unknown): value is DoorbellTrigger;
21
+ export declare const SOURCE_CAP_ACTIVE_FIELD: Readonly<Record<string, string>>;
22
+ /** Cap names whose presence in a device's bindings qualify it as a source. */
23
+ export declare const SOURCE_CAPS: readonly string[];
24
+ /** True when a cap is a recognised binary/switch doorbell source. */
25
+ export declare function isSourceCap(capName: string): boolean;
13
26
  /**
14
- * One camera's virtual-doorbell wiring: which source device drives it,
15
- * on which trigger, and (for `trigger: 'event'`) which event type.
27
+ * One camera's virtual-doorbell wiring: a source device that rings it. A
28
+ * camera with several sources produces several assignments (one per source).
16
29
  */
17
30
  export interface DoorbellAssignment {
18
31
  readonly cameraId: number;
19
32
  readonly sourceDeviceId: number;
20
- readonly trigger: DoorbellTrigger;
21
- /** Only meaningful when `trigger === 'event'` — matched verbatim
22
- * against the event-emitter `onEvent` payload `eventType`. */
23
- readonly eventType: string | null;
24
33
  }
25
34
  /** Debounce window guarding against sensor chatter (reed-switch bounce,
26
35
  * duplicate HA state pushes). One ring per camera per window. */
@@ -29,10 +38,10 @@ export declare const DEFAULT_DEBOUNCE_MS = 2000;
29
38
  * Stateful (but side-effect-free towards the outside) trigger engine.
30
39
  *
31
40
  * Tracks the last observed binary value per (sourceDevice, cap) so a
32
- * `DeviceStateChanged` re-emission (the slice's `lastChangedAt` moves
33
- * while the binary value does not) never double-fires, and the FIRST
34
- * observation after boot only seeds the baseline — a state hydration
35
- * at startup must not ring anybody's doorbell.
41
+ * `DeviceStateChanged` re-emission (the slice's `lastChangedAt` moves while
42
+ * the binary value does not) never double-fires, and the FIRST observation
43
+ * after boot only seeds the baseline — a state hydration at startup must not
44
+ * ring anybody's doorbell.
36
45
  */
37
46
  export declare class DoorbellTriggerEngine {
38
47
  private assignments;
@@ -53,17 +62,10 @@ export declare class DoorbellTriggerEngine {
53
62
  hasSource(deviceId: number): boolean;
54
63
  /**
55
64
  * Feed one `DeviceStateChanged` slice. Returns the camera ids whose
56
- * doorbell should ring (already debounced). Baseline-only sightings
57
- * and non-transitions return an empty array.
65
+ * doorbell should ring (already debounced). Only a false→true rise on a
66
+ * recognised source cap fires; baseline-only sightings, re-emissions, and
67
+ * the falling edge return an empty array.
58
68
  */
59
69
  onStateSlice(deviceId: number, capName: string, slice: Record<string, unknown>): number[];
60
- /**
61
- * Feed one event-emitter `onEvent` firing. Returns the camera ids to
62
- * ring — assignments with `trigger: 'event'`, a matching source and a
63
- * verbatim `eventType` match. An assignment with no configured
64
- * eventType never matches (misconfiguration must not ring on every
65
- * event the device emits).
66
- */
67
- onEmitterEvent(deviceId: number, eventType: string): number[];
68
70
  private matchAndDebounce;
69
71
  }