@camstack/addon-export-alexa 1.2.144 → 1.2.146

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.
@@ -5,7 +5,7 @@ import "node:fs";
5
5
  import "node:path";
6
6
  import "node:child_process";
7
7
  import "node:zlib";
8
- //#region ../types/dist/event-category-C4hVnRo8.mjs
8
+ //#region ../types/dist/event-category-BvOFi6PY.mjs
9
9
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
10
10
  EventCategory["SystemBoot"] = "system.boot";
11
11
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -69,6 +69,24 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
69
69
  * consumes this category and maps it to the `addon-crash-loop` kind.
70
70
  */
71
71
  EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
72
+ /**
73
+ * A runner whose addon refused to start with a typed reason (D877) - parked
74
+ * in `refused`, never counted by the crash breaker, retried on operator
75
+ * action. One event per refusal.
76
+ */
77
+ EventCategory["AddonRunnerRefused"] = "addon.runner-refused";
78
+ /**
79
+ * A runner settled on a state (D877): `running` (READY), `crashed`, `failed` or
80
+ * `refused`. In-process telemetry for the boot check (re-evaluate while any
81
+ * runner is still pending) and the alert centre (close a recovered runner).
82
+ */
83
+ EventCategory["ProcessStateChanged"] = "process.state-changed";
84
+ /**
85
+ * The node's one-shot runner boot check (D877): planned runners against the
86
+ * process service, `degraded` when any is failed, refused or missing. The
87
+ * alert centre turns it into one alert and a direct critical push.
88
+ */
89
+ EventCategory["SystemRunnerCheck"] = "system.runner-check";
72
90
  EventCategory["AddonError"] = "addon.error";
73
91
  EventCategory["AddonPageReady"] = "addon.page-ready";
