@camstack/addon-provider-homeassistant 1.2.639 → 1.2.640

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.
@@ -5588,7 +5588,7 @@ var ZodIssueCode = {
5588
5588
  var ZodFirstPartyTypeKind;
5589
5589
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5590
5590
  //#endregion
5591
- //#region ../types/dist/sleep-6eG9C8Xl.mjs
5591
+ //#region ../types/dist/sleep-CeyhrcOk.mjs
5592
5592
  /**
5593
5593
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5594
5594
  * window to float samples (D455).
@@ -7655,6 +7655,7 @@ var BaseAddon = class {
7655
7655
  * a warning and falls back rather than crashing boot.
7656
7656
  */
7657
7657
  state(key, schema, fallback) {
7658
+ this._declaredStateKeys.add(key);
7658
7659
  return createDurableState({
7659
7660
  key,
7660
7661
  schema,
@@ -7670,6 +7671,96 @@ var BaseAddon = class {
7670
7671
  } })
7671
7672
  });
7672
7673
  }
7674
+ /** Durable-state keys this process registered through {@link state}. */
7675
+ _declaredStateKeys = /* @__PURE__ */ new Set();
7676
+ /**
7677
+ * Stale-data audit: every global-settings key this addon still declares —
7678
+ * constructor defaults, settings-schema fields (layout containers included),
7679
+ * durable-state keys and the root of each `secretStatePaths` entry. Answered
7680
+ * here, in the addon's own process, so the hub keeps no copy of it.
7681
+ */
7682
+ async getDeclaredSettingsKeys() {
7683
+ return { keys: [...this.declaredKeySet()].toSorted() };
7684
+ }
7685
+ declaredKeySet() {
7686
+ const keys = new Set(Object.keys(this.defaults));
7687
+ const schema = this.globalSettingsSchema();
7688
+ if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
7689
+ for (const key of this._declaredStateKeys) keys.add(key);
7690
+ for (const path of this.secretStatePaths) {
7691
+ const root = path.split(/[.[]/, 1)[0];
7692
+ if (root) keys.add(root);
7693
+ }
7694
+ return keys;
7695
+ }
7696
+ /**
7697
+ * Stale-data audit: remove stored global-settings keys the addon no longer
7698
+ * declares, and clear the keychain values their markers held — except a value
7699
+ * a surviving key still points at. A declared or absent key is refused.
7700
+ */
7701
+ async unsetStoredSettingsKeys(keys) {
7702
+ const settings = this._ctx?.settings;
7703
+ const raw = this.rawSettings();
7704
+ if (!settings || !raw) return {
7705
+ removed: [],
7706
+ refused: keys.map((key) => ({
7707
+ key,
7708
+ reason: "no settings store"
7709
+ })),
7710
+ secretsCleared: 0
7711
+ };
7712
+ let stored;
7713
+ try {
7714
+ stored = await raw.readAddonStore();
7715
+ } catch (error) {
7716
+ const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
7717
+ return {
7718
+ removed: [],
7719
+ refused: keys.map((key) => ({
7720
+ key,
7721
+ reason
7722
+ })),
7723
+ secretsCleared: 0
7724
+ };
7725
+ }
7726
+ const declared = this.declaredKeySet();
7727
+ const removed = [];
7728
+ const refused = [];
7729
+ for (const key of keys) {
7730
+ const base = key.split("@", 1)[0] ?? key;
7731
+ if (declared.has(base)) refused.push({
7732
+ key,
7733
+ reason: "still declared by the addon"
7734
+ });
7735
+ else if (!(key in stored)) refused.push({
7736
+ key,
7737
+ reason: "not stored"
7738
+ });
7739
+ else removed.push(key);
7740
+ }
7741
+ if (removed.length === 0) return {
7742
+ removed,
7743
+ refused,
7744
+ secretsCleared: 0
7745
+ };
7746
+ const kept = /* @__PURE__ */ new Set();
7747
+ const held = /* @__PURE__ */ new Set();
7748
+ for (const [key, value] of Object.entries(stored)) {
7749
+ const target = removed.includes(key) ? held : kept;
7750
+ for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
7751
+ }
7752
+ const refsToClear = [...held].filter((ref) => !kept.has(ref));
7753
+ const patch = {};
7754
+ for (const key of removed) patch[key] = void 0;
7755
+ await settings.writeAddonStore(patch);
7756
+ const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
7757
+ await this.resolveConfig();
7758
+ return {
7759
+ removed,
7760
+ refused,
7761
+ secretsCleared
7762
+ };
7763
+ }
7673
7764
  /** Per-device variant of {@link state}, backed by the per-device store. */
7674
7765
  deviceState(deviceId, key, schema, fallback) {
7675
7766
  return createDurableState({
@@ -7940,6 +8031,21 @@ function dropPerNodeFields(fields) {
7940
8031
  }
7941
8032
  return kept;
7942
8033
  }
8034
+ function collectFieldKeys(fields) {
8035
+ const collected = [];
8036
+ for (const field of fields) {
8037
+ if (field.type === "group") {
8038
+ collected.push(...collectFieldKeys(field.fields));
8039
+ continue;
8040
+ }
8041
+ if (field.type === "sub-tabs") {
8042
+ for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
8043
+ continue;
8044
+ }
8045
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8046
+ }
8047
+ return collected;
8048
+ }
7943
8049
  function collectPerNodeFieldKeys(fields) {
7944
8050
  const collected = [];
7945
8051
  for (const field of fields) {
@@ -9172,6 +9278,7 @@ var CAP_NAMES = Object.freeze([
9172
9278
  "recording-signal",
9173
9279
  "records",
9174
9280
  "remote",
9281
+ "remote-layout-components",
9175
9282
  "running-state",
9176
9283
  "scene-monitor",
9177
9284
  "script-runner",
@@ -11017,7 +11124,7 @@ object({
11017
11124
  params: record(string(), union([string(), number()])).optional()
11018
11125
  });
11019
11126
  //#endregion
11020
- //#region ../types/dist/err-msg-DWXWc0Di.mjs
11127
+ //#region ../types/dist/err-msg-BD5buxvT.mjs
11021
11128
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11022
11129
  DeviceType["Camera"] = "camera";
11023
11130
  DeviceType["Hub"] = "hub";
@@ -11123,6 +11230,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11123
11230
  DeviceType["Washer"] = "washer";
11124
11231
  /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
11125
11232
  DeviceType["Dryer"] = "dryer";
11233
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
11234
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
11235
+ * address, connection state) and the actions the box really supports
11236
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
11237
+ * adopted beside it — and its connection settings stay on the integration. */
11238
+ DeviceType["Gateway"] = "gateway";
11126
11239
  return DeviceType;
11127
11240
  }({});
11128
11241
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11383,7 +11496,7 @@ function errMsg(err) {
11383
11496
  return String(err);
11384
11497
  }
11385
11498
  //#endregion
11386
- //#region ../types/dist/composition-kSuOifuG.mjs
11499
+ //#region ../types/dist/composition-C7WdThmP.mjs
11387
11500
  /**
11388
11501
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11389
11502
  *
@@ -16044,246 +16157,630 @@ object({
16044
16157
  path: ["expr"]
16045
16158
  });
16046
16159
  });
16047
- /** How a leaf compares a device field to a value. Derived from the field's
16048
- * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16049
- var AutomationConditionOperatorSchema = _enum([
16050
- "eq",
16051
- "ne",
16052
- "gt",
16053
- "gte",
16054
- "lt",
16055
- "lte",
16056
- "contains",
16057
- "in"
16058
- ]);
16059
- var AutomationConditionLeafSchema = object({
16060
- kind: literal("condition"),
16061
- deviceId: number().int().nonnegative(),
16062
- cap: string().min(1),
16063
- fieldPath: string().min(1),
16064
- operator: AutomationConditionOperatorSchema,
16065
- value: union([
16066
- string(),
16067
- number(),
16068
- boolean(),
16069
- array(union([string(), number()]))
16070
- ])
16071
- });
16072
- var AutomationConditionStationaryPresentSchema = object({
16073
- kind: literal("stationary-present"),
16074
- deviceId: number().int().nonnegative(),
16075
- /** Class names; a macro class matches every raw class that maps to it. */
16076
- classes: array(string().min(1)).min(1).optional(),
16077
- /** The parked object must be in at least one of these zones. */
16078
- zones: array(string().min(1)).min(1).optional()
16079
- });
16080
16160
  /**
16081
- * The expression leaf, declared as a plain object rather than an intersection
16082
- * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
16083
- * read `kind` off each option, and an intersection hides it. The author-time
16084
- * validation is the SAME function `ExpressionSourceSchema` runs, so the two
16085
- * cannot drift — an expression that one accepts, the other accepts.
16161
+ * Identity envelope for a device's upstream-system metadata.
16162
+ *
16163
+ * Two jobs:
16164
+ *
16165
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
16166
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
16167
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
16168
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
16169
+ * without round-tripping through `stableId` / `id` mappings.
16170
+ *
16171
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
16172
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
16173
+ * for vendors where MAC is the natural key). For systems where the
16174
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
16175
+ * providers detect renames by matching `uniqueId` and patch `id`
16176
+ * in-place so dispatch keeps working.
16177
+ *
16178
+ * **Display unit and precision are NOT stored here.** They are a read-time
16179
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
16180
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
16181
+ * neither is repeated on each sample. The `device_class` tag is an
16182
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
16183
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
16184
+ *
16185
+ * The `raw` escape hatch carries provider-specific attributes that don't
16186
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
16187
+ * details, …). Providers control what lands here.
16086
16188
  */
16087
- var AutomationConditionExpressionSchema = object({
16088
- kind: literal("expression"),
16089
- expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
16090
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
16091
- }).superRefine((src, ctx) => {
16092
- const err = validateExpressionSource(src);
16093
- if (err !== null) ctx.addIssue({
16094
- code: "custom",
16095
- message: err,
16096
- path: ["expr"]
16097
- });
16189
+ var SourceInfoSchema = object({
16190
+ /** Live dispatch key — mutable when the source system allows rename. */
16191
+ id: string(),
16192
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
16193
+ system: string(),
16194
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
16195
+ * Used to detect rename when `id` changes. */
16196
+ uniqueId: string().optional(),
16197
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
16198
+ quantity: ReadingQuantitySchema.optional(),
16199
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
16200
+ dateTimeShape: _enum([
16201
+ "date",
16202
+ "time",
16203
+ "datetime"
16204
+ ]).optional(),
16205
+ /** Provider-specific extras that don't fit the structured slots.
16206
+ * Whitelist what goes here per provider — do NOT dump entire HA
16207
+ * attribute blobs (would explode the meta JSON column). */
16208
+ raw: record(string(), unknown()).optional()
16098
16209
  });
16099
- var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
16100
- object({
16101
- kind: literal("all"),
16102
- children: array(AutomationConditionSchema)
16103
- }),
16104
- object({
16105
- kind: literal("any"),
16106
- children: array(AutomationConditionSchema)
16107
- }),
16108
- object({
16109
- kind: literal("not"),
16110
- child: AutomationConditionSchema
16111
- }),
16112
- AutomationConditionLeafSchema,
16113
- AutomationConditionExpressionSchema,
16114
- AutomationConditionStationaryPresentSchema
16115
- ]));
16116
16210
  /**
16117
- * What starts a run.
16118
- *
16119
- * D8 compliance, and it is the reason `device-state` is not merely an event
16120
- * subscription: the trigger evaluates against the **state mirror**, which is
16121
- * reconciled, and an event only WAKES the evaluation. A dropped event therefore
16122
- * DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
16123
- * already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
16124
- * not.
16211
+ * Build the synthetic SourceInfo every existing provider falls back to
16212
+ * when no upstream value has been persisted. Keeps non-migrated providers
16213
+ * (Reolink / Hikvision / ONVIF / RTSP) functional without code
16214
+ * changes — `id` reuses the CamStack stableId, `system` reuses the addon
16215
+ * id. Real upstream identity replaces this once a provider migrates and
16216
+ * calls `updateSourceInfo()` with concrete values.
16125
16217
  */
16126
- var AutomationTriggerSchema = discriminatedUnion("kind", [
16127
- object({
16128
- kind: literal("device-state"),
16129
- deviceId: number().int().nonnegative(),
16130
- cap: string().min(1),
16131
- fieldPath: string().min(1),
16132
- /** Fire when the field takes this value. Omit to fire on any change. */
16133
- becomes: union([
16134
- string(),
16135
- number(),
16136
- boolean()
16137
- ]).optional(),
16138
- /** Only on a CHANGE of value, not on every re-report. */
16139
- edge: boolean().optional(),
16140
- /** The condition must hold this long before the run starts. */
16141
- forMs: number().int().min(0).max(864e5).optional(),
16142
- /** Collapse a burst into one run. */
16143
- debounceMs: number().int().min(0).max(6e5).optional()
16144
- }),
16145
- object({
16146
- kind: literal("device-event"),
16147
- /** An `EventCategory` value. */
16148
- category: string().min(1),
16149
- deviceId: number().int().nonnegative().optional()
16150
- }),
16151
- object({
16152
- kind: literal("schedule"),
16153
- cron: string().min(1).max(120)
16154
- }),
16155
- object({ kind: literal("manual") }),
16156
- object({
16157
- kind: literal("detection"),
16158
- /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
16159
- * matches every raw class that maps to it; any other name matches
16160
- * exactly. */
16161
- classes: array(string().min(1)).min(1),
16162
- deviceIds: array(number().int().nonnegative()).min(1),
16163
- /** The track must have visited at least one of these zones. */
16164
- zones: array(string().min(1)).min(1).optional(),
16165
- /** Best-so-far confidence the track must reach. */
16166
- minScore: number().min(0).max(1).optional()
16167
- }),
16168
- object({
16169
- kind: literal("stationary"),
16170
- deviceIds: array(number().int().nonnegative()).min(1),
16171
- /** Class names; a macro class matches like the detection trigger. */
16172
- classes: array(string().min(1)).min(1).optional(),
16173
- /** The parked object must be in at least one of these zones. */
16174
- zones: array(string().min(1)).min(1).optional(),
16175
- /** `appeared`: it became parked. `departed`: the registry retired it.
16176
- * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
16177
- phase: _enum(["appeared", "departed"])
16178
- }),
16179
- object({
16180
- kind: literal("interval"),
16181
- minutes: number().int().min(1).max(1440)
16182
- })
16183
- ]);
16218
+ function synthesizeSourceInfo(input) {
16219
+ return {
16220
+ id: input.stableId,
16221
+ system: input.addonId
16222
+ };
16223
+ }
16184
16224
  /**
16185
- * One action step.
16225
+ * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
16226
+ * patch values are ignored (no field clobber); to clear a field
16227
+ * explicitly, providers omit it from the patch and rebuild from scratch
16228
+ * via `updateSourceInfo({})` cycles — uncommon enough that we don't
16229
+ * bother with a `null`-means-delete sentinel.
16186
16230
  *
16187
- * `wait` and `cap` are `NcRuleActionSchema`'s two members, kept structurally
16188
- * identical so `NcRuleActionRunner` runs them unchanged — its device-scope
16189
- * check, stop-at-first-failure and per-sequence throttle are the whole reason
16190
- * to reuse it, and none of them are re-implemented here.
16231
+ * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
16232
+ * merges over existing `raw` keys instead of replacing the whole bag.
16233
+ */
16234
+ function mergeSourceInfo(prev, patch) {
16235
+ const next = { ...prev };
16236
+ for (const [k, v] of Object.entries(patch)) {
16237
+ if (v === void 0) continue;
16238
+ if (k === "raw" && typeof v === "object" && v !== null) {
16239
+ next.raw = {
16240
+ ...prev.raw,
16241
+ ...v
16242
+ };
16243
+ continue;
16244
+ }
16245
+ next[k] = v;
16246
+ }
16247
+ return next;
16248
+ }
16249
+ /**
16250
+ * Coerce common unit-of-measurement string variants to a canonical form
16251
+ * so the UI's display templates can rely on stable strings. Pure
16252
+ * passthrough for unknown units — kept conservative on purpose, the goal
16253
+ * is to normalise the long tail of HA `unit_of_measurement` values that
16254
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
16191
16255
  *
16192
- * **The one divergence, and it is forced.** `NcRuleActionSchema.cap.deviceId` is
16193
- * a literal `z.number().int()`, and the NC runner's own `RunSequencesInput`
16194
- * documents its subject device as *"for the log tag, never for routing"*. So an
16195
- * NC action can never target the device that triggered it — which is fine for
16196
- * the NC (its rules already scope to a device) and fatal for an automation
16197
- * ("sound the siren of the camera that saw the person"). `deviceId` therefore
16198
- * also accepts `{ $var }`, resolved from the run's `vars` bag BEFORE the runner
16199
- * is called. The runner still receives a number and is untouched; the
16200
- * resolution is the recipe's job, not the runner's.
16256
+ * Extend the map as we encounter real-world data. Returning the raw
16257
+ * input for unknown values is intentional: providers should never
16258
+ * lose information silently here.
16201
16259
  */
16202
- var AutomationWaitStepSchema = object({
16203
- kind: literal("wait"),
16204
- seconds: number().min(0).max(300)
16205
- });
16206
- var AutomationCapStepSchema = object({
16207
- kind: literal("cap"),
16208
- /**
16209
- * One device, several same-family devices (locks / climate family / …),
16210
- * or `{ $var }` for the device that triggered the run. An array is
16211
- * fan-out at codegen — each id gets the same cap.method call.
16212
- */
16213
- deviceId: union([
16214
- number().int(),
16215
- array(number().int()).min(1),
16216
- object({ $var: string().min(1) })
16217
- ]),
16218
- cap: string().min(1),
16219
- method: string().min(1),
16220
- /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
16221
- * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
16222
- * the expression leaf. */
16223
- args: record(string(), unknown()).optional()
16224
- });
16260
+ function normalizeUnit(raw) {
16261
+ if (raw == null) return void 0;
16262
+ const trimmed = raw.trim();
16263
+ if (trimmed === "") return void 0;
16264
+ const lower = trimmed.toLowerCase();
16265
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
16266
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
16267
+ if (lower === "k" || lower === "kelvin") return "K";
16268
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
16269
+ if (lower === "lx" || lower === "lux") return "lx";
16270
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
16271
+ if (lower === "kpa") return "kPa";
16272
+ if (lower === "pa") return "Pa";
16273
+ if (lower === "mbar" || lower === "millibar") return "mbar";
16274
+ if (lower === "w" || lower === "watt") return "W";
16275
+ if (lower === "kw" || lower === "kilowatt") return "kW";
16276
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
16277
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
16278
+ if (lower === "v" || lower === "volt") return "V";
16279
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
16280
+ if (lower === "ma" || lower === "milliampere") return "mA";
16281
+ if (lower === "va") return "VA";
16282
+ if (lower === "ppm") return "ppm";
16283
+ if (lower === "ppb") return "ppb";
16284
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
16285
+ if (lower === "km/h" || lower === "kph") return "km/h";
16286
+ if (lower === "mph") return "mph";
16287
+ if (lower === "m/s") return "m/s";
16288
+ if (lower === "m") return "m";
16289
+ if (lower === "m²" || lower === "m2") return "m²";
16290
+ if (lower === "mm") return "mm";
16291
+ if (lower === "cm") return "cm";
16292
+ if (lower === "km") return "km";
16293
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
16294
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
16295
+ if (lower === "kg") return "kg";
16296
+ if (lower === "g") return "g";
16297
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
16298
+ if (lower === "ml") return "mL";
16299
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
16300
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
16301
+ if (lower === "ms") return "ms";
16302
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
16303
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
16304
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
16305
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
16306
+ if (lower === "kib") return "KiB";
16307
+ if (lower === "mib") return "MiB";
16308
+ if (lower === "gib") return "GiB";
16309
+ if (lower === "tib") return "TiB";
16310
+ if (lower === "kb") return "kB";
16311
+ if (lower === "mb") return "MB";
16312
+ if (lower === "gb") return "GB";
16313
+ if (lower === "dbm") return "dBm";
16314
+ if (lower === "dbfs") return "dBFS";
16315
+ if (lower === "inhg") return "inHg";
16316
+ if (lower === "mm/h") return "mm/h";
16317
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
16318
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
16319
+ if (lower === "kbps") return "kbps";
16320
+ if (lower === "fps") return "fps";
16321
+ return trimmed;
16322
+ }
16225
16323
  /**
16226
- * The steps an internal Action may contain: cap and wait, and nothing that can
16227
- * call back into an Action — so an Action can never reach itself (no cycles).
16324
+ * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
16325
+ * blob is a free-form `Record<string, unknown>` shared with hardware-
16326
+ * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
16327
+ * lives under a single nested key to keep it composable with those.
16228
16328
  */
16229
- var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
16230
- var AutomationActionSchema = discriminatedUnion("kind", [
16231
- AutomationWaitStepSchema,
16232
- AutomationCapStepSchema,
16233
- object({
16234
- kind: literal("action"),
16235
- /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
16236
- actionId: string().min(1),
16237
- /** Values for the action's declared variables; the others take their default. */
16238
- variables: record(string(), union([
16239
- string(),
16240
- number(),
16241
- boolean()
16242
- ])).optional()
16243
- }),
16244
- object({
16245
- kind: literal("code"),
16246
- /** Compiled into the automation's OWN block by esbuild — not a third
16247
- * runtime, not a `vm`, and not dynamically evaluated. */
16248
- code: string().min(1).max(2e4)
16249
- })
16250
- ]);
16329
+ var SOURCE_INFO_METADATA_KEY = "sourceInfo";
16251
16330
  /**
16252
- * How a trigger that arrives while the automation is busy (or cooling down) is
16253
- * handled. One of the automation's execution modes, never a property of its
16254
- * actions. Absent means `single`.
16331
+ * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
16332
+ * with Zod. Returns `null` when the blob is missing or the nested
16333
+ * `sourceInfo` key fails validation — callers fall back to the
16334
+ * synthetic default in that case (see `synthesizeSourceInfo`).
16335
+ */
16336
+ function extractSourceInfoFromMetadata(metadata) {
16337
+ if (!metadata) return null;
16338
+ const raw = metadata[SOURCE_INFO_METADATA_KEY];
16339
+ if (!raw) return null;
16340
+ const parsed = SourceInfoSchema.safeParse(raw);
16341
+ if (!parsed.success) return null;
16342
+ return parsed.data;
16343
+ }
16344
+ /**
16345
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
16346
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
16347
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
16348
+ * decimals). K derives from `°C = K − 273.15`.
16349
+ */
16350
+ var UNIT_TABLE = {
16351
+ "°C": {
16352
+ dimension: "temperature",
16353
+ factor: 1,
16354
+ offset: 0
16355
+ },
16356
+ "°F": {
16357
+ dimension: "temperature",
16358
+ factor: 5 / 9,
16359
+ offset: -160 / 9
16360
+ },
16361
+ K: {
16362
+ dimension: "temperature",
16363
+ factor: 1,
16364
+ offset: -273.15
16365
+ },
16366
+ hPa: {
16367
+ dimension: "pressure",
16368
+ factor: 1,
16369
+ offset: 0
16370
+ },
16371
+ kPa: {
16372
+ dimension: "pressure",
16373
+ factor: 10,
16374
+ offset: 0
16375
+ },
16376
+ Pa: {
16377
+ dimension: "pressure",
16378
+ factor: .01,
16379
+ offset: 0
16380
+ },
16381
+ mbar: {
16382
+ dimension: "pressure",
16383
+ factor: 1,
16384
+ offset: 0
16385
+ },
16386
+ bar: {
16387
+ dimension: "pressure",
16388
+ factor: 1e3,
16389
+ offset: 0
16390
+ },
16391
+ inHg: {
16392
+ dimension: "pressure",
16393
+ factor: 33.8639,
16394
+ offset: 0
16395
+ },
16396
+ mmHg: {
16397
+ dimension: "pressure",
16398
+ factor: 1.33322,
16399
+ offset: 0
16400
+ },
16401
+ psi: {
16402
+ dimension: "pressure",
16403
+ factor: 68.9476,
16404
+ offset: 0
16405
+ },
16406
+ "m/s": {
16407
+ dimension: "speed",
16408
+ factor: 1,
16409
+ offset: 0
16410
+ },
16411
+ "km/h": {
16412
+ dimension: "speed",
16413
+ factor: 1 / 3.6,
16414
+ offset: 0
16415
+ },
16416
+ mph: {
16417
+ dimension: "speed",
16418
+ factor: .44704,
16419
+ offset: 0
16420
+ },
16421
+ kn: {
16422
+ dimension: "speed",
16423
+ factor: .514444,
16424
+ offset: 0
16425
+ },
16426
+ "mm/h": {
16427
+ dimension: "precipitation-rate",
16428
+ factor: 1,
16429
+ offset: 0
16430
+ },
16431
+ "in/h": {
16432
+ dimension: "precipitation-rate",
16433
+ factor: 25.4,
16434
+ offset: 0
16435
+ },
16436
+ m: {
16437
+ dimension: "length",
16438
+ factor: 1,
16439
+ offset: 0
16440
+ },
16441
+ mm: {
16442
+ dimension: "length",
16443
+ factor: .001,
16444
+ offset: 0
16445
+ },
16446
+ cm: {
16447
+ dimension: "length",
16448
+ factor: .01,
16449
+ offset: 0
16450
+ },
16451
+ km: {
16452
+ dimension: "length",
16453
+ factor: 1e3,
16454
+ offset: 0
16455
+ },
16456
+ in: {
16457
+ dimension: "length",
16458
+ factor: .0254,
16459
+ offset: 0
16460
+ },
16461
+ ft: {
16462
+ dimension: "length",
16463
+ factor: .3048,
16464
+ offset: 0
16465
+ },
16466
+ mi: {
16467
+ dimension: "length",
16468
+ factor: 1609.344,
16469
+ offset: 0
16470
+ },
16471
+ "m²": {
16472
+ dimension: "area",
16473
+ factor: 1,
16474
+ offset: 0
16475
+ },
16476
+ lx: {
16477
+ dimension: "illuminance",
16478
+ factor: 1,
16479
+ offset: 0
16480
+ },
16481
+ "W/m²": {
16482
+ dimension: "irradiance",
16483
+ factor: 1,
16484
+ offset: 0
16485
+ },
16486
+ W: {
16487
+ dimension: "power",
16488
+ factor: 1,
16489
+ offset: 0
16490
+ },
16491
+ kW: {
16492
+ dimension: "power",
16493
+ factor: 1e3,
16494
+ offset: 0
16495
+ },
16496
+ VA: {
16497
+ dimension: "apparent-power",
16498
+ factor: 1,
16499
+ offset: 0
16500
+ },
16501
+ Wh: {
16502
+ dimension: "energy",
16503
+ factor: 1,
16504
+ offset: 0
16505
+ },
16506
+ kWh: {
16507
+ dimension: "energy",
16508
+ factor: 1e3,
16509
+ offset: 0
16510
+ },
16511
+ MWh: {
16512
+ dimension: "energy",
16513
+ factor: 1e6,
16514
+ offset: 0
16515
+ },
16516
+ V: {
16517
+ dimension: "voltage",
16518
+ factor: 1,
16519
+ offset: 0
16520
+ },
16521
+ mV: {
16522
+ dimension: "voltage",
16523
+ factor: .001,
16524
+ offset: 0
16525
+ },
16526
+ A: {
16527
+ dimension: "current",
16528
+ factor: 1,
16529
+ offset: 0
16530
+ },
16531
+ mA: {
16532
+ dimension: "current",
16533
+ factor: .001,
16534
+ offset: 0
16535
+ },
16536
+ ppm: {
16537
+ dimension: "concentration-volume",
16538
+ factor: 1,
16539
+ offset: 0
16540
+ },
16541
+ ppb: {
16542
+ dimension: "concentration-volume",
16543
+ factor: .001,
16544
+ offset: 0
16545
+ },
16546
+ "µg/m³": {
16547
+ dimension: "concentration-mass",
16548
+ factor: 1,
16549
+ offset: 0
16550
+ },
16551
+ "mg/m³": {
16552
+ dimension: "concentration-mass",
16553
+ factor: 1e3,
16554
+ offset: 0
16555
+ },
16556
+ kg: {
16557
+ dimension: "mass",
16558
+ factor: 1,
16559
+ offset: 0
16560
+ },
16561
+ g: {
16562
+ dimension: "mass",
16563
+ factor: .001,
16564
+ offset: 0
16565
+ },
16566
+ lb: {
16567
+ dimension: "mass",
16568
+ factor: .453592,
16569
+ offset: 0
16570
+ },
16571
+ oz: {
16572
+ dimension: "mass",
16573
+ factor: .0283495,
16574
+ offset: 0
16575
+ },
16576
+ "%": {
16577
+ dimension: "percentage",
16578
+ factor: 1,
16579
+ offset: 0
16580
+ },
16581
+ L: {
16582
+ dimension: "volume",
16583
+ factor: 1,
16584
+ offset: 0
16585
+ },
16586
+ mL: {
16587
+ dimension: "volume",
16588
+ factor: .001,
16589
+ offset: 0
16590
+ },
16591
+ gal: {
16592
+ dimension: "volume",
16593
+ factor: 3.785411784,
16594
+ offset: 0
16595
+ },
16596
+ s: {
16597
+ dimension: "duration",
16598
+ factor: 1,
16599
+ offset: 0
16600
+ },
16601
+ ms: {
16602
+ dimension: "duration",
16603
+ factor: .001,
16604
+ offset: 0
16605
+ },
16606
+ min: {
16607
+ dimension: "duration",
16608
+ factor: 60,
16609
+ offset: 0
16610
+ },
16611
+ h: {
16612
+ dimension: "duration",
16613
+ factor: 3600,
16614
+ offset: 0
16615
+ },
16616
+ d: {
16617
+ dimension: "duration",
16618
+ factor: 86400,
16619
+ offset: 0
16620
+ },
16621
+ B: {
16622
+ dimension: "information",
16623
+ factor: 1,
16624
+ offset: 0
16625
+ },
16626
+ KiB: {
16627
+ dimension: "information",
16628
+ factor: 1024,
16629
+ offset: 0
16630
+ },
16631
+ MiB: {
16632
+ dimension: "information",
16633
+ factor: 1024 ** 2,
16634
+ offset: 0
16635
+ },
16636
+ GiB: {
16637
+ dimension: "information",
16638
+ factor: 1024 ** 3,
16639
+ offset: 0
16640
+ },
16641
+ TiB: {
16642
+ dimension: "information",
16643
+ factor: 1024 ** 4,
16644
+ offset: 0
16645
+ },
16646
+ kB: {
16647
+ dimension: "information",
16648
+ factor: 1e3,
16649
+ offset: 0
16650
+ },
16651
+ MB: {
16652
+ dimension: "information",
16653
+ factor: 1e6,
16654
+ offset: 0
16655
+ },
16656
+ GB: {
16657
+ dimension: "information",
16658
+ factor: 1e9,
16659
+ offset: 0
16660
+ },
16661
+ "°": {
16662
+ dimension: "angle",
16663
+ factor: 1,
16664
+ offset: 0
16665
+ },
16666
+ dBFS: {
16667
+ dimension: "sound-level",
16668
+ factor: 1,
16669
+ offset: 0
16670
+ },
16671
+ dBm: {
16672
+ dimension: "signal-level",
16673
+ factor: 1,
16674
+ offset: 0
16675
+ },
16676
+ mired: {
16677
+ dimension: "colour-temperature",
16678
+ factor: 1,
16679
+ offset: 0
16680
+ },
16681
+ kbps: {
16682
+ dimension: "bitrate",
16683
+ factor: 1,
16684
+ offset: 0
16685
+ },
16686
+ Mbps: {
16687
+ dimension: "bitrate",
16688
+ factor: 1e3,
16689
+ offset: 0
16690
+ },
16691
+ fps: {
16692
+ dimension: "rate",
16693
+ factor: 1,
16694
+ offset: 0
16695
+ }
16696
+ };
16697
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
16698
+ function specFor(unit) {
16699
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
16700
+ }
16701
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
16702
+ function unitDimension(unit) {
16703
+ return specFor(unit)?.dimension;
16704
+ }
16705
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
16706
+ function toCanonical(value, spec) {
16707
+ return value * spec.factor + spec.offset;
16708
+ }
16709
+ /** Canonical-unit value → the unit described by `spec`. */
16710
+ function fromCanonical(canonical, spec) {
16711
+ return (canonical - spec.offset) / spec.factor;
16712
+ }
16713
+ /**
16714
+ * Graceful variant for the render path: returns the converted value, or
16715
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
16716
+ * Never throws — a render caller falls back to the raw value + source unit.
16717
+ */
16718
+ function tryConvertUnit(value, from, to) {
16719
+ if (from === to) return value;
16720
+ const fromSpec = specFor(from);
16721
+ if (!fromSpec) return void 0;
16722
+ const toSpec = specFor(to);
16723
+ if (!toSpec) return void 0;
16724
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
16725
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
16726
+ }
16727
+ /**
16728
+ * Every known unit spelling in a dimension (table insertion order — canonical
16729
+ * unit first). Powers the UI's same-dimension unit picker.
16730
+ */
16731
+ /**
16732
+ * Convert a difference, not a reading. Affine units (°F) shift an absolute
16733
+ * value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
16734
+ */
16735
+ function tryConvertDelta(value, from, to) {
16736
+ if (from === to) return value;
16737
+ const origin = tryConvertUnit(0, from, to);
16738
+ const shifted = tryConvertUnit(value, from, to);
16739
+ if (origin === void 0 || shifted === void 0) return void 0;
16740
+ return shifted - origin;
16741
+ }
16742
+ /**
16743
+ * A number a person types for a physical quantity — the input side of the
16744
+ * value + unit contract (D972).
16255
16745
  *
16256
- * - `single` — drop the trigger while a run is in progress (traced + logged).
16257
- * - `restart` — abort the run in progress (at its next step boundary), start fresh.
16258
- * - `queued` — queue up to `max` triggers, drop the rest (traced).
16259
- * - `parallel` — up to `max` runs at once, drop the rest (traced).
16260
- * - `cooldown` — `single`, and no new run until `minutes` after the last run
16261
- * STARTED; persisted across a restart. A manual trigger bypasses only this
16262
- * wait, never the single/queued limits.
16746
+ * Readings are stored in the base unit of their quantity (D944); a cap method
16747
+ * param is stored in the unit its input schema DECLARES (`paramMeta`). The
16748
+ * wire pair {@link QuantityValue} says "this number is expressed in this
16749
+ * unit"; {@link convertQuantityInput} brings it to the stored unit and refuses,
16750
+ * with a typed {@link QuantityUnitError}, a unit that cannot be that quantity.
16263
16751
  */
16264
- var AutomationRunModeSchema = discriminatedUnion("mode", [
16265
- object({ mode: literal("single") }),
16266
- object({ mode: literal("restart") }),
16267
- object({
16268
- mode: literal("queued"),
16269
- max: number().int().min(1).max(10)
16270
- }),
16271
- object({
16272
- mode: literal("parallel"),
16273
- max: number().int().min(1).max(10)
16274
- }),
16275
- object({
16276
- mode: literal("cooldown"),
16277
- minutes: number().int().min(1).max(1440)
16278
- })
16279
- ]);
16280
- var AutomationRecipeSchema = object({
16281
- triggers: array(AutomationTriggerSchema),
16282
- conditions: AutomationConditionSchema.optional(),
16283
- actions: array(AutomationActionSchema),
16284
- runMode: AutomationRunModeSchema.optional(),
16285
- /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
16286
- keepTraces: number().int().min(1).max(100).optional()
16752
+ /** A unit that cannot be applied to the number it was given with. */
16753
+ var QuantityUnitError = class extends Error {
16754
+ code;
16755
+ constructor(code, message) {
16756
+ super(`[${code}] ${message}`);
16757
+ this.name = "QuantityUnitError";
16758
+ this.code = code;
16759
+ }
16760
+ };
16761
+ /** The wire pair: `value` is expressed in `unit`. */
16762
+ var QuantityValueSchema = object({
16763
+ value: number(),
16764
+ unit: string().min(1)
16765
+ });
16766
+ /**
16767
+ * Zod `.meta()` bag for a cap METHOD param that is a physical quantity — the
16768
+ * input-side twin of `readingMeta`. The value is carried in `unit` (a gram
16769
+ * portion stays in grams); the unit must belong to the quantity, checked once
16770
+ * when the cap is declared.
16771
+ */
16772
+ function paramMeta(declared) {
16773
+ const base = baseUnitOf(declared.quantity);
16774
+ if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
16775
+ if (tryConvertUnit(1, declared.unit, base) === void 0) throw new QuantityUnitError("cross-dimension", `"${declared.unit}" is not a unit of ${declared.quantity}`);
16776
+ return {
16777
+ quantity: declared.quantity,
16778
+ unit: declared.unit
16779
+ };
16780
+ }
16781
+ object({
16782
+ quantity: ReadingQuantitySchema,
16783
+ unit: string().min(1)
16287
16784
  });
16288
16785
  new Set([
16289
16786
  {
@@ -16330,16 +16827,610 @@ var COCO_TO_MACRO = {
16330
16827
  },
16331
16828
  preserveOriginal: false
16332
16829
  };
16830
+ /**
16831
+ * WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
16832
+ * animal, package) plus an OPTIONAL label that narrows it.
16833
+ *
16834
+ * The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
16835
+ * second list. The label that fits depends on the macro class:
16836
+ *
16837
+ * - `animal` → a sub-class (`dog`, `cat`, …);
16838
+ * - `person` → a known face (an identity of the `face-gallery`);
16839
+ * - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
16840
+ * - `package` → a sub-class.
16841
+ *
16842
+ * `other` exists only so a class the taxonomy does not know (a custom model's
16843
+ * `forklift`) survives the conversion of a stored recipe without widening.
16844
+ *
16845
+ * A label is decided LATE (D683): face recognition and LPR land after the
16846
+ * detection starts. The track-lifecycle `update` event carries the newly
16847
+ * resolved `label` / `plateText`, which is why a leaf with a label fires when
16848
+ * the subject GETS it — not at the first detection.
16849
+ */
16850
+ var SubClassLabelSchema = object({
16851
+ kind: literal("sub-class"),
16852
+ className: string().min(1)
16853
+ });
16854
+ var IdentityLabelSchema = object({
16855
+ kind: literal("identity"),
16856
+ /** The `face-gallery` identity id (stable across a rename). */
16857
+ identityId: string().min(1),
16858
+ /** The identity's name — what a track-lifecycle event carries as `label`. */
16859
+ name: string().min(1)
16860
+ });
16861
+ var PlateLabelSchema = object({
16862
+ kind: literal("plate"),
16863
+ text: string().min(1)
16864
+ });
16865
+ function macroOf$1(className) {
16866
+ return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
16867
+ }
16868
+ /** A sub-class label must belong to the macro it is under. */
16869
+ function subClassBelongs(macro, label) {
16870
+ return label === void 0 || macroOf$1(label.className) === macro;
16871
+ }
16872
+ var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
16873
+ var DetectionSubjectSchema = discriminatedUnion("macro", [
16874
+ object({
16875
+ macro: literal("animal"),
16876
+ label: subClassOf("animal").optional()
16877
+ }),
16878
+ object({
16879
+ macro: literal("person"),
16880
+ label: IdentityLabelSchema.optional()
16881
+ }),
16882
+ object({
16883
+ macro: literal("vehicle"),
16884
+ label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
16885
+ }),
16886
+ object({
16887
+ macro: literal("package"),
16888
+ label: subClassOf("package").optional()
16889
+ }),
16890
+ object({
16891
+ macro: literal("other"),
16892
+ className: string().min(1)
16893
+ })
16894
+ ]);
16895
+ /**
16896
+ * The subject of a PARKED object. The stationary registry's event carries a
16897
+ * class and a zone, no face and no plate — an identity or plate label there
16898
+ * could never match, so the schema refuses it instead of letting it sit dead.
16899
+ */
16900
+ var StationarySubjectSchema = DetectionSubjectSchema.refine((s) => s.macro === "other" || s.label === void 0 || s.label.kind === "sub-class", { message: "a parked object carries no face or plate — only a class can narrow it" });
16901
+ /**
16902
+ * A stored class string as a subject: a macro name stays a macro; a raw class
16903
+ * the taxonomy files under a macro becomes that macro plus the sub-class label
16904
+ * (`dog` → animal / dog); anything else is `other`, kept verbatim.
16905
+ */
16906
+ function subjectFromClassName(className) {
16907
+ const name = className.trim().toLowerCase();
16908
+ const macro = COCO_TO_MACRO.mapping[name];
16909
+ if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
16910
+ if (macro === void 0) return {
16911
+ macro: "other",
16912
+ className
16913
+ };
16914
+ const label = {
16915
+ kind: "sub-class",
16916
+ className: name
16917
+ };
16918
+ switch (macro) {
16919
+ case "animal": return {
16920
+ macro: "animal",
16921
+ label
16922
+ };
16923
+ case "vehicle": return {
16924
+ macro: "vehicle",
16925
+ label
16926
+ };
16927
+ case "package": return {
16928
+ macro: "package",
16929
+ label
16930
+ };
16931
+ default: return {
16932
+ macro: "other",
16933
+ className
16934
+ };
16935
+ }
16936
+ }
16937
+ function macroOnly(macro) {
16938
+ switch (macro) {
16939
+ case "animal": return { macro: "animal" };
16940
+ case "person": return { macro: "person" };
16941
+ case "vehicle": return { macro: "vehicle" };
16942
+ case "package": return { macro: "package" };
16943
+ default: return {
16944
+ macro: "other",
16945
+ className: macro
16946
+ };
16947
+ }
16948
+ }
16949
+ /** How a state leaf compares a device field to a value. Derived from the
16950
+ * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
16951
+ var AutomationConditionOperatorSchema = _enum([
16952
+ "eq",
16953
+ "ne",
16954
+ "gt",
16955
+ "gte",
16956
+ "lt",
16957
+ "lte",
16958
+ "contains",
16959
+ "in"
16960
+ ]);
16961
+ /**
16962
+ * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
16963
+ * subscription: it evaluates against the **state mirror**, which is reconciled,
16964
+ * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
16965
+ * fire; it does not lose it.
16966
+ */
16967
+ var DeviceBecomesLeafSchema = object({
16968
+ kind: literal("device-becomes"),
16969
+ deviceId: number().int().nonnegative(),
16970
+ cap: string().min(1),
16971
+ fieldPath: string().min(1),
16972
+ /** Fire when the field takes this value. Omit to fire on any change. */
16973
+ becomes: union([
16974
+ string(),
16975
+ number(),
16976
+ boolean()
16977
+ ]).optional(),
16978
+ /** Only on a CHANGE of value, not on every re-report. */
16979
+ edge: boolean().optional(),
16980
+ /** The new value must hold this long before it counts. */
16981
+ forMs: number().int().min(0).max(864e5).optional(),
16982
+ /** Collapse a burst into one occurrence. */
16983
+ debounceMs: number().int().min(0).max(6e5).optional()
16984
+ });
16985
+ var DeviceEventLeafSchema = object({
16986
+ kind: literal("device-event"),
16987
+ /** An `EventCategory` value. */
16988
+ category: string().min(1),
16989
+ deviceId: number().int().nonnegative().optional()
16990
+ });
16991
+ var ScheduleLeafSchema = object({
16992
+ kind: literal("schedule"),
16993
+ cron: string().min(1).max(120)
16994
+ });
16995
+ var ManualLeafSchema = object({ kind: literal("manual") });
16996
+ var IntervalLeafSchema = object({
16997
+ kind: literal("interval"),
16998
+ minutes: number().int().min(1).max(1440)
16999
+ });
17000
+ var DetectionLeafSchema = object({
17001
+ kind: literal("detection"),
17002
+ /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
17003
+ subjects: array(DetectionSubjectSchema).min(1),
17004
+ deviceIds: array(number().int().nonnegative()).min(1),
17005
+ /** The track must have visited at least one of these zones. */
17006
+ zones: array(string().min(1)).min(1).optional(),
17007
+ /** Best-so-far confidence the track must reach. */
17008
+ minScore: number().min(0).max(1).optional()
17009
+ });
17010
+ var StationaryLeafSchema = object({
17011
+ kind: literal("stationary"),
17012
+ deviceIds: array(number().int().nonnegative()).min(1),
17013
+ subjects: array(StationarySubjectSchema).min(1).optional(),
17014
+ zones: array(string().min(1)).min(1).optional(),
17015
+ /** `appeared`: it became parked. `departed`: the registry retired it. */
17016
+ phase: _enum(["appeared", "departed"])
17017
+ });
17018
+ /** "is": one device field compared to one value, NOW. */
17019
+ var DeviceIsLeafSchema = object({
17020
+ kind: literal("device-is"),
17021
+ deviceId: number().int().nonnegative(),
17022
+ cap: string().min(1),
17023
+ fieldPath: string().min(1),
17024
+ operator: AutomationConditionOperatorSchema,
17025
+ /**
17026
+ * The value. A number on a quantity field is stored in the field's base unit
17027
+ * (D944); a quantity value is the WRITE form, which the save boundary
17028
+ * replaces with the converted number and `unit`. A leaf that still carries
17029
+ * one is never evaluated (it fails).
17030
+ */
17031
+ value: union([
17032
+ string(),
17033
+ number(),
17034
+ boolean(),
17035
+ array(union([string(), number()])),
17036
+ QuantityValueSchema
17037
+ ]),
17038
+ /** The unit the person picked for `value` (display only; `value` is in the base unit). */
17039
+ unit: string().min(1).optional()
17040
+ });
17041
+ /**
17042
+ * True when the camera currently has a PARKED object matching `subjects` in at
17043
+ * least one of `zones`. Reads the zone-analytics snapshot through the same
17044
+ * mirror as every other state leaf, so a dropped event never decides it (D49).
17045
+ */
17046
+ var StationaryPresentLeafSchema = object({
17047
+ kind: literal("stationary-present"),
17048
+ deviceId: number().int().nonnegative(),
17049
+ subjects: array(StationarySubjectSchema).min(1).optional(),
17050
+ zones: array(string().min(1)).min(1).optional()
17051
+ });
17052
+ /**
17053
+ * The escape hatch: the bounded expression engine over named device bindings.
17054
+ * Declared as a plain object so a discriminated union can read `kind`; the
17055
+ * author-time validation is the SAME function `ExpressionSourceSchema` runs.
17056
+ */
17057
+ var ExpressionLeafSchema = object({
17058
+ kind: literal("expression"),
17059
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
17060
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
17061
+ }).superRefine((src, ctx) => {
17062
+ const err = validateExpressionSource(src);
17063
+ if (err !== null) ctx.addIssue({
17064
+ code: "custom",
17065
+ message: err,
17066
+ path: ["expr"]
17067
+ });
17068
+ });
17069
+ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
17070
+ object({
17071
+ kind: literal("all"),
17072
+ children: array(AutomationWhenSchema)
17073
+ }),
17074
+ object({
17075
+ kind: literal("any"),
17076
+ children: array(AutomationWhenSchema)
17077
+ }),
17078
+ object({
17079
+ kind: literal("not"),
17080
+ child: AutomationWhenSchema
17081
+ }),
17082
+ DeviceBecomesLeafSchema,
17083
+ DeviceEventLeafSchema,
17084
+ ScheduleLeafSchema,
17085
+ ManualLeafSchema,
17086
+ IntervalLeafSchema,
17087
+ DetectionLeafSchema,
17088
+ StationaryLeafSchema,
17089
+ DeviceIsLeafSchema,
17090
+ StationaryPresentLeafSchema,
17091
+ ExpressionLeafSchema
17092
+ ]));
17093
+ /**
17094
+ * The recipe shape before the single `when` tree: a flat list of triggers
17095
+ * (OR'd, each an instant) and an optional condition tree. Stored recipes —
17096
+ * the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
17097
+ * older client still posts — are in this shape, so it stays READABLE here, and
17098
+ * {@link convertLegacyRecipe} turns it into the new one, losslessly and one
17099
+ * way. Nothing writes this shape any more.
17100
+ *
17101
+ * `{ triggers: [t1…tn], conditions?: C }`
17102
+ * → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
17103
+ * a single trigger is not wrapped in `any`.
17104
+ * a `condition` leaf → a `device-is` leaf (the "is" verb);
17105
+ * a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
17106
+ * `classes: ['dog']` → the macro `animal` with the label `dog`.
17107
+ */
17108
+ var LegacyTriggerSchema = discriminatedUnion("kind", [
17109
+ object({
17110
+ kind: literal("device-state"),
17111
+ deviceId: number().int().nonnegative(),
17112
+ cap: string().min(1),
17113
+ fieldPath: string().min(1),
17114
+ becomes: union([
17115
+ string(),
17116
+ number(),
17117
+ boolean()
17118
+ ]).optional(),
17119
+ edge: boolean().optional(),
17120
+ forMs: number().int().min(0).max(864e5).optional(),
17121
+ debounceMs: number().int().min(0).max(6e5).optional()
17122
+ }),
17123
+ object({
17124
+ kind: literal("device-event"),
17125
+ category: string().min(1),
17126
+ deviceId: number().int().nonnegative().optional()
17127
+ }),
17128
+ object({
17129
+ kind: literal("schedule"),
17130
+ cron: string().min(1).max(120)
17131
+ }),
17132
+ object({ kind: literal("manual") }),
17133
+ object({
17134
+ kind: literal("detection"),
17135
+ classes: array(string().min(1)).min(1),
17136
+ deviceIds: array(number().int().nonnegative()).min(1),
17137
+ zones: array(string().min(1)).min(1).optional(),
17138
+ minScore: number().min(0).max(1).optional()
17139
+ }),
17140
+ object({
17141
+ kind: literal("stationary"),
17142
+ deviceIds: array(number().int().nonnegative()).min(1),
17143
+ classes: array(string().min(1)).min(1).optional(),
17144
+ zones: array(string().min(1)).min(1).optional(),
17145
+ phase: _enum(["appeared", "departed"])
17146
+ }),
17147
+ object({
17148
+ kind: literal("interval"),
17149
+ minutes: number().int().min(1).max(1440)
17150
+ })
17151
+ ]);
17152
+ var LegacyConditionLeafSchema = object({
17153
+ kind: literal("condition"),
17154
+ deviceId: number().int().nonnegative(),
17155
+ cap: string().min(1),
17156
+ fieldPath: string().min(1),
17157
+ operator: AutomationConditionOperatorSchema,
17158
+ value: union([
17159
+ string(),
17160
+ number(),
17161
+ boolean(),
17162
+ array(union([string(), number()])),
17163
+ QuantityValueSchema
17164
+ ]),
17165
+ unit: string().min(1).optional()
17166
+ });
17167
+ var LegacyStationaryPresentSchema = object({
17168
+ kind: literal("stationary-present"),
17169
+ deviceId: number().int().nonnegative(),
17170
+ classes: array(string().min(1)).min(1).optional(),
17171
+ zones: array(string().min(1)).min(1).optional()
17172
+ });
17173
+ var LegacyExpressionSchema = object({
17174
+ kind: literal("expression"),
17175
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
17176
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
17177
+ }).superRefine((src, ctx) => {
17178
+ const err = validateExpressionSource(src);
17179
+ if (err !== null) ctx.addIssue({
17180
+ code: "custom",
17181
+ message: err,
17182
+ path: ["expr"]
17183
+ });
17184
+ });
17185
+ var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
17186
+ object({
17187
+ kind: literal("all"),
17188
+ children: array(LegacyConditionSchema)
17189
+ }),
17190
+ object({
17191
+ kind: literal("any"),
17192
+ children: array(LegacyConditionSchema)
17193
+ }),
17194
+ object({
17195
+ kind: literal("not"),
17196
+ child: LegacyConditionSchema
17197
+ }),
17198
+ LegacyConditionLeafSchema,
17199
+ LegacyExpressionSchema,
17200
+ LegacyStationaryPresentSchema
17201
+ ]));
17202
+ function convertTrigger(trigger) {
17203
+ switch (trigger.kind) {
17204
+ case "device-state": return {
17205
+ ...trigger,
17206
+ kind: "device-becomes"
17207
+ };
17208
+ case "detection": {
17209
+ const { classes, ...rest } = trigger;
17210
+ return {
17211
+ ...rest,
17212
+ subjects: classes.map(subjectFromClassName)
17213
+ };
17214
+ }
17215
+ case "stationary": {
17216
+ const { classes, ...rest } = trigger;
17217
+ return {
17218
+ ...rest,
17219
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17220
+ };
17221
+ }
17222
+ default: return trigger;
17223
+ }
17224
+ }
17225
+ /** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
17226
+ function convertLegacyCondition(condition) {
17227
+ switch (condition.kind) {
17228
+ case "condition": return {
17229
+ ...condition,
17230
+ kind: "device-is"
17231
+ };
17232
+ case "stationary-present": {
17233
+ const { classes, ...rest } = condition;
17234
+ return {
17235
+ ...rest,
17236
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
17237
+ };
17238
+ }
17239
+ case "expression": return condition;
17240
+ case "not": return {
17241
+ kind: "not",
17242
+ child: convertLegacyCondition(condition.child)
17243
+ };
17244
+ case "all":
17245
+ case "any": return {
17246
+ kind: condition.kind,
17247
+ children: condition.children.map(convertLegacyCondition)
17248
+ };
17249
+ }
17250
+ }
17251
+ /** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
17252
+ function convertLegacyWhen(half) {
17253
+ const leaves = half.triggers.map(convertTrigger);
17254
+ const first = leaves[0];
17255
+ const triggers = leaves.length === 1 && first !== void 0 ? first : {
17256
+ kind: "any",
17257
+ children: leaves
17258
+ };
17259
+ if (half.conditions === void 0) return triggers;
17260
+ return {
17261
+ kind: "all",
17262
+ children: [triggers, convertLegacyCondition(half.conditions)]
17263
+ };
17264
+ }
17265
+ /**
17266
+ * The automation RECIPE — what a visually-authored automation is, before it
17267
+ * becomes code.
17268
+ *
17269
+ * This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
17270
+ * Phase 6. A recipe is not an engine and never evaluates anything: it is a
17271
+ * declarative description that {@link generateAutomationBlock} compiles to the
17272
+ * TypeScript of an ordinary core block. There is therefore no second rule
17273
+ * engine, no second store and no second runtime — the block runner is the
17274
+ * runtime, `core_blocks` is the store (§2.1).
17275
+ *
17276
+ * Two parts:
17277
+ *
17278
+ * - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
17279
+ * (instants) and state leaves (durations). It replaces the flat trigger list
17280
+ * plus the separate condition tree; the firing rule is in that file and in
17281
+ * ADR "one tree".
17282
+ * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
17283
+ * fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
17284
+ * this had to diverge from "verbatim", and why.
17285
+ *
17286
+ * A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
17287
+ * {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
17288
+ */
17289
+ /**
17290
+ * One action step.
17291
+ *
17292
+ * `wait` and `cap` are `NcRuleActionSchema`'s two members, kept structurally
17293
+ * identical so `NcRuleActionRunner` runs them unchanged — its device-scope
17294
+ * check, stop-at-first-failure and per-sequence throttle are the whole reason
17295
+ * to reuse it, and none of them are re-implemented here.
17296
+ *
17297
+ * **The one divergence, and it is forced.** `NcRuleActionSchema.cap.deviceId` is
17298
+ * a literal `z.number().int()`, and the NC runner's own `RunSequencesInput`
17299
+ * documents its subject device as *"for the log tag, never for routing"*. So an
17300
+ * NC action can never target the device that triggered it — which is fine for
17301
+ * the NC (its rules already scope to a device) and fatal for an automation
17302
+ * ("sound the siren of the camera that saw the person"). `deviceId` therefore
17303
+ * also accepts `{ $var }`, resolved from the run's `vars` bag BEFORE the runner
17304
+ * is called. The runner still receives a number and is untouched; the
17305
+ * resolution is the recipe's job, not the runner's.
17306
+ */
17307
+ var AutomationWaitStepSchema = object({
17308
+ kind: literal("wait"),
17309
+ seconds: number().min(0).max(300)
17310
+ });
17311
+ var AutomationCapStepSchema = object({
17312
+ kind: literal("cap"),
17313
+ /**
17314
+ * One device, several same-family devices (locks / climate family / …),
17315
+ * or `{ $var }` for the device that triggered the run. An array is
17316
+ * fan-out at codegen — each id gets the same cap.method call.
17317
+ */
17318
+ deviceId: union([
17319
+ number().int(),
17320
+ array(number().int()).min(1),
17321
+ object({ $var: string().min(1) })
17322
+ ]),
17323
+ cap: string().min(1),
17324
+ method: string().min(1),
17325
+ /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
17326
+ * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
17327
+ * the expression leaf. */
17328
+ args: record(string(), unknown()).optional(),
17329
+ /**
17330
+ * The unit the person picked per arg (display only). `args` always hold the
17331
+ * value in the unit the method's input DECLARES (`paramMeta`); on a write an
17332
+ * arg may be a {@link QuantityValue}, which the save boundary converts.
17333
+ */
17334
+ argUnits: record(string(), string().min(1)).optional()
17335
+ });
17336
+ /**
17337
+ * The steps an internal Action may contain: cap and wait, and nothing that can
17338
+ * call back into an Action — so an Action can never reach itself (no cycles).
17339
+ */
17340
+ var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
17341
+ var AutomationActionSchema = discriminatedUnion("kind", [
17342
+ AutomationWaitStepSchema,
17343
+ AutomationCapStepSchema,
17344
+ object({
17345
+ kind: literal("action"),
17346
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
17347
+ actionId: string().min(1),
17348
+ /** Values for the action's declared variables; the others take their default. */
17349
+ variables: record(string(), union([
17350
+ string(),
17351
+ number(),
17352
+ boolean()
17353
+ ])).optional()
17354
+ }),
17355
+ object({
17356
+ kind: literal("code"),
17357
+ /** Compiled into the automation's OWN block by esbuild — not a third
17358
+ * runtime, not a `vm`, and not dynamically evaluated. */
17359
+ code: string().min(1).max(2e4)
17360
+ })
17361
+ ]);
17362
+ /**
17363
+ * How a trigger that arrives while the automation is busy (or cooling down) is
17364
+ * handled. One of the automation's execution modes, never a property of its
17365
+ * actions. Absent means `single`.
17366
+ *
17367
+ * - `single` — drop the trigger while a run is in progress (traced + logged).
17368
+ * - `restart` — abort the run in progress (at its next step boundary), start fresh.
17369
+ * - `queued` — queue up to `max` triggers, drop the rest (traced).
17370
+ * - `parallel` — up to `max` runs at once, drop the rest (traced).
17371
+ * - `cooldown` — `single`, and no new run until `minutes` after the last run
17372
+ * STARTED; persisted across a restart. A manual trigger bypasses only this
17373
+ * wait, never the single/queued limits.
17374
+ */
17375
+ var AutomationRunModeSchema = discriminatedUnion("mode", [
17376
+ object({ mode: literal("single") }),
17377
+ object({ mode: literal("restart") }),
17378
+ object({
17379
+ mode: literal("queued"),
17380
+ max: number().int().min(1).max(10)
17381
+ }),
17382
+ object({
17383
+ mode: literal("parallel"),
17384
+ max: number().int().min(1).max(10)
17385
+ }),
17386
+ object({
17387
+ mode: literal("cooldown"),
17388
+ minutes: number().int().min(1).max(1440)
17389
+ })
17390
+ ]);
17391
+ var CurrentAutomationRecipeSchema = object({
17392
+ when: AutomationWhenSchema,
17393
+ actions: array(AutomationActionSchema),
17394
+ runMode: AutomationRunModeSchema.optional(),
17395
+ /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
17396
+ keepTraces: number().int().min(1).max(100).optional()
17397
+ });
17398
+ /** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
17399
+ var LegacyAutomationRecipeSchema = object({
17400
+ triggers: array(LegacyTriggerSchema),
17401
+ conditions: LegacyConditionSchema.optional(),
17402
+ actions: array(AutomationActionSchema),
17403
+ runMode: AutomationRunModeSchema.optional(),
17404
+ keepTraces: number().int().min(1).max(100).optional()
17405
+ });
17406
+ /** The old shape as the new one, lossless. */
17407
+ function convertLegacyRecipe(legacy) {
17408
+ const { triggers, conditions, ...rest } = legacy;
17409
+ return {
17410
+ when: convertLegacyWhen({
17411
+ triggers,
17412
+ conditions
17413
+ }),
17414
+ ...rest
17415
+ };
17416
+ }
17417
+ /**
17418
+ * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
17419
+ * saved block carries): the new shape as it is, or the old shape converted to
17420
+ * it. The output type is always the new one.
17421
+ */
17422
+ var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
16333
17423
  new Set(Object.values(COCO_TO_MACRO.mapping));
16334
17424
  /**
16335
17425
  * Run traces — what an automation (or, later, an Action run) DID, node by node,
16336
17426
  * so the editor can paint every block green or red with its reason.
16337
17427
  *
16338
- * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
16339
- * every condition node (containers and leaves, including the ones a
16340
- * short-circuit never reached), the run-mode verdict and every action step —
16341
- * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
16342
- * for an Action run). Skipped
17428
+ * A trace is data, not a log: it covers the WHOLE tree — the event leaf that
17429
+ * fired, every node of the `when` tree (containers and leaves, including the
17430
+ * ones a short-circuit never reached), the run-mode verdict and every action
17431
+ * step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
17432
+ * for an Action run). Traces written before the single tree carry
17433
+ * `conditions.*` paths and still parse. Skipped
16343
17434
  * evaluations are traced too, and identical consecutive skips collapse into a
16344
17435
  * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
16345
17436
  *
@@ -16371,6 +17462,8 @@ var TraceNodeOutcomeSchema = _enum([
16371
17462
  ]);
16372
17463
  var AutomationTriggerTraceSchema = object({
16373
17464
  kind: string().min(1),
17465
+ /** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
17466
+ path: string().min(1).optional(),
16374
17467
  outcome: _enum(["pass", "fail"]),
16375
17468
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
16376
17469
  reason: string(),
@@ -16378,15 +17471,23 @@ var AutomationTriggerTraceSchema = object({
16378
17471
  vars: record(string(), TraceValueSchema)
16379
17472
  });
16380
17473
  var AutomationConditionTraceNodeSchema = object({
16381
- /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
17474
+ /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
16382
17475
  path: string().min(1),
16383
17476
  kind: _enum([
16384
17477
  "all",
16385
17478
  "any",
16386
17479
  "not",
16387
17480
  "condition",
17481
+ "device-is",
16388
17482
  "expression",
16389
- "stationary-present"
17483
+ "stationary-present",
17484
+ "device-becomes",
17485
+ "device-event",
17486
+ "schedule",
17487
+ "interval",
17488
+ "manual",
17489
+ "detection",
17490
+ "stationary"
16390
17491
  ]),
16391
17492
  outcome: TraceNodeOutcomeSchema,
16392
17493
  reason: string(),
@@ -16420,6 +17521,12 @@ var AutomationStepTraceSchema = object({
16420
17521
  deviceId: number().int().optional(),
16421
17522
  startedAt: number().optional(),
16422
17523
  endedAt: number().optional(),
17524
+ /**
17525
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
17526
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
17527
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
17528
+ */
17529
+ args: record(string(), TraceValueSchema).optional(),
16423
17530
  outcome: _enum([
16424
17531
  "ok",
16425
17532
  "error",
@@ -16600,6 +17707,16 @@ object({
16600
17707
  lastRunAt: number().nullable(),
16601
17708
  lastError: string().nullable()
16602
17709
  });
17710
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
17711
+ var UnitWarningSchema = object({
17712
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
17713
+ path: string(),
17714
+ deviceId: number().int().optional(),
17715
+ field: string(),
17716
+ value: number(),
17717
+ assumedUnit: string(),
17718
+ message: string()
17719
+ });
16603
17720
  /**
16604
17721
  * notification-output — canonical, capability-gated notification delivery.
16605
17722
  *
@@ -17338,6 +18455,23 @@ method(object({
17338
18455
  addonId: string(),
17339
18456
  nodeId: string().optional()
17340
18457
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
18458
+ addonId: string(),
18459
+ nodeId: string().optional()
18460
+ }), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
18461
+ addonId: string(),
18462
+ nodeId: string().optional(),
18463
+ keys: array(string())
18464
+ }), object({
18465
+ removed: array(string()),
18466
+ refused: array(object({
18467
+ key: string(),
18468
+ reason: string()
18469
+ })),
18470
+ secretsCleared: number()
18471
+ }), {
18472
+ kind: "mutation",
18473
+ auth: "admin"
18474
+ }), method(object({
17341
18475
  addonId: string(),
17342
18476
  deviceId: number(),
17343
18477
  nodeId: string().optional()
@@ -18773,8 +19907,21 @@ var BrokerProviderInfoSchema = object({
18773
19907
  * (`{ sessionId }`) drops the session. The dialog that edits a broker
18774
19908
  * offers it as the default path; the fields the session fills are
18775
19909
  * marked `filledBySignIn` in the kind's settings schema.
19910
+ *
19911
+ * `methods` lists the ways the session can sign in, recommended first:
19912
+ * `'device-code'` adds `POST {basePath}/code/start` (body: the form's
19913
+ * settings) answering `{ sessionId, userCode, verificationUrl,
19914
+ * expiresAt }` — the operator types `userCode` at `verificationUrl` on
19915
+ * any device, and the same `status` / `commit` / `cancel` routes follow
19916
+ * (status then carries `method`, `userCode`, `verificationUrl`,
19917
+ * `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
19918
+ * `'proxy'` is the `{basePath}/start` flow above. Absent means
19919
+ * `['proxy']`.
18776
19920
  */
18777
- signIn: object({ basePath: string().min(1) }).optional()
19921
+ signIn: object({
19922
+ basePath: string().min(1),
19923
+ methods: array(_enum(["device-code", "proxy"])).optional()
19924
+ }).optional()
18778
19925
  }))
18779
19926
  });
18780
19927
  var ListInputSchema = object({
@@ -21403,6 +22550,8 @@ var StorageKindSchema = object({
21403
22550
  "orphaned",
21404
22551
  "dropped"
21405
22552
  ]),
22553
+ /** An owner declared this kind since the engine started. */
22554
+ declaredThisBoot: boolean(),
21406
22555
  /** `null` when the caller asked for no counts (`counts: false`). */
21407
22556
  rows: number().int().nonnegative().nullable(),
21408
22557
  /** Rows x the average size of a sample; null for an empty kind. */
@@ -21414,6 +22563,51 @@ var StorageKindSchema = object({
21414
22563
  /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
21415
22564
  declaration: RecordKindDeclarationSchema
21416
22565
  });
22566
+ _enum([
22567
+ "orphaned",
22568
+ "stale",
22569
+ "unknown"
22570
+ ]);
22571
+ /** A record kind nobody declared since its owner started, or whose owner is gone. */
22572
+ var StaleKindFindingSchema = object({
22573
+ owner: string(),
22574
+ kind: string(),
22575
+ /** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
22576
+ verdict: _enum(["orphaned", "unknown"]),
22577
+ reason: string(),
22578
+ rows: number().int().nonnegative().nullable(),
22579
+ /** local-auth's tables are never dropped by this audit. */
22580
+ protected: boolean()
22581
+ });
22582
+ /** A stored global-settings key the running addon no longer declares. */
22583
+ var StaleSettingsKeyFindingSchema = object({
22584
+ addonId: string(),
22585
+ /** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
22586
+ key: string(),
22587
+ /**
22588
+ * Keychain refs whose ONLY marker sits under this key: in use by stale data,
22589
+ * not orphans yet. Removing the key releases them.
22590
+ */
22591
+ secretRefs: array(string())
22592
+ });
22593
+ /** An addon the audit could not judge, with the reason. Never reported as stale. */
22594
+ var StaleUnknownSchema = object({
22595
+ subject: string(),
22596
+ reason: string()
22597
+ });
22598
+ var StaleDataReportSchema = object({
22599
+ /** Epoch ms of the last finished scan; `null` before the first. */
22600
+ scannedAt: number().nullable(),
22601
+ scanning: boolean(),
22602
+ trigger: _enum([
22603
+ "boot",
22604
+ "interval",
22605
+ "manual"
22606
+ ]).nullable(),
22607
+ kinds: array(StaleKindFindingSchema),
22608
+ settingsKeys: array(StaleSettingsKeyFindingSchema),
22609
+ unknown: array(StaleUnknownSchema)
22610
+ });
21417
22611
  method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
21418
22612
  owner: string().min(1).optional(),
21419
22613
  /** `false` skips the per-kind row count (a catalog read only). Default true. */
@@ -21421,7 +22615,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
21421
22615
  }), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
21422
22616
  owner: string().min(1),
21423
22617
  kind: RecordKindNameSchema
21424
- }), RecordQueryResultSchema, { auth: "admin" });
22618
+ }), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
22619
+ kind: "mutation",
22620
+ auth: "admin"
22621
+ }), method(object({
22622
+ owner: string().min(1),
22623
+ kind: RecordKindNameSchema
22624
+ }), object({
22625
+ deleted: number().int().nonnegative(),
22626
+ secretsReleased: number().int().nonnegative()
22627
+ }), {
22628
+ kind: "mutation",
22629
+ auth: "admin",
22630
+ caller: "required"
22631
+ }), method(object({
22632
+ addonId: string().min(1),
22633
+ key: string().min(1)
22634
+ }), object({
22635
+ removed: boolean(),
22636
+ secretsCleared: number().int().nonnegative()
22637
+ }), {
22638
+ kind: "mutation",
22639
+ auth: "admin",
22640
+ caller: "required"
22641
+ });
21425
22642
  /**
21426
22643
  * record-entries - the ONE admin surface for every list of user-created entries
21427
22644
  * (D881): notification targets, brokers, LLM profiles, anything an addon
@@ -21560,6 +22777,12 @@ method(object({
21560
22777
  }), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
21561
22778
  kind: "mutation",
21562
22779
  auth: "admin"
22780
+ }), method(object({
22781
+ refs: RefListSchema,
22782
+ onBehalfOf: string().min(1)
22783
+ }), object({ released: number().int() }), {
22784
+ kind: "mutation",
22785
+ auth: "admin"
21563
22786
  }), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
21564
22787
  /**
21565
22788
  * device-history — the ONE mechanism for numeric history (D850).
@@ -22425,190 +23648,6 @@ function parseAdminBase(baseUrl) {
22425
23648
  };
22426
23649
  }
22427
23650
  /**
22428
- * Identity envelope for a device's upstream-system metadata.
22429
- *
22430
- * Two jobs:
22431
- *
22432
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
22433
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
22434
- * WS-Discovery endpoint UUID, …). Provider code keeps a
22435
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
22436
- * without round-tripping through `stableId` / `id` mappings.
22437
- *
22438
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
22439
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
22440
- * for vendors where MAC is the natural key). For systems where the
22441
- * live dispatch key can be renamed (HA `entity_id` can be edited),
22442
- * providers detect renames by matching `uniqueId` and patch `id`
22443
- * in-place so dispatch keeps working.
22444
- *
22445
- * **Display unit and precision are NOT stored here.** They are a read-time
22446
- * preference (D944). A polymorphic reading stores its `quantity` here once,
22447
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
22448
- * neither is repeated on each sample. The `device_class` tag is an
22449
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
22450
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
22451
- *
22452
- * The `raw` escape hatch carries provider-specific attributes that don't
22453
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
22454
- * details, …). Providers control what lands here.
22455
- */
22456
- var SourceInfoSchema = object({
22457
- /** Live dispatch key — mutable when the source system allows rename. */
22458
- id: string(),
22459
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
22460
- system: string(),
22461
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
22462
- * Used to detect rename when `id` changes. */
22463
- uniqueId: string().optional(),
22464
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
22465
- quantity: ReadingQuantitySchema.optional(),
22466
- /** Date / time / datetime shape of a string reading, written once at adopt. */
22467
- dateTimeShape: _enum([
22468
- "date",
22469
- "time",
22470
- "datetime"
22471
- ]).optional(),
22472
- /** Provider-specific extras that don't fit the structured slots.
22473
- * Whitelist what goes here per provider — do NOT dump entire HA
22474
- * attribute blobs (would explode the meta JSON column). */
22475
- raw: record(string(), unknown()).optional()
22476
- });
22477
- /**
22478
- * Build the synthetic SourceInfo every existing provider falls back to
22479
- * when no upstream value has been persisted. Keeps non-migrated providers
22480
- * (Reolink / Hikvision / ONVIF / RTSP) functional without code
22481
- * changes — `id` reuses the CamStack stableId, `system` reuses the addon
22482
- * id. Real upstream identity replaces this once a provider migrates and
22483
- * calls `updateSourceInfo()` with concrete values.
22484
- */
22485
- function synthesizeSourceInfo(input) {
22486
- return {
22487
- id: input.stableId,
22488
- system: input.addonId
22489
- };
22490
- }
22491
- /**
22492
- * Shallow-merge a partial patch over a prior SourceInfo. `undefined`
22493
- * patch values are ignored (no field clobber); to clear a field
22494
- * explicitly, providers omit it from the patch and rebuild from scratch
22495
- * via `updateSourceInfo({})` cycles — uncommon enough that we don't
22496
- * bother with a `null`-means-delete sentinel.
22497
- *
22498
- * The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
22499
- * merges over existing `raw` keys instead of replacing the whole bag.
22500
- */
22501
- function mergeSourceInfo(prev, patch) {
22502
- const next = { ...prev };
22503
- for (const [k, v] of Object.entries(patch)) {
22504
- if (v === void 0) continue;
22505
- if (k === "raw" && typeof v === "object" && v !== null) {
22506
- next.raw = {
22507
- ...prev.raw,
22508
- ...v
22509
- };
22510
- continue;
22511
- }
22512
- next[k] = v;
22513
- }
22514
- return next;
22515
- }
22516
- /**
22517
- * Coerce common unit-of-measurement string variants to a canonical form
22518
- * so the UI's display templates can rely on stable strings. Pure
22519
- * passthrough for unknown units — kept conservative on purpose, the goal
22520
- * is to normalise the long tail of HA `unit_of_measurement` values that
22521
- * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
22522
- *
22523
- * Extend the map as we encounter real-world data. Returning the raw
22524
- * input for unknown values is intentional: providers should never
22525
- * lose information silently here.
22526
- */
22527
- function normalizeUnit(raw) {
22528
- if (raw == null) return void 0;
22529
- const trimmed = raw.trim();
22530
- if (trimmed === "") return void 0;
22531
- const lower = trimmed.toLowerCase();
22532
- if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
22533
- if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
22534
- if (lower === "k" || lower === "kelvin") return "K";
22535
- if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
22536
- if (lower === "lx" || lower === "lux") return "lx";
22537
- if (lower === "hpa" || lower === "hectopascal") return "hPa";
22538
- if (lower === "kpa") return "kPa";
22539
- if (lower === "pa") return "Pa";
22540
- if (lower === "mbar" || lower === "millibar") return "mbar";
22541
- if (lower === "w" || lower === "watt") return "W";
22542
- if (lower === "kw" || lower === "kilowatt") return "kW";
22543
- if (lower === "wh" || lower === "watt-hour") return "Wh";
22544
- if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
22545
- if (lower === "v" || lower === "volt") return "V";
22546
- if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
22547
- if (lower === "ma" || lower === "milliampere") return "mA";
22548
- if (lower === "va") return "VA";
22549
- if (lower === "ppm") return "ppm";
22550
- if (lower === "ppb") return "ppb";
22551
- if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
22552
- if (lower === "km/h" || lower === "kph") return "km/h";
22553
- if (lower === "mph") return "mph";
22554
- if (lower === "m/s") return "m/s";
22555
- if (lower === "m") return "m";
22556
- if (lower === "m²" || lower === "m2") return "m²";
22557
- if (lower === "mm") return "mm";
22558
- if (lower === "cm") return "cm";
22559
- if (lower === "km") return "km";
22560
- if (lower === "in" || lower === "inch" || lower === "inches") return "in";
22561
- if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
22562
- if (lower === "kg") return "kg";
22563
- if (lower === "g") return "g";
22564
- if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
22565
- if (lower === "ml") return "mL";
22566
- if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
22567
- if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
22568
- if (lower === "ms") return "ms";
22569
- if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
22570
- if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
22571
- if (lower === "d" || lower === "day" || lower === "days") return "d";
22572
- if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
22573
- if (lower === "kib") return "KiB";
22574
- if (lower === "mib") return "MiB";
22575
- if (lower === "gib") return "GiB";
22576
- if (lower === "tib") return "TiB";
22577
- if (lower === "kb") return "kB";
22578
- if (lower === "mb") return "MB";
22579
- if (lower === "gb") return "GB";
22580
- if (lower === "dbm") return "dBm";
22581
- if (lower === "dbfs") return "dBFS";
22582
- if (lower === "inhg") return "inHg";
22583
- if (lower === "mm/h") return "mm/h";
22584
- if (lower === "w/m²" || lower === "w/m2") return "W/m²";
22585
- if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
22586
- if (lower === "kbps") return "kbps";
22587
- if (lower === "fps") return "fps";
22588
- return trimmed;
22589
- }
22590
- /**
22591
- * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
22592
- * blob is a free-form `Record<string, unknown>` shared with hardware-
22593
- * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
22594
- * lives under a single nested key to keep it composable with those.
22595
- */
22596
- var SOURCE_INFO_METADATA_KEY = "sourceInfo";
22597
- /**
22598
- * Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
22599
- * with Zod. Returns `null` when the blob is missing or the nested
22600
- * `sourceInfo` key fails validation — callers fall back to the
22601
- * synthetic default in that case (see `synthesizeSourceInfo`).
22602
- */
22603
- function extractSourceInfoFromMetadata(metadata) {
22604
- if (!metadata) return null;
22605
- const raw = metadata[SOURCE_INFO_METADATA_KEY];
22606
- if (!raw) return null;
22607
- const parsed = SourceInfoSchema.safeParse(raw);
22608
- if (!parsed.success) return null;
22609
- return parsed.data;
22610
- }
22611
- /**
22612
23651
  * device-discovery — device-scoped capability for parents that host /
22613
23652
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
22614
23653
  * integrations). The parent advertises a list of discoverable children,
@@ -22680,7 +23719,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
22680
23719
  /** Upstream-system identity + rendering hints for the candidate. */
