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