@camstack/addon-pipeline-orchestrator 1.1.19 → 1.1.21

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/index.mjs CHANGED
@@ -4627,7 +4627,7 @@ function _instanceof(cls, params = {}) {
4627
4627
  return inst;
4628
4628
  }
4629
4629
  //#endregion
4630
- //#region ../types/dist/sleep-BO1nweKv.mjs
4630
+ //#region ../types/dist/sleep-BiDFW0E7.mjs
4631
4631
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4632
4632
  EventCategory["SystemBoot"] = "system.boot";
4633
4633
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5456,6 +5456,100 @@ function createDurableState(deps) {
5456
5456
  };
5457
5457
  }
5458
5458
  /**
5459
+ * Per-node scoping for the shared addon-settings blob.
5460
+ *
5461
+ * An addon's settings store is hub-central (the `addon-settings` cap is
5462
+ * hub-routed — the hub instance answers for every node), so fields whose
5463
+ * value is a NODE fact (decoder backend, engine pick, probed hardware
5464
+ * capabilities) persist under the node-scoped key `<key>@<nodeId>` inside
5465
+ * the single shared blob. The SCHEMA field key stays bare: the write path
5466
+ * ({@link scopePatch}) maps the bare field onto the target node's scoped
5467
+ * key; the read/hydrate path ({@link projectStore}) maps THIS node's value
5468
+ * back, so UI forms and `resolveConfig` only ever see the bare key.
5469
+ *
5470
+ * Fields opt in via the `perNode: true` schema marker on `ConfigFieldBase`
5471
+ * (`interfaces/config-ui.ts`); `BaseAddon` derives the key set from the
5472
+ * schema and routes reads/writes through these helpers.
5473
+ *
5474
+ * ## No bare-key fallback — deliberate
5475
+ *
5476
+ * {@link readNodeValue} reads the node-scoped key ONLY. A node with no
5477
+ * scoped key resolves to `undefined` so the field's schema `default` wins —
5478
+ * NEVER `store[base]` and never another node's value. A bare legacy key in
5479
+ * the store is invisible to every node, hub included, so one node's
5480
+ * selection can never leak onto another. (This generalizes the
5481
+ * `decoder-backend-keys.ts` semantics — the no-fallback variant — over an
5482
+ * arbitrary set of per-node field keys.)
5483
+ *
5484
+ * Pure functions only — no I/O, no imports beyond the language. This is a
5485
+ * LEAF module: import it via its deep path, never from the root barrel.
5486
+ */
5487
+ /**
5488
+ * Normalize a raw kernel node id to the bare node id used for scoping.
5489
+ * `localNodeId` can carry a `<node>/<addon>` suffix on forked child
5490
+ * processes; per-node settings are per-NODE, so strip the addon segment.
5491
+ * `undefined` / `null` / empty falls back to `'hub'`.
5492
+ */
5493
+ function normalizeNodeId(raw) {
5494
+ if (raw === void 0 || raw === null || raw === "") return "hub";
5495
+ const slashIdx = raw.indexOf("/");
5496
+ if (slashIdx < 0) return raw;
5497
+ const bare = raw.slice(0, slashIdx);
5498
+ return bare === "" ? "hub" : bare;
5499
+ }
5500
+ /** The node-scoped store key for a per-node field: `<base>@<nodeId>`. */
5501
+ function nodeScopedKey(base, nodeId) {
5502
+ return `${base}@${normalizeNodeId(nodeId)}`;
5503
+ }
5504
+ /**
5505
+ * Read a node's value for a per-node field from the raw shared store:
5506
+ * the node-scoped key when present, otherwise `undefined`.
5507
+ * Deliberately NO bare-key fallback (see module doc) — the caller lets the
5508
+ * schema `default` win on `undefined`.
5509
+ */
5510
+ function readNodeValue(store, base, nodeId) {
5511
+ return store[nodeScopedKey(base, nodeId)];
5512
+ }
5513
+ /**
5514
+ * Re-map a UI/settings patch so every bare perNode field persists under the
5515
+ * TARGET node's scoped key; all other keys pass through unchanged. Used on
5516
+ * the write path so a save for one node never clobbers another node's value
5517
+ * (and the bare key is never written). Returns a new object — the input
5518
+ * patch is not mutated.
5519
+ */
5520
+ function scopePatch(patch, perNodeKeys, nodeId) {
5521
+ const out = {};
5522
+ for (const [key, value] of Object.entries(patch)) out[perNodeKeys.has(key) ? nodeScopedKey(key, nodeId) : key] = value;
5523
+ return out;
5524
+ }
5525
+ /**
5526
+ * Project the raw shared store onto the bare schema keys for ONE node so a
5527
+ * UI schema (whose field keys are bare) hydrates from that node's own
5528
+ * values:
5529
+ *
5530
+ * - EVERY `@`-scoped key (any node's) is dropped from the pass-through.
5531
+ * - Every bare perNode key is dropped from the pass-through (a stray bare
5532
+ * legacy key must never hydrate any node — no bare fallback).
5533
+ * - THIS node's effective value ({@link readNodeValue}) is then laid onto
5534
+ * each bare perNode key; when the node has no scoped key the bare key is
5535
+ * left ABSENT so the field's schema `default` wins.
5536
+ *
5537
+ * Returns a new object — the input store is not mutated.
5538
+ */
5539
+ function projectStore(store, perNodeKeys, nodeId) {
5540
+ const out = {};
5541
+ for (const [key, value] of Object.entries(store)) {
5542
+ if (key.includes("@")) continue;
5543
+ if (perNodeKeys.has(key)) continue;
5544
+ out[key] = value;
5545
+ }
5546
+ for (const base of perNodeKeys) {
5547
+ const value = readNodeValue(store, base, nodeId);
5548
+ if (value !== void 0) out[base] = value;
5549
+ }
5550
+ return out;
5551
+ }
5552
+ /**
5459
5553
  * Base class for CamStack addons. Eliminates settings boilerplate:
5460
5554
  *
5461
5555
  * - Typed `config` property with automatic resolution from store + defaults
@@ -5623,23 +5717,63 @@ var BaseAddon = class {
5623
5717
  deviceSettingsSchema() {
5624
5718
  return null;
5625
5719
  }
5626
- async getGlobalSettings(overlay, cap, _nodeId) {
5720
+ async getGlobalSettings(overlay, cap, nodeId) {
5627
5721
  const schema = this.globalSettingsSchema(cap);
5628
5722
  if (!schema) return { sections: [] };
5629
- const raw = await this._ctx?.settings?.readAddonStore() ?? {};
5723
+ const projected = await this.resolveGlobalStore(nodeId, cap);
5630
5724
  return hydrateSchema(schema, overlay ? {
5631
- ...raw,
5725
+ ...projected,
5632
5726
  ...overlay
5633
- } : raw);
5727
+ } : projected);
5634
5728
  }
5635
- async updateGlobalSettings(patch, _nodeId) {
5636
- await this._ctx?.settings?.writeAddonStore(patch);
5729
+ /**
5730
+ * The raw addon store PROJECTED onto the target node's bare per-node keys:
5731
+ * every `perNode: true` field carries THAT node's scoped value on its bare
5732
+ * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
5733
+ * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
5734
+ * A no-op passthrough when the schema declares no `perNode` field.
5735
+ *
5736
+ * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
5737
+ * the store for custom option logic (option narrowing, value snapping) to
5738
+ * read it per-node — never `ctx.settings.readAddonStore()` directly.
5739
+ * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
5740
+ */
5741
+ async resolveGlobalStore(nodeId, cap) {
5742
+ const raw = await this._ctx?.settings?.readAddonStore() ?? {};
5743
+ const keys = this.perNodeKeys(cap);
5744
+ const node = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
5745
+ return keys.size > 0 ? projectStore(raw, keys, node) : raw;
5746
+ }
5747
+ async updateGlobalSettings(patch, nodeId) {
5748
+ const keys = this.perNodeKeys();
5749
+ const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
5750
+ const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
5751
+ const barePatch = patch;
5752
+ const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
5753
+ await this._ctx?.settings?.writeAddonStore(scoped);
5754
+ if (target !== localNode) return;
5637
5755
  await this.resolveConfig();
5638
5756
  await this.onConfigChanged();
5639
5757
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
5640
5758
  this.maybeAutoRestart(patch, this.globalSettingsSchema());
5641
5759
  }