22681
23720
  sourceInfo: SourceInfoSchema.optional(),
22682
23721
  /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
22683
- children: array(DiscoveredChildDeviceSchema).readonly().optional()
23722
+ children: array(DiscoveredChildDeviceSchema).readonly().optional(),
23723
+ /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
23724
+ parentStableId: string().optional()
22684
23725
  }));
22685
23726
  /**
22686
23727
  * Status block — populated by the integration after each
@@ -43092,6 +44133,79 @@ object({
43092
44133
  metadata: record(string(), unknown()).optional()
43093
44134
  });
43094
44135
  /**
44136
+ * HOW a provider can be found on the network. The provider only DECLARES the
44137
+ * mode; the `local-network` scan engine runs it once for every provider that
44138
+ * asked (one multicast browse, one host sweep) and hands the raw hits back to
44139
+ * `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
44140
+ * except through a `custom` mode.
44141
+ */
44142
+ var DiscoveryModeSchema = discriminatedUnion("kind", [
44143
+ object({
44144
+ kind: literal("mdns"),
44145
+ /** DNS-SD service type, e.g. `_esphomelib._tcp`. */
44146
+ serviceType: string().min(1),
44147
+ /** Regular expression an instance name must match (case-insensitive). */
44148
+ instancePattern: string().optional()
44149
+ }),
44150
+ object({
44151
+ kind: literal("ws-discovery"),
44152
+ /** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
44153
+ types: array(string()).optional()
44154
+ }),
44155
+ object({
44156
+ kind: literal("ssdp"),
44157
+ searchTarget: string().min(1)
44158
+ }),
44159
+ object({
44160
+ kind: literal("udp-broadcast"),
44161
+ port: number().int().min(1).max(65535),
44162
+ payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
44163
+ /** Port replies arrive on, when the device answers somewhere other than the source port. */
44164
+ listenPort: number().int().min(1).max(65535).optional()
44165
+ }),
44166
+ object({
44167
+ kind: literal("http-probe"),
44168
+ port: number().int().min(1).max(65535),
44169
+ path: string().startsWith("/"),
44170
+ method: _enum(["GET", "POST"]).default("GET"),
44171
+ bodyText: string().optional(),
44172
+ contentType: string().optional(),
44173
+ tls: boolean().default(false),
44174
+ /** Status codes that count as a hit. Omitted = any HTTP answer. */
44175
+ acceptStatus: array(number().int()).optional()
44176
+ }),
44177
+ object({
44178
+ kind: literal("custom"),
44179
+ /** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
44180
+ reason: string().min(1)
44181
+ })
44182
+ ]);
44183
+ /** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
44184
+ var DiscoveryHitSchema = object({
44185
+ /** Index into the provider's own `getDiscoveryModes()` array. */
44186
+ modeIndex: number().int().min(0),
44187
+ host: string(),
44188
+ port: number().int(),
44189
+ http: object({
44190
+ status: number().int(),
44191
+ headers: record(string(), string()),
44192
+ /** At most 16 KiB. */
44193
+ bodyText: string()
44194
+ }).optional(),
44195
+ mdns: object({
44196
+ instance: string(),
44197
+ host: string(),
44198
+ port: number().int(),
44199
+ txt: record(string(), string())
44200
+ }).optional(),
44201
+ udp: object({ payloadHex: string() }).optional(),
44202
+ wsd: object({
44203
+ xaddrs: array(string()),
44204
+ types: array(string()),
44205
+ scopes: array(string())
44206
+ }).optional()
44207
+ });
44208
+ /**
43095
44209
  * Candidate handed back from discovery and accepted by
43096
44210
  * `adoptDiscoveredDevice`. Shape mirrors the in-process
43097
44211
  * `DiscoveredDevice` interface used by `DeviceDiscovery`.
@@ -43126,7 +44240,17 @@ var DiscoveryCandidateSchema = object({
43126
44240
  /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
43127
44241
  onboardedDeviceId: number().optional(),
43128
44242
  /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
43129
- onboardedName: string().optional()
44243
+ onboardedName: string().optional(),
44244
+ /**
44245
+ * This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
44246
+ * not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
44247
+ * which is still read for one release.
44248
+ */
44249
+ setupIntegration: boolean().optional(),
44250
+ /** `stableId` of the gateway candidate this device was found BEHIND. */
44251
+ parentStableId: string().optional(),
44252
+ /** `false` = shown for context but not pickable. Default true. */
44253
+ selectable: boolean().optional()
43130
44254
  });
