@camstack/addon-provider-reolink 1.2.221 → 1.2.222

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/dist/addon.js CHANGED
@@ -106544,7 +106544,7 @@ var ZodIssueCode = {
106544
106544
  var ZodFirstPartyTypeKind;
106545
106545
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
106546
106546
  //#endregion
106547
- //#region ../types/dist/sleep-BKClVk0K.mjs
106547
+ //#region ../types/dist/sleep-Cv4Zmie9.mjs
106548
106548
  /**
106549
106549
  * The audio chunk plane's byte format, and the ONE expansion from a coded
106550
106550
  * window to float samples (D455).
@@ -107141,7 +107141,7 @@ var NOT_RUN = {
107141
107141
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
107142
107142
  * because the hub (and this addon's boot) must not hang on a migration.
107143
107143
  */
107144
- async function runAddonMigrations(host, migrations, logger) {
107144
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
107145
107145
  const first = migrations[0];
107146
107146
  if (first === void 0) return NOT_RUN;
107147
107147
  const owner = first.meta.owner;
@@ -107170,7 +107170,9 @@ async function runAddonMigrations(host, migrations, logger) {
107170
107170
  else if (status === "blocked") blocked.push(id);
107171
107171
  else skipped.push(id);
107172
107172
  };
107173
+ const selected = options.only === void 0 ? null : new Set(options.only);
107173
107174
  for (const step of declaration.steps) {
107175
+ if (selected !== null && !selected.has(step.id)) continue;
107174
107176
  const bound = byId.get(step.id);
107175
107177
  if (bound === void 0 || step.action === "skip") {
107176
107178
  skipped.push(step.id);
@@ -108288,14 +108290,18 @@ var BaseAddon = class {
108288
108290
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
108289
108291
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
108290
108292
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
108293
+ *
108294
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
108295
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
108296
+ * omitted applied ids "newer").
108291
108297
  */
108292
- async runMigrations(migrations) {
108298
+ async runMigrations(migrations, options) {
108293
108299
  const api = this.ctx.api.migrations;
108294
108300
  const report = await runAddonMigrations({
108295
108301
  declareOwner: (input) => api.declareOwner.mutate(input),
108296
108302
  startMigration: (input) => api.startMigration.mutate(input),
108297
108303
  finishMigration: (input) => api.finishMigration.mutate(input)
108298
- }, migrations, this.ctx.logger);
108304
+ }, migrations, this.ctx.logger, options);
108299
108305
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
108300
108306
  return report;
108301
108307
  }
@@ -108636,6 +108642,10 @@ var BaseAddon = class {
108636
108642
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
108637
108643
  const patch = {};
108638
108644
  for (const key of keys) {
108645
+ if (tree.get(key)?.kind === "array") {
108646
+ patch[key] = [];
108647
+ continue;
108648
+ }
108639
108649
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
108640
108650
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
108641
108651
  }
@@ -109362,7 +109372,8 @@ function isZodSecret(schema) {
109362
109372
  }
109363
109373
  /**
109364
109374
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
109365
- * element of this array" (`connection.password`, `rows[].token`). A declared
109375
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
109376
+ * map" (`applianceSecrets{}`). A declared
109366
109377
  * object or array is reported as one path and not descended into — everything
109367
109378
  * under it is secret.
109368
109379
  */
@@ -109373,6 +109384,7 @@ function collectZodSecretPaths(schema, prefix = "") {
109373
109384
  const def = defOf(layer);
109374
109385
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
109375
109386
  else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
109387
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
109376
109388
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
109377
109389
  else if (def.type === "intersection") {
109378
109390
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -112251,7 +112263,7 @@ function sleep(ms) {
112251
112263
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
112252
112264
  }
112253
112265
  //#endregion
112254
- //#region ../types/dist/composition-BodkvOV0.mjs
112266
+ //#region ../types/dist/composition-CRNwcFYY.mjs
112255
112267
  /**
112256
112268
  * The ffmpeg build every CamStack node downloads, per platform and arch.
112257
112269
  *
@@ -113014,38 +113026,6 @@ function validateExpressionSource(src) {
113014
113026
  return null;
113015
113027
  }
113016
113028
  /**
113017
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
113018
- *
113019
- * Standby of the control board is under 5 W; a motor or a heater is far
113020
- * above that, so 5 W is the line. A door lock or a short pump spike is not
113021
- * a cycle, so running starts only after 30 s above the line.
113022
- *
113023
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
113024
- * bursts) lasts several minutes. Ending the cycle on the first dip would
113025
- * report "finished" in the middle of the program, so the stop dwell is
113026
- * 10 minutes on every type. The number is stored per type so one machine
113027
- * can move later without moving the others.
113028
- */
113029
- var APPLIANCE_TEMPLATE_ID = "appliance";
113030
- var APPLIANCE_KINDS = [
113031
- "washer",
113032
- "dryer",
113033
- "dishwasher"
113034
- ];
113035
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
113036
- /**
113037
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
113038
- *
113039
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
113040
- * (D967), so the heater switches exactly as the plug does. Running is the
113041
- * plug being on; when a power reading is also named, running is the draw
113042
- * staying above the line instead (a plug left on with the stove's own
113043
- * thermostat open is not heating). 50 W is far above any standby and below
113044
- * the smallest radiator.
113045
- */
113046
- var HEATER_TEMPLATE_ID = "heater";
113047
- DeviceType.Heater;
113048
- /**
113049
113029
  * Statistics and integrals over a history read (D944).
113050
113030
  *
113051
113031
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -113189,34 +113169,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
113189
113169
  /** Actuator groups only. Readings leave this unset. */
113190
113170
  active: ActiveCriterionSchema.optional()
113191
113171
  }),
113192
- object({
113193
- kind: literal("appliance"),
113194
- templateId: literal(APPLIANCE_TEMPLATE_ID),
113195
- appliance: _enum(APPLIANCE_KINDS),
113196
- thresholdWatts: number().positive(),
113197
- startDwellMs: number().int().nonnegative(),
113198
- stopDwellMs: number().int().nonnegative(),
113199
- member: HelperMemberSchema,
113200
- /**
113201
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
113202
- * composition carries a `power-source` feature, so the hub can cut and restore
113203
- * that plug (power-source link of a NEW-target composition).
113204
- */
113205
- powerSwitch: HelperMemberSchema.optional(),
113206
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
113207
- offSeconds: number().optional()
113208
- }),
113209
- object({
113210
- kind: literal("heater"),
113211
- templateId: literal(HEATER_TEMPLATE_ID),
113212
- /** The plug, read and commanded at `switch.on`. */
113213
- plug: HelperMemberSchema,
113214
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
113215
- meter: HelperMemberSchema.optional(),
113216
- thresholdWatts: number().positive(),
113217
- startDwellMs: number().int().nonnegative(),
113218
- stopDwellMs: number().int().nonnegative()
113219
- }),
113220
113172
  object({
113221
113173
  kind: literal("statistic"),
113222
113174
  stat: _enum(STATISTIC_STATS),
@@ -113440,7 +113392,7 @@ var CompositionSchema = object({
113440
113392
  kind: literal("existing"),
113441
113393
  device: CompositionSourceRefSchema
113442
113394
  })]),
113443
- features: array(CompositionFeatureSchema).min(1).max(16),
113395
+ features: array(CompositionFeatureSchema).min(0).max(16),
113444
113396
  /**
113445
113397
  * Present when the block is a helper template. Absent on a free
113446
113398
  * composition (the room example, device 4603). The editor reads this,
@@ -114744,7 +114696,13 @@ var MigrationLedgerEntrySchema = object({
114744
114696
  * joined by the engine from the catalogues it knows this boot. Absent when no
114745
114697
  * catalogue lists the id (a newer database).
114746
114698
  */
114747
- meta: MigrationMetaSchema.optional()
114699
+ meta: MigrationMetaSchema.optional(),
114700
+ /**
114701
+ * Set when this is an applied row of an addon owner whose catalogue the answering
114702
+ * process has not loaded (the owner has not declared yet): the row is applied, the
114703
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
114704
+ */
114705
+ ownerNotLoaded: boolean().optional()
114748
114706
  });
114749
114707
  /** A catalogued migration that is not applied, with where it stands. */
114750
114708
  var MigrationPendingEntrySchema = object({
@@ -114777,6 +114735,146 @@ var MigrationOwnerStepSchema = object({
114777
114735
  /** The ledger state the engine found: `pending` has no row yet. */
114778
114736
  state: union([literal("pending"), MigrationStatusSchema])
114779
114737
  });
114738
+ function ordinalOf(id) {
114739
+ const digits = id.replace(/\D+/g, "");
114740
+ return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
114741
+ }
114742
+ /** The lowest-numbered eligible integration, or null. */
114743
+ function defaultIntegrationOf(integrations, eligible) {
114744
+ return [...eligible === void 0 ? integrations : integrations.filter(eligible)].sort((a, b) => ordinalOf(a.id) - ordinalOf(b.id) || (a.id < b.id ? -1 : 1))[0] ?? null;
114745
+ }
114746
+ function unstamped(device) {
114747
+ return device.integrationId === void 0 || device.integrationId === null || device.integrationId === "";
114748
+ }
114749
+ /**
114750
+ * An allocation reservation is not a device (D376): `allocateDeviceId` writes a row of type
114751
+ * `generic`, named by its stable id, that is not registered and holds no configuration. The hub
114752
+ * listing does not carry the `registered` bit, so the shape picks the candidates and the missing
114753
+ * configuration decides.
114754
+ */
114755
+ async function isReservation(ports, device) {
114756
+ if (device.type !== "generic" || device.name === void 0 || device.name !== device.stableId) return false;
114757
+ return !await ports.hasConfig(device.id);
114758
+ }
114759
+ async function claimDevicesToDefaultIntegration(ports, spec) {
114760
+ const all = await ports.listDevices(spec.addonId);
114761
+ const byId = new Map(all.map((d) => [d.id, d]));
114762
+ const reservations = /* @__PURE__ */ new Set();
114763
+ for (const device of all) if (unstamped(device) && await isReservation(ports, device)) reservations.add(device.id);
114764
+ const orphans = all.filter((d) => unstamped(d) && !reservations.has(d.id));
114765
+ const summaryBase = { reservations: reservations.size };
114766
+ if (orphans.length === 0) return {
114767
+ outcome: "already-done",
114768
+ summary: {
114769
+ claimed: 0,
114770
+ unresolved: 0,
114771
+ ...summaryBase
114772
+ }
114773
+ };
114774
+ const fallback = defaultIntegrationOf(await ports.listIntegrations(spec.addonId), spec.eligible);
114775
+ const plan = /* @__PURE__ */ new Map();
114776
+ const unresolved = /* @__PURE__ */ new Set();
114777
+ const failed = /* @__PURE__ */ new Set();
114778
+ const resolve = (device, seen) => {
114779
+ if (!unstamped(device)) return device.integrationId ?? null;
114780
+ const planned = plan.get(device.id);
114781
+ if (planned !== void 0) return planned;
114782
+ if (failed.has(device.id)) return null;
114783
+ let target = null;
114784
+ const parentId = device.parentDeviceId ?? null;
114785
+ if (parentId === null) target = fallback?.id ?? null;
114786
+ else {
114787
+ const parent = byId.get(parentId);
114788
+ if (parent !== void 0 && !reservations.has(parentId) && !seen.has(parentId)) target = resolve(parent, new Set([...seen, device.id]));
114789
+ }
114790
+ if (target === null) failed.add(device.id);
114791
+ else plan.set(device.id, target);
114792
+ return target;
114793
+ };
114794
+ if (orphans.some((d) => (d.parentDeviceId ?? null) === null) && fallback === null) throw new Error(`${orphans.length} device(s) of '${spec.addonId}' carry no integration and the addon has no integration to claim them to`);
114795
+ for (const device of orphans) if (resolve(device, new Set([device.id])) === null) unresolved.add(device.id);
114796
+ for (const id of unresolved) ports.warnDevice(`device ${id} of '${spec.addonId}' carries no integration and its owner chain does not resolve to one: left unstamped`, id);
114797
+ for (const [deviceId, integrationId] of plan) await ports.setIntegrationId(deviceId, integrationId);
114798
+ const wrong = [];
114799
+ for (const [deviceId, integrationId] of plan) if (await ports.readIntegrationId(deviceId) !== integrationId) wrong.push(deviceId);
114800
+ if (wrong.length > 0) throw new Error(`device(s) ${wrong.join(", ")} did not read back their integration after the claim`);
114801
+ const claimed = plan.size;
114802
+ ports.info(`${claimed} device(s) of '${spec.addonId}' claimed to their owner's integration or the default; ${unresolved.size} unresolved, ${reservations.size} reservation(s) skipped`);
114803
+ return {
114804
+ outcome: claimed > 0 ? "done" : "already-done",
114805
+ summary: {
114806
+ claimed,
114807
+ unresolved: unresolved.size,
114808
+ ...summaryBase
114809
+ }
114810
+ };
114811
+ }
114812
+ /** The hub side of the device claim, over `ctx.api`. */
114813
+ function claimDevicesPortsOver(deps) {
114814
+ return {
114815
+ listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
114816
+ id: i.id,
114817
+ name: i.name,
114818
+ info: i.info ?? {}
114819
+ })),
114820
+ listDevices: (addonId) => deps.api.listDevices(addonId),
114821
+ hasConfig: (deviceId) => deps.api.readDeviceHasConfig(deviceId),
114822
+ setIntegrationId: (deviceId, integrationId) => deps.api.setDeviceIntegrationId(deviceId, integrationId),
114823
+ readIntegrationId: (deviceId) => deps.api.readDeviceIntegrationId(deviceId),
114824
+ info: deps.info,
114825
+ warnDevice: deps.warnDevice
114826
+ };
114827
+ }
114828
+ function defaultIntegrationMigrationIds(addonId, date) {
114829
+ return {
114830
+ rowsToRecord: `${addonId}/${date}-integration-rows-to-record`,
114831
+ claimDevices: `${addonId}/${date}-claim-devices-to-default-integration`
114832
+ };
114833
+ }
114834
+ /**
114835
+ * The device claim alone, for an addon that keeps its integration settings where they are for now
114836
+ * (its own wave moves them) but whose persisted devices must name their default integration.
114837
+ */
114838
+ function claimDevicesMigration(spec) {
114839
+ return {
114840
+ meta: {
114841
+ id: defaultIntegrationMigrationIds(spec.addonId, spec.date).claimDevices,
114842
+ owner: spec.addonId,
114843
+ level: "addon-state",
114844
+ seq: spec.seq,
114845
+ dependsOn: [...spec.dependsOn],
114846
+ store: "settings",
114847
+ scope: "cluster",
114848
+ onFailure: "tolerate",
114849
+ bodyKind: "procedure",
114850
+ description: "Claim the persisted devices that carry no integration to the addon's default integration, keeping their device and stable ids",
114851
+ fixtureMark: "a device config field the claim does not know"
114852
+ },
114853
+ procedure: (ports) => claimDevicesToDefaultIntegration(ports, {
114854
+ addonId: spec.addonId,
114855
+ ...spec.eligible !== void 0 ? { eligible: spec.eligible } : {}
114856
+ })
114857
+ };
114858
+ }
114859
+ function defaultIntegrationApiOver(api) {
114860
+ return {
114861
+ list: () => api.integrations.list.query(),
114862
+ getSettings: (input) => api.integrations.getSettings.query(input),
114863
+ clearSettings: (input) => api.integrations.clearSettings.mutate(input),
114864
+ listDevices: (addonId) => api.deviceManager.listAll.query({
114865
+ addonId,
114866
+ projection: "slim"
114867
+ }),
114868
+ setDeviceIntegrationId: async (deviceId, integrationId) => {
114869
+ await api.deviceManager.setIntegrationId.mutate({
114870
+ deviceId,
114871
+ integrationId
114872
+ });
114873
+ },
114874
+ readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
114875
+ readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
114876
+ };
114877
+ }
114780
114878
  /**
114781
114879
  * The trainer contract — what the hub hands the cloud trainer and what it
114782
114880
  * gets back (cloud training, D805+).
@@ -150752,117 +150850,6 @@ function actuator(input) {
150752
150850
  };
150753
150851
  }
150754
150852
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
150755
- var PowerSourceRefusalReasonSchema = _enum([
150756
- "no-power-source",
150757
- "switch-missing",
150758
- "switch-offline",
150759
- "switch-unknown",
150760
- "restart-in-progress",
150761
- "switch-call-failed"
150762
- ]);
150763
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
150764
- accepted: literal(false),
150765
- reason: PowerSourceRefusalReasonSchema,
150766
- /** One line for the operator, naming the switch. */
150767
- message: string()
150768
- })]);
150769
- var PowerSourceInfoSchema = object({
150770
- /** False when the device has no power source linked; the rest is then empty. */
150771
- linked: boolean(),
150772
- switchDeviceId: number().nullable(),
150773
- switchName: string().nullable(),
150774
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
150775
- switchOn: boolean().nullable(),
150776
- switchOnline: boolean().nullable(),
150777
- restartInProgress: boolean(),
150778
- offSeconds: number().int()
150779
- });
150780
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
150781
- var PowerSourceStateSchema = object({
150782
- /** Null while the switch has not reported: unknown is never `false` (D393). */
150783
- switchOn: boolean().nullable(),
150784
- offSeconds: number().int(),
150785
- lastFetchedAt: number()
150786
- });
150787
- var POWER_SOURCE_DEVICE_TYPES = [
150788
- DeviceType.Gateway,
150789
- DeviceType.Hub,
150790
- DeviceType.Camera,
150791
- DeviceType.Sensor,
150792
- DeviceType.MediaPlayer,
150793
- DeviceType.Vacuum,
150794
- DeviceType.Generic,
150795
- DeviceType.Washer,
150796
- DeviceType.Dryer,
150797
- DeviceType.Dishwasher,
150798
- DeviceType.PetFountain,
150799
- DeviceType.Fan,
150800
- DeviceType.Humidifier,
150801
- DeviceType.Climate,
150802
- DeviceType.WaterHeater
150803
- ];
150804
- var powerSourceCustomize = {
150805
- version: 1,
150806
- targets: [{
150807
- id: "switch",
150808
- cardinality: "scalar",
150809
- vocabulary: {
150810
- cap: "power-source",
150811
- stateField: "switchOn"
150812
- },
150813
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
150814
- apply: "add",
150815
- path: "switchOn",
150816
- sources: [{
150817
- kind: "switch",
150818
- transforms: [{ kind: "identity" }]
150819
- }],
150820
- companions: [{
150821
- kind: "constant",
150822
- path: "offSeconds",
150823
- value: 10,
150824
- when: "add"
150825
- }, {
150826
- kind: "clock",
150827
- path: "lastFetchedAt",
150828
- when: "always"
150829
- }],
150830
- sourceReport: { status: "assignment-status" }
150831
- }]
150832
- };
150833
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
150834
- var powerSourceCapability = {
150835
- name: "power-source",
150836
- scope: "device",
150837
- mode: "singleton",
150838
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
150839
- methods: {
150840
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
150841
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
150842
- kind: "mutation",
150843
- auth: "admin"
150844
- }),
150845
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
150846
- kind: "mutation",
150847
- auth: "admin"
150848
- }),
150849
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
150850
- kind: "mutation",
150851
- auth: "admin"
150852
- })
150853
- },
150854
- runtimeState: PowerSourceStateSchema,
150855
- customize: powerSourceCustomize,
150856
- /**
150857
- * Runtime-state durability: **session** — mirrored from the linked switch on
150858
- * its next report; a restored value would outlive the reading it names.
150859
- *
150860
- * See `RuntimeStateDurability`. Enforced by
150861
- * `scripts/check-runtime-state-durability.ts`.
150862
- */
150863
- durability: "session",
150864
- history: "none"
150865
- };
150866
150853
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
150867
150854
  DeviceType.Camera;
