@camstack/addon-provider-ecowitt 0.2.155 → 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,18 @@ 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
+
16
+ ## [0.2.156] - 2026-11-05
17
+
18
+ ### Changed
19
+
20
+ - Copying an integration into its record reads the leftover rows through the migration-only door and resolves their secrets in this addon
21
+
10
22
  ## [0.2.155] - 2026-11-04
11
23
 
12
24
  ### 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-D1jjWAr8.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 });
@@ -7928,10 +7954,9 @@ var BaseAddon = class {
7928
7954
  *
7929
7955
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7930
7956
  * needs its connection fields, the default integration the addon's own sections. It is the
7931
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7932
- * unchanged (`integrations.setSettings` for an account integration).
7957
+ * SHAPE of the form only — the values live on the instance record.
7933
7958
  */
7934
- async integrationSettingsSchema(_instance) {
7959
+ async integrationFormSchema(_instance) {
7935
7960
  return null;
7936
7961
  }
7937
7962
  /**
@@ -7977,7 +8002,7 @@ var BaseAddon = class {
7977
8002
  * per-instance schema when it declares one, else its global schema (what they always showed).
7978
8003
  */
7979
8004
  async getIntegrationFormSchema(instance, nodeId) {
7980
- const scoped = await this.integrationSettingsSchema(instance);
8005
+ const scoped = await this.integrationFormSchema(instance);
7981
8006
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7982
8007
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7983
8008
  }
@@ -8025,6 +8050,7 @@ var BaseAddon = class {
8025
8050
  * resolved blob back is a no-op for every unchanged secret.
8026
8051
  */
8027
8052
  async resolveGlobalStore(nodeId, cap) {
8053
+ await this.loadScopedSchemas(cap);
8028
8054
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8029
8055
  return this.projectForNode(raw, nodeId, cap);
8030
8056
  }
@@ -8035,6 +8061,7 @@ var BaseAddon = class {
8035
8061
  }
8036
8062
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8037
8063
  async readRawProjectedStore(nodeId, cap) {
8064
+ await this.loadScopedSchemas(cap);
8038
8065
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8039
8066
  return this.projectForNode(raw, nodeId, cap);
8040
8067
  }
@@ -8137,6 +8164,7 @@ var BaseAddon = class {
8137
8164
  await this.applyGlobalPatch(patch, nodeId);
8138
8165
  }
8139
8166
  async applyGlobalPatch(barePatch, nodeId) {
8167
+ await this.loadScopedSchemas();
8140
8168
  const keys = this.perNodeKeys();
8141
8169
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8142
8170
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8150,7 +8178,8 @@ var BaseAddon = class {
8150
8178
  await this.resolveConfig();
8151
8179
  await this.onConfigChanged();
8152
8180
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8153
- 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)]);
8154
8183
  }
8155
8184
  /**
8156
8185
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8158,14 +8187,67 @@ var BaseAddon = class {
8158
8187
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8159
8188
  * settings API behaves exactly like the legacy node-agnostic one.
8160
8189
  */
8161
- _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
+ */
8162
8240
  perNodeKeys(cap) {
8163
8241
  const cacheKey = cap ?? "";
8164
- const cached = this._perNodeKeysCache.get(cacheKey);
8242
+ const cached = this._nodeKeysByCap.get(cacheKey);
8165
8243
  if (cached) return cached;
8166
- const schema = this.globalSettingsSchema(cap);
8167
- const keys = new Set(schema ? schema.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
8168
- 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);
8169
8251
  return keys;
8170
8252
  }
8171
8253
  /**
@@ -8175,10 +8257,9 @@ var BaseAddon = class {
8175
8257
  * time to return its response before the addon is torn down and
8176
8258
  * re-initialised by `AddonRegistryService.restartAddon`.
8177
8259
  */
8178
- maybeAutoRestart(patchKeys, schema) {
8179
- if (!schema) return;
8260
+ maybeAutoRestart(patchKeys, schemas) {
8180
8261
  const restartKeys = /* @__PURE__ */ new Set();
8181
- 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) {
8182
8263
  if (field.type === "separator" || field.type === "info") continue;
8183
8264
  if (field.requiresRestart) restartKeys.add(field.key);
8184
8265
  }
@@ -8374,12 +8455,14 @@ var BaseAddon = class {
8374
8455
  * here, in the addon's own process, so the hub keeps no copy of it.
8375
8456
  */
8376
8457
  async getDeclaredSettingsKeys() {
8458
+ await this.loadScopedSchemas();
8377
8459
  return { keys: [...this.declaredKeySet()].toSorted() };
8378
8460
  }
8379
8461
  declaredKeySet() {
8380
8462
  const keys = new Set(Object.keys(this.defaults));
8381
8463
  const schema = this.globalSettingsSchema();
8382
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);
8383
8466
  for (const key of this._declaredStateKeys) keys.add(key);
8384
8467
  for (const path of this.secretStatePaths) {
8385
8468
  const root = path.split(/[.[]/, 1)[0];
@@ -8475,7 +8558,11 @@ var BaseAddon = class {
8475
8558
  }
8476
8559
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8477
8560
  addonSecretTree() {
8478
- 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
+ }
8479
8566
  return this._addonSecretTree;
8480
8567
  }
8481
8568
  deviceSecretTree() {
@@ -8725,6 +8812,23 @@ function dropPerNodeFields(fields) {
8725
8812
  }
8726
8813
  return kept;
8727
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
+ }
8728
8832
  function collectFieldKeys(fields) {
8729
8833
  const collected = [];
8730
8834
  for (const field of fields) {
@@ -11951,7 +12055,7 @@ function userError(code, params, englishFallback) {
11951
12055
  return new CamstackError(code, params, englishFallback);
11952
12056
  }
11953
12057
  //#endregion
11954
- //#region ../types/dist/composition-CRNwcFYY.mjs
12058
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11955
12059
  /**
11956
12060
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11957
12061
  *
@@ -13423,12 +13527,22 @@ var RecordIntegrationInstances = class {
13423
13527
  ...this.hooks.description,
13424
13528
  mintsId: true
13425
13529
  };
13426
- 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));
13427
13541
  listInstances = async () => {
13428
13542
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13429
13543
  };
13430
13544
  getInstance = async ({ id }) => {
13431
- const record = await this.handle.get(id);
13545
+ const record = await this.handle.get(await this.resolveRecordId(id));
13432
13546
  return record === null ? null : this.viewOf(record);
13433
13547
  };
13434
13548
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13445,7 +13559,8 @@ var RecordIntegrationInstances = class {
13445
13559
  await this.hooks.onChanged?.(minted, "created");
13446
13560
  return { id: minted };
13447
13561
  };
13448
- updateInstance = async ({ id, name, values }) => {
13562
+ updateInstance = async ({ id: askedId, name, values }) => {
13563
+ const id = await this.resolveRecordId(askedId);
13449
13564
  const patch = {};
13450
13565
  for (const [key, value] of Object.entries(values)) {
13451
13566
  if (value === void 0) continue;
@@ -13480,7 +13595,10 @@ var RecordIntegrationInstances = class {
13480
13595
  status: "unsupported",
13481
13596
  error: "This integration declares no connection test"
13482
13597
  };
13483
- return this.hooks.test(input);
13598
+ return this.hooks.test(input.id === null ? input : {
13599
+ ...input,
13600
+ id: await this.resolveRecordId(input.id)
13601
+ });
13484
13602
  };
13485
13603
  /** The flat provider object to hand to `registerProvider`. */
13486
13604
  toProvider() {
@@ -14659,20 +14777,28 @@ async function moveIntegrationRowsToRecords(ports, spec) {
14659
14777
  };
14660
14778
  }
14661
14779
  /**
14662
- * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
14663
- * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
14664
- * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
14665
- * the new record through the addon's own records handle. The hub deletes the rows and their
14666
- * keychain values; the addon never touches the registry.
14780
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416). An addon runs the move
14781
+ * from its own process, so the leftover rows are reached through the migration-only door
14782
+ * (`migrations.readLegacyIntegrationRows` / `clearLegacyIntegrationRows`) on the hub's connection.
14783
+ * The door returns markers. This process is the owner, so it resolves and releases them with
14784
+ * `resolveSecretMarkers` — the same call the old settings door used — and only then asks the
14785
+ * door to delete the rows. The addon's own record kind is its records handle.
14667
14786
  */
14787
+ async function resolvedRows(stored, secrets) {
14788
+ if (secrets === null) return stored;
14789
+ const { value, unresolved } = await resolveSecretMarkers(stored, secrets);
14790
+ if (unresolved.length > 0) throw new Error(`${unresolved.length} secret(s) cannot be read from the keychain`);
14791
+ return value;
14792
+ }
14668
14793
  function integrationRowsPortsOverApi(deps) {
14794
+ const secrets = deps.secrets ?? null;
14669
14795
  return {
14670
14796
  warn: (message) => deps.log.warn(message),
14671
14797
  listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
14672
14798
  id: i.id,
14673
14799
  name: i.name
14674
14800
  })),
14675
- readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
14801
+ readRows: async (integrationId) => resolvedRows(await deps.api.readLegacyIntegrationRows({ integrationId }), secrets),
14676
14802
  putRecord: async (id, body) => {
14677
14803
  if (await deps.records.get(id) !== null) return "exists";
14678
14804
  try {
@@ -14691,15 +14817,17 @@ function integrationRowsPortsOverApi(deps) {
14691
14817
  await deps.records.delete(id);
14692
14818
  },
14693
14819
  clearRows: async (integrationId) => {
14694
- await deps.api.clearSettings({ id: integrationId });
14820
+ const refs = collectSecretMarkerRefs(await deps.api.readLegacyIntegrationRows({ integrationId }));
14821
+ if (refs.length > 0 && secrets !== null) await secrets.delete(refs);
14822
+ await deps.api.clearLegacyIntegrationRows({ integrationId });
14695
14823
  }
14696
14824
  };
14697
14825
  }
14698
14826
  function defaultIntegrationApiOver(api) {
14699
14827
  return {
14700
14828
  list: () => api.integrations.list.query(),
14701
- getSettings: (input) => api.integrations.getSettings.query(input),
14702
- clearSettings: (input) => api.integrations.clearSettings.mutate(input),
14829
+ readLegacyIntegrationRows: (input) => api.migrations.readLegacyIntegrationRows.query(input),
14830
+ clearLegacyIntegrationRows: (input) => api.migrations.clearLegacyIntegrationRows.mutate(input),
14703
14831
  listDevices: (addonId) => api.deviceManager.listAll.query({
14704
14832
  addonId,
14705
14833
  projection: "slim"
@@ -50377,6 +50505,9 @@ var RunningProposalEvidenceSchema = object({
50377
50505
  runsFound: number().int().nonnegative(),
50378
50506
  pausesFound: number().int().nonnegative(),
50379
50507
  longestPauseMs: number().nonnegative(),
50508
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50509
+ pulsesFound: number().int().nonnegative(),
50510
+ longestPulseMs: number().nonnegative(),
50380
50511
  idleFloorW: number(),
50381
50512
  noiseCeilingW: number(),
50382
50513
  plateauW: number(),
@@ -56243,11 +56374,7 @@ var CreateIntegrationInputSchema = object({
56243
56374
  info: record(string(), unknown()).optional(),
56244
56375
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56245
56376
  originStableId: string().optional(),
56246
- settings: record(string(), unknown()).optional(),
56247
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56248
- * the keychain, not the table (D787). */
56249
- secretKeys: array(string()).optional(),
56250
- skipRestart: boolean().optional()
56377
+ settings: record(string(), unknown()).optional()
56251
56378
  });
56252
56379
  var UpdateIntegrationInputSchema = object({
56253
56380
  id: string(),
@@ -56268,22 +56395,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56268
56395
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
56269
56396
  kind: "mutation",
56270
56397
  auth: "admin"
56271
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
56272
- id: string(),
56273
- settings: record(string(), unknown()),
56274
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56275
- secretKeys: array(string()).optional(),
56276
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56277
- skipRestart: boolean().optional()
56278
- }), object({ success: boolean() }), {
56279
- kind: "mutation",
56280
- auth: "admin"
56281
- }), method(object({ id: string() }), object({
56282
- rows: number().int().nonnegative(),
56283
- secrets: number().int().nonnegative()
56284
- }), {
56285
- kind: "mutation",
56286
- auth: "admin"
56287
56398
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56288
56399
  addonId: string(),
56289
56400
  settings: record(string(), unknown())
@@ -59260,6 +59371,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59260
59371
  }), {
59261
59372
  kind: "mutation",
59262
59373
  auth: "admin"
59374
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
59375
+ rows: number().int().nonnegative(),
59376
+ secrets: number().int().nonnegative()
59377
+ }), {
59378
+ kind: "mutation",
59379
+ auth: "admin"
59380
+ }), method(object({
59381
+ integrationId: string().min(1),
59382
+ patch: record(string(), unknown())
59383
+ }), object({ written: boolean() }), {
59384
+ kind: "mutation",
59385
+ auth: "admin"
59386
+ }), method(object({
59387
+ integrationId: string().min(1),
59388
+ keys: array(string().min(1))
59389
+ }), object({
59390
+ present: boolean(),
59391
+ removed: record(string(), unknown())
59392
+ }), {
59393
+ kind: "mutation",
59394
+ auth: "admin"
59263
59395
  });
59264
59396
  method(object({
59265
59397
  sourceUrl: string(),
@@ -80678,11 +80810,15 @@ discriminatedUnion("kind", [object({
80678
80810
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80679
80811
  * near F. A device that is not near one level most of the time has no floor.
80680
80812
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80681
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80682
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80813
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80814
+ * display waking): it is neither a run nor a pause. Among the remaining
80815
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80816
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80817
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80818
+ * is no run.
80683
80819
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80684
- * grid a person would type; stop dwell covers the longest pause; start dwell
80685
- * covers the longest standby blip above T.
80820
+ * grid a person would type; stop dwell covers the longest pause and the
80821
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80686
80822
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80687
80823
  * run must hold exactly one true interval and nothing may flap outside.
80688
80824
  */
@@ -80693,6 +80829,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80693
80829
  30 * SECOND_MS;
80694
80830
  60 * SECOND_MS;
80695
80831
  20 * MINUTE_MS$1;
80832
+ 90 * SECOND_MS;
80833
+ 30 * MINUTE_MS$1;
80834
+ 10 * MINUTE_MS$1;
80835
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80696
80836
  10 * SECOND_MS;
80697
80837
  10 * SECOND_MS;
80698
80838
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85977,13 +86117,6 @@ Object.freeze({
85977
86117
  access: "create",
85978
86118
  role: "admin"
85979
86119
  },
85980
- "integrations.clearSettings": {
85981
- capName: "integrations",
85982
- capScope: "system",
85983
- addonId: null,
85984
- access: "delete",
85985
- role: "admin"
85986
- },
85987
86120
  "integrations.create": {
85988
86121
  capName: "integrations",
85989
86122
  capScope: "system",
@@ -86019,13 +86152,6 @@ Object.freeze({
86019
86152
  access: "view",
86020
86153
  role: "admin"
86021
86154
  },
86022
- "integrations.getSettings": {
86023
- capName: "integrations",
86024
- capScope: "system",
86025
- addonId: null,
86026
- access: "view",
86027
- role: "admin"
86028
- },
86029
86155
  "integrations.list": {
86030
86156
  capName: "integrations",
86031
86157
  capScope: "system",
@@ -86033,13 +86159,6 @@ Object.freeze({
86033
86159
  access: "view",
86034
86160
  role: "admin"
86035
86161
  },
86036
- "integrations.setSettings": {
86037
- capName: "integrations",
86038
- capScope: "system",
86039
- addonId: null,
86040
- access: "create",
86041
- role: "admin"
86042
- },
86043
86162
  "integrations.testConnection": {
86044
86163
  capName: "integrations",
86045
86164
  capScope: "system",
@@ -87034,6 +87153,13 @@ Object.freeze({
87034
87153
  access: "view",
87035
87154
  role: "admin"
87036
87155
  },
87156
+ "migrations.clearLegacyIntegrationRows": {
87157
+ capName: "migrations",
87158
+ capScope: "system",
87159
+ addonId: null,
87160
+ access: "delete",
87161
+ role: "admin"
87162
+ },
87037
87163
  "migrations.declareOwner": {
87038
87164
  capName: "migrations",
87039
87165
  capScope: "system",
@@ -87041,6 +87167,13 @@ Object.freeze({
87041
87167
  access: "create",
87042
87168
  role: "admin"
87043
87169
  },
87170
+ "migrations.deleteLegacyIntegrationRowKeys": {
87171
+ capName: "migrations",
87172
+ capScope: "system",
87173
+ addonId: null,
87174
+ access: "delete",
87175
+ role: "admin"
87176
+ },
87044
87177
  "migrations.dryRun": {
87045
87178
  capName: "migrations",
87046
87179
  capScope: "system",
@@ -87076,6 +87209,20 @@ Object.freeze({
87076
87209
  access: "create",
87077
87210
  role: "admin"
87078
87211
  },
87212
+ "migrations.patchLegacyIntegrationRows": {
87213
+ capName: "migrations",
87214
+ capScope: "system",
87215
+ addonId: null,
87216
+ access: "create",
87217
+ role: "admin"
87218
+ },
87219
+ "migrations.readLegacyIntegrationRows": {
87220
+ capName: "migrations",
87221
+ capScope: "system",
87222
+ addonId: null,
87223
+ access: "view",
87224
+ role: "admin"
87225
+ },
87079
87226
  "migrations.restorePreMigration": {
87080
87227
  capName: "migrations",
87081
87228
  capScope: "system",
@@ -97450,7 +97597,7 @@ function gatewayKeyOf(integration) {
97450
97597
  return origin.length > 0 ? origin : `ecowitt:${slugifyHost(integration.id)}`;
97451
97598
  }
97452
97599
  /**
97453
- * 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
97454
97601
  * INTEGRATION, which carries the name. The `password` field is what puts the value in the keychain.
97455
97602
  */
97456
97603
  function buildConnectionFormSchema() {
@@ -105324,6 +105471,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
105324
105471
  if (records === void 0) throw new Error("provider-ecowitt: ctx.records is required (hub-only addon)");
105325
105472
  const rowsPorts = integrationRowsPortsOverApi({
105326
105473
  api: defaultIntegrationApiOver(this.ctx.api),
105474
+ secrets: this.ctx.secrets ?? null,
105327
105475
  records: records.kind({
105328
105476
  name: ecowittConnectionsEntryKind.name,
105329
105477
  declaration: ecowittConnectionsEntryKind.declaration,
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-D1jjWAr8.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 });
@@ -7927,10 +7953,9 @@ var BaseAddon = class {
7927
7953
  *
7928
7954
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
7929
7955
  * needs its connection fields, the default integration the addon's own sections. It is the
7930
- * SHAPE of the form only — the values live in `integrations.getSettings`, and the write path is
7931
- * unchanged (`integrations.setSettings` for an account integration).
7956
+ * SHAPE of the form only — the values live on the instance record.
7932
7957
  */
7933
- async integrationSettingsSchema(_instance) {
7958
+ async integrationFormSchema(_instance) {
7934
7959
  return null;
7935
7960
  }
7936
7961
  /**
@@ -7976,7 +8001,7 @@ var BaseAddon = class {
7976
8001
  * per-instance schema when it declares one, else its global schema (what they always showed).
7977
8002
  */
7978
8003
  async getIntegrationFormSchema(instance, nodeId) {
7979
- const scoped = await this.integrationSettingsSchema(instance);
8004
+ const scoped = await this.integrationFormSchema(instance);
7980
8005
  if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
7981
8006
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
7982
8007
  }
@@ -8024,6 +8049,7 @@ var BaseAddon = class {
8024
8049
  * resolved blob back is a no-op for every unchanged secret.
8025
8050
  */
8026
8051
  async resolveGlobalStore(nodeId, cap) {
8052
+ await this.loadScopedSchemas(cap);
8027
8053
  const raw = await this._ctx?.settings?.readAddonStore() ?? {};
8028
8054
  return this.projectForNode(raw, nodeId, cap);
8029
8055
  }
@@ -8034,6 +8060,7 @@ var BaseAddon = class {
8034
8060
  }
8035
8061
  /** The PROJECTED store with its markers — the form hydrate only (D798). */
8036
8062
  async readRawProjectedStore(nodeId, cap) {
8063
+ await this.loadScopedSchemas(cap);
8037
8064
  const raw = await this.rawSettings()?.readAddonStore() ?? {};
8038
8065
  return this.projectForNode(raw, nodeId, cap);
8039
8066
  }
@@ -8136,6 +8163,7 @@ var BaseAddon = class {
8136
8163
  await this.applyGlobalPatch(patch, nodeId);
8137
8164
  }
8138
8165
  async applyGlobalPatch(barePatch, nodeId) {
8166
+ await this.loadScopedSchemas();
8139
8167
  const keys = this.perNodeKeys();
8140
8168
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8141
8169
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
@@ -8149,7 +8177,8 @@ var BaseAddon = class {
8149
8177
  await this.resolveConfig();
8150
8178
  await this.onConfigChanged();
8151
8179
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8152
- 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)]);
8153
8182
  }
8154
8183
  /**
8155
8184
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8157,14 +8186,67 @@ var BaseAddon = class {
8157
8186
  * declarations). Empty set ⇒ every per-node code path is bypassed and the
8158
8187
  * settings API behaves exactly like the legacy node-agnostic one.
8159
8188
  */
8160
- _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
+ */
8161
8239
  perNodeKeys(cap) {
8162
8240
  const cacheKey = cap ?? "";
8163
- const cached = this._perNodeKeysCache.get(cacheKey);
8241
+ const cached = this._nodeKeysByCap.get(cacheKey);
8164
8242
  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);
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);
8168
8250
  return keys;
8169
8251
  }
8170
8252
  /**
@@ -8174,10 +8256,9 @@ var BaseAddon = class {
8174
8256
  * time to return its response before the addon is torn down and
8175
8257
  * re-initialised by `AddonRegistryService.restartAddon`.
8176
8258
  */
8177
- maybeAutoRestart(patchKeys, schema) {
8178
- if (!schema) return;
8259
+ maybeAutoRestart(patchKeys, schemas) {
8179
8260
  const restartKeys = /* @__PURE__ */ new Set();
8180
- 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) {
8181
8262
  if (field.type === "separator" || field.type === "info") continue;
8182
8263
  if (field.requiresRestart) restartKeys.add(field.key);
8183
8264
  }
@@ -8373,12 +8454,14 @@ var BaseAddon = class {
8373
8454
  * here, in the addon's own process, so the hub keeps no copy of it.
8374
8455
  */
8375
8456
  async getDeclaredSettingsKeys() {
8457
+ await this.loadScopedSchemas();
8376
8458
  return { keys: [...this.declaredKeySet()].toSorted() };
8377
8459
  }
8378
8460
  declaredKeySet() {
8379
8461
  const keys = new Set(Object.keys(this.defaults));
8380
8462
  const schema = this.globalSettingsSchema();
8381
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);
8382
8465
  for (const key of this._declaredStateKeys) keys.add(key);
8383
8466
  for (const path of this.secretStatePaths) {
8384
8467
  const root = path.split(/[.[]/, 1)[0];
@@ -8474,7 +8557,11 @@ var BaseAddon = class {
8474
8557
  }
8475
8558
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
8476
8559
  addonSecretTree() {
8477
- 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
+ }
8478
8565
  return this._addonSecretTree;
8479
8566
  }
8480
8567
  deviceSecretTree() {
@@ -8724,6 +8811,23 @@ function dropPerNodeFields(fields) {
8724
8811
  }
8725
8812
  return kept;
8726
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
+ }
8727
8831
  function collectFieldKeys(fields) {
8728
8832
  const collected = [];
8729
8833
  for (const field of fields) {
@@ -11950,7 +12054,7 @@ function userError(code, params, englishFallback) {
11950
12054
  return new CamstackError(code, params, englishFallback);
11951
12055
  }
11952
12056
  //#endregion
11953
- //#region ../types/dist/composition-CRNwcFYY.mjs
12057
+ //#region ../types/dist/composition-pOHbTzRB.mjs
11954
12058
  /**
11955
12059
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11956
12060
  *
@@ -13422,12 +13526,22 @@ var RecordIntegrationInstances = class {
13422
13526
  ...this.hooks.description,
13423
13527
  mintsId: true
13424
13528
  };
13425
- 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));
13426
13540
  listInstances = async () => {
13427
13541
  return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
13428
13542
  };
13429
13543
  getInstance = async ({ id }) => {
13430
- const record = await this.handle.get(id);
13544
+ const record = await this.handle.get(await this.resolveRecordId(id));
13431
13545
  return record === null ? null : this.viewOf(record);
13432
13546
  };
13433
13547
  /** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
@@ -13444,7 +13558,8 @@ var RecordIntegrationInstances = class {
13444
13558
  await this.hooks.onChanged?.(minted, "created");
13445
13559
  return { id: minted };
13446
13560
  };
13447
- updateInstance = async ({ id, name, values }) => {
13561
+ updateInstance = async ({ id: askedId, name, values }) => {
13562
+ const id = await this.resolveRecordId(askedId);
13448
13563
  const patch = {};
13449
13564
  for (const [key, value] of Object.entries(values)) {
13450
13565
  if (value === void 0) continue;
@@ -13479,7 +13594,10 @@ var RecordIntegrationInstances = class {
13479
13594
  status: "unsupported",
13480
13595
  error: "This integration declares no connection test"
13481
13596
  };
13482
- return this.hooks.test(input);
13597
+ return this.hooks.test(input.id === null ? input : {
13598
+ ...input,
13599
+ id: await this.resolveRecordId(input.id)
13600
+ });
13483
13601
  };
13484
13602
  /** The flat provider object to hand to `registerProvider`. */
13485
13603
  toProvider() {
@@ -14658,20 +14776,28 @@ async function moveIntegrationRowsToRecords(ports, spec) {
14658
14776
  };
14659
14777
  }
14660
14778
  /**
14661
- * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416): an addon runs the move
14662
- * from its own process, so the legacy rows are reached through the hub (`ctx.api.integrations`:
14663
- * `list`, `getSettings` - which answers the OWNER its secrets resolved - and `clearSettings`) and
14664
- * the new record through the addon's own records handle. The hub deletes the rows and their
14665
- * keychain values; the addon never touches the registry.
14779
+ * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416). An addon runs the move
14780
+ * from its own process, so the leftover rows are reached through the migration-only door
14781
+ * (`migrations.readLegacyIntegrationRows` / `clearLegacyIntegrationRows`) on the hub's connection.
14782
+ * The door returns markers. This process is the owner, so it resolves and releases them with
14783
+ * `resolveSecretMarkers` — the same call the old settings door used — and only then asks the
14784
+ * door to delete the rows. The addon's own record kind is its records handle.
14666
14785
  */
14786
+ async function resolvedRows(stored, secrets) {
14787
+ if (secrets === null) return stored;
14788
+ const { value, unresolved } = await resolveSecretMarkers(stored, secrets);
14789
+ if (unresolved.length > 0) throw new Error(`${unresolved.length} secret(s) cannot be read from the keychain`);
14790
+ return value;
14791
+ }
14667
14792
  function integrationRowsPortsOverApi(deps) {
14793
+ const secrets = deps.secrets ?? null;
14668
14794
  return {
14669
14795
  warn: (message) => deps.log.warn(message),
14670
14796
  listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
14671
14797
  id: i.id,
14672
14798
  name: i.name
14673
14799
  })),
14674
- readRows: (integrationId) => deps.api.getSettings({ id: integrationId }),
14800
+ readRows: async (integrationId) => resolvedRows(await deps.api.readLegacyIntegrationRows({ integrationId }), secrets),
14675
14801
  putRecord: async (id, body) => {
14676
14802
  if (await deps.records.get(id) !== null) return "exists";
14677
14803
  try {
@@ -14690,15 +14816,17 @@ function integrationRowsPortsOverApi(deps) {
14690
14816
  await deps.records.delete(id);
14691
14817
  },
14692
14818
  clearRows: async (integrationId) => {
14693
- await deps.api.clearSettings({ id: integrationId });
14819
+ const refs = collectSecretMarkerRefs(await deps.api.readLegacyIntegrationRows({ integrationId }));
14820
+ if (refs.length > 0 && secrets !== null) await secrets.delete(refs);
14821
+ await deps.api.clearLegacyIntegrationRows({ integrationId });
14694
14822
  }
14695
14823
  };
14696
14824
  }
14697
14825
  function defaultIntegrationApiOver(api) {
14698
14826
  return {
14699
14827
  list: () => api.integrations.list.query(),
14700
- getSettings: (input) => api.integrations.getSettings.query(input),
14701
- clearSettings: (input) => api.integrations.clearSettings.mutate(input),
14828
+ readLegacyIntegrationRows: (input) => api.migrations.readLegacyIntegrationRows.query(input),
14829
+ clearLegacyIntegrationRows: (input) => api.migrations.clearLegacyIntegrationRows.mutate(input),
14702
14830
  listDevices: (addonId) => api.deviceManager.listAll.query({
14703
14831
  addonId,
14704
14832
  projection: "slim"
@@ -50376,6 +50504,9 @@ var RunningProposalEvidenceSchema = object({
50376
50504
  runsFound: number().int().nonnegative(),
50377
50505
  pausesFound: number().int().nonnegative(),
50378
50506
  longestPauseMs: number().nonnegative(),
50507
+ /** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
50508
+ pulsesFound: number().int().nonnegative(),
50509
+ longestPulseMs: number().nonnegative(),
50379
50510
  idleFloorW: number(),
50380
50511
  noiseCeilingW: number(),
50381
50512
  plateauW: number(),
@@ -56242,11 +56373,7 @@ var CreateIntegrationInputSchema = object({
56242
56373
  info: record(string(), unknown()).optional(),
56243
56374
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
56244
56375
  originStableId: string().optional(),
56245
- settings: record(string(), unknown()).optional(),
56246
- /** Keys of `settings` the form's schema declares secret (by type) — kept in
56247
- * the keychain, not the table (D787). */
56248
- secretKeys: array(string()).optional(),
56249
- skipRestart: boolean().optional()
56376
+ settings: record(string(), unknown()).optional()
56250
56377
  });
56251
56378
  var UpdateIntegrationInputSchema = object({
56252
56379
  id: string(),
@@ -56267,22 +56394,6 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56267
56394
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
56268
56395
  kind: "mutation",
56269
56396
  auth: "admin"
56270
- }), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
56271
- id: string(),
56272
- settings: record(string(), unknown()),
56273
- /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
56274
- secretKeys: array(string()).optional(),
56275
- /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
56276
- skipRestart: boolean().optional()
56277
- }), object({ success: boolean() }), {
56278
- kind: "mutation",
56279
- auth: "admin"
56280
- }), method(object({ id: string() }), object({
56281
- rows: number().int().nonnegative(),
56282
- secrets: number().int().nonnegative()
56283
- }), {
56284
- kind: "mutation",
56285
- auth: "admin"
56286
56397
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56287
56398
  addonId: string(),
56288
56399
  settings: record(string(), unknown())
@@ -59259,6 +59370,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
59259
59370
  }), {
59260
59371
  kind: "mutation",
59261
59372
  auth: "admin"
59373
+ }), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
59374
+ rows: number().int().nonnegative(),
59375
+ secrets: number().int().nonnegative()
59376
+ }), {
59377
+ kind: "mutation",
59378
+ auth: "admin"
59379
+ }), method(object({
59380
+ integrationId: string().min(1),
59381
+ patch: record(string(), unknown())
59382
+ }), object({ written: boolean() }), {
59383
+ kind: "mutation",
59384
+ auth: "admin"
59385
+ }), method(object({
59386
+ integrationId: string().min(1),
59387
+ keys: array(string().min(1))
59388
+ }), object({
59389
+ present: boolean(),
59390
+ removed: record(string(), unknown())
59391
+ }), {
59392
+ kind: "mutation",
59393
+ auth: "admin"
59262
59394
  });
59263
59395
  method(object({
59264
59396
  sourceUrl: string(),
@@ -80677,11 +80809,15 @@ discriminatedUnion("kind", [object({
80677
80809
  * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
80678
80810
  * near F. A device that is not near one level most of the time has no floor.
80679
80811
  * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
80680
- * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
80681
- * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
80812
+ * 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
80813
+ * display waking): it is neither a run nor a pause. Among the remaining
80814
+ * episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
80815
+ * two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
80816
+ * a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
80817
+ * is no run.
80682
80818
  * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
80683
- * grid a person would type; stop dwell covers the longest pause; start dwell
80684
- * covers the longest standby blip above T.
80819
+ * grid a person would type; stop dwell covers the longest pause and the
80820
+ * kind's floor; start dwell covers the longest pulse and standby blip above T.
80685
80821
  * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
80686
80822
  * run must hold exactly one true interval and nothing may flap outside.
80687
80823
  */
@@ -80692,6 +80828,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
80692
80828
  30 * SECOND_MS;
80693
80829
  60 * SECOND_MS;
80694
80830
  20 * MINUTE_MS$1;
80831
+ 90 * SECOND_MS;
80832
+ 30 * MINUTE_MS$1;
80833
+ 10 * MINUTE_MS$1;
80834
+ DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
80695
80835
  10 * SECOND_MS;
80696
80836
  10 * SECOND_MS;
80697
80837
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
@@ -85976,13 +86116,6 @@ Object.freeze({
85976
86116
  access: "create",
85977
86117
  role: "admin"
85978
86118
  },
85979
- "integrations.clearSettings": {
85980
- capName: "integrations",
85981
- capScope: "system",
85982
- addonId: null,
85983
- access: "delete",
85984
- role: "admin"
85985
- },
85986
86119
  "integrations.create": {
85987
86120
  capName: "integrations",
85988
86121
  capScope: "system",
@@ -86018,13 +86151,6 @@ Object.freeze({
86018
86151
  access: "view",
86019
86152
  role: "admin"
86020
86153
  },
86021
- "integrations.getSettings": {
86022
- capName: "integrations",
86023
- capScope: "system",
86024
- addonId: null,
86025
- access: "view",
86026
- role: "admin"
86027
- },
86028
86154
  "integrations.list": {
86029
86155
  capName: "integrations",
86030
86156
  capScope: "system",
@@ -86032,13 +86158,6 @@ Object.freeze({
86032
86158
  access: "view",
86033
86159
  role: "admin"
86034
86160
  },
86035
- "integrations.setSettings": {
86036
- capName: "integrations",
86037
- capScope: "system",
86038
- addonId: null,
86039
- access: "create",
86040
- role: "admin"
86041
- },
86042
86161
  "integrations.testConnection": {
86043
86162
  capName: "integrations",
86044
86163
  capScope: "system",
@@ -87033,6 +87152,13 @@ Object.freeze({
87033
87152
  access: "view",
87034
87153
  role: "admin"
87035
87154
  },
87155
+ "migrations.clearLegacyIntegrationRows": {
87156
+ capName: "migrations",
87157
+ capScope: "system",
87158
+ addonId: null,
87159
+ access: "delete",
87160
+ role: "admin"
87161
+ },
87036
87162
  "migrations.declareOwner": {
87037
87163
  capName: "migrations",
87038
87164
  capScope: "system",
@@ -87040,6 +87166,13 @@ Object.freeze({
87040
87166
  access: "create",
87041
87167
  role: "admin"
87042
87168
  },
87169
+ "migrations.deleteLegacyIntegrationRowKeys": {
87170
+ capName: "migrations",
87171
+ capScope: "system",
87172
+ addonId: null,
87173
+ access: "delete",
87174
+ role: "admin"
87175
+ },
87043
87176
  "migrations.dryRun": {
87044
87177
  capName: "migrations",
87045
87178
  capScope: "system",
@@ -87075,6 +87208,20 @@ Object.freeze({
87075
87208
  access: "create",
87076
87209
  role: "admin"
87077
87210
  },
87211
+ "migrations.patchLegacyIntegrationRows": {
87212
+ capName: "migrations",
87213
+ capScope: "system",
87214
+ addonId: null,
87215
+ access: "create",
87216
+ role: "admin"
87217
+ },
87218
+ "migrations.readLegacyIntegrationRows": {
87219
+ capName: "migrations",
87220
+ capScope: "system",
87221
+ addonId: null,
87222
+ access: "view",
87223
+ role: "admin"
87224
+ },
87078
87225
  "migrations.restorePreMigration": {
87079
87226
  capName: "migrations",
87080
87227
  capScope: "system",
@@ -97449,7 +97596,7 @@ function gatewayKeyOf(integration) {
97449
97596
  return origin.length > 0 ? origin : `ecowitt:${slugifyHost(integration.id)}`;
97450
97597
  }
97451
97598
  /**
97452
- * 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
97453
97600
  * INTEGRATION, which carries the name. The `password` field is what puts the value in the keychain.
97454
97601
  */
97455
97602
  function buildConnectionFormSchema() {
@@ -105323,6 +105470,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
105323
105470
  if (records === void 0) throw new Error("provider-ecowitt: ctx.records is required (hub-only addon)");
105324
105471
  const rowsPorts = integrationRowsPortsOverApi({
105325
105472
  api: defaultIntegrationApiOver(this.ctx.api),
105473
+ secrets: this.ctx.secrets ?? null,
105326
105474
  records: records.kind({
105327
105475
  name: ecowittConnectionsEntryKind.name,
105328
105476
  declaration: ecowittConnectionsEntryKind.declaration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-ecowitt",
3
- "version": "0.2.155",
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.765"
66
+ "server": ">=1.2.777"
67
67
  }
68
68
  },
69
69
  "files": [