43131
44255
  /**
43132
44256
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -43223,6 +44347,24 @@ var deviceProviderCapability = {
43223
44347
  auth: "admin"
43224
44348
  }),
43225
44349
  /**
44350
+ * How this provider is found on the network. `[]` = the provider has no
44351
+ * declarative mode and keeps answering `discoverDevices` itself.
44352
+ */
44353
+ getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
44354
+ /**
44355
+ * Turn the engine's raw hits into candidates. The provider may make
44356
+ * follow-up UNICAST requests to a hit host (an authenticated call, an
44357
+ * identity check) and never sweeps a subnet. `params` is the per-scan form
44358
+ * from `getDiscoveryParamsSchema`.
44359
+ */
44360
+ classifyDiscoveryHits: method(object({
44361
+ hits: array(DiscoveryHitSchema),
44362
+ params: record(string(), unknown()).optional()
44363
+ }), array(DiscoveryCandidateSchema), {
44364
+ kind: "mutation",
44365
+ auth: "admin"
44366
+ }),
44367
+ /**
43226
44368
  * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
43227
44369
  * provider accepts (e.g. Gree's broadcast address for a different subnet).
43228
44370
  * `null` when the provider takes no extra scan params — the generic
@@ -43987,17 +45129,23 @@ method(object({
43987
45129
  }), method(object({}), object({ providers: array(object({
43988
45130
  addonId: string(),
43989
45131
  label: string()
43990
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
43991
- addonId: string(),
43992
- label: string(),
43993
- candidates: array(DiscoveryCandidateSchema).readonly(),
43994
- error: string().nullable()
43995
- })).readonly() }), {
43996
- kind: "mutation",
43997
- auth: "admin"
43998
- }), method(object({
45132
+ })).readonly() }), { auth: "admin" }), method(
45133
+ /** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
45134
+ object({ subnets: array(string()).optional() }),
45135
+ object({ groups: array(object({
45136
+ addonId: string(),
45137
+ label: string(),
45138
+ candidates: array(DiscoveryCandidateSchema).readonly(),
45139
+ error: string().nullable()
45140
+ })).readonly() }),
45141
+ {
45142
+ kind: "mutation",
45143
+ auth: "admin"
45144
+ }
45145
+ ), method(object({
43999
45146
  addonId: string(),
44000
- params: record(string(), unknown()).optional()
45147
+ params: record(string(), unknown()).optional(),
45148
+ subnets: array(string()).optional()
44001
45149
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
44002
45150
  kind: "mutation",
44003
45151
  auth: "admin"
@@ -55637,11 +56785,13 @@ var ImageContractSchema = object({
55637
56785
  * The shell verdict (see {@link WrapperKindSchema}) with the proofs that
55638
56786
  * produced it.
55639
56787
  *
55640
- * There is deliberately NO `canSelfUpdate` and no apply action: a wrapper
55641
- * update is an ANNOUNCEMENT. The notification body carries the exact command
55642
- * for a `docker` shell and says the app restarts into the new build for
55643
- * `electron`, and that sentence is chosen from `kind` alone — a boolean
55644
- * nothing reads is the defect this repo keeps paying for.
56788
+ * There is deliberately NO `canSelfUpdate` boolean: a wrapper update is an
56789
+ * ANNOUNCEMENT. The notification body carries the exact command for a `docker`
56790
+ * shell and says the app restarts into the new build for `electron`, and that
56791
+ * sentence is chosen from `kind` alone — a boolean nothing reads is the defect
56792
+ * this repo keeps paying for. The one shell that CAN be acted on is the
56793
+ * Electron app, and only through `installDesktopUpdate` (see
56794
+ * {@link DesktopAppStatusSchema} for what the node reads from the app first).
55645
56795
  */
