@camstack/addon-provider-homeassistant 1.2.661 → 1.2.662

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.662] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - The integration form is declared as the instance form; brokers and their stored values are unchanged.
15
+
10
16
  ## [1.2.661] - 2026-11-05
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_legacy_integration_rows = require("./legacy-integration-rows-B_X4VL29.js");
2
+ const require_legacy_integration_rows = require("./legacy-integration-rows-BaTsNMtt.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-admin-link.ts
@@ -9326,7 +9326,7 @@ var HaProviderAddon = class HaProviderAddon extends require_legacy_integration_r
9326
9326
  eb.subscribe({ category: "integration.disabled" }, handler);
9327
9327
  eb.subscribe({ category: "integration.deleted" }, handler);
9328
9328
  }
9329
- globalSettingsSchema(_cap) {
9329
+ async integrationFormSchema(_instance) {
9330
9330
  return this.schema({ sections: [{
9331
9331
  id: "ha-broker",
9332
9332
  title: "Home Assistant broker",
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as fanControlCapability, $t as createEvent, A as bestLocationMatch, At as runningFromMediaPlayer, B as climateControlCapability, Bt as tryConvertDelta, C as accessoriesCapability, Ct as pressureSensorCapability, D as ambientLightSensorCapability, Dt as resolveAttachmentUrls, E as alarmPanelCapability, Et as quantityFromUnit, F as buildAddonRouteProvider, Ft as smokeCapability, G as contactCapability, Gt as vibrationCapability, H as connectivityCapability, Ht as updateCapability, I as buildDeviceAdminUrl, J as deviceAdminLinkCapability, K as controlCapability, Kt as waterHeaterCapability, L as buttonCapability, Lt as switchCapability, M as boardKeySourceCapability, Mt as runningStateCapability, N as brightnessCapability, Nt as scriptRunnerCapability, O as automationControlCapability, P as brokerCapability, Q as eventEmitterCapability, Qt as baseUnitOf, R as calendarSourceCapability, Rt as tamperCapability, S as TargetSchema, Sn as EventCategory, St as presenceCapability, T as airQualitySensorCapability, Tt as publicEndpointReaderFrom, Ut as vacuumControlCapability, V as colorCapability, Vt as tryConvertUnit, Wt as valveCapability, Xt as RecordSecretUnavailableError, Y as deviceAdoptionCapability, Z as enumSensorCapability, _n as url, at as humiditySensorCapability, b as RUNNING_STATE_COLD, bn as DeviceType, bt as preconditionOf, cn as boolean, ct as integrationInstancesCapability, dn as number, dt as mediaPlayerCapability, en as defineAddonMigrations, et as flattenNestedRecords, fn as object, ft as motionCapability, gn as unknown, gt as numericSensorCapability, hn as union, ht as notifierCapability, it as humidifierCapability, j as binaryCapability, jt as runningFromVacuumState, k as batteryCapability, kt as runningFromLawnMower, ln as discriminatedUnion, lt as lawnMowerControlCapability, m as EnumSensorDateTimeFormatSchema, mn as string, mt as notificationOutputCapability, n as HA_BROKER_KIND, nn as defineRecordKinds, nt as floodCapability, o as BaseDevice, on as _enum, ot as imageCapability, pn as record, pt as normalizeUnit, q as coverCapability, qt as weatherCapability, rn as entryBody, rt as gasCapability, s as BaseDeviceProvider, sn as array, st as ingestReading, t as haInstanceSettingsPortsOver, tn as defineEntryKind, tt as flattenPortsOver, un as literal, ut as lockControlCapability, vn as DeviceFeature, vt as parseAdminBase, w as addonRoutesCapability, x as RecordIntegrationInstances, xn as errMsg, xt as prepareNotification, yn as DeviceRole, yt as powerMeterCapability, z as carbonMonoxideCapability, zt as temperatureSensorCapability } from "./legacy-integration-rows-Bhvthv-s.mjs";
1
+ import { $ as fanControlCapability, $t as createEvent, A as bestLocationMatch, At as runningFromMediaPlayer, B as climateControlCapability, Bt as tryConvertDelta, C as accessoriesCapability, Ct as pressureSensorCapability, D as ambientLightSensorCapability, Dt as resolveAttachmentUrls, E as alarmPanelCapability, Et as quantityFromUnit, F as buildAddonRouteProvider, Ft as smokeCapability, G as contactCapability, Gt as vibrationCapability, H as connectivityCapability, Ht as updateCapability, I as buildDeviceAdminUrl, J as deviceAdminLinkCapability, K as controlCapability, Kt as waterHeaterCapability, L as buttonCapability, Lt as switchCapability, M as boardKeySourceCapability, Mt as runningStateCapability, N as brightnessCapability, Nt as scriptRunnerCapability, O as automationControlCapability, P as brokerCapability, Q as eventEmitterCapability, Qt as baseUnitOf, R as calendarSourceCapability, Rt as tamperCapability, S as TargetSchema, Sn as EventCategory, St as presenceCapability, T as airQualitySensorCapability, Tt as publicEndpointReaderFrom, Ut as vacuumControlCapability, V as colorCapability, Vt as tryConvertUnit, Wt as valveCapability, Xt as RecordSecretUnavailableError, Y as deviceAdoptionCapability, Z as enumSensorCapability, _n as url, at as humiditySensorCapability, b as RUNNING_STATE_COLD, bn as DeviceType, bt as preconditionOf, cn as boolean, ct as integrationInstancesCapability, dn as number, dt as mediaPlayerCapability, en as defineAddonMigrations, et as flattenNestedRecords, fn as object, ft as motionCapability, gn as unknown, gt as numericSensorCapability, hn as union, ht as notifierCapability, it as humidifierCapability, j as binaryCapability, jt as runningFromVacuumState, k as batteryCapability, kt as runningFromLawnMower, ln as discriminatedUnion, lt as lawnMowerControlCapability, m as EnumSensorDateTimeFormatSchema, mn as string, mt as notificationOutputCapability, n as HA_BROKER_KIND, nn as defineRecordKinds, nt as floodCapability, o as BaseDevice, on as _enum, ot as imageCapability, pn as record, pt as normalizeUnit, q as coverCapability, qt as weatherCapability, rn as entryBody, rt as gasCapability, s as BaseDeviceProvider, sn as array, st as ingestReading, t as haInstanceSettingsPortsOver, tn as defineEntryKind, tt as flattenPortsOver, un as literal, ut as lockControlCapability, vn as DeviceFeature, vt as parseAdminBase, w as addonRoutesCapability, x as RecordIntegrationInstances, xn as errMsg, xt as prepareNotification, yn as DeviceRole, yt as powerMeterCapability, z as carbonMonoxideCapability, zt as temperatureSensorCapability } from "./legacy-integration-rows-uGrBo3a3.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-admin-link.ts
@@ -9305,7 +9305,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9305
9305
  eb.subscribe({ category: "integration.disabled" }, handler);
9306
9306
  eb.subscribe({ category: "integration.deleted" }, handler);
9307
9307
  }
9308
- globalSettingsSchema(_cap) {
9308
+ async integrationFormSchema(_instance) {
9309
9309
  return this.schema({ sections: [{
9310
9310
  id: "ha-broker",
9311
9311
  title: "Home Assistant broker",
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_legacy_integration_rows = require("../legacy-integration-rows-B_X4VL29.js");
27
+ const require_legacy_integration_rows = require("../legacy-integration-rows-BaTsNMtt.js");
28
28
  let node_crypto = require("node:crypto");
29
29
  require("node:zlib");
30
30
  let node_fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { F as buildAddonRouteProvider, It as statusInstanceOf, Jt as BaseAddon, Ot as runExportedAction, Pt as sendToExportedTarget, Qt as baseUnitOf, Sn as EventCategory, U as consumableFormOf, W as consumableKindOf, X as deviceExportCapability, Zt as RevConflictError, _ as HvacModeSchema, _t as oauthIntegrationCapability, a as AlarmArmModeSchema, an as nodePin, bt as preconditionOf, c as CAMERA_SWITCH_ORDER, cn as boolean, ct as integrationInstancesCapability, d as ConsumableFormSchema, dn as number, en as defineAddonMigrations, f as ConsumableItemSchema, fn as object, g as FanDirectionSchema, gn as unknown, h as ExportInstanceUnknownError, i as listHomeAssistantBrokers, in as isReadingQuantity, l as COCO_TO_MACRO, mn as string, on as _enum, p as ConsumableStatusSchema, sn as array, t as haInstanceSettingsPortsOver, tn as defineEntryKind, u as CameraSwitchIdSchema, v as MediaPlayerRepeatSchema, w as addonRoutesCapability, wt as pruneDeletedEntities, x as RecordIntegrationInstances, y as PLUG_APPLIANCE_TYPES } from "../legacy-integration-rows-Bhvthv-s.mjs";
1
+ import { F as buildAddonRouteProvider, It as statusInstanceOf, Jt as BaseAddon, Ot as runExportedAction, Pt as sendToExportedTarget, Qt as baseUnitOf, Sn as EventCategory, U as consumableFormOf, W as consumableKindOf, X as deviceExportCapability, Zt as RevConflictError, _ as HvacModeSchema, _t as oauthIntegrationCapability, a as AlarmArmModeSchema, an as nodePin, bt as preconditionOf, c as CAMERA_SWITCH_ORDER, cn as boolean, ct as integrationInstancesCapability, d as ConsumableFormSchema, dn as number, en as defineAddonMigrations, f as ConsumableItemSchema, fn as object, g as FanDirectionSchema, gn as unknown, h as ExportInstanceUnknownError, i as listHomeAssistantBrokers, in as isReadingQuantity, l as COCO_TO_MACRO, mn as string, on as _enum, p as ConsumableStatusSchema, sn as array, t as haInstanceSettingsPortsOver, tn as defineEntryKind, u as CameraSwitchIdSchema, v as MediaPlayerRepeatSchema, w as addonRoutesCapability, wt as pruneDeletedEntities, x as RecordIntegrationInstances, y as PLUG_APPLIANCE_TYPES } from "../legacy-integration-rows-uGrBo3a3.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  import "node:zlib";
4
4
  import "node:fs";
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
5981
5981
  var ZodFirstPartyTypeKind;
5982
5982
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5983
5983
  //#endregion
5984
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5984
+ //#region ../types/dist/sleep-DEZaLbiu.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).
@@ -6800,6 +6800,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6800
6800
  }
6801
6801
  return out;
6802
6802
  }
6803
+ /** Every scope, in the order the guard and the docs list them. */
6804
+ var SCOPED_SETTINGS_SCOPES = [
6805
+ "cluster",
6806
+ "device-defaults",
6807
+ "node"
6808
+ ];
6803
6809
  /**
6804
6810
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6805
6811
  *
@@ -7691,6 +7697,13 @@ var BaseAddon = class {
7691
7697
  */
7692
7698
  secretStatePaths = [];
7693
7699
  _addonSecretTree = null;
7700
+ /**
7701
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7702
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7703
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7704
+ */
7705
+ _scopedByCap = /* @__PURE__ */ new Map();
7706
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7694
7707
  _deviceSecretTree = null;
7695
7708
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7696
7709
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7760,6 +7773,7 @@ var BaseAddon = class {
7760
7773
  this._ctx = context;
7761
7774
  this._secretSettings = null;
7762
7775
  }
7776
+ await this.loadScopedSchemas();
7763
7777
  await this.resolveConfig();
7764
7778
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7765
7779
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7769,6 +7783,7 @@ var BaseAddon = class {
7769
7783
  const normalized = normalizeAddonInitResult(result);
7770
7784
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7771
7785
  this._registeredCapNames = providers.map((p) => p.capability.name);
7786
+ await this.loadScopedSchemas();
7772
7787
  return normalized;
7773
7788
  }
7774
7789
  /**
@@ -7857,6 +7872,16 @@ var BaseAddon = class {
7857
7872
  schema(schema) {
7858
7873
  return schema;
7859
7874
  }
7875
+ /**
7876
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7877
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7878
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7879
+ * must already know which keys are per node. Names the providers register at init are added
7880
+ * once known.
7881
+ */
7882
+ scopedSettingsCapabilities() {
7883
+ return [];
7884
+ }
7860
7885
  /** Override to provide global-level settings UI schema. */
7861
7886
  globalSettingsSchema(_cap) {
7862
7887
  return null;
@@ -7900,7 +7925,8 @@ var BaseAddon = class {
7900
7925
  return [];
7901
7926
  }
7902
7927
  async getGlobalSettings(overlay, cap, nodeId) {
7903
- const schema = this.globalSettingsSchema(cap);
7928
+ await this.loadScopedSchemas(cap);
7929
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7904
7930
  if (!schema) return { sections: [] };
7905
7931
  const projected = await this.readRawProjectedStore(nodeId, cap);
7906
7932
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8031,6 +8057,7 @@ var BaseAddon = class {
8031
8057
  * resolved blob back is a no-op for every unchanged secret.
8032
8058
  */
8033
8059
  async resolveGlobalStore(nodeId, cap) {
8060
+ await this.loadScopedSchemas(cap);
8034
8061
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8035
8062
  return this.projectForNode(raw, nodeId, cap);
8036
8063
  }
@@ -8041,6 +8068,7 @@ var BaseAddon = class {
8041
8068
  }
8042
8069
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8043
8070
  async readRawProjectedStore(nodeId, cap) {
8071
+ await this.loadScopedSchemas(cap);
8044
8072
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8045
8073
  return this.projectForNode(raw, nodeId, cap);
8046
8074
  }
@@ -8143,6 +8171,7 @@ var BaseAddon = class {
8143
8171
  await this.applyGlobalPatch(patch, nodeId);
8144
8172
  }
8145
8173
  async applyGlobalPatch(barePatch, nodeId) {
8174
+ await this.loadScopedSchemas();
8146
8175
  const keys = this.perNodeKeys();
8147
8176
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8148
8177
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8156,7 +8185,8 @@ var BaseAddon = class {
8156
8185
  await this.resolveConfig();
8157
8186
  await this.onConfigChanged();
8158
8187
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8159
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8188
+ const legacy = this.globalSettingsSchema();
8189
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8160
8190
  }
8161
8191
  /**
8162
8192
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8164,14 +8194,67 @@ var BaseAddon = class {
8164
8194
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8165
8195
  * settings API behaves exactly like the legacy node-agnostic one.
8166
8196
  */
8167
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8197
+ /**
8198
+ * Load the scoped schemas of every capability this addon declares (the override and the
8199
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8200
+ * derived from the previous set. Idempotent.
8201
+ */
8202
+ async loadScopedSchemas(extraCap) {
8203
+ const caps = new Set([
8204
+ ...this.scopedSettingsCapabilities(),
8205
+ ...this._registeredCapNames,
8206
+ ...extraCap === void 0 ? [] : [extraCap]
8207
+ ]);
8208
+ let added = false;
8209
+ for (const cap of caps) {
8210
+ if (this._scopedByCap.has(cap)) continue;
8211
+ const loaded = [];
8212
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8213
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8214
+ if (schema !== null) loaded.push({
8215
+ scope,
8216
+ schema
8217
+ });
8218
+ }
8219
+ this._scopedByCap.set(cap, loaded);
8220
+ added = true;
8221
+ }
8222
+ if (!added) return;
8223
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8224
+ this._addonSecretTree = null;
8225
+ }
8226
+ /**
8227
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8228
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8229
+ */
8230
+ scopedFormSchema(cap) {
8231
+ const byId = /* @__PURE__ */ new Map();
8232
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8233
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8234
+ }
8235
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8236
+ loadedScopedSchemas(cap) {
8237
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8238
+ return [...this._scopedByCap.values()].flat();
8239
+ }
8240
+ /**
8241
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8242
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8243
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8244
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8245
+ * Call {@link loadScopedSchemas} first (the async seams do).
8246
+ */
8168
8247
  perNodeKeys(cap) {
8169
8248
  const cacheKey = cap ?? "";
8170
- const cached = this._perNodeKeysCache.get(cacheKey);
8249
+ const cached = this._nodeKeysByCap.get(cacheKey);
8171
8250
  if (cached) return cached;
8172
- const schema = this.globalSettingsSchema(cap);
8173
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8174
- this._perNodeKeysCache.set(cacheKey, keys);
8251
+ const legacy = this.globalSettingsSchema(cap);
8252
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8253
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8254
+ if (scope !== "node") continue;
8255
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8256
+ }
8257
+ this._nodeKeysByCap.set(cacheKey, keys);
8175
8258
  return keys;
8176
8259
  }
8177
8260
  /**
@@ -8181,10 +8264,9 @@ var BaseAddon = class {
8181
8264
  * time to return its response before the addon is torn down and
8182
8265
  * re-initialised by `AddonRegistryService.restartAddon`.
8183
8266
  */
8184
- maybeAutoRestart(patchKeys, schema) {
8185
- if (!schema) return;
8267
+ maybeAutoRestart(patchKeys, schemas) {
8186
8268
  const restartKeys = /* @__PURE__ */ new Set();
8187
- for (const section of schema.sections) for (const field of section.fields) {
8269
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8188
8270
  if (field.type === "separator" || field.type === "info") continue;
8189
8271
  if (field.requiresRestart) restartKeys.add(field.key);
8190
8272
  }
@@ -8380,12 +8462,14 @@ var BaseAddon = class {
8380
8462
  * here, in the addon's own process, so the hub keeps no copy of it.
8381
8463
  */
8382
8464
  async getDeclaredSettingsKeys() {
8465
+ await this.loadScopedSchemas();
8383
8466
  return { keys: [...this.declaredKeySet()].toSorted() };
8384
8467
  }
8385
8468
  declaredKeySet() {
8386
8469
  const keys = new Set(Object.keys(this.defaults));
8387
8470
  const schema = this.globalSettingsSchema();
8388
8471
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8472
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8389
8473
  for (const key of this._declaredStateKeys) keys.add(key);
8390
8474
  for (const path of this.secretStatePaths) {
8391
8475
  const root = path.split(/[.[]/, 1)[0];
@@ -8481,7 +8565,11 @@ var BaseAddon = class {
8481
8565
  }
8482
8566
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8483
8567
  addonSecretTree() {
8484
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8568
+ if (this._addonSecretTree === null) {
8569
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8570
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8571
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8572
+ }
8485
8573
  return this._addonSecretTree;
8486
8574
  }
8487
8575
  deviceSecretTree() {
@@ -8731,6 +8819,23 @@ function dropPerNodeFields(fields) {
8731
8819
  }
8732
8820
  return kept;
8733
8821
  }
8822
+ /** The keys of the value-holding fields, recursing into layout containers. */
8823
+ function collectValueFieldKeys(fields) {
8824
+ const collected = [];
8825
+ for (const field of fields) {
8826
+ if (field.type === "group") {
8827
+ collected.push(...collectValueFieldKeys(field.fields));
8828
+ continue;
8829
+ }
8830
+ if (field.type === "sub-tabs") {
8831
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8832
+ continue;
8833
+ }
8834
+ if (field.type === "separator" || field.type === "info") continue;
8835
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8836
+ }
8837
+ return collected;
8838
+ }
8734
8839
  function collectFieldKeys(fields) {
8735
8840
  const collected = [];
8736
8841
  for (const field of fields) {
@@ -13585,12 +13690,22 @@ var RecordIntegrationInstances = class {
13585
13690
  ...this.hooks.description,
13586
13691
  mintsId: true
13587
13692
  };
13588
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13693
+ /**
13694
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13695
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13696
+ * record id answers as it is. An id that names nothing comes back unchanged.
13697
+ */
13698
+ async resolveRecordId(id) {
13699
+ if (await this.handle.get(id) !== null) return id;
13700
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13701
+ return linked === void 0 ? id : linked.id;
13702
+ }
13703
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13589
13704
  listInstances = async () => {
13590
13705
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13591
13706
  };
13592
13707
  getInstance = async ({ id }) => {
13593
- const record = await this.handle.get(id);
13708
+ const record = await this.handle.get(await this.resolveRecordId(id));
13594
13709
  return record === null ? null : this.viewOf(record);
13595
13710
  };
13596
13711
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13607,7 +13722,8 @@ var RecordIntegrationInstances = class {
13607
13722
  await this.hooks.onChanged?.(minted, "created");
13608
13723
  return { id: minted };
13609
13724
  };
13610
- updateInstance = async ({ id, name, values }) => {
13725
+ updateInstance = async ({ id: askedId, name, values }) => {
13726
+ const id = await this.resolveRecordId(askedId);
13611
13727
  const patch = {};
13612
13728
  for (const [key, value] of Object.entries(values)) {
13613
13729
  if (value === void 0) continue;
@@ -13642,7 +13758,10 @@ var RecordIntegrationInstances = class {
13642
13758
  status: "unsupported",
13643
13759
  error: "This integration declares no connection test"
13644
13760
  };
13645
- return this.hooks.test(input);
13761
+ return this.hooks.test(input.id === null ? input : {
13762
+ ...input,
13763
+ id: await this.resolveRecordId(input.id)
13764
+ });
13646
13765
  };
13647
13766
  /** The flat provider object to hand to `registerProvider`. */
13648
13767
  toProvider() {
@@ -52300,12 +52419,12 @@ var deviceExportCapability = {
52300
52419
  kind: "poll"
52301
52420
  },
52302
52421
  /**
52303
- * Each export provider contributes its own per-device "Export" panel
52304
- * (enabled toggle, preferred stream, addon-specific knobs like the
52305
- * HA-MQTT discovery prefix). The three framework methods
52306
- * `getDeviceSettingsContribution` / `getDeviceLiveContribution` /
52307
- * `applyDeviceSettingsPatch` are auto-injected on the provider
52308
- * interface and routed to the device-details aggregator.
52422
+ * Each export provider MAY contribute its own per-device "Export" panel
52423
+ * (enabled toggle, preferred stream, addon-specific knobs). The three
52424
+ * framework methods are mounted on the cap router when present.
52425
+ * `IDeviceExportProvider` leaves them optional: an exporter with no
52426
+ * per-device panel omits them (a `deviceConfig` cap's UI is derived and
52427
+ * never hand-written — D14 — and an exporter is not one of those).
52309
52428
  */
52310
52429
  exposesDeviceSettings: true,
52311
52430
  methods: {
@@ -5980,7 +5980,7 @@ var ZodIssueCode = {
5980
5980
  var ZodFirstPartyTypeKind;
5981
5981
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5982
5982
  //#endregion
5983
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5983
+ //#region ../types/dist/sleep-DEZaLbiu.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).
@@ -6799,6 +6799,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6799
6799
  }
6800
6800
  return out;
6801
6801
  }
6802
+ /** Every scope, in the order the guard and the docs list them. */
6803
+ var SCOPED_SETTINGS_SCOPES = [
6804
+ "cluster",
6805
+ "device-defaults",
6806
+ "node"
6807
+ ];
6802
6808
  /**
6803
6809
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6804
6810
  *
@@ -7690,6 +7696,13 @@ var BaseAddon = class {
7690
7696
  */
7691
7697
  secretStatePaths = [];
7692
7698
  _addonSecretTree = null;
7699
+ /**
7700
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7701
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7702
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7703
+ */
7704
+ _scopedByCap = /* @__PURE__ */ new Map();
7705
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7693
7706
  _deviceSecretTree = null;
7694
7707
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7695
7708
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7759,6 +7772,7 @@ var BaseAddon = class {
7759
7772
  this._ctx = context;
7760
7773
  this._secretSettings = null;
7761
7774
  }
7775
+ await this.loadScopedSchemas();
7762
7776
  await this.resolveConfig();
7763
7777
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7764
7778
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7768,6 +7782,7 @@ var BaseAddon = class {
7768
7782
  const normalized = normalizeAddonInitResult(result);
7769
7783
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7770
7784
  this._registeredCapNames = providers.map((p) => p.capability.name);
7785
+ await this.loadScopedSchemas();
7771
7786
  return normalized;
7772
7787
  }
7773
7788
  /**
@@ -7856,6 +7871,16 @@ var BaseAddon = class {
7856
7871
  schema(schema) {
7857
7872
  return schema;
7858
7873
  }
7874
+ /**
7875
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7876
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7877
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7878
+ * must already know which keys are per node. Names the providers register at init are added
7879
+ * once known.
7880
+ */
7881
+ scopedSettingsCapabilities() {
7882
+ return [];
7883
+ }
7859
7884
  /** Override to provide global-level settings UI schema. */
7860
7885
  globalSettingsSchema(_cap) {
7861
7886
  return null;
@@ -7899,7 +7924,8 @@ var BaseAddon = class {
7899
7924
  return [];
7900
7925
  }
7901
7926
  async getGlobalSettings(overlay, cap, nodeId) {
7902
- const schema = this.globalSettingsSchema(cap);
7927
+ await this.loadScopedSchemas(cap);
7928
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7903
7929
  if (!schema) return { sections: [] };
7904
7930
  const projected = await this.readRawProjectedStore(nodeId, cap);
7905
7931
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8030,6 +8056,7 @@ var BaseAddon = class {
8030
8056
  * resolved blob back is a no-op for every unchanged secret.
8031
8057
  */
8032
8058
  async resolveGlobalStore(nodeId, cap) {
8059
+ await this.loadScopedSchemas(cap);
8033
8060
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8034
8061
  return this.projectForNode(raw, nodeId, cap);
8035
8062
  }
@@ -8040,6 +8067,7 @@ var BaseAddon = class {
8040
8067
  }
8041
8068
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8042
8069
  async readRawProjectedStore(nodeId, cap) {
8070
+ await this.loadScopedSchemas(cap);
8043
8071
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8044
8072
  return this.projectForNode(raw, nodeId, cap);
8045
8073
  }
@@ -8142,6 +8170,7 @@ var BaseAddon = class {
8142
8170
  await this.applyGlobalPatch(patch, nodeId);
8143
8171
  }
8144
8172
  async applyGlobalPatch(barePatch, nodeId) {
8173
+ await this.loadScopedSchemas();
8145
8174
  const keys = this.perNodeKeys();
8146
8175
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8147
8176
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8155,7 +8184,8 @@ var BaseAddon = class {
8155
8184
  await this.resolveConfig();
8156
8185
  await this.onConfigChanged();
8157
8186
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8158
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8187
+ const legacy = this.globalSettingsSchema();
8188
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8159
8189
  }
8160
8190
  /**
8161
8191
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8163,14 +8193,67 @@ var BaseAddon = class {
8163
8193
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8164
8194
  * settings API behaves exactly like the legacy node-agnostic one.
8165
8195
  */
8166
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8196
+ /**
8197
+ * Load the scoped schemas of every capability this addon declares (the override and the
8198
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8199
+ * derived from the previous set. Idempotent.
8200
+ */
8201
+ async loadScopedSchemas(extraCap) {
8202
+ const caps = new Set([
8203
+ ...this.scopedSettingsCapabilities(),
8204
+ ...this._registeredCapNames,
8205
+ ...extraCap === void 0 ? [] : [extraCap]
8206
+ ]);
8207
+ let added = false;
8208
+ for (const cap of caps) {
8209
+ if (this._scopedByCap.has(cap)) continue;
8210
+ const loaded = [];
8211
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8212
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8213
+ if (schema !== null) loaded.push({
8214
+ scope,
8215
+ schema
8216
+ });
8217
+ }
8218
+ this._scopedByCap.set(cap, loaded);
8219
+ added = true;
8220
+ }
8221
+ if (!added) return;
8222
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8223
+ this._addonSecretTree = null;
8224
+ }
8225
+ /**
8226
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8227
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8228
+ */
8229
+ scopedFormSchema(cap) {
8230
+ const byId = /* @__PURE__ */ new Map();
8231
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8232
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8233
+ }
8234
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8235
+ loadedScopedSchemas(cap) {
8236
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8237
+ return [...this._scopedByCap.values()].flat();
8238
+ }
8239
+ /**
8240
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8241
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8242
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8243
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8244
+ * Call {@link loadScopedSchemas} first (the async seams do).
8245
+ */
8167
8246
  perNodeKeys(cap) {
8168
8247
  const cacheKey = cap ?? "";
8169
- const cached = this._perNodeKeysCache.get(cacheKey);
8248
+ const cached = this._nodeKeysByCap.get(cacheKey);
8170
8249
  if (cached) return cached;
8171
- const schema = this.globalSettingsSchema(cap);
8172
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8173
- this._perNodeKeysCache.set(cacheKey, keys);
8250
+ const legacy = this.globalSettingsSchema(cap);
8251
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8252
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8253
+ if (scope !== "node") continue;
8254
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8255
+ }
8256
+ this._nodeKeysByCap.set(cacheKey, keys);
8174
8257
  return keys;
8175
8258
  }
8176
8259
  /**
@@ -8180,10 +8263,9 @@ var BaseAddon = class {
8180
8263
  * time to return its response before the addon is torn down and
8181
8264
  * re-initialised by `AddonRegistryService.restartAddon`.
8182
8265
  */
8183
- maybeAutoRestart(patchKeys, schema) {
8184
- if (!schema) return;
8266
+ maybeAutoRestart(patchKeys, schemas) {
8185
8267
  const restartKeys = /* @__PURE__ */ new Set();
8186
- for (const section of schema.sections) for (const field of section.fields) {
8268
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8187
8269
  if (field.type === "separator" || field.type === "info") continue;
8188
8270
  if (field.requiresRestart) restartKeys.add(field.key);
8189
8271
  }
@@ -8379,12 +8461,14 @@ var BaseAddon = class {
8379
8461
  * here, in the addon's own process, so the hub keeps no copy of it.
8380
8462
  */
8381
8463
  async getDeclaredSettingsKeys() {
8464
+ await this.loadScopedSchemas();
8382
8465
  return { keys: [...this.declaredKeySet()].toSorted() };
8383
8466
  }
8384
8467
  declaredKeySet() {
8385
8468
  const keys = new Set(Object.keys(this.defaults));
8386
8469
  const schema = this.globalSettingsSchema();
8387
8470
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8471
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8388
8472
  for (const key of this._declaredStateKeys) keys.add(key);
8389
8473
  for (const path of this.secretStatePaths) {
8390
8474
  const root = path.split(/[.[]/, 1)[0];
@@ -8480,7 +8564,11 @@ var BaseAddon = class {
8480
8564
  }
8481
8565
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8482
8566
  addonSecretTree() {
8483
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8567
+ if (this._addonSecretTree === null) {
8568
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8569
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8570
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8571
+ }
8484
8572
  return this._addonSecretTree;
8485
8573
  }
8486
8574
  deviceSecretTree() {
@@ -8730,6 +8818,23 @@ function dropPerNodeFields(fields) {
8730
8818
  }
8731
8819
  return kept;
8732
8820
  }
8821
+ /** The keys of the value-holding fields, recursing into layout containers. */
8822
+ function collectValueFieldKeys(fields) {
8823
+ const collected = [];
8824
+ for (const field of fields) {
8825
+ if (field.type === "group") {
8826
+ collected.push(...collectValueFieldKeys(field.fields));
8827
+ continue;
8828
+ }
8829
+ if (field.type === "sub-tabs") {
8830
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8831
+ continue;
8832
+ }
8833
+ if (field.type === "separator" || field.type === "info") continue;
8834
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8835
+ }
8836
+ return collected;
8837
+ }
8733
8838
  function collectFieldKeys(fields) {
8734
8839
  const collected = [];
8735
8840
  for (const field of fields) {
@@ -13584,12 +13689,22 @@ var RecordIntegrationInstances = class {
13584
13689
  ...this.hooks.description,
13585
13690
  mintsId: true
13586
13691
  };
13587
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13692
+ /**
13693
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13694
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13695
+ * record id answers as it is. An id that names nothing comes back unchanged.
13696
+ */
13697
+ async resolveRecordId(id) {
13698
+ if (await this.handle.get(id) !== null) return id;
13699
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13700
+ return linked === void 0 ? id : linked.id;
13701
+ }
13702
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13588
13703
  listInstances = async () => {
13589
13704
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13590
13705
  };
13591
13706
  getInstance = async ({ id }) => {
13592
- const record = await this.handle.get(id);
13707
+ const record = await this.handle.get(await this.resolveRecordId(id));
13593
13708
  return record === null ? null : this.viewOf(record);
13594
13709
  };
13595
13710
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13606,7 +13721,8 @@ var RecordIntegrationInstances = class {
13606
13721
  await this.hooks.onChanged?.(minted, "created");
13607
13722
  return { id: minted };
13608
13723
  };
13609
- updateInstance = async ({ id, name, values }) => {
13724
+ updateInstance = async ({ id: askedId, name, values }) => {
13725
+ const id = await this.resolveRecordId(askedId);
13610
13726
  const patch = {};
13611
13727
  for (const [key, value] of Object.entries(values)) {
13612
13728
  if (value === void 0) continue;
@@ -13641,7 +13757,10 @@ var RecordIntegrationInstances = class {
13641
13757
  status: "unsupported",
13642
13758
  error: "This integration declares no connection test"
13643
13759
  };
13644
- return this.hooks.test(input);
13760
+ return this.hooks.test(input.id === null ? input : {
13761
+ ...input,
13762
+ id: await this.resolveRecordId(input.id)
13763
+ });
13645
13764
  };
13646
13765
  /** The flat provider object to hand to `registerProvider`. */
13647
13766
  toProvider() {
@@ -52299,12 +52418,12 @@ var deviceExportCapability = {
52299
52418
  kind: "poll"
52300
52419
  },
52301
52420
  /**
52302
- * Each export provider contributes its own per-device "Export" panel
52303
- * (enabled toggle, preferred stream, addon-specific knobs like the
52304
- * HA-MQTT discovery prefix). The three framework methods
52305
- * `getDeviceSettingsContribution` / `getDeviceLiveContribution` /
52306
- * `applyDeviceSettingsPatch` are auto-injected on the provider
52307
- * interface and routed to the device-details aggregator.
52421
+ * Each export provider MAY contribute its own per-device "Export" panel
52422
+ * (enabled toggle, preferred stream, addon-specific knobs). The three
52423
+ * framework methods are mounted on the cap router when present.
52424
+ * `IDeviceExportProvider` leaves them optional: an exporter with no
52425
+ * per-device panel omits them (a `deviceConfig` cap's UI is derived and
52426
+ * never hand-written — D14 — and an exporter is not one of those).
52308
52427
  */
52309
52428
  exposesDeviceSettings: true,
52310
52429
  methods: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.661",
3
+ "version": "1.2.662",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -137,7 +137,7 @@
137
137
  "description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
138
138
  },
139
139
  "requires": {
140
- "server": ">=1.2.770"
140
+ "server": ">=1.2.777"
141
141
  }
142
142
  },
143
143
  "files": [