@camstack/addon-provider-bosch 0.1.51 → 0.1.52

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.
@@ -802,7 +802,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
802
802
  return EventCategory;
803
803
  }({});
804
804
  //#endregion
805
- //#region ../types/dist/composed-stable-id-DFNyOFzz.mjs
805
+ //#region ../types/dist/composed-stable-id-CPp3votu.mjs
806
806
  /**
807
807
  import { errMsg } from '@camstack/types'
808
808
  * Extract a human-readable message from an unknown error value.
@@ -821,12 +821,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
821
821
  DeviceType["Siren"] = "siren";
822
822
  DeviceType["Switch"] = "switch";
823
823
  DeviceType["Sensor"] = "sensor";
824
- DeviceType["Thermostat"] = "thermostat";
825
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
826
- * `climate-control` cap surface with `Thermostat` but renders a
827
- * dedicated AC-appropriate control UI (mode chips, fan speed,
828
- * independent vertical/horizontal swing). Sources: native Gree, and
829
- * reusable by other AC integrations. */
824
+ /** Every climate device: an air conditioner, a room thermostat, a radiator
825
+ * valve, a floor-heating zone, a heat-pump circuit. They share one set of
826
+ * controls (`climate-control`), so they are one type; WHICH kind of climate
827
+ * device it is is its {@link DeviceRole} (`climate-*`), which the UI and the
828
+ * exports branch on. The retired `thermostat` type is read as this one
829
+ * (`canonicalStoredDeviceType`). */
830
830
  DeviceType["Climate"] = "climate";
831
831
  DeviceType["Button"] = "button";
832
832
  /** Generic stateless event emitter — carries a device's EXACT declared
@@ -917,10 +917,6 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
917
917
  /** Washing machine. A plug-built one is running while its power stays
918
918
  * above a threshold. No program cap — that surface is the dishwasher's. */
919
919
  DeviceType["Washer"] = "washer";
920
- /** A dumb plug-in stove or heater: a switch (and, with a meter, a power
921
- * reading) that only runs while the plug is on. Built from a smart plug;
922
- * the `timed-run` cap switches it off again after a chosen time. */
923
- DeviceType["Heater"] = "heater";
924
920
  /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
925
921
  DeviceType["Dryer"] = "dryer";
926
922
  /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
@@ -1140,6 +1136,27 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1140
1136
  /** A smart plug: a `switch` whose source says it is an outlet (HA
1141
1137
  * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1142
1138
  DeviceRole["Outlet"] = "outlet";
1139
+ /** A dumb plug-in stove or heater behind a switch: it runs only while the
1140
+ * switch is on. Offered `timed-run`, which switches it off again after a
1141
+ * chosen time. */
1142
+ DeviceRole["SwitchHeater"] = "switch-heater";
1143
+ /** A plain fan behind a switch (a smart plug powering it): drawn as a fan,
1144
+ * with the switch's on/off only. Speed or oscillation appear only when the
1145
+ * device also has a fan cap (D1033). */
1146
+ DeviceRole["SwitchFan"] = "switch-fan";
1147
+ /** Air conditioner / split unit: cooling, fan speed, louvres. */
1148
+ DeviceRole["ClimateAc"] = "climate-ac";
1149
+ /** Room thermostat: a setpoint and a room temperature. The default. */
1150
+ DeviceRole["ClimateThermostat"] = "climate-thermostat";
1151
+ /** Floor-heating zone actuator. */
1152
+ DeviceRole["ClimateFloor"] = "climate-floor";
1153
+ /** Radiator valve (thermostatic radiator head). */
1154
+ DeviceRole["ClimateRadiator"] = "climate-radiator";
1155
+ /** A heater with its own thermostat (stove, convector). */
1156
+ DeviceRole["ClimateHeater"] = "climate-heater";
1157
+ /** One heating/cooling circuit of a heat pump: a water loop with comfort /
1158
+ * eco setpoints and a heat/cool mode, no room sensor. */
1159
+ DeviceRole["ClimateHeatPumpCircuit"] = "climate-heat-pump-circuit";
1143
1160
  /** Generic motion / occupancy / moving event source. Distinct from
1144
1161
  * the camera accessory PirSensor role: that one is a camera child;
1145
1162
  * this is a standalone HA / 3rd-party motion sensor. */