55646
56796
  var WrapperIdentitySchema = object({
55647
56797
  kind: _enum([
@@ -55689,6 +56839,49 @@ var ServerRollbackInfoSchema = object({
55689
56839
  reason: string()
55690
56840
  });
55691
56841
  /**
56842
+ * What the native desktop app (Electron) that hosts this node says about
56843
+ * itself, read from `<dataDir>/server-root/desktop-app-state.json` (ADR-0971).
56844
+ * Absent on the status = no desktop app, or it never wrote the file.
56845
+ *
56846
+ * `stale` is the honest answer to an app that stopped writing (closed, hung,
56847
+ * crashed): the update facts are then the LAST ones known, not current ones,
56848
+ * and a consumer must not read `update.available: false` from a stale record
56849
+ * as "no update". `stateAtMs` is when the app last wrote.
56850
+ */
56851
+ var DesktopAppStatusSchema = object({
56852
+ /** The Electron app's own version (not the node closure's). */
56853
+ version: string(),
56854
+ update: object({
56855
+ available: boolean(),
56856
+ /** The new build is downloaded and `installDesktopUpdate` can apply it. */
56857
+ downloaded: boolean(),
56858
+ version: string().nullable(),
56859
+ checking: boolean(),
56860
+ /** Why the app's last update check produced no answer; `null` when it did. */
56861
+ error: string().nullable()
56862
+ }),
56863
+ stateAtMs: number(),
56864
+ stale: boolean()
56865
+ });
56866
+ /**
56867
+ * Why `installDesktopUpdate` did nothing. Stable names a UI maps to a sentence:
56868
+ * - `not-electron` — this node does not run inside the desktop app.
56869
+ * - `no-app-state` — the app never published its state (or the file is unreadable).
56870
+ * - `app-state-stale` — the app stopped writing; it is probably not running.
56871
+ * - `not-downloaded` — no update is downloaded yet; nothing to install.
56872
+ */
56873
+ var InstallDesktopUpdateReasonSchema = _enum([
56874
+ "not-electron",
56875
+ "no-app-state",
56876
+ "app-state-stale",
56877
+ "not-downloaded"
56878
+ ]);
56879
+ var InstallDesktopUpdateResultSchema = object({
56880
+ accepted: boolean(),
56881
+ /** Non-null exactly when `accepted` is false. */
56882
+ reason: InstallDesktopUpdateReasonSchema.nullable()
56883
+ });
56884
+ /**
55692
56885
  * The runner boot check (D877): the runners this node PLANNED (hub: one per
55693
56886
  * `resolveRunnerId` over its forked addons; agent: the runners its process
55694
56887
  * service holds, because only the hub plans) against the states the process
@@ -55792,7 +56985,12 @@ var ServerPackageStatusSchema = object({
55792
56985
  * on a provider that predates it. Absent is "not checked yet", never "all
55793
56986
  * well": a consumer that needs the answer treats it as pending.
55794
56987
  */
55795
- runners: RunnerBootCheckSchema.optional()
56988
+ runners: RunnerBootCheckSchema.optional(),
56989
+ /**
56990
+ * The desktop app hosting this node (see {@link DesktopAppStatusSchema}).
56991
+ * Optional for version skew AND because most nodes have no desktop app.
56992
+ */
56993
+ desktopApp: DesktopAppStatusSchema.optional()
55796
56994
  });
55797
56995
  var ServerUpdateCheckResultSchema = object({
55798
56996
  packageName: string(),
@@ -55897,6 +57095,9 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
55897
57095
  kind: "mutation",
55898
57096
  auth: "admin",
55899
57097
  timeoutMs: 16 * 6e4
57098
+ }), method(_void(), InstallDesktopUpdateResultSchema, {
57099
+ kind: "mutation",
57100
+ auth: "admin"
55900
57101
  }), method(_void(), ServerUpdateActionResultSchema, {
55901
57102
  kind: "mutation",
55902
57103
  auth: "admin"
@@ -56627,6 +57828,12 @@ var TerminalInstanceInfoSchema = object({
56627
57828
  instanceId: string(),
56628
57829
  cameraStableId: string(),
56629
57830
  nodeId: string(),
57831
+ /**
57832
+ * False while the node the instance is bound to is not in the cluster
57833
+ * topology (forgotten, renamed, re-paired). The instance is kept; the
57834
+ * operator re-points it with `updateInstance({ targetNodeId })`.
57835
+ */
57836
+ nodeInCluster: boolean(),
56630
57837
  profileId: string(),
56631
57838
  profileLabel: string(),
56632
57839
  name: string(),
@@ -56676,6 +57883,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
56676
57883
  auth: "admin"
56677
57884
  }), method(object({
56678
57885
  instanceId: string().min(1),
57886
+ /** Re-point the instance to another node; the profile must exist there. */
57887
+ targetNodeId: string().min(1).max(256).optional(),
56679
57888
  name: string().trim().min(1).max(160).optional(),
56680
57889
  executable: string().max(1024).optional(),
56681
57890
  args: array(string().max(2048)).max(64).optional(),
@@ -59085,13 +60294,19 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
59085
60294
  }), method(ActionIdInput, _void(), {
59086
60295
  kind: "mutation",
59087
60296
  auth: "admin"
59088
- }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
60297
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({
60298
+ action: ActionEntrySchema,
60299
+ warnings: array(UnitWarningSchema).optional()
60300
+ }), {
59089
60301
  kind: "mutation",
59090
60302
  auth: "admin"
59091
60303
  }), method(object({
59092
60304
  actionId: string().min(1),
59093
60305
  action: ActionInputSchema.partial()
59094
- }), object({ action: ActionEntrySchema }), {
60306
+ }), object({
60307
+ action: ActionEntrySchema,
60308
+ warnings: array(UnitWarningSchema).optional()
60309
+ }), {
59095
60310
  kind: "mutation",
59096
60311
  auth: "admin"
59097
60312
  }), method(ActionIdInput, _void(), {
@@ -59673,6 +60888,30 @@ object({
59673
60888
  at: number()
59674
60889
  }).nullable()
59675
60890
  });
60891
+ var RemoteButtonShapeSchema = _enum([
60892
+ "rect",
60893
+ "rounded",
60894
+ "round",
60895
+ "pill"
60896
+ ]);
60897
+ var RemoteButtonToneSchema = _enum([
60898
+ "neutral",
60899
+ "accent",
60900
+ "danger",
60901
+ "red",
60902
+ "green",
60903
+ "yellow",
60904
+ "blue"
60905
+ ]);
60906
+ var RemoteGroupFrameSchema = _enum([
60907
+ "none",
60908
+ "panel",
60909
+ "divider",
60910
+ "dpad",
60911
+ "rocker-v",
60912
+ "rocker-h",
60913
+ "keypad"
60914
+ ]);
59676
60915
  var RemoteLayoutButtonSchema = object({
59677
60916
  id: string().min(1).max(40),
59678
60917
  /** 0-based grid position. */
@@ -59682,12 +60921,94 @@ var RemoteLayoutButtonSchema = object({
59682
60921
  colSpan: number().int().min(1).max(24),
59683
60922
  icon: string().min(1).max(60).optional(),
59684
60923
  label: string().min(1).max(40).optional(),
59685
- role: RemoteRoleSchema
60924
+ role: RemoteRoleSchema,
60925
+ /** Absent = the plain rendering every layout had before shapes existed. */
60926
+ shape: RemoteButtonShapeSchema.optional(),
60927
+ tone: RemoteButtonToneSchema.optional(),
60928
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
60929
+ groupId: string().min(1).max(40).optional()
59686
60930
  });
60931
+ /**
60932
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
60933
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
60934
+ * entry it came from (informational: the layout is self-contained).
60935
+ */
60936
+ var RemoteLayoutGroupSchema = object({
60937
+ id: string().min(1).max(40),
60938
+ componentId: string().min(1).max(80),
60939
+ frame: RemoteGroupFrameSchema,
60940
+ row: number().int().min(0).max(24),
60941
+ col: number().int().min(0).max(24),
60942
+ rowSpan: number().int().min(1).max(24),
60943
+ colSpan: number().int().min(1).max(24),
60944
+ title: string().min(1).max(40).optional()
60945
+ });
60946
+ function rectsOverlap(a, b) {
60947
+ return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
60948
+ }
60949
+ function rectContains(outer, inner) {
60950
+ return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
60951
+ }
60952
+ /**
60953
+ * The group rules of a layout: inside the grid, no two groups overlap, a
60954
+ * member lies inside its group, a divider holds nothing, and a button that is
60955
+ * not a member never sits inside someone else's rectangle (a group moves as
60956
+ * one thing, so a stray key under it would be left behind).
60957
+ */
60958
+ function refineGroups(grid, ctx) {
60959
+ const groups = grid.groups ?? [];
60960
+ const byId = /* @__PURE__ */ new Map();
60961
+ const fail = (message, path) => {
60962
+ ctx.addIssue({
60963
+ code: "custom",
60964
+ message,
60965
+ path
60966
+ });
60967
+ };
60968
+ groups.forEach((group, index) => {
60969
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
60970
+ "groups",
60971
+ index,
60972
+ "id"
60973
+ ]);
60974
+ byId.set(group.id, group);
60975
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
60976
+ for (let other = 0; other < index; other += 1) {
60977
+ const earlier = groups[other];
60978
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
60979
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
60980
+ return;
60981
+ }
60982
+ }
60983
+ });
60984
+ grid.buttons.forEach((button, index) => {
60985
+ if (button.groupId !== void 0) {
60986
+ const group = byId.get(button.groupId);
60987
+ if (group === void 0) {
60988
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
60989
+ "buttons",
60990
+ index,
60991
+ "groupId"
60992
+ ]);
60993
+ return;
60994
+ }
60995
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
60996
+ "buttons",
60997
+ index,
60998
+ "groupId"
60999
+ ]);
61000
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
61001
+ return;
61002
+ }
61003
+ const foreign = groups.find((group) => rectsOverlap(group, button));
61004
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
61005
+ });
61006
+ }
59687
61007
  object({
59688
61008
  columns: number().int().min(1).max(24),
59689
61009
  rows: number().int().min(1).max(24),
59690
- buttons: array(RemoteLayoutButtonSchema).max(200)
61010
+ buttons: array(RemoteLayoutButtonSchema).max(200),
61011
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
59691
61012
  }).superRefine((grid, ctx) => {
59692
61013
  const ids = /* @__PURE__ */ new Set();
59693
61014
  const taken = /* @__PURE__ */ new Map();
@@ -59724,6 +61045,7 @@ object({
59724
61045
  taken.set(cell, button.id);
59725
61046
  }
59726
61047
  });
61048
+ refineGroups(grid, ctx);
59727
61049
  });
