@camstack/addon-provider-bosch 0.1.51 → 0.1.53

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-D0roI_CZ.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: [{
@@ -16181,6 +16242,10 @@ Object.freeze({
16181
16242
  "path": "lastEvent.eventType",
16182
16243
  "valueType": "string"
16183
16244
  },
16245
+ {
16246
+ "path": "lastEvent.sourceId",
16247
+ "valueType": "string"
16248
+ },
16184
16249
  {
16185
16250
  "path": "lastEvent.timestamp",
16186
16251
  "valueType": "number"
@@ -17444,7 +17509,7 @@ Object.freeze({
17444
17509
  }
17445
17510
  });
17446
17511
  //#endregion
17447
- //#region ../types/dist/implemented-methods-M5aguGon.mjs
17512
+ //#region ../types/dist/implemented-methods-5YMDeE3f.mjs
17448
17513
  /**
17449
17514
  * The ffmpeg build every CamStack node downloads, per platform and arch.
17450
17515
  *
@@ -44153,6 +44218,13 @@ var AutomationConditionOperatorSchema = _enum([
44153
44218
  "in"
44154
44219
  ]);
44155
44220
  /**
44221
+ * The stable name of an event leaf. A path (`when.0.1`) moves when a row is
44222
+ * reordered; the id does not, so a `triggered-by` leaf keeps pointing at the
44223
+ * same row. Optional: a leaf nothing refers to needs none.
44224
+ */
44225
+ var EVENT_LEAF_ID_RE = /^[A-Za-z0-9_-]{1,40}$/;
44226
+ var eventLeafId = { id: string().regex(EVENT_LEAF_ID_RE).optional() };
44227
+ /**
44156
44228
  * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
44157
44229
  * subscription: it evaluates against the **state mirror**, which is reconciled,
44158
44230
  * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
@@ -44160,6 +44232,7 @@ var AutomationConditionOperatorSchema = _enum([
44160
44232
  */
44161
44233
  var DeviceBecomesLeafSchema = object({
44162
44234
  kind: literal("device-becomes"),
44235
+ ...eventLeafId,
44163
44236
  deviceId: number$1().int().nonnegative(),
44164
44237
  cap: string().min(1),
44165
44238
  fieldPath: string().min(1),
@@ -44212,6 +44285,7 @@ var DeviceBecomesLeafSchema = object({
44212
44285
  });
44213
44286
  var DeviceEventLeafSchema = object({
44214
44287
  kind: literal("device-event"),
44288
+ ...eventLeafId,
44215
44289
  /** An `EventCategory` value. */
44216
44290
  category: string().min(1),
44217
44291
  deviceId: number$1().int().nonnegative().optional()
@@ -44224,18 +44298,24 @@ var DeviceEventLeafSchema = object({
44224
44298
  */
44225
44299
  var ScheduleLeafSchema = object({
44226
44300
  kind: literal("schedule"),
44301
+ ...eventLeafId,
44227
44302
  at: TimeSpecSchema.optional(),
44228
44303
  days: array(WeekdayKeySchema).min(1).max(7).optional(),
44229
44304
  /** @deprecated legacy cron — new schedules use `at`. */
44230
44305
  cron: string().min(1).max(120).optional()
44231
44306
  }).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") });
44307
+ var ManualLeafSchema = object({
44308
+ kind: literal("manual"),
44309
+ ...eventLeafId
44310
+ });
44233
44311
  var IntervalLeafSchema = object({
44234
44312
  kind: literal("interval"),
44313
+ ...eventLeafId,
44235
44314
  minutes: number$1().int().min(1).max(1440)
44236
44315
  });
44237
44316
  var DetectionLeafSchema = object({
44238
44317
  kind: literal("detection"),
44318
+ ...eventLeafId,
44239
44319
  /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
44240
44320
  subjects: array(DetectionSubjectSchema).min(1),
44241
44321
  deviceIds: array(number$1().int().nonnegative()).min(1),
@@ -44246,6 +44326,7 @@ var DetectionLeafSchema = object({
44246
44326
  });
44247
44327
  var StationaryLeafSchema = object({
44248
44328
  kind: literal("stationary"),
44329
+ ...eventLeafId,
44249
44330
  deviceIds: array(number$1().int().nonnegative()).min(1),
44250
44331
  subjects: array(StationarySubjectSchema).min(1).optional(),
44251
44332
  zones: array(string().min(1)).min(1).optional(),
@@ -44291,6 +44372,41 @@ var StationaryPresentLeafSchema = object({
44291
44372
  minCount: number$1().int().positive().optional()
44292
44373
  });
44293
44374
  /**
44375
+ * True while the site-local clock is inside a daily window and/or on one of
44376
+ * the named days. `days` names the local day a window STARTS on, so
44377
+ * 22:00-06:00 on weekdays is still inside at 01:00 Saturday. A one-date spec
44378
+ * (`kind: 'date'`) is refused: only a clock time or a sun event makes a window
44379
+ * that repeats every day.
44380
+ */
44381
+ var TimeIsLeafSchema = object({
44382
+ kind: literal("time-is"),
44383
+ between: object({
44384
+ from: TimeSpecSchema,
44385
+ to: TimeSpecSchema
44386
+ }).optional(),
44387
+ days: array(WeekdayKeySchema).min(1).max(7).optional()
44388
+ }).superRefine((leaf, ctx) => {
44389
+ if (leaf.between === void 0 && leaf.days === void 0) ctx.addIssue({
44390
+ code: "custom",
44391
+ message: "a time-is leaf needs a between window, days, or both"
44392
+ });
44393
+ for (const end of ["from", "to"]) if (leaf.between?.[end].kind === "date") ctx.addIssue({
44394
+ code: "custom",
44395
+ message: "a one-date time cannot bound a daily window: use a clock time or a sun event",
44396
+ path: ["between", end]
44397
+ });
44398
+ });
44399
+ /**
44400
+ * True for the run that one of the named event leaves started. It reads the
44401
+ * run, not a device: in the tree it is judged against the leaf that fired, in
44402
+ * a step guard against the trigger the run carries. A run no event leaf
44403
+ * started (a state-only tree becoming true) is false.
44404
+ */
44405
+ var TriggeredByLeafSchema = object({
44406
+ kind: literal("triggered-by"),
44407
+ leafIds: array(string().regex(EVENT_LEAF_ID_RE)).min(1).max(20)
44408
+ });
44409
+ /**
44294
44410
  * The escape hatch: the bounded expression engine over named device bindings.
44295
44411
  * Declared as a plain object so a discriminated union can read `kind`; the
44296
44412
  * author-time validation is the SAME function `ExpressionSourceSchema` runs.
@@ -44336,7 +44452,9 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44336
44452
  StationaryLeafSchema,
44337
44453
  DeviceIsLeafSchema,
44338
44454
  StationaryPresentLeafSchema,
44339
- ExpressionLeafSchema
44455
+ ExpressionLeafSchema,
44456
+ TimeIsLeafSchema,
44457
+ TriggeredByLeafSchema
44340
44458
  ]));
44341
44459
  var EVENT_KINDS = new Set([
44342
44460
  "device-becomes",
@@ -44390,6 +44508,28 @@ function hasEventLeaves(node) {
44390
44508
  });
44391
44509
  return found;
44392
44510
  }
44511
+ /** The event leaves of the tree that carry an id, in tree order. */
44512
+ function identifiedEventLeaves(when) {
44513
+ const found = [];
44514
+ forEachLeaf(when, (leaf, path) => {
44515
+ if (isEventLeaf(leaf) && leaf.id !== void 0) found.push({
44516
+ id: leaf.id,
44517
+ path,
44518
+ leaf
44519
+ });
44520
+ });
44521
+ return found;
44522
+ }
44523
+ /** Does the tree hold a `triggered-by` leaf? */
44524
+ function hasTriggeredByLeaves(when) {
44525
+ let found = false;
44526
+ forEachLeaf(when, (leaf) => {
44527
+ if (leaf.kind === "triggered-by") found = true;
44528
+ });
44529
+ return found;
44530
+ }
44531
+ /** Why a `triggered-by` leaf is refused outside an automation: there is no trigger row to name. */
44532
+ 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
44533
  function hasEmptyContainer(node) {
44394
44534
  if (isLeaf(node)) return false;
44395
44535
  if (node.kind === "not") return hasEmptyContainer(node.child);
@@ -44448,6 +44588,11 @@ function validateWhenTree(when) {
44448
44588
  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
44589
  const leaves = countWhenLeaves(when);
44450
44590
  if (leaves > 32) return `the rules use ${leaves} rows — the limit is 32`;
44591
+ const seen = /* @__PURE__ */ new Set();
44592
+ for (const entry of identifiedEventLeaves(when)) {
44593
+ if (seen.has(entry.id)) return `two trigger rows share the id "${entry.id}" — an id names one row`;
44594
+ seen.add(entry.id);
44595
+ }
44451
44596
  const need = eventNeed(when, "when", false);
44452
44597
  if ("error" in need) return need.error;
44453
44598
  const [first, second] = need.paths;
@@ -44659,9 +44804,12 @@ function convertLegacyWhen(half) {
44659
44804
  * every caller — automations, Actions, and the notification centre, which runs
44660
44805
  * only Actions (planning#473).
44661
44806
  */
44807
+ /** The longest one `wait` step lasts. It is cut by a restart of the run and does not survive a hub restart. */
44808
+ var WAIT_MAX_SECONDS = 3600;
44809
+ var WAIT_TOO_LONG = `a wait is at most one hour (${WAIT_MAX_SECONDS} s)`;
44662
44810
  var AutomationWaitStepSchema = object({
44663
44811
  kind: literal("wait"),
44664
- seconds: number$1().min(0).max(300)
44812
+ seconds: number$1().min(0).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG)
44665
44813
  });
44666
44814
  /** The legacy `deviceId` of a cap step: still READ, never written (D983). */
44667
44815
  var LegacyStepDeviceSchema = union([
@@ -44731,28 +44879,227 @@ var elseNeedsIf = {
44731
44879
  path: ["else"]
44732
44880
  };
44733
44881
  var GuardedWaitStepSchema = AutomationWaitStepSchema.extend(stepGuardShape).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44734
- var GuardedCapStepSchema = AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
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, AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44888
+ message: "a cap step needs a target",
44889
+ path: ["target"]
44890
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44891
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
44892
+ path: ["actionId"]
44893
+ }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf)]).superRefine((step, ctx) => {
44894
+ if (step.if !== void 0 && hasTriggeredByLeaves(step.if)) ctx.addIssue({
44895
+ code: "custom",
44896
+ message: TRIGGERED_BY_NEEDS_AUTOMATION,
44897
+ path: ["if"]
44898
+ });
44899
+ });
44900
+ /**
44901
+ * Flow control of an AUTOMATION's list (#500). A standalone list (an action, a
44902
+ * scheduler point, a preset) stays `wait` and `cap`: it has no run of its own
44903
+ * to end and no variable bag to write.
44904
+ */
44905
+ var continueOnErrorShape = {
44906
+ /** A failure of this step is traced `failed-continued` and the next step runs. */
44907
+ continueOnError: literal(true).optional() };
44908
+ /** An automation's cap step: the shared guarded cap step, plus `continueOnError`. */
44909
+ var AutomationGuardedCapStepSchema = AutomationCapStepBaseSchema.extend({
44910
+ ...stepGuardShape,
44911
+ ...continueOnErrorShape
44912
+ }).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44735
44913
  message: "a cap step needs a target",
44736
44914
  path: ["target"]
44737
44915
  }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44738
44916
  message: "a domain target needs the vocabulary action it applies to (actionId)",
44739
44917
  path: ["actionId"]
44740
44918
  }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44741
- var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, GuardedCapStepSchema]);
44919
+ /** End the run here, on purpose. `reason` may carry `{{vars.x}}` slots. */
44920
+ var AutomationStopStepSchema = object({
44921
+ kind: literal("stop"),
44922
+ reason: string().min(1, "a stop needs a reason: it is what the run trace shows").max(200),
44923
+ /** `ok`: the run ends as a success. `failed`: it ends as a failure, with the reason. */
44924
+ outcome: _enum(["ok", "failed"]),
44925
+ /** Stop only when this reads true; false or unknown does not stop. */
44926
+ if: AutomationWhenSchema.optional()
44927
+ });
44928
+ var SET_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,39}$/;
44929
+ /** Names the run itself writes and reads (`leaf` is what `triggered-by` judges): a step may not overwrite them. */
44930
+ var RESERVED_RUN_VARS = [
44931
+ "kind",
44932
+ "leaf",
44933
+ "at"
44934
+ ];
44935
+ /**
44936
+ * Write variables into the run's bag, for the steps after it. SUBSTITUTION
44937
+ * only, never evaluation: a text value may carry `{{vars.x}}` slots.
44938
+ */
44939
+ var AutomationSetStepSchema = object({
44940
+ kind: literal("set"),
44941
+ vars: record(string(), union([
44942
+ string().max(500),
44943
+ number$1(),
44944
+ boolean()
44945
+ ])).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` }),
44946
+ /** Set only when this reads true; false or unknown leaves the bag as it is. */
44947
+ if: AutomationWhenSchema.optional()
44948
+ });
44949
+ var AutomationRunActionStepSchema = object({
44950
+ kind: literal("action"),
44951
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
44952
+ actionId: string().min(1),
44953
+ /** Values for the action's declared variables; the others take their default. */
44954
+ variables: record(string(), union([
44955
+ string(),
44956
+ number$1(),
44957
+ boolean()
44958
+ ])).optional(),
44959
+ ...continueOnErrorShape
44960
+ });
44961
+ /**
44962
+ * A step INSIDE a branch of a `choose`: a flow step with no condition of its
44963
+ * own (the branch is the condition) and no block inside it. Strict, so an `if`
44964
+ * written on such a step is refused rather than dropped.
44965
+ */
44966
+ /** The longest a `wait-until` may wait: six hours. */
44967
+ var WAIT_UNTIL_TIMEOUT_MAX_S = 21600;
44968
+ /**
44969
+ * Pause the run until a condition is true (#500). The timeout is mandatory: a
44970
+ * run never waits without an end. `onTimeout` says what a wait that ran out
44971
+ * does: `continue` goes on to the next step, `stop` ends the run as a failure.
44972
+ * A condition that cannot be read is not "true": the wait goes on (D49).
44973
+ */
44974
+ var AutomationWaitUntilStepSchema = object({
44975
+ kind: literal("wait-until"),
44976
+ when: AutomationWhenSchema,
44977
+ 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)`),
44978
+ onTimeout: _enum(["continue", "stop"])
44979
+ }).strict();
44980
+ /** The step kinds that hold steps of their own, or pause on a condition: they nest two levels at most. */
44981
+ var BLOCK_STEP_KINDS = [
44982
+ "choose",
44983
+ "repeat",
44984
+ "parallel",
44985
+ "wait-until"
44986
+ ];
44987
+ var NEST_TOO_DEEP = `steps nest two levels at most: a step inside a nested choose or repeat can be a plain step only`;
44988
+ function kindOf(input) {
44989
+ return typeof input === "object" && input !== null ? Reflect.get(input, "kind") : void 0;
44990
+ }
44991
+ /** Where the nesting ends, a block step is refused in words rather than as an unknown kind. */
44992
+ function nestTooDeep(issue) {
44993
+ const kind = kindOf(issue.input);
44994
+ return typeof kind === "string" && BLOCK_STEP_KINDS.includes(kind) ? NEST_TOO_DEEP : void 0;
44995
+ }
44996
+ var plainStepSchemas = [
44997
+ AutomationWaitStepSchema.strict(),
44998
+ AutomationCapStepBaseSchema.extend(continueOnErrorShape).strict().refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44999
+ message: "a cap step needs a target",
45000
+ path: ["target"]
45001
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
45002
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
45003
+ path: ["actionId"]
45004
+ }),
45005
+ AutomationStopStepSchema.omit({ if: true }).strict(),
45006
+ AutomationSetStepSchema.omit({ if: true }).strict(),
45007
+ AutomationRunActionStepSchema.strict()
45008
+ ];
45009
+ /**
45010
+ * A plain step: a flow step with no condition of its own and no step inside
45011
+ * it. The second (last) level of nesting holds these only.
45012
+ */
45013
+ var AutomationBranchStepSchema = discriminatedUnion("kind", plainStepSchemas, { error: nestTooDeep });
45014
+ /** A step that can run beside others: it writes no variable and does not end the run. */
45015
+ 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");
45016
+ /**
45017
+ * Several steps at once (#500): all are started together and all are awaited.
45018
+ * A failure fails the run once the others finished, as in a sequence; a member
45019
+ * marked "continue if this fails" does not.
45020
+ */
45021
+ var AutomationParallelStepSchema = object({
45022
+ kind: literal("parallel"),
45023
+ steps: array(AutomationParallelMemberSchema).min(2, "a parallel needs at least two steps").max(5, `a parallel takes at most 5 steps`)
45024
+ }).strict();
45025
+ var REPEAT_NEEDS_MAX = "a repeat on a condition needs a maximum number of iterations";
45026
+ /** A loop on a condition: the bound is mandatory, so a run never loops without an end. */
45027
+ var repeatConditionShape = {
45028
+ when: AutomationWhenSchema,
45029
+ 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`),
45030
+ /** A pause between two iterations, never after the last. */
45031
+ waitS: number$1().int().min(1).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG).optional()
45032
+ };
45033
+ /** What decides how many times a `repeat` runs. */
45034
+ var AutomationRepeatLoopSchema = discriminatedUnion("by", [
45035
+ object({
45036
+ by: literal("count"),
45037
+ count: number$1().int().min(1).max(20, `a repeat runs at most 20 times`)
45038
+ }).strict(),
45039
+ object({
45040
+ by: literal("while"),
45041
+ ...repeatConditionShape
45042
+ }).strict(),
45043
+ object({
45044
+ by: literal("until"),
45045
+ ...repeatConditionShape
45046
+ }).strict(),
45047
+ object({
45048
+ by: literal("forEach"),
45049
+ target: object({
45050
+ kind: literal("devices"),
45051
+ 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`)
45052
+ }).strict()
45053
+ }).strict()
45054
+ ]);
45055
+ /**
45056
+ * The same steps several times (#500). Every form is bounded at save, so a
45057
+ * run cannot loop without an end. A failure or a `stop` in the steps ends the
45058
+ * loop and the run, as in a sequence.
45059
+ */
45060
+ function repeatSchemaOf(step) {
45061
+ return object({
45062
+ kind: literal("repeat"),
45063
+ loop: AutomationRepeatLoopSchema,
45064
+ steps: array(step).min(1, "a repeat needs at least one step").max(10, `a repeat takes at most 10 steps`)
45065
+ }).strict();
45066
+ }
45067
+ /**
45068
+ * One step, several ways (#500): the branches are read in order and the FIRST
45069
+ * whose condition is true runs; `else` runs when every branch is false. A
45070
+ * branch that cannot be read ends the choice: nothing runs, not the branches
45071
+ * after it and not the else (D49). The #472 `if` / `else` of a wait or cap
45072
+ * step is the one-branch case and stays as it is stored.
45073
+ */
45074
+ function chooseSchemaOf(step) {
45075
+ const steps = array(step).max(10);
45076
+ return object({
45077
+ kind: literal("choose"),
45078
+ branches: array(object({
45079
+ if: AutomationWhenSchema,
45080
+ steps: steps.min(1, "a branch needs at least one step")
45081
+ })).min(1, "a choose needs at least one branch").max(6, `a choose takes at most 6 branches`),
45082
+ else: steps.optional()
45083
+ });
45084
+ }
45085
+ var AutomationBodyStepSchema = discriminatedUnion("kind", [
45086
+ ...plainStepSchemas,
45087
+ AutomationWaitUntilStepSchema,
45088
+ AutomationParallelStepSchema,
45089
+ chooseSchemaOf(AutomationBranchStepSchema),
45090
+ repeatSchemaOf(AutomationBranchStepSchema)
45091
+ ]);
45092
+ var AutomationRepeatStepSchema = repeatSchemaOf(AutomationBodyStepSchema);
44742
45093
  var AutomationActionSchema = discriminatedUnion("kind", [
44743
45094
  GuardedWaitStepSchema,
44744
- GuardedCapStepSchema,
44745
- object({
44746
- kind: literal("action"),
44747
- /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
44748
- actionId: string().min(1),
44749
- /** Values for the action's declared variables; the others take their default. */
44750
- variables: record(string(), union([
44751
- string(),
44752
- number$1(),
44753
- boolean()
44754
- ])).optional()
44755
- }),
45095
+ AutomationGuardedCapStepSchema,
45096
+ AutomationStopStepSchema,
45097
+ AutomationSetStepSchema,
45098
+ chooseSchemaOf(AutomationBodyStepSchema),
45099
+ AutomationWaitUntilStepSchema,
45100
+ AutomationParallelStepSchema,
45101
+ AutomationRepeatStepSchema,
45102
+ AutomationRunActionStepSchema,
44756
45103
  object({
44757
45104
  kind: literal("code"),
44758
45105
  /** Compiled into the automation's OWN block by esbuild — not a third
@@ -44815,11 +45162,6 @@ function convertLegacyRecipe(legacy) {
44815
45162
  ...rest
44816
45163
  };
44817
45164
  }
44818
- /**
44819
- * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
44820
- * saved block carries): the new shape as it is, or the old shape converted to
44821
- * it. The output type is always the new one.
44822
- */
44823
45165
  var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
44824
45166
  var DETECTION_DESCRIPTIONS = {
44825
45167
  deviceId: "the camera that saw it",
@@ -44835,6 +45177,11 @@ var STATIONARY_DESCRIPTIONS = {
44835
45177
  phase: "appeared or departed",
44836
45178
  entryId: "the stopped object"
44837
45179
  };
45180
+ var BECOMES_DESCRIPTIONS = {
45181
+ deviceId: "the device that changed",
45182
+ from: "the value before the change, empty when it is not known",
45183
+ to: "the value it changed to"
45184
+ };
44838
45185
  function describe$1(descriptions) {
44839
45186
  return Object.entries(descriptions).map(([name, description]) => ({
44840
45187
  name,
@@ -44843,6 +45190,10 @@ function describe$1(descriptions) {
44843
45190
  }
44844
45191
  describe$1(DETECTION_DESCRIPTIONS);
44845
45192
  describe$1(STATIONARY_DESCRIPTIONS);
45193
+ describe$1(BECOMES_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
45194
+ ...v,
45195
+ hintKey: "becomesDeviceId"
45196
+ } : v);
44846
45197
  new Set(Object.values(COCO_TO_MACRO.mapping));
44847
45198
  /**
44848
45199
  * Schedulers (D983) — an entity that performs actions over the day.
@@ -44867,6 +45218,13 @@ var SchedulerStateTreeSchema = AutomationWhenSchema.superRefine((when, ctx) => {
44867
45218
  });
44868
45219
  return;
44869
45220
  }
45221
+ if (hasTriggeredByLeaves(when)) {
45222
+ ctx.addIssue({
45223
+ code: "custom",
45224
+ message: TRIGGERED_BY_NEEDS_AUTOMATION
45225
+ });
45226
+ return;
45227
+ }
44870
45228
  const problem = validateWhenTree(when);
44871
45229
  if (problem !== null) ctx.addIssue({
44872
45230
  code: "custom",
@@ -44888,12 +45246,16 @@ var HoldOverrideSchema = discriminatedUnion("mode", [object({ mode: literal("res
44888
45246
  })]);
44889
45247
  /** A missed edge up to this late still runs. */
44890
45248
  var DEFAULT_MAX_LATE_MS = 10 * 6e4;
45249
+ var SchedulerStepSchema = ActionStepSchema.refine((step) => step.kind !== "wait" || step.seconds <= 300, {
45250
+ message: `a scheduler point waits at most five minutes (300 s); put a longer wait in an action`,
45251
+ path: ["seconds"]
45252
+ });
44891
45253
  var SchedulerPointSchema = object({
44892
45254
  id: string().min(1).max(64),
44893
45255
  at: SchedulerTimeSpecSchema,
44894
45256
  mode: SchedulerPointModeSchema,
44895
45257
  /** The actions of an `edge` or `hold`; a `release` has none. */
44896
- steps: array(ActionStepSchema).max(20).default([]),
45258
+ steps: array(SchedulerStepSchema).max(20).default([]),
44897
45259
  /** Checked when the point runs (an edge) or is applied (a hold). */
44898
45260
  conditions: SchedulerStateTreeSchema.optional(),
44899
45261
  /** Hold only; absent means `respect`. */
@@ -45087,6 +45449,8 @@ var AutomationConditionTraceNodeSchema = object({
45087
45449
  "device-is",
45088
45450
  "expression",
45089
45451
  "stationary-present",
45452
+ "time-is",
45453
+ "triggered-by",
45090
45454
  "device-becomes",
45091
45455
  "device-event",
45092
45456
  "schedule",
@@ -45159,18 +45523,46 @@ var StepTraceBaseSchema = object({
45159
45523
  ]).optional(),
45160
45524
  /** Why the condition read what it read, in the evaluator's own words. */
45161
45525
  conditionReason: string().optional(),
45162
- /** `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. */
45526
+ /** `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). */
45163
45527
  outcome: _enum([
45164
45528
  "ok",
45165
45529
  "error",
45166
45530
  "not-run",
45167
45531
  "empty",
45168
- "skipped"
45532
+ "skipped",
45533
+ "failed-continued",
45534
+ "timed-out"
45169
45535
  ]),
45170
45536
  message: string().optional()
45171
45537
  });
45538
+ /** What a block step adds to its row: which list ran, what each branch read, how many rounds. */
45539
+ var blockTraceShape = {
45540
+ /** A `choose` step (#500): the list that ran, `branches.<n>` or `else`. Absent when none did. */
45541
+ branch: string().optional(),
45542
+ /** A `choose` step: what each branch read, in order, up to the one that decided. */
45543
+ branchChecks: array(object({
45544
+ condition: _enum([
45545
+ "true",
45546
+ "false",
45547
+ "unknown"
45548
+ ]),
45549
+ conditionReason: string().optional()
45550
+ })).optional(),
45551
+ /** A `repeat` step (#500): how many times its steps ran. */
45552
+ iterations: number$1().int().optional()
45553
+ };
45554
+ /** A step one level down: it may be a block step itself, whose own steps are plain rows (the nesting ends there). */
45555
+ var NestedStepTraceSchema = StepTraceBaseSchema.extend({
45556
+ ...blockTraceShape,
45557
+ steps: array(StepTraceBaseSchema).optional()
45558
+ });
45172
45559
  /** A step trace; the `else` steps that ran are one level of the same shape (the nesting is bounded at one). */
45173
- var AutomationStepTraceSchema = StepTraceBaseSchema.extend({ else: array(StepTraceBaseSchema).optional() });
45560
+ var AutomationStepTraceSchema = StepTraceBaseSchema.extend({
45561
+ else: array(StepTraceBaseSchema).optional(),
45562
+ ...blockTraceShape,
45563
+ /** A `choose` step: the steps of the list that ran. A `parallel`: its members. A `repeat`: the LAST iteration. */
45564
+ steps: array(NestedStepTraceSchema).optional()
45565
+ });
45174
45566
  var AutomationRunTraceSchema = object({
45175
45567
  runId: string().min(1),
45176
45568
  startedAt: number$1(),
@@ -45420,7 +45812,7 @@ object({
45420
45812
  * is irrelevant; the array is treated as a set by consumers. */
45421
45813
  hiddenChildIds: array(number$1()).readonly()
45422
45814
  });
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({
45815
+ 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
45816
  deviceId: number$1().int().nonnegative(),
45425
45817
  childDeviceId: number$1().int().nonnegative(),
45426
45818
  hidden: boolean()
@@ -45577,7 +45969,9 @@ var AddonHttpRouteSchema = object({
45577
45969
  ]).optional(),
45578
45970
  description: string().optional(),
45579
45971
  /** The hub limits this route per client address before the handler runs. */
45580
- rateLimit: _enum(["auth"]).optional()
45972
+ rateLimit: _enum(["auth"]).optional(),
45973
+ /** Served once per integration instance; the handler reads `instanceId`. */
45974
+ perInstance: boolean().optional()
45581
45975
  });
45582
45976
  /**
45583
45977
  * Cross-process route invocation envelope. The hub captures the
@@ -45594,6 +45988,8 @@ var InvokeRequestSchema = object({
45594
45988
  query: record(string(), string()),
45595
45989
  body: unknown(),
45596
45990
  headers: record(string(), string()),
45991
+ /** The instance the hub resolved for a `perInstance` route. */
45992
+ instanceId: string().optional(),
45597
45993
  user: object({
45598
45994
  id: string(),
45599
45995
  username: string(),
@@ -47718,7 +48114,7 @@ var BATTERY_DEVICE_TYPES = [
47718
48114
  DeviceType.Switch,
47719
48115
  DeviceType.PetFeeder,
47720
48116
  DeviceType.Container,
47721
- DeviceType.Thermostat,
48117
+ DeviceType.Climate,
47722
48118
  DeviceType.EventEmitter
47723
48119
  ];
47724
48120
  /** Generic Customize target for the percentage reading (D936). */
@@ -49481,7 +49877,14 @@ var climateControlCapability = {
49481
49877
  scope: "device",
49482
49878
  deviceNative: true,
49483
49879
  mode: "singleton",
49484
- deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
49880
+ deviceTypes: [DeviceType.Climate],
49881
+ power: {
49882
+ kind: "mode",
49883
+ field: "mode",
49884
+ offValue: "off",
49885
+ set: "setMode",
49886
+ lastField: "lastActiveMode"
49887
+ },
49485
49888
  methods: {
49486
49889
  setMode: method(object({
49487
49890
  deviceId: number$1().int().nonnegative(),
@@ -49866,6 +50269,7 @@ var CompositionProblemSchema = object({
49866
50269
  "nested-field",
49867
50270
  "missing-field",
49868
50271
  "kind-mismatch",
50272
+ "source-kind-mismatch",
49869
50273
  "may-be-null",
49870
50274
  "source-missing",
49871
50275
  "source-unreadable",
@@ -49887,7 +50291,8 @@ var CompositionProblemSchema = object({
49887
50291
  "command-method-unknown",
49888
50292
  "command-source-unknown",
49889
50293
  "command-input-unknown",
49890
- "command-input-missing"
50294
+ "command-input-missing",
50295
+ "power-source-is-actuator"
49891
50296
  ]),
49892
50297
  path: string(),
49893
50298
  message: string()
@@ -50925,6 +51330,13 @@ var mediaPlayerCapability = {
50925
51330
  deviceNative: true,
50926
51331
  mode: "singleton",
50927
51332
  deviceTypes: [DeviceType.MediaPlayer],
51333
+ power: {
51334
+ kind: "methods",
51335
+ field: "state",
51336
+ offValues: ["off", "standby"],
51337
+ on: "turnOn",
51338
+ off: "turnOff"
51339
+ },
50928
51340
  methods: {
50929
51341
  turnOn: optionalMethod(deviceOnly, _void(), {
50930
51342
  kind: "mutation",
@@ -51055,7 +51467,7 @@ var mediaPlayerCapability = {
51055
51467
  isMuted: { kind: "boolean" }
51056
51468
  } }
51057
51469
  };
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;
51470
+ 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
51471
  var NUMBER = {
51060
51472
  kind: "fixed",
51061
51473
  result: ["number"]
@@ -51138,7 +51550,7 @@ function numeric(input) {
51138
51550
  }, ...alsoFrom === void 0 ? [] : [alsoFrom.accept]]
51139
51551
  };
51140
51552
  }
51141
- var CLIMATE_DEVICES = [DeviceType.Thermostat, DeviceType.Climate];
51553
+ var CLIMATE_DEVICES = [DeviceType.Climate];
51142
51554
  function binary(input) {
51143
51555
  return {
51144
51556
  ...input,
@@ -51460,8 +51872,8 @@ var GROUP_TEMPLATES = [
51460
51872
  actuator({
51461
51873
  id: "climate",
51462
51874
  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],
51875
+ deviceType: DeviceType.Climate,
51876
+ memberDeviceTypes: [DeviceType.Climate],
51465
51877
  outputCap: "group-active",
51466
51878
  outputField: "active",
51467
51879
  member: {
@@ -51504,6 +51916,118 @@ function actuator(input) {
51504
51916
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
51505
51917
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
51506
51918
  DeviceType.Camera;
51919
+ var PowerSourceRefusalReasonSchema = _enum([
51920
+ "no-power-source",
51921
+ "switch-missing",
51922
+ "switch-offline",
51923
+ "switch-unknown",
51924
+ "restart-in-progress",
51925
+ "switch-call-failed",
51926
+ "switch-is-actuator"
51927
+ ]);
51928
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
51929
+ accepted: literal(false),
51930
+ reason: PowerSourceRefusalReasonSchema,
51931
+ /** One line for the operator, naming the switch. */
51932
+ message: string()
51933
+ })]);
51934
+ var PowerSourceInfoSchema = object({
51935
+ /** False when the device has no power source linked; the rest is then empty. */
51936
+ linked: boolean(),
51937
+ switchDeviceId: number$1().nullable(),
51938
+ switchName: string().nullable(),
51939
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
51940
+ switchOn: boolean().nullable(),
51941
+ switchOnline: boolean().nullable(),
51942
+ restartInProgress: boolean(),
51943
+ offSeconds: number$1().int()
51944
+ });
51945
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
51946
+ var PowerSourceStateSchema = object({
51947
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
51948
+ switchOn: boolean().nullable(),
51949
+ offSeconds: number$1().int(),
51950
+ lastFetchedAt: number$1()
51951
+ });
51952
+ var POWER_SOURCE_DEVICE_TYPES = [
51953
+ DeviceType.Gateway,
51954
+ DeviceType.Camera,
51955
+ DeviceType.Sensor,
51956
+ DeviceType.MediaPlayer,
51957
+ DeviceType.Vacuum,
51958
+ DeviceType.Generic,
51959
+ DeviceType.Washer,
51960
+ DeviceType.Dryer,
51961
+ DeviceType.Dishwasher,
51962
+ DeviceType.PetFountain,
51963
+ DeviceType.Fan,
51964
+ DeviceType.Humidifier,
51965
+ DeviceType.Climate,
51966
+ DeviceType.WaterHeater
51967
+ ];
51968
+ var powerSourceCustomize = {
51969
+ version: 1,
51970
+ targets: [{
51971
+ id: "switch",
51972
+ cardinality: "scalar",
51973
+ vocabulary: {
51974
+ cap: "power-source",
51975
+ stateField: "switchOn"
51976
+ },
51977
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
51978
+ apply: "add",
51979
+ path: "switchOn",
51980
+ sources: [{
51981
+ kind: "switch",
51982
+ transforms: [{ kind: "identity" }]
51983
+ }],
51984
+ companions: [{
51985
+ kind: "constant",
51986
+ path: "offSeconds",
51987
+ value: 10,
51988
+ when: "add",
51989
+ formerValues: [0]
51990
+ }, {
51991
+ kind: "clock",
51992
+ path: "lastFetchedAt",
51993
+ when: "always"
51994
+ }],
51995
+ sourceReport: { status: "assignment-status" }
51996
+ }]
51997
+ };
51998
+ var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
51999
+ var powerSourceCapability = {
52000
+ name: "power-source",
52001
+ scope: "device",
52002
+ mode: "singleton",
52003
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
52004
+ methods: {
52005
+ getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "protected" }),
52006
+ powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52007
+ kind: "mutation",
52008
+ auth: "protected"
52009
+ }),
52010
+ powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52011
+ kind: "mutation",
52012
+ auth: "protected"
52013
+ }),
52014
+ forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52015
+ kind: "mutation",
52016
+ auth: "protected"
52017
+ })
52018
+ },
52019
+ runtimeState: PowerSourceStateSchema,
52020
+ customize: powerSourceCustomize,
52021
+ /**
52022
+ * Runtime-state durability: **session** — mirrored from the linked switch on
52023
+ * its next report; a restored value would outlive the reading it names.
52024
+ *
52025
+ * See `RuntimeStateDurability`. Enforced by
52026
+ * `scripts/check-runtime-state-durability.ts`.
52027
+ */
52028
+ durability: "session",
52029
+ history: "none"
52030
+ };
51507
52031
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
51508
52032
  /**
51509
52033
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -54038,8 +54562,14 @@ method(object({
54038
54562
  "string",
54039
54563
  "number",
54040
54564
  "boolean",
54041
- "enum"
54565
+ "enum",
54566
+ "union"
54042
54567
  ]),
54568
+ unionMembers: array(_enum([
54569
+ "string",
54570
+ "number",
54571
+ "boolean"
54572
+ ])).optional(),
54043
54573
  enumValues: array(string()).optional(),
54044
54574
  item: boolean().optional(),
54045
54575
  quantity: ReadingQuantitySchema.optional()
@@ -56131,25 +56661,81 @@ var enumSensorCapability = {
56131
56661
  * whether persisting is worth a SQLite commit. */
56132
56662
  volatileStateFields: ["lastFetchedAt", "timestamp"]
56133
56663
  };
56664
+ var EventVocabularyKindSchema = _enum([
56665
+ "press",
56666
+ "long-press",
56667
+ "long-press-start",
56668
+ "release",
56669
+ "repeat",
56670
+ "double-press",
56671
+ "other"
56672
+ ]);
56673
+ /** The part of the device an event comes from: a button, a channel. */
56674
+ var EventVocabularySourceSchema = object({
56675
+ id: string().min(1),
56676
+ label: string().min(1)
56677
+ });
56678
+ /** One scalar field of an event's `data`, by dotted path. */
56679
+ var EventVocabularyFieldSchema = object({
56680
+ path: string().min(1),
56681
+ kind: _enum([
56682
+ "string",
56683
+ "number",
56684
+ "boolean",
56685
+ "enum"
56686
+ ]),
56687
+ enumValues: array(string()).optional(),
56688
+ label: string().min(1).optional()
56689
+ });
56690
+ var EventVocabularyEntrySchema = object({
56691
+ /** The native token, verbatim. */
56692
+ id: string().min(1),
56693
+ source: EventVocabularySourceSchema.optional(),
56694
+ label: string().min(1),
56695
+ description: string().optional(),
56696
+ kind: EventVocabularyKindSchema.optional(),
56697
+ payload: array(EventVocabularyFieldSchema).optional()
56698
+ });
56699
+ /**
56700
+ * The vocabulary of a source that declares tokens and nothing else: one
56701
+ * entry per token, labelled with the token itself.
56702
+ */
56703
+ function plainEventVocabulary(eventTypes) {
56704
+ return [...new Set(eventTypes)].filter((eventType) => eventType !== "").map((eventType) => ({
56705
+ id: eventType,
56706
+ label: eventType
56707
+ }));
56708
+ }
56134
56709
  /**
56135
56710
  * Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
56136
56711
  * device. Carries the device's EXACT declared event vocabulary verbatim
56137
56712
  * (NO normalization). Two shapes:
56138
- * - structured source (HA `event.*` entity) → `eventTypes` is the fixed
56139
- * declared vocabulary (e.g. ['press_short','press_long',...]).
56140
- * - generic/legacy source (HA bus events, e.g. zha_event) → `eventTypes`
56713
+ * - structured source (HA `event.*` entity) → `vocabulary` is the fixed
56714
+ * declared set (e.g. press_short, press_long, …).
56715
+ * - generic/legacy source (HA bus events, e.g. zha_event) → `vocabulary`
56141
56716
  * is [] (open) and `lastEvent.data` carries the complex payload.
56717
+ * `vocabulary` (D1044) names each event for a person and types it for a
56718
+ * rule; `eventTypes` is its list of native tokens, kept for older readers.
56719
+ * A fire names the part of the device it came from in `sourceId` when the
56720
+ * vocabulary declares sources (a remote's buttons).
56142
56721
  * `seq` is monotonic so two identical events still produce distinct slice writes.
56143
56722
  */
56144
56723
  var EventFireSchema = object({
56145
56724
  deviceId: number$1(),
56146
56725
  eventType: string(),
56726
+ sourceId: string().optional(),
56147
56727
  data: record(string(), unknown()).nullable(),
56148
56728
  timestamp: number$1().meta(readingMeta("none")),
56149
56729
  seq: number$1().meta(readingMeta("count"))
56150
56730
  });
56151
56731
  var EventEmitterStatusSchema = object({
56152
56732
  eventTypes: array(string()),
56733
+ /**
56734
+ * Required of a provider by the type. A status written before the field
56735
+ * existed still parses, as an empty vocabulary: read it through
56736
+ * `readEventVocabulary`, which then answers with the plain tokens.
56737
+ */
56738
+ vocabulary: array(EventVocabularyEntrySchema).default([]),
56153
56739
  lastEvent: EventFireSchema.nullable(),
56154
56740
  eventCountSinceStart: number$1().meta(readingMeta("count"))
56155
56741
  });
@@ -56667,8 +57253,85 @@ method(_void(), array(FailureContributionSchema).readonly());
56667
57253
  * - `FanPreset` — preset selector
56668
57254
  * - `FanDirection` — direction toggle (ceiling fans)
56669
57255
  * - `FanOscillating` — oscillation toggle
57256
+ *
57257
+ * Per-axis oscillation (`oscillationAxes`, `setOscillationAxis`, D1034): an air
57258
+ * circulator sweeps horizontally and vertically, each axis with its own
57259
+ * switch, fixed angle, sweep bounds and limits. A provider may report them
57260
+ * natively; otherwise the OPERATOR links sibling devices to each field in
57261
+ * Customize (a `replace` over the fields listed in `overridable` — nothing is
57262
+ * linked or inferred by itself), and a write to a linked field is forwarded to
57263
+ * its source. `oscillating` stays the single summary flag: when axes exist it
57264
+ * means "any axis oscillates" (`anyAxisOscillating`).
56670
57265
  */
56671
57266
  var FanDirectionSchema = _enum(["forward", "reverse"]);
57267
+ var FAN_OSCILLATION_AXES = ["horizontal", "vertical"];
57268
+ var FanOscillationAxisNameSchema = _enum(FAN_OSCILLATION_AXES);
57269
+ var FanOscillationAxisFieldSchema = _enum([
57270
+ "oscillating",
57271
+ "angle",
57272
+ "sweepFrom",
57273
+ "sweepTo"
57274
+ ]);
57275
+ /** The limits of one axis, read from the source that serves its angle. */
57276
+ var FAN_OSCILLATION_AXIS_LIMITS = [
57277
+ "min",
57278
+ "max",
57279
+ "step"
57280
+ ];
57281
+ /**
57282
+ * One oscillation axis. Every reading the device has not reported is `null`,
57283
+ * never `0` (D393): `0°` is a real position. Each leaf defaults to `null`, so
57284
+ * a slice that names only the leaves it knows (a Customize link of one field)
57285
+ * is still a whole axis.
57286
+ */
57287
+ var FanOscillationAxisSchema = object({
57288
+ /** Whether the axis sweeps. */
57289
+ oscillating: boolean().nullable().default(null),
57290
+ /** The fixed position (degrees) the axis holds when it does not sweep. */
57291
+ angle: number$1().nullable().default(null),
57292
+ /** The sweep bounds (degrees): left → right, bottom → top. */
57293
+ sweepFrom: number$1().nullable().default(null),
57294
+ sweepTo: number$1().nullable().default(null),
57295
+ /** The angles the axis accepts. Null when the device states none. */
57296
+ min: number$1().nullable().default(null),
57297
+ max: number$1().nullable().default(null),
57298
+ step: number$1().nullable().default(null)
57299
+ });
57300
+ /** Both axes; an axis the device does not have is `null`. */
57301
+ var FanOscillationAxesSchema = object({
57302
+ horizontal: FanOscillationAxisSchema.nullable().default(null),
57303
+ vertical: FanOscillationAxisSchema.nullable().default(null)
57304
+ });
57305
+ var FanOscillationAxisRefusalSchema = _enum([
57306
+ "nothing-to-set",
57307
+ "axis-not-linked",
57308
+ "out-of-range",
57309
+ "source-gone",
57310
+ "source-unknown",
57311
+ "source-call-failed"
57312
+ ]);
57313
+ var FanOscillationAxisResultSchema = discriminatedUnion("status", [object({ status: literal("applied") }), object({
57314
+ status: literal("refused"),
57315
+ reason: FanOscillationAxisRefusalSchema,
57316
+ /** The field at fault, when one is. */
57317
+ field: FanOscillationAxisFieldSchema.optional(),
57318
+ /** One line for the operator, naming the axis, the field and the source. */
57319
+ detail: string(),
57320
+ /** Fields that DID land before the refusal (writes run in order, stop at the first failure). */
57321
+ applied: array(FanOscillationAxisFieldSchema)
57322
+ })]);
57323
+ var SetOscillationAxisInputSchema = object({
57324
+ deviceId: number$1().int().nonnegative(),
57325
+ axis: FanOscillationAxisNameSchema,
57326
+ oscillating: boolean().optional(),
57327
+ angle: number$1().optional(),
57328
+ sweepFrom: number$1().optional(),
57329
+ sweepTo: number$1().optional()
57330
+ });
57331
+ /** The state path of one axis field: `oscillationAxes.<axis>.<field>`. */
57332
+ function fanAxisPath(axis, field) {
57333
+ return `oscillationAxes.${axis}.${field}`;
57334
+ }
56672
57335
  var FanControlStatusSchema = object({
56673
57336
  /** Active speed as 0..100 inclusive. Null when the device has no
56674
57337
  * speed surface (single-speed fan). */
@@ -56687,9 +57350,102 @@ var FanControlStatusSchema = object({
56687
57350
  /** Oscillation toggle. Null when the device has no oscillation
56688
57351
  * surface. */
56689
57352
  oscillating: boolean().nullable(),
57353
+ /**
57354
+ * Per-axis oscillation (air circulators). Absent when the device has no
57355
+ * per-axis surface and nothing is linked in Customize.
57356
+ */
57357
+ oscillationAxes: FanOscillationAxesSchema.optional(),
56690
57358
  /** Ms epoch when the slice was last updated. */
56691
57359
  lastChangedAt: number$1()
56692
57360
  });
57361
+ var FAN_CUSTOMIZE_DEVICE_TYPES = [DeviceType.Fan];
57362
+ /** The Customize section the axis targets are listed under. */
57363
+ var AXES_SECTION = "oscillation-axes";
57364
+ function axisOscillatingTarget(axis) {
57365
+ return {
57366
+ id: `${axis}-oscillating`,
57367
+ cardinality: "scalar",
57368
+ vocabulary: {
57369
+ cap: "fan-control",
57370
+ stateField: "oscillating"
57371
+ },
57372
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57373
+ apply: "replace",
57374
+ section: AXES_SECTION,
57375
+ path: fanAxisPath(axis, "oscillating"),
57376
+ sources: [{
57377
+ kind: "switch",
57378
+ transforms: [{ kind: "identity" }]
57379
+ }],
57380
+ sourceReport: { status: "assignment-status" }
57381
+ };
57382
+ }
57383
+ /**
57384
+ * A numeric axis field. The `angle` link also carries the axis limits, read
57385
+ * from the SAME linked control's own range (`min`/`max`/`step`), so the limits
57386
+ * are the source's, never guessed.
57387
+ */
57388
+ function axisNumericTarget(axis, field) {
57389
+ const base = {
57390
+ id: `${axis}-${field}`,
57391
+ cardinality: "scalar",
57392
+ vocabulary: { cap: "fan-control" },
57393
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57394
+ apply: "replace",
57395
+ section: AXES_SECTION,
57396
+ path: fanAxisPath(axis, field),
57397
+ sources: [{
57398
+ kind: "numeric",
57399
+ transforms: [{ kind: "identity" }]
57400
+ }],
57401
+ sourceReport: { status: "assignment-status" }
57402
+ };
57403
+ if (field !== "angle") return base;
57404
+ return {
57405
+ ...base,
57406
+ companions: FAN_OSCILLATION_AXIS_LIMITS.map((limit) => ({
57407
+ kind: "source-field",
57408
+ path: fanAxisPath(axis, limit),
57409
+ field: limit
57410
+ }))
57411
+ };
57412
+ }
57413
+ var AXIS_TARGETS = FAN_OSCILLATION_AXES.flatMap((axis) => [
57414
+ axisOscillatingTarget(axis),
57415
+ axisNumericTarget(axis, "angle"),
57416
+ axisNumericTarget(axis, "sweepFrom"),
57417
+ axisNumericTarget(axis, "sweepTo")
57418
+ ]);
57419
+ /** A select (an enum control) the operator links to the fan's `preset`. */
57420
+ var PRESET_TARGET = {
57421
+ id: "preset",
57422
+ cardinality: "scalar",
57423
+ vocabulary: { cap: "fan-control" },
57424
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57425
+ apply: "replace",
57426
+ path: "preset",
57427
+ sources: [{
57428
+ kind: "field",
57429
+ valueKinds: ["string"],
57430
+ transforms: [{ kind: "identity" }]
57431
+ }],
57432
+ sourceReport: { status: "assignment-status" }
57433
+ };
57434
+ var FAN_CONTROL_TARGETS = [...AXIS_TARGETS, PRESET_TARGET];
57435
+ /**
57436
+ * Customize (D1034): the fan already serves `fan-control` natively, so every
57437
+ * target is a `replace` over a field listed in `overridable` — the axis leaves
57438
+ * and `preset`, nothing else. Nothing is pre-linked: the operator picks each
57439
+ * source (D356).
57440
+ */
57441
+ var fanControlCustomize = {
57442
+ version: 1,
57443
+ overridable: FAN_CONTROL_TARGETS.map((target) => ({
57444
+ path: target.path,
57445
+ by: target.id
57446
+ })),
57447
+ targets: FAN_CONTROL_TARGETS
57448
+ };
56693
57449
  var fanControlCapability = {
56694
57450
  name: "fan-control",
56695
57451
  scope: "device",
@@ -56724,6 +57480,18 @@ var fanControlCapability = {
56724
57480
  }), _void(), {
56725
57481
  kind: "mutation",
56726
57482
  auth: "protected"
57483
+ }),
57484
+ /**
57485
+ * Sets one oscillation axis: its switch, fixed angle and/or sweep bounds.
57486
+ * Fields left out are untouched. A field linked in Customize is forwarded
57487
+ * to its linked source (D1034); otherwise the native provider serves it;
57488
+ * neither → `axis-not-linked`. Ranges are checked against the axis limits
57489
+ * before anything is written; writes run in order and stop at the first
57490
+ * failure, which the result names.
57491
+ */
57492
+ setOscillationAxis: optionalMethod(SetOscillationAxisInputSchema, FanOscillationAxisResultSchema, {
57493
+ kind: "mutation",
57494
+ auth: "protected"
56727
57495
  })
56728
57496
  },
56729
57497
  status: {
@@ -56735,6 +57503,7 @@ var fanControlCapability = {
56735
57503
  * sliders read `speed` for live updates.
56736
57504
  */
56737
57505
  runtimeState: FanControlStatusSchema,
57506
+ customize: fanControlCustomize,
56738
57507
  /**
56739
57508
  * Runtime-state durability: **session** — as `brightness`.
56740
57509
  *
@@ -56754,16 +57523,33 @@ var fanControlCapability = {
56754
57523
  oscillating: { kind: "boolean" }
56755
57524
  } }
56756
57525
  };
57526
+ /** One axis as a preset wants it; an absent field is left as it is. */
57527
+ var FanPresetAxisSchema = object({
57528
+ oscillating: boolean().optional(),
57529
+ angle: number$1().optional(),
57530
+ sweepFrom: number$1().optional(),
57531
+ sweepTo: number$1().optional()
57532
+ });
57533
+ /** The fan-control state a preset sets; an absent field is left as it is. */
57534
+ var FanPresetStateSchema = object({
57535
+ speed: number$1().min(0).max(100).optional(),
57536
+ /** The fan's own preset mode (or the linked select's option). */
57537
+ preset: string().min(1).optional(),
57538
+ direction: FanDirectionSchema.optional(),
57539
+ oscillationAxes: object({
57540
+ horizontal: FanPresetAxisSchema.optional(),
57541
+ vertical: FanPresetAxisSchema.optional()
57542
+ }).optional()
57543
+ });
56757
57544
  var FanPresetSchema = object({
56758
57545
  /** Stable key; never reused after a delete within one device's list. */
56759
57546
  id: string().min(1).max(64),
56760
57547
  /** What the operator and a voice assistant call it. */
56761
57548
  name: string().trim().min(1).max(40),
56762
- steps: array(ActionStepSchema).min(1).max(20)
57549
+ state: FanPresetStateSchema
56763
57550
  });
56764
57551
  var FanPresetsRefusalSchema = _enum([
56765
- "not-sibling",
56766
- "bad-target",
57552
+ "empty-state",
56767
57553
  "duplicate-name",
56768
57554
  "duplicate-id",
56769
57555
  "too-many",
@@ -56775,72 +57561,55 @@ var FanPresetsSetResultSchema = discriminatedUnion("status", [object({
56775
57561
  }), object({
56776
57562
  status: literal("refused"),
56777
57563
  reason: FanPresetsRefusalSchema,
56778
- /** Names the preset and step at fault. */
57564
+ /** Names the preset at fault. */
56779
57565
  detail: string()
56780
57566
  })]);
56781
- var FanPresetStepResultSchema = object({
56782
- index: number$1().int().nonnegative(),
57567
+ var FanPresetFieldResultSchema = object({
57568
+ /** The state path: `speed`, `preset`, `direction`, `oscillationAxes.horizontal.angle`. */
57569
+ field: string(),
56783
57570
  outcome: _enum([
56784
57571
  "applied",
56785
57572
  "failed",
56786
57573
  "not-run",
56787
57574
  "refused"
56788
57575
  ]),
56789
- deviceId: number$1().int().nonnegative().optional(),
56790
- cap: string().optional(),
56791
- method: string().optional(),
56792
57576
  /** The cause, for every outcome but `applied`. */
56793
57577
  message: string().optional()
56794
57578
  });
56795
57579
  var FanPresetApplyResultSchema = object({
56796
- /** `applied`: every step ran. `partial`: some ran, one failed. `refused`: nothing ran. */
57580
+ /** `applied`: every field landed. `partial`: some did, one did not. `refused`: nothing ran. */
56797
57581
  status: _enum([
56798
57582
  "applied",
56799
57583
  "partial",
56800
57584
  "refused"
56801
57585
  ]),
56802
- steps: array(FanPresetStepResultSchema)
57586
+ fields: array(FanPresetFieldResultSchema)
56803
57587
  });
56804
57588
  var FanCaptureFieldSchema = _enum([
56805
57589
  "speed",
56806
- "oscillating",
56807
57590
  "direction",
56808
- "preset"
57591
+ "preset",
57592
+ "oscillationAxes"
56809
57593
  ]);
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
57594
  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)
57595
+ /** The fan's CURRENT fan-control state, as a preset; NOT saved. */
57596
+ state: FanPresetStateSchema,
57597
+ omitted: array(object({
57598
+ /** A state path (`speed`, `oscillationAxes.vertical.angle`). */
57599
+ what: string(),
57600
+ /** Why no value was taken: an unknown reading is never stored as 0 (D393). */
57601
+ reason: string()
57602
+ }))
56831
57603
  });
56832
- var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
57604
+ var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
56833
57605
  DeviceType.Fan;
56834
- method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
57606
+ method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
56835
57607
  kind: "mutation",
56836
57608
  auth: "admin"
56837
- }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
57609
+ }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
56838
57610
  kind: "mutation",
56839
57611
  auth: "protected"
56840
- }), method(deviceInput$2.extend({
56841
- fields: array(FanCaptureFieldSchema).optional(),
56842
- steps: array(ActionStepSchema).max(20).default([])
56843
- }), FanPresetCaptureResultSchema, { auth: "protected" });
57612
+ }), method(deviceInput$1.extend({ fields: array(FanCaptureFieldSchema).optional() }), FanPresetCaptureResultSchema, { auth: "protected" });
56844
57613
  /**
56845
57614
  * Per-device feature/identity probe slice. Holds the runtime-resolved
56846
57615
  * truth about what a device CAN do — which the kernel uses to:
@@ -57052,8 +57821,7 @@ var groupActiveCapability = {
57052
57821
  DeviceType.Valve,
57053
57822
  DeviceType.Lock,
57054
57823
  DeviceType.MediaPlayer,
57055
- DeviceType.Climate,
57056
- DeviceType.Thermostat
57824
+ DeviceType.Climate
57057
57825
  ],
57058
57826
  methods: {},
57059
57827
  status: {
@@ -57095,7 +57863,6 @@ var groupCommandsCapability = {
57095
57863
  DeviceType.Valve,
57096
57864
  DeviceType.Lock,
57097
57865
  DeviceType.MediaPlayer,
57098
- DeviceType.Thermostat,
57099
57866
  DeviceType.Climate
57100
57867
  ],
57101
57868
  methods: {},
@@ -57151,7 +57918,6 @@ var groupStatusCapability = {
57151
57918
  DeviceType.Valve,
57152
57919
  DeviceType.Lock,
57153
57920
  DeviceType.MediaPlayer,
57154
- DeviceType.Thermostat,
57155
57921
  DeviceType.Climate
57156
57922
  ],
57157
57923
  methods: {},
@@ -57507,7 +58273,7 @@ var heatingValveCapability = {
57507
58273
  scope: "device",
57508
58274
  deviceNative: true,
57509
58275
  mode: "singleton",
57510
- deviceTypes: [DeviceType.Thermostat],
58276
+ deviceTypes: [DeviceType.Climate],
57511
58277
  methods: {},
57512
58278
  status: {
57513
58279
  schema: HeatingValveStatusSchema,
@@ -57537,13 +58303,18 @@ var heatingValveCapability = {
57537
58303
  * retried in a loop - D49); a future one is re-armed.
57538
58304
  *
57539
58305
  * 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,
58306
+ * and role ({@link timedRunLimitOf}): the maximum is a safety bound, not a preference,
57541
58307
  * so a request above it is REFUSED with a typed reason, never clamped.
57542
58308
  */
57543
58309
  var TimedRunUnitSchema = _enum(["s", "min"]);
57544
58310
  _enum(["switch", "pet-fountain"]);
57545
- DeviceType.Heater, DeviceType.PetFountain;
57546
- var TIMED_RUN_DEVICE_TYPES = [DeviceType.Heater, DeviceType.PetFountain];
58311
+ /**
58312
+ * The device types `timed-run` may be offered on. A `switch` is narrowed by
58313
+ * role ({@link TIMED_RUN_DEVICE_ROLES}): only a plug-in heater runs on a timer.
58314
+ */
58315
+ var TIMED_RUN_DEVICE_TYPES = [DeviceType.Switch, DeviceType.PetFountain];
58316
+ /** The roles that make a device of these types timed-run-able (D1033). */
58317
+ var TIMED_RUN_DEVICE_ROLES = { [DeviceType.Switch]: [DeviceRole.SwitchHeater] };
57547
58318
  /** One duration, as the one-tap run asks for it. */
57548
58319
  var TimedRunQuickRunSchema = object({
57549
58320
  duration: number$1().positive().finite(),
@@ -57593,6 +58364,7 @@ var timedRunCapability = {
57593
58364
  kind: "wrapper",
57594
58365
  defaultActive: true,
57595
58366
  deviceTypes: TIMED_RUN_DEVICE_TYPES,
58367
+ deviceRoles: TIMED_RUN_DEVICE_ROLES,
57596
58368
  deviceConfig: { ui: {
57597
58369
  kind: "derived-form",
57598
58370
  builderId: "timed-run",
@@ -57753,8 +58525,8 @@ var HeroSceneSchema = object({
57753
58525
  /** 0..1: volume, cover position, cycle progress. */
57754
58526
  level: number$1().min(0).max(1).nullable()
57755
58527
  });
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;
58528
+ 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;
58529
+ 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
58530
  /** A string in both languages, as the cap vocabulary writes it (D920): no `t()` in the model. */
57759
58531
  var HeroTextSchema = object({
57760
58532
  en: string(),
@@ -58139,8 +58911,8 @@ var HeroRowModelSchema = object({
58139
58911
  extras: array(HeroControlSchema).max(2).optional(),
58140
58912
  trailing: HeroFactSchema.nullable()
58141
58913
  });
58142
- DeviceType.Climate, DeviceType.Thermostat;
58143
- DeviceType.Climate, DeviceType.Thermostat, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
58914
+ DeviceType.Climate;
58915
+ DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
58144
58916
  /**
58145
58917
  * hero-layout — what a device's hero says (D1023). Read-only: the FIXED
58146
58918
  * layouts live in `@camstack/types` and nothing here stores or edits one.
@@ -58172,7 +58944,10 @@ method(heroInput, HeroModelSchema.nullable(), {
58172
58944
  }), method(rowModelsInput, array(HeroRowModelSchema.nullable()), {
58173
58945
  auth: "protected",
58174
58946
  role: "viewer"
58175
- }), method(object({ deviceType: string() }), HeroLayoutRowsSchema, {
58947
+ }), method(object({
58948
+ deviceType: string(),
58949
+ role: string().nullable().optional()
58950
+ }), HeroLayoutRowsSchema, {
58176
58951
  auth: "protected",
58177
58952
  role: "viewer"
58178
58953
  });
@@ -58219,6 +58994,11 @@ var humidifierCapability = {
58219
58994
  deviceNative: true,
58220
58995
  mode: "singleton",
58221
58996
  deviceTypes: [DeviceType.Humidifier],
58997
+ power: {
58998
+ kind: "field",
58999
+ field: "on",
59000
+ set: "setOn"
59001
+ },
58222
59002
  methods: {
58223
59003
  setOn: method(object({
58224
59004
  deviceId: number$1().int().nonnegative(),
@@ -58292,7 +59072,7 @@ var humiditySensorCapability = {
58292
59072
  scope: "device",
58293
59073
  deviceNative: true,
58294
59074
  mode: "singleton",
58295
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
59075
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
58296
59076
  methods: {},
58297
59077
  status: {
58298
59078
  schema: HumiditySensorStatusSchema,
@@ -71474,7 +72254,18 @@ var OauthIntegrationDescriptorSchema = object({
71474
72254
  * The value is baked into the authorization code at `/authorize` and travels
71475
72255
  * on the tokens, so editing this field changes FUTURE links only.
71476
72256
  */
71477
- refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional()
72257
+ refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional(),
72258
+ /**
72259
+ * The integration instances an account can be linked to (planning#415),
72260
+ * beside the default one. `/api/oauth2/authorize?instance=<id>` is accepted
72261
+ * only for an id listed here; with no `instance=` the link is made to the
72262
+ * default instance, which is never listed and always accepted — it is the
72263
+ * address third parties already hold. Omit for a single-instance integration.
72264
+ */
72265
+ instances: array(object({
72266
+ id: string(),
72267
+ displayName: string()
72268
+ })).optional()
71478
72269
  });
71479
72270
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
71480
72271
  /**
@@ -71964,6 +72755,11 @@ var petFountainCapability = {
71964
72755
  deviceNative: true,
71965
72756
  mode: "singleton",
71966
72757
  deviceTypes: [DeviceType.PetFountain],
72758
+ power: {
72759
+ kind: "field",
72760
+ field: "pumpRunning",
72761
+ set: "setPump"
72762
+ },
71967
72763
  methods: {
71968
72764
  /** Start or stop the pump. */
71969
72765
  setPump: method(object({
@@ -73539,7 +74335,6 @@ var POWER_METER_DEVICE_TYPES = [
73539
74335
  DeviceType.Light,
73540
74336
  DeviceType.Fan,
73541
74337
  DeviceType.Climate,
73542
- DeviceType.Thermostat,
73543
74338
  DeviceType.Humidifier,
73544
74339
  DeviceType.WaterHeater,
73545
74340
  DeviceType.MediaPlayer,
@@ -73548,8 +74343,7 @@ var POWER_METER_DEVICE_TYPES = [
73548
74343
  DeviceType.Dishwasher,
73549
74344
  DeviceType.Vacuum,
73550
74345
  DeviceType.LawnMower,
73551
- DeviceType.PetFeeder,
73552
- DeviceType.Heater
74346
+ DeviceType.PetFeeder
73553
74347
  ];
73554
74348
  /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
73555
74349
  var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
@@ -73662,117 +74456,6 @@ var powerMeterCapability = {
73662
74456
  volatileStateFields: ["lastFetchedAt"],
73663
74457
  customize: powerMeterCustomize
73664
74458
  };
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
74459
  /**
73777
74460
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
73778
74461
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -77247,9 +77930,7 @@ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
77247
77930
  DeviceType.Humidifier,
77248
77931
  DeviceType.Fan,
77249
77932
  DeviceType.Climate,
77250
- DeviceType.Thermostat,
77251
- DeviceType.PetFeeder,
77252
- DeviceType.Heater
77933
+ DeviceType.PetFeeder
77253
77934
  ];
77254
77935
  var runningStateCapability = {
77255
77936
  name: "running-state",
@@ -78521,6 +79202,13 @@ var SsoBridgeClaimsSchema = object({
78521
79202
  /** OAuth authorization-code binding — set only on `oauth-code` tokens. */
78522
79203
  redirectUri: string().optional(),
78523
79204
  integrationId: string().optional(),
79205
+ /**
79206
+ * The integration INSTANCE an OAuth link was made for (planning#415). Set on
79207
+ * `oauth-code`, `oauth-access` and `oauth-refresh` tokens of a link made
79208
+ * with `instance=`; absent on a link to the default instance. A cloud relay
79209
+ * reads it off the access token to route; the hub answers from the session.
79210
+ */
79211
+ instanceId: string().optional(),
78524
79212
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
78525
79213
  jti: string().optional(),
78526
79214
  /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
@@ -79337,8 +80025,7 @@ var switchCapability = {
79337
80025
  DeviceType.Switch,
79338
80026
  DeviceType.Siren,
79339
80027
  DeviceType.Light,
79340
- DeviceType.Fan,
79341
- DeviceType.Heater
80028
+ DeviceType.Fan
79342
80029
  ],
79343
80030
  methods: { setState: method(object({
79344
80031
  deviceId: number$1(),
@@ -80032,7 +80719,7 @@ var temperatureSensorCapability = {
80032
80719
  scope: "device",
80033
80720
  deviceNative: true,
80034
80721
  mode: "singleton",
80035
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
80722
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
80036
80723
  methods: {},
80037
80724
  status: {
80038
80725
  schema: TemperatureSensorStatusSchema,
@@ -80900,6 +81587,8 @@ var OauthSessionSummarySchema = object({
80900
81587
  userId: string(),
80901
81588
  username: string(),
80902
81589
  integrationId: string(),
81590
+ /** The instance the link was made for; `null` = the default instance. */
81591
+ instanceId: string().nullable().optional(),
80903
81592
  scopes: array(TokenScopeSchema),
80904
81593
  createdAt: number$1(),
80905
81594
  lastUsedAt: number$1(),
@@ -80999,6 +81688,9 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
80999
81688
  access: "view"
81000
81689
  }), method(object({
81001
81690
  integrationId: string(),
81691
+ /** The integration instance the link is for. Omit for the default
81692
+ * instance — never pass the addon id to mean it. */
81693
+ instanceId: string().optional(),
81002
81694
  userId: string(),
81003
81695
  username: string(),
81004
81696
  scopes: array(TokenScopeSchema),
@@ -81037,7 +81729,13 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
81037
81729
  }), method(object({ token: string() }), object({
81038
81730
  userId: string(),
81039
81731
  username: string(),
81040
- scopes: array(TokenScopeSchema)
81732
+ scopes: array(TokenScopeSchema),
81733
+ /** The instance the link was made for, read from the SESSION.
81734
+ * Absent = the default instance. `oauthLinkServesInstance` is the
81735
+ * helper a per-instance route calls to compare it with the instance
81736
+ * of its path; no route declares `perInstance` yet (the Alexa
81737
+ * per-instance route, T3, is the first consumer). */
81738
+ instanceId: string().optional()
81041
81739
  }).nullable(), { access: "view" }), method(object({ userId: string() }), array(ExternalIdentitySchema), { auth: "admin" }), method(ExternalIdentityKeySchema, object({
81042
81740
  userId: string(),
81043
81741
  issuer: string()
@@ -81679,6 +82377,13 @@ var waterHeaterCapability = {
81679
82377
  deviceNative: true,
81680
82378
  mode: "singleton",
81681
82379
  deviceTypes: [DeviceType.WaterHeater],
82380
+ power: {
82381
+ kind: "mode",
82382
+ field: "operationMode",
82383
+ offValue: "off",
82384
+ set: "setOperationMode",
82385
+ lastField: "lastActiveMode"
82386
+ },
81682
82387
  methods: {
81683
82388
  setTargetTemp: method(object({
81684
82389
  deviceId: number$1().int().nonnegative(),
@@ -84370,15 +85075,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84370
85075
  });
84371
85076
  const meta = await this.ctx.api.deviceManager.loadMeta.query({ deviceId: id });
84372
85077
  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;
85078
+ const Class = this.deviceClassForRow({
85079
+ type: meta.type,
85080
+ stableId
85081
+ });
84375
85082
  if (!Class) throw new Error(`${this.providerName}: no device class for type "${meta.type}" — "${stableId}" is an accessory child; reload its parent instead`);
84376
85083
  await devices.create(stableId, Class, {}, meta.parentDeviceId ?? null);
84377
85084
  const rows = await this.ctx.api.deviceManager.listPersistedByAddon.query({ addonId: this.addonId });
84378
85085
  for (const row of rows) {
84379
85086
  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;
85087
+ const ChildClass = this.deviceClassForRow(row);
84382
85088
  if (!ChildClass) continue;
84383
85089
  try {
84384
85090
  await devices.create(row.stableId, ChildClass, {}, id);
@@ -84405,6 +85111,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84405
85111
  return { deviceId: id };
84406
85112
  }
84407
85113
  /**
85114
+ * The class a PERSISTED row is rebuilt with. Default: by type, from
85115
+ * `deviceClasses`. A provider whose distinct devices share one type (D1033:
85116
+ * a radiator valve and a room climate are both `climate`) overrides this
85117
+ * and tells them apart by the row's stable id or config.
85118
+ */
85119
+ deviceClassForRow(row) {
85120
+ const type = Object.values(DeviceType).find((t) => t === row.type);
85121
+ return type !== void 0 ? this.deviceClasses[type] : void 0;
85122
+ }
85123
+ /**
84408
85124
  * Restore devices from persisted state. Two-pass:
84409
85125
  *
84410
85126
  * 1. **Top-level pass** — invokes `kernel.devices.create()` for every
@@ -84465,7 +85181,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84465
85181
  restored.add(saved.id);
84466
85182
  return;
84467
85183
  }
84468
- const Class = this.deviceClasses[saved.type];
85184
+ const Class = this.deviceClassForRow(saved);
84469
85185
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
84470
85186
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
84471
85187
  await this.healSavedConfig(saved, savedDevices);
@@ -84482,7 +85198,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84482
85198
  };
84483
85199
  const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
84484
85200
  const restoreOne = async (saved) => {
84485
- if (!this.deviceClasses[saved.type]) {
85201
+ if (!this.deviceClassForRow(saved)) {
84486
85202
  this.ctx.logger.warn("No device class registered for restored type — skipping", {
84487
85203
  tags: {
84488
85204
  deviceId: saved.id,
@@ -84521,7 +85237,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84521
85237
  const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
84522
85238
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
84523
85239
  for (const saved of childRows) {
84524
- if (!this.deviceClasses[saved.type]) continue;
85240
+ if (!this.deviceClassForRow(saved)) continue;
84525
85241
  if (saved.parentDeviceId === null) continue;
84526
85242
  if (restored.has(saved.parentDeviceId)) {
84527
85243
  try {
@@ -84575,7 +85291,9 @@ var BaseDeviceProvider = class extends BaseAddon {
84575
85291
  }
84576
85292
  };
84577
85293
  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;
85294
+ DeviceType.Climate;
85295
+ DeviceRole.ClimateThermostat, DeviceRole.ClimateAc, DeviceRole.ClimateRadiator, DeviceRole.ClimateFloor, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit;
85296
+ 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
85297
  new Set(Object.values(DeviceType));
84580
85298
  /** Parses the raw slice with the cap's own schema; `undefined` when it does not parse. */
84581
85299
  function contribution(cap, schema, build) {
@@ -86581,7 +87299,7 @@ var HEAT_PUMP_FAMILY_CONTRIBUTIONS = { "heat-pump-status": contribution("heat-pu
86581
87299
  ...Object.values(CLIMATE_FAMILY_CONTRIBUTIONS),
86582
87300
  ...Object.values(HEAT_PUMP_FAMILY_CONTRIBUTIONS)
86583
87301
  ];
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;
87302
+ 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
87303
  /**
86586
87304
  * Who looks like what (D1023). One look per {@link DeviceType}, refined by a
86587
87305
  * {@link DeviceRole} where the role changes how the device reads (a leak
@@ -87225,9 +87943,9 @@ lookOf({
87225
87943
  idle: INFO,
87226
87944
  standby: MUTED
87227
87945
  });
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;
87946
+ DeviceType.Climate;
87947
+ 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;
87948
+ 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
87949
  Object.freeze({
87232
87950
  "accessories.getStatus": {
87233
87951
  capName: "accessories",
@@ -90624,6 +91342,13 @@ Object.freeze({
90624
91342
  access: "create",
90625
91343
  role: "viewer"
90626
91344
  },
91345
+ "fanControl.setOscillationAxis": {
91346
+ capName: "fan-control",
91347
+ capScope: "device",
91348
+ addonId: null,
91349
+ access: "create",
91350
+ role: "viewer"
91351
+ },
90627
91352
  "fanControl.setPercentage": {
90628
91353
  capName: "fan-control",
90629
91354
  capScope: "device",
@@ -97645,7 +98370,7 @@ function projectAction(cap, action) {
97645
98370
  };
97646
98371
  }
97647
98372
  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;
98373
+ 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
98374
  DeviceFeature.BatteryOperated;
97650
98375
  function bound(cap, methods = [], authority = "reported") {
97651
98376
  return {
@@ -97704,6 +98429,149 @@ function offered(received) {
97704
98429
  };
97705
98430
  }
97706
98431
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
98432
+ var PowerSwitchRefusedError = class extends Error {
98433
+ reason;
98434
+ constructor(reason, message) {
98435
+ super(message);
98436
+ this.name = "PowerSwitchRefusedError";
98437
+ this.reason = reason;
98438
+ }
98439
+ };
98440
+ /** `true` / `false` from the owner slice, `null` when the hinted field is
98441
+ * absent or not of the hinted shape. */
98442
+ function powerOnFromSlice(hint, slice) {
98443
+ const value = slice?.[hint.field];
98444
+ switch (hint.kind) {
98445
+ case "field": return typeof value === "boolean" ? value : null;
98446
+ case "methods": return typeof value === "string" ? !hint.offValues.includes(value) : null;
98447
+ case "mode": return typeof value === "string" ? value !== hint.offValue : null;
98448
+ }
98449
+ }
98450
+ function ownerMethod(owner, name, capName) {
98451
+ const fn = Reflect.get(owner, name);
98452
+ if (typeof fn !== "function") throw new PowerSwitchRefusedError("owner-method-missing", `${capName} provider does not serve '${name}', which its power hint names`);
98453
+ return async (input) => fn.call(owner, input);
98454
+ }
98455
+ async function lastNonOffMode(host, capName, hint) {
98456
+ const isMode = (v) => typeof v === "string" && v.length > 0 && v !== hint.offValue;
98457
+ const fromHub = (await host.readHubSlice(capName).catch(() => null))?.[hint.lastField];
98458
+ if (isMode(fromHub)) return fromHub;
98459
+ const local = host.runtimeState.getCapState(capName)?.[hint.lastField];
98460
+ return isMode(local) ? local : null;
98461
+ }
98462
+ async function applyPower(host, capName, hint, owner, on) {
98463
+ const deviceId = host.deviceId;
98464
+ switch (hint.kind) {
98465
+ case "field":
98466
+ await ownerMethod(owner, hint.set, capName)({
98467
+ deviceId,
98468
+ on
98469
+ });
98470
+ return;
98471
+ case "methods":
98472
+ await ownerMethod(owner, on ? hint.on : hint.off, capName)({ deviceId });
98473
+ return;
98474
+ case "mode": {
98475
+ const set = ownerMethod(owner, hint.set, capName);
98476
+ if (!on) {
98477
+ await set({
98478
+ deviceId,
98479
+ mode: hint.offValue
98480
+ });
98481
+ return;
98482
+ }
98483
+ const mode = await lastNonOffMode(host, capName, hint);
98484
+ if (mode === null) throw new PowerSwitchRefusedError("no-last-mode", `device ${deviceId}: ${capName} has no last non-off mode to restore; pick a mode instead`);
98485
+ await set({
98486
+ deviceId,
98487
+ mode
98488
+ });
98489
+ return;
98490
+ }
98491
+ }
98492
+ }
98493
+ function hintedMethods(hint) {
98494
+ return hint.kind === "methods" ? [hint.on, hint.off] : [hint.set];
98495
+ }
98496
+ /** Whether `owner` serves every method its cap's power hint names. A media
98497
+ * player whose `turnOn` / `turnOff` are not served has no power, so no switch. */
98498
+ function powerServedBy(hint, owner) {
98499
+ if (hint === void 0) return false;
98500
+ return hintedMethods(hint).every((name) => typeof Reflect.get(owner, name) === "function");
98501
+ }
98502
+ /** Builds the `switch` provider and starts the slice projection. Exported
98503
+ * for the specs; providers call `registerPowerSwitch`. */
98504
+ function buildPowerSwitch(host, cap, owner) {
98505
+ const hint = cap.power;
98506
+ if (hint === void 0) throw new Error(`capability '${cap.name}' has no power hint: it has no switch to project`);
98507
+ const read = () => powerOnFromSlice(hint, host.runtimeState.getCapState(cap.name));
98508
+ const project = () => {
98509
+ const on = read();
98510
+ const current = host.runtimeState.getCapState(switchCapability.name);
98511
+ if (current !== void 0 && current["on"] === on) return;
98512
+ const next = {
98513
+ on,
98514
+ lastChangedAt: Date.now()
98515
+ };
98516
+ host.runtimeState.setCapState(switchCapability.name, next);
98517
+ };
98518
+ const stop = host.runtimeState.subscribeCap(cap.name, project);
98519
+ if (host.runtimeState.getCapState(cap.name) !== void 0) project();
98520
+ return {
98521
+ provider: {
98522
+ getStatus: async () => {
98523
+ const current = host.runtimeState.getCapState(switchCapability.name);
98524
+ const lastChangedAt = typeof current?.["lastChangedAt"] === "number" ? current["lastChangedAt"] : 0;
98525
+ return {
98526
+ on: read(),
98527
+ lastChangedAt
98528
+ };
98529
+ },
98530
+ setState: async ({ on }) => applyPower(host, cap.name, hint, owner, on)
98531
+ },
98532
+ dispose: stop
98533
+ };
98534
+ }
98535
+ /**
98536
+ * Registers `switch` for a device whose `cap` carries a `power` hint, built
98537
+ * from `owner` (the provider the device registered for `cap`). Call it right
98538
+ * after `registerNativeCap(cap, owner)`.
98539
+ */
98540
+ function registerPowerSwitch(device, cap, owner) {
98541
+ const deviceId = device.ctx.id;
98542
+ if (cap.power !== void 0 && !powerServedBy(cap.power, owner)) {
98543
+ device.ctx.logger.debug("power switch: owner serves no power control, no switch registered", {
98544
+ tags: { deviceId },
98545
+ meta: { capName: cap.name }
98546
+ });
98547
+ return () => {};
98548
+ }
98549
+ const host = {
98550
+ deviceId,
98551
+ runtimeState: device.runtimeState,
98552
+ registerNativeCap: (c, p) => device.ctx.registerNativeCap(c, p),
98553
+ readHubSlice: async (capName) => {
98554
+ try {
98555
+ return await device.ctx.api.deviceState.getCapSlice.query({
98556
+ deviceId,
98557
+ capName
98558
+ });
98559
+ } catch (err) {
98560
+ device.ctx.logger.warn("power switch: hub slice read failed", {
98561
+ tags: { deviceId },
98562
+ meta: {
98563
+ capName,
98564
+ error: err instanceof Error ? err.message : String(err)
98565
+ }
98566
+ });
98567
+ return null;
98568
+ }
98569
+ }
98570
+ };
98571
+ const built = buildPowerSwitch(host, cap, owner);
98572
+ host.registerNativeCap(switchCapability, built.provider);
98573
+ return built.dispose;
98574
+ }
97707
98575
  Object.freeze({
97708
98576
  "accessories.getStatus": [{
97709
98577
  name: "deviceId",
@@ -98697,6 +99565,11 @@ Object.freeze({
98697
99565
  form: "single",
98698
99566
  optional: false
98699
99567
  }],
99568
+ "fanControl.setOscillationAxis": [{
99569
+ name: "deviceId",
99570
+ form: "single",
99571
+ optional: false
99572
+ }],
98700
99573
  "fanControl.setPercentage": [{
98701
99574
  name: "deviceId",
98702
99575
  form: "single",
@@ -102566,6 +103439,12 @@ Object.defineProperty(exports, "object", {
102566
103439
  return object;
102567
103440
  }
102568
103441
  });
103442
+ Object.defineProperty(exports, "plainEventVocabulary", {
103443
+ enumerable: true,
103444
+ get: function() {
103445
+ return plainEventVocabulary;
103446
+ }
103447
+ });
102569
103448
  Object.defineProperty(exports, "powerMeterCapability", {
102570
103449
  enumerable: true,
102571
103450
  get: function() {
@@ -102584,6 +103463,12 @@ Object.defineProperty(exports, "record", {
102584
103463
  return record;
102585
103464
  }
102586
103465
  });
103466
+ Object.defineProperty(exports, "registerPowerSwitch", {
103467
+ enumerable: true,
103468
+ get: function() {
103469
+ return registerPowerSwitch;
103470
+ }
103471
+ });
102587
103472
  Object.defineProperty(exports, "rowsDepsOver", {
102588
103473
  enumerable: true,
102589
103474
  get: function() {