@@ -6027,7 +6044,7 @@ var ZodIssueCode = {
6027
6044
  var ZodFirstPartyTypeKind;
6028
6045
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6029
6046
  //#endregion
6030
- //#region ../types/dist/sleep-D_NpiLFP.mjs
6047
+ //#region ../types/dist/sleep-Bn346wCJ.mjs
6031
6048
  /**
6032
6049
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6033
6050
  * window to float samples (D455).
@@ -11804,6 +11821,50 @@ var ACTUATOR_CAP_VOCABULARY = {
11804
11821
  value: false
11805
11822
  },
11806
11823
  args: { oscillating: false }
11824
+ },
11825
+ {
11826
+ id: "fan.horizontalOscillateOn",
11827
+ method: "setOscillationAxis",
11828
+ label: t("Horizontal oscillation on", "Oscillazione orizzontale attiva"),
11829
+ phrase: t("Start the horizontal oscillation of {device}", "Attiva l’oscillazione orizzontale di {device}"),
11830
+ icon: "move-horizontal",
11831
+ args: {
11832
+ axis: "horizontal",
11833
+ oscillating: true
11834
+ }
11835
+ },
11836
+ {
11837
+ id: "fan.horizontalOscillateOff",
11838
+ method: "setOscillationAxis",
11839
+ label: t("Horizontal oscillation off", "Oscillazione orizzontale spenta"),
11840
+ phrase: t("Stop the horizontal oscillation of {device}", "Ferma l’oscillazione orizzontale di {device}"),
11841
+ icon: "move-horizontal",
11842
+ args: {
11843
+ axis: "horizontal",
11844
+ oscillating: false
11845
+ }
11846
+ },
11847
+ {
11848
+ id: "fan.verticalOscillateOn",
11849
+ method: "setOscillationAxis",
11850
+ label: t("Vertical oscillation on", "Oscillazione verticale attiva"),
11851
+ phrase: t("Start the vertical oscillation of {device}", "Attiva l’oscillazione verticale di {device}"),
11852
+ icon: "move-vertical",
11853
+ args: {
11854
+ axis: "vertical",
11855
+ oscillating: true
11856
+ }
11857
+ },
11858
+ {
11859
+ id: "fan.verticalOscillateOff",
11860
+ method: "setOscillationAxis",
11861
+ label: t("Vertical oscillation off", "Oscillazione verticale spenta"),
11862
+ phrase: t("Stop the vertical oscillation of {device}", "Ferma l’oscillazione verticale di {device}"),
11863
+ icon: "move-vertical",
11864
+ args: {
11865
+ axis: "vertical",
11866
+ oscillating: false
11867
+ }
11807
11868
  }
11808
11869
  ],
11809
11870
  state: [{
@@ -17444,7 +17505,7 @@ Object.freeze({
17444
17505
  }
17445
17506
  });
17446
17507
  //#endregion
17447
- //#region ../types/dist/implemented-methods-M5aguGon.mjs
17508
+ //#region ../types/dist/implemented-methods-5YMDeE3f.mjs
17448
17509
  /**
17449
17510
  * The ffmpeg build every CamStack node downloads, per platform and arch.
17450
17511
  *
@@ -44153,6 +44214,13 @@ var AutomationConditionOperatorSchema = _enum([
44153
44214
  "in"
44154
44215
  ]);
44155
44216
  /**
44217
+ * The stable name of an event leaf. A path (`when.0.1`) moves when a row is
44218
+ * reordered; the id does not, so a `triggered-by` leaf keeps pointing at the
44219
+ * same row. Optional: a leaf nothing refers to needs none.
44220
+ */
44221
+ var EVENT_LEAF_ID_RE = /^[A-Za-z0-9_-]{1,40}$/;
44222
+ var eventLeafId = { id: string().regex(EVENT_LEAF_ID_RE).optional() };
44223
+ /**
44156
44224
  * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
44157
44225
  * subscription: it evaluates against the **state mirror**, which is reconciled,
44158
44226
  * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
@@ -44160,6 +44228,7 @@ var AutomationConditionOperatorSchema = _enum([
44160
44228
  */
44161
44229
  var DeviceBecomesLeafSchema = object({
44162
44230
  kind: literal("device-becomes"),
44231
+ ...eventLeafId,
44163
44232
  deviceId: number$1().int().nonnegative(),
44164
44233
  cap: string().min(1),
44165
44234
  fieldPath: string().min(1),
@@ -44212,6 +44281,7 @@ var DeviceBecomesLeafSchema = object({
44212
44281
  });
44213
44282
  var DeviceEventLeafSchema = object({
44214
44283
  kind: literal("device-event"),
44284
+ ...eventLeafId,
44215
44285
  /** An `EventCategory` value. */
44216
44286
  category: string().min(1),
44217
44287
  deviceId: number$1().int().nonnegative().optional()
@@ -44224,18 +44294,24 @@ var DeviceEventLeafSchema = object({
44224
44294
  */
44225
44295
  var ScheduleLeafSchema = object({
44226
44296
  kind: literal("schedule"),
44297
+ ...eventLeafId,
44227
44298
  at: TimeSpecSchema.optional(),
44228
44299
  days: array(WeekdayKeySchema).min(1).max(7).optional(),
44229
44300
  /** @deprecated legacy cron — new schedules use `at`. */
44230
44301
  cron: string().min(1).max(120).optional()
44231
44302
  }).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" });
44232
- var ManualLeafSchema = object({ kind: literal("manual") });
44303
+ var ManualLeafSchema = object({
44304
+ kind: literal("manual"),
44305
+ ...eventLeafId
44306
+ });
44233
44307
  var IntervalLeafSchema = object({
44234
44308
  kind: literal("interval"),
44309
+ ...eventLeafId,
44235
44310
  minutes: number$1().int().min(1).max(1440)
44236
44311
  });
44237
44312
  var DetectionLeafSchema = object({
44238
44313
  kind: literal("detection"),
44314
+ ...eventLeafId,
44239
44315
  /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
44240
44316
  subjects: array(DetectionSubjectSchema).min(1),
44241
44317
  deviceIds: array(number$1().int().nonnegative()).min(1),
@@ -44246,6 +44322,7 @@ var DetectionLeafSchema = object({
44246
44322
  });
44247
44323
  var StationaryLeafSchema = object({
44248
44324
  kind: literal("stationary"),
44325
+ ...eventLeafId,
44249
44326
  deviceIds: array(number$1().int().nonnegative()).min(1),
44250
44327
  subjects: array(StationarySubjectSchema).min(1).optional(),
44251
44328
  zones: array(string().min(1)).min(1).optional(),
@@ -44291,6 +44368,41 @@ var StationaryPresentLeafSchema = object({
44291
44368
  minCount: number$1().int().positive().optional()
44292
44369
  });
44293
44370
  /**
44371
+ * True while the site-local clock is inside a daily window and/or on one of
44372
+ * the named days. `days` names the local day a window STARTS on, so
44373
+ * 22:00-06:00 on weekdays is still inside at 01:00 Saturday. A one-date spec
44374
+ * (`kind: 'date'`) is refused: only a clock time or a sun event makes a window
44375
+ * that repeats every day.
44376
+ */
44377
+ var TimeIsLeafSchema = object({
44378
+ kind: literal("time-is"),
44379
+ between: object({
44380
+ from: TimeSpecSchema,
44381
+ to: TimeSpecSchema
44382
+ }).optional(),
44383
+ days: array(WeekdayKeySchema).min(1).max(7).optional()
44384
+ }).superRefine((leaf, ctx) => {
44385
+ if (leaf.between === void 0 && leaf.days === void 0) ctx.addIssue({
44386
+ code: "custom",
44387
+ message: "a time-is leaf needs a between window, days, or both"
44388
+ });
44389
+ for (const end of ["from", "to"]) if (leaf.between?.[end].kind === "date") ctx.addIssue({
44390
+ code: "custom",
44391
+ message: "a one-date time cannot bound a daily window: use a clock time or a sun event",
44392
+ path: ["between", end]
44393
+ });
44394
+ });
44395
+ /**
44396
+ * True for the run that one of the named event leaves started. It reads the
44397
+ * run, not a device: in the tree it is judged against the leaf that fired, in
44398
+ * a step guard against the trigger the run carries. A run no event leaf
44399
+ * started (a state-only tree becoming true) is false.
44400
+ */
44401
+ var TriggeredByLeafSchema = object({
44402
+ kind: literal("triggered-by"),
44403
+ leafIds: array(string().regex(EVENT_LEAF_ID_RE)).min(1).max(20)
44404
+ });
44405
+ /**
44294
44406
  * The escape hatch: the bounded expression engine over named device bindings.
44295
44407
  * Declared as a plain object so a discriminated union can read `kind`; the
44296
44408
  * author-time validation is the SAME function `ExpressionSourceSchema` runs.
@@ -44336,7 +44448,9 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44336
44448
  StationaryLeafSchema,
44337
44449
  DeviceIsLeafSchema,
44338
44450
  StationaryPresentLeafSchema,
44339
- ExpressionLeafSchema
44451
+ ExpressionLeafSchema,
44452
+ TimeIsLeafSchema,
44453
+ TriggeredByLeafSchema
44340
44454
  ]));
44341
44455
  var EVENT_KINDS = new Set([
44342
44456
  "device-becomes",
@@ -44390,6 +44504,28 @@ function hasEventLeaves(node) {
44390
44504
  });
44391
44505
  return found;
44392
44506
  }
44507
+ /** The event leaves of the tree that carry an id, in tree order. */
44508
+ function identifiedEventLeaves(when) {
44509
+ const found = [];
44510
+ forEachLeaf(when, (leaf, path) => {
44511
+ if (isEventLeaf(leaf) && leaf.id !== void 0) found.push({
44512
+ id: leaf.id,
44513
+ path,
44514
+ leaf
44515
+ });
44516
+ });
44517
+ return found;
44518
+ }
44519
+ /** Does the tree hold a `triggered-by` leaf? */
44520
+ function hasTriggeredByLeaves(when) {
44521
+ let found = false;
44522
+ forEachLeaf(when, (leaf) => {
44523
+ if (leaf.kind === "triggered-by") found = true;
44524
+ });
44525
+ return found;
44526
+ }
44527
+ /** Why a `triggered-by` leaf is refused outside an automation: there is no trigger row to name. */
44528
+ 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";
44393
44529
  function hasEmptyContainer(node) {
44394
44530
  if (isLeaf(node)) return false;
44395
44531
  if (node.kind === "not") return hasEmptyContainer(node.child);
@@ -44448,6 +44584,11 @@ function validateWhenTree(when) {
44448
44584
  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`;
44449
44585
  const leaves = countWhenLeaves(when);
44450
44586
  if (leaves > 32) return `the rules use ${leaves} rows — the limit is 32`;
44587
+ const seen = /* @__PURE__ */ new Set();
44588
+ for (const entry of identifiedEventLeaves(when)) {
44589
+ if (seen.has(entry.id)) return `two trigger rows share the id "${entry.id}" — an id names one row`;
44590
+ seen.add(entry.id);
44591
+ }
44451
44592
  const need = eventNeed(when, "when", false);
44452
44593
  if ("error" in need) return need.error;
44453
44594
  const [first, second] = need.paths;
@@ -44738,7 +44879,18 @@ var GuardedCapStepSchema = AutomationCapStepBaseSchema.extend(stepGuardShape).re
44738
44879
  message: "a domain target needs the vocabulary action it applies to (actionId)",
44739
44880
  path: ["actionId"]
44740
44881
  }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44741
- var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, GuardedCapStepSchema]);
44882
+ /**
44883
+ * A step of a STANDALONE list (an action, a scheduler point, a preset): the
44884
+ * same two step kinds an automation runs, minus a guard that reads the
44885
+ * automation's trigger, which such a list does not have.
44886
+ */
44887
+ var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, GuardedCapStepSchema]).superRefine((step, ctx) => {
44888
+ if (step.if !== void 0 && hasTriggeredByLeaves(step.if)) ctx.addIssue({
44889
+ code: "custom",
44890
+ message: TRIGGERED_BY_NEEDS_AUTOMATION,
44891
+ path: ["if"]
44892
+ });
44893
+ });
44742
44894
  var AutomationActionSchema = discriminatedUnion("kind", [
44743
44895
  GuardedWaitStepSchema,
44744
44896
  GuardedCapStepSchema,
@@ -44835,6 +44987,11 @@ var STATIONARY_DESCRIPTIONS = {
44835
44987
  phase: "appeared or departed",
44836
44988
  entryId: "the stopped object"
44837
44989
  };
44990
+ var BECOMES_DESCRIPTIONS = {
44991
+ deviceId: "the device that changed",
44992
+ from: "the value before the change, empty when it is not known",
44993
+ to: "the value it changed to"
44994
+ };
44838
44995
  function describe$1(descriptions) {
44839
44996
  return Object.entries(descriptions).map(([name, description]) => ({
44840
44997
  name,
@@ -44843,6 +45000,10 @@ function describe$1(descriptions) {
44843
45000
  }
44844
45001
  describe$1(DETECTION_DESCRIPTIONS);
44845
45002
  describe$1(STATIONARY_DESCRIPTIONS);
45003
+ describe$1(BECOMES_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
45004
+ ...v,
45005
+ hintKey: "becomesDeviceId"
45006
+ } : v);
44846
45007
  new Set(Object.values(COCO_TO_MACRO.mapping));
44847
45008
  /**
44848
45009
  * Schedulers (D983) — an entity that performs actions over the day.
@@ -44867,6 +45028,13 @@ var SchedulerStateTreeSchema = AutomationWhenSchema.superRefine((when, ctx) => {
44867
45028
  });
44868
45029
  return;
44869
45030
  }
45031
+ if (hasTriggeredByLeaves(when)) {
45032
+ ctx.addIssue({
45033
+ code: "custom",
45034
+ message: TRIGGERED_BY_NEEDS_AUTOMATION
45035
+ });
45036
+ return;
45037
+ }
44870
45038
  const problem = validateWhenTree(when);
44871
45039
  if (problem !== null) ctx.addIssue({
44872
45040
  code: "custom",
@@ -45087,6 +45255,8 @@ var AutomationConditionTraceNodeSchema = object({
45087
45255
  "device-is",
45088
45256
  "expression",
45089
45257
  "stationary-present",
45258
+ "time-is",
45259
+ "triggered-by",
45090
45260
  "device-becomes",
45091
45261
  "device-event",
45092
45262
  "schedule",
@@ -45420,7 +45590,7 @@ object({
45420
45590
  * is irrelevant; the array is treated as a set by consumers. */
45421
45591
  hiddenChildIds: array(number$1()).readonly()
45422
45592
  });
45423
- DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Cover, DeviceType.Lock, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Generic, method(object({
45593
+ DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Sensor, DeviceType.Climate, DeviceType.Cover, DeviceType.Lock, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Generic, method(object({
45424
45594
  deviceId: number$1().int().nonnegative(),
45425
45595
  childDeviceId: number$1().int().nonnegative(),
45426
45596
  hidden: boolean()
@@ -47718,7 +47888,7 @@ var BATTERY_DEVICE_TYPES = [
47718
47888
  DeviceType.Switch,
47719
47889
  DeviceType.PetFeeder,
47720
47890
  DeviceType.Container,
47721
- DeviceType.Thermostat,
47891
+ DeviceType.Climate,
47722
47892
  DeviceType.EventEmitter
47723
47893
  ];
47724
47894
  /** Generic Customize target for the percentage reading (D936). */
@@ -49481,7 +49651,14 @@ var climateControlCapability = {
49481
49651
  scope: "device",
49482
49652
  deviceNative: true,
49483
49653
  mode: "singleton",
49484
- deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
49654
+ deviceTypes: [DeviceType.Climate],
49655
+ power: {
49656
+ kind: "mode",
49657
+ field: "mode",
49658
+ offValue: "off",
49659
+ set: "setMode",
49660
+ lastField: "lastActiveMode"
49661
+ },
49485
49662
  methods: {
49486
49663
  setMode: method(object({
49487
49664
  deviceId: number$1().int().nonnegative(),
@@ -49866,6 +50043,7 @@ var CompositionProblemSchema = object({
49866
50043
  "nested-field",
49867
50044
  "missing-field",
49868
50045
  "kind-mismatch",
50046
+ "source-kind-mismatch",
49869
50047
  "may-be-null",
49870
50048
  "source-missing",
49871
50049
  "source-unreadable",
@@ -49887,7 +50065,8 @@ var CompositionProblemSchema = object({
49887
50065
  "command-method-unknown",
49888
50066
  "command-source-unknown",
49889
50067
  "command-input-unknown",
49890
- "command-input-missing"
50068
+ "command-input-missing",
50069
+ "power-source-is-actuator"
49891
50070
  ]),
49892
50071
  path: string(),
49893
50072
  message: string()
@@ -50925,6 +51104,13 @@ var mediaPlayerCapability = {
50925
51104
  deviceNative: true,
50926
51105
  mode: "singleton",
50927
51106
  deviceTypes: [DeviceType.MediaPlayer],
51107
+ power: {
51108
+ kind: "methods",
51109
+ field: "state",
51110
+ offValues: ["off", "standby"],
51111
+ on: "turnOn",
51112
+ off: "turnOff"
51113
+ },
50928
51114
  methods: {
50929
51115
  turnOn: optionalMethod(deviceOnly, _void(), {
50930
51116
  kind: "mutation",
@@ -51055,7 +51241,7 @@ var mediaPlayerCapability = {
51055
51241
  isMuted: { kind: "boolean" }
51056
51242
  } }
51057
51243
  };
51058
- 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.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;
51244
+ 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;
51059
51245
  var NUMBER = {
51060
51246
  kind: "fixed",
51061
51247
  result: ["number"]
@@ -51138,7 +51324,7 @@ function numeric(input) {
51138
51324
  }, ...alsoFrom === void 0 ? [] : [alsoFrom.accept]]
51139
51325
  };
51140
51326
  }
51141
- var CLIMATE_DEVICES = [DeviceType.Thermostat, DeviceType.Climate];
51327
+ var CLIMATE_DEVICES = [DeviceType.Climate];
51142
51328
  function binary(input) {
51143
51329
  return {
51144
51330
  ...input,
@@ -51460,8 +51646,8 @@ var GROUP_TEMPLATES = [
51460
51646
  actuator({
51461
51647
  id: "climate",
51462
51648
  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.",
51463
- deviceType: DeviceType.Thermostat,
51464
- memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
51649
+ deviceType: DeviceType.Climate,
51650
+ memberDeviceTypes: [DeviceType.Climate],
51465
51651
  outputCap: "group-active",
51466
51652
  outputField: "active",
51467
51653
  member: {
@@ -51504,6 +51690,118 @@ function actuator(input) {
51504
51690
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
51505
51691
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
51506
51692
  DeviceType.Camera;
51693
+ var PowerSourceRefusalReasonSchema = _enum([
51694
+ "no-power-source",
51695
+ "switch-missing",
51696
+ "switch-offline",
51697
+ "switch-unknown",
51698
+ "restart-in-progress",
51699
+ "switch-call-failed",
51700
+ "switch-is-actuator"
51701
+ ]);
51702
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
51703
+ accepted: literal(false),
51704
+ reason: PowerSourceRefusalReasonSchema,
51705
+ /** One line for the operator, naming the switch. */
51706
+ message: string()
51707
+ })]);
51708
+ var PowerSourceInfoSchema = object({
51709
+ /** False when the device has no power source linked; the rest is then empty. */
51710
+ linked: boolean(),
51711
+ switchDeviceId: number$1().nullable(),
51712
+ switchName: string().nullable(),
51713
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
51714
+ switchOn: boolean().nullable(),
51715
+ switchOnline: boolean().nullable(),
51716
+ restartInProgress: boolean(),
51717
+ offSeconds: number$1().int()
51718
+ });
51719
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
51720
+ var PowerSourceStateSchema = object({
51721
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
51722
+ switchOn: boolean().nullable(),
51723
+ offSeconds: number$1().int(),
51724
+ lastFetchedAt: number$1()
51725
+ });
51726
+ var POWER_SOURCE_DEVICE_TYPES = [
51727
+ DeviceType.Gateway,
51728
+ DeviceType.Camera,
51729
+ DeviceType.Sensor,
51730
+ DeviceType.MediaPlayer,
51731
+ DeviceType.Vacuum,
51732
+ DeviceType.Generic,
51733
+ DeviceType.Washer,
51734
+ DeviceType.Dryer,
51735
+ DeviceType.Dishwasher,
51736
+ DeviceType.PetFountain,
51737
+ DeviceType.Fan,
51738
+ DeviceType.Humidifier,
51739
+ DeviceType.Climate,
51740
+ DeviceType.WaterHeater
51741
+ ];
51742
+ var powerSourceCustomize = {
51743
+ version: 1,
51744
+ targets: [{
51745
+ id: "switch",
51746
+ cardinality: "scalar",
51747
+ vocabulary: {
51748
+ cap: "power-source",
51749
+ stateField: "switchOn"
51750
+ },
51751
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
51752
+ apply: "add",
51753
+ path: "switchOn",
51754
+ sources: [{
51755
+ kind: "switch",
51756
+ transforms: [{ kind: "identity" }]
51757
+ }],
51758
+ companions: [{
51759
+ kind: "constant",
51760
+ path: "offSeconds",
51761
+ value: 10,
51762
+ when: "add",
51763
+ formerValues: [0]
51764
+ }, {
51765
+ kind: "clock",
51766
+ path: "lastFetchedAt",
51767
+ when: "always"
51768
+ }],
51769
+ sourceReport: { status: "assignment-status" }
51770
+ }]
51771
+ };
51772
+ var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
51773
+ var powerSourceCapability = {
51774
+ name: "power-source",
51775
+ scope: "device",
51776
+ mode: "singleton",
51777
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
51778
+ methods: {
51779
+ getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "protected" }),
51780
+ powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
51781
+ kind: "mutation",
51782
+ auth: "protected"
51783
+ }),
51784
+ powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
51785
+ kind: "mutation",
51786
+ auth: "protected"
51787
+ }),
51788
+ forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
51789
+ kind: "mutation",
51790
+ auth: "protected"
51791
+ })
51792
+ },
51793
+ runtimeState: PowerSourceStateSchema,
51794
+ customize: powerSourceCustomize,
51795
+ /**
51796
+ * Runtime-state durability: **session** — mirrored from the linked switch on
51797
+ * its next report; a restored value would outlive the reading it names.
51798
+ *
51799
+ * See `RuntimeStateDurability`. Enforced by
51800
+ * `scripts/check-runtime-state-durability.ts`.
51801
+ */
51802
+ durability: "session",
51803
+ history: "none"
51804
+ };
51507
51805
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
51508
51806
  /**
51509
51807
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -54038,8 +54336,14 @@ method(object({
54038
54336
  "string",
54039
54337
  "number",
54040
54338
  "boolean",
54041
- "enum"
54339
+ "enum",
54340
+ "union"
54042
54341
  ]),
54342
+ unionMembers: array(_enum([
54343
+ "string",
54344
+ "number",
54345
+ "boolean"
54346
+ ])).optional(),
54043
54347
  enumValues: array(string()).optional(),
54044
54348
  item: boolean().optional(),
54045
54349
  quantity: ReadingQuantitySchema.optional()
@@ -56667,8 +56971,85 @@ method(_void(), array(FailureContributionSchema).readonly());
56667
56971
  * - `FanPreset` — preset selector
56668
56972
  * - `FanDirection` — direction toggle (ceiling fans)
56669
56973
  * - `FanOscillating` — oscillation toggle
56974
+ *
56975
+ * Per-axis oscillation (`oscillationAxes`, `setOscillationAxis`, D1034): an air
56976
+ * circulator sweeps horizontally and vertically, each axis with its own
56977
+ * switch, fixed angle, sweep bounds and limits. A provider may report them
56978
+ * natively; otherwise the OPERATOR links sibling devices to each field in
56979
+ * Customize (a `replace` over the fields listed in `overridable` — nothing is
56980
+ * linked or inferred by itself), and a write to a linked field is forwarded to
56981
+ * its source. `oscillating` stays the single summary flag: when axes exist it
56982
+ * means "any axis oscillates" (`anyAxisOscillating`).
56670
56983
  */
56671
56984
  var FanDirectionSchema = _enum(["forward", "reverse"]);
56985
+ var FAN_OSCILLATION_AXES = ["horizontal", "vertical"];
56986
+ var FanOscillationAxisNameSchema = _enum(FAN_OSCILLATION_AXES);
56987
+ var FanOscillationAxisFieldSchema = _enum([
56988
+ "oscillating",
56989
+ "angle",
56990
+ "sweepFrom",
56991
+ "sweepTo"
56992
+ ]);
56993
+ /** The limits of one axis, read from the source that serves its angle. */
56994
+ var FAN_OSCILLATION_AXIS_LIMITS = [
56995
+ "min",
56996
+ "max",
56997
+ "step"
56998
+ ];
56999
+ /**
57000
+ * One oscillation axis. Every reading the device has not reported is `null`,
57001
+ * never `0` (D393): `0°` is a real position. Each leaf defaults to `null`, so
57002
+ * a slice that names only the leaves it knows (a Customize link of one field)
57003
+ * is still a whole axis.
57004
+ */
57005
+ var FanOscillationAxisSchema = object({
57006
+ /** Whether the axis sweeps. */
57007
+ oscillating: boolean().nullable().default(null),
57008
+ /** The fixed position (degrees) the axis holds when it does not sweep. */
57009
+ angle: number$1().nullable().default(null),
57010
+ /** The sweep bounds (degrees): left → right, bottom → top. */
57011
+ sweepFrom: number$1().nullable().default(null),
57012
+ sweepTo: number$1().nullable().default(null),
57013
+ /** The angles the axis accepts. Null when the device states none. */
57014
+ min: number$1().nullable().default(null),
57015
+ max: number$1().nullable().default(null),
57016
+ step: number$1().nullable().default(null)
57017
+ });
57018
+ /** Both axes; an axis the device does not have is `null`. */
57019
+ var FanOscillationAxesSchema = object({
57020
+ horizontal: FanOscillationAxisSchema.nullable().default(null),
57021
+ vertical: FanOscillationAxisSchema.nullable().default(null)
57022
+ });
57023
+ var FanOscillationAxisRefusalSchema = _enum([
57024
+ "nothing-to-set",
57025
+ "axis-not-linked",
57026
+ "out-of-range",
57027
+ "source-gone",
57028
+ "source-unknown",
57029
+ "source-call-failed"
57030
+ ]);
57031
+ var FanOscillationAxisResultSchema = discriminatedUnion("status", [object({ status: literal("applied") }), object({
57032
+ status: literal("refused"),
57033
+ reason: FanOscillationAxisRefusalSchema,
57034
+ /** The field at fault, when one is. */
57035
+ field: FanOscillationAxisFieldSchema.optional(),
57036
+ /** One line for the operator, naming the axis, the field and the source. */
57037
+ detail: string(),
57038
+ /** Fields that DID land before the refusal (writes run in order, stop at the first failure). */
57039
+ applied: array(FanOscillationAxisFieldSchema)
57040
+ })]);
57041
+ var SetOscillationAxisInputSchema = object({
57042
+ deviceId: number$1().int().nonnegative(),
57043
+ axis: FanOscillationAxisNameSchema,
57044
+ oscillating: boolean().optional(),
57045
+ angle: number$1().optional(),
57046
+ sweepFrom: number$1().optional(),
57047
+ sweepTo: number$1().optional()
57048
+ });
57049
+ /** The state path of one axis field: `oscillationAxes.<axis>.<field>`. */
57050
+ function fanAxisPath(axis, field) {
57051
+ return `oscillationAxes.${axis}.${field}`;
57052
+ }
56672
57053
  var FanControlStatusSchema = object({
56673
57054
  /** Active speed as 0..100 inclusive. Null when the device has no
56674
57055
  * speed surface (single-speed fan). */
@@ -56687,9 +57068,102 @@ var FanControlStatusSchema = object({
56687
57068
  /** Oscillation toggle. Null when the device has no oscillation
56688
57069
  * surface. */
56689
57070
  oscillating: boolean().nullable(),
57071
+ /**
57072
+ * Per-axis oscillation (air circulators). Absent when the device has no
57073
+ * per-axis surface and nothing is linked in Customize.
57074
+ */
57075
+ oscillationAxes: FanOscillationAxesSchema.optional(),
56690
57076
  /** Ms epoch when the slice was last updated. */
56691
57077
  lastChangedAt: number$1()
56692
57078
  });
57079
+ var FAN_CUSTOMIZE_DEVICE_TYPES = [DeviceType.Fan];
57080
+ /** The Customize section the axis targets are listed under. */
57081
+ var AXES_SECTION = "oscillation-axes";
57082
+ function axisOscillatingTarget(axis) {
57083
+ return {
57084
+ id: `${axis}-oscillating`,
57085
+ cardinality: "scalar",
57086
+ vocabulary: {
57087
+ cap: "fan-control",
57088
+ stateField: "oscillating"
57089
+ },
57090
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57091
+ apply: "replace",
57092
+ section: AXES_SECTION,
57093
+ path: fanAxisPath(axis, "oscillating"),
57094
+ sources: [{
57095
+ kind: "switch",
57096
+ transforms: [{ kind: "identity" }]
57097
+ }],
57098
+ sourceReport: { status: "assignment-status" }
57099
+ };
57100
+ }
57101
+ /**
57102
+ * A numeric axis field. The `angle` link also carries the axis limits, read
57103
+ * from the SAME linked control's own range (`min`/`max`/`step`), so the limits
57104
+ * are the source's, never guessed.
57105
+ */
57106
+ function axisNumericTarget(axis, field) {
57107
+ const base = {
57108
+ id: `${axis}-${field}`,
57109
+ cardinality: "scalar",
57110
+ vocabulary: { cap: "fan-control" },
57111
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57112
+ apply: "replace",
57113
+ section: AXES_SECTION,
57114
+ path: fanAxisPath(axis, field),
57115
+ sources: [{
57116
+ kind: "numeric",
57117
+ transforms: [{ kind: "identity" }]
57118
+ }],
57119
+ sourceReport: { status: "assignment-status" }
57120
+ };
57121
+ if (field !== "angle") return base;
57122
+ return {
57123
+ ...base,
57124
+ companions: FAN_OSCILLATION_AXIS_LIMITS.map((limit) => ({
57125
+ kind: "source-field",
57126
+ path: fanAxisPath(axis, limit),
57127
+ field: limit
57128
+ }))
57129
+ };
57130
+ }
57131
+ var AXIS_TARGETS = FAN_OSCILLATION_AXES.flatMap((axis) => [
57132
+ axisOscillatingTarget(axis),
57133
+ axisNumericTarget(axis, "angle"),
57134
+ axisNumericTarget(axis, "sweepFrom"),
57135
+ axisNumericTarget(axis, "sweepTo")
57136
+ ]);
57137
+ /** A select (an enum control) the operator links to the fan's `preset`. */
57138
+ var PRESET_TARGET = {
57139
+ id: "preset",
57140
+ cardinality: "scalar",
57141
+ vocabulary: { cap: "fan-control" },
57142
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57143
+ apply: "replace",
57144
+ path: "preset",
57145
+ sources: [{
57146
+ kind: "field",
57147
+ valueKinds: ["string"],
57148
+ transforms: [{ kind: "identity" }]
57149
+ }],
57150
+ sourceReport: { status: "assignment-status" }
57151
+ };
57152
+ var FAN_CONTROL_TARGETS = [...AXIS_TARGETS, PRESET_TARGET];
57153
+ /**
57154
+ * Customize (D1034): the fan already serves `fan-control` natively, so every
57155
+ * target is a `replace` over a field listed in `overridable` — the axis leaves
57156
+ * and `preset`, nothing else. Nothing is pre-linked: the operator picks each
57157
+ * source (D356).
57158
+ */
57159
+ var fanControlCustomize = {
57160
+ version: 1,
57161
+ overridable: FAN_CONTROL_TARGETS.map((target) => ({
57162
+ path: target.path,
57163
+ by: target.id
57164
+ })),
57165
+ targets: FAN_CONTROL_TARGETS
57166
+ };
56693
57167
  var fanControlCapability = {
56694
57168
  name: "fan-control",
56695
57169
  scope: "device",
@@ -56724,6 +57198,18 @@ var fanControlCapability = {
56724
57198
  }), _void(), {
56725
57199
  kind: "mutation",
56726
57200
  auth: "protected"
57201
+ }),
57202
+ /**
57203
+ * Sets one oscillation axis: its switch, fixed angle and/or sweep bounds.
57204
+ * Fields left out are untouched. A field linked in Customize is forwarded
57205
+ * to its linked source (D1034); otherwise the native provider serves it;
57206
+ * neither → `axis-not-linked`. Ranges are checked against the axis limits
57207
+ * before anything is written; writes run in order and stop at the first
57208
+ * failure, which the result names.
57209
+ */
57210
+ setOscillationAxis: optionalMethod(SetOscillationAxisInputSchema, FanOscillationAxisResultSchema, {
57211
+ kind: "mutation",
57212
+ auth: "protected"
56727
57213
  })
56728
57214
  },
56729
57215
  status: {
@@ -56735,6 +57221,7 @@ var fanControlCapability = {
56735
57221
  * sliders read `speed` for live updates.
56736
57222
  */
56737
57223
  runtimeState: FanControlStatusSchema,
57224
+ customize: fanControlCustomize,
56738
57225
  /**
56739
57226
  * Runtime-state durability: **session** — as `brightness`.
56740
57227
  *
@@ -56754,16 +57241,33 @@ var fanControlCapability = {
56754
57241
  oscillating: { kind: "boolean" }
56755
57242
  } }
56756
57243
  };
57244
+ /** One axis as a preset wants it; an absent field is left as it is. */
57245
+ var FanPresetAxisSchema = object({
57246
+ oscillating: boolean().optional(),
57247
+ angle: number$1().optional(),
57248
+ sweepFrom: number$1().optional(),
57249
+ sweepTo: number$1().optional()
57250
+ });
57251
+ /** The fan-control state a preset sets; an absent field is left as it is. */
57252
+ var FanPresetStateSchema = object({
57253
+ speed: number$1().min(0).max(100).optional(),
57254
+ /** The fan's own preset mode (or the linked select's option). */
57255
+ preset: string().min(1).optional(),
57256
+ direction: FanDirectionSchema.optional(),
57257
+ oscillationAxes: object({
57258
+ horizontal: FanPresetAxisSchema.optional(),
57259
+ vertical: FanPresetAxisSchema.optional()
57260
+ }).optional()
57261
+ });
56757
57262
  var FanPresetSchema = object({
56758
57263
  /** Stable key; never reused after a delete within one device's list. */
56759
57264
  id: string().min(1).max(64),
56760
57265
  /** What the operator and a voice assistant call it. */
56761
57266
  name: string().trim().min(1).max(40),
56762
- steps: array(ActionStepSchema).min(1).max(20)
57267
+ state: FanPresetStateSchema
56763
57268
  });
56764
57269
  var FanPresetsRefusalSchema = _enum([
56765
- "not-sibling",
56766
- "bad-target",
57270
+ "empty-state",
56767
57271
  "duplicate-name",
56768
57272
  "duplicate-id",
56769
57273
  "too-many",
@@ -56775,72 +57279,55 @@ var FanPresetsSetResultSchema = discriminatedUnion("status", [object({
56775
57279
  }), object({
56776
57280
  status: literal("refused"),
56777
57281
  reason: FanPresetsRefusalSchema,
56778
- /** Names the preset and step at fault. */
57282
+ /** Names the preset at fault. */
56779
57283
  detail: string()
56780
57284
  })]);
56781
- var FanPresetStepResultSchema = object({
56782
- index: number$1().int().nonnegative(),
57285
+ var FanPresetFieldResultSchema = object({
57286
+ /** The state path: `speed`, `preset`, `direction`, `oscillationAxes.horizontal.angle`. */
57287
+ field: string(),
56783
57288
  outcome: _enum([
56784
57289
  "applied",
56785
57290
  "failed",
56786
57291
  "not-run",
56787
57292
  "refused"
56788
57293
  ]),
56789
- deviceId: number$1().int().nonnegative().optional(),
56790
- cap: string().optional(),
56791
- method: string().optional(),
56792
57294
  /** The cause, for every outcome but `applied`. */
56793
57295
  message: string().optional()
56794
57296
  });
56795
57297
  var FanPresetApplyResultSchema = object({
56796
- /** `applied`: every step ran. `partial`: some ran, one failed. `refused`: nothing ran. */
57298
+ /** `applied`: every field landed. `partial`: some did, one did not. `refused`: nothing ran. */
56797
57299
  status: _enum([
56798
57300
  "applied",
56799
57301
  "partial",
56800
57302
  "refused"
56801
57303
  ]),
56802
- steps: array(FanPresetStepResultSchema)
57304
+ fields: array(FanPresetFieldResultSchema)
56803
57305
  });
56804
57306
  var FanCaptureFieldSchema = _enum([
56805
57307
  "speed",
56806
- "oscillating",
56807
57308
  "direction",
56808
- "preset"
57309
+ "preset",
57310
+ "oscillationAxes"
56809
57311
  ]);
56810
- var FanCaptureOmissionSchema = object({
56811
- /** A fan field (`speed`) or `step <n>` for a sibling step. */
56812
- what: string(),
56813
- /** Why no value was taken: unknown reading (never stored as 0, D393), or a step that cannot be read. */
56814
- reason: string()
56815
- });
56816
- var FanCaptureItemSchema = object({
56817
- group: _enum([
56818
- "fan",
56819
- "oscillation",
56820
- "angles"
56821
- ]),
56822
- /** What the step sets, in the operator's words (a device name, a fan field). */
56823
- label: string()
56824
- });
56825
57312
  var FanPresetCaptureResultSchema = object({
56826
- /** The steps filled from the device's current values; NOT saved. */
56827
- steps: array(ActionStepSchema),
56828
- /** One entry per step, same order: its review group and label. */
56829
- items: array(FanCaptureItemSchema),
56830
- omitted: array(FanCaptureOmissionSchema)
57313
+ /** The fan's CURRENT fan-control state, as a preset; NOT saved. */
57314
+ state: FanPresetStateSchema,
57315
+ omitted: array(object({
57316
+ /** A state path (`speed`, `oscillationAxes.vertical.angle`). */
57317
+ what: string(),
57318
+ /** Why no value was taken: an unknown reading is never stored as 0 (D393). */
57319
+ reason: string()
57320
+ }))
56831
57321
  });
56832
- var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
57322
+ var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
56833
57323
  DeviceType.Fan;
56834
- method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
57324
+ method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
56835
57325
  kind: "mutation",
56836
57326
  auth: "admin"
56837
- }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
57327
+ }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
56838
57328
  kind: "mutation",
56839
57329
  auth: "protected"
56840
- }), method(deviceInput$2.extend({
56841
- fields: array(FanCaptureFieldSchema).optional(),
56842
- steps: array(ActionStepSchema).max(20).default([])
56843
- }), FanPresetCaptureResultSchema, { auth: "protected" });
57330
+ }), method(deviceInput$1.extend({ fields: array(FanCaptureFieldSchema).optional() }), FanPresetCaptureResultSchema, { auth: "protected" });
56844
57331
  /**
56845
57332
  * Per-device feature/identity probe slice. Holds the runtime-resolved
56846
57333
  * truth about what a device CAN do — which the kernel uses to:
@@ -57052,8 +57539,7 @@ var groupActiveCapability = {
57052
57539
  DeviceType.Valve,
57053
57540
  DeviceType.Lock,
57054
57541
  DeviceType.MediaPlayer,
57055
- DeviceType.Climate,
57056
- DeviceType.Thermostat
57542
+ DeviceType.Climate
57057
57543
  ],
57058
57544
  methods: {},
57059
57545
  status: {
@@ -57095,7 +57581,6 @@ var groupCommandsCapability = {
57095
57581
  DeviceType.Valve,
57096
57582
  DeviceType.Lock,
57097
57583
  DeviceType.MediaPlayer,
57098
- DeviceType.Thermostat,
57099
57584
  DeviceType.Climate
57100
57585
  ],
57101
57586
  methods: {},
@@ -57151,7 +57636,6 @@ var groupStatusCapability = {
57151
57636
  DeviceType.Valve,
57152
57637
  DeviceType.Lock,
57153
57638
  DeviceType.MediaPlayer,
57154
- DeviceType.Thermostat,
57155
57639
  DeviceType.Climate
57156
57640
  ],
57157
57641
  methods: {},
@@ -57507,7 +57991,7 @@ var heatingValveCapability = {
57507
57991
  scope: "device",
57508
57992
  deviceNative: true,
57509
57993
  mode: "singleton",
57510
- deviceTypes: [DeviceType.Thermostat],
57994
+ deviceTypes: [DeviceType.Climate],
57511
57995
  methods: {},
57512
57996
  status: {
57513
57997
  schema: HeatingValveStatusSchema,
@@ -57537,13 +58021,18 @@ var heatingValveCapability = {
57537
58021
  * retried in a loop - D49); a future one is re-armed.
57538
58022
  *
57539
58023
  * What a device may be run for, and for how long at most, is per device type
57540
- * ({@link TIMED_RUN_LIMITS}): the maximum is a safety bound, not a preference,
58024
+ * and role ({@link timedRunLimitOf}): the maximum is a safety bound, not a preference,
57541
58025
  * so a request above it is REFUSED with a typed reason, never clamped.
57542
58026
  */
57543
58027
  var TimedRunUnitSchema = _enum(["s", "min"]);
57544
58028
  _enum(["switch", "pet-fountain"]);
57545
- DeviceType.Heater, DeviceType.PetFountain;
57546
- var TIMED_RUN_DEVICE_TYPES = [DeviceType.Heater, DeviceType.PetFountain];
58029
+ /**
58030
+ * The device types `timed-run` may be offered on. A `switch` is narrowed by
58031
+ * role ({@link TIMED_RUN_DEVICE_ROLES}): only a plug-in heater runs on a timer.
58032
+ */
58033
+ var TIMED_RUN_DEVICE_TYPES = [DeviceType.Switch, DeviceType.PetFountain];
58034
+ /** The roles that make a device of these types timed-run-able (D1033). */
58035
+ var TIMED_RUN_DEVICE_ROLES = { [DeviceType.Switch]: [DeviceRole.SwitchHeater] };
57547
58036
  /** One duration, as the one-tap run asks for it. */
57548
58037
  var TimedRunQuickRunSchema = object({
57549
58038
  duration: number$1().positive().finite(),
@@ -57593,6 +58082,7 @@ var timedRunCapability = {
57593
58082
  kind: "wrapper",
57594
58083
  defaultActive: true,
57595
58084
  deviceTypes: TIMED_RUN_DEVICE_TYPES,
58085
+ deviceRoles: TIMED_RUN_DEVICE_ROLES,
57596
58086
  deviceConfig: { ui: {
57597
58087
  kind: "derived-form",
57598
58088
  builderId: "timed-run",
@@ -57753,8 +58243,8 @@ var HeroSceneSchema = object({
57753
58243
  /** 0..1: volume, cover position, cycle progress. */
57754
58244
  level: number$1().min(0).max(1).nullable()
57755
58245
  });
57756
- 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;
57757
- 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;
58246
+ 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;
58247
+ 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;
57758
58248
  /** A string in both languages, as the cap vocabulary writes it (D920): no `t()` in the model. */
57759
58249
  var HeroTextSchema = object({
57760
58250
  en: string(),
@@ -58139,8 +58629,8 @@ var HeroRowModelSchema = object({
58139
58629
  extras: array(HeroControlSchema).max(2).optional(),
58140
58630
  trailing: HeroFactSchema.nullable()
58141
58631
  });
58142
- DeviceType.Climate, DeviceType.Thermostat;
58143
- DeviceType.Climate, DeviceType.Thermostat, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
58632
+ DeviceType.Climate;
58633
+ DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
58144
58634
  /**
58145
58635
  * hero-layout — what a device's hero says (D1023). Read-only: the FIXED
58146
58636
  * layouts live in `@camstack/types` and nothing here stores or edits one.
@@ -58172,7 +58662,10 @@ method(heroInput, HeroModelSchema.nullable(), {
58172
58662
  }), method(rowModelsInput, array(HeroRowModelSchema.nullable()), {
58173
58663
  auth: "protected",
58174
58664
  role: "viewer"
58175
- }), method(object({ deviceType: string() }), HeroLayoutRowsSchema, {
58665
+ }), method(object({
58666
+ deviceType: string(),
58667
+ role: string().nullable().optional()
58668
+ }), HeroLayoutRowsSchema, {
58176
58669
  auth: "protected",
58177
58670
  role: "viewer"
58178
58671
  });
@@ -58219,6 +58712,11 @@ var humidifierCapability = {
58219
58712
  deviceNative: true,
58220
58713
  mode: "singleton",
58221
58714
  deviceTypes: [DeviceType.Humidifier],
58715
+ power: {
58716
+ kind: "field",
58717
+ field: "on",
58718
+ set: "setOn"
58719
+ },
58222
58720
  methods: {
58223
58721
  setOn: method(object({
58224
58722
  deviceId: number$1().int().nonnegative(),
@@ -58292,7 +58790,7 @@ var humiditySensorCapability = {
58292
58790
  scope: "device",
58293
58791
  deviceNative: true,
58294
58792
  mode: "singleton",
58295
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
58793
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
58296
58794
  methods: {},
58297
58795
  status: {
58298
58796
  schema: HumiditySensorStatusSchema,
@@ -71964,6 +72462,11 @@ var petFountainCapability = {
71964
72462
  deviceNative: true,
71965
72463
  mode: "singleton",
71966
72464
  deviceTypes: [DeviceType.PetFountain],
72465
+ power: {
72466
+ kind: "field",
72467
+ field: "pumpRunning",
72468
+ set: "setPump"
72469
+ },
71967
72470
  methods: {
71968
72471
  /** Start or stop the pump. */
71969
72472
  setPump: method(object({
@@ -73539,7 +74042,6 @@ var POWER_METER_DEVICE_TYPES = [
73539
74042
  DeviceType.Light,
73540
74043
  DeviceType.Fan,
73541
74044
  DeviceType.Climate,
73542
- DeviceType.Thermostat,
73543
74045
  DeviceType.Humidifier,
73544
74046
  DeviceType.WaterHeater,
73545
74047
  DeviceType.MediaPlayer,
@@ -73548,8 +74050,7 @@ var POWER_METER_DEVICE_TYPES = [
73548
74050
  DeviceType.Dishwasher,
73549
74051
  DeviceType.Vacuum,
73550
74052
  DeviceType.LawnMower,
73551
- DeviceType.PetFeeder,
73552
- DeviceType.Heater
74053
+ DeviceType.PetFeeder
73553
74054
  ];
73554
74055
  /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
73555
74056
  var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
@@ -73662,117 +74163,6 @@ var powerMeterCapability = {
73662
74163
  volatileStateFields: ["lastFetchedAt"],
73663
74164
  customize: powerMeterCustomize
73664
74165
  };
73665
- var PowerSourceRefusalReasonSchema = _enum([
73666
- "no-power-source",
73667
- "switch-missing",
73668
- "switch-offline",
73669
- "switch-unknown",
73670
- "restart-in-progress",
73671
- "switch-call-failed"
73672
- ]);
73673
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
73674
- accepted: literal(false),
73675
- reason: PowerSourceRefusalReasonSchema,
73676
- /** One line for the operator, naming the switch. */
73677
- message: string()
73678
- })]);
73679
- var PowerSourceInfoSchema = object({
73680
- /** False when the device has no power source linked; the rest is then empty. */
73681
- linked: boolean(),
73682
- switchDeviceId: number$1().nullable(),
73683
- switchName: string().nullable(),
73684
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
73685
- switchOn: boolean().nullable(),
73686
- switchOnline: boolean().nullable(),
73687
- restartInProgress: boolean(),
73688
- offSeconds: number$1().int()
73689
- });
73690
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
73691
- var PowerSourceStateSchema = object({
73692
- /** Null while the switch has not reported: unknown is never `false` (D393). */
73693
- switchOn: boolean().nullable(),
73694
- offSeconds: number$1().int(),
73695
- lastFetchedAt: number$1()
73696
- });
73697
- var POWER_SOURCE_DEVICE_TYPES = [
73698
- DeviceType.Gateway,
73699
- DeviceType.Camera,
73700
- DeviceType.Sensor,
73701
- DeviceType.MediaPlayer,
73702
- DeviceType.Vacuum,
73703
- DeviceType.Generic,
73704
- DeviceType.Washer,
73705
- DeviceType.Dryer,
73706
- DeviceType.Dishwasher,
73707
- DeviceType.PetFountain,
73708
- DeviceType.Fan,
73709
- DeviceType.Humidifier,
73710
- DeviceType.Climate,
73711
- DeviceType.WaterHeater
73712
- ];
73713
- var powerSourceCustomize = {
73714
- version: 1,
73715
- targets: [{
73716
- id: "switch",
73717
- cardinality: "scalar",
73718
- vocabulary: {
73719
- cap: "power-source",
73720
- stateField: "switchOn"
73721
- },
73722
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
73723
- apply: "add",
73724
- path: "switchOn",
73725
- sources: [{
73726
- kind: "switch",
73727
- transforms: [{ kind: "identity" }]
73728
- }],
73729
- companions: [{
73730
- kind: "constant",
73731
- path: "offSeconds",
73732
- value: 10,
73733
- when: "add",
73734
- formerValues: [0]
73735
- }, {
73736
- kind: "clock",
73737
- path: "lastFetchedAt",
73738
- when: "always"
73739
- }],
73740
- sourceReport: { status: "assignment-status" }
73741
- }]
73742
- };
73743
- var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
73744
- var powerSourceCapability = {
73745
- name: "power-source",
73746
- scope: "device",
73747
- mode: "singleton",
73748
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
73749
- methods: {
73750
- getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "protected" }),
73751
- powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73752
- kind: "mutation",
73753
- auth: "protected"
73754
- }),
73755
- powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73756
- kind: "mutation",
73757
- auth: "protected"
73758
- }),
73759
- forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73760
- kind: "mutation",
73761
- auth: "protected"
73762
- })
73763
- },
73764
- runtimeState: PowerSourceStateSchema,
73765
- customize: powerSourceCustomize,
73766
- /**
73767
- * Runtime-state durability: **session** — mirrored from the linked switch on
73768
- * its next report; a restored value would outlive the reading it names.
73769
- *
73770
- * See `RuntimeStateDurability`. Enforced by
73771
- * `scripts/check-runtime-state-durability.ts`.
73772
- */
73773
- durability: "session",
73774
- history: "none"
73775
- };
73776
74166
  /**
73777
74167
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
73778
74168
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -77247,9 +77637,7 @@ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
77247
77637
  DeviceType.Humidifier,
77248
77638
  DeviceType.Fan,
77249
77639
  DeviceType.Climate,
77250
- DeviceType.Thermostat,
77251
- DeviceType.PetFeeder,
77252
- DeviceType.Heater
77640
+ DeviceType.PetFeeder
77253
77641
  ];
77254
77642
  var runningStateCapability = {
77255
77643
  name: "running-state",
@@ -79337,8 +79725,7 @@ var switchCapability = {
79337
79725
  DeviceType.Switch,
79338
79726
  DeviceType.Siren,
79339
79727
  DeviceType.Light,
79340
- DeviceType.Fan,
79341
- DeviceType.Heater
79728
+ DeviceType.Fan
79342
79729
  ],
79343
79730
  methods: { setState: method(object({
79344
79731
  deviceId: number$1(),
@@ -80032,7 +80419,7 @@ var temperatureSensorCapability = {
80032
80419
  scope: "device",
80033
80420
  deviceNative: true,
80034
80421
  mode: "singleton",
80035
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
80422
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
80036
80423
  methods: {},
80037
80424
  status: {
80038
80425
  schema: TemperatureSensorStatusSchema,
@@ -81679,6 +82066,13 @@ var waterHeaterCapability = {
81679
82066
  deviceNative: true,
81680
82067
  mode: "singleton",
81681
82068
  deviceTypes: [DeviceType.WaterHeater],
82069
+ power: {
82070
+ kind: "mode",
82071
+ field: "operationMode",
82072
+ offValue: "off",
82073
+ set: "setOperationMode",
82074
+ lastField: "lastActiveMode"
82075
+ },
81682
82076
  methods: {
81683
82077
  setTargetTemp: method(object({
81684
82078
  deviceId: number$1().int().nonnegative(),
@@ -84370,15 +84764,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84370
84764
  });
84371
84765
  const meta = await this.ctx.api.deviceManager.loadMeta.query({ deviceId: id });
84372
84766
  if (meta === null) throw new Error(`${this.providerName}: no persisted meta for "${stableId}" (id ${id}) — cannot reload`);
84373
- const deviceType = Object.values(DeviceType).find((t) => t === meta.type);
84374
- const Class = deviceType !== void 0 ? this.deviceClasses[deviceType] : void 0;
84767
+ const Class = this.deviceClassForRow({
84768
+ type: meta.type,
84769
+ stableId
84770
+ });
84375
84771
  if (!Class) throw new Error(`${this.providerName}: no device class for type "${meta.type}" — "${stableId}" is an accessory child; reload its parent instead`);
84376
84772
  await devices.create(stableId, Class, {}, meta.parentDeviceId ?? null);
84377
84773
  const rows = await this.ctx.api.deviceManager.listPersistedByAddon.query({ addonId: this.addonId });
84378
84774
  for (const row of rows) {
84379
84775
  if (row.parentDeviceId !== id) continue;
84380
- const childType = Object.values(DeviceType).find((t) => t === row.type);
84381
- const ChildClass = childType !== void 0 ? this.deviceClasses[childType] : void 0;
84776
+ const ChildClass = this.deviceClassForRow(row);
84382
84777
  if (!ChildClass) continue;
84383
84778
  try {
84384
84779
  await devices.create(row.stableId, ChildClass, {}, id);
@@ -84405,6 +84800,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84405
84800
  return { deviceId: id };
84406
84801
  }
84407
84802
  /**
84803
+ * The class a PERSISTED row is rebuilt with. Default: by type, from
84804
+ * `deviceClasses`. A provider whose distinct devices share one type (D1033:
84805
+ * a radiator valve and a room climate are both `climate`) overrides this
84806
+ * and tells them apart by the row's stable id or config.
84807
+ */
84808
+ deviceClassForRow(row) {
84809
+ const type = Object.values(DeviceType).find((t) => t === row.type);
84810
+ return type !== void 0 ? this.deviceClasses[type] : void 0;
84811
+ }
84812
+ /**
84408
84813
  * Restore devices from persisted state. Two-pass:
84409
84814
  *
84410
84815
  * 1. **Top-level pass** — invokes `kernel.devices.create()` for every
@@ -84465,7 +84870,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84465
84870
  restored.add(saved.id);
84466
84871
  return;
84467
84872
  }
84468
- const Class = this.deviceClasses[saved.type];
84873
+ const Class = this.deviceClassForRow(saved);
84469
84874
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
84470
84875
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
84471
84876
  await this.healSavedConfig(saved, savedDevices);
@@ -84482,7 +84887,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84482
84887
  };
84483
84888
  const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
84484
84889
  const restoreOne = async (saved) => {
84485
- if (!this.deviceClasses[saved.type]) {
84890
+ if (!this.deviceClassForRow(saved)) {
84486
84891
  this.ctx.logger.warn("No device class registered for restored type — skipping", {
84487
84892
  tags: {
84488
84893
  deviceId: saved.id,
@@ -84521,7 +84926,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84521
84926
  const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
84522
84927
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
84523
84928
  for (const saved of childRows) {
84524
- if (!this.deviceClasses[saved.type]) continue;
84929
+ if (!this.deviceClassForRow(saved)) continue;
84525
84930
  if (saved.parentDeviceId === null) continue;
84526
84931
  if (restored.has(saved.parentDeviceId)) {
84527
84932
  try {
@@ -84575,7 +84980,9 @@ var BaseDeviceProvider = class extends BaseAddon {
84575
84980
  }
84576
84981
  };
84577
84982
  DeviceType.HeatPump;
84578
- 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;
84983
+ DeviceType.Climate;
84984
+ DeviceRole.ClimateThermostat, DeviceRole.ClimateAc, DeviceRole.ClimateRadiator, DeviceRole.ClimateFloor, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit;
84985
+ 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;
84579
84986
  new Set(Object.values(DeviceType));
84580
84987
  /** Parses the raw slice with the cap's own schema; `undefined` when it does not parse. */
84581
84988
  function contribution(cap, schema, build) {
@@ -86581,7 +86988,7 @@ var HEAT_PUMP_FAMILY_CONTRIBUTIONS = { "heat-pump-status": contribution("heat-pu
86581
86988
  ...Object.values(CLIMATE_FAMILY_CONTRIBUTIONS),
86582
86989
  ...Object.values(HEAT_PUMP_FAMILY_CONTRIBUTIONS)
86583
86990
  ];
86584
- 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;
86991
+ 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;
86585
86992
  /**
86586
86993
  * Who looks like what (D1023). One look per {@link DeviceType}, refined by a
86587
86994
  * {@link DeviceRole} where the role changes how the device reads (a leak
@@ -87225,9 +87632,9 @@ lookOf({
87225
87632
  idle: INFO,
87226
87633
  standby: MUTED
87227
87634
  });
87228
- DeviceType.Climate, DeviceType.Thermostat;
87229
- 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;
87230
- 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;
87635
+ DeviceType.Climate;
87636
+ 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;
87637
+ 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;
87231
87638
  Object.freeze({
87232
87639
  "accessories.getStatus": {
87233
87640
  capName: "accessories",
@@ -90624,6 +91031,13 @@ Object.freeze({
90624
91031
  access: "create",
90625
91032
  role: "viewer"
90626
91033
  },
91034
+ "fanControl.setOscillationAxis": {
91035
+ capName: "fan-control",
91036
+ capScope: "device",
91037
+ addonId: null,
91038
+ access: "create",
91039
+ role: "viewer"
91040
+ },
90627
91041
  "fanControl.setPercentage": {
90628
91042
  capName: "fan-control",
90629
91043
  capScope: "device",
@@ -97645,7 +98059,7 @@ function projectAction(cap, action) {
97645
98059
  };
97646
98060
  }
97647
98061
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
97648
- 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;
98062
+ 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;
97649
98063
  DeviceFeature.BatteryOperated;
97650
98064
  function bound(cap, methods = [], authority = "reported") {
97651
98065
  return {
@@ -97704,6 +98118,149 @@ function offered(received) {
97704
98118
  };
97705
98119
  }
97706
98120
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
98121
+ var PowerSwitchRefusedError = class extends Error {
98122
+ reason;
98123
+ constructor(reason, message) {
98124
+ super(message);
98125
+ this.name = "PowerSwitchRefusedError";
98126
+ this.reason = reason;
98127
+ }
98128
+ };
98129
+ /** `true` / `false` from the owner slice, `null` when the hinted field is
98130
+ * absent or not of the hinted shape. */
98131
+ function powerOnFromSlice(hint, slice) {
98132
+ const value = slice?.[hint.field];
98133
+ switch (hint.kind) {
98134
+ case "field": return typeof value === "boolean" ? value : null;
98135
+ case "methods": return typeof value === "string" ? !hint.offValues.includes(value) : null;
98136
+ case "mode": return typeof value === "string" ? value !== hint.offValue : null;
98137
+ }
98138
+ }
98139
+ function ownerMethod(owner, name, capName) {
98140
+ const fn = Reflect.get(owner, name);
98141
+ if (typeof fn !== "function") throw new PowerSwitchRefusedError("owner-method-missing", `${capName} provider does not serve '${name}', which its power hint names`);
98142
+ return async (input) => fn.call(owner, input);
98143
+ }
98144
+ async function lastNonOffMode(host, capName, hint) {
98145
+ const isMode = (v) => typeof v === "string" && v.length > 0 && v !== hint.offValue;
98146
+ const fromHub = (await host.readHubSlice(capName).catch(() => null))?.[hint.lastField];
98147
+ if (isMode(fromHub)) return fromHub;
98148
+ const local = host.runtimeState.getCapState(capName)?.[hint.lastField];
98149
+ return isMode(local) ? local : null;
98150
+ }
98151
+ async function applyPower(host, capName, hint, owner, on) {
98152
+ const deviceId = host.deviceId;
98153
+ switch (hint.kind) {
98154
+ case "field":
98155
+ await ownerMethod(owner, hint.set, capName)({
98156
+ deviceId,
98157
+ on
98158
+ });
98159
+ return;
98160
+ case "methods":
98161
+ await ownerMethod(owner, on ? hint.on : hint.off, capName)({ deviceId });
98162
+ return;
98163
+ case "mode": {
98164
+ const set = ownerMethod(owner, hint.set, capName);
98165
+ if (!on) {
98166
+ await set({
98167
+ deviceId,
98168
+ mode: hint.offValue
98169
+ });
98170
+ return;
98171
+ }
98172
+ const mode = await lastNonOffMode(host, capName, hint);
98173
+ if (mode === null) throw new PowerSwitchRefusedError("no-last-mode", `device ${deviceId}: ${capName} has no last non-off mode to restore; pick a mode instead`);
98174
+ await set({
98175
+ deviceId,
98176
+ mode
98177
+ });
98178
+ return;
98179
+ }
98180
+ }
98181
+ }
98182
+ function hintedMethods(hint) {
98183
+ return hint.kind === "methods" ? [hint.on, hint.off] : [hint.set];
98184
+ }
98185
+ /** Whether `owner` serves every method its cap's power hint names. A media
98186
+ * player whose `turnOn` / `turnOff` are not served has no power, so no switch. */
98187
+ function powerServedBy(hint, owner) {
98188
+ if (hint === void 0) return false;
98189
+ return hintedMethods(hint).every((name) => typeof Reflect.get(owner, name) === "function");
98190
+ }
98191
+ /** Builds the `switch` provider and starts the slice projection. Exported
98192
+ * for the specs; providers call `registerPowerSwitch`. */
98193
+ function buildPowerSwitch(host, cap, owner) {
98194
+ const hint = cap.power;
98195
+ if (hint === void 0) throw new Error(`capability '${cap.name}' has no power hint: it has no switch to project`);
98196
+ const read = () => powerOnFromSlice(hint, host.runtimeState.getCapState(cap.name));
98197
+ const project = () => {
98198
+ const on = read();
98199
+ const current = host.runtimeState.getCapState(switchCapability.name);
98200
+ if (current !== void 0 && current["on"] === on) return;
98201
+ const next = {
98202
+ on,
98203
+ lastChangedAt: Date.now()
98204
+ };
98205
+ host.runtimeState.setCapState(switchCapability.name, next);
98206
+ };
98207
+ const stop = host.runtimeState.subscribeCap(cap.name, project);
98208
+ if (host.runtimeState.getCapState(cap.name) !== void 0) project();
98209
+ return {
98210
+ provider: {
98211
+ getStatus: async () => {
98212
+ const current = host.runtimeState.getCapState(switchCapability.name);
98213
+ const lastChangedAt = typeof current?.["lastChangedAt"] === "number" ? current["lastChangedAt"] : 0;
98214
+ return {
98215
+ on: read(),
98216
+ lastChangedAt
98217
+ };
98218
+ },
98219
+ setState: async ({ on }) => applyPower(host, cap.name, hint, owner, on)
98220
+ },
98221
+ dispose: stop
98222
+ };
98223
+ }
98224
+ /**
98225
+ * Registers `switch` for a device whose `cap` carries a `power` hint, built
98226
+ * from `owner` (the provider the device registered for `cap`). Call it right
98227
+ * after `registerNativeCap(cap, owner)`.
98228
+ */
98229
+ function registerPowerSwitch(device, cap, owner) {
98230
+ const deviceId = device.ctx.id;
98231
+ if (cap.power !== void 0 && !powerServedBy(cap.power, owner)) {
98232
+ device.ctx.logger.debug("power switch: owner serves no power control, no switch registered", {
98233
+ tags: { deviceId },
98234
+ meta: { capName: cap.name }
98235
+ });
98236
+ return () => {};
98237
+ }
98238
+ const host = {
98239
+ deviceId,
98240
+ runtimeState: device.runtimeState,
98241
+ registerNativeCap: (c, p) => device.ctx.registerNativeCap(c, p),
98242
+ readHubSlice: async (capName) => {
98243
+ try {
98244
+ return await device.ctx.api.deviceState.getCapSlice.query({
98245
+ deviceId,
98246
+ capName
98247
+ });
98248
+ } catch (err) {
98249
+ device.ctx.logger.warn("power switch: hub slice read failed", {
98250
+ tags: { deviceId },
98251
+ meta: {
98252
+ capName,
98253
+ error: err instanceof Error ? err.message : String(err)
98254
+ }
98255
+ });
98256
+ return null;
98257
+ }
98258
+ }
98259
+ };
98260
+ const built = buildPowerSwitch(host, cap, owner);
98261
+ host.registerNativeCap(switchCapability, built.provider);
98262
+ return built.dispose;
98263
+ }
97707
98264
  Object.freeze({
97708
98265
  "accessories.getStatus": [{
97709
98266
  name: "deviceId",
@@ -98697,6 +99254,11 @@ Object.freeze({
98697
99254
  form: "single",
98698
99255
  optional: false
98699
99256
  }],
99257
+ "fanControl.setOscillationAxis": [{
99258
+ name: "deviceId",
99259
+ form: "single",
99260
+ optional: false
99261
+ }],
98700
99262
  "fanControl.setPercentage": [{
98701
99263
  name: "deviceId",
98702
99264
  form: "single",
@@ -102584,6 +103146,12 @@ Object.defineProperty(exports, "record", {
102584
103146
  return record;
102585
103147
  }
102586
103148
  });
103149
+ Object.defineProperty(exports, "registerPowerSwitch", {
103150
+ enumerable: true,
103151
+ get: function() {
103152
+ return registerPowerSwitch;
103153
+ }
103154
+ });
102587
103155
  Object.defineProperty(exports, "rowsDepsOver", {
102588
103156
  enumerable: true,
102589
103157
  get: function() {