@camstack/addon-provider-ecowitt 0.2.156 → 0.2.157

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
+ ## [0.2.157] - 2026-11-05
11
+
12
+ ### Changed
13
+
14
+ - Internal: the connection form comment names the instance form; no behaviour change.
15
+
10
16
  ## [0.2.156] - 2026-11-05
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -5984,7 +5984,7 @@ var ZodIssueCode$1 = {
5984
5984
  var ZodFirstPartyTypeKind$1;
5985
5985
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5986
5986
  //#endregion
5987
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5987
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
5988
5988
  /**
5989
5989
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5990
5990
  * window to float samples (D455).
@@ -6793,6 +6793,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6793
6793
  }
6794
6794
  return out;
6795
6795
  }
6796
+ /** Every scope, in the order the guard and the docs list them. */
6797
+ var SCOPED_SETTINGS_SCOPES = [
6798
+ "cluster",
6799
+ "device-defaults",
6800
+ "node"
6801
+ ];
6796
6802
  /**
6797
6803
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6798
6804
  *
@@ -7684,6 +7690,13 @@ var BaseAddon = class {
7684
7690
  */
7685
7691
  secretStatePaths = [];
7686
7692
  _addonSecretTree = null;
7693
+ /**
7694
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7695
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7696
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7697
+ */
7698
+ _scopedByCap = /* @__PURE__ */ new Map();
7699
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7687
7700
  _deviceSecretTree = null;
7688
7701
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7689
7702
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7753,6 +7766,7 @@ var BaseAddon = class {
7753
7766
  this._ctx = context;
7754
7767
  this._secretSettings = null;
7755
7768
  }
7769
+ await this.loadScopedSchemas();
7756
7770
  await this.resolveConfig();
7757
7771
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7758
7772
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7762,6 +7776,7 @@ var BaseAddon = class {
7762
7776
  const normalized = normalizeAddonInitResult(result);
7763
7777
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7764
7778
  this._registeredCapNames = providers.map((p) => p.capability.name);
7779
+ await this.loadScopedSchemas();
7765
7780
  return normalized;
7766
7781
  }
7767
7782
  /**
@@ -7850,6 +7865,16 @@ var BaseAddon = class {
7850
7865
  schema(schema) {
7851
7866
  return schema;
7852
7867
  }
7868
+ /**
7869
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7870
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7871
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7872
+ * must already know which keys are per node. Names the providers register at init are added
7873
+ * once known.
7874
+ */
7875
+ scopedSettingsCapabilities() {
7876
+ return [];
7877
+ }
7853
7878
  /** Override to provide global-level settings UI schema. */
7854
7879
  globalSettingsSchema(_cap) {
7855
7880
  return null;
@@ -7893,7 +7918,8 @@ var BaseAddon = class {
7893
7918
  return [];
7894
7919
  }
7895
7920
  async getGlobalSettings(overlay, cap, nodeId) {
7896
- const schema = this.globalSettingsSchema(cap);
7921
+ await this.loadScopedSchemas(cap);
7922
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7897
7923
  if (!schema) return { sections: [] };
7898
7924
  const projected = await this.readRawProjectedStore(nodeId, cap);
7899
7925
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8024,6 +8050,7 @@ var BaseAddon = class {
8024
8050
  * resolved blob back is a no-op for every unchanged secret.
8025
8051
  */
8026
8052
  async resolveGlobalStore(nodeId, cap) {
8053
+ await this.loadScopedSchemas(cap);
8027
8054
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8028
8055
  return this.projectForNode(raw, nodeId, cap);
8029
8056
  }
@@ -8034,6 +8061,7 @@ var BaseAddon = class {
8034
8061
  }
8035
8062
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8036
8063
  async readRawProjectedStore(nodeId, cap) {
8064
+ await this.loadScopedSchemas(cap);
8037
8065
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8038
8066
  return this.projectForNode(raw, nodeId, cap);
8039
8067
  }
@@ -8136,6 +8164,7 @@ var BaseAddon = class {
8136
8164
  await this.applyGlobalPatch(patch, nodeId);
8137
8165
  }
8138
8166
  async applyGlobalPatch(barePatch, nodeId) {
8167
+ await this.loadScopedSchemas();
8139
8168
  const keys = this.perNodeKeys();
8140
8169
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8141
8170
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8149,7 +8178,8 @@ var BaseAddon = class {
8149
8178
  await this.resolveConfig();
8150
8179
  await this.onConfigChanged();
8151
8180
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8152
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8181
+ const legacy = this.globalSettingsSchema();
8182
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8153
8183
  }
8154
8184
  /**
8155
8185
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8157,14 +8187,67 @@ var BaseAddon = class {
8157
8187
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8158
8188
  * settings API behaves exactly like the legacy node-agnostic one.
8159
8189
  */
8160
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8190
+ /**
8191
+ * Load the scoped schemas of every capability this addon declares (the override and the
8192
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8193
+ * derived from the previous set. Idempotent.
8194
+ */
8195
+ async loadScopedSchemas(extraCap) {
8196
+ const caps = new Set([
8197
+ ...this.scopedSettingsCapabilities(),
8198
+ ...this._registeredCapNames,
8199
+ ...extraCap === void 0 ? [] : [extraCap]
8200
+ ]);
8201
+ let added = false;
8202
+ for (const cap of caps) {
8203
+ if (this._scopedByCap.has(cap)) continue;
8204
+ const loaded = [];
8205
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8206
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8207
+ if (schema !== null) loaded.push({
8208
+ scope,
8209
+ schema
8210
+ });
8211
+ }
8212
+ this._scopedByCap.set(cap, loaded);
8213
+ added = true;
8214
+ }
8215
+ if (!added) return;
8216
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8217
+ this._addonSecretTree = null;
8218
+ }
8219
+ /**
8220
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8221
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8222
+ */
8223
+ scopedFormSchema(cap) {
8224
+ const byId = /* @__PURE__ */ new Map();
8225
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8226
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8227
+ }
8228
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8229
+ loadedScopedSchemas(cap) {
8230
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8231
+ return [...this._scopedByCap.values()].flat();
8232
+ }
8233
+ /**
8234
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8235
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8236
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8237
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8238
+ * Call {@link loadScopedSchemas} first (the async seams do).
8239
+ */
8161
8240
  perNodeKeys(cap) {
8162
8241
  const cacheKey = cap ?? "";
8163
- const cached = this._perNodeKeysCache.get(cacheKey);
8242
+ const cached = this._nodeKeysByCap.get(cacheKey);
8164
8243
  if (cached) return cached;
8165
- const schema = this.globalSettingsSchema(cap);
8166
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8167
- this._perNodeKeysCache.set(cacheKey, keys);
8244
+ const legacy = this.globalSettingsSchema(cap);
8245
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8246
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8247
+ if (scope !== "node") continue;
8248
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8249
+ }
8250
+ this._nodeKeysByCap.set(cacheKey, keys);
8168
8251
  return keys;
8169
8252
  }
8170
8253
  /**
@@ -8174,10 +8257,9 @@ var BaseAddon = class {
8174
8257
  * time to return its response before the addon is torn down and
8175
8258
  * re-initialised by `AddonRegistryService.restartAddon`.
8176
8259
  */
8177
- maybeAutoRestart(patchKeys, schema) {
8178
- if (!schema) return;
8260
+ maybeAutoRestart(patchKeys, schemas) {
8179
8261
  const restartKeys = /* @__PURE__ */ new Set();
8180
- for (const section of schema.sections) for (const field of section.fields) {
8262
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8181
8263
  if (field.type === "separator" || field.type === "info") continue;
8182
8264
  if (field.requiresRestart) restartKeys.add(field.key);
8183
8265
  }
@@ -8373,12 +8455,14 @@ var BaseAddon = class {
8373
8455
  * here, in the addon's own process, so the hub keeps no copy of it.
8374
8456
  */
8375
8457
  async getDeclaredSettingsKeys() {
8458
+ await this.loadScopedSchemas();
8376
8459
  return { keys: [...this.declaredKeySet()].toSorted() };
8377
8460
  }
8378
8461
  declaredKeySet() {
8379
8462
  const keys = new Set(Object.keys(this.defaults));
8380
8463
  const schema = this.globalSettingsSchema();
8381
8464
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8465
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8382
8466
  for (const key of this._declaredStateKeys) keys.add(key);
8383
8467
  for (const path of this.secretStatePaths) {
8384
8468
  const root = path.split(/[.[]/, 1)[0];
@@ -8474,7 +8558,11 @@ var BaseAddon = class {
8474
8558
  }
8475
8559
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8476
8560
  addonSecretTree() {
8477
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8561
+ if (this._addonSecretTree === null) {
8562
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8563
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8564
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8565
+ }
8478
8566
  return this._addonSecretTree;
8479
8567
  }
8480
8568
  deviceSecretTree() {
@@ -8724,6 +8812,23 @@ function dropPerNodeFields(fields) {
8724
8812
  }
8725
8813
  return kept;
8726
8814
  }
8815
+ /** The keys of the value-holding fields, recursing into layout containers. */
8816
+ function collectValueFieldKeys(fields) {
8817
+ const collected = [];
8818
+ for (const field of fields) {
8819
+ if (field.type === "group") {
8820
+ collected.push(...collectValueFieldKeys(field.fields));
8821
+ continue;
8822
+ }
8823
+ if (field.type === "sub-tabs") {
8824
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8825
+ continue;
8826
+ }
8827
+ if (field.type === "separator" || field.type === "info") continue;
8828
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8829
+ }
8830
+ return collected;
8831
+ }
8727
8832
  function collectFieldKeys(fields) {
8728
8833
  const collected = [];
8729
8834
  for (const field of fields) {
@@ -13422,12 +13527,22 @@ var RecordIntegrationInstances = class {
13422
13527
  ...this.hooks.description,
13423
13528
  mintsId: true
13424
13529
  };
13425
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13530
+ /**
13531
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13532
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13533
+ * record id answers as it is. An id that names nothing comes back unchanged.
13534
+ */
13535
+ async resolveRecordId(id) {
13536
+ if (await this.handle.get(id) !== null) return id;
13537
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13538
+ return linked === void 0 ? id : linked.id;
13539
+ }
13540
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13426
13541
  listInstances = async () => {
13427
13542
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13428
13543
  };
13429
13544
  getInstance = async ({ id }) => {
13430
- const record = await this.handle.get(id);
13545
+ const record = await this.handle.get(await this.resolveRecordId(id));
13431
13546
  return record === null ? null : this.viewOf(record);
13432
13547
  };
13433
13548
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13444,7 +13559,8 @@ var RecordIntegrationInstances = class {
13444
13559
  await this.hooks.onChanged?.(minted, "created");
13445
13560
  return { id: minted };
13446
13561
  };
13447
- updateInstance = async ({ id, name, values }) => {
13562
+ updateInstance = async ({ id: askedId, name, values }) => {
13563
+ const id = await this.resolveRecordId(askedId);
13448
13564
  const patch = {};
13449
13565
  for (const [key, value] of Object.entries(values)) {
13450
13566
  if (value === void 0) continue;
@@ -13479,7 +13595,10 @@ var RecordIntegrationInstances = class {
13479
13595
  status: "unsupported",
13480
13596
  error: "This integration declares no connection test"
13481
13597
  };
13482
- return this.hooks.test(input);
13598
+ return this.hooks.test(input.id === null ? input : {
13599
+ ...input,
13600
+ id: await this.resolveRecordId(input.id)
13601
+ });
13483
13602
  };
13484
13603
  /** The flat provider object to hand to `registerProvider`. */
13485
13604
  toProvider() {
@@ -97478,7 +97597,7 @@ function gatewayKeyOf(integration) {
97478
97597
  return origin.length > 0 ? origin : `ecowitt:${slugifyHost(integration.id)}`;
97479
97598
  }
97480
97599
  /**
97481
- * The integration's settings form (`globalSettingsSchema`): the connection lives on the
97600
+ * The integration instance form (the `formSchema` of its instances): the connection lives on the
97482
97601
  * INTEGRATION, which carries the name. The `password` field is what puts the value in the keychain.
97483
97602
  */
97484
97603
  function buildConnectionFormSchema() {
package/dist/addon.mjs CHANGED
@@ -5983,7 +5983,7 @@ var ZodIssueCode$1 = {
5983
5983
  var ZodFirstPartyTypeKind$1;
5984
5984
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5985
5985
  //#endregion
5986
- //#region ../types/dist/sleep-9TIHFj1w.mjs
5986
+ //#region ../types/dist/sleep-DEZaLbiu.mjs
5987
5987
  /**
5988
5988
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5989
5989
  * window to float samples (D455).
@@ -6792,6 +6792,12 @@ function projectStore(store, perNodeKeys, nodeId) {
6792
6792
  }
6793
6793
  return out;
6794
6794
  }
6795
+ /** Every scope, in the order the guard and the docs list them. */
6796
+ var SCOPED_SETTINGS_SCOPES = [
6797
+ "cluster",
6798
+ "device-defaults",
6799
+ "node"
6800
+ ];
6795
6801
  /**
6796
6802
  * The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
6797
6803
  *
@@ -7683,6 +7689,13 @@ var BaseAddon = class {
7683
7689
  */
7684
7690
  secretStatePaths = [];
7685
7691
  _addonSecretTree = null;
7692
+ /**
7693
+ * D986 M4: the scoped schemas this addon declared, resolved once per capability
7694
+ * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
7695
+ * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
7696
+ */
7697
+ _scopedByCap = /* @__PURE__ */ new Map();
7698
+ _nodeKeysByCap = /* @__PURE__ */ new Map();
7686
7699
  _deviceSecretTree = null;
7687
7700
  /** Refs the last resolved config pointed at — a change to one re-resolves. */
7688
7701
  _configSecretRefs = /* @__PURE__ */ new Set();
@@ -7752,6 +7765,7 @@ var BaseAddon = class {
7752
7765
  this._ctx = context;
7753
7766
  this._secretSettings = null;
7754
7767
  }
7768
+ await this.loadScopedSchemas();
7755
7769
  await this.resolveConfig();
7756
7770
  if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
7757
7771
  if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
@@ -7761,6 +7775,7 @@ var BaseAddon = class {
7761
7775
  const normalized = normalizeAddonInitResult(result);
7762
7776
  const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
7763
7777
  this._registeredCapNames = providers.map((p) => p.capability.name);
7778
+ await this.loadScopedSchemas();
7764
7779
  return normalized;
7765
7780
  }
7766
7781
  /**
@@ -7849,6 +7864,16 @@ var BaseAddon = class {
7849
7864
  schema(schema) {
7850
7865
  return schema;
7851
7866
  }
7867
+ /**
7868
+ * The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
7869
+ * {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
7870
+ * `onInitialize`, when the providers it registers are not known yet: the first config resolve
7871
+ * must already know which keys are per node. Names the providers register at init are added
7872
+ * once known.
7873
+ */
7874
+ scopedSettingsCapabilities() {
7875
+ return [];
7876
+ }
7852
7877
  /** Override to provide global-level settings UI schema. */
7853
7878
  globalSettingsSchema(_cap) {
7854
7879
  return null;
@@ -7892,7 +7917,8 @@ var BaseAddon = class {
7892
7917
  return [];
7893
7918
  }
7894
7919
  async getGlobalSettings(overlay, cap, nodeId) {
7895
- const schema = this.globalSettingsSchema(cap);
7920
+ await this.loadScopedSchemas(cap);
7921
+ const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
7896
7922
  if (!schema) return { sections: [] };
7897
7923
  const projected = await this.readRawProjectedStore(nodeId, cap);
7898
7924
  return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
@@ -8023,6 +8049,7 @@ var BaseAddon = class {
8023
8049
  * resolved blob back is a no-op for every unchanged secret.
8024
8050
  */
8025
8051
  async resolveGlobalStore(nodeId, cap) {
8052
+ await this.loadScopedSchemas(cap);
8026
8053
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8027
8054
  return this.projectForNode(raw, nodeId, cap);
8028
8055
  }
@@ -8033,6 +8060,7 @@ var BaseAddon = class {
8033
8060
  }
8034
8061
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8035
8062
  async readRawProjectedStore(nodeId, cap) {
8063
+ await this.loadScopedSchemas(cap);
8036
8064
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8037
8065
  return this.projectForNode(raw, nodeId, cap);
8038
8066
  }
@@ -8135,6 +8163,7 @@ var BaseAddon = class {
8135
8163
  await this.applyGlobalPatch(patch, nodeId);
8136
8164
  }
8137
8165
  async applyGlobalPatch(barePatch, nodeId) {
8166
+ await this.loadScopedSchemas();
8138
8167
  const keys = this.perNodeKeys();
8139
8168
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8140
8169
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8148,7 +8177,8 @@ var BaseAddon = class {
8148
8177
  await this.resolveConfig();
8149
8178
  await this.onConfigChanged();
8150
8179
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8151
- this.maybeAutoRestart(Object.keys(barePatch), this.globalSettingsSchema());
8180
+ const legacy = this.globalSettingsSchema();
8181
+ this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
8152
8182
  }
8153
8183
  /**
8154
8184
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8156,14 +8186,67 @@ var BaseAddon = class {
8156
8186
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8157
8187
  * settings API behaves exactly like the legacy node-agnostic one.
8158
8188
  */
8159
- _perNodeKeysCache = /* @__PURE__ */ new Map();
8189
+ /**
8190
+ * Load the scoped schemas of every capability this addon declares (the override and the
8191
+ * providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
8192
+ * derived from the previous set. Idempotent.
8193
+ */
8194
+ async loadScopedSchemas(extraCap) {
8195
+ const caps = new Set([
8196
+ ...this.scopedSettingsCapabilities(),
8197
+ ...this._registeredCapNames,
8198
+ ...extraCap === void 0 ? [] : [extraCap]
8199
+ ]);
8200
+ let added = false;
8201
+ for (const cap of caps) {
8202
+ if (this._scopedByCap.has(cap)) continue;
8203
+ const loaded = [];
8204
+ for (const scope of SCOPED_SETTINGS_SCOPES) {
8205
+ const schema = await this.getScopedSettingsSchema(scope, cap);
8206
+ if (schema !== null) loaded.push({
8207
+ scope,
8208
+ schema
8209
+ });
8210
+ }
8211
+ this._scopedByCap.set(cap, loaded);
8212
+ added = true;
8213
+ }
8214
+ if (!added) return;
8215
+ this._nodeKeysByCap = /* @__PURE__ */ new Map();
8216
+ this._addonSecretTree = null;
8217
+ }
8218
+ /**
8219
+ * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8220
+ * sections of its scoped schemas, so dropping the old hook does not blank the form.
8221
+ */
8222
+ scopedFormSchema(cap) {
8223
+ const byId = /* @__PURE__ */ new Map();
8224
+ for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
8225
+ return byId.size === 0 ? null : { sections: [...byId.values()] };
8226
+ }
8227
+ /** Every loaded scoped schema of `cap`, or of every loaded capability. */
8228
+ loadedScopedSchemas(cap) {
8229
+ if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
8230
+ return [...this._scopedByCap.values()].flat();
8231
+ }
8232
+ /**
8233
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8234
+ * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
8235
+ * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8236
+ * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8237
+ * Call {@link loadScopedSchemas} first (the async seams do).
8238
+ */
8160
8239
  perNodeKeys(cap) {
8161
8240
  const cacheKey = cap ?? "";
8162
- const cached = this._perNodeKeysCache.get(cacheKey);
8241
+ const cached = this._nodeKeysByCap.get(cacheKey);
8163
8242
  if (cached) return cached;
8164
- const schema = this.globalSettingsSchema(cap);
8165
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8166
- this._perNodeKeysCache.set(cacheKey, keys);
8243
+ const legacy = this.globalSettingsSchema(cap);
8244
+ const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8245
+ for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8246
+ if (scope !== "node") continue;
8247
+ for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
8248
+ }
8249
+ this._nodeKeysByCap.set(cacheKey, keys);
8167
8250
  return keys;
8168
8251
  }
8169
8252
  /**
@@ -8173,10 +8256,9 @@ var BaseAddon = class {
8173
8256
  * time to return its response before the addon is torn down and
8174
8257
  * re-initialised by `AddonRegistryService.restartAddon`.
8175
8258
  */
8176
- maybeAutoRestart(patchKeys, schema) {
8177
- if (!schema) return;
8259
+ maybeAutoRestart(patchKeys, schemas) {
8178
8260
  const restartKeys = /* @__PURE__ */ new Set();
8179
- for (const section of schema.sections) for (const field of section.fields) {
8261
+ for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
8180
8262
  if (field.type === "separator" || field.type === "info") continue;
8181
8263
  if (field.requiresRestart) restartKeys.add(field.key);
8182
8264
  }
@@ -8372,12 +8454,14 @@ var BaseAddon = class {
8372
8454
  * here, in the addon's own process, so the hub keeps no copy of it.
8373
8455
  */
8374
8456
  async getDeclaredSettingsKeys() {
8457
+ await this.loadScopedSchemas();
8375
8458
  return { keys: [...this.declaredKeySet()].toSorted() };
8376
8459
  }
8377
8460
  declaredKeySet() {
8378
8461
  const keys = new Set(Object.keys(this.defaults));
8379
8462
  const schema = this.globalSettingsSchema();
8380
8463
  if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8464
+ for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
8381
8465
  for (const key of this._declaredStateKeys) keys.add(key);
8382
8466
  for (const path of this.secretStatePaths) {
8383
8467
  const root = path.split(/[.[]/, 1)[0];
@@ -8473,7 +8557,11 @@ var BaseAddon = class {
8473
8557
  }
8474
8558
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8475
8559
  addonSecretTree() {
8476
- this._addonSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.globalSettingsSchema()), this.secretStatePaths);
8560
+ if (this._addonSecretTree === null) {
8561
+ const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
8562
+ for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
8563
+ this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
8564
+ }
8477
8565
  return this._addonSecretTree;
8478
8566
  }
8479
8567
  deviceSecretTree() {
@@ -8723,6 +8811,23 @@ function dropPerNodeFields(fields) {
8723
8811
  }
8724
8812
  return kept;
8725
8813
  }
8814
+ /** The keys of the value-holding fields, recursing into layout containers. */
8815
+ function collectValueFieldKeys(fields) {
8816
+ const collected = [];
8817
+ for (const field of fields) {
8818
+ if (field.type === "group") {
8819
+ collected.push(...collectValueFieldKeys(field.fields));
8820
+ continue;
8821
+ }
8822
+ if (field.type === "sub-tabs") {
8823
+ for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
8824
+ continue;
8825
+ }
8826
+ if (field.type === "separator" || field.type === "info") continue;
8827
+ if ("key" in field && typeof field.key === "string") collected.push(field.key);
8828
+ }
8829
+ return collected;
8830
+ }
8726
8831
  function collectFieldKeys(fields) {
8727
8832
  const collected = [];
8728
8833
  for (const field of fields) {
@@ -13421,12 +13526,22 @@ var RecordIntegrationInstances = class {
13421
13526
  ...this.hooks.description,
13422
13527
  mintsId: true
13423
13528
  };
13424
- instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13529
+ /**
13530
+ * The record an id names. A caller holding the `integrations` registry id (the Integrations
13531
+ * page) reaches a kind whose body links to it through `integrationId`; an id that is itself a
13532
+ * record id answers as it is. An id that names nothing comes back unchanged.
13533
+ */
13534
+ async resolveRecordId(id) {
13535
+ if (await this.handle.get(id) !== null) return id;
13536
+ const linked = (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.find((record) => record.data["integrationId"] === id);
13537
+ return linked === void 0 ? id : linked.id;
13538
+ }
13539
+ instanceFormSchema = async ({ id }) => this.hooks.formSchema(id === null ? null : await this.resolveRecordId(id));
13425
13540
  listInstances = async () => {
13426
13541
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13427
13542
  };
13428
13543
  getInstance = async ({ id }) => {
13429
- const record = await this.handle.get(id);
13544
+ const record = await this.handle.get(await this.resolveRecordId(id));
13430
13545
  return record === null ? null : this.viewOf(record);
13431
13546
  };
13432
13547
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13443,7 +13558,8 @@ var RecordIntegrationInstances = class {
13443
13558
  await this.hooks.onChanged?.(minted, "created");
13444
13559
  return { id: minted };
13445
13560
  };
13446
- updateInstance = async ({ id, name, values }) => {
13561
+ updateInstance = async ({ id: askedId, name, values }) => {
13562
+ const id = await this.resolveRecordId(askedId);
13447
13563
  const patch = {};
13448
13564
  for (const [key, value] of Object.entries(values)) {
13449
13565
  if (value === void 0) continue;
@@ -13478,7 +13594,10 @@ var RecordIntegrationInstances = class {
13478
13594
  status: "unsupported",
13479
13595
  error: "This integration declares no connection test"
13480
13596
  };
13481
- return this.hooks.test(input);
13597
+ return this.hooks.test(input.id === null ? input : {
13598
+ ...input,
13599
+ id: await this.resolveRecordId(input.id)
13600
+ });
13482
13601
  };
13483
13602
  /** The flat provider object to hand to `registerProvider`. */
13484
13603
  toProvider() {
@@ -97477,7 +97596,7 @@ function gatewayKeyOf(integration) {
97477
97596
  return origin.length > 0 ? origin : `ecowitt:${slugifyHost(integration.id)}`;
97478
97597
  }
97479
97598
  /**
97480
- * The integration's settings form (`globalSettingsSchema`): the connection lives on the
97599
+ * The integration instance form (the `formSchema` of its instances): the connection lives on the
97481
97600
  * INTEGRATION, which carries the name. The `password` field is what puts the value in the keychain.
97482
97601
  */
97483
97602
  function buildConnectionFormSchema() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-ecowitt",
3
- "version": "0.2.156",
3
+ "version": "0.2.157",
4
4
  "description": "Ecowitt weather-station device-provider addon for CamStack — wraps the @apocaliss92/nodewitt local-poll / push client",
5
5
  "keywords": [
6
6
  "camstack",
@@ -63,7 +63,7 @@
63
63
  }
64
64
  ],
65
65
  "requires": {
66
- "server": ">=1.2.770"
66
+ "server": ">=1.2.777"
67
67
  }
68
68
  },
69
69
  "files": [