74
92
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -5393,7 +5411,7 @@ var ZodIssueCode = {
5393
5411
  var ZodFirstPartyTypeKind;
5394
5412
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5395
5413
  //#endregion
5396
- //#region ../types/dist/sleep-DRqxtc0C.mjs
5414
+ //#region ../types/dist/sleep-bZIhUvjd.mjs
5397
5415
  /**
5398
5416
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5399
5417
  * window to float samples (D455).
@@ -5510,7 +5528,7 @@ function isKeychainStoredField(field) {
5510
5528
  var SECRET_CLEAR = { $secretClear: true };
5511
5529
  /** The `'***'` token D634 redaction writes for non-admins. */
5512
5530
  var LEGACY_DEVICE_REDACTION = "***";
5513
- function isRecord$2(value) {
5531
+ function isRecord$5(value) {
5514
5532
  return typeof value === "object" && value !== null && !Array.isArray(value);
5515
5533
  }
5516
5534
  function makeSecretMarker(ref) {
@@ -5521,12 +5539,12 @@ function makeSecretMarker(ref) {
5521
5539
  }
5522
5540
  /** A stored marker: an object whose `$secret` is a non-empty string. */
5523
5541
  function isSecretMarker(value) {
5524
- if (!isRecord$2(value)) return false;
5542
+ if (!isRecord$5(value)) return false;
5525
5543
  const ref = value["$secret"];
5526
5544
  return typeof ref === "string" && ref.length > 0;
5527
5545
  }
5528
5546
  function isSecretClear(value) {
5529
- return isRecord$2(value) && value["$secretClear"] === true;
5547
+ return isRecord$5(value) && value["$secretClear"] === true;
5530
5548
  }
5531
5549
  /**
5532
5550
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -6136,7 +6154,7 @@ var KeychainWriteRefusedError = class extends Error {
6136
6154
  }
6137
6155
  };
6138
6156
  var KEEP = { kind: "keep" };
6139
- function isRecord$1(value) {
6157
+ function isRecord$4(value) {
6140
6158
  return typeof value === "object" && value !== null && !Array.isArray(value);
6141
6159
  }
6142
6160
  function defaultMintId() {
@@ -6149,7 +6167,7 @@ function collectSecretMarkerRefs(value, out = []) {
6149
6167
  return out;
6150
6168
  }
6151
6169
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6152
- else if (isRecord$1(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6170
+ else if (isRecord$4(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6153
6171
  return out;
6154
6172
  }
6155
6173
  async function storedValueEquals(secrets, marker, candidate) {
@@ -6204,7 +6222,7 @@ function rowId(row, idKey) {
6204
6222
  return null;
6205
6223
  }
6206
6224
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
6207
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord$1) : [];
6225
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$4) : [];
6208
6226
  if (!Array.isArray(incoming)) {
6209
6227
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6210
6228
  return {
@@ -6220,7 +6238,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6220
6238
  const kept = /* @__PURE__ */ new Set();
6221
6239
  const rows = [];
6222
6240
  for (const row of incoming) {
6223
- if (!isRecord$1(row)) {
6241
+ if (!isRecord$4(row)) {
6224
6242
  rows.push(row);
6225
6243
  continue;
6226
6244
  }
@@ -6245,7 +6263,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6245
6263
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
6246
6264
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
6247
6265
  case "object": {
6248
- if (!isRecord$1(incoming)) {
6266
+ if (!isRecord$4(incoming)) {
6249
6267
  if (incoming === void 0) return KEEP;
6250
6268
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
6251
6269
  return {
@@ -6253,7 +6271,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6253
6271
  value: incoming
6254
6272
  };
6255
6273
  }
6256
- const storedObj = isRecord$1(stored) ? stored : {};
6274
+ const storedObj = isRecord$4(stored) ? stored : {};
6257
6275
  return {
6258
6276
  kind: "value",
6259
6277
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -6376,7 +6394,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
6376
6394
  value: items
6377
6395
  };
6378
6396
  }
6379
- if (isRecord$1(v)) {
6397
+ if (isRecord$4(v)) {
6380
6398
  const out = {};
6381
6399
  for (const [k, child] of Object.entries(v)) {
6382
6400
  const s = substitute(child);
@@ -6644,7 +6662,6 @@ function createHandle(raw, opts) {
6644
6662
  const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
6645
6663
  const readAddonStoreResult = raw.readAddonStoreResult;
6646
6664
  const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
6647
- const readDeviceStoreBatch = raw.readDeviceStoreBatch;
6648
6665
  const overrides = {
6649
6666
  readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
6650
6667
  ...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
@@ -6668,12 +6685,12 @@ function createHandle(raw, opts) {
6668
6685
  };
6669
6686
  } } : {},
6670
6687
  readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
6671
- ...readDeviceStoreBatch !== void 0 ? { readDeviceStoreBatch: async (deviceIds) => {
6672
- const rows = await readDeviceStoreBatch.call(raw, deviceIds);
6688
+ readDeviceStoreBatch: async (deviceIds) => {
6689
+ const rows = await raw.readDeviceStoreBatch(deviceIds);
6673
6690
  const out = /* @__PURE__ */ new Map();
6674
6691
  for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
6675
6692
  return out;
6676
- } } : {},
6693
+ },
6677
6694
  writeAddonStore: async (patch) => {
6678
6695
  await writeAddon(patch);
6679
6696
  },
@@ -7300,7 +7317,7 @@ var BaseAddon = class {
7300
7317
  if (!ctx) return;
7301
7318
  const addonId = ctx.id;
7302
7319
  setImmediate(() => {
7303
- ctx.api.addons?.restartAddon?.mutate({ addonId }).then(() => {
7320
+ ctx.api.addons.restartAddon.mutate({ addonId }).then(() => {
7304
7321
  ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: patchKeys.filter((k) => restartKeys.has(k)) } });
7305
7322
  }).catch((err) => {
7306
7323
  ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -7500,7 +7517,7 @@ var BaseAddon = class {
7500
7517
  return this._addonSecretTree;
7501
7518
  }
7502
7519
  deviceSecretTree() {
7503
- this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
7520
+ this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
7504
7521
  return this._deviceSecretTree;
7505
7522
  }
7506
7523
  /**
@@ -7576,42 +7593,135 @@ var BaseAddon = class {
7576
7593
  this[timerOf] = timer;
7577
7594
  }
7578
7595
  /**
7579
- * Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
7580
- * transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
7581
- * swap, or any race where the SqliteSettingsBackend is between shutdown and
7582
- * re-initialize) doesn't propagate up into `initialize()` and leave the
7596
+ * Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
7597
+ * settings-store outage doesn't propagate up into `initialize()` and leave the
7583
7598
  * addon permanently broken.
7584
7599
  *
7585
- * Retry only the two known infra fingerprints. Anything else propagates so
7586
- * real bugs surface immediately. After the budget expires we fall back to
7587
- * `{}` (defaults) — the addon's first successful patch will rehydrate.
7600
+ * Two kinds of outage, two budgets:
7601
+ * - the store's own restart fingerprints (`SqliteSettingsBackend not
7602
+ * initialized`, `provider not available`): a short budget, then the addon
7603
+ * falls back to `{}` (defaults) - its first successful patch rehydrates;
7604
+ * - a TRANSPORT failure (the hub is restarting while this runner boots:
7605
+ * `discovery deadline exceeded`, `parent could not route`, a refused or
7606
+ * timed-out connection). Since D870 a runner's settings read THROWS instead
7607
+ * of answering `{}`, so without a budget a hub restart ends in a respawn
7608
+ * loop. A long, bounded budget; only after it the error propagates - the
7609
+ * addon never runs on defaults for a store it could not ask.
7610
+ * Anything else propagates immediately so real bugs surface.
7588
7611
  */
7589
7612
  async readAddonStoreWithRetry() {
7590
7613
  const settings = this.rawSettings();
7591
7614
  if (!settings) return {};
7592
- const delaysMs = [
7593
- 150,
7594
- 350,
7595
- 600,
7596
- 900
7597
- ];
7598
7615
  let lastErr;
7599
- for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7616
+ for (let attempt = 0;; attempt++) try {
7600
7617
  const stored = await settings.readAddonStore() ?? {};
7601
7618
  this.settingsStoreReady = true;
7602
7619
  return stored;
7603
7620
  } catch (err) {
7604
7621
  lastErr = err;
7605
- const msg = err instanceof Error ? err.message : String(err);
7606
- if (!(msg.includes("SqliteSettingsBackend not initialized") || msg.includes("provider not available"))) throw err;
7607
- if (attempt === delaysMs.length) break;
7608
- await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7622
+ const kind = classifyStoreError(err);
7623
+ if (kind === "permanent") throw err;
7624
+ const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
7625
+ if (delay === void 0) {
7626
+ if (kind === "transport") throw err;
7627
+ break;
7628
+ }
7629
+ await new Promise((r) => setTimeout(r, delay));
7609
7630
  }
7610
7631
  this.settingsStoreReady = false;
7611
7632
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7612
7633
  return {};
7613
7634
  }
7614
7635
  };
7636
+ /** The settings store's own restart fingerprints. */
7637
+ var RESTART_RETRY_DELAYS_MS = [
7638
+ 150,
7639
+ 350,
7640
+ 600,
7641
+ 900
7642
+ ];
7643
+ /** A hub restarting under a booting runner: ~100 s in total. */
7644
+ var TRANSPORT_RETRY_DELAYS_MS = [
7645
+ 150,
7646
+ 350,
7647
+ 600,
7648
+ 900,
7649
+ 2e3,
7650
+ 5e3,
7651
+ 1e4,
7652
+ 2e4,
7653
+ 3e4,
7654
+ 3e4
7655
+ ];
7656
+ var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
7657
+ /** Node / socket error codes of a connection that did not happen or broke. */
7658
+ var TRANSPORT_CODES = new Set([
7659
+ "ECONNREFUSED",
7660
+ "ECONNRESET",
7661
+ "ETIMEDOUT",
7662
+ "EPIPE",
7663
+ "EHOSTUNREACH",
7664
+ "ENETUNREACH"
7665
+ ]);
7666
+ /** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
7667
+ var UNAVAILABLE_TRPC_CODES = new Set([
7668
+ "TIMEOUT",
7669
+ "SERVICE_UNAVAILABLE",
7670
+ "BAD_GATEWAY",
7671
+ "GATEWAY_TIMEOUT"
7672
+ ]);
7673
+ /** The exact phrases OUR transport raises when it cannot reach the hub. */
7674
+ var TRANSPORT_PHRASES = [
7675
+ "discovery deadline exceeded",
7676
+ "parent could not route child unowned cap call",
7677
+ "UDS request timed out after",
7678
+ "channel closed"
7679
+ ];
7680
+ /** Moleculer error `type`s of a call that could not be served right now. */
7681
+ var MOLECULER_TRANSIENT_TYPES = new Set([
7682
+ "REQUEST_TIMEOUT",
7683
+ "SERVICE_NOT_FOUND",
7684
+ "SERVICE_NOT_AVAILABLE",
7685
+ "REQUEST_REJECTED",
7686
+ "BROKER_DISCONNECTED"
7687
+ ]);
7688
+ function stringProp(value, key) {
7689
+ if (typeof value !== "object" || value === null) return void 0;
7690
+ const found = Reflect.get(value, key);
7691
+ return typeof found === "string" ? found : void 0;
7692
+ }
7693
+ /** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
7694
+ function errorCode(err) {
7695
+ const out = {};
7696
+ const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
7697
+ const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
7698
+ const node = stringProp(err, "code") ?? stringProp(cause, "code");
7699
+ if (node !== void 0) out.node = node;
7700
+ const trpc = stringProp(data, "code");
7701
+ if (trpc !== void 0) out.trpc = trpc;
7702
+ return out;
7703
+ }
7704
+ /**
7705
+ * Is a settings read failure worth retrying, and with which budget? Only
7706
+ * EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
7707
+ * one of the exact phrases our own transport raises. A message that merely
7708
+ * contains "timed out" or "unreachable" is not evidence of anything, and
7709
+ * `settings store not wired` is a wiring bug that retrying cannot fix.
7710
+ */
7711
+ function classifyStoreError(err) {
7712
+ const message = err instanceof Error ? err.message : String(err);
7713
+ if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
7714
+ const code = errorCode(err);
7715
+ if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
7716
+ if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
7717
+ if (typeof err === "object" && err !== null) {
7718
+ if (Reflect.get(err, "retryable") === true) return "transport";
7719
+ const type = stringProp(err, "type");
7720
+ if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
7721
+ }
7722
+ if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
7723
+ return "permanent";
7724
+ }
7615
7725
  /**
7616
7726
  * Collect the keys of every field marked `perNode: true`, recursing into
7617
7727
  * layout containers (`group` fields and `sub-tabs` tabs) the same way
@@ -7701,6 +7811,89 @@ var PeerBytesTicketSchema = object({
7701
7811
  */
7702
7812
  declaredBytes: number().int().nonnegative().nullable()
7703
7813
  });
7814
+ /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
7815
+ /** The files a kind may live in. `history` is device-history's (D850), never records. */
7816
+ var RECORD_DATABASES = ["settings", "events"];
7817
+ /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
7818
+ var RECORD_CLASSES = [
7819
+ "config",
7820
+ "ledger",
7821
+ "audit",
7822
+ "mirror",
7823
+ "ephemeral"
7824
+ ];
7825
+ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
7826
+ var RECORD_MAX_DATA_BYTES = 1024 * 1024;
7827
+ var RECORD_BATCH_MAX = 1e3;
7828
+ var RECORD_QUERY_MAX = 1e3;
7829
+ /**
7830
+ * Record kinds (D881): the declaration an addon makes of one kind of fact, the
7831
+ * Zod row schema that describes it, and the helper that turns the two into the
7832
+ * JSON the engine stores.
7833
+ *
7834
+ * A kind is declared ONCE, next to the code that owns it, and the engine's
7835
+ * reconciler creates its indexes - nothing creates DDL at runtime from a write.
7836
+ * Everything the generic machinery needs (secret fields, the entry descriptor)
7837
+ * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
7838
+ * never a field name (D879).
7839
+ */
7840
+ /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
7841
+ var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
7842
+ var RecordIndexSchema = object({
7843
+ name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
7844
+ fields: array(RecordFieldPathSchema).min(1).max(4),
7845
+ unique: boolean().optional()
7846
+ }).strict();
7847
+ var RecordRetentionSchema = union([
7848
+ object({
7849
+ ttlMs: number().int().positive(),
7850
+ from: _enum(["at", "write"])
7851
+ }).strict(),
7852
+ object({
7853
+ maxRows: number().int().positive().optional(),
7854
+ maxRowsPerDevice: number().int().positive().optional()
7855
+ }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
7856
+ object({
7857
+ owner: string().min(1),
7858
+ reason: string().min(1)
7859
+ }).strict(),
7860
+ object({ none: string().min(1) }).strict()
7861
+ ]);
7862
+ /** The descriptor a generic entries UI reads (D881 "generic entries"). */
7863
+ var EntryDescriptorSchema = object({
7864
+ title: string().min(1),
7865
+ /** The record body field shown as the entry's name. */
7866
+ nameField: string().min(1),
7867
+ /** JSON Schema of the entry body; `secret: true` marks a password field. */
7868
+ fields: record(string(), unknown())
7869
+ }).strict();
7870
+ /** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
7871
+ var RecordKindDeclarationSchema = object({
7872
+ database: _enum(RECORD_DATABASES).default("settings"),
7873
+ class: _enum(RECORD_CLASSES),
7874
+ writeTrigger: string().min(1),
7875
+ device: _enum([
7876
+ "required",
7877
+ "optional",
7878
+ "none"
7879
+ ]).default("none"),
7880
+ at: _enum(["required", "none"]).default("none"),
7881
+ indexes: array(RecordIndexSchema).max(8).default([]),
7882
+ retention: RecordRetentionSchema,
7883
+ deviceRemoval: _enum(["delete", "none"]).default("none"),
7884
+ maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
7885
+ /** Derived from the Zod schema; never written by hand. */
7886
+ secretPaths: array(string().min(1)).default([]),
7887
+ /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
7888
+ secretArrayKeys: record(string(), string()).default({}),
7889
+ /** The legacy table this kind adopts from; the boot guard reads it (D853: no migration code). */
7890
+ adoptsFrom: string().min(1).optional(),
7891
+ /** Kinds that must agree through `transact` sit in one file under one family name. */
7892
+ transactFamily: string().min(1).optional(),
7893
+ entry: EntryDescriptorSchema.optional()
7894
+ }).strict();
7895
+ /** The kind names: lower-case, dash separated, short enough for an index name. */
7896
+ var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7704
7897
  /** Shared Zod schemas used across streaming capabilities. */
7705
7898
  var CamProfileSchema = _enum([
7706
7899
  "high",
@@ -8188,7 +8381,7 @@ object({
8188
8381
  params: record(string(), union([string(), number()])).optional()
8189
8382
  });
8190
8383
  //#endregion
8191
- //#region ../types/dist/err-msg-DLerKiVO.mjs
8384
+ //#region ../types/dist/err-msg--2RrYiLO.mjs
8192
8385
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
8193
8386
  DeviceType["Camera"] = "camera";
8194
8387
  DeviceType["Hub"] = "hub";
@@ -8457,6 +8650,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
8457
8650
  /** Virtual HA toggle (input_boolean.*) — distinguishable from a
8458
8651
  * real Switch device for UI rendering / export adapters. */
8459
8652
  DeviceRole["BinaryHelper"] = "binary-helper";
8653
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
8654
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
8655
+ DeviceRole["Outlet"] = "outlet";
8460
8656
  /** Generic motion / occupancy / moving event source. Distinct from
8461
8657
  * the camera accessory PirSensor role: that one is a camera child;
8462
8658
  * this is a standalone HA / 3rd-party motion sensor. */
@@ -8534,7 +8730,7 @@ function errMsg(err) {
8534
8730
  return String(err);
8535
8731
  }
8536
8732
  //#endregion
8537
- //#region ../types/dist/composition-EQ9Ok0Ee.mjs
8733
+ //#region ../types/dist/composition-DOwQB_Ua.mjs
8538
8734
  /**
8539
8735
  * The ffmpeg build every CamStack node downloads, per platform and arch.
8540
8736
  *
@@ -11995,11 +12191,10 @@ var ClipModelMetaSchema = object({
11995
12191
  /**
11996
12192
  * The `vector-store` index this model's vectors live in. ONE feature space
11997
12193
  * per index: two models share an index only when their vectors are ranked
11998
- * against each other, which for CLIP means never across families. MobileCLIP
11999
- * S1 and S2 share `object-clip` for history (separated by the row's
12000
- * `modelId`); any other model gets its own.
12194
+ * against each other, which for CLIP means never across families. Every
12195
+ * model gets its own.
12001
12196
  */
12002
- vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
12197
+ vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
12003
12198
  /**
12004
12199
  * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
12005
12200
  * must be unique per tokenizer: every model's siblings land in ONE flat
@@ -15652,6 +15847,41 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
15652
15847
  keyField: string()
15653
15848
  }).optional()
15654
15849
  })) });
15850
+ /**
15851
+ * The report of a one-shot "move this owner's plaintext secrets into the
15852
+ * keychain" run (D878). COUNTS only - never a value, never a ref.
15853
+ *
15854
+ * The owner runs it on its own rows, through its own write path; an operator
15855
+ * script (`scripts/hub/migrate-raw-secrets.ts`) calls the owner's cap method.
15856
+ * Dry run (`apply: false`) reports what WOULD move and changes nothing.
15857
+ */
15858
+ var RawSecretMigrationInputSchema = object({
15859
+ /** `false` (the default of the script) = report only. */
15860
+ apply: boolean() });
15861
+ var RawSecretMigrationReportSchema = object({
15862
+ apply: boolean(),
15863
+ /** Rows that can carry a secret (a profile, an enrolment). */
15864
+ rows: number().int(),
15865
+ /** Rows whose secret was plaintext when the run started. */
15866
+ raw: number().int(),
15867
+ /** Rows that already held a keychain marker when the run started. */
15868
+ markers: number().int(),
15869
+ /** Plaintext rows moved by THIS run (always 0 on a dry run). */
15870
+ migrated: number().int(),
15871
+ /** Plaintext rows left in place because the move or its read-back failed. */
15872
+ failed: number().int(),
15873
+ /** After the run: rows still holding plaintext. */
15874
+ remainingRaw: number().int(),
15875
+ /** After the run: markers the keychain could not resolve right now. */
15876
+ unresolvedMarkers: number().int(),
15877
+ /**
15878
+ * A second, redundant copy of the secrets' rows that the run found (and, on
15879
+ * `apply`, deleted after verifying the first). Owners with a single copy omit it.
15880
+ */
15881
+ redundantCopies: number().int().optional(),
15882
+ /** True when nothing is raw and every marker resolves. */
15883
+ verified: boolean()
15884
+ });
15655
15885
  /** Where a block runs. The operator chooses — a block driving a device on an
15656
15886
  * agent is the reason placement is not fixed to the hub. */
15657
15887
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
@@ -15878,6 +16108,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
15878
16108
  }), SettingsUpdateResultSchema, {
15879
16109
  kind: "mutation",
15880
16110
  auth: "admin"
16111
+ }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
16112
+ kind: "mutation",
16113
+ auth: "admin"
15881
16114
  }), method(object({}), object({ libs: array(object({
15882
16115
  filePath: string(),
15883
16116
  content: string()
@@ -16141,6 +16374,347 @@ id: union([string(), number()]) }), {
16141
16374
  auth: "admin"
16142
16375
  });
16143
16376
  /**
16377
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
16378
+ * config documents, mirrors. The third storage API next to `ctx.settings`
16379
+ * (operator intent, D870) and device-history (numbers over time, D850).
16380
+ *
16381
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
16382
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
16383
+ * that with the addon id closed over). It is deliberately NOT in any input
16384
+ * schema, so a session can never supply it, and a call with no stamp is refused
16385
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
16386
+ * addons go through the owner's capability.
16387
+ *
16388
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
16389
+ * never `[]`. Secret fields are never on the wire: a read shows the
16390
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
16391
+ * resolves itself, and a write may carry only a keychain marker bound to the
16392
+ * row's own ref.
16393
+ *
16394
+ * Internal: addons reach it through `ctx.records`, never directly.
16395
+ */
16396
+ var RecordIdSchema = string().min(1).max(256);
16397
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
16398
+ var RecordBodySchema = record(string(), unknown());
16399
+ var RecordInputSchema = object({
16400
+ id: RecordIdSchema,
16401
+ /** The device this row belongs to (D865 removal, per-device reads). */
16402
+ deviceId: number().int().optional(),
16403
+ /** The domain clock, ms: observed / created / opened. */
16404
+ at: number().int().optional(),
16405
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
16406
+ expiresAt: number().int().optional(),
16407
+ data: RecordBodySchema
16408
+ });
16409
+ var RecordViewSchema = object({
16410
+ id: string(),
16411
+ deviceId: number().int().nullable(),
16412
+ at: number().nullable(),
16413
+ expiresAt: number().nullable(),
16414
+ rev: number().int(),
16415
+ updatedAt: number(),
16416
+ data: RecordBodySchema
16417
+ });
16418
+ /** Declared index fields only. Anything else does not compile, by design. */
16419
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
16420
+ string(),
16421
+ number(),
16422
+ boolean(),
16423
+ object({ in: array(union([
16424
+ string(),
16425
+ number(),
16426
+ boolean()
16427
+ ])).min(1).max(RECORD_BATCH_MAX) }),
16428
+ object({
16429
+ gt: union([string(), number()]).optional(),
16430
+ gte: union([string(), number()]).optional(),
16431
+ lt: union([string(), number()]).optional(),
16432
+ lte: union([string(), number()]).optional()
16433
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
16434
+ ]));
16435
+ var KindShape = { kind: RecordKindNameSchema };
16436
+ var TimeWindowShape = {
16437
+ deviceId: number().int().optional(),
16438
+ atFrom: number().optional(),
16439
+ atTo: number().optional()
16440
+ };
16441
+ var RecordQueryInputSchema = object({
16442
+ ...KindShape,
16443
+ ...TimeWindowShape,
16444
+ where: RecordWhereSchema.optional(),
16445
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
16446
+ order: _enum(["at", "id"]).optional(),
16447
+ desc: boolean().optional(),
16448
+ limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
16449
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
16450
+ cursor: string().min(1).optional()
16451
+ });
16452
+ var RecordQueryResultSchema = object({
16453
+ records: array(RecordViewSchema),
16454
+ nextCursor: string().nullable()
16455
+ });
16456
+ var RecordKindStateSchema = _enum([
16457
+ "created",
16458
+ "unchanged",
16459
+ "updated"
16460
+ ]);
16461
+ var DeclareKindResultSchema = object({
16462
+ kindId: number().int(),
16463
+ state: RecordKindStateSchema,
16464
+ createdIndexes: array(string()),
16465
+ /** The boot guard: the kind is empty while its legacy table still holds rows. */
16466
+ adoptionPending: object({
16467
+ legacyTable: string(),
16468
+ legacyRows: number().int()
16469
+ }).nullable()
16470
+ });
16471
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
16472
+ var SecretRefsList = array(string());
16473
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
16474
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
16475
+ kind: "mutation",
16476
+ auth: "admin"
16477
+ }), method(object({
16478
+ ...KindShape,
16479
+ id: RecordIdSchema
16480
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
16481
+ ...KindShape,
16482
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
16483
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
16484
+ ...KindShape,
16485
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
16486
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
16487
+ ...KindShape,
16488
+ record: RecordInputSchema,
16489
+ /** Compare-and-set. `0` means "the row must not exist". */
16490
+ ifRev: number().int().min(0).optional()
16491
+ }), object({
16492
+ rev: number().int(),
16493
+ releasedRefs: SecretRefsList
16494
+ }), {
16495
+ kind: "mutation",
16496
+ auth: "admin"
16497
+ }), method(object({
16498
+ ...KindShape,
16499
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
16500
+ }), object({
16501
+ count: number().int(),
16502
+ releasedRefs: SecretRefsList
16503
+ }), {
16504
+ kind: "mutation",
16505
+ auth: "admin"
16506
+ }), method(object({
16507
+ ...KindShape,
16508
+ id: RecordIdSchema,
16509
+ data: RecordBodySchema,
16510
+ at: number().int().nullable().optional(),
16511
+ expiresAt: number().int().nullable().optional(),
16512
+ ifRev: number().int().min(1).optional()
16513
+ }), object({
16514
+ rev: number().int(),
16515
+ releasedRefs: SecretRefsList
16516
+ }), {
16517
+ kind: "mutation",
16518
+ auth: "admin"
16519
+ }), method(object({
16520
+ ...KindShape,
16521
+ id: RecordIdSchema
16522
+ }), object({
16523
+ deleted: boolean(),
16524
+ secretRefs: SecretRefsList
16525
+ }), {
16526
+ kind: "mutation",
16527
+ auth: "admin"
16528
+ }), method(object({
16529
+ ...KindShape,
16530
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
16531
+ }), object({
16532
+ deleted: number().int(),
16533
+ secretRefs: SecretRefsList
16534
+ }), {
16535
+ kind: "mutation",
16536
+ auth: "admin"
16537
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
16538
+ ...KindShape,
16539
+ ...TimeWindowShape,
16540
+ where: RecordWhereSchema.optional()
16541
+ }), object({ count: number().int() }), { auth: "admin" }), method(object({
16542
+ ...KindShape,
16543
+ deviceId: number().int().optional(),
16544
+ from: number(),
16545
+ to: number(),
16546
+ bucketMs: number().int().positive()
16547
+ }), object({ buckets: array(object({
16548
+ bucketStart: number(),
16549
+ count: number().int()
16550
+ })) }), { auth: "admin" }), method(object({
16551
+ ...KindShape,
16552
+ op: _enum([
16553
+ "sum",
16554
+ "min",
16555
+ "max",
16556
+ "count"
16557
+ ]),
16558
+ field: RecordFieldPathSchema.optional(),
16559
+ groupBy: RecordFieldPathSchema.optional(),
16560
+ where: RecordWhereSchema.optional()
16561
+ }), object({ rows: array(object({
16562
+ group: union([
16563
+ string(),
16564
+ number(),
16565
+ _null()
16566
+ ]),
16567
+ value: number().nullable()
16568
+ })) }), { auth: "admin" }), method(object({
16569
+ ...KindShape,
16570
+ deviceId: number().int().optional(),
16571
+ atBefore: number().optional(),
16572
+ where: RecordWhereSchema.optional()
16573
+ }), object({
16574
+ deleted: number().int(),
16575
+ secretRefs: SecretRefsList
16576
+ }), {
16577
+ kind: "mutation",
16578
+ auth: "admin"
16579
+ }), method(object({
16580
+ deviceId: number().int(),
16581
+ kinds: array(RecordKindNameSchema).optional()
16582
+ }), object({
16583
+ deleted: record(string(), number().int()),
16584
+ secretRefs: SecretRefsList
16585
+ }), {
16586
+ kind: "mutation",
16587
+ auth: "admin"
16588
+ }), method(object(KindShape), object({ id: number().int() }), {
16589
+ kind: "mutation",
16590
+ auth: "admin"
16591
+ });
16592
+ /** One table of the settings database. */
16593
+ var StorageTableSchema = object({
16594
+ /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
16595
+ name: string(),
16596
+ /** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
16597
+ owner: string().nullable(),
16598
+ rows: number().int().nonnegative(),
16599
+ /** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
16600
+ bytes: number().int().nonnegative().nullable(),
16601
+ /** Declared by an addon since this engine process started (a running addon claims it). */
16602
+ declaredThisBoot: boolean()
16603
+ });
16604
+ /** One declared record kind (D881) as System -> Data shows it. */
16605
+ var StorageKindSchema = object({
16606
+ owner: string(),
16607
+ kind: string(),
16608
+ database: _enum(["settings", "events"]),
16609
+ class: string(),
16610
+ state: _enum([
16611
+ "declared",
16612
+ "orphaned",
16613
+ "dropped"
16614
+ ]),
16615
+ /** `null` when the caller asked for no counts (`counts: false`). */
16616
+ rows: number().int().nonnegative().nullable(),
16617
+ /** Rows x the average size of a sample; null for an empty kind. */
16618
+ bytesEstimate: number().int().nonnegative().nullable(),
16619
+ writeTrigger: string(),
16620
+ retention: RecordRetentionSchema,
16621
+ /** `true` when the kind is an entry kind (the generic entries UI lists it). */
16622
+ entry: boolean(),
16623
+ /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
16624
+ declaration: RecordKindDeclarationSchema
16625
+ });
16626
+ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
16627
+ collection: string().min(1),
16628
+ expectRows: number().int().nonnegative()
16629
+ }), object({
16630
+ dropped: literal(true),
16631
+ rows: number().int().nonnegative()
16632
+ }), {
16633
+ kind: "mutation",
16634
+ auth: "admin"
16635
+ }), method(object({
16636
+ owner: string().min(1).optional(),
16637
+ /** `false` skips the per-kind row count (a catalog read only). Default true. */
16638
+ counts: boolean().optional()
16639
+ }), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
16640
+ owner: string().min(1),
16641
+ kind: RecordKindNameSchema
16642
+ }), RecordQueryResultSchema, { auth: "admin" }), method(object({
16643
+ owner: string().min(1),
16644
+ kind: RecordKindNameSchema,
16645
+ records: array(RecordInputSchema).max(RECORD_BATCH_MAX),
16646
+ /** The LAST batch: the copy is complete, the boot guard stops asking. */
16647
+ complete: boolean().optional()
16648
+ }).refine((input) => input.records.length > 0 || input.complete === true, { message: "records must be non-empty unless complete is true" }), object({ adopted: number().int() }), {
16649
+ kind: "mutation",
16650
+ auth: "admin"
16651
+ }), method(object({
16652
+ collection: string().min(1),
16653
+ cursor: string().optional(),
16654
+ limit: number().int().min(1).max(1e3).optional()
16655
+ }), object({
16656
+ rows: array(object({
16657
+ id: string(),
16658
+ data: record(string(), unknown())
16659
+ })),
16660
+ nextCursor: string().nullable()
16661
+ }), { auth: "admin" }), method(object({ collection: string().min(1) }), object({ adopted: boolean() }), { auth: "admin" });
16662
+ /**
16663
+ * record-entries - the ONE admin surface for every list of user-created entries
16664
+ * (D881): notification targets, brokers, LLM profiles, anything an addon
16665
+ * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
16666
+ *
16667
+ * The UI is generic. `listEntryKinds` hands it, per kind, the JSON Schema of
16668
+ * the entry body with `secret: true` on every credential field, so a password
16669
+ * field is derived from the schema and never from a name. `putEntry` takes the
16670
+ * redaction token for an untouched secret (the stored value is kept), a string
16671
+ * for a new one, and the clear instruction to remove one. A read never carries
16672
+ * a secret value, and a locked keychain refuses the write with
16673
+ * `records/secret-unavailable` instead of storing plaintext.
16674
+ *
16675
+ * Hub-only, served in-process (it acts for each entry's owner on the keychain).
16676
+ */
16677
+ var EntryKindInfoSchema = object({
16678
+ owner: string(),
16679
+ kind: RecordKindNameSchema,
16680
+ title: string(),
16681
+ nameField: string(),
16682
+ /** JSON Schema of the body; `secret: true` marks a password field. */
16683
+ fields: record(string(), unknown()),
16684
+ secretPaths: array(string()),
16685
+ rows: number().int().nonnegative()
16686
+ });
16687
+ var EntryViewSchema = object({
16688
+ id: RecordIdSchema,
16689
+ name: string(),
16690
+ rev: number().int(),
16691
+ updatedAt: number(),
16692
+ /** Secret fields hold the redaction token. */
16693
+ data: RecordBodySchema
16694
+ });
16695
+ var EntryAddress = {
16696
+ owner: string().min(1),
16697
+ kind: RecordKindNameSchema
16698
+ };
16699
+ method(object({}), object({ kinds: array(EntryKindInfoSchema) }), { auth: "admin" }), method(object({ ...EntryAddress }), object({ entries: array(EntryViewSchema) }), { auth: "admin" }), method(object({
16700
+ ...EntryAddress,
16701
+ id: RecordIdSchema.optional(),
16702
+ data: RecordBodySchema,
16703
+ ifRev: number().int().min(1).optional()
16704
+ }), object({
16705
+ id: string(),
16706
+ rev: number().int()
16707
+ }), {
16708
+ kind: "mutation",
16709
+ auth: "admin"
16710
+ }), method(object({
16711
+ ...EntryAddress,
16712
+ id: RecordIdSchema
16713
+ }), object({ deleted: boolean() }), {
16714
+ kind: "mutation",
16715
+ auth: "admin"
16716
+ });
16717
+ /**
16144
16718
  * `keychain` — the hub's encrypted store of secret values (D787).
16145
16719
  *
16146
16720
  * Settings blobs never hold a secret: they hold a marker
@@ -16681,6 +17255,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
16681
17255
  kind: "mutation",
16682
17256
  auth: "admin",
16683
17257
  caller: "required"
17258
+ }), method(object({
17259
+ ref: string().min(1),
17260
+ value: string().min(1)
17261
+ }), object({
17262
+ ref: string(),
17263
+ version: number().int()
17264
+ }), {
17265
+ kind: "mutation",
17266
+ auth: "admin",
17267
+ caller: "required"
16684
17268
  }), method(object({ refs: array(string().min(1)).max(1e3) }), object({
16685
17269
  deleted: array(string()).readonly(),
16686
17270
  kept: array(object({
@@ -39175,6 +39759,9 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
39175
39759
  }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
39176
39760
  kind: "mutation",
39177
39761
  auth: "admin"
39762
+ }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
39763
+ kind: "mutation",
39764
+ auth: "admin"
39178
39765
  }), method(ProfileRefInputSchema, _void(), {
39179
39766
  kind: "mutation",
39180
39767
  auth: "admin"
@@ -49614,6 +50201,41 @@ var ServerRollbackInfoSchema = object({
49614
50201
  atMs: number(),
49615
50202
  reason: string()
49616
50203
  });
50204
+ /**
50205
+ * The runner boot check (D877): the runners this node PLANNED (hub: one per
50206
+ * `resolveRunnerId` over its forked addons; agent: the runners its process
50207
+ * service holds, because only the hub plans) against the states the process
50208
+ * service reports. Kept in memory, recomputed by the same one-shot check after
50209
+ * every boot, served on `getServerPackageStatus`.
50210
+ *
50211
+ * `failed` is the D6 breaker's terminal state, `refused` a deterministic Init
50212
+ * refusal (an addon that said "I will not start, and here is why"), `missing` a
50213
+ * planned runner the process service has no entry for.
50214
+ */
50215
+ var RunnerRefusalSchema = object({
50216
+ runnerId: string(),
50217
+ addonIds: array(string()),
50218
+ reason: string()
50219
+ });
50220
+ var RunnerBootCheckSchema = object({
50221
+ checkedAtMs: number(),
50222
+ /** Runners planned for this node. */
50223
+ expected: number(),
50224
+ /** Planned runners whose process is `running`. */
50225
+ running: number(),
50226
+ failed: array(string()),
50227
+ refused: array(RunnerRefusalSchema),
50228
+ missing: array(string()),
50229
+ /**
50230
+ * Runners still on their way: `starting`, or `crashed` waiting on a backoff
50231
+ * respawn. The check is FINAL only when this is empty; until then nothing is
50232
+ * pushed and the verdict is re-evaluated on the next runner state change.
50233
+ * Optional for version skew (absent = settled).
50234
+ */
50235
+ pending: array(string()).optional(),
50236
+ /** True when the plan is the hub's (`missing` is then meaningful); false on an agent. */
50237
+ planKnown: boolean()
50238
+ });
49617
50239
  var ServerPackageStatusSchema = object({
49618
50240
  /** Root package name (`@camstack/server` on the hub). */
49619
50241
  packageName: string(),
@@ -49676,7 +50298,14 @@ var ServerPackageStatusSchema = object({
49676
50298
  * for version skew: an older provider's payload simply omits it, and an
49677
50299
  * absent wrapper is unknown — never `native`.
49678
50300
  */
49679
- wrapper: WrapperIdentitySchema.optional()
50301
+ wrapper: WrapperIdentitySchema.optional(),
50302
+ /**
50303
+ * What this node PLANNED to run against what its process service reports
50304
+ * (D877). Absent until the one-shot boot check has run (~60 s after boot) and
50305
+ * on a provider that predates it. Absent is "not checked yet", never "all
50306
+ * well": a consumer that needs the answer treats it as pending.
50307
+ */
50308
+ runners: RunnerBootCheckSchema.optional()
49680
50309
  });
49681
50310
  var ServerUpdateCheckResultSchema = object({
49682
50311
  packageName: string(),
@@ -49727,6 +50356,13 @@ var ServerUpdateOutcomeSchema = _enum([
49727
50356
  "refused",
49728
50357
  "failed"
49729
50358
  ]);
50359
+ /** One addon the pre-flight found the target version would refuse (D877). */
50360
+ var UpdateBlockerSchema = object({
50361
+ addonId: string(),
50362
+ reason: string(),
50363
+ /** What the operator does about it. */
50364
+ remedy: string()
50365
+ });
49730
50366
  var ServerUpdateActionResultSchema = object({
49731
50367
  accepted: boolean(),
49732
50368
  targetVersion: string().nullable(),
@@ -49744,14 +50380,27 @@ var ServerUpdateActionResultSchema = object({
49744
50380
  * The failure itself, verbatim enough to act on (the npm output for a
49745
50381
  * staging install). Non-null only for `failed`.
49746
50382
  */
49747
- error: string().nullable().optional()
50383
+ error: string().nullable().optional(),
50384
+ /**
50385
+ * The addons the target version would refuse to start on this node's live
50386
+ * store (D877), present when the pre-flight refused the update
50387
+ * (`reason: 'addon-would-refuse'`). Optional for version skew.
50388
+ */
50389
+ blockers: array(UpdateBlockerSchema).optional()
49748
50390
  });
49749
50391
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
49750
50392
  kind: "mutation",
49751
50393
  auth: "admin"
49752
- }), method(object({
49753
- /** Explicit target version; omitted = latest from the registry. */
49754
- version: string().optional() }), ServerUpdateActionResultSchema, {
50394
+ }), method(object({
50395
+ /** Explicit target version; omitted = latest from the registry. */
50396
+ version: string().optional(),
50397
+ /**
50398
+ * Apply even though the pre-flight (D877) found addons the target would
50399
+ * refuse to start. Off by default: those addons stay down until the
50400
+ * remedy is run, which is the incident this gate exists to prevent.
50401
+ */
50402
+ acknowledgeRefusals: boolean().optional()
50403
+ }), ServerUpdateActionResultSchema, {
49755
50404
  kind: "mutation",
49756
50405
  auth: "admin",
49757
50406
  timeoutMs: 16 * 6e4
@@ -52164,7 +52813,9 @@ var ManifestShapeSchema = object({
52164
52813
  var AddonProcessSchema = object({
52165
52814
  pid: number().optional(),
52166
52815
  mode: string(),
52167
- state: string()
52816
+ state: string(),
52817
+ /** Why the runner is `refused` (an addon's own words), when it is (D877). */
52818
+ reason: string().optional()
52168
52819
  });
52169
52820
  /**
52170
52821
  * Per-addon health snapshot from the kernel's AddonHealthMonitor.
@@ -53138,7 +53789,7 @@ var HvacModeSchema = _enum([
53138
53789
  "fan_only",
53139
53790
  "dry"
53140
53791
  ]);
53141
- object({
53792
+ var ClimateControlStatusSchema = object({
53142
53793
  /** Active HVAC mode. */
53143
53794
  mode: HvacModeSchema,
53144
53795
  /** Available HVAC modes the device accepts. Subset of HvacMode. */
@@ -53179,6 +53830,12 @@ object({
53179
53830
  /** Horizontal louver swing on/off. Null when the device has no
53180
53831
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
53181
53832
  swingHorizontal: boolean().nullable(),
53833
+ /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
53834
+ minTemp: number().nullable().optional(),
53835
+ /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
53836
+ maxTemp: number().nullable().optional(),
53837
+ /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
53838
+ tempStep: number().positive().nullable().optional(),
53182
53839
  /** Ms epoch when the slice was last updated (push or command). */
53183
53840
  lastFetchedAt: number()
53184
53841
  });
@@ -55019,7 +55676,19 @@ var TlsStatusSchema = object({
55019
55676
  sans: array(string()),
55020
55677
  caCertPem: string().nullable(),
55021
55678
  reissueError: string().nullable(),
55022
- restartRequired: boolean()
55679
+ restartRequired: boolean(),
55680
+ /** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
55681
+ caMissing: boolean(),
55682
+ /** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
55683
+ localCaConstrained: boolean().nullable(),
55684
+ /** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
55685
+ caReplacedAt: string().nullable(),
55686
+ /** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
55687
+ storedUpload: object({
55688
+ leafFingerprintSha256: string(),
55689
+ validTo: string(),
55690
+ hasCa: boolean()
55691
+ }).nullable()
55023
55692
  });
55024
55693
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
55025
55694
  /**
@@ -55075,6 +55744,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
55075
55744
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
55076
55745
  kind: "mutation",
55077
55746
  auth: "admin"
55747
+ }), method(_void(), TlsStatusSchema, {
55748
+ kind: "mutation",
55749
+ auth: "admin"
55078
55750
  });
55079
55751
  var LockControlStatusSchema = object({
55080
55752
  /** Lifecycle state of the lock. `jammed` means the motor reported
@@ -60227,7 +60899,11 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
60227
60899
  kind: "mutation",
60228
60900
  auth: "admin",
60229
60901
  access: "delete"
60230
- }), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(object({
60902
+ }), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
60903
+ kind: "mutation",
60904
+ auth: "admin",
60905
+ access: "create"
60906
+ }), method(object({
60231
60907
  userId: string(),
60232
60908
  code: string()
60233
60909
  }), object({ valid: boolean() }), {
@@ -62428,6 +63104,12 @@ Object.freeze({
62428
63104
  addonId: null,
62429
63105
  access: "view"
62430
63106
  },
63107
+ "coreBlocks.migrateRawSecrets": {
63108
+ capName: "core-blocks",
63109
+ capScope: "system",
63110
+ addonId: null,
63111
+ access: "create"
63112
+ },
62431
63113
  "coreBlocks.restart": {
62432
63114
  capName: "core-blocks",
62433
63115
  capScope: "system",
@@ -64114,6 +64796,12 @@ Object.freeze({
64114
64796
  addonId: null,
64115
64797
  access: "create"
64116
64798
  },
64799
+ "keychainAdmin.importSecret": {
64800
+ capName: "keychain-admin",
64801
+ capScope: "system",
64802
+ addonId: null,
64803
+ access: "create"
64804
+ },
64117
64805
  "keychainAdmin.listAudit": {
64118
64806
  capName: "keychain-admin",
64119
64807
  capScope: "system",
@@ -64282,6 +64970,12 @@ Object.freeze({
64282
64970
  addonId: null,
64283
64971
  access: "view"
64284
64972
  },
64973
+ "llm.migrateRawSecrets": {
64974
+ capName: "llm",
64975
+ capScope: "system",
64976
+ addonId: null,
64977
+ access: "create"
64978
+ },
64285
64979
  "llm.resolveModelRef": {
64286
64980
  capName: "llm",
64287
64981
  capScope: "system",
@@ -64510,6 +65204,12 @@ Object.freeze({
64510
65204
  addonId: null,
64511
65205
  access: "create"
64512
65206
  },
65207
+ "localNetwork.useStoredUploadedCertificate": {
65208
+ capName: "local-network",
65209
+ capScope: "system",
65210
+ addonId: null,
65211
+ access: "create"
65212
+ },
64513
65213
  "lockControl.getStatus": {
64514
65214
  capName: "lock-control",
64515
65215
  capScope: "device",
@@ -67162,6 +67862,30 @@ Object.freeze({
67162
67862
  addonId: null,
67163
67863
  access: "create"
67164
67864
  },
67865
+ "recordEntries.deleteEntry": {
67866
+ capName: "record-entries",
67867
+ capScope: "system",
67868
+ addonId: null,
67869
+ access: "delete"
67870
+ },
67871
+ "recordEntries.listEntries": {
67872
+ capName: "record-entries",
67873
+ capScope: "system",
67874
+ addonId: null,
67875
+ access: "view"
67876
+ },
67877
+ "recordEntries.listEntryKinds": {
67878
+ capName: "record-entries",
67879
+ capScope: "system",
67880
+ addonId: null,
67881
+ access: "view"
67882
+ },
67883
+ "recordEntries.putEntry": {
67884
+ capName: "record-entries",
67885
+ capScope: "system",
67886
+ addonId: null,
67887
+ access: "create"
67888
+ },
67165
67889
  "recording.getAvailability": {
67166
67890
  capName: "recording",
67167
67891
  capScope: "device",
@@ -67462,6 +68186,102 @@ Object.freeze({
67462
68186
  addonId: null,
67463
68187
  access: "view"
67464
68188
  },
68189
+ "records.aggregate": {
68190
+ capName: "records",
68191
+ capScope: "system",
68192
+ addonId: null,
68193
+ access: "view"
68194
+ },
68195
+ "records.allocateId": {
68196
+ capName: "records",
68197
+ capScope: "system",
68198
+ addonId: null,
68199
+ access: "create"
68200
+ },
68201
+ "records.count": {
68202
+ capName: "records",
68203
+ capScope: "system",
68204
+ addonId: null,
68205
+ access: "view"
68206
+ },
68207
+ "records.declareKinds": {
68208
+ capName: "records",
68209
+ capScope: "system",
68210
+ addonId: null,
68211
+ access: "create"
68212
+ },
68213
+ "records.delete": {
68214
+ capName: "records",
68215
+ capScope: "system",
68216
+ addonId: null,
68217
+ access: "delete"
68218
+ },
68219
+ "records.deleteDevice": {
68220
+ capName: "records",
68221
+ capScope: "system",
68222
+ addonId: null,
68223
+ access: "delete"
68224
+ },
68225
+ "records.deleteMany": {
68226
+ capName: "records",
68227
+ capScope: "system",
68228
+ addonId: null,
68229
+ access: "delete"
68230
+ },
68231
+ "records.deleteWhere": {
68232
+ capName: "records",
68233
+ capScope: "system",
68234
+ addonId: null,
68235
+ access: "delete"
68236
+ },
68237
+ "records.get": {
68238
+ capName: "records",
68239
+ capScope: "system",
68240
+ addonId: null,
68241
+ access: "view"
68242
+ },
68243
+ "records.getMany": {
68244
+ capName: "records",
68245
+ capScope: "system",
68246
+ addonId: null,
68247
+ access: "view"
68248
+ },
68249
+ "records.histogram": {
68250
+ capName: "records",
68251
+ capScope: "system",
68252
+ addonId: null,
68253
+ access: "view"
68254
+ },
68255
+ "records.patch": {
68256
+ capName: "records",
68257
+ capScope: "system",
68258
+ addonId: null,
68259
+ access: "create"
68260
+ },
68261
+ "records.put": {
68262
+ capName: "records",
68263
+ capScope: "system",
68264
+ addonId: null,
68265
+ access: "create"
68266
+ },
68267
+ "records.putMany": {
68268
+ capName: "records",
68269
+ capScope: "system",
68270
+ addonId: null,
68271
+ access: "create"
68272
+ },
68273
+ "records.query": {
68274
+ capName: "records",
68275
+ capScope: "system",
68276
+ addonId: null,
68277
+ access: "view"
68278
+ },
68279
+ "records.secretRefs": {
68280
+ capName: "records",
68281
+ capScope: "system",
68282
+ addonId: null,
68283
+ access: "view"
68284
+ },
67465
68285
  "sceneMonitor.captureReference": {
67466
68286
  capName: "scene-monitor",
67467
68287
  capScope: "device",
@@ -67894,6 +68714,48 @@ Object.freeze({
67894
68714
  addonId: null,
67895
68715
  access: "create"
67896
68716
  },
68717
+ "storageAdmin.adoptRecords": {
68718
+ capName: "storage-admin",
68719
+ capScope: "system",
68720
+ addonId: null,
68721
+ access: "create"
68722
+ },
68723
+ "storageAdmin.dropCollection": {
68724
+ capName: "storage-admin",
68725
+ capScope: "system",
68726
+ addonId: null,
68727
+ access: "delete"
68728
+ },
68729
+ "storageAdmin.isAdoptedLegacyTable": {
68730
+ capName: "storage-admin",
68731
+ capScope: "system",
68732
+ addonId: null,
68733
+ access: "view"
68734
+ },
68735
+ "storageAdmin.listKinds": {
68736
+ capName: "storage-admin",
68737
+ capScope: "system",
68738
+ addonId: null,
68739
+ access: "view"
68740
+ },
68741
+ "storageAdmin.listTables": {
68742
+ capName: "storage-admin",
68743
+ capScope: "system",
68744
+ addonId: null,
68745
+ access: "view"
68746
+ },
68747
+ "storageAdmin.readLegacyCollection": {
68748
+ capName: "storage-admin",
68749
+ capScope: "system",
68750
+ addonId: null,
68751
+ access: "view"
68752
+ },
68753
+ "storageAdmin.readRecords": {
68754
+ capName: "storage-admin",
68755
+ capScope: "system",
68756
+ addonId: null,
68757
+ access: "view"
68758
+ },
67897
68759
  "storageEvictable.evict": {
67898
68760
  capName: "storage-evictable",
67899
68761
  capScope: "system",
@@ -68806,6 +69668,12 @@ Object.freeze({
68806
69668
  addonId: null,
68807
69669
  access: "view"
68808
69670
  },
69671
+ "userManagement.migrateRawSecrets": {
69672
+ capName: "user-management",
69673
+ capScope: "system",
69674
+ addonId: null,
69675
+ access: "create"
69676
+ },
68809
69677
  "userManagement.oauthExchangeCode": {
68810
69678
  capName: "user-management",
68811
69679
  capScope: "system",
@@ -71482,6 +72350,37 @@ Object.freeze({
71482
72350
  form: "single",
71483
72351
  optional: false
71484
72352
  }],
72353
+ "records.count": [{
72354
+ name: "deviceId",
72355
+ form: "single",
72356
+ optional: true
72357
+ }],
72358
+ "records.deleteDevice": [{
72359
+ name: "deviceId",
72360
+ form: "single",
72361
+ optional: false
72362
+ }],
72363
+ "records.deleteWhere": [{
72364
+ name: "deviceId",
72365
+ form: "single",
72366
+ optional: true
72367
+ }],
72368
+ "records.histogram": [{
72369
+ name: "deviceId",
72370
+ form: "single",
72371
+ optional: true
72372
+ }],
72373
+ "records.putMany": [{
72374
+ name: "records",
72375
+ form: "object-array",
72376
+ optional: false,
72377
+ itemField: "deviceId"
72378
+ }],
72379
+ "records.query": [{
72380
+ name: "deviceId",
72381
+ form: "single",
72382
+ optional: true
72383
+ }],
71485
72384
  "sceneMonitor.captureReference": [{
71486
72385
  name: "deviceId",
71487
72386
  form: "single",
@@ -71598,6 +72497,17 @@ Object.freeze({
71598
72497
  form: "single",
71599
72498
  optional: false
71600
72499
  }],
72500
+ "storageAdmin.adoptRecords": [{
72501
+ name: "records",
72502
+ form: "object-array",
72503
+ optional: false,
72504
+ itemField: "deviceId"
72505
+ }],
72506
+ "storageAdmin.readRecords": [{
72507
+ name: "deviceId",
72508
+ form: "single",
72509
+ optional: true
72510
+ }],
71601
72511
  "streamBroker.acquireEgressTranscode": [{
71602
72512
  name: "deviceId",
71603
72513
  form: "single",
@@ -73998,6 +74908,778 @@ async function revokeBrokerCredential(fetchImpl, brokerUrl, credential) {
73998
74908
  await call(fetchImpl, brokerUrl, "/broker/revoke", { bearer: credential }, unknown());
73999
74909
  }
74000
74910
  //#endregion
74911
+ //#region src/endpoints/adapters/climate-model.ts
74912
+ /**
74913
+ * What a `climate-control` device is to Alexa (D874) — pure, so the discovery
74914
+ * shape, the slice mapping and the directive handlers share one definition.
74915
+ *
74916
+ * `Alexa.ThermostatController` 3.1 targetSetpoint, lowerSetpoint /
74917
+ * upperSetpoint (a device with a range),
74918
+ * thermostatMode HEAT / COOL / AUTO / ECO /
74919
+ * OFF
74920
+ * `Alexa.TemperatureSensor` the current temperature
74921
+ * `Alexa.PowerController` when the device can be turned off
74922
+ * `Alexa.ModeController` Climate.Mode only when the device has a mode
74923
+ * Amazon's thermostat vocabulary lacks
74924
+ * (dry, fan_only)
74925
+ * `Alexa.ModeController` Climate.FanSpeed the device's own fan modes
74926
+ *
74927
+ * Amazon's `thermostatMode` has no value for `dry` / `fan_only`, and a CUSTOM
74928
+ * mode is not a documented way to carry them: they travel through the
74929
+ * `Climate.Mode` ModeController, and `thermostatMode` is simply not reported
74930
+ * while the device is in one of them.
74931
+ *
74932
+ * Temperatures are Celsius throughout (the cap's contract); Alexa converts for
74933
+ * the account.
74934
+ */
74935
+ var INSTANCE_MODE = "Climate.Mode";
74936
+ var INSTANCE_FAN = "Climate.FanSpeed";
74937
+ var FAN_PREFIX = "Fan.";
74938
+ var DEFAULT_TEMP_STEP = .5;
74939
+ var FEATURE_DUAL_SETPOINT = "climate-dual-setpoint";
74940
+ var ALEXA_ECO_PRESET = "eco";
74941
+ var SINGLE_SETPOINT_MODES = [
74942
+ "heat",
74943
+ "cool",
74944
+ "auto",
74945
+ "dry"
74946
+ ];
74947
+ /** What TurnOn switches to, best first. */
74948
+ var TURN_ON_PREFERENCE = [
74949
+ "auto",
74950
+ "heat_cool",
74951
+ "cool",
74952
+ "heat"
74953
+ ];
74954
+ var SliceShape$1 = ClimateControlStatusSchema.pick({
74955
+ mode: true,
74956
+ availableModes: true,
74957
+ fanMode: true,
74958
+ availableFanModes: true,
74959
+ preset: true,
74960
+ availablePresets: true,
74961
+ target: true,
74962
+ targetHigh: true,
74963
+ targetLow: true,
74964
+ currentTemp: true,
74965
+ minTemp: true,
74966
+ maxTemp: true,
74967
+ tempStep: true
74968
+ });
74969
+ /** The slice fields the adapter reads, or null when the slice is not a climate one. */
74970
+ function readClimateSlice(slice) {
74971
+ const parsed = SliceShape$1.safeParse(slice);
74972
+ return parsed.success ? parsed.data : null;
74973
+ }
74974
+ function boundsOf(slice) {
74975
+ const min = slice.minTemp ?? 7;
74976
+ const max = slice.maxTemp ?? 35;
74977
+ const step = slice.tempStep ?? .5;
74978
+ return min < max ? {
74979
+ min,
74980
+ max,
74981
+ step
74982
+ } : {
74983
+ min: 7,
74984
+ max: 35,
74985
+ step: DEFAULT_TEMP_STEP
74986
+ };
74987
+ }
74988
+ /** One decimal, the precision Alexa's own conversions keep. */
74989
+ function round1(value) {
74990
+ return Math.round(value * 10) / 10;
74991
+ }
74992
+ /**
74993
+ * Snap a setpoint to the device's step (anchored at its minimum), then clamp:
74994
+ * a maximum off the step grid (max 30.2, step 0.5) must not snap above itself.
74995
+ */
74996
+ function snapToStep(value, bounds) {
74997
+ const snapped = bounds.min + Math.round((value - bounds.min) / bounds.step) * bounds.step;
74998
+ return round1(Math.min(bounds.max, Math.max(bounds.min, snapped)));
74999
+ }
75000
+ function temperatureValue(celsius) {
75001
+ return {
75002
+ value: round1(celsius),
75003
+ scale: "CELSIUS"
75004
+ };
75005
+ }
75006
+ function hasMode(slice, mode) {
75007
+ return slice !== null && slice.availableModes.includes(mode);
75008
+ }
75009
+ /** Dual setpoints (a heat/cool range). */
75010
+ function supportsDual(slice, features) {
75011
+ return slice === null ? features.includes(FEATURE_DUAL_SETPOINT) : hasMode(slice, "heat_cool");
75012
+ }
75013
+ /** A single setpoint (everything but a range-only device). */
75014
+ function supportsSingle(slice) {
75015
+ return slice === null || slice.availableModes.length === 0 ? true : slice.availableModes.some((m) => SINGLE_SETPOINT_MODES.includes(m));
75016
+ }
75017
+ /** The device's own name for its `eco` preset, if it has one. */
75018
+ function ecoPresetOf(slice) {
75019
+ return slice?.availablePresets.find((p) => p.toLowerCase() === ALEXA_ECO_PRESET) ?? null;
75020
+ }
75021
+ /** Amazon thermostat modes the device can be put in, in a stable order. */
75022
+ function supportedThermostatModes(slice) {
75023
+ if (slice === null || slice.availableModes.length === 0) return [
75024
+ "HEAT",
75025
+ "COOL",
75026
+ "AUTO",
75027
+ "OFF"
75028
+ ];
75029
+ const out = [];
75030
+ if (hasMode(slice, "heat")) out.push("HEAT");
75031
+ if (hasMode(slice, "cool")) out.push("COOL");
75032
+ if (hasMode(slice, "auto") || hasMode(slice, "heat_cool")) out.push("AUTO");
75033
+ if (ecoPresetOf(slice) !== null && out.length > 0) out.push("ECO");
75034
+ if (hasMode(slice, "off")) out.push("OFF");
75035
+ return out;
75036
+ }
75037
+ /** The HVAC mode TurnOn / leaving OFF switches to. */
75038
+ function onModeOf(slice) {
75039
+ return TURN_ON_PREFERENCE.find((m) => hasMode(slice, m)) ?? null;
75040
+ }
75041
+ function canPowerOff(slice) {
75042
+ return slice === null || slice.availableModes.length === 0 || hasMode(slice, "off");
75043
+ }
75044
+ /** A device with a mode Amazon's thermostat vocabulary lacks. */
75045
+ function hasExtraModes(slice) {
75046
+ return hasMode(slice, "dry") || hasMode(slice, "fan_only");
75047
+ }
75048
+ var MODE_VALUE = {
75049
+ heat: "Mode.Heat",
75050
+ cool: "Mode.Cool",
75051
+ auto: "Mode.Auto",
75052
+ dry: "Mode.Dry",
75053
+ fan_only: "Mode.Fan",
75054
+ off: "Mode.Off"
75055
+ };
75056
+ var MODE_NAMES = {
75057
+ "Mode.Heat": ["Riscaldamento", "Heating"],
75058
+ "Mode.Cool": ["Raffrescamento", "Cooling"],
75059
+ "Mode.Auto": ["Automatico", "Auto"],
75060
+ "Mode.Dry": ["Deumidificazione", "Dry"],
75061
+ "Mode.Fan": ["Ventilazione", "Fan only"],
75062
+ "Mode.Off": ["Spento", "Off"]
75063
+ };
75064
+ /** The ModeController value a HVAC mode reports as (a range is "auto"). */
75065
+ function modeValueOf(mode) {
75066
+ return mode === "heat_cool" ? MODE_VALUE.auto : MODE_VALUE[mode];
75067
+ }
75068
+ /** The HVAC mode a ModeController value asks for, among those the device has. */
75069
+ function hvacModeForValue(value, slice) {
75070
+ if (value === MODE_VALUE.auto) return hasMode(slice, "auto") ? "auto" : hasMode(slice, "heat_cool") ? "heat_cool" : null;
75071
+ const mode = Object.entries(MODE_VALUE).find(([, v]) => v === value)?.[0];
75072
+ if (mode === void 0) return null;
75073
+ const candidate = SliceShape$1.shape.mode.safeParse(mode);
75074
+ return candidate.success && hasMode(slice, candidate.data) ? candidate.data : null;
75075
+ }
75076
+ /** The ModeController values the device offers, in a stable order. */
75077
+ function modeValuesOf(slice) {
75078
+ return [
75079
+ "heat",
75080
+ "cool",
75081
+ "auto",
75082
+ "dry",
75083
+ "fan_only",
75084
+ "off"
75085
+ ].filter((m) => m === "auto" ? hasMode(slice, "auto") || hasMode(slice, "heat_cool") : hasMode(slice, m)).map((m) => MODE_VALUE[m]);
75086
+ }
75087
+ function textNames(it, en) {
75088
+ return [{
75089
+ "@type": "text",
75090
+ value: {
75091
+ text: it,
75092
+ locale: "it-IT"
75093
+ }
75094
+ }, {
75095
+ "@type": "text",
75096
+ value: {
75097
+ text: en,
75098
+ locale: "en-US"
75099
+ }
75100
+ }];
75101
+ }
75102
+ function modeCapability(slice) {
75103
+ const values = modeValuesOf(slice);
75104
+ if (values.length < 2) return null;
75105
+ return {
75106
+ type: "AlexaInterface",
75107
+ interface: "Alexa.ModeController",
75108
+ version: "3",
75109
+ instance: INSTANCE_MODE,
75110
+ capabilityResources: { friendlyNames: [{
75111
+ "@type": "asset",
75112
+ value: { assetId: "Alexa.Setting.Mode" }
75113
+ }, ...textNames("Modalità", "Mode")] },
75114
+ properties: {
75115
+ supported: [{ name: "mode" }],
75116
+ proactivelyReported: true,
75117
+ retrievable: true
75118
+ },
75119
+ configuration: {
75120
+ ordered: false,
75121
+ supportedModes: values.map((value) => {
75122
+ const [it, en] = MODE_NAMES[value] ?? [value, value];
75123
+ return {
75124
+ value,
75125
+ modeResources: { friendlyNames: textNames(it, en) }
75126
+ };
75127
+ })
75128
+ }
75129
+ };
75130
+ }
75131
+ var FAN_ASSETS = {
75132
+ low: "Alexa.Value.Low",
75133
+ medium: "Alexa.Value.Medium",
75134
+ high: "Alexa.Value.High"
75135
+ };
75136
+ var FAN_TEXTS = {
75137
+ auto: ["Automatico", "Auto"],
75138
+ quiet: ["Silenzioso", "Quiet"],
75139
+ turbo: ["Turbo", "Turbo"],
75140
+ on: ["Acceso", "On"],
75141
+ off: ["Spento", "Off"],
75142
+ middle: ["Medio", "Medium"],
75143
+ mediumlow: ["Medio basso", "Medium low"],
75144
+ mediumhigh: ["Medio alto", "Medium high"],
75145
+ medium_low: ["Medio basso", "Medium low"],
75146
+ medium_high: ["Medio alto", "Medium high"]
75147
+ };
75148
+ function capitalize(word) {
75149
+ return word.charAt(0).toUpperCase() + word.slice(1);
75150
+ }
75151
+ /** The ModeController value of a vendor fan mode (`medium_high` -> `Fan.MediumHigh`). */
75152
+ function fanValueOf(fanMode) {
75153
+ const words = fanMode.split(/[^A-Za-z0-9]+/).filter((w) => w.length > 0);
75154
+ return words.length === 0 ? null : `${FAN_PREFIX}${words.map(capitalize).join("")}`;
75155
+ }
75156
+ /** The fan modes with a usable, unique value: `[value, deviceFanMode]`. */
75157
+ function fanModesOf(slice) {
75158
+ const seen = /* @__PURE__ */ new Set();
75159
+ const out = [];
75160
+ for (const fanMode of slice.availableFanModes) {
75161
+ const value = fanValueOf(fanMode);
75162
+ if (value === null || seen.has(value)) continue;
75163
+ seen.add(value);
75164
+ out.push([value, fanMode]);
75165
+ }
75166
+ return out;
75167
+ }
75168
+ function hasFanModes(slice) {
75169
+ return slice !== null && fanModesOf(slice).length >= 2;
75170
+ }
75171
+ function fanFriendlyNames(fanMode) {
75172
+ const key = fanMode.toLowerCase();
75173
+ const asset = FAN_ASSETS[key];
75174
+ if (asset !== void 0) return [{
75175
+ "@type": "asset",
75176
+ value: { assetId: asset }
75177
+ }];
75178
+ const [it, en] = FAN_TEXTS[key] ?? [fanMode.replace(/_/g, " "), fanMode.replace(/_/g, " ")];
75179
+ return textNames(it, en);
75180
+ }
75181
+ function fanCapability(slice) {
75182
+ const modes = fanModesOf(slice);
75183
+ if (modes.length < 2) return null;
75184
+ return {
75185
+ type: "AlexaInterface",
75186
+ interface: "Alexa.ModeController",
75187
+ version: "3",
75188
+ instance: INSTANCE_FAN,
75189
+ capabilityResources: { friendlyNames: [{
75190
+ "@type": "asset",
75191
+ value: { assetId: "Alexa.Setting.FanSpeed" }
75192
+ }, ...textNames("Velocità ventola", "Fan speed")] },
75193
+ properties: {
75194
+ supported: [{ name: "mode" }],
75195
+ proactivelyReported: true,
75196
+ retrievable: true
75197
+ },
75198
+ configuration: {
75199
+ ordered: false,
75200
+ supportedModes: modes.map(([value, fanMode]) => ({
75201
+ value,
75202
+ modeResources: { friendlyNames: fanFriendlyNames(fanMode) }
75203
+ }))
75204
+ }
75205
+ };
75206
+ }
75207
+ function propertiesBlock(names) {
75208
+ return {
75209
+ supported: names.map((name) => ({ name })),
75210
+ proactivelyReported: true,
75211
+ retrievable: true
75212
+ };
75213
+ }
75214
+ function thermostatCapability(slice, modes, features) {
75215
+ return {
75216
+ type: "AlexaInterface",
75217
+ interface: "Alexa.ThermostatController",
75218
+ version: "3.1",
75219
+ properties: propertiesBlock([
75220
+ ...supportsSingle(slice) ? ["targetSetpoint"] : [],
75221
+ ...supportsDual(slice, features) ? ["lowerSetpoint", "upperSetpoint"] : [],
75222
+ "thermostatMode"
75223
+ ]),
75224
+ configuration: {
75225
+ supportedModes: modes,
75226
+ supportsScheduling: false
75227
+ }
75228
+ };
75229
+ }
75230
+ /** A thermostat is a THERMOSTAT, any other climate device an air conditioner. */
75231
+ function climateCategoryFor(deviceType) {
75232
+ return deviceType === "thermostat" ? "THERMOSTAT" : "AIR_CONDITIONER";
75233
+ }
75234
+ function describeClimate(device) {
75235
+ const slice = readClimateSlice(device.slices["climate-control"]);
75236
+ const modes = supportedThermostatModes(slice);
75237
+ if (modes.length === 0) return {
75238
+ kind: "skip",
75239
+ reason: "climate: the device offers no mode Alexa has a thermostat value for"
75240
+ };
75241
+ const extras = [];
75242
+ if (slice !== null) {
75243
+ const mode = hasExtraModes(slice) ? modeCapability(slice) : null;
75244
+ const fan = hasFanModes(slice) ? fanCapability(slice) : null;
75245
+ if (mode !== null) extras.push(mode);
75246
+ if (fan !== null) extras.push(fan);
75247
+ }
75248
+ return {
75249
+ kind: "publish",
75250
+ capabilities: [
75251
+ thermostatCapability(slice, modes, device.features),
75252
+ {
75253
+ type: "AlexaInterface",
75254
+ interface: "Alexa.TemperatureSensor",
75255
+ version: "3",
75256
+ properties: propertiesBlock(["temperature"])
75257
+ },
75258
+ ...canPowerOff(slice) ? [{
75259
+ type: "AlexaInterface",
75260
+ interface: "Alexa.PowerController",
75261
+ version: "3",
75262
+ properties: propertiesBlock(["powerState"])
75263
+ }] : [],
75264
+ ...extras
75265
+ ],
75266
+ category: climateCategoryFor(device.type)
75267
+ };
75268
+ }
75269
+ /** Amazon's `thermostatMode` for the state, or null while in a mode it lacks. */
75270
+ function thermostatModeOf(slice) {
75271
+ if (slice.mode === "off") return "OFF";
75272
+ const eco = ecoPresetOf(slice);
75273
+ if (eco !== null && slice.preset === eco) return "ECO";
75274
+ switch (slice.mode) {
75275
+ case "heat": return "HEAT";
75276
+ case "cool": return "COOL";
75277
+ case "auto":
75278
+ case "heat_cool": return "AUTO";
75279
+ default: return null;
75280
+ }
75281
+ }
75282
+ function climateProperties(slice) {
75283
+ const s = readClimateSlice(slice);
75284
+ if (s === null) return [];
75285
+ const out = [];
75286
+ const thermostat = (name, value) => {
75287
+ out.push({
75288
+ namespace: "Alexa.ThermostatController",
75289
+ name,
75290
+ value
75291
+ });
75292
+ };
75293
+ if (hasMode(s, "heat_cool") && s.targetLow !== null && s.targetHigh !== null && s.mode === "heat_cool") {
75294
+ thermostat("lowerSetpoint", temperatureValue(s.targetLow ?? 0));
75295
+ thermostat("upperSetpoint", temperatureValue(s.targetHigh ?? 0));
75296
+ } else if (s.target !== null && supportsSingle(s)) thermostat("targetSetpoint", temperatureValue(s.target));
75297
+ const mode = thermostatModeOf(s);
75298
+ if (mode !== null) thermostat("thermostatMode", mode);
75299
+ if (s.currentTemp !== null) out.push({
75300
+ namespace: "Alexa.TemperatureSensor",
75301
+ name: "temperature",
75302
+ value: temperatureValue(s.currentTemp)
75303
+ });
75304
+ if (canPowerOff(s)) out.push({
75305
+ namespace: "Alexa.PowerController",
75306
+ name: "powerState",
75307
+ value: s.mode === "off" ? "OFF" : "ON"
75308
+ });
75309
+ if (hasExtraModes(s) && modeValuesOf(s).length >= 2) out.push({
75310
+ namespace: "Alexa.ModeController",
75311
+ instance: INSTANCE_MODE,
75312
+ name: "mode",
75313
+ value: modeValueOf(s.mode)
75314
+ });
75315
+ const fan = fanValueOf(s.fanMode);
75316
+ if (hasFanModes(s) && fan !== null && fanModesOf(s).some(([value]) => value === fan)) out.push({
75317
+ namespace: "Alexa.ModeController",
75318
+ instance: INSTANCE_FAN,
75319
+ name: "mode",
75320
+ value: fan
75321
+ });
75322
+ return out;
75323
+ }
75324
+ //#endregion
75325
+ //#region src/handlers/climate-controller.ts
75326
+ var CAP_CLIMATE = "climate-control";
75327
+ var CAP_SWITCH$1 = "switch";
75328
+ var UNCERTAINTY_MS = 500;
75329
+ var PRESET_NONE = "none";
75330
+ var ScaleSchema = _enum([
75331
+ "CELSIUS",
75332
+ "FAHRENHEIT",
75333
+ "KELVIN"
75334
+ ]);
75335
+ var TemperatureSchema = object({
75336
+ value: number(),
75337
+ scale: ScaleSchema
75338
+ });
75339
+ var SetTemperaturePayload = object({
75340
+ targetSetpoint: TemperatureSchema.optional(),
75341
+ lowerSetpoint: TemperatureSchema.optional(),
75342
+ upperSetpoint: TemperatureSchema.optional()
75343
+ });
75344
+ var AdjustTemperaturePayload = object({ targetSetpointDelta: TemperatureSchema });
75345
+ var ThermostatModePayload = object({ thermostatMode: union([string(), object({ value: string() })]) });
75346
+ var ModePayload = object({ mode: string() });
75347
+ function isRefusal(outcome) {
75348
+ return "refusal" in outcome;
75349
+ }
75350
+ function toCelsius(t) {
75351
+ if (t.scale === "FAHRENHEIT") return (t.value - 32) * 5 / 9;
75352
+ if (t.scale === "KELVIN") return t.value - 273.15;
75353
+ return t.value;
75354
+ }
75355
+ /** A DELTA converts without the scale's offset. */
75356
+ function deltaToCelsius(t) {
75357
+ return t.scale === "FAHRENHEIT" ? t.value * 5 / 9 : t.value;
75358
+ }
75359
+ function withPayload(envelope, extra) {
75360
+ return { event: {
75361
+ ...envelope.event,
75362
+ payload: {
75363
+ ...envelope.event.payload,
75364
+ ...extra
75365
+ }
75366
+ } };
75367
+ }
75368
+ function outOfRange$1(header, bounds) {
75369
+ return { refusal: withPayload(errorEnvelope(header, "TEMPERATURE_VALUE_OUT_OF_RANGE", `the setpoint must be between ${bounds.min} and ${bounds.max} C`), { validRange: {
75370
+ minimumValue: temperatureValue(bounds.min),
75371
+ maximumValue: temperatureValue(bounds.max)
75372
+ } }) };
75373
+ }
75374
+ function tooClose(header, bounds) {
75375
+ return { refusal: withPayload(errorEnvelope(header, "REQUESTED_SETPOINTS_TOO_CLOSE", "lowerSetpoint must be below upperSetpoint"), { minimumTemperatureDelta: temperatureValue(bounds.step) }) };
75376
+ }
75377
+ function refuse$1(header, type, message) {
75378
+ return { refusal: errorEnvelope(header, type, message) };
75379
+ }
75380
+ function inRange(celsius, bounds) {
75381
+ const v = round1(celsius);
75382
+ return v >= bounds.min && v <= bounds.max;
75383
+ }
75384
+ var THERMOSTAT_PROPERTIES = new Set([
75385
+ "targetSetpoint",
75386
+ "lowerSetpoint",
75387
+ "upperSetpoint",
75388
+ "thermostatMode"
75389
+ ]);
75390
+ function thermostatProperty(property) {
75391
+ return property.namespace === "Alexa.ThermostatController" && THERMOSTAT_PROPERTIES.has(property.name);
75392
+ }
75393
+ function commandsFor(api, deviceId) {
75394
+ return {
75395
+ setMode: (mode) => api.climateControl.setMode.mutate({
75396
+ deviceId,
75397
+ mode
75398
+ }),
75399
+ setPreset: (preset) => api.climateControl.setPreset.mutate({
75400
+ deviceId,
75401
+ preset
75402
+ }),
75403
+ setFanMode: (fanMode) => api.climateControl.setFanMode.mutate({
75404
+ deviceId,
75405
+ fanMode
75406
+ }),
75407
+ setTarget: (target) => api.climateControl.setTarget.mutate({
75408
+ deviceId,
75409
+ target
75410
+ }),
75411
+ setTargetRange: (targetLow, targetHigh) => api.climateControl.setTargetRange.mutate({
75412
+ deviceId,
75413
+ targetLow,
75414
+ targetHigh
75415
+ })
75416
+ };
75417
+ }
75418
+ function planRange(header, slice, cmd, low, high) {
75419
+ const bounds = boundsOf(slice);
75420
+ if (!supportsDual(slice, [])) return refuse$1(header, "DUAL_SETPOINTS_UNSUPPORTED", "this thermostat has a single setpoint");
75421
+ if (!inRange(low, bounds) || !inRange(high, bounds)) return outOfRange$1(header, bounds);
75422
+ const targetLow = snapToStep(low, bounds);
75423
+ const targetHigh = snapToStep(high, bounds);
75424
+ if (targetHigh - targetLow < bounds.step) return tooClose(header, bounds);
75425
+ return {
75426
+ commands: [...slice.mode === "heat_cool" ? [] : [() => cmd.setMode("heat_cool")], () => cmd.setTargetRange(targetLow, targetHigh)],
75427
+ projected: {
75428
+ ...slice,
75429
+ mode: "heat_cool",
75430
+ targetLow,
75431
+ targetHigh
75432
+ },
75433
+ keep: thermostatProperty
75434
+ };
75435
+ }
75436
+ function planSingle(header, slice, cmd, celsius) {
75437
+ const bounds = boundsOf(slice);
75438
+ if (!supportsSingle(slice) || slice.mode === "heat_cool") return refuse$1(header, "UNWILLING_TO_SET_VALUE", "the thermostat is in range mode: set a lower and an upper setpoint");
75439
+ if (!inRange(celsius, bounds)) return outOfRange$1(header, bounds);
75440
+ const target = snapToStep(celsius, bounds);
75441
+ return {
75442
+ commands: [() => cmd.setTarget(target)],
75443
+ projected: {
75444
+ ...slice,
75445
+ target
75446
+ },
75447
+ keep: thermostatProperty
75448
+ };
75449
+ }
75450
+ function planSetTemperature(c, slice, cmd) {
75451
+ const parsed = SetTemperaturePayload.safeParse(c.payload);
75452
+ if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
75453
+ const { targetSetpoint, lowerSetpoint, upperSetpoint } = parsed.data;
75454
+ if (targetSetpoint !== void 0 && lowerSetpoint !== void 0 && upperSetpoint !== void 0) return refuse$1(c.header, "TRIPLE_SETPOINTS_UNSUPPORTED", "a target AND a range were requested");
75455
+ if (lowerSetpoint !== void 0 && upperSetpoint !== void 0 && targetSetpoint === void 0) return planRange(c.header, slice, cmd, toCelsius(lowerSetpoint), toCelsius(upperSetpoint));
75456
+ if (targetSetpoint !== void 0 && lowerSetpoint === void 0 && upperSetpoint === void 0) return planSingle(c.header, slice, cmd, toCelsius(targetSetpoint));
75457
+ return refuse$1(c.header, "INVALID_DIRECTIVE", "give a targetSetpoint, or a lower and an upper");
75458
+ }
75459
+ function planAdjust(c, slice, cmd) {
75460
+ const parsed = AdjustTemperaturePayload.safeParse(c.payload);
75461
+ if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
75462
+ const delta = deltaToCelsius(parsed.data.targetSetpointDelta);
75463
+ if (slice.mode === "heat_cool" && slice.targetLow !== null && slice.targetHigh !== null) return planRange(c.header, slice, cmd, slice.targetLow + delta, slice.targetHigh + delta);
75464
+ if (slice.target === null) return refuse$1(c.header, "ENDPOINT_UNREACHABLE", "the current setpoint is unknown, so it cannot be adjusted");
75465
+ return planSingle(c.header, slice, cmd, slice.target + delta);
75466
+ }
75467
+ var ECO_FROM_OFF_MESSAGE = "the thermostat has no mode to leave OFF into";
75468
+ function hvacModeFor(requested, slice) {
75469
+ const has = (m) => slice.availableModes.includes(m);
75470
+ switch (requested) {
75471
+ case "HEAT": return has("heat") ? "heat" : null;
75472
+ case "COOL": return has("cool") ? "cool" : null;
75473
+ case "AUTO": return has("auto") ? "auto" : has("heat_cool") ? "heat_cool" : null;
75474
+ case "OFF": return has("off") ? "off" : null;
75475
+ default: return null;
75476
+ }
75477
+ }
75478
+ /** The preset that clears `eco` (HA and Gree both call it `none`), if the device has one. */
75479
+ function presetClearing(slice) {
75480
+ return slice.availablePresets.find((p) => p.toLowerCase() === PRESET_NONE) ?? null;
75481
+ }
75482
+ function planEco(c, slice, cmd) {
75483
+ const eco = ecoPresetOf(slice);
75484
+ if (eco === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", "ECO is not supported");
75485
+ const wake = slice.mode === "off" ? onModeOf(slice) : null;
75486
+ if (slice.mode === "off" && wake === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", ECO_FROM_OFF_MESSAGE);
75487
+ return {
75488
+ commands: [...wake === null ? [] : [() => cmd.setMode(wake)], () => cmd.setPreset(eco)],
75489
+ projected: {
75490
+ ...slice,
75491
+ mode: wake ?? slice.mode,
75492
+ preset: eco
75493
+ },
75494
+ keep: thermostatProperty
75495
+ };
75496
+ }
75497
+ function planSetThermostatMode(c, slice, cmd) {
75498
+ const parsed = ThermostatModePayload.safeParse(c.payload);
75499
+ if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
75500
+ const raw = parsed.data.thermostatMode;
75501
+ const requested = (typeof raw === "string" ? raw : raw.value).toUpperCase();
75502
+ if (requested === "ECO") return planEco(c, slice, cmd);
75503
+ const mode = hvacModeFor(requested, slice);
75504
+ if (mode === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", `${requested} is not a mode this thermostat has`);
75505
+ const eco = ecoPresetOf(slice);
75506
+ const clear = eco !== null && slice.preset === eco ? presetClearing(slice) : null;
75507
+ return {
75508
+ commands: [...clear === null ? [] : [() => cmd.setPreset(clear)], () => cmd.setMode(mode)],
75509
+ projected: {
75510
+ ...slice,
75511
+ mode,
75512
+ preset: clear ?? slice.preset
75513
+ },
75514
+ keep: thermostatProperty
75515
+ };
75516
+ }
75517
+ function planPower(c, slice, cmd) {
75518
+ const turnOn = c.header.name === "TurnOn";
75519
+ const keep = (p) => p.namespace === "Alexa.PowerController";
75520
+ if (turnOn) {
75521
+ if (slice.mode !== "off") return {
75522
+ commands: [],
75523
+ projected: slice,
75524
+ keep
75525
+ };
75526
+ const mode = onModeOf(slice);
75527
+ if (mode === null) return refuse$1(c.header, "INVALID_DIRECTIVE", "no mode to turn on into");
75528
+ return {
75529
+ commands: [() => cmd.setMode(mode)],
75530
+ projected: {
75531
+ ...slice,
75532
+ mode
75533
+ },
75534
+ keep
75535
+ };
75536
+ }
75537
+ if (!slice.availableModes.includes("off")) return refuse$1(c.header, "INVALID_DIRECTIVE", "this device cannot be turned off");
75538
+ if (slice.mode === "off") return {
75539
+ commands: [],
75540
+ projected: slice,
75541
+ keep
75542
+ };
75543
+ return {
75544
+ commands: [() => cmd.setMode("off")],
75545
+ projected: {
75546
+ ...slice,
75547
+ mode: "off"
75548
+ },
75549
+ keep
75550
+ };
75551
+ }
75552
+ function planModeController(c, slice, cmd) {
75553
+ const parsed = ModePayload.safeParse(c.payload);
75554
+ const instance = c.header.instance;
75555
+ if (!parsed.success || instance === void 0) return refuse$1(c.header, "INVALID_DIRECTIVE", "a mode and an instance are required");
75556
+ const keep = (p) => p.instance === instance;
75557
+ if (instance === "Climate.Mode") {
75558
+ const mode = hvacModeForValue(parsed.data.mode, slice);
75559
+ if (mode === null) return refuse$1(c.header, "INVALID_VALUE", `unknown mode ${parsed.data.mode}`);
75560
+ return {
75561
+ commands: [() => cmd.setMode(mode)],
75562
+ projected: {
75563
+ ...slice,
75564
+ mode
75565
+ },
75566
+ keep
75567
+ };
75568
+ }
75569
+ if (instance === "Climate.FanSpeed") {
75570
+ const fan = fanModesOf(slice).find(([value]) => value === parsed.data.mode);
75571
+ if (fan === void 0) return refuse$1(c.header, "INVALID_VALUE", `unknown fan mode ${parsed.data.mode}`);
75572
+ return {
75573
+ commands: [() => cmd.setFanMode(fan[1])],
75574
+ projected: {
75575
+ ...slice,
75576
+ fanMode: fan[1]
75577
+ },
75578
+ keep
75579
+ };
75580
+ }
75581
+ return refuse$1(c.header, "INVALID_DIRECTIVE", `unknown instance ${instance}`);
75582
+ }
75583
+ function plan(c, slice, cmd) {
75584
+ switch (`${c.header.namespace}/${c.header.name}`) {
75585
+ case "Alexa.ThermostatController/SetTargetTemperature": return planSetTemperature(c, slice, cmd);
75586
+ case "Alexa.ThermostatController/AdjustTargetTemperature": return planAdjust(c, slice, cmd);
75587
+ case "Alexa.ThermostatController/SetThermostatMode": return planSetThermostatMode(c, slice, cmd);
75588
+ case "Alexa.PowerController/TurnOn":
75589
+ case "Alexa.PowerController/TurnOff": return planPower(c, slice, cmd);
75590
+ case "Alexa.ModeController/SetMode": return planModeController(c, slice, cmd);
75591
+ default: return refuse$1(c.header, "INVALID_DIRECTIVE", `${c.header.namespace}/${c.header.name}`);
75592
+ }
75593
+ }
75594
+ async function readSlice$1(c, deps) {
75595
+ try {
75596
+ return readClimateSlice(await deps.ctx.api.deviceState.getCapSlice.query({
75597
+ deviceId: c.deviceId,
75598
+ capName: CAP_CLIMATE
75599
+ }));
75600
+ } catch (err) {
75601
+ deps.logger.warn("alexa climate-controller: the climate state could not be read", {
75602
+ tags: { deviceId: c.deviceId },
75603
+ meta: { error: err instanceof Error ? err.message : String(err) }
75604
+ });
75605
+ return null;
75606
+ }
75607
+ }
75608
+ function successResponse$1(c, properties) {
75609
+ const now = (/* @__PURE__ */ new Date()).toISOString();
75610
+ return {
75611
+ event: {
75612
+ header: {
75613
+ namespace: "Alexa",
75614
+ name: "Response",
75615
+ messageId: c.header.messageId,
75616
+ ...c.header.correlationToken !== void 0 ? { correlationToken: c.header.correlationToken } : {},
75617
+ payloadVersion: "3"
75618
+ },
75619
+ endpoint: { endpointId: c.endpoint.endpointId },
75620
+ payload: {}
75621
+ },
75622
+ context: { properties: properties.map((p) => ({
75623
+ ...p,
75624
+ timeOfSample: now,
75625
+ uncertaintyInMilliseconds: UNCERTAINTY_MS
75626
+ })) }
75627
+ };
75628
+ }
75629
+ /** The one handler behind every climate directive. */
75630
+ async function handleClimateDirective(c) {
75631
+ const deps = c.deps.climateController;
75632
+ const slice = await readSlice$1(c, deps);
75633
+ if (slice === null) return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the climate state could not be read");
75634
+ const outcome = plan(c, slice, commandsFor(deps.ctx.api, c.deviceId));
75635
+ if (isRefusal(outcome)) return outcome.refusal;
75636
+ try {
75637
+ for (const command of outcome.commands) await command();
75638
+ } catch (err) {
75639
+ deps.logger.warn("alexa climate-controller: the climate command failed", {
75640
+ tags: { deviceId: c.deviceId },
75641
+ meta: {
75642
+ directive: c.header.name,
75643
+ error: err instanceof Error ? err.message : String(err)
75644
+ }
75645
+ });
75646
+ return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the device did not accept the command");
75647
+ }
75648
+ deps.logger.info(`alexa climate-controller: ${c.header.name}`, {
75649
+ tags: { deviceId: c.deviceId },
75650
+ meta: { commands: outcome.commands.length }
75651
+ });
75652
+ return successResponse$1(c, climateProperties(outcome.projected).filter(outcome.keep));
75653
+ }
75654
+ /**
75655
+ * Last answer per device, per deps instance: a failed bindings read changes
75656
+ * nothing (AGENTS D49) — a thermostat's TurnOff must not fall through to the
75657
+ * switch handler because one `getBindings` call failed.
75658
+ */
75659
+ var lastPowerClaim = /* @__PURE__ */ new WeakMap();
75660
+ /** PowerController on a climate endpoint, not on a switch: decided by the bound caps. */
75661
+ async function handlesPower(c) {
75662
+ const deps = c.deps.climateController;
75663
+ const known = lastPowerClaim.get(deps) ?? /* @__PURE__ */ new Map();
75664
+ lastPowerClaim.set(deps, known);
75665
+ try {
75666
+ const caps = (await deps.ctx.api.deviceManager.getBindings.query({ deviceId: c.deviceId })).entries.map((e) => e.capName);
75667
+ const claim = caps.includes(CAP_CLIMATE) && !caps.includes(CAP_SWITCH$1);
75668
+ known.set(c.deviceId, claim);
75669
+ return claim;
75670
+ } catch (err) {
75671
+ const previous = known.get(c.deviceId);
75672
+ deps.logger.warn("alexa climate-controller: the bound caps could not be read", {
75673
+ tags: { deviceId: c.deviceId },
75674
+ meta: {
75675
+ error: err instanceof Error ? err.message : String(err),
75676
+ decidedFrom: previous === void 0 ? "no previous answer: not claimed" : "previous answer"
75677
+ }
75678
+ });
75679
+ return previous ?? false;
75680
+ }
75681
+ }
75682
+ //#endregion
74001
75683
  //#region src/endpoints/adapters/shared.ts
74002
75684
  /** The base `Alexa` interface every endpoint declares. */
74003
75685
  var BASE = {
@@ -74048,6 +75730,88 @@ function numericValueField(value) {
74048
75730
  return typeof raw === "number" ? raw : null;
74049
75731
  }
74050
75732
  //#endregion
75733
+ //#region src/endpoints/adapters/temperature.ts
75734
+ /** A temperature change under half a degree is not worth a ChangeReport. */
75735
+ var TEMPERATURE_DEADBAND_CELSIUS = .5;
75736
+ /** A numeric sensor reports at most once a minute per property (D869). */
75737
+ var SENSOR_MIN_INTERVAL_MS = 6e4;
75738
+ /** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
75739
+ var temperatureAdapter = {
75740
+ cap: "temperature-sensor",
75741
+ stateCaps: ["temperature-sensor"],
75742
+ discoveryReads: [],
75743
+ describe: () => ({
75744
+ kind: "publish",
75745
+ capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
75746
+ category: "TEMPERATURE_SENSOR"
75747
+ }),
75748
+ properties: (_cap, slice) => {
75749
+ const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
75750
+ return p.success ? [{
75751
+ namespace: "Alexa.TemperatureSensor",
75752
+ name: "temperature",
75753
+ value: {
75754
+ value: Math.round(p.data.celsius * 10) / 10,
75755
+ scale: "CELSIUS"
75756
+ }
75757
+ }] : [];
75758
+ },
75759
+ directives: [],
75760
+ shaping: () => ({
75761
+ deadband: TEMPERATURE_DEADBAND_CELSIUS,
75762
+ minIntervalMs: SENSOR_MIN_INTERVAL_MS,
75763
+ numericOf: numericValueField
75764
+ })
75765
+ };
75766
+ //#endregion
75767
+ //#region src/endpoints/adapters/climate.ts
75768
+ var OWN_INSTANCES = new Set([INSTANCE_MODE, INSTANCE_FAN]);
75769
+ var THERMOSTAT_DIRECTIVES = [
75770
+ "Alexa.ThermostatController/SetTargetTemperature",
75771
+ "Alexa.ThermostatController/AdjustTargetTemperature",
75772
+ "Alexa.ThermostatController/SetThermostatMode"
75773
+ ].map((key) => ({
75774
+ key,
75775
+ needsReachableDevice: true,
75776
+ handle: handleClimateDirective
75777
+ }));
75778
+ var POWER_DIRECTIVES$1 = ["Alexa.PowerController/TurnOn", "Alexa.PowerController/TurnOff"].map((key) => ({
75779
+ key,
75780
+ needsReachableDevice: true,
75781
+ handle: handleClimateDirective,
75782
+ claims: handlesPower
75783
+ }));
75784
+ var MODE_DIRECTIVE = {
75785
+ key: "Alexa.ModeController/SetMode",
75786
+ needsReachableDevice: true,
75787
+ handle: handleClimateDirective,
75788
+ claims: async (c) => c.header.instance !== void 0 && OWN_INSTANCES.has(c.header.instance)
75789
+ };
75790
+ /**
75791
+ * `climate-control` -> a thermostat (Home Assistant) or an air conditioner
75792
+ * (Gree) (D874): `Alexa.ThermostatController` 3.1 with the current temperature,
75793
+ * power, and the modes Amazon's vocabulary lacks and the fan mode as
75794
+ * ModeControllers. The current temperature is shaped like any temperature
75795
+ * sensor; setpoints, mode and fan report on every change.
75796
+ */
75797
+ var climateAdapter = {
75798
+ cap: "climate-control",
75799
+ stateCaps: ["climate-control"],
75800
+ discoveryReads: ["climate-control"],
75801
+ describe: describeClimate,
75802
+ properties: (_cap, slice) => climateProperties(slice),
75803
+ directives: [
75804
+ ...THERMOSTAT_DIRECTIVES,
75805
+ ...POWER_DIRECTIVES$1,
75806
+ MODE_DIRECTIVE
75807
+ ],
75808
+ shaping: (property) => property.namespace === "Alexa.TemperatureSensor" ? {
75809
+ deadband: TEMPERATURE_DEADBAND_CELSIUS,
75810
+ minIntervalMs: SENSOR_MIN_INTERVAL_MS,
75811
+ numericOf: numericValueField
75812
+ } : null
75813
+ };
75814
+ //#endregion
74051
75815
  //#region src/endpoints/adapters/contact.ts
74052
75816
  /** `contact` → `Alexa.ContactSensor` (CONTACT_SENSOR). Amazon: DETECTED = open. */
74053
75817
  var contactAdapter = {
@@ -74712,7 +76476,8 @@ var coverAdapter = {
74712
76476
  ].map((key) => ({
74713
76477
  key,
74714
76478
  needsReachableDevice: true,
74715
- handle: handleCoverDirective
76479
+ handle: handleCoverDirective,
76480
+ claims: async (c) => c.header.instance?.startsWith("Blind.") ?? false
74716
76481
  })),
74717
76482
  shaping: (property) => property.namespace === "Alexa.RangeController" ? {
74718
76483
  deadband: 1,
@@ -74720,40 +76485,6 @@ var coverAdapter = {
74720
76485
  numericOf: plainNumber
74721
76486
  } : { debounceMs: COVER_DEBOUNCE_MS }
74722
76487
  };
74723
- //#endregion
74724
- //#region src/endpoints/adapters/temperature.ts
74725
- /** A temperature change under half a degree is not worth a ChangeReport. */
74726
- var TEMPERATURE_DEADBAND_CELSIUS = .5;
74727
- /** A numeric sensor reports at most once a minute per property (D869). */
74728
- var SENSOR_MIN_INTERVAL_MS = 6e4;
74729
- /** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
74730
- var temperatureAdapter = {
74731
- cap: "temperature-sensor",
74732
- stateCaps: ["temperature-sensor"],
74733
- discoveryReads: [],
74734
- describe: () => ({
74735
- kind: "publish",
74736
- capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
74737
- category: "TEMPERATURE_SENSOR"
74738
- }),
74739
- properties: (_cap, slice) => {
74740
- const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
74741
- return p.success ? [{
74742
- namespace: "Alexa.TemperatureSensor",
74743
- name: "temperature",
74744
- value: {
74745
- value: Math.round(p.data.celsius * 10) / 10,
74746
- scale: "CELSIUS"
74747
- }
74748
- }] : [];
74749
- },
74750
- directives: [],
74751
- shaping: () => ({
74752
- deadband: TEMPERATURE_DEADBAND_CELSIUS,
74753
- minIntervalMs: SENSOR_MIN_INTERVAL_MS,
74754
- numericOf: numericValueField
74755
- })
74756
- };
74757
76488
  /** `humidity-sensor` → `Alexa.HumiditySensor` (TEMPERATURE_SENSOR). */
74758
76489
  var humidityAdapter = {
74759
76490
  cap: "humidity-sensor",
@@ -75052,6 +76783,11 @@ var BRIGHTNESS_DIRECTIVE = {
75052
76783
  needsReachableDevice: true,
75053
76784
  handle: (c) => handleSetBrightness(c.header, c.endpoint, c.payload, c.deps.brightnessController, c.identity)
75054
76785
  };
76786
+ /** An outlet (D875: its source says so, e.g. HA `device_class: outlet`) is a
76787
+ * smart plug; every other switch is a SWITCH. */
76788
+ function switchCategory(device) {
76789
+ return device.role === DeviceRole.Outlet ? "SMARTPLUG" : "SWITCH";
76790
+ }
75055
76791
  //#endregion
75056
76792
  //#region src/endpoints/adapters/index.ts
75057
76793
  var GENERIC_ADAPTERS = [
@@ -75059,6 +76795,7 @@ var GENERIC_ADAPTERS = [
75059
76795
  contactAdapter,
75060
76796
  motionAdapter,
75061
76797
  presenceAdapter,
76798
+ climateAdapter,
75062
76799
  temperatureAdapter,
75063
76800
  humidityAdapter,
75064
76801
  {
@@ -75070,7 +76807,7 @@ var GENERIC_ADAPTERS = [
75070
76807
  return {
75071
76808
  kind: "publish",
75072
76809
  capabilities: [iface("Alexa.PowerController", "powerState"), ...dimmable ? [iface("Alexa.BrightnessController", "brightness")] : []],
75073
- category: dimmable || device.type === "light" ? "LIGHT" : "SWITCH"
76810
+ category: dimmable || device.type === "light" ? "LIGHT" : switchCategory(device)
75074
76811
  };
75075
76812
  },
75076
76813
  properties: (capName, slice) => {
@@ -75116,12 +76853,19 @@ function shapingFor(capName, property) {
75116
76853
  function discoveryReadCaps(capNames) {
75117
76854
  return [...new Set(GENERIC_ADAPTERS.filter((a) => capNames.includes(a.cap)).flatMap((a) => a.discoveryReads))];
75118
76855
  }
75119
- /** A directive an adapter handles on a generic endpoint, by `Namespace/Name`. */
75120
- function genericDirective(key) {
75121
- for (const adapter of GENERIC_ADAPTERS) {
75122
- const found = adapter.directives.find((d) => d.key === key);
75123
- if (found !== void 0) return found;
75124
- }
76856
+ /** Every directive an adapter declares for `Namespace/Name`, in registry order. */
76857
+ function genericDirectives(key) {
76858
+ return GENERIC_ADAPTERS.flatMap((a) => a.directives.filter((d) => d.key === key));
76859
+ }
76860
+ /**
76861
+ * The directive that serves this endpoint: of the adapters declaring the key,
76862
+ * in registry order, the first that claims it (two caps may answer one key -
76863
+ * power on a switch and on a thermostat). Null = nobody claims it.
76864
+ */
76865
+ async function resolveGenericDirective(context) {
76866
+ const key = `${context.header.namespace}/${context.header.name}`;
76867
+ for (const directive of genericDirectives(key)) if (directive.claims === void 0 || await directive.claims(context)) return directive;
76868
+ return null;
75125
76869
  }
75126
76870
  /** Every directive key the registry handles. */
75127
76871
  var GENERIC_DIRECTIVE_KEYS = [...new Set(GENERIC_ADAPTERS.flatMap((a) => a.directives.map((d) => d.key)))];
@@ -75163,9 +76907,9 @@ function planGenericDevice(device) {
75163
76907
  category
75164
76908
  };
75165
76909
  }
75166
- /** Device types this export may publish as a generic endpoint. A `thermostat`
75167
- * is listed so discovery can say WHY it is not published (no climate adapter
75168
- * yet, D869) instead of calling it a device with no mappable cap. */
76910
+ /** Device types this export may publish as a generic endpoint. `thermostat`
76911
+ * (Home Assistant) and `climate` (an air conditioner, e.g. Gree) both carry
76912
+ * the `climate-control` cap (D874). */
75169
76913
  var GENERIC_DEVICE_TYPES = [
75170
76914
  "light",
75171
76915
  "switch",
@@ -75174,11 +76918,13 @@ var GENERIC_DEVICE_TYPES = [
75174
76918
  "lock",
75175
76919
  "presence",
75176
76920
  "thermostat",
76921
+ "climate",
75177
76922
  "cover",
75178
76923
  "generic"
75179
76924
  ];
75180
76925
  function toEndpoint(input, plan) {
75181
- const name = input.name.trim();
76926
+ const override = input.alexaName?.trim() ?? "";
76927
+ const name = override.length > 0 ? override : input.name.trim();
75182
76928
  return {
75183
76929
  endpointId: `${input.deviceId}__dev`,
75184
76930
  manufacturerName: "camstack",
@@ -75189,13 +76935,7 @@ function toEndpoint(input, plan) {
75189
76935
  cookie: { stableId: input.stableId }
75190
76936
  };
75191
76937
  }
75192
- /** Why a climate device is withheld until its adapter exists. */
75193
- var THERMOSTAT_SKIP_REASON = "thermostat: Alexa climate control (Alexa.ThermostatController) has no adapter yet - not published until it does (D869, P3)";
75194
76938
  function planGenericEndpoint(input) {
75195
- if (input.type === "thermostat" && input.capNames.includes("climate-control")) return {
75196
- kind: "skip",
75197
- reasons: [THERMOSTAT_SKIP_REASON]
75198
- };
75199
76939
  const plan = planGenericDevice({
75200
76940
  deviceId: input.deviceId,
75201
76941
  type: input.type,
@@ -75211,6 +76951,74 @@ function planGenericEndpoint(input) {
75211
76951
  return plan;
75212
76952
  }
75213
76953
  //#endregion
76954
+ //#region src/endpoints/alexa-name.ts
76955
+ /**
76956
+ * The per-device "Alexa name" (D875): what Alexa calls the device, when it
76957
+ * should not be the device's own name.
76958
+ *
76959
+ * Stored under {@link ALEXA_NAME_KEY} in this addon's per-device settings store
76960
+ * (D870) and used as the endpoint `friendlyName`. It never reaches the endpoint
76961
+ * id (`<deviceId>__dev`), so renaming updates the SAME Alexa device on the next
76962
+ * AddOrUpdateReport instead of creating a duplicate.
76963
+ *
76964
+ * Amazon's friendlyName rule (Alexa.Discovery objects, endpoint object): "Up
76965
+ * to 256 alphanumeric characters and spaces. Don't include special characters
76966
+ * or punctuation." Letters of any script count as alphanumeric, so an Italian
76967
+ * name keeps its accents. The validator rejects what Amazon would reject, with
76968
+ * a message the operator can act on.
76969
+ */
76970
+ /** Per-device store key of the saved Alexa name. */
76971
+ var ALEXA_NAME_KEY = "alexaName";
76972
+ /** The device-settings form field key, namespaced per exporter (see the addon). */
76973
+ function alexaNameKey(deviceId) {
76974
+ return `alexa:${deviceId}:alexaName`;
76975
+ }
76976
+ /** Letters (any script, with their combining marks), digits and spaces. */
76977
+ var ALLOWED_PATTERN = /^[\p{L}\p{M}\p{N} ]+$/u;
76978
+ /**
76979
+ * Validate an operator-typed Alexa name. Blank clears the override
76980
+ * (`name: null`, the device name is used).
76981
+ */
76982
+ function validateAlexaName(raw) {
76983
+ if (raw === null || raw === void 0) return {
76984
+ ok: true,
76985
+ name: null
76986
+ };
76987
+ if (typeof raw !== "string") return {
76988
+ ok: false,
76989
+ error: "The Alexa name must be text."
76990
+ };
76991
+ const name = raw.trim().replace(/\s+/g, " ");
76992
+ if (name.length === 0) return {
76993
+ ok: true,
76994
+ name: null
76995
+ };
76996
+ if (name.length > 256) return {
76997
+ ok: false,
76998
+ error: `The Alexa name is ${name.length} characters; Amazon allows at most 256.`
76999
+ };
77000
+ if (!ALLOWED_PATTERN.test(name)) return {
77001
+ ok: false,
77002
+ error: "The Alexa name may contain only letters, digits and spaces (Amazon rejects punctuation and special characters)."
77003
+ };
77004
+ return {
77005
+ ok: true,
77006
+ name
77007
+ };
77008
+ }
77009
+ var StoredNameSchema = string();
77010
+ /**
77011
+ * The saved Alexa name in a per-device store blob, or null (never saved, or a
77012
+ * stored value that no longer validates: the device name is used rather than
77013
+ * pushing a name Amazon would refuse).
77014
+ */
77015
+ function readAlexaName(store) {
77016
+ const parsed = StoredNameSchema.safeParse(store[ALEXA_NAME_KEY]);
77017
+ if (!parsed.success) return null;
77018
+ const check = validateAlexaName(parsed.data);
77019
+ return check.ok ? check.name : null;
77020
+ }
77021
+ //#endregion
75214
77022
  //#region src/handlers/security-panel-arm-state.ts
75215
77023
  /**
75216
77024
  * The panel modes behind each Alexa arm state, PRIMARY first — the mirror of
@@ -75884,6 +77692,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
75884
77692
  deviceId: parentInfo.id,
75885
77693
  stableId: parentInfo.stableId,
75886
77694
  name: parentInfo.name,
77695
+ alexaName: deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id),
75887
77696
  type: String(device.type),
75888
77697
  capNames: parentInfo.capNames,
75889
77698
  role: parentInfo.role,
@@ -75942,11 +77751,12 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
75942
77751
  return [];
75943
77752
  });
75944
77753
  const objectDetectionClasses = deps.objectDetection === void 0 ? [] : await deps.objectDetection(parentInfo.id);
77754
+ const cameraName = (deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id)) ?? parentInfo.name;
75945
77755
  const out = [];
75946
77756
  out.push(buildParentCameraEndpoint({
75947
77757
  deviceId: parentInfo.id,
75948
77758
  stableId: parentInfo.stableId,
75949
- friendlyName: parentInfo.name,
77759
+ friendlyName: cameraName,
75950
77760
  isDoorbell,
75951
77761
  hasIntercom,
75952
77762
  hasMotion,
@@ -75960,7 +77770,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
75960
77770
  for (const detectionClass of classes) out.push(buildDetectionSensorEndpoint({
75961
77771
  deviceId: parentInfo.id,
75962
77772
  stableId: parentInfo.stableId,
75963
- cameraName: parentInfo.name,
77773
+ cameraName,
75964
77774
  detectionClass
75965
77775
  }));
75966
77776
  }
@@ -76454,6 +78264,36 @@ function validateInstanced(c, push) {
76454
78264
  if (c.semantics.stateMappings !== void 0) validateStateMappings(c, c.semantics.stateMappings, push, validMode);
76455
78265
  }
76456
78266
  //#endregion
78267
+ //#region src/endpoints/validate-thermostat.ts
78268
+ var PROPERTIES = new Set([
78269
+ "targetSetpoint",
78270
+ "lowerSetpoint",
78271
+ "upperSetpoint",
78272
+ "thermostatMode"
78273
+ ]);
78274
+ var MODES = new Set([
78275
+ "AUTO",
78276
+ "COOL",
78277
+ "HEAT",
78278
+ "ECO",
78279
+ "OFF"
78280
+ ]);
78281
+ function validateThermostat(c, push) {
78282
+ const supported = new Set((c.properties?.supported ?? []).map((p) => p.name));
78283
+ for (const name of supported) if (!PROPERTIES.has(name)) push(`properties.supported ${name} is not a thermostat property`);
78284
+ if (!supported.has("thermostatMode")) push("properties.supported must include thermostatMode");
78285
+ if (supported.has("lowerSetpoint") !== supported.has("upperSetpoint")) push("lowerSetpoint and upperSetpoint are declared together");
78286
+ if (!supported.has("targetSetpoint") && !supported.has("lowerSetpoint")) push("at least one setpoint property is required");
78287
+ const modes = c.configuration?.["supportedModes"];
78288
+ if (!Array.isArray(modes) || modes.length === 0) push("configuration.supportedModes must list at least one mode");
78289
+ else {
78290
+ for (const mode of modes) if (typeof mode !== "string" || !MODES.has(mode)) push(`supportedModes ${String(mode)} is not AUTO, COOL, HEAT, ECO or OFF`);
78291
+ if (new Set(modes).size !== modes.length) push("supportedModes values must be unique");
78292
+ }
78293
+ const scheduling = c.configuration?.["supportsScheduling"];
78294
+ if (scheduling !== void 0 && typeof scheduling !== "boolean") push("configuration.supportsScheduling must be a boolean");
78295
+ }
78296
+ //#endregion
76457
78297
  //#region src/endpoints/validate-endpoint.ts
76458
78298
  /**
76459
78299
  * Interfaces this skill may declare, with the version Amazon documents. An
@@ -76477,6 +78317,7 @@ var ALLOWED_INTERFACE_VERSIONS = {
76477
78317
  "Alexa.RangeController": "3",
76478
78318
  "Alexa.SecurityPanelController": "3",
76479
78319
  "Alexa.LockController": "3",
78320
+ "Alexa.ThermostatController": "3.1",
76480
78321
  "Alexa.SmartVision.SnapshotProvider": "1.1",
76481
78322
  "Alexa.SmartVision.ObjectDetectionSensor": "1.0",
76482
78323
  "Alexa.DataController": "1.0"
@@ -76516,6 +78357,8 @@ var DISPLAY_CATEGORIES = new Set([
76516
78357
  "SMARTLOCK",
76517
78358
  "INTERIOR_BLIND",
76518
78359
  "EXTERIOR_BLIND",
78360
+ "THERMOSTAT",
78361
+ "AIR_CONDITIONER",
76519
78362
  "DOOR",
76520
78363
  "GATE",
76521
78364
  "GARAGE_DOOR",
@@ -76594,6 +78437,7 @@ function validateCapability(endpointId, c, out, declared) {
76594
78437
  }
76595
78438
  }
76596
78439
  validateInstanced(c, push);
78440
+ if (c.interface === "Alexa.ThermostatController") validateThermostat(c, push);
76597
78441
  if (c.interface === "Alexa.SmartVision.ObjectDetectionSensor") validateObjectDetection(c, push);
76598
78442
  if (c.interface === "Alexa.DataController") validateDataController(c, declared, push);
76599
78443
  if (c.interface === "Alexa.SmartVision.SnapshotProvider") {
@@ -78524,20 +80368,27 @@ async function dispatchParsedDirective(rawBody, deps, callerIdentity) {
78524
80368
  status: 200,
78525
80369
  body: await handleSessionDisconnected(header, endpoint, payload, deps.rtcSession, callerIdentity)
78526
80370
  };
78527
- const adapterDirective = genericDirective(key);
78528
- if (adapterDirective !== void 0 && endpoint !== void 0) {
80371
+ if (genericDirectives(key).length > 0 && endpoint !== void 0) {
78529
80372
  const target = parseCameraEndpointId(endpoint.endpointId);
78530
- if (target.kind === "generic") return {
78531
- status: 200,
78532
- body: await adapterDirective.handle({
80373
+ if (target.kind === "generic") {
80374
+ const directiveContext = {
78533
80375
  header,
78534
80376
  endpoint,
78535
80377
  payload,
78536
80378
  deviceId: target.deviceId,
78537
80379
  deps,
78538
80380
  identity: callerIdentity
78539
- })
78540
- };
80381
+ };
80382
+ const adapterDirective = await resolveGenericDirective(directiveContext);
80383
+ if (adapterDirective === null) return {
80384
+ status: 200,
80385
+ body: errorEnvelope(header, "INVALID_DIRECTIVE", `${key}: no handler for this endpoint`)
80386
+ };
80387
+ return {
80388
+ status: 200,
80389
+ body: await adapterDirective.handle(directiveContext)
80390
+ };
80391
+ }
78541
80392
  }
78542
80393
  if (key === "Alexa.ModeController/SetMode") return {
78543
80394
  status: 200,
@@ -82680,6 +84531,7 @@ var SUPPORTED_KINDS = [
82680
84531
  DeviceType.Lock,
82681
84532
  DeviceType.Presence,
82682
84533
  DeviceType.Thermostat,
84534
+ DeviceType.Climate,
82683
84535
  DeviceType.Cover,
82684
84536
  DeviceType.Generic
82685
84537
  ];
@@ -82818,6 +84670,10 @@ var ExportAlexaAddon = class extends BaseAddon {
82818
84670
  ctx: this.ctx,
82819
84671
  logger: this.ctx.logger
82820
84672
  },
84673
+ climateController: {
84674
+ ctx: this.ctx,
84675
+ logger: this.ctx.logger
84676
+ },
82821
84677
  coverController: {
82822
84678
  ctx: this.ctx,
82823
84679
  logger: this.ctx.logger,
@@ -83074,6 +84930,7 @@ var ExportAlexaAddon = class extends BaseAddon {
83074
84930
  logger: this.ctx.logger,
83075
84931
  snapshots: this.config.snapshots,
83076
84932
  ...this.config.detectionSensors ? { detectionClasses: async (id) => effectiveSensorClasses(await classesOf(id), parseSensorSelection(await vision.readPrefs(id))) } : {},
84933
+ alexaName: async (id) => readAlexaName(await vision.readPrefs(id)),
83077
84934
  ...this.config.objectDetection ? { objectDetection: async (id) => vision.buildClasses(id, await classesOf(id)) } : {}
83078
84935
  });
83079
84936
  }
@@ -83641,12 +85498,28 @@ var ExportAlexaAddon = class extends BaseAddon {
83641
85498
  value: enabled,
83642
85499
  immediate: true
83643
85500
  },
85501
+ await this.alexaNameField(deviceId, enabledKey),
83644
85502
  ...kind === "camera" ? this.cameraOnlyExportFields(deviceId, settings, enabledKey) : [],
83645
85503
  ...kind === "camera" ? await this.cameraDetectionFields(deviceId, enabledKey) : []
83646
85504
  ]
83647
85505
  }]
83648
85506
  };
83649
85507
  }
85508
+ /** D875: the "Alexa name" field, with the validation error of the saved value. */
85509
+ async alexaNameField(deviceId, enabledKey) {
85510
+ const saved = readAlexaName(await this.vision.readPrefs(deviceId));
85511
+ return {
85512
+ type: "text",
85513
+ key: alexaNameKey(deviceId),
85514
+ label: "Alexa name",
85515
+ description: `What Alexa calls this device. Leave empty to use the device name. Letters, digits and spaces only (no punctuation), up to 256 characters. Renaming updates the same Alexa device; it does not create a duplicate.`,
85516
+ value: saved ?? "",
85517
+ showWhen: {
85518
+ field: enabledKey,
85519
+ equals: true
85520
+ }
85521
+ };
85522
+ }
83650
85523
  /**
83651
85524
  * The camera's detection fields: which classes become Alexa sensors (D778;
83652
85525
  * only the classes its pipeline produces are offered) and its object
@@ -83668,6 +85541,24 @@ var ExportAlexaAddon = class extends BaseAddon {
83668
85541
  })];
83669
85542
  }
83670
85543
  /**
85544
+ * D875: save the Alexa name from a device-settings patch. A name Amazon would
85545
+ * refuse throws BEFORE anything is written, so the form shows the reason and
85546
+ * the old name stays. True when the saved name changed.
85547
+ */
85548
+ async applyAlexaNamePatch(deviceId, patch) {
85549
+ const key = alexaNameKey(deviceId);
85550
+ if (!(key in patch)) return false;
85551
+ const check = validateAlexaName(patch[key]);
85552
+ if (!check.ok) throw new Error(`export-alexa: ${check.error}`);
85553
+ if (readAlexaName(await this.vision.readPrefs(deviceId)) === check.name) return false;
85554
+ await this.vision.writePrefs(deviceId, { [ALEXA_NAME_KEY]: check.name ?? void 0 });
85555
+ this.ctx.logger.info("export-alexa: Alexa name saved", {
85556
+ tags: { deviceId },
85557
+ meta: { name: check.name }
85558
+ });
85559
+ return true;
85560
+ }
85561
+ /**
83671
85562
  * Persist the camera's detection preferences from a device-settings patch
83672
85563
  * into this addon's per-device store. True when anything changed — the
83673
85564
  * camera's Alexa shape then needs a reconcile.
@@ -83732,6 +85623,7 @@ var ExportAlexaAddon = class extends BaseAddon {
83732
85623
  const current = this.findEntry(deviceId);
83733
85624
  const wasEnabled = current !== null;
83734
85625
  const prefsChanged = await this.applyDetectionPrefsPatch(deviceId, patch);
85626
+ const nameChanged = await this.applyAlexaNamePatch(deviceId, patch);
83735
85627
  const enabledKey = `alexa:${deviceId}:enabled`;
83736
85628
  const streamPreferenceKey = `alexa:${deviceId}:streamPreference`;
83737
85629
  const allowTranscodeKey = `alexa:${deviceId}:allowTranscode`;
@@ -83757,6 +85649,12 @@ var ExportAlexaAddon = class extends BaseAddon {
83757
85649
  const currentAllow = current?.settings?.allowTranscode !== false;
83758
85650
  await this.patchEntrySettings(deviceId, nextSettings);
83759
85651
  const sourceChanged = currentPref !== streamPreference || currentAllow !== allowTranscode;
85652
+ if (nameChanged && !sourceChanged) this.requireSyncEngine().pushDevice(deviceId).catch((err) => {
85653
+ this.ctx.logger.warn("export-alexa: push after an Alexa name change failed", {
85654
+ tags: { deviceId },
85655
+ meta: { error: errMsg(err) }
85656
+ });
85657
+ });
83760
85658
  if (prefsChanged && !sourceChanged) this.requireSyncEngine().reconcileDevice(deviceId).catch((err) => {
83761
85659
  this.ctx.logger.warn("export-alexa: re-sync after a detection preference change failed", {
83762
85660
  tags: { deviceId },