@camstack/addon-provider-reolink 1.2.220 → 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
@@ -101481,7 +101481,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
101481
101481
  return EventCategory;
101482
101482
  }({});
101483
101483
  //#endregion
101484
- //#region ../types/dist/device-type-CfHw_quR.mjs
101484
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
101485
101485
  /**
101486
101486
  import { errMsg } from '@camstack/types'
101487
101487
  * Extract a human-readable message from an unknown error value.
@@ -101712,6 +101712,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
101712
101712
  DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
101713
101713
  /** Media player exposes stop distinct from pause. */
101714
101714
  DeviceFeature["MediaPlayerStop"] = "media-player-stop";
101715
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
101716
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
101717
+ /** Media player exposes channel up / down in steps. */
101718
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
101719
+ /** Media player exposes power on / off. */
101720
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
101715
101721
  /** Alarm panel requires a PIN code on arm/disarm. */
101716
101722
  DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
101717
101723
  /** Presence device carries GPS coordinates (lat/lng/accuracy) in
@@ -106538,7 +106544,7 @@ var ZodIssueCode = {
106538
106544
  var ZodFirstPartyTypeKind;
106539
106545
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
106540
106546
  //#endregion
106541
- //#region ../types/dist/sleep-hmSP-eJt.mjs
106547
+ //#region ../types/dist/sleep-Cv4Zmie9.mjs
106542
106548
  /**
106543
106549
  * The audio chunk plane's byte format, and the ONE expansion from a coded
106544
106550
  * window to float samples (D455).
@@ -106617,6 +106623,22 @@ function buildAlawTable() {
106617
106623
  }
106618
106624
  buildUlawTable();
106619
106625
  buildAlawTable();
106626
+ /**
106627
+ * An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
106628
+ * so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
106629
+ * co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
106630
+ * bundles its own copy of this module, so the class the runner sees is not the one that threw.
106631
+ */
106632
+ var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
106633
+ var AddonMigrationsBlockedError = class extends Error {
106634
+ /** The ledger ids that did not apply and block the owner. */
106635
+ migrationIds;
106636
+ constructor(migrationIds) {
106637
+ super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
106638
+ this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
106639
+ this.migrationIds = migrationIds;
106640
+ }
106641
+ };
106620
106642
  /** Is this field's stored value a credential? */