5642
5760
  /**
5761
+ * The set of field keys the global settings schema declares `perNode: true`
5762
+ * — derived once per `cap` argument and memoized (schemas are static
5763
+ * declarations). Empty set ⇒ every per-node code path is bypassed and the
5764
+ * settings API behaves exactly like the legacy node-agnostic one.
5765
+ */
5766
+ _perNodeKeysCache = /* @__PURE__ */ new Map();
5767
+ perNodeKeys(cap) {
5768
+ const cacheKey = cap ?? "";
5769
+ const cached = this._perNodeKeysCache.get(cacheKey);
5770
+ if (cached) return cached;
5771
+ const schema = this.globalSettingsSchema(cap);
5772
+ const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
5773
+ this._perNodeKeysCache.set(cacheKey, keys);
5774
+ return keys;
5775
+ }
5776
+ /**
5643
5777
  * If any field in `patch` is marked `requiresRestart` in `schema`,
5644
5778
  * schedule an addon restart for the next tick. Deferred via
5645
5779
  * `setImmediate` so the tRPC mutation that triggered the write has
@@ -5792,12 +5926,19 @@ var BaseAddon = class {
5792
5926
  * The merge is shallow: each key in `defaults` is checked against the store.
5793
5927
  * Only keys present in defaults are read — the store can contain extra keys
5794
5928
  * (e.g. from older versions) without polluting the typed config.
5929
+ *
5930
+ * Keys the global settings schema declares `perNode: true` resolve from
5931
+ * THIS node's scoped key (`<key>@<localNode>`) via `readNodeValue` — never
5932
+ * from the bare key — so a per-node field resolves to this node's own
5933
+ * selection at boot (absent scoped key ⇒ the constructor default wins).
5795
5934
  */
