@camstack/addon-provider-enpal 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -3,7 +3,7 @@ let node_child_process = require("node:child_process");
3
3
  let node_fs = require("node:fs");
4
4
  let node_path = require("node:path");
5
5
  let node_url = require("node:url");
6
- //#region ../types/dist/event-category-78tcBemo.mjs
6
+ //#region ../types/dist/event-category-Dv-Gcv1f.mjs
7
7
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
8
8
  EventCategory["SystemBoot"] = "system.boot";
9
9
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -444,6 +444,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
444
444
  */
445
445
  EventCategory["DeviceStateChanged"] = "device.state-changed";
446
446
  /**
447
+ * A device COMMAND (a vocabulary action, D920/D983) settled in the hub's
448
+ * transition audit (D1022): confirmed, already in place, unconfirmed or
449
+ * failed. Never a device's own change. Payload: `DeviceCommandSettledEvent`
450
+ * (`{deviceId, action, outcome, actor, at}`, no arguments). It feeds
451
+ * the Notification Center `device-command` kind. Telemetry (D8): losing one
452
+ * costs a notification, never state; the audit row is the record.
453
+ */
454
+ EventCategory["DeviceCommandSettled"] = "device.command-settled";
455
+ /**
447
456
  * The NATIVE shadow of a cap a composition has claimed fields of moved
448
457
  * (D663). Payload: `{ deviceId, capName, native }` — what the provider
449
458
  * wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
@@ -806,7 +815,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
806
815
  return EventCategory;
807
816
  }({});
808
817
  //#endregion
809
- //#region ../types/dist/composed-stable-id-jnvhwGoH.mjs
818
+ //#region ../types/dist/composed-stable-id-CPp3votu.mjs
810
819
  /**
811
820
  import { errMsg } from '@camstack/types'
812
821
  * Extract a human-readable message from an unknown error value.
@@ -819,17 +828,18 @@ function errMsg(err) {
819
828
  }
820
829
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
821
830
  DeviceType["Camera"] = "camera";
831
+ /** Legacy stored value, read only by the hub-to-integration migrations; no live device carries it. */
822
832
  DeviceType["Hub"] = "hub";
823
833
  DeviceType["Light"] = "light";
824
834
  DeviceType["Siren"] = "siren";
825
835
  DeviceType["Switch"] = "switch";
826
836
  DeviceType["Sensor"] = "sensor";
827
- DeviceType["Thermostat"] = "thermostat";
828
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
829
- * `climate-control` cap surface with `Thermostat` but renders a
830
- * dedicated AC-appropriate control UI (mode chips, fan speed,
831
- * independent vertical/horizontal swing). Sources: native Gree, and
832
- * reusable by other AC integrations. */
837
+ /** Every climate device: an air conditioner, a room thermostat, a radiator
838
+ * valve, a floor-heating zone, a heat-pump circuit. They share one set of
839
+ * controls (`climate-control`), so they are one type; WHICH kind of climate
840
+ * device it is is its {@link DeviceRole} (`climate-*`), which the UI and the
841
+ * exports branch on. The retired `thermostat` type is read as this one
842
+ * (`canonicalStoredDeviceType`). */
833
843
  DeviceType["Climate"] = "climate";
834
844
  DeviceType["Button"] = "button";
835
845
  /** Generic stateless event emitter — carries a device's EXACT declared
@@ -920,10 +930,6 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
920
930
  /** Washing machine. A plug-built one is running while its power stays
921
931
  * above a threshold. No program cap — that surface is the dishwasher's. */
922
932
  DeviceType["Washer"] = "washer";
923
- /** A dumb plug-in stove or heater: a switch (and, with a meter, a power
924
- * reading) that only runs while the plug is on. Built from a smart plug;
925
- * the `timed-run` cap switches it off again after a chosen time. */
926
- DeviceType["Heater"] = "heater";
927
933
  /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
928
934
  DeviceType["Dryer"] = "dryer";
929
935
  /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
@@ -1143,6 +1149,27 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1143
1149
  /** A smart plug: a `switch` whose source says it is an outlet (HA
1144
1150
  * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1145
1151
  DeviceRole["Outlet"] = "outlet";
1152
+ /** A dumb plug-in stove or heater behind a switch: it runs only while the
1153
+ * switch is on. Offered `timed-run`, which switches it off again after a
1154
+ * chosen time. */
1155
+ DeviceRole["SwitchHeater"] = "switch-heater";
1156
+ /** A plain fan behind a switch (a smart plug powering it): drawn as a fan,
1157
+ * with the switch's on/off only. Speed or oscillation appear only when the
1158
+ * device also has a fan cap (D1033). */
1159
+ DeviceRole["SwitchFan"] = "switch-fan";
1160
+ /** Air conditioner / split unit: cooling, fan speed, louvres. */
1161
+ DeviceRole["ClimateAc"] = "climate-ac";
1162
+ /** Room thermostat: a setpoint and a room temperature. The default. */
1163
+ DeviceRole["ClimateThermostat"] = "climate-thermostat";
1164
+ /** Floor-heating zone actuator. */
1165
+ DeviceRole["ClimateFloor"] = "climate-floor";
1166
+ /** Radiator valve (thermostatic radiator head). */
1167
+ DeviceRole["ClimateRadiator"] = "climate-radiator";
1168
+ /** A heater with its own thermostat (stove, convector). */
1169
+ DeviceRole["ClimateHeater"] = "climate-heater";
1170
+ /** One heating/cooling circuit of a heat pump: a water loop with comfort /
1171
+ * eco setpoints and a heat/cool mode, no room sensor. */
1172
+ DeviceRole["ClimateHeatPumpCircuit"] = "climate-heat-pump-circuit";
1146
1173
  /** Generic motion / occupancy / moving event source. Distinct from
1147
1174
  * the camera accessory PirSensor role: that one is a camera child;
1148
1175
  * this is a standalone HA / 3rd-party motion sensor. */
