@camstack/addon-provider-homematic 1.1.13 → 1.1.15

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 (3) hide show
  1. package/dist/addon.js +1470 -103
  2. package/dist/addon.mjs +1470 -103
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -4651,7 +4651,7 @@ function preprocess(fn, schema) {
4651
4651
  });
4652
4652
  }
4653
4653
  //#endregion
4654
- //#region ../types/dist/sleep-C2M2zF7x.mjs
4654
+ //#region ../types/dist/sleep-BiDFW0E7.mjs
4655
4655
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4656
4656
  EventCategory["SystemBoot"] = "system.boot";
4657
4657
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5464,6 +5464,100 @@ function createDurableState(deps) {
5464
5464
  };
5465
5465
  }
5466
5466
  /**
5467
+ * Per-node scoping for the shared addon-settings blob.
5468
+ *
5469
+ * An addon's settings store is hub-central (the `addon-settings` cap is
5470
+ * hub-routed — the hub instance answers for every node), so fields whose
5471
+ * value is a NODE fact (decoder backend, engine pick, probed hardware
5472
+ * capabilities) persist under the node-scoped key `<key>@<nodeId>` inside
5473
+ * the single shared blob. The SCHEMA field key stays bare: the write path
5474
+ * ({@link scopePatch}) maps the bare field onto the target node's scoped
5475
+ * key; the read/hydrate path ({@link projectStore}) maps THIS node's value
5476
+ * back, so UI forms and `resolveConfig` only ever see the bare key.
5477
+ *
5478
+ * Fields opt in via the `perNode: true` schema marker on `ConfigFieldBase`
5479
+ * (`interfaces/config-ui.ts`); `BaseAddon` derives the key set from the
5480
+ * schema and routes reads/writes through these helpers.
5481
+ *
5482
+ * ## No bare-key fallback — deliberate
5483
+ *
5484
+ * {@link readNodeValue} reads the node-scoped key ONLY. A node with no
5485
+ * scoped key resolves to `undefined` so the field's schema `default` wins —
5486
+ * NEVER `store[base]` and never another node's value. A bare legacy key in
5487
+ * the store is invisible to every node, hub included, so one node's
5488
+ * selection can never leak onto another. (This generalizes the
5489
+ * `decoder-backend-keys.ts` semantics — the no-fallback variant — over an
5490
+ * arbitrary set of per-node field keys.)
5491
+ *
5492
+ * Pure functions only — no I/O, no imports beyond the language. This is a
5493
+ * LEAF module: import it via its deep path, never from the root barrel.
5494
+ */
5495
+ /**
5496
+ * Normalize a raw kernel node id to the bare node id used for scoping.
5497
+ * `localNodeId` can carry a `<node>/<addon>` suffix on forked child
5498
+ * processes; per-node settings are per-NODE, so strip the addon segment.
5499
+ * `undefined` / `null` / empty falls back to `'hub'`.
5500
+ */
5501
+ function normalizeNodeId(raw) {
5502
+ if (raw === void 0 || raw === null || raw === "") return "hub";
5503
+ const slashIdx = raw.indexOf("/");
5504
+ if (slashIdx < 0) return raw;
5505
+ const bare = raw.slice(0, slashIdx);
5506
+ return bare === "" ? "hub" : bare;
5507
+ }
5508
+ /** The node-scoped store key for a per-node field: `<base>@<nodeId>`. */
5509
+ function nodeScopedKey(base, nodeId) {
5510
+ return `${base}@${normalizeNodeId(nodeId)}`;
5511
+ }
5512
+ /**
5513
+ * Read a node's value for a per-node field from the raw shared store:
5514
+ * the node-scoped key when present, otherwise `undefined`.
5515
+ * Deliberately NO bare-key fallback (see module doc) — the caller lets the
5516
+ * schema `default` win on `undefined`.
5517
+ */
5518
+ function readNodeValue(store, base, nodeId) {
5519
+ return store[nodeScopedKey(base, nodeId)];
5520
+ }
5521
+ /**
5522
+ * Re-map a UI/settings patch so every bare perNode field persists under the
5523
+ * TARGET node's scoped key; all other keys pass through unchanged. Used on
5524
+ * the write path so a save for one node never clobbers another node's value
5525
+ * (and the bare key is never written). Returns a new object — the input
5526
+ * patch is not mutated.
5527
+ */
5528
+ function scopePatch(patch, perNodeKeys, nodeId) {
5529
+ const out = {};
5530
+ for (const [key, value] of Object.entries(patch)) out[perNodeKeys.has(key) ? nodeScopedKey(key, nodeId) : key] = value;
5531
+ return out;
5532
+ }
5533
+ /**
5534
+ * Project the raw shared store onto the bare schema keys for ONE node so a
5535
+ * UI schema (whose field keys are bare) hydrates from that node's own
5536
+ * values:
5537
+ *
5538
+ * - EVERY `@`-scoped key (any node's) is dropped from the pass-through.
5539
+ * - Every bare perNode key is dropped from the pass-through (a stray bare
5540
+ * legacy key must never hydrate any node — no bare fallback).
5541
+ * - THIS node's effective value ({@link readNodeValue}) is then laid onto
5542
+ * each bare perNode key; when the node has no scoped key the bare key is
5543
+ * left ABSENT so the field's schema `default` wins.
5544
+ *
5545
+ * Returns a new object — the input store is not mutated.
5546
+ */
5547
+ function projectStore(store, perNodeKeys, nodeId) {
5548
+ const out = {};
5549
+ for (const [key, value] of Object.entries(store)) {
5550
+ if (key.includes("@")) continue;
5551
+ if (perNodeKeys.has(key)) continue;
5552
+ out[key] = value;
5553
+ }
5554
+ for (const base of perNodeKeys) {
5555
+ const value = readNodeValue(store, base, nodeId);
5556
+ if (value !== void 0) out[base] = value;
5557
+ }
5558
+ return out;
5559
+ }
5560
+ /**
5467
5561
  * Base class for CamStack addons. Eliminates settings boilerplate:
5468
5562
  *
5469
5563
  * - Typed `config` property with automatic resolution from store + defaults
@@ -5631,23 +5725,63 @@ var BaseAddon = class {
5631
5725
  deviceSettingsSchema() {
5632
5726
  return null;
5633
5727
  }
5634
- async getGlobalSettings(overlay, cap, _nodeId) {
5728
+ async getGlobalSettings(overlay, cap, nodeId) {
5635
5729
  const schema = this.globalSettingsSchema(cap);
5636
5730
  if (!schema) return { sections: [] };
5637
- const raw = await this._ctx?.settings?.readAddonStore() ?? {};
5731
+ const projected = await this.resolveGlobalStore(nodeId, cap);
5638
5732
  return hydrateSchema(schema, overlay ? {
5639
- ...raw,
5733
+ ...projected,
5640
5734
  ...overlay
5641
- } : raw);
5735
+ } : projected);
5642
5736
  }
5643
- async updateGlobalSettings(patch, _nodeId) {
5644
- await this._ctx?.settings?.writeAddonStore(patch);
5737
+ /**
5738
+ * The raw addon store PROJECTED onto the target node's bare per-node keys:
5739
+ * every `perNode: true` field carries THAT node's scoped value on its bare
5740
+ * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
5741
+ * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
5742
+ * A no-op passthrough when the schema declares no `perNode` field.
5743
+ *
5744
+ * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
5745
+ * the store for custom option logic (option narrowing, value snapping) to
5746
+ * read it per-node — never `ctx.settings.readAddonStore()` directly.
5747
+ * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
5748
+ */
5749
+ async resolveGlobalStore(nodeId, cap) {
5750
+ const raw = await this._ctx?.settings?.readAddonStore() ?? {};
5751
+ const keys = this.perNodeKeys(cap);
5752
+ const node = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
5753
+ return keys.size > 0 ? projectStore(raw, keys, node) : raw;
5754
+ }
5755
+ async updateGlobalSettings(patch, nodeId) {
5756
+ const keys = this.perNodeKeys();
5757
+ const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
5758
+ const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
5759
+ const barePatch = patch;
5760
+ const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
5761
+ await this._ctx?.settings?.writeAddonStore(scoped);
5762
+ if (target !== localNode) return;
5645
5763
  await this.resolveConfig();
5646
5764
  await this.onConfigChanged();
5647
5765
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
5648
5766
  this.maybeAutoRestart(patch, this.globalSettingsSchema());
5649
5767
  }
5650
5768
  /**
5769
+ * The set of field keys the global settings schema declares `perNode: true`
5770
+ * — derived once per `cap` argument and memoized (schemas are static
5771
+ * declarations). Empty set ⇒ every per-node code path is bypassed and the
5772
+ * settings API behaves exactly like the legacy node-agnostic one.
5773
+ */
5774
+ _perNodeKeysCache = /* @__PURE__ */ new Map();
5775
+ perNodeKeys(cap) {
5776
+ const cacheKey = cap ?? "";
5777
+ const cached = this._perNodeKeysCache.get(cacheKey);
5778
+ if (cached) return cached;
5779
+ const schema = this.globalSettingsSchema(cap);
5780
+ const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
5781
+ this._perNodeKeysCache.set(cacheKey, keys);
5782
+ return keys;
5783
+ }
5784
+ /**
5651
5785
  * If any field in `patch` is marked `requiresRestart` in `schema`,
5652
5786
  * schedule an addon restart for the next tick. Deferred via
5653
5787
  * `setImmediate` so the tRPC mutation that triggered the write has
@@ -5800,12 +5934,19 @@ var BaseAddon = class {
5800
5934
  * The merge is shallow: each key in `defaults` is checked against the store.
5801
5935
  * Only keys present in defaults are read — the store can contain extra keys
5802
5936
  * (e.g. from older versions) without polluting the typed config.
5937
+ *
5938
+ * Keys the global settings schema declares `perNode: true` resolve from
5939
+ * THIS node's scoped key (`<key>@<localNode>`) via `readNodeValue` — never
5940
+ * from the bare key — so a per-node field resolves to this node's own
5941
+ * selection at boot (absent scoped key ⇒ the constructor default wins).
5803
5942
  */