5796
5935
  async resolveConfig() {
5797
5936
  const stored = await this.readAddonStoreWithRetry();
5937
+ const perNode = this.perNodeKeys();
5938
+ const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
5798
5939
  const resolved = { ...this.defaults };
5799
5940
  for (const key of Object.keys(this.defaults)) {
5800
- const storedValue = stored[key];
5941
+ const storedValue = perNode.has(key) ? readNodeValue(stored, key, localNode) : stored[key];
5801
5942
  if (storedValue !== void 0 && storedValue !== null) {
5802
5943
  const defaultType = typeof this.defaults[key];
5803
5944
  if (typeof storedValue === defaultType) resolved[key] = storedValue;
@@ -5881,6 +6022,27 @@ var BaseAddon = class {
5881
6022
  }
5882
6023
  };
5883
6024
  /**
6025
+ * Collect the keys of every field marked `perNode: true`, recursing into
6026
+ * layout containers (`group` fields and `sub-tabs` tabs) the same way
6027
+ * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6028
+ * don't declare `perNode` and are excluded by the `in` narrowing.
6029
+ */
6030
+ function collectPerNodeFieldKeys(fields) {
6031
+ const collected = [];
6032
+ for (const field of fields) {
6033
+ if (field.type === "group") {
6034
+ collected.push(...collectPerNodeFieldKeys(field.fields));
6035
+ continue;
6036
+ }
6037
+ if (field.type === "sub-tabs") {
6038
+ for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
6039
+ continue;
6040
+ }
6041
+ if ("perNode" in field && field.perNode === true) collected.push(field.key);
6042
+ }
6043
+ return collected;
6044
+ }
6045
+ /**
5884
6046
  * Normalize an `ICamstackAddon.initialize()` return value into the
5885
6047
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
5886
6048
  * envelopes pass through; void stays void.
@@ -6648,6 +6810,13 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
6648
6810
  /** Single still-image entity (HA `image.*`). Read-only display of an
6649
6811
  * `entity_picture` signed URL the browser loads directly. `image` cap. */
6650
6812
  DeviceType["Image"] = "image";
6813
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
6814
+ * level, battery, desiccant life, feeding state and manual-feed /
6815
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
6816
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
6817
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
6818
+ * integrations sharing the same food/desiccant/hopper surface. */
6819
+ DeviceType["PetFeeder"] = "pet-feeder";
6651
6820
  return DeviceType;
6652
6821
  }({});
6653
6822
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10360,7 +10529,8 @@ var MotionAnalysisResultSchema = object({
10360
10529
  });
10361
10530
  method(object({
10362
10531
  deviceId: number(),
10363
- frame: FrameInputSchema
10532
+ frame: FrameInputSchema.optional(),
10533
+ frameHandle: FrameHandleSchema.optional()
10364
10534
  }), MotionAnalysisResultSchema, { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), object({
10365
10535
  deviceId: number(),
10366
10536
  detected: boolean(),
@@ -10667,11 +10837,20 @@ var pipelineExecutorCapability = {
10667
10837
  * legacy call shape used by existing benchmark code; once all
10668
10838
  * callers pass it explicitly we make it required.
10669
10839
  *
10670
- * Exactly one of `frame`, `imageBase64`, `referenceImage` must be
10671
- * provided:
10840
+ * Exactly one of `frame`, `frameHandle`, `imageBase64`,
10841
+ * `referenceImage` must be provided:
10672
10842
  * - `frame`: runtime dispatch path (runner → decoded broker frame).
10673
10843
  * Carries the raw buffer, dimensions, and format; the executor
10674
10844
  * uses it directly without base64 round-tripping.
10845
+ * - `frameHandle` (CB5): a zero-pixel shm `FrameHandle` for the SAME
10846
+ * decoded frame. Both runner and executor are hub-local processes
10847
+ * sharing `/dev/shm`, so the executor maps the named segment and
10848
+ * reads the pixels back zero-copy — eliminating the ~1.2MB
10849
+ * re-serialisation over UDS/MsgPack the `frame` path pays per call.
10850
+ * High-risk: the FrameRing is a latest-wins seqlock with no
10851
+ * refcount, so a recycled slot yields a null read; the executor
10852
+ * then degrades to an empty result and the runner ships pixels via
10853
+ * `frame` as the fallback (queue-depth gated on the runner side).
10675
10854
  * - `imageBase64`: one-shot test path (benchmark ImageTab).
10676
10855
  * - `referenceImage`: named file from the reference-image store.
10677
10856
  */
@@ -10679,6 +10858,12 @@ var pipelineExecutorCapability = {
10679
10858
  engine: PipelineEngineChoiceSchema.optional(),
10680
10859
  steps: array(PipelineStepInputSchema).min(1),
10681
10860
  frame: FrameInputSchema.optional(),
10861
+ /**
10862
+ * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
10863
+ * the decoded pixels live in. One more member of the one-of
10864
+ * frame/frameHandle/image/imageBase64/referenceImage group.
10865
+ */
10866
+ frameHandle: FrameHandleSchema.optional(),
10682
10867
  imageBase64: string().optional(),
10683
10868
  /**
10684
10869
  * Binary JPEG bytes — preferred over `imageBase64` on internal
@@ -11572,6 +11757,113 @@ object({
11572
11757
  lastFetchedAt: number()
11573
11758
  });
11574
11759
  DeviceType.Sensor;
11760
+ /**
11761
+ * Feeder connectivity / power status — mirrors the HA petkit device-status
11762
+ * enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
11763
+ * `on_batteries` (running on battery backup). `null` until first reported.
11764
+ */
11765
+ var PetFeederDeviceStatusSchema = _enum([
11766
+ "normal",
11767
+ "offline",
11768
+ "on_batteries"
11769
+ ]);
11770
+ var gramsPortion = number().int().min(4).max(200);
11771
+ object({
11772
+ /** Food currently in the bowl (grams). Null when the device has not
11773
+ * reported a reading yet. On dual-hopper models this is the combined
11774
+ * bowl reading; per-hopper levels live in `food1`/`food2`. */
11775
+ foodLevel: number().nullable(),
11776
+ /** Hopper-1 food level (grams) on dual-hopper feeders; null on
11777
+ * single-hopper models. */
11778
+ food1: number().nullable(),
11779
+ /** Hopper-2 food level (grams) on dual-hopper feeders; null on
11780
+ * single-hopper models. */
11781
+ food2: number().nullable(),
11782
+ /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
11783
+ * (`device_class: problem`, on = low). True when the bowl is empty /
11784
+ * below the feeder's low threshold. */
11785
+ lowFood: boolean(),
11786
+ /** Battery charge 0..100 (%). Null on mains-powered models or when the
11787
+ * device has no battery reading. */
11788
+ batteryPower: number().min(0).max(100).nullable(),
11789
+ /** Days of desiccant life remaining. Null when the model has no
11790
+ * desiccant sensor. */
11791
+ desiccantLeftDays: number().nullable(),
11792
+ /** True while a feed is in progress. */
11793
+ feeding: boolean(),
11794
+ /** Decoded connectivity / power status (HA petkit device-status enum).
11795
+ * Null until the device has reported a status. */
11796
+ status: PetFeederDeviceStatusSchema.nullable(),
11797
+ /** Decoded human-readable fault message. Null (or the device's `no_error`
11798
+ * sentinel) means healthy; a non-null string is an active fault. Pairs
11799
+ * with `errorCode` for consumers that want the raw integer. */
11800
+ error: string().nullable(),
11801
+ /** Raw device error code (0 / null = no error). */
11802
+ errorCode: number().nullable(),
11803
+ /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
11804
+ isDualHopper: boolean(),
11805
+ /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
11806
+ childLock: boolean(),
11807
+ /** Front indicator-light setting. */
11808
+ indicatorLight: boolean(),
11809
+ /** Play a chime when dispensing. */
11810
+ feedSound: boolean(),
11811
+ /** Speaker / prompt volume level (device-scaled integer). */
11812
+ volume: number(),
11813
+ /** Ms epoch when the slice was last refreshed from the cloud. */
11814
+ lastFetchedAt: number()
11815
+ });
11816
+ DeviceType.PetFeeder, method(object({
11817
+ deviceId: number().int().nonnegative(),
11818
+ grams: gramsPortion.optional(),
11819
+ hopper1: gramsPortion.optional(),
11820
+ hopper2: gramsPortion.optional()
11821
+ }), _void(), {
11822
+ kind: "mutation",
11823
+ auth: "admin"
11824
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
11825
+ kind: "mutation",
11826
+ auth: "admin"
11827
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
11828
+ kind: "mutation",
11829
+ auth: "admin"
11830
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
11831
+ kind: "mutation",
11832
+ auth: "admin"
11833
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
11834
+ kind: "mutation",
11835
+ auth: "admin"
11836
+ }), method(object({
11837
+ deviceId: number().int().nonnegative(),
11838
+ soundId: number().int().nonnegative()
11839
+ }), _void(), {
11840
+ kind: "mutation",
11841
+ auth: "admin"
11842
+ }), method(object({
11843
+ deviceId: number().int().nonnegative(),
11844
+ on: boolean()
11845
+ }), _void(), {
11846
+ kind: "mutation",
11847
+ auth: "admin"
11848
+ }), method(object({
11849
+ deviceId: number().int().nonnegative(),
11850
+ on: boolean()
11851
+ }), _void(), {
11852
+ kind: "mutation",
11853
+ auth: "admin"
11854
+ }), method(object({
11855
+ deviceId: number().int().nonnegative(),
11856
+ on: boolean()
11857
+ }), _void(), {
11858
+ kind: "mutation",
11859
+ auth: "admin"
11860
+ }), method(object({
11861
+ deviceId: number().int().nonnegative(),
11862
+ level: number().int().nonnegative()
11863
+ }), _void(), {
11864
+ kind: "mutation",
11865
+ auth: "admin"
11866
+ });
11575
11867
  object({
11576
11868
  /** Instantaneous power draw in watts. */
11577
11869
  watts: number().optional(),
@@ -13505,10 +13797,12 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
13505
13797
  url: string()
13506
13798
  }), _void()), method(object({
13507
13799
  sessionId: string(),
13508
- maxCount: number().default(1)
13800
+ maxCount: number().default(1),
13801
+ waitMs: number().optional()
13509
13802
  }), array(DecodedFrameSchema)), method(object({
13510
13803
  sessionId: string(),
13511
- maxCount: number().default(1)
13804
+ maxCount: number().default(1),
13805
+ waitMs: number().optional()
13512
13806
  }), array(FrameHandleSchema)), method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()), method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()), method(object({
13513
13807
  sessionId: string(),
13514
13808
  config: DecoderSessionConfigSchema.partial()
@@ -15480,7 +15774,10 @@ var AgentLoadSummarySchema = object({
15480
15774
  online: boolean(),
15481
15775
  load: RunnerLocalLoadSchema,
15482
15776
  /** Computed score used by the L2 capacity balancer (lower = less loaded). */
15483
- score: number()
15777
+ score: number(),
15778
+ /** This node's decode hwaccel backend (per-node `probedBestHwaccel`), or null
15779
+ * when not yet probed — for the cluster Pipeline table UI (P0.2). */
15780
+ decodeHwaccel: string().nullable()
15484
15781
  });
15485
15782
  /**
15486
15783
  * Aggregate metrics across the whole detection cluster. Replaces the legacy
@@ -18173,7 +18470,10 @@ var HwAccelBackendInputSchema = _enum([
18173
18470
  "webgpu",
18174
18471
  "none"
18175
18472
  ]).nullable().optional();
18176
- var HwAccelResolutionSchema = object({ preferred: array(string()).readonly() });
18473
+ var HwAccelResolutionSchema = object({
18474
+ preferred: array(string()).readonly(),
18475
+ rationale: string()
18476
+ });
18177
18477
  var HardwareEncoderIdSchema = _enum([
18178
18478
  "h264_videotoolbox",
18179
18479
  "hevc_videotoolbox",
@@ -18188,7 +18488,7 @@ var HardwareEncoderIdSchema = _enum([
18188
18488
  "libx264",
18189
18489
  "libx265"
18190
18490
  ]);
18191
- var HardwareEncodersSchema = object({
18491
+ object({
18192
18492
  encoders: array(object({
18193
18493
  encoder: HardwareEncoderIdSchema,
18194
18494
  codec: _enum(["H264", "H265"]),
@@ -18207,15 +18507,7 @@ var HardwareEncodersSchema = object({
18207
18507
  defaultH265: HardwareEncoderIdSchema,
18208
18508
  probedAt: number()
18209
18509
  });
18210
- /**
18211
- * Decode-side companion to {@link HardwareEncodersSchema}: the `-hwaccel`
18212
- * methods the configured ffmpeg binary actually supports (parsed from
18213
- * `ffmpeg -hwaccels`). Used to gate decode-hwaccel attempts so the transcode
18214
- * egress never spends a spawn on a backend this build cannot offer. Per-stream
18215
- * decode fragility (e.g. VideoToolbox HEVC) is still handled by the egress's
18216
- * software fallback — this only filters out wholly-unsupported backends.
18217
- */
18218
- var HardwareDecodeAccelsSchema = object({
18510
+ object({
18219
18511
  methods: array(string()).readonly(),
18220
18512
  probedAt: number()
18221
18513
  });
@@ -18278,16 +18570,7 @@ var ResolvedInferenceConfigSchema = object({
18278
18570
  format: ModelFormatSchema,
18279
18571
  reason: string()
18280
18572
  });
18281
- method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema), method(object({ requirements: array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema), method(object({
18282
- prefer: HwAccelBackendInputSchema,
18283
- nodeId: string().optional()
18284
- }), HwAccelResolutionSchema), method(_void(), HardwareEncodersSchema), method(_void(), HardwareEncodersSchema, {
18285
- kind: "mutation",
18286
- auth: "admin"
18287
- }), method(_void(), HardwareDecodeAccelsSchema), method(_void(), HardwareDecodeAccelsSchema, {
18288
- kind: "mutation",
18289
- auth: "admin"
18290
- });
18573
+ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema), method(object({ requirements: array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema), method(object({ prefer: HwAccelBackendInputSchema }), HwAccelResolutionSchema);
18291
18574
  var PtzPresetSchema = object({
18292
18575
  id: string(),
18293
18576
  name: string()
@@ -21246,6 +21529,66 @@ Object.freeze({
21246
21529
  addonId: null,
21247
21530
  access: "create"
21248
21531
  },
21532
+ "petFeeder.callPet": {
21533
+ capName: "pet-feeder",
21534
+ capScope: "device",
21535
+ addonId: null,
21536
+ access: "create"
21537
+ },
21538
+ "petFeeder.cancelFeed": {
21539
+ capName: "pet-feeder",
21540
+ capScope: "device",
21541
+ addonId: null,
21542
+ access: "create"
21543
+ },
21544
+ "petFeeder.feed": {
21545
+ capName: "pet-feeder",
21546
+ capScope: "device",
21547
+ addonId: null,
21548
+ access: "create"
21549
+ },
21550
+ "petFeeder.markFoodReplenished": {
21551
+ capName: "pet-feeder",
21552
+ capScope: "device",
21553
+ addonId: null,
21554
+ access: "create"
21555
+ },
21556
+ "petFeeder.playSound": {
21557
+ capName: "pet-feeder",
21558
+ capScope: "device",
21559
+ addonId: null,
21560
+ access: "create"
21561
+ },
21562
+ "petFeeder.resetDesiccant": {
21563
+ capName: "pet-feeder",
21564
+ capScope: "device",
21565
+ addonId: null,
21566
+ access: "delete"
21567
+ },
21568
+ "petFeeder.setChildLock": {
21569
+ capName: "pet-feeder",
21570
+ capScope: "device",
21571
+ addonId: null,
21572
+ access: "create"
21573
+ },
21574
+ "petFeeder.setFeedSound": {
21575
+ capName: "pet-feeder",
21576
+ capScope: "device",
21577
+ addonId: null,
21578
+ access: "create"
21579
+ },
21580
+ "petFeeder.setIndicatorLight": {
21581
+ capName: "pet-feeder",
21582
+ capScope: "device",
21583
+ addonId: null,
21584
+ access: "create"
21585
+ },
21586
+ "petFeeder.setVolume": {
21587
+ capName: "pet-feeder",
21588
+ capScope: "device",
21589
+ addonId: null,
21590
+ access: "create"
21591
+ },
21249
21592
  "pipelineAnalytics.clearTracks": {
21250
21593
  capName: "pipeline-analytics",
21251
21594
  capScope: "device",
@@ -21852,30 +22195,6 @@ Object.freeze({
21852
22195
  addonId: null,
21853
22196
  access: "view"
21854
22197
  },
21855
- "platformProbe.getHardwareDecodeAccels": {
21856
- capName: "platform-probe",
21857
- capScope: "system",
21858
- addonId: null,
21859
- access: "view"
21860
- },
21861
- "platformProbe.getHardwareEncoders": {
21862
- capName: "platform-probe",
21863
- capScope: "system",
21864
- addonId: null,
21865
- access: "view"
21866
- },
21867
- "platformProbe.refreshHardwareDecodeAccels": {
21868
- capName: "platform-probe",
21869
- capScope: "system",
21870
- addonId: null,
21871
- access: "create"
21872
- },
21873
- "platformProbe.refreshHardwareEncoders": {
21874
- capName: "platform-probe",
21875
- capScope: "system",
21876
- addonId: null,
21877
- access: "create"
21878
- },
21879
22198
  "platformProbe.resolveHwAccel": {
21880
22199
  capName: "platform-probe",
21881
22200
  capScope: "system",
@@ -24618,6 +24937,14 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24618
24937
  if (!this._cameraSettingsState) this._cameraSettingsState = this.state("cameraSettings", CameraSettingsMapSchema, {});
24619
24938
  return this._cameraSettingsState;
24620
24939
  }
24940
+ /** One-shot `migrateLegacyFlagsToBindings` guard flag. Absent or
24941
+ * corrupt ⇒ `false` (the migration runs — same fallback as the old
24942
+ * raw `store[key] === true` check). */
24943
+ _bindingsMigrationDoneState = null;
24944
+ get bindingsMigrationDoneState() {
24945
+ if (!this._bindingsMigrationDoneState) this._bindingsMigrationDoneState = this.state("bindingsMigration_v1_done", boolean(), false);
24946
+ return this._bindingsMigrationDoneState;
24947
+ }
24621
24948
  /**
24622
24949
  * Per-camera zones CRUD provider. Constructed lazily in `onInitialize`
24623
24950
  * because it captures `this.ctx` for settings + api access; cleared
@@ -24797,7 +25124,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
24797
25124
  async onInitialize() {
24798
25125
  this.initTimestamp = Date.now();
24799
25126
  try {
24800
- const stored = await this.ctx.settings?.readAddonStore() ?? {};
25127
+ const stored = await this.resolveGlobalStore();
24801
25128
  this.globalSettings = { ...stored };
24802
25129
  this.applyRuntimeSettings(this.globalSettings);
24803
25130
  } catch (err) {
@@ -25009,7 +25336,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25009
25336
  this.pendingRetryTimer = setInterval(() => void this.retryPendingDispatches(), PENDING_RETRY_INTERVAL_MS);
25010
25337
  this.ctx.logger.info("Pipeline orchestrator detection-wiring subscriptions installed");
25011
25338
  this.migrateLegacyFlagsToBindings().catch((err) => {
25012
- this.ctx.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg(err) } });
25339
+ this.ctxIfReady?.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg(err) } });
25013
25340
  });
25014
25341
  this.zoneRulesProvider = new ZoneRulesProvider({
25015
25342
  logger: this.ctx.logger.child("zone-rules"),
@@ -25139,9 +25466,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25139
25466
  * persist the flag after a successful pass.
25140
25467
  */
25141
25468
  async migrateLegacyFlagsToBindings() {
25142
- const MIGRATION_KEY = "bindingsMigration_v1_done";
25143
- const store = await this.ctx.settings?.readAddonStore() ?? {};
25144
- if (store[MIGRATION_KEY] === true) return;
25469
+ if (await this.bindingsMigrationDoneState.get()) return;
25145
25470
  let api = this.api;
25146
25471
  for (let i = 0; !api && i < 30; i++) {
25147
25472
  await new Promise((r) => setTimeout(r, 200));
@@ -25197,10 +25522,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25197
25522
  });
25198
25523
  detectionDisabled++;
25199
25524
  }
25200
- await this.ctx.settings?.writeAddonStore({
25201
- ...store,
25202
- [MIGRATION_KEY]: true
25203
- });
25525
+ await this.bindingsMigrationDoneState.set(true);
25204
25526
  this.ctx.logger.info("bindings migration complete", { meta: {
25205
25527
  cameras: cameras.length,
25206
25528
  audioDisabled,
@@ -25605,6 +25927,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25605
25927
  * re-dispatching).
25606
25928
  */
25607
25929
  async getAgentLoad() {
25930
+ await this.refreshDecodeHwaccels();
25608
25931
  await this.collectAgentLoad();
25609
25932
  return [...this.cachedAgentLoad.values()];
25610
25933
  }
@@ -25870,11 +26193,48 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
25870
26193
  nodeId: load.nodeId,
25871
26194
  online: true,
25872
26195
  load,
25873
- score: computeCapacityScore(load)
26196
+ score: computeCapacityScore(load),
26197
+ decodeHwaccel: this.decodeHwaccelByNode.get(load.nodeId) ?? null
25874
26198
  });
25875
26199
  this.cachedAgentLoad = next;
25876
26200
  }
25877
26201
  /**
26202
+ * Per-node decode hwaccel cache (`nodeId` → backend), refreshed by
26203
+ * {@link refreshDecodeHwaccels} before a UI-facing {@link getAgentLoad}. Read
26204
+ * from the decoder addon's per-node `probedBestHwaccel@<node>` setting — a
26205
+ * node-LOCAL probe published to a per-node setting — via `addonSettings`,
26206
+ * NEVER the `platform-probe` singleton (whose pin answers with hub hardware).
26207
+ */
26208
+ decodeHwaccelByNode = /* @__PURE__ */ new Map();
26209
+ /** Refresh {@link decodeHwaccelByNode} for every known runner node. */
26210
+ async refreshDecodeHwaccels() {
26211
+ await Promise.all([...this.knownRunnerNodes].map(async (nodeId) => {
26212
+ const hwaccel = await this.readNodeDecodeHwaccel(nodeId);
26213
+ if (hwaccel !== void 0) this.decodeHwaccelByNode.set(nodeId, hwaccel);
26214
+ }));
26215
+ }
26216
+ /**
26217
+ * Read a node's `probedBestHwaccel` from the decoder-ffmpeg addon's per-node
26218
+ * settings via the hub-routed `addonSettings` cap. Returns `null` when the
26219
+ * value is empty/unset, `undefined` on a read failure (keep the last known).
26220
+ * Mirrors {@link readDetectionPipelineEngine}.
26221
+ */
26222
+ async readNodeDecodeHwaccel(nodeId) {
26223
+ const api = this.ctx.api;
26224
+ if (!api?.addonSettings) return void 0;
26225
+ try {
26226
+ const schema = await api.addonSettings.getGlobalSettings.query({
26227
+ addonId: "decoder-ffmpeg",
26228
+ nodeId
26229
+ });
26230
+ if (!schema) return void 0;
26231
+ for (const s of schema.sections) for (const f of s.fields) if (f.key === "probedBestHwaccel") return typeof f.value === "string" && f.value.length > 0 ? f.value : null;
26232
+ return null;
26233
+ } catch {
26234
+ return;
26235
+ }
26236
+ }
26237
+ /**
25878
26238
  * Per-node `getLocalLoad` budget (ms). A WEDGED runner — transport up
25879
26239
  * enough to stay in the service registry, but whose `broker.call` neither
25880
26240
  * resolves nor rejects (observed live as
@@ -27826,8 +28186,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
27826
28186
  ] });
27827
28187
  }
27828
28188
  async updateGlobalSettings(patch) {
27829
- await this.ctx.settings.writeAddonStore(patch);
27830
- const full = await this.ctx.settings?.readAddonStore() ?? {};
28189
+ await super.updateGlobalSettings(patch);
28190
+ const full = await this.resolveGlobalStore();
27831
28191
  this.globalSettings = { ...full };
27832
28192
  this.applyRuntimeSettings(full);
27833
28193
  const pausedIds = [...this.loadShedState.entries()].filter(([, s]) => s.pausedAt !== null).map(([id]) => id);