106621
106643
  function isSecretConfigField(field) {
106622
106644
  if (field.type === "password") return true;
@@ -107119,7 +107141,7 @@ var NOT_RUN = {
107119
107141
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
107120
107142
  * because the hub (and this addon's boot) must not hang on a migration.
107121
107143
  */
107122
- async function runAddonMigrations(host, migrations, logger) {
107144
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
107123
107145
  const first = migrations[0];
107124
107146
  if (first === void 0) return NOT_RUN;
107125
107147
  const owner = first.meta.owner;
@@ -107148,7 +107170,9 @@ async function runAddonMigrations(host, migrations, logger) {
107148
107170
  else if (status === "blocked") blocked.push(id);
107149
107171
  else skipped.push(id);
107150
107172
  };
107173
+ const selected = options.only === void 0 ? null : new Set(options.only);
107151
107174
  for (const step of declaration.steps) {
107175
+ if (selected !== null && !selected.has(step.id)) continue;
107152
107176
  const bound = byId.get(step.id);
107153
107177
  if (bound === void 0 || step.action === "skip") {
107154
107178
  skipped.push(step.id);
@@ -108266,15 +108290,19 @@ var BaseAddon = class {
108266
108290
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
108267
108291
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
108268
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").
108269
108297
  */
108270
- async runMigrations(migrations) {
108298
+ async runMigrations(migrations, options) {
108271
108299
  const api = this.ctx.api.migrations;
108272
108300
  const report = await runAddonMigrations({
108273
108301
  declareOwner: (input) => api.declareOwner.mutate(input),
108274
108302
  startMigration: (input) => api.startMigration.mutate(input),
108275
108303
  finishMigration: (input) => api.finishMigration.mutate(input)
108276
- }, migrations, this.ctx.logger);
108277
- if (report.blockOwner.length > 0) throw new Error(`migration(s) ${report.blockOwner.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
108304
+ }, migrations, this.ctx.logger, options);
108305
+ if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
108278
108306
  return report;
108279
108307
  }
108280
108308
  /**
@@ -108614,6 +108642,10 @@ var BaseAddon = class {
108614
108642
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
108615
108643
  const patch = {};
108616
108644
  for (const key of keys) {
108645
+ if (tree.get(key)?.kind === "array") {
108646
+ patch[key] = [];
108647
+ continue;
108648
+ }
108617
108649
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
108618
108650
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
108619
108651
  }
@@ -109340,7 +109372,8 @@ function isZodSecret(schema) {
109340
109372
  }
109341
109373
  /**
109342
109374
  * Every declared secret path in a config schema: dotted keys, `[]` for "each
109343
- * 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
109344
109377
  * object or array is reported as one path and not descended into — everything
109345
109378
  * under it is secret.
109346
109379
  */
@@ -109351,6 +109384,7 @@ function collectZodSecretPaths(schema, prefix = "") {
109351
109384
  const def = defOf(layer);
109352
109385
  if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
109353
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}{}`));
109354
109388
  else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
109355
109389
  else if (def.type === "intersection") {
109356
109390
  if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
@@ -112229,7 +112263,7 @@ function sleep(ms) {
112229
112263
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
112230
112264
  }
112231
112265
  //#endregion
112232
- //#region ../types/dist/composition-Dh3RfMx5.mjs
112266
+ //#region ../types/dist/composition-CRNwcFYY.mjs
112233
112267
  /**
112234
112268
  * The ffmpeg build every CamStack node downloads, per platform and arch.
112235
112269
  *
@@ -112992,38 +113026,6 @@ function validateExpressionSource(src) {
112992
113026
  return null;
112993
113027
  }
112994
113028
  /**
112995
- * One power reading becomes a washer, dryer or dishwasher (D944 helper).
112996
- *
112997
- * Standby of the control board is under 5 W; a motor or a heater is far
112998
- * above that, so 5 W is the line. A door lock or a short pump spike is not
112999
- * a cycle, so running starts only after 30 s above the line.
113000
- *
113001
- * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
113002
- * bursts) lasts several minutes. Ending the cycle on the first dip would
113003
- * report "finished" in the middle of the program, so the stop dwell is
113004
- * 10 minutes on every type. The number is stored per type so one machine
113005
- * can move later without moving the others.
113006
- */
113007
- var APPLIANCE_TEMPLATE_ID = "appliance";
113008
- var APPLIANCE_KINDS = [
113009
- "washer",
113010
- "dryer",
113011
- "dishwasher"
113012
- ];
113013
- DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
113014
- /**
113015
- * A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
113016
- *
113017
- * The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
113018
- * (D967), so the heater switches exactly as the plug does. Running is the
113019
- * plug being on; when a power reading is also named, running is the draw
113020
- * staying above the line instead (a plug left on with the stove's own
113021
- * thermostat open is not heating). 50 W is far above any standby and below
113022
- * the smallest radiator.
113023
- */
113024
- var HEATER_TEMPLATE_ID = "heater";
113025
- DeviceType.Heater;
113026
- /**
113027
113029
  * Statistics and integrals over a history read (D944).
113028
113030
  *
113029
113031
  * Pure: the same points always give the same answer, so a restart recomputes
@@ -113167,34 +113169,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
113167
113169
  /** Actuator groups only. Readings leave this unset. */
113168
113170
  active: ActiveCriterionSchema.optional()
113169
113171
  }),
113170
- object({
113171
- kind: literal("appliance"),
113172
- templateId: literal(APPLIANCE_TEMPLATE_ID),
113173
- appliance: _enum(APPLIANCE_KINDS),
113174
- thresholdWatts: number().positive(),
113175
- startDwellMs: number().int().nonnegative(),
113176
- stopDwellMs: number().int().nonnegative(),
113177
- member: HelperMemberSchema,
113178
- /**
113179
- * The plug that powers the appliance (`switch.on`). Optional: when set, the
113180
- * composition carries a `power-source` feature, so the hub can cut and restore
113181
- * that plug (power-source link of a NEW-target composition).
113182
- */
113183
- powerSwitch: HelperMemberSchema.optional(),
113184
- /** How long a forced restart keeps the plug off; the cap's default when unset. */
113185
- offSeconds: number().optional()
113186
- }),
113187
- object({
113188
- kind: literal("heater"),
113189
- templateId: literal(HEATER_TEMPLATE_ID),
113190
- /** The plug, read and commanded at `switch.on`. */
113191
- plug: HelperMemberSchema,
113192
- /** The plug's power reading, when it has one: running is then the draw, not the switch. */
113193
- meter: HelperMemberSchema.optional(),
113194
- thresholdWatts: number().positive(),
113195
- startDwellMs: number().int().nonnegative(),
113196
- stopDwellMs: number().int().nonnegative()
113197
- }),
113198
113172
  object({
113199
113173
  kind: literal("statistic"),
113200
113174
  stat: _enum(STATISTIC_STATS),
@@ -113366,7 +113340,14 @@ var CompositionForwardCommandSchema = object({
113366
113340
  method: string().min(1),
113367
113341
  args: record(string().min(1), CompositionForwardArgSchema).optional()
113368
113342
  });
113369
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
113343
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [
113344
+ CompositionForwardCommandSchema,
113345
+ CompositionCodeSourceSchema,
113346
+ object({
113347
+ kind: literal("extension"),
113348
+ target: literal("remote")
113349
+ })
113350
+ ]);
113370
113351
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
113371
113352
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
113372
113353
  /**
@@ -113411,7 +113392,7 @@ var CompositionSchema = object({
113411
113392
  kind: literal("existing"),
113412
113393
  device: CompositionSourceRefSchema
113413
113394
  })]),
113414
- features: array(CompositionFeatureSchema).min(1).max(16),
113395
+ features: array(CompositionFeatureSchema).min(0).max(16),
113415
113396
  /**
113416
113397
  * Present when the block is a helper template. Absent on a free
113417
113398
  * composition (the room example, device 4603). The editor reads this,
@@ -114715,7 +114696,13 @@ var MigrationLedgerEntrySchema = object({
114715
114696
  * joined by the engine from the catalogues it knows this boot. Absent when no
114716
114697
  * catalogue lists the id (a newer database).
114717
114698
  */
114718
- 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()
114719
114706
  });
114720
114707
  /** A catalogued migration that is not applied, with where it stands. */
114721
114708
  var MigrationPendingEntrySchema = object({
@@ -114748,6 +114735,146 @@ var MigrationOwnerStepSchema = object({
114748
114735
  /** The ledger state the engine found: `pending` has no row yet. */
114749
114736
  state: union([literal("pending"), MigrationStatusSchema])
114750
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
+ }
114751
114878
  /**
114752
114879
  * The trainer contract — what the hub hands the cloud trainer and what it
114753
114880
  * gets back (cloud training, D805+).
@@ -120476,6 +120603,48 @@ var APPLIANCE_CAP_VOCABULARY = {
120476
120603
  scope: "device",
120477
120604
  noun: t("Media player", "Lettore multimediale"),
120478
120605
  actions: [
120606
+ {
120607
+ id: "media.turnOn",
120608
+ method: "turnOn",
120609
+ label: t("Turn on", "Accendi"),
120610
+ phrase: t("Turn on {device}", "Accendi {device}"),
120611
+ icon: "power"
120612
+ },
120613
+ {
120614
+ id: "media.turnOff",
120615
+ method: "turnOff",
120616
+ label: t("Turn off", "Spegni"),
120617
+ phrase: t("Turn off {device}", "Spegni {device}"),
120618
+ icon: "power"
120619
+ },
120620
+ {
120621
+ id: "media.volumeUp",
120622
+ method: "volumeUp",
120623
+ label: t("Volume up", "Alza il volume"),
120624
+ phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
120625
+ icon: "volume-2"
120626
+ },
120627
+ {
120628
+ id: "media.volumeDown",
120629
+ method: "volumeDown",
120630
+ label: t("Volume down", "Abbassa il volume"),
120631
+ phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
120632
+ icon: "volume-2"
120633
+ },
120634
+ {
120635
+ id: "media.channelUp",
120636
+ method: "channelUp",
120637
+ label: t("Channel up", "Canale successivo"),
120638
+ phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
120639
+ icon: "skip-forward"
120640
+ },
120641
+ {
120642
+ id: "media.channelDown",
120643
+ method: "channelDown",
120644
+ label: t("Channel down", "Canale precedente"),
120645
+ phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
120646
+ icon: "skip-back"
120647
+ },
120479
120648
  {
120480
120649
  id: "media.play",
120481
120650
  method: "play",
@@ -149996,6 +150165,244 @@ var controlCapability = {
149996
150165
  */
149997
150166
  history: "none"
149998
150167
  };
150168
+ /**
150169
+ * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
150170
+ * Apple TV, Spotify, Roku, generic OTT receivers, …) on
150171
+ * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
150172
+ * surface — those use `DeviceType.Camera` + the broker pipeline.
150173
+ *
150174
+ * The cap declares a wide method surface; not every backend supports
150175
+ * every method. Drivers signal sub-capability via the
150176
+ * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
150177
+ * corresponding method) so the UI gates buttons without a slice
150178
+ * fetch — same pattern as `PanTiltZoom` for cameras.
150179
+ */
150180
+ var MediaPlayerStateSchema = _enum([
150181
+ "off",
150182
+ "on",
150183
+ "idle",
150184
+ "playing",
150185
+ "paused",
150186
+ "buffering",
150187
+ "standby"
150188
+ ]);
150189
+ var MediaPlayerRepeatSchema = _enum([
150190
+ "off",
150191
+ "all",
150192
+ "one"
150193
+ ]);
150194
+ var MediaInfoSchema = object({
150195
+ /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
150196
+ * `podcast`, …). */
150197
+ type: string(),
150198
+ /** Human-readable title. */
150199
+ title: string(),
150200
+ /** Optional artist / channel / station label. */
150201
+ artist: string().optional(),
150202
+ /** Optional album / season / show name. */
150203
+ album: string().optional(),
150204
+ /** Optional cover-art / thumbnail URL. */
150205
+ imageUrl: string().optional()
150206
+ });
150207
+ /**
150208
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
150209
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
150210
+ * tracks what it last SENT; it moves only after a send was accepted, and a
150211
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
150212
+ * guess, never as a reading.
150213
+ */
150214
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
150215
+ /** The fields of a media-player status a driver can only guess. */
150216
+ var MediaPlayerAssumedFieldSchema = _enum([
150217
+ "state",
150218
+ "volumeLevel",
150219
+ "isMuted",
150220
+ "source"
150221
+ ]);
150222
+ var MediaPlayerStatusSchema = object({
150223
+ /** Playback lifecycle state. */
150224
+ state: MediaPlayerStateSchema,
150225
+ /**
150226
+ * Whether the state fields are readings or the driver's own bookkeeping.
150227
+ * A slice written before this field existed parses as `reported`.
150228
+ */
150229
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
150230
+ /**
150231
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
150232
+ * this is not empty; a field not listed here is a reading even then (power
150233
+ * from a running-state source, mute guessed from the last send).
150234
+ */
150235
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
150236
+ /** Volume as 0..100 inclusive. Null when the device has no volume
150237
+ * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
150238
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
150239
+ /** Mute toggle distinct from volume=0. Null when no mute surface.
150240
+ * Pair with `DeviceFeature.MediaPlayerMute`. */
150241
+ isMuted: boolean().nullable(),
150242
+ /** Active source / input. Empty when no source surface. */
150243
+ source: string(),
150244
+ /** Selectable sources. Empty when no source surface.
150245
+ * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
150246
+ availableSources: array(string()),
150247
+ /** Currently-playing media info. Null when nothing is playing. */
150248
+ currentMedia: MediaInfoSchema.nullable(),
150249
+ /** Current playback position in ms. Null when not seekable or
150250
+ * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
150251
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
150252
+ /** Total duration of the current media in ms. Null when unknown
150253
+ * (live stream) or nothing is playing. */
150254
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
150255
+ /** Shuffle toggle. Null when no shuffle surface.
150256
+ * Pair with `DeviceFeature.MediaPlayerShuffle`. */
150257
+ shuffle: boolean().nullable(),
150258
+ /** Repeat mode. Null when no repeat surface.
150259
+ * Pair with `DeviceFeature.MediaPlayerRepeat`. */
150260
+ repeat: MediaPlayerRepeatSchema.nullable(),
150261
+ /** Ms epoch when the slice was last updated. */
150262
+ lastChangedAt: number()
150263
+ });
150264
+ var stepInput = object({
150265
+ deviceId: number().int().nonnegative(),
150266
+ /** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
150267
+ steps: number().int().min(1).max(50).default(1)
150268
+ });
150269
+ var deviceOnly = object({ deviceId: number().int().nonnegative() });
150270
+ var mediaPlayerCapability = {
150271
+ name: "media-player",
150272
+ scope: "device",
150273
+ deviceNative: true,
150274
+ mode: "singleton",
150275
+ deviceTypes: [DeviceType.MediaPlayer],
150276
+ methods: {
150277
+ turnOn: method(deviceOnly, _void(), {
150278
+ kind: "mutation",
150279
+ auth: "protected"
150280
+ }),
150281
+ turnOff: method(deviceOnly, _void(), {
150282
+ kind: "mutation",
150283
+ auth: "protected"
150284
+ }),
150285
+ volumeUp: method(stepInput, _void(), {
150286
+ kind: "mutation",
150287
+ auth: "protected"
150288
+ }),
150289
+ volumeDown: method(stepInput, _void(), {
150290
+ kind: "mutation",
150291
+ auth: "protected"
150292
+ }),
150293
+ channelUp: method(stepInput, _void(), {
150294
+ kind: "mutation",
150295
+ auth: "protected"
150296
+ }),
150297
+ channelDown: method(stepInput, _void(), {
150298
+ kind: "mutation",
150299
+ auth: "protected"
150300
+ }),
150301
+ play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
150302
+ kind: "mutation",
150303
+ auth: "protected"
150304
+ }),
150305
+ pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
150306
+ kind: "mutation",
150307
+ auth: "protected"
150308
+ }),
150309
+ stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
150310
+ kind: "mutation",
150311
+ auth: "protected"
150312
+ }),
150313
+ next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
150314
+ kind: "mutation",
150315
+ auth: "protected"
150316
+ }),
150317
+ previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
150318
+ kind: "mutation",
150319
+ auth: "protected"
150320
+ }),
150321
+ seek: method(object({
150322
+ deviceId: number().int().nonnegative(),
150323
+ position: number().int().nonnegative()
150324
+ }), _void(), {
150325
+ kind: "mutation",
150326
+ auth: "protected"
150327
+ }),
150328
+ setVolume: method(object({
150329
+ deviceId: number().int().nonnegative(),
150330
+ volumeLevel: number().min(0).max(100)
150331
+ }), _void(), {
150332
+ kind: "mutation",
150333
+ auth: "protected"
150334
+ }),
150335
+ setMute: method(object({
150336
+ deviceId: number().int().nonnegative(),
150337
+ muted: boolean()
150338
+ }), _void(), {
150339
+ kind: "mutation",
150340
+ auth: "protected"
150341
+ }),
150342
+ setShuffle: method(object({
150343
+ deviceId: number().int().nonnegative(),
150344
+ shuffle: boolean()
150345
+ }), _void(), {
150346
+ kind: "mutation",
150347
+ auth: "protected"
150348
+ }),
150349
+ setRepeat: method(object({
150350
+ deviceId: number().int().nonnegative(),
150351
+ repeat: MediaPlayerRepeatSchema
150352
+ }), _void(), {
150353
+ kind: "mutation",
150354
+ auth: "protected"
150355
+ }),
150356
+ selectSource: method(object({
150357
+ deviceId: number().int().nonnegative(),
150358
+ source: string().min(1)
150359
+ }), _void(), {
150360
+ kind: "mutation",
150361
+ auth: "protected"
150362
+ }),
150363
+ playMedia: method(object({
150364
+ deviceId: number().int().nonnegative(),
150365
+ /** Media identifier / URL. */
150366
+ mediaId: string().min(1),
150367
+ /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
150368
+ * passes it through to the upstream service. */
150369
+ mediaType: string().min(1)
150370
+ }), _void(), {
150371
+ kind: "mutation",
150372
+ auth: "protected"
150373
+ })
150374
+ },
150375
+ status: {
150376
+ schema: MediaPlayerStatusSchema,
150377
+ kind: "push"
150378
+ },
150379
+ /**
150380
+ * Runtime-state slice — mirrored by the kernel. UI players read the
150381
+ * full slice for live now-playing, volume, and progress updates
150382
+ * without polling.
150383
+ */
150384
+ runtimeState: MediaPlayerStatusSchema,
150385
+ /**
150386
+ * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
150387
+ *
150388
+ * See `RuntimeStateDurability`. Enforced by
150389
+ * `scripts/check-runtime-state-durability.ts`.
150390
+ */
150391
+ durability: "session",
150392
+ /**
150393
+ * History (D850): volume (a level an operator or an automation sets; the
150394
+ * default 10 s floor folds a slider drag into its end value) and mute.
150395
+ * `position` is not recorded: it moves continuously while playing.
150396
+ */
150397
+ history: { fields: {
150398
+ volumeLevel: {
150399
+ kind: "gauge",
150400
+ unit: "%",
150401
+ deadband: 1
150402
+ },
150403
+ isMuted: { kind: "boolean" }
150404
+ } }
150405
+ };
149999
150406
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
150000
150407
  var NUMBER = {
150001
150408
  kind: "fixed",
@@ -150443,117 +150850,6 @@ function actuator(input) {
150443
150850
  };
150444
150851
  }
150445
150852
  new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
150446
- var PowerSourceRefusalReasonSchema = _enum([
150447
- "no-power-source",
150448
- "switch-missing",
150449
- "switch-offline",
150450
- "switch-unknown",
150451
- "restart-in-progress",
150452
- "switch-call-failed"
150453
- ]);
150454
- var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
150455
- accepted: literal(false),
150456
- reason: PowerSourceRefusalReasonSchema,
150457
- /** One line for the operator, naming the switch. */
150458
- message: string()
150459
- })]);
150460
- var PowerSourceInfoSchema = object({
150461
- /** False when the device has no power source linked; the rest is then empty. */
150462
- linked: boolean(),
150463
- switchDeviceId: number().nullable(),
150464
- switchName: string().nullable(),
150465
- /** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
150466
- switchOn: boolean().nullable(),
150467
- switchOnline: boolean().nullable(),
150468
- restartInProgress: boolean(),
150469
- offSeconds: number().int()
150470
- });
150471
- /** The composed slice: the switch state the link mirrors, plus the stored off time. */
150472
- var PowerSourceStateSchema = object({
150473
- /** Null while the switch has not reported: unknown is never `false` (D393). */
150474
- switchOn: boolean().nullable(),
150475
- offSeconds: number().int(),
150476
- lastFetchedAt: number()
150477
- });
150478
- var POWER_SOURCE_DEVICE_TYPES = [
150479
- DeviceType.Gateway,
150480
- DeviceType.Hub,
150481
- DeviceType.Camera,
150482
- DeviceType.Sensor,
150483
- DeviceType.MediaPlayer,
150484
- DeviceType.Vacuum,
150485
- DeviceType.Generic,
150486
- DeviceType.Washer,
150487
- DeviceType.Dryer,
150488
- DeviceType.Dishwasher,
150489
- DeviceType.PetFountain,
150490
- DeviceType.Fan,
150491
- DeviceType.Humidifier,
150492
- DeviceType.Climate,
150493
- DeviceType.WaterHeater
150494
- ];
150495
- var powerSourceCustomize = {
150496
- version: 1,
150497
- targets: [{
150498
- id: "switch",
150499
- cardinality: "scalar",
150500
- vocabulary: {
150501
- cap: "power-source",
150502
- stateField: "switchOn"
150503
- },
150504
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
150505
- apply: "add",
150506
- path: "switchOn",
150507
- sources: [{
150508
- kind: "switch",
150509
- transforms: [{ kind: "identity" }]
150510
- }],
150511
- companions: [{
150512
- kind: "constant",
150513
- path: "offSeconds",
150514
- value: 10,
150515
- when: "add"
150516
- }, {
150517
- kind: "clock",
150518
- path: "lastFetchedAt",
150519
- when: "always"
150520
- }],
150521
- sourceReport: { status: "assignment-status" }
150522
- }]
150523
- };
150524
- var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
150525
- var powerSourceCapability = {
150526
- name: "power-source",
150527
- scope: "device",
150528
- mode: "singleton",
150529
- deviceTypes: POWER_SOURCE_DEVICE_TYPES,
150530
- methods: {
150531
- getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
150532
- powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
150533
- kind: "mutation",
150534
- auth: "admin"
150535
- }),
150536
- powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
150537
- kind: "mutation",
150538
- auth: "admin"
150539
- }),
150540
- forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
150541
- kind: "mutation",
150542
- auth: "admin"
150543
- })
150544
- },
150545
- runtimeState: PowerSourceStateSchema,
150546
- customize: powerSourceCustomize,
150547
- /**
150548
- * Runtime-state durability: **session** — mirrored from the linked switch on
150549
- * its next report; a restored value would outlive the reading it names.
150550
- *
150551
- * See `RuntimeStateDurability`. Enforced by
150552
- * `scripts/check-runtime-state-durability.ts`.
150553
- */
150554
- durability: "session",
150555
- history: "none"
150556
- };
150557
150853
  DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