5804
5943
  async resolveConfig() {
5805
5944
  const stored = await this.readAddonStoreWithRetry();
5945
+ const perNode = this.perNodeKeys();
5946
+ const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
5806
5947
  const resolved = { ...this.defaults };
5807
5948
  for (const key of Object.keys(this.defaults)) {
5808
- const storedValue = stored[key];
5949
+ const storedValue = perNode.has(key) ? readNodeValue(stored, key, localNode) : stored[key];
5809
5950
  if (storedValue !== void 0 && storedValue !== null) {
5810
5951
  const defaultType = typeof this.defaults[key];
5811
5952
  if (typeof storedValue === defaultType) resolved[key] = storedValue;
@@ -5889,6 +6030,27 @@ var BaseAddon = class {
5889
6030
  }
5890
6031
  };
5891
6032
  /**
6033
+ * Collect the keys of every field marked `perNode: true`, recursing into
6034
+ * layout containers (`group` fields and `sub-tabs` tabs) the same way
6035
+ * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6036
+ * don't declare `perNode` and are excluded by the `in` narrowing.
6037
+ */
6038
+ function collectPerNodeFieldKeys(fields) {
6039
+ const collected = [];
6040
+ for (const field of fields) {
6041
+ if (field.type === "group") {
6042
+ collected.push(...collectPerNodeFieldKeys(field.fields));
6043
+ continue;
6044
+ }
6045
+ if (field.type === "sub-tabs") {
6046
+ for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
6047
+ continue;
6048
+ }
6049
+ if ("perNode" in field && field.perNode === true) collected.push(field.key);
6050
+ }
6051
+ return collected;
6052
+ }
6053
+ /**
5892
6054
  * Normalize an `ICamstackAddon.initialize()` return value into the
5893
6055
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
5894
6056
  * envelopes pass through; void stays void.
@@ -5913,6 +6075,7 @@ var CamStreamKindSchema = _enum([
5913
6075
  "pull-rtsp",
5914
6076
  "pull-rtmp",
5915
6077
  "pull-http",
6078
+ "pull-flv",
5916
6079
  "pull-rfc4571",
5917
6080
  "push-annexb",
5918
6081
  "derived"
@@ -6295,6 +6458,13 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
6295
6458
  /** Single still-image entity (HA `image.*`). Read-only display of an
6296
6459
  * `entity_picture` signed URL the browser loads directly. `image` cap. */
6297
6460
  DeviceType["Image"] = "image";
6461
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
6462
+ * level, battery, desiccant life, feeding state and manual-feed /
6463
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
6464
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
6465
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
6466
+ * integrations sharing the same food/desiccant/hopper surface. */
6467
+ DeviceType["PetFeeder"] = "pet-feeder";
6298
6468
  return DeviceType;
6299
6469
  }({});
6300
6470
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -7459,6 +7629,601 @@ var _macroLookup = /* @__PURE__ */ new Map();
7459
7629
  for (const [k, v] of Object.entries(YAMNET_TO_MACRO.mapping)) _macroLookup.set(k.toLowerCase(), v);
7460
7630
  for (const [k, v] of Object.entries(APPLE_SA_TO_MACRO.mapping)) _macroLookup.set(k.toLowerCase(), v);
7461
7631
  /**
7632
+ * Error types for the safe expression engine. Two distinct classes so callers
7633
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
7634
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
7635
+ */
7636
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
7637
+ * the failure is anchored to a character (author-facing inline feedback). */
7638
+ var ExpressionParseError = class extends Error {
7639
+ position;
7640
+ constructor(message, position) {
7641
+ super(message);
7642
+ this.name = "ExpressionParseError";
7643
+ this.position = position;
7644
+ }
7645
+ };
7646
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
7647
+ * result, unknown builtin, step-budget exceeded). */
7648
+ var ExpressionEvalError = class extends Error {
7649
+ constructor(message) {
7650
+ super(message);
7651
+ this.name = "ExpressionEvalError";
7652
+ }
7653
+ };
7654
+ /**
7655
+ * Resource-bound constants for the safe expression engine.
7656
+ *
7657
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
7658
+ * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
7659
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
7660
+ * work a single author-supplied expression can request, so a hostile or
7661
+ * accidental pathological string can never spend unbounded CPU/memory.
7662
+ */
7663
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
7664
+ * rejected without allocation. */
7665
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
7666
+ /** A legal binding / identifier name. */
7667
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7668
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
7669
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
7670
+ var RESERVED_BINDING_NAMES = new Set([
7671
+ "now",
7672
+ "true",
7673
+ "false",
7674
+ "null"
7675
+ ]);
7676
+ /**
7677
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
7678
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
7679
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
7680
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
7681
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
7682
+ * is a parse error with a source position, so member access / assignment /
7683
+ * template literals are lexically impossible.
7684
+ */
7685
+ var KEYWORDS = new Set([
7686
+ "true",
7687
+ "false",
7688
+ "null"
7689
+ ]);
7690
+ function isDigit(ch) {
7691
+ return ch >= "0" && ch <= "9";
7692
+ }
7693
+ function isIdentStart(ch) {
7694
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
7695
+ }
7696
+ function isIdentPart(ch) {
7697
+ return isIdentStart(ch) || isDigit(ch);
7698
+ }
7699
+ function isWhitespace(ch) {
7700
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
7701
+ }
7702
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
7703
+ * Throws `ExpressionParseError` on any illegal character or unterminated
7704
+ * string. */
7705
+ function tokenize(source) {
7706
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
7707
+ const tokens = [];
7708
+ let i = 0;
7709
+ const n = source.length;
7710
+ while (i < n) {
7711
+ const ch = source[i];
7712
+ if (isWhitespace(ch)) {
7713
+ i += 1;
7714
+ continue;
7715
+ }
7716
+ if (isDigit(ch)) {
7717
+ const start = i;
7718
+ while (i < n && isDigit(source[i])) i += 1;
7719
+ if (i < n && source[i] === ".") {
7720
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
7721
+ i += 1;
7722
+ while (i < n && isDigit(source[i])) i += 1;
7723
+ }
7724
+ const text = source.slice(start, i);
7725
+ const value = Number(text);
7726
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
7727
+ tokens.push({
7728
+ type: "number",
7729
+ value,
7730
+ pos: start
7731
+ });
7732
+ continue;
7733
+ }
7734
+ if (ch === "'" || ch === "\"") {
7735
+ const quote = ch;
7736
+ const start = i;
7737
+ i += 1;
7738
+ let out = "";
7739
+ let closed = false;
7740
+ while (i < n) {
7741
+ const c = source[i];
7742
+ if (c === "\\") {
7743
+ const next = i + 1 < n ? source[i + 1] : "";
7744
+ if (next === "\\" || next === "'" || next === "\"") {
7745
+ out += next;
7746
+ i += 2;
7747
+ continue;
7748
+ }
7749
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
7750
+ }
7751
+ if (c === quote) {
7752
+ closed = true;
7753
+ i += 1;
7754
+ break;
7755
+ }
7756
+ out += c;
7757
+ i += 1;
7758
+ }
7759
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
7760
+ tokens.push({
7761
+ type: "string",
7762
+ value: out,
7763
+ pos: start
7764
+ });
7765
+ continue;
7766
+ }
7767
+ if (isIdentStart(ch)) {
7768
+ const start = i;
7769
+ while (i < n && isIdentPart(source[i])) i += 1;
7770
+ const text = source.slice(start, i);
7771
+ if (KEYWORDS.has(text)) tokens.push({
7772
+ type: "keyword",
7773
+ keyword: keywordOf(text),
7774
+ pos: start
7775
+ });
7776
+ else tokens.push({
7777
+ type: "identifier",
7778
+ name: text,
7779
+ pos: start
7780
+ });
7781
+ continue;
7782
+ }
7783
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
7784
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
7785
+ tokens.push({
7786
+ type: "punct",
7787
+ punct: two,
7788
+ pos: i
7789
+ });
7790
+ i += 2;
7791
+ continue;
7792
+ }
7793
+ if (isSinglePunct(ch)) {
7794
+ tokens.push({
7795
+ type: "punct",
7796
+ punct: ch,
7797
+ pos: i
7798
+ });
7799
+ i += 1;
7800
+ continue;
7801
+ }
7802
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
7803
+ }
7804
+ tokens.push({
7805
+ type: "eof",
7806
+ pos: n
7807
+ });
7808
+ return tokens;
7809
+ }
7810
+ function keywordOf(text) {
7811
+ if (text === "true") return "true";
7812
+ if (text === "false") return "false";
7813
+ return "null";
7814
+ }
7815
+ function isSinglePunct(ch) {
7816
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
7817
+ }
7818
+ /**
7819
+ * Frozen, null-prototype builtin function table for the expression engine
7820
+ * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
7821
+ * parser rejects any callee not in it, and the evaluator gates each call on an
7822
+ * own-property check against it.
7823
+ *
7824
+ * Because the object has a NULL prototype AND is `Object.freeze`d:
7825
+ * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
7826
+ * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
7827
+ * (there is no `Object.prototype` in the chain), so those names are not
7828
+ * callable — they are simply "unknown function" at parse time.
7829
+ *
7830
+ * Every numeric argument is validated as a finite number and every numeric
7831
+ * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
7832
+ * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
7833
+ * closed rather than emitting a garbage value.
7834
+ */
7835
+ function asFiniteNumber(value, name, index) {
7836
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
7837
+ return value;
7838
+ }
7839
+ function asString$1(value, name, index) {
7840
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
7841
+ return value;
7842
+ }
7843
+ function finiteResult(value, name) {
7844
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
7845
+ return value;
7846
+ }
7847
+ function allFiniteNumbers(args, name) {
7848
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
7849
+ }
7850
+ var INF = Number.POSITIVE_INFINITY;
7851
+ var table = {
7852
+ min: {
7853
+ minArgs: 1,
7854
+ maxArgs: INF,
7855
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
7856
+ },
7857
+ max: {
7858
+ minArgs: 1,
7859
+ maxArgs: INF,
7860
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
7861
+ },
7862
+ abs: {
7863
+ minArgs: 1,
7864
+ maxArgs: 1,
7865
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
7866
+ },
7867
+ floor: {
7868
+ minArgs: 1,
7869
+ maxArgs: 1,
7870
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
7871
+ },
7872
+ ceil: {
7873
+ minArgs: 1,
7874
+ maxArgs: 1,
7875
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
7876
+ },
7877
+ sqrt: {
7878
+ minArgs: 1,
7879
+ maxArgs: 1,
7880
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
7881
+ },
7882
+ round: {
7883
+ minArgs: 1,
7884
+ maxArgs: 2,
7885
+ apply: (args) => {
7886
+ const x = asFiniteNumber(args[0], "round", 0);
7887
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
7888
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
7889
+ const factor = 10 ** digits;
7890
+ return finiteResult(Math.round(x * factor) / factor, "round");
7891
+ }
7892
+ },
7893
+ pow: {
7894
+ minArgs: 2,
7895
+ maxArgs: 2,
7896
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
7897
+ },
7898
+ clamp: {
7899
+ minArgs: 3,
7900
+ maxArgs: 3,
7901
+ apply: (args) => {
7902
+ const x = asFiniteNumber(args[0], "clamp", 0);
7903
+ const lo = asFiniteNumber(args[1], "clamp", 1);
7904
+ const hi = asFiniteNumber(args[2], "clamp", 2);
7905
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
7906
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
7907
+ }
7908
+ },
7909
+ avg: {
7910
+ minArgs: 1,
7911
+ maxArgs: INF,
7912
+ apply: (args) => {
7913
+ const nums = allFiniteNumbers(args, "avg");
7914
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
7915
+ }
7916
+ },
7917
+ sum: {
7918
+ minArgs: 1,
7919
+ maxArgs: INF,
7920
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
7921
+ },
7922
+ coalesce: {
7923
+ minArgs: 1,
7924
+ maxArgs: INF,
7925
+ apply: (args) => {
7926
+ for (const a of args) if (a !== null) return a;
7927
+ return null;
7928
+ }
7929
+ },
7930
+ age: {
7931
+ minArgs: 2,
7932
+ maxArgs: 2,
7933
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
7934
+ },
7935
+ convert: {
7936
+ minArgs: 3,
7937
+ maxArgs: 3,
7938
+ apply: (args, hooks) => {
7939
+ const x = asFiniteNumber(args[0], "convert", 0);
7940
+ const from = asString$1(args[1], "convert", 1).trim();
7941
+ const to = asString$1(args[2], "convert", 2).trim();
7942
+ if (hooks.convert) {
7943
+ const out = hooks.convert(x, from, to);
7944
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
7945
+ return finiteResult(out, "convert");
7946
+ }
7947
+ if (from === to) return x;
7948
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
7949
+ }
7950
+ }
7951
+ };
7952
+ Object.freeze(Object.assign(Object.create(null), table));
7953
+ /** The set of valid builtin names — used by the parser to reject unknown
7954
+ * callees at parse time (immediate author feedback). */
7955
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
7956
+ /**
7957
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
7958
+ *
7959
+ * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
7960
+ * → relational → additive → multiplicative → unary `! -` → call / primary.
7961
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
7962
+ * string validated against the builtin table at parse time, so an unknown
7963
+ * function is rejected immediately (author feedback) and a persisted expression
7964
+ * that references a since-removed builtin degrades at read.
7965
+ *
7966
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
7967
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
7968
+ */
7969
+ /** Binary/logical operator precedence (higher binds tighter). */
7970
+ var BINARY_PRECEDENCE = {
7971
+ "||": 1,
7972
+ "&&": 2,
7973
+ "==": 3,
7974
+ "!=": 3,
7975
+ "<": 4,
7976
+ "<=": 4,
7977
+ ">": 4,
7978
+ ">=": 4,
7979
+ "+": 5,
7980
+ "-": 5,
7981
+ "*": 6,
7982
+ "/": 6,
7983
+ "%": 6
7984
+ };
7985
+ function isLogicalOp(op) {
7986
+ return op === "&&" || op === "||";
7987
+ }
7988
+ function isBinaryOp(op) {
7989
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
7990
+ }
7991
+ var Parser = class {
7992
+ tokens;
7993
+ pos = 0;
7994
+ nodeCount = 0;
7995
+ identifiers = /* @__PURE__ */ new Set();
7996
+ callees = /* @__PURE__ */ new Set();
7997
+ constructor(tokens) {
7998
+ this.tokens = tokens;
7999
+ }
8000
+ parse() {
8001
+ const ast = this.parseTernary();
8002
+ const tok = this.peek();
8003
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
8004
+ return {
8005
+ ast,
8006
+ identifiers: this.identifiers,
8007
+ callees: this.callees,
8008
+ nodeCount: this.nodeCount
8009
+ };
8010
+ }
8011
+ peek() {
8012
+ return this.tokens[this.pos];
8013
+ }
8014
+ next() {
8015
+ return this.tokens[this.pos++];
8016
+ }
8017
+ /** Consume a punctuator token, erroring if the next token isn't it. */
8018
+ expectPunct(punct) {
8019
+ const tok = this.peek();
8020
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
8021
+ this.pos += 1;
8022
+ }
8023
+ matchPunct(punct) {
8024
+ const tok = this.peek();
8025
+ if (tok.type === "punct" && tok.punct === punct) {
8026
+ this.pos += 1;
8027
+ return true;
8028
+ }
8029
+ return false;
8030
+ }
8031
+ countNode() {
8032
+ this.nodeCount += 1;
8033
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
8034
+ }
8035
+ parseTernary() {
8036
+ const test = this.parseBinary(1);
8037
+ if (this.matchPunct("?")) {
8038
+ const consequent = this.parseTernary();
8039
+ this.expectPunct(":");
8040
+ const alternate = this.parseTernary();
8041
+ this.countNode();
8042
+ return {
8043
+ kind: "conditional",
8044
+ test,
8045
+ consequent,
8046
+ alternate
8047
+ };
8048
+ }
8049
+ return test;
8050
+ }
8051
+ parseBinary(minPrec) {
8052
+ let left = this.parseUnary();
8053
+ for (;;) {
8054
+ const tok = this.peek();
8055
+ if (tok.type !== "punct") break;
8056
+ const prec = BINARY_PRECEDENCE[tok.punct];
8057
+ if (prec === void 0 || prec < minPrec) break;
8058
+ const op = tok.punct;
8059
+ this.pos += 1;
8060
+ const right = this.parseBinary(prec + 1);
8061
+ this.countNode();
8062
+ if (isLogicalOp(op)) left = {
8063
+ kind: "logical",
8064
+ op,
8065
+ left,
8066
+ right
8067
+ };
8068
+ else if (isBinaryOp(op)) left = {
8069
+ kind: "binary",
8070
+ op,
8071
+ left,
8072
+ right
8073
+ };
8074
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
8075
+ }
8076
+ return left;
8077
+ }
8078
+ parseUnary() {
8079
+ const tok = this.peek();
8080
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
8081
+ const op = tok.punct;
8082
+ this.pos += 1;
8083
+ const operand = this.parseUnary();
8084
+ this.countNode();
8085
+ return {
8086
+ kind: "unary",
8087
+ op,
8088
+ operand
8089
+ };
8090
+ }
8091
+ return this.parsePrimary();
8092
+ }
8093
+ parsePrimary() {
8094
+ const tok = this.next();
8095
+ switch (tok.type) {
8096
+ case "number":
8097
+ this.countNode();
8098
+ return {
8099
+ kind: "literal",
8100
+ value: tok.value
8101
+ };
8102
+ case "string":
8103
+ this.countNode();
8104
+ return {
8105
+ kind: "literal",
8106
+ value: tok.value
8107
+ };
8108
+ case "keyword":
8109
+ this.countNode();
8110
+ return {
8111
+ kind: "literal",
8112
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
8113
+ };
8114
+ case "identifier": {
8115
+ const nextTok = this.peek();
8116
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
8117
+ this.identifiers.add(tok.name);
8118
+ this.countNode();
8119
+ return {
8120
+ kind: "identifier",
8121
+ name: tok.name
8122
+ };
8123
+ }
8124
+ case "punct":
8125
+ if (tok.punct === "(") {
8126
+ const inner = this.parseTernary();
8127
+ this.expectPunct(")");
8128
+ return inner;
8129
+ }
8130
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
8131
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
8132
+ }
8133
+ }
8134
+ parseCall(callee, pos) {
8135
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
8136
+ this.expectPunct("(");
8137
+ const args = [];
8138
+ if (!this.matchPunct(")")) for (;;) {
8139
+ args.push(this.parseTernary());
8140
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
8141
+ if (this.matchPunct(",")) continue;
8142
+ this.expectPunct(")");
8143
+ break;
8144
+ }
8145
+ this.callees.add(callee);
8146
+ this.countNode();
8147
+ return {
8148
+ kind: "call",
8149
+ callee,
8150
+ args
8151
+ };
8152
+ }
8153
+ };
8154
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
8155
+ * `ExpressionParseError` on any lexical or grammatical failure. */
8156
+ function parseExpression(source) {
8157
+ return new Parser(tokenize(source)).parse();
8158
+ }
8159
+ Object.freeze({});
8160
+ /**
8161
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
8162
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
8163
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
8164
+ * one per read on a hot resolve path.
8165
+ *
8166
+ * The cache is a module-level singleton: entries are pure, content-addressed
8167
+ * ASTs keyed by the raw source string, so sharing one instance across all
8168
+ * callers is safe and maximises hit rate.
8169
+ */
8170
+ var cache = /* @__PURE__ */ new Map();
8171
+ function getCached(source) {
8172
+ const hit = cache.get(source);
8173
+ if (hit !== void 0) {
8174
+ cache.delete(source);
8175
+ cache.set(source, hit);
8176
+ return hit;
8177
+ }
8178
+ let result;
8179
+ try {
8180
+ result = {
8181
+ ok: true,
8182
+ parsed: parseExpression(source)
8183
+ };
8184
+ } catch (err) {
8185
+ result = {
8186
+ ok: false,
8187
+ error: err instanceof ExpressionParseError ? err.message : String(err)
8188
+ };
8189
+ }
8190
+ cache.set(source, result);
8191
+ if (cache.size > 256) {
8192
+ const oldest = cache.keys().next().value;
8193
+ if (oldest !== void 0) cache.delete(oldest);
8194
+ }
8195
+ return result;
8196
+ }
8197
+ /** Compile `source`, returning a discriminated result instead of throwing.
8198
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
8199
+ function compileExpressionSafe(source) {
8200
+ return getCached(source);
8201
+ }
8202
+ /**
8203
+ * Author-time validation. Returns `null` when the source is valid, else a
8204
+ * human-readable error message. Checks: the expression compiles; binding count
8205
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
8206
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
8207
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
8208
+ */
8209
+ function validateExpressionSource(src) {
8210
+ const names = Object.keys(src.bindings);
8211
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
8212
+ for (const name of names) {
8213
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
8214
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
8215
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
8216
+ }
8217
+ const compiled = compileExpressionSafe(src.expr);
8218
+ if (!compiled.ok) return compiled.error;
8219
+ const bound = new Set(names);
8220
+ for (const id of compiled.parsed.identifiers) {
8221
+ if (id === "now") continue;
8222
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
8223
+ }
8224
+ return null;
8225
+ }
8226
+ /**
7462
8227
  * Accessory device helpers — shared across drivers.
7463
8228
  *
7464
8229
  * Many vendor-specific drivers register accessory child devices on
@@ -8303,7 +9068,13 @@ onStatusChanged: { data: object({
8303
9068
  }) } },
8304
9069
  status: {
8305
9070
  schema: BatteryStatusSchema,
8306
- kind: "push"
9071
+ kind: "push",
9072
+ empty: {
9073
+ percentage: 0,
9074
+ charging: "none",
9075
+ sleeping: false,
9076
+ lastUpdated: 0
9077
+ }
8307
9078
  },
8308
9079
  /**
8309
9080
  * Runtime-state slice — every provider that registers this cap
@@ -9246,21 +10017,38 @@ var connectivityCapability = {
9246
10017
  },
9247
10018
  runtimeState: ConnectivityStatusSchema
9248
10019
  };
10020
+ /**
10021
+ * Generic device-consumables capability — surfaces a device's
10022
+ * maintenance items (vacuum filters/brushes, replaceable cartridges,
10023
+ * descaling cycles, …) with their remaining life and an optional
10024
+ * "Replaced" reset action. Device-agnostic: any provider that knows its
10025
+ * device tracks consumables can register it; the cap declares no
10026
+ * vocabulary of its own — the provider names each item verbatim.
10027
+ *
10028
+ * Like `childLayout`, the cap is INERT until a provider sets items: no
10029
+ * provider populates it by guessing (no HA inference). The UI renders a
10030
+ * "No consumables reported" placeholder when `items` is empty.
10031
+ */
10032
+ /** A single consumable item. Either a continuous `level` (remaining
10033
+ * life %) or a discrete `status` may be known — both may be null when a
10034
+ * provider only knows the item exists. `level` and `status` are not
10035
+ * mutually exclusive; a provider may report both. */
10036
+ var ConsumableItemSchema = object({
10037
+ /** Stable id, e.g. 'main-brush'. */
10038
+ key: string().min(1),
10039
+ /** Display name. */
10040
+ label: string().min(1),
10041
+ /** Remaining life % when known (0..100). */
10042
+ level: number().min(0).max(100).nullable(),
10043
+ /** Discrete state when known (binary mode). */
10044
+ status: _enum(["ok", "replace"]).nullable(),
10045
+ /** Ms epoch of the last replace, when known. */
10046
+ lastResetAt: number().nullable(),
10047
+ /** Whether `reset()` is meaningful for this item. */
10048
+ resettable: boolean()
10049
+ });
9249
10050
  var ConsumablesStatusSchema = object({
9250
- items: array(object({
9251
- /** Stable id, e.g. 'main-brush'. */
9252
- key: string().min(1),
9253
- /** Display name. */
9254
- label: string().min(1),
9255
- /** Remaining life % when known (0..100). */
9256
- level: number().min(0).max(100).nullable(),
9257
- /** Discrete state when known (binary mode). */
9258
- status: _enum(["ok", "replace"]).nullable(),
9259
- /** Ms epoch of the last replace, when known. */
9260
- lastResetAt: number().nullable(),
9261
- /** Whether `reset()` is meaningful for this item. */
9262
- resettable: boolean()
9263
- })),
10051
+ items: array(ConsumableItemSchema),
9264
10052
  lastChangedAt: number()
9265
10053
  });
