@camstack/addon-provider-homeassistant 1.2.655 → 1.2.656

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.
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
5981
5981
  var ZodFirstPartyTypeKind;
5982
5982
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5983
5983
  //#endregion
5984
- //#region ../types/dist/sleep-BkzQdpzF.mjs
5984
+ //#region ../types/dist/sleep-BaWhXZFm.mjs
5985
5985
  /**
5986
5986
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5987
5987
  * window to float samples (D455).
@@ -8783,6 +8783,13 @@ var RecordsError = class extends Error {
8783
8783
  this.detail = detail;
8784
8784
  }
8785
8785
  };
8786
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
8787
+ var RevConflictError = class extends RecordsError {
8788
+ constructor(detail) {
8789
+ super("rev-conflict", detail);
8790
+ this.name = "RevConflictError";
8791
+ }
8792
+ };
8786
8793
  /** The keychain could not store or serve a value: explicit, never an empty string. */
8787
8794
  var RecordSecretUnavailableError = class extends RecordsError {
8788
8795
  constructor(detail) {
@@ -8827,6 +8834,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8827
8834
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8828
8835
  * (`auth.accessToken`, `headers[].value`).
8829
8836
  *
8837
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8838
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8839
+ * key releases exactly that key's secret.
8840
+ *
8830
8841
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8831
8842
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8832
8843
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8839,11 +8850,13 @@ function parseSecretPath(path) {
8839
8850
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8840
8851
  return path.split(".").map((raw) => {
8841
8852
  const each = raw.endsWith("[]");
8842
- const key = each ? raw.slice(0, -2) : raw;
8853
+ const map = raw.endsWith("{}");
8854
+ const key = each || map ? raw.slice(0, -2) : raw;
8843
8855
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8844
8856
  return {
8845
8857
  key,
8846
- each
8858
+ each,
8859
+ map
8847
8860
  };
8848
8861
  });
8849
8862
  }
@@ -8895,6 +8908,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8895
8908
  [segment.key]: mapped
8896
8909
  };
8897
8910
  }