150868
150855
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -150890,6 +150877,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
150890
150877
  }).optional()
150891
150878
  })) });
150892
150879
  /**
150880
+ * A saved Customize assignment (D936). The schema is the type: callers use
150881
+ * `z.infer`, they do not restate the shape.
150882
+ */
150883
+ var OverrideSourceSchema = object({
150884
+ addonId: string().min(1),
150885
+ stableId: string().min(1),
150886
+ cap: string().min(1),
150887
+ fieldPath: string().min(1)
150888
+ });
150889
+ var IdentityTransformDraftSchema = object({ kind: literal("identity") });
150890
+ var InvertTransformDraftSchema = object({ kind: literal("invert") });
150891
+ var ThresholdTransformDraftSchema = object({
150892
+ kind: literal("threshold"),
150893
+ comparison: _enum(["gte", "lte"]),
150894
+ threshold: number(),
150895
+ startDwellMs: number().nonnegative(),
150896
+ stopDwellMs: number().nonnegative()
150897
+ });
150898
+ var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
150899
+ IdentityTransformDraftSchema,
150900
+ InvertTransformDraftSchema,
150901
+ ThresholdTransformDraftSchema,
150902
+ object({
150903
+ kind: literal("linear"),
150904
+ scale: number(),
150905
+ offset: number(),
150906
+ clampMin: number().optional(),
150907
+ clampMax: number().optional()
150908
+ }),
150909
+ object({
150910
+ kind: literal("enum-map"),
150911
+ mapping: record(string(), string().nullable())
150912
+ }),
150913
+ object({ kind: literal("number") })
150914
+ ]);
150915
+ var ScalarOverrideDraftSchema = object({
150916
+ kind: literal("scalar"),
150917
+ cap: string().min(1),
150918
+ targetId: string().min(1),
150919
+ source: OverrideSourceSchema,
150920
+ transform: CustomizeTransformDraftSchema
150921
+ });
150922
+ var KeyedArrayOverrideDraftSchema = object({
150923
+ kind: literal("keyedArray"),
150924
+ cap: string().min(1),
150925
+ targetId: string().min(1),
150926
+ key: string().min(1),
150927
+ label: string().min(1),
150928
+ fieldPath: string().min(1),
150929
+ source: OverrideSourceSchema,
150930
+ transform: CustomizeTransformDraftSchema
150931
+ });
150932
+ var PulseOverrideDraftSchema = object({
150933
+ kind: literal("pulse"),
150934
+ cap: string().min(1),
150935
+ targetId: string().min(1),
150936
+ enabled: boolean(),
150937
+ debounceMs: number().nonnegative(),
150938
+ edges: array(_enum([
150939
+ "rising",
150940
+ "button",
150941
+ "native-event"
150942
+ ])).min(1).optional(),
150943
+ sources: array(OverrideSourceSchema).min(1)
150944
+ });
150945
+ var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
150946
+ "in-addition",
150947
+ "confirm",
150948
+ "replace"
150949
+ ]).optional() });
150950
+ discriminatedUnion("kind", [
150951
+ ScalarOverrideDraftSchema,
150952
+ KeyedArrayOverrideDraftSchema,
150953
+ PulseOverrideDraftSchema,
150954
+ object({
150955
+ kind: literal("span"),
150956
+ cap: string().min(1),
150957
+ targetId: string().min(1),
150958
+ timeoutMs: number().nonnegative(),
150959
+ cooldownMs: number().nonnegative().optional(),
150960
+ sources: array(SpanSourceDraftSchema)
150961
+ })
150962
+ ]);
150963
+ object({
150964
+ targetId: string().min(1),
150965
+ configured: boolean(),
150966
+ source: _enum(["native", "assigned"]),
150967
+ state: _enum([
150968
+ "active",
150969
+ "stale",
150970
+ "released",
150971
+ "unavailable"
150972
+ ]),
150973
+ reason: string().nullable()
150974
+ });
150975
+ /**
150976
+ * What a running-threshold proposal says (planning#405). The schema is the
150977
+ * type: the algorithm, the capability method and the UI all read `z.infer`.
150978
+ *
150979
+ * A proposal is read-only advice: it carries the numbers it was derived from
150980
+ * (`evidence`) so the operator can judge it, and an `insufficient` answer names
150981
+ * WHY with a typed reason and the catalog defaults, never an invented number.
150982
+ */
150983
+ var RunningProposalTierSchema = _enum(["raw", "5m"]);
150984
+ var RunningProposalConfidenceSchema = _enum([
150985
+ "high",
150986
+ "medium",
150987
+ "low"
150988
+ ]);
150989
+ var RunningProposalInsufficientReasonSchema = _enum([
150990
+ "no-history",
150991
+ "span-too-short",
150992
+ "truncated",
150993
+ "no-idle-floor",
150994
+ "no-runs",
150995
+ "no-separation"
150996
+ ]);
150997
+ var RunningProposalReplaySchema = object({
150998
+ /** Intervals the real dwell expression held true over the window. */
150999
+ intervals: number().int().nonnegative(),
151000
+ /** Intervals outside every run, or a second interval inside one. */
151001
+ extra: number().int().nonnegative(),
151002
+ /** Runs without an interval of their own (none, or merged into a neighbour). */
151003
+ missed: number().int().nonnegative()
151004
+ });
151005
+ var RunningProposalEvidenceSchema = object({
151006
+ tier: RunningProposalTierSchema,
151007
+ spanMs: number().nonnegative(),
151008
+ from: number(),
151009
+ to: number(),
151010
+ points: number().int().nonnegative(),
151011
+ runsFound: number().int().nonnegative(),
151012
+ pausesFound: number().int().nonnegative(),
151013
+ longestPauseMs: number().nonnegative(),
151014
+ idleFloorW: number(),
151015
+ noiseCeilingW: number(),
151016
+ plateauW: number(),
151017
+ replay: RunningProposalReplaySchema
151018
+ });
151019
+ var RunningProposalInsufficientEvidenceSchema = object({
151020
+ spanMs: number().nonnegative(),
151021
+ points: number().int().nonnegative(),
151022
+ runsFound: number().int().nonnegative()
151023
+ });
151024
+ var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
151025
+ state: literal("proposal"),
151026
+ transform: ThresholdTransformDraftSchema,
151027
+ evidence: RunningProposalEvidenceSchema,
151028
+ confidence: RunningProposalConfidenceSchema,
151029
+ /** Why the confidence is not higher; empty when nothing held it back. */
151030
+ notes: array(string())
151031
+ }), object({
151032
+ state: literal("insufficient"),
151033
+ reason: RunningProposalInsufficientReasonSchema,
151034
+ evidence: RunningProposalInsufficientEvidenceSchema,
151035
+ /** The catalog defaults, offered as what stands when nothing better is known. */
151036
+ fallback: ThresholdTransformDraftSchema
151037
+ })]);
151038
+ /**
150893
151039
  * The report of a one-shot "move this owner's plaintext secrets into the
150894
151040
  * keychain" run (D878). COUNTS only - never a value, never a ref.
150895
151041
  *
@@ -151179,6 +151325,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
151179
151325
  kind: "mutation",
151180
151326
  auth: "admin",
151181
151327
  access: "view"
151328
+ }), method(object({
151329
+ deviceId: number().int(),
151330
+ cap: string().min(1),
151331
+ fieldPath: string().min(1),
151332
+ deviceType: _enum(DeviceType).optional()
151333
+ }), RunningThresholdProposalSchema, {
151334
+ auth: "admin",
151335
+ access: "view"
151182
151336
  }), method(object({
151183
151337
  blockId: string().min(1),
151184
151338
  trace: AutomationRunTraceSchema.optional(),
@@ -152155,7 +152309,27 @@ method(object({
152155
152309
  * exposes only the COMMON surface: link state, supported device
152156
152310
  * kinds (so the UI can filter the device picker), and per-device
152157
152311
  * expose/unexpose.
152158
- */
152312
+ *
152313
+ * INSTANCES. One export addon can serve several independent targets (a
152314
+ * HomeKit bridge, a Home Assistant broker). `listInstances` names them.
152315
+ * `instanceId` on a READ is optional: absent = the whole addon (linked if any
152316
+ * enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
152317
+ * default instance's); present = that instance; an id the provider does not
152318
+ * serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
152319
+ * never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
152320
+ * change always names the target it changes. A single-instance provider
152321
+ * accepts an absent id on a read or its own id, and refuses any other.
152322
+ */
152323
+ /** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
152324
+ var InstanceIdSchema$1 = string().min(1);
152325
+ var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
152326
+ /** One independent export target of an addon. */
152327
+ var ExportInstanceSchema = object({
152328
+ instanceId: InstanceIdSchema$1,
152329
+ name: string(),
152330
+ enabled: boolean(),
152331
+ kind: string().optional()
152332
+ });
152159
152333
  var LinkStateSchema = _enum([
152160
152334
  "unlinked",
152161
152335
  "linked",
@@ -152206,10 +152380,14 @@ var ExposedDeviceSchema = object({
152206
152380
  capabilities: array(string()).optional()
152207
152381
  });
152208
152382
  var ExposeInputSchema = object({
152383
+ instanceId: InstanceIdSchema$1,
152209
152384
  deviceId: string(),
152210
152385
  capabilities: array(string()).optional()
152211
152386
  });
152212
- var UnexposeInputSchema = object({ deviceId: string() });
152387
+ var UnexposeInputSchema = object({
152388
+ instanceId: InstanceIdSchema$1,
152389
+ deviceId: string()
152390
+ });
152213
152391
  /**
152214
152392
  * What an exporter may expose besides devices (D968). An action and a
152215
152393
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -152238,15 +152416,21 @@ var ExposedEntitySchema = object({
152238
152416
  entity: ExportEntityRefSchema,
152239
152417
  exposedAs: string().optional()
152240
152418
  });
152241
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
152242
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
152243
- method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
152419
+ var ExposeEntityInputSchema = object({
152420
+ instanceId: InstanceIdSchema$1,
152421
+ entity: ExportEntityRefSchema
152422
+ });
152423
+ var UnexposeEntityInputSchema = object({
152424
+ instanceId: InstanceIdSchema$1,
152425
+ entity: ExportEntityRefSchema
152426
+ });
152427
+ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
152244
152428
  kind: "mutation",
152245
152429
  auth: "admin"
152246
152430
  }), method(UnexposeInputSchema, _void(), {
152247
152431
  kind: "mutation",
152248
152432
  auth: "admin"
152249
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
152433
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
152250
152434
  kind: "mutation",
152251
152435
  auth: "admin"
152252
152436
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -155869,15 +156053,15 @@ var FanPresetCaptureResultSchema = object({
155869
156053
  items: array(FanCaptureItemSchema),
155870
156054
  omitted: array(FanCaptureOmissionSchema)
155871
156055
  });
155872
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
156056
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
155873
156057
  DeviceType.Fan;
155874
- method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
156058
+ method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
155875
156059
  kind: "mutation",
155876
156060
  auth: "admin"
155877
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
156061
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
155878
156062
  kind: "mutation",
155879
156063
  auth: "protected"
155880
- }), method(deviceInput$1.extend({
156064
+ }), method(deviceInput$2.extend({
155881
156065
  fields: array(FanCaptureFieldSchema).optional(),
155882
156066
  steps: array(ActionStepSchema).max(20).default([])
155883
156067
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -156760,6 +156944,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
156760
156944
  }), object({ success: boolean() }), {
156761
156945
  kind: "mutation",
156762
156946
  auth: "admin"
156947
+ }), method(object({ id: string() }), object({
156948
+ rows: number().int().nonnegative(),
156949
+ secrets: number().int().nonnegative()
156950
+ }), {
156951
+ kind: "mutation",
156952
+ auth: "admin"
156763
156953
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
156764
156954
  addonId: string(),
156765
156955
  settings: record(string(), unknown())
@@ -156788,13 +156978,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
156788
156978
  */
156789
156979
  var ConfigSchemaPassthrough$3 = custom();
156790
156980
  var IntegrationInstanceIdSchema = string().min(1);
156981
+ var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
156982
+ /**
156983
+ * One kind of instance a provider can create (an adapter, an account type). The sign-in and
156984
+ * connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
156985
+ * hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
156986
+ * interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
156987
+ * instance id.
156988
+ */
156989
+ var IntegrationInstanceKindSchema = object({
156990
+ /** Stable id of the kind, sent back as `createInstance.kind`. */
156991
+ id: string().min(1),
156992
+ label: string().min(1),
156993
+ /** The kind signs in through a server-held interactive session. */
156994
+ signIn: object({
156995
+ basePath: string().min(1),
156996
+ methods: array(_enum(["device-code", "proxy"])).optional()
156997
+ }).optional(),
156998
+ /** Relative hub path to start an interactive account link of an instance of this kind. */
156999
+ connectPath: string().min(1).optional(),
157000
+ /** Relative hub path to disconnect that account. */
157001
+ disconnectPath: string().min(1).optional()
157002
+ });
156791
157003
  var IntegrationInstanceDescriptionSchema = object({
157004
+ /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
157005
+ role: _enum([
157006
+ "devices",
157007
+ "services",
157008
+ "both"
157009
+ ]),
157010
+ /** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
157011
+ discovery: object({
157012
+ mode: _enum([
157013
+ "none",
157014
+ "lan",
157015
+ "account"
157016
+ ]),
157017
+ scopedToInstance: boolean()
157018
+ }).optional(),
156792
157019
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
156793
- instanceMode: _enum(["single", "multiple"]),
157020
+ instanceMode: IntegrationInstanceModeSchema,
156794
157021
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
156795
157022
  kind: string(),
156796
157023
  /** True when `testInstance` really probes the remote. */
156797
- canTest: boolean()
157024
+ canTest: boolean(),
157025
+ /**
157026
+ * The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
157027
+ * Add flow asks which first and sends it as `createInstance.kind`.
157028
+ */
157029
+ kinds: array(IntegrationInstanceKindSchema).optional(),
157030
+ /**
157031
+ * The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
157032
+ * devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
157033
+ */
157034
+ allowRestoreId: boolean().optional(),
157035
+ /**
157036
+ * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
157037
+ * send an id (the UI then mints an opaque one).
157038
+ */
157039
+ mintsId: boolean().optional()
156798
157040
  });
156799
157041
  var IntegrationInstanceStatusSchema = object({
156800
157042
  state: _enum([
@@ -156803,7 +157045,9 @@ var IntegrationInstanceStatusSchema = object({
156803
157045
  "error",
156804
157046
  "unknown"
156805
157047
  ]),
156806
- detail: string().optional()
157048
+ detail: string().optional(),
157049
+ /** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
157050
+ label: string().optional()
156807
157051
  });
156808
157052
  var IntegrationInstanceSchema = object({
156809
157053
  id: IntegrationInstanceIdSchema,
@@ -156811,13 +157055,18 @@ var IntegrationInstanceSchema = object({
156811
157055
  /** The addon's own rule (an URL is present, a client id is present...). */
156812
157056
  configured: boolean(),
156813
157057
  /**
156814
- * The instance's own on/off switch (D996): always present, derived from the record body
156815
- * (`enabled !== false`, so a row written before the field existed is on).
157058
+ * The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
157059
+ * so a row written before the field existed is on). Absent when the `integrations` registry
157060
+ * owns the switch (`describe().role` is `devices`): read it from the registry row.
156816
157061
  */
156817
- enabled: boolean(),
157062
+ enabled: boolean().optional(),
157063
+ /** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
157064
+ kind: string().optional(),
156818
157065
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
156819
157066
  formValues: record(string(), unknown()),
156820
- status: IntegrationInstanceStatusSchema.optional()
157067
+ status: IntegrationInstanceStatusSchema.optional(),
157068
+ /** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
157069
+ lastCheckedAt: number().optional()
156821
157070
  });
156822
157071
  var IntegrationInstancePatchSchema = object({
156823
157072
  id: IntegrationInstanceIdSchema,
@@ -156826,8 +157075,9 @@ var IntegrationInstancePatchSchema = object({
156826
157075
  values: record(string(), unknown())
156827
157076
  });
156828
157077
  method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
156829
- id: IntegrationInstanceIdSchema,
156830
- name: string().min(1)
157078
+ id: IntegrationInstanceIdSchema.optional(),
157079
+ name: string().min(1),
157080
+ kind: string().min(1).optional()
156831
157081
  }), object({ id: string() }), {
156832
157082
  kind: "mutation",
156833
157083
  auth: "admin"
@@ -158888,6 +159138,11 @@ var LogEntrySchema = object({
158888
159138
  * flight, and not asking for the next slice ends the scan.
158889
159139
  */
158890
159140
  var LogReadQuerySchema = object({
159141
+ /**
159142
+ * Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
159143
+ * the provider's first readable one, so a caller written before instances existed is unchanged.
159144
+ */
159145
+ instanceId: string().min(1).max(200).optional(),
158891
159146
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
158892
159147
  requestId: string().min(1).max(64).optional(),
158893
159148
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -158950,6 +159205,8 @@ var LogReadResultSchema = object({
158950
159205
  /** True when `cancelRead` ended this slice early. */
158951
159206
  cancelled: boolean().optional()
158952
159207
  });
159208
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
159209
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
158953
159210
  var LogReadCapabilitiesSchema = object({
158954
159211
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
158955
159212
  readable: boolean(),
@@ -158968,7 +159225,7 @@ var LogReadCapabilitiesSchema = object({
158968
159225
  /** How far back the store reaches, when the provider knows (days). */
158969
159226
  retentionDays: number().nullable()
158970
159227
  });
158971
- method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(_void(), LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
159228
+ method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
158972
159229
  kind: "mutation",
158973
159230
  auth: "admin"
158974
159231
  });
@@ -171361,6 +171618,14 @@ var powerMeterCapability = {
171361
171618
  deviceNative: true,
171362
171619
  mode: "singleton",
171363
171620
  deviceTypes: POWER_METER_DEVICE_TYPES,
171621
+ deviceConfig: { ui: {
171622
+ kind: "widget",
171623
+ widgetId: "host/power-meter-line",
171624
+ tab: "power",
171625
+ placement: "hero",
171626
+ label: "Power",
171627
+ order: 4
171628
+ } },
171364
171629
  methods: {},
171365
171630
  status: {
171366
171631
  schema: PowerMeterStatusSchema,
@@ -171420,6 +171685,117 @@ var powerMeterCapability = {
171420
171685
  volatileStateFields: ["lastFetchedAt"],
171421
171686
  customize: powerMeterCustomize
171422
171687
  };
171688
+ var PowerSourceRefusalReasonSchema = _enum([
171689
+ "no-power-source",
171690
+ "switch-missing",
171691
+ "switch-offline",
171692
+ "switch-unknown",
171693
+ "restart-in-progress",
171694
+ "switch-call-failed"
171695
+ ]);
171696
+ var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
171697
+ accepted: literal(false),
171698
+ reason: PowerSourceRefusalReasonSchema,
171699
+ /** One line for the operator, naming the switch. */
171700
+ message: string()
171701
+ })]);
171702
+ var PowerSourceInfoSchema = object({
171703
+ /** False when the device has no power source linked; the rest is then empty. */
171704
+ linked: boolean(),
171705
+ switchDeviceId: number().nullable(),
171706
+ switchName: string().nullable(),
171707
+ /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
171708
+ switchOn: boolean().nullable(),
171709
+ switchOnline: boolean().nullable(),
171710
+ restartInProgress: boolean(),
171711
+ offSeconds: number().int()
171712
+ });
171713
+ /** The composed slice: the switch state the link mirrors, plus the stored off time. */
171714
+ var PowerSourceStateSchema = object({
171715
+ /** Null while the switch has not reported: unknown is never `false` (D393). */
171716
+ switchOn: boolean().nullable(),
171717
+ offSeconds: number().int(),
171718
+ lastFetchedAt: number()
171719
+ });
171720
+ var POWER_SOURCE_DEVICE_TYPES = [
171721
+ DeviceType.Gateway,
171722
+ DeviceType.Hub,
171723
+ DeviceType.Camera,
171724
+ DeviceType.Sensor,
171725
+ DeviceType.MediaPlayer,
171726
+ DeviceType.Vacuum,
171727
+ DeviceType.Generic,
171728
+ DeviceType.Washer,
171729
+ DeviceType.Dryer,
171730
+ DeviceType.Dishwasher,
171731
+ DeviceType.PetFountain,
171732
+ DeviceType.Fan,
171733
+ DeviceType.Humidifier,
171734
+ DeviceType.Climate,
171735
+ DeviceType.WaterHeater
171736
+ ];
171737
+ var powerSourceCustomize = {
171738
+ version: 1,
171739
+ targets: [{
171740
+ id: "switch",
171741
+ cardinality: "scalar",
171742
+ vocabulary: {
171743
+ cap: "power-source",
171744
+ stateField: "switchOn"
171745
+ },
171746
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
171747
+ apply: "add",
171748
+ path: "switchOn",
171749
+ sources: [{
171750
+ kind: "switch",
171751
+ transforms: [{ kind: "identity" }]
171752
+ }],
171753
+ companions: [{
171754
+ kind: "constant",
171755
+ path: "offSeconds",
171756
+ value: 10,
171757
+ when: "add"
171758
+ }, {
171759
+ kind: "clock",
171760
+ path: "lastFetchedAt",
171761
+ when: "always"
171762
+ }],
171763
+ sourceReport: { status: "assignment-status" }
171764
+ }]
171765
+ };
171766
+ var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
171767
+ var powerSourceCapability = {
171768
+ name: "power-source",
171769
+ scope: "device",
171770
+ mode: "singleton",
171771
+ deviceTypes: POWER_SOURCE_DEVICE_TYPES,
171772
+ methods: {
171773
+ getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
171774
+ powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
171775
+ kind: "mutation",
171776
+ auth: "admin"
171777
+ }),
171778
+ powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
171779
+ kind: "mutation",
171780
+ auth: "admin"
171781
+ }),
171782
+ forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
171783
+ kind: "mutation",
171784
+ auth: "admin"
171785
+ })
171786
+ },
171787
+ runtimeState: PowerSourceStateSchema,
171788
+ customize: powerSourceCustomize,
171789
+ /**
171790
+ * Runtime-state durability: **session** — mirrored from the linked switch on
171791
+ * its next report; a restored value would outlive the reading it names.
171792
+ *
171793
+ * See `RuntimeStateDurability`. Enforced by
171794
+ * `scripts/check-runtime-state-durability.ts`.
171795
+ */
171796
+ durability: "session",
171797
+ history: "none"
171798
+ };
171423
171799
  /**
171424
171800
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
171425
171801
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -181900,6 +182276,25 @@ function buildStreamParamsConfigSchema(options, status) {
181900
182276
  if (sections.length === 0) return null;
181901
182277
  return { sections };
181902
182278
  }
182279
+ /**
182280
+ * What a remote key is CALLED and what it LOOKS like, for every role (D963),
182281
+ * shared by the admin UI and the viewer. A role is data on the wire; each
182282
+ * client resolves its name from `label` (English and Italian side by side) and
182283
+ * its glyph from `icon`, a lucide id from the shared icon catalogue — the
182284
+ * server sends an id, a client maps it to its own icon set.
182285
+ *
182286
+ * `legend` is text printed on the key INSTEAD of a glyph (a digit has no
182287
+ * pictogram): a client that can draw text shows it, one that cannot falls back
182288
+ * to `icon`.
182289
+ */
182290
+ function digit(n) {
182291
+ return {
182292
+ label: t(n, n),
182293
+ icon: "hash",
182294
+ legend: n
182295
+ };
182296
+ }
182297
+ digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
181903
182298
  DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
181904
182299
  var btn = (idSuffix, row, col, init) => ({
181905
182300
  idSuffix,
@@ -182028,6 +182423,34 @@ discriminatedUnion("kind", [object({
182028
182423
  code: string()
182029
182424
  })]);
182030
182425
  /**
182426
+ * Propose a running threshold from a device's own power history (planning#405).
182427
+ *
182428
+ * Pure and deterministic: the same series gives the same answer. The series is
182429
+ * a HELD-VALUE step series — a point's value stands until the next point — which
182430
+ * is how device-history stores a gauge, so "time at a level" is exact, not a
182431
+ * count of samples.
182432
+ *
182433
+ * 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
182434
+ * near F. A device that is not near one level most of the time has no floor.
182435
+ * 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
182436
+ * 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
182437
+ * the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
182438
+ * 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
182439
+ * grid a person would type; stop dwell covers the longest pause; start dwell
182440
+ * covers the longest standby blip above T.
182441
+ * 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
182442
+ * run must hold exactly one true interval and nothing may flap outside.
182443
+ */
182444
+ var SECOND_MS = 1e3;
182445
+ var MINUTE_MS$1 = 60 * SECOND_MS;
182446
+ 24 * (60 * MINUTE_MS$1);
182447
+ 10 * MINUTE_MS$1;
182448
+ 30 * SECOND_MS;
182449
+ 60 * SECOND_MS;
182450
+ 20 * MINUTE_MS$1;
182451
+ 10 * SECOND_MS;
182452
+ 10 * SECOND_MS;
182453
+ /**
182031
182454
  * Which of OUR object detections a clip window holds — the ONE overlap rule
182032
182455
  * and the ONE label ranking, for every clip source (D923).
182033
182456
  *
@@ -185280,6 +185703,13 @@ Object.freeze({
185280
185703
  access: "create",
185281
185704
  role: "admin"
185282
185705
  },
185706
+ "coreBlocks.proposeRunningThreshold": {
185707
+ capName: "core-blocks",
185708
+ capScope: "system",
185709
+ addonId: null,
185710
+ access: "view",
185711
+ role: "admin"
185712
+ },
185283
185713
  "coreBlocks.reportAutomationRun": {
185284
185714
  capName: "core-blocks",
185285
185715
  capScope: "system",
@@ -185791,6 +186221,13 @@ Object.freeze({
185791
186221
  access: "view",
185792
186222
  role: "admin"
185793
186223
  },
186224
+ "deviceExport.listInstances": {
186225
+ capName: "device-export",
186226
+ capScope: "system",
186227
+ addonId: null,
186228
+ access: "view",
186229
+ role: "admin"
186230
+ },
185794
186231
  "deviceExport.listSupportedDeviceKinds": {
185795
186232
  capName: "device-export",
185796
186233
  capScope: "system",
@@ -187443,6 +187880,13 @@ Object.freeze({
187443
187880
  access: "create",
187444
187881
  role: "admin"
187445
187882
  },
187883
+ "integrations.clearSettings": {
187884
+ capName: "integrations",
187885
+ capScope: "system",
187886
+ addonId: null,
187887
+ access: "delete",
187888
+ role: "admin"
187889
+ },
187446
187890
  "integrations.create": {
187447
187891
  capName: "integrations",
187448
187892
  capScope: "system",
@@ -194890,6 +195334,11 @@ Object.freeze({
194890
195334
  form: "single",
194891
195335
  optional: false
194892
195336
  }],
195337
+ "coreBlocks.proposeRunningThreshold": [{
195338
+ name: "deviceId",
195339
+ form: "single",
195340
+ optional: false
195341
+ }],
194893
195342
  "cover.close": [{
194894
195343
  name: "deviceId",
194895
195344
  form: "single",
@@ -197553,99 +198002,6 @@ Object.freeze({
197553
198002
  "network-access": "ingress",
197554
198003
  "smtp-provider": "email"
197555
198004
  });
197556
- /**
197557
- * A saved Customize assignment (D936). The schema is the type: callers use
197558
- * `z.infer`, they do not restate the shape.
197559
- */
197560
- var OverrideSourceSchema = object({
197561
- addonId: string().min(1),
197562
- stableId: string().min(1),
197563
- cap: string().min(1),
197564
- fieldPath: string().min(1)
197565
- });
197566
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
197567
- object({ kind: literal("identity") }),
197568
- object({ kind: literal("invert") }),
197569
- object({
197570
- kind: literal("threshold"),
197571
- comparison: _enum(["gte", "lte"]),
197572
- threshold: number(),
197573
- startDwellMs: number().nonnegative(),
197574
- stopDwellMs: number().nonnegative()
197575
- }),
197576
- object({
197577
- kind: literal("linear"),
197578
- scale: number(),
197579
- offset: number(),
197580
- clampMin: number().optional(),
197581
- clampMax: number().optional()
197582
- }),
197583
- object({
197584
- kind: literal("enum-map"),
197585
- mapping: record(string(), string().nullable())
197586
- }),
197587
- object({ kind: literal("number") })
197588
- ]);
197589
- var ScalarOverrideDraftSchema = object({
197590
- kind: literal("scalar"),
197591
- cap: string().min(1),
197592
- targetId: string().min(1),
197593
- source: OverrideSourceSchema,
197594
- transform: CustomizeTransformDraftSchema
197595
- });
197596
- var KeyedArrayOverrideDraftSchema = object({
197597
- kind: literal("keyedArray"),
197598
- cap: string().min(1),
197599
- targetId: string().min(1),
197600
- key: string().min(1),
197601
- label: string().min(1),
197602
- fieldPath: string().min(1),
197603
- source: OverrideSourceSchema,
197604
- transform: CustomizeTransformDraftSchema
197605
- });
197606
- var PulseOverrideDraftSchema = object({
197607
- kind: literal("pulse"),
197608
- cap: string().min(1),
197609
- targetId: string().min(1),
197610
- enabled: boolean(),
197611
- debounceMs: number().nonnegative(),
197612
- edges: array(_enum([
197613
- "rising",
197614
- "button",
197615
- "native-event"
197616
- ])).min(1).optional(),
197617
- sources: array(OverrideSourceSchema).min(1)
197618
- });
197619
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
197620
- "in-addition",
197621
- "confirm",
197622
- "replace"
197623
- ]).optional() });
197624
- discriminatedUnion("kind", [
197625
- ScalarOverrideDraftSchema,
197626
- KeyedArrayOverrideDraftSchema,
197627
- PulseOverrideDraftSchema,
197628
- object({
197629
- kind: literal("span"),
197630
- cap: string().min(1),
197631
- targetId: string().min(1),
197632
- timeoutMs: number().nonnegative(),
197633
- cooldownMs: number().nonnegative().optional(),
197634
- sources: array(SpanSourceDraftSchema)
197635
- })
197636
- ]);
197637
- object({
197638
- targetId: string().min(1),
197639
- configured: boolean(),
197640
- source: _enum(["native", "assigned"]),
197641
- state: _enum([
197642
- "active",
197643
- "stale",
197644
- "released",
197645
- "unavailable"
197646
- ]),
197647
- reason: string().nullable()
197648
- });
197649
198005
  var G711_SCALE_CORRECTION_DB = {
197650
198006
  PCMU: 20 * Math.log10(4),
197651
198007
  PCMA: 20 * Math.log10(8)
@@ -219657,6 +220013,285 @@ async function startLegacyWebhookIntake(deps) {
219657
220013
  };
219658
220014
  }
219659
220015
  //#endregion
220016
+ //#region src/reolink-discovery-map.ts
220017
+ /**
220018
+ * Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
220019
+ * discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
220020
+ */
220021
+ function slugifyReolinkHost(host) {
220022
+ return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
220023
+ }
220024
+ /**
220025
+ * Decide whether a discovered host is a device this provider already owns.
220026
+ *
220027
+ * Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
220028
+ * wins even when the host differs, because the address is the thing that moves.
220029
+ */
220030
+ function findOnboardedMatch(discovered, onboarded) {
220031
+ if (discovered.uid !== void 0 && discovered.uid !== "") {
220032
+ const byUid = onboarded.find((o) => o.uid === discovered.uid);
220033
+ if (byUid !== void 0) return byUid;
220034
+ }
220035
+ return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
220036
+ }
220037
+ /** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
220038
+ var HUB_BAICHUAN_PORT = 9e3;
220039
+ /**
220040
+ * `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
220041
+ * rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
220042
+ * as a camera to add.
220043
+ */
220044
+ function looksLikeReolinkHub(model) {
220045
+ if (model === void 0) return false;
220046
+ return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
220047
+ }
220048
+ /**
220049
+ * The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
220050
+ * second. The boot migration derives the SAME id for an old Hub device, so the integration it
220051
+ * creates hides this candidate through `originStableId`.
220052
+ */
220053
+ function reolinkHubStableId(identity) {
220054
+ const uid = identity.uid?.trim() ?? "";
220055
+ if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
220056
+ return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
220057
+ }
220058
+ function isKnownHub(d, known) {
220059
+ return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
220060
+ }
220061
+ /**
220062
+ * Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
220063
+ * same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
220064
+ * first responder for a host wins. Pure: no I/O.
220065
+ *
220066
+ * **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
220067
+ * (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
220068
+ * that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
220069
+ * pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
220070
+ * closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
220071
+ * child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
220072
+ * never a scan result.
220073
+ *
220074
+ * The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
220075
+ * can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
220076
+ * which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
220077
+ * in the first place, so a scan says so instead of offering it again.
220078
+ */
220079
+ function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
220080
+ const username = credentials.username?.trim() ?? "";
220081
+ const password = credentials.password ?? "";
220082
+ const byHost = /* @__PURE__ */ new Map();
220083
+ for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
220084
+ return [...byHost.values()].flatMap((d) => {
220085
+ const displayName = d.name ?? d.model ?? d.host;
220086
+ if (looksLikeReolinkHub(d.model)) {
220087
+ if (isKnownHub(d, hubIntegrations)) return [];
220088
+ return [{
220089
+ stableId: reolinkHubStableId(d),
220090
+ type: DeviceType.Gateway,
220091
+ suggestedName: displayName,
220092
+ prefilledConfig: {
220093
+ host: d.host,
220094
+ port: HUB_BAICHUAN_PORT,
220095
+ ...d.uid ? { uid: d.uid } : {},
220096
+ ...username ? { username } : {}
220097
+ },
220098
+ setupIntegration: true
220099
+ }];
220100
+ }
220101
+ const match = findOnboardedMatch(d, onboarded);
220102
+ return [{
220103
+ stableId: `host-${slugifyReolinkHost(d.host)}`,
220104
+ type: DeviceType.Camera,
220105
+ suggestedName: displayName,
220106
+ prefilledConfig: {
220107
+ name: displayName,
220108
+ host: d.host,
220109
+ transport: "auto",
220110
+ ...d.uid ? { uid: d.uid } : {},
220111
+ ...username ? { username } : {},
220112
+ ...password ? { password } : {}
220113
+ },
220114
+ ...match !== void 0 ? {
220115
+ alreadyOnboarded: true,
220116
+ onboardedDeviceId: match.deviceId,
220117
+ onboardedName: match.name
220118
+ } : {}
220119
+ }];
220120
+ });
220121
+ }
220122
+ //#endregion
220123
+ //#region src/reolink-hub-migration.ts
220124
+ /**
220125
+ * D985 — boot migration of the old Reolink Hub DEVICE into an INTEGRATION.
220126
+ *
220127
+ * A Hub used to be a device with the NVR's credentials in its config and its channels as
220128
+ * parented child cameras. Now the NVR is an integration (credentials in its keychain-backed
220129
+ * settings) and the channels are top-level cameras stamped with its id. For every Hub row:
220130
+ *
220131
+ * 1. create (or reuse) an integration named after it, config + password moved as settings;
220132
+ * 2. keep every child's deviceId and stableId, stamp it with the integrationId, un-parent it
220133
+ * and make sure its persisted blob can be restored on its own;
220134
+ * 3. only AFTER the children restored and the hub has none left, retire the hub by tombstone.
220135
+ *
220136
+ * Idempotent (a second run finds no Hub rows) and all-or-nothing per hub: a step that throws
220137
+ * leaves the hub device and its children's parentage exactly as they were, and the next boot
220138
+ * starts over. A locked secret is never read as an empty password and no replacement is minted.
220139
+ */
220140
+ /** The marker an integration carries so a re-run reuses it instead of minting a second one. */
220141
+ var MIGRATED_FROM_HUB_KEY = "migratedFromHubDeviceId";
220142
+ function positivePort(value) {
220143
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
220144
+ }
220145
+ var CONNECTION_FIELDS = [
220146
+ "host",
220147
+ "port",
220148
+ "username",
220149
+ "transport"
220150
+ ];
220151
+ function str(value) {
220152
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
220153
+ }
220154
+ function migratedChildConfig(child, hub, integrationId) {
220155
+ const filled = {};
220156
+ for (const key of CONNECTION_FIELDS) {
220157
+ const own = child[key];
220158
+ const inherited = hub[key];
220159
+ if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
220160
+ }
220161
+ return {
220162
+ ...child,
220163
+ ...filled,
220164
+ integrationId
220165
+ };
220166
+ }
220167
+ async function migrateOneHub(hub, children, integrations, ports) {
220168
+ const read = readHubConnection(hub.config);
220169
+ if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
220170
+ const { connection } = read;
220171
+ const uid = str(hub.config["uid"]);
220172
+ const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
220173
+ addonId: ports.addonId,
220174
+ name: hub.name,
220175
+ info: { ["migratedFromHubDeviceId"]: hub.id },
220176
+ originStableId: reolinkHubStableId({
220177
+ ...uid !== void 0 ? { uid } : {},
220178
+ host: connection.host
220179
+ }),
220180
+ settings: {
220181
+ host: connection.host,
220182
+ port: connection.port,
220183
+ rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
220184
+ username: connection.username,
220185
+ password: connection.password,
220186
+ transport: connection.transport,
220187
+ ...uid !== void 0 ? { uid } : {},
220188
+ debugGeneral: connection.debugGeneral,
220189
+ debugSocketLogs: connection.debugSocketLogs
220190
+ },
220191
+ secretKeys: ["password"]
220192
+ })).id;
220193
+ const rewritten = [];
220194
+ for (const child of children) {
220195
+ const config = migratedChildConfig(child.config, hub.config, integrationId);
220196
+ await ports.persistConfig(child.stableId, config);
220197
+ await ports.setIntegrationId(child.id, integrationId);
220198
+ rewritten.push({
220199
+ ...child,
220200
+ config,
220201
+ parentDeviceId: null
220202
+ });
220203
+ }
220204
+ return {
220205
+ rows: rewritten,
220206
+ retirement: {
220207
+ hubDeviceId: hub.id,
220208
+ hubName: hub.name,
220209
+ integrationId
220210
+ }
220211
+ };
220212
+ }
220213
+ /** Plan and persist the migration of every Hub row in `saved`. Never throws. */
220214
+ async function planHubMigration(saved, ports) {
220215
+ const hubs = saved.filter((row) => row.type === DeviceType.Hub);
220216
+ if (hubs.length === 0) return {
220217
+ rows: saved,
220218
+ retirements: []
220219
+ };
220220
+ let integrations;
220221
+ try {
220222
+ integrations = await ports.listIntegrations();
220223
+ } catch (err) {
220224
+ ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
220225
+ return {
220226
+ rows: saved.filter((row) => row.type !== DeviceType.Hub),
220227
+ retirements: []
220228
+ };
220229
+ }
220230
+ const hubIds = new Set(hubs.map((h) => h.id));
220231
+ const rewrittenById = /* @__PURE__ */ new Map();
220232
+ const retirements = [];
220233
+ for (const hub of hubs) {
220234
+ const children = saved.filter((row) => row.parentDeviceId === hub.id);
220235
+ try {
220236
+ const done = await migrateOneHub(hub, children, integrations, ports);
220237
+ for (const row of done.rows) rewrittenById.set(row.id, row);
220238
+ retirements.push(done.retirement);
220239
+ ports.logger.info("Reolink hub migrated to an integration", {
220240
+ tags: { deviceId: hub.id },
220241
+ meta: {
220242
+ integrationId: done.retirement.integrationId,
220243
+ children: children.length,
220244
+ name: hub.name
220245
+ }
220246
+ });
220247
+ } catch (err) {
220248
+ ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
220249
+ tags: { deviceId: hub.id },
220250
+ meta: {
220251
+ error: errMsg$3(err),
220252
+ name: hub.name
220253
+ }
220254
+ });
220255
+ }
220256
+ }
220257
+ return {
220258
+ rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
220259
+ retirements
220260
+ };
220261
+ }
220262
+ /**
220263
+ * Retire the hubs whose children all restored. A hub that still has a child in the store is left
220264
+ * and named: removing it would cascade the camera away with it.
220265
+ */
220266
+ async function retireMigratedHubs(retirements, ports) {
220267
+ let retired = 0;
220268
+ for (const r of retirements) try {
220269
+ const remaining = await ports.childIdsOf(r.hubDeviceId);
220270
+ if (remaining.length > 0) {
220271
+ ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
220272
+ tags: { deviceId: r.hubDeviceId },
220273
+ meta: {
220274
+ remaining: [...remaining],
220275
+ integrationId: r.integrationId
220276
+ }
220277
+ });
220278
+ continue;
220279
+ }
220280
+ await ports.removeDevice(r.hubDeviceId);
220281
+ retired++;
220282
+ ports.logger.info("Reolink hub device retired", {
220283
+ tags: { deviceId: r.hubDeviceId },
220284
+ meta: { integrationId: r.integrationId }
220285
+ });
220286
+ } catch (err) {
220287
+ ports.logger.warn("Reolink hub retirement failed — retried next boot", {
220288
+ tags: { deviceId: r.hubDeviceId },
220289
+ meta: { error: errMsg$3(err) }
220290
+ });
220291
+ }
220292
+ return retired;
220293
+ }
220294
+ //#endregion
219660
220295
  //#region src/migrations/drop-webhook-intake-settings.ts
219661
220296
  /**
219662
220297
  * The camera webhook is a plane of the addon's HTTP data plane, not a private listener
@@ -219781,6 +220416,18 @@ var REOLINK_MIGRATIONS = [{
219781
220416
  },
219782
220417
  plan: planDropWebhookIntakeSettings
219783
220418
  }];
220419
+ /**
220420
+ * Every persisted camera that names no integration is claimed to the addon's default one. The
220421
+ * integration a hub migration created is never the default, so it is not eligible. The integration
220422
+ * settings themselves stay where they are until this addon's own wave moves them.
220423
+ */
220424
+ var REOLINK_CLAIM_MIGRATION = claimDevicesMigration({
220425
+ addonId: "provider-reolink",
220426
+ date: "2026-11-04",
220427
+ seq: 3,
220428
+ dependsOn: [],
220429
+ eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
220430
+ });
219784
220431
  //#endregion
219785
220432
  //#region src/reolink-adoption-provider.ts
219786
220433
  var DEVICES_FILTER = {
@@ -219946,113 +220593,6 @@ function buildReolinkAdoptionProvider(deps) {
219946
220593
  };
219947
220594
  }
219948
220595
  //#endregion
219949
- //#region src/reolink-discovery-map.ts
219950
- /**
219951
- * Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
219952
- * discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
219953
- */
219954
- function slugifyReolinkHost(host) {
219955
- return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
219956
- }
219957
- /**
219958
- * Decide whether a discovered host is a device this provider already owns.
219959
- *
219960
- * Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
219961
- * wins even when the host differs, because the address is the thing that moves.
219962
- */
219963
- function findOnboardedMatch(discovered, onboarded) {
219964
- if (discovered.uid !== void 0 && discovered.uid !== "") {
219965
- const byUid = onboarded.find((o) => o.uid === discovered.uid);
219966
- if (byUid !== void 0) return byUid;
219967
- }
219968
- return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
219969
- }
219970
- /** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
219971
- var HUB_BAICHUAN_PORT = 9e3;
219972
- /**
219973
- * `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
219974
- * rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
219975
- * as a camera to add.
219976
- */
219977
- function looksLikeReolinkHub(model) {
219978
- if (model === void 0) return false;
219979
- return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
219980
- }
219981
- /**
219982
- * The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
219983
- * second. The boot migration derives the SAME id for an old Hub device, so the integration it
219984
- * creates hides this candidate through `originStableId`.
219985
- */
219986
- function reolinkHubStableId(identity) {
219987
- const uid = identity.uid?.trim() ?? "";
219988
- if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
219989
- return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
219990
- }
219991
- function isKnownHub(d, known) {
219992
- return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
219993
- }
219994
- /**
219995
- * Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
219996
- * same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
219997
- * first responder for a host wins. Pure: no I/O.
219998
- *
219999
- * **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
220000
- * (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
220001
- * that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
220002
- * pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
220003
- * closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
220004
- * child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
220005
- * never a scan result.
220006
- *
220007
- * The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
220008
- * can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
220009
- * which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
220010
- * in the first place, so a scan says so instead of offering it again.
220011
- */
220012
- function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
220013
- const username = credentials.username?.trim() ?? "";
220014
- const password = credentials.password ?? "";
220015
- const byHost = /* @__PURE__ */ new Map();
220016
- for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
220017
- return [...byHost.values()].flatMap((d) => {
220018
- const displayName = d.name ?? d.model ?? d.host;
220019
- if (looksLikeReolinkHub(d.model)) {
220020
- if (isKnownHub(d, hubIntegrations)) return [];
220021
- return [{
220022
- stableId: reolinkHubStableId(d),
220023
- type: DeviceType.Gateway,
220024
- suggestedName: displayName,
220025
- prefilledConfig: {
220026
- host: d.host,
220027
- port: HUB_BAICHUAN_PORT,
220028
- ...d.uid ? { uid: d.uid } : {},
220029
- ...username ? { username } : {}
220030
- },
220031
- setupIntegration: true
220032
- }];
220033
- }
220034
- const match = findOnboardedMatch(d, onboarded);
220035
- return [{
220036
- stableId: `host-${slugifyReolinkHost(d.host)}`,
220037
- type: DeviceType.Camera,
220038
- suggestedName: displayName,
220039
- prefilledConfig: {
220040
- name: displayName,
220041
- host: d.host,
220042
- transport: "auto",
220043
- ...d.uid ? { uid: d.uid } : {},
220044
- ...username ? { username } : {},
220045
- ...password ? { password } : {}
220046
- },
220047
- ...match !== void 0 ? {
220048
- alreadyOnboarded: true,
220049
- onboardedDeviceId: match.deviceId,
220050
- onboardedName: match.name
220051
- } : {}
220052
- }];
220053
- });
220054
- }
220055
- //#endregion
220056
220596
  //#region src/reolink-gateway-device.ts
220057
220597
  var reolinkGatewayDeviceSchema = object({
220058
220598
  integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
@@ -220209,158 +220749,6 @@ function createReolinkHubManager(deps, registry) {
220209
220749
  connect: connectReolinkHub
220210
220750
  });
220211
220751
  }
220212
- function positivePort(value) {
220213
- return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
220214
- }
220215
- var CONNECTION_FIELDS = [
220216
- "host",
220217
- "port",
220218
- "username",
220219
- "transport"
220220
- ];
220221
- function str(value) {
220222
- return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
220223
- }
220224
- function migratedChildConfig(child, hub, integrationId) {
220225
- const filled = {};
220226
- for (const key of CONNECTION_FIELDS) {
220227
- const own = child[key];
220228
- const inherited = hub[key];
220229
- if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
220230
- }
220231
- return {
220232
- ...child,
220233
- ...filled,
220234
- integrationId
220235
- };
220236
- }
220237
- async function migrateOneHub(hub, children, integrations, ports) {
220238
- const read = readHubConnection(hub.config);
220239
- if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
220240
- const { connection } = read;
220241
- const uid = str(hub.config["uid"]);
220242
- const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
220243
- addonId: ports.addonId,
220244
- name: hub.name,
220245
- info: { ["migratedFromHubDeviceId"]: hub.id },
220246
- originStableId: reolinkHubStableId({
220247
- ...uid !== void 0 ? { uid } : {},
220248
- host: connection.host
220249
- }),
220250
- settings: {
220251
- host: connection.host,
220252
- port: connection.port,
220253
- rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
220254
- username: connection.username,
220255
- password: connection.password,
220256
- transport: connection.transport,
220257
- ...uid !== void 0 ? { uid } : {},
220258
- debugGeneral: connection.debugGeneral,
220259
- debugSocketLogs: connection.debugSocketLogs
220260
- },
220261
- secretKeys: ["password"]
220262
- })).id;
220263
- const rewritten = [];
220264
- for (const child of children) {
220265
- const config = migratedChildConfig(child.config, hub.config, integrationId);
220266
- await ports.persistConfig(child.stableId, config);
220267
- await ports.setIntegrationId(child.id, integrationId);
220268
- rewritten.push({
220269
- ...child,
220270
- config,
220271
- parentDeviceId: null
220272
- });
220273
- }
220274
- return {
220275
- rows: rewritten,
220276
- retirement: {
220277
- hubDeviceId: hub.id,
220278
- hubName: hub.name,
220279
- integrationId
220280
- }
220281
- };
220282
- }
220283
- /** Plan and persist the migration of every Hub row in `saved`. Never throws. */
220284
- async function planHubMigration(saved, ports) {
220285
- const hubs = saved.filter((row) => row.type === DeviceType.Hub);
220286
- if (hubs.length === 0) return {
220287
- rows: saved,
220288
- retirements: []
220289
- };
220290
- let integrations;
220291
- try {
220292
- integrations = await ports.listIntegrations();
220293
- } catch (err) {
220294
- ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
220295
- return {
220296
- rows: saved.filter((row) => row.type !== DeviceType.Hub),
220297
- retirements: []
220298
- };
220299
- }
220300
- const hubIds = new Set(hubs.map((h) => h.id));
220301
- const rewrittenById = /* @__PURE__ */ new Map();
220302
- const retirements = [];
220303
- for (const hub of hubs) {
220304
- const children = saved.filter((row) => row.parentDeviceId === hub.id);
220305
- try {
220306
- const done = await migrateOneHub(hub, children, integrations, ports);
220307
- for (const row of done.rows) rewrittenById.set(row.id, row);
220308
- retirements.push(done.retirement);
220309
- ports.logger.info("Reolink hub migrated to an integration", {
220310
- tags: { deviceId: hub.id },
220311
- meta: {
220312
- integrationId: done.retirement.integrationId,
220313
- children: children.length,
220314
- name: hub.name
220315
- }
220316
- });
220317
- } catch (err) {
220318
- ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
220319
- tags: { deviceId: hub.id },
220320
- meta: {
220321
- error: errMsg$3(err),
220322
- name: hub.name
220323
- }
220324
- });
220325
- }
220326
- }
220327
- return {
220328
- rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
220329
- retirements
220330
- };
220331
- }
220332
- /**
220333
- * Retire the hubs whose children all restored. A hub that still has a child in the store is left
220334
- * and named: removing it would cascade the camera away with it.
220335
- */
220336
- async function retireMigratedHubs(retirements, ports) {
220337
- let retired = 0;
220338
- for (const r of retirements) try {
220339
- const remaining = await ports.childIdsOf(r.hubDeviceId);
220340
- if (remaining.length > 0) {
220341
- ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
220342
- tags: { deviceId: r.hubDeviceId },
220343
- meta: {
220344
- remaining: [...remaining],
220345
- integrationId: r.integrationId
220346
- }
220347
- });
220348
- continue;
220349
- }
220350
- await ports.removeDevice(r.hubDeviceId);
220351
- retired++;
220352
- ports.logger.info("Reolink hub device retired", {
220353
- tags: { deviceId: r.hubDeviceId },
220354
- meta: { integrationId: r.integrationId }
220355
- });
220356
- } catch (err) {
220357
- ports.logger.warn("Reolink hub retirement failed — retried next boot", {
220358
- tags: { deviceId: r.hubDeviceId },
220359
- meta: { error: errMsg$3(err) }
220360
- });
220361
- }
220362
- return retired;
220363
- }
220364
220752
  //#endregion
220365
220753
  //#region src/reolink-subnet-scan.ts
220366
220754
  /**
@@ -221179,7 +221567,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
221179
221567
  this.wireClipCatalogRefreshers();
221180
221568
  await this.serveClipPlanes(this.clipService);
221181
221569
  await this.serveWebhookPlane();
221182
- await this.runMigrations(defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()));
221570
+ await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations([REOLINK_CLAIM_MIGRATION], claimDevicesPortsOver({
221571
+ api: defaultIntegrationApiOver(this.ctx.api),
221572
+ info: (message) => this.ctx.logger.info(message),
221573
+ warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
221574
+ }))]);
221183
221575
  this.wireEmailBridge();
221184
221576
  const wireSleepPushResolvers = (event, via) => {
221185
221577
  const data = event.data;