9266
10054
  var consumablesCapability = {
@@ -9319,7 +10107,25 @@ reset: method(object({
9319
10107
  }) },
9320
10108
  status: {
9321
10109
  schema: ConsumablesStatusSchema,
9322
- kind: "push"
10110
+ kind: "push",
10111
+ empty: {
10112
+ items: [],
10113
+ lastChangedAt: 0
10114
+ },
10115
+ itemArray: {
10116
+ path: "items",
10117
+ keyField: "key",
10118
+ labelField: "label",
10119
+ itemSchema: ConsumableItemSchema,
10120
+ emptyItem: {
10121
+ key: "",
10122
+ label: "",
10123
+ level: null,
10124
+ status: null,
10125
+ lastResetAt: null,
10126
+ resettable: false
10127
+ }
10128
+ }
9323
10129
  },
9324
10130
  runtimeState: ConsumablesStatusSchema.extend({ lastFetchedAt: number() })
9325
10131
  };
@@ -10561,7 +11367,8 @@ var MotionAnalysisResultSchema = object({
10561
11367
  });
10562
11368
  method(object({
10563
11369
  deviceId: number(),
10564
- frame: FrameInputSchema
11370
+ frame: FrameInputSchema.optional(),
11371
+ frameHandle: FrameHandleSchema.optional()
10565
11372
  }), MotionAnalysisResultSchema, { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), object({
10566
11373
  deviceId: number(),
10567
11374
  detected: boolean(),
@@ -10808,6 +11615,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
10808
11615
  engine: PipelineEngineChoiceSchema.optional(),
10809
11616
  steps: array(PipelineStepInputSchema).min(1),
10810
11617
  frame: FrameInputSchema.optional(),
11618
+ /**
11619
+ * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
11620
+ * the decoded pixels live in. One more member of the one-of
11621
+ * frame/frameHandle/image/imageBase64/referenceImage group.
11622
+ */
11623
+ frameHandle: FrameHandleSchema.optional(),
10811
11624
  imageBase64: string().optional(),
10812
11625
  /**
10813
11626
  * Binary JPEG bytes — preferred over `imageBase64` on internal
@@ -11050,6 +11863,31 @@ var ReportMotionInputSchema = object({
11050
11863
  regions: array(MotionRegionSchema).readonly().optional()
11051
11864
  });
11052
11865
  /**
11866
+ * Where a runner gets a camera's decoded frames (cross-node Phase 2,
11867
+ * restream-owner model — P2c).
11868
+ *
11869
+ * - `local-broker` (DEFAULT): today's path — subscribe to the co-located
11870
+ * stream-broker's shm frame plane. Every pre-P2c attach payload (no
11871
+ * `frameSource` key) parses to this, so the field is additive with zero
11872
+ * behavior change.
11873
+ * - `remote-restream`: the detect node is NOT the camera's source-owner.
11874
+ * The runner acquires the owner's COMPRESSED passthrough restream
11875
+ * (`streamBroker.getStreamWithCodec({video:'copy'})` — refcounted, the
11876
+ * double-pull guard) and decodes LOCALLY via a satellite frame plane +
11877
+ * pull-mode decoder session pinned to its own node. The shm ring stays
11878
+ * node-local; only H.264/H.265 packets cross the wire.
11879
+ * `hubHostnameOverride` mirrors the recorder's `recordingHubHostname`:
11880
+ * when set it overrides the `CAMSTACK_HUB_URL`-derived host the runner
11881
+ * dials for the owner's restream.
11882
+ */
11883
+ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal("local-broker") }), object({
11884
+ kind: literal("remote-restream"),
11885
+ /** The camera's source-owner node (slice 1: always the hub). */
11886
+ ownerNodeId: string(),
11887
+ /** Operator override for the owner host the runner dials. */
11888
+ hubHostnameOverride: string().optional()
11889
+ })]).describe("Per-camera frame-source mode for the runner (P2c)");
11890
+ /**
11053
11891
  * Camera assignment payload sent by `addon-pipeline-orchestrator` to a
11054
11892
  * specific runner instance via `attachCamera`. Carries everything the
11055
11893
  * runner needs to subscribe to the local broker and execute inference.
@@ -11147,7 +11985,15 @@ var RunnerCameraConfigSchema = object({
11147
11985
  */