@@ -6030,7 +6057,7 @@ var ZodIssueCode = {
6030
6057
  var ZodFirstPartyTypeKind;
6031
6058
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6032
6059
  //#endregion
6033
- //#region ../types/dist/sleep-Cw1uUXvl.mjs
6060
+ //#region ../types/dist/sleep-Czv-4wFH.mjs
6034
6061
  /**
6035
6062
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6036
6063
  * window to float samples (D455).
@@ -6713,6 +6740,13 @@ async function runAddonMigrations(host, migrations, logger, options = {}) {
6713
6740
  else skipped.push(id);
6714
6741
  };
6715
6742
  const selected = options.only === void 0 ? null : new Set(options.only);
6743
+ const deferred = /* @__PURE__ */ new Set();
6744
+ const deferredBy = (reason) => {
6745
+ if (selected === null || reason === null) return null;
6746
+ const dep = /^dependency-(?:pending|blocked):(.+)$/.exec(reason)?.[1];
6747
+ if (dep === void 0 || !byId.has(dep)) return null;
6748
+ return !selected.has(dep) || deferred.has(dep) ? dep : null;
6749
+ };
6716
6750
  for (const step of declaration.steps) {
6717
6751
  if (selected !== null && !selected.has(step.id)) continue;
6718
6752
  const bound = byId.get(step.id);
@@ -6724,6 +6758,19 @@ async function runAddonMigrations(host, migrations, logger, options = {}) {
6724
6758
  try {
6725
6759
  const gate = await host.startMigration({ id: step.id });
6726
6760
  if (!gate.proceed) {
6761
+ const waitsFor = deferredBy(gate.reason);
6762
+ if (waitsFor !== null) {
6763
+ log.info("migration deferred: its predecessor runs in another phase of this owner", {
6764
+ tags,
6765
+ meta: {
6766
+ waitsFor,
6767
+ reason: gate.reason
6768
+ }
6769
+ });
6770
+ deferred.add(step.id);
6771
+ skipped.push(step.id);
6772
+ continue;
6773
+ }
6727
6774
  log.warn("migration not run", {
6728
6775
  tags,
6729
6776
  meta: {
@@ -10261,6 +10308,13 @@ var DeviceTransitionSchema = object({
10261
10308
  actor: TransitionActorSchema,
10262
10309
  command: TransitionCommandSchema.optional()
10263
10310
  });
10311
+ object({
10312
+ deviceId: number$1().int(),
10313
+ action: string(),
10314
+ outcome: TransitionOutcomeSchema,
10315
+ actor: TransitionActorSchema,
10316
+ at: number$1()
10317
+ });
10264
10318
  var READING_QUANTITIES = [
10265
10319
  "temperature",
10266
10320
  "pressure",
@@ -11807,6 +11861,50 @@ var ACTUATOR_CAP_VOCABULARY = {
11807
11861
  value: false
11808
11862
  },
11809
11863
  args: { oscillating: false }
11864
+ },
11865
+ {
11866
+ id: "fan.horizontalOscillateOn",
11867
+ method: "setOscillationAxis",
11868
+ label: t("Horizontal oscillation on", "Oscillazione orizzontale attiva"),
11869
+ phrase: t("Start the horizontal oscillation of {device}", "Attiva l’oscillazione orizzontale di {device}"),
11870
+ icon: "move-horizontal",
11871
+ args: {
11872
+ axis: "horizontal",
11873
+ oscillating: true
11874
+ }
11875
+ },
11876
+ {
11877
+ id: "fan.horizontalOscillateOff",
11878
+ method: "setOscillationAxis",
11879
+ label: t("Horizontal oscillation off", "Oscillazione orizzontale spenta"),
11880
+ phrase: t("Stop the horizontal oscillation of {device}", "Ferma l’oscillazione orizzontale di {device}"),
11881
+ icon: "move-horizontal",
11882
+ args: {
11883
+ axis: "horizontal",
11884
+ oscillating: false
11885
+ }
11886
+ },
11887
+ {
11888
+ id: "fan.verticalOscillateOn",
11889
+ method: "setOscillationAxis",
11890
+ label: t("Vertical oscillation on", "Oscillazione verticale attiva"),
11891
+ phrase: t("Start the vertical oscillation of {device}", "Attiva l’oscillazione verticale di {device}"),
11892
+ icon: "move-vertical",
11893
+ args: {
11894
+ axis: "vertical",
11895
+ oscillating: true
11896
+ }
11897
+ },
11898
+ {
11899
+ id: "fan.verticalOscillateOff",
11900
+ method: "setOscillationAxis",
11901
+ label: t("Vertical oscillation off", "Oscillazione verticale spenta"),
11902
+ phrase: t("Stop the vertical oscillation of {device}", "Ferma l’oscillazione verticale di {device}"),
11903
+ icon: "move-vertical",
11904
+ args: {
11905
+ axis: "vertical",
11906
+ oscillating: false
11907
+ }
11810
11908
  }
11811
11909
  ],
11812
11910
  state: [{
@@ -12240,7 +12338,10 @@ var ACTUATOR_CAP_VOCABULARY = {
12240
12338
  "ir-transmitter": {
12241
12339
  scope: "device",
12242
12340
  noun: t("IR / RF transmitter", "Trasmettitore IR / RF"),
12243
- notOperatorFacing: { send: "a raw pulse train is not something an operator composes in an automation; the remote that owns the command table sends it" }
12341
+ notOperatorFacing: {
12342
+ send: "a raw pulse train is not something an operator composes in an automation; the remote that owns the command table sends it",
12343
+ learn: "an interactive capture the operator starts from the source dialog and answers by pressing a physical remote; no automation can supply the signal"
12344
+ }
12244
12345
  },
12245
12346
  control: {
12246
12347
  scope: "device",
@@ -13896,10 +13997,6 @@ var CAMERA_CAP_VOCABULARY = {
13896
13997
  scope: "device",
13897
13998
  noun: t("Admin link", "Link di amministrazione")
13898
13999
  },
13899
- "device-discovery": {
13900
- scope: "device",
13901
- noun: t("Discovery", "Rilevamento dispositivi")
13902
- },
13903
14000
  "device-extension": {
13904
14001
  scope: "device",
13905
14002
  noun: t("Device extension", "Estensione del dispositivo")
@@ -15357,7 +15454,6 @@ var CAP_NAMES = Object.freeze([
15357
15454
  "device-admin-link",
15358
15455
  "device-adoption",
15359
15456
  "device-credentials",
15360
- "device-discovery",
15361
15457
  "device-export",
15362
15458
  "device-extension",
15363
15459
  "device-history",
@@ -15979,13 +16075,6 @@ Object.freeze({
15979
16075
  "valueType": "string"
15980
16076
  }
15981
16077
  ],
15982
- "device-discovery": [{
15983
- "path": "lastDiscoveryAt",
15984
- "valueType": "number"
15985
- }, {
15986
- "path": "lastError",
15987
- "valueType": "string"
15988
- }],
15989
16078
  "device-export": [
15990
16079
  {
15991
16080
  "path": "linkState",
@@ -16174,6 +16263,10 @@ Object.freeze({
16174
16263
  "path": "lastEvent.eventType",
16175
16264
  "valueType": "string"
16176
16265
  },
16266
+ {
16267
+ "path": "lastEvent.sourceId",
16268
+ "valueType": "string"
16269
+ },
16177
16270
  {
16178
16271
  "path": "lastEvent.timestamp",
16179
16272
  "valueType": "number"
@@ -17437,7 +17530,7 @@ Object.freeze({
17437
17530
  }
17438
17531
  });
17439
17532
  //#endregion
17440
- //#region ../types/dist/implemented-methods-OitwyO1Y.mjs
17533
+ //#region ../types/dist/implemented-methods-5YMDeE3f.mjs
17441
17534
  /**
17442
17535
  * The ffmpeg build every CamStack node downloads, per platform and arch.
17443
17536
  *
@@ -44098,6 +44191,41 @@ function macroOnly(macro) {
44098
44191
  };
44099
44192
  }
44100
44193
  }
44194
+ var EventVocabularyKindSchema = _enum([
44195
+ "press",
44196
+ "long-press",
44197
+ "long-press-start",
44198
+ "release",
44199
+ "repeat",
44200
+ "double-press",
44201
+ "other"
44202
+ ]);
44203
+ /** The part of the device an event comes from: a button, a channel. */
44204
+ var EventVocabularySourceSchema = object({
44205
+ id: string().min(1),
44206
+ label: string().min(1)
44207
+ });
44208
+ /** One scalar field of an event's `data`, by dotted path. */
44209
+ var EventVocabularyFieldSchema = object({
44210
+ path: string().min(1),
44211
+ kind: _enum([
44212
+ "string",
44213
+ "number",
44214
+ "boolean",
44215
+ "enum"
44216
+ ]),
44217
+ enumValues: array(string()).optional(),
44218
+ label: string().min(1).optional()
44219
+ });
44220
+ var EventVocabularyEntrySchema = object({
44221
+ /** The native token, verbatim. */
44222
+ id: string().min(1),
44223
+ source: EventVocabularySourceSchema.optional(),
44224
+ label: string().min(1),
44225
+ description: string().optional(),
44226
+ kind: EventVocabularyKindSchema.optional(),
44227
+ payload: array(EventVocabularyFieldSchema).optional()
44228
+ });
44101
44229
  Math.PI / 180;
44102
44230
  /**
44103
44231
  * WHEN something happens, as data (D983) — the one time vocabulary of the
@@ -44150,6 +44278,62 @@ var WeekdayKeySchema = _enum([
44150
44278
  "sat",
44151
44279
  "sun"
44152
44280
  ]);
44281
+ /**
44282
+ * Hold semantics — "for N seconds" on a leaf of the `when` tree (#498).
44283
+ *
44284
+ * ONE primitive, `for: { ms, mode, tolerance? }`, shared by `device-becomes`,
44285
+ * `device-is` and `expression`:
44286
+ *
44287
+ * - `held`: the condition stays true for `ms`;
44288
+ * - `stable`: the value does not move by more than `tolerance` (absolute, in
44289
+ * the field's base unit) for `ms` while the condition holds.
44290
+ *
44291
+ * {@link HoldCore} is the pure state machine (an input observation → a reading).
44292
+ * {@link HoldTracker} keys cores by a leaf path for the STATE leaves the tree
44293
+ * evaluator judges. {@link BecomesWatch} wraps a core with a timer for the
44294
+ * `device-becomes` EVENT leaf: the deadline is armed when the hold starts and
44295
+ * cancelled when it breaks, and the leaf fires ONCE per episode.
44296
+ *
44297
+ * Hold state is memory only. A hold that is already running when the watcher
44298
+ * first looks (a restart) is INHERITED: nobody knows when it began, so it never
44299
+ * fires for that episode and a state leaf reads it as unknown until the window
44300
+ * has elapsed from the moment the watcher started (D49: not-known is not false).
44301
+ */
44302
+ var HOLD_MAX_MS = 864e5;
44303
+ var HoldModeSchema = _enum(["held", "stable"]);
44304
+ var HoldSpecSchema = object({
44305
+ /** How long the condition (or the value) must hold. */
44306
+ ms: number$1().int().min(1).max(HOLD_MAX_MS),
44307
+ mode: HoldModeSchema,
44308
+ /** `stable` only: the largest move of the value that still counts as unchanged (absolute, base unit). */
44309
+ tolerance: number$1().min(0).optional()
44310
+ });
44311
+ /** What a trace says about a hold. */
44312
+ var HoldTraceSchema = object({
44313
+ phase: _enum([
44314
+ "pending",
44315
+ "cancelled",
44316
+ "fired",
44317
+ "reseeded"
44318
+ ]),
44319
+ ms: number$1().int(),
44320
+ mode: HoldModeSchema,
44321
+ /** When the hold started (ms epoch). */
44322
+ since: number$1().optional(),
44323
+ /** When it completes (ms epoch), for a pending hold. */
44324
+ deadline: number$1().optional()
44325
+ });
44326
+ /** The operators a `device-becomes` leaf may carry: a threshold or a range. */
44327
+ var BecomesOperatorSchema = _enum([
44328
+ "eq",
44329
+ "ne",
44330
+ "gt",
44331
+ "gte",
44332
+ "lt",
44333
+ "lte",
44334
+ "between",
44335
+ "outside"
44336
+ ]);
44153
44337
  /** How a state leaf compares a device field to a value. Derived from the
44154
44338
  * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
44155
44339
  var AutomationConditionOperatorSchema = _enum([
@@ -44163,6 +44347,13 @@ var AutomationConditionOperatorSchema = _enum([
44163
44347
  "in"
44164
44348
  ]);
44165
44349
  /**
44350
+ * The stable name of an event leaf. A path (`when.0.1`) moves when a row is
44351
+ * reordered; the id does not, so a `triggered-by` leaf keeps pointing at the
44352
+ * same row. Optional: a leaf nothing refers to needs none.
44353
+ */
44354
+ var EVENT_LEAF_ID_RE = /^[A-Za-z0-9_-]{1,40}$/;
44355
+ var eventLeafId = { id: string().regex(EVENT_LEAF_ID_RE).optional() };
44356
+ /**
44166
44357
  * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
44167
44358
  * subscription: it evaluates against the **state mirror**, which is reconciled,
44168
44359
  * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
@@ -44170,24 +44361,60 @@ var AutomationConditionOperatorSchema = _enum([
44170
44361
  */
44171
44362
  var DeviceBecomesLeafSchema = object({
44172
44363
  kind: literal("device-becomes"),
44364
+ ...eventLeafId,
44173
44365
  deviceId: number$1().int().nonnegative(),
44174
44366
  cap: string().min(1),
44175
44367
  fieldPath: string().min(1),
44176
- /** Fire when the field takes this value. Omit to fire on any change. */
44368
+ /** Fire when the field takes this value. Omit (with no `operator`) to fire on any change. */
44177
44369
  becomes: union([
44178
44370
  string(),
44179
44371
  number$1(),
44180
44372
  boolean()
44181
44373
  ]).optional(),
44182
- /** Only on a CHANGE of value, not on every re-report. */
44374
+ /**
44375
+ * A threshold or a range instead of an equality: "crosses above 3000", "enters
44376
+ * 18..24". The leaf fires on the TRANSITION into the predicate, never on a re-report.
44377
+ * `value` is in the field's base unit; `between` / `outside` take `[low, high]`.
44378
+ */
44379
+ operator: BecomesOperatorSchema.optional(),
44380
+ value: union([
44381
+ number$1(),
44382
+ string(),
44383
+ boolean(),
44384
+ array(number$1()).length(2)
44385
+ ]).optional(),
44386
+ /**
44387
+ * Hold: the transition (or, with no predicate, the new value) must stay for
44388
+ * `for.ms` before the leaf fires, ONCE per episode. See `hold.ts`.
44389
+ */
44390
+ for: HoldSpecSchema.optional(),
44391
+ /** @deprecated every leaf fires on a transition now; read as always on. */
44183
44392
  edge: boolean().optional(),
44184
- /** The new value must hold this long before it counts. */
44185
- forMs: number$1().int().min(0).max(864e5).optional(),
44393
+ /** @deprecated legacy `held` window — read through `holdOf` as `for: { ms, mode: 'held' }`. */
44394
+ forMs: number$1().int().min(0).max(HOLD_MAX_MS).optional(),
44186
44395
  /** Collapse a burst into one occurrence. */
44187
44396
  debounceMs: number$1().int().min(0).max(6e5).optional()
44397
+ }).superRefine((leaf, ctx) => {
44398
+ if (leaf.becomes !== void 0 && leaf.operator !== void 0) ctx.addIssue({
44399
+ code: "custom",
44400
+ message: "a leaf names `becomes` or `operator`, not both",
44401
+ path: ["operator"]
44402
+ });
44403
+ if (leaf.operator !== void 0 && leaf.value === void 0) ctx.addIssue({
44404
+ code: "custom",
44405
+ message: "an operator needs the value to compare to",
44406
+ path: ["value"]
44407
+ });
44408
+ const range = leaf.operator === "between" || leaf.operator === "outside";
44409
+ if (leaf.operator !== void 0 && range !== Array.isArray(leaf.value)) ctx.addIssue({
44410
+ code: "custom",
44411
+ message: range ? "a range takes [low, high]" : "only a range takes [low, high]",
44412
+ path: ["value"]
44413
+ });
44188
44414
  });
44189
44415
  var DeviceEventLeafSchema = object({
44190
44416
  kind: literal("device-event"),
44417
+ ...eventLeafId,
44191
44418
  /** An `EventCategory` value. */
44192
44419
  category: string().min(1),
44193
44420
  deviceId: number$1().int().nonnegative().optional()
@@ -44200,18 +44427,51 @@ var DeviceEventLeafSchema = object({
44200
44427
  */
44201
44428
  var ScheduleLeafSchema = object({
44202
44429
  kind: literal("schedule"),
44430
+ ...eventLeafId,
44203
44431
  at: TimeSpecSchema.optional(),
44204
44432
  days: array(WeekdayKeySchema).min(1).max(7).optional(),
44205
44433
  /** @deprecated legacy cron — new schedules use `at`. */
44206
44434
  cron: string().min(1).max(120).optional()
44207
44435
  }).refine((leaf) => leaf.at === void 0 !== (leaf.cron === void 0), { message: "a schedule names exactly one of `at` (a time spec) and `cron` (legacy)" }).refine((leaf) => leaf.days === void 0 || leaf.at !== void 0, { message: "`days` goes with `at`; a cron expression carries its own days" });
44208
- var ManualLeafSchema = object({ kind: literal("manual") });
44436
+ /**
44437
+ * One payload predicate of a `device-fired` row: the value at `path` inside
44438
+ * the fire's `data` (dotted) equals `equals`. Compared strictly, no coercion.
44439
+ */
44440
+ var DeviceFiredMatchSchema = object({
44441
+ path: string().min(1).max(120),
44442
+ equals: union([
44443
+ string(),
44444
+ number$1(),
44445
+ boolean()
44446
+ ])
44447
+ });
44448
+ /**
44449
+ * "fires": one device of the `event-emitter` capability reported an event
44450
+ * (D1045). Every list is an OR, the lists together an AND; an absent list does
44451
+ * not narrow. `eventIds` are the device's own tokens, `sourceIds` the parts of
44452
+ * the device (a button), `kinds` the shared press kinds of its vocabulary.
44453
+ */
44454
+ var DeviceFiredLeafSchema = object({
44455
+ kind: literal("device-fired"),
44456
+ ...eventLeafId,
44457
+ deviceId: number$1().int().nonnegative(),
44458
+ eventIds: array(string().min(1)).min(1).max(64).optional(),
44459
+ sourceIds: array(string().min(1)).min(1).max(64).optional(),
44460
+ kinds: array(EventVocabularyKindSchema).min(1).optional(),
44461
+ match: array(DeviceFiredMatchSchema).min(1).max(8).optional()
44462
+ });
44463
+ var ManualLeafSchema = object({
44464
+ kind: literal("manual"),
44465
+ ...eventLeafId
44466
+ });
44209
44467
  var IntervalLeafSchema = object({
44210
44468
  kind: literal("interval"),
44469
+ ...eventLeafId,
44211
44470
  minutes: number$1().int().min(1).max(1440)
44212
44471
  });
44213
44472
  var DetectionLeafSchema = object({
44214
44473
  kind: literal("detection"),
44474
+ ...eventLeafId,
44215
44475
  /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
44216
44476
  subjects: array(DetectionSubjectSchema).min(1),
44217
44477
  deviceIds: array(number$1().int().nonnegative()).min(1),
@@ -44222,6 +44482,7 @@ var DetectionLeafSchema = object({
44222
44482
  });
44223
44483
  var StationaryLeafSchema = object({
44224
44484
  kind: literal("stationary"),
44485
+ ...eventLeafId,
44225
44486
  deviceIds: array(number$1().int().nonnegative()).min(1),
44226
44487
  subjects: array(StationarySubjectSchema).min(1).optional(),
44227
44488
  zones: array(string().min(1)).min(1).optional(),
@@ -44249,7 +44510,9 @@ var DeviceIsLeafSchema = object({
44249
44510
  QuantityValueSchema
44250
44511
  ]),
44251
44512
  /** The unit the person picked for `value` (display only; `value` is in the base unit). */
44252
- unit: string().min(1).optional()
44513
+ unit: string().min(1).optional(),
44514
+ /** The comparison must hold (`held`) or the value stay put (`stable`) this long. */
44515
+ for: HoldSpecSchema.optional()
44253
44516
  });
44254
44517
  /**
44255
44518
  * True when the camera currently has a PARKED object matching `subjects` in at
@@ -44265,6 +44528,41 @@ var StationaryPresentLeafSchema = object({
44265
44528
  minCount: number$1().int().positive().optional()
44266
44529
  });
44267
44530
  /**
44531
+ * True while the site-local clock is inside a daily window and/or on one of
44532
+ * the named days. `days` names the local day a window STARTS on, so
44533
+ * 22:00-06:00 on weekdays is still inside at 01:00 Saturday. A one-date spec
44534
+ * (`kind: 'date'`) is refused: only a clock time or a sun event makes a window
44535
+ * that repeats every day.
44536
+ */
44537
+ var TimeIsLeafSchema = object({
44538
+ kind: literal("time-is"),
44539
+ between: object({
44540
+ from: TimeSpecSchema,
44541
+ to: TimeSpecSchema
44542
+ }).optional(),
44543
+ days: array(WeekdayKeySchema).min(1).max(7).optional()
44544
+ }).superRefine((leaf, ctx) => {
44545
+ if (leaf.between === void 0 && leaf.days === void 0) ctx.addIssue({
44546
+ code: "custom",
44547
+ message: "a time-is leaf needs a between window, days, or both"
44548
+ });
44549
+ for (const end of ["from", "to"]) if (leaf.between?.[end].kind === "date") ctx.addIssue({
44550
+ code: "custom",
44551
+ message: "a one-date time cannot bound a daily window: use a clock time or a sun event",
44552
+ path: ["between", end]
44553
+ });
44554
+ });
44555
+ /**
44556
+ * True for the run that one of the named event leaves started. It reads the
44557
+ * run, not a device: in the tree it is judged against the leaf that fired, in
44558
+ * a step guard against the trigger the run carries. A run no event leaf
44559
+ * started (a state-only tree becoming true) is false.
44560
+ */
44561
+ var TriggeredByLeafSchema = object({
44562
+ kind: literal("triggered-by"),
44563
+ leafIds: array(string().regex(EVENT_LEAF_ID_RE)).min(1).max(20)
44564
+ });
44565
+ /**
44268
44566
  * The escape hatch: the bounded expression engine over named device bindings.
44269
44567
  * Declared as a plain object so a discriminated union can read `kind`; the
44270
44568
  * author-time validation is the SAME function `ExpressionSourceSchema` runs.
@@ -44272,7 +44570,9 @@ var StationaryPresentLeafSchema = object({
44272
44570
  var ExpressionLeafSchema = object({
44273
44571
  kind: literal("expression"),
44274
44572
  expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
44275
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
44573
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema),
44574
+ /** The expression must stay true this long. An expression has no single value, so only `held`. */
44575
+ for: HoldSpecSchema.optional()
44276
44576
  }).superRefine((src, ctx) => {
44277
44577
  const err = validateExpressionSource(src);
44278
44578
  if (err !== null) ctx.addIssue({
@@ -44280,6 +44580,11 @@ var ExpressionLeafSchema = object({
44280
44580
  message: err,
44281
44581
  path: ["expr"]
44282
44582
  });
44583
+ if (src.for?.mode === "stable") ctx.addIssue({
44584
+ code: "custom",
44585
+ message: "an expression is held, not stable: it has no single value to keep still",
44586
+ path: ["for", "mode"]
44587
+ });
44283
44588
  });
44284
44589
  var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44285
44590
  object({
@@ -44296,6 +44601,7 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44296
44601
  }),
44297
44602
  DeviceBecomesLeafSchema,
44298
44603
  DeviceEventLeafSchema,
44604
+ DeviceFiredLeafSchema,
44299
44605
  ScheduleLeafSchema,
44300
44606
  ManualLeafSchema,
44301
44607
  IntervalLeafSchema,
@@ -44303,11 +44609,14 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
44303
44609
  StationaryLeafSchema,
44304
44610
  DeviceIsLeafSchema,
44305
44611
  StationaryPresentLeafSchema,
44306
- ExpressionLeafSchema
44612
+ ExpressionLeafSchema,
44613
+ TimeIsLeafSchema,
44614
+ TriggeredByLeafSchema
44307
44615
  ]));
44308
44616
  var EVENT_KINDS = new Set([
44309
44617
  "device-becomes",
44310
44618
  "device-event",
44619
+ "device-fired",
44311
44620
  "schedule",
44312
44621
  "manual",
44313
44622
  "interval",
@@ -44357,6 +44666,28 @@ function hasEventLeaves(node) {
44357
44666
  });
44358
44667
  return found;
44359
44668
  }
44669
+ /** The event leaves of the tree that carry an id, in tree order. */
44670
+ function identifiedEventLeaves(when) {
44671
+ const found = [];
44672
+ forEachLeaf(when, (leaf, path) => {
44673
+ if (isEventLeaf(leaf) && leaf.id !== void 0) found.push({
44674
+ id: leaf.id,
44675
+ path,
44676
+ leaf
44677
+ });
44678
+ });
44679
+ return found;
44680
+ }
44681
+ /** Does the tree hold a `triggered-by` leaf? */
44682
+ function hasTriggeredByLeaves(when) {
44683
+ let found = false;
44684
+ forEachLeaf(when, (leaf) => {
44685
+ if (leaf.kind === "triggered-by") found = true;
44686
+ });
44687
+ return found;
44688
+ }
44689
+ /** Why a `triggered-by` leaf is refused outside an automation: there is no trigger row to name. */
44690
+ var TRIGGERED_BY_NEEDS_AUTOMATION = "\"triggered by\" reads which trigger started an automation — an action or a scheduler has no trigger rows, so it could never be true here";
44360
44691
  function hasEmptyContainer(node) {
44361
44692
  if (isLeaf(node)) return false;
44362
44693
  if (node.kind === "not") return hasEmptyContainer(node.child);
@@ -44366,6 +44697,7 @@ function hasEmptyContainer(node) {
44366
44697
  var LEAF_WORDS = {
44367
44698
  "device-becomes": "a device value that becomes",
44368
44699
  "device-event": "a device event",
44700
+ "device-fired": "a device firing an event",
44369
44701
  schedule: "a schedule",
44370
44702
  manual: "a manual run",
44371
44703
  interval: "an interval",
@@ -44415,6 +44747,11 @@ function validateWhenTree(when) {
44415
44747
  if (depth > 4) return `the rules nest ${depth} levels deep — the limit is 4. "not (any of …)" is the same as "all of (not …)", which is one level shallower`;
44416
44748
  const leaves = countWhenLeaves(when);
44417
44749
  if (leaves > 32) return `the rules use ${leaves} rows — the limit is 32`;
44750
+ const seen = /* @__PURE__ */ new Set();
44751
+ for (const entry of identifiedEventLeaves(when)) {
44752
+ if (seen.has(entry.id)) return `two trigger rows share the id "${entry.id}" — an id names one row`;
44753
+ seen.add(entry.id);
44754
+ }
44418
44755
  const need = eventNeed(when, "when", false);
44419
44756
  if ("error" in need) return need.error;
44420
44757
  const [first, second] = need.paths;
@@ -44626,9 +44963,12 @@ function convertLegacyWhen(half) {
44626
44963
  * every caller — automations, Actions, and the notification centre, which runs
44627
44964
  * only Actions (planning#473).
44628
44965
  */
44966
+ /** The longest one `wait` step lasts. It is cut by a restart of the run and does not survive a hub restart. */
44967
+ var WAIT_MAX_SECONDS = 3600;
44968
+ var WAIT_TOO_LONG = `a wait is at most one hour (${WAIT_MAX_SECONDS} s)`;
44629
44969
  var AutomationWaitStepSchema = object({
44630
44970
  kind: literal("wait"),
44631
- seconds: number$1().min(0).max(300)
44971
+ seconds: number$1().min(0).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG)
44632
44972
  });
44633
44973
  /** The legacy `deviceId` of a cap step: still READ, never written (D983). */
44634
44974
  var LegacyStepDeviceSchema = union([
@@ -44698,28 +45038,227 @@ var elseNeedsIf = {
44698
45038
  path: ["else"]
44699
45039
  };
44700
45040
  var GuardedWaitStepSchema = AutomationWaitStepSchema.extend(stepGuardShape).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44701
- var GuardedCapStepSchema = AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
45041
+ /**
45042
+ * A step of a STANDALONE list (an action, a scheduler point, a preset): the
45043
+ * same two step kinds an automation runs, minus a guard that reads the
45044
+ * automation's trigger, which such a list does not have.
45045
+ */
45046
+ var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
45047
+ message: "a cap step needs a target",
45048
+ path: ["target"]
45049
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
45050
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
45051
+ path: ["actionId"]
45052
+ }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf)]).superRefine((step, ctx) => {
45053
+ if (step.if !== void 0 && hasTriggeredByLeaves(step.if)) ctx.addIssue({
45054
+ code: "custom",
45055
+ message: TRIGGERED_BY_NEEDS_AUTOMATION,
45056
+ path: ["if"]
45057
+ });
45058
+ });
45059
+ /**
45060
+ * Flow control of an AUTOMATION's list (#500). A standalone list (an action, a
45061
+ * scheduler point, a preset) stays `wait` and `cap`: it has no run of its own
45062
+ * to end and no variable bag to write.
45063
+ */
45064
+ var continueOnErrorShape = {
45065
+ /** A failure of this step is traced `failed-continued` and the next step runs. */
45066
+ continueOnError: literal(true).optional() };
45067
+ /** An automation's cap step: the shared guarded cap step, plus `continueOnError`. */
45068
+ var AutomationGuardedCapStepSchema = AutomationCapStepBaseSchema.extend({
45069
+ ...stepGuardShape,
45070
+ ...continueOnErrorShape
45071
+ }).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
44702
45072
  message: "a cap step needs a target",
44703
45073
  path: ["target"]
44704
45074
  }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
44705
45075
  message: "a domain target needs the vocabulary action it applies to (actionId)",
44706
45076
  path: ["actionId"]
44707
45077
  }).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
44708
- var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, GuardedCapStepSchema]);
45078
+ /** End the run here, on purpose. `reason` may carry `{{vars.x}}` slots. */
45079
+ var AutomationStopStepSchema = object({
45080
+ kind: literal("stop"),
45081
+ reason: string().min(1, "a stop needs a reason: it is what the run trace shows").max(200),
45082
+ /** `ok`: the run ends as a success. `failed`: it ends as a failure, with the reason. */
45083
+ outcome: _enum(["ok", "failed"]),
45084
+ /** Stop only when this reads true; false or unknown does not stop. */
45085
+ if: AutomationWhenSchema.optional()
45086
+ });
45087
+ var SET_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,39}$/;
45088
+ /** Names the run itself writes and reads (`leaf` is what `triggered-by` judges): a step may not overwrite them. */
45089
+ var RESERVED_RUN_VARS = [
45090
+ "kind",
45091
+ "leaf",
45092
+ "at"
45093
+ ];
45094
+ /**
45095
+ * Write variables into the run's bag, for the steps after it. SUBSTITUTION
45096
+ * only, never evaluation: a text value may carry `{{vars.x}}` slots.
45097
+ */
45098
+ var AutomationSetStepSchema = object({
45099
+ kind: literal("set"),
45100
+ vars: record(string(), union([
45101
+ string().max(500),
45102
+ number$1(),
45103
+ boolean()
45104
+ ])).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` }),
45105
+ /** Set only when this reads true; false or unknown leaves the bag as it is. */
45106
+ if: AutomationWhenSchema.optional()
45107
+ });
45108
+ var AutomationRunActionStepSchema = object({
45109
+ kind: literal("action"),
45110
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
45111
+ actionId: string().min(1),
45112
+ /** Values for the action's declared variables; the others take their default. */
45113
+ variables: record(string(), union([
45114
+ string(),
45115
+ number$1(),
45116
+ boolean()
45117
+ ])).optional(),
45118
+ ...continueOnErrorShape
45119
+ });
45120
+ /**
45121
+ * A step INSIDE a branch of a `choose`: a flow step with no condition of its
45122
+ * own (the branch is the condition) and no block inside it. Strict, so an `if`
45123
+ * written on such a step is refused rather than dropped.
45124
+ */
45125
+ /** The longest a `wait-until` may wait: six hours. */
45126
+ var WAIT_UNTIL_TIMEOUT_MAX_S = 21600;
45127
+ /**
45128
+ * Pause the run until a condition is true (#500). The timeout is mandatory: a
45129
+ * run never waits without an end. `onTimeout` says what a wait that ran out
45130
+ * does: `continue` goes on to the next step, `stop` ends the run as a failure.
45131
+ * A condition that cannot be read is not "true": the wait goes on (D49).
45132
+ */
45133
+ var AutomationWaitUntilStepSchema = object({
45134
+ kind: literal("wait-until"),
45135
+ when: AutomationWhenSchema,
45136
+ 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)`),
45137
+ onTimeout: _enum(["continue", "stop"])
45138
+ }).strict();
45139
+ /** The step kinds that hold steps of their own, or pause on a condition: they nest two levels at most. */
45140
+ var BLOCK_STEP_KINDS = [
45141
+ "choose",
45142
+ "repeat",
45143
+ "parallel",
45144
+ "wait-until"
45145
+ ];
45146
+ var NEST_TOO_DEEP = `steps nest two levels at most: a step inside a nested choose or repeat can be a plain step only`;
45147
+ function kindOf(input) {
45148
+ return typeof input === "object" && input !== null ? Reflect.get(input, "kind") : void 0;
45149
+ }
45150
+ /** Where the nesting ends, a block step is refused in words rather than as an unknown kind. */
45151
+ function nestTooDeep(issue) {
45152
+ const kind = kindOf(issue.input);
45153
+ return typeof kind === "string" && BLOCK_STEP_KINDS.includes(kind) ? NEST_TOO_DEEP : void 0;
45154
+ }
45155
+ var plainStepSchemas = [
45156
+ AutomationWaitStepSchema.strict(),
45157
+ AutomationCapStepBaseSchema.extend(continueOnErrorShape).strict().refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
45158
+ message: "a cap step needs a target",
45159
+ path: ["target"]
45160
+ }).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
45161
+ message: "a domain target needs the vocabulary action it applies to (actionId)",
45162
+ path: ["actionId"]
45163
+ }),
45164
+ AutomationStopStepSchema.omit({ if: true }).strict(),
45165
+ AutomationSetStepSchema.omit({ if: true }).strict(),
45166
+ AutomationRunActionStepSchema.strict()
45167
+ ];
45168
+ /**
45169
+ * A plain step: a flow step with no condition of its own and no step inside
45170
+ * it. The second (last) level of nesting holds these only.
45171
+ */
45172
+ var AutomationBranchStepSchema = discriminatedUnion("kind", plainStepSchemas, { error: nestTooDeep });
45173
+ /** A step that can run beside others: it writes no variable and does not end the run. */
45174
+ 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");
45175
+ /**
45176
+ * Several steps at once (#500): all are started together and all are awaited.
45177
+ * A failure fails the run once the others finished, as in a sequence; a member
45178
+ * marked "continue if this fails" does not.
45179
+ */
45180
+ var AutomationParallelStepSchema = object({
45181
+ kind: literal("parallel"),
45182
+ steps: array(AutomationParallelMemberSchema).min(2, "a parallel needs at least two steps").max(5, `a parallel takes at most 5 steps`)
45183
+ }).strict();
45184
+ var REPEAT_NEEDS_MAX = "a repeat on a condition needs a maximum number of iterations";
45185
+ /** A loop on a condition: the bound is mandatory, so a run never loops without an end. */
45186
+ var repeatConditionShape = {
45187
+ when: AutomationWhenSchema,
45188
+ 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`),
45189
+ /** A pause between two iterations, never after the last. */
45190
+ waitS: number$1().int().min(1).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG).optional()
45191
+ };
45192
+ /** What decides how many times a `repeat` runs. */
45193
+ var AutomationRepeatLoopSchema = discriminatedUnion("by", [
45194
+ object({
45195
+ by: literal("count"),
45196
+ count: number$1().int().min(1).max(20, `a repeat runs at most 20 times`)
45197
+ }).strict(),
45198
+ object({
45199
+ by: literal("while"),
45200
+ ...repeatConditionShape
45201
+ }).strict(),
45202
+ object({
45203
+ by: literal("until"),
45204
+ ...repeatConditionShape
45205
+ }).strict(),
45206
+ object({
45207
+ by: literal("forEach"),
45208
+ target: object({
45209
+ kind: literal("devices"),
45210
+ 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`)
45211
+ }).strict()
45212
+ }).strict()
45213
+ ]);
45214
+ /**
45215
+ * The same steps several times (#500). Every form is bounded at save, so a
45216
+ * run cannot loop without an end. A failure or a `stop` in the steps ends the
45217
+ * loop and the run, as in a sequence.
45218
+ */
45219
+ function repeatSchemaOf(step) {
45220
+ return object({
45221
+ kind: literal("repeat"),
45222
+ loop: AutomationRepeatLoopSchema,
45223
+ steps: array(step).min(1, "a repeat needs at least one step").max(10, `a repeat takes at most 10 steps`)
45224
+ }).strict();
45225
+ }
45226
+ /**
45227
+ * One step, several ways (#500): the branches are read in order and the FIRST
45228
+ * whose condition is true runs; `else` runs when every branch is false. A
45229
+ * branch that cannot be read ends the choice: nothing runs, not the branches
45230
+ * after it and not the else (D49). The #472 `if` / `else` of a wait or cap
45231
+ * step is the one-branch case and stays as it is stored.
45232
+ */
45233
+ function chooseSchemaOf(step) {
45234
+ const steps = array(step).max(10);
45235
+ return object({
45236
+ kind: literal("choose"),
45237
+ branches: array(object({
45238
+ if: AutomationWhenSchema,
45239
+ steps: steps.min(1, "a branch needs at least one step")
45240
+ })).min(1, "a choose needs at least one branch").max(6, `a choose takes at most 6 branches`),
45241
+ else: steps.optional()
45242
+ });
45243
+ }
45244
+ var AutomationBodyStepSchema = discriminatedUnion("kind", [
45245
+ ...plainStepSchemas,
45246
+ AutomationWaitUntilStepSchema,
45247
+ AutomationParallelStepSchema,
45248
+ chooseSchemaOf(AutomationBranchStepSchema),
45249
+ repeatSchemaOf(AutomationBranchStepSchema)
45250
+ ]);
45251
+ var AutomationRepeatStepSchema = repeatSchemaOf(AutomationBodyStepSchema);
44709
45252
  var AutomationActionSchema = discriminatedUnion("kind", [
44710
45253
  GuardedWaitStepSchema,
44711
- GuardedCapStepSchema,
44712
- object({
44713
- kind: literal("action"),
44714
- /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
44715
- actionId: string().min(1),
44716
- /** Values for the action's declared variables; the others take their default. */
44717
- variables: record(string(), union([
44718
- string(),
44719
- number$1(),
44720
- boolean()
44721
- ])).optional()
44722
- }),
45254
+ AutomationGuardedCapStepSchema,
45255
+ AutomationStopStepSchema,
45256
+ AutomationSetStepSchema,
45257
+ chooseSchemaOf(AutomationBodyStepSchema),
45258
+ AutomationWaitUntilStepSchema,
45259
+ AutomationParallelStepSchema,
45260
+ AutomationRepeatStepSchema,
45261
+ AutomationRunActionStepSchema,
44723
45262
  object({
44724
45263
  kind: literal("code"),
44725
45264
  /** Compiled into the automation's OWN block by esbuild — not a third
@@ -44782,11 +45321,6 @@ function convertLegacyRecipe(legacy) {
44782
45321
  ...rest
44783
45322
  };
44784
45323
  }
44785
- /**
44786
- * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
44787
- * saved block carries): the new shape as it is, or the old shape converted to
44788
- * it. The output type is always the new one.
44789
- */
44790
45324
  var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
44791
45325
  var DETECTION_DESCRIPTIONS = {
44792
45326
  deviceId: "the camera that saw it",
@@ -44802,6 +45336,21 @@ var STATIONARY_DESCRIPTIONS = {
44802
45336
  phase: "appeared or departed",
44803
45337
  entryId: "the stopped object"
44804
45338
  };
45339
+ var BECOMES_DESCRIPTIONS = {
45340
+ deviceId: "the device that changed",
45341
+ from: "the value before the change, empty when it is not known",
45342
+ to: "the value it changed to"
45343
+ };
45344
+ /**
45345
+ * A fire's payload is offered too, as `data.<path>` (`{{vars.data.channel}}`):
45346
+ * those names depend on the device, so they are not in this list.
45347
+ */
45348
+ var FIRED_DESCRIPTIONS = {
45349
+ deviceId: "the device that fired",
45350
+ eventType: "the event, as the device names it",
45351
+ sourceId: "the part of the device that fired (a button), empty when it names none",
45352
+ eventKind: "the kind of press (press, long-press …), empty when the device declares none"
45353
+ };
44805
45354
  function describe$1(descriptions) {
44806
45355
  return Object.entries(descriptions).map(([name, description]) => ({
44807
45356
  name,
@@ -44810,8 +45359,74 @@ function describe$1(descriptions) {
44810
45359
  }
44811
45360
  describe$1(DETECTION_DESCRIPTIONS);
44812
45361
  describe$1(STATIONARY_DESCRIPTIONS);
45362
+ describe$1(FIRED_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
45363
+ ...v,
45364
+ hintKey: "firedDeviceId"
45365
+ } : v);
45366
+ describe$1(BECOMES_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
45367
+ ...v,
45368
+ hintKey: "becomesDeviceId"
45369
+ } : v);
44813
45370
  new Set(Object.values(COCO_TO_MACRO.mapping));
44814
45371
  /**
45372
+ * Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
45373
+ * device. Carries the device's EXACT declared event vocabulary verbatim
45374
+ * (NO normalization). Two shapes:
45375
+ * - structured source (HA `event.*` entity) → `vocabulary` is the fixed
45376
+ * declared set (e.g. press_short, press_long, …).
45377
+ * - generic/legacy source (HA bus events, e.g. zha_event) → `vocabulary`
45378
+ * is [] (open) and `lastEvent.data` carries the complex payload.
45379
+ * `vocabulary` (D1044) names each event for a person and types it for a
45380
+ * rule; `eventTypes` is its list of native tokens, kept for older readers.
45381
+ * A fire names the part of the device it came from in `sourceId` when the
45382
+ * vocabulary declares sources (a remote's buttons).
45383
+ * `seq` is monotonic so two identical events still produce distinct slice writes.
45384
+ */
45385
+ var EventFireSchema = object({
45386
+ deviceId: number$1(),
45387
+ eventType: string(),
45388
+ sourceId: string().optional(),
45389
+ data: record(string(), unknown()).nullable(),
45390
+ timestamp: number$1().meta(readingMeta("none")),
45391
+ seq: number$1().meta(readingMeta("count"))
45392
+ });
45393
+ var EventEmitterStatusSchema = object({
45394
+ eventTypes: array(string()),
45395
+ /**
45396
+ * Required of a provider by the type. A status written before the field
45397
+ * existed still parses, as an empty vocabulary: read it through
45398
+ * `readEventVocabulary`, which then answers with the plain tokens.
45399
+ */
45400
+ vocabulary: array(EventVocabularyEntrySchema).default([]),
45401
+ lastEvent: EventFireSchema.nullable(),
45402
+ eventCountSinceStart: number$1().meta(readingMeta("count"))
45403
+ });
45404
+ var eventEmitterCapability = {
45405
+ name: "event-emitter",
45406
+ scope: "device",
45407
+ deviceNative: true,
45408
+ mode: "singleton",
45409
+ deviceTypes: [DeviceType.EventEmitter],
45410
+ methods: {},
45411
+ events: { onEvent: { data: EventFireSchema } },
45412
+ status: {
45413
+ schema: EventEmitterStatusSchema,
45414
+ kind: "push"
45415
+ },
45416
+ runtimeState: EventEmitterStatusSchema,
45417
+ /**
45418
+ * Runtime-state durability: **session** — `eventCountSinceStart` names its own scope.
45419
+ *
45420
+ * See `RuntimeStateDurability`. Enforced by
45421
+ * `scripts/check-runtime-state-durability.ts`.
45422
+ */
45423
+ durability: "session",
45424
+ /**
45425
+ * History (D850): none — `eventCountSinceStart` resets with the addon; events live in ledgers.
45426
+ */
45427
+ history: "none"
45428
+ };
45429
+ /**
44815
45430
  * Schedulers (D983) — an entity that performs actions over the day.
44816
45431
  *
44817
45432
  * A scheduler has profiles; a profile has weekdays and a timeline of POINTS.
@@ -44834,6 +45449,13 @@ var SchedulerStateTreeSchema = AutomationWhenSchema.superRefine((when, ctx) => {
44834
45449
  });
44835
45450
  return;
44836
45451
  }
45452
+ if (hasTriggeredByLeaves(when)) {
45453
+ ctx.addIssue({
45454
+ code: "custom",
45455
+ message: TRIGGERED_BY_NEEDS_AUTOMATION
45456
+ });
45457
+ return;
45458
+ }
44837
45459
  const problem = validateWhenTree(when);
44838
45460
  if (problem !== null) ctx.addIssue({
44839
45461
  code: "custom",
@@ -44855,12 +45477,16 @@ var HoldOverrideSchema = discriminatedUnion("mode", [object({ mode: literal("res
44855
45477
  })]);
44856
45478
  /** A missed edge up to this late still runs. */
44857
45479
  var DEFAULT_MAX_LATE_MS = 10 * 6e4;
45480
+ var SchedulerStepSchema = ActionStepSchema.refine((step) => step.kind !== "wait" || step.seconds <= 300, {
45481
+ message: `a scheduler point waits at most five minutes (300 s); put a longer wait in an action`,
45482
+ path: ["seconds"]
45483
+ });
44858
45484
  var SchedulerPointSchema = object({
44859
45485
  id: string().min(1).max(64),
44860
45486
  at: SchedulerTimeSpecSchema,
44861
45487
  mode: SchedulerPointModeSchema,
44862
45488
  /** The actions of an `edge` or `hold`; a `release` has none. */
44863
- steps: array(ActionStepSchema).max(20).default([]),
45489
+ steps: array(SchedulerStepSchema).max(20).default([]),
44864
45490
  /** Checked when the point runs (an edge) or is applied (a hold). */
44865
45491
  conditions: SchedulerStateTreeSchema.optional(),
44866
45492
  /** Hold only; absent means `respect`. */
@@ -45039,7 +45665,9 @@ var AutomationTriggerTraceSchema = object({
45039
45665
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
45040
45666
  reason: string(),
45041
45667
  /** deviceId, className, trackId, score … — only what the trigger exposes. */
45042
- vars: record(string(), TraceValueSchema)
45668
+ vars: record(string(), TraceValueSchema),
45669
+ /** A hold (`for`) this occurrence went through: pending, cancelled, fired or re-seeded. */
45670
+ hold: HoldTraceSchema.optional()
45043
45671
  });
45044
45672
  var AutomationConditionTraceNodeSchema = object({
45045
45673
  /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
@@ -45052,8 +45680,11 @@ var AutomationConditionTraceNodeSchema = object({
45052
45680
  "device-is",
45053
45681
  "expression",
45054
45682
  "stationary-present",
45683
+ "time-is",
45684
+ "triggered-by",
45055
45685
  "device-becomes",
45056
45686
  "device-event",
45687
+ "device-fired",
45057
45688
  "schedule",
45058
45689
  "interval",
45059
45690
  "manual",
@@ -45124,18 +45755,46 @@ var StepTraceBaseSchema = object({
45124
45755
  ]).optional(),
45125
45756
  /** Why the condition read what it read, in the evaluator's own words. */
45126
45757
  conditionReason: string().optional(),
45127
- /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. */
45758
+ /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. `failed-continued`: it failed and was marked to be passed over (#500). */
45128
45759
  outcome: _enum([
45129
45760
  "ok",
45130
45761
  "error",
45131
45762
  "not-run",
45132
45763
  "empty",
45133
- "skipped"
45764
+ "skipped",
45765
+ "failed-continued",
45766
+ "timed-out"
45134
45767
  ]),
45135
45768
  message: string().optional()
45136
45769
  });
45770
+ /** What a block step adds to its row: which list ran, what each branch read, how many rounds. */
45771
+ var blockTraceShape = {
45772
+ /** A `choose` step (#500): the list that ran, `branches.<n>` or `else`. Absent when none did. */
45773
+ branch: string().optional(),
45774
+ /** A `choose` step: what each branch read, in order, up to the one that decided. */
45775
+ branchChecks: array(object({
45776
+ condition: _enum([
45777
+ "true",
45778
+ "false",
45779
+ "unknown"
45780
+ ]),
45781
+ conditionReason: string().optional()
45782
+ })).optional(),
45783
+ /** A `repeat` step (#500): how many times its steps ran. */
45784
+ iterations: number$1().int().optional()
45785
+ };
45786
+ /** A step one level down: it may be a block step itself, whose own steps are plain rows (the nesting ends there). */
45787
+ var NestedStepTraceSchema = StepTraceBaseSchema.extend({
45788
+ ...blockTraceShape,
45789
+ steps: array(StepTraceBaseSchema).optional()
45790
+ });
45137
45791
  /** A step trace; the `else` steps that ran are one level of the same shape (the nesting is bounded at one). */
45138
- var AutomationStepTraceSchema = StepTraceBaseSchema.extend({ else: array(StepTraceBaseSchema).optional() });
45792
+ var AutomationStepTraceSchema = StepTraceBaseSchema.extend({
45793
+ else: array(StepTraceBaseSchema).optional(),
45794
+ ...blockTraceShape,
45795
+ /** A `choose` step: the steps of the list that ran. A `parallel`: its members. A `repeat`: the LAST iteration. */
45796
+ steps: array(NestedStepTraceSchema).optional()
45797
+ });
45139
45798
  var AutomationRunTraceSchema = object({
45140
45799
  runId: string().min(1),
45141
45800
  startedAt: number$1(),
@@ -45385,7 +46044,7 @@ object({
45385
46044
  * is irrelevant; the array is treated as a set by consumers. */
45386
46045
  hiddenChildIds: array(number$1()).readonly()
45387
46046
  });
45388
- DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Light, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Cover, DeviceType.Lock, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Generic, method(object({
46047
+ DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Sensor, DeviceType.Climate, DeviceType.Cover, DeviceType.Lock, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Generic, method(object({
45389
46048
  deviceId: number$1().int().nonnegative(),
45390
46049
  childDeviceId: number$1().int().nonnegative(),
45391
46050
  hidden: boolean()
@@ -45542,7 +46201,9 @@ var AddonHttpRouteSchema = object({
45542
46201
  ]).optional(),
45543
46202
  description: string().optional(),
45544
46203
  /** The hub limits this route per client address before the handler runs. */
45545
- rateLimit: _enum(["auth"]).optional()
46204
+ rateLimit: _enum(["auth"]).optional(),
46205
+ /** Served once per integration instance; the handler reads `instanceId`. */
46206
+ perInstance: boolean().optional()
45546
46207
  });
45547
46208
  /**
45548
46209
  * Cross-process route invocation envelope. The hub captures the
@@ -45559,6 +46220,8 @@ var InvokeRequestSchema = object({
45559
46220
  query: record(string(), string()),
45560
46221
  body: unknown(),
45561
46222
  headers: record(string(), string()),
46223
+ /** The instance the hub resolved for a `perInstance` route. */
46224
+ instanceId: string().optional(),
45562
46225
  user: object({
45563
46226
  id: string(),
45564
46227
  username: string(),
@@ -47683,7 +48346,7 @@ var BATTERY_DEVICE_TYPES = [
47683
48346
  DeviceType.Switch,
47684
48347
  DeviceType.PetFeeder,
47685
48348
  DeviceType.Container,
47686
- DeviceType.Thermostat,
48349
+ DeviceType.Climate,
47687
48350
  DeviceType.EventEmitter
47688
48351
  ];
47689
48352
  /** Generic Customize target for the percentage reading (D936). */
@@ -49446,7 +50109,14 @@ var climateControlCapability = {
49446
50109
  scope: "device",
49447
50110
  deviceNative: true,
49448
50111
  mode: "singleton",
49449
- deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
50112
+ deviceTypes: [DeviceType.Climate],
50113
+ power: {
50114
+ kind: "mode",
50115
+ field: "mode",
50116
+ offValue: "off",
50117
+ set: "setMode",
50118
+ lastField: "lastActiveMode"
50119
+ },
49450
50120
  methods: {
49451
50121
  setMode: method(object({
49452
50122
  deviceId: number$1().int().nonnegative(),
@@ -49831,6 +50501,7 @@ var CompositionProblemSchema = object({
49831
50501
  "nested-field",
49832
50502
  "missing-field",
49833
50503
  "kind-mismatch",
50504
+ "source-kind-mismatch",
49834
50505
  "may-be-null",
49835
50506
  "source-missing",
49836
50507
  "source-unreadable",
@@ -49852,7 +50523,8 @@ var CompositionProblemSchema = object({
49852
50523
  "command-method-unknown",
49853
50524
  "command-source-unknown",
49854
50525
  "command-input-unknown",
49855
- "command-input-missing"
50526
+ "command-input-missing",
50527
+ "power-source-is-actuator"
49856
50528
  ]),
49857
50529
  path: string(),
49858
50530
  message: string()
@@ -50534,11 +51206,13 @@ var IrCodecError = class extends Error {
50534
51206
  this.code = code;
50535
51207
  }
50536
51208
  };
51209
+ /** Maximum number of mark/space entries one pulse train may carry. */
51210
+ var IR_TIMINGS_MAX = 1024;
50537
51211
  /**
50538
51212
  * A pulse train in signed microseconds: mark (+) first, then space (-), strictly
50539
51213
  * alternating, no zeros, 2..1024 entries.
50540
51214
  */
50541
- var IrTimingsSchema = array(number$1().int()).min(2).max(1024).superRefine((timings, ctx) => {
51215
+ var IrTimingsSchema = array(number$1().int()).min(2).max(IR_TIMINGS_MAX).superRefine((timings, ctx) => {
50542
51216
  timings.forEach((value, index) => {
50543
51217
  const expectMark = index % 2 === 0;
50544
51218
  if (value === 0 || value > 0 !== expectMark) ctx.addIssue({
@@ -50585,15 +51259,186 @@ function decodePronto(code) {
50585
51259
  carrierHz: Math.round(1e6 / unitUs)
50586
51260
  };
50587
51261
  }
50588
- /** What a remote command stores: a raw pulse train, or a learned Pronto code. */
50589
- var IrPayloadSchema = discriminatedUnion("kind", [object({
50590
- kind: literal("raw"),
50591
- timings: array(number$1().int()),
50592
- settings: TransmitSettingsSchema.optional()
50593
- }), object({
50594
- kind: literal("pronto"),
50595
- code: string()
50596
- })]);
51262
+ /**
51263
+ * FastLZ level 1: a byte-oriented LZ77 stream. A control byte below 32 starts a
51264
+ * literal run of `ctrl + 1` bytes; otherwise it is a back-reference whose length
51265
+ * is `ctrl >> 5` (extended by one byte when 7) and whose distance is split
51266
+ * between the low 5 bits and the next byte.
51267
+ */
51268
+ var MAX_DISTANCE = 8192;
51269
+ var MAX_LITERAL_RUN = 32;
51270
+ var MIN_MATCH = 3;
51271
+ var MAX_MATCH = 264;
51272
+ /**
51273
+ * Most bytes a decoded IR code may hold: two bytes per timing. A level-1 stream
51274
+ * can expand ~130x, so the decoder checks this bound before every write.
51275
+ */
51276
+ var IR_DECOMPRESSED_MAX = IR_TIMINGS_MAX * 2;
51277
+ function fastlzDecompress(input) {
51278
+ const out = [];
51279
+ let ip = 0;
51280
+ const room = (count) => {
51281
+ if (out.length + count > IR_DECOMPRESSED_MAX) throw new IrCodecError("tuya-malformed", "decompressed code is larger than any IR train");
51282
+ };
51283
+ const next = () => {
51284
+ const byte = input[ip++];
51285
+ if (byte === void 0) throw new IrCodecError("tuya-malformed", "compressed stream is truncated");
51286
+ return byte;
51287
+ };
51288
+ while (ip < input.length) {
51289
+ const ctrl = next();
51290
+ if (ctrl < 32) {
51291
+ room(ctrl + 1);
51292
+ for (let i = 0; i <= ctrl; i++) out.push(next());
51293
+ continue;
51294
+ }
51295
+ let len = (ctrl >> 5) - 1;
51296
+ if (len === 6) len += next();
51297
+ const distance = ((ctrl & 31) << 8) + next() + 1;
51298
+ const start = out.length - distance;
51299
+ if (start < 0) throw new IrCodecError("tuya-malformed", "back-reference points before the stream");
51300
+ room(len + 3);
51301
+ for (let i = 0; i < len + 3; i++) {
51302
+ const byte = out[start + i];
51303
+ if (byte === void 0) throw new IrCodecError("tuya-malformed", "back-reference is invalid");
51304
+ out.push(byte);
51305
+ }
51306
+ }
51307
+ return Uint8Array.from(out);
51308
+ }
51309
+ function fastlzCompress(input) {
51310
+ const out = [];
51311
+ const lastSeen = /* @__PURE__ */ new Map();
51312
+ let literalStart = 0;
51313
+ const flushLiterals = (end) => {
51314
+ for (let from = literalStart; from < end; from += MAX_LITERAL_RUN) {
51315
+ const to = Math.min(from + MAX_LITERAL_RUN, end);
51316
+ out.push(to - from - 1);
51317
+ for (let i = from; i < to; i++) out.push(input[i] ?? 0);
51318
+ }
51319
+ };
51320
+ let pos = 0;
51321
+ while (pos < input.length) {
51322
+ let matchLen = 0;
51323
+ let matchDistance = 0;
51324
+ if (pos + MIN_MATCH <= input.length) {
51325
+ const key = (input[pos] ?? 0) << 16 | (input[pos + 1] ?? 0) << 8 | (input[pos + 2] ?? 0);
51326
+ const candidate = lastSeen.get(key);
51327
+ lastSeen.set(key, pos);
51328
+ if (candidate !== void 0 && pos - candidate <= MAX_DISTANCE) {
51329
+ let len = 0;
51330
+ while (pos + len < input.length && len < MAX_MATCH && input[candidate + len] === input[pos + len]) len++;
51331
+ if (len >= MIN_MATCH) {
51332
+ matchLen = len;
51333
+ matchDistance = pos - candidate - 1;
51334
+ }
51335
+ }
51336
+ }
51337
+ if (matchLen === 0) {
51338
+ pos++;
51339
+ continue;
51340
+ }
51341
+ flushLiterals(pos);
51342
+ const enc = matchLen - 2;
51343
+ if (enc < 7) out.push(enc << 5 | matchDistance >> 8);
51344
+ else out.push(224 | matchDistance >> 8, enc - 7);
51345
+ out.push(matchDistance & 255);
51346
+ pos += matchLen;
51347
+ literalStart = pos;
51348
+ }
51349
+ flushLiterals(input.length);
51350
+ return Uint8Array.from(out);
51351
+ }
51352
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
51353
+ var MAX_DURATION_US = 65535;
51354
+ function toBase64(bytes) {
51355
+ let out = "";
51356
+ for (let i = 0; i < bytes.length; i += 3) {
51357
+ const a = bytes[i] ?? 0;
51358
+ const b = bytes[i + 1] ?? 0;
51359
+ const c = bytes[i + 2] ?? 0;
51360
+ const word = a << 16 | b << 8 | c;
51361
+ out += ALPHABET[word >> 18 & 63];
51362
+ out += ALPHABET[word >> 12 & 63];
51363
+ out += i + 1 < bytes.length ? ALPHABET[word >> 6 & 63] : "=";
51364
+ out += i + 2 < bytes.length ? ALPHABET[word & 63] : "=";
51365
+ }
51366
+ return out;
51367
+ }
51368
+ function fromBase64(text) {
51369
+ const clean = text.trim().replace(/=+$/, "");
51370
+ if (clean.length === 0 || clean.length % 4 === 1) throw new IrCodecError("tuya-malformed", "not a base64 string");
51371
+ const out = [];
51372
+ let bits = 0;
51373
+ let acc = 0;
51374
+ for (const ch of clean) {
51375
+ const value = ALPHABET.indexOf(ch);
51376
+ if (value < 0) throw new IrCodecError("tuya-malformed", "not a base64 string");
51377
+ acc = acc << 6 | value;
51378
+ bits += 6;
51379
+ if (bits >= 8) {
51380
+ bits -= 8;
51381
+ out.push(acc >> bits & 255);
51382
+ }
51383
+ }
51384
+ return Uint8Array.from(out);
51385
+ }
51386
+ /** Signed pulse train to a Tuya IR code: base64(FastLZ(uint16 LE durations)). */
51387
+ function encodeTuya(timings) {
51388
+ const bytes = new Uint8Array(timings.length * 2);
51389
+ timings.forEach((value, index) => {
51390
+ const duration = Math.abs(value);
51391
+ if (duration === 0 || duration > MAX_DURATION_US) throw new IrCodecError("invalid-timings", `timing ${index} does not fit 16 bits`);
51392
+ bytes[index * 2] = duration & 255;
51393
+ bytes[index * 2 + 1] = duration >> 8;
51394
+ });
51395
+ return toBase64(fastlzCompress(bytes));
51396
+ }
51397
+ /** A Tuya IR code to a signed pulse train (mark first). Throws `IrCodecError`. */
51398
+ function decodeTuya(code) {
51399
+ const bytes = fastlzDecompress(fromBase64(code));
51400
+ if (bytes.length === 0 || bytes.length % 2 !== 0) throw new IrCodecError("tuya-malformed", "payload is not a list of 16-bit durations");
51401
+ const timings = [];
51402
+ for (let i = 0; i < bytes.length; i += 2) {
51403
+ const duration = (bytes[i] ?? 0) | (bytes[i + 1] ?? 0) << 8;
51404
+ timings.push(i / 2 % 2 === 0 ? duration : -duration);
51405
+ }
51406
+ const checked = IrTimingsSchema.safeParse(timings);
51407
+ if (!checked.success) throw new IrCodecError("tuya-malformed", checked.error.message);
51408
+ return checked.data;
51409
+ }
51410
+ /** Device-native IR code formats a transmitter can replay verbatim. */
51411
+ var IrNativeFormatSchema = _enum(["tuya"]);
51412
+ /**
51413
+ * Longest native code accepted: base64 of the worst-case compressed stream
51414
+ * (every decoded byte as a literal, plus one control byte per 32), with margin.
51415
+ */
51416
+ var IR_NATIVE_CODE_MAX_CHARS = Math.ceil((IR_DECOMPRESSED_MAX + Math.ceil(IR_DECOMPRESSED_MAX / 32)) / 3) * 4 + 256;
51417
+ var IrNativeCodeSchema = object({
51418
+ format: IrNativeFormatSchema,
51419
+ code: string().min(1).max(IR_NATIVE_CODE_MAX_CHARS)
51420
+ });
51421
+ var IR_NATIVE_CODECS = { tuya: {
51422
+ encode: encodeTuya,
51423
+ decode: decodeTuya
51424
+ } };
51425
+ /** What a remote command stores: a raw pulse train, a learned Pronto code, or a device-native code. */
51426
+ var IrPayloadSchema = discriminatedUnion("kind", [
51427
+ object({
51428
+ kind: literal("raw"),
51429
+ timings: array(number$1().int()),
51430
+ settings: TransmitSettingsSchema.optional()
51431
+ }),
51432
+ object({
51433
+ kind: literal("pronto"),
51434
+ code: string()
51435
+ }),
51436
+ object({
51437
+ kind: literal("native"),
51438
+ format: IrNativeFormatSchema,
51439
+ code: string()
51440
+ })
51441
+ ]);
50597
51442
  /** Resolve a payload to what `ir-transmitter.send` takes. Throws `IrCodecError`. */
50598
51443
  function toTimings(payload) {
50599
51444
  if (payload.kind === "pronto") {
@@ -50603,6 +51448,10 @@ function toTimings(payload) {
50603
51448
  settings: { carrierHz: decoded.carrierHz }
50604
51449
  };
50605
51450
  }
51451
+ if (payload.kind === "native") return {
51452
+ timings: IR_NATIVE_CODECS[payload.format].decode(payload.code),
51453
+ settings: {}
51454
+ };
50606
51455
  const checked = IrTimingsSchema.safeParse(payload.timings);
50607
51456
  if (!checked.success) throw new IrCodecError("invalid-timings", checked.error.message);
50608
51457
  return {
@@ -50890,6 +51739,13 @@ var mediaPlayerCapability = {
50890
51739
  deviceNative: true,
50891
51740
  mode: "singleton",
50892
51741
  deviceTypes: [DeviceType.MediaPlayer],
51742
+ power: {
51743
+ kind: "methods",
51744
+ field: "state",
51745
+ offValues: ["off", "standby"],
51746
+ on: "turnOn",
51747
+ off: "turnOff"
51748
+ },
50893
51749
  methods: {
50894
51750
  turnOn: optionalMethod(deviceOnly, _void(), {
50895
51751
  kind: "mutation",
@@ -51020,7 +51876,7 @@ var mediaPlayerCapability = {
51020
51876
  isMuted: { kind: "boolean" }
51021
51877
  } }
51022
51878
  };
51023
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.MediaPlayer;
51879
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Climate, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Gateway, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.PetFeeder, DeviceType.MediaPlayer;
51024
51880
  var NUMBER$1 = {
51025
51881
  kind: "fixed",
51026
51882
  result: ["number"]
@@ -51103,7 +51959,7 @@ function numeric$1(input) {
51103
51959
  }, ...alsoFrom === void 0 ? [] : [alsoFrom.accept]]
51104
51960
  };
51105
51961
  }
51106
- var CLIMATE_DEVICES = [DeviceType.Thermostat, DeviceType.Climate];
51962
+ var CLIMATE_DEVICES = [DeviceType.Climate];
51107
51963
  function binary(input) {
51108
51964
  return {
51109
51965
  ...input,
@@ -51425,8 +52281,8 @@ var GROUP_TEMPLATES = [
51425
52281
  actuator({
51426
52282
  id: "climate",
51427
52283
  summary: "Running when at least one thermostat or air conditioner is not off, or only when all of them are. Target temperature and fan are offered only when every device has them. A device that reports nothing is left out, never read as off.",
51428
- deviceType: DeviceType.Thermostat,
51429
- memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
52284
+ deviceType: DeviceType.Climate,
52285
+ memberDeviceTypes: [DeviceType.Climate],
51430
52286
  outputCap: "group-active",
51431
52287
  outputField: "active",
51432
52288
  member: {
@@ -51469,6 +52325,118 @@ function actuator(input) {
51469
52325
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
51470
52326
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
51471
52327
  DeviceType.Camera;
52328
+ var PowerSourceRefusalReasonSchema = _enum([
52329
+ "no-power-source",
52330
+ "switch-missing",
52331
+ "switch-offline",
52332
+ "switch-unknown",
52333
+ "restart-in-progress",
52334
+ "switch-call-failed",
52335
+ "switch-is-actuator"
52336
+ ]);
52337
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
52338
+ accepted: literal(false),
52339
+ reason: PowerSourceRefusalReasonSchema,
52340
+ /** One line for the operator, naming the switch. */
52341
+ message: string()
52342
+ })]);
52343
+ var PowerSourceInfoSchema = object({
52344
+ /** False when the device has no power source linked; the rest is then empty. */
52345
+ linked: boolean(),
52346
+ switchDeviceId: number$1().nullable(),
52347
+ switchName: string().nullable(),
52348
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
52349
+ switchOn: boolean().nullable(),
52350
+ switchOnline: boolean().nullable(),
52351
+ restartInProgress: boolean(),
52352
+ offSeconds: number$1().int()
52353
+ });
52354
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
52355
+ var PowerSourceStateSchema = object({
52356
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
52357
+ switchOn: boolean().nullable(),
52358
+ offSeconds: number$1().int(),
52359
+ lastFetchedAt: number$1()
52360
+ });
52361
+ var POWER_SOURCE_DEVICE_TYPES = [
52362
+ DeviceType.Gateway,
52363
+ DeviceType.Camera,
52364
+ DeviceType.Sensor,
52365
+ DeviceType.MediaPlayer,
52366
+ DeviceType.Vacuum,
52367
+ DeviceType.Generic,
52368
+ DeviceType.Washer,
52369
+ DeviceType.Dryer,
52370
+ DeviceType.Dishwasher,
52371
+ DeviceType.PetFountain,
52372
+ DeviceType.Fan,
52373
+ DeviceType.Humidifier,
52374
+ DeviceType.Climate,
52375
+ DeviceType.WaterHeater
52376
+ ];
52377
+ var powerSourceCustomize = {
52378
+ version: 1,
52379
+ targets: [{
52380
+ id: "switch",
52381
+ cardinality: "scalar",
52382
+ vocabulary: {
52383
+ cap: "power-source",
52384
+ stateField: "switchOn"
52385
+ },
52386
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
52387
+ apply: "add",
52388
+ path: "switchOn",
52389
+ sources: [{
52390
+ kind: "switch",
52391
+ transforms: [{ kind: "identity" }]
52392
+ }],
52393
+ companions: [{
52394
+ kind: "constant",
52395
+ path: "offSeconds",
52396
+ value: 10,
52397
+ when: "add",
52398
+ formerValues: [0]
52399
+ }, {
52400
+ kind: "clock",
52401
+ path: "lastFetchedAt",
52402
+ when: "always"
52403
+ }],
52404
+ sourceReport: { status: "assignment-status" }
52405
+ }]
52406
+ };
52407
+ var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
52408
+ var powerSourceCapability = {
52409
+ name: "power-source",
52410
+ scope: "device",
52411
+ mode: "singleton",
52412
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
52413
+ methods: {
52414
+ getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "protected" }),
52415
+ powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52416
+ kind: "mutation",
52417
+ auth: "protected"
52418
+ }),
52419
+ powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52420
+ kind: "mutation",
52421
+ auth: "protected"
52422
+ }),
52423
+ forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
52424
+ kind: "mutation",
52425
+ auth: "protected"
52426
+ })
52427
+ },
52428
+ runtimeState: PowerSourceStateSchema,
52429
+ customize: powerSourceCustomize,
52430
+ /**
52431
+ * Runtime-state durability: **session** — mirrored from the linked switch on
52432
+ * its next report; a restored value would outlive the reading it names.
52433
+ *
52434
+ * See `RuntimeStateDurability`. Enforced by
52435
+ * `scripts/check-runtime-state-durability.ts`.
52436
+ */
52437
+ durability: "session",
52438
+ history: "none"
52439
+ };
51472
52440
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
51473
52441
  /**
51474
52442
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -52513,27 +53481,35 @@ var deviceAdminLinkCapability = {
52513
53481
  getAdminLink: method(object({ deviceId: number$1().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
52514
53482
  };
52515
53483
  /**
52516
- * device-discovery — device-scoped capability for parents that host /
52517
- * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
52518
- * integrations). The parent advertises a list of discoverable children,
52519
- * and the operator promotes a subset to real devices via `adoptDevice`.
52520
- * Child lifecycle (kernel device create/remove, parent reference) is
52521
- * owned by the framework so a single shared `<DiscoveryPanel />` can
52522
- * drive any integration.
52523
- *
52524
- * Boundaries:
52525
- * - The cap exposes ENUMERATION + ADOPT/RELEASE only. The protocol
52526
- * used to discover children (CGI poll, mDNS, push from hub, ...)
52527
- * is the integration's business; results land in `runtimeState`
52528
- * so the panel can render without a fresh round-trip.
52529
- * - `childNativeId` is integration-defined and must be stable across
52530
- * reboots so re-discovery doesn't duplicate adopted children.
52531
- * - `adoptDevice` returns the new `deviceId` so the UI can navigate
52532
- * directly to the freshly-created child's settings page.
52533
- */
52534
- /**
52535
- * Status enum for a discovered device. Mirrors the Reolink simpleEvent
52536
- * lifecycle (`online` / `sleeping` / `offline`) — generic enough for
53484
+ * `device-adoption` — generic discovery + adoption surface,
53485
+ * system-scoped collection (one provider per integration addon), routed
53486
+ * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
53487
+ * pin model as `connection-test`. The admin UI keeps using
53488
+ * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
53489
+ * successor to `ha-discovery`:
53490
+ * keyed by `integrationId` (the addon resolves integrationId → broker
53491
+ * internally) rather than a connection-specific `brokerId`, and it
53492
+ * OWNS the shared `DiscoveredChildDevice` candidate type
53493
+ * (declared below) so a single shared adoption modal renders any integration's
53494
+ * candidate tree.
53495
+ *
53496
+ * UI flow (Integrations page → integration detail):
53497
+ * 1. `listCandidates({integrationId, page, pageSize, filter})` →
53498
+ * paginated candidate list, each row collapsed.
53499
+ * 2. User expands a row → `getCandidate({integrationId,
53500
+ * childNativeId})` returns the single candidate (with its nested
53501
+ * `children`) for the accordion render.
53502
+ * 3. User clicks "Adopt" on one or more candidates →
53503
+ * `adopt({integrationId, childNativeIds, perCandidate?})`
53504
+ * materialises one parent CamStack device per candidate plus its
53505
+ * accessory children. `perCandidate[childNativeId]` carries an
53506
+ * optional display-name override + a `hiddenChildIds` pre-hide set
53507
+ * (the parent's `accessories.hiddenChildIds` gates UI visibility).
53508
+ * 4. User clicks "Release" → `release({integrationId, camDeviceId})`
53509
+ * removes the parent + every child from the kernel registry.
53510
+ */
53511
+ /**
53512
+ * Status enum for a discovered device. Generic enough for
52537
53513
  * battery cams, awake cams, and unreachable cams alike.
52538
53514
  */
52539
53515
  var DiscoveredChildStatusSchema = _enum([
@@ -52550,12 +53526,12 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
52550
53526
  /** DeviceType the child should be created with on adopt. */
52551
53527
  type: _enum(DeviceType),
52552
53528
  status: DiscoveredChildStatusSchema,
52553
- /** Free-form integration-specific metadata surfaced in the panel. */
53529
+ /** Free-form integration-specific metadata surfaced in the adoption modal. */
52554
53530
  metadata: object({
52555
53531
  model: string().optional(),
52556
53532
  serialNumber: string().optional(),
52557
53533
  uid: string().optional(),
52558
- /** Reolink: 0-based channel index inside the parent NVR/Hub. */
53534
+ /** Reolink: 0-based channel index inside the parent NVR. */
52559
53535
  rtspChannel: number$1().int().nonnegative().optional(),
52560
53536
  isBattery: boolean().optional(),
52561
53537
  isDoorbell: boolean().optional(),
@@ -52571,137 +53547,24 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
52571
53547
  }).default({}),
52572
53548
  /**
52573
53549
  * `true` when the framework already created a child device for this
52574
- * `childNativeId` under the current parent. The panel uses it to
53550
+ * `childNativeId` under the current parent. The adoption modal uses it to
52575
53551
  * gate the Add/Remove button and surface the existing `deviceId`.
52576
53552
  */
52577
53553
  alreadyAdopted: boolean(),
52578
53554
  /** When `alreadyAdopted=true`, the framework-assigned child device id. */
52579
53555
  adoptedDeviceId: number$1().int().nonnegative().nullable(),
52580
- /** Capability names the candidate advertises — integrations populate, Hubs omit. */
53556
+ /** Capability names the candidate advertises — integrations populate, flat enumerations omit. */
52581
53557
  capabilities: array(string()).readonly().optional(),
52582
- /** Feature flags the candidate advertises — integrations populate, Hubs omit. */
53558
+ /** Feature flags the candidate advertises — integrations populate, flat enumerations omit. */
52583
53559
  features: array(string()).readonly().optional(),
52584
53560
  /** Upstream-system identity + rendering hints for the candidate. */
52585
53561
  sourceInfo: SourceInfoSchema.optional(),
52586
- /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
53562
+ /** Nested entity-children for accordion preview — integrations populate, flat enumerations omit. */
52587
53563
  children: array(DiscoveredChildDeviceSchema).readonly().optional(),
52588
53564
  /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
52589
53565
  parentStableId: string().optional()
52590
53566
  }));
52591
53567
  /**
52592
- * Status block — populated by the integration after each
52593
- * `refreshDiscovery` (or push-driven update). The runtime-state slice
52594
- * is what the panel reads via the per-device proxy; `getStatus`
52595
- * returns the same payload via the codegen'd auto-injection.
52596
- */
52597
- var DeviceDiscoveryStatusSchema = object({
52598
- discovered: array(DiscoveredChildDeviceSchema),
52599
- /** Wall-clock ms of the last successful enumeration. */
52600
- lastDiscoveryAt: number$1().int().nonnegative().nullable(),
52601
- /** Last error surfaced from the source (rendered as a banner). */
52602
- lastError: string().nullable()
52603
- });
52604
- var deviceDiscoveryCapability = {
52605
- name: "device-discovery",
52606
- scope: "device",
52607
- deviceNative: true,
52608
- mode: "singleton",
52609
- deviceTypes: [DeviceType.Hub],
52610
- status: {
52611
- schema: DeviceDiscoveryStatusSchema,
52612
- kind: "poll"
52613
- },
52614
- runtimeState: DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number$1().int().nonnegative() }),
52615
- /**
52616
- * Runtime-state durability: **session** — 5.7 KB of scan output on the largest device, fully re-derivable by re-scanning.
52617
- *
52618
- * See `RuntimeStateDurability`. Enforced by
52619
- * `scripts/check-runtime-state-durability.ts`.
52620
- */
52621
- durability: "session",
52622
- /**
52623
- * History (D850): none — inventory.
52624
- */
52625
- history: "none",
52626
- methods: {
52627
- /**
52628
- * Snapshot of the current `discovered` list. Returns the
52629
- * runtime-state cache — call `refreshDiscovery` first if a
52630
- * fresh round-trip to the source is required.
52631
- */
52632
- listDiscovered: method(object({ deviceId: number$1().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly()),
52633
- /**
52634
- * Force the integration to re-enumerate and update the
52635
- * runtime-state slice. Returns the freshly-enumerated list (also
52636
- * available via `listDiscovered` post-call).
52637
- */
52638
- refreshDiscovery: method(object({ deviceId: number$1().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly(), {
52639
- kind: "mutation",
52640
- auth: "admin"
52641
- }),
52642
- /**
52643
- * Promote a discovered entry to a real child device. The framework
52644
- * creates the child via `kernel.devices.create()` with
52645
- * `parentDeviceId = parent.id` and seeds the child's config from
52646
- * `childInitialConfig` (driver-defined; usually carries channel +
52647
- * uid + parent reference). Returns the kernel-assigned numeric id.
52648
- */
52649
- adoptDevice: method(object({
52650
- deviceId: number$1().int().nonnegative(),
52651
- childNativeId: string(),
52652
- /** Optional override for the child's display name. */
52653
- name: string().optional()
52654
- }), object({
52655
- deviceId: number$1().int().nonnegative(),
52656
- stableId: string()
52657
- }), {
52658
- kind: "mutation",
52659
- auth: "admin"
52660
- }),
52661
- /**
52662
- * Inverse of `adoptDevice`: removes the child device from the
52663
- * kernel registry. The discovered entry remains in the
52664
- * enumeration (status updates resume) so the operator can re-adopt
52665
- * it later without a fresh refresh.
52666
- */
52667
- releaseDevice: method(object({
52668
- deviceId: number$1().int().nonnegative(),
52669
- childDeviceId: number$1().int().nonnegative()
52670
- }), _void(), {
52671
- kind: "mutation",
52672
- auth: "admin"
52673
- })
52674
- }
52675
- };
52676
- /**
52677
- * `device-adoption` — generic discovery + adoption surface,
52678
- * system-scoped collection (one provider per integration addon), routed
52679
- * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
52680
- * pin model as `connection-test`. The admin UI keeps using
52681
- * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
52682
- * successor to `ha-discovery`:
52683
- * keyed by `integrationId` (the addon resolves integrationId → broker
52684
- * internally) rather than a connection-specific `brokerId`, and it
52685
- * REUSES the shared `DiscoveredChildDevice` candidate type from
52686
- * `device-discovery.cap.ts` so a single shared adoption panel renders
52687
- * any integration's candidate tree.
52688
- *
52689
- * UI flow (Integrations page → integration detail):
52690
- * 1. `listCandidates({integrationId, page, pageSize, filter})` →
52691
- * paginated candidate list, each row collapsed.
52692
- * 2. User expands a row → `getCandidate({integrationId,
52693
- * childNativeId})` returns the single candidate (with its nested
52694
- * `children`) for the accordion render.
52695
- * 3. User clicks "Adopt" on one or more candidates →
52696
- * `adopt({integrationId, childNativeIds, perCandidate?})`
52697
- * materialises one parent CamStack device per candidate plus its
52698
- * accessory children. `perCandidate[childNativeId]` carries an
52699
- * optional display-name override + a `hiddenChildIds` pre-hide set
52700
- * (the parent's `accessories.hiddenChildIds` gates UI visibility).
52701
- * 4. User clicks "Release" → `release({integrationId, camDeviceId})`
52702
- * removes the parent + every child from the kernel registry.
52703
- */
52704
- /**
52705
53568
  * Provider-declared discovery *granularity* filter (the keystone of the
52706
53569
  * single-entity-import design). A provider advertises which discovery
52707
53570
  * granularities it supports via `listCandidateFilters`; the operator picks
@@ -54129,8 +54992,14 @@ method(object({
54129
54992
  "string",
54130
54993
  "number",
54131
54994
  "boolean",
54132
- "enum"
54995
+ "enum",
54996
+ "union"
54133
54997
  ]),
54998
+ unionMembers: array(_enum([
54999
+ "string",
55000
+ "number",
55001
+ "boolean"
55002
+ ])).optional(),
54134
55003
  enumValues: array(string()).optional(),
54135
55004
  item: boolean().optional(),
54136
55005
  quantity: ReadingQuantitySchema.optional()
@@ -56222,53 +57091,6 @@ var enumSensorCapability = {
56222
57091
  * whether persisting is worth a SQLite commit. */
56223
57092
  volatileStateFields: ["lastFetchedAt", "timestamp"]
56224
57093
  };
56225
- /**
56226
- * Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
56227
- * device. Carries the device's EXACT declared event vocabulary verbatim
56228
- * (NO normalization). Two shapes:
56229
- * - structured source (HA `event.*` entity) → `eventTypes` is the fixed
56230
- * declared vocabulary (e.g. ['press_short','press_long',...]).
56231
- * - generic/legacy source (HA bus events, e.g. zha_event) → `eventTypes`
56232
- * is [] (open) and `lastEvent.data` carries the complex payload.
56233
- * `seq` is monotonic so two identical events still produce distinct slice writes.
56234
- */
56235
- var EventFireSchema = object({
56236
- deviceId: number$1(),
56237
- eventType: string(),
56238
- data: record(string(), unknown()).nullable(),
56239
- timestamp: number$1().meta(readingMeta("none")),
56240
- seq: number$1().meta(readingMeta("count"))
56241
- });
56242
- var EventEmitterStatusSchema = object({
56243
- eventTypes: array(string()),
56244
- lastEvent: EventFireSchema.nullable(),
56245
- eventCountSinceStart: number$1().meta(readingMeta("count"))
56246
- });
56247
- var eventEmitterCapability = {
56248
- name: "event-emitter",
56249
- scope: "device",
56250
- deviceNative: true,
56251
- mode: "singleton",
56252
- deviceTypes: [DeviceType.EventEmitter],
56253
- methods: {},
56254
- events: { onEvent: { data: EventFireSchema } },
56255
- status: {
56256
- schema: EventEmitterStatusSchema,
56257
- kind: "push"
56258
- },
56259
- runtimeState: EventEmitterStatusSchema,
56260
- /**
56261
- * Runtime-state durability: **session** — `eventCountSinceStart` names its own scope.
56262
- *
56263
- * See `RuntimeStateDurability`. Enforced by
56264
- * `scripts/check-runtime-state-durability.ts`.
56265
- */
56266
- durability: "session",
56267
- /**
56268
- * History (D850): none — `eventCountSinceStart` resets with the addon; events live in ledgers.
56269
- */
56270
- history: "none"
56271
- };
56272
57094
  var IdentitySchema = object({
56273
57095
  id: string(),
56274
57096
  name: string(),
@@ -56758,8 +57580,85 @@ method(_void(), array(FailureContributionSchema).readonly());
56758
57580
  * - `FanPreset` — preset selector
56759
57581
  * - `FanDirection` — direction toggle (ceiling fans)
56760
57582
  * - `FanOscillating` — oscillation toggle
57583
+ *
57584
+ * Per-axis oscillation (`oscillationAxes`, `setOscillationAxis`, D1034): an air
57585
+ * circulator sweeps horizontally and vertically, each axis with its own
57586
+ * switch, fixed angle, sweep bounds and limits. A provider may report them
57587
+ * natively; otherwise the OPERATOR links sibling devices to each field in
57588
+ * Customize (a `replace` over the fields listed in `overridable` — nothing is
57589
+ * linked or inferred by itself), and a write to a linked field is forwarded to
57590
+ * its source. `oscillating` stays the single summary flag: when axes exist it
57591
+ * means "any axis oscillates" (`anyAxisOscillating`).
56761
57592
  */
56762
57593
  var FanDirectionSchema = _enum(["forward", "reverse"]);
57594
+ var FAN_OSCILLATION_AXES = ["horizontal", "vertical"];
57595
+ var FanOscillationAxisNameSchema = _enum(FAN_OSCILLATION_AXES);
57596
+ var FanOscillationAxisFieldSchema = _enum([
57597
+ "oscillating",
57598
+ "angle",
57599
+ "sweepFrom",
57600
+ "sweepTo"
57601
+ ]);
57602
+ /** The limits of one axis, read from the source that serves its angle. */
57603
+ var FAN_OSCILLATION_AXIS_LIMITS = [
57604
+ "min",
57605
+ "max",
57606
+ "step"
57607
+ ];
57608
+ /**
57609
+ * One oscillation axis. Every reading the device has not reported is `null`,
57610
+ * never `0` (D393): `0°` is a real position. Each leaf defaults to `null`, so
57611
+ * a slice that names only the leaves it knows (a Customize link of one field)
57612
+ * is still a whole axis.
57613
+ */
57614
+ var FanOscillationAxisSchema = object({
57615
+ /** Whether the axis sweeps. */
57616
+ oscillating: boolean().nullable().default(null),
57617
+ /** The fixed position (degrees) the axis holds when it does not sweep. */
57618
+ angle: number$1().nullable().default(null),
57619
+ /** The sweep bounds (degrees): left → right, bottom → top. */
57620
+ sweepFrom: number$1().nullable().default(null),
57621
+ sweepTo: number$1().nullable().default(null),
57622
+ /** The angles the axis accepts. Null when the device states none. */
57623
+ min: number$1().nullable().default(null),
57624
+ max: number$1().nullable().default(null),
57625
+ step: number$1().nullable().default(null)
57626
+ });
57627
+ /** Both axes; an axis the device does not have is `null`. */
57628
+ var FanOscillationAxesSchema = object({
57629
+ horizontal: FanOscillationAxisSchema.nullable().default(null),
57630
+ vertical: FanOscillationAxisSchema.nullable().default(null)
57631
+ });
57632
+ var FanOscillationAxisRefusalSchema = _enum([
57633
+ "nothing-to-set",
57634
+ "axis-not-linked",
57635
+ "out-of-range",
57636
+ "source-gone",
57637
+ "source-unknown",
57638
+ "source-call-failed"
57639
+ ]);
57640
+ var FanOscillationAxisResultSchema = discriminatedUnion("status", [object({ status: literal("applied") }), object({
57641
+ status: literal("refused"),
57642
+ reason: FanOscillationAxisRefusalSchema,
57643
+ /** The field at fault, when one is. */
57644
+ field: FanOscillationAxisFieldSchema.optional(),
57645
+ /** One line for the operator, naming the axis, the field and the source. */
57646
+ detail: string(),
57647
+ /** Fields that DID land before the refusal (writes run in order, stop at the first failure). */
57648
+ applied: array(FanOscillationAxisFieldSchema)
57649
+ })]);
57650
+ var SetOscillationAxisInputSchema = object({
57651
+ deviceId: number$1().int().nonnegative(),
57652
+ axis: FanOscillationAxisNameSchema,
57653
+ oscillating: boolean().optional(),
57654
+ angle: number$1().optional(),
57655
+ sweepFrom: number$1().optional(),
57656
+ sweepTo: number$1().optional()
57657
+ });
57658
+ /** The state path of one axis field: `oscillationAxes.<axis>.<field>`. */
57659
+ function fanAxisPath(axis, field) {
57660
+ return `oscillationAxes.${axis}.${field}`;
57661
+ }
56763
57662
  var FanControlStatusSchema = object({
56764
57663
  /** Active speed as 0..100 inclusive. Null when the device has no
56765
57664
  * speed surface (single-speed fan). */
@@ -56778,9 +57677,102 @@ var FanControlStatusSchema = object({
56778
57677
  /** Oscillation toggle. Null when the device has no oscillation
56779
57678
  * surface. */
56780
57679
  oscillating: boolean().nullable(),
57680
+ /**
57681
+ * Per-axis oscillation (air circulators). Absent when the device has no
57682
+ * per-axis surface and nothing is linked in Customize.
57683
+ */
57684
+ oscillationAxes: FanOscillationAxesSchema.optional(),
56781
57685
  /** Ms epoch when the slice was last updated. */
56782
57686
  lastChangedAt: number$1()
56783
57687
  });
57688
+ var FAN_CUSTOMIZE_DEVICE_TYPES = [DeviceType.Fan];
57689
+ /** The Customize section the axis targets are listed under. */
57690
+ var AXES_SECTION = "oscillation-axes";
57691
+ function axisOscillatingTarget(axis) {
57692
+ return {
57693
+ id: `${axis}-oscillating`,
57694
+ cardinality: "scalar",
57695
+ vocabulary: {
57696
+ cap: "fan-control",
57697
+ stateField: "oscillating"
57698
+ },
57699
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57700
+ apply: "replace",
57701
+ section: AXES_SECTION,
57702
+ path: fanAxisPath(axis, "oscillating"),
57703
+ sources: [{
57704
+ kind: "switch",
57705
+ transforms: [{ kind: "identity" }]
57706
+ }],
57707
+ sourceReport: { status: "assignment-status" }
57708
+ };
57709
+ }
57710
+ /**
57711
+ * A numeric axis field. The `angle` link also carries the axis limits, read
57712
+ * from the SAME linked control's own range (`min`/`max`/`step`), so the limits
57713
+ * are the source's, never guessed.
57714
+ */
57715
+ function axisNumericTarget(axis, field) {
57716
+ const base = {
57717
+ id: `${axis}-${field}`,
57718
+ cardinality: "scalar",
57719
+ vocabulary: { cap: "fan-control" },
57720
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57721
+ apply: "replace",
57722
+ section: AXES_SECTION,
57723
+ path: fanAxisPath(axis, field),
57724
+ sources: [{
57725
+ kind: "numeric",
57726
+ transforms: [{ kind: "identity" }]
57727
+ }],
57728
+ sourceReport: { status: "assignment-status" }
57729
+ };
57730
+ if (field !== "angle") return base;
57731
+ return {
57732
+ ...base,
57733
+ companions: FAN_OSCILLATION_AXIS_LIMITS.map((limit) => ({
57734
+ kind: "source-field",
57735
+ path: fanAxisPath(axis, limit),
57736
+ field: limit
57737
+ }))
57738
+ };
57739
+ }
57740
+ var AXIS_TARGETS = FAN_OSCILLATION_AXES.flatMap((axis) => [
57741
+ axisOscillatingTarget(axis),
57742
+ axisNumericTarget(axis, "angle"),
57743
+ axisNumericTarget(axis, "sweepFrom"),
57744
+ axisNumericTarget(axis, "sweepTo")
57745
+ ]);
57746
+ /** A select (an enum control) the operator links to the fan's `preset`. */
57747
+ var PRESET_TARGET = {
57748
+ id: "preset",
57749
+ cardinality: "scalar",
57750
+ vocabulary: { cap: "fan-control" },
57751
+ deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
57752
+ apply: "replace",
57753
+ path: "preset",
57754
+ sources: [{
57755
+ kind: "field",
57756
+ valueKinds: ["string"],
57757
+ transforms: [{ kind: "identity" }]
57758
+ }],
57759
+ sourceReport: { status: "assignment-status" }
57760
+ };
57761
+ var FAN_CONTROL_TARGETS = [...AXIS_TARGETS, PRESET_TARGET];
57762
+ /**
57763
+ * Customize (D1034): the fan already serves `fan-control` natively, so every
57764
+ * target is a `replace` over a field listed in `overridable` — the axis leaves
57765
+ * and `preset`, nothing else. Nothing is pre-linked: the operator picks each
57766
+ * source (D356).
57767
+ */
57768
+ var fanControlCustomize = {
57769
+ version: 1,
57770
+ overridable: FAN_CONTROL_TARGETS.map((target) => ({
57771
+ path: target.path,
57772
+ by: target.id
57773
+ })),
57774
+ targets: FAN_CONTROL_TARGETS
57775
+ };
56784
57776
  var fanControlCapability = {
56785
57777
  name: "fan-control",
56786
57778
  scope: "device",
@@ -56815,6 +57807,18 @@ var fanControlCapability = {
56815
57807
  }), _void(), {
56816
57808
  kind: "mutation",
56817
57809
  auth: "protected"
57810
+ }),
57811
+ /**
57812
+ * Sets one oscillation axis: its switch, fixed angle and/or sweep bounds.
57813
+ * Fields left out are untouched. A field linked in Customize is forwarded
57814
+ * to its linked source (D1034); otherwise the native provider serves it;
57815
+ * neither → `axis-not-linked`. Ranges are checked against the axis limits
57816
+ * before anything is written; writes run in order and stop at the first
57817
+ * failure, which the result names.
57818
+ */
57819
+ setOscillationAxis: optionalMethod(SetOscillationAxisInputSchema, FanOscillationAxisResultSchema, {
57820
+ kind: "mutation",
57821
+ auth: "protected"
56818
57822
  })
56819
57823
  },
56820
57824
  status: {
@@ -56826,6 +57830,7 @@ var fanControlCapability = {
56826
57830
  * sliders read `speed` for live updates.
56827
57831
  */
56828
57832
  runtimeState: FanControlStatusSchema,
57833
+ customize: fanControlCustomize,
56829
57834
  /**
56830
57835
  * Runtime-state durability: **session** — as `brightness`.
56831
57836
  *
@@ -56845,16 +57850,33 @@ var fanControlCapability = {
56845
57850
  oscillating: { kind: "boolean" }
56846
57851
  } }
56847
57852
  };
57853
+ /** One axis as a preset wants it; an absent field is left as it is. */
57854
+ var FanPresetAxisSchema = object({
57855
+ oscillating: boolean().optional(),
57856
+ angle: number$1().optional(),
57857
+ sweepFrom: number$1().optional(),
57858
+ sweepTo: number$1().optional()
57859
+ });
57860
+ /** The fan-control state a preset sets; an absent field is left as it is. */
57861
+ var FanPresetStateSchema = object({
57862
+ speed: number$1().min(0).max(100).optional(),
57863
+ /** The fan's own preset mode (or the linked select's option). */
57864
+ preset: string().min(1).optional(),
57865
+ direction: FanDirectionSchema.optional(),
57866
+ oscillationAxes: object({
57867
+ horizontal: FanPresetAxisSchema.optional(),
57868
+ vertical: FanPresetAxisSchema.optional()
57869
+ }).optional()
57870
+ });
56848
57871
  var FanPresetSchema = object({
56849
57872
  /** Stable key; never reused after a delete within one device's list. */
56850
57873
  id: string().min(1).max(64),
56851
57874
  /** What the operator and a voice assistant call it. */
56852
57875
  name: string().trim().min(1).max(40),
56853
- steps: array(ActionStepSchema).min(1).max(20)
57876
+ state: FanPresetStateSchema
56854
57877
  });
56855
57878
  var FanPresetsRefusalSchema = _enum([
56856
- "not-sibling",
56857
- "bad-target",
57879
+ "empty-state",
56858
57880
  "duplicate-name",
56859
57881
  "duplicate-id",
56860
57882
  "too-many",
@@ -56866,72 +57888,55 @@ var FanPresetsSetResultSchema = discriminatedUnion("status", [object({
56866
57888
  }), object({
56867
57889
  status: literal("refused"),
56868
57890
  reason: FanPresetsRefusalSchema,
56869
- /** Names the preset and step at fault. */
57891
+ /** Names the preset at fault. */
56870
57892
  detail: string()
56871
57893
  })]);
56872
- var FanPresetStepResultSchema = object({
56873
- index: number$1().int().nonnegative(),
57894
+ var FanPresetFieldResultSchema = object({
57895
+ /** The state path: `speed`, `preset`, `direction`, `oscillationAxes.horizontal.angle`. */
57896
+ field: string(),
56874
57897
  outcome: _enum([
56875
57898
  "applied",
56876
57899
  "failed",
56877
57900
  "not-run",
56878
57901
  "refused"
56879
57902
  ]),
56880
- deviceId: number$1().int().nonnegative().optional(),
56881
- cap: string().optional(),
56882
- method: string().optional(),
56883
57903
  /** The cause, for every outcome but `applied`. */
56884
57904
  message: string().optional()
56885
57905
  });
56886
57906
  var FanPresetApplyResultSchema = object({
56887
- /** `applied`: every step ran. `partial`: some ran, one failed. `refused`: nothing ran. */
57907
+ /** `applied`: every field landed. `partial`: some did, one did not. `refused`: nothing ran. */
56888
57908
  status: _enum([
56889
57909
  "applied",
56890
57910
  "partial",
56891
57911
  "refused"
56892
57912
  ]),
56893
- steps: array(FanPresetStepResultSchema)
57913
+ fields: array(FanPresetFieldResultSchema)
56894
57914
  });
56895
57915
  var FanCaptureFieldSchema = _enum([
56896
57916
  "speed",
56897
- "oscillating",
56898
57917
  "direction",
56899
- "preset"
57918
+ "preset",
57919
+ "oscillationAxes"
56900
57920
  ]);
56901
- var FanCaptureOmissionSchema = object({
56902
- /** A fan field (`speed`) or `step <n>` for a sibling step. */
56903
- what: string(),
56904
- /** Why no value was taken: unknown reading (never stored as 0, D393), or a step that cannot be read. */
56905
- reason: string()
56906
- });
56907
- var FanCaptureItemSchema = object({
56908
- group: _enum([
56909
- "fan",
56910
- "oscillation",
56911
- "angles"
56912
- ]),
56913
- /** What the step sets, in the operator's words (a device name, a fan field). */
56914
- label: string()
56915
- });
56916
57921
  var FanPresetCaptureResultSchema = object({
56917
- /** The steps filled from the device's current values; NOT saved. */
56918
- steps: array(ActionStepSchema),
56919
- /** One entry per step, same order: its review group and label. */
56920
- items: array(FanCaptureItemSchema),
56921
- omitted: array(FanCaptureOmissionSchema)
57922
+ /** The fan's CURRENT fan-control state, as a preset; NOT saved. */
57923
+ state: FanPresetStateSchema,
57924
+ omitted: array(object({
57925
+ /** A state path (`speed`, `oscillationAxes.vertical.angle`). */
57926
+ what: string(),
57927
+ /** Why no value was taken: an unknown reading is never stored as 0 (D393). */
57928
+ reason: string()
57929
+ }))
56922
57930
  });
56923
- var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
57931
+ var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
56924
57932
  DeviceType.Fan;
56925
- method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
57933
+ method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
56926
57934
  kind: "mutation",
56927
57935
  auth: "admin"
56928
- }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
57936
+ }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
56929
57937
  kind: "mutation",
56930
57938
  auth: "protected"
56931
- }), method(deviceInput$2.extend({
56932
- fields: array(FanCaptureFieldSchema).optional(),
56933
- steps: array(ActionStepSchema).max(20).default([])
56934
- }), FanPresetCaptureResultSchema, { auth: "protected" });
57939
+ }), method(deviceInput$1.extend({ fields: array(FanCaptureFieldSchema).optional() }), FanPresetCaptureResultSchema, { auth: "protected" });
56935
57940
  /**
56936
57941
  * Per-device feature/identity probe slice. Holds the runtime-resolved
56937
57942
  * truth about what a device CAN do — which the kernel uses to:
@@ -57143,8 +58148,7 @@ var groupActiveCapability = {
57143
58148
  DeviceType.Valve,
57144
58149
  DeviceType.Lock,
57145
58150
  DeviceType.MediaPlayer,
57146
- DeviceType.Climate,
57147
- DeviceType.Thermostat
58151
+ DeviceType.Climate
57148
58152
  ],
57149
58153
  methods: {},
57150
58154
  status: {
@@ -57186,7 +58190,6 @@ var groupCommandsCapability = {
57186
58190
  DeviceType.Valve,
57187
58191
  DeviceType.Lock,
57188
58192
  DeviceType.MediaPlayer,
57189
- DeviceType.Thermostat,
57190
58193
  DeviceType.Climate
57191
58194
  ],
57192
58195
  methods: {},
@@ -57242,7 +58245,6 @@ var groupStatusCapability = {
57242
58245
  DeviceType.Valve,
57243
58246
  DeviceType.Lock,
57244
58247
  DeviceType.MediaPlayer,
57245
- DeviceType.Thermostat,
57246
58248
  DeviceType.Climate
57247
58249
  ],
57248
58250
  methods: {},
@@ -57571,7 +58573,7 @@ var heatingValveCapability = {
57571
58573
  scope: "device",
57572
58574
  deviceNative: true,
57573
58575
  mode: "singleton",
57574
- deviceTypes: [DeviceType.Thermostat],
58576
+ deviceTypes: [DeviceType.Climate],
57575
58577
  methods: {},
57576
58578
  status: {
57577
58579
  schema: HeatingValveStatusSchema,
@@ -57601,13 +58603,18 @@ var heatingValveCapability = {
57601
58603
  * retried in a loop - D49); a future one is re-armed.
57602
58604
  *
57603
58605
  * What a device may be run for, and for how long at most, is per device type
57604
- * ({@link TIMED_RUN_LIMITS}): the maximum is a safety bound, not a preference,
58606
+ * and role ({@link timedRunLimitOf}): the maximum is a safety bound, not a preference,
57605
58607
  * so a request above it is REFUSED with a typed reason, never clamped.
57606
58608
  */
57607
58609
  var TimedRunUnitSchema = _enum(["s", "min"]);
57608
58610
  _enum(["switch", "pet-fountain"]);
57609
- DeviceType.Heater, DeviceType.PetFountain;
57610
- var TIMED_RUN_DEVICE_TYPES = [DeviceType.Heater, DeviceType.PetFountain];
58611
+ /**
58612
+ * The device types `timed-run` may be offered on. A `switch` is narrowed by
58613
+ * role ({@link TIMED_RUN_DEVICE_ROLES}): only a plug-in heater runs on a timer.
58614
+ */
58615
+ var TIMED_RUN_DEVICE_TYPES = [DeviceType.Switch, DeviceType.PetFountain];
58616
+ /** The roles that make a device of these types timed-run-able (D1033). */
58617
+ var TIMED_RUN_DEVICE_ROLES = { [DeviceType.Switch]: [DeviceRole.SwitchHeater] };
57611
58618
  /** One duration, as the one-tap run asks for it. */
57612
58619
  var TimedRunQuickRunSchema = object({
57613
58620
  duration: number$1().positive().finite(),
@@ -57657,6 +58664,7 @@ var timedRunCapability = {
57657
58664
  kind: "wrapper",
57658
58665
  defaultActive: true,
57659
58666
  deviceTypes: TIMED_RUN_DEVICE_TYPES,
58667
+ deviceRoles: TIMED_RUN_DEVICE_ROLES,
57660
58668
  deviceConfig: { ui: {
57661
58669
  kind: "derived-form",
57662
58670
  builderId: "timed-run",
@@ -57817,8 +58825,8 @@ var HeroSceneSchema = object({
57817
58825
  /** 0..1: volume, cover position, cycle progress. */
57818
58826
  level: number$1().min(0).max(1).nullable()
57819
58827
  });
57820
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway;
57821
- DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
58828
+ 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;
58829
+ DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.SwitchHeater, DeviceRole.SwitchFan, DeviceRole.ClimateAc, DeviceRole.ClimateThermostat, DeviceRole.ClimateFloor, DeviceRole.ClimateRadiator, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
57822
58830
  /** A string in both languages, as the cap vocabulary writes it (D920): no `t()` in the model. */
57823
58831
  var HeroTextSchema = object({
57824
58832
  en: string(),
@@ -57884,13 +58892,11 @@ var HeroConfirmSchema = _enum([
57884
58892
  "press"
57885
58893
  ]);
57886
58894
  var HeroActionSchema = object({
57887
- /** Unique within the model; the `gated` list and the specs name it. */
57888
58895
  id: string(),
57889
58896
  label: HeroTextSchema,
57890
58897
  icon: IconKeySchema,
57891
58898
  call: HeroCallSchema,
57892
58899
  emphasis: _enum(["primary", "normal"]),
57893
- /** Ask before running it. */
57894
58900
  confirm: HeroConfirmSchema.nullable()
57895
58901
  });
57896
58902
  var HeroControlSchema = discriminatedUnion("kind", [
@@ -57973,9 +58979,16 @@ var HeroBadgeSchema = object({
57973
58979
  value: number$1().nullable(),
57974
58980
  label: HeroTextSchema
57975
58981
  });
57976
- var HeroRemoteSlotSchema = object({
57977
- /** Saved commands; `null` = not known yet (the button still shows). */
57978
- commands: number$1().int().nullable() });
58982
+ var HeroRemoteSlotSchema = object({
58983
+ /** Saved commands; `null` = not known yet (the button still shows). */
58984
+ commands: number$1().int().nullable(),
58985
+ /**
58986
+ * The control a list row draws as the "open the remote" icon. Recognised by its id
58987
+ * ({@link REMOTE_OPEN_ACTION_ID}); it changes no state, so a client opens the remote
58988
+ * instead of running `call`. Absent on a model from an older server.
58989
+ */
58990
+ open: HeroActionSchema.optional()
58991
+ });
57979
58992
  var HeroConsumablesSlotSchema = object({
57980
58993
  items: array(object({
57981
58994
  key: string(),
@@ -58143,6 +59156,8 @@ var HeroModelSchema = object({
58143
59156
  * the FIRST row that has any (a later row's control is never promoted). When
58144
59157
  * the model lists that part in `gated`, the row has no inline control: the
58145
59158
  * projection never falls through to another control the caller could use.
59159
+ * - a climate or thermostat row's primary is its `climate.mode` control (no power
59160
+ * toggle: "off" is a mode); when the model gates it the rule above applies as before;
58146
59161
  * - a timed run, when the model carries the slot, sits beside that primary:
58147
59162
  * Run while idle, Stop while armed. Gating the slot hides the control.
58148
59163
  * - the status line is the head (what plays, else the state) plus at most
@@ -58150,6 +59165,13 @@ var HeroModelSchema = object({
58150
59165
  * time left on an armed timed run;
58151
59166
  * - the trailing fact is the first of power, battery, consumables.
58152
59167
  *
59168
+ * - `extras` are the further inline controls of the types that carry more than one
59169
+ * ({@link ROW_EXTRAS_BY_TYPE}): climate setpoint, a robot's return to base,
59170
+ * a player's volume and remote. Each is a control the model already has, named by id,
59171
+ * in that order and at most {@link ROW_EXTRAS_MAX}; one the model lacks, or lists in
59172
+ * `gated`, is absent, and the one `primary` already is never repeats. Empty = the
59173
+ * row of an older server: `primary` alone.
59174
+ *
58153
59175
  * `projectDeviceRow` is pure and computes nothing: it selects from a
58154
59176
  * {@link HeroModel}. A row that needed a value the hero lacks would mean the
58155
59177
  * model is missing it, so the fix goes in the model.
@@ -58181,8 +59203,25 @@ var HeroRowModelSchema = object({
58181
59203
  * while armed. `null` when there is no slot, or the caller may not act.
58182
59204
  */
58183
59205
  timedRun: HeroRowTimedRunSchema.nullable(),
59206
+ /**
59207
+ * The further inline controls after `primary`, in order (see the header). The remote
59208
+ * is an `action` with id `remote.open`: the client opens the remote, it does not run
59209
+ * the call. Absent or empty = `primary` alone.
59210
+ */
59211
+ extras: array(HeroControlSchema).max(2).optional(),
58184
59212
  trailing: HeroFactSchema.nullable()
58185
59213
  });
59214
+ new Set([
59215
+ "temperature",
59216
+ "humidity",
59217
+ "pressure",
59218
+ "illuminance",
59219
+ "reading",
59220
+ "air-quality",
59221
+ "valve"
59222
+ ]);
59223
+ DeviceType.Climate;
59224
+ DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
58186
59225
  /**
58187
59226
  * hero-layout — what a device's hero says (D1023). Read-only: the FIXED
58188
59227
  * layouts live in `@camstack/types` and nothing here stores or edits one.
@@ -58214,7 +59253,10 @@ method(heroInput, HeroModelSchema.nullable(), {
58214
59253
  }), method(rowModelsInput, array(HeroRowModelSchema.nullable()), {
58215
59254
  auth: "protected",
58216
59255
  role: "viewer"
58217
- }), method(object({ deviceType: string() }), HeroLayoutRowsSchema, {
59256
+ }), method(object({
59257
+ deviceType: string(),
59258
+ role: string().nullable().optional()
59259
+ }), HeroLayoutRowsSchema, {
58218
59260
  auth: "protected",
58219
59261
  role: "viewer"
58220
59262
  });
@@ -58261,6 +59303,11 @@ var humidifierCapability = {
58261
59303
  deviceNative: true,
58262
59304
  mode: "singleton",
58263
59305
  deviceTypes: [DeviceType.Humidifier],
59306
+ power: {
59307
+ kind: "field",
59308
+ field: "on",
59309
+ set: "setOn"
59310
+ },
58264
59311
  methods: {
58265
59312
  setOn: method(object({
58266
59313
  deviceId: number$1().int().nonnegative(),
@@ -58334,7 +59381,7 @@ var humiditySensorCapability = {
58334
59381
  scope: "device",
58335
59382
  deviceNative: true,
58336
59383
  mode: "singleton",
58337
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
59384
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
58338
59385
  methods: {},
58339
59386
  status: {
58340
59387
  schema: HumiditySensorStatusSchema,
@@ -58628,6 +59675,12 @@ var IntegrationLiteSchema = object({
58628
59675
  var AvailableIntegrationTypeSchema = object({
58629
59676
  addonId: string(),
58630
59677
  name: string(),
59678
+ /**
59679
+ * Product name a new integration is prefilled from (`<label> - Home`).
59680
+ * Absent: the client uses `name`. Set when the catalog title is not the
59681
+ * product (Bosch SHC, MQTT).
59682
+ */
59683
+ providerLabel: string().optional(),
58631
59684
  description: string(),
58632
59685
  iconUrl: string().nullable(),
58633
59686
  color: string(),
@@ -59255,13 +60308,32 @@ var IrSendResultSchema = object({
59255
60308
  var IrTransmitterInfoSchema = object({
59256
60309
  media: array(IrMediumSchema),
59257
60310
  settable: array(TransmitSettingKeySchema),
59258
- maxTimings: number$1().int().positive()
60311
+ maxTimings: number$1().int().positive(),
60312
+ /** The transmitter can learn a code (`learn`). */
60313
+ learn: boolean().default(false),
60314
+ /** Native code formats the transmitter replays verbatim (`send.native`). */
60315
+ formats: array(IrNativeFormatSchema).default([])
60316
+ });
60317
+ var IR_LEARN_TIMEOUT_DEFAULT_MS = 2e4;
60318
+ var IR_LEARN_TIMEOUT_MAX_MS = 6e4;
60319
+ var IrLearnResultSchema = object({
60320
+ /** The code in the device's own format, when it reports one. */
60321
+ native: IrNativeCodeSchema.nullable(),
60322
+ timings: IrTimingsSchema,
60323
+ carrierHz: number$1().int().positive().nullable()
59259
60324
  });
59260
60325
  DeviceType.Generic, method(object({
59261
60326
  deviceId: number$1().int().nonnegative(),
59262
- timings: IrTimingsSchema,
60327
+ timings: IrTimingsSchema.optional(),
60328
+ native: IrNativeCodeSchema.optional(),
59263
60329
  settings: TransmitSettingsSchema.optional()
59264
- }), IrSendResultSchema, {
60330
+ }).refine((input) => input.timings === void 0 !== (input.native === void 0), { message: "send takes exactly one of timings or native" }), IrSendResultSchema, {
60331
+ kind: "mutation",
60332
+ auth: "protected"
60333
+ }), method(object({
60334
+ deviceId: number$1().int().nonnegative(),
60335
+ timeoutMs: number$1().int().positive().max(IR_LEARN_TIMEOUT_MAX_MS).default(IR_LEARN_TIMEOUT_DEFAULT_MS)
60336
+ }), IrLearnResultSchema, {
59265
60337
  kind: "mutation",
59266
60338
  auth: "protected"
59267
60339
  }), method(object({ deviceId: number$1().int().nonnegative() }), IrTransmitterInfoSchema, {
@@ -69047,6 +70119,7 @@ var NcSystemEventKindSchema = _enum([
69047
70119
  "recording-degraded",
69048
70120
  "device-attention-required",
69049
70121
  "device-attention-resolved",
70122
+ "device-command",
69050
70123
  "storage-location-low",
69051
70124
  "storage-location-critical",
69052
70125
  "storage-location-ok",
@@ -69109,11 +70182,6 @@ var NC_ALARM_SYSTEM_EVENT_KINDS = [
69109
70182
  "alarm-sensor-bypass",
69110
70183
  "alarm-disarmed"
69111
70184
  ];
69112
- /**
69113
- * One coherent system-event condition. `kinds` is the required opt-in safety
69114
- * gate; the remaining lists are optional narrowing filters relevant to the
69115
- * selected kinds.
69116
- */
69117
70185
  var NcSystemEventConditionSchema = object({
69118
70186
  kinds: array(NcSystemEventKindSchema).min(1),
69119
70187
  deviceIds: array(number$1().int()).min(1).optional(),
@@ -69145,6 +70213,28 @@ var NcSystemEventConditionSchema = object({
69145
70213
  nodeIds: array(string().min(1)).min(1).optional(),
69146
70214
  packageNames: array(string().min(1)).min(1).optional(),
69147
70215
  /**
70216
+ * `device-command` only: the vocabulary ACTION IDS (D983, `powerSource.forceRestart`)
70217
+ * this rule speaks about - the same dictionary the automation action blocks
70218
+ * pick from. The hub refuses an id the action index does not know.
70219
+ */
70220
+ commandActions: array(string().min(1)).min(1).optional(),
70221
+ /** `device-command` only: how the command settled. A device's own change is not a command. */
70222
+ commandOutcomes: array(_enum([
70223
+ "confirmed",
70224
+ "already",
70225
+ "unconfirmed",
70226
+ "failed"
70227
+ ])).min(1).optional(),
70228
+ /** `device-command` only: who asked (`user`, `addon`, `action`, `scheduler`, `composition`, `node`). */
70229
+ commandActorKinds: array(_enum([
70230
+ "user",
70231
+ "addon",
70232
+ "action",
70233
+ "scheduler",
70234
+ "composition",
70235
+ "node"
70236
+ ])).min(1).optional(),
70237
+ /**
69148
70238
  * At or below this remaining-life %, a consumable is LOW for THIS rule
69149
70239
  * (D642). Absent = {@link NC_CONSUMABLE_LOW_PERCENT_DEFAULT}; at most
69150
70240
  * {@link NC_CONSUMABLE_LOW_PERCENT_MAX}, so every episode can recover. Scoped to the
@@ -70125,6 +71215,13 @@ var NcRuleInputSchema = object({
70125
71215
  */
70126
71216
  groupingSec: number$1().int().min(5).max(60).optional(),
70127
71217
  /**
71218
+ * Hold deliveries of this rule to one target and, when two or more arrive
71219
+ * before the window ends, send them as one summary. Absent = the kind's
71220
+ * default (`groupWithinSecOf`): 5 s for a system-event rule, 0 for every
71221
+ * other kind. `0` sends each delivery as it does today.
71222
+ */
71223
+ groupWithinSec: number$1().int().min(0).max(300).optional(),
71224
+ /**
70128
71225
  * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
70129
71226
  * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
70130
71227
  * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
@@ -70561,7 +71658,12 @@ var NcHistoryEntrySchema = object({
70561
71658
  * Assistant export) must render as "no image right now", never as a
70562
71659
  * broken link.
70563
71660
  */
70564
- artifactIds: array(string().min(1)).optional()
71661
+ artifactIds: array(string().min(1)).optional(),
71662
+ /**
71663
+ * When this row was folded into a group-window summary, the outbox id of
71664
+ * that one notification. Absent on a row that was sent as itself.
71665
+ */
71666
+ groupedInto: string().min(1).optional()
70565
71667
  });
70566
71668
  /**
70567
71669
  * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
@@ -71516,7 +72618,18 @@ var OauthIntegrationDescriptorSchema = object({
71516
72618
  * The value is baked into the authorization code at `/authorize` and travels
71517
72619
  * on the tokens, so editing this field changes FUTURE links only.
71518
72620
  */
71519
- refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional()
72621
+ refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional(),
72622
+ /**
72623
+ * The integration instances an account can be linked to (planning#415),
72624
+ * beside the default one. `/api/oauth2/authorize?instance=<id>` is accepted
72625
+ * only for an id listed here; with no `instance=` the link is made to the
72626
+ * default instance, which is never listed and always accepted — it is the
72627
+ * address third parties already hold. Omit for a single-instance integration.
72628
+ */
72629
+ instances: array(object({
72630
+ id: string(),
72631
+ displayName: string()
72632
+ })).optional()
71520
72633
  });
71521
72634
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
71522
72635
  /**
@@ -71939,9 +73052,13 @@ var petFeederCapability = {
71939
73052
  */
71940
73053
  durability: "session",
71941
73054
  /**
71942
- * History (D850): recorded series.
73055
+ * History (D850): recorded series. `feeding` / `eating` are the meals the
73056
+ * device hero's "Meals" button names — change-only booleans, so a dispense
73057
+ * or a pet at the bowl is one span on the history.
71943
73058
  */
71944
73059
  history: { fields: {
73060
+ feeding: { kind: "boolean" },
73061
+ eating: { kind: "boolean" },
71945
73062
  foodLevel: { kind: "gauge" },
71946
73063
  bowlFood: { kind: "gauge" },
71947
73064
  food1: { kind: "gauge" },
@@ -72002,6 +73119,11 @@ var petFountainCapability = {
72002
73119
  deviceNative: true,
72003
73120
  mode: "singleton",
72004
73121
  deviceTypes: [DeviceType.PetFountain],
73122
+ power: {
73123
+ kind: "field",
73124
+ field: "pumpRunning",
73125
+ set: "setPump"
73126
+ },
72005
73127
  methods: {
72006
73128
  /** Start or stop the pump. */
72007
73129
  setPump: method(object({
@@ -73577,7 +74699,6 @@ var POWER_METER_DEVICE_TYPES = [
73577
74699
  DeviceType.Light,
73578
74700
  DeviceType.Fan,
73579
74701
  DeviceType.Climate,
73580
- DeviceType.Thermostat,
73581
74702
  DeviceType.Humidifier,
73582
74703
  DeviceType.WaterHeater,
73583
74704
  DeviceType.MediaPlayer,
@@ -73586,8 +74707,7 @@ var POWER_METER_DEVICE_TYPES = [
73586
74707
  DeviceType.Dishwasher,
73587
74708
  DeviceType.Vacuum,
73588
74709
  DeviceType.LawnMower,
73589
- DeviceType.PetFeeder,
73590
- DeviceType.Heater
74710
+ DeviceType.PetFeeder
73591
74711
  ];
73592
74712
  /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
73593
74713
  var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
@@ -73700,118 +74820,6 @@ var powerMeterCapability = {
73700
74820
  volatileStateFields: ["lastFetchedAt"],
73701
74821
  customize: powerMeterCustomize
73702
74822
  };
73703
- var PowerSourceRefusalReasonSchema = _enum([
73704
- "no-power-source",
73705
- "switch-missing",
73706
- "switch-offline",
73707
- "switch-unknown",
73708
- "restart-in-progress",
73709
- "switch-call-failed"
73710
- ]);
73711
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
73712
- accepted: literal(false),
73713
- reason: PowerSourceRefusalReasonSchema,
73714
- /** One line for the operator, naming the switch. */
73715
- message: string()
73716
- })]);
73717
- var PowerSourceInfoSchema = object({
73718
- /** False when the device has no power source linked; the rest is then empty. */
73719
- linked: boolean(),
73720
- switchDeviceId: number$1().nullable(),
73721
- switchName: string().nullable(),
73722
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
73723
- switchOn: boolean().nullable(),
73724
- switchOnline: boolean().nullable(),
73725
- restartInProgress: boolean(),
73726
- offSeconds: number$1().int()
73727
- });
73728
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
73729
- var PowerSourceStateSchema = object({
73730
- /** Null while the switch has not reported: unknown is never `false` (D393). */
73731
- switchOn: boolean().nullable(),
73732
- offSeconds: number$1().int(),
73733
- lastFetchedAt: number$1()
73734
- });
73735
- var POWER_SOURCE_DEVICE_TYPES = [
73736
- DeviceType.Gateway,
73737
- DeviceType.Hub,
73738
- DeviceType.Camera,
73739
- DeviceType.Sensor,
73740
- DeviceType.MediaPlayer,
73741
- DeviceType.Vacuum,
73742
- DeviceType.Generic,
73743
- DeviceType.Washer,
73744
- DeviceType.Dryer,
73745
- DeviceType.Dishwasher,
73746
- DeviceType.PetFountain,
73747
- DeviceType.Fan,
73748
- DeviceType.Humidifier,
73749
- DeviceType.Climate,
73750
- DeviceType.WaterHeater
73751
- ];
73752
- var powerSourceCustomize = {
73753
- version: 1,
73754
- targets: [{
73755
- id: "switch",
73756
- cardinality: "scalar",
73757
- vocabulary: {
73758
- cap: "power-source",
73759
- stateField: "switchOn"
73760
- },
73761
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
73762
- apply: "add",
73763
- path: "switchOn",
73764
- sources: [{
73765
- kind: "switch",
73766
- transforms: [{ kind: "identity" }]
73767
- }],
73768
- companions: [{
73769
- kind: "constant",
73770
- path: "offSeconds",
73771
- value: 10,
73772
- when: "add",
73773
- formerValues: [0]
73774
- }, {
73775
- kind: "clock",
73776
- path: "lastFetchedAt",
73777
- when: "always"
73778
- }],
73779
- sourceReport: { status: "assignment-status" }
73780
- }]
73781
- };
73782
- var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
73783
- var powerSourceCapability = {
73784
- name: "power-source",
73785
- scope: "device",
73786
- mode: "singleton",
73787
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
73788
- methods: {
73789
- getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "protected" }),
73790
- powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73791
- kind: "mutation",
73792
- auth: "protected"
73793
- }),
73794
- powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73795
- kind: "mutation",
73796
- auth: "protected"
73797
- }),
73798
- forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
73799
- kind: "mutation",
73800
- auth: "protected"
73801
- })
73802
- },
73803
- runtimeState: PowerSourceStateSchema,
73804
- customize: powerSourceCustomize,
73805
- /**
73806
- * Runtime-state durability: **session** — mirrored from the linked switch on
73807
- * its next report; a restored value would outlive the reading it names.
73808
- *
73809
- * See `RuntimeStateDurability`. Enforced by
73810
- * `scripts/check-runtime-state-durability.ts`.
73811
- */
73812
- durability: "session",
73813
- history: "none"
73814
- };
73815
74823
  /**
73816
74824
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
73817
74825
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -77278,9 +78286,7 @@ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
77278
78286
  DeviceType.Humidifier,
77279
78287
  DeviceType.Fan,
77280
78288
  DeviceType.Climate,
77281
- DeviceType.Thermostat,
77282
- DeviceType.PetFeeder,
77283
- DeviceType.Heater
78289
+ DeviceType.PetFeeder
77284
78290
  ];
77285
78291
  var runningStateCapability = {
77286
78292
  name: "running-state",
@@ -78552,6 +79558,13 @@ var SsoBridgeClaimsSchema = object({
78552
79558
  /** OAuth authorization-code binding — set only on `oauth-code` tokens. */
78553
79559
  redirectUri: string().optional(),
78554
79560
  integrationId: string().optional(),
79561
+ /**
79562
+ * The integration INSTANCE an OAuth link was made for (planning#415). Set on
79563
+ * `oauth-code`, `oauth-access` and `oauth-refresh` tokens of a link made
79564
+ * with `instance=`; absent on a link to the default instance. A cloud relay
79565
+ * reads it off the access token to route; the hub answers from the session.
79566
+ */
79567
+ instanceId: string().optional(),
78555
79568
  /** JWT ID — unique per issued code; consumed-set enforces single-use. */
78556
79569
  jti: string().optional(),
78557
79570
  /** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
@@ -79368,8 +80381,7 @@ var switchCapability = {
79368
80381
  DeviceType.Switch,
79369
80382
  DeviceType.Siren,
79370
80383
  DeviceType.Light,
79371
- DeviceType.Fan,
79372
- DeviceType.Heater
80384
+ DeviceType.Fan
79373
80385
  ],
79374
80386
  methods: { setState: method(object({
79375
80387
  deviceId: number$1(),
@@ -80063,7 +81075,7 @@ var temperatureSensorCapability = {
80063
81075
  scope: "device",
80064
81076
  deviceNative: true,
80065
81077
  mode: "singleton",
80066
- deviceTypes: [DeviceType.Sensor, DeviceType.Thermostat],
81078
+ deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
80067
81079
  methods: {},
80068
81080
  status: {
80069
81081
  schema: TemperatureSensorStatusSchema,
@@ -80931,6 +81943,8 @@ var OauthSessionSummarySchema = object({
80931
81943
  userId: string(),
80932
81944
  username: string(),
80933
81945
  integrationId: string(),
81946
+ /** The instance the link was made for; `null` = the default instance. */
81947
+ instanceId: string().nullable().optional(),
80934
81948
  scopes: array(TokenScopeSchema),
80935
81949
  createdAt: number$1(),
80936
81950
  lastUsedAt: number$1(),
@@ -81030,6 +82044,9 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
81030
82044
  access: "view"
81031
82045
  }), method(object({
81032
82046
  integrationId: string(),
82047
+ /** The integration instance the link is for. Omit for the default
82048
+ * instance — never pass the addon id to mean it. */
82049
+ instanceId: string().optional(),
81033
82050
  userId: string(),
81034
82051
  username: string(),
81035
82052
  scopes: array(TokenScopeSchema),
@@ -81068,7 +82085,13 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
81068
82085
  }), method(object({ token: string() }), object({
81069
82086
  userId: string(),
81070
82087
  username: string(),
81071
- scopes: array(TokenScopeSchema)
82088
+ scopes: array(TokenScopeSchema),
82089
+ /** The instance the link was made for, read from the SESSION.
82090
+ * Absent = the default instance. `oauthLinkServesInstance` is the
82091
+ * helper a per-instance route calls to compare it with the instance
82092
+ * of its path; no route declares `perInstance` yet (the Alexa
82093
+ * per-instance route, T3, is the first consumer). */
82094
+ instanceId: string().optional()
81072
82095
  }).nullable(), { access: "view" }), method(object({ userId: string() }), array(ExternalIdentitySchema), { auth: "admin" }), method(ExternalIdentityKeySchema, object({
81073
82096
  userId: string(),
81074
82097
  issuer: string()
@@ -81710,6 +82733,13 @@ var waterHeaterCapability = {
81710
82733
  deviceNative: true,
81711
82734
  mode: "singleton",
81712
82735
  deviceTypes: [DeviceType.WaterHeater],
82736
+ power: {
82737
+ kind: "mode",
82738
+ field: "operationMode",
82739
+ offValue: "off",
82740
+ set: "setOperationMode",
82741
+ lastField: "lastActiveMode"
82742
+ },
81713
82743
  methods: {
81714
82744
  setTargetTemp: method(object({
81715
82745
  deviceId: number$1().int().nonnegative(),
@@ -83368,7 +84398,6 @@ var DEVICE_LOCAL_STATE_CAPS = {
83368
84398
  control: controlCapability,
83369
84399
  cover: coverCapability,
83370
84400
  dayNight: dayNightCapability,
83371
- deviceDiscovery: deviceDiscoveryCapability,
83372
84401
  deviceStatus: deviceStatusCapability,
83373
84402
  dishwasher: dishwasherCapability,
83374
84403
  doNotDisturb: doNotDisturbCapability,
@@ -84463,15 +85492,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84463
85492
  });
84464
85493
  const meta = await this.ctx.api.deviceManager.loadMeta.query({ deviceId: id });
84465
85494
  if (meta === null) throw new Error(`${this.providerName}: no persisted meta for "${stableId}" (id ${id}) — cannot reload`);
84466
- const deviceType = Object.values(DeviceType).find((t) => t === meta.type);
84467
- const Class = deviceType !== void 0 ? this.deviceClasses[deviceType] : void 0;
85495
+ const Class = this.deviceClassForRow({
85496
+ type: meta.type,
85497
+ stableId
85498
+ });
84468
85499
  if (!Class) throw new Error(`${this.providerName}: no device class for type "${meta.type}" — "${stableId}" is an accessory child; reload its parent instead`);
84469
85500
  await devices.create(stableId, Class, {}, meta.parentDeviceId ?? null);
84470
85501
  const rows = await this.ctx.api.deviceManager.listPersistedByAddon.query({ addonId: this.addonId });
84471
85502
  for (const row of rows) {
84472
85503
  if (row.parentDeviceId !== id) continue;
84473
- const childType = Object.values(DeviceType).find((t) => t === row.type);
84474
- const ChildClass = childType !== void 0 ? this.deviceClasses[childType] : void 0;
85504
+ const ChildClass = this.deviceClassForRow(row);
84475
85505
  if (!ChildClass) continue;
84476
85506
  try {
84477
85507
  await devices.create(row.stableId, ChildClass, {}, id);
@@ -84498,6 +85528,16 @@ var BaseDeviceProvider = class extends BaseAddon {
84498
85528
  return { deviceId: id };
84499
85529
  }
84500
85530
  /**
85531
+ * The class a PERSISTED row is rebuilt with. Default: by type, from
85532
+ * `deviceClasses`. A provider whose distinct devices share one type (D1033:
85533
+ * a radiator valve and a room climate are both `climate`) overrides this
85534
+ * and tells them apart by the row's stable id or config.
85535
+ */
85536
+ deviceClassForRow(row) {
85537
+ const type = Object.values(DeviceType).find((t) => t === row.type);
85538
+ return type !== void 0 ? this.deviceClasses[type] : void 0;
85539
+ }
85540
+ /**
84501
85541
  * Restore devices from persisted state. Two-pass:
84502
85542
  *
84503
85543
  * 1. **Top-level pass** — invokes `kernel.devices.create()` for every
@@ -84558,7 +85598,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84558
85598
  restored.add(saved.id);
84559
85599
  return;
84560
85600
  }
84561
- const Class = this.deviceClasses[saved.type];
85601
+ const Class = this.deviceClassForRow(saved);
84562
85602
  if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
84563
85603
  if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
84564
85604
  await this.healSavedConfig(saved, savedDevices);
@@ -84575,7 +85615,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84575
85615
  };
84576
85616
  const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
84577
85617
  const restoreOne = async (saved) => {
84578
- if (!this.deviceClasses[saved.type]) {
85618
+ if (!this.deviceClassForRow(saved)) {
84579
85619
  this.ctx.logger.warn("No device class registered for restored type — skipping", {
84580
85620
  tags: {
84581
85621
  deviceId: saved.id,
@@ -84614,7 +85654,7 @@ var BaseDeviceProvider = class extends BaseAddon {
84614
85654
  const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
84615
85655
  const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
84616
85656
  for (const saved of childRows) {
84617
- if (!this.deviceClasses[saved.type]) continue;
85657
+ if (!this.deviceClassForRow(saved)) continue;
84618
85658
  if (saved.parentDeviceId === null) continue;
84619
85659
  if (restored.has(saved.parentDeviceId)) {
84620
85660
  try {
@@ -84668,7 +85708,9 @@ var BaseDeviceProvider = class extends BaseAddon {
84668
85708
  }
84669
85709
  };
84670
85710
  DeviceType.HeatPump;
84671
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Heater, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
85711
+ DeviceType.Climate;
85712
+ DeviceRole.ClimateThermostat, DeviceRole.ClimateAc, DeviceRole.ClimateRadiator, DeviceRole.ClimateFloor, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit;
85713
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
84672
85714
  new Set(Object.values(DeviceType));
84673
85715
  /** Parses the raw slice with the cap's own schema; `undefined` when it does not parse. */
84674
85716
  function contribution(cap, schema, build) {
@@ -86674,7 +87716,7 @@ var HEAT_PUMP_FAMILY_CONTRIBUTIONS = { "heat-pump-status": contribution("heat-pu
86674
87716
  ...Object.values(CLIMATE_FAMILY_CONTRIBUTIONS),
86675
87717
  ...Object.values(HEAT_PUMP_FAMILY_CONTRIBUTIONS)
86676
87718
  ];
86677
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway;
87719
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
86678
87720
  /**
86679
87721
  * Who looks like what (D1023). One look per {@link DeviceType}, refined by a
86680
87722
  * {@link DeviceRole} where the role changes how the device reads (a leak
@@ -87318,9 +88360,9 @@ lookOf({
87318
88360
  idle: INFO,
87319
88361
  standby: MUTED
87320
88362
  });
87321
- DeviceType.Climate, DeviceType.Thermostat;
87322
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway;
87323
- DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
88363
+ DeviceType.Climate;
88364
+ 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;
88365
+ DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.SwitchHeater, DeviceRole.SwitchFan, DeviceRole.ClimateAc, DeviceRole.ClimateThermostat, DeviceRole.ClimateFloor, DeviceRole.ClimateRadiator, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
87324
88366
  Object.freeze({
87325
88367
  "accessories.getStatus": {
87326
88368
  capName: "accessories",
@@ -89170,41 +90212,6 @@ Object.freeze({
89170
90212
  access: "create",
89171
90213
  role: "admin"
89172
90214
  },
89173
- "deviceDiscovery.adoptDevice": {
89174
- capName: "device-discovery",
89175
- capScope: "device",
89176
- addonId: null,
89177
- access: "create",
89178
- role: "admin"
89179
- },
89180
- "deviceDiscovery.getStatus": {
89181
- capName: "device-discovery",
89182
- capScope: "device",
89183
- addonId: null,
89184
- access: "view",
89185
- role: "viewer"
89186
- },
89187
- "deviceDiscovery.listDiscovered": {
89188
- capName: "device-discovery",
89189
- capScope: "device",
89190
- addonId: null,
89191
- access: "view",
89192
- role: "viewer"
89193
- },
89194
- "deviceDiscovery.refreshDiscovery": {
89195
- capName: "device-discovery",
89196
- capScope: "device",
89197
- addonId: null,
89198
- access: "create",
89199
- role: "admin"
89200
- },
89201
- "deviceDiscovery.releaseDevice": {
89202
- capName: "device-discovery",
89203
- capScope: "device",
89204
- addonId: null,
89205
- access: "create",
89206
- role: "admin"
89207
- },
89208
90215
  "deviceExport.applyDeviceSettingsPatch": {
89209
90216
  capName: "device-export",
89210
90217
  capScope: "system",
@@ -90752,6 +91759,13 @@ Object.freeze({
90752
91759
  access: "create",
90753
91760
  role: "viewer"
90754
91761
  },
91762
+ "fanControl.setOscillationAxis": {
91763
+ capName: "fan-control",
91764
+ capScope: "device",
91765
+ addonId: null,
91766
+ access: "create",
91767
+ role: "viewer"
91768
+ },
90755
91769
  "fanControl.setPercentage": {
90756
91770
  capName: "fan-control",
90757
91771
  capScope: "device",
@@ -91214,6 +92228,13 @@ Object.freeze({
91214
92228
  access: "view",
91215
92229
  role: "viewer"
91216
92230
  },
92231
+ "irTransmitter.learn": {
92232
+ capName: "ir-transmitter",
92233
+ capScope: "device",
92234
+ addonId: null,
92235
+ access: "create",
92236
+ role: "viewer"
92237
+ },
91217
92238
  "irTransmitter.send": {
91218
92239
  capName: "ir-transmitter",
91219
92240
  capScope: "device",
@@ -97773,7 +98794,7 @@ function projectAction(cap, action) {
97773
98794
  };
97774
98795
  }
97775
98796
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
97776
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Heater, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
98797
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
97777
98798
  DeviceFeature.BatteryOperated;
97778
98799
  function bound(cap, methods = [], authority = "reported") {
97779
98800
  return {
@@ -98262,35 +99283,6 @@ Object.freeze({
98262
99283
  form: "single",
98263
99284
  optional: false
98264
99285
  }],
98265
- "deviceDiscovery.adoptDevice": [{
98266
- name: "deviceId",
98267
- form: "single",
98268
- optional: false
98269
- }],
98270
- "deviceDiscovery.getStatus": [{
98271
- name: "deviceId",
98272
- form: "single",
98273
- optional: false
98274
- }],
98275
- "deviceDiscovery.listDiscovered": [{
98276
- name: "deviceId",
98277
- form: "single",
98278
- optional: false
98279
- }],
98280
- "deviceDiscovery.refreshDiscovery": [{
98281
- name: "deviceId",
98282
- form: "single",
98283
- optional: false
98284
- }],
98285
- "deviceDiscovery.releaseDevice": [{
98286
- name: "childDeviceId",
98287
- form: "single",
98288
- optional: false
98289
- }, {
98290
- name: "deviceId",
98291
- form: "single",
98292
- optional: false
98293
- }],
98294
99286
  "deviceExport.applyDeviceSettingsPatch": [{
98295
99287
  name: "deviceId",
98296
99288
  form: "single",
@@ -98854,6 +99846,11 @@ Object.freeze({
98854
99846
  form: "single",
98855
99847
  optional: false
98856
99848
  }],
99849
+ "fanControl.setOscillationAxis": [{
99850
+ name: "deviceId",
99851
+ form: "single",
99852
+ optional: false
99853
+ }],
98857
99854
  "fanControl.setPercentage": [{
98858
99855
  name: "deviceId",
98859
99856
  form: "single",
@@ -99089,6 +100086,11 @@ Object.freeze({
99089
100086
  form: "single",
99090
100087
  optional: false
99091
100088
  }],
100089
+ "irTransmitter.learn": [{
100090
+ name: "deviceId",
100091
+ form: "single",
100092
+ optional: false
100093
+ }],
99092
100094
  "irTransmitter.send": [{
99093
100095
  name: "deviceId",
99094
100096
  form: "single",
@@ -101028,7 +102030,10 @@ var NC_SYSTEM_EVENT_FILTER_KEYS = [
101028
102030
  "deviceTypes",
101029
102031
  "nodeIds",
101030
102032
  "packageNames",
101031
- "consumableLowPercent"
102033
+ "consumableLowPercent",
102034
+ "commandActions",
102035
+ "commandOutcomes",
102036
+ "commandActorKinds"
101032
102037
  ];
101033
102038
  new Map(Object.entries({
101034
102039
  "device-online": "device-events",
@@ -101051,6 +102056,7 @@ new Map(Object.entries({
101051
102056
  "recording-degraded": "device-events",
101052
102057
  "device-attention-required": "device-events",
101053
102058
  "device-attention-resolved": "device-events",
102059
+ "device-command": "device-events",
101054
102060
  "node-online": "system",
101055
102061
  "node-offline": "system",
101056
102062
  "node-restarting": "system",
@@ -101151,6 +102157,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
101151
102157
  label: "Needs attention",
101152
102158
  kinds: ["device-attention-required", "device-attention-resolved"]
101153
102159
  },
102160
+ {
102161
+ id: "device-command",
102162
+ family: "device-events",
102163
+ label: "Commands given to devices",
102164
+ kinds: ["device-command"]
102165
+ },
101154
102166
  {
101155
102167
  id: "nodes",
101156
102168
  family: "system",
@@ -101423,6 +102435,7 @@ var DEVICE_EVENTS = [
101423
102435
  "recording-healthy",
101424
102436
  "recording-degraded",
101425
102437
  ...DEVICE_ATTENTION,
102438
+ "device-command",
101426
102439
  ...ALARM
101427
102440
  ];
101428
102441
  /** Every kind a summary may speak about — the two summarisable families. */
@@ -101576,6 +102589,9 @@ var STATE_SUMMARY_VARS = [
101576
102589
  [...RULE_BODY_ONLY],
101577
102590
  [...RULE_TEXT],
101578
102591
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
102592
+ [...RULE_TEXT],
102593
+ [...RULE_TEXT],
102594
+ ([...RULE_TEXT], [...DEVICE_EVENTS]),
101579
102595
  ([...RULE_TEXT], [...COMBINED_ALARM]),
101580
102596
  ([...RULE_TEXT], [...COMBINED_ALARM]),
101581
102597
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
@@ -102328,6 +103344,27 @@ function ingestReading(input) {
102328
103344
  value: converted
102329
103345
  };
102330
103346
  }
103347
+ function collapse(value) {
103348
+ return value.trim().replace(/\s+/g, " ");
103349
+ }
103350
+ /** "KLF-200" → "KLF200". A hyphen between words ("Home Hub") stays. */
103351
+ function compactModelToken(value) {
103352
+ return value.replace(/([A-Za-z])-(\d)/g, "$1$2");
103353
+ }
103354
+ /**
103355
+ * A Gateway device: "<Brand> <Model>". An unknown model is the brand alone
103356
+ * (the caller passes the brand-level fallback it already has, e.g. "Velux KLF-200").
103357
+ */
103358
+ function gatewayDeviceName(brand, model) {
103359
+ const brandPart = compactModelToken(collapse(brand));
103360
+ const raw = collapse(model ?? "");
103361
+ if (raw.length === 0) return brandPart;
103362
+ const modelPart = compactModelToken(raw);
103363
+ const prefix = `${brandPart.toLowerCase()} `;
103364
+ if (modelPart.toLowerCase() === brandPart.toLowerCase()) return brandPart;
103365
+ if (modelPart.toLowerCase().startsWith(prefix)) return modelPart;
103366
+ return `${brandPart} ${modelPart}`;
103367
+ }
102331
103368
  /**
102332
103369
  * Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
102333
103370
  * event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
@@ -103714,7 +104751,7 @@ function buildEnpalCandidates(input) {
103714
104751
  const boxChildren = box === void 0 ? [] : reportedChildren(box, snapshot);
103715
104752
  const out = [{
103716
104753
  childNativeId: "box",
103717
- name: input.box.name,
104754
+ name: gatewayDeviceName("Enpal", "Box"),
103718
104755
  type: DeviceType.Gateway,
103719
104756
  status: input.box.online ? "online" : "offline",
103720
104757
  metadata: METADATA,
@@ -103838,7 +104875,7 @@ function buildEnpalAdoptionProvider(deps) {
103838
104875
  const specIds = reportedSpecIds(equipment, candidate);
103839
104876
  const { deviceId, accessoryDeviceIds } = await deps.adoptGateway({
103840
104877
  integrationId,
103841
- name: perCandidate?.[equipmentId]?.name ?? boxName(integrationId),
104878
+ name: perCandidate?.[equipmentId]?.name ?? gatewayDeviceName("Enpal", "Box"),
103842
104879
  specIds,
103843
104880
  childLayout: childLayoutOf(equipment, specIds)
103844
104881
  });
@@ -104637,7 +105674,39 @@ var enpalConnectionsEntryKind = defineEntryKind("connections", {
104637
105674
  });
104638
105675
  /** Every kind this addon declares, keyed by its name. */
104639
105676
  var enpalRecordKinds = { connections: enpalConnectionsEntryKind };
105677
+ var REJECTED_KINDS = new Set([
105678
+ "unauthorized",
105679
+ "token-missing",
105680
+ "empty"
105681
+ ]);
105682
+ function connectionForTest(values) {
105683
+ const host = values["host"];
105684
+ if (typeof host !== "string" || host.trim().length === 0) return null;
105685
+ const parsed = enpalConnectionSchema.safeParse({ ...values });
105686
+ return parsed.success ? parsed.data : null;
105687
+ }
105688
+ function testFailure(error) {
105689
+ if (error instanceof EnpalError) return {
105690
+ success: false,
105691
+ status: REJECTED_KINDS.has(error.kind) ? "rejected" : "inconclusive",
105692
+ error: error.message
105693
+ };
105694
+ return {
105695
+ success: false,
105696
+ status: "inconclusive",
105697
+ error: "The Enpal box did not answer"
105698
+ };
105699
+ }
105700
+ function liveProbe(blazor) {
105701
+ return (connection) => readEnpalSnapshot({
105702
+ connection,
105703
+ fetch: globalThis.fetch,
105704
+ now: () => Date.now(),
105705
+ blazor: blazor?.()
105706
+ });
105707
+ }
104640
105708
  function buildEnpalInstances(records, deps) {
105709
+ const probe = deps.probe ?? liveProbe(deps.blazor);
104641
105710
  return new RecordIntegrationInstances(records, enpalConnectionsEntryKind, {
104642
105711
  description: {
104643
105712
  role: "devices",
@@ -104647,18 +105716,36 @@ function buildEnpalInstances(records, deps) {
104647
105716
  },
104648
105717
  instanceMode: "multiple",
104649
105718
  kind: "server",
104650
- canTest: false,
105719
+ canTest: true,
104651
105720
  kinds: [{
104652
105721
  id: "connection",
104653
105722
  label: "Enpal",
104654
105723
  functionCap: "device-provider",
104655
- role: "devices"
105724
+ role: "devices",
105725
+ canTest: true
104656
105726
  }]
104657
105727
  },
104658
105728
  formSchema: () => buildConnectionFormSchema(),
104659
105729
  initialBody: (name) => ({ name }),
104660
105730
  configured: (body) => (body.host ?? "").length > 0,
104661
- onChanged: () => deps.onChanged()
105731
+ onChanged: () => deps.onChanged(),
105732
+ test: async ({ values }) => {
105733
+ const connection = connectionForTest(values);
105734
+ if (connection === null) return {
105735
+ success: false,
105736
+ status: "rejected",
105737
+ error: "Host is required"
105738
+ };
105739
+ try {
105740
+ await probe(connection);
105741
+ return {
105742
+ success: true,
105743
+ status: "validated"
105744
+ };
105745
+ } catch (error) {
105746
+ return testFailure(error);
105747
+ }
105748
+ }
104662
105749
  });
104663
105750
  }
104664
105751
  /**
@@ -104951,7 +106038,10 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
104951
106038
  ...rowsDepsOver(api),
104952
106039
  kind: enpalConnectionsEntryKind
104953
106040
  }));
104954
- this.instances = buildEnpalInstances(records, { onChanged: () => this.reconcileIntegrations() });
106041
+ this.instances = buildEnpalInstances(records, {
106042
+ onChanged: () => this.reconcileIntegrations(),
106043
+ blazor: () => this.blazorReader ?? void 0
106044
+ });
104955
106045
  this.blazorReader = await this.createBlazorBridge();
104956
106046
  this.clients = new EnpalClientRegistry(this.ctx.logger, {
104957
106047
  blazor: () => this.blazorReader ?? void 0,