59728
61050
  var RemoteBoundCommandSchema = object({
59729
61051
  role: RemoteRoleSchema,
@@ -59855,6 +61177,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
59855
61177
  kind: "mutation",
59856
61178
  auth: "admin"
59857
61179
  });
61180
+ var RemoteComponentCategorySchema = _enum([
61181
+ "power",
61182
+ "navigation",
61183
+ "volume",
61184
+ "numbers",
61185
+ "media",
61186
+ "colour",
61187
+ "apps",
61188
+ "layout"
61189
+ ]);
61190
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
61191
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
61192
+ var RemoteComponentButtonSchema = object({
61193
+ /** Joined to the instance id when the component is inserted. */
61194
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
61195
+ row: number().int().min(0).max(24),
61196
+ col: number().int().min(0).max(24),
61197
+ rowSpan: number().int().min(1).max(24),
61198
+ colSpan: number().int().min(1).max(24),
61199
+ role: RemoteRoleSchema,
61200
+ icon: string().min(1).max(60).optional(),
61201
+ label: string().min(1).max(40).optional(),
61202
+ shape: RemoteButtonShapeSchema.optional(),
61203
+ tone: RemoteButtonToneSchema.optional()
61204
+ });
61205
+ var RemoteLayoutComponentSchema = object({
61206
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
61207
+ name: string().min(1).max(60),
61208
+ nameI18n: object({
61209
+ en: string().min(1).max(60),
61210
+ it: string().min(1).max(60)
61211
+ }).optional(),
61212
+ /** A display icon key, resolved by the UI like a button icon. */
61213
+ icon: string().min(1).max(60),
61214
+ category: RemoteComponentCategorySchema,
61215
+ size: object({
61216
+ rows: number().int().min(1).max(24),
61217
+ cols: number().int().min(1).max(24)
61218
+ }),
61219
+ /** The frame drawn around the whole component, or null for loose buttons. */
61220
+ group: object({
61221
+ frame: RemoteGroupFrameSchema,
61222
+ title: string().min(1).max(40).optional()
61223
+ }).nullable(),
61224
+ buttons: array(RemoteComponentButtonSchema).max(40)
61225
+ }).superRefine((component, ctx) => {
61226
+ const suffixes = /* @__PURE__ */ new Set();
61227
+ component.buttons.forEach((button, index) => {
61228
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
61229
+ code: "custom",
61230
+ message: `button "${button.idSuffix}" is listed twice`,
61231
+ path: [
61232
+ "buttons",
61233
+ index,
61234
+ "idSuffix"
61235
+ ]
61236
+ });
61237
+ suffixes.add(button.idSuffix);
61238
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
61239
+ ctx.addIssue({
61240
+ code: "custom",
61241
+ message: `button "${button.idSuffix}" leaves the component`,
61242
+ path: ["buttons", index]
61243
+ });
61244
+ return;
61245
+ }
61246
+ for (let other = 0; other < index; other += 1) {
61247
+ const earlier = component.buttons[other];
61248
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
61249
+ ctx.addIssue({
61250
+ code: "custom",
61251
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
61252
+ path: ["buttons", index]
61253
+ });
61254
+ return;
61255
+ }
61256
+ }
61257
+ });
61258
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
61259
+ code: "custom",
61260
+ message: "a divider component holds no buttons",
61261
+ path: ["buttons"]
61262
+ });
61263
+ });
61264
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
59858
61265
  /**
59859
61266
  * button — device-scoped capability for HA `button.*` / `input_button.*`
59860
61267
  * fire-only actuators. A button entity has no readable state — pressing it
@@ -63219,7 +64626,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
63219
64626
  id: string(),
63220
64627
  settings: record(string(), unknown()),
63221
64628
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
63222
- secretKeys: array(string()).optional()
64629
+ secretKeys: array(string()).optional(),
64630
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
64631
+ skipRestart: boolean().optional()
63223
64632
  }), object({ success: boolean() }), {
63224
64633
  kind: "mutation",
63225
64634
  auth: "admin"
@@ -63847,6 +65256,39 @@ var TlsStatusSchema = object({
63847
65256
  hasCa: boolean()
63848
65257
  }).nullable()
63849
65258
  });
65259
+ /** One provider's ask of the scan engine: the modes it declared. */
65260
+ var ScanRequestSchema = object({
65261
+ providerId: string().min(1),
65262
+ modes: array(DiscoveryModeSchema)
65263
+ });
65264
+ var ScanErrorSchema = object({
65265
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
65266
+ providerId: string().optional(),
65267
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
65268
+ mode: string().optional(),
65269
+ message: string()
65270
+ });
65271
+ /** What one scan did, per mode family; the engine logs the same numbers once. */
65272
+ var ScanStatsSchema = object({
65273
+ hostsProbed: number().int(),
65274
+ hits: number().int(),
65275
+ timeouts: number().int(),
65276
+ byMode: record(string(), object({
65277
+ probed: number().int(),
65278
+ hits: number().int(),
65279
+ timeouts: number().int()
65280
+ }))
65281
+ });
65282
+ var ScanNetworkResultSchema = object({
65283
+ hits: array(object({
65284
+ providerId: string(),
65285
+ hit: DiscoveryHitSchema
65286
+ })),
65287
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
65288
+ subnetsScanned: array(string()),
65289
+ errors: array(ScanErrorSchema),
65290
+ stats: ScanStatsSchema
65291
+ });
63850
65292
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
63851
65293
  /**
63852
65294
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -63901,6 +65343,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
63901
65343
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
63902
65344
  kind: "mutation",
63903
65345
  auth: "admin"
65346
+ }), method(object({
65347
+ requests: array(ScanRequestSchema),
65348
+ subnets: array(string()).optional()
65349
+ }), ScanNetworkResultSchema, {
65350
+ kind: "mutation",
65351
+ auth: "admin",
65352
+ timeoutMs: 9e4
65353
+ }), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
65354
+ kind: "mutation",
65355
+ auth: "admin"
63904
65356
  }), method(_void(), TlsStatusSchema, {
63905
65357
  kind: "mutation",
63906
65358
  auth: "admin"
@@ -65988,6 +67440,14 @@ var PetFeederDeviceStatusSchema = _enum([
65988
67440
  "on_batteries"
65989
67441
  ]);
65990
67442
  var gramsPortion = number().int().min(4).max(200);
67443
+ /** A portion REQUESTED of the feeder is a mass the method takes in GRAMS (the
67444
+ * hardware's own unit). The bounds are grams; a caller entering another unit
67445
+ * converts to grams first (`paramMeta`, D972). The STATUS fields above stay
67446
+ * plain grams — they are not readings in the kilogram base. */
67447
+ var gramsParam = gramsPortion.meta(paramMeta({
67448
+ quantity: "mass",
67449
+ unit: "g"
67450
+ }));
65991
67451
  var PetFeederStatusSchema = object({
65992
67452
  /** Food in the TANK (hopper), as the feeder's own sensor reports it
65993
67453
  * (`state.food`). A coarse level code, not grams — `0` is empty. The
@@ -66076,9 +67536,9 @@ var petFeederCapability = {
66076
67536
  */
66077
67537
  feed: method(object({
66078
67538
  deviceId: number().int().nonnegative(),
66079
- grams: gramsPortion.optional(),
66080
- hopper1: gramsPortion.optional(),
66081
- hopper2: gramsPortion.optional()
67539
+ grams: gramsParam.optional(),
67540
+ hopper1: gramsParam.optional(),
67541
+ hopper2: gramsParam.optional()
66082
67542
  }), _void(), {
66083
67543
  kind: "mutation",
66084
67544
  auth: "protected"
@@ -67620,7 +69080,7 @@ var runningStateCapability = {
67620
69080
  volatileStateFields: ["lastFetchedAt"],
67621
69081
  customize: runningCustomize
67622
69082
  };
67623
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
69083
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
67624
69084
  kind: "mutation",
67625
69085
  auth: "admin"
67626
69086
  });
@@ -72233,6 +73693,124 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
72233
73693
  kind: "mutation",
72234
73694
  auth: "admin"
72235
73695
  });