8911
+ if (segment.map) {
8912
+ if (!isRecord$4(child)) return node;
8913
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8914
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8915
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8916
+ return [mapKey, visit({
8917
+ declared,
8918
+ concrete,
8919
+ value
8920
+ })];
8921
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8922
+ return {
8923
+ ...node,
8924
+ [segment.key]: Object.fromEntries(kept)
8925
+ };
8926
+ }
8898
8927
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8899
8928
  if (last) {
8900
8929
  const replacement = visit({
@@ -9036,7 +9065,8 @@ function isZodSecret(schema) {
9036
9065
  }
9037
9066
  /**
9038
9067
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9039
- * element of this array" (`connection.password`, `rows[].token`). A declared
9068
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9069
+ * map" (`applianceSecrets{}`). A declared
9040
9070
  * object or array is reported as one path and not descended into — everything
9041
9071
  * under it is secret.
9042
9072
  */
@@ -9047,6 +9077,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9047
9077
  const def = defOf(layer);
9048
9078
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9049
9079
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9080
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9050
9081
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9051
9082
  else if (def.type === "intersection") {
9052
9083
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -13423,11 +13454,49 @@ function buildCapturingReply(envelope) {
13423
13454
  };
13424
13455
  return wrapper;
13425
13456
  }
13457
+ /**
13458
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13459
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13460
+ *
13461
+ * Every instance is one record of the addon's own kind, keyed by the
13462
+ * `integrations` registry id. The records door does the secret work: a field the
13463
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13464
+ * deleting the row deletes the secret, and one row's value is never another's.
13465
+ *
13466
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13467
+ * token or the view the form was given KEEP the stored secret;
13468
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13469
+ */
13426
13470
  /** The most instances one addon lists (an integrations page, not a table). */
13427
13471
  var INSTANCES_LIST_MAX = 1e3;
13428
13472
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13429
13473
  function topLevelSecretKeys(paths) {
13430
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13474
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13475
+ }
13476
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13477
+ function secretMapKeys(paths) {
13478
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13479
+ }
13480
+ /**
13481
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13482
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13483
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13484
+ */
13485
+ function secretMapPatch(value) {
13486
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13487
+ const out = {};
13488
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13489
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13490
+ return Object.keys(out).length > 0 ? out : null;
13491
+ }
13492
+ /** Does the kind's body schema carry an `enabled` field? */
13493
+ function declaresEnabled(kind) {
13494
+ const json = toJSONSchema(kind.schema, {
13495
+ unrepresentable: "any",
13496
+ io: "input"
13497
+ });
13498
+ const properties = Reflect.get(json, "properties");
13499
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13431
13500
  }
13432
13501
  var RecordIntegrationInstances = class {
13433
13502
  records;
@@ -13435,14 +13504,20 @@ var RecordIntegrationInstances = class {
13435
13504
  hooks;
13436
13505
  handle;
13437
13506
  secretKeys;
13507
+ secretMaps;
13438
13508
  nameField;
13509
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13510
+ registryOwnsEnabled;
13439
13511
  constructor(records, kind, hooks) {
13440
13512
  this.records = records;
13441
13513
  this.kind = kind;
13442
13514
  this.hooks = hooks;
13443
13515
  this.handle = records.kind(kind);
13444
13516
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13517
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13445
13518
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13519
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13520
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
13446
13521
  }
13447
13522
  /**
13448
13523
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13493,7 +13568,7 @@ var RecordIntegrationInstances = class {
13493
13568
  id: record.id,
13494
13569
  name: typeof rawName === "string" ? rawName : record.id,
13495
13570
  configured: this.hooks.configured(record.data),
13496
- enabled: record.data.enabled !== false,
13571
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13497
13572
  formValues: shown,
13498
13573
  ...kind !== void 0 ? { kind } : {},
13499
13574
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13523,32 +13598,36 @@ var RecordIntegrationInstances = class {
13523
13598
  id: minted,
13524
13599
  data: this.hooks.initialBody(name, kind)
13525
13600
  }, { ifRev: 0 });
13526
- await this.hooks.onChanged?.(minted);
13601
+ await this.hooks.onChanged?.(minted, "created");
13527
13602
  return { id: minted };
13528
13603
  };
13529
13604
  updateInstance = async ({ id, name, values }) => {
13530
13605
  const patch = {};
13531
13606
  for (const [key, value] of Object.entries(values)) {
13532
13607
  if (value === void 0) continue;
13533
- if (!this.secretKeys.has(key)) patch[key] = value;
13608
+ if (this.secretMaps.has(key)) {
13609
+ const entries = secretMapPatch(value);
13610
+ if (entries !== null) patch[key] = entries;
13611
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13534
13612
  else if (isSecretClear(value)) patch[key] = null;
13535
13613
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13536
13614
  }
13537
13615
  if (name !== void 0) patch[this.nameField] = name;
13538
13616
  await this.handle.patch(id, patch);
13539
- await this.hooks.onChanged?.(id);
13617
+ await this.hooks.onChanged?.(id, "updated");
13540
13618
  return { ok: true };
13541
13619
  };
13542
13620
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13543
13621
  setInstanceEnabled = async ({ id, enabled }) => {
13622
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13544
13623
  await this.handle.patch(id, { enabled });
13545
- await this.hooks.onChanged?.(id);
13624
+ await this.hooks.onChanged?.(id, "enabled");
13546
13625
  return { ok: true };
13547
13626
  };
13548
13627
  /** The row and its keychain refs go together (the records door). */
13549
13628
  deleteInstance = async ({ id }) => {
13550
13629
  await this.handle.delete(id);
13551
- await this.hooks.onChanged?.(id);
13630
+ await this.hooks.onChanged?.(id, "deleted");
13552
13631
  return { ok: true };
13553
13632
  };
13554
13633
  testInstance = async (input) => {
@@ -56775,6 +56854,21 @@ var IntegrationInstanceKindSchema = object({
56775
56854
  disconnectPath: string().min(1).optional()
56776
56855
  });
56777
56856
  var IntegrationInstanceDescriptionSchema = object({
56857
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56858
+ role: _enum([
56859
+ "devices",
56860
+ "services",
56861
+ "both"
56862
+ ]),
56863
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56864
+ discovery: object({
56865
+ mode: _enum([
56866
+ "none",
56867
+ "lan",
56868
+ "account"
56869
+ ]),
56870
+ scopedToInstance: boolean()
56871
+ }).optional(),
56778
56872
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56779
56873
  instanceMode: IntegrationInstanceModeSchema,
56780
56874
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56814,10 +56908,11 @@ var IntegrationInstanceSchema = object({
56814
56908
  /** The addon's own rule (an URL is present, a client id is present...). */
56815
56909
  configured: boolean(),
56816
56910
  /**
56817
- * The instance's own on/off switch (D996): always present, derived from the record body
56818
- * (`enabled !== false`, so a row written before the field existed is on).
56911
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56912
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56913
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56819
56914
  */
56820
- enabled: boolean(),
56915
+ enabled: boolean().optional(),
56821
56916
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56822
56917
  kind: string().optional(),
56823
56918
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -98300,21 +98395,6 @@ function haInstanceSettingsPortsOver(api) {
98300
98395
  }
98301
98396
  };
98302
98397
  }
98303
- /** The brokerId → settings view of the instances: what a reader keeps per broker. */
98304
- function settingsByBroker(rows) {
98305
- const out = /* @__PURE__ */ new Map();
98306
- for (const row of rows) if (row.brokerId !== null) out.set(row.brokerId, row.settings);
98307
- return out;
98308
- }
98309
- /** Read the instances, or report why not and answer nothing (a reader falls back to the legacy key). */
98310
- async function listInstancesOrNothing(ports, onError) {
98311
- try {
98312
- return await ports.listInstances();
98313
- } catch (err) {
98314
- onError(errMsg(err));
98315
- return [];
98316
- }
98317
- }
98318
98398
  //#endregion
98319
98399
  Object.defineProperty(exports, "AlarmArmModeSchema", {
98320
98400
  enumerable: true,
@@ -98472,6 +98552,12 @@ Object.defineProperty(exports, "RecordSecretUnavailableError", {
98472
98552
  return RecordSecretUnavailableError;
98473
98553
  }
98474
98554
  });
98555
+ Object.defineProperty(exports, "RevConflictError", {
98556
+ enumerable: true,
98557
+ get: function() {
98558
+ return RevConflictError;
98559
+ }
98560
+ });
98475
98561
  Object.defineProperty(exports, "TargetSchema", {
98476
98562
  enumerable: true,
98477
98563
  get: function() {
@@ -98808,12 +98894,6 @@ Object.defineProperty(exports, "listHomeAssistantBrokers", {
98808
98894
  return listHomeAssistantBrokers;
98809
98895
  }
98810
98896
  });
98811
- Object.defineProperty(exports, "listInstancesOrNothing", {
98812
- enumerable: true,
98813
- get: function() {
98814
- return listInstancesOrNothing;
98815
- }
98816
- });
98817
98897
  Object.defineProperty(exports, "literal", {
98818
98898
  enumerable: true,
98819
98899
  get: function() {
@@ -98994,12 +99074,6 @@ Object.defineProperty(exports, "sendToExportedTarget", {
98994
99074
  return sendToExportedTarget;
98995
99075
  }
98996
99076
  });
98997
- Object.defineProperty(exports, "settingsByBroker", {
98998
- enumerable: true,
98999
- get: function() {
99000
- return settingsByBroker;
99001
- }
99002
- });
99003
99077
  Object.defineProperty(exports, "smokeCapability", {
99004
99078
  enumerable: true,
99005
99079
  get: function() {
@@ -5980,7 +5980,7 @@ var ZodIssueCode = {
5980
5980
  var ZodFirstPartyTypeKind;
5981
5981
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5982
5982
  //#endregion
5983
- //#region ../types/dist/sleep-BkzQdpzF.mjs
5983
+ //#region ../types/dist/sleep-BaWhXZFm.mjs
5984
5984
  /**
5985
5985
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5986
5986
  * window to float samples (D455).
@@ -8782,6 +8782,13 @@ var RecordsError = class extends Error {
8782
8782
  this.detail = detail;
8783
8783
  }
8784
8784
  };
8785
+ /** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
8786
+ var RevConflictError = class extends RecordsError {
8787
+ constructor(detail) {
8788
+ super("rev-conflict", detail);
8789
+ this.name = "RevConflictError";
8790
+ }
8791
+ };
8785
8792
  /** The keychain could not store or serve a value: explicit, never an empty string. */
8786
8793
  var RecordSecretUnavailableError = class extends RecordsError {
8787
8794
  constructor(detail) {
@@ -8826,6 +8833,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
8826
8833
  * (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
8827
8834
  * (`auth.accessToken`, `headers[].value`).
8828
8835
  *
8836
+ * `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
8837
+ * names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
8838
+ * key releases exactly that key's secret.
8839
+ *
8829
8840
  * Array elements are matched by a DECLARED id key (`secretArrayKeys`:
8830
8841
  * `{ headers: 'name' }`), never by index, so reordering a list keeps every
8831
8842
  * keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
@@ -8838,11 +8849,13 @@ function parseSecretPath(path) {
8838
8849
  if (path.length === 0) throw new InvalidRecordError("an empty secret path");
8839
8850
  return path.split(".").map((raw) => {
8840
8851
  const each = raw.endsWith("[]");
8841
- const key = each ? raw.slice(0, -2) : raw;
8852
+ const map = raw.endsWith("{}");
8853
+ const key = each || map ? raw.slice(0, -2) : raw;
8842
8854
  if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
8843
8855
  return {
8844
8856
  key,
8845
- each
8857
+ each,
8858
+ map
8846
8859
  };
8847
8860
  });
8848
8861
  }
@@ -8894,6 +8907,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
8894
8907
  [segment.key]: mapped
8895
8908
  };
8896
8909
  }
8910
+ if (segment.map) {
8911
+ if (!isRecord$4(child)) return node;
8912
+ const kept = Object.entries(child).map(([mapKey, value]) => {
8913
+ const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
8914
+ if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
8915
+ return [mapKey, visit({
8916
+ declared,
8917
+ concrete,
8918
+ value
8919
+ })];
8920
+ }).filter(([, value]) => value !== REMOVE_LEAF);
8921
+ return {
8922
+ ...node,
8923
+ [segment.key]: Object.fromEntries(kept)
8924
+ };
8925
+ }
8897
8926
  const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
8898
8927
  if (last) {
8899
8928
  const replacement = visit({
@@ -9035,7 +9064,8 @@ function isZodSecret(schema) {
9035
9064
  }
9036
9065
  /**
9037
9066
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
9038
- * element of this array" (`connection.password`, `rows[].token`). A declared
9067
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9068
+ * map" (`applianceSecrets{}`). A declared
9039
9069
  * object or array is reported as one path and not descended into — everything
9040
9070
  * under it is secret.
9041
9071
  */
@@ -9046,6 +9076,7 @@ function collectZodSecretPaths(schema, prefix = "") {
9046
9076
  const def = defOf(layer);
9047
9077
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9048
9078
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9079
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9049
9080
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9050
9081
  else if (def.type === "intersection") {
9051
9082
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -13422,11 +13453,49 @@ function buildCapturingReply(envelope) {
13422
13453
  };
13423
13454
  return wrapper;
13424
13455
  }
13456
+ /**
13457
+ * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13458
+ * entry kind, so an addon that holds a connection serves it in ~30 lines.
13459
+ *
13460
+ * Every instance is one record of the addon's own kind, keyed by the
13461
+ * `integrations` registry id. The records door does the secret work: a field the
13462
+ * kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
13463
+ * deleting the row deletes the secret, and one row's value is never another's.
13464
+ *
13465
+ * Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
13466
+ * token or the view the form was given KEEP the stored secret;
13467
+ * `{ $secretClear: true }` clears it; any other string replaces it.
13468
+ */
13425
13469
  /** The most instances one addon lists (an integrations page, not a table). */
13426
13470
  var INSTANCES_LIST_MAX = 1e3;
13427
13471
  /** The keys of the kind's secret fields that sit at the top level of the body. */
13428
13472
  function topLevelSecretKeys(paths) {
13429
- return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
13473
+ return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
13474
+ }
13475
+ /** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
13476
+ function secretMapKeys(paths) {
13477
+ return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
13478
+ }
13479
+ /**
13480
+ * A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
13481
+ * view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
13482
+ * or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
13483
+ */
13484
+ function secretMapPatch(value) {
13485
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13486
+ const out = {};
13487
+ for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
13488
+ else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
13489
+ return Object.keys(out).length > 0 ? out : null;
13490
+ }
13491
+ /** Does the kind's body schema carry an `enabled` field? */
13492
+ function declaresEnabled(kind) {
13493
+ const json = toJSONSchema(kind.schema, {
13494
+ unrepresentable: "any",
13495
+ io: "input"
13496
+ });
13497
+ const properties = Reflect.get(json, "properties");
13498
+ return typeof properties === "object" && properties !== null && "enabled" in properties;
13430
13499
  }
13431
13500
  var RecordIntegrationInstances = class {
13432
13501
  records;
@@ -13434,14 +13503,20 @@ var RecordIntegrationInstances = class {
13434
13503
  hooks;
13435
13504
  handle;
13436
13505
  secretKeys;
13506
+ secretMaps;
13437
13507
  nameField;
13508
+ /** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
13509
+ registryOwnsEnabled;
13438
13510
  constructor(records, kind, hooks) {
13439
13511
  this.records = records;
13440
13512
  this.kind = kind;
13441
13513
  this.hooks = hooks;
13442
13514
  this.handle = records.kind(kind);
13443
13515
  this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
13516
+ this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
13444
13517
  this.nameField = kind.declaration.entry?.nameField ?? "name";
13518
+ this.registryOwnsEnabled = hooks.description.role === "devices";
13519
+ if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
13445
13520
  }
13446
13521
  /**
13447
13522
  * One instance read for the addon's own runtime; null when absent. When the keychain is
@@ -13492,7 +13567,7 @@ var RecordIntegrationInstances = class {
13492
13567
  id: record.id,
13493
13568
  name: typeof rawName === "string" ? rawName : record.id,
13494
13569
  configured: this.hooks.configured(record.data),
13495
- enabled: record.data.enabled !== false,
13570
+ ...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
13496
13571
  formValues: shown,
13497
13572
  ...kind !== void 0 ? { kind } : {},
13498
13573
  ...health?.status !== void 0 ? { status: health.status } : {},
@@ -13522,32 +13597,36 @@ var RecordIntegrationInstances = class {
13522
13597
  id: minted,
13523
13598
  data: this.hooks.initialBody(name, kind)
13524
13599
  }, { ifRev: 0 });
13525
- await this.hooks.onChanged?.(minted);
13600
+ await this.hooks.onChanged?.(minted, "created");
13526
13601
  return { id: minted };
13527
13602
  };
13528
13603
  updateInstance = async ({ id, name, values }) => {
13529
13604
  const patch = {};
13530
13605
  for (const [key, value] of Object.entries(values)) {
13531
13606
  if (value === void 0) continue;
13532
- if (!this.secretKeys.has(key)) patch[key] = value;
13607
+ if (this.secretMaps.has(key)) {
13608
+ const entries = secretMapPatch(value);
13609
+ if (entries !== null) patch[key] = entries;
13610
+ } else if (!this.secretKeys.has(key)) patch[key] = value;
13533
13611
  else if (isSecretClear(value)) patch[key] = null;
13534
13612
  else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
13535
13613
  }
13536
13614
  if (name !== void 0) patch[this.nameField] = name;
13537
13615
  await this.handle.patch(id, patch);
13538
- await this.hooks.onChanged?.(id);
13616
+ await this.hooks.onChanged?.(id, "updated");
13539
13617
  return { ok: true };
13540
13618
  };
13541
13619
  /** The switch lives in the record body; the addon is told so it applies it (D996). */
13542
13620
  setInstanceEnabled = async ({ id, enabled }) => {
13621
+ if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
13543
13622
  await this.handle.patch(id, { enabled });
13544
- await this.hooks.onChanged?.(id);
13623
+ await this.hooks.onChanged?.(id, "enabled");
13545
13624
  return { ok: true };
13546
13625
  };
13547
13626
  /** The row and its keychain refs go together (the records door). */
13548
13627
  deleteInstance = async ({ id }) => {
13549
13628
  await this.handle.delete(id);
13550
- await this.hooks.onChanged?.(id);
13629
+ await this.hooks.onChanged?.(id, "deleted");
13551
13630
  return { ok: true };
13552
13631
  };
13553
13632
  testInstance = async (input) => {
@@ -56774,6 +56853,21 @@ var IntegrationInstanceKindSchema = object({
56774
56853
  disconnectPath: string().min(1).optional()
56775
56854
  });
56776
56855
  var IntegrationInstanceDescriptionSchema = object({
56856
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
56857
+ role: _enum([
56858
+ "devices",
56859
+ "services",
56860
+ "both"
56861
+ ]),
56862
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
56863
+ discovery: object({
56864
+ mode: _enum([
56865
+ "none",
56866
+ "lan",
56867
+ "account"
56868
+ ]),
56869
+ scopedToInstance: boolean()
56870
+ }).optional(),
56777
56871
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
56778
56872
  instanceMode: IntegrationInstanceModeSchema,
56779
56873
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -56813,10 +56907,11 @@ var IntegrationInstanceSchema = object({
56813
56907
  /** The addon's own rule (an URL is present, a client id is present...). */
56814
56908
  configured: boolean(),
56815
56909
  /**
56816
- * The instance's own on/off switch (D996): always present, derived from the record body
56817
- * (`enabled !== false`, so a row written before the field existed is on).
56910
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
56911
+ * so a row written before the field existed is on). Absent when the `integrations` registry
56912
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
56818
56913
  */
56819
- enabled: boolean(),
56914
+ enabled: boolean().optional(),
56820
56915
  /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
56821
56916
  kind: string().optional(),
56822
56917
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
@@ -98299,20 +98394,5 @@ function haInstanceSettingsPortsOver(api) {
98299
98394
  }
98300
98395
  };
98301
98396
  }
98302
- /** The brokerId → settings view of the instances: what a reader keeps per broker. */
98303
- function settingsByBroker(rows) {
98304
- const out = /* @__PURE__ */ new Map();
98305
- for (const row of rows) if (row.brokerId !== null) out.set(row.brokerId, row.settings);
98306
- return out;
98307
- }
98308
- /** Read the instances, or report why not and answer nothing (a reader falls back to the legacy key). */
98309
- async function listInstancesOrNothing(ports, onError) {
98310
- try {
98311
- return await ports.listInstances();
98312
- } catch (err) {
98313
- onError(errMsg(err));
98314
- return [];
98315
- }
98316
- }
98317
98397
  //#endregion
98318
- export { enumSensorCapability as $, baseUnitOf as $t, automationControlCapability as A, runExportedAction as At, calendarSourceCapability as B, tamperCapability as Bt, RecordIntegrationInstances as C, EventCategory as Cn, prepareNotification as Ct, airQualitySensorCapability as D, publicEndpointReaderFrom as Dt, addonRoutesCapability as E, pruneDeletedEntities as Et, brightnessCapability as F, scriptRunnerCapability as Ft, consumableFormOf as G, vacuumControlCapability as Gt, climateControlCapability as H, tryConvertDelta as Ht, brokerCapability as I, sendToExportedTarget as It, controlCapability as J, waterHeaterCapability as Jt, consumableKindOf as K, valveCapability as Kt, buildAddonRouteProvider as L, smokeCapability as Lt, bestLocationMatch as M, runningFromMediaPlayer as Mt, binaryCapability as N, runningFromVacuumState as Nt, alarmPanelCapability as O, quantityFromUnit as Ot, boardKeySourceCapability as P, runningStateCapability as Pt, deviceExportCapability as Q, RecordSecretUnavailableError as Qt, buildDeviceAdminUrl as R, statusInstanceOf as Rt, RUNNING_STATE_COLD as S, errMsg as Sn, preconditionOf as St, accessoriesCapability as T, pressureSensorCapability as Tt, colorCapability as U, tryConvertUnit as Ut, carbonMonoxideCapability as V, temperatureSensorCapability as Vt, connectivityCapability as W, updateCapability as Wt, deviceAdminLinkCapability as X, BaseAddon as Xt, coverCapability as Y, weatherCapability as Yt, deviceAdoptionCapability as Z, REDACTED_SECRET as Zt, ExportInstanceUnknownError as _, unknown as _n, notifierCapability as _t, HA_PROVIDER_ADDON_ID$1 as a, isReadingQuantity as an, gasCapability as at, MediaPlayerRepeatSchema as b, DeviceRole as bn, parseAdminBase as bt, BaseDevice as c, array as cn, imageCapability as ct, COCO_TO_MACRO as d, literal as dn, lawnMowerControlCapability as dt, createEvent as en, eventEmitterCapability as et, CameraSwitchIdSchema as f, number as fn, lockControlCapability as ft, EnumSensorDateTimeFormatSchema as g, union as gn, notificationOutputCapability as gt, ConsumableStatusSchema as h, string as hn, normalizeUnit as ht, HA_BROKER_KIND as i, entryBody as in, floodCapability as it, batteryCapability as j, runningFromLawnMower as jt, ambientLightSensorCapability as k, resolveAttachmentUrls as kt, BaseDeviceProvider as l, boolean as ln, ingestReading as lt, ConsumableItemSchema as m, record as mn, motionCapability as mt, listInstancesOrNothing as n, defineEntryKind as nn, flattenNestedRecords as nt, listHomeAssistantBrokers as o, nodePin as on, humidifierCapability as ot, ConsumableFormSchema as p, object as pn, mediaPlayerCapability as pt, contactCapability as q, vibrationCapability as qt, settingsByBroker as r, defineRecordKinds as rn, flattenPortsOver as rt, AlarmArmModeSchema as s, _enum as sn, humiditySensorCapability as st, haInstanceSettingsPortsOver as t, defineAddonMigrations as tn, fanControlCapability as tt, CAMERA_SWITCH_ORDER as u, discriminatedUnion as un, integrationInstancesCapability as ut, FanDirectionSchema as v, url as vn, numericSensorCapability as vt, TargetSchema as w, presenceCapability as wt, PLUG_APPLIANCE_TYPES as x, DeviceType as xn, powerMeterCapability as xt, HvacModeSchema as y, DeviceFeature as yn, oauthIntegrationCapability as yt, buttonCapability as z, switchCapability as zt };
98398
+ export { fanControlCapability as $, createEvent as $t, bestLocationMatch as A, runningFromMediaPlayer as At, climateControlCapability as B, tryConvertDelta as Bt, accessoriesCapability as C, pressureSensorCapability as Ct, ambientLightSensorCapability as D, resolveAttachmentUrls as Dt, alarmPanelCapability as E, quantityFromUnit as Et, buildAddonRouteProvider as F, smokeCapability as Ft, contactCapability as G, vibrationCapability as Gt, connectivityCapability as H, updateCapability as Ht, buildDeviceAdminUrl as I, statusInstanceOf as It, deviceAdminLinkCapability as J, BaseAddon as Jt, controlCapability as K, waterHeaterCapability as Kt, buttonCapability as L, switchCapability as Lt, boardKeySourceCapability as M, runningStateCapability as Mt, brightnessCapability as N, scriptRunnerCapability as Nt, automationControlCapability as O, runExportedAction as Ot, brokerCapability as P, sendToExportedTarget as Pt, eventEmitterCapability as Q, baseUnitOf as Qt, calendarSourceCapability as R, tamperCapability as Rt, TargetSchema as S, EventCategory as Sn, presenceCapability as St, airQualitySensorCapability as T, publicEndpointReaderFrom as Tt, consumableFormOf as U, vacuumControlCapability as Ut, colorCapability as V, tryConvertUnit as Vt, consumableKindOf as W, valveCapability as Wt, deviceExportCapability as X, RecordSecretUnavailableError as Xt, deviceAdoptionCapability as Y, REDACTED_SECRET as Yt, enumSensorCapability as Z, RevConflictError as Zt, HvacModeSchema as _, url as _n, oauthIntegrationCapability as _t, AlarmArmModeSchema as a, nodePin as an, humiditySensorCapability as at, RUNNING_STATE_COLD as b, DeviceType as bn, preconditionOf as bt, CAMERA_SWITCH_ORDER as c, boolean as cn, integrationInstancesCapability as ct, ConsumableFormSchema as d, number as dn, mediaPlayerCapability as dt, defineAddonMigrations as en, flattenNestedRecords as et, ConsumableItemSchema as f, object as fn, motionCapability as ft, FanDirectionSchema as g, unknown as gn, numericSensorCapability as gt, ExportInstanceUnknownError as h, union as hn, notifierCapability as ht, listHomeAssistantBrokers as i, isReadingQuantity as in, humidifierCapability as it, binaryCapability as j, runningFromVacuumState as jt, batteryCapability as k, runningFromLawnMower as kt, COCO_TO_MACRO as l, discriminatedUnion as ln, lawnMowerControlCapability as lt, EnumSensorDateTimeFormatSchema as m, string as mn, notificationOutputCapability as mt, HA_BROKER_KIND as n, defineRecordKinds as nn, floodCapability as nt, BaseDevice as o, _enum as on, imageCapability as ot, ConsumableStatusSchema as p, record as pn, normalizeUnit as pt, coverCapability as q, weatherCapability as qt, HA_PROVIDER_ADDON_ID$1 as r, entryBody as rn, gasCapability as rt, BaseDeviceProvider as s, array as sn, ingestReading as st, haInstanceSettingsPortsOver as t, defineEntryKind as tn, flattenPortsOver as tt, CameraSwitchIdSchema as u, literal as un, lockControlCapability as ut, MediaPlayerRepeatSchema as v, DeviceFeature as vn, parseAdminBase as vt, addonRoutesCapability as w, pruneDeletedEntities as wt, RecordIntegrationInstances as x, errMsg as xn, prepareNotification as xt, PLUG_APPLIANCE_TYPES as y, DeviceRole as yn, powerMeterCapability as yt, carbonMonoxideCapability as z, temperatureSensorCapability as zt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.655",
3
+ "version": "1.2.656",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -134,7 +134,7 @@
134
134
  "description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
135
135
  },
136
136
  "requires": {
137
- "server": ">=1.2.761"
137
+ "server": ">=1.2.762"
138
138
  }
139
139
  },
140
140
  "files": [