@camstack/addon-provider-enpal 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -806,7 +806,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
806
806
  return EventCategory;
807
807
  }({});
808
808
  //#endregion
809
- //#region ../types/dist/composed-stable-id-jnvhwGoH.mjs
809
+ //#region ../types/dist/composed-stable-id-CPp3votu.mjs
810
810
  /**
811
811
  import { errMsg } from '@camstack/types'
812
812
  * Extract a human-readable message from an unknown error value.
@@ -819,17 +819,18 @@ function errMsg(err) {
819
819
  }
820
820
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
821
821
  DeviceType["Camera"] = "camera";
822
+ /** Legacy stored value, read only by the hub-to-integration migrations; no live device carries it. */
822
823
  DeviceType["Hub"] = "hub";
823
824
  DeviceType["Light"] = "light";
824
825
  DeviceType["Siren"] = "siren";
825
826
  DeviceType["Switch"] = "switch";
826
827
  DeviceType["Sensor"] = "sensor";
827
- DeviceType["Thermostat"] = "thermostat";
828
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
829
- * `climate-control` cap surface with `Thermostat` but renders a
830
- * dedicated AC-appropriate control UI (mode chips, fan speed,
831
- * independent vertical/horizontal swing). Sources: native Gree, and
832
- * reusable by other AC integrations. */
828
+ /** Every climate device: an air conditioner, a room thermostat, a radiator
829
+ * valve, a floor-heating zone, a heat-pump circuit. They share one set of
830
+ * controls (`climate-control`), so they are one type; WHICH kind of climate
831
+ * device it is is its {@link DeviceRole} (`climate-*`), which the UI and the
832
+ * exports branch on. The retired `thermostat` type is read as this one
833
+ * (`canonicalStoredDeviceType`). */
833
834
  DeviceType["Climate"] = "climate";
834
835
  DeviceType["Button"] = "button";
835
836
  /** Generic stateless event emitter — carries a device's EXACT declared
@@ -920,10 +921,6 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
920
921
  /** Washing machine. A plug-built one is running while its power stays
921
922
  * above a threshold. No program cap — that surface is the dishwasher's. */
922
923
  DeviceType["Washer"] = "washer";
923
- /** A dumb plug-in stove or heater: a switch (and, with a meter, a power
924
- * reading) that only runs while the plug is on. Built from a smart plug;
925
- * the `timed-run` cap switches it off again after a chosen time. */
926
- DeviceType["Heater"] = "heater";
927
924
  /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
928
925
  DeviceType["Dryer"] = "dryer";
929
926
  /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
@@ -1143,6 +1140,27 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1143
1140
  /** A smart plug: a `switch` whose source says it is an outlet (HA
1144
1141
  * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1145
1142
  DeviceRole["Outlet"] = "outlet";
1143
+ /** A dumb plug-in stove or heater behind a switch: it runs only while the
1144
+ * switch is on. Offered `timed-run`, which switches it off again after a
1145
+ * chosen time. */
1146
+ DeviceRole["SwitchHeater"] = "switch-heater";
1147
+ /** A plain fan behind a switch (a smart plug powering it): drawn as a fan,
1148
+ * with the switch's on/off only. Speed or oscillation appear only when the
1149
+ * device also has a fan cap (D1033). */
1150
+ DeviceRole["SwitchFan"] = "switch-fan";
1151
+ /** Air conditioner / split unit: cooling, fan speed, louvres. */
1152
+ DeviceRole["ClimateAc"] = "climate-ac";
1153
+ /** Room thermostat: a setpoint and a room temperature. The default. */
1154
+ DeviceRole["ClimateThermostat"] = "climate-thermostat";
1155
+ /** Floor-heating zone actuator. */
1156
+ DeviceRole["ClimateFloor"] = "climate-floor";
1157
+ /** Radiator valve (thermostatic radiator head). */
1158
+ DeviceRole["ClimateRadiator"] = "climate-radiator";
1159
+ /** A heater with its own thermostat (stove, convector). */
1160
+ DeviceRole["ClimateHeater"] = "climate-heater";
1161
+ /** One heating/cooling circuit of a heat pump: a water loop with comfort /
1162
+ * eco setpoints and a heat/cool mode, no room sensor. */
1163
+ DeviceRole["ClimateHeatPumpCircuit"] = "climate-heat-pump-circuit";
1146
1164
  /** Generic motion / occupancy / moving event source. Distinct from
1147
1165
  * the camera accessory PirSensor role: that one is a camera child;
1148
1166
  * this is a standalone HA / 3rd-party motion sensor. */