73696
+ var btn = (idSuffix, row, col, init) => ({
73697
+ idSuffix,
73698
+ row,
73699
+ col,
73700
+ rowSpan: 1,
73701
+ colSpan: 1,
73702
+ ...init
73703
+ });
73704
+ function rocker(id, name, it, frame, title, buttons, size) {
73705
+ return {
73706
+ id,
73707
+ name,
73708
+ nameI18n: {
73709
+ en: name,
73710
+ it
73711
+ },
73712
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
73713
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
73714
+ size,
73715
+ group: title === null ? { frame } : {
73716
+ frame,
73717
+ title
73718
+ },
73719
+ buttons
73720
+ };
73721
+ }
73722
+ btn("power", 0, 0, {
73723
+ role: "power",
73724
+ shape: "round",
73725
+ tone: "danger"
73726
+ }), btn("up", 0, 1, {
73727
+ role: "up",
73728
+ icon: "chevron-up"
73729
+ }), btn("left", 1, 0, {
73730
+ role: "left",
73731
+ icon: "chevron-left"
73732
+ }), btn("ok", 1, 1, {
73733
+ role: "ok",
73734
+ shape: "round",
73735
+ tone: "accent"
73736
+ }), btn("right", 1, 2, {
73737
+ role: "right",
73738
+ icon: "chevron-right"
73739
+ }), btn("down", 2, 1, {
73740
+ role: "down",
73741
+ icon: "chevron-down"
73742
+ }), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
73743
+ role: "volume_up",
73744
+ icon: "plus",
73745
+ shape: "pill"
73746
+ }), btn("down", 1, 0, {
73747
+ role: "volume_down",
73748
+ icon: "minus",
73749
+ shape: "pill"
73750
+ })], {
73751
+ rows: 2,
73752
+ cols: 1
73753
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
73754
+ role: "channel_up",
73755
+ icon: "chevron-up",
73756
+ shape: "pill"
73757
+ }), btn("down", 1, 0, {
73758
+ role: "channel_down",
73759
+ icon: "chevron-down",
73760
+ shape: "pill"
73761
+ })], {
73762
+ rows: 2,
73763
+ cols: 1
73764
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
73765
+ role: "custom:minus",
73766
+ label: "Minus",
73767
+ icon: "minus",
73768
+ shape: "pill"
73769
+ }), btn("plus", 0, 1, {
73770
+ role: "custom:plus",
73771
+ label: "Plus",
73772
+ icon: "plus",
73773
+ shape: "pill"
73774
+ })], {
73775
+ rows: 1,
73776
+ cols: 2
73777
+ }), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
73778
+ role: "custom:color_red",
73779
+ label: "Red",
73780
+ tone: "red"
73781
+ }), btn("green", 0, 1, {
73782
+ role: "custom:color_green",
73783
+ label: "Green",
73784
+ tone: "green"
73785
+ }), btn("yellow", 0, 2, {
73786
+ role: "custom:color_yellow",
73787
+ label: "Yellow",
73788
+ tone: "yellow"
73789
+ }), btn("blue", 0, 3, {
73790
+ role: "custom:color_blue",
73791
+ label: "Blue",
73792
+ tone: "blue"
73793
+ }), btn("rewind", 0, 0, {
73794
+ role: "rewind",
73795
+ icon: "rewind"
73796
+ }), btn("play", 0, 1, {
73797
+ role: "play",
73798
+ icon: "play"
73799
+ }), btn("pause", 0, 2, {
73800
+ role: "pause",
73801
+ icon: "pause"
73802
+ }), btn("forward", 0, 3, {
73803
+ role: "fast_forward",
73804
+ icon: "fast-forward"
73805
+ }), btn("stop", 0, 4, {
73806
+ role: "stop",
73807
+ icon: "square"
73808
+ }), [
73809
+ 1,
73810
+ 2,
73811
+ 3,
73812
+ 4
73813
+ ].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
72236
73814
  discriminatedUnion("kind", [object({
72237
73815
  kind: literal("raw"),
72238
73816
  timings: array(number().int()),
@@ -72279,406 +73857,8 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
72279
73857
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
72280
73858
  }
72281
73859
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
72282
- /**
72283
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
72284
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
72285
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
72286
- * decimals). K derives from `°C = K − 273.15`.
72287
- */
72288
- var UNIT_TABLE = {
72289
- "°C": {
72290
- dimension: "temperature",
72291
- factor: 1,
72292
- offset: 0
72293
- },
72294
- "°F": {
72295
- dimension: "temperature",
72296
- factor: 5 / 9,
72297
- offset: -160 / 9
72298
- },
72299
- K: {
72300
- dimension: "temperature",
72301
- factor: 1,
72302
- offset: -273.15
72303
- },
72304
- hPa: {
72305
- dimension: "pressure",
72306
- factor: 1,
72307
- offset: 0
72308
- },
72309
- kPa: {
72310
- dimension: "pressure",
72311
- factor: 10,
72312
- offset: 0
72313
- },
72314
- Pa: {
72315
- dimension: "pressure",
72316
- factor: .01,
72317
- offset: 0
72318
- },
72319
- mbar: {
72320
- dimension: "pressure",
72321
- factor: 1,
72322
- offset: 0
72323
- },
72324
- bar: {
72325
- dimension: "pressure",
72326
- factor: 1e3,
72327
- offset: 0
72328
- },
72329
- inHg: {
72330
- dimension: "pressure",
72331
- factor: 33.8639,
72332
- offset: 0
72333
- },
72334
- mmHg: {
72335
- dimension: "pressure",
72336
- factor: 1.33322,
72337
- offset: 0
72338
- },
72339
- psi: {
72340
- dimension: "pressure",
72341
- factor: 68.9476,
72342
- offset: 0
72343
- },
72344
- "m/s": {
72345
- dimension: "speed",
72346
- factor: 1,
72347
- offset: 0
72348
- },
72349
- "km/h": {
72350
- dimension: "speed",
72351
- factor: 1 / 3.6,
72352
- offset: 0
72353
- },
72354
- mph: {
72355
- dimension: "speed",
72356
- factor: .44704,
72357
- offset: 0
72358
- },
72359
- kn: {
72360
- dimension: "speed",
72361
- factor: .514444,
72362
- offset: 0
72363
- },
72364
- "mm/h": {
72365
- dimension: "precipitation-rate",
72366
- factor: 1,
72367
- offset: 0
72368
- },
72369
- "in/h": {
72370
- dimension: "precipitation-rate",
72371
- factor: 25.4,
72372
- offset: 0
72373
- },
72374
- m: {
72375
- dimension: "length",
72376
- factor: 1,
72377
- offset: 0
72378
- },
72379
- mm: {
72380
- dimension: "length",
72381
- factor: .001,
72382
- offset: 0
72383
- },
72384
- cm: {
72385
- dimension: "length",
72386
- factor: .01,
72387
- offset: 0
72388
- },
72389
- km: {
72390
- dimension: "length",
72391
- factor: 1e3,
72392
- offset: 0
72393
- },
72394
- in: {
72395
- dimension: "length",
72396
- factor: .0254,
72397
- offset: 0
72398
- },
72399
- ft: {
72400
- dimension: "length",
72401
- factor: .3048,
72402
- offset: 0
72403
- },
72404
- mi: {
72405
- dimension: "length",
72406
- factor: 1609.344,
72407
- offset: 0
72408
- },
72409
- "m²": {
72410
- dimension: "area",
72411
- factor: 1,
72412
- offset: 0
72413
- },
72414
- lx: {
72415
- dimension: "illuminance",
72416
- factor: 1,
72417
- offset: 0
72418
- },
72419
- "W/m²": {
72420
- dimension: "irradiance",
72421
- factor: 1,
72422
- offset: 0
72423
- },
72424
- W: {
72425
- dimension: "power",
72426
- factor: 1,
72427
- offset: 0
72428
- },
72429
- kW: {
72430
- dimension: "power",
72431
- factor: 1e3,
72432
- offset: 0
72433
- },
72434
- VA: {
72435
- dimension: "apparent-power",
72436
- factor: 1,
72437
- offset: 0
72438
- },
72439
- Wh: {
72440
- dimension: "energy",
72441
- factor: 1,
72442
- offset: 0
72443
- },
72444
- kWh: {
72445
- dimension: "energy",
72446
- factor: 1e3,
72447
- offset: 0
72448
- },
72449
- MWh: {
72450
- dimension: "energy",
72451
- factor: 1e6,
72452
- offset: 0
72453
- },
72454
- V: {
72455
- dimension: "voltage",
72456
- factor: 1,
72457
- offset: 0
72458
- },
72459
- mV: {
72460
- dimension: "voltage",
72461
- factor: .001,
72462
- offset: 0
72463
- },
72464
- A: {
72465
- dimension: "current",
72466
- factor: 1,
72467
- offset: 0
72468
- },
72469
- mA: {
72470
- dimension: "current",
72471
- factor: .001,
72472
- offset: 0
72473
- },
72474
- ppm: {
72475
- dimension: "concentration-volume",
72476
- factor: 1,
72477
- offset: 0
72478
- },
72479
- ppb: {
72480
- dimension: "concentration-volume",
72481
- factor: .001,
72482
- offset: 0
72483
- },
72484
- "µg/m³": {
72485
- dimension: "concentration-mass",
72486
- factor: 1,
72487
- offset: 0
72488
- },
72489
- "mg/m³": {
72490
- dimension: "concentration-mass",
72491
- factor: 1e3,
72492
- offset: 0
72493
- },
72494
- kg: {
72495
- dimension: "mass",
72496
- factor: 1,
72497
- offset: 0
72498
- },
72499
- g: {
72500
- dimension: "mass",
72501
- factor: .001,
72502
- offset: 0
72503
- },
72504
- lb: {
72505
- dimension: "mass",
72506
- factor: .453592,
72507
- offset: 0
72508
- },
72509
- oz: {
72510
- dimension: "mass",
72511
- factor: .0283495,
72512
- offset: 0
72513
- },
72514
- "%": {
72515
- dimension: "percentage",
72516
- factor: 1,
72517
- offset: 0
72518
- },
72519
- L: {
72520
- dimension: "volume",
72521
- factor: 1,
72522
- offset: 0
72523
- },
72524
- mL: {
72525
- dimension: "volume",
72526
- factor: .001,
72527
- offset: 0
72528
- },
72529
- gal: {
72530
- dimension: "volume",
72531
- factor: 3.785411784,
72532
- offset: 0
72533
- },
72534
- s: {
72535
- dimension: "duration",
72536
- factor: 1,
72537
- offset: 0
72538
- },
72539
- ms: {
72540
- dimension: "duration",
72541
- factor: .001,
72542
- offset: 0
72543
- },
72544
- min: {
72545
- dimension: "duration",
72546
- factor: 60,
72547
- offset: 0
72548
- },
72549
- h: {
72550
- dimension: "duration",
72551
- factor: 3600,
72552
- offset: 0
72553
- },
72554
- d: {
72555
- dimension: "duration",
72556
- factor: 86400,
72557
- offset: 0
72558
- },
72559
- B: {
72560
- dimension: "information",
72561
- factor: 1,
72562
- offset: 0
72563
- },
72564
- KiB: {
72565
- dimension: "information",
72566
- factor: 1024,
72567
- offset: 0
72568
- },
72569
- MiB: {
72570
- dimension: "information",
72571
- factor: 1024 ** 2,
72572
- offset: 0
72573
- },
72574
- GiB: {
72575
- dimension: "information",
72576
- factor: 1024 ** 3,
72577
- offset: 0
72578
- },
72579
- TiB: {
72580
- dimension: "information",
72581
- factor: 1024 ** 4,
72582
- offset: 0
72583
- },
72584
- kB: {
72585
- dimension: "information",
72586
- factor: 1e3,
72587
- offset: 0
72588
- },
72589
- MB: {
72590
- dimension: "information",
72591
- factor: 1e6,
72592
- offset: 0
72593
- },
72594
- GB: {
72595
- dimension: "information",
72596
- factor: 1e9,
72597
- offset: 0
72598
- },
72599
- "°": {
72600
- dimension: "angle",
72601
- factor: 1,
72602
- offset: 0
72603
- },
72604
- dBFS: {
72605
- dimension: "sound-level",
72606
- factor: 1,
72607
- offset: 0
72608
- },
72609
- dBm: {
72610
- dimension: "signal-level",
72611
- factor: 1,
72612
- offset: 0
72613
- },
72614
- mired: {
72615
- dimension: "colour-temperature",
72616
- factor: 1,
72617
- offset: 0
72618
- },
72619
- kbps: {
72620
- dimension: "bitrate",
72621
- factor: 1,
72622
- offset: 0
72623
- },
72624
- Mbps: {
72625
- dimension: "bitrate",
72626
- factor: 1e3,
72627
- offset: 0
72628
- },
72629
- fps: {
72630
- dimension: "rate",
72631
- factor: 1,
72632
- offset: 0
72633
- }
72634
- };
72635
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
72636
- function specFor(unit) {
72637
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
72638
- }
72639
- /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
72640
- function unitDimension(unit) {
72641
- return specFor(unit)?.dimension;
72642
- }
72643
- /** Value in `spec`'s unit → its dimension's canonical unit. */
72644
- function toCanonical(value, spec) {
72645
- return value * spec.factor + spec.offset;
72646
- }
72647
- /** Canonical-unit value → the unit described by `spec`. */
72648
- function fromCanonical(canonical, spec) {
72649
- return (canonical - spec.offset) / spec.factor;
72650
- }
72651
- /**
72652
- * Graceful variant for the render path: returns the converted value, or
72653
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
72654
- * Never throws — a render caller falls back to the raw value + source unit.
72655
- */
72656
- function tryConvertUnit(value, from, to) {
72657
- if (from === to) return value;
72658
- const fromSpec = specFor(from);
72659
- if (!fromSpec) return void 0;
72660
- const toSpec = specFor(to);
72661
- if (!toSpec) return void 0;
72662
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
72663
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
72664
- }
72665
- /**
72666
- * Every known unit spelling in a dimension (table insertion order — canonical
72667
- * unit first). Powers the UI's same-dimension unit picker.
72668
- */
72669
- /**
72670
- * Convert a difference, not a reading. Affine units (°F) shift an absolute
72671
- * value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
72672
- */
72673
- function tryConvertDelta(value, from, to) {
72674
- if (from === to) return value;
72675
- const origin = tryConvertUnit(0, from, to);
72676
- const shifted = tryConvertUnit(value, from, to);
72677
- if (origin === void 0 || shifted === void 0) return void 0;
72678
- return shifted - origin;
72679
- }
72680
73860
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
72681
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
73861
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
72682
73862
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
72683
73863
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
72684
73864
  new Set(CAP_NAMES);
@@ -73800,6 +74980,17 @@ var BaseDeviceProvider = class extends BaseAddon {
73800
74980
  async discoverDevices(_input) {
73801
74981
  return [];
73802
74982
  }
74983
+ /**
74984
+ * How the scan engine finds this provider. Default `[]`: the provider is
74985
+ * not migrated and keeps answering `discoverDevices` itself.
74986
+ */
74987
+ async getDiscoveryModes() {
74988
+ return [];
74989
+ }
74990
+ /** Turn the scan engine's raw hits into candidates. Default: none. */
74991
+ async classifyDiscoveryHits(_input) {
74992
+ return [];
74993
+ }
73803
74994
  /** Extra per-scan input form (e.g. a broadcast address for another subnet).
73804
74995
  * Null = no extra params. Override in providers that support scoped scans. */
73805
74996
  async getDiscoveryParamsSchema() {
@@ -74280,7 +75471,7 @@ var BaseDeviceProvider = class extends BaseAddon {
74280
75471
  return toDeviceSummary(device, this.addonId);
74281
75472
  }
74282
75473
  };
74283
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
75474
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
74284
75475
  new Set(Object.values(DeviceType));
74285
75476
  Object.freeze({
74286
75477
  "accessories.getStatus": {
@@ -74668,6 +75859,13 @@ Object.freeze({
74668
75859
  access: "create",
74669
75860
  role: "admin"
74670
75861
  },
75862
+ "addonSettings.getDeclaredSettingsKeys": {
75863
+ capName: "addon-settings",
75864
+ capScope: "system",
75865
+ addonId: null,
75866
+ access: "view",
75867
+ role: "admin"
75868
+ },
74671
75869
  "addonSettings.getDeviceSettings": {
74672
75870
  capName: "addon-settings",
74673
75871
  capScope: "system",
@@ -74689,6 +75887,13 @@ Object.freeze({
74689
75887
  access: "view",
74690
75888
  role: "admin"
74691
75889
  },
75890
+ "addonSettings.unsetStoredSettingsKeys": {
75891
+ capName: "addon-settings",
75892
+ capScope: "system",
75893
+ addonId: null,
75894
+ access: "create",
75895
+ role: "admin"
75896
+ },
74692
75897
  "addonSettings.updateDeviceSettings": {
74693
75898
  capName: "addon-settings",
74694
75899
  capScope: "system",
@@ -77104,6 +78309,13 @@ Object.freeze({
77104
78309
  access: "create",
77105
78310
  role: "admin"
77106
78311
  },
78312
+ "deviceProvider.classifyDiscoveryHits": {
78313
+ capName: "device-provider",
78314
+ capScope: "system",
78315
+ addonId: null,
78316
+ access: "create",
78317
+ role: "admin"
78318
+ },
77107
78319
  "deviceProvider.createDevice": {
77108
78320
  capName: "device-provider",
77109
78321
  capScope: "system",
@@ -77132,6 +78344,13 @@ Object.freeze({
77132
78344
  access: "view",
77133
78345
  role: "admin"
77134
78346
  },
78347
+ "deviceProvider.getDiscoveryModes": {
78348
+ capName: "device-provider",
78349
+ capScope: "system",
78350
+ addonId: null,
78351
+ access: "view",
78352
+ role: "admin"
78353
+ },
77135
78354
  "deviceProvider.getDiscoveryParamsSchema": {
77136
78355
  capName: "device-provider",
77137
78356
  capScope: "system",
@@ -77909,6 +79128,13 @@ Object.freeze({
77909
79128
  access: "create",
77910
79129
  role: "admin"
77911
79130
  },
79131
+ "keychain.releaseRecordRefs": {
79132
+ capName: "keychain",
79133
+ capScope: "system",
79134
+ addonId: null,
79135
+ access: "create",
79136
+ role: "admin"
79137
+ },
77912
79138
  "keychain.resolve": {
77913
79139
  capName: "keychain",
77914
79140
  capScope: "system",
@@ -78329,6 +79555,13 @@ Object.freeze({
78329
79555
  access: "view",
78330
79556
  role: "admin"
78331
79557
  },
79558
+ "localNetwork.getScanSubnets": {
79559
+ capName: "local-network",
79560
+ capScope: "system",
79561
+ addonId: null,
79562
+ access: "view",
79563
+ role: "admin"
79564
+ },
78332
79565
  "localNetwork.getTlsStatus": {
78333
79566
  capName: "local-network",
78334
79567
  capScope: "system",
@@ -78378,6 +79611,13 @@ Object.freeze({
78378
79611
  access: "create",
78379
79612
  role: "admin"
78380
79613
  },
79614
+ "localNetwork.scanNetwork": {
79615
+ capName: "local-network",
79616
+ capScope: "system",
79617
+ addonId: null,
79618
+ access: "create",
79619
+ role: "admin"
79620
+ },
78381
79621
  "localNetwork.setAddressRole": {
78382
79622
  capName: "local-network",
78383
79623
  capScope: "system",
@@ -78399,6 +79639,13 @@ Object.freeze({
78399
79639
  access: "create",
78400
79640
  role: "admin"
78401
79641
  },
79642
+ "localNetwork.setScanSubnets": {
79643
+ capName: "local-network",
79644
+ capScope: "system",
79645
+ addonId: null,
79646
+ access: "create",
79647
+ role: "admin"
79648
+ },
78402
79649
  "localNetwork.setViewerEndpoints": {
78403
79650
  capName: "local-network",
78404
79651
  capScope: "system",
@@ -82081,6 +83328,13 @@ Object.freeze({
82081
83328
  access: "create",
82082
83329
  role: "admin"
82083
83330
  },
83331
+ "remoteLayoutComponents.listComponents": {
83332
+ capName: "remote-layout-components",
83333
+ capScope: "system",
83334
+ addonId: null,
83335
+ access: "view",
83336
+ role: "admin"
83337
+ },
82084
83338
  "runningState.getStatus": {
82085
83339
  capName: "running-state",
82086
83340
  capScope: "device",
@@ -82207,6 +83461,13 @@ Object.freeze({
82207
83461
  access: "view",
82208
83462
  role: "admin"
82209
83463
  },
83464
+ "serverManagement.installDesktopUpdate": {
83465
+ capName: "server-management",
83466
+ capScope: "system",
83467
+ addonId: null,
83468
+ access: "create",
83469
+ role: "admin"
83470
+ },
82210
83471
  "serverManagement.restartServer": {
82211
83472
  capName: "server-management",
82212
83473
  capScope: "system",
@@ -82606,6 +83867,13 @@ Object.freeze({
82606
83867
  access: "create",
82607
83868
  role: "admin"
82608
83869
  },
83870
+ "storageAdmin.dropOrphanedKind": {
83871
+ capName: "storage-admin",
83872
+ capScope: "system",
83873
+ addonId: null,
83874
+ access: "delete",
83875
+ role: "admin"
83876
+ },
82609
83877
  "storageAdmin.listKinds": {
82610
83878
  capName: "storage-admin",
82611
83879
  capScope: "system",
@@ -82627,6 +83895,27 @@ Object.freeze({
82627
83895
  access: "view",
82628
83896
  role: "admin"
82629
83897
  },
83898
+ "storageAdmin.scanStaleData": {
83899
+ capName: "storage-admin",
83900
+ capScope: "system",
83901
+ addonId: null,
83902
+ access: "create",
83903
+ role: "admin"
83904
+ },
83905
+ "storageAdmin.staleDataReport": {
83906
+ capName: "storage-admin",
83907
+ capScope: "system",
83908
+ addonId: null,
83909
+ access: "view",
83910
+ role: "admin"
83911
+ },
83912
+ "storageAdmin.unsetStaleSettingsKey": {
83913
+ capName: "storage-admin",
83914
+ capScope: "system",
83915
+ addonId: null,
83916
+ access: "create",
83917
+ role: "admin"
83918
+ },
82630
83919
  "storageEvictable.evict": {
82631
83920
  capName: "storage-evictable",
82632
83921
  capScope: "system",
@@ -86117,7 +87406,8 @@ var APPLIANCE_CAP_VOCABULARY = {
86117
87406
  min: 4,
86118
87407
  max: 200,
86119
87408
  step: 1,
86120
- unit: "g"
87409
+ unit: "g",
87410
+ quantity: "mass"
86121
87411
  },
86122
87412
  optional: true
86123
87413
  }]
@@ -86136,7 +87426,8 @@ var APPLIANCE_CAP_VOCABULARY = {
86136
87426
  min: 4,
86137
87427
  max: 200,
86138
87428
  step: 1,
86139
- unit: "g"
87429
+ unit: "g",
87430
+ quantity: "mass"
86140
87431
  },
86141
87432
  optional: true
86142
87433
  }, {
@@ -86147,7 +87438,8 @@ var APPLIANCE_CAP_VOCABULARY = {
86147
87438
  min: 4,
86148
87439
  max: 200,
86149
87440
  step: 1,
86150
- unit: "g"
87441
+ unit: "g",
87442
+ quantity: "mass"
86151
87443
  },
86152
87444
  optional: true
86153
87445
  }]
@@ -88247,6 +89539,7 @@ var SYSTEM_CAP_VOCABULARY = {
88247
89539
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
88248
89540
  records: system(INTERNAL),
88249
89541
  remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
89542
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
88250
89543
  "server-management": system(CLUSTER),
88251
89544
  "settings-store": system(INTERNAL),
88252
89545
  "smtp-provider": system(INTERNAL),
@@ -88319,7 +89612,7 @@ function projectAction(cap, action) {
88319
89612
  };
88320
89613
  }
88321
89614
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
88322
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
89615
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
88323
89616
  DeviceFeature.BatteryOperated;
88324
89617
  /**
88325
89618
  * How a washer, dryer or dishwasher leaves CamStack.
@@ -92918,6 +94211,67 @@ object({
92918
94211
  commands: record(string(), unknown())
92919
94212
  });
92920
94213
  //#endregion
94214
+ //#region src/ha-broker-source.ts
94215
+ /**
94216
+ * The export half's ONLY read of the `broker` registry — and the record of why
94217
+ * it is NOT pinned, which is the opposite of what this file said for one deploy.
94218
+ *
94219
+ * ── What a pin would buy, and why it cannot be paid for here ───────────────
94220
+ * `broker` is a `mode:'collection'` capability: several addons register a
94221
+ * provider, each owning a DISJOINT set of brokers (`provider-homematic` owns
94222
+ * `hm_*`, `provider-homeassistant` owns `ha_*`). For an ID-KEYED method that is
94223
+ * decisive, and the export half's `getBrokerConfig` call IS pinned for exactly
94224
+ * that reason — measured on the live hub:
94225
+ *
94226
+ * broker.getBrokerConfig { id: 'ha_001' } → null
94227
+ * broker.getBrokerConfig { id: 'ha_001', addonId: 'provider-…' } → the config
94228
+ *
94229
+ * `list` is different, and the difference is not cosmetic. Pinning it makes the
94230
+ * read depend on `provider-homeassistant` being REGISTERED at the moment of the
94231
+ * call — and that provider registers its capabilities only at the very END of
94232
+ * its `initialize()`, after restoring every HA device. Pinned, the export half
94233
+ * hard-fails through that whole window with
94234
+ *
94235
+ * BAD_REQUEST: Capability "broker" has no provider with addonId
94236
+ * "provider-homeassistant"
94237
+ *
94238
+ * which is strictly worse than the union: unpinned, `list` is fanned across
94239
+ * every registered provider, so it returns `ha_001` as soon as the provider is
94240
+ * up and simply omits it while it is not. Shipped pinned on 2026-08-09 and it
94241
+ * took the operator from "one instance, switched off" to "no instance at all".
94242
+ *
94243
+ * ── The rule this leaves behind ────────────────────────────────────────────
94244
+ * Pin a call that NAMES a resource (`{ id }`, `{ brokerId }`) — only one
94245
+ * provider can answer it and a wrong answer is `null`, indistinguishable from
94246
+ * "no such resource". Do NOT pin an enumeration: its contract is already the
94247
+ * union, and a pin converts a provider's absence from "returns fewer rows" into
94248
+ * "throws". `scripts/check-collection-cap-pin.ts` enforces exactly that split.
94249
+ *
94250
+ * ── The other half of the lesson: never pin a cap you provide yourself ─────
94251
+ * The provider half's own `broker.*` calls (subscribe / getState / publish)
94252
+ * must stay unpinned too, and for a sharper reason: they run DURING
94253
+ * `registry.restore()`, i.e. before `initialize()` returns the provider that
94254
+ * would satisfy the pin. Pinning them deadlocks the addon's own boot — the cap
94255
+ * cannot register until restore finishes, and restore cannot finish until the
94256
+ * cap resolves. In-process they need no pin anyway: `localProviderLink`
94257
+ * short-circuits any cap the calling process already provides.
94258
+ */
94259
+ /** The addon that owns the import direction, and every `ha_*` broker. */
94260
+ var HA_PROVIDER_ADDON_ID = "provider-homeassistant";
94261
+ /** The `BrokerInfo.kind` tag the Home Assistant provider stamps. */
94262
+ var HA_BROKER_KIND = "home-assistant";
94263
+ /**
94264
+ * Every Home Assistant broker currently registered, cluster-wide.
94265
+ *
94266
+ * Unpinned by design (see the module docblock) and filtered by `kind`, which is
94267
+ * the contract check that matters: whatever mix of providers answered the
94268
+ * union, only `home-assistant` brokers are ever exported to over Home
94269
+ * Assistant's transport.
94270
+ */
94271
+ async function listHomeAssistantBrokers(query) {
94272
+ return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
94273
+ }
94274
+ //#endregion
92921
94275
  Object.defineProperty(exports, "AlarmArmModeSchema", {
92922
94276
  enumerable: true,
92923
94277
  get: function() {
@@ -93014,6 +94368,18 @@ Object.defineProperty(exports, "FanDirectionSchema", {
93014
94368
  return FanDirectionSchema;
93015
94369
  }
93016
94370
  });
94371
+ Object.defineProperty(exports, "HA_BROKER_KIND", {
94372
+ enumerable: true,
94373
+ get: function() {
94374
+ return HA_BROKER_KIND;
94375
+ }
94376
+ });
94377
+ Object.defineProperty(exports, "HA_PROVIDER_ADDON_ID", {
94378
+ enumerable: true,
94379
+ get: function() {
94380
+ return HA_PROVIDER_ADDON_ID;
94381
+ }
94382
+ });
93017
94383
  Object.defineProperty(exports, "HvacModeSchema", {
93018
94384
  enumerable: true,
93019
94385
  get: function() {
@@ -93338,6 +94704,12 @@ Object.defineProperty(exports, "lawnMowerControlCapability", {
93338
94704
  return lawnMowerControlCapability;
93339
94705
  }
93340
94706
  });
94707
+ Object.defineProperty(exports, "listHomeAssistantBrokers", {
94708
+ enumerable: true,
94709
+ get: function() {
94710
+ return listHomeAssistantBrokers;
94711
+ }
94712
+ });
93341
94713
  Object.defineProperty(exports, "literal", {
93342
94714
  enumerable: true,
93343
94715
  get: function() {