@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.
@@ -34,7 +34,7 @@ let node_path = require("node:path");
34
34
  node_path = __toESM(node_path, 1);
35
35
  require("node:child_process");
36
36
  require("node:zlib");
37
- //#region ../types/dist/event-category-C4hVnRo8.mjs
37
+ //#region ../types/dist/event-category-BvOFi6PY.mjs
38
38
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
39
39
  EventCategory["SystemBoot"] = "system.boot";
40
40
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -98,6 +98,24 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
98
98
  * consumes this category and maps it to the `addon-crash-loop` kind.
99
99
  */
100
100
  EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
101
+ /**
102
+ * A runner whose addon refused to start with a typed reason (D877) - parked
103
+ * in `refused`, never counted by the crash breaker, retried on operator
104
+ * action. One event per refusal.
105
+ */
106
+ EventCategory["AddonRunnerRefused"] = "addon.runner-refused";
107
+ /**
108
+ * A runner settled on a state (D877): `running` (READY), `crashed`, `failed` or
109
+ * `refused`. In-process telemetry for the boot check (re-evaluate while any
110
+ * runner is still pending) and the alert centre (close a recovered runner).
111
+ */
112
+ EventCategory["ProcessStateChanged"] = "process.state-changed";
113
+ /**
114
+ * The node's one-shot runner boot check (D877): planned runners against the
115
+ * process service, `degraded` when any is failed, refused or missing. The
116
+ * alert centre turns it into one alert and a direct critical push.
117
+ */
118
+ EventCategory["SystemRunnerCheck"] = "system.runner-check";
101
119
  EventCategory["AddonError"] = "addon.error";
102
120
  EventCategory["AddonPageReady"] = "addon.page-ready";