11148
11986
  onboardMotionDrivesAnalyzer: boolean().default(true),
11149
11987
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
11150
- occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default)
11988
+ occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
11989
+ /**
11990
+ * Where this runner gets the camera's decoded frames (P2c). Defaulted so
11991
+ * every existing payload behaves as `local-broker` — the pre-Phase-2 path.
11992
+ * Populated with `remote-restream` by the orchestrator ONLY when the
11993
+ * camera's detect node differs from its source-owner (P2d, gated by the
11994
+ * `remoteSourcingNodes` rollout setting).
11995
+ */
11996
+ frameSource: RunnerFrameSourceSchema.default({ kind: "local-broker" })
11151
11997
  });
11152
11998
  motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
11153
11999
  /**
@@ -11711,6 +12557,157 @@ var numericSensorCapability = {
11711
12557
  runtimeState: NumericSensorStatusSchema
11712
12558
  };
11713
12559
  /**
12560
+ * Feeder connectivity / power status — mirrors the HA petkit device-status
12561
+ * enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
12562
+ * `on_batteries` (running on battery backup). `null` until first reported.
12563
+ */
12564
+ var PetFeederDeviceStatusSchema = _enum([
12565
+ "normal",
12566
+ "offline",
12567
+ "on_batteries"
12568
+ ]);
12569
+ var gramsPortion = number().int().min(4).max(200);
12570
+ var PetFeederStatusSchema = object({
12571
+ /** Food currently in the bowl (grams). Null when the device has not
12572
+ * reported a reading yet. On dual-hopper models this is the combined
12573
+ * bowl reading; per-hopper levels live in `food1`/`food2`. */
12574
+ foodLevel: number().nullable(),
12575
+ /** Hopper-1 food level (grams) on dual-hopper feeders; null on
12576
+ * single-hopper models. */
12577
+ food1: number().nullable(),
12578
+ /** Hopper-2 food level (grams) on dual-hopper feeders; null on
12579
+ * single-hopper models. */
12580
+ food2: number().nullable(),
12581
+ /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
12582
+ * (`device_class: problem`, on = low). True when the bowl is empty /
12583
+ * below the feeder's low threshold. */
12584
+ lowFood: boolean(),
12585
+ /** Battery charge 0..100 (%). Null on mains-powered models or when the
12586
+ * device has no battery reading. */
12587
+ batteryPower: number().min(0).max(100).nullable(),
12588
+ /** Days of desiccant life remaining. Null when the model has no
12589
+ * desiccant sensor. */
12590
+ desiccantLeftDays: number().nullable(),
12591
+ /** True while a feed is in progress. */
12592
+ feeding: boolean(),
12593
+ /** Decoded connectivity / power status (HA petkit device-status enum).
12594
+ * Null until the device has reported a status. */
12595
+ status: PetFeederDeviceStatusSchema.nullable(),
12596
+ /** Decoded human-readable fault message. Null (or the device's `no_error`
12597
+ * sentinel) means healthy; a non-null string is an active fault. Pairs
12598
+ * with `errorCode` for consumers that want the raw integer. */
12599
+ error: string().nullable(),
12600
+ /** Raw device error code (0 / null = no error). */
12601
+ errorCode: number().nullable(),
12602
+ /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
12603
+ isDualHopper: boolean(),
12604
+ /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
12605
+ childLock: boolean(),
12606
+ /** Front indicator-light setting. */
12607
+ indicatorLight: boolean(),
12608
+ /** Play a chime when dispensing. */
12609
+ feedSound: boolean(),
12610
+ /** Speaker / prompt volume level (device-scaled integer). */
12611
+ volume: number(),
12612
+ /** Ms epoch when the slice was last refreshed from the cloud. */
12613
+ lastFetchedAt: number()
12614
+ });
12615
+ var petFeederCapability = {
12616
+ name: "pet-feeder",
12617
+ scope: "device",
12618
+ deviceNative: true,
12619
+ mode: "singleton",
12620
+ deviceTypes: [DeviceType.PetFeeder],
12621
+ methods: {
12622
+ /**
12623
+ * Dispense food now. Single-hopper feeders take `grams`; dual-hopper
12624
+ * feeders (D4S/D4SH) accept `hopper1`/`hopper2` to target one or both
12625
+ * hoppers. All portions honour the 4–200 g hardware range. At least
12626
+ * one of the three must be present — the provider rejects an empty
12627
+ * request.
12628
+ */
12629
+ feed: method(object({
12630
+ deviceId: number().int().nonnegative(),
12631
+ grams: gramsPortion.optional(),
12632
+ hopper1: gramsPortion.optional(),
12633
+ hopper2: gramsPortion.optional()
12634
+ }), _void(), {
12635
+ kind: "mutation",
12636
+ auth: "admin"
12637
+ }),
12638
+ /** Cancel an in-progress manual feed. */
12639
+ cancelFeed: method(object({ deviceId: number().int().nonnegative() }), _void(), {
12640
+ kind: "mutation",
12641
+ auth: "admin"
12642
+ }),
12643
+ /** Reset the desiccant "days remaining" counter after replacing it. */
12644
+ resetDesiccant: method(object({ deviceId: number().int().nonnegative() }), _void(), {
12645
+ kind: "mutation",
12646
+ auth: "admin"
12647
+ }),
12648
+ /** Mark a hopper as refilled (D4H/D4S/D4SH). */
12649
+ markFoodReplenished: method(object({ deviceId: number().int().nonnegative() }), _void(), {
12650
+ kind: "mutation",
12651
+ auth: "admin"
12652
+ }),
12653
+ /** Call the pet with the recorded prompt (D3). */
12654
+ callPet: method(object({ deviceId: number().int().nonnegative() }), _void(), {
12655
+ kind: "mutation",
12656
+ auth: "admin"
12657
+ }),
12658
+ /** Play a stored sound by id (D3 / D4H / D4SH). */
12659
+ playSound: method(object({
12660
+ deviceId: number().int().nonnegative(),
12661
+ soundId: number().int().nonnegative()
12662
+ }), _void(), {
12663
+ kind: "mutation",
12664
+ auth: "admin"
12665
+ }),
12666
+ /** Toggle the child-lock (manual-lock) setting. */
12667
+ setChildLock: method(object({
12668
+ deviceId: number().int().nonnegative(),
12669
+ on: boolean()
12670
+ }), _void(), {
12671
+ kind: "mutation",
12672
+ auth: "admin"
12673
+ }),
12674
+ /** Toggle the front indicator light. */
12675
+ setIndicatorLight: method(object({
12676
+ deviceId: number().int().nonnegative(),
12677
+ on: boolean()
12678
+ }), _void(), {
12679
+ kind: "mutation",
12680
+ auth: "admin"
12681
+ }),
12682
+ /** Toggle the dispense chime. */
12683
+ setFeedSound: method(object({
12684
+ deviceId: number().int().nonnegative(),
12685
+ on: boolean()
12686
+ }), _void(), {
12687
+ kind: "mutation",
12688
+ auth: "admin"
12689
+ }),
12690
+ /** Set the speaker / prompt volume level. */
12691
+ setVolume: method(object({
12692
+ deviceId: number().int().nonnegative(),
12693
+ level: number().int().nonnegative()
12694
+ }), _void(), {
12695
+ kind: "mutation",
12696
+ auth: "admin"
12697
+ })
12698
+ },
12699
+ status: {
12700
+ schema: PetFeederStatusSchema,
12701
+ kind: "poll"
12702
+ },
12703
+ /**
12704
+ * Runtime-state slice — mirrored by the kernel. UI feeder cards read
12705
+ * the full slice via `device.state.petFeeder.value` and refresh on
12706
+ * every poll without re-querying the provider.
12707
+ */
12708
+ runtimeState: PetFeederStatusSchema
12709
+ };
12710
+ /**
11714
12711
  * Multi-metric electrical meter. One slice can carry any combination
11715
12712
  * of instantaneous power (W), cumulative energy (kWh), voltage (V),
11716
12713
  * and current (A) — all fields optional so a single-metric source
@@ -13013,6 +14010,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
13013
14010
  nativeObjectDetection: nativeObjectDetectionCapability,
13014
14011
  notifier: notifierCapability,
13015
14012
  numericSensor: numericSensorCapability,
14013
+ petFeeder: petFeederCapability,
13016
14014
  powerMeter: powerMeterCapability,
13017
14015
  presence: presenceCapability,
13018
14016
  pressureSensor: pressureSensorCapability,
@@ -14978,10 +15976,12 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
14978
15976
  url: string()
14979
15977
  }), _void()), method(object({
14980
15978
  sessionId: string(),
14981
- maxCount: number().default(1)
15979
+ maxCount: number().default(1),
15980
+ waitMs: number().optional()
14982
15981
  }), array(DecodedFrameSchema)), method(object({
14983
15982
  sessionId: string(),
14984
- maxCount: number().default(1)
15983
+ maxCount: number().default(1),
15984
+ waitMs: number().optional()
14985
15985
  }), array(FrameHandleSchema)), method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()), method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()), method(object({
14986
15986
  sessionId: string(),
14987
15987
  config: DecoderSessionConfigSchema.partial()
@@ -15285,14 +16285,63 @@ var ChildLayoutEntrySchema = object({
15285
16285
  collapsed: boolean().optional()
15286
16286
  });
15287
16287
  /** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
15288
- * `device-management.ts`. */
16288
+ * `device-management.ts`. Source is a union: a FIELD source copies a sibling
16289
+ * accessory's status field (`kind` optional/absent for wire compat); a
16290
+ * LITERAL source carries a per-device constant (no sibling is read); a
16291
+ * GLOBAL source (P2e) copies ANY device's status field, addressed by the
16292
+ * source device's full re-sync-stable `stableId`. */
16293
+ var DeviceLinkFieldSourceSchema = object({
16294
+ kind: literal("field").optional(),
16295
+ sourceKey: string(),
16296
+ cap: string(),
16297
+ fieldPath: string()
16298
+ });
16299
+ var DeviceLinkLiteralSourceSchema = object({
16300
+ kind: literal("literal"),
16301
+ value: union([
16302
+ string(),
16303
+ number(),
16304
+ boolean(),
16305
+ _null()
16306
+ ])
16307
+ });
16308
+ var DeviceLinkGlobalSourceSchema = object({
16309
+ kind: literal("global"),
16310
+ sourceStableId: string(),
16311
+ cap: string(),
16312
+ fieldPath: string()
16313
+ });
16314
+ /** Expression source (Stage X): compute the target field from N named bindings
16315
+ * via the safe expression engine. Bindings are field | literal | global — never
16316
+ * another expression (no nesting). The `superRefine` runs the SAME author-time
16317
+ * validation as `validateExpressionSource` (compiles the expr, checks binding
16318
+ * names + identifier coverage) so every wire boundary that parses a DeviceLink
16319
+ * (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
16320
+ * Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
16321
+ var DeviceLinkExpressionSourceSchema = object({
16322
+ kind: literal("expression"),
16323
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
16324
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
16325
+ DeviceLinkFieldSourceSchema,
16326
+ DeviceLinkLiteralSourceSchema,
16327
+ DeviceLinkGlobalSourceSchema
16328
+ ]))
16329
+ }).superRefine((src, ctx) => {
16330
+ const err = validateExpressionSource(src);
16331
+ if (err !== null) ctx.addIssue({
16332
+ code: "custom",
16333
+ message: err,
16334
+ path: ["expr"]
16335
+ });
16336
+ });
15289
16337
  var DeviceLinkSchema = object({
15290
16338
  id: string(),
15291
- source: object({
15292
- sourceKey: string(),
15293
- cap: string(),
15294
- fieldPath: string()
15295
- }),
16339
+ source: union([
16340
+ DeviceLinkFieldSourceSchema,
16341
+ DeviceLinkLiteralSourceSchema,
16342
+ DeviceLinkGlobalSourceSchema,
16343
+ DeviceLinkExpressionSourceSchema
16344
+ ]),
15296
16345
  target: object({
15297
16346
  cap: string(),
15298
16347
  fieldPath: string(),
@@ -15321,6 +16370,31 @@ var DeviceLinkSchema = object({
15321
16370
  })
15322
16371
  ]).optional()
15323
16372
  });
16373
+ /** Cap-wire shape of a per-cap display refinement — mirrors
16374
+ * `DeviceCapDisplayOverride` in `device-management.ts`. */
16375
+ var DeviceCapDisplayOverrideSchema = object({
16376
+ unit: string().min(1).optional(),
16377
+ precision: number().int().min(0).max(10).optional()
16378
+ });
16379
+ /** Cap-wire shape of an operator-authored per-device display override —
16380
+ * mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
16381
+ * bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
16382
+ var DeviceDisplayOverrideSchema = object({
16383
+ icon: string().min(1).optional(),
16384
+ label: string().min(1).optional(),
16385
+ unit: string().min(1).optional(),
16386
+ precision: number().int().min(0).max(10).optional(),
16387
+ hidden: boolean().optional(),
16388
+ perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
16389
+ });
16390
+ /** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
16391
+ * in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
16392
+ * the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
16393
+ var RoleDisplayDefaultSchema = object({
16394
+ unit: string().min(1).optional(),
16395
+ precision: number().int().min(0).max(10).optional(),
16396
+ icon: string().min(1).optional()
16397
+ });
15324
16398
  /**
15325
16399
  * Serializable projection of a live IDevice.
15326
16400
  * Returned by listAll, getDevice, getChildren.
@@ -15376,7 +16450,9 @@ var DeviceInfoSchema = object({
15376
16450
  * `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
15377
16451
  childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
15378
16452
  /** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
15379
- deviceLinks: array(DeviceLinkSchema).readonly().optional()
16453
+ deviceLinks: array(DeviceLinkSchema).readonly().optional(),
16454
+ /** Operator-authored per-device display override. See `DeviceMeta.display`. */
16455
+ display: DeviceDisplayOverrideSchema.optional()
15380
16456
  });