@@ -6030,7 +6048,7 @@ var ZodIssueCode = {
6030
6048
  var ZodFirstPartyTypeKind;
6031
6049
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6032
6050
  //#endregion
6033
- //#region ../types/dist/sleep-Cw1uUXvl.mjs
6051
+ //#region ../types/dist/sleep-D0roI_CZ.mjs
6034
6052
  /**
6035
6053
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6036
6054
  * window to float samples (D455).
@@ -11807,6 +11825,50 @@ var ACTUATOR_CAP_VOCABULARY = {
11807
11825
  value: false
11808
11826
  },
11809
11827
  args: { oscillating: false }
11828
+ },
11829
+ {
11830
+ id: "fan.horizontalOscillateOn",
11831
+ method: "setOscillationAxis",
11832
+ label: t("Horizontal oscillation on", "Oscillazione orizzontale attiva"),
11833
+ phrase: t("Start the horizontal oscillation of {device}", "Attiva l’oscillazione orizzontale di {device}"),
11834
+ icon: "move-horizontal",
11835
+ args: {
11836
+ axis: "horizontal",
11837
+ oscillating: true
11838
+ }
11839
+ },
11840
+ {
11841
+ id: "fan.horizontalOscillateOff",
11842
+ method: "setOscillationAxis",
11843
+ label: t("Horizontal oscillation off", "Oscillazione orizzontale spenta"),
11844
+ phrase: t("Stop the horizontal oscillation of {device}", "Ferma l’oscillazione orizzontale di {device}"),
11845
+ icon: "move-horizontal",
11846
+ args: {
11847
+ axis: "horizontal",
11848
+ oscillating: false
11849
+ }
11850
+ },
11851
+ {
11852
+ id: "fan.verticalOscillateOn",
11853
+ method: "setOscillationAxis",
11854
+ label: t("Vertical oscillation on", "Oscillazione verticale attiva"),
11855
+ phrase: t("Start the vertical oscillation of {device}", "Attiva l’oscillazione verticale di {device}"),
11856
+ icon: "move-vertical",
11857
+ args: {
11858
+ axis: "vertical",
11859
+ oscillating: true
11860
+ }
11861
+ },
11862
+ {
11863
+ id: "fan.verticalOscillateOff",
11864
+ method: "setOscillationAxis",
11865
+ label: t("Vertical oscillation off", "Oscillazione verticale spenta"),
11866
+ phrase: t("Stop the vertical oscillation of {device}", "Ferma l’oscillazione verticale di {device}"),
11867
+ icon: "move-vertical",
11868
+ args: {
11869
+ axis: "vertical",
11870
+ oscillating: false
11871
+ }
11810
11872
  }
11811
11873
  ],
11812
11874
  state: [{
@@ -13896,10 +13958,6 @@ var CAMERA_CAP_VOCABULARY = {
13896
13958
  scope: "device",
13897
13959
  noun: t("Admin link", "Link di amministrazione")
13898
13960
  },
13899
- "device-discovery": {
13900
- scope: "device",
13901
- noun: t("Discovery", "Rilevamento dispositivi")
13902
- },
13903
13961
  "device-extension": {
13904
13962
  scope: "device",
13905
13963
  noun: t("Device extension", "Estensione del dispositivo")
@@ -15357,7 +15415,6 @@ var CAP_NAMES = Object.freeze([
15357
15415
  "device-admin-link",
15358
15416
  "device-adoption",
15359
15417
  "device-credentials",
15360
- "device-discovery",
15361
15418
  "device-export",
15362
15419
  "device-extension",
15363
15420
  "device-history",
@@ -15979,13 +16036,6 @@ Object.freeze({
15979
16036
  "valueType": "string"
15980
16037
  }
15981
16038
  ],
15982
- "device-discovery": [{
15983
- "path": "lastDiscoveryAt",
15984
- "valueType": "number"
15985
- }, {
15986
- "path": "lastError",
15987
- "valueType": "string"
15988
- }],
15989
16039
  "device-export": [
15990
16040
  {
15991
16041
  "path": "linkState",
@@ -16174,6 +16224,10 @@ Object.freeze({
16174
16224
  "path": "lastEvent.eventType",
16175
16225
  "valueType": "string"
16176
16226
  },
16227
+ {
16228
+ "path": "lastEvent.sourceId",
16229
+ "valueType": "string"
16230
+ },
16177
16231
  {
16178
16232
  "path": "lastEvent.timestamp",
16179
16233
  "valueType": "number"
@@ -17437,7 +17491,7 @@ Object.freeze({
17437
17491
  }
17438
17492
  });
17439
17493
  //#endregion
17440
- //#region ../types/dist/implemented-methods-OitwyO1Y.mjs
17494
+ //#region ../types/dist/implemented-methods-5YMDeE3f.mjs
17441
17495
  /**
17442
17496
  * The ffmpeg build every CamStack node downloads, per platform and arch.
17443
17497
  *
@@ -44098,6 +44152,41 @@ function macroOnly(macro) {
44098
44152
  };
44099
44153
  }
44100
44154
  }
44155
+ var EventVocabularyKindSchema = _enum([
44156
+ "press",
44157
+ "long-press",
44158
+ "long-press-start",
44159
+ "release",
44160
+ "repeat",
44161
+ "double-press",
44162
+ "other"
44163
+ ]);
44164
+ /** The part of the device an event comes from: a button, a channel. */
44165
+ var EventVocabularySourceSchema = object({
44166
+ id: string().min(1),
44167
+ label: string().min(1)
44168
+ });
44169
+ /** One scalar field of an event's `data`, by dotted path. */
44170
+ var EventVocabularyFieldSchema = object({
44171
+ path: string().min(1),
44172
+ kind: _enum([
44173
+ "string",
44174
+ "number",
44175
+ "boolean",
44176
+ "enum"
44177
+ ]),
44178
+ enumValues: array(string()).optional(),
44179
+ label: string().min(1).optional()
44180
+ });
44181
+ var EventVocabularyEntrySchema = object({
44182
+ /** The native token, verbatim. */
44183
+ id: string().min(1),
44184
+ source: EventVocabularySourceSchema.optional(),
44185
+ label: string().min(1),
44186
+ description: string().optional(),
44187
+ kind: EventVocabularyKindSchema.optional(),
44188
+ payload: array(EventVocabularyFieldSchema).optional()
44189
+ });
44101
44190
  Math.PI / 180;
44102
44191
  /**
44103
44192
  * WHEN something happens, as data (D983) — the one time vocabulary of the
@@ -44150,6 +44239,62 @@ var WeekdayKeySchema = _enum([
44150
44239
  "sat",
44151
44240
  "sun"
44152
44241
  ]);
44242
+ /**
44243
+ * Hold semantics — "for N seconds" on a leaf of the `when` tree (#498).
44244
+ *
44245
+ * ONE primitive, `for: { ms, mode, tolerance? }`, shared by `device-becomes`,
44246
+ * `device-is` and `expression`:
44247
+ *
44248
+ * - `held`: the condition stays true for `ms`;
44249
+ * - `stable`: the value does not move by more than `tolerance` (absolute, in
44250
+ * the field's base unit) for `ms` while the condition holds.
44251
+ *
44252
+ * {@link HoldCore} is the pure state machine (an input observation → a reading).
44253
+ * {@link HoldTracker} keys cores by a leaf path for the STATE leaves the tree
44254
+ * evaluator judges. {@link BecomesWatch} wraps a core with a timer for the
44255
+ * `device-becomes` EVENT leaf: the deadline is armed when the hold starts and
44256
+ * cancelled when it breaks, and the leaf fires ONCE per episode.
44257
+ *
44258
+ * Hold state is memory only. A hold that is already running when the watcher
44259
+ * first looks (a restart) is INHERITED: nobody knows when it began, so it never
44260
+ * fires for that episode and a state leaf reads it as unknown until the window
44261
+ * has elapsed from the moment the watcher started (D49: not-known is not false).
44262
+ */
44263
+ var HOLD_MAX_MS = 864e5;
44264
+ var HoldModeSchema = _enum(["held", "stable"]);
44265
+ var HoldSpecSchema = object({
44266
+ /** How long the condition (or the value) must hold. */
44267
+ ms: number$1().int().min(1).max(HOLD_MAX_MS),
44268
+ mode: HoldModeSchema,
44269
+ /** `stable` only: the largest move of the value that still counts as unchanged (absolute, base unit). */
44270
+ tolerance: number$1().min(0).optional()
44271
+ });
44272
+ /** What a trace says about a hold. */
44273
+ var HoldTraceSchema = object({
44274
+ phase: _enum([
44275
+ "pending",
44276
+ "cancelled",
44277
+ "fired",
44278
+ "reseeded"
44279
+ ]),
44280
+ ms: number$1().int(),
44281
+ mode: HoldModeSchema,
44282
+ /** When the hold started (ms epoch). */
44283
+ since: number$1().optional(),
44284
+ /** When it completes (ms epoch), for a pending hold. */
44285
+ deadline: number$1().optional()
44286
+ });
44287
+ /** The operators a `device-becomes` leaf may carry: a threshold or a range. */
44288
+ var BecomesOperatorSchema = _enum([
44289
+ "eq",
44290
+ "ne",
44291
+ "gt",
44292
+ "gte",
44293
+ "lt",
44294
+ "lte",
44295
+ "between",
44296
+ "outside"
44297
+ ]);
44153
44298
  /** How a state leaf compares a device field to a value. Derived from the
44154
44299
  * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
44155
44300
  var AutomationConditionOperatorSchema = _enum([
@@ -44163,6 +44308,13 @@ var AutomationConditionOperatorSchema = _enum([
44163
44308
  "in"
44164
44309
  ]);
44165
44310
  /**
44311
+ * The stable name of an event leaf. A path (`when.0.1`) moves when a row is
44312
+ * reordered; the id does not, so a `triggered-by` leaf keeps pointing at the
44313
+ * same row. Optional: a leaf nothing refers to needs none.
44314
+ */
44315
+ var EVENT_LEAF_ID_RE = /^[A-Za-z0-9_-]{1,40}$/;
44316
+ var eventLeafId = { id: string().regex(EVENT_LEAF_ID_RE).optional() };
44317
+ /**
44166
44318
  * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
44167
44319
  * subscription: it evaluates against the **state mirror**, which is reconciled,
44168
44320
  * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
@@ -44170,24 +44322,60 @@ var AutomationConditionOperatorSchema = _enum([
44170
44322
  */
44171
44323
  var DeviceBecomesLeafSchema = object({
44172
44324
  kind: literal("device-becomes"),
44325
+ ...eventLeafId,
44173
44326
  deviceId: number$1().int().nonnegative(),
44174
44327
  cap: string().min(1),
44175
44328
  fieldPath: string().min(1),
44176
- /** Fire when the field takes this value. Omit to fire on any change. */
44329
+ /** Fire when the field takes this value. Omit (with no `operator`) to fire on any change. */
44177
44330
  becomes: union([
44178
44331
  string(),
44179
44332
  number$1(),
44180
44333
  boolean()
44181
44334
  ]).optional(),
44182
- /** Only on a CHANGE of value, not on every re-report. */
44335
+ /**
44336
+ * A threshold or a range instead of an equality: "crosses above 3000", "enters
44337
+ * 18..24". The leaf fires on the TRANSITION into the predicate, never on a re-report.
44338
+ * `value` is in the field's base unit; `between` / `outside` take `[low, high]`.
44339
+ */
44340
+ operator: BecomesOperatorSchema.optional(),
44341
+ value: union([
44342
+ number$1(),
44343
+ string(),
44344
+ boolean(),
44345
+ array(number$1()).length(2)
44346
+ ]).optional(),
44347
+ /**
44348
+ * Hold: the transition (or, with no predicate, the new value) must stay for
44349
+ * `for.ms` before the leaf fires, ONCE per episode. See `hold.ts`.
44350
+ */
44351
+ for: HoldSpecSchema.optional(),
44352
+ /** @deprecated every leaf fires on a transition now; read as always on. */
44183
44353
  edge: boolean().optional(),
44184
- /** The new value must hold this long before it counts. */
44185
- forMs: number$1().int().min(0).max(864e5).optional(),
44354
+ /** @deprecated legacy `held` window — read through `holdOf` as `for: { ms, mode: 'held' }`. */
44355
+ forMs: number$1().int().min(0).max(HOLD_MAX_MS).optional(),
44186
44356
  /** Collapse a burst into one occurrence. */
44187
44357
  debounceMs: number$1().int().min(0).max(6e5).optional()
44358
+ }).superRefine((leaf, ctx) => {
44359
+ if (leaf.becomes !== void 0 && leaf.operator !== void 0) ctx.addIssue({
44360
+ code: "custom",
44361
+ message: "a leaf names `becomes` or `operator`, not both",
44362
+ path: ["operator"]
44363
+ });
44364
+ if (leaf.operator !== void 0 && leaf.value === void 0) ctx.addIssue({
44365
+ code: "custom",
44366
+ message: "an operator needs the value to compare to",
44367
+ path: ["value"]
44368
+ });
44369
+ const range = leaf.operator === "between" || leaf.operator === "outside";
44370
+ if (leaf.operator !== void 0 && range !== Array.isArray(leaf.value)) ctx.addIssue({
44371
+ code: "custom",
44372
+ message: range ? "a range takes [low, high]" : "only a range takes [low, high]",
44373
+ path: ["value"]
44374
+ });
44188
44375
  });
44189
44376
  var DeviceEventLeafSchema = object({
44190
44377
  kind: literal("device-event"),
44378
+ ...eventLeafId,
44191
44379
  /** An `EventCategory` value. */
44192
44380
  category: string().min(1),
44193
44381
  deviceId: number$1().int().nonnegative().optional()
@@ -44200,18 +44388,51 @@ var DeviceEventLeafSchema = object({
44200
44388
  */
44201
44389
  var ScheduleLeafSchema = object({
44202
44390
  kind: literal("schedule"),
44391
+ ...eventLeafId,
44203
44392
  at: TimeSpecSchema.optional(),
44204
44393
  days: array(WeekdayKeySchema).min(1).max(7).optional(),
44205
44394
  /** @deprecated legacy cron — new schedules use `at`. */
44206
44395
  cron: string().min(1).max(120).optional()
44207
44396
  }).refine((leaf) => leaf.at === void 0 !== (leaf.cron === void 0), { message: "a schedule names exactly one of `at` (a time spec) and `cron` (legacy)" }).refine((leaf) => leaf.days === void 0 || leaf.at !== void 0, { message: "`days` goes with `at`; a cron expression carries its own days" });
44208
- var ManualLeafSchema = object({ kind: literal("manual") });
44397
+ /**
44398
+ * One payload predicate of a `device-fired` row: the value at `path` inside
44399
+ * the fire's `data` (dotted) equals `equals`. Compared strictly, no coercion.
44400
+ */
44401
+ var DeviceFiredMatchSchema = object({
44402
+ path: string().min(1).max(120),
44403
+ equals: union([
44404
+ string(),
44405
+ number$1(),
44406
+ boolean()
44407
+ ])
44408
+ });
44409
+ /**
44410
+ * "fires": one device of the `event-emitter` capability reported an event
44411
+ * (D1045). Every list is an OR, the lists together an AND; an absent list does
44412
+ * not narrow. `eventIds` are the device's own tokens, `sourceIds` the parts of
44413
+ * the device (a button), `kinds` the shared press kinds of its vocabulary.
44414
+ */
44415
+ var DeviceFiredLeafSchema = object({
44416
+ kind: literal("device-fired"),
44417
+ ...eventLeafId,
44418
+ deviceId: number$1().int().nonnegative(),
44419
+ eventIds: array(string().min(1)).min(1).max(64).optional(),
44420
+ sourceIds: array(string().min(1)).min(1).max(64).optional(),
44421
+ kinds: array(EventVocabularyKindSchema).min(1).optional(),
44422
+ match: array(DeviceFiredMatchSchema).min(1).max(8).optional()
44423
+ });
44424
+ var ManualLeafSchema = object({
44425
+ kind: literal("manual"),
44426
+ ...eventLeafId
44427
+ });
44209
44428
  var IntervalLeafSchema = object({
44210
44429
  kind: literal("interval"),
44430
+ ...eventLeafId,
44211
44431
  minutes: number$1().int().min(1).max(1440)
44212
44432
  });
44213
44433
  var DetectionLeafSchema = object({
44214
44434
  kind: literal("detection"),
44435
+ ...eventLeafId,
44215
44436
  /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
44216
44437
  subjects: array(DetectionSubjectSchema).min(1),
44217
44438
  deviceIds: array(number$1().int().nonnegative()).min(1),
@@ -44222,6 +44443,7 @@ var DetectionLeafSchema = object({
44222
44443
  });
44223
44444
  var StationaryLeafSchema = object({
44224
44445
  kind: literal("stationary"),
44446
+ ...eventLeafId,
44225
44447
  deviceIds: array(number$1().int().nonnegative()).min(1),
44226
44448
  subjects: array(StationarySubjectSchema).min(1).optional(),
44227
44449
  zones: array(string().min(1)).min(1).optional(),
@@ -44249,7 +44471,9 @@ var DeviceIsLeafSchema = object({
44249
44471
  QuantityValueSchema
44250
44472
  ]),
44251
44473
  /** The unit the person picked for `value` (display only; `value` is in the base unit). */
44252
- unit: string().min(1).optional()
44474
+ unit: string().min(1).optional(),
44475
+ /** The comparison must hold (`held`) or the value stay put (`stable`) this long. */
44476
+ for: HoldSpecSchema.optional()
44253
44477
  });
44254
44478
  /**
44255
44479
  * True when the camera currently has a PARKED object matching `subjects` in at
@@ -44265,6 +44489,41 @@ var StationaryPresentLeafSchema = object({
44265
44489
  minCount: number$1().int().positive().optional()
44266
44490
  });
44267
44491
  /**
44492
+ * True while the site-local clock is inside a daily window and/or on one of
44493
+ * the named days. `days` names the local day a window STARTS on, so
44494
+ * 22:00-06:00 on weekdays is still inside at 01:00 Saturday. A one-date spec
44495
+ * (`kind: 'date'`) is refused: only a clock time or a sun event makes a window
44496
+ * that repeats every day.
44497
+ */
44498
+ var TimeIsLeafSchema = object({
44499
+ kind: literal("time-is"),
44500
+ between: object({
44501
+ from: TimeSpecSchema,
44502
+ to: TimeSpecSchema
44503
+ }).optional(),
44504
+ days: array(WeekdayKeySchema).min(1).max(7).optional()
44505
+ }).superRefine((leaf, ctx) => {
44506
+ if (leaf.between === void 0 && leaf.days === void 0) ctx.addIssue({
44507
+ code: "custom",
44508
+ message: "a time-is leaf needs a between window, days, or both"
44509
+ });
44510
+ for (const end of ["from", "to"]) if (leaf.between?.[end].kind === "date") ctx.addIssue({
44511
+ code: "custom",
44512
+ message: "a one-date time cannot bound a daily window: use a clock time or a sun event",
44513
+ path: ["between", end]
44514
+ });
44515
+ });
44516
+ /**
44517
+ * True for the run that one of the named event leaves started. It reads the
44518
+ * run, not a device: in the tree it is judged against the leaf that fired, in
44519
+ * a step guard against the trigger the run carries. A run no event leaf
44520
+ * started (a state-only tree becoming true) is false.
44521
+ */
44522
+ var TriggeredByLeafSchema = object({
44523
+ kind: literal("triggered-by"),
44524
+ leafIds: array(string().regex(EVENT_LEAF_ID_RE)).min(1).max(20)
44525
+ });
44526
+ /**
44268
44527
  * The escape hatch: the bounded expression engine over named device bindings.
44269
44528
  * Declared as a plain object so a discriminated union can read `kind`; the
44270
44529
  * author-time validation is the SAME function `ExpressionSourceSchema` runs.
@@ -44272,7 +44531,9 @@ var StationaryPresentLeafSchema = object({
44272
44531
  var ExpressionLeafSchema = object({
44273
44532
  kind: literal("expression"),
44274
44533
  expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
44275
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
44534
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema),
44535
+ /** The expression must stay true this long. An expression has no single value, so only `held`. */
44536
+ for: HoldSpecSchema.optional()
44276
44537
  }).superRefine((src, ctx) => {
44277
44538
  const err = validateExpressionSource(src);
44278
44539
  if (err !== null) ctx.addIssue({
@@ -44280,6 +44541,11 @@ var ExpressionLeafSchema = object({
44280
44541
  message: err,
44281
44542
  path: ["expr"]
44282
44543
  });
44544
+ if (src.for?.mode === "stable") ctx.addIssue({
44545
+ code: "custom",
44546
+ message: "an expression is held, not stable: it has no single value to keep still",
44547
+ path: ["for", "mode"]
44548
+ });
44283
44549
  });
44284
44550
  var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44285
44551
  object({
@@ -44296,6 +44562,7 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44296
44562
  }),
44297
44563
  DeviceBecomesLeafSchema,
44298
44564
  DeviceEventLeafSchema,
44565
+ DeviceFiredLeafSchema,
44299
44566
  ScheduleLeafSchema,
44300
44567
  ManualLeafSchema,
44301
44568
  IntervalLeafSchema,
@@ -44303,11 +44570,14 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44303
44570
  StationaryLeafSchema,
44304
44571
  DeviceIsLeafSchema,
44305
44572
  StationaryPresentLeafSchema,
44306
- ExpressionLeafSchema
44573
+ ExpressionLeafSchema,
44574
+ TimeIsLeafSchema,
44575
+ TriggeredByLeafSchema
44307
44576
  ]));
44308
44577
  var EVENT_KINDS = new Set([
44309
44578
  "device-becomes",
44310
44579
  "device-event",
44580
+ "device-fired",
44311
44581
  "schedule",
44312
44582
  "manual",
44313
44583
  "interval",
@@ -44357,6 +44627,28 @@ function hasEventLeaves(node) {
44357
44627
  });
44358
44628
  return found;
44359
44629
  }
44630
+ /** The event leaves of the tree that carry an id, in tree order. */
44631
+ function identifiedEventLeaves(when) {
44632
+ const found = [];
44633
+ forEachLeaf(when, (leaf, path) => {
44634
+ if (isEventLeaf(leaf) && leaf.id !== void 0) found.push({
44635
+ id: leaf.id,
44636
+ path,
44637
+ leaf
44638
+ });
44639
+ });
44640
+ return found;
44641
+ }
44642
+ /** Does the tree hold a `triggered-by` leaf? */
44643
+ function hasTriggeredByLeaves(when) {
44644
+ let found = false;
44645
+ forEachLeaf(when, (leaf) => {
44646
+ if (leaf.kind === "triggered-by") found = true;
44647
+ });
44648
+ return found;
44649
+ }
44650
+ /** Why a `triggered-by` leaf is refused outside an automation: there is no trigger row to name. */
44651
+ var TRIGGERED_BY_NEEDS_AUTOMATION = "\"triggered by\" reads which trigger started an automation — an action or a scheduler has no trigger rows, so it could never be true here";
44360
44652
  function hasEmptyContainer(node) {
44361
44653
  if (isLeaf(node)) return false;
44362
44654
  if (node.kind === "not") return hasEmptyContainer(node.child);
@@ -44366,6 +44658,7 @@ function hasEmptyContainer(node) {
44366
44658
  var LEAF_WORDS = {
44367
44659
  "device-becomes": "a device value that becomes",
44368
44660
  "device-event": "a device event",
44661
+ "device-fired": "a device firing an event",
44369
44662
  schedule: "a schedule",
44370
44663
  manual: "a manual run",
44371
44664
  interval: "an interval",
@@ -44415,6 +44708,11 @@ function validateWhenTree(when) {
44415
44708
  if (depth > 4) return `the rules nest ${depth} levels deep — the limit is 4. "not (any of …)" is the same as "all of (not …)", which is one level shallower`;
44416
44709
  const leaves = countWhenLeaves(when);
44417
44710
  if (leaves > 32) return `the rules use ${leaves} rows — the limit is 32`;
44711
+ const seen = /* @__PURE__ */ new Set();
44712
+ for (const entry of identifiedEventLeaves(when)) {
44713
+ if (seen.has(entry.id)) return `two trigger rows share the id "${entry.id}" — an id names one row`;
44714
+ seen.add(entry.id);
44715
+ }
44418
44716
  const need = eventNeed(when, "when", false);
44419
44717
  if ("error" in need) return need.error;
44420
44718
  const [first, second] = need.paths;
@@ -44626,9 +44924,12 @@ function convertLegacyWhen(half) {
44626
44924
  * every caller — automations, Actions, and the notification centre, which runs
44627
44925
  * only Actions (planning#473).
44628
44926
  */
44927
+ /** The longest one `wait` step lasts. It is cut by a restart of the run and does not survive a hub restart. */
44928
+ var WAIT_MAX_SECONDS = 3600;
44929
+ var WAIT_TOO_LONG = `a wait is at most one hour (${WAIT_MAX_SECONDS} s)`;
44629
44930
  var AutomationWaitStepSchema = object({
44630
44931
  kind: literal("wait"),
44631
- seconds: number$1().min(0).max(300)
44932
+ seconds: number$1().min(0).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG)
44632
44933
  });
44633
44934
  /** The legacy `deviceId` of a cap step: still READ, never written (D983). */
44634
44935
  var LegacyStepDeviceSchema = union([
@@ -44698,28 +44999,227 @@ var elseNeedsIf = {
44698
44999
  path: ["else"]
44699
45000
  };
44700
45001
  var GuardedWaitStepSchema = AutomationWaitStepSchema.extend(stepGuardShape).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44701
- var GuardedCapStepSchema = AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
45002
+ /**
45003
+ * A step of a STANDALONE list (an action, a scheduler point, a preset): the
45004
+ * same two step kinds an automation runs, minus a guard that reads the
45005
+ * automation's trigger, which such a list does not have.
45006
+ */
45007
+ var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
45008
+ message: "a cap step needs a target",
45009
+ path: ["target"]
45010
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
45011
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
45012
+ path: ["actionId"]
45013
+ }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf)]).superRefine((step, ctx) => {
45014
+ if (step.if !== void 0 && hasTriggeredByLeaves(step.if)) ctx.addIssue({
45015
+ code: "custom",
45016
+ message: TRIGGERED_BY_NEEDS_AUTOMATION,
45017
+ path: ["if"]
45018
+ });
45019
+ });
45020
+ /**
45021
+ * Flow control of an AUTOMATION's list (#500). A standalone list (an action, a
45022
+ * scheduler point, a preset) stays `wait` and `cap`: it has no run of its own
45023
+ * to end and no variable bag to write.
45024
+ */
45025
+ var continueOnErrorShape = {
45026
+ /** A failure of this step is traced `failed-continued` and the next step runs. */
45027
+ continueOnError: literal(true).optional() };
45028
+ /** An automation's cap step: the shared guarded cap step, plus `continueOnError`. */
45029
+ var AutomationGuardedCapStepSchema = AutomationCapStepBaseSchema.extend({
45030
+ ...stepGuardShape,
45031
+ ...continueOnErrorShape
45032
+ }).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44702
45033
  message: "a cap step needs a target",
44703
45034
  path: ["target"]
44704
45035
  }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44705
45036
  message: "a domain target needs the vocabulary action it applies to (actionId)",
44706
45037
  path: ["actionId"]
44707
45038
  }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44708
- var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, GuardedCapStepSchema]);
45039
+ /** End the run here, on purpose. `reason` may carry `{{vars.x}}` slots. */
45040
+ var AutomationStopStepSchema = object({
45041
+ kind: literal("stop"),
45042
+ reason: string().min(1, "a stop needs a reason: it is what the run trace shows").max(200),
45043
+ /** `ok`: the run ends as a success. `failed`: it ends as a failure, with the reason. */
45044
+ outcome: _enum(["ok", "failed"]),
45045
+ /** Stop only when this reads true; false or unknown does not stop. */
45046
+ if: AutomationWhenSchema.optional()
45047
+ });
45048
+ var SET_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,39}$/;
45049
+ /** Names the run itself writes and reads (`leaf` is what `triggered-by` judges): a step may not overwrite them. */
45050
+ var RESERVED_RUN_VARS = [
45051
+ "kind",
45052
+ "leaf",
45053
+ "at"
45054
+ ];
45055
+ /**
45056
+ * Write variables into the run's bag, for the steps after it. SUBSTITUTION
45057
+ * only, never evaluation: a text value may carry `{{vars.x}}` slots.
45058
+ */
45059
+ var AutomationSetStepSchema = object({
45060
+ kind: literal("set"),
45061
+ vars: record(string(), union([
45062
+ string().max(500),
45063
+ number$1(),
45064
+ boolean()
45065
+ ])).refine((vars) => Object.keys(vars).every((name) => SET_VAR_NAME_RE.test(name)), { message: "a variable name is letters, digits and _, not starting with a digit (max 40)" }).refine((vars) => Object.keys(vars).length >= 1, { message: "set needs at least one variable" }).refine((vars) => Object.keys(vars).length <= 20, { message: `set holds at most 20 variables` }).refine((vars) => Object.keys(vars).every((name) => !RESERVED_RUN_VARS.includes(name)), { message: `the names ${RESERVED_RUN_VARS.join(", ")} belong to the run and cannot be set` }),
45066
+ /** Set only when this reads true; false or unknown leaves the bag as it is. */
45067
+ if: AutomationWhenSchema.optional()
45068
+ });
45069
+ var AutomationRunActionStepSchema = object({
45070
+ kind: literal("action"),
45071
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
45072
+ actionId: string().min(1),
45073
+ /** Values for the action's declared variables; the others take their default. */
45074
+ variables: record(string(), union([
45075
+ string(),
45076
+ number$1(),
45077
+ boolean()
45078
+ ])).optional(),
45079
+ ...continueOnErrorShape
45080
+ });
45081
+ /**
45082
+ * A step INSIDE a branch of a `choose`: a flow step with no condition of its
45083
+ * own (the branch is the condition) and no block inside it. Strict, so an `if`
45084
+ * written on such a step is refused rather than dropped.
45085
+ */
45086
+ /** The longest a `wait-until` may wait: six hours. */
45087
+ var WAIT_UNTIL_TIMEOUT_MAX_S = 21600;
45088
+ /**
45089
+ * Pause the run until a condition is true (#500). The timeout is mandatory: a
45090
+ * run never waits without an end. `onTimeout` says what a wait that ran out
45091
+ * does: `continue` goes on to the next step, `stop` ends the run as a failure.
45092
+ * A condition that cannot be read is not "true": the wait goes on (D49).
45093
+ */
45094
+ var AutomationWaitUntilStepSchema = object({
45095
+ kind: literal("wait-until"),
45096
+ when: AutomationWhenSchema,
45097
+ timeoutS: number$1().int().min(1, "a wait needs a timeout of at least one second").max(WAIT_UNTIL_TIMEOUT_MAX_S, `a wait for a condition is at most six hours (${WAIT_UNTIL_TIMEOUT_MAX_S} s)`),
45098
+ onTimeout: _enum(["continue", "stop"])
45099
+ }).strict();
45100
+ /** The step kinds that hold steps of their own, or pause on a condition: they nest two levels at most. */
45101
+ var BLOCK_STEP_KINDS = [
45102
+ "choose",
45103
+ "repeat",
45104
+ "parallel",
45105
+ "wait-until"
45106
+ ];
45107
+ var NEST_TOO_DEEP = `steps nest two levels at most: a step inside a nested choose or repeat can be a plain step only`;
45108
+ function kindOf(input) {
45109
+ return typeof input === "object" && input !== null ? Reflect.get(input, "kind") : void 0;
45110
+ }
45111
+ /** Where the nesting ends, a block step is refused in words rather than as an unknown kind. */
45112
+ function nestTooDeep(issue) {
45113
+ const kind = kindOf(issue.input);
45114
+ return typeof kind === "string" && BLOCK_STEP_KINDS.includes(kind) ? NEST_TOO_DEEP : void 0;
45115
+ }
45116
+ var plainStepSchemas = [
45117
+ AutomationWaitStepSchema.strict(),
45118
+ AutomationCapStepBaseSchema.extend(continueOnErrorShape).strict().refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
45119
+ message: "a cap step needs a target",
45120
+ path: ["target"]
45121
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
45122
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
45123
+ path: ["actionId"]
45124
+ }),
45125
+ AutomationStopStepSchema.omit({ if: true }).strict(),
45126
+ AutomationSetStepSchema.omit({ if: true }).strict(),
45127
+ AutomationRunActionStepSchema.strict()
45128
+ ];
45129
+ /**
45130
+ * A plain step: a flow step with no condition of its own and no step inside
45131
+ * it. The second (last) level of nesting holds these only.
45132
+ */
45133
+ var AutomationBranchStepSchema = discriminatedUnion("kind", plainStepSchemas, { error: nestTooDeep });
45134
+ /** A step that can run beside others: it writes no variable and does not end the run. */
45135
+ var AutomationParallelMemberSchema = AutomationBranchStepSchema.refine((step) => step.kind === "cap" || step.kind === "action" || step.kind === "wait", "a parallel holds device commands, actions and waits only: a set or a stop beside other steps has no order");
45136
+ /**
45137
+ * Several steps at once (#500): all are started together and all are awaited.
45138
+ * A failure fails the run once the others finished, as in a sequence; a member
45139
+ * marked "continue if this fails" does not.
45140
+ */
45141
+ var AutomationParallelStepSchema = object({
45142
+ kind: literal("parallel"),
45143
+ steps: array(AutomationParallelMemberSchema).min(2, "a parallel needs at least two steps").max(5, `a parallel takes at most 5 steps`)
45144
+ }).strict();
45145
+ var REPEAT_NEEDS_MAX = "a repeat on a condition needs a maximum number of iterations";
45146
+ /** A loop on a condition: the bound is mandatory, so a run never loops without an end. */
45147
+ var repeatConditionShape = {
45148
+ when: AutomationWhenSchema,
45149
+ maxIterations: number$1({ error: REPEAT_NEEDS_MAX }).int().min(1, REPEAT_NEEDS_MAX).max(50, `a repeat on a condition runs at most 50 times`),
45150
+ /** A pause between two iterations, never after the last. */
45151
+ waitS: number$1().int().min(1).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG).optional()
45152
+ };
45153
+ /** What decides how many times a `repeat` runs. */
45154
+ var AutomationRepeatLoopSchema = discriminatedUnion("by", [
45155
+ object({
45156
+ by: literal("count"),
45157
+ count: number$1().int().min(1).max(20, `a repeat runs at most 20 times`)
45158
+ }).strict(),
45159
+ object({
45160
+ by: literal("while"),
45161
+ ...repeatConditionShape
45162
+ }).strict(),
45163
+ object({
45164
+ by: literal("until"),
45165
+ ...repeatConditionShape
45166
+ }).strict(),
45167
+ object({
45168
+ by: literal("forEach"),
45169
+ target: object({
45170
+ kind: literal("devices"),
45171
+ ids: array(number$1().int()).min(1, "a repeat over devices needs at least one device").max(50, `a repeat over devices takes at most 50 devices`)
45172
+ }).strict()
45173
+ }).strict()
45174
+ ]);
45175
+ /**
45176
+ * The same steps several times (#500). Every form is bounded at save, so a
45177
+ * run cannot loop without an end. A failure or a `stop` in the steps ends the
45178
+ * loop and the run, as in a sequence.
45179
+ */
45180
+ function repeatSchemaOf(step) {
45181
+ return object({
45182
+ kind: literal("repeat"),
45183
+ loop: AutomationRepeatLoopSchema,
45184
+ steps: array(step).min(1, "a repeat needs at least one step").max(10, `a repeat takes at most 10 steps`)
45185
+ }).strict();
45186
+ }
45187
+ /**
45188
+ * One step, several ways (#500): the branches are read in order and the FIRST
45189
+ * whose condition is true runs; `else` runs when every branch is false. A
45190
+ * branch that cannot be read ends the choice: nothing runs, not the branches
45191
+ * after it and not the else (D49). The #472 `if` / `else` of a wait or cap
45192
+ * step is the one-branch case and stays as it is stored.
45193
+ */
45194
+ function chooseSchemaOf(step) {
45195
+ const steps = array(step).max(10);
45196
+ return object({
45197
+ kind: literal("choose"),
45198
+ branches: array(object({
45199
+ if: AutomationWhenSchema,
45200
+ steps: steps.min(1, "a branch needs at least one step")
45201
+ })).min(1, "a choose needs at least one branch").max(6, `a choose takes at most 6 branches`),
45202
+ else: steps.optional()
45203
+ });
45204
+ }
45205
+ var AutomationBodyStepSchema = discriminatedUnion("kind", [
45206
+ ...plainStepSchemas,
45207
+ AutomationWaitUntilStepSchema,
45208
+ AutomationParallelStepSchema,
45209
+ chooseSchemaOf(AutomationBranchStepSchema),
45210
+ repeatSchemaOf(AutomationBranchStepSchema)
45211
+ ]);
45212
+ var AutomationRepeatStepSchema = repeatSchemaOf(AutomationBodyStepSchema);
44709
45213
  var AutomationActionSchema = discriminatedUnion("kind", [
44710
45214
  GuardedWaitStepSchema,
44711
- GuardedCapStepSchema,
44712
- object({
44713
- kind: literal("action"),
44714
- /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
44715
- actionId: string().min(1),
44716
- /** Values for the action's declared variables; the others take their default. */
44717
- variables: record(string(), union([
44718
- string(),
44719
- number$1(),
44720
- boolean()
44721
- ])).optional()
44722
- }),
45215
+ AutomationGuardedCapStepSchema,
45216
+ AutomationStopStepSchema,
45217
+ AutomationSetStepSchema,
45218
+ chooseSchemaOf(AutomationBodyStepSchema),
45219
+ AutomationWaitUntilStepSchema,
45220
+ AutomationParallelStepSchema,
45221
+ AutomationRepeatStepSchema,
45222
+ AutomationRunActionStepSchema,
44723
45223
  object({
44724
45224
  kind: literal("code"),
44725
45225
  /** Compiled into the automation's OWN block by esbuild — not a third
@@ -44782,11 +45282,6 @@ function convertLegacyRecipe(legacy) {
44782
45282
  ...rest
44783
45283
  };
44784
45284
  }
44785
- /**
44786
- * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
44787
- * saved block carries): the new shape as it is, or the old shape converted to
44788
- * it. The output type is always the new one.
44789
- */
44790
45285
  var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
44791
45286
  var DETECTION_DESCRIPTIONS = {
44792
45287
  deviceId: "the camera that saw it",
@@ -44802,6 +45297,21 @@ var STATIONARY_DESCRIPTIONS = {
44802
45297
  phase: "appeared or departed",
44803
45298
  entryId: "the stopped object"
44804
45299
  };
45300
+ var BECOMES_DESCRIPTIONS = {
45301
+ deviceId: "the device that changed",
45302
+ from: "the value before the change, empty when it is not known",
45303
+ to: "the value it changed to"
45304
+ };
45305
+ /**
45306
+ * A fire's payload is offered too, as `data.<path>` (`{{vars.data.channel}}`):
45307
+ * those names depend on the device, so they are not in this list.
45308
+ */
45309
+ var FIRED_DESCRIPTIONS = {
45310
+ deviceId: "the device that fired",
45311
+ eventType: "the event, as the device names it",
45312
+ sourceId: "the part of the device that fired (a button), empty when it names none",
45313
+ eventKind: "the kind of press (press, long-press …), empty when the device declares none"
45314
+ };
44805
45315
  function describe$1(descriptions) {
44806
45316
  return Object.entries(descriptions).map(([name, description]) => ({
44807
45317
  name,
@@ -44810,8 +45320,74 @@ function describe$1(descriptions) {
44810
45320
  }
44811
45321
  describe$1(DETECTION_DESCRIPTIONS);
44812
45322
  describe$1(STATIONARY_DESCRIPTIONS);
45323
+ describe$1(FIRED_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
45324
+ ...v,
45325
+ hintKey: "firedDeviceId"
45326
+ } : v);
45327
+ describe$1(BECOMES_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
45328
+ ...v,
45329
+ hintKey: "becomesDeviceId"
45330
+ } : v);
44813
45331
  new Set(Object.values(COCO_TO_MACRO.mapping));
44814
45332
  /**
45333
+ * Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
45334
+ * device. Carries the device's EXACT declared event vocabulary verbatim
45335
+ * (NO normalization). Two shapes:
45336
+ * - structured source (HA `event.*` entity) → `vocabulary` is the fixed
45337
+ * declared set (e.g. press_short, press_long, …).
45338
+ * - generic/legacy source (HA bus events, e.g. zha_event) → `vocabulary`
45339
+ * is [] (open) and `lastEvent.data` carries the complex payload.
45340
+ * `vocabulary` (D1044) names each event for a person and types it for a
45341
+ * rule; `eventTypes` is its list of native tokens, kept for older readers.
45342
+ * A fire names the part of the device it came from in `sourceId` when the
45343
+ * vocabulary declares sources (a remote's buttons).
45344
+ * `seq` is monotonic so two identical events still produce distinct slice writes.
45345
+ */
45346
+ var EventFireSchema = object({
45347
+ deviceId: number$1(),
45348
+ eventType: string(),
45349
+ sourceId: string().optional(),
45350
+ data: record(string(), unknown()).nullable(),
45351
+ timestamp: number$1().meta(readingMeta("none")),
45352
+ seq: number$1().meta(readingMeta("count"))
45353
+ });
45354
+ var EventEmitterStatusSchema = object({
45355
+ eventTypes: array(string()),
45356
+ /**
45357
+ * Required of a provider by the type. A status written before the field
45358
+ * existed still parses, as an empty vocabulary: read it through
45359
+ * `readEventVocabulary`, which then answers with the plain tokens.
45360
+ */
45361
+ vocabulary: array(EventVocabularyEntrySchema).default([]),
45362
+ lastEvent: EventFireSchema.nullable(),
45363
+ eventCountSinceStart: number$1().meta(readingMeta("count"))
45364
+ });
45365
+ var eventEmitterCapability = {
45366
+ name: "event-emitter",
45367
+ scope: "device",
45368
+ deviceNative: true,
45369
+ mode: "singleton",
45370
+ deviceTypes: [DeviceType.EventEmitter],
45371
+ methods: {},
45372
+ events: { onEvent: { data: EventFireSchema } },
45373
+ status: {
45374
+ schema: EventEmitterStatusSchema,
45375
+ kind: "push"
45376
+ },
45377
+ runtimeState: EventEmitterStatusSchema,
45378
+ /**
45379
+ * Runtime-state durability: **session** — `eventCountSinceStart` names its own scope.
45380
+ *
45381
+ * See `RuntimeStateDurability`. Enforced by
45382
+ * `scripts/check-runtime-state-durability.ts`.
45383
+ */
45384
+ durability: "session",
45385
+ /**
45386
+ * History (D850): none — `eventCountSinceStart` resets with the addon; events live in ledgers.
45387
+ */
45388
+ history: "none"
45389
+ };
45390
+ /**
44815
45391
  * Schedulers (D983) — an entity that performs actions over the day.
44816
45392
  *
44817
45393
  * A scheduler has profiles; a profile has weekdays and a timeline of POINTS.
@@ -44834,6 +45410,13 @@ var SchedulerStateTreeSchema = AutomationWhenSchema.superRefine((when, ctx) => {
44834
45410
  });
44835
45411
  return;
44836
45412
  }
45413
+ if (hasTriggeredByLeaves(when)) {
45414
+ ctx.addIssue({
45415
+ code: "custom",
45416
+ message: TRIGGERED_BY_NEEDS_AUTOMATION
45417
+ });
45418
+ return;
45419
+ }
44837
45420
  const problem = validateWhenTree(when);
44838
45421
  if (problem !== null) ctx.addIssue({
44839
45422
  code: "custom",
@@ -44855,12 +45438,16 @@ var HoldOverrideSchema = discriminatedUnion("mode", [object({ mode: literal("res
44855
45438
  })]);
44856
45439
  /** A missed edge up to this late still runs. */
44857
45440
  var DEFAULT_MAX_LATE_MS = 10 * 6e4;
45441
+ var SchedulerStepSchema = ActionStepSchema.refine((step) => step.kind !== "wait" || step.seconds <= 300, {
45442
+ message: `a scheduler point waits at most five minutes (300 s); put a longer wait in an action`,
45443
+ path: ["seconds"]
45444
+ });
44858
45445
  var SchedulerPointSchema = object({
44859
45446
  id: string().min(1).max(64),
44860
45447
  at: SchedulerTimeSpecSchema,
44861
45448
  mode: SchedulerPointModeSchema,
44862
45449
  /** The actions of an `edge` or `hold`; a `release` has none. */
44863
- steps: array(ActionStepSchema).max(20).default([]),
45450
+ steps: array(SchedulerStepSchema).max(20).default([]),
44864
45451
  /** Checked when the point runs (an edge) or is applied (a hold). */
44865
45452
  conditions: SchedulerStateTreeSchema.optional(),
44866
45453
  /** Hold only; absent means `respect`. */
@@ -45039,7 +45626,9 @@ var AutomationTriggerTraceSchema = object({
45039
45626
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
45040
45627
  reason: string(),
45041
45628
  /** deviceId, className, trackId, score … — only what the trigger exposes. */
45042
- vars: record(string(), TraceValueSchema)
45629
+ vars: record(string(), TraceValueSchema),
45630
+ /** A hold (`for`) this occurrence went through: pending, cancelled, fired or re-seeded. */
45631
+ hold: HoldTraceSchema.optional()
45043
45632
  });
45044
45633
  var AutomationConditionTraceNodeSchema = object({
45045
45634
  /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
@@ -45052,8 +45641,11 @@ var AutomationConditionTraceNodeSchema = object({
45052
45641
  "device-is",
45053
45642
  "expression",
45054
45643
  "stationary-present",
45644
+ "time-is",
45645
+ "triggered-by",
45055
45646
  "device-becomes",
45056
45647
  "device-event",
45648
+ "device-fired",
45057
45649
  "schedule",
45058
45650
  "interval",
45059
45651
  "manual",
@@ -45124,18 +45716,46 @@ var StepTraceBaseSchema = object({
45124
45716
  ]).optional(),
45125
45717
  /** Why the condition read what it read, in the evaluator's own words. */
45126
45718
  conditionReason: string().optional(),
45127
- /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. */
45719
+ /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. `failed-continued`: it failed and was marked to be passed over (#500). */
45128
45720
  outcome: _enum([
45129
45721
  "ok",
45130
45722
  "error",
45131
45723
  "not-run",
45132
45724
  "empty",
45133
- "skipped"
45725
+ "skipped",
45726
+ "failed-continued",
45727
+ "timed-out"
45134
45728
  ]),
45135
45729
  message: string().optional()
45136
45730
  });
45731
+ /** What a block step adds to its row: which list ran, what each branch read, how many rounds. */
45732
+ var blockTraceShape = {
45733
+ /** A `choose` step (#500): the list that ran, `branches.<n>` or `else`. Absent when none did. */
45734
+ branch: string().optional(),
45735
+ /** A `choose` step: what each branch read, in order, up to the one that decided. */
45736
+ branchChecks: array(object({
45737
+ condition: _enum([
45738
+ "true",
45739
+ "false",
45740
+ "unknown"
45741
+ ]),
45742
+ conditionReason: string().optional()
45743
+ })).optional(),
45744
+ /** A `repeat` step (#500): how many times its steps ran. */
45745
+ iterations: number$1().int().optional()
45746
+ };
45747
+ /** A step one level down: it may be a block step itself, whose own steps are plain rows (the nesting ends there). */
45748
+ var NestedStepTraceSchema = StepTraceBaseSchema.extend({
45749
+ ...blockTraceShape,
45750
+ steps: array(StepTraceBaseSchema).optional()
45751
+ });
45137
45752
  /** A step trace; the `else` steps that ran are one level of the same shape (the nesting is bounded at one). */
45138
- var AutomationStepTraceSchema = StepTraceBaseSchema.extend({ else: array(StepTraceBaseSchema).optional() });
45753
+ var AutomationStepTraceSchema = StepTraceBaseSchema.extend({
45754
+ else: array(StepTraceBaseSchema).optional(),
45755
+ ...blockTraceShape,
45756
+ /** A `choose` step: the steps of the list that ran. A `parallel`: its members. A `repeat`: the LAST iteration. */
45757
+ steps: array(NestedStepTraceSchema).optional()
45758
+ });
45139
45759
  var AutomationRunTraceSchema = object({
45140
45760
  runId: string().min(1),
45141
45761
  startedAt: number$1(),
@@ -45385,7 +46005,7 @@ object({
45385
46005
  * is irrelevant; the array is treated as a set by consumers. */
45386
46006
  hiddenChildIds: array(number$1()).readonly()
45387
46007
  });
45388
- DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Light, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Cover, DeviceType.Lock, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Generic, method(object({
46008
+ DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Sensor, DeviceType.Climate, DeviceType.Cover, DeviceType.Lock, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Generic, method(object({
45389
46009
  deviceId: number$1().int().nonnegative(),
45390
46010
  childDeviceId: number$1().int().nonnegative(),
45391
46011
  hidden: boolean()
@@ -45542,7 +46162,9 @@ var AddonHttpRouteSchema = object({
45542
46162
  ]).optional(),
45543
46163
  description: string().optional(),
45544
46164
  /** The hub limits this route per client address before the handler runs. */
45545
- rateLimit: _enum(["auth"]).optional()
46165
+ rateLimit: _enum(["auth"]).optional(),
46166
+ /** Served once per integration instance; the handler reads `instanceId`. */
46167
+ perInstance: boolean().optional()
45546
46168
  });
45547
46169
  /**
45548
46170
  * Cross-process route invocation envelope. The hub captures the
@@ -45559,6 +46181,8 @@ var InvokeRequestSchema = object({
45559
46181
  query: record(string(), string()),
45560
46182
  body: unknown(),
45561
46183
  headers: record(string(), string()),
46184
+ /** The instance the hub resolved for a `perInstance` route. */
46185
+ instanceId: string().optional(),
45562
46186
  user: object({
45563
46187
  id: string(),
45564
46188
  username: string(),
@@ -47683,7 +48307,7 @@ var BATTERY_DEVICE_TYPES = [
47683
48307
  DeviceType.Switch,
47684
48308
  DeviceType.PetFeeder,
47685
48309
  DeviceType.Container,
47686
- DeviceType.Thermostat,
48310
+ DeviceType.Climate,
47687
48311
  DeviceType.EventEmitter
47688
48312
  ];
47689
48313
  /** Generic Customize target for the percentage reading (D936). */
@@ -49446,7 +50070,14 @@ var climateControlCapability = {
49446
50070
  scope: "device",
49447
50071
  deviceNative: true,
49448
50072
  mode: "singleton",
49449
- deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
50073
+ deviceTypes: [DeviceType.Climate],
50074
+ power: {
50075
+ kind: "mode",
50076
+ field: "mode",
50077
+ offValue: "off",
50078
+ set: "setMode",
50079
+ lastField: "lastActiveMode"
50080
+ },
49450
50081
  methods: {
49451
50082
  setMode: method(object({
49452
50083
  deviceId: number$1().int().nonnegative(),
@@ -49831,6 +50462,7 @@ var CompositionProblemSchema = object({
49831
50462
  "nested-field",
49832
50463
  "missing-field",
49833
50464
  "kind-mismatch",
50465
+ "source-kind-mismatch",
49834
50466
  "may-be-null",
49835
50467
  "source-missing",
49836
50468
  "source-unreadable",
@@ -49852,7 +50484,8 @@ var CompositionProblemSchema = object({
49852
50484
  "command-method-unknown",
49853
50485
  "command-source-unknown",
49854
50486
  "command-input-unknown",
49855
- "command-input-missing"
50487
+ "command-input-missing",
50488
+ "power-source-is-actuator"
49856
50489
  ]),
49857
50490
  path: string(),
49858
50491
  message: string()
@@ -50890,6 +51523,13 @@ var mediaPlayerCapability = {
50890
51523
  deviceNative: true,
50891
51524
  mode: "singleton",
50892
51525
  deviceTypes: [DeviceType.MediaPlayer],
51526
+ power: {
51527
+ kind: "methods",
51528
+ field: "state",
51529
+ offValues: ["off", "standby"],
51530
+ on: "turnOn",
51531
+ off: "turnOff"
51532
+ },
50893
51533
  methods: {
50894
51534
  turnOn: optionalMethod(deviceOnly, _void(), {
50895
51535
  kind: "mutation",
@@ -51020,7 +51660,7 @@ var mediaPlayerCapability = {
51020
51660
  isMuted: { kind: "boolean" }
51021
51661
  } }
51022
51662
  };
51023
- 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.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, 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.Heater, 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.Heater, 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.Heater, 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.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, 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, DeviceType.Heater, DeviceType.MediaPlayer;
51663
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Climate, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, DeviceType.Camera, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, 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.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.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.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Gateway, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.PetFeeder, DeviceType.MediaPlayer;
51024
51664
  var NUMBER$1 = {
51025
51665
  kind: "fixed",
51026
51666
  result: ["number"]
@@ -51103,7 +51743,7 @@ function numeric$1(input) {
51103
51743
  }, ...alsoFrom === void 0 ? [] : [alsoFrom.accept]]
51104
51744
  };
51105
51745
  }
51106
- var CLIMATE_DEVICES = [DeviceType.Thermostat, DeviceType.Climate];
51746
+ var CLIMATE_DEVICES = [DeviceType.Climate];
51107
51747
  function binary(input) {
51108
51748
  return {
51109
51749
  ...input,
@@ -51425,8 +52065,8 @@ var GROUP_TEMPLATES = [
51425
52065
  actuator({
51426
52066
  id: "climate",
51427
52067
  summary: "Running when at least one thermostat or air conditioner is not off, or only when all of them are. Target temperature and fan are offered only when every device has them. A device that reports nothing is left out, never read as off.",
51428
- deviceType: DeviceType.Thermostat,
51429
- memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
52068
+ deviceType: DeviceType.Climate,
52069
+ memberDeviceTypes: [DeviceType.Climate],
51430
52070
  outputCap: "group-active",
51431
52071
  outputField: "active",
51432
52072
  member: {
@@ -51469,6 +52109,118 @@ function actuator(input) {
51469
52109
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
51470
52110
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
51471
52111
  DeviceType.Camera;
52112
+ var PowerSourceRefusalReasonSchema = _enum([
52113
+ "no-power-source",
52114
+ "switch-missing",
52115
+ "switch-offline",
52116
+ "switch-unknown",
52117
+ "restart-in-progress",
52118
+ "switch-call-failed",
52119
+ "switch-is-actuator"
52120
+ ]);
52121
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
52122
+ accepted: literal(false),
52123
+ reason: PowerSourceRefusalReasonSchema,
52124
+ /** One line for the operator, naming the switch. */
52125
+ message: string()
52126
+ })]);
52127
+ var PowerSourceInfoSchema = object({
52128
+ /** False when the device has no power source linked; the rest is then empty. */
52129
+ linked: boolean(),
52130
+ switchDeviceId: number$1().nullable(),
52131
+ switchName: string().nullable(),
52132
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
52133
+ switchOn: boolean().nullable(),
52134
+ switchOnline: boolean().nullable(),
52135
+ restartInProgress: boolean(),
52136
+ offSeconds: number$1().int()
52137
+ });
52138
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
52139
+ var PowerSourceStateSchema = object({
52140
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
52141
+ switchOn: boolean().nullable(),
52142
+ offSeconds: number$1().int(),
52143
+ lastFetchedAt: number$1()
52144
+ });
52145
+ var POWER_SOURCE_DEVICE_TYPES = [
52146
+ DeviceType.Gateway,
52147
+ DeviceType.Camera,
52148
+ DeviceType.Sensor,
52149
+ DeviceType.MediaPlayer,
52150
+ DeviceType.Vacuum,
52151
+ DeviceType.Generic,
52152
+ DeviceType.Washer,
52153
+ DeviceType.Dryer,
52154
+ DeviceType.Dishwasher,
52155
+ DeviceType.PetFountain,
52156
+ DeviceType.Fan,
52157
+ DeviceType.Humidifier,
52158
+ DeviceType.Climate,
52159
+ DeviceType.WaterHeater
52160
+ ];
52161
+ var powerSourceCustomize = {
52162
+ version: 1,
52163
+ targets: [{
52164
+ id: "switch",
52165
+ cardinality: "scalar",
52166
+ vocabulary: {
52167
+ cap: "power-source",
52168
+ stateField: "switchOn"
52169
+ },
52170
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
52171
+ apply: "add",
52172
+ path: "switchOn",
52173
+ sources: [{
52174
+ kind: "switch",
52175
+ transforms: [{ kind: "identity" }]
52176
+ }],
52177
+ companions: [{
52178
+ kind: "constant",
52179
+ path: "offSeconds",
52180
+ value: 10,
52181
+ when: "add",
52182
+ formerValues: [0]
52183
+ }, {
52184
+ kind: "clock",
52185
+ path: "lastFetchedAt",
52186
+ when: "always"
52187
+ }],
52188
+ sourceReport: { status: "assignment-status" }
52189
+ }]
52190
+ };
52191
+ var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
52192
+ var powerSourceCapability = {
52193
+ name: "power-source",
52194
+ scope: "device",
52195
+ mode: "singleton",
52196
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
52197
+ methods: {
52198
+ getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "protected" }),
52199
+ powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52200
+ kind: "mutation",
52201
+ auth: "protected"
52202
+ }),
52203
+ powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52204
+ kind: "mutation",
52205
+ auth: "protected"
52206
+ }),
52207
+ forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52208
+ kind: "mutation",
52209
+ auth: "protected"
52210
+ })
52211
+ },
52212
+ runtimeState: PowerSourceStateSchema,
52213
+ customize: powerSourceCustomize,
52214
+ /**
52215
+ * Runtime-state durability: **session** — mirrored from the linked switch on
52216
+ * its next report; a restored value would outlive the reading it names.
52217
+ *
52218
+ * See `RuntimeStateDurability`. Enforced by
52219
+ * `scripts/check-runtime-state-durability.ts`.
52220
+ */
52221
+ durability: "session",
52222
+ history: "none"
52223
+ };
51472
52224
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
51473
52225
  /**
51474
52226
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -52513,27 +53265,35 @@ var deviceAdminLinkCapability = {
52513
53265
  getAdminLink: method(object({ deviceId: number$1().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
52514
53266
  };
52515
53267
  /**
52516
- * device-discovery — device-scoped capability for parents that host /
52517
- * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
52518
- * integrations). The parent advertises a list of discoverable children,
52519
- * and the operator promotes a subset to real devices via `adoptDevice`.
52520
- * Child lifecycle (kernel device create/remove, parent reference) is
52521
- * owned by the framework so a single shared `<DiscoveryPanel />` can
52522
- * drive any integration.
52523
- *
52524
- * Boundaries:
52525
- * - The cap exposes ENUMERATION + ADOPT/RELEASE only. The protocol
52526
- * used to discover children (CGI poll, mDNS, push from hub, ...)
52527
- * is the integration's business; results land in `runtimeState`
52528
- * so the panel can render without a fresh round-trip.
52529
- * - `childNativeId` is integration-defined and must be stable across
52530
- * reboots so re-discovery doesn't duplicate adopted children.
52531
- * - `adoptDevice` returns the new `deviceId` so the UI can navigate
52532
- * directly to the freshly-created child's settings page.
52533
- */
52534
- /**
52535
- * Status enum for a discovered device. Mirrors the Reolink simpleEvent
52536
- * lifecycle (`online` / `sleeping` / `offline`) — generic enough for
53268
+ * `device-adoption` — generic discovery + adoption surface,
53269
+ * system-scoped collection (one provider per integration addon), routed
53270
+ * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
53271
+ * pin model as `connection-test`. The admin UI keeps using
53272
+ * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
53273
+ * successor to `ha-discovery`:
53274
+ * keyed by `integrationId` (the addon resolves integrationId → broker
53275
+ * internally) rather than a connection-specific `brokerId`, and it
53276
+ * OWNS the shared `DiscoveredChildDevice` candidate type
53277
+ * (declared below) so a single shared adoption modal renders any integration's
53278
+ * candidate tree.
53279
+ *
53280
+ * UI flow (Integrations page → integration detail):
53281
+ * 1. `listCandidates({integrationId, page, pageSize, filter})` →
53282
+ * paginated candidate list, each row collapsed.
53283
+ * 2. User expands a row → `getCandidate({integrationId,
53284
+ * childNativeId})` returns the single candidate (with its nested
53285
+ * `children`) for the accordion render.
53286
+ * 3. User clicks "Adopt" on one or more candidates →
53287
+ * `adopt({integrationId, childNativeIds, perCandidate?})`
53288
+ * materialises one parent CamStack device per candidate plus its
53289
+ * accessory children. `perCandidate[childNativeId]` carries an
53290
+ * optional display-name override + a `hiddenChildIds` pre-hide set
53291
+ * (the parent's `accessories.hiddenChildIds` gates UI visibility).
53292
+ * 4. User clicks "Release" → `release({integrationId, camDeviceId})`
53293
+ * removes the parent + every child from the kernel registry.
53294
+ */
53295
+ /**
53296
+ * Status enum for a discovered device. Generic enough for
52537
53297
  * battery cams, awake cams, and unreachable cams alike.
52538
53298
  */
52539
53299
  var DiscoveredChildStatusSchema = _enum([
@@ -52550,12 +53310,12 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
52550
53310
  /** DeviceType the child should be created with on adopt. */
52551
53311
  type: _enum(DeviceType),
52552
53312
  status: DiscoveredChildStatusSchema,
52553
- /** Free-form integration-specific metadata surfaced in the panel. */
53313
+ /** Free-form integration-specific metadata surfaced in the adoption modal. */
52554
53314
  metadata: object({
52555
53315
  model: string().optional(),
52556
53316
  serialNumber: string().optional(),
52557
53317
  uid: string().optional(),
52558
- /** Reolink: 0-based channel index inside the parent NVR/Hub. */
53318
+ /** Reolink: 0-based channel index inside the parent NVR. */
52559
53319
  rtspChannel: number$1().int().nonnegative().optional(),
52560
53320
  isBattery: boolean().optional(),
52561
53321
  isDoorbell: boolean().optional(),
@@ -52571,137 +53331,24 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
52571
53331
  }).default({}),
52572
53332
  /**
52573
53333
  * `true` when the framework already created a child device for this
52574
- * `childNativeId` under the current parent. The panel uses it to
53334
+ * `childNativeId` under the current parent. The adoption modal uses it to
52575
53335
  * gate the Add/Remove button and surface the existing `deviceId`.
52576
53336
  */
52577
53337
  alreadyAdopted: boolean(),
52578
53338
  /** When `alreadyAdopted=true`, the framework-assigned child device id. */
52579
53339
  adoptedDeviceId: number$1().int().nonnegative().nullable(),
52580
- /** Capability names the candidate advertises — integrations populate, Hubs omit. */
53340
+ /** Capability names the candidate advertises — integrations populate, flat enumerations omit. */
52581
53341
  capabilities: array(string()).readonly().optional(),
52582
- /** Feature flags the candidate advertises — integrations populate, Hubs omit. */
53342
+ /** Feature flags the candidate advertises — integrations populate, flat enumerations omit. */
52583
53343
  features: array(string()).readonly().optional(),
52584
53344
  /** Upstream-system identity + rendering hints for the candidate. */
52585
53345
  sourceInfo: SourceInfoSchema.optional(),
52586
- /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
53346
+ /** Nested entity-children for accordion preview — integrations populate, flat enumerations omit. */
52587
53347
  children: array(DiscoveredChildDeviceSchema).readonly().optional(),
52588
53348
  /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
52589
53349
  parentStableId: string().optional()
52590
53350
  }));
52591
53351
  /**
52592
- * Status block — populated by the integration after each
52593
- * `refreshDiscovery` (or push-driven update). The runtime-state slice
52594
- * is what the panel reads via the per-device proxy; `getStatus`
52595
- * returns the same payload via the codegen'd auto-injection.
52596
- */
52597
- var DeviceDiscoveryStatusSchema = object({
52598
- discovered: array(DiscoveredChildDeviceSchema),
52599
- /** Wall-clock ms of the last successful enumeration. */
52600
- lastDiscoveryAt: number$1().int().nonnegative().nullable(),
52601
- /** Last error surfaced from the source (rendered as a banner). */
52602
- lastError: string().nullable()
52603
- });
52604
- var deviceDiscoveryCapability = {
52605
- name: "device-discovery",
52606
- scope: "device",
52607
- deviceNative: true,
52608
- mode: "singleton",
52609
- deviceTypes: [DeviceType.Hub],
52610
- status: {
52611
- schema: DeviceDiscoveryStatusSchema,
52612
- kind: "poll"
52613
- },
52614
- runtimeState: DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number$1().int().nonnegative() }),
52615
- /**
52616
- * Runtime-state durability: **session** — 5.7 KB of scan output on the largest device, fully re-derivable by re-scanning.
52617
- *
52618
- * See `RuntimeStateDurability`. Enforced by
52619
- * `scripts/check-runtime-state-durability.ts`.
52620
- */
52621
- durability: "session",
52622
- /**
52623
- * History (D850): none — inventory.
52624
- */
52625
- history: "none",
52626
- methods: {
52627
- /**
52628
- * Snapshot of the current `discovered` list. Returns the
52629
- * runtime-state cache — call `refreshDiscovery` first if a
52630
- * fresh round-trip to the source is required.
52631
- */
52632
- listDiscovered: method(object({ deviceId: number$1().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly()),
52633
- /**
52634
- * Force the integration to re-enumerate and update the
52635
- * runtime-state slice. Returns the freshly-enumerated list (also
52636
- * available via `listDiscovered` post-call).
52637
- */
52638
- refreshDiscovery: method(object({ deviceId: number$1().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly(), {
52639
- kind: "mutation",
52640
- auth: "admin"
52641
- }),
52642
- /**
52643
- * Promote a discovered entry to a real child device. The framework
52644
- * creates the child via `kernel.devices.create()` with
52645
- * `parentDeviceId = parent.id` and seeds the child's config from
52646
- * `childInitialConfig` (driver-defined; usually carries channel +
52647
- * uid + parent reference). Returns the kernel-assigned numeric id.
52648
- */
52649
- adoptDevice: method(object({
52650
- deviceId: number$1().int().nonnegative(),
52651
- childNativeId: string(),
52652
- /** Optional override for the child's display name. */
52653
- name: string().optional()
52654
- }), object({
52655
- deviceId: number$1().int().nonnegative(),
52656
- stableId: string()
52657
- }), {
52658
- kind: "mutation",
52659
- auth: "admin"
52660
- }),
52661
- /**
52662
- * Inverse of `adoptDevice`: removes the child device from the
52663
- * kernel registry. The discovered entry remains in the
52664
- * enumeration (status updates resume) so the operator can re-adopt
52665
- * it later without a fresh refresh.
52666
- */
52667
- releaseDevice: method(object({
52668
- deviceId: number$1().int().nonnegative(),
52669
- childDeviceId: number$1().int().nonnegative()
52670
- }), _void(), {
52671
- kind: "mutation",
52672
- auth: "admin"
52673
- })
52674
- }
52675
- };
52676
- /**
52677
- * `device-adoption` — generic discovery + adoption surface,
52678
- * system-scoped collection (one provider per integration addon), routed
52679
- * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
52680
- * pin model as `connection-test`. The admin UI keeps using
52681
- * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
52682
- * successor to `ha-discovery`:
52683
- * keyed by `integrationId` (the addon resolves integrationId → broker
52684
- * internally) rather than a connection-specific `brokerId`, and it
52685
- * REUSES the shared `DiscoveredChildDevice` candidate type from
52686
- * `device-discovery.cap.ts` so a single shared adoption panel renders
52687
- * any integration's candidate tree.
52688
- *
52689
- * UI flow (Integrations page → integration detail):
52690
- * 1. `listCandidates({integrationId, page, pageSize, filter})` →
52691
- * paginated candidate list, each row collapsed.
52692
- * 2. User expands a row → `getCandidate({integrationId,
52693
- * childNativeId})` returns the single candidate (with its nested
52694
- * `children`) for the accordion render.
52695
- * 3. User clicks "Adopt" on one or more candidates →
52696
- * `adopt({integrationId, childNativeIds, perCandidate?})`
52697
- * materialises one parent CamStack device per candidate plus its
52698
- * accessory children. `perCandidate[childNativeId]` carries an
52699
- * optional display-name override + a `hiddenChildIds` pre-hide set
52700
- * (the parent's `accessories.hiddenChildIds` gates UI visibility).
52701
- * 4. User clicks "Release" → `release({integrationId, camDeviceId})`
52702
- * removes the parent + every child from the kernel registry.
52703
- */
52704
- /**
52705
53352
  * Provider-declared discovery *granularity* filter (the keystone of the
52706
53353
  * single-entity-import design). A provider advertises which discovery
52707
53354
  * granularities it supports via `listCandidateFilters`; the operator picks
@@ -54129,8 +54776,14 @@ method(object({
54129
54776
  "string",
54130
54777
  "number",
54131
54778
  "boolean",
54132
- "enum"
54779
+ "enum",
54780
+ "union"
54133
54781
  ]),
54782
+ unionMembers: array(_enum([
54783
+ "string",
54784
+ "number",
54785
+ "boolean"
54786
+ ])).optional(),
54134
54787
  enumValues: array(string()).optional(),
54135
54788
  item: boolean().optional(),
54136
54789
  quantity: ReadingQuantitySchema.optional()
@@ -56222,53 +56875,6 @@ var enumSensorCapability = {
56222
56875
  * whether persisting is worth a SQLite commit. */
56223
56876
  volatileStateFields: ["lastFetchedAt", "timestamp"]
56224
56877
  };
56225
- /**
56226
- * Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
56227
- * device. Carries the device's EXACT declared event vocabulary verbatim
56228
- * (NO normalization). Two shapes:
56229
- * - structured source (HA `event.*` entity) → `eventTypes` is the fixed
56230
- * declared vocabulary (e.g. ['press_short','press_long',...]).
56231
- * - generic/legacy source (HA bus events, e.g. zha_event) → `eventTypes`
56232
- * is [] (open) and `lastEvent.data` carries the complex payload.
56233
- * `seq` is monotonic so two identical events still produce distinct slice writes.
56234
- */
56235
- var EventFireSchema = object({
56236
- deviceId: number$1(),
56237
- eventType: string(),
56238
- data: record(string(), unknown()).nullable(),
56239
- timestamp: number$1().meta(readingMeta("none")),
56240
- seq: number$1().meta(readingMeta("count"))
56241
- });
56242
- var EventEmitterStatusSchema = object({
56243
- eventTypes: array(string()),
56244
- lastEvent: EventFireSchema.nullable(),
56245
- eventCountSinceStart: number$1().meta(readingMeta("count"))
56246
- });
56247
- var eventEmitterCapability = {
56248
- name: "event-emitter",
56249
- scope: "device",
56250
- deviceNative: true,
56251
- mode: "singleton",
56252
- deviceTypes: [DeviceType.EventEmitter],
56253
- methods: {},
56254
- events: { onEvent: { data: EventFireSchema } },
56255
- status: {
56256
- schema: EventEmitterStatusSchema,
56257
- kind: "push"
56258
- },
56259
- runtimeState: EventEmitterStatusSchema,
56260
- /**
56261
- * Runtime-state durability: **session** — `eventCountSinceStart` names its own scope.
56262
- *
56263
- * See `RuntimeStateDurability`. Enforced by
56264
- * `scripts/check-runtime-state-durability.ts`.
56265
- */
56266
- durability: "session",
56267
- /**
56268
- * History (D850): none — `eventCountSinceStart` resets with the addon; events live in ledgers.
56269
- */
56270
- history: "none"
56271
- };
56272
56878
  var IdentitySchema = object({
56273
56879
  id: string(),
56274
56880
  name: string(),
@@ -56758,8 +57364,85 @@ method(_void(), array(FailureContributionSchema).readonly());
56758
57364
  * - `FanPreset` — preset selector
56759
57365
  * - `FanDirection` — direction toggle (ceiling fans)
56760
57366
  * - `FanOscillating` — oscillation toggle
57367
+ *
57368
+ * Per-axis oscillation (`oscillationAxes`, `setOscillationAxis`, D1034): an air
57369
+ * circulator sweeps horizontally and vertically, each axis with its own
57370
+ * switch, fixed angle, sweep bounds and limits. A provider may report them
57371
+ * natively; otherwise the OPERATOR links sibling devices to each field in
57372
+ * Customize (a `replace` over the fields listed in `overridable` — nothing is
57373
+ * linked or inferred by itself), and a write to a linked field is forwarded to
57374
+ * its source. `oscillating` stays the single summary flag: when axes exist it
57375
+ * means "any axis oscillates" (`anyAxisOscillating`).
56761
57376
  */
56762
57377
  var FanDirectionSchema = _enum(["forward", "reverse"]);
57378
+ var FAN_OSCILLATION_AXES = ["horizontal", "vertical"];
57379
+ var FanOscillationAxisNameSchema = _enum(FAN_OSCILLATION_AXES);
57380
+ var FanOscillationAxisFieldSchema = _enum([
57381
+ "oscillating",
57382
+ "angle",
57383
+ "sweepFrom",
57384
+ "sweepTo"
57385
+ ]);
57386
+ /** The limits of one axis, read from the source that serves its angle. */
57387
+ var FAN_OSCILLATION_AXIS_LIMITS = [
57388
+ "min",
57389
+ "max",
57390
+ "step"
57391
+ ];
57392
+ /**
57393
+ * One oscillation axis. Every reading the device has not reported is `null`,
57394
+ * never `0` (D393): `0°` is a real position. Each leaf defaults to `null`, so
57395
+ * a slice that names only the leaves it knows (a Customize link of one field)
57396
+ * is still a whole axis.
57397
+ */
57398
+ var FanOscillationAxisSchema = object({
57399
+ /** Whether the axis sweeps. */
57400
+ oscillating: boolean().nullable().default(null),
57401
+ /** The fixed position (degrees) the axis holds when it does not sweep. */
57402
+ angle: number$1().nullable().default(null),
57403
+ /** The sweep bounds (degrees): left → right, bottom → top. */
57404
+ sweepFrom: number$1().nullable().default(null),
57405
+ sweepTo: number$1().nullable().default(null),
57406
+ /** The angles the axis accepts. Null when the device states none. */
57407
+ min: number$1().nullable().default(null),
57408
+ max: number$1().nullable().default(null),
57409
+ step: number$1().nullable().default(null)
57410
+ });
57411
+ /** Both axes; an axis the device does not have is `null`. */
57412
+ var FanOscillationAxesSchema = object({
57413
+ horizontal: FanOscillationAxisSchema.nullable().default(null),
57414
+ vertical: FanOscillationAxisSchema.nullable().default(null)
57415
+ });
57416
+ var FanOscillationAxisRefusalSchema = _enum([
57417
+ "nothing-to-set",
57418
+ "axis-not-linked",
57419
+ "out-of-range",
57420
+ "source-gone",
57421
+ "source-unknown",
57422
+ "source-call-failed"
57423
+ ]);
57424
+ var FanOscillationAxisResultSchema = discriminatedUnion("status", [object({ status: literal("applied") }), object({
57425
+ status: literal("refused"),
57426
+ reason: FanOscillationAxisRefusalSchema,
57427
+ /** The field at fault, when one is. */
57428
+ field: FanOscillationAxisFieldSchema.optional(),
57429
+ /** One line for the operator, naming the axis, the field and the source. */
57430
+ detail: string(),
57431
+ /** Fields that DID land before the refusal (writes run in order, stop at the first failure). */
57432
+ applied: array(FanOscillationAxisFieldSchema)
57433
+ })]);
57434
+ var SetOscillationAxisInputSchema = object({
57435
+ deviceId: number$1().int().nonnegative(),
57436
+ axis: FanOscillationAxisNameSchema,
57437
+ oscillating: boolean().optional(),
57438
+ angle: number$1().optional(),
57439
+ sweepFrom: number$1().optional(),
57440
+ sweepTo: number$1().optional()
57441
+ });
57442
+ /** The state path of one axis field: `oscillationAxes.<axis>.<field>`. */
57443
+ function fanAxisPath(axis, field) {
57444
+ return `oscillationAxes.${axis}.${field}`;
57445
+ }
56763
57446
  var FanControlStatusSchema = object({
56764
57447
  /** Active speed as 0..100 inclusive. Null when the device has no
56765
57448
  * speed surface (single-speed fan). */
@@ -56778,9 +57461,102 @@ var FanControlStatusSchema = object({
56778
57461
  /** Oscillation toggle. Null when the device has no oscillation
56779
57462
  * surface. */
56780
57463
  oscillating: boolean().nullable(),
57464
+ /**
57465
+ * Per-axis oscillation (air circulators). Absent when the device has no
57466
+ * per-axis surface and nothing is linked in Customize.
57467
+ */
57468
+ oscillationAxes: FanOscillationAxesSchema.optional(),
56781
57469
  /** Ms epoch when the slice was last updated. */
56782
57470
  lastChangedAt: number$1()
56783
57471
  });
57472
+ var FAN_CUSTOMIZE_DEVICE_TYPES = [DeviceType.Fan];
57473
+ /** The Customize section the axis targets are listed under. */
57474
+ var AXES_SECTION = "oscillation-axes";
57475
+ function axisOscillatingTarget(axis) {
57476
+ return {
57477
+ id: `${axis}-oscillating`,
57478
+ cardinality: "scalar",
57479
+ vocabulary: {
57480
+ cap: "fan-control",
57481
+ stateField: "oscillating"
57482
+ },
57483
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57484
+ apply: "replace",
57485
+ section: AXES_SECTION,
57486
+ path: fanAxisPath(axis, "oscillating"),
57487
+ sources: [{
57488
+ kind: "switch",
57489
+ transforms: [{ kind: "identity" }]
57490
+ }],
57491
+ sourceReport: { status: "assignment-status" }
57492
+ };
57493
+ }
57494
+ /**
57495
+ * A numeric axis field. The `angle` link also carries the axis limits, read
57496
+ * from the SAME linked control's own range (`min`/`max`/`step`), so the limits
57497
+ * are the source's, never guessed.
57498
+ */
57499
+ function axisNumericTarget(axis, field) {
57500
+ const base = {
57501
+ id: `${axis}-${field}`,
57502
+ cardinality: "scalar",
57503
+ vocabulary: { cap: "fan-control" },
57504
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57505
+ apply: "replace",
57506
+ section: AXES_SECTION,
57507
+ path: fanAxisPath(axis, field),
57508
+ sources: [{
57509
+ kind: "numeric",
57510
+ transforms: [{ kind: "identity" }]
57511
+ }],
57512
+ sourceReport: { status: "assignment-status" }
57513
+ };
57514
+ if (field !== "angle") return base;
57515
+ return {
57516
+ ...base,
57517
+ companions: FAN_OSCILLATION_AXIS_LIMITS.map((limit) => ({
57518
+ kind: "source-field",
57519
+ path: fanAxisPath(axis, limit),
57520
+ field: limit
57521
+ }))
57522
+ };
57523
+ }
57524
+ var AXIS_TARGETS = FAN_OSCILLATION_AXES.flatMap((axis) => [
57525
+ axisOscillatingTarget(axis),
57526
+ axisNumericTarget(axis, "angle"),
57527
+ axisNumericTarget(axis, "sweepFrom"),
57528
+ axisNumericTarget(axis, "sweepTo")
57529
+ ]);
57530
+ /** A select (an enum control) the operator links to the fan's `preset`. */
57531
+ var PRESET_TARGET = {
57532
+ id: "preset",
57533
+ cardinality: "scalar",
57534
+ vocabulary: { cap: "fan-control" },
57535
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57536
+ apply: "replace",
57537
+ path: "preset",
57538
+ sources: [{
57539
+ kind: "field",
57540
+ valueKinds: ["string"],
57541
+ transforms: [{ kind: "identity" }]
57542
+ }],
57543
+ sourceReport: { status: "assignment-status" }
57544
+ };
57545
+ var FAN_CONTROL_TARGETS = [...AXIS_TARGETS, PRESET_TARGET];
57546
+ /**
57547
+ * Customize (D1034): the fan already serves `fan-control` natively, so every
57548
+ * target is a `replace` over a field listed in `overridable` — the axis leaves
57549
+ * and `preset`, nothing else. Nothing is pre-linked: the operator picks each
57550
+ * source (D356).
57551
+ */
57552
+ var fanControlCustomize = {
57553
+ version: 1,
57554
+ overridable: FAN_CONTROL_TARGETS.map((target) => ({
57555
+ path: target.path,
57556
+ by: target.id
57557
+ })),
57558
+ targets: FAN_CONTROL_TARGETS
57559
+ };
56784
57560
  var fanControlCapability = {
56785
57561
  name: "fan-control",
56786
57562
  scope: "device",
@@ -56815,6 +57591,18 @@ var fanControlCapability = {
56815
57591
  }), _void(), {
56816
57592
  kind: "mutation",
56817
57593
  auth: "protected"
57594
+ }),
57595
+ /**
57596
+ * Sets one oscillation axis: its switch, fixed angle and/or sweep bounds.
57597
+ * Fields left out are untouched. A field linked in Customize is forwarded
57598
+ * to its linked source (D1034); otherwise the native provider serves it;
57599
+ * neither → `axis-not-linked`. Ranges are checked against the axis limits
57600
+ * before anything is written; writes run in order and stop at the first
57601
+ * failure, which the result names.
57602
+ */
57603
+ setOscillationAxis: optionalMethod(SetOscillationAxisInputSchema, FanOscillationAxisResultSchema, {
57604
+ kind: "mutation",
57605
+ auth: "protected"
56818
57606
  })
56819
57607
  },
56820
57608
  status: {
@@ -56826,6 +57614,7 @@ var fanControlCapability = {
56826
57614
  * sliders read `speed` for live updates.
56827
57615
  */
56828
57616
  runtimeState: FanControlStatusSchema,
57617
+ customize: fanControlCustomize,
56829
57618
  /**
56830
57619
  * Runtime-state durability: **session** — as `brightness`.
56831
57620
  *
@@ -56845,16 +57634,33 @@ var fanControlCapability = {
56845
57634
  oscillating: { kind: "boolean" }
56846
57635
  } }
56847
57636
  };
57637
+ /** One axis as a preset wants it; an absent field is left as it is. */
57638
+ var FanPresetAxisSchema = object({
57639
+ oscillating: boolean().optional(),
57640
+ angle: number$1().optional(),
57641
+ sweepFrom: number$1().optional(),
57642
+ sweepTo: number$1().optional()
57643
+ });
57644
+ /** The fan-control state a preset sets; an absent field is left as it is. */
57645
+ var FanPresetStateSchema = object({
57646
+ speed: number$1().min(0).max(100).optional(),
57647
+ /** The fan's own preset mode (or the linked select's option). */
57648
+ preset: string().min(1).optional(),
57649
+ direction: FanDirectionSchema.optional(),
57650
+ oscillationAxes: object({
57651
+ horizontal: FanPresetAxisSchema.optional(),
57652
+ vertical: FanPresetAxisSchema.optional()
57653
+ }).optional()
57654
+ });
56848
57655
  var FanPresetSchema = object({
56849
57656
  /** Stable key; never reused after a delete within one device's list. */
56850
57657
  id: string().min(1).max(64),
56851
57658
  /** What the operator and a voice assistant call it. */
56852
57659
  name: string().trim().min(1).max(40),
56853
- steps: array(ActionStepSchema).min(1).max(20)
57660
+ state: FanPresetStateSchema
56854
57661
  });
56855
57662
  var FanPresetsRefusalSchema = _enum([
56856
- "not-sibling",
56857
- "bad-target",
57663
+ "empty-state",
56858
57664
  "duplicate-name",
56859
57665
  "duplicate-id",
56860
57666
  "too-many",
@@ -56866,72 +57672,55 @@ var FanPresetsSetResultSchema = discriminatedUnion("status", [object({
56866
57672
  }), object({
56867
57673
  status: literal("refused"),
56868
57674
  reason: FanPresetsRefusalSchema,
56869
- /** Names the preset and step at fault. */
57675
+ /** Names the preset at fault. */
56870
57676
  detail: string()
56871
57677
  })]);
56872
- var FanPresetStepResultSchema = object({
56873
- index: number$1().int().nonnegative(),
57678
+ var FanPresetFieldResultSchema = object({
57679
+ /** The state path: `speed`, `preset`, `direction`, `oscillationAxes.horizontal.angle`. */
57680
+ field: string(),
56874
57681
  outcome: _enum([
56875
57682
  "applied",
56876
57683
  "failed",
56877
57684
  "not-run",
56878
57685
  "refused"
56879
57686
  ]),
56880
- deviceId: number$1().int().nonnegative().optional(),
56881
- cap: string().optional(),
56882
- method: string().optional(),
56883
57687
  /** The cause, for every outcome but `applied`. */
56884
57688
  message: string().optional()
56885
57689
  });
56886
57690
  var FanPresetApplyResultSchema = object({
56887
- /** `applied`: every step ran. `partial`: some ran, one failed. `refused`: nothing ran. */
57691
+ /** `applied`: every field landed. `partial`: some did, one did not. `refused`: nothing ran. */
56888
57692
  status: _enum([
56889
57693
  "applied",
56890
57694
  "partial",
56891
57695
  "refused"
56892
57696
  ]),
56893
- steps: array(FanPresetStepResultSchema)
57697
+ fields: array(FanPresetFieldResultSchema)
56894
57698
  });
56895
57699
  var FanCaptureFieldSchema = _enum([
56896
57700
  "speed",
56897
- "oscillating",
56898
57701
  "direction",
56899
- "preset"
57702
+ "preset",
57703
+ "oscillationAxes"
56900
57704
  ]);
56901
- var FanCaptureOmissionSchema = object({
56902
- /** A fan field (`speed`) or `step <n>` for a sibling step. */
56903
- what: string(),
56904
- /** Why no value was taken: unknown reading (never stored as 0, D393), or a step that cannot be read. */
56905
- reason: string()
56906
- });
56907
- var FanCaptureItemSchema = object({
56908
- group: _enum([
56909
- "fan",
56910
- "oscillation",
56911
- "angles"
56912
- ]),
56913
- /** What the step sets, in the operator's words (a device name, a fan field). */
56914
- label: string()
56915
- });
56916
57705
  var FanPresetCaptureResultSchema = object({
56917
- /** The steps filled from the device's current values; NOT saved. */
56918
- steps: array(ActionStepSchema),
56919
- /** One entry per step, same order: its review group and label. */
56920
- items: array(FanCaptureItemSchema),
56921
- omitted: array(FanCaptureOmissionSchema)
57706
+ /** The fan's CURRENT fan-control state, as a preset; NOT saved. */
57707
+ state: FanPresetStateSchema,
57708
+ omitted: array(object({
57709
+ /** A state path (`speed`, `oscillationAxes.vertical.angle`). */
57710
+ what: string(),
57711
+ /** Why no value was taken: an unknown reading is never stored as 0 (D393). */
57712
+ reason: string()
57713
+ }))
56922
57714
  });
56923
- var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
57715
+ var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
56924
57716
  DeviceType.Fan;
56925
- method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
57717
+ method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
56926
57718
  kind: "mutation",
56927
57719
  auth: "admin"
56928
- }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
57720
+ }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
56929
57721
  kind: "mutation",
56930
57722
  auth: "protected"
56931
- }), method(deviceInput$2.extend({
56932
- fields: array(FanCaptureFieldSchema).optional(),
56933
- steps: array(ActionStepSchema).max(20).default([])
56934
- }), FanPresetCaptureResultSchema, { auth: "protected" });
57723
+ }), method(deviceInput$1.extend({ fields: array(FanCaptureFieldSchema).optional() }), FanPresetCaptureResultSchema, { auth: "protected" });
56935
57724
  /**
56936
57725
  * Per-device feature/identity probe slice. Holds the runtime-resolved
56937
57726
  * truth about what a device CAN do — which the kernel uses to:
@@ -57143,8 +57932,7 @@ var groupActiveCapability = {
57143
57932
  DeviceType.Valve,
57144
57933
  DeviceType.Lock,
57145
57934
  DeviceType.MediaPlayer,
57146
- DeviceType.Climate,
57147
- DeviceType.Thermostat
57935
+ DeviceType.Climate
57148
57936
  ],
57149
57937
  methods: {},
57150
57938
  status: {
@@ -57186,7 +57974,6 @@ var groupCommandsCapability = {
57186
57974
  DeviceType.Valve,
57187
57975
  DeviceType.Lock,
57188
57976
  DeviceType.MediaPlayer,
57189
- DeviceType.Thermostat,
57190
57977
  DeviceType.Climate
57191
57978
  ],
57192
57979
  methods: {},
@@ -57242,7 +58029,6 @@ var groupStatusCapability = {
57242
58029
  DeviceType.Valve,
57243
58030
  DeviceType.Lock,
57244
58031
  DeviceType.MediaPlayer,
57245
- DeviceType.Thermostat,
57246
58032
  DeviceType.Climate
57247
58033
  ],
57248
58034
  methods: {},
@@ -57571,7 +58357,7 @@ var heatingValveCapability = {
57571
58357
  scope: "device",
57572
58358
  deviceNative: true,
57573
58359
  mode: "singleton",
57574
- deviceTypes: [DeviceType.Thermostat],
58360
+ deviceTypes: [DeviceType.Climate],
57575
58361
  methods: {},
57576
58362
  status: {
57577
58363
  schema: HeatingValveStatusSchema,
@@ -57601,13 +58387,18 @@ var heatingValveCapability = {
57601
58387
  * retried in a loop - D49); a future one is re-armed.
57602
58388
  *
57603
58389
  * What a device may be run for, and for how long at most, is per device type
57604
- * ({@link TIMED_RUN_LIMITS}): the maximum is a safety bound, not a preference,
58390
+ * and role ({@link timedRunLimitOf}): the maximum is a safety bound, not a preference,
57605
58391
  * so a request above it is REFUSED with a typed reason, never clamped.
57606
58392
  */
57607
58393
  var TimedRunUnitSchema = _enum(["s", "min"]);
57608
58394
  _enum(["switch", "pet-fountain"]);
57609
- DeviceType.Heater, DeviceType.PetFountain;
57610
- var TIMED_RUN_DEVICE_TYPES = [DeviceType.Heater, DeviceType.PetFountain];
58395
+ /**
58396
+ * The device types `timed-run` may be offered on. A `switch` is narrowed by
58397
+ * role ({@link TIMED_RUN_DEVICE_ROLES}): only a plug-in heater runs on a timer.
58398
+ */
58399
+ var TIMED_RUN_DEVICE_TYPES = [DeviceType.Switch, DeviceType.PetFountain];
58400
+ /** The roles that make a device of these types timed-run-able (D1033). */
58401
+ var TIMED_RUN_DEVICE_ROLES = { [DeviceType.Switch]: [DeviceRole.SwitchHeater] };
57611
58402
  /** One duration, as the one-tap run asks for it. */
57612
58403
  var TimedRunQuickRunSchema = object({
57613
58404
  duration: number$1().positive().finite(),
@@ -57657,6 +58448,7 @@ var timedRunCapability = {
57657
58448
  kind: "wrapper",
57658
58449
  defaultActive: true,
57659
58450
  deviceTypes: TIMED_RUN_DEVICE_TYPES,
58451
+ deviceRoles: TIMED_RUN_DEVICE_ROLES,
57660
58452
  deviceConfig: { ui: {
57661
58453
  kind: "derived-form",
57662
58454
  builderId: "timed-run",
@@ -57817,8 +58609,8 @@ var HeroSceneSchema = object({
57817
58609
  /** 0..1: volume, cover position, cycle progress. */
57818
58610
  level: number$1().min(0).max(1).nullable()
57819
58611
  });
57820
- 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.HeatPump, 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.Heater, DeviceType.Dryer, DeviceType.Gateway;
57821
- DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
58612
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
58613
+ DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.SwitchHeater, DeviceRole.SwitchFan, DeviceRole.ClimateAc, DeviceRole.ClimateThermostat, DeviceRole.ClimateFloor, DeviceRole.ClimateRadiator, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
57822
58614
  /** A string in both languages, as the cap vocabulary writes it (D920): no `t()` in the model. */
57823
58615
  var HeroTextSchema = object({
57824
58616
  en: string(),
@@ -57884,13 +58676,11 @@ var HeroConfirmSchema = _enum([
57884
58676
  "press"
57885
58677
  ]);
57886
58678
  var HeroActionSchema = object({
57887
- /** Unique within the model; the `gated` list and the specs name it. */
57888
58679
  id: string(),
57889
58680
  label: HeroTextSchema,
57890
58681
  icon: IconKeySchema,
57891
58682
  call: HeroCallSchema,
57892
58683
  emphasis: _enum(["primary", "normal"]),
57893
- /** Ask before running it. */
57894
58684
  confirm: HeroConfirmSchema.nullable()
57895
58685
  });
57896
58686
  var HeroControlSchema = discriminatedUnion("kind", [
@@ -57973,9 +58763,16 @@ var HeroBadgeSchema = object({
57973
58763
  value: number$1().nullable(),
57974
58764
  label: HeroTextSchema
57975
58765
  });
57976
- var HeroRemoteSlotSchema = object({
57977
- /** Saved commands; `null` = not known yet (the button still shows). */
57978
- commands: number$1().int().nullable() });
58766
+ var HeroRemoteSlotSchema = object({
58767
+ /** Saved commands; `null` = not known yet (the button still shows). */
58768
+ commands: number$1().int().nullable(),
58769
+ /**
58770
+ * The control a list row draws as the "open the remote" icon. Recognised by its id
58771
+ * ({@link REMOTE_OPEN_ACTION_ID}); it changes no state, so a client opens the remote
58772
+ * instead of running `call`. Absent on a model from an older server.
58773
+ */
58774
+ open: HeroActionSchema.optional()
58775
+ });
57979
58776
  var HeroConsumablesSlotSchema = object({
57980
58777
  items: array(object({
57981
58778
  key: string(),
@@ -58143,6 +58940,8 @@ var HeroModelSchema = object({
58143
58940
  * the FIRST row that has any (a later row's control is never promoted). When
58144
58941
  * the model lists that part in `gated`, the row has no inline control: the
58145
58942
  * projection never falls through to another control the caller could use.
58943
+ * - a climate or thermostat row's primary is its `climate.mode` control (no power
58944
+ * toggle: "off" is a mode); when the model gates it the rule above applies as before;
58146
58945
  * - a timed run, when the model carries the slot, sits beside that primary:
58147
58946
  * Run while idle, Stop while armed. Gating the slot hides the control.
58148
58947
  * - the status line is the head (what plays, else the state) plus at most
@@ -58150,6 +58949,13 @@ var HeroModelSchema = object({
58150
58949
  * time left on an armed timed run;
58151
58950
  * - the trailing fact is the first of power, battery, consumables.
58152
58951
  *
58952
+ * - `extras` are the further inline controls of the types that carry more than one
58953
+ * ({@link ROW_EXTRAS_BY_TYPE}): climate setpoint, a robot's return to base,
58954
+ * a player's volume and remote. Each is a control the model already has, named by id,
58955
+ * in that order and at most {@link ROW_EXTRAS_MAX}; one the model lacks, or lists in
58956
+ * `gated`, is absent, and the one `primary` already is never repeats. Empty = the
58957
+ * row of an older server: `primary` alone.
58958
+ *
58153
58959
  * `projectDeviceRow` is pure and computes nothing: it selects from a
58154
58960
  * {@link HeroModel}. A row that needed a value the hero lacks would mean the
58155
58961
  * model is missing it, so the fix goes in the model.
@@ -58181,8 +58987,16 @@ var HeroRowModelSchema = object({
58181
58987
  * while armed. `null` when there is no slot, or the caller may not act.
58182
58988
  */
58183
58989
  timedRun: HeroRowTimedRunSchema.nullable(),
58990
+ /**
58991
+ * The further inline controls after `primary`, in order (see the header). The remote
58992
+ * is an `action` with id `remote.open`: the client opens the remote, it does not run
58993
+ * the call. Absent or empty = `primary` alone.
58994
+ */
58995
+ extras: array(HeroControlSchema).max(2).optional(),
58184
58996
  trailing: HeroFactSchema.nullable()
58185
58997
  });
58998
+ DeviceType.Climate;
58999
+ DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
58186
59000
  /**
58187
59001
  * hero-layout — what a device's hero says (D1023). Read-only: the FIXED
58188
59002
  * layouts live in `@camstack/types` and nothing here stores or edits one.
@@ -58214,7 +59028,10 @@ method(heroInput, HeroModelSchema.nullable(), {
58214
59028
  }), method(rowModelsInput, array(HeroRowModelSchema.nullable()), {
58215
59029
  auth: "protected",
58216
59030
  role: "viewer"
58217
- }), method(object({ deviceType: string() }), HeroLayoutRowsSchema, {
59031
+ }), method(object({
59032
+ deviceType: string(),
59033
+ role: string().nullable().optional()
59034
+ }), HeroLayoutRowsSchema, {
58218
59035
  auth: "protected",
58219
59036
  role: "viewer"
58220
59037
  });
@@ -58261,6 +59078,11 @@ var humidifierCapability = {
58261
59078
  deviceNative: true,
58262
59079
  mode: "singleton",
58263
59080
  deviceTypes: [DeviceType.Humidifier],
59081
+ power: {
59082
+ kind: "field",
59083
+ field: "on",
59084
+ set: "setOn"
59085
+ },
58264
59086
  methods: {
58265
59087
  setOn: method(object({
58266
59088
  deviceId: number$1().int().nonnegative(),
@@ -58334,7 +59156,7 @@ var humiditySensorCapability = {
58334
59156
  scope: "device",
58335
59157
  deviceNative: true,
58336
59158
  mode: "singleton",
58337
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
59159
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
58338
59160
  methods: {},
58339
59161
  status: {
58340
59162
  schema: HumiditySensorStatusSchema,
@@ -58628,6 +59450,12 @@ var IntegrationLiteSchema = object({
58628
59450
  var AvailableIntegrationTypeSchema = object({
58629
59451
  addonId: string(),
58630
59452
  name: string(),
59453
+ /**
59454
+ * Product name a new integration is prefilled from (`<label> - Home`).
59455
+ * Absent: the client uses `name`. Set when the catalog title is not the
59456
+ * product (Bosch SHC, MQTT).
59457
+ */
59458
+ providerLabel: string().optional(),
58631
59459
  description: string(),
58632
59460
  iconUrl: string().nullable(),
58633
59461
  color: string(),
@@ -71516,7 +72344,18 @@ var OauthIntegrationDescriptorSchema = object({
71516
72344
  * The value is baked into the authorization code at `/authorize` and travels
71517
72345
  * on the tokens, so editing this field changes FUTURE links only.
71518
72346
  */
71519
- refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional()
72347
+ refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional(),
72348
+ /**
72349
+ * The integration instances an account can be linked to (planning#415),
72350
+ * beside the default one. `/api/oauth2/authorize?instance=<id>` is accepted
72351
+ * only for an id listed here; with no `instance=` the link is made to the
72352
+ * default instance, which is never listed and always accepted — it is the
72353
+ * address third parties already hold. Omit for a single-instance integration.
72354
+ */
72355
+ instances: array(object({
72356
+ id: string(),
72357
+ displayName: string()
72358
+ })).optional()
71520
72359
  });
71521
72360
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
71522
72361
  /**
@@ -71939,9 +72778,13 @@ var petFeederCapability = {
71939
72778
  */
71940
72779
  durability: "session",
71941
72780
  /**
71942
- * History (D850): recorded series.
72781
+ * History (D850): recorded series. `feeding` / `eating` are the meals the
72782
+ * device hero's "Meals" button names — change-only booleans, so a dispense
72783
+ * or a pet at the bowl is one span on the history.
71943
72784
  */
71944
72785
  history: { fields: {
72786
+ feeding: { kind: "boolean" },
72787
+ eating: { kind: "boolean" },
71945
72788
  foodLevel: { kind: "gauge" },
71946
72789
  bowlFood: { kind: "gauge" },
71947
72790
  food1: { kind: "gauge" },
@@ -72002,6 +72845,11 @@ var petFountainCapability = {
72002
72845
  deviceNative: true,
72003
72846
  mode: "singleton",
72004
72847
  deviceTypes: [DeviceType.PetFountain],
72848
+ power: {
72849
+ kind: "field",
72850
+ field: "pumpRunning",
72851
+ set: "setPump"
72852
+ },
72005
72853
  methods: {
72006
72854
  /** Start or stop the pump. */
72007
72855
  setPump: method(object({
@@ -73577,7 +74425,6 @@ var POWER_METER_DEVICE_TYPES = [
73577
74425
  DeviceType.Light,
73578
74426
  DeviceType.Fan,
73579
74427
  DeviceType.Climate,
73580
- DeviceType.Thermostat,
73581
74428
  DeviceType.Humidifier,
73582
74429
  DeviceType.WaterHeater,
73583
74430
  DeviceType.MediaPlayer,
@@ -73586,8 +74433,7 @@ var POWER_METER_DEVICE_TYPES = [
73586
74433
  DeviceType.Dishwasher,
73587
74434
  DeviceType.Vacuum,
73588
74435
  DeviceType.LawnMower,
73589
- DeviceType.PetFeeder,
73590
- DeviceType.Heater
74436
+ DeviceType.PetFeeder
73591
74437
  ];
73592
74438
  /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
73593
74439
  var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
@@ -73700,118 +74546,6 @@ var powerMeterCapability = {
73700
74546
  volatileStateFields: ["lastFetchedAt"],
73701
74547
  customize: powerMeterCustomize
73702
74548
  };
73703
- var PowerSourceRefusalReasonSchema = _enum([
73704
- "no-power-source",
73705
- "switch-missing",
73706
- "switch-offline",
73707
- "switch-unknown",
73708
- "restart-in-progress",
73709
- "switch-call-failed"
73710
- ]);
73711
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
73712
- accepted: literal(false),
73713
- reason: PowerSourceRefusalReasonSchema,
73714
- /** One line for the operator, naming the switch. */
73715
- message: string()
73716
- })]);
73717
- var PowerSourceInfoSchema = object({
73718
- /** False when the device has no power source linked; the rest is then empty. */
73719
- linked: boolean(),
73720
- switchDeviceId: number$1().nullable(),
73721
- switchName: string().nullable(),
73722
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
73723
- switchOn: boolean().nullable(),
73724
- switchOnline: boolean().nullable(),
73725
- restartInProgress: boolean(),
73726
- offSeconds: number$1().int()
73727
- });
73728
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
73729
- var PowerSourceStateSchema = object({
73730
- /** Null while the switch has not reported: unknown is never `false` (D393). */
73731
- switchOn: boolean().nullable(),
73732
- offSeconds: number$1().int(),
73733
- lastFetchedAt: number$1()
73734
- });
73735
- var POWER_SOURCE_DEVICE_TYPES = [
73736
- DeviceType.Gateway,
73737
- DeviceType.Hub,
73738
- DeviceType.Camera,
73739
- DeviceType.Sensor,
73740
- DeviceType.MediaPlayer,
73741
- DeviceType.Vacuum,
73742
- DeviceType.Generic,
73743
- DeviceType.Washer,
73744
- DeviceType.Dryer,
73745
- DeviceType.Dishwasher,
73746
- DeviceType.PetFountain,
73747
- DeviceType.Fan,
73748
- DeviceType.Humidifier,
73749
- DeviceType.Climate,
73750
- DeviceType.WaterHeater
73751
- ];
73752
- var powerSourceCustomize = {
73753
- version: 1,
73754
- targets: [{
73755
- id: "switch",
73756
- cardinality: "scalar",
73757
- vocabulary: {
73758
- cap: "power-source",
73759
- stateField: "switchOn"
73760
- },
73761
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
73762
- apply: "add",
73763
- path: "switchOn",
73764
- sources: [{
73765
- kind: "switch",
73766
- transforms: [{ kind: "identity" }]
73767
- }],
73768
- companions: [{
73769
- kind: "constant",
73770
- path: "offSeconds",
73771
- value: 10,
73772
- when: "add",
73773
- formerValues: [0]
73774
- }, {
73775
- kind: "clock",
73776
- path: "lastFetchedAt",
73777
- when: "always"
73778
- }],
73779
- sourceReport: { status: "assignment-status" }
73780
- }]
73781
- };
73782
- var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
73783
- var powerSourceCapability = {
73784
- name: "power-source",
73785
- scope: "device",
73786
- mode: "singleton",
73787
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
73788
- methods: {
73789
- getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "protected" }),
73790
- powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73791
- kind: "mutation",
73792
- auth: "protected"
73793
- }),
73794
- powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73795
- kind: "mutation",
73796
- auth: "protected"
73797
- }),
73798
- forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73799
- kind: "mutation",
73800
- auth: "protected"
73801
- })
73802
- },
73803
- runtimeState: PowerSourceStateSchema,
73804
- customize: powerSourceCustomize,
73805
- /**
73806
- * Runtime-state durability: **session** — mirrored from the linked switch on
73807
- * its next report; a restored value would outlive the reading it names.
73808
- *
73809
- * See `RuntimeStateDurability`. Enforced by
73810
- * `scripts/check-runtime-state-durability.ts`.
73811
- */
73812
- durability: "session",
73813
- history: "none"
73814
- };
73815
74549
  /**
73816
74550
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
73817
74551
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -77278,9 +78012,7 @@ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
77278
78012
  DeviceType.Humidifier,
77279
78013
  DeviceType.Fan,
77280
78014
  DeviceType.Climate,
77281
- DeviceType.Thermostat,
77282
- DeviceType.PetFeeder,
77283
- DeviceType.Heater
78015
+ DeviceType.PetFeeder
77284
78016
  ];
77285
78017
  var runningStateCapability = {
77286
78018
  name: "running-state",
@@ -78552,6 +79284,13 @@ var SsoBridgeClaimsSchema = object({
78552
79284
  /** OAuth authorization-code binding — set only on `oauth-code` tokens. */
78553
79285
  redirectUri: string().optional(),
78554
79286
  integrationId: string().optional(),
79287
+ /**
79288
+ * The integration INSTANCE an OAuth link was made for (planning#415). Set on
79289
+ * `oauth-code`, `oauth-access` and `oauth-refresh` tokens of a link made
79290
+ * with `instance=`; absent on a link to the default instance. A cloud relay
79291
+ * reads it off the access token to route; the hub answers from the session.
79292
+ */
79293
+ instanceId: string().optional(),
78555
79294
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
78556
79295
  jti: string().optional(),
78557
79296
  /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
@@ -79368,8 +80107,7 @@ var switchCapability = {
79368
80107
  DeviceType.Switch,
79369
80108
  DeviceType.Siren,
79370
80109
  DeviceType.Light,
79371
- DeviceType.Fan,
79372
- DeviceType.Heater
80110
+ DeviceType.Fan
79373
80111
  ],
79374
80112
  methods: { setState: method(object({
79375
80113
  deviceId: number$1(),
@@ -80063,7 +80801,7 @@ var temperatureSensorCapability = {
80063
80801
  scope: "device",
80064
80802
  deviceNative: true,
80065
80803
  mode: "singleton",
80066
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
80804
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
80067
80805
  methods: {},
80068
80806
  status: {
80069
80807
  schema: TemperatureSensorStatusSchema,
@@ -80931,6 +81669,8 @@ var OauthSessionSummarySchema = object({
80931
81669
  userId: string(),
80932
81670
  username: string(),
80933
81671
  integrationId: string(),
81672
+ /** The instance the link was made for; `null` = the default instance. */
81673
+ instanceId: string().nullable().optional(),
80934
81674
  scopes: array(TokenScopeSchema),
80935
81675
  createdAt: number$1(),
80936
81676
  lastUsedAt: number$1(),
@@ -81030,6 +81770,9 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
81030
81770
  access: "view"
81031
81771
  }), method(object({
81032
81772
  integrationId: string(),
81773
+ /** The integration instance the link is for. Omit for the default
81774
+ * instance — never pass the addon id to mean it. */
81775
+ instanceId: string().optional(),
81033
81776
  userId: string(),
81034
81777
  username: string(),
81035
81778
  scopes: array(TokenScopeSchema),
@@ -81068,7 +81811,13 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
81068
81811
  }), method(object({ token: string() }), object({
81069
81812
  userId: string(),
81070
81813
  username: string(),
81071
- scopes: array(TokenScopeSchema)
81814
+ scopes: array(TokenScopeSchema),
81815
+ /** The instance the link was made for, read from the SESSION.
81816
+ * Absent = the default instance. `oauthLinkServesInstance` is the
81817
+ * helper a per-instance route calls to compare it with the instance
81818
+ * of its path; no route declares `perInstance` yet (the Alexa
81819
+ * per-instance route, T3, is the first consumer). */
81820
+ instanceId: string().optional()
81072
81821
  }).nullable(), { access: "view" }), method(object({ userId: string() }), array(ExternalIdentitySchema), { auth: "admin" }), method(ExternalIdentityKeySchema, object({
81073
81822
  userId: string(),
81074
81823
  issuer: string()
@@ -81710,6 +82459,13 @@ var waterHeaterCapability = {
81710
82459
  deviceNative: true,
81711
82460
  mode: "singleton",
81712
82461
  deviceTypes: [DeviceType.WaterHeater],
82462
+ power: {
82463
+ kind: "mode",
82464
+ field: "operationMode",
82465
+ offValue: "off",
82466
+ set: "setOperationMode",
82467
+ lastField: "lastActiveMode"
82468
+ },
81713
82469
  methods: {
81714
82470
  setTargetTemp: method(object({
81715
82471
  deviceId: number$1().int().nonnegative(),
@@ -83368,7 +84124,6 @@ var DEVICE_LOCAL_STATE_CAPS = {
83368
84124
  control: controlCapability,
83369
84125
  cover: coverCapability,
83370
84126
  dayNight: dayNightCapability,
83371
- deviceDiscovery: deviceDiscoveryCapability,
83372
84127
  deviceStatus: deviceStatusCapability,
83373
84128
  dishwasher: dishwasherCapability,
83374
84129
  doNotDisturb: doNotDisturbCapability,
@@ -84463,15 +85218,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84463
85218
  });
84464
85219
  const meta = await this.ctx.api.deviceManager.loadMeta.query({ deviceId: id });
84465
85220
  if (meta === null) throw new Error(`${this.providerName}: no persisted meta for "${stableId}" (id ${id}) — cannot reload`);
84466
- const deviceType = Object.values(DeviceType).find((t) => t === meta.type);
84467
- const Class = deviceType !== void 0 ? this.deviceClasses[deviceType] : void 0;
85221
+ const Class = this.deviceClassForRow({
85222
+ type: meta.type,
85223
+ stableId
85224
+ });
84468
85225
  if (!Class) throw new Error(`${this.providerName}: no device class for type "${meta.type}" — "${stableId}" is an accessory child; reload its parent instead`);
84469
85226
  await devices.create(stableId, Class, {}, meta.parentDeviceId ?? null);
84470
85227
  const rows = await this.ctx.api.deviceManager.listPersistedByAddon.query({ addonId: this.addonId });
84471
85228
  for (const row of rows) {
84472
85229
  if (row.parentDeviceId !== id) continue;
84473
- const childType = Object.values(DeviceType).find((t) => t === row.type);
84474
- const ChildClass = childType !== void 0 ? this.deviceClasses[childType] : void 0;
85230
+ const ChildClass = this.deviceClassForRow(row);
84475
85231
  if (!ChildClass) continue;
84476
85232
  try {
84477
85233
  await devices.create(row.stableId, ChildClass, {}, id);
@@ -84498,6 +85254,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84498
85254
  return { deviceId: id };
84499
85255
  }
84500
85256
  /**
85257
+ * The class a PERSISTED row is rebuilt with. Default: by type, from
85258
+ * `deviceClasses`. A provider whose distinct devices share one type (D1033:
85259
+ * a radiator valve and a room climate are both `climate`) overrides this
85260
+ * and tells them apart by the row's stable id or config.
85261
+ */
85262
+ deviceClassForRow(row) {
85263
+ const type = Object.values(DeviceType).find((t) => t === row.type);
85264
+ return type !== void 0 ? this.deviceClasses[type] : void 0;
85265
+ }
85266
+ /**
84501
85267
  * Restore devices from persisted state. Two-pass:
84502
85268
  *
84503
85269
  * 1. **Top-level pass** — invokes `kernel.devices.create()` for every
@@ -84558,7 +85324,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84558
85324
  restored.add(saved.id);
84559
85325
  return;
84560
85326
  }
84561
- const Class = this.deviceClasses[saved.type];
85327
+ const Class = this.deviceClassForRow(saved);
84562
85328
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
84563
85329
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
84564
85330
  await this.healSavedConfig(saved, savedDevices);
@@ -84575,7 +85341,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84575
85341
  };
84576
85342
  const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
84577
85343
  const restoreOne = async (saved) => {
84578
- if (!this.deviceClasses[saved.type]) {
85344
+ if (!this.deviceClassForRow(saved)) {
84579
85345
  this.ctx.logger.warn("No device class registered for restored type — skipping", {
84580
85346
  tags: {
84581
85347
  deviceId: saved.id,
@@ -84614,7 +85380,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84614
85380
  const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
84615
85381
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
84616
85382
  for (const saved of childRows) {
84617
- if (!this.deviceClasses[saved.type]) continue;
85383
+ if (!this.deviceClassForRow(saved)) continue;
84618
85384
  if (saved.parentDeviceId === null) continue;
84619
85385
  if (restored.has(saved.parentDeviceId)) {
84620
85386
  try {
@@ -84668,7 +85434,9 @@ var BaseDeviceProvider = class extends BaseAddon {
84668
85434
  }
84669
85435
  };
84670
85436
  DeviceType.HeatPump;
84671
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, 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.Heater, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
85437
+ DeviceType.Climate;
85438
+ DeviceRole.ClimateThermostat, DeviceRole.ClimateAc, DeviceRole.ClimateRadiator, DeviceRole.ClimateFloor, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit;
85439
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, 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;
84672
85440
  new Set(Object.values(DeviceType));
84673
85441
  /** Parses the raw slice with the cap's own schema; `undefined` when it does not parse. */
84674
85442
  function contribution(cap, schema, build) {
@@ -86674,7 +87442,7 @@ var HEAT_PUMP_FAMILY_CONTRIBUTIONS = { "heat-pump-status": contribution("heat-pu
86674
87442
  ...Object.values(CLIMATE_FAMILY_CONTRIBUTIONS),
86675
87443
  ...Object.values(HEAT_PUMP_FAMILY_CONTRIBUTIONS)
86676
87444
  ];
86677
- 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.HeatPump, 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.Heater, DeviceType.Dryer, DeviceType.Gateway;
87445
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
86678
87446
  /**
86679
87447
  * Who looks like what (D1023). One look per {@link DeviceType}, refined by a
86680
87448
  * {@link DeviceRole} where the role changes how the device reads (a leak
@@ -87318,9 +88086,9 @@ lookOf({
87318
88086
  idle: INFO,
87319
88087
  standby: MUTED
87320
88088
  });
87321
- DeviceType.Climate, DeviceType.Thermostat;
87322
- 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.HeatPump, 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.Heater, DeviceType.Dryer, DeviceType.Gateway;
87323
- DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
88089
+ DeviceType.Climate;
88090
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
88091
+ DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.SwitchHeater, DeviceRole.SwitchFan, DeviceRole.ClimateAc, DeviceRole.ClimateThermostat, DeviceRole.ClimateFloor, DeviceRole.ClimateRadiator, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
87324
88092
  Object.freeze({
87325
88093
  "accessories.getStatus": {
87326
88094
  capName: "accessories",
@@ -89170,41 +89938,6 @@ Object.freeze({
89170
89938
  access: "create",
89171
89939
  role: "admin"
89172
89940
  },
89173
- "deviceDiscovery.adoptDevice": {
89174
- capName: "device-discovery",
89175
- capScope: "device",
89176
- addonId: null,
89177
- access: "create",
89178
- role: "admin"
89179
- },
89180
- "deviceDiscovery.getStatus": {
89181
- capName: "device-discovery",
89182
- capScope: "device",
89183
- addonId: null,
89184
- access: "view",
89185
- role: "viewer"
89186
- },
89187
- "deviceDiscovery.listDiscovered": {
89188
- capName: "device-discovery",
89189
- capScope: "device",
89190
- addonId: null,
89191
- access: "view",
89192
- role: "viewer"
89193
- },
89194
- "deviceDiscovery.refreshDiscovery": {
89195
- capName: "device-discovery",
89196
- capScope: "device",
89197
- addonId: null,
89198
- access: "create",
89199
- role: "admin"
89200
- },
89201
- "deviceDiscovery.releaseDevice": {
89202
- capName: "device-discovery",
89203
- capScope: "device",
89204
- addonId: null,
89205
- access: "create",
89206
- role: "admin"
89207
- },
89208
89941
  "deviceExport.applyDeviceSettingsPatch": {
89209
89942
  capName: "device-export",
89210
89943
  capScope: "system",
@@ -90752,6 +91485,13 @@ Object.freeze({
90752
91485
  access: "create",
90753
91486
  role: "viewer"
90754
91487
  },
91488
+ "fanControl.setOscillationAxis": {
91489
+ capName: "fan-control",
91490
+ capScope: "device",
91491
+ addonId: null,
91492
+ access: "create",
91493
+ role: "viewer"
91494
+ },
90755
91495
  "fanControl.setPercentage": {
90756
91496
  capName: "fan-control",
90757
91497
  capScope: "device",
@@ -97773,7 +98513,7 @@ function projectAction(cap, action) {
97773
98513
  };
97774
98514
  }
97775
98515
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
97776
- 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.HeatPump, 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.Heater, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
98516
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
97777
98517
  DeviceFeature.BatteryOperated;
97778
98518
  function bound(cap, methods = [], authority = "reported") {
97779
98519
  return {
@@ -98262,35 +99002,6 @@ Object.freeze({
98262
99002
  form: "single",
98263
99003
  optional: false
98264
99004
  }],
98265
- "deviceDiscovery.adoptDevice": [{
98266
- name: "deviceId",
98267
- form: "single",
98268
- optional: false
98269
- }],
98270
- "deviceDiscovery.getStatus": [{
98271
- name: "deviceId",
98272
- form: "single",
98273
- optional: false
98274
- }],
98275
- "deviceDiscovery.listDiscovered": [{
98276
- name: "deviceId",
98277
- form: "single",
98278
- optional: false
98279
- }],
98280
- "deviceDiscovery.refreshDiscovery": [{
98281
- name: "deviceId",
98282
- form: "single",
98283
- optional: false
98284
- }],
98285
- "deviceDiscovery.releaseDevice": [{
98286
- name: "childDeviceId",
98287
- form: "single",
98288
- optional: false
98289
- }, {
98290
- name: "deviceId",
98291
- form: "single",
98292
- optional: false
98293
- }],
98294
99005
  "deviceExport.applyDeviceSettingsPatch": [{
98295
99006
  name: "deviceId",
98296
99007
  form: "single",
@@ -98854,6 +99565,11 @@ Object.freeze({
98854
99565
  form: "single",
98855
99566
  optional: false
98856
99567
  }],
99568
+ "fanControl.setOscillationAxis": [{
99569
+ name: "deviceId",
99570
+ form: "single",
99571
+ optional: false
99572
+ }],
98857
99573
  "fanControl.setPercentage": [{
98858
99574
  name: "deviceId",
98859
99575
  form: "single",
@@ -102328,6 +103044,27 @@ function ingestReading(input) {
102328
103044
  value: converted
102329
103045
  };
102330
103046
  }
103047
+ function collapse(value) {
103048
+ return value.trim().replace(/\s+/g, " ");
103049
+ }
103050
+ /** "KLF-200" → "KLF200". A hyphen between words ("Home Hub") stays. */
103051
+ function compactModelToken(value) {
103052
+ return value.replace(/([A-Za-z])-(\d)/g, "$1$2");
103053
+ }
103054
+ /**
103055
+ * A Gateway device: "<Brand> <Model>". An unknown model is the brand alone
103056
+ * (the caller passes the brand-level fallback it already has, e.g. "Velux KLF-200").
103057
+ */
103058
+ function gatewayDeviceName(brand, model) {
103059
+ const brandPart = compactModelToken(collapse(brand));
103060
+ const raw = collapse(model ?? "");
103061
+ if (raw.length === 0) return brandPart;
103062
+ const modelPart = compactModelToken(raw);
103063
+ const prefix = `${brandPart.toLowerCase()} `;
103064
+ if (modelPart.toLowerCase() === brandPart.toLowerCase()) return brandPart;
103065
+ if (modelPart.toLowerCase().startsWith(prefix)) return modelPart;
103066
+ return `${brandPart} ${modelPart}`;
103067
+ }
102331
103068
  /**
102332
103069
  * Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
102333
103070
  * event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
@@ -103714,7 +104451,7 @@ function buildEnpalCandidates(input) {
103714
104451
  const boxChildren = box === void 0 ? [] : reportedChildren(box, snapshot);
103715
104452
  const out = [{
103716
104453
  childNativeId: "box",
103717
- name: input.box.name,
104454
+ name: gatewayDeviceName("Enpal", "Box"),
103718
104455
  type: DeviceType.Gateway,
103719
104456
  status: input.box.online ? "online" : "offline",
103720
104457
  metadata: METADATA,
@@ -103838,7 +104575,7 @@ function buildEnpalAdoptionProvider(deps) {
103838
104575
  const specIds = reportedSpecIds(equipment, candidate);
103839
104576
  const { deviceId, accessoryDeviceIds } = await deps.adoptGateway({
103840
104577
  integrationId,
103841
- name: perCandidate?.[equipmentId]?.name ?? boxName(integrationId),
104578
+ name: perCandidate?.[equipmentId]?.name ?? gatewayDeviceName("Enpal", "Box"),
103842
104579
  specIds,
103843
104580
  childLayout: childLayoutOf(equipment, specIds)
103844
104581
  });