103
121
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -5422,7 +5440,7 @@ var ZodIssueCode = {
5422
5440
  var ZodFirstPartyTypeKind;
5423
5441
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5424
5442
  //#endregion
5425
- //#region ../types/dist/sleep-DRqxtc0C.mjs
5443
+ //#region ../types/dist/sleep-bZIhUvjd.mjs
5426
5444
  /**
5427
5445
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5428
5446
  * window to float samples (D455).
@@ -5539,7 +5557,7 @@ function isKeychainStoredField(field) {
5539
5557
  var SECRET_CLEAR = { $secretClear: true };
5540
5558
  /** The `'***'` token D634 redaction writes for non-admins. */
5541
5559
  var LEGACY_DEVICE_REDACTION = "***";
5542
- function isRecord$2(value) {
5560
+ function isRecord$5(value) {
5543
5561
  return typeof value === "object" && value !== null && !Array.isArray(value);
5544
5562
  }
5545
5563
  function makeSecretMarker(ref) {
@@ -5550,12 +5568,12 @@ function makeSecretMarker(ref) {
5550
5568
  }
5551
5569
  /** A stored marker: an object whose `$secret` is a non-empty string. */
5552
5570
  function isSecretMarker(value) {
5553
- if (!isRecord$2(value)) return false;
5571
+ if (!isRecord$5(value)) return false;
5554
5572
  const ref = value["$secret"];
5555
5573
  return typeof ref === "string" && ref.length > 0;
5556
5574
  }
5557
5575
  function isSecretClear(value) {
5558
- return isRecord$2(value) && value["$secretClear"] === true;
5576
+ return isRecord$5(value) && value["$secretClear"] === true;
5559
5577
  }
5560
5578
  /**
5561
5579
  * The UI view of a stored secret field value: a marker becomes "set", inline
@@ -6165,7 +6183,7 @@ var KeychainWriteRefusedError = class extends Error {
6165
6183
  }
6166
6184
  };
6167
6185
  var KEEP = { kind: "keep" };
6168
- function isRecord$1(value) {
6186
+ function isRecord$4(value) {
6169
6187
  return typeof value === "object" && value !== null && !Array.isArray(value);
6170
6188
  }
6171
6189
  function defaultMintId() {
@@ -6178,7 +6196,7 @@ function collectSecretMarkerRefs(value, out = []) {
6178
6196
  return out;
6179
6197
  }
6180
6198
  if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
6181
- else if (isRecord$1(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6199
+ else if (isRecord$4(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
6182
6200
  return out;
6183
6201
  }
6184
6202
  async function storedValueEquals(secrets, marker, candidate) {
@@ -6233,7 +6251,7 @@ function rowId(row, idKey) {
6233
6251
  return null;
6234
6252
  }
6235
6253
  async function writeArray(ctx, node, incoming, stored, fieldPath) {
6236
- const storedRows = Array.isArray(stored) ? stored.filter(isRecord$1) : [];
6254
+ const storedRows = Array.isArray(stored) ? stored.filter(isRecord$4) : [];
6237
6255
  if (!Array.isArray(incoming)) {
6238
6256
  ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
6239
6257
  return {
@@ -6249,7 +6267,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6249
6267
  const kept = /* @__PURE__ */ new Set();
6250
6268
  const rows = [];
6251
6269
  for (const row of incoming) {
6252
- if (!isRecord$1(row)) {
6270
+ if (!isRecord$4(row)) {
6253
6271
  rows.push(row);
6254
6272
  continue;
6255
6273
  }
@@ -6274,7 +6292,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6274
6292
  case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
6275
6293
  case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
6276
6294
  case "object": {
6277
- if (!isRecord$1(incoming)) {
6295
+ if (!isRecord$4(incoming)) {
6278
6296
  if (incoming === void 0) return KEEP;
6279
6297
  ctx.deletes.push(...collectSecretMarkerRefs(stored));
6280
6298
  return {
@@ -6282,7 +6300,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
6282
6300
  value: incoming
6283
6301
  };
6284
6302
  }
6285
- const storedObj = isRecord$1(stored) ? stored : {};
6303
+ const storedObj = isRecord$4(stored) ? stored : {};
6286
6304
  return {
6287
6305
  kind: "value",
6288
6306
  value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
@@ -6405,7 +6423,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
6405
6423
  value: items
6406
6424
  };
6407
6425
  }
6408
- if (isRecord$1(v)) {
6426
+ if (isRecord$4(v)) {
6409
6427
  const out = {};
6410
6428
  for (const [k, child] of Object.entries(v)) {
6411
6429
  const s = substitute(child);
@@ -6673,7 +6691,6 @@ function createHandle(raw, opts) {
6673
6691
  const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
6674
6692
  const readAddonStoreResult = raw.readAddonStoreResult;
6675
6693
  const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
6676
- const readDeviceStoreBatch = raw.readDeviceStoreBatch;
6677
6694
  const overrides = {
6678
6695
  readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
6679
6696
  ...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
@@ -6697,12 +6714,12 @@ function createHandle(raw, opts) {
6697
6714
  };
6698
6715
  } } : {},
6699
6716
  readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
6700
- ...readDeviceStoreBatch !== void 0 ? { readDeviceStoreBatch: async (deviceIds) => {
6701
- const rows = await readDeviceStoreBatch.call(raw, deviceIds);
6717
+ readDeviceStoreBatch: async (deviceIds) => {
6718
+ const rows = await raw.readDeviceStoreBatch(deviceIds);
6702
6719
  const out = /* @__PURE__ */ new Map();
6703
6720
  for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
6704
6721
  return out;
6705
- } } : {},
6722
+ },
6706
6723
  writeAddonStore: async (patch) => {
6707
6724
  await writeAddon(patch);
6708
6725
  },
@@ -7329,7 +7346,7 @@ var BaseAddon = class {
7329
7346
  if (!ctx) return;
7330
7347
  const addonId = ctx.id;
7331
7348
  setImmediate(() => {
7332
- ctx.api.addons?.restartAddon?.mutate({ addonId }).then(() => {
7349
+ ctx.api.addons.restartAddon.mutate({ addonId }).then(() => {
7333
7350
  ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: patchKeys.filter((k) => restartKeys.has(k)) } });
7334
7351
  }).catch((err) => {
7335
7352
  ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -7529,7 +7546,7 @@ var BaseAddon = class {
7529
7546
  return this._addonSecretTree;
7530
7547
  }
7531
7548
  deviceSecretTree() {
7532
- this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
7549
+ this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
7533
7550
  return this._deviceSecretTree;
7534
7551
  }
7535
7552
  /**
@@ -7605,42 +7622,135 @@ var BaseAddon = class {
7605
7622
  this[timerOf] = timer;
7606
7623
  }
7607
7624
  /**
7608
- * Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
7609
- * transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
7610
- * swap, or any race where the SqliteSettingsBackend is between shutdown and
7611
- * re-initialize) doesn't propagate up into `initialize()` and leave the
7625
+ * Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
7626
+ * settings-store outage doesn't propagate up into `initialize()` and leave the
7612
7627
  * addon permanently broken.
7613
7628
  *
7614
- * Retry only the two known infra fingerprints. Anything else propagates so
7615
- * real bugs surface immediately. After the budget expires we fall back to
7616
- * `{}` (defaults) — the addon's first successful patch will rehydrate.
7629
+ * Two kinds of outage, two budgets:
7630
+ * - the store's own restart fingerprints (`SqliteSettingsBackend not
7631
+ * initialized`, `provider not available`): a short budget, then the addon
7632
+ * falls back to `{}` (defaults) - its first successful patch rehydrates;
7633
+ * - a TRANSPORT failure (the hub is restarting while this runner boots:
7634
+ * `discovery deadline exceeded`, `parent could not route`, a refused or
7635
+ * timed-out connection). Since D870 a runner's settings read THROWS instead
7636
+ * of answering `{}`, so without a budget a hub restart ends in a respawn
7637
+ * loop. A long, bounded budget; only after it the error propagates - the
7638
+ * addon never runs on defaults for a store it could not ask.
7639
+ * Anything else propagates immediately so real bugs surface.
7617
7640
  */
7618
7641
  async readAddonStoreWithRetry() {
7619
7642
  const settings = this.rawSettings();
7620
7643
  if (!settings) return {};
7621
- const delaysMs = [
7622
- 150,
7623
- 350,
7624
- 600,
7625
- 900
7626
- ];
7627
7644
  let lastErr;
7628
- for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7645
+ for (let attempt = 0;; attempt++) try {
7629
7646
  const stored = await settings.readAddonStore() ?? {};
7630
7647
  this.settingsStoreReady = true;
7631
7648
  return stored;
7632
7649
  } catch (err) {
7633
7650
  lastErr = err;
7634
- const msg = err instanceof Error ? err.message : String(err);
7635
- if (!(msg.includes("SqliteSettingsBackend not initialized") || msg.includes("provider not available"))) throw err;
7636
- if (attempt === delaysMs.length) break;
7637
- await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7651
+ const kind = classifyStoreError(err);
7652
+ if (kind === "permanent") throw err;
7653
+ const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
7654
+ if (delay === void 0) {
7655
+ if (kind === "transport") throw err;
7656
+ break;
7657
+ }
7658
+ await new Promise((r) => setTimeout(r, delay));
7638
7659
  }
7639
7660
  this.settingsStoreReady = false;
7640
7661
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7641
7662
  return {};
7642
7663
  }
7643
7664
  };
7665
+ /** The settings store's own restart fingerprints. */
7666
+ var RESTART_RETRY_DELAYS_MS = [
7667
+ 150,
7668
+ 350,
7669
+ 600,
7670
+ 900
7671
+ ];
7672
+ /** A hub restarting under a booting runner: ~100 s in total. */
7673
+ var TRANSPORT_RETRY_DELAYS_MS = [
7674
+ 150,
7675
+ 350,
7676
+ 600,
7677
+ 900,
7678
+ 2e3,
7679
+ 5e3,
7680
+ 1e4,
7681
+ 2e4,
7682
+ 3e4,
7683
+ 3e4
7684
+ ];
7685
+ var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
7686
+ /** Node / socket error codes of a connection that did not happen or broke. */
7687
+ var TRANSPORT_CODES = new Set([
7688
+ "ECONNREFUSED",
7689
+ "ECONNRESET",
7690
+ "ETIMEDOUT",
7691
+ "EPIPE",
7692
+ "EHOSTUNREACH",
7693
+ "ENETUNREACH"
7694
+ ]);
7695
+ /** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
7696
+ var UNAVAILABLE_TRPC_CODES = new Set([
7697
+ "TIMEOUT",
7698
+ "SERVICE_UNAVAILABLE",
7699
+ "BAD_GATEWAY",
7700
+ "GATEWAY_TIMEOUT"
7701
+ ]);
7702
+ /** The exact phrases OUR transport raises when it cannot reach the hub. */
7703
+ var TRANSPORT_PHRASES = [
7704
+ "discovery deadline exceeded",
7705
+ "parent could not route child unowned cap call",
7706
+ "UDS request timed out after",
7707
+ "channel closed"
7708
+ ];
7709
+ /** Moleculer error `type`s of a call that could not be served right now. */
7710
+ var MOLECULER_TRANSIENT_TYPES = new Set([
7711
+ "REQUEST_TIMEOUT",
7712
+ "SERVICE_NOT_FOUND",
7713
+ "SERVICE_NOT_AVAILABLE",
7714
+ "REQUEST_REJECTED",
7715
+ "BROKER_DISCONNECTED"
7716
+ ]);
7717
+ function stringProp(value, key) {
7718
+ if (typeof value !== "object" || value === null) return void 0;
7719
+ const found = Reflect.get(value, key);
7720
+ return typeof found === "string" ? found : void 0;
7721
+ }
7722
+ /** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
7723
+ function errorCode(err) {
7724
+ const out = {};
7725
+ const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
7726
+ const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
7727
+ const node = stringProp(err, "code") ?? stringProp(cause, "code");
7728
+ if (node !== void 0) out.node = node;
7729
+ const trpc = stringProp(data, "code");
7730
+ if (trpc !== void 0) out.trpc = trpc;
7731
+ return out;
7732
+ }
7733
+ /**
7734
+ * Is a settings read failure worth retrying, and with which budget? Only
7735
+ * EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
7736
+ * one of the exact phrases our own transport raises. A message that merely
7737
+ * contains "timed out" or "unreachable" is not evidence of anything, and
7738
+ * `settings store not wired` is a wiring bug that retrying cannot fix.
7739
+ */
7740
+ function classifyStoreError(err) {
7741
+ const message = err instanceof Error ? err.message : String(err);
7742
+ if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
7743
+ const code = errorCode(err);
7744
+ if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
7745
+ if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
7746
+ if (typeof err === "object" && err !== null) {
7747
+ if (Reflect.get(err, "retryable") === true) return "transport";
7748
+ const type = stringProp(err, "type");
7749
+ if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
7750
+ }
7751
+ if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
7752
+ return "permanent";
7753
+ }
7644
7754
  /**
7645
7755
  * Collect the keys of every field marked `perNode: true`, recursing into
7646
7756
  * layout containers (`group` fields and `sub-tabs` tabs) the same way
@@ -7730,6 +7840,89 @@ var PeerBytesTicketSchema = object({
7730
7840
  */
7731
7841
  declaredBytes: number().int().nonnegative().nullable()
7732
7842
  });
7843
+ /** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
7844
+ /** The files a kind may live in. `history` is device-history's (D850), never records. */
7845
+ var RECORD_DATABASES = ["settings", "events"];
7846
+ /** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
7847
+ var RECORD_CLASSES = [
7848
+ "config",
7849
+ "ledger",
7850
+ "audit",
7851
+ "mirror",
7852
+ "ephemeral"
7853
+ ];
7854
+ var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
7855
+ var RECORD_MAX_DATA_BYTES = 1024 * 1024;
7856
+ var RECORD_BATCH_MAX = 1e3;
7857
+ var RECORD_QUERY_MAX = 1e3;
7858
+ /**
7859
+ * Record kinds (D881): the declaration an addon makes of one kind of fact, the
7860
+ * Zod row schema that describes it, and the helper that turns the two into the
7861
+ * JSON the engine stores.
7862
+ *
7863
+ * A kind is declared ONCE, next to the code that owns it, and the engine's
7864
+ * reconciler creates its indexes - nothing creates DDL at runtime from a write.
7865
+ * Everything the generic machinery needs (secret fields, the entry descriptor)
7866
+ * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
7867
+ * never a field name (D879).
7868
+ */
7869
+ /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
7870
+ var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
7871
+ var RecordIndexSchema = object({
7872
+ name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
7873
+ fields: array(RecordFieldPathSchema).min(1).max(4),
7874
+ unique: boolean().optional()
7875
+ }).strict();
7876
+ var RecordRetentionSchema = union([
7877
+ object({
7878
+ ttlMs: number().int().positive(),
7879
+ from: _enum(["at", "write"])
7880
+ }).strict(),
7881
+ object({
7882
+ maxRows: number().int().positive().optional(),
7883
+ maxRowsPerDevice: number().int().positive().optional()
7884
+ }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
7885
+ object({
7886
+ owner: string().min(1),
7887
+ reason: string().min(1)
7888
+ }).strict(),
7889
+ object({ none: string().min(1) }).strict()
7890
+ ]);
7891
+ /** The descriptor a generic entries UI reads (D881 "generic entries"). */
7892
+ var EntryDescriptorSchema = object({
7893
+ title: string().min(1),
7894
+ /** The record body field shown as the entry's name. */
7895
+ nameField: string().min(1),
7896
+ /** JSON Schema of the entry body; `secret: true` marks a password field. */
7897
+ fields: record(string(), unknown())
7898
+ }).strict();
7899
+ /** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
7900
+ var RecordKindDeclarationSchema = object({
7901
+ database: _enum(RECORD_DATABASES).default("settings"),
7902
+ class: _enum(RECORD_CLASSES),
7903
+ writeTrigger: string().min(1),
7904
+ device: _enum([
7905
+ "required",
7906
+ "optional",
7907
+ "none"
7908
+ ]).default("none"),
7909
+ at: _enum(["required", "none"]).default("none"),
7910
+ indexes: array(RecordIndexSchema).max(8).default([]),
7911
+ retention: RecordRetentionSchema,
7912
+ deviceRemoval: _enum(["delete", "none"]).default("none"),
7913
+ maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
7914
+ /** Derived from the Zod schema; never written by hand. */
7915
+ secretPaths: array(string().min(1)).default([]),
7916
+ /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
7917
+ secretArrayKeys: record(string(), string()).default({}),
7918
+ /** The legacy table this kind adopts from; the boot guard reads it (D853: no migration code). */
7919
+ adoptsFrom: string().min(1).optional(),
7920
+ /** Kinds that must agree through `transact` sit in one file under one family name. */
7921
+ transactFamily: string().min(1).optional(),
7922
+ entry: EntryDescriptorSchema.optional()
7923
+ }).strict();
7924
+ /** The kind names: lower-case, dash separated, short enough for an index name. */
7925
+ var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7733
7926
  /** Shared Zod schemas used across streaming capabilities. */
7734
7927
  var CamProfileSchema = _enum([
7735
7928
  "high",
@@ -8217,7 +8410,7 @@ object({
8217
8410
  params: record(string(), union([string(), number()])).optional()
8218
8411
  });
8219
8412
  //#endregion
8220
- //#region ../types/dist/err-msg-DLerKiVO.mjs
8413
+ //#region ../types/dist/err-msg--2RrYiLO.mjs
8221
8414
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
8222
8415
  DeviceType["Camera"] = "camera";
8223
8416
  DeviceType["Hub"] = "hub";
@@ -8486,6 +8679,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
8486
8679
  /** Virtual HA toggle (input_boolean.*) — distinguishable from a
8487
8680
  * real Switch device for UI rendering / export adapters. */
8488
8681
  DeviceRole["BinaryHelper"] = "binary-helper";
8682
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
8683
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
8684
+ DeviceRole["Outlet"] = "outlet";
8489
8685
  /** Generic motion / occupancy / moving event source. Distinct from
8490
8686
  * the camera accessory PirSensor role: that one is a camera child;
8491
8687
  * this is a standalone HA / 3rd-party motion sensor. */
@@ -8563,7 +8759,7 @@ function errMsg(err) {
8563
8759
  return String(err);
8564
8760
  }
8565
8761
  //#endregion
8566
- //#region ../types/dist/composition-EQ9Ok0Ee.mjs
8762
+ //#region ../types/dist/composition-DOwQB_Ua.mjs
8567
8763
  /**
8568
8764
  * The ffmpeg build every CamStack node downloads, per platform and arch.
8569
8765
  *
@@ -12024,11 +12220,10 @@ var ClipModelMetaSchema = object({
12024
12220
  /**
12025
12221
  * The `vector-store` index this model's vectors live in. ONE feature space
12026
12222
  * per index: two models share an index only when their vectors are ranked
12027
- * against each other, which for CLIP means never across families. MobileCLIP
12028
- * S1 and S2 share `object-clip` for history (separated by the row's
12029
- * `modelId`); any other model gets its own.
12223
+ * against each other, which for CLIP means never across families. Every
12224
+ * model gets its own.
12030
12225
  */
12031
- vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
12226
+ vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
12032
12227
  /**
12033
12228
  * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
12034
12229
  * must be unique per tokenizer: every model's siblings land in ONE flat
@@ -15681,6 +15876,41 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
15681
15876
  keyField: string()
15682
15877
  }).optional()
15683
15878
  })) });
15879
+ /**
15880
+ * The report of a one-shot "move this owner's plaintext secrets into the
15881
+ * keychain" run (D878). COUNTS only - never a value, never a ref.
15882
+ *
15883
+ * The owner runs it on its own rows, through its own write path; an operator
15884
+ * script (`scripts/hub/migrate-raw-secrets.ts`) calls the owner's cap method.
15885
+ * Dry run (`apply: false`) reports what WOULD move and changes nothing.
15886
+ */
15887
+ var RawSecretMigrationInputSchema = object({
15888
+ /** `false` (the default of the script) = report only. */
15889
+ apply: boolean() });
15890
+ var RawSecretMigrationReportSchema = object({
15891
+ apply: boolean(),
15892
+ /** Rows that can carry a secret (a profile, an enrolment). */
15893
+ rows: number().int(),
15894
+ /** Rows whose secret was plaintext when the run started. */
15895
+ raw: number().int(),
15896
+ /** Rows that already held a keychain marker when the run started. */
15897
+ markers: number().int(),
15898
+ /** Plaintext rows moved by THIS run (always 0 on a dry run). */
15899
+ migrated: number().int(),
15900
+ /** Plaintext rows left in place because the move or its read-back failed. */
15901
+ failed: number().int(),
15902
+ /** After the run: rows still holding plaintext. */
15903
+ remainingRaw: number().int(),
15904
+ /** After the run: markers the keychain could not resolve right now. */
15905
+ unresolvedMarkers: number().int(),
15906
+ /**
15907
+ * A second, redundant copy of the secrets' rows that the run found (and, on
15908
+ * `apply`, deleted after verifying the first). Owners with a single copy omit it.
15909
+ */
15910
+ redundantCopies: number().int().optional(),
15911
+ /** True when nothing is raw and every marker resolves. */
15912
+ verified: boolean()
15913
+ });
15684
15914
  /** Where a block runs. The operator chooses — a block driving a device on an
15685
15915
  * agent is the reason placement is not fixed to the hub. */
15686
15916
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
@@ -15907,6 +16137,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
15907
16137
  }), SettingsUpdateResultSchema, {
15908
16138
  kind: "mutation",
15909
16139
  auth: "admin"
16140
+ }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
16141
+ kind: "mutation",
16142
+ auth: "admin"
15910
16143
  }), method(object({}), object({ libs: array(object({
15911
16144
  filePath: string(),
15912
16145
  content: string()
@@ -16170,6 +16403,347 @@ id: union([string(), number()]) }), {
16170
16403
  auth: "admin"
16171
16404
  });
16172
16405
  /**
16406
+ * records - an addon's own facts and documents (D881): ledgers, audit rows,
16407
+ * config documents, mirrors. The third storage API next to `ctx.settings`
16408
+ * (operator intent, D870) and device-history (numbers over time, D850).
16409
+ *
16410
+ * Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
16411
+ * crossed a process and stated by a hub in-process caller (`ctx.records` does
16412
+ * that with the addon id closed over). It is deliberately NOT in any input
16413
+ * schema, so a session can never supply it, and a call with no stamp is refused
16414
+ * `records/no-caller`. A kind is readable and writable only by its owner; other
16415
+ * addons go through the owner's capability.
16416
+ *
16417
+ * Every read is three-way: a row, `null`, or the call THROWS. A failed read is
16418
+ * never `[]`. Secret fields are never on the wire: a read shows the
16419
+ * redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
16420
+ * resolves itself, and a write may carry only a keychain marker bound to the
16421
+ * row's own ref.
16422
+ *
16423
+ * Internal: addons reach it through `ctx.records`, never directly.
16424
+ */
16425
+ var RecordIdSchema = string().min(1).max(256);
16426
+ /** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
16427
+ var RecordBodySchema = record(string(), unknown());
16428
+ var RecordInputSchema = object({
16429
+ id: RecordIdSchema,
16430
+ /** The device this row belongs to (D865 removal, per-device reads). */
16431
+ deviceId: number().int().optional(),
16432
+ /** The domain clock, ms: observed / created / opened. */
16433
+ at: number().int().optional(),
16434
+ /** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
16435
+ expiresAt: number().int().optional(),
16436
+ data: RecordBodySchema
16437
+ });
16438
+ var RecordViewSchema = object({
16439
+ id: string(),
16440
+ deviceId: number().int().nullable(),
16441
+ at: number().nullable(),
16442
+ expiresAt: number().nullable(),
16443
+ rev: number().int(),
16444
+ updatedAt: number(),
16445
+ data: RecordBodySchema
16446
+ });
16447
+ /** Declared index fields only. Anything else does not compile, by design. */
16448
+ var RecordWhereSchema = record(RecordFieldPathSchema, union([
16449
+ string(),
16450
+ number(),
16451
+ boolean(),
16452
+ object({ in: array(union([
16453
+ string(),
16454
+ number(),
16455
+ boolean()
16456
+ ])).min(1).max(RECORD_BATCH_MAX) }),
16457
+ object({
16458
+ gt: union([string(), number()]).optional(),
16459
+ gte: union([string(), number()]).optional(),
16460
+ lt: union([string(), number()]).optional(),
16461
+ lte: union([string(), number()]).optional()
16462
+ }).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
16463
+ ]));
16464
+ var KindShape = { kind: RecordKindNameSchema };
16465
+ var TimeWindowShape = {
16466
+ deviceId: number().int().optional(),
16467
+ atFrom: number().optional(),
16468
+ atTo: number().optional()
16469
+ };
16470
+ var RecordQueryInputSchema = object({
16471
+ ...KindShape,
16472
+ ...TimeWindowShape,
16473
+ where: RecordWhereSchema.optional(),
16474
+ /** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
16475
+ order: _enum(["at", "id"]).optional(),
16476
+ desc: boolean().optional(),
16477
+ limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
16478
+ /** Opaque keyset cursor from a previous answer. There is no offset. */
16479
+ cursor: string().min(1).optional()
16480
+ });
16481
+ var RecordQueryResultSchema = object({
16482
+ records: array(RecordViewSchema),
16483
+ nextCursor: string().nullable()
16484
+ });
16485
+ var RecordKindStateSchema = _enum([
16486
+ "created",
16487
+ "unchanged",
16488
+ "updated"
16489
+ ]);
16490
+ var DeclareKindResultSchema = object({
16491
+ kindId: number().int(),
16492
+ state: RecordKindStateSchema,
16493
+ createdIndexes: array(string()),
16494
+ /** The boot guard: the kind is empty while its legacy table still holds rows. */
16495
+ adoptionPending: object({
16496
+ legacyTable: string(),
16497
+ legacyRows: number().int()
16498
+ }).nullable()
16499
+ });
16500
+ /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
16501
+ var SecretRefsList = array(string());
16502
+ var RecordSecretRefsSchema = record(string(), record(string(), string()));
16503
+ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
16504
+ kind: "mutation",
16505
+ auth: "admin"
16506
+ }), method(object({
16507
+ ...KindShape,
16508
+ id: RecordIdSchema
16509
+ }), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
16510
+ ...KindShape,
16511
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
16512
+ }), array(RecordViewSchema), { auth: "admin" }), method(object({
16513
+ ...KindShape,
16514
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
16515
+ }), RecordSecretRefsSchema, { auth: "admin" }), method(object({
16516
+ ...KindShape,
16517
+ record: RecordInputSchema,
16518
+ /** Compare-and-set. `0` means "the row must not exist". */
16519
+ ifRev: number().int().min(0).optional()
16520
+ }), object({
16521
+ rev: number().int(),
16522
+ releasedRefs: SecretRefsList
16523
+ }), {
16524
+ kind: "mutation",
16525
+ auth: "admin"
16526
+ }), method(object({
16527
+ ...KindShape,
16528
+ records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
16529
+ }), object({
16530
+ count: number().int(),
16531
+ releasedRefs: SecretRefsList
16532
+ }), {
16533
+ kind: "mutation",
16534
+ auth: "admin"
16535
+ }), method(object({
16536
+ ...KindShape,
16537
+ id: RecordIdSchema,
16538
+ data: RecordBodySchema,
16539
+ at: number().int().nullable().optional(),
16540
+ expiresAt: number().int().nullable().optional(),
16541
+ ifRev: number().int().min(1).optional()
16542
+ }), object({
16543
+ rev: number().int(),
16544
+ releasedRefs: SecretRefsList
16545
+ }), {
16546
+ kind: "mutation",
16547
+ auth: "admin"
16548
+ }), method(object({
16549
+ ...KindShape,
16550
+ id: RecordIdSchema
16551
+ }), object({
16552
+ deleted: boolean(),
16553
+ secretRefs: SecretRefsList
16554
+ }), {
16555
+ kind: "mutation",
16556
+ auth: "admin"
16557
+ }), method(object({
16558
+ ...KindShape,
16559
+ ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
16560
+ }), object({
16561
+ deleted: number().int(),
16562
+ secretRefs: SecretRefsList
16563
+ }), {
16564
+ kind: "mutation",
16565
+ auth: "admin"
16566
+ }), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
16567
+ ...KindShape,
16568
+ ...TimeWindowShape,
16569
+ where: RecordWhereSchema.optional()
16570
+ }), object({ count: number().int() }), { auth: "admin" }), method(object({
16571
+ ...KindShape,
16572
+ deviceId: number().int().optional(),
16573
+ from: number(),
16574
+ to: number(),
16575
+ bucketMs: number().int().positive()
16576
+ }), object({ buckets: array(object({
16577
+ bucketStart: number(),
16578
+ count: number().int()
16579
+ })) }), { auth: "admin" }), method(object({
16580
+ ...KindShape,
16581
+ op: _enum([
16582
+ "sum",
16583
+ "min",
16584
+ "max",
16585
+ "count"
16586
+ ]),
16587
+ field: RecordFieldPathSchema.optional(),
16588
+ groupBy: RecordFieldPathSchema.optional(),
16589
+ where: RecordWhereSchema.optional()
16590
+ }), object({ rows: array(object({
16591
+ group: union([
16592
+ string(),
16593
+ number(),
16594
+ _null()
16595
+ ]),
16596
+ value: number().nullable()
16597
+ })) }), { auth: "admin" }), method(object({
16598
+ ...KindShape,
16599
+ deviceId: number().int().optional(),
16600
+ atBefore: number().optional(),
16601
+ where: RecordWhereSchema.optional()
16602
+ }), object({
16603
+ deleted: number().int(),
16604
+ secretRefs: SecretRefsList
16605
+ }), {
16606
+ kind: "mutation",
16607
+ auth: "admin"
16608
+ }), method(object({
16609
+ deviceId: number().int(),
16610
+ kinds: array(RecordKindNameSchema).optional()
16611
+ }), object({
16612
+ deleted: record(string(), number().int()),
16613
+ secretRefs: SecretRefsList
16614
+ }), {
16615
+ kind: "mutation",
16616
+ auth: "admin"
16617
+ }), method(object(KindShape), object({ id: number().int() }), {
16618
+ kind: "mutation",
16619
+ auth: "admin"
16620
+ });
16621
+ /** One table of the settings database. */
16622
+ var StorageTableSchema = object({
16623
+ /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
16624
+ name: string(),
16625
+ /** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
16626
+ owner: string().nullable(),
16627
+ rows: number().int().nonnegative(),
16628
+ /** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
16629
+ bytes: number().int().nonnegative().nullable(),
16630
+ /** Declared by an addon since this engine process started (a running addon claims it). */
16631
+ declaredThisBoot: boolean()
16632
+ });
16633
+ /** One declared record kind (D881) as System -> Data shows it. */
16634
+ var StorageKindSchema = object({
16635
+ owner: string(),
16636
+ kind: string(),
16637
+ database: _enum(["settings", "events"]),
16638
+ class: string(),
16639
+ state: _enum([
16640
+ "declared",
16641
+ "orphaned",
16642
+ "dropped"
16643
+ ]),
16644
+ /** `null` when the caller asked for no counts (`counts: false`). */
16645
+ rows: number().int().nonnegative().nullable(),
16646
+ /** Rows x the average size of a sample; null for an empty kind. */
16647
+ bytesEstimate: number().int().nonnegative().nullable(),
16648
+ writeTrigger: string(),
16649
+ retention: RecordRetentionSchema,
16650
+ /** `true` when the kind is an entry kind (the generic entries UI lists it). */
16651
+ entry: boolean(),
16652
+ /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
16653
+ declaration: RecordKindDeclarationSchema
16654
+ });
16655
+ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
16656
+ collection: string().min(1),
16657
+ expectRows: number().int().nonnegative()
16658
+ }), object({
16659
+ dropped: literal(true),
16660
+ rows: number().int().nonnegative()
16661
+ }), {
16662
+ kind: "mutation",
16663
+ auth: "admin"
16664
+ }), method(object({
16665
+ owner: string().min(1).optional(),
16666
+ /** `false` skips the per-kind row count (a catalog read only). Default true. */
16667
+ counts: boolean().optional()
16668
+ }), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
16669
+ owner: string().min(1),
16670
+ kind: RecordKindNameSchema
16671
+ }), RecordQueryResultSchema, { auth: "admin" }), method(object({
16672
+ owner: string().min(1),
16673
+ kind: RecordKindNameSchema,
16674
+ records: array(RecordInputSchema).max(RECORD_BATCH_MAX),
16675
+ /** The LAST batch: the copy is complete, the boot guard stops asking. */
16676
+ complete: boolean().optional()
16677
+ }).refine((input) => input.records.length > 0 || input.complete === true, { message: "records must be non-empty unless complete is true" }), object({ adopted: number().int() }), {
16678
+ kind: "mutation",
16679
+ auth: "admin"
16680
+ }), method(object({
16681
+ collection: string().min(1),
16682
+ cursor: string().optional(),
16683
+ limit: number().int().min(1).max(1e3).optional()
16684
+ }), object({
16685
+ rows: array(object({
16686
+ id: string(),
16687
+ data: record(string(), unknown())
16688
+ })),
16689
+ nextCursor: string().nullable()
16690
+ }), { auth: "admin" }), method(object({ collection: string().min(1) }), object({ adopted: boolean() }), { auth: "admin" });
16691
+ /**
16692
+ * record-entries - the ONE admin surface for every list of user-created entries
16693
+ * (D881): notification targets, brokers, LLM profiles, anything an addon
16694
+ * declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
16695
+ *
16696
+ * The UI is generic. `listEntryKinds` hands it, per kind, the JSON Schema of
16697
+ * the entry body with `secret: true` on every credential field, so a password
16698
+ * field is derived from the schema and never from a name. `putEntry` takes the
16699
+ * redaction token for an untouched secret (the stored value is kept), a string
16700
+ * for a new one, and the clear instruction to remove one. A read never carries
16701
+ * a secret value, and a locked keychain refuses the write with
16702
+ * `records/secret-unavailable` instead of storing plaintext.
16703
+ *
16704
+ * Hub-only, served in-process (it acts for each entry's owner on the keychain).
16705
+ */
16706
+ var EntryKindInfoSchema = object({
16707
+ owner: string(),
16708
+ kind: RecordKindNameSchema,
16709
+ title: string(),
16710
+ nameField: string(),
16711
+ /** JSON Schema of the body; `secret: true` marks a password field. */
16712
+ fields: record(string(), unknown()),
16713
+ secretPaths: array(string()),
16714
+ rows: number().int().nonnegative()
16715
+ });
16716
+ var EntryViewSchema = object({
16717
+ id: RecordIdSchema,
16718
+ name: string(),
16719
+ rev: number().int(),
16720
+ updatedAt: number(),
16721
+ /** Secret fields hold the redaction token. */
16722
+ data: RecordBodySchema
16723
+ });
16724
+ var EntryAddress = {
16725
+ owner: string().min(1),
16726
+ kind: RecordKindNameSchema
16727
+ };
16728
+ method(object({}), object({ kinds: array(EntryKindInfoSchema) }), { auth: "admin" }), method(object({ ...EntryAddress }), object({ entries: array(EntryViewSchema) }), { auth: "admin" }), method(object({
16729
+ ...EntryAddress,
16730
+ id: RecordIdSchema.optional(),
16731
+ data: RecordBodySchema,
16732
+ ifRev: number().int().min(1).optional()
16733
+ }), object({
16734
+ id: string(),
16735
+ rev: number().int()
16736
+ }), {
16737
+ kind: "mutation",
16738
+ auth: "admin"
16739
+ }), method(object({
16740
+ ...EntryAddress,
16741
+ id: RecordIdSchema
16742
+ }), object({ deleted: boolean() }), {
16743
+ kind: "mutation",
16744
+ auth: "admin"
16745
+ });
16746
+ /**
16173
16747
  * `keychain` — the hub's encrypted store of secret values (D787).
16174
16748
  *
16175
16749
  * Settings blobs never hold a secret: they hold a marker
@@ -16710,6 +17284,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
16710
17284
  kind: "mutation",
16711
17285
  auth: "admin",
16712
17286
  caller: "required"
17287
+ }), method(object({
17288
+ ref: string().min(1),
17289
+ value: string().min(1)
17290
+ }), object({
17291
+ ref: string(),
17292
+ version: number().int()
17293
+ }), {
17294
+ kind: "mutation",
17295
+ auth: "admin",
17296
+ caller: "required"
16713
17297
  }), method(object({ refs: array(string().min(1)).max(1e3) }), object({
16714
17298
  deleted: array(string()).readonly(),
16715
17299
  kept: array(object({
@@ -39204,6 +39788,9 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
39204
39788
  }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
39205
39789
  kind: "mutation",
39206
39790
  auth: "admin"
39791
+ }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
39792
+ kind: "mutation",
39793
+ auth: "admin"
39207
39794
  }), method(ProfileRefInputSchema, _void(), {
39208
39795
  kind: "mutation",
39209
39796
  auth: "admin"
@@ -49643,6 +50230,41 @@ var ServerRollbackInfoSchema = object({
49643
50230
  atMs: number(),
49644
50231
  reason: string()
49645
50232
  });
50233
+ /**
50234
+ * The runner boot check (D877): the runners this node PLANNED (hub: one per
50235
+ * `resolveRunnerId` over its forked addons; agent: the runners its process
50236
+ * service holds, because only the hub plans) against the states the process
50237
+ * service reports. Kept in memory, recomputed by the same one-shot check after
50238
+ * every boot, served on `getServerPackageStatus`.
50239
+ *
50240
+ * `failed` is the D6 breaker's terminal state, `refused` a deterministic Init
50241
+ * refusal (an addon that said "I will not start, and here is why"), `missing` a
50242
+ * planned runner the process service has no entry for.
50243
+ */
50244
+ var RunnerRefusalSchema = object({
50245
+ runnerId: string(),
50246
+ addonIds: array(string()),
50247
+ reason: string()
50248
+ });
50249
+ var RunnerBootCheckSchema = object({
50250
+ checkedAtMs: number(),
50251
+ /** Runners planned for this node. */
50252
+ expected: number(),
50253
+ /** Planned runners whose process is `running`. */
50254
+ running: number(),
50255
+ failed: array(string()),
50256
+ refused: array(RunnerRefusalSchema),
50257
+ missing: array(string()),
50258
+ /**
50259
+ * Runners still on their way: `starting`, or `crashed` waiting on a backoff
50260
+ * respawn. The check is FINAL only when this is empty; until then nothing is
50261
+ * pushed and the verdict is re-evaluated on the next runner state change.
50262
+ * Optional for version skew (absent = settled).
50263
+ */
50264
+ pending: array(string()).optional(),
50265
+ /** True when the plan is the hub's (`missing` is then meaningful); false on an agent. */
50266
+ planKnown: boolean()
50267
+ });
49646
50268
  var ServerPackageStatusSchema = object({
49647
50269
  /** Root package name (`@camstack/server` on the hub). */
49648
50270
  packageName: string(),
@@ -49705,7 +50327,14 @@ var ServerPackageStatusSchema = object({
49705
50327
  * for version skew: an older provider's payload simply omits it, and an
49706
50328
  * absent wrapper is unknown — never `native`.
49707
50329
  */
49708
- wrapper: WrapperIdentitySchema.optional()
50330
+ wrapper: WrapperIdentitySchema.optional(),
50331
+ /**
50332
+ * What this node PLANNED to run against what its process service reports
50333
+ * (D877). Absent until the one-shot boot check has run (~60 s after boot) and
50334
+ * on a provider that predates it. Absent is "not checked yet", never "all
50335
+ * well": a consumer that needs the answer treats it as pending.
50336
+ */
50337
+ runners: RunnerBootCheckSchema.optional()
49709
50338
  });
49710
50339
  var ServerUpdateCheckResultSchema = object({
49711
50340
  packageName: string(),
@@ -49756,6 +50385,13 @@ var ServerUpdateOutcomeSchema = _enum([
49756
50385
  "refused",
49757
50386
  "failed"
49758
50387
  ]);
50388
+ /** One addon the pre-flight found the target version would refuse (D877). */
50389
+ var UpdateBlockerSchema = object({
50390
+ addonId: string(),
50391
+ reason: string(),
50392
+ /** What the operator does about it. */
50393
+ remedy: string()
50394
+ });
49759
50395
  var ServerUpdateActionResultSchema = object({
49760
50396
  accepted: boolean(),
49761
50397
  targetVersion: string().nullable(),
@@ -49773,14 +50409,27 @@ var ServerUpdateActionResultSchema = object({
49773
50409
  * The failure itself, verbatim enough to act on (the npm output for a
49774
50410
  * staging install). Non-null only for `failed`.
49775
50411
  */
49776
- error: string().nullable().optional()
50412
+ error: string().nullable().optional(),
50413
+ /**
50414
+ * The addons the target version would refuse to start on this node's live
50415
+ * store (D877), present when the pre-flight refused the update
50416
+ * (`reason: 'addon-would-refuse'`). Optional for version skew.
50417
+ */
50418
+ blockers: array(UpdateBlockerSchema).optional()
49777
50419
  });
49778
50420
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
49779
50421
  kind: "mutation",
49780
50422
  auth: "admin"
49781
- }), method(object({
49782
- /** Explicit target version; omitted = latest from the registry. */
49783
- version: string().optional() }), ServerUpdateActionResultSchema, {
50423
+ }), method(object({
50424
+ /** Explicit target version; omitted = latest from the registry. */
50425
+ version: string().optional(),
50426
+ /**
50427
+ * Apply even though the pre-flight (D877) found addons the target would
50428
+ * refuse to start. Off by default: those addons stay down until the
50429
+ * remedy is run, which is the incident this gate exists to prevent.
50430
+ */
50431
+ acknowledgeRefusals: boolean().optional()
50432
+ }), ServerUpdateActionResultSchema, {
49784
50433
  kind: "mutation",
49785
50434
  auth: "admin",
49786
50435
  timeoutMs: 16 * 6e4
@@ -52193,7 +52842,9 @@ var ManifestShapeSchema = object({
52193
52842
  var AddonProcessSchema = object({
52194
52843
  pid: number().optional(),
52195
52844
  mode: string(),
52196
- state: string()
52845
+ state: string(),
52846
+ /** Why the runner is `refused` (an addon's own words), when it is (D877). */
52847
+ reason: string().optional()
52197
52848
  });
52198
52849
  /**
52199
52850
  * Per-addon health snapshot from the kernel's AddonHealthMonitor.
@@ -53167,7 +53818,7 @@ var HvacModeSchema = _enum([
53167
53818
  "fan_only",
53168
53819
  "dry"
53169
53820
  ]);
53170
- object({
53821
+ var ClimateControlStatusSchema = object({
53171
53822
  /** Active HVAC mode. */
53172
53823
  mode: HvacModeSchema,
53173
53824
  /** Available HVAC modes the device accepts. Subset of HvacMode. */
@@ -53208,6 +53859,12 @@ object({
53208
53859
  /** Horizontal louver swing on/off. Null when the device has no
53209
53860
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
53210
53861
  swingHorizontal: boolean().nullable(),
53862
+ /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
53863
+ minTemp: number().nullable().optional(),
53864
+ /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
53865
+ maxTemp: number().nullable().optional(),
53866
+ /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
53867
+ tempStep: number().positive().nullable().optional(),
53211
53868
  /** Ms epoch when the slice was last updated (push or command). */
53212
53869
  lastFetchedAt: number()
53213
53870
  });
@@ -55048,7 +55705,19 @@ var TlsStatusSchema = object({
55048
55705
  sans: array(string()),
55049
55706
  caCertPem: string().nullable(),
55050
55707
  reissueError: string().nullable(),
55051
- restartRequired: boolean()
55708
+ restartRequired: boolean(),
55709
+ /** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
55710
+ caMissing: boolean(),
55711
+ /** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
55712
+ localCaConstrained: boolean().nullable(),
55713
+ /** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
55714
+ caReplacedAt: string().nullable(),
55715
+ /** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
55716
+ storedUpload: object({
55717
+ leafFingerprintSha256: string(),
55718
+ validTo: string(),
55719
+ hasCa: boolean()
55720
+ }).nullable()
55052
55721
  });
55053
55722
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
55054
55723
  /**
@@ -55104,6 +55773,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
55104
55773
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
55105
55774
  kind: "mutation",
55106
55775
  auth: "admin"
55776
+ }), method(_void(), TlsStatusSchema, {
55777
+ kind: "mutation",
55778
+ auth: "admin"
55107
55779
  });
55108
55780
  var LockControlStatusSchema = object({
55109
55781
  /** Lifecycle state of the lock. `jammed` means the motor reported
@@ -60256,7 +60928,11 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
60256
60928
  kind: "mutation",
60257
60929
  auth: "admin",
60258
60930
  access: "delete"
60259
- }), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(object({
60931
+ }), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
60932
+ kind: "mutation",
60933
+ auth: "admin",
60934
+ access: "create"
60935
+ }), method(object({
60260
60936
  userId: string(),
60261
60937
  code: string()
60262
60938
  }), object({ valid: boolean() }), {
@@ -62457,6 +63133,12 @@ Object.freeze({
62457
63133
  addonId: null,
62458
63134
  access: "view"
62459
63135
  },
63136
+ "coreBlocks.migrateRawSecrets": {
63137
+ capName: "core-blocks",
63138
+ capScope: "system",
63139
+ addonId: null,
63140
+ access: "create"
63141
+ },
62460
63142
  "coreBlocks.restart": {
62461
63143
  capName: "core-blocks",
62462
63144
  capScope: "system",
@@ -64143,6 +64825,12 @@ Object.freeze({
64143
64825
  addonId: null,
64144
64826
  access: "create"
64145
64827
  },
64828
+ "keychainAdmin.importSecret": {
64829
+ capName: "keychain-admin",
64830
+ capScope: "system",
64831
+ addonId: null,
64832
+ access: "create"
64833
+ },
64146
64834
  "keychainAdmin.listAudit": {
64147
64835
  capName: "keychain-admin",
64148
64836
  capScope: "system",
@@ -64311,6 +64999,12 @@ Object.freeze({
64311
64999
  addonId: null,
64312
65000
  access: "view"
64313
65001
  },
65002
+ "llm.migrateRawSecrets": {
65003
+ capName: "llm",
65004
+ capScope: "system",
65005
+ addonId: null,
65006
+ access: "create"
65007
+ },
64314
65008
  "llm.resolveModelRef": {
64315
65009
  capName: "llm",
64316
65010
  capScope: "system",
@@ -64539,6 +65233,12 @@ Object.freeze({
64539
65233
  addonId: null,
64540
65234
  access: "create"
64541
65235
  },
65236
+ "localNetwork.useStoredUploadedCertificate": {
65237
+ capName: "local-network",
65238
+ capScope: "system",
65239
+ addonId: null,
65240
+ access: "create"
65241
+ },
64542
65242
  "lockControl.getStatus": {
64543
65243
  capName: "lock-control",
64544
65244
  capScope: "device",
@@ -67191,6 +67891,30 @@ Object.freeze({
67191
67891
  addonId: null,
67192
67892
  access: "create"
67193
67893
  },
67894
+ "recordEntries.deleteEntry": {
67895
+ capName: "record-entries",
67896
+ capScope: "system",
67897
+ addonId: null,
67898
+ access: "delete"
67899
+ },
67900
+ "recordEntries.listEntries": {
67901
+ capName: "record-entries",
67902
+ capScope: "system",
67903
+ addonId: null,
67904
+ access: "view"
67905
+ },
67906
+ "recordEntries.listEntryKinds": {
67907
+ capName: "record-entries",
67908
+ capScope: "system",
67909
+ addonId: null,
67910
+ access: "view"
67911
+ },
67912
+ "recordEntries.putEntry": {
67913
+ capName: "record-entries",
67914
+ capScope: "system",
67915
+ addonId: null,
67916
+ access: "create"
67917
+ },
67194
67918
  "recording.getAvailability": {
67195
67919
  capName: "recording",
67196
67920
  capScope: "device",
@@ -67491,6 +68215,102 @@ Object.freeze({
67491
68215
  addonId: null,
67492
68216
  access: "view"
67493
68217
  },
68218
+ "records.aggregate": {
68219
+ capName: "records",
68220
+ capScope: "system",
68221
+ addonId: null,
68222
+ access: "view"
68223
+ },
68224
+ "records.allocateId": {
68225
+ capName: "records",
68226
+ capScope: "system",
68227
+ addonId: null,
68228
+ access: "create"
68229
+ },
68230
+ "records.count": {
68231
+ capName: "records",
68232
+ capScope: "system",
68233
+ addonId: null,
68234
+ access: "view"
68235
+ },
68236
+ "records.declareKinds": {
68237
+ capName: "records",
68238
+ capScope: "system",
68239
+ addonId: null,
68240
+ access: "create"
68241
+ },
68242
+ "records.delete": {
68243
+ capName: "records",
68244
+ capScope: "system",
68245
+ addonId: null,
68246
+ access: "delete"
68247
+ },
68248
+ "records.deleteDevice": {
68249
+ capName: "records",
68250
+ capScope: "system",
68251
+ addonId: null,
68252
+ access: "delete"
68253
+ },
68254
+ "records.deleteMany": {
68255
+ capName: "records",
68256
+ capScope: "system",
68257
+ addonId: null,
68258
+ access: "delete"
68259
+ },
68260
+ "records.deleteWhere": {
68261
+ capName: "records",
68262
+ capScope: "system",
68263
+ addonId: null,
68264
+ access: "delete"
68265
+ },
68266
+ "records.get": {
68267
+ capName: "records",
68268
+ capScope: "system",
68269
+ addonId: null,
68270
+ access: "view"
68271
+ },
68272
+ "records.getMany": {
68273
+ capName: "records",
68274
+ capScope: "system",
68275
+ addonId: null,
68276
+ access: "view"
68277
+ },
68278
+ "records.histogram": {
68279
+ capName: "records",
68280
+ capScope: "system",
68281
+ addonId: null,
68282
+ access: "view"
68283
+ },
68284
+ "records.patch": {
68285
+ capName: "records",
68286
+ capScope: "system",
68287
+ addonId: null,
68288
+ access: "create"
68289
+ },
68290
+ "records.put": {
68291
+ capName: "records",
68292
+ capScope: "system",
68293
+ addonId: null,
68294
+ access: "create"
68295
+ },
68296
+ "records.putMany": {
68297
+ capName: "records",
68298
+ capScope: "system",
68299
+ addonId: null,
68300
+ access: "create"
68301
+ },
68302
+ "records.query": {
68303
+ capName: "records",
68304
+ capScope: "system",
68305
+ addonId: null,
68306
+ access: "view"
68307
+ },
68308
+ "records.secretRefs": {
68309
+ capName: "records",
68310
+ capScope: "system",
68311
+ addonId: null,
68312
+ access: "view"
68313
+ },
67494
68314
  "sceneMonitor.captureReference": {
67495
68315
  capName: "scene-monitor",
67496
68316
  capScope: "device",
@@ -67923,6 +68743,48 @@ Object.freeze({
67923
68743
  addonId: null,
67924
68744
  access: "create"
67925
68745
  },
68746
+ "storageAdmin.adoptRecords": {
68747
+ capName: "storage-admin",
68748
+ capScope: "system",
68749
+ addonId: null,
68750
+ access: "create"
68751
+ },
68752
+ "storageAdmin.dropCollection": {
68753
+ capName: "storage-admin",
68754
+ capScope: "system",
68755
+ addonId: null,
68756
+ access: "delete"
68757
+ },
68758
+ "storageAdmin.isAdoptedLegacyTable": {
68759
+ capName: "storage-admin",
68760
+ capScope: "system",
68761
+ addonId: null,
68762
+ access: "view"
68763
+ },
68764
+ "storageAdmin.listKinds": {
68765
+ capName: "storage-admin",
68766
+ capScope: "system",
68767
+ addonId: null,
68768
+ access: "view"
68769
+ },
68770
+ "storageAdmin.listTables": {
68771
+ capName: "storage-admin",
68772
+ capScope: "system",
68773
+ addonId: null,
68774
+ access: "view"
68775
+ },
68776
+ "storageAdmin.readLegacyCollection": {
68777
+ capName: "storage-admin",
68778
+ capScope: "system",
68779
+ addonId: null,
68780
+ access: "view"
68781
+ },
68782
+ "storageAdmin.readRecords": {
68783
+ capName: "storage-admin",
68784
+ capScope: "system",
68785
+ addonId: null,
68786
+ access: "view"
68787
+ },
67926
68788
  "storageEvictable.evict": {
67927
68789
  capName: "storage-evictable",
67928
68790
  capScope: "system",
@@ -68835,6 +69697,12 @@ Object.freeze({
68835
69697
  addonId: null,
68836
69698
  access: "view"
68837
69699
  },
69700
+ "userManagement.migrateRawSecrets": {
69701
+ capName: "user-management",
69702
+ capScope: "system",
69703
+ addonId: null,
69704
+ access: "create"
69705
+ },
68838
69706
  "userManagement.oauthExchangeCode": {
68839
69707
  capName: "user-management",
68840
69708
  capScope: "system",
@@ -71511,6 +72379,37 @@ Object.freeze({
71511
72379
  form: "single",
71512
72380
  optional: false
71513
72381
  }],
72382
+ "records.count": [{
72383
+ name: "deviceId",
72384
+ form: "single",
72385
+ optional: true
72386
+ }],
72387
+ "records.deleteDevice": [{
72388
+ name: "deviceId",
72389
+ form: "single",
72390
+ optional: false
72391
+ }],
72392
+ "records.deleteWhere": [{
72393
+ name: "deviceId",
72394
+ form: "single",
72395
+ optional: true
72396
+ }],
72397
+ "records.histogram": [{
72398
+ name: "deviceId",
72399
+ form: "single",
72400
+ optional: true
72401
+ }],
72402
+ "records.putMany": [{
72403
+ name: "records",
72404
+ form: "object-array",
72405
+ optional: false,
72406
+ itemField: "deviceId"
72407
+ }],
72408
+ "records.query": [{
72409
+ name: "deviceId",
72410
+ form: "single",
72411
+ optional: true
72412
+ }],
71514
72413
  "sceneMonitor.captureReference": [{
71515
72414
  name: "deviceId",
71516
72415
  form: "single",
@@ -71627,6 +72526,17 @@ Object.freeze({
71627
72526
  form: "single",
71628
72527
  optional: false
71629
72528
  }],
72529
+ "storageAdmin.adoptRecords": [{
72530
+ name: "records",
72531
+ form: "object-array",
72532
+ optional: false,
72533
+ itemField: "deviceId"
72534
+ }],
72535
+ "storageAdmin.readRecords": [{
72536
+ name: "deviceId",
72537
+ form: "single",
72538
+ optional: true
72539
+ }],
71630
72540
  "streamBroker.acquireEgressTranscode": [{
71631
72541
  name: "deviceId",
71632
72542
  form: "single",
@@ -74027,6 +74937,778 @@ async function revokeBrokerCredential(fetchImpl, brokerUrl, credential) {
74027
74937
  await call(fetchImpl, brokerUrl, "/broker/revoke", { bearer: credential }, unknown());
74028
74938
  }
74029
74939
  //#endregion
74940
+ //#region src/endpoints/adapters/climate-model.ts
74941
+ /**
74942
+ * What a `climate-control` device is to Alexa (D874) — pure, so the discovery
74943
+ * shape, the slice mapping and the directive handlers share one definition.
74944
+ *
74945
+ * `Alexa.ThermostatController` 3.1 targetSetpoint, lowerSetpoint /
74946
+ * upperSetpoint (a device with a range),
74947
+ * thermostatMode HEAT / COOL / AUTO / ECO /
74948
+ * OFF
74949
+ * `Alexa.TemperatureSensor` the current temperature
74950
+ * `Alexa.PowerController` when the device can be turned off
74951
+ * `Alexa.ModeController` Climate.Mode only when the device has a mode
74952
+ * Amazon's thermostat vocabulary lacks
74953
+ * (dry, fan_only)
74954
+ * `Alexa.ModeController` Climate.FanSpeed the device's own fan modes
74955
+ *
74956
+ * Amazon's `thermostatMode` has no value for `dry` / `fan_only`, and a CUSTOM
74957
+ * mode is not a documented way to carry them: they travel through the
74958
+ * `Climate.Mode` ModeController, and `thermostatMode` is simply not reported
74959
+ * while the device is in one of them.
74960
+ *
74961
+ * Temperatures are Celsius throughout (the cap's contract); Alexa converts for
74962
+ * the account.
74963
+ */
74964
+ var INSTANCE_MODE = "Climate.Mode";
74965
+ var INSTANCE_FAN = "Climate.FanSpeed";
74966
+ var FAN_PREFIX = "Fan.";
74967
+ var DEFAULT_TEMP_STEP = .5;
74968
+ var FEATURE_DUAL_SETPOINT = "climate-dual-setpoint";
74969
+ var ALEXA_ECO_PRESET = "eco";
74970
+ var SINGLE_SETPOINT_MODES = [
74971
+ "heat",
74972
+ "cool",
74973
+ "auto",
74974
+ "dry"
74975
+ ];
74976
+ /** What TurnOn switches to, best first. */
74977
+ var TURN_ON_PREFERENCE = [
74978
+ "auto",
74979
+ "heat_cool",
74980
+ "cool",
74981
+ "heat"
74982
+ ];
74983
+ var SliceShape$1 = ClimateControlStatusSchema.pick({
74984
+ mode: true,
74985
+ availableModes: true,
74986
+ fanMode: true,
74987
+ availableFanModes: true,
74988
+ preset: true,
74989
+ availablePresets: true,
74990
+ target: true,
74991
+ targetHigh: true,
74992
+ targetLow: true,
74993
+ currentTemp: true,
74994
+ minTemp: true,
74995
+ maxTemp: true,
74996
+ tempStep: true
74997
+ });
74998
+ /** The slice fields the adapter reads, or null when the slice is not a climate one. */
74999
+ function readClimateSlice(slice) {
75000
+ const parsed = SliceShape$1.safeParse(slice);
75001
+ return parsed.success ? parsed.data : null;
75002
+ }
75003
+ function boundsOf(slice) {
75004
+ const min = slice.minTemp ?? 7;
75005
+ const max = slice.maxTemp ?? 35;
75006
+ const step = slice.tempStep ?? .5;
75007
+ return min < max ? {
75008
+ min,
75009
+ max,
75010
+ step
75011
+ } : {
75012
+ min: 7,
75013
+ max: 35,
75014
+ step: DEFAULT_TEMP_STEP
75015
+ };
75016
+ }
75017
+ /** One decimal, the precision Alexa's own conversions keep. */
75018
+ function round1(value) {
75019
+ return Math.round(value * 10) / 10;
75020
+ }
75021
+ /**
75022
+ * Snap a setpoint to the device's step (anchored at its minimum), then clamp:
75023
+ * a maximum off the step grid (max 30.2, step 0.5) must not snap above itself.
75024
+ */
75025
+ function snapToStep(value, bounds) {
75026
+ const snapped = bounds.min + Math.round((value - bounds.min) / bounds.step) * bounds.step;
75027
+ return round1(Math.min(bounds.max, Math.max(bounds.min, snapped)));
75028
+ }
75029
+ function temperatureValue(celsius) {
75030
+ return {
75031
+ value: round1(celsius),
75032
+ scale: "CELSIUS"
75033
+ };
75034
+ }
75035
+ function hasMode(slice, mode) {
75036
+ return slice !== null && slice.availableModes.includes(mode);
75037
+ }
75038
+ /** Dual setpoints (a heat/cool range). */
75039
+ function supportsDual(slice, features) {
75040
+ return slice === null ? features.includes(FEATURE_DUAL_SETPOINT) : hasMode(slice, "heat_cool");
75041
+ }
75042
+ /** A single setpoint (everything but a range-only device). */
75043
+ function supportsSingle(slice) {
75044
+ return slice === null || slice.availableModes.length === 0 ? true : slice.availableModes.some((m) => SINGLE_SETPOINT_MODES.includes(m));
75045
+ }
75046
+ /** The device's own name for its `eco` preset, if it has one. */
75047
+ function ecoPresetOf(slice) {
75048
+ return slice?.availablePresets.find((p) => p.toLowerCase() === ALEXA_ECO_PRESET) ?? null;
75049
+ }
75050
+ /** Amazon thermostat modes the device can be put in, in a stable order. */
75051
+ function supportedThermostatModes(slice) {
75052
+ if (slice === null || slice.availableModes.length === 0) return [
75053
+ "HEAT",
75054
+ "COOL",
75055
+ "AUTO",
75056
+ "OFF"
75057
+ ];
75058
+ const out = [];
75059
+ if (hasMode(slice, "heat")) out.push("HEAT");
75060
+ if (hasMode(slice, "cool")) out.push("COOL");
75061
+ if (hasMode(slice, "auto") || hasMode(slice, "heat_cool")) out.push("AUTO");
75062
+ if (ecoPresetOf(slice) !== null && out.length > 0) out.push("ECO");
75063
+ if (hasMode(slice, "off")) out.push("OFF");
75064
+ return out;
75065
+ }
75066
+ /** The HVAC mode TurnOn / leaving OFF switches to. */
75067
+ function onModeOf(slice) {
75068
+ return TURN_ON_PREFERENCE.find((m) => hasMode(slice, m)) ?? null;
75069
+ }
75070
+ function canPowerOff(slice) {
75071
+ return slice === null || slice.availableModes.length === 0 || hasMode(slice, "off");
75072
+ }
75073
+ /** A device with a mode Amazon's thermostat vocabulary lacks. */
75074
+ function hasExtraModes(slice) {
75075
+ return hasMode(slice, "dry") || hasMode(slice, "fan_only");
75076
+ }
75077
+ var MODE_VALUE = {
75078
+ heat: "Mode.Heat",
75079
+ cool: "Mode.Cool",
75080
+ auto: "Mode.Auto",
75081
+ dry: "Mode.Dry",
75082
+ fan_only: "Mode.Fan",
75083
+ off: "Mode.Off"
75084
+ };
75085
+ var MODE_NAMES = {
75086
+ "Mode.Heat": ["Riscaldamento", "Heating"],
75087
+ "Mode.Cool": ["Raffrescamento", "Cooling"],
75088
+ "Mode.Auto": ["Automatico", "Auto"],
75089
+ "Mode.Dry": ["Deumidificazione", "Dry"],
75090
+ "Mode.Fan": ["Ventilazione", "Fan only"],
75091
+ "Mode.Off": ["Spento", "Off"]
75092
+ };
75093
+ /** The ModeController value a HVAC mode reports as (a range is "auto"). */
75094
+ function modeValueOf(mode) {
75095
+ return mode === "heat_cool" ? MODE_VALUE.auto : MODE_VALUE[mode];
75096
+ }
75097
+ /** The HVAC mode a ModeController value asks for, among those the device has. */
75098
+ function hvacModeForValue(value, slice) {
75099
+ if (value === MODE_VALUE.auto) return hasMode(slice, "auto") ? "auto" : hasMode(slice, "heat_cool") ? "heat_cool" : null;
75100
+ const mode = Object.entries(MODE_VALUE).find(([, v]) => v === value)?.[0];
75101
+ if (mode === void 0) return null;
75102
+ const candidate = SliceShape$1.shape.mode.safeParse(mode);
75103
+ return candidate.success && hasMode(slice, candidate.data) ? candidate.data : null;
75104
+ }
75105
+ /** The ModeController values the device offers, in a stable order. */
75106
+ function modeValuesOf(slice) {
75107
+ return [
75108
+ "heat",
75109
+ "cool",
75110
+ "auto",
75111
+ "dry",
75112
+ "fan_only",
75113
+ "off"
75114
+ ].filter((m) => m === "auto" ? hasMode(slice, "auto") || hasMode(slice, "heat_cool") : hasMode(slice, m)).map((m) => MODE_VALUE[m]);
75115
+ }
75116
+ function textNames(it, en) {
75117
+ return [{
75118
+ "@type": "text",
75119
+ value: {
75120
+ text: it,
75121
+ locale: "it-IT"
75122
+ }
75123
+ }, {
75124
+ "@type": "text",
75125
+ value: {
75126
+ text: en,
75127
+ locale: "en-US"
75128
+ }
75129
+ }];
75130
+ }
75131
+ function modeCapability(slice) {
75132
+ const values = modeValuesOf(slice);
75133
+ if (values.length < 2) return null;
75134
+ return {
75135
+ type: "AlexaInterface",
75136
+ interface: "Alexa.ModeController",
75137
+ version: "3",
75138
+ instance: INSTANCE_MODE,
75139
+ capabilityResources: { friendlyNames: [{
75140
+ "@type": "asset",
75141
+ value: { assetId: "Alexa.Setting.Mode" }
75142
+ }, ...textNames("Modalità", "Mode")] },
75143
+ properties: {
75144
+ supported: [{ name: "mode" }],
75145
+ proactivelyReported: true,
75146
+ retrievable: true
75147
+ },
75148
+ configuration: {
75149
+ ordered: false,
75150
+ supportedModes: values.map((value) => {
75151
+ const [it, en] = MODE_NAMES[value] ?? [value, value];
75152
+ return {
75153
+ value,
75154
+ modeResources: { friendlyNames: textNames(it, en) }
75155
+ };
75156
+ })
75157
+ }
75158
+ };
75159
+ }
75160
+ var FAN_ASSETS = {
75161
+ low: "Alexa.Value.Low",
75162
+ medium: "Alexa.Value.Medium",
75163
+ high: "Alexa.Value.High"
75164
+ };
75165
+ var FAN_TEXTS = {
75166
+ auto: ["Automatico", "Auto"],
75167
+ quiet: ["Silenzioso", "Quiet"],
75168
+ turbo: ["Turbo", "Turbo"],
75169
+ on: ["Acceso", "On"],
75170
+ off: ["Spento", "Off"],
75171
+ middle: ["Medio", "Medium"],
75172
+ mediumlow: ["Medio basso", "Medium low"],
75173
+ mediumhigh: ["Medio alto", "Medium high"],
75174
+ medium_low: ["Medio basso", "Medium low"],
75175
+ medium_high: ["Medio alto", "Medium high"]
75176
+ };
75177
+ function capitalize(word) {
75178
+ return word.charAt(0).toUpperCase() + word.slice(1);
75179
+ }
75180
+ /** The ModeController value of a vendor fan mode (`medium_high` -> `Fan.MediumHigh`). */
75181
+ function fanValueOf(fanMode) {
75182
+ const words = fanMode.split(/[^A-Za-z0-9]+/).filter((w) => w.length > 0);
75183
+ return words.length === 0 ? null : `${FAN_PREFIX}${words.map(capitalize).join("")}`;
75184
+ }
75185
+ /** The fan modes with a usable, unique value: `[value, deviceFanMode]`. */
75186
+ function fanModesOf(slice) {
75187
+ const seen = /* @__PURE__ */ new Set();
75188
+ const out = [];
75189
+ for (const fanMode of slice.availableFanModes) {
75190
+ const value = fanValueOf(fanMode);
75191
+ if (value === null || seen.has(value)) continue;
75192
+ seen.add(value);
75193
+ out.push([value, fanMode]);
75194
+ }
75195
+ return out;
75196
+ }
75197
+ function hasFanModes(slice) {
75198
+ return slice !== null && fanModesOf(slice).length >= 2;
75199
+ }
75200
+ function fanFriendlyNames(fanMode) {
75201
+ const key = fanMode.toLowerCase();
75202
+ const asset = FAN_ASSETS[key];
75203
+ if (asset !== void 0) return [{
75204
+ "@type": "asset",
75205
+ value: { assetId: asset }
75206
+ }];
75207
+ const [it, en] = FAN_TEXTS[key] ?? [fanMode.replace(/_/g, " "), fanMode.replace(/_/g, " ")];
75208
+ return textNames(it, en);
75209
+ }
75210
+ function fanCapability(slice) {
75211
+ const modes = fanModesOf(slice);
75212
+ if (modes.length < 2) return null;
75213
+ return {
75214
+ type: "AlexaInterface",
75215
+ interface: "Alexa.ModeController",
75216
+ version: "3",
75217
+ instance: INSTANCE_FAN,
75218
+ capabilityResources: { friendlyNames: [{
75219
+ "@type": "asset",
75220
+ value: { assetId: "Alexa.Setting.FanSpeed" }
75221
+ }, ...textNames("Velocità ventola", "Fan speed")] },
75222
+ properties: {
75223
+ supported: [{ name: "mode" }],
75224
+ proactivelyReported: true,
75225
+ retrievable: true
75226
+ },
75227
+ configuration: {
75228
+ ordered: false,
75229
+ supportedModes: modes.map(([value, fanMode]) => ({
75230
+ value,
75231
+ modeResources: { friendlyNames: fanFriendlyNames(fanMode) }
75232
+ }))
75233
+ }
75234
+ };
75235
+ }
75236
+ function propertiesBlock(names) {
75237
+ return {
75238
+ supported: names.map((name) => ({ name })),
75239
+ proactivelyReported: true,
75240
+ retrievable: true
75241
+ };
75242
+ }
75243
+ function thermostatCapability(slice, modes, features) {
75244
+ return {
75245
+ type: "AlexaInterface",
75246
+ interface: "Alexa.ThermostatController",
75247
+ version: "3.1",
75248
+ properties: propertiesBlock([
75249
+ ...supportsSingle(slice) ? ["targetSetpoint"] : [],
75250
+ ...supportsDual(slice, features) ? ["lowerSetpoint", "upperSetpoint"] : [],
75251
+ "thermostatMode"
75252
+ ]),
75253
+ configuration: {
75254
+ supportedModes: modes,
75255
+ supportsScheduling: false
75256
+ }
75257
+ };
75258
+ }
75259
+ /** A thermostat is a THERMOSTAT, any other climate device an air conditioner. */
75260
+ function climateCategoryFor(deviceType) {
75261
+ return deviceType === "thermostat" ? "THERMOSTAT" : "AIR_CONDITIONER";
75262
+ }
75263
+ function describeClimate(device) {
75264
+ const slice = readClimateSlice(device.slices["climate-control"]);
75265
+ const modes = supportedThermostatModes(slice);
75266
+ if (modes.length === 0) return {
75267
+ kind: "skip",
75268
+ reason: "climate: the device offers no mode Alexa has a thermostat value for"
75269
+ };
75270
+ const extras = [];
75271
+ if (slice !== null) {
75272
+ const mode = hasExtraModes(slice) ? modeCapability(slice) : null;
75273
+ const fan = hasFanModes(slice) ? fanCapability(slice) : null;
75274
+ if (mode !== null) extras.push(mode);
75275
+ if (fan !== null) extras.push(fan);
75276
+ }
75277
+ return {
75278
+ kind: "publish",
75279
+ capabilities: [
75280
+ thermostatCapability(slice, modes, device.features),
75281
+ {
75282
+ type: "AlexaInterface",
75283
+ interface: "Alexa.TemperatureSensor",
75284
+ version: "3",
75285
+ properties: propertiesBlock(["temperature"])
75286
+ },
75287
+ ...canPowerOff(slice) ? [{
75288
+ type: "AlexaInterface",
75289
+ interface: "Alexa.PowerController",
75290
+ version: "3",
75291
+ properties: propertiesBlock(["powerState"])
75292
+ }] : [],
75293
+ ...extras
75294
+ ],
75295
+ category: climateCategoryFor(device.type)
75296
+ };
75297
+ }
75298
+ /** Amazon's `thermostatMode` for the state, or null while in a mode it lacks. */
75299
+ function thermostatModeOf(slice) {
75300
+ if (slice.mode === "off") return "OFF";
75301
+ const eco = ecoPresetOf(slice);
75302
+ if (eco !== null && slice.preset === eco) return "ECO";
75303
+ switch (slice.mode) {
75304
+ case "heat": return "HEAT";
75305
+ case "cool": return "COOL";
75306
+ case "auto":
75307
+ case "heat_cool": return "AUTO";
75308
+ default: return null;
75309
+ }
75310
+ }
75311
+ function climateProperties(slice) {
75312
+ const s = readClimateSlice(slice);
75313
+ if (s === null) return [];
75314
+ const out = [];
75315
+ const thermostat = (name, value) => {
75316
+ out.push({
75317
+ namespace: "Alexa.ThermostatController",
75318
+ name,
75319
+ value
75320
+ });
75321
+ };
75322
+ if (hasMode(s, "heat_cool") && s.targetLow !== null && s.targetHigh !== null && s.mode === "heat_cool") {
75323
+ thermostat("lowerSetpoint", temperatureValue(s.targetLow ?? 0));
75324
+ thermostat("upperSetpoint", temperatureValue(s.targetHigh ?? 0));
75325
+ } else if (s.target !== null && supportsSingle(s)) thermostat("targetSetpoint", temperatureValue(s.target));
75326
+ const mode = thermostatModeOf(s);
75327
+ if (mode !== null) thermostat("thermostatMode", mode);
75328
+ if (s.currentTemp !== null) out.push({
75329
+ namespace: "Alexa.TemperatureSensor",
75330
+ name: "temperature",
75331
+ value: temperatureValue(s.currentTemp)
75332
+ });
75333
+ if (canPowerOff(s)) out.push({
75334
+ namespace: "Alexa.PowerController",
75335
+ name: "powerState",
75336
+ value: s.mode === "off" ? "OFF" : "ON"
75337
+ });
75338
+ if (hasExtraModes(s) && modeValuesOf(s).length >= 2) out.push({
75339
+ namespace: "Alexa.ModeController",
75340
+ instance: INSTANCE_MODE,
75341
+ name: "mode",
75342
+ value: modeValueOf(s.mode)
75343
+ });
75344
+ const fan = fanValueOf(s.fanMode);
75345
+ if (hasFanModes(s) && fan !== null && fanModesOf(s).some(([value]) => value === fan)) out.push({
75346
+ namespace: "Alexa.ModeController",
75347
+ instance: INSTANCE_FAN,
75348
+ name: "mode",
75349
+ value: fan
75350
+ });
75351
+ return out;
75352
+ }
75353
+ //#endregion
75354
+ //#region src/handlers/climate-controller.ts
75355
+ var CAP_CLIMATE = "climate-control";
75356
+ var CAP_SWITCH$1 = "switch";
75357
+ var UNCERTAINTY_MS = 500;
75358
+ var PRESET_NONE = "none";
75359
+ var ScaleSchema = _enum([
75360
+ "CELSIUS",
75361
+ "FAHRENHEIT",
75362
+ "KELVIN"
75363
+ ]);
75364
+ var TemperatureSchema = object({
75365
+ value: number(),
75366
+ scale: ScaleSchema
75367
+ });
75368
+ var SetTemperaturePayload = object({
75369
+ targetSetpoint: TemperatureSchema.optional(),
75370
+ lowerSetpoint: TemperatureSchema.optional(),
75371
+ upperSetpoint: TemperatureSchema.optional()
75372
+ });
75373
+ var AdjustTemperaturePayload = object({ targetSetpointDelta: TemperatureSchema });
75374
+ var ThermostatModePayload = object({ thermostatMode: union([string(), object({ value: string() })]) });
75375
+ var ModePayload = object({ mode: string() });
75376
+ function isRefusal(outcome) {
75377
+ return "refusal" in outcome;
75378
+ }
75379
+ function toCelsius(t) {
75380
+ if (t.scale === "FAHRENHEIT") return (t.value - 32) * 5 / 9;
75381
+ if (t.scale === "KELVIN") return t.value - 273.15;
75382
+ return t.value;
75383
+ }
75384
+ /** A DELTA converts without the scale's offset. */
75385
+ function deltaToCelsius(t) {
75386
+ return t.scale === "FAHRENHEIT" ? t.value * 5 / 9 : t.value;
75387
+ }
75388
+ function withPayload(envelope, extra) {
75389
+ return { event: {
75390
+ ...envelope.event,
75391
+ payload: {
75392
+ ...envelope.event.payload,
75393
+ ...extra
75394
+ }
75395
+ } };
75396
+ }
75397
+ function outOfRange$1(header, bounds) {
75398
+ return { refusal: withPayload(errorEnvelope(header, "TEMPERATURE_VALUE_OUT_OF_RANGE", `the setpoint must be between ${bounds.min} and ${bounds.max} C`), { validRange: {
75399
+ minimumValue: temperatureValue(bounds.min),
75400
+ maximumValue: temperatureValue(bounds.max)
75401
+ } }) };
75402
+ }
75403
+ function tooClose(header, bounds) {
75404
+ return { refusal: withPayload(errorEnvelope(header, "REQUESTED_SETPOINTS_TOO_CLOSE", "lowerSetpoint must be below upperSetpoint"), { minimumTemperatureDelta: temperatureValue(bounds.step) }) };
75405
+ }
75406
+ function refuse$1(header, type, message) {
75407
+ return { refusal: errorEnvelope(header, type, message) };
75408
+ }
75409
+ function inRange(celsius, bounds) {
75410
+ const v = round1(celsius);
75411
+ return v >= bounds.min && v <= bounds.max;
75412
+ }
75413
+ var THERMOSTAT_PROPERTIES = new Set([
75414
+ "targetSetpoint",
75415
+ "lowerSetpoint",
75416
+ "upperSetpoint",
75417
+ "thermostatMode"
75418
+ ]);
75419
+ function thermostatProperty(property) {
75420
+ return property.namespace === "Alexa.ThermostatController" && THERMOSTAT_PROPERTIES.has(property.name);
75421
+ }
75422
+ function commandsFor(api, deviceId) {
75423
+ return {
75424
+ setMode: (mode) => api.climateControl.setMode.mutate({
75425
+ deviceId,
75426
+ mode
75427
+ }),
75428
+ setPreset: (preset) => api.climateControl.setPreset.mutate({
75429
+ deviceId,
75430
+ preset
75431
+ }),
75432
+ setFanMode: (fanMode) => api.climateControl.setFanMode.mutate({
75433
+ deviceId,
75434
+ fanMode
75435
+ }),
75436
+ setTarget: (target) => api.climateControl.setTarget.mutate({
75437
+ deviceId,
75438
+ target
75439
+ }),
75440
+ setTargetRange: (targetLow, targetHigh) => api.climateControl.setTargetRange.mutate({
75441
+ deviceId,
75442
+ targetLow,
75443
+ targetHigh
75444
+ })
75445
+ };
75446
+ }
75447
+ function planRange(header, slice, cmd, low, high) {
75448
+ const bounds = boundsOf(slice);
75449
+ if (!supportsDual(slice, [])) return refuse$1(header, "DUAL_SETPOINTS_UNSUPPORTED", "this thermostat has a single setpoint");
75450
+ if (!inRange(low, bounds) || !inRange(high, bounds)) return outOfRange$1(header, bounds);
75451
+ const targetLow = snapToStep(low, bounds);
75452
+ const targetHigh = snapToStep(high, bounds);
75453
+ if (targetHigh - targetLow < bounds.step) return tooClose(header, bounds);
75454
+ return {
75455
+ commands: [...slice.mode === "heat_cool" ? [] : [() => cmd.setMode("heat_cool")], () => cmd.setTargetRange(targetLow, targetHigh)],
75456
+ projected: {
75457
+ ...slice,
75458
+ mode: "heat_cool",
75459
+ targetLow,
75460
+ targetHigh
75461
+ },
75462
+ keep: thermostatProperty
75463
+ };
75464
+ }
75465
+ function planSingle(header, slice, cmd, celsius) {
75466
+ const bounds = boundsOf(slice);
75467
+ 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");
75468
+ if (!inRange(celsius, bounds)) return outOfRange$1(header, bounds);
75469
+ const target = snapToStep(celsius, bounds);
75470
+ return {
75471
+ commands: [() => cmd.setTarget(target)],
75472
+ projected: {
75473
+ ...slice,
75474
+ target
75475
+ },
75476
+ keep: thermostatProperty
75477
+ };
75478
+ }
75479
+ function planSetTemperature(c, slice, cmd) {
75480
+ const parsed = SetTemperaturePayload.safeParse(c.payload);
75481
+ if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
75482
+ const { targetSetpoint, lowerSetpoint, upperSetpoint } = parsed.data;
75483
+ 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");
75484
+ if (lowerSetpoint !== void 0 && upperSetpoint !== void 0 && targetSetpoint === void 0) return planRange(c.header, slice, cmd, toCelsius(lowerSetpoint), toCelsius(upperSetpoint));
75485
+ if (targetSetpoint !== void 0 && lowerSetpoint === void 0 && upperSetpoint === void 0) return planSingle(c.header, slice, cmd, toCelsius(targetSetpoint));
75486
+ return refuse$1(c.header, "INVALID_DIRECTIVE", "give a targetSetpoint, or a lower and an upper");
75487
+ }
75488
+ function planAdjust(c, slice, cmd) {
75489
+ const parsed = AdjustTemperaturePayload.safeParse(c.payload);
75490
+ if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
75491
+ const delta = deltaToCelsius(parsed.data.targetSetpointDelta);
75492
+ if (slice.mode === "heat_cool" && slice.targetLow !== null && slice.targetHigh !== null) return planRange(c.header, slice, cmd, slice.targetLow + delta, slice.targetHigh + delta);
75493
+ if (slice.target === null) return refuse$1(c.header, "ENDPOINT_UNREACHABLE", "the current setpoint is unknown, so it cannot be adjusted");
75494
+ return planSingle(c.header, slice, cmd, slice.target + delta);
75495
+ }
75496
+ var ECO_FROM_OFF_MESSAGE = "the thermostat has no mode to leave OFF into";
75497
+ function hvacModeFor(requested, slice) {
75498
+ const has = (m) => slice.availableModes.includes(m);
75499
+ switch (requested) {
75500
+ case "HEAT": return has("heat") ? "heat" : null;
75501
+ case "COOL": return has("cool") ? "cool" : null;
75502
+ case "AUTO": return has("auto") ? "auto" : has("heat_cool") ? "heat_cool" : null;
75503
+ case "OFF": return has("off") ? "off" : null;
75504
+ default: return null;
75505
+ }
75506
+ }
75507
+ /** The preset that clears `eco` (HA and Gree both call it `none`), if the device has one. */
75508
+ function presetClearing(slice) {
75509
+ return slice.availablePresets.find((p) => p.toLowerCase() === PRESET_NONE) ?? null;
75510
+ }
75511
+ function planEco(c, slice, cmd) {
75512
+ const eco = ecoPresetOf(slice);
75513
+ if (eco === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", "ECO is not supported");
75514
+ const wake = slice.mode === "off" ? onModeOf(slice) : null;
75515
+ if (slice.mode === "off" && wake === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", ECO_FROM_OFF_MESSAGE);
75516
+ return {
75517
+ commands: [...wake === null ? [] : [() => cmd.setMode(wake)], () => cmd.setPreset(eco)],
75518
+ projected: {
75519
+ ...slice,
75520
+ mode: wake ?? slice.mode,
75521
+ preset: eco
75522
+ },
75523
+ keep: thermostatProperty
75524
+ };
75525
+ }
75526
+ function planSetThermostatMode(c, slice, cmd) {
75527
+ const parsed = ThermostatModePayload.safeParse(c.payload);
75528
+ if (!parsed.success) return refuse$1(c.header, "INVALID_DIRECTIVE", parsed.error.message);
75529
+ const raw = parsed.data.thermostatMode;
75530
+ const requested = (typeof raw === "string" ? raw : raw.value).toUpperCase();
75531
+ if (requested === "ECO") return planEco(c, slice, cmd);
75532
+ const mode = hvacModeFor(requested, slice);
75533
+ if (mode === null) return refuse$1(c.header, "UNSUPPORTED_THERMOSTAT_MODE", `${requested} is not a mode this thermostat has`);
75534
+ const eco = ecoPresetOf(slice);
75535
+ const clear = eco !== null && slice.preset === eco ? presetClearing(slice) : null;
75536
+ return {
75537
+ commands: [...clear === null ? [] : [() => cmd.setPreset(clear)], () => cmd.setMode(mode)],
75538
+ projected: {
75539
+ ...slice,
75540
+ mode,
75541
+ preset: clear ?? slice.preset
75542
+ },
75543
+ keep: thermostatProperty
75544
+ };
75545
+ }
75546
+ function planPower(c, slice, cmd) {
75547
+ const turnOn = c.header.name === "TurnOn";
75548
+ const keep = (p) => p.namespace === "Alexa.PowerController";
75549
+ if (turnOn) {
75550
+ if (slice.mode !== "off") return {
75551
+ commands: [],
75552
+ projected: slice,
75553
+ keep
75554
+ };
75555
+ const mode = onModeOf(slice);
75556
+ if (mode === null) return refuse$1(c.header, "INVALID_DIRECTIVE", "no mode to turn on into");
75557
+ return {
75558
+ commands: [() => cmd.setMode(mode)],
75559
+ projected: {
75560
+ ...slice,
75561
+ mode
75562
+ },
75563
+ keep
75564
+ };
75565
+ }
75566
+ if (!slice.availableModes.includes("off")) return refuse$1(c.header, "INVALID_DIRECTIVE", "this device cannot be turned off");
75567
+ if (slice.mode === "off") return {
75568
+ commands: [],
75569
+ projected: slice,
75570
+ keep
75571
+ };
75572
+ return {
75573
+ commands: [() => cmd.setMode("off")],
75574
+ projected: {
75575
+ ...slice,
75576
+ mode: "off"
75577
+ },
75578
+ keep
75579
+ };
75580
+ }
75581
+ function planModeController(c, slice, cmd) {
75582
+ const parsed = ModePayload.safeParse(c.payload);
75583
+ const instance = c.header.instance;
75584
+ if (!parsed.success || instance === void 0) return refuse$1(c.header, "INVALID_DIRECTIVE", "a mode and an instance are required");
75585
+ const keep = (p) => p.instance === instance;
75586
+ if (instance === "Climate.Mode") {
75587
+ const mode = hvacModeForValue(parsed.data.mode, slice);
75588
+ if (mode === null) return refuse$1(c.header, "INVALID_VALUE", `unknown mode ${parsed.data.mode}`);
75589
+ return {
75590
+ commands: [() => cmd.setMode(mode)],
75591
+ projected: {
75592
+ ...slice,
75593
+ mode
75594
+ },
75595
+ keep
75596
+ };
75597
+ }
75598
+ if (instance === "Climate.FanSpeed") {
75599
+ const fan = fanModesOf(slice).find(([value]) => value === parsed.data.mode);
75600
+ if (fan === void 0) return refuse$1(c.header, "INVALID_VALUE", `unknown fan mode ${parsed.data.mode}`);
75601
+ return {
75602
+ commands: [() => cmd.setFanMode(fan[1])],
75603
+ projected: {
75604
+ ...slice,
75605
+ fanMode: fan[1]
75606
+ },
75607
+ keep
75608
+ };
75609
+ }
75610
+ return refuse$1(c.header, "INVALID_DIRECTIVE", `unknown instance ${instance}`);
75611
+ }
75612
+ function plan(c, slice, cmd) {
75613
+ switch (`${c.header.namespace}/${c.header.name}`) {
75614
+ case "Alexa.ThermostatController/SetTargetTemperature": return planSetTemperature(c, slice, cmd);
75615
+ case "Alexa.ThermostatController/AdjustTargetTemperature": return planAdjust(c, slice, cmd);
75616
+ case "Alexa.ThermostatController/SetThermostatMode": return planSetThermostatMode(c, slice, cmd);
75617
+ case "Alexa.PowerController/TurnOn":
75618
+ case "Alexa.PowerController/TurnOff": return planPower(c, slice, cmd);
75619
+ case "Alexa.ModeController/SetMode": return planModeController(c, slice, cmd);
75620
+ default: return refuse$1(c.header, "INVALID_DIRECTIVE", `${c.header.namespace}/${c.header.name}`);
75621
+ }
75622
+ }
75623
+ async function readSlice$1(c, deps) {
75624
+ try {
75625
+ return readClimateSlice(await deps.ctx.api.deviceState.getCapSlice.query({
75626
+ deviceId: c.deviceId,
75627
+ capName: CAP_CLIMATE
75628
+ }));
75629
+ } catch (err) {
75630
+ deps.logger.warn("alexa climate-controller: the climate state could not be read", {
75631
+ tags: { deviceId: c.deviceId },
75632
+ meta: { error: err instanceof Error ? err.message : String(err) }
75633
+ });
75634
+ return null;
75635
+ }
75636
+ }
75637
+ function successResponse$1(c, properties) {
75638
+ const now = (/* @__PURE__ */ new Date()).toISOString();
75639
+ return {
75640
+ event: {
75641
+ header: {
75642
+ namespace: "Alexa",
75643
+ name: "Response",
75644
+ messageId: c.header.messageId,
75645
+ ...c.header.correlationToken !== void 0 ? { correlationToken: c.header.correlationToken } : {},
75646
+ payloadVersion: "3"
75647
+ },
75648
+ endpoint: { endpointId: c.endpoint.endpointId },
75649
+ payload: {}
75650
+ },
75651
+ context: { properties: properties.map((p) => ({
75652
+ ...p,
75653
+ timeOfSample: now,
75654
+ uncertaintyInMilliseconds: UNCERTAINTY_MS
75655
+ })) }
75656
+ };
75657
+ }
75658
+ /** The one handler behind every climate directive. */
75659
+ async function handleClimateDirective(c) {
75660
+ const deps = c.deps.climateController;
75661
+ const slice = await readSlice$1(c, deps);
75662
+ if (slice === null) return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the climate state could not be read");
75663
+ const outcome = plan(c, slice, commandsFor(deps.ctx.api, c.deviceId));
75664
+ if (isRefusal(outcome)) return outcome.refusal;
75665
+ try {
75666
+ for (const command of outcome.commands) await command();
75667
+ } catch (err) {
75668
+ deps.logger.warn("alexa climate-controller: the climate command failed", {
75669
+ tags: { deviceId: c.deviceId },
75670
+ meta: {
75671
+ directive: c.header.name,
75672
+ error: err instanceof Error ? err.message : String(err)
75673
+ }
75674
+ });
75675
+ return errorEnvelope(c.header, "ENDPOINT_UNREACHABLE", "the device did not accept the command");
75676
+ }
75677
+ deps.logger.info(`alexa climate-controller: ${c.header.name}`, {
75678
+ tags: { deviceId: c.deviceId },
75679
+ meta: { commands: outcome.commands.length }
75680
+ });
75681
+ return successResponse$1(c, climateProperties(outcome.projected).filter(outcome.keep));
75682
+ }
75683
+ /**
75684
+ * Last answer per device, per deps instance: a failed bindings read changes
75685
+ * nothing (AGENTS D49) — a thermostat's TurnOff must not fall through to the
75686
+ * switch handler because one `getBindings` call failed.
75687
+ */
75688
+ var lastPowerClaim = /* @__PURE__ */ new WeakMap();
75689
+ /** PowerController on a climate endpoint, not on a switch: decided by the bound caps. */
75690
+ async function handlesPower(c) {
75691
+ const deps = c.deps.climateController;
75692
+ const known = lastPowerClaim.get(deps) ?? /* @__PURE__ */ new Map();
75693
+ lastPowerClaim.set(deps, known);
75694
+ try {
75695
+ const caps = (await deps.ctx.api.deviceManager.getBindings.query({ deviceId: c.deviceId })).entries.map((e) => e.capName);
75696
+ const claim = caps.includes(CAP_CLIMATE) && !caps.includes(CAP_SWITCH$1);
75697
+ known.set(c.deviceId, claim);
75698
+ return claim;
75699
+ } catch (err) {
75700
+ const previous = known.get(c.deviceId);
75701
+ deps.logger.warn("alexa climate-controller: the bound caps could not be read", {
75702
+ tags: { deviceId: c.deviceId },
75703
+ meta: {
75704
+ error: err instanceof Error ? err.message : String(err),
75705
+ decidedFrom: previous === void 0 ? "no previous answer: not claimed" : "previous answer"
75706
+ }
75707
+ });
75708
+ return previous ?? false;
75709
+ }
75710
+ }
75711
+ //#endregion
74030
75712
  //#region src/endpoints/adapters/shared.ts
74031
75713
  /** The base `Alexa` interface every endpoint declares. */
74032
75714
  var BASE = {
@@ -74077,6 +75759,88 @@ function numericValueField(value) {
74077
75759
  return typeof raw === "number" ? raw : null;
74078
75760
  }
74079
75761
  //#endregion
75762
+ //#region src/endpoints/adapters/temperature.ts
75763
+ /** A temperature change under half a degree is not worth a ChangeReport. */
75764
+ var TEMPERATURE_DEADBAND_CELSIUS = .5;
75765
+ /** A numeric sensor reports at most once a minute per property (D869). */
75766
+ var SENSOR_MIN_INTERVAL_MS = 6e4;
75767
+ /** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
75768
+ var temperatureAdapter = {
75769
+ cap: "temperature-sensor",
75770
+ stateCaps: ["temperature-sensor"],
75771
+ discoveryReads: [],
75772
+ describe: () => ({
75773
+ kind: "publish",
75774
+ capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
75775
+ category: "TEMPERATURE_SENSOR"
75776
+ }),
75777
+ properties: (_cap, slice) => {
75778
+ const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
75779
+ return p.success ? [{
75780
+ namespace: "Alexa.TemperatureSensor",
75781
+ name: "temperature",
75782
+ value: {
75783
+ value: Math.round(p.data.celsius * 10) / 10,
75784
+ scale: "CELSIUS"
75785
+ }
75786
+ }] : [];
75787
+ },
75788
+ directives: [],
75789
+ shaping: () => ({
75790
+ deadband: TEMPERATURE_DEADBAND_CELSIUS,
75791
+ minIntervalMs: SENSOR_MIN_INTERVAL_MS,
75792
+ numericOf: numericValueField
75793
+ })
75794
+ };
75795
+ //#endregion
75796
+ //#region src/endpoints/adapters/climate.ts
75797
+ var OWN_INSTANCES = new Set([INSTANCE_MODE, INSTANCE_FAN]);
75798
+ var THERMOSTAT_DIRECTIVES = [
75799
+ "Alexa.ThermostatController/SetTargetTemperature",
75800
+ "Alexa.ThermostatController/AdjustTargetTemperature",
75801
+ "Alexa.ThermostatController/SetThermostatMode"
75802
+ ].map((key) => ({
75803
+ key,
75804
+ needsReachableDevice: true,
75805
+ handle: handleClimateDirective
75806
+ }));
75807
+ var POWER_DIRECTIVES$1 = ["Alexa.PowerController/TurnOn", "Alexa.PowerController/TurnOff"].map((key) => ({
75808
+ key,
75809
+ needsReachableDevice: true,
75810
+ handle: handleClimateDirective,
75811
+ claims: handlesPower
75812
+ }));
75813
+ var MODE_DIRECTIVE = {
75814
+ key: "Alexa.ModeController/SetMode",
75815
+ needsReachableDevice: true,
75816
+ handle: handleClimateDirective,
75817
+ claims: async (c) => c.header.instance !== void 0 && OWN_INSTANCES.has(c.header.instance)
75818
+ };
75819
+ /**
75820
+ * `climate-control` -> a thermostat (Home Assistant) or an air conditioner
75821
+ * (Gree) (D874): `Alexa.ThermostatController` 3.1 with the current temperature,
75822
+ * power, and the modes Amazon's vocabulary lacks and the fan mode as
75823
+ * ModeControllers. The current temperature is shaped like any temperature
75824
+ * sensor; setpoints, mode and fan report on every change.
75825
+ */
75826
+ var climateAdapter = {
75827
+ cap: "climate-control",
75828
+ stateCaps: ["climate-control"],
75829
+ discoveryReads: ["climate-control"],
75830
+ describe: describeClimate,
75831
+ properties: (_cap, slice) => climateProperties(slice),
75832
+ directives: [
75833
+ ...THERMOSTAT_DIRECTIVES,
75834
+ ...POWER_DIRECTIVES$1,
75835
+ MODE_DIRECTIVE
75836
+ ],
75837
+ shaping: (property) => property.namespace === "Alexa.TemperatureSensor" ? {
75838
+ deadband: TEMPERATURE_DEADBAND_CELSIUS,
75839
+ minIntervalMs: SENSOR_MIN_INTERVAL_MS,
75840
+ numericOf: numericValueField
75841
+ } : null
75842
+ };
75843
+ //#endregion
74080
75844
  //#region src/endpoints/adapters/contact.ts
74081
75845
  /** `contact` → `Alexa.ContactSensor` (CONTACT_SENSOR). Amazon: DETECTED = open. */
74082
75846
  var contactAdapter = {
@@ -74741,7 +76505,8 @@ var coverAdapter = {
74741
76505
  ].map((key) => ({
74742
76506
  key,
74743
76507
  needsReachableDevice: true,
74744
- handle: handleCoverDirective
76508
+ handle: handleCoverDirective,
76509
+ claims: async (c) => c.header.instance?.startsWith("Blind.") ?? false
74745
76510
  })),
74746
76511
  shaping: (property) => property.namespace === "Alexa.RangeController" ? {
74747
76512
  deadband: 1,
@@ -74749,40 +76514,6 @@ var coverAdapter = {
74749
76514
  numericOf: plainNumber
74750
76515
  } : { debounceMs: COVER_DEBOUNCE_MS }
74751
76516
  };
74752
- //#endregion
74753
- //#region src/endpoints/adapters/temperature.ts
74754
- /** A temperature change under half a degree is not worth a ChangeReport. */
74755
- var TEMPERATURE_DEADBAND_CELSIUS = .5;
74756
- /** A numeric sensor reports at most once a minute per property (D869). */
74757
- var SENSOR_MIN_INTERVAL_MS = 6e4;
74758
- /** `temperature-sensor` → `Alexa.TemperatureSensor` (TEMPERATURE_SENSOR). */
74759
- var temperatureAdapter = {
74760
- cap: "temperature-sensor",
74761
- stateCaps: ["temperature-sensor"],
74762
- discoveryReads: [],
74763
- describe: () => ({
74764
- kind: "publish",
74765
- capabilities: [iface("Alexa.TemperatureSensor", "temperature")],
74766
- category: "TEMPERATURE_SENSOR"
74767
- }),
74768
- properties: (_cap, slice) => {
74769
- const p = TemperatureSensorStatusSchema.pick({ celsius: true }).safeParse(slice);
74770
- return p.success ? [{
74771
- namespace: "Alexa.TemperatureSensor",
74772
- name: "temperature",
74773
- value: {
74774
- value: Math.round(p.data.celsius * 10) / 10,
74775
- scale: "CELSIUS"
74776
- }
74777
- }] : [];
74778
- },
74779
- directives: [],
74780
- shaping: () => ({
74781
- deadband: TEMPERATURE_DEADBAND_CELSIUS,
74782
- minIntervalMs: SENSOR_MIN_INTERVAL_MS,
74783
- numericOf: numericValueField
74784
- })
74785
- };
74786
76517
  /** `humidity-sensor` → `Alexa.HumiditySensor` (TEMPERATURE_SENSOR). */
74787
76518
  var humidityAdapter = {
74788
76519
  cap: "humidity-sensor",
@@ -75081,6 +76812,11 @@ var BRIGHTNESS_DIRECTIVE = {
75081
76812
  needsReachableDevice: true,
75082
76813
  handle: (c) => handleSetBrightness(c.header, c.endpoint, c.payload, c.deps.brightnessController, c.identity)
75083
76814
  };
76815
+ /** An outlet (D875: its source says so, e.g. HA `device_class: outlet`) is a
76816
+ * smart plug; every other switch is a SWITCH. */
76817
+ function switchCategory(device) {
76818
+ return device.role === DeviceRole.Outlet ? "SMARTPLUG" : "SWITCH";
76819
+ }
75084
76820
  //#endregion
75085
76821
  //#region src/endpoints/adapters/index.ts
75086
76822
  var GENERIC_ADAPTERS = [
@@ -75088,6 +76824,7 @@ var GENERIC_ADAPTERS = [
75088
76824
  contactAdapter,
75089
76825
  motionAdapter,
75090
76826
  presenceAdapter,
76827
+ climateAdapter,
75091
76828
  temperatureAdapter,
75092
76829
  humidityAdapter,
75093
76830
  {
@@ -75099,7 +76836,7 @@ var GENERIC_ADAPTERS = [
75099
76836
  return {
75100
76837
  kind: "publish",
75101
76838
  capabilities: [iface("Alexa.PowerController", "powerState"), ...dimmable ? [iface("Alexa.BrightnessController", "brightness")] : []],
75102
- category: dimmable || device.type === "light" ? "LIGHT" : "SWITCH"
76839
+ category: dimmable || device.type === "light" ? "LIGHT" : switchCategory(device)
75103
76840
  };
75104
76841
  },
75105
76842
  properties: (capName, slice) => {
@@ -75145,12 +76882,19 @@ function shapingFor(capName, property) {
75145
76882
  function discoveryReadCaps(capNames) {
75146
76883
  return [...new Set(GENERIC_ADAPTERS.filter((a) => capNames.includes(a.cap)).flatMap((a) => a.discoveryReads))];
75147
76884
  }
75148
- /** A directive an adapter handles on a generic endpoint, by `Namespace/Name`. */
75149
- function genericDirective(key) {
75150
- for (const adapter of GENERIC_ADAPTERS) {
75151
- const found = adapter.directives.find((d) => d.key === key);
75152
- if (found !== void 0) return found;
75153
- }
76885
+ /** Every directive an adapter declares for `Namespace/Name`, in registry order. */
76886
+ function genericDirectives(key) {
76887
+ return GENERIC_ADAPTERS.flatMap((a) => a.directives.filter((d) => d.key === key));
76888
+ }
76889
+ /**
76890
+ * The directive that serves this endpoint: of the adapters declaring the key,
76891
+ * in registry order, the first that claims it (two caps may answer one key -
76892
+ * power on a switch and on a thermostat). Null = nobody claims it.
76893
+ */
76894
+ async function resolveGenericDirective(context) {
76895
+ const key = `${context.header.namespace}/${context.header.name}`;
76896
+ for (const directive of genericDirectives(key)) if (directive.claims === void 0 || await directive.claims(context)) return directive;
76897
+ return null;
75154
76898
  }
75155
76899
  /** Every directive key the registry handles. */
75156
76900
  var GENERIC_DIRECTIVE_KEYS = [...new Set(GENERIC_ADAPTERS.flatMap((a) => a.directives.map((d) => d.key)))];
@@ -75192,9 +76936,9 @@ function planGenericDevice(device) {
75192
76936
  category
75193
76937
  };
75194
76938
  }
75195
- /** Device types this export may publish as a generic endpoint. A `thermostat`
75196
- * is listed so discovery can say WHY it is not published (no climate adapter
75197
- * yet, D869) instead of calling it a device with no mappable cap. */
76939
+ /** Device types this export may publish as a generic endpoint. `thermostat`
76940
+ * (Home Assistant) and `climate` (an air conditioner, e.g. Gree) both carry
76941
+ * the `climate-control` cap (D874). */
75198
76942
  var GENERIC_DEVICE_TYPES = [
75199
76943
  "light",
75200
76944
  "switch",
@@ -75203,11 +76947,13 @@ var GENERIC_DEVICE_TYPES = [
75203
76947
  "lock",
75204
76948
  "presence",
75205
76949
  "thermostat",
76950
+ "climate",
75206
76951
  "cover",
75207
76952
  "generic"
75208
76953
  ];
75209
76954
  function toEndpoint(input, plan) {
75210
- const name = input.name.trim();
76955
+ const override = input.alexaName?.trim() ?? "";
76956
+ const name = override.length > 0 ? override : input.name.trim();
75211
76957
  return {
75212
76958
  endpointId: `${input.deviceId}__dev`,
75213
76959
  manufacturerName: "camstack",
@@ -75218,13 +76964,7 @@ function toEndpoint(input, plan) {
75218
76964
  cookie: { stableId: input.stableId }
75219
76965
  };
75220
76966
  }
75221
- /** Why a climate device is withheld until its adapter exists. */
75222
- var THERMOSTAT_SKIP_REASON = "thermostat: Alexa climate control (Alexa.ThermostatController) has no adapter yet - not published until it does (D869, P3)";
75223
76967
  function planGenericEndpoint(input) {
75224
- if (input.type === "thermostat" && input.capNames.includes("climate-control")) return {
75225
- kind: "skip",
75226
- reasons: [THERMOSTAT_SKIP_REASON]
75227
- };
75228
76968
  const plan = planGenericDevice({
75229
76969
  deviceId: input.deviceId,
75230
76970
  type: input.type,
@@ -75240,6 +76980,74 @@ function planGenericEndpoint(input) {
75240
76980
  return plan;
75241
76981
  }
75242
76982
  //#endregion
76983
+ //#region src/endpoints/alexa-name.ts
76984
+ /**
76985
+ * The per-device "Alexa name" (D875): what Alexa calls the device, when it
76986
+ * should not be the device's own name.
76987
+ *
76988
+ * Stored under {@link ALEXA_NAME_KEY} in this addon's per-device settings store
76989
+ * (D870) and used as the endpoint `friendlyName`. It never reaches the endpoint
76990
+ * id (`<deviceId>__dev`), so renaming updates the SAME Alexa device on the next
76991
+ * AddOrUpdateReport instead of creating a duplicate.
76992
+ *
76993
+ * Amazon's friendlyName rule (Alexa.Discovery objects, endpoint object): "Up
76994
+ * to 256 alphanumeric characters and spaces. Don't include special characters
76995
+ * or punctuation." Letters of any script count as alphanumeric, so an Italian
76996
+ * name keeps its accents. The validator rejects what Amazon would reject, with
76997
+ * a message the operator can act on.
76998
+ */
76999
+ /** Per-device store key of the saved Alexa name. */
77000
+ var ALEXA_NAME_KEY = "alexaName";
77001
+ /** The device-settings form field key, namespaced per exporter (see the addon). */
77002
+ function alexaNameKey(deviceId) {
77003
+ return `alexa:${deviceId}:alexaName`;
77004
+ }
77005
+ /** Letters (any script, with their combining marks), digits and spaces. */
77006
+ var ALLOWED_PATTERN = /^[\p{L}\p{M}\p{N} ]+$/u;
77007
+ /**
77008
+ * Validate an operator-typed Alexa name. Blank clears the override
77009
+ * (`name: null`, the device name is used).
77010
+ */
77011
+ function validateAlexaName(raw) {
77012
+ if (raw === null || raw === void 0) return {
77013
+ ok: true,
77014
+ name: null
77015
+ };
77016
+ if (typeof raw !== "string") return {
77017
+ ok: false,
77018
+ error: "The Alexa name must be text."
77019
+ };
77020
+ const name = raw.trim().replace(/\s+/g, " ");
77021
+ if (name.length === 0) return {
77022
+ ok: true,
77023
+ name: null
77024
+ };
77025
+ if (name.length > 256) return {
77026
+ ok: false,
77027
+ error: `The Alexa name is ${name.length} characters; Amazon allows at most 256.`
77028
+ };
77029
+ if (!ALLOWED_PATTERN.test(name)) return {
77030
+ ok: false,
77031
+ error: "The Alexa name may contain only letters, digits and spaces (Amazon rejects punctuation and special characters)."
77032
+ };
77033
+ return {
77034
+ ok: true,
77035
+ name
77036
+ };
77037
+ }
77038
+ var StoredNameSchema = string();
77039
+ /**
77040
+ * The saved Alexa name in a per-device store blob, or null (never saved, or a
77041
+ * stored value that no longer validates: the device name is used rather than
77042
+ * pushing a name Amazon would refuse).
77043
+ */
77044
+ function readAlexaName(store) {
77045
+ const parsed = StoredNameSchema.safeParse(store[ALEXA_NAME_KEY]);
77046
+ if (!parsed.success) return null;
77047
+ const check = validateAlexaName(parsed.data);
77048
+ return check.ok ? check.name : null;
77049
+ }
77050
+ //#endregion
75243
77051
  //#region src/handlers/security-panel-arm-state.ts
75244
77052
  /**
75245
77053
  * The panel modes behind each Alexa arm state, PRIMARY first — the mirror of
@@ -75913,6 +77721,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
75913
77721
  deviceId: parentInfo.id,
75914
77722
  stableId: parentInfo.stableId,
75915
77723
  name: parentInfo.name,
77724
+ alexaName: deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id),
75916
77725
  type: String(device.type),
75917
77726
  capNames: parentInfo.capNames,
75918
77727
  role: parentInfo.role,
@@ -75971,11 +77780,12 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
75971
77780
  return [];
75972
77781
  });
75973
77782
  const objectDetectionClasses = deps.objectDetection === void 0 ? [] : await deps.objectDetection(parentInfo.id);
77783
+ const cameraName = (deps.alexaName === void 0 ? null : await deps.alexaName(parentInfo.id)) ?? parentInfo.name;
75974
77784
  const out = [];
75975
77785
  out.push(buildParentCameraEndpoint({
75976
77786
  deviceId: parentInfo.id,
75977
77787
  stableId: parentInfo.stableId,
75978
- friendlyName: parentInfo.name,
77788
+ friendlyName: cameraName,
75979
77789
  isDoorbell,
75980
77790
  hasIntercom,
75981
77791
  hasMotion,
@@ -75989,7 +77799,7 @@ async function buildEndpointsForExposedDevice(deviceId, deps) {
75989
77799
  for (const detectionClass of classes) out.push(buildDetectionSensorEndpoint({
75990
77800
  deviceId: parentInfo.id,
75991
77801
  stableId: parentInfo.stableId,
75992
- cameraName: parentInfo.name,
77802
+ cameraName,
75993
77803
  detectionClass
75994
77804
  }));
75995
77805
  }
@@ -76483,6 +78293,36 @@ function validateInstanced(c, push) {
76483
78293
  if (c.semantics.stateMappings !== void 0) validateStateMappings(c, c.semantics.stateMappings, push, validMode);
76484
78294
  }
76485
78295
  //#endregion
78296
+ //#region src/endpoints/validate-thermostat.ts
78297
+ var PROPERTIES = new Set([
78298
+ "targetSetpoint",
78299
+ "lowerSetpoint",
78300
+ "upperSetpoint",
78301
+ "thermostatMode"
78302
+ ]);
78303
+ var MODES = new Set([
78304
+ "AUTO",
78305
+ "COOL",
78306
+ "HEAT",
78307
+ "ECO",
78308
+ "OFF"
78309
+ ]);
78310
+ function validateThermostat(c, push) {
78311
+ const supported = new Set((c.properties?.supported ?? []).map((p) => p.name));
78312
+ for (const name of supported) if (!PROPERTIES.has(name)) push(`properties.supported ${name} is not a thermostat property`);
78313
+ if (!supported.has("thermostatMode")) push("properties.supported must include thermostatMode");
78314
+ if (supported.has("lowerSetpoint") !== supported.has("upperSetpoint")) push("lowerSetpoint and upperSetpoint are declared together");
78315
+ if (!supported.has("targetSetpoint") && !supported.has("lowerSetpoint")) push("at least one setpoint property is required");
78316
+ const modes = c.configuration?.["supportedModes"];
78317
+ if (!Array.isArray(modes) || modes.length === 0) push("configuration.supportedModes must list at least one mode");
78318
+ else {
78319
+ for (const mode of modes) if (typeof mode !== "string" || !MODES.has(mode)) push(`supportedModes ${String(mode)} is not AUTO, COOL, HEAT, ECO or OFF`);
78320
+ if (new Set(modes).size !== modes.length) push("supportedModes values must be unique");
78321
+ }
78322
+ const scheduling = c.configuration?.["supportsScheduling"];
78323
+ if (scheduling !== void 0 && typeof scheduling !== "boolean") push("configuration.supportsScheduling must be a boolean");
78324
+ }
78325
+ //#endregion
76486
78326
  //#region src/endpoints/validate-endpoint.ts
76487
78327
  /**
76488
78328
  * Interfaces this skill may declare, with the version Amazon documents. An
@@ -76506,6 +78346,7 @@ var ALLOWED_INTERFACE_VERSIONS = {
76506
78346
  "Alexa.RangeController": "3",
76507
78347
  "Alexa.SecurityPanelController": "3",
76508
78348
  "Alexa.LockController": "3",
78349
+ "Alexa.ThermostatController": "3.1",
76509
78350
  "Alexa.SmartVision.SnapshotProvider": "1.1",
76510
78351
  "Alexa.SmartVision.ObjectDetectionSensor": "1.0",
76511
78352
  "Alexa.DataController": "1.0"
@@ -76545,6 +78386,8 @@ var DISPLAY_CATEGORIES = new Set([
76545
78386
  "SMARTLOCK",
76546
78387
  "INTERIOR_BLIND",
76547
78388
  "EXTERIOR_BLIND",
78389
+ "THERMOSTAT",
78390
+ "AIR_CONDITIONER",
76548
78391
  "DOOR",
76549
78392
  "GATE",
76550
78393
  "GARAGE_DOOR",
@@ -76623,6 +78466,7 @@ function validateCapability(endpointId, c, out, declared) {
76623
78466
  }
76624
78467
  }
76625
78468
  validateInstanced(c, push);
78469
+ if (c.interface === "Alexa.ThermostatController") validateThermostat(c, push);
76626
78470
  if (c.interface === "Alexa.SmartVision.ObjectDetectionSensor") validateObjectDetection(c, push);
76627
78471
  if (c.interface === "Alexa.DataController") validateDataController(c, declared, push);
76628
78472
  if (c.interface === "Alexa.SmartVision.SnapshotProvider") {
@@ -78553,20 +80397,27 @@ async function dispatchParsedDirective(rawBody, deps, callerIdentity) {
78553
80397
  status: 200,
78554
80398
  body: await handleSessionDisconnected(header, endpoint, payload, deps.rtcSession, callerIdentity)
78555
80399
  };
78556
- const adapterDirective = genericDirective(key);
78557
- if (adapterDirective !== void 0 && endpoint !== void 0) {
80400
+ if (genericDirectives(key).length > 0 && endpoint !== void 0) {
78558
80401
  const target = parseCameraEndpointId(endpoint.endpointId);
78559
- if (target.kind === "generic") return {
78560
- status: 200,
78561
- body: await adapterDirective.handle({
80402
+ if (target.kind === "generic") {
80403
+ const directiveContext = {
78562
80404
  header,
78563
80405
  endpoint,
78564
80406
  payload,
78565
80407
  deviceId: target.deviceId,
78566
80408
  deps,
78567
80409
  identity: callerIdentity
78568
- })
78569
- };
80410
+ };
80411
+ const adapterDirective = await resolveGenericDirective(directiveContext);
80412
+ if (adapterDirective === null) return {
80413
+ status: 200,
80414
+ body: errorEnvelope(header, "INVALID_DIRECTIVE", `${key}: no handler for this endpoint`)
80415
+ };
80416
+ return {
80417
+ status: 200,
80418
+ body: await adapterDirective.handle(directiveContext)
80419
+ };
80420
+ }
78570
80421
  }
78571
80422
  if (key === "Alexa.ModeController/SetMode") return {
78572
80423
  status: 200,
@@ -82709,6 +84560,7 @@ var SUPPORTED_KINDS = [
82709
84560
  DeviceType.Lock,
82710
84561
  DeviceType.Presence,
82711
84562
  DeviceType.Thermostat,
84563
+ DeviceType.Climate,
82712
84564
  DeviceType.Cover,
82713
84565
  DeviceType.Generic
82714
84566
  ];
@@ -82847,6 +84699,10 @@ var ExportAlexaAddon = class extends BaseAddon {
82847
84699
  ctx: this.ctx,
82848
84700
  logger: this.ctx.logger
82849
84701
  },
84702
+ climateController: {
84703
+ ctx: this.ctx,
84704
+ logger: this.ctx.logger
84705
+ },
82850
84706
  coverController: {
82851
84707
  ctx: this.ctx,
82852
84708
  logger: this.ctx.logger,
@@ -83103,6 +84959,7 @@ var ExportAlexaAddon = class extends BaseAddon {
83103
84959
  logger: this.ctx.logger,
83104
84960
  snapshots: this.config.snapshots,
83105
84961
  ...this.config.detectionSensors ? { detectionClasses: async (id) => effectiveSensorClasses(await classesOf(id), parseSensorSelection(await vision.readPrefs(id))) } : {},
84962
+ alexaName: async (id) => readAlexaName(await vision.readPrefs(id)),
83106
84963
  ...this.config.objectDetection ? { objectDetection: async (id) => vision.buildClasses(id, await classesOf(id)) } : {}
83107
84964
  });
83108
84965
  }
@@ -83670,12 +85527,28 @@ var ExportAlexaAddon = class extends BaseAddon {
83670
85527
  value: enabled,
83671
85528
  immediate: true
83672
85529
  },
85530
+ await this.alexaNameField(deviceId, enabledKey),
83673
85531
  ...kind === "camera" ? this.cameraOnlyExportFields(deviceId, settings, enabledKey) : [],
83674
85532
  ...kind === "camera" ? await this.cameraDetectionFields(deviceId, enabledKey) : []
83675
85533
  ]
83676
85534
  }]
83677
85535
  };
83678
85536
  }
85537
+ /** D875: the "Alexa name" field, with the validation error of the saved value. */
85538
+ async alexaNameField(deviceId, enabledKey) {
85539
+ const saved = readAlexaName(await this.vision.readPrefs(deviceId));
85540
+ return {
85541
+ type: "text",
85542
+ key: alexaNameKey(deviceId),
85543
+ label: "Alexa name",
85544
+ 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.`,
85545
+ value: saved ?? "",
85546
+ showWhen: {
85547
+ field: enabledKey,
85548
+ equals: true
85549
+ }
85550
+ };
85551
+ }
83679
85552
  /**
83680
85553
  * The camera's detection fields: which classes become Alexa sensors (D778;
83681
85554
  * only the classes its pipeline produces are offered) and its object
@@ -83697,6 +85570,24 @@ var ExportAlexaAddon = class extends BaseAddon {
83697
85570
  })];
83698
85571
  }
83699
85572
  /**
85573
+ * D875: save the Alexa name from a device-settings patch. A name Amazon would
85574
+ * refuse throws BEFORE anything is written, so the form shows the reason and
85575
+ * the old name stays. True when the saved name changed.
85576
+ */
85577
+ async applyAlexaNamePatch(deviceId, patch) {
85578
+ const key = alexaNameKey(deviceId);
85579
+ if (!(key in patch)) return false;
85580
+ const check = validateAlexaName(patch[key]);
85581
+ if (!check.ok) throw new Error(`export-alexa: ${check.error}`);
85582
+ if (readAlexaName(await this.vision.readPrefs(deviceId)) === check.name) return false;
85583
+ await this.vision.writePrefs(deviceId, { [ALEXA_NAME_KEY]: check.name ?? void 0 });
85584
+ this.ctx.logger.info("export-alexa: Alexa name saved", {
85585
+ tags: { deviceId },
85586
+ meta: { name: check.name }
85587
+ });
85588
+ return true;
85589
+ }
85590
+ /**
83700
85591
  * Persist the camera's detection preferences from a device-settings patch
83701
85592
  * into this addon's per-device store. True when anything changed — the
83702
85593
  * camera's Alexa shape then needs a reconcile.
@@ -83761,6 +85652,7 @@ var ExportAlexaAddon = class extends BaseAddon {
83761
85652
  const current = this.findEntry(deviceId);
83762
85653
  const wasEnabled = current !== null;
83763
85654
  const prefsChanged = await this.applyDetectionPrefsPatch(deviceId, patch);
85655
+ const nameChanged = await this.applyAlexaNamePatch(deviceId, patch);
83764
85656
  const enabledKey = `alexa:${deviceId}:enabled`;
83765
85657
  const streamPreferenceKey = `alexa:${deviceId}:streamPreference`;
83766
85658
  const allowTranscodeKey = `alexa:${deviceId}:allowTranscode`;
@@ -83786,6 +85678,12 @@ var ExportAlexaAddon = class extends BaseAddon {
83786
85678
  const currentAllow = current?.settings?.allowTranscode !== false;
83787
85679
  await this.patchEntrySettings(deviceId, nextSettings);
83788
85680
  const sourceChanged = currentPref !== streamPreference || currentAllow !== allowTranscode;
85681
+ if (nameChanged && !sourceChanged) this.requireSyncEngine().pushDevice(deviceId).catch((err) => {
85682
+ this.ctx.logger.warn("export-alexa: push after an Alexa name change failed", {
85683
+ tags: { deviceId },
85684
+ meta: { error: errMsg(err) }
85685
+ });
85686
+ });
83789
85687
  if (prefsChanged && !sourceChanged) this.requireSyncEngine().reconcileDevice(deviceId).catch((err) => {
83790
85688
  this.ctx.logger.warn("export-alexa: re-sync after a detection preference change failed", {
83791
85689
  tags: { deviceId },