15381
16457
  var ConfigEntrySchema = object({
15382
16458
  key: string(),
@@ -15441,7 +16517,9 @@ var DeviceMetaSchema = object({
15441
16517
  deviceLinks: array(DeviceLinkSchema).readonly().optional(),
15442
16518
  /** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
15443
16519
  * Optional: only present for accessory children that carry a known role. */
15444
- role: string().nullable().optional()
16520
+ role: string().nullable().optional(),
16521
+ /** Operator-authored per-device display override. See `DeviceMeta.display`. */
16522
+ display: DeviceDisplayOverrideSchema.optional()
15445
16523
  });
15446
16524
  /** ConfigUISchema passed through as unknown — mirrors device-provider's CreationSchemaOutputSchema */
15447
16525
  var ConfigUISchemaOutput = unknown().nullable();
@@ -15535,7 +16613,19 @@ method(object({
15535
16613
  }), _void(), {
15536
16614
  kind: "mutation",
15537
16615
  auth: "admin"
15538
- }), method(object({ deviceId: number() }), object({ caps: array(object({
16616
+ }), method(object({
16617
+ deviceId: number(),
16618
+ display: DeviceDisplayOverrideSchema.nullable()
16619
+ }), _void(), {
16620
+ kind: "mutation",
16621
+ auth: "admin"
16622
+ }), method(object({}), object({ defaults: record(string(), RoleDisplayDefaultSchema) }), { kind: "query" }), method(object({ defaults: record(string(), RoleDisplayDefaultSchema) }), _void(), {
16623
+ kind: "mutation",
16624
+ auth: "admin"
16625
+ }), method(object({
16626
+ deviceId: number(),
16627
+ includeSynthesizable: boolean().optional()
16628
+ }), object({ caps: array(object({
15539
16629
  cap: string(),
15540
16630
  fields: array(object({
15541
16631
  path: string(),
@@ -15545,8 +16635,13 @@ method(object({
15545
16635
  "boolean",
15546
16636
  "enum"
15547
16637
  ]),
15548
- enumValues: array(string()).optional()
15549
- })).readonly()
16638
+ enumValues: array(string()).optional(),
16639
+ item: boolean().optional()
16640
+ })).readonly(),
16641
+ itemArray: object({
16642
+ path: string(),
16643
+ keyField: string()
16644
+ }).optional()
15550
16645
  })).readonly() }), { kind: "query" }), method(object({
15551
16646
  deviceId: number(),
15552
16647
  role: string().nullable()
@@ -15616,7 +16711,11 @@ method(object({
15616
16711
  deviceId: number(),
15617
16712
  entries: array(object({
15618
16713
  capName: string(),
15619
- kind: _enum(["native", "wrapped"]),
16714
+ kind: _enum([
16715
+ "native",
16716
+ "wrapped",
16717
+ "linked"
16718
+ ]),
15620
16719
  providerAddonId: string(),
15621
16720
  providerNodeId: string(),
15622
16721
  nativeAddonId: string()
@@ -15625,7 +16724,11 @@ method(object({
15625
16724
  deviceId: number(),
15626
16725
  entries: array(object({
15627
16726
  capName: string(),
15628
- kind: _enum(["native", "wrapped"]),
16727
+ kind: _enum([
16728
+ "native",
16729
+ "wrapped",
16730
+ "linked"
16731
+ ]),
15629
16732
  providerAddonId: string(),
15630
16733
  providerNodeId: string(),
15631
16734
  nativeAddonId: string()
@@ -16115,7 +17218,7 @@ var AddBrokerInputSchema = object({
16115
17218
  });
16116
17219
  var AddBrokerResultSchema = object({ id: string() });
16117
17220
  var IdInputSchema = object({ id: string() });
16118
- var TestResultSchema = discriminatedUnion("ok", [object({
17221
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
16119
17222
  ok: literal(true),
16120
17223
  latencyMs: number()
16121
17224
  }), object({
@@ -16138,7 +17241,7 @@ var StatusSchema = object({
16138
17241
  brokerCount: number(),
16139
17242
  embeddedRunning: boolean()
16140
17243
  });
16141
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
17244
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
16142
17245
  var NetworkEndpointSchema = object({
16143
17246
  url: string(),
16144
17247
  hostname: string(),
@@ -16172,23 +17275,198 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16172
17275
  sourcePort: number().optional()
16173
17276
  });
16174
17277
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16175
- method(object({
16176
- title: string(),
17278
+ /**
17279
+ * notification-output — canonical, capability-gated notification delivery.
17280
+ *
17281
+ * Apprise-derived model (see
17282
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
17283
+ * callers emit ONE canonical `Notification`; each provider declares a
17284
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
17285
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
17286
+ * message to what the kind supports — callers never special-case a service.
17287
+ *
17288
+ * DESIGN DECISIONS (locked):
17289
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
17290
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
17291
+ * cap. Rationale: the admin UI needs one uniform surface across the
17292
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
17293
+ * alternative would fork the UI per addon and cannot host the
17294
+ * discovery→adopt flow.
17295
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
17296
+ * the generated cap-mount auto-`concatCollection`-fans them across every
17297
+ * registered provider (notifiers addon + HA addon) so one catalog is
17298
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
17299
+ * `addonId` the generated collection router extracts from the call input.
17300
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
17301
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
17302
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
17303
+ * base64 fallback needed.
17304
+ *
17305
+ * TODO (deferred, closed-set change — separate decision): add
17306
+ * `providerKind: 'notify'` so notification providers surface on the unified
17307
+ * admin "Integrations" page.
17308
+ */
17309
+ /**
17310
+ * Zentik-derived typed-media enum — the superset across every kind. Each
17311
+ * adapter picks what it supports and the degrade engine filters the rest.
17312
+ */
17313
+ var AttachmentMediaTypeSchema = _enum([
17314
+ "image",
17315
+ "video",
17316
+ "gif",
17317
+ "audio",
17318
+ "icon"
17319
+ ]);
17320
+ /**
17321
+ * A single attachment. Exactly one of `url` (remote source, most adapters
17322
+ * prefer this) or `bytes` (inline source; required for Pushover-style
17323
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
17324
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
17325
+ */
17326
+ var AttachmentSchema = object({
17327
+ mediaType: AttachmentMediaTypeSchema,
17328
+ url: string().optional(),
17329
+ bytes: _instanceof(Uint8Array).optional(),
17330
+ mime: string().optional(),
17331
+ name: string().optional()
17332
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
17333
+ var NotificationFormatSchema = _enum([
17334
+ "text",
17335
+ "markdown",
17336
+ "html"
17337
+ ]);
17338
+ /** A single tap-through action button. */
17339
+ var NotificationActionSchema = object({
17340
+ id: string(),
17341
+ label: string(),
17342
+ url: string().optional()
17343
+ });
17344
+ /**
17345
+ * The canonical notification. `body` is the only hard field (Apprise model).
17346
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
17347
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
17348
+ * the adapter maps this ordinal onto its native level. `level?` is an
17349
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
17350
+ * `priority` for that one target.
17351
+ */
17352
+ var NotificationSchema = object({
16177
17353
  body: string(),
16178
- imageUrl: string().optional(),
17354
+ title: string().optional(),
17355
+ format: NotificationFormatSchema.default("text"),
17356
+ priority: number().int().min(1).max(5).default(3),
17357
+ level: string().optional(),
17358
+ attachments: array(AttachmentSchema).optional(),
17359
+ clickUrl: string().optional(),
17360
+ actions: array(NotificationActionSchema).optional(),
17361
+ sound: string().optional(),
17362
+ ttl: number().optional(),
17363
+ tag: string().optional(),
16179
17364
  deviceId: number().optional(),
16180
17365
  eventId: string().optional(),
16181
- priority: _enum([
16182
- "low",
16183
- "normal",
16184
- "high",
16185
- "critical"
16186
- ]).default("normal"),
16187
17366
  metadata: record(string(), unknown()).optional()
16188
- }), _void(), { kind: "mutation" }), method(_void(), object({
17367
+ });
17368
+ /** One declared native severity/priority level for a kind. */
17369
+ var TargetKindLevelSchema = object({
17370
+ id: string(),
17371
+ label: string(),
17372
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
17373
+ ordinal: number().int().min(1).max(5).nullable(),
17374
+ flags: object({
17375
+ critical: boolean().optional(),
17376
+ silent: boolean().optional(),
17377
+ noPush: boolean().optional()
17378
+ }).optional(),
17379
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
17380
+ requires: array(string()).optional(),
17381
+ description: string().optional()
17382
+ });
17383
+ /** The full capability block consulted before dispatch. */
17384
+ var TargetKindCapsSchema = object({
17385
+ attachments: object({
17386
+ mediaTypes: array(AttachmentMediaTypeSchema),
17387
+ mode: _enum([
17388
+ "url",
17389
+ "bytes",
17390
+ "both"
17391
+ ]),
17392
+ max: number().int().nonnegative(),
17393
+ maxBytes: number().int().positive().optional()
17394
+ }),
17395
+ /** Max action buttons (0 = none). */
17396
+ actions: number().int().nonnegative(),
17397
+ levels: array(TargetKindLevelSchema),
17398
+ format: array(NotificationFormatSchema),
17399
+ clickUrl: boolean(),
17400
+ sound: boolean(),
17401
+ ttl: boolean(),
17402
+ bodyMaxLen: number().int().positive()
17403
+ });
17404
+ /**
17405
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
17406
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
17407
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
17408
+ * the union is large and not meant for runtime validation here; the exported
17409
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
17410
+ */
17411
+ var ConfigSchemaPassthrough = unknown();
17412
+ var TargetKindSchema = object({
17413
+ kind: string(),
17414
+ label: string(),
17415
+ icon: string(),
17416
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
17417
+ addonId: string(),
17418
+ configSchema: ConfigSchemaPassthrough,
17419
+ supportsDiscovery: boolean(),
17420
+ caps: TargetKindCapsSchema
17421
+ });
17422
+ /**
17423
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
17424
+ * (return a presence marker only) when serving `listTargets` — never
17425
+ * round-trip a stored secret to the UI.
17426
+ */
17427
+ var TargetSchema = object({
17428
+ id: string(),
17429
+ name: string(),
17430
+ kind: string(),
17431
+ addonId: string(),
17432
+ enabled: boolean(),
17433
+ config: record(string(), unknown())
17434
+ });
17435
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
17436
+ var DiscoveredTargetSchema = object({
17437
+ kind: string(),
17438
+ suggestedName: string(),
17439
+ config: record(string(), unknown())
17440
+ });
17441
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
17442
+ var RenderedAsSchema = object({
17443
+ level: string(),
17444
+ format: NotificationFormatSchema,
17445
+ attachmentsSent: number().int().nonnegative(),
17446
+ actionsSent: number().int().nonnegative(),
17447
+ truncated: boolean(),
17448
+ dropped: array(string())
17449
+ });
17450
+ var SendResultSchema = object({
16189
17451
  success: boolean(),
16190
- error: string().optional()
16191
- }), { kind: "mutation" });
17452
+ error: string().optional(),
17453
+ renderedAs: RenderedAsSchema.optional()
17454
+ });
17455
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
17456
+ var TestResultSchema = SendResultSchema;
17457
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
17458
+ kind: string(),
17459
+ config: record(string(), unknown()).optional()
17460
+ }), array(DiscoveredTargetSchema)), method(object({
17461
+ targetId: string(),
17462
+ notification: NotificationSchema
17463
+ }), SendResultSchema, { kind: "mutation" }), method(object({
17464
+ targetId: string(),
17465
+ sample: NotificationSchema.optional()
17466
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
17467
+ targetId: string(),
17468
+ enabled: boolean()
17469
+ }), _void(), { kind: "mutation" });
16192
17470
  /**
16193
17471
  * Zod schemas for persisted record types.
16194
17472
  *
@@ -16692,7 +17970,10 @@ var AgentLoadSummarySchema = object({
16692
17970
  online: boolean(),
16693
17971
  load: RunnerLocalLoadSchema,
16694
17972
  /** Computed score used by the L2 capacity balancer (lower = less loaded). */
16695
- score: number()
17973
+ score: number(),
17974
+ /** This node's decode hwaccel backend (per-node `probedBestHwaccel`), or null
17975
+ * when not yet probed — for the cluster Pipeline table UI (P0.2). */
17976
+ decodeHwaccel: string().nullable()
16696
17977
  });
16697
17978
  /**
16698
17979
  * Aggregate metrics across the whole detection cluster. Replaces the legacy
@@ -19210,7 +20491,10 @@ var HwAccelBackendInputSchema = _enum([
19210
20491
  "webgpu",
19211
20492
  "none"
19212
20493
  ]).nullable().optional();
19213
- var HwAccelResolutionSchema = object({ preferred: array(string()).readonly() });
20494
+ var HwAccelResolutionSchema = object({
20495
+ preferred: array(string()).readonly(),
20496
+ rationale: string()
20497
+ });
19214
20498
  var HardwareEncoderIdSchema = _enum([
19215
20499
  "h264_videotoolbox",
19216
20500
  "hevc_videotoolbox",
@@ -19225,7 +20509,7 @@ var HardwareEncoderIdSchema = _enum([
19225
20509
  "libx264",
19226
20510
  "libx265"
19227
20511
  ]);
19228
- var HardwareEncodersSchema = object({
20512
+ object({
19229
20513
  encoders: array(object({
19230
20514
  encoder: HardwareEncoderIdSchema,
19231
20515
  codec: _enum(["H264", "H265"]),
@@ -19244,15 +20528,7 @@ var HardwareEncodersSchema = object({
19244
20528
  defaultH265: HardwareEncoderIdSchema,
19245
20529
  probedAt: number()
19246
20530
  });
19247
- /**
19248
- * Decode-side companion to {@link HardwareEncodersSchema}: the `-hwaccel`
19249
- * methods the configured ffmpeg binary actually supports (parsed from
19250
- * `ffmpeg -hwaccels`). Used to gate decode-hwaccel attempts so the transcode
19251
- * egress never spends a spawn on a backend this build cannot offer. Per-stream
19252
- * decode fragility (e.g. VideoToolbox HEVC) is still handled by the egress's
19253
- * software fallback — this only filters out wholly-unsupported backends.
19254
- */
19255
- var HardwareDecodeAccelsSchema = object({
20531
+ object({
19256
20532
  methods: array(string()).readonly(),
19257
20533
  probedAt: number()
19258
20534
  });
@@ -19315,16 +20591,7 @@ var ResolvedInferenceConfigSchema = object({
19315
20591
  format: ModelFormatSchema,
19316
20592
  reason: string()
19317
20593
  });
19318
- method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema), method(object({ requirements: array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema), method(object({
19319
- prefer: HwAccelBackendInputSchema,
19320
- nodeId: string().optional()
19321
- }), HwAccelResolutionSchema), method(_void(), HardwareEncodersSchema), method(_void(), HardwareEncodersSchema, {
19322
- kind: "mutation",
19323
- auth: "admin"
19324
- }), method(_void(), HardwareDecodeAccelsSchema), method(_void(), HardwareDecodeAccelsSchema, {
19325
- kind: "mutation",
19326
- auth: "admin"
19327
- });
20594
+ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema), method(object({ requirements: array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema), method(object({ prefer: HwAccelBackendInputSchema }), HwAccelResolutionSchema);
19328
20595
  var PtzPresetSchema = object({
19329
20596
  id: string(),
19330
20597
  name: string()
@@ -19377,6 +20644,16 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceI
19377
20644
  kind: "mutation",
19378
20645
  auth: "admin"
19379
20646
  });
20647
+ /**
20648
+ * `recording` cap — footage availability + HLS playback manifests + per-device
20649
+ * recording config. NOTE on events (source of truth, R5/C3): this cap carries
20650
+ * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
20651
+ * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
20652
+ * rows); the recorder's internal EventMap markers are ephemeral in-RAM
20653
+ * annotations that are not exposed here and must not be treated as an event
20654
+ * feed. Event<->footage joins are by time, padded with the shared `EVENT_PAD_MS`
20655
+ * (`interfaces/recording-config.ts`).
20656
+ */
19380
20657
  var RecordingStatusSchema = object({
19381
20658
  deviceId: number(),
19382
20659
  enabled: boolean(),
@@ -21013,6 +22290,12 @@ Object.freeze({
21013
22290
  addonId: null,
21014
22291
  access: "view"
21015
22292
  },
22293
+ "deviceManager.getRoleDisplayDefaults": {
22294
+ capName: "device-manager",
22295
+ capScope: "system",
22296
+ addonId: null,
22297
+ access: "view"
22298
+ },
21016
22299
  "deviceManager.getSettingsSchema": {
21017
22300
  capName: "device-manager",
21018
22301
  capScope: "system",
@@ -21163,6 +22446,12 @@ Object.freeze({
21163
22446
  addonId: null,
21164
22447
  access: "create"
21165
22448
  },
22449
+ "deviceManager.setDisplay": {
22450
+ capName: "device-manager",
22451
+ capScope: "system",
22452
+ addonId: null,
22453
+ access: "create"
22454
+ },
21166
22455
  "deviceManager.setIntegrationId": {
21167
22456
  capName: "device-manager",
21168
22457
  capScope: "system",
@@ -21205,6 +22494,12 @@ Object.freeze({
21205
22494
  addonId: null,
21206
22495
  access: "create"
21207
22496
  },
22497
+ "deviceManager.setRoleDisplayDefaults": {
22498
+ capName: "device-manager",
22499
+ capScope: "system",
22500
+ addonId: null,
22501
+ access: "create"
22502
+ },
21208
22503
  "deviceManager.setStreamProfileMap": {
21209
22504
  capName: "device-manager",
21210
22505
  capScope: "system",
@@ -22183,13 +23478,49 @@ Object.freeze({
22183
23478
  addonId: null,
22184
23479
  access: "create"
22185
23480
  },
23481
+ "notificationOutput.deleteTarget": {
23482
+ capName: "notification-output",
23483
+ capScope: "system",
23484
+ addonId: null,
23485
+ access: "delete"
23486
+ },
23487
+ "notificationOutput.discoverTargets": {
23488
+ capName: "notification-output",
23489
+ capScope: "system",
23490
+ addonId: null,
23491
+ access: "view"
23492
+ },
23493
+ "notificationOutput.listTargetKinds": {
23494
+ capName: "notification-output",
23495
+ capScope: "system",
23496
+ addonId: null,
23497
+ access: "view"
23498
+ },
23499
+ "notificationOutput.listTargets": {
23500
+ capName: "notification-output",
23501
+ capScope: "system",
23502
+ addonId: null,
23503
+ access: "view"
23504
+ },
22186
23505
  "notificationOutput.send": {
22187
23506
  capName: "notification-output",
22188
23507
  capScope: "system",
22189
23508
  addonId: null,
22190
23509
  access: "create"
22191
23510
  },
22192
- "notificationOutput.sendTest": {
23511
+ "notificationOutput.setTargetEnabled": {
23512
+ capName: "notification-output",
23513
+ capScope: "system",
23514
+ addonId: null,
23515
+ access: "create"
23516
+ },
23517
+ "notificationOutput.testTarget": {
23518
+ capName: "notification-output",
23519
+ capScope: "system",
23520
+ addonId: null,
23521
+ access: "create"
23522
+ },
23523
+ "notificationOutput.upsertTarget": {
22193
23524
  capName: "notification-output",
22194
23525
  capScope: "system",
22195
23526
  addonId: null,
@@ -22219,6 +23550,66 @@ Object.freeze({
22219
23550
  addonId: null,
22220
23551
  access: "create"
22221
23552
  },
23553
+ "petFeeder.callPet": {
23554
+ capName: "pet-feeder",
23555
+ capScope: "device",
23556
+ addonId: null,
23557
+ access: "create"
23558
+ },
23559
+ "petFeeder.cancelFeed": {
23560
+ capName: "pet-feeder",
23561
+ capScope: "device",
23562
+ addonId: null,
23563
+ access: "create"
23564
+ },
23565
+ "petFeeder.feed": {
23566
+ capName: "pet-feeder",
23567
+ capScope: "device",
23568
+ addonId: null,
23569
+ access: "create"
23570
+ },
23571
+ "petFeeder.markFoodReplenished": {
23572
+ capName: "pet-feeder",
23573
+ capScope: "device",
23574
+ addonId: null,
23575
+ access: "create"
23576
+ },
23577
+ "petFeeder.playSound": {
23578
+ capName: "pet-feeder",
23579
+ capScope: "device",
23580
+ addonId: null,
23581
+ access: "create"
23582
+ },
23583
+ "petFeeder.resetDesiccant": {
23584
+ capName: "pet-feeder",
23585
+ capScope: "device",
23586
+ addonId: null,
23587
+ access: "delete"
23588
+ },
23589
+ "petFeeder.setChildLock": {
23590
+ capName: "pet-feeder",
23591
+ capScope: "device",
23592
+ addonId: null,
23593
+ access: "create"
23594
+ },
23595
+ "petFeeder.setFeedSound": {
23596
+ capName: "pet-feeder",
23597
+ capScope: "device",
23598
+ addonId: null,
23599
+ access: "create"
23600
+ },
23601
+ "petFeeder.setIndicatorLight": {
23602
+ capName: "pet-feeder",
23603
+ capScope: "device",
23604
+ addonId: null,
23605
+ access: "create"
23606
+ },
23607
+ "petFeeder.setVolume": {
23608
+ capName: "pet-feeder",
23609
+ capScope: "device",
23610
+ addonId: null,
23611
+ access: "create"
23612
+ },
22222
23613
  "pipelineAnalytics.clearTracks": {
22223
23614
  capName: "pipeline-analytics",
22224
23615
  capScope: "device",
@@ -22825,30 +24216,6 @@ Object.freeze({
22825
24216
  addonId: null,
22826
24217
  access: "view"
22827
24218
  },
22828
- "platformProbe.getHardwareDecodeAccels": {
22829
- capName: "platform-probe",
22830
- capScope: "system",
22831
- addonId: null,
22832
- access: "view"
22833
- },
22834
- "platformProbe.getHardwareEncoders": {
22835
- capName: "platform-probe",
22836
- capScope: "system",
22837
- addonId: null,
22838
- access: "view"
22839
- },
22840
- "platformProbe.refreshHardwareDecodeAccels": {
22841
- capName: "platform-probe",
22842
- capScope: "system",
22843
- addonId: null,
22844
- access: "create"
22845
- },
22846
- "platformProbe.refreshHardwareEncoders": {
22847
- capName: "platform-probe",
22848
- capScope: "system",
22849
- addonId: null,
22850
- access: "create"
22851
- },
22852
24219
  "platformProbe.resolveHwAccel": {
22853
24220
  capName: "platform-probe",
22854
24221
  capScope: "system",
@@ -49661,7 +51028,7 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
49661
51028
  }
49662
51029
  async onInitialize() {
49663
51030
  const regs = await super.onInitialize();
49664
- const migrated = migrateLegacyConfig(await this.ctx.settings?.readAddonStore() ?? {});
51031
+ const migrated = migrateLegacyConfig(await this.resolveGlobalStore());
49665
51032
  if (this.brokersDiffer(migrated.brokers, this.config.brokers)) await this.updateGlobalSettings({ brokers: migrated.brokers });
49666
51033
  this.registry = new HmBrokerRegistry(this.ctx.logger);
49667
51034
  this.registry.setOnBrokerConnected((brokerId) => {