@camstack/types 1.1.21 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon/base-addon.d.ts +30 -4
- package/dist/addon/per-node-store.d.ts +68 -0
- package/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/capabilities/camera-streams.cap.d.ts +5 -5
- package/dist/capabilities/decoder.cap.d.ts +2 -0
- package/dist/capabilities/index.d.ts +3 -1
- package/dist/capabilities/metrics-provider.cap.d.ts +2 -2
- package/dist/capabilities/motion-detection.cap.d.ts +20 -2
- package/dist/capabilities/pet-feeder.cap.d.ts +185 -0
- package/dist/capabilities/pipeline-executor.cap.d.ts +29 -2
- package/dist/capabilities/platform-probe.cap.d.ts +1 -1
- package/dist/capabilities/schemas/streaming-shared.d.ts +2 -2
- package/dist/capabilities/stream-broker.cap.d.ts +1 -1
- package/dist/device/device-type.d.ts +8 -1
- package/dist/generated/addon-api.d.ts +302 -12
- package/dist/generated/cap-status-types.d.ts +3 -1
- package/dist/generated/capability-router-map.d.ts +5 -2
- package/dist/generated/device-local-state.d.ts +3 -0
- package/dist/generated/device-proxy.d.ts +3 -0
- package/dist/generated/method-access-map.d.ts +1 -1
- package/dist/index.js +288 -11
- package/dist/index.mjs +285 -12
- package/dist/interfaces/addon.d.ts +15 -2
- package/dist/interfaces/config-ui.d.ts +19 -1
- package/dist/interfaces/pipeline-executor-capability.d.ts +10 -0
- package/dist/{sleep-DaQgDq90.js → sleep-B8cp-HUn.js} +192 -7
- package/dist/{sleep-BO1nweKv.mjs → sleep-BiDFW0E7.mjs} +192 -7
- package/package.json +1 -1
|
@@ -907,6 +907,102 @@ function createDurableState(deps) {
|
|
|
907
907
|
};
|
|
908
908
|
}
|
|
909
909
|
//#endregion
|
|
910
|
+
//#region src/addon/per-node-store.ts
|
|
911
|
+
/**
|
|
912
|
+
* Per-node scoping for the shared addon-settings blob.
|
|
913
|
+
*
|
|
914
|
+
* An addon's settings store is hub-central (the `addon-settings` cap is
|
|
915
|
+
* hub-routed — the hub instance answers for every node), so fields whose
|
|
916
|
+
* value is a NODE fact (decoder backend, engine pick, probed hardware
|
|
917
|
+
* capabilities) persist under the node-scoped key `<key>@<nodeId>` inside
|
|
918
|
+
* the single shared blob. The SCHEMA field key stays bare: the write path
|
|
919
|
+
* ({@link scopePatch}) maps the bare field onto the target node's scoped
|
|
920
|
+
* key; the read/hydrate path ({@link projectStore}) maps THIS node's value
|
|
921
|
+
* back, so UI forms and `resolveConfig` only ever see the bare key.
|
|
922
|
+
*
|
|
923
|
+
* Fields opt in via the `perNode: true` schema marker on `ConfigFieldBase`
|
|
924
|
+
* (`interfaces/config-ui.ts`); `BaseAddon` derives the key set from the
|
|
925
|
+
* schema and routes reads/writes through these helpers.
|
|
926
|
+
*
|
|
927
|
+
* ## No bare-key fallback — deliberate
|
|
928
|
+
*
|
|
929
|
+
* {@link readNodeValue} reads the node-scoped key ONLY. A node with no
|
|
930
|
+
* scoped key resolves to `undefined` so the field's schema `default` wins —
|
|
931
|
+
* NEVER `store[base]` and never another node's value. A bare legacy key in
|
|
932
|
+
* the store is invisible to every node, hub included, so one node's
|
|
933
|
+
* selection can never leak onto another. (This generalizes the
|
|
934
|
+
* `decoder-backend-keys.ts` semantics — the no-fallback variant — over an
|
|
935
|
+
* arbitrary set of per-node field keys.)
|
|
936
|
+
*
|
|
937
|
+
* Pure functions only — no I/O, no imports beyond the language. This is a
|
|
938
|
+
* LEAF module: import it via its deep path, never from the root barrel.
|
|
939
|
+
*/
|
|
940
|
+
/**
|
|
941
|
+
* Normalize a raw kernel node id to the bare node id used for scoping.
|
|
942
|
+
* `localNodeId` can carry a `<node>/<addon>` suffix on forked child
|
|
943
|
+
* processes; per-node settings are per-NODE, so strip the addon segment.
|
|
944
|
+
* `undefined` / `null` / empty falls back to `'hub'`.
|
|
945
|
+
*/
|
|
946
|
+
function normalizeNodeId(raw) {
|
|
947
|
+
if (raw === void 0 || raw === null || raw === "") return "hub";
|
|
948
|
+
const slashIdx = raw.indexOf("/");
|
|
949
|
+
if (slashIdx < 0) return raw;
|
|
950
|
+
const bare = raw.slice(0, slashIdx);
|
|
951
|
+
return bare === "" ? "hub" : bare;
|
|
952
|
+
}
|
|
953
|
+
/** The node-scoped store key for a per-node field: `<base>@<nodeId>`. */
|
|
954
|
+
function nodeScopedKey(base, nodeId) {
|
|
955
|
+
return `${base}@${normalizeNodeId(nodeId)}`;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Read a node's value for a per-node field from the raw shared store:
|
|
959
|
+
* the node-scoped key when present, otherwise `undefined`.
|
|
960
|
+
* Deliberately NO bare-key fallback (see module doc) — the caller lets the
|
|
961
|
+
* schema `default` win on `undefined`.
|
|
962
|
+
*/
|
|
963
|
+
function readNodeValue(store, base, nodeId) {
|
|
964
|
+
return store[nodeScopedKey(base, nodeId)];
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Re-map a UI/settings patch so every bare perNode field persists under the
|
|
968
|
+
* TARGET node's scoped key; all other keys pass through unchanged. Used on
|
|
969
|
+
* the write path so a save for one node never clobbers another node's value
|
|
970
|
+
* (and the bare key is never written). Returns a new object — the input
|
|
971
|
+
* patch is not mutated.
|
|
972
|
+
*/
|
|
973
|
+
function scopePatch(patch, perNodeKeys, nodeId) {
|
|
974
|
+
const out = {};
|
|
975
|
+
for (const [key, value] of Object.entries(patch)) out[perNodeKeys.has(key) ? nodeScopedKey(key, nodeId) : key] = value;
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Project the raw shared store onto the bare schema keys for ONE node so a
|
|
980
|
+
* UI schema (whose field keys are bare) hydrates from that node's own
|
|
981
|
+
* values:
|
|
982
|
+
*
|
|
983
|
+
* - EVERY `@`-scoped key (any node's) is dropped from the pass-through.
|
|
984
|
+
* - Every bare perNode key is dropped from the pass-through (a stray bare
|
|
985
|
+
* legacy key must never hydrate any node — no bare fallback).
|
|
986
|
+
* - THIS node's effective value ({@link readNodeValue}) is then laid onto
|
|
987
|
+
* each bare perNode key; when the node has no scoped key the bare key is
|
|
988
|
+
* left ABSENT so the field's schema `default` wins.
|
|
989
|
+
*
|
|
990
|
+
* Returns a new object — the input store is not mutated.
|
|
991
|
+
*/
|
|
992
|
+
function projectStore(store, perNodeKeys, nodeId) {
|
|
993
|
+
const out = {};
|
|
994
|
+
for (const [key, value] of Object.entries(store)) {
|
|
995
|
+
if (key.includes("@")) continue;
|
|
996
|
+
if (perNodeKeys.has(key)) continue;
|
|
997
|
+
out[key] = value;
|
|
998
|
+
}
|
|
999
|
+
for (const base of perNodeKeys) {
|
|
1000
|
+
const value = readNodeValue(store, base, nodeId);
|
|
1001
|
+
if (value !== void 0) out[base] = value;
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
//#endregion
|
|
910
1006
|
//#region src/addon/base-addon.ts
|
|
911
1007
|
/**
|
|
912
1008
|
* Base class for CamStack addons. Eliminates settings boilerplate:
|
|
@@ -1076,23 +1172,63 @@ var BaseAddon = class {
|
|
|
1076
1172
|
deviceSettingsSchema() {
|
|
1077
1173
|
return null;
|
|
1078
1174
|
}
|
|
1079
|
-
async getGlobalSettings(overlay, cap,
|
|
1175
|
+
async getGlobalSettings(overlay, cap, nodeId) {
|
|
1080
1176
|
const schema = this.globalSettingsSchema(cap);
|
|
1081
1177
|
if (!schema) return { sections: [] };
|
|
1082
|
-
const
|
|
1178
|
+
const projected = await this.resolveGlobalStore(nodeId, cap);
|
|
1083
1179
|
return hydrateSchema(schema, overlay ? {
|
|
1084
|
-
...
|
|
1180
|
+
...projected,
|
|
1085
1181
|
...overlay
|
|
1086
|
-
} :
|
|
1182
|
+
} : projected);
|
|
1087
1183
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1184
|
+
/**
|
|
1185
|
+
* The raw addon store PROJECTED onto the target node's bare per-node keys:
|
|
1186
|
+
* every `perNode: true` field carries THAT node's scoped value on its bare
|
|
1187
|
+
* key (absent scoped key ⇒ key absent, so the schema `default` wins — no
|
|
1188
|
+
* bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
|
|
1189
|
+
* A no-op passthrough when the schema declares no `perNode` field.
|
|
1190
|
+
*
|
|
1191
|
+
* This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
|
|
1192
|
+
* the store for custom option logic (option narrowing, value snapping) to
|
|
1193
|
+
* read it per-node — never `ctx.settings.readAddonStore()` directly.
|
|
1194
|
+
* `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
|
|
1195
|
+
*/
|
|
1196
|
+
async resolveGlobalStore(nodeId, cap) {
|
|
1197
|
+
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
1198
|
+
const keys = this.perNodeKeys(cap);
|
|
1199
|
+
const node = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
1200
|
+
return keys.size > 0 ? projectStore(raw, keys, node) : raw;
|
|
1201
|
+
}
|
|
1202
|
+
async updateGlobalSettings(patch, nodeId) {
|
|
1203
|
+
const keys = this.perNodeKeys();
|
|
1204
|
+
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
1205
|
+
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
1206
|
+
const barePatch = patch;
|
|
1207
|
+
const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
|
|
1208
|
+
await this._ctx?.settings?.writeAddonStore(scoped);
|
|
1209
|
+
if (target !== localNode) return;
|
|
1090
1210
|
await this.resolveConfig();
|
|
1091
1211
|
await this.onConfigChanged();
|
|
1092
1212
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
1093
1213
|
this.maybeAutoRestart(patch, this.globalSettingsSchema());
|
|
1094
1214
|
}
|
|
1095
1215
|
/**
|
|
1216
|
+
* The set of field keys the global settings schema declares `perNode: true`
|
|
1217
|
+
* — derived once per `cap` argument and memoized (schemas are static
|
|
1218
|
+
* declarations). Empty set ⇒ every per-node code path is bypassed and the
|
|
1219
|
+
* settings API behaves exactly like the legacy node-agnostic one.
|
|
1220
|
+
*/
|
|
1221
|
+
_perNodeKeysCache = /* @__PURE__ */ new Map();
|
|
1222
|
+
perNodeKeys(cap) {
|
|
1223
|
+
const cacheKey = cap ?? "";
|
|
1224
|
+
const cached = this._perNodeKeysCache.get(cacheKey);
|
|
1225
|
+
if (cached) return cached;
|
|
1226
|
+
const schema = this.globalSettingsSchema(cap);
|
|
1227
|
+
const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
|
|
1228
|
+
this._perNodeKeysCache.set(cacheKey, keys);
|
|
1229
|
+
return keys;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1096
1232
|
* If any field in `patch` is marked `requiresRestart` in `schema`,
|
|
1097
1233
|
* schedule an addon restart for the next tick. Deferred via
|
|
1098
1234
|
* `setImmediate` so the tRPC mutation that triggered the write has
|
|
@@ -1245,12 +1381,19 @@ var BaseAddon = class {
|
|
|
1245
1381
|
* The merge is shallow: each key in `defaults` is checked against the store.
|
|
1246
1382
|
* Only keys present in defaults are read — the store can contain extra keys
|
|
1247
1383
|
* (e.g. from older versions) without polluting the typed config.
|
|
1384
|
+
*
|
|
1385
|
+
* Keys the global settings schema declares `perNode: true` resolve from
|
|
1386
|
+
* THIS node's scoped key (`<key>@<localNode>`) via `readNodeValue` — never
|
|
1387
|
+
* from the bare key — so a per-node field resolves to this node's own
|
|
1388
|
+
* selection at boot (absent scoped key ⇒ the constructor default wins).
|
|
1248
1389
|
*/
|
|
1249
1390
|
async resolveConfig() {
|
|
1250
1391
|
const stored = await this.readAddonStoreWithRetry();
|
|
1392
|
+
const perNode = this.perNodeKeys();
|
|
1393
|
+
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
1251
1394
|
const resolved = { ...this.defaults };
|
|
1252
1395
|
for (const key of Object.keys(this.defaults)) {
|
|
1253
|
-
const storedValue = stored[key];
|
|
1396
|
+
const storedValue = perNode.has(key) ? readNodeValue(stored, key, localNode) : stored[key];
|
|
1254
1397
|
if (storedValue !== void 0 && storedValue !== null) {
|
|
1255
1398
|
const defaultType = typeof this.defaults[key];
|
|
1256
1399
|
if (typeof storedValue === defaultType) resolved[key] = storedValue;
|
|
@@ -1334,6 +1477,27 @@ var BaseAddon = class {
|
|
|
1334
1477
|
}
|
|
1335
1478
|
};
|
|
1336
1479
|
/**
|
|
1480
|
+
* Collect the keys of every field marked `perNode: true`, recursing into
|
|
1481
|
+
* layout containers (`group` fields and `sub-tabs` tabs) the same way
|
|
1482
|
+
* `hydrateSchema` does. Valueless structural fields (separator/info/…)
|
|
1483
|
+
* don't declare `perNode` and are excluded by the `in` narrowing.
|
|
1484
|
+
*/
|
|
1485
|
+
function collectPerNodeFieldKeys(fields) {
|
|
1486
|
+
const collected = [];
|
|
1487
|
+
for (const field of fields) {
|
|
1488
|
+
if (field.type === "group") {
|
|
1489
|
+
collected.push(...collectPerNodeFieldKeys(field.fields));
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (field.type === "sub-tabs") {
|
|
1493
|
+
for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if ("perNode" in field && field.perNode === true) collected.push(field.key);
|
|
1497
|
+
}
|
|
1498
|
+
return collected;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1337
1501
|
* Normalize an `ICamstackAddon.initialize()` return value into the
|
|
1338
1502
|
* `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
|
|
1339
1503
|
* envelopes pass through; void stays void.
|
|
@@ -2239,6 +2403,13 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
2239
2403
|
/** Single still-image entity (HA `image.*`). Read-only display of an
|
|
2240
2404
|
* `entity_picture` signed URL the browser loads directly. `image` cap. */
|
|
2241
2405
|
DeviceType["Image"] = "image";
|
|
2406
|
+
/** Smart pet feeder — cloud-connected food dispenser with a bowl food
|
|
2407
|
+
* level, battery, desiccant life, feeding state and manual-feed /
|
|
2408
|
+
* call-pet / maintenance actions. Installed with the `pet-feeder` cap;
|
|
2409
|
+
* dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
|
|
2410
|
+
* native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
|
|
2411
|
+
* integrations sharing the same food/desiccant/hopper surface. */
|
|
2412
|
+
DeviceType["PetFeeder"] = "pet-feeder";
|
|
2242
2413
|
return DeviceType;
|
|
2243
2414
|
}({});
|
|
2244
2415
|
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
@@ -2898,6 +3069,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
2898
3069
|
nativeObjectDetection: createSliceHandle(stateSource, binding.deviceId, "native-object-detection"),
|
|
2899
3070
|
notifier: createSliceHandle(stateSource, binding.deviceId, "notifier"),
|
|
2900
3071
|
numericSensor: createSliceHandle(stateSource, binding.deviceId, "numeric-sensor"),
|
|
3072
|
+
petFeeder: createSliceHandle(stateSource, binding.deviceId, "pet-feeder"),
|
|
2901
3073
|
powerMeter: createSliceHandle(stateSource, binding.deviceId, "power-meter"),
|
|
2902
3074
|
presence: createSliceHandle(stateSource, binding.deviceId, "presence"),
|
|
2903
3075
|
pressureSensor: createSliceHandle(stateSource, binding.deviceId, "pressure-sensor"),
|
|
@@ -3121,6 +3293,19 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3121
3293
|
setOverlay: (input) => dispatch("osd", "osd", "setOverlay", "mutation", input),
|
|
3122
3294
|
getStatus: (input) => dispatch("osd", "osd", "getStatus", "query", input)
|
|
3123
3295
|
},
|
|
3296
|
+
petFeeder: {
|
|
3297
|
+
feed: (input) => dispatch("pet-feeder", "petFeeder", "feed", "mutation", input),
|
|
3298
|
+
cancelFeed: (input) => dispatch("pet-feeder", "petFeeder", "cancelFeed", "mutation", input),
|
|
3299
|
+
resetDesiccant: (input) => dispatch("pet-feeder", "petFeeder", "resetDesiccant", "mutation", input),
|
|
3300
|
+
markFoodReplenished: (input) => dispatch("pet-feeder", "petFeeder", "markFoodReplenished", "mutation", input),
|
|
3301
|
+
callPet: (input) => dispatch("pet-feeder", "petFeeder", "callPet", "mutation", input),
|
|
3302
|
+
playSound: (input) => dispatch("pet-feeder", "petFeeder", "playSound", "mutation", input),
|
|
3303
|
+
setChildLock: (input) => dispatch("pet-feeder", "petFeeder", "setChildLock", "mutation", input),
|
|
3304
|
+
setIndicatorLight: (input) => dispatch("pet-feeder", "petFeeder", "setIndicatorLight", "mutation", input),
|
|
3305
|
+
setFeedSound: (input) => dispatch("pet-feeder", "petFeeder", "setFeedSound", "mutation", input),
|
|
3306
|
+
setVolume: (input) => dispatch("pet-feeder", "petFeeder", "setVolume", "mutation", input),
|
|
3307
|
+
getStatus: (input) => dispatch("pet-feeder", "petFeeder", "getStatus", "query", input)
|
|
3308
|
+
},
|
|
3124
3309
|
pipelineAnalytics: {
|
|
3125
3310
|
getActiveTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getActiveTracks", "query", input),
|
|
3126
3311
|
getTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrack", "query", input),
|
|
@@ -907,6 +907,102 @@ function createDurableState(deps) {
|
|
|
907
907
|
};
|
|
908
908
|
}
|
|
909
909
|
//#endregion
|
|
910
|
+
//#region src/addon/per-node-store.ts
|
|
911
|
+
/**
|
|
912
|
+
* Per-node scoping for the shared addon-settings blob.
|
|
913
|
+
*
|
|
914
|
+
* An addon's settings store is hub-central (the `addon-settings` cap is
|
|
915
|
+
* hub-routed — the hub instance answers for every node), so fields whose
|
|
916
|
+
* value is a NODE fact (decoder backend, engine pick, probed hardware
|
|
917
|
+
* capabilities) persist under the node-scoped key `<key>@<nodeId>` inside
|
|
918
|
+
* the single shared blob. The SCHEMA field key stays bare: the write path
|
|
919
|
+
* ({@link scopePatch}) maps the bare field onto the target node's scoped
|
|
920
|
+
* key; the read/hydrate path ({@link projectStore}) maps THIS node's value
|
|
921
|
+
* back, so UI forms and `resolveConfig` only ever see the bare key.
|
|
922
|
+
*
|
|
923
|
+
* Fields opt in via the `perNode: true` schema marker on `ConfigFieldBase`
|
|
924
|
+
* (`interfaces/config-ui.ts`); `BaseAddon` derives the key set from the
|
|
925
|
+
* schema and routes reads/writes through these helpers.
|
|
926
|
+
*
|
|
927
|
+
* ## No bare-key fallback — deliberate
|
|
928
|
+
*
|
|
929
|
+
* {@link readNodeValue} reads the node-scoped key ONLY. A node with no
|
|
930
|
+
* scoped key resolves to `undefined` so the field's schema `default` wins —
|
|
931
|
+
* NEVER `store[base]` and never another node's value. A bare legacy key in
|
|
932
|
+
* the store is invisible to every node, hub included, so one node's
|
|
933
|
+
* selection can never leak onto another. (This generalizes the
|
|
934
|
+
* `decoder-backend-keys.ts` semantics — the no-fallback variant — over an
|
|
935
|
+
* arbitrary set of per-node field keys.)
|
|
936
|
+
*
|
|
937
|
+
* Pure functions only — no I/O, no imports beyond the language. This is a
|
|
938
|
+
* LEAF module: import it via its deep path, never from the root barrel.
|
|
939
|
+
*/
|
|
940
|
+
/**
|
|
941
|
+
* Normalize a raw kernel node id to the bare node id used for scoping.
|
|
942
|
+
* `localNodeId` can carry a `<node>/<addon>` suffix on forked child
|
|
943
|
+
* processes; per-node settings are per-NODE, so strip the addon segment.
|
|
944
|
+
* `undefined` / `null` / empty falls back to `'hub'`.
|
|
945
|
+
*/
|
|
946
|
+
function normalizeNodeId(raw) {
|
|
947
|
+
if (raw === void 0 || raw === null || raw === "") return "hub";
|
|
948
|
+
const slashIdx = raw.indexOf("/");
|
|
949
|
+
if (slashIdx < 0) return raw;
|
|
950
|
+
const bare = raw.slice(0, slashIdx);
|
|
951
|
+
return bare === "" ? "hub" : bare;
|
|
952
|
+
}
|
|
953
|
+
/** The node-scoped store key for a per-node field: `<base>@<nodeId>`. */
|
|
954
|
+
function nodeScopedKey(base, nodeId) {
|
|
955
|
+
return `${base}@${normalizeNodeId(nodeId)}`;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Read a node's value for a per-node field from the raw shared store:
|
|
959
|
+
* the node-scoped key when present, otherwise `undefined`.
|
|
960
|
+
* Deliberately NO bare-key fallback (see module doc) — the caller lets the
|
|
961
|
+
* schema `default` win on `undefined`.
|
|
962
|
+
*/
|
|
963
|
+
function readNodeValue(store, base, nodeId) {
|
|
964
|
+
return store[nodeScopedKey(base, nodeId)];
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Re-map a UI/settings patch so every bare perNode field persists under the
|
|
968
|
+
* TARGET node's scoped key; all other keys pass through unchanged. Used on
|
|
969
|
+
* the write path so a save for one node never clobbers another node's value
|
|
970
|
+
* (and the bare key is never written). Returns a new object — the input
|
|
971
|
+
* patch is not mutated.
|
|
972
|
+
*/
|
|
973
|
+
function scopePatch(patch, perNodeKeys, nodeId) {
|
|
974
|
+
const out = {};
|
|
975
|
+
for (const [key, value] of Object.entries(patch)) out[perNodeKeys.has(key) ? nodeScopedKey(key, nodeId) : key] = value;
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Project the raw shared store onto the bare schema keys for ONE node so a
|
|
980
|
+
* UI schema (whose field keys are bare) hydrates from that node's own
|
|
981
|
+
* values:
|
|
982
|
+
*
|
|
983
|
+
* - EVERY `@`-scoped key (any node's) is dropped from the pass-through.
|
|
984
|
+
* - Every bare perNode key is dropped from the pass-through (a stray bare
|
|
985
|
+
* legacy key must never hydrate any node — no bare fallback).
|
|
986
|
+
* - THIS node's effective value ({@link readNodeValue}) is then laid onto
|
|
987
|
+
* each bare perNode key; when the node has no scoped key the bare key is
|
|
988
|
+
* left ABSENT so the field's schema `default` wins.
|
|
989
|
+
*
|
|
990
|
+
* Returns a new object — the input store is not mutated.
|
|
991
|
+
*/
|
|
992
|
+
function projectStore(store, perNodeKeys, nodeId) {
|
|
993
|
+
const out = {};
|
|
994
|
+
for (const [key, value] of Object.entries(store)) {
|
|
995
|
+
if (key.includes("@")) continue;
|
|
996
|
+
if (perNodeKeys.has(key)) continue;
|
|
997
|
+
out[key] = value;
|
|
998
|
+
}
|
|
999
|
+
for (const base of perNodeKeys) {
|
|
1000
|
+
const value = readNodeValue(store, base, nodeId);
|
|
1001
|
+
if (value !== void 0) out[base] = value;
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
//#endregion
|
|
910
1006
|
//#region src/addon/base-addon.ts
|
|
911
1007
|
/**
|
|
912
1008
|
* Base class for CamStack addons. Eliminates settings boilerplate:
|
|
@@ -1076,23 +1172,63 @@ var BaseAddon = class {
|
|
|
1076
1172
|
deviceSettingsSchema() {
|
|
1077
1173
|
return null;
|
|
1078
1174
|
}
|
|
1079
|
-
async getGlobalSettings(overlay, cap,
|
|
1175
|
+
async getGlobalSettings(overlay, cap, nodeId) {
|
|
1080
1176
|
const schema = this.globalSettingsSchema(cap);
|
|
1081
1177
|
if (!schema) return { sections: [] };
|
|
1082
|
-
const
|
|
1178
|
+
const projected = await this.resolveGlobalStore(nodeId, cap);
|
|
1083
1179
|
return hydrateSchema(schema, overlay ? {
|
|
1084
|
-
...
|
|
1180
|
+
...projected,
|
|
1085
1181
|
...overlay
|
|
1086
|
-
} :
|
|
1182
|
+
} : projected);
|
|
1087
1183
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1184
|
+
/**
|
|
1185
|
+
* The raw addon store PROJECTED onto the target node's bare per-node keys:
|
|
1186
|
+
* every `perNode: true` field carries THAT node's scoped value on its bare
|
|
1187
|
+
* key (absent scoped key ⇒ key absent, so the schema `default` wins — no
|
|
1188
|
+
* bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
|
|
1189
|
+
* A no-op passthrough when the schema declares no `perNode` field.
|
|
1190
|
+
*
|
|
1191
|
+
* This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
|
|
1192
|
+
* the store for custom option logic (option narrowing, value snapping) to
|
|
1193
|
+
* read it per-node — never `ctx.settings.readAddonStore()` directly.
|
|
1194
|
+
* `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
|
|
1195
|
+
*/
|
|
1196
|
+
async resolveGlobalStore(nodeId, cap) {
|
|
1197
|
+
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
1198
|
+
const keys = this.perNodeKeys(cap);
|
|
1199
|
+
const node = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
1200
|
+
return keys.size > 0 ? projectStore(raw, keys, node) : raw;
|
|
1201
|
+
}
|
|
1202
|
+
async updateGlobalSettings(patch, nodeId) {
|
|
1203
|
+
const keys = this.perNodeKeys();
|
|
1204
|
+
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
1205
|
+
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
1206
|
+
const barePatch = patch;
|
|
1207
|
+
const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
|
|
1208
|
+
await this._ctx?.settings?.writeAddonStore(scoped);
|
|
1209
|
+
if (target !== localNode) return;
|
|
1090
1210
|
await this.resolveConfig();
|
|
1091
1211
|
await this.onConfigChanged();
|
|
1092
1212
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
1093
1213
|
this.maybeAutoRestart(patch, this.globalSettingsSchema());
|
|
1094
1214
|
}
|
|
1095
1215
|
/**
|
|
1216
|
+
* The set of field keys the global settings schema declares `perNode: true`
|
|
1217
|
+
* — derived once per `cap` argument and memoized (schemas are static
|
|
1218
|
+
* declarations). Empty set ⇒ every per-node code path is bypassed and the
|
|
1219
|
+
* settings API behaves exactly like the legacy node-agnostic one.
|
|
1220
|
+
*/
|
|
1221
|
+
_perNodeKeysCache = /* @__PURE__ */ new Map();
|
|
1222
|
+
perNodeKeys(cap) {
|
|
1223
|
+
const cacheKey = cap ?? "";
|
|
1224
|
+
const cached = this._perNodeKeysCache.get(cacheKey);
|
|
1225
|
+
if (cached) return cached;
|
|
1226
|
+
const schema = this.globalSettingsSchema(cap);
|
|
1227
|
+
const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
|
|
1228
|
+
this._perNodeKeysCache.set(cacheKey, keys);
|
|
1229
|
+
return keys;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1096
1232
|
* If any field in `patch` is marked `requiresRestart` in `schema`,
|
|
1097
1233
|
* schedule an addon restart for the next tick. Deferred via
|
|
1098
1234
|
* `setImmediate` so the tRPC mutation that triggered the write has
|
|
@@ -1245,12 +1381,19 @@ var BaseAddon = class {
|
|
|
1245
1381
|
* The merge is shallow: each key in `defaults` is checked against the store.
|
|
1246
1382
|
* Only keys present in defaults are read — the store can contain extra keys
|
|
1247
1383
|
* (e.g. from older versions) without polluting the typed config.
|
|
1384
|
+
*
|
|
1385
|
+
* Keys the global settings schema declares `perNode: true` resolve from
|
|
1386
|
+
* THIS node's scoped key (`<key>@<localNode>`) via `readNodeValue` — never
|
|
1387
|
+
* from the bare key — so a per-node field resolves to this node's own
|
|
1388
|
+
* selection at boot (absent scoped key ⇒ the constructor default wins).
|
|
1248
1389
|
*/
|
|
1249
1390
|
async resolveConfig() {
|
|
1250
1391
|
const stored = await this.readAddonStoreWithRetry();
|
|
1392
|
+
const perNode = this.perNodeKeys();
|
|
1393
|
+
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
1251
1394
|
const resolved = { ...this.defaults };
|
|
1252
1395
|
for (const key of Object.keys(this.defaults)) {
|
|
1253
|
-
const storedValue = stored[key];
|
|
1396
|
+
const storedValue = perNode.has(key) ? readNodeValue(stored, key, localNode) : stored[key];
|
|
1254
1397
|
if (storedValue !== void 0 && storedValue !== null) {
|
|
1255
1398
|
const defaultType = typeof this.defaults[key];
|
|
1256
1399
|
if (typeof storedValue === defaultType) resolved[key] = storedValue;
|
|
@@ -1334,6 +1477,27 @@ var BaseAddon = class {
|
|
|
1334
1477
|
}
|
|
1335
1478
|
};
|
|
1336
1479
|
/**
|
|
1480
|
+
* Collect the keys of every field marked `perNode: true`, recursing into
|
|
1481
|
+
* layout containers (`group` fields and `sub-tabs` tabs) the same way
|
|
1482
|
+
* `hydrateSchema` does. Valueless structural fields (separator/info/…)
|
|
1483
|
+
* don't declare `perNode` and are excluded by the `in` narrowing.
|
|
1484
|
+
*/
|
|
1485
|
+
function collectPerNodeFieldKeys(fields) {
|
|
1486
|
+
const collected = [];
|
|
1487
|
+
for (const field of fields) {
|
|
1488
|
+
if (field.type === "group") {
|
|
1489
|
+
collected.push(...collectPerNodeFieldKeys(field.fields));
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (field.type === "sub-tabs") {
|
|
1493
|
+
for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if ("perNode" in field && field.perNode === true) collected.push(field.key);
|
|
1497
|
+
}
|
|
1498
|
+
return collected;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1337
1501
|
* Normalize an `ICamstackAddon.initialize()` return value into the
|
|
1338
1502
|
* `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
|
|
1339
1503
|
* envelopes pass through; void stays void.
|
|
@@ -2239,6 +2403,13 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
2239
2403
|
/** Single still-image entity (HA `image.*`). Read-only display of an
|
|
2240
2404
|
* `entity_picture` signed URL the browser loads directly. `image` cap. */
|
|
2241
2405
|
DeviceType["Image"] = "image";
|
|
2406
|
+
/** Smart pet feeder — cloud-connected food dispenser with a bowl food
|
|
2407
|
+
* level, battery, desiccant life, feeding state and manual-feed /
|
|
2408
|
+
* call-pet / maintenance actions. Installed with the `pet-feeder` cap;
|
|
2409
|
+
* dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
|
|
2410
|
+
* native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
|
|
2411
|
+
* integrations sharing the same food/desiccant/hopper surface. */
|
|
2412
|
+
DeviceType["PetFeeder"] = "pet-feeder";
|
|
2242
2413
|
return DeviceType;
|
|
2243
2414
|
}({});
|
|
2244
2415
|
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
@@ -2898,6 +3069,7 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
2898
3069
|
nativeObjectDetection: createSliceHandle(stateSource, binding.deviceId, "native-object-detection"),
|
|
2899
3070
|
notifier: createSliceHandle(stateSource, binding.deviceId, "notifier"),
|
|
2900
3071
|
numericSensor: createSliceHandle(stateSource, binding.deviceId, "numeric-sensor"),
|
|
3072
|
+
petFeeder: createSliceHandle(stateSource, binding.deviceId, "pet-feeder"),
|
|
2901
3073
|
powerMeter: createSliceHandle(stateSource, binding.deviceId, "power-meter"),
|
|
2902
3074
|
presence: createSliceHandle(stateSource, binding.deviceId, "presence"),
|
|
2903
3075
|
pressureSensor: createSliceHandle(stateSource, binding.deviceId, "pressure-sensor"),
|
|
@@ -3121,6 +3293,19 @@ function createDeviceProxy(api, binding, opts) {
|
|
|
3121
3293
|
setOverlay: (input) => dispatch("osd", "osd", "setOverlay", "mutation", input),
|
|
3122
3294
|
getStatus: (input) => dispatch("osd", "osd", "getStatus", "query", input)
|
|
3123
3295
|
},
|
|
3296
|
+
petFeeder: {
|
|
3297
|
+
feed: (input) => dispatch("pet-feeder", "petFeeder", "feed", "mutation", input),
|
|
3298
|
+
cancelFeed: (input) => dispatch("pet-feeder", "petFeeder", "cancelFeed", "mutation", input),
|
|
3299
|
+
resetDesiccant: (input) => dispatch("pet-feeder", "petFeeder", "resetDesiccant", "mutation", input),
|
|
3300
|
+
markFoodReplenished: (input) => dispatch("pet-feeder", "petFeeder", "markFoodReplenished", "mutation", input),
|
|
3301
|
+
callPet: (input) => dispatch("pet-feeder", "petFeeder", "callPet", "mutation", input),
|
|
3302
|
+
playSound: (input) => dispatch("pet-feeder", "petFeeder", "playSound", "mutation", input),
|
|
3303
|
+
setChildLock: (input) => dispatch("pet-feeder", "petFeeder", "setChildLock", "mutation", input),
|
|
3304
|
+
setIndicatorLight: (input) => dispatch("pet-feeder", "petFeeder", "setIndicatorLight", "mutation", input),
|
|
3305
|
+
setFeedSound: (input) => dispatch("pet-feeder", "petFeeder", "setFeedSound", "mutation", input),
|
|
3306
|
+
setVolume: (input) => dispatch("pet-feeder", "petFeeder", "setVolume", "mutation", input),
|
|
3307
|
+
getStatus: (input) => dispatch("pet-feeder", "petFeeder", "getStatus", "query", input)
|
|
3308
|
+
},
|
|
3124
3309
|
pipelineAnalytics: {
|
|
3125
3310
|
getActiveTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getActiveTracks", "query", input),
|
|
3126
3311
|
getTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrack", "query", input),
|