150558
150854
  DeviceType.Camera;
150559
150855
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
@@ -150581,6 +150877,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
150581
150877
  }).optional()
150582
150878
  })) });
150583
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
+ /**
150584
151039
  * The report of a one-shot "move this owner's plaintext secrets into the
150585
151040
  * keychain" run (D878). COUNTS only - never a value, never a ref.
150586
151041
  *
@@ -150870,6 +151325,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
150870
151325
  kind: "mutation",
150871
151326
  auth: "admin",
150872
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"
150873
151336
  }), method(object({
150874
151337
  blockId: string().min(1),
150875
151338
  trace: AutomationRunTraceSchema.optional(),
@@ -151846,7 +152309,27 @@ method(object({
151846
152309
  * exposes only the COMMON surface: link state, supported device
151847
152310
  * kinds (so the UI can filter the device picker), and per-device
151848
152311
  * expose/unexpose.
151849
- */
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
+ });
151850
152333
  var LinkStateSchema = _enum([
151851
152334
  "unlinked",
151852
152335
  "linked",
@@ -151897,10 +152380,14 @@ var ExposedDeviceSchema = object({
151897
152380
  capabilities: array(string()).optional()
151898
152381
  });
151899
152382
  var ExposeInputSchema = object({
152383
+ instanceId: InstanceIdSchema$1,
151900
152384
  deviceId: string(),
151901
152385
  capabilities: array(string()).optional()
151902
152386
  });
151903
- var UnexposeInputSchema = object({ deviceId: string() });
152387
+ var UnexposeInputSchema = object({
152388
+ instanceId: InstanceIdSchema$1,
152389
+ deviceId: string()
152390
+ });
151904
152391
  /**
151905
152392
  * What an exporter may expose besides devices (D968). An action and a
151906
152393
  * notification target are LIST ENTRIES, never devices (D954): they enter an
@@ -151929,15 +152416,21 @@ var ExposedEntitySchema = object({
151929
152416
  entity: ExportEntityRefSchema,
151930
152417
  exposedAs: string().optional()
151931
152418
  });
151932
- var ExposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
151933
- var UnexposeEntityInputSchema = object({ entity: ExportEntityRefSchema });
151934
- 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(), {
151935
152428
  kind: "mutation",
151936
152429
  auth: "admin"
151937
152430
  }), method(UnexposeInputSchema, _void(), {
151938
152431
  kind: "mutation",
151939
152432
  auth: "admin"
151940
- }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(_void(), array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
152433
+ }), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
151941
152434
  kind: "mutation",
151942
152435
  auth: "admin"
151943
152436
  }), optionalMethod(UnexposeEntityInputSchema, _void(), {
@@ -155560,15 +156053,15 @@ var FanPresetCaptureResultSchema = object({
155560
156053
  items: array(FanCaptureItemSchema),
155561
156054
  omitted: array(FanCaptureOmissionSchema)
155562
156055
  });
155563
- var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
156056
+ var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
155564
156057
  DeviceType.Fan;
155565
- 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, {
155566
156059
  kind: "mutation",
155567
156060
  auth: "admin"
155568
- }), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
156061
+ }), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
155569
156062
  kind: "mutation",
155570
156063
  auth: "protected"
155571
- }), method(deviceInput$1.extend({
156064
+ }), method(deviceInput$2.extend({
155572
156065
  fields: array(FanCaptureFieldSchema).optional(),
155573
156066
  steps: array(ActionStepSchema).max(20).default([])
155574
156067
  }), FanPresetCaptureResultSchema, { auth: "protected" });
@@ -156451,6 +156944,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
156451
156944
  }), object({ success: boolean() }), {
156452
156945
  kind: "mutation",
156453
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"
156454
156953
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
156455
156954
  addonId: string(),
156456
156955
  settings: record(string(), unknown())
@@ -156479,13 +156978,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
156479
156978
  */
156480
156979
  var ConfigSchemaPassthrough$3 = custom();
156481
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
+ });
156482
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(),
156483
157019
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
156484
- instanceMode: _enum(["single", "multiple"]),
157020
+ instanceMode: IntegrationInstanceModeSchema,
156485
157021
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
156486
157022
  kind: string(),
156487
157023
  /** True when `testInstance` really probes the remote. */
156488
- 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()
156489
157040
  });
156490
157041
  var IntegrationInstanceStatusSchema = object({
156491
157042
  state: _enum([
@@ -156494,7 +157045,9 @@ var IntegrationInstanceStatusSchema = object({
156494
157045
  "error",
156495
157046
  "unknown"
156496
157047
  ]),
156497
- 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()
156498
157051
  });
156499
157052
  var IntegrationInstanceSchema = object({
156500
157053
  id: IntegrationInstanceIdSchema,
@@ -156502,13 +157055,18 @@ var IntegrationInstanceSchema = object({
156502
157055
  /** The addon's own rule (an URL is present, a client id is present...). */
156503
157056
  configured: boolean(),
156504
157057
  /**
156505
- * The instance's own on/off switch (D996): always present, derived from the record body
156506
- * (`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.
156507
157061
  */
156508
- 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(),
156509
157065
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
156510
157066
  formValues: record(string(), unknown()),
156511
- 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()
156512
157070
  });
156513
157071
  var IntegrationInstancePatchSchema = object({
156514
157072
  id: IntegrationInstanceIdSchema,
@@ -156517,8 +157075,9 @@ var IntegrationInstancePatchSchema = object({
156517
157075
  values: record(string(), unknown())
156518
157076
  });
156519
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({
156520
- id: IntegrationInstanceIdSchema,
156521
- name: string().min(1)
157078
+ id: IntegrationInstanceIdSchema.optional(),
157079
+ name: string().min(1),
157080
+ kind: string().min(1).optional()
156522
157081
  }), object({ id: string() }), {
156523
157082
  kind: "mutation",
156524
157083
  auth: "admin"
@@ -158579,6 +159138,11 @@ var LogEntrySchema = object({
158579
159138
  * flight, and not asking for the next slice ends the scan.
158580
159139
  */
158581
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(),
158582
159146
  /** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
158583
159147
  requestId: string().min(1).max(64).optional(),
158584
159148
  /** The device the line is about (`tags.deviceId`, numeric). */
@@ -158641,6 +159205,8 @@ var LogReadResultSchema = object({
158641
159205
  /** True when `cancelRead` ended this slice early. */
158642
159206
  cancelled: boolean().optional()
158643
159207
  });
159208
+ /** Names one destination instance; absent ⇒ the provider's first readable one. */
159209
+ var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
158644
159210
  var LogReadCapabilitiesSchema = object({
158645
159211
  /** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
158646
159212
  readable: boolean(),
@@ -158659,7 +159225,7 @@ var LogReadCapabilitiesSchema = object({
158659
159225
  /** How far back the store reaches, when the provider knows (days). */
158660
159226
  retentionDays: number().nullable()
158661
159227
  });
158662
- 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() }), {
158663
159229
  kind: "mutation",
158664
159230
  auth: "admin"
158665
159231
  });
@@ -158763,214 +159329,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
158763
159329
  ]);
158764
159330
  method(_void(), array(LoginMethodContributionSchema).readonly());
158765
159331
  /**
158766
- * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
158767
- * Apple TV, Spotify, Roku, generic OTT receivers, …) on
158768
- * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
158769
- * surface — those use `DeviceType.Camera` + the broker pipeline.
158770
- *
158771
- * The cap declares a wide method surface; not every backend supports
158772
- * every method. Drivers signal sub-capability via the
158773
- * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
158774
- * corresponding method) so the UI gates buttons without a slice
158775
- * fetch — same pattern as `PanTiltZoom` for cameras.
158776
- */
158777
- var MediaPlayerStateSchema = _enum([
158778
- "off",
158779
- "on",
158780
- "idle",
158781
- "playing",
158782
- "paused",
158783
- "buffering",
158784
- "standby"
158785
- ]);
158786
- var MediaPlayerRepeatSchema = _enum([
158787
- "off",
158788
- "all",
158789
- "one"
158790
- ]);
158791
- var MediaInfoSchema = object({
158792
- /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
158793
- * `podcast`, …). */
158794
- type: string(),
158795
- /** Human-readable title. */
158796
- title: string(),
158797
- /** Optional artist / channel / station label. */
158798
- artist: string().optional(),
158799
- /** Optional album / season / show name. */
158800
- album: string().optional(),
158801
- /** Optional cover-art / thumbnail URL. */
158802
- imageUrl: string().optional()
158803
- });
158804
- /**
158805
- * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
158806
- * the device said so. `assumed`: the driver has no feedback (infrared) and
158807
- * tracks what it last SENT; it moves only after a send was accepted, and a
158808
- * human at the real remote makes it wrong. A UI shows an assumed value as a
158809
- * guess, never as a reading.
158810
- */
158811
- var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
158812
- /** The fields of a media-player status a driver can only guess. */
158813
- var MediaPlayerAssumedFieldSchema = _enum([
158814
- "state",
158815
- "volumeLevel",
158816
- "isMuted",
158817
- "source"
158818
- ]);
158819
- var MediaPlayerStatusSchema = object({
158820
- /** Playback lifecycle state. */
158821
- state: MediaPlayerStateSchema,
158822
- /**
158823
- * Whether the state fields are readings or the driver's own bookkeeping.
158824
- * A slice written before this field existed parses as `reported`.
158825
- */
158826
- stateSource: MediaPlayerStateSourceSchema.default("reported"),
158827
- /**
158828
- * Which fields are the driver's own guess. `stateSource` is `assumed` when
158829
- * this is not empty; a field not listed here is a reading even then (power
158830
- * from a running-state source, mute guessed from the last send).
158831
- */
158832
- assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
158833
- /** Volume as 0..100 inclusive. Null when the device has no volume
158834
- * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
158835
- volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
158836
- /** Mute toggle distinct from volume=0. Null when no mute surface.
158837
- * Pair with `DeviceFeature.MediaPlayerMute`. */
158838
- isMuted: boolean().nullable(),
158839
- /** Active source / input. Empty when no source surface. */
158840
- source: string(),
158841
- /** Selectable sources. Empty when no source surface.
158842
- * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
158843
- availableSources: array(string()),
158844
- /** Currently-playing media info. Null when nothing is playing. */
158845
- currentMedia: MediaInfoSchema.nullable(),
158846
- /** Current playback position in ms. Null when not seekable or
158847
- * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
158848
- position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
158849
- /** Total duration of the current media in ms. Null when unknown
158850
- * (live stream) or nothing is playing. */
158851
- duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
158852
- /** Shuffle toggle. Null when no shuffle surface.
158853
- * Pair with `DeviceFeature.MediaPlayerShuffle`. */
158854
- shuffle: boolean().nullable(),
158855
- /** Repeat mode. Null when no repeat surface.
158856
- * Pair with `DeviceFeature.MediaPlayerRepeat`. */
158857
- repeat: MediaPlayerRepeatSchema.nullable(),
158858
- /** Ms epoch when the slice was last updated. */
158859
- lastChangedAt: number()
158860
- });
158861
- var mediaPlayerCapability = {
158862
- name: "media-player",
158863
- scope: "device",
158864
- deviceNative: true,
158865
- mode: "singleton",
158866
- deviceTypes: [DeviceType.MediaPlayer],
158867
- methods: {
158868
- play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
158869
- kind: "mutation",
158870
- auth: "protected"
158871
- }),
158872
- pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
158873
- kind: "mutation",
158874
- auth: "protected"
158875
- }),
158876
- stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
158877
- kind: "mutation",
158878
- auth: "protected"
158879
- }),
158880
- next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
158881
- kind: "mutation",
158882
- auth: "protected"
158883
- }),
158884
- previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
158885
- kind: "mutation",
158886
- auth: "protected"
158887
- }),
158888
- seek: method(object({
158889
- deviceId: number().int().nonnegative(),
158890
- position: number().int().nonnegative()
158891
- }), _void(), {
158892
- kind: "mutation",
158893
- auth: "protected"
158894
- }),
158895
- setVolume: method(object({
158896
- deviceId: number().int().nonnegative(),
158897
- volumeLevel: number().min(0).max(100)
158898
- }), _void(), {
158899
- kind: "mutation",
158900
- auth: "protected"
158901
- }),
158902
- setMute: method(object({
158903
- deviceId: number().int().nonnegative(),
158904
- muted: boolean()
158905
- }), _void(), {
158906
- kind: "mutation",
158907
- auth: "protected"
158908
- }),
158909
- setShuffle: method(object({
158910
- deviceId: number().int().nonnegative(),
158911
- shuffle: boolean()
158912
- }), _void(), {
158913
- kind: "mutation",
158914
- auth: "protected"
158915
- }),
158916
- setRepeat: method(object({
158917
- deviceId: number().int().nonnegative(),
158918
- repeat: MediaPlayerRepeatSchema
158919
- }), _void(), {
158920
- kind: "mutation",
158921
- auth: "protected"
158922
- }),
158923
- selectSource: method(object({
158924
- deviceId: number().int().nonnegative(),
158925
- source: string().min(1)
158926
- }), _void(), {
158927
- kind: "mutation",
158928
- auth: "protected"
158929
- }),
158930
- playMedia: method(object({
158931
- deviceId: number().int().nonnegative(),
158932
- /** Media identifier / URL. */
158933
- mediaId: string().min(1),
158934
- /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
158935
- * passes it through to the upstream service. */
158936
- mediaType: string().min(1)
158937
- }), _void(), {
158938
- kind: "mutation",
158939
- auth: "protected"
158940
- })
158941
- },
158942
- status: {
158943
- schema: MediaPlayerStatusSchema,
158944
- kind: "push"
158945
- },
158946
- /**
158947
- * Runtime-state slice — mirrored by the kernel. UI players read the
158948
- * full slice for live now-playing, volume, and progress updates
158949
- * without polling.
158950
- */
158951
- runtimeState: MediaPlayerStatusSchema,
158952
- /**
158953
- * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
158954
- *
158955
- * See `RuntimeStateDurability`. Enforced by
158956
- * `scripts/check-runtime-state-durability.ts`.
158957
- */
158958
- durability: "session",
158959
- /**
158960
- * History (D850): volume (a level an operator or an automation sets; the
158961
- * default 10 s floor folds a slider drag into its end value) and mute.
158962
- * `position` is not recorded: it moves continuously while playing.
158963
- */
158964
- history: { fields: {
158965
- volumeLevel: {
158966
- kind: "gauge",
158967
- unit: "%",
158968
- deadband: 1
158969
- },
158970
- isMuted: { kind: "boolean" }
158971
- } }
158972
- };
158973
- /**
158974
159332
  * mesh-network — collection cap for mesh-VPN providers.
158975
159333
  *
158976
159334
  * Different shape from `network-access` (public tunnels):
@@ -167749,6 +168107,21 @@ var NcTemplateVarKindSchema = _enum([
167749
168107
  "sound",
167750
168108
  "system"
167751
168109
  ]);
168110
+ /**
168111
+ * The default title/body a rule of one SITUATION is delivered with when the
168112
+ * operator wrote no template — served so an editor can pre-fill it (and the
168113
+ * operator can rebuild it from placeholders) instead of starting from a blank
168114
+ * box that, once saved, silently replaces it. `id` names the situation inside
168115
+ * a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
168116
+ * appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
168117
+ * the hub's current language and with its overrides applied.
168118
+ */
168119
+ var NcTemplateDefaultSchema = object({
168120
+ id: _enum(["appliance-cycle"]),
168121
+ kind: NcRuleKindSchema,
168122
+ title: string(),
168123
+ body: string()
168124
+ });
167752
168125
  /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
167753
168126
  var NcTemplateFamilySchema = _enum([
167754
168127
  "rule",
@@ -168604,7 +168977,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
168604
168977
  }), method(object({}), object({
168605
168978
  catalog: array(NcConditionDescriptorSchema),
168606
168979
  taxonomy: NcTaxonomySchema.optional()
168607
- })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
168980
+ })), method(object({}), object({
168981
+ vars: array(NcTemplateVarDescriptorSchema),
168982
+ /** Per-situation default texts; absent on a hub that predates them. */
168983
+ defaults: array(NcTemplateDefaultSchema)
168984
+ })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
168608
168985
  kind: "mutation",
168609
168986
  access: "view"
168610
168987
  }), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
@@ -171241,6 +171618,14 @@ var powerMeterCapability = {
171241
171618
  deviceNative: true,
171242
171619
  mode: "singleton",
171243
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
+ } },
171244
171629
  methods: {},
171245
171630
  status: {
171246
171631
  schema: PowerMeterStatusSchema,
@@ -171300,6 +171685,117 @@ var powerMeterCapability = {
171300
171685
  volatileStateFields: ["lastFetchedAt"],
171301
171686
  customize: powerMeterCustomize
171302
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
+ };
171303
171799
  /**
171304
171800
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
171305
171801
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -175362,7 +175858,13 @@ var RemoteMediaCommandSchema = _enum([
175362
175858
  "next",
175363
175859
  "previous",
175364
175860
  "setMute",
175365
- "selectSource"
175861
+ "selectSource",
175862
+ "turnOn",
175863
+ "turnOff",
175864
+ "volumeUp",
175865
+ "volumeDown",
175866
+ "channelUp",
175867
+ "channelDown"
175366
175868
  ]);
175367
175869
  /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
175368
175870
  var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
@@ -175383,7 +175885,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
175383
175885
  }), RemoteSendResultSchema, {
175384
175886
  kind: "mutation",
175385
175887
  auth: "protected"
175386
- }), method(deviceInput.extend({
175888
+ }), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
175387
175889
  command: RemoteMediaCommandSchema,
175388
175890
  muted: boolean().optional(),
175389
175891
  source: string().optional(),
@@ -177733,7 +178235,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
177733
178235
  * `getSettingsUISchema()` surface.
177734
178236
  */
177735
178237
  var SwitchStatusSchema = object({
177736
- on: boolean().meta({ label: "On" }),
178238
+ /**
178239
+ * `null` = the device has never reported its state (a measurement that
178240
+ * has not happened is unknown, never `false`).
178241
+ */
178242
+ on: boolean().nullable().meta({ label: "On" }),
177737
178243
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
177738
178244
  lastChangedAt: number()
177739
178245
  });
@@ -177758,7 +178264,7 @@ var switchCapability = {
177758
178264
  }) },
177759
178265
  events: { onStateChanged: { data: object({
177760
178266
  deviceId: number(),
177761
- on: boolean(),
178267
+ on: boolean().nullable(),
177762
178268
  lastChangedAt: number()
177763
178269
  }) } },
177764
178270
  status: {
@@ -181770,6 +182276,26 @@ function buildStreamParamsConfigSchema(options, status) {
181770
182276
  if (sections.length === 0) return null;
181771
182277
  return { sections };
181772
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");
182298
+ DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
181773
182299
  var btn = (idSuffix, row, col, init) => ({
181774
182300
  idSuffix,
181775
182301
  row,
@@ -181897,6 +182423,34 @@ discriminatedUnion("kind", [object({
181897
182423
  code: string()
181898
182424
  })]);
181899
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
+ /**
181900
182454
  * Which of OUR object detections a clip window holds — the ONE overlap rule
181901
182455
  * and the ONE label ranking, for every clip source (D923).
181902
182456
  *
@@ -185149,6 +185703,13 @@ Object.freeze({
185149
185703
  access: "create",
185150
185704
  role: "admin"
185151
185705
  },
185706
+ "coreBlocks.proposeRunningThreshold": {
185707
+ capName: "core-blocks",
185708
+ capScope: "system",
185709
+ addonId: null,
185710
+ access: "view",
185711
+ role: "admin"
185712
+ },
185152
185713
  "coreBlocks.reportAutomationRun": {
185153
185714
  capName: "core-blocks",
185154
185715
  capScope: "system",
@@ -185660,6 +186221,13 @@ Object.freeze({
185660
186221
  access: "view",
185661
186222
  role: "admin"
185662
186223
  },
186224
+ "deviceExport.listInstances": {
186225
+ capName: "device-export",
186226
+ capScope: "system",
186227
+ addonId: null,
186228
+ access: "view",
186229
+ role: "admin"
186230
+ },
185663
186231
  "deviceExport.listSupportedDeviceKinds": {
185664
186232
  capName: "device-export",
185665
186233
  capScope: "system",
@@ -187312,6 +187880,13 @@ Object.freeze({
187312
187880
  access: "create",
187313
187881
  role: "admin"
187314
187882
  },
187883
+ "integrations.clearSettings": {
187884
+ capName: "integrations",
187885
+ capScope: "system",
187886
+ addonId: null,
187887
+ access: "delete",
187888
+ role: "admin"
187889
+ },
187315
187890
  "integrations.create": {
187316
187891
  capName: "integrations",
187317
187892
  capScope: "system",
@@ -188096,6 +188671,20 @@ Object.freeze({
188096
188671
  access: "view",
188097
188672
  role: "admin"
188098
188673
  },
188674
+ "mediaPlayer.channelDown": {
188675
+ capName: "media-player",
188676
+ capScope: "device",
188677
+ addonId: null,
188678
+ access: "create",
188679
+ role: "viewer"
188680
+ },
188681
+ "mediaPlayer.channelUp": {
188682
+ capName: "media-player",
188683
+ capScope: "device",
188684
+ addonId: null,
188685
+ access: "create",
188686
+ role: "viewer"
188687
+ },
188099
188688
  "mediaPlayer.getStatus": {
188100
188689
  capName: "media-player",
188101
188690
  capScope: "device",
@@ -188187,6 +188776,34 @@ Object.freeze({
188187
188776
  access: "create",
188188
188777
  role: "viewer"
188189
188778
  },
188779
+ "mediaPlayer.turnOff": {
188780
+ capName: "media-player",
188781
+ capScope: "device",
188782
+ addonId: null,
188783
+ access: "create",
188784
+ role: "viewer"
188785
+ },
188786
+ "mediaPlayer.turnOn": {
188787
+ capName: "media-player",
188788
+ capScope: "device",
188789
+ addonId: null,
188790
+ access: "create",
188791
+ role: "viewer"
188792
+ },
188793
+ "mediaPlayer.volumeDown": {
188794
+ capName: "media-player",
188795
+ capScope: "device",
188796
+ addonId: null,
188797
+ access: "create",
188798
+ role: "viewer"
188799
+ },
188800
+ "mediaPlayer.volumeUp": {
188801
+ capName: "media-player",
188802
+ capScope: "device",
188803
+ addonId: null,
188804
+ access: "create",
188805
+ role: "viewer"
188806
+ },
188190
188807
  "meshNetwork.getStatus": {
188191
188808
  capName: "mesh-network",
188192
188809
  capScope: "system",
@@ -191743,6 +192360,13 @@ Object.freeze({
191743
192360
  access: "create",
191744
192361
  role: "admin"
191745
192362
  },
192363
+ "remote.getAssumedStatus": {
192364
+ capName: "remote",
192365
+ capScope: "system",
192366
+ addonId: null,
192367
+ access: "view",
192368
+ role: "admin"
192369
+ },
191746
192370
  "remote.getConfig": {
191747
192371
  capName: "remote",
191748
192372
  capScope: "system",
@@ -194710,6 +195334,11 @@ Object.freeze({
194710
195334
  form: "single",
194711
195335
  optional: false
194712
195336
  }],
195337
+ "coreBlocks.proposeRunningThreshold": [{
195338
+ name: "deviceId",
195339
+ form: "single",
195340
+ optional: false
195341
+ }],
194713
195342
  "cover.close": [{
194714
195343
  name: "deviceId",
194715
195344
  form: "single",
@@ -195561,6 +196190,16 @@ Object.freeze({
195561
196190
  form: "single",
195562
196191
  optional: true
195563
196192
  }],
196193
+ "mediaPlayer.channelDown": [{
196194
+ name: "deviceId",
196195
+ form: "single",
196196
+ optional: false
196197
+ }],
196198
+ "mediaPlayer.channelUp": [{
196199
+ name: "deviceId",
196200
+ form: "single",
196201
+ optional: false
196202
+ }],
195564
196203
  "mediaPlayer.getStatus": [{
195565
196204
  name: "deviceId",
195566
196205
  form: "single",
@@ -195626,6 +196265,26 @@ Object.freeze({
195626
196265
  form: "single",
195627
196266
  optional: false
195628
196267
  }],
196268
+ "mediaPlayer.turnOff": [{
196269
+ name: "deviceId",
196270
+ form: "single",
196271
+ optional: false
196272
+ }],
196273
+ "mediaPlayer.turnOn": [{
196274
+ name: "deviceId",
196275
+ form: "single",
196276
+ optional: false
196277
+ }],
196278
+ "mediaPlayer.volumeDown": [{
196279
+ name: "deviceId",
196280
+ form: "single",
196281
+ optional: false
196282
+ }],
196283
+ "mediaPlayer.volumeUp": [{
196284
+ name: "deviceId",
196285
+ form: "single",
196286
+ optional: false
196287
+ }],
195629
196288
  "meter.getStatus": [{
195630
196289
  name: "deviceId",
195631
196290
  form: "single",
@@ -196791,6 +197450,11 @@ Object.freeze({
196791
197450
  form: "single",
196792
197451
  optional: true
196793
197452
  }],
197453
+ "remote.getAssumedStatus": [{
197454
+ name: "deviceId",
197455
+ form: "single",
197456
+ optional: false
197457
+ }],
196794
197458
  "remote.getConfig": [{
196795
197459
  name: "deviceId",
196796
197460
  form: "single",
@@ -197338,99 +198002,6 @@ Object.freeze({
197338
198002
  "network-access": "ingress",
197339
198003
  "smtp-provider": "email"
197340
198004
  });
197341
- /**
197342
- * A saved Customize assignment (D936). The schema is the type: callers use
197343
- * `z.infer`, they do not restate the shape.
197344
- */
197345
- var OverrideSourceSchema = object({
197346
- addonId: string().min(1),
197347
- stableId: string().min(1),
197348
- cap: string().min(1),
197349
- fieldPath: string().min(1)
197350
- });
197351
- var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
197352
- object({ kind: literal("identity") }),
197353
- object({ kind: literal("invert") }),
197354
- object({
197355
- kind: literal("threshold"),
197356
- comparison: _enum(["gte", "lte"]),
197357
- threshold: number(),
197358
- startDwellMs: number().nonnegative(),
197359
- stopDwellMs: number().nonnegative()
197360
- }),
197361
- object({
197362
- kind: literal("linear"),
197363
- scale: number(),
197364
- offset: number(),
197365
- clampMin: number().optional(),
197366
- clampMax: number().optional()
197367
- }),
197368
- object({
197369
- kind: literal("enum-map"),
197370
- mapping: record(string(), string().nullable())
197371
- }),
197372
- object({ kind: literal("number") })
197373
- ]);
197374
- var ScalarOverrideDraftSchema = object({
197375
- kind: literal("scalar"),
197376
- cap: string().min(1),
197377
- targetId: string().min(1),
197378
- source: OverrideSourceSchema,
197379
- transform: CustomizeTransformDraftSchema
197380
- });
197381
- var KeyedArrayOverrideDraftSchema = object({
197382
- kind: literal("keyedArray"),
197383
- cap: string().min(1),
197384
- targetId: string().min(1),
197385
- key: string().min(1),
197386
- label: string().min(1),
197387
- fieldPath: string().min(1),
197388
- source: OverrideSourceSchema,
197389
- transform: CustomizeTransformDraftSchema
197390
- });
197391
- var PulseOverrideDraftSchema = object({
197392
- kind: literal("pulse"),
197393
- cap: string().min(1),
197394
- targetId: string().min(1),
197395
- enabled: boolean(),
197396
- debounceMs: number().nonnegative(),
197397
- edges: array(_enum([
197398
- "rising",
197399
- "button",
197400
- "native-event"
197401
- ])).min(1).optional(),
197402
- sources: array(OverrideSourceSchema).min(1)
197403
- });
197404
- var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
197405
- "in-addition",
197406
- "confirm",
197407
- "replace"
197408
- ]).optional() });
197409
- discriminatedUnion("kind", [
197410
- ScalarOverrideDraftSchema,
197411
- KeyedArrayOverrideDraftSchema,
197412
- PulseOverrideDraftSchema,
197413
- object({
197414
- kind: literal("span"),
197415
- cap: string().min(1),
197416
- targetId: string().min(1),
197417
- timeoutMs: number().nonnegative(),
197418
- cooldownMs: number().nonnegative().optional(),
197419
- sources: array(SpanSourceDraftSchema)
197420
- })
197421
- ]);
197422
- object({
197423
- targetId: string().min(1),
197424
- configured: boolean(),
197425
- source: _enum(["native", "assigned"]),
197426
- state: _enum([
197427
- "active",
197428
- "stale",
197429
- "released",
197430
- "unavailable"
197431
- ]),
197432
- reason: string().nullable()
197433
- });
197434
198005
  var G711_SCALE_CORRECTION_DB = {
197435
198006
  PCMU: 20 * Math.log10(4),
197436
198007
  PCMA: 20 * Math.log10(8)
@@ -198046,6 +198617,7 @@ var STATE_SUMMARY_VARS = [
198046
198617
  [...RULE_TEXT],
198047
198618
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
198048
198619
  ([...RULE_TEXT], [...COMBINED_ALARM]),
198620
+ ([...RULE_TEXT], [...COMBINED_ALARM]),
198049
198621
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
198050
198622
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
198051
198623
  ([...RULE_TEXT], [...CONSUMABLE]),
@@ -208245,7 +208817,7 @@ var PirAccessory = class extends BaseDevice {
208245
208817
  async applySettingsPatch(patch) {
208246
208818
  await this.config.setAll(patch);
208247
208819
  if ("pirSensitive" in patch || "pirReduceAlarm" in patch || "pirIntervalS" in patch) try {
208248
- await this.pushConfigToFirmware(this.switchView().on);
208820
+ await this.pushConfigToFirmware(this.switchView().on === true);
208249
208821
  } catch (err) {
208250
208822
  this.ctx.logger.warn("pir firmware push failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
208251
208823
  throw err;
@@ -219441,6 +220013,285 @@ async function startLegacyWebhookIntake(deps) {
219441
220013
  };
219442
220014
  }
219443
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
219444
220295
  //#region src/migrations/drop-webhook-intake-settings.ts
219445
220296
  /**
219446
220297
  * The camera webhook is a plane of the addon's HTTP data plane, not a private listener
@@ -219565,6 +220416,18 @@ var REOLINK_MIGRATIONS = [{
219565
220416
  },
219566
220417
  plan: planDropWebhookIntakeSettings
219567
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
+ });
219568
220431
  //#endregion
219569
220432
  //#region src/reolink-adoption-provider.ts
219570
220433
  var DEVICES_FILTER = {
@@ -219730,113 +220593,6 @@ function buildReolinkAdoptionProvider(deps) {
219730
220593
  };
219731
220594
  }
219732
220595
  //#endregion
219733
- //#region src/reolink-discovery-map.ts
219734
- /**
219735
- * Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
219736
- * discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
219737
- */
219738
- function slugifyReolinkHost(host) {
219739
- return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
219740
- }
219741
- /**
219742
- * Decide whether a discovered host is a device this provider already owns.
219743
- *
219744
- * Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
219745
- * wins even when the host differs, because the address is the thing that moves.
219746
- */
219747
- function findOnboardedMatch(discovered, onboarded) {
219748
- if (discovered.uid !== void 0 && discovered.uid !== "") {
219749
- const byUid = onboarded.find((o) => o.uid === discovered.uid);
219750
- if (byUid !== void 0) return byUid;
219751
- }
219752
- return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
219753
- }
219754
- /** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
219755
- var HUB_BAICHUAN_PORT = 9e3;
219756
- /**
219757
- * `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
219758
- * rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
219759
- * as a camera to add.
219760
- */
219761
- function looksLikeReolinkHub(model) {
219762
- if (model === void 0) return false;
219763
- return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
219764
- }
219765
- /**
219766
- * The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
219767
- * second. The boot migration derives the SAME id for an old Hub device, so the integration it
219768
- * creates hides this candidate through `originStableId`.
219769
- */
219770
- function reolinkHubStableId(identity) {
219771
- const uid = identity.uid?.trim() ?? "";
219772
- if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
219773
- return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
219774
- }
219775
- function isKnownHub(d, known) {
219776
- return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
219777
- }
219778
- /**
219779
- * Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
219780
- * same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
219781
- * first responder for a host wins. Pure: no I/O.
219782
- *
219783
- * **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
219784
- * (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
219785
- * that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
219786
- * pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
219787
- * closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
219788
- * child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
219789
- * never a scan result.
219790
- *
219791
- * The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
219792
- * can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
219793
- * which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
219794
- * in the first place, so a scan says so instead of offering it again.
219795
- */
219796
- function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
219797
- const username = credentials.username?.trim() ?? "";
219798
- const password = credentials.password ?? "";
219799
- const byHost = /* @__PURE__ */ new Map();
219800
- for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
219801
- return [...byHost.values()].flatMap((d) => {
219802
- const displayName = d.name ?? d.model ?? d.host;
219803
- if (looksLikeReolinkHub(d.model)) {
219804
- if (isKnownHub(d, hubIntegrations)) return [];
219805
- return [{
219806
- stableId: reolinkHubStableId(d),
219807
- type: DeviceType.Gateway,
219808
- suggestedName: displayName,
219809
- prefilledConfig: {
219810
- host: d.host,
219811
- port: HUB_BAICHUAN_PORT,
219812
- ...d.uid ? { uid: d.uid } : {},
219813
- ...username ? { username } : {}
219814
- },
219815
- setupIntegration: true
219816
- }];
219817
- }
219818
- const match = findOnboardedMatch(d, onboarded);
219819
- return [{
219820
- stableId: `host-${slugifyReolinkHost(d.host)}`,
219821
- type: DeviceType.Camera,
219822
- suggestedName: displayName,
219823
- prefilledConfig: {
219824
- name: displayName,
219825
- host: d.host,
219826
- transport: "auto",
219827
- ...d.uid ? { uid: d.uid } : {},
219828
- ...username ? { username } : {},
219829
- ...password ? { password } : {}
219830
- },
219831
- ...match !== void 0 ? {
219832
- alreadyOnboarded: true,
219833
- onboardedDeviceId: match.deviceId,
219834
- onboardedName: match.name
219835
- } : {}
219836
- }];
219837
- });
219838
- }
219839
- //#endregion
219840
220596
  //#region src/reolink-gateway-device.ts
219841
220597
  var reolinkGatewayDeviceSchema = object({
219842
220598
  integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
@@ -219993,158 +220749,6 @@ function createReolinkHubManager(deps, registry) {
219993
220749
  connect: connectReolinkHub
219994
220750
  });
219995
220751
  }
219996
- function positivePort(value) {
219997
- return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
219998
- }
219999
- var CONNECTION_FIELDS = [
220000
- "host",
220001
- "port",
220002
- "username",
220003
- "transport"
220004
- ];
220005
- function str(value) {
220006
- return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
220007
- }
220008
- function migratedChildConfig(child, hub, integrationId) {
220009
- const filled = {};
220010
- for (const key of CONNECTION_FIELDS) {
220011
- const own = child[key];
220012
- const inherited = hub[key];
220013
- if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
220014
- }
220015
- return {
220016
- ...child,
220017
- ...filled,
220018
- integrationId
220019
- };
220020
- }
220021
- async function migrateOneHub(hub, children, integrations, ports) {
220022
- const read = readHubConnection(hub.config);
220023
- if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
220024
- const { connection } = read;
220025
- const uid = str(hub.config["uid"]);
220026
- const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
220027
- addonId: ports.addonId,
220028
- name: hub.name,
220029
- info: { ["migratedFromHubDeviceId"]: hub.id },
220030
- originStableId: reolinkHubStableId({
220031
- ...uid !== void 0 ? { uid } : {},
220032
- host: connection.host
220033
- }),
220034
- settings: {
220035
- host: connection.host,
220036
- port: connection.port,
220037
- rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
220038
- username: connection.username,
220039
- password: connection.password,
220040
- transport: connection.transport,
220041
- ...uid !== void 0 ? { uid } : {},
220042
- debugGeneral: connection.debugGeneral,
220043
- debugSocketLogs: connection.debugSocketLogs
220044
- },
220045
- secretKeys: ["password"]
220046
- })).id;
220047
- const rewritten = [];
220048
- for (const child of children) {
220049
- const config = migratedChildConfig(child.config, hub.config, integrationId);
220050
- await ports.persistConfig(child.stableId, config);
220051
- await ports.setIntegrationId(child.id, integrationId);
220052
- rewritten.push({
220053
- ...child,
220054
- config,
220055
- parentDeviceId: null
220056
- });
220057
- }
220058
- return {
220059
- rows: rewritten,
220060
- retirement: {
220061
- hubDeviceId: hub.id,
220062
- hubName: hub.name,
220063
- integrationId
220064
- }
220065
- };
220066
- }
220067
- /** Plan and persist the migration of every Hub row in `saved`. Never throws. */
220068
- async function planHubMigration(saved, ports) {
220069
- const hubs = saved.filter((row) => row.type === DeviceType.Hub);
220070
- if (hubs.length === 0) return {
220071
- rows: saved,
220072
- retirements: []
220073
- };
220074
- let integrations;
220075
- try {
220076
- integrations = await ports.listIntegrations();
220077
- } catch (err) {
220078
- ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
220079
- return {
220080
- rows: saved.filter((row) => row.type !== DeviceType.Hub),
220081
- retirements: []
220082
- };
220083
- }
220084
- const hubIds = new Set(hubs.map((h) => h.id));
220085
- const rewrittenById = /* @__PURE__ */ new Map();
220086
- const retirements = [];
220087
- for (const hub of hubs) {
220088
- const children = saved.filter((row) => row.parentDeviceId === hub.id);
220089
- try {
220090
- const done = await migrateOneHub(hub, children, integrations, ports);
220091
- for (const row of done.rows) rewrittenById.set(row.id, row);
220092
- retirements.push(done.retirement);
220093
- ports.logger.info("Reolink hub migrated to an integration", {
220094
- tags: { deviceId: hub.id },
220095
- meta: {
220096
- integrationId: done.retirement.integrationId,
220097
- children: children.length,
220098
- name: hub.name
220099
- }
220100
- });
220101
- } catch (err) {
220102
- ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
220103
- tags: { deviceId: hub.id },
220104
- meta: {
220105
- error: errMsg$3(err),
220106
- name: hub.name
220107
- }
220108
- });
220109
- }
220110
- }
220111
- return {
220112
- rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
220113
- retirements
220114
- };
220115
- }
220116
- /**
220117
- * Retire the hubs whose children all restored. A hub that still has a child in the store is left
220118
- * and named: removing it would cascade the camera away with it.
220119
- */
220120
- async function retireMigratedHubs(retirements, ports) {
220121
- let retired = 0;
220122
- for (const r of retirements) try {
220123
- const remaining = await ports.childIdsOf(r.hubDeviceId);
220124
- if (remaining.length > 0) {
220125
- ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
220126
- tags: { deviceId: r.hubDeviceId },
220127
- meta: {
220128
- remaining: [...remaining],
220129
- integrationId: r.integrationId
220130
- }
220131
- });
220132
- continue;
220133
- }
220134
- await ports.removeDevice(r.hubDeviceId);
220135
- retired++;
220136
- ports.logger.info("Reolink hub device retired", {
220137
- tags: { deviceId: r.hubDeviceId },
220138
- meta: { integrationId: r.integrationId }
220139
- });
220140
- } catch (err) {
220141
- ports.logger.warn("Reolink hub retirement failed — retried next boot", {
220142
- tags: { deviceId: r.hubDeviceId },
220143
- meta: { error: errMsg$3(err) }
220144
- });
220145
- }
220146
- return retired;
220147
- }
220148
220752
  //#endregion
220149
220753
  //#region src/reolink-subnet-scan.ts
220150
220754
  /**
@@ -220963,7 +221567,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
220963
221567
  this.wireClipCatalogRefreshers();
220964
221568
  await this.serveClipPlanes(this.clipService);
220965
221569
  await this.serveWebhookPlane();
220966
- 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
+ }))]);
220967
221575
  this.wireEmailBridge();
220968
221576
  const wireSleepPushResolvers = (event, via) => {
220969
221577
  const data = event.data;