@camstack/addon-provider-homeassistant 1.2.647 → 1.2.648

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.648] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - Home Assistant export: the Hub URL setting is gone; each Home Assistant instance now chooses whether its image links use the Internal address or the Public endpoint of the Network page.
15
+
10
16
  ## [1.2.647] - 2026-11-02
11
17
 
12
18
  ### Changed
package/README.md CHANGED
@@ -23,7 +23,7 @@ Edit these from System, Addons, this addon, Settings (a device's own settings ar
23
23
  - Home Assistant broker: Brokers are managed separately
24
24
  - Home Assistant connection: Auth mode, HA base URL, Long-lived access token, Supervisor token
25
25
  - Home Assistant instances: No brokers
26
- - Connection: Hub URL Home Assistant can reach
26
+ - Connection: per Home Assistant instance, the hub address role its image links use (Internal, or Public endpoint)
27
27
  - Advanced: Reconcile interval (s)
28
28
 
29
29
  ## Requirements
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_ha_broker_source = require("./ha-broker-source-BFFi4NDH.js");
2
+ const require_ha_broker_source = require("./ha-broker-source-Ba20mhmb.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-admin-link.ts
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as humiditySensorCapability, $t as number, A as brokerCapability, At as temperatureSensorCapability, Bt as RecordSecretUnavailableError, C as alarmPanelCapability, Ct as runningFromVacuumState, D as bestLocationMatch, Dt as smokeCapability, E as batteryCapability, F as carbonMonoxideCapability, Ft as valveCapability, G as deviceAdoptionCapability, Gt as entryBody, H as controlCapability, Ht as createEvent, I as climateControlCapability, It as vibrationCapability, J as eventEmitterCapability, Jt as _enum, L as colorCapability, Lt as waterHeaterCapability, M as buildDeviceAdminUrl, Mt as tryConvertUnit, N as buttonCapability, Nt as updateCapability, O as binaryCapability, Ot as switchCapability, P as calendarSourceCapability, Pt as vacuumControlCapability, Q as humidifierCapability, Qt as literal, R as connectivityCapability, Rt as weatherCapability, S as airQualitySensorCapability, St as runningFromMediaPlayer, T as automationControlCapability, Tt as scriptRunnerCapability, U as coverCapability, Ut as defineEntryKind, V as contactCapability, Vt as baseUnitOf, W as deviceAdminLinkCapability, Wt as defineRecordKinds, X as floodCapability, Xt as boolean, Y as fanControlCapability, Yt as array, Z as gasCapability, Zt as discriminatedUnion, _t as publicEndpointReaderFrom, a as BaseDevice, an as url, at as motionCapability, b as accessoriesCapability, cn as DeviceType, ct as notifierCapability, dt as parseAdminBase, en as object, et as imageCapability, ft as powerMeterCapability, ht as pressureSensorCapability, in as unknown, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as tryConvertDelta, k as brightnessCapability, kt as tamperCapability, ln as errMsg, lt as numericSensorCapability, mt as presenceCapability, nn as string, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as DeviceFeature, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, pt as prepareNotification, q as enumSensorCapability, rn as union, rt as lockControlCapability, sn as DeviceRole, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as record, tt as ingestReading, un as EventCategory, v as RUNNING_STATE_COLD, vt as quantityFromUnit, w as ambientLightSensorCapability, wt as runningStateCapability, x as addonRoutesCapability, xt as runningFromLawnMower, y as TargetSchema, yt as resolveAttachmentUrls } from "./ha-broker-source-B6wqfEUz.mjs";
1
+ import { $ as humiditySensorCapability, $t as discriminatedUnion, A as brokerCapability, At as tamperCapability, C as alarmPanelCapability, Ct as runningFromMediaPlayer, D as bestLocationMatch, E as batteryCapability, Et as scriptRunnerCapability, F as carbonMonoxideCapability, Ft as vacuumControlCapability, G as deviceAdoptionCapability, Gt as defineEntryKind, H as controlCapability, Ht as baseUnitOf, I as climateControlCapability, It as valveCapability, J as eventEmitterCapability, Kt as defineRecordKinds, L as colorCapability, Lt as vibrationCapability, M as buildDeviceAdminUrl, Mt as tryConvertDelta, N as buttonCapability, Nt as tryConvertUnit, O as binaryCapability, Ot as smokeCapability, P as calendarSourceCapability, Pt as updateCapability, Q as humidifierCapability, Qt as boolean, R as connectivityCapability, Rt as waterHeaterCapability, S as airQualitySensorCapability, St as runningFromLawnMower, T as automationControlCapability, Tt as runningStateCapability, U as coverCapability, Ut as createEvent, V as contactCapability, Vt as RecordSecretUnavailableError, W as deviceAdminLinkCapability, X as floodCapability, Xt as _enum, Y as fanControlCapability, Z as gasCapability, Zt as array, a as BaseDevice, an as union, at as motionCapability, b as accessoriesCapability, bt as resolveAttachmentUrls, cn as DeviceFeature, ct as notifierCapability, dn as errMsg, dt as parseAdminBase, en as literal, et as imageCapability, fn as EventCategory, ft as powerMeterCapability, gt as pressureSensorCapability, ht as presenceCapability, in as string, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as temperatureSensorCapability, k as brightnessCapability, kt as switchCapability, ln as DeviceRole, lt as numericSensorCapability, mt as prepareNotification, nn as object, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as unknown, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, q as enumSensorCapability, qt as entryBody, rn as record, rt as lockControlCapability, sn as url, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as number, tt as ingestReading, un as DeviceType, v as RUNNING_STATE_COLD, vt as publicEndpointReaderFrom, w as ambientLightSensorCapability, wt as runningFromVacuumState, x as addonRoutesCapability, y as TargetSchema, yt as quantityFromUnit, zt as weatherCapability } from "./ha-broker-source-iLA_DsW8.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-admin-link.ts
@@ -5972,7 +5972,7 @@ var ZodIssueCode = {
5972
5972
  var ZodFirstPartyTypeKind;
5973
5973
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5974
5974
  //#endregion
5975
- //#region ../types/dist/sleep-B-vp7x15.mjs
5975
+ //#region ../types/dist/sleep-Bei295Bz.mjs
5976
5976
  /**
5977
5977
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5978
5978
  * window to float samples (D455).
@@ -6488,6 +6488,53 @@ function emitReadiness(bus, params) {
6488
6488
  ts
6489
6489
  }));
6490
6490
  }
6491
+ /** Why a catalogue entry is not a runnable body of its declared kind. */
6492
+ var AddonMigrationDefinitionError = class extends Error {
6493
+ id;
6494
+ constructor(id, message) {
6495
+ super(`${id}: ${message}`);
6496
+ this.id = id;
6497
+ this.name = "AddonMigrationDefinitionError";
6498
+ }
6499
+ };
6500
+ /**
6501
+ * Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
6502
+ * an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
6503
+ * body). A throwing body never escapes `execute`: it is reported as a failed result.
6504
+ */
6505
+ function defineAddonMigrations(catalogue, ports) {
6506
+ return catalogue.map((entry) => {
6507
+ const { meta } = entry;
6508
+ if ("plan" in entry) {
6509
+ if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
6510
+ return {
6511
+ meta,
6512
+ execute: () => guarded(async () => ({
6513
+ kind: "plan",
6514
+ plan: await entry.plan(ports)
6515
+ }))
6516
+ };
6517
+ }
6518
+ if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
6519
+ return {
6520
+ meta,
6521
+ execute: () => guarded(async () => ({
6522
+ kind: "procedure",
6523
+ result: await entry.procedure(ports)
6524
+ }))
6525
+ };
6526
+ });
6527
+ }
6528
+ async function guarded(work) {
6529
+ try {
6530
+ return await work();
6531
+ } catch (err) {
6532
+ return {
6533
+ kind: "failed",
6534
+ error: errMsg(err)
6535
+ };
6536
+ }
6537
+ }
6491
6538
  var NOT_RUN = {
6492
6539
  declared: false,
6493
6540
  snapshot: null,
@@ -14124,6 +14171,16 @@ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
14124
14171
  precondition: PreconditionSchema
14125
14172
  })
14126
14173
  ])) });
14174
+ /** `JSON.stringify` with object keys sorted, so equal values spell the same. */
14175
+ function canonicalJson(value) {
14176
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
14177
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
14178
+ return JSON.stringify(value) ?? "null";
14179
+ }
14180
+ /** The precondition string for a value a plan read (`null` when it read nothing). */
14181
+ function preconditionOf(value) {
14182
+ return canonicalJson(value ?? null);
14183
+ }
14127
14184
  /**
14128
14185
  * What a `procedure` body reports. `already-done` means the rerun observed the
14129
14186
  * work finished (a crash between the work and the ledger row, or a manual fix)
@@ -16072,8 +16129,18 @@ var LogChannelDescriptorSchema = object({
16072
16129
  * the operator narrows to one camera, sees nothing, and concludes the code
16073
16130
  * path was never taken.
16074
16131
  */
16075
- perDevice: boolean()
16076
- });
16132
+ perDevice: boolean(),
16133
+ /**
16134
+ * What the channel is FOR, so the UI can list the channels an addon offers.
16135
+ *
16136
+ * - `diagnostic` (the default): a per-component diagnostic armed from the
16137
+ * Levels page, emitted at its declared level.
16138
+ * - `debug`: a `debug.<channel>` dump watched live for a short window
16139
+ * (`ctx.debug`). Always per-device, and its lines are denied by the log
16140
+ * ring unless somebody is watching that device.
16141
+ */
16142
+ kind: _enum(["diagnostic", "debug"]).default("diagnostic")
16143
+ }).refine((d) => d.kind !== "debug" || d.name.startsWith("debug.") && d.perDevice, { message: "a debug channel is named debug.<channel> and is per-device" });
16077
16144
  /**
16078
16145
  * An armed window over one channel, as the document hands it to a mirror.
16079
16146
  *
@@ -51615,6 +51682,45 @@ var deviceProviderCapability = {
51615
51682
  }
51616
51683
  };
51617
51684
  /**
51685
+ * What the passive listener of `local-network` saw on the wire, before any
51686
+ * provider classified it. Device-manager turns an observation into the hits
51687
+ * of the providers whose declared discovery modes it matches.
51688
+ */
51689
+ var DiscoveryObservationSchema = discriminatedUnion("kind", [object({
51690
+ kind: literal("mdns"),
51691
+ /** Normalised DNS-SD service type, e.g. `_esphomelib._tcp`. */
51692
+ serviceType: string(),
51693
+ instance: string(),
51694
+ host: string(),
51695
+ /** The A record address when the announcement carried one. */
51696
+ address: string().nullable(),
51697
+ port: number().int(),
51698
+ txt: record(string(), string())
51699
+ }), object({
51700
+ kind: literal("ssdp"),
51701
+ /** Sender address of the datagram. */
51702
+ host: string(),
51703
+ port: number().int(),
51704
+ /** `NT` of a NOTIFY, `ST` of a search response. */
51705
+ target: string(),
51706
+ headers: record(string(), string())
51707
+ })]);
51708
+ object({
51709
+ addonId: string(),
51710
+ candidate: DiscoveryCandidateSchema,
51711
+ firstSeenAt: number(),
51712
+ lastSeenAt: number(),
51713
+ dismissed: boolean()
51714
+ });
51715
+ var DiscoveryProposalSchema = object({
51716
+ id: string(),
51717
+ addonId: string(),
51718
+ candidate: DiscoveryCandidateSchema,
51719
+ firstSeenAt: number(),
51720
+ lastSeenAt: number(),
51721
+ dismissed: boolean()
51722
+ });
51723
+ /**
51618
51724
  * Device Manager capability — hub-side singleton that unifies device persistence,
51619
51725
  * live registry access, and all management operations into a single tRPC surface.
51620
51726
  *
@@ -52343,6 +52449,19 @@ method(object({
52343
52449
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
52344
52450
  kind: "mutation",
52345
52451
  auth: "admin"
52452
+ }), method(object({}), object({ proposals: array(DiscoveryProposalSchema).readonly() }), { auth: "admin" }), method(object({ id: string() }), object({ dismissed: boolean() }), {
52453
+ kind: "mutation",
52454
+ auth: "admin"
52455
+ }), method(object({ observations: array(DiscoveryObservationSchema).max(512) }), object({ proposed: number().int() }), {
52456
+ kind: "mutation",
52457
+ auth: "admin"
52458
+ }), method(object({}), object({
52459
+ providers: number().int(),
52460
+ proposed: number().int()
52461
+ }), {
52462
+ kind: "mutation",
52463
+ auth: "admin",
52464
+ timeoutMs: 12e4
52346
52465
  }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
52347
52466
  deviceId: number(),
52348
52467
  key: string(),
@@ -76690,6 +76809,40 @@ var LoggingSettingsStateSchema = object({
76690
76809
  opsLogRetentionDays: number().int().min(0),
76691
76810
  persisted: boolean()
76692
76811
  });
76812
+ /**
76813
+ * Debug WATCH — a short, per-device window over `debug.<channel>` lines.
76814
+ *
76815
+ * `start` opens (or re-arms) a 10 minute window; `rearm` extends a LIVE one
76816
+ * and never revives a lapsed one; `stop` closes it; `list` only reads. No
76817
+ * chain of re-arms outlives 60 minutes from the first start. The hub holds the
76818
+ * watches in memory and pushes the interest to every node, so a watch lapses
76819
+ * by itself and survives nothing — a restart is a stop.
76820
+ */
76821
+ var DebugWatchInputSchema = object({
76822
+ action: _enum([
76823
+ "list",
76824
+ "start",
76825
+ "rearm",
76826
+ "stop"
76827
+ ]),
76828
+ /** Full channel names, `debug.<channel>`, each declared with `kind: 'debug'`. */
76829
+ channels: array(string().min(1)).readonly().default([]),
76830
+ /** Numeric device ids. Required (non-empty) for `start`. */
76831
+ deviceIds: array(number().int()).readonly().default([])
76832
+ });
76833
+ var DebugWatchStateSchema = object({
76834
+ channel: string(),
76835
+ deviceIds: array(number().int()).readonly(),
76836
+ startedAtMs: number(),
76837
+ untilMs: number(),
76838
+ capAtMs: number()
76839
+ });
76840
+ var DebugWatchResultSchema = object({
76841
+ nowMs: number(),
76842
+ watches: array(DebugWatchStateSchema).readonly(),
76843
+ /** Channels a `rearm` found already lapsed: nothing was revived. */
76844
+ lapsed: array(string()).readonly()
76845
+ });
76693
76846
  method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
76694
76847
  kind: "mutation",
76695
76848
  auth: "admin"
@@ -76705,6 +76858,9 @@ method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_
76705
76858
  }), method(_void(), RequestCensusStatusSchema, { auth: "admin" }), method(_void(), array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }), method(_void(), array(ReportedFailureContributionSchema).readonly(), { auth: "admin" }), method(GetLoggingSettingsInputSchema, LoggingSettingsStateSchema, { auth: "admin" }), method(SetLoggingSettingsInputSchema, LoggingSettingsStateSchema, {
76706
76859
  kind: "mutation",
76707
76860
  auth: "admin"
76861
+ }), method(DebugWatchInputSchema, DebugWatchResultSchema, {
76862
+ kind: "mutation",
76863
+ auth: "admin"
76708
76864
  });
76709
76865
  /**
76710
76866
  * Tamper / case-open detection sensor. Drives Home Assistant
@@ -83814,6 +83970,13 @@ Object.freeze({
83814
83970
  access: "view",
83815
83971
  role: "admin"
83816
83972
  },
83973
+ "deviceManager.dismissDiscoveryProposal": {
83974
+ capName: "device-manager",
83975
+ capScope: "system",
83976
+ addonId: null,
83977
+ access: "create",
83978
+ role: "admin"
83979
+ },
83817
83980
  "deviceManager.enable": {
83818
83981
  capName: "device-manager",
83819
83982
  capScope: "system",
@@ -83996,6 +84159,13 @@ Object.freeze({
83996
84159
  access: "view",
83997
84160
  role: "viewer"
83998
84161
  },
84162
+ "deviceManager.listDiscoveryProposals": {
84163
+ capName: "device-manager",
84164
+ capScope: "system",
84165
+ addonId: null,
84166
+ access: "view",
84167
+ role: "admin"
84168
+ },
83999
84169
  "deviceManager.listLocationDescriptions": {
84000
84170
  capName: "device-manager",
84001
84171
  capScope: "system",
@@ -84150,6 +84320,13 @@ Object.freeze({
84150
84320
  access: "create",
84151
84321
  role: "admin"
84152
84322
  },
84323
+ "deviceManager.reportDiscoveryObservations": {
84324
+ capName: "device-manager",
84325
+ capScope: "system",
84326
+ addonId: null,
84327
+ access: "create",
84328
+ role: "admin"
84329
+ },
84153
84330
  "deviceManager.runDeviceAction": {
84154
84331
  capName: "device-manager",
84155
84332
  capScope: "system",
@@ -84157,6 +84334,13 @@ Object.freeze({
84157
84334
  access: "create",
84158
84335
  role: "admin"
84159
84336
  },
84337
+ "deviceManager.runDiscoverySweep": {
84338
+ capName: "device-manager",
84339
+ capScope: "system",
84340
+ addonId: null,
84341
+ access: "create",
84342
+ role: "admin"
84343
+ },
84160
84344
  "deviceManager.setChildLayout": {
84161
84345
  capName: "device-manager",
84162
84346
  capScope: "system",
@@ -90744,6 +90928,13 @@ Object.freeze({
90744
90928
  access: "create",
90745
90929
  role: "viewer"
90746
90930
  },
90931
+ "system.debugWatch": {
90932
+ capName: "system",
90933
+ capScope: "system",
90934
+ addonId: null,
90935
+ access: "create",
90936
+ role: "admin"
90937
+ },
90747
90938
  "system.detectSiteLocation": {
90748
90939
  capName: "system",
90749
90940
  capScope: "system",
@@ -94673,6 +94864,11 @@ Object.freeze({
94673
94864
  form: "single",
94674
94865
  optional: false
94675
94866
  }],
94867
+ "system.debugWatch": [{
94868
+ name: "deviceIds",
94869
+ form: "array",
94870
+ optional: true
94871
+ }],
94676
94872
  "tamper.getStatus": [{
94677
94873
  name: "deviceId",
94678
94874
  form: "single",
@@ -97157,6 +97353,12 @@ Object.defineProperty(exports, "createEvent", {
97157
97353
  return createEvent;
97158
97354
  }
97159
97355
  });
97356
+ Object.defineProperty(exports, "defineAddonMigrations", {
97357
+ enumerable: true,
97358
+ get: function() {
97359
+ return defineAddonMigrations;
97360
+ }
97361
+ });
97160
97362
  Object.defineProperty(exports, "defineEntryKind", {
97161
97363
  enumerable: true,
97162
97364
  get: function() {
@@ -97361,6 +97563,12 @@ Object.defineProperty(exports, "powerMeterCapability", {
97361
97563
  return powerMeterCapability;
97362
97564
  }
97363
97565
  });
97566
+ Object.defineProperty(exports, "preconditionOf", {
97567
+ enumerable: true,
97568
+ get: function() {
97569
+ return preconditionOf;
97570
+ }
97571
+ });
97364
97572
  Object.defineProperty(exports, "prepareNotification", {
97365
97573
  enumerable: true,
97366
97574
  get: function() {
@@ -5971,7 +5971,7 @@ var ZodIssueCode = {
5971
5971
  var ZodFirstPartyTypeKind;
5972
5972
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5973
5973
  //#endregion
5974
- //#region ../types/dist/sleep-B-vp7x15.mjs
5974
+ //#region ../types/dist/sleep-Bei295Bz.mjs
5975
5975
  /**
5976
5976
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5977
5977
  * window to float samples (D455).
@@ -6487,6 +6487,53 @@ function emitReadiness(bus, params) {
6487
6487
  ts
6488
6488
  }));
6489
6489
  }
6490
+ /** Why a catalogue entry is not a runnable body of its declared kind. */
6491
+ var AddonMigrationDefinitionError = class extends Error {
6492
+ id;
6493
+ constructor(id, message) {
6494
+ super(`${id}: ${message}`);
6495
+ this.id = id;
6496
+ this.name = "AddonMigrationDefinitionError";
6497
+ }
6498
+ };
6499
+ /**
6500
+ * Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
6501
+ * an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
6502
+ * body). A throwing body never escapes `execute`: it is reported as a failed result.
6503
+ */
6504
+ function defineAddonMigrations(catalogue, ports) {
6505
+ return catalogue.map((entry) => {
6506
+ const { meta } = entry;
6507
+ if ("plan" in entry) {
6508
+ if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
6509
+ return {
6510
+ meta,
6511
+ execute: () => guarded(async () => ({
6512
+ kind: "plan",
6513
+ plan: await entry.plan(ports)
6514
+ }))
6515
+ };
6516
+ }
6517
+ if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
6518
+ return {
6519
+ meta,
6520
+ execute: () => guarded(async () => ({
6521
+ kind: "procedure",
6522
+ result: await entry.procedure(ports)
6523
+ }))
6524
+ };
6525
+ });
6526
+ }
6527
+ async function guarded(work) {
6528
+ try {
6529
+ return await work();
6530
+ } catch (err) {
6531
+ return {
6532
+ kind: "failed",
6533
+ error: errMsg(err)
6534
+ };
6535
+ }
6536
+ }
6490
6537
  var NOT_RUN = {
6491
6538
  declared: false,
6492
6539
  snapshot: null,
@@ -14123,6 +14170,16 @@ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
14123
14170
  precondition: PreconditionSchema
14124
14171
  })
14125
14172
  ])) });
14173
+ /** `JSON.stringify` with object keys sorted, so equal values spell the same. */
14174
+ function canonicalJson(value) {
14175
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
14176
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
14177
+ return JSON.stringify(value) ?? "null";
14178
+ }
14179
+ /** The precondition string for a value a plan read (`null` when it read nothing). */
14180
+ function preconditionOf(value) {
14181
+ return canonicalJson(value ?? null);
14182
+ }
14126
14183
  /**
14127
14184
  * What a `procedure` body reports. `already-done` means the rerun observed the
14128
14185
  * work finished (a crash between the work and the ledger row, or a manual fix)
@@ -16071,8 +16128,18 @@ var LogChannelDescriptorSchema = object({
16071
16128
  * the operator narrows to one camera, sees nothing, and concludes the code
16072
16129
  * path was never taken.
16073
16130
  */
16074
- perDevice: boolean()
16075
- });
16131
+ perDevice: boolean(),
16132
+ /**
16133
+ * What the channel is FOR, so the UI can list the channels an addon offers.
16134
+ *
16135
+ * - `diagnostic` (the default): a per-component diagnostic armed from the
16136
+ * Levels page, emitted at its declared level.
16137
+ * - `debug`: a `debug.<channel>` dump watched live for a short window
16138
+ * (`ctx.debug`). Always per-device, and its lines are denied by the log
16139
+ * ring unless somebody is watching that device.
16140
+ */
16141
+ kind: _enum(["diagnostic", "debug"]).default("diagnostic")
16142
+ }).refine((d) => d.kind !== "debug" || d.name.startsWith("debug.") && d.perDevice, { message: "a debug channel is named debug.<channel> and is per-device" });
16076
16143
  /**
16077
16144
  * An armed window over one channel, as the document hands it to a mirror.
16078
16145
  *
@@ -51614,6 +51681,45 @@ var deviceProviderCapability = {
51614
51681
  }
51615
51682
  };
51616
51683
  /**
51684
+ * What the passive listener of `local-network` saw on the wire, before any
51685
+ * provider classified it. Device-manager turns an observation into the hits
51686
+ * of the providers whose declared discovery modes it matches.
51687
+ */
51688
+ var DiscoveryObservationSchema = discriminatedUnion("kind", [object({
51689
+ kind: literal("mdns"),
51690
+ /** Normalised DNS-SD service type, e.g. `_esphomelib._tcp`. */
51691
+ serviceType: string(),
51692
+ instance: string(),
51693
+ host: string(),
51694
+ /** The A record address when the announcement carried one. */
51695
+ address: string().nullable(),
51696
+ port: number().int(),
51697
+ txt: record(string(), string())
51698
+ }), object({
51699
+ kind: literal("ssdp"),
51700
+ /** Sender address of the datagram. */
51701
+ host: string(),
51702
+ port: number().int(),
51703
+ /** `NT` of a NOTIFY, `ST` of a search response. */
51704
+ target: string(),
51705
+ headers: record(string(), string())
51706
+ })]);
51707
+ object({
51708
+ addonId: string(),
51709
+ candidate: DiscoveryCandidateSchema,
51710
+ firstSeenAt: number(),
51711
+ lastSeenAt: number(),
51712
+ dismissed: boolean()
51713
+ });
51714
+ var DiscoveryProposalSchema = object({
51715
+ id: string(),
51716
+ addonId: string(),
51717
+ candidate: DiscoveryCandidateSchema,
51718
+ firstSeenAt: number(),
51719
+ lastSeenAt: number(),
51720
+ dismissed: boolean()
51721
+ });
51722
+ /**
51617
51723
  * Device Manager capability — hub-side singleton that unifies device persistence,
51618
51724
  * live registry access, and all management operations into a single tRPC surface.
51619
51725
  *
@@ -52342,6 +52448,19 @@ method(object({
52342
52448
  }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
52343
52449
  kind: "mutation",
52344
52450
  auth: "admin"
52451
+ }), method(object({}), object({ proposals: array(DiscoveryProposalSchema).readonly() }), { auth: "admin" }), method(object({ id: string() }), object({ dismissed: boolean() }), {
52452
+ kind: "mutation",
52453
+ auth: "admin"
52454
+ }), method(object({ observations: array(DiscoveryObservationSchema).max(512) }), object({ proposed: number().int() }), {
52455
+ kind: "mutation",
52456
+ auth: "admin"
52457
+ }), method(object({}), object({
52458
+ providers: number().int(),
52459
+ proposed: number().int()
52460
+ }), {
52461
+ kind: "mutation",
52462
+ auth: "admin",
52463
+ timeoutMs: 12e4
52345
52464
  }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
52346
52465
  deviceId: number(),
52347
52466
  key: string(),
@@ -76689,6 +76808,40 @@ var LoggingSettingsStateSchema = object({
76689
76808
  opsLogRetentionDays: number().int().min(0),
76690
76809
  persisted: boolean()
76691
76810
  });
76811
+ /**
76812
+ * Debug WATCH — a short, per-device window over `debug.<channel>` lines.
76813
+ *
76814
+ * `start` opens (or re-arms) a 10 minute window; `rearm` extends a LIVE one
76815
+ * and never revives a lapsed one; `stop` closes it; `list` only reads. No
76816
+ * chain of re-arms outlives 60 minutes from the first start. The hub holds the
76817
+ * watches in memory and pushes the interest to every node, so a watch lapses
76818
+ * by itself and survives nothing — a restart is a stop.
76819
+ */
76820
+ var DebugWatchInputSchema = object({
76821
+ action: _enum([
76822
+ "list",
76823
+ "start",
76824
+ "rearm",
76825
+ "stop"
76826
+ ]),
76827
+ /** Full channel names, `debug.<channel>`, each declared with `kind: 'debug'`. */
76828
+ channels: array(string().min(1)).readonly().default([]),
76829
+ /** Numeric device ids. Required (non-empty) for `start`. */
76830
+ deviceIds: array(number().int()).readonly().default([])
76831
+ });
76832
+ var DebugWatchStateSchema = object({
76833
+ channel: string(),
76834
+ deviceIds: array(number().int()).readonly(),
76835
+ startedAtMs: number(),
76836
+ untilMs: number(),
76837
+ capAtMs: number()
76838
+ });
76839
+ var DebugWatchResultSchema = object({
76840
+ nowMs: number(),
76841
+ watches: array(DebugWatchStateSchema).readonly(),
76842
+ /** Channels a `rearm` found already lapsed: nothing was revived. */
76843
+ lapsed: array(string()).readonly()
76844
+ });
76692
76845
  method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
76693
76846
  kind: "mutation",
76694
76847
  auth: "admin"
@@ -76704,6 +76857,9 @@ method(_void(), SystemInfoSchema), method(_void(), HealthStatusSchema), method(_
76704
76857
  }), method(_void(), RequestCensusStatusSchema, { auth: "admin" }), method(_void(), array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }), method(_void(), array(ReportedFailureContributionSchema).readonly(), { auth: "admin" }), method(GetLoggingSettingsInputSchema, LoggingSettingsStateSchema, { auth: "admin" }), method(SetLoggingSettingsInputSchema, LoggingSettingsStateSchema, {
76705
76858
  kind: "mutation",
76706
76859
  auth: "admin"
76860
+ }), method(DebugWatchInputSchema, DebugWatchResultSchema, {
76861
+ kind: "mutation",
76862
+ auth: "admin"
76707
76863
  });
76708
76864
  /**
76709
76865
  * Tamper / case-open detection sensor. Drives Home Assistant
@@ -83813,6 +83969,13 @@ Object.freeze({
83813
83969
  access: "view",
83814
83970
  role: "admin"
83815
83971
  },
83972
+ "deviceManager.dismissDiscoveryProposal": {
83973
+ capName: "device-manager",
83974
+ capScope: "system",
83975
+ addonId: null,
83976
+ access: "create",
83977
+ role: "admin"
83978
+ },
83816
83979
  "deviceManager.enable": {
83817
83980
  capName: "device-manager",
83818
83981
  capScope: "system",
@@ -83995,6 +84158,13 @@ Object.freeze({
83995
84158
  access: "view",
83996
84159
  role: "viewer"
83997
84160
  },
84161
+ "deviceManager.listDiscoveryProposals": {
84162
+ capName: "device-manager",
84163
+ capScope: "system",
84164
+ addonId: null,
84165
+ access: "view",
84166
+ role: "admin"
84167
+ },
83998
84168
  "deviceManager.listLocationDescriptions": {
83999
84169
  capName: "device-manager",
84000
84170
  capScope: "system",
@@ -84149,6 +84319,13 @@ Object.freeze({
84149
84319
  access: "create",
84150
84320
  role: "admin"
84151
84321
  },
84322
+ "deviceManager.reportDiscoveryObservations": {
84323
+ capName: "device-manager",
84324
+ capScope: "system",
84325
+ addonId: null,
84326
+ access: "create",
84327
+ role: "admin"
84328
+ },
84152
84329
  "deviceManager.runDeviceAction": {
84153
84330
  capName: "device-manager",
84154
84331
  capScope: "system",
@@ -84156,6 +84333,13 @@ Object.freeze({
84156
84333
  access: "create",
84157
84334
  role: "admin"
84158
84335
  },
84336
+ "deviceManager.runDiscoverySweep": {
84337
+ capName: "device-manager",
84338
+ capScope: "system",
84339
+ addonId: null,
84340
+ access: "create",
84341
+ role: "admin"
84342
+ },
84159
84343
  "deviceManager.setChildLayout": {
84160
84344
  capName: "device-manager",
84161
84345
  capScope: "system",
@@ -90743,6 +90927,13 @@ Object.freeze({
90743
90927
  access: "create",
90744
90928
  role: "viewer"
90745
90929
  },
90930
+ "system.debugWatch": {
90931
+ capName: "system",
90932
+ capScope: "system",
90933
+ addonId: null,
90934
+ access: "create",
90935
+ role: "admin"
90936
+ },
90746
90937
  "system.detectSiteLocation": {
90747
90938
  capName: "system",
90748
90939
  capScope: "system",
@@ -94672,6 +94863,11 @@ Object.freeze({
94672
94863
  form: "single",
94673
94864
  optional: false
94674
94865
  }],
94866
+ "system.debugWatch": [{
94867
+ name: "deviceIds",
94868
+ form: "array",
94869
+ optional: true
94870
+ }],
94675
94871
  "tamper.getStatus": [{
94676
94872
  name: "deviceId",
94677
94873
  form: "single",
@@ -96838,4 +97034,4 @@ async function listHomeAssistantBrokers(query) {
96838
97034
  return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
96839
97035
  }
96840
97036
  //#endregion
96841
- export { humiditySensorCapability as $, number as $t, brokerCapability as A, temperatureSensorCapability as At, consumableKindOf as B, RecordSecretUnavailableError as Bt, alarmPanelCapability as C, runningFromVacuumState as Ct, bestLocationMatch as D, smokeCapability as Dt, batteryCapability as E, sendToExportedTarget as Et, carbonMonoxideCapability as F, valveCapability as Ft, deviceAdoptionCapability as G, entryBody as Gt, controlCapability as H, createEvent as Ht, climateControlCapability as I, vibrationCapability as It, eventEmitterCapability as J, _enum as Jt, deviceExportCapability as K, isReadingQuantity as Kt, colorCapability as L, waterHeaterCapability as Lt, buildDeviceAdminUrl as M, tryConvertUnit as Mt, buttonCapability as N, updateCapability as Nt, binaryCapability as O, switchCapability as Ot, calendarSourceCapability as P, vacuumControlCapability as Pt, humidifierCapability as Q, literal as Qt, connectivityCapability as R, weatherCapability as Rt, airQualitySensorCapability as S, runningFromMediaPlayer as St, automationControlCapability as T, scriptRunnerCapability as Tt, coverCapability as U, defineEntryKind as Ut, contactCapability as V, baseUnitOf as Vt, deviceAdminLinkCapability as W, defineRecordKinds as Wt, floodCapability as X, boolean as Xt, fanControlCapability as Y, array as Yt, gasCapability as Z, discriminatedUnion as Zt, PLUG_APPLIANCE_TYPES as _, publicEndpointReaderFrom as _t, BaseDevice as a, url as an, motionCapability as at, accessoriesCapability as b, runExportedAction as bt, COCO_TO_MACRO as c, DeviceType as cn, notifierCapability as ct, ConsumableItemSchema as d, parseAdminBase as dt, object as en, imageCapability as et, ConsumableStatusSchema as f, powerMeterCapability as ft, MediaPlayerRepeatSchema as g, pruneDeletedEntities as gt, HvacModeSchema as h, pressureSensorCapability as ht, AlarmArmModeSchema as i, unknown as in, mediaPlayerCapability as it, buildAddonRouteProvider as j, tryConvertDelta as jt, brightnessCapability as k, tamperCapability as kt, CameraSwitchIdSchema as l, errMsg as ln, numericSensorCapability as lt, FanDirectionSchema as m, presenceCapability as mt, HA_PROVIDER_ADDON_ID as n, string as nn, lawnMowerControlCapability as nt, BaseDeviceProvider as o, DeviceFeature as on, normalizeUnit as ot, EnumSensorDateTimeFormatSchema as p, prepareNotification as pt, enumSensorCapability as q, nodePin as qt, listHomeAssistantBrokers as r, union as rn, lockControlCapability as rt, CAMERA_SWITCH_ORDER as s, DeviceRole as sn, notificationOutputCapability as st, HA_BROKER_KIND as t, record as tn, ingestReading as tt, ConsumableFormSchema as u, EventCategory as un, oauthIntegrationCapability as ut, RUNNING_STATE_COLD as v, quantityFromUnit as vt, ambientLightSensorCapability as w, runningStateCapability as wt, addonRoutesCapability as x, runningFromLawnMower as xt, TargetSchema as y, resolveAttachmentUrls as yt, consumableFormOf as z, BaseAddon as zt };
97037
+ export { humiditySensorCapability as $, discriminatedUnion as $t, brokerCapability as A, tamperCapability as At, consumableKindOf as B, BaseAddon as Bt, alarmPanelCapability as C, runningFromMediaPlayer as Ct, bestLocationMatch as D, sendToExportedTarget as Dt, batteryCapability as E, scriptRunnerCapability as Et, carbonMonoxideCapability as F, vacuumControlCapability as Ft, deviceAdoptionCapability as G, defineEntryKind as Gt, controlCapability as H, baseUnitOf as Ht, climateControlCapability as I, valveCapability as It, eventEmitterCapability as J, isReadingQuantity as Jt, deviceExportCapability as K, defineRecordKinds as Kt, colorCapability as L, vibrationCapability as Lt, buildDeviceAdminUrl as M, tryConvertDelta as Mt, buttonCapability as N, tryConvertUnit as Nt, binaryCapability as O, smokeCapability as Ot, calendarSourceCapability as P, updateCapability as Pt, humidifierCapability as Q, boolean as Qt, connectivityCapability as R, waterHeaterCapability as Rt, airQualitySensorCapability as S, runningFromLawnMower as St, automationControlCapability as T, runningStateCapability as Tt, coverCapability as U, createEvent as Ut, contactCapability as V, RecordSecretUnavailableError as Vt, deviceAdminLinkCapability as W, defineAddonMigrations as Wt, floodCapability as X, _enum as Xt, fanControlCapability as Y, nodePin as Yt, gasCapability as Z, array as Zt, PLUG_APPLIANCE_TYPES as _, pruneDeletedEntities as _t, BaseDevice as a, union as an, motionCapability as at, accessoriesCapability as b, resolveAttachmentUrls as bt, COCO_TO_MACRO as c, DeviceFeature as cn, notifierCapability as ct, ConsumableItemSchema as d, errMsg as dn, parseAdminBase as dt, literal as en, imageCapability as et, ConsumableStatusSchema as f, EventCategory as fn, powerMeterCapability as ft, MediaPlayerRepeatSchema as g, pressureSensorCapability as gt, HvacModeSchema as h, presenceCapability as ht, AlarmArmModeSchema as i, string as in, mediaPlayerCapability as it, buildAddonRouteProvider as j, temperatureSensorCapability as jt, brightnessCapability as k, switchCapability as kt, CameraSwitchIdSchema as l, DeviceRole as ln, numericSensorCapability as lt, FanDirectionSchema as m, prepareNotification as mt, HA_PROVIDER_ADDON_ID as n, object as nn, lawnMowerControlCapability as nt, BaseDeviceProvider as o, unknown as on, normalizeUnit as ot, EnumSensorDateTimeFormatSchema as p, preconditionOf as pt, enumSensorCapability as q, entryBody as qt, listHomeAssistantBrokers as r, record as rn, lockControlCapability as rt, CAMERA_SWITCH_ORDER as s, url as sn, notificationOutputCapability as st, HA_BROKER_KIND as t, number as tn, ingestReading as tt, ConsumableFormSchema as u, DeviceType as un, oauthIntegrationCapability as ut, RUNNING_STATE_COLD as v, publicEndpointReaderFrom as vt, ambientLightSensorCapability as w, runningFromVacuumState as wt, addonRoutesCapability as x, runExportedAction as xt, TargetSchema as y, quantityFromUnit as yt, consumableFormOf as z, weatherCapability as zt };
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_ha_broker_source = require("../ha-broker-source-BFFi4NDH.js");
27
+ const require_ha_broker_source = require("../ha-broker-source-Ba20mhmb.js");
28
28
  let node_crypto = require("node:crypto");
29
29
  require("node:zlib");
30
30
  let node_fs = require("node:fs");
@@ -4940,6 +4940,49 @@ async function call(slice, capName, make) {
4940
4940
  }
4941
4941
  return { ok: true };
4942
4942
  }
4943
+ //#endregion
4944
+ //#region src/ha-export/migrations/drop-public-base-url.ts
4945
+ /**
4946
+ * The media base is no longer a setting of this addon (D986 §5): each Home Assistant
4947
+ * instance chooses a hub address ROLE (`mediaRole:<brokerId>`) and the address is read from
4948
+ * the palette on every use. The `publicBaseUrl` key older versions persisted is deleted.
4949
+ * Every other key is left exactly as stored.
4950
+ *
4951
+ * Idempotent: a store without the key yields an empty plan, which writes nothing.
4952
+ */
4953
+ /** The settings scope of this addon (`<addonId>:addon-settings`). */
4954
+ var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
4955
+ var RETIRED_URL_KEYS = ["publicBaseUrl"];
4956
+ async function planDropPublicBaseUrl(ports) {
4957
+ const stored = await ports.readStored();
4958
+ const present = RETIRED_URL_KEYS.filter((key) => key in stored);
4959
+ if (present.length === 0) return { edits: [] };
4960
+ return { edits: [{
4961
+ op: "deleteKeys",
4962
+ scope: HA_EXPORT_SETTINGS_SCOPE,
4963
+ keys: [...present],
4964
+ precondition: require_ha_broker_source.preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
4965
+ }] };
4966
+ }
4967
+ //#endregion
4968
+ //#region src/ha-export/migrations/index.ts
4969
+ /** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
4970
+ var ADDON_MIGRATIONS = [{
4971
+ meta: {
4972
+ id: "homeassistant-export/2026-11-02-drop-public-base-url",
4973
+ owner: "homeassistant-export",
4974
+ level: "addon-state",
4975
+ seq: 1,
4976
+ dependsOn: [],
4977
+ store: "settings",
4978
+ scope: "cluster",
4979
+ onFailure: "tolerate",
4980
+ bodyKind: "plan",
4981
+ description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
4982
+ fixtureMark: "the reconcile interval the migration does not know"
4983
+ },
4984
+ plan: planDropPublicBaseUrl
4985
+ }];
4943
4986
  process.platform, process.env["SystemRoot"];
4944
4987
  /**
4945
4988
  * The ONE derivation of "a signed, expiring URL".
@@ -5019,15 +5062,26 @@ function verifyExpiringUrl(input) {
5019
5062
  * an expired link is refused whether or not its signature is valid, so a leaked
5020
5063
  * URL stops working on its own.
5021
5064
  */
5065
+ /** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
5066
+ function mediaRoleKey(brokerId) {
5067
+ return `mediaRole:${brokerId}`;
5068
+ }
5069
+ /** A broker's chosen role; anything stored that is not a known role is `internal`. */
5070
+ function haMediaRoleOf(store, brokerId) {
5071
+ return store[mediaRoleKey(brokerId)] === "publicEndpoint" ? "publicEndpoint" : "internal";
5072
+ }
5022
5073
  /**
5023
- * The base a media link is minted on when the operator left the addon's own
5024
- * field empty: the hub's `internal` role (D734) — the address flagged
5025
- * "Internal" on the Network page, else the detected LAN address. Home
5026
- * Assistant is a LAN device pulling from the hub, so never the public
5027
- * endpoint, and never loopback (`null` when nothing qualifies).
5074
+ * The base a media link is minted on: the hub's role (D734) — `internal` unless the
5075
+ * broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
5076
+ * when the role resolves to nothing): a link Home Assistant cannot open is worse
5077
+ * than no image.
5028
5078
  */
5029
- function haMediaBaseUrl(book) {
5030
- return book.roles.internal.resolved;
5079
+ function haMediaBaseUrl(book, role = "internal") {
5080
+ return book.roles[role].resolved;
5081
+ }
5082
+ /** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
5083
+ function mediaBasesByBroker(store, brokerIds, book) {
5084
+ return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId))]));
5031
5085
  }
5032
5086
  /** How long a minted link stays valid. Long enough for HA to render it. */
5033
5087
  var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
@@ -7168,8 +7222,7 @@ var DEFAULT_CONFIG = {
7168
7222
  exportedEntities: {
7169
7223
  actions: [],
7170
7224
  targets: []
7171
- },
7172
- publicBaseUrl: ""
7225
+ }
7173
7226
  };
7174
7227
  var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
7175
7228
  links = /* @__PURE__ */ new Map();
@@ -7178,7 +7231,6 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
7178
7231
  /** numeric deviceId → deviceKey, for the event path. */
7179
7232
  keyByDeviceId = /* @__PURE__ */ new Map();
7180
7233
  mediaSecret = "";
7181
- mediaBaseUrl = null;
7182
7234
  reconcileTimer = null;
7183
7235
  reconcileInFlight = false;
7184
7236
  /** A reason asked for while one was running. Re-run, never dropped. */
@@ -7278,6 +7330,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
7278
7330
  super({ ...DEFAULT_CONFIG });
7279
7331
  }
7280
7332
  async onInitialize() {
7333
+ await this.runMigrations(require_ha_broker_source.defineAddonMigrations(ADDON_MIGRATIONS, { readStored: async () => this.resolveGlobalStore() }));
7281
7334
  const secret = this.state("haExportMediaSecret", require_ha_broker_source.string(), "");
7282
7335
  const existing = await secret.get();
7283
7336
  if (existing.length === 0) {
@@ -7678,32 +7731,39 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
7678
7731
  const lastSeen = readNumber(data, "lastSeen") ?? Date.now();
7679
7732
  const zonesVisited = readStringArray(data, "zonesVisited");
7680
7733
  const fusedIntoTrackId = readString(data, "fusedIntoTrackId");
7681
- const mediaUrl = this.mintTrackMediaUrl(trackId, deviceId);
7682
- const values = projectTrackLifecycle(deviceKey, {
7683
- deviceId,
7684
- trackId,
7685
- phase,
7686
- classes,
7687
- ...bestClassName !== null ? { bestClassName } : {},
7688
- ...label !== null ? { label } : {},
7689
- ...plateText !== null ? { plateText } : {},
7690
- lastSeen,
7691
- zonesVisited,
7692
- ...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
7693
- }, mediaUrl);
7694
- if (values.length === 0) {
7695
- this.ctx.logger.debug("ha-export: track produced no exportable state", {
7696
- tags: { deviceId },
7697
- meta: {
7698
- trackId,
7699
- bestClassName,
7700
- phase,
7701
- fusedIntoTrackId
7702
- }
7703
- });
7704
- return;
7734
+ const exported = this.exported.get(deviceKey);
7735
+ if (exported === void 0) return;
7736
+ const bases = await this.mediaBasesFor(exported.brokerIds, deviceId);
7737
+ let produced = false;
7738
+ for (const brokerId of exported.brokerIds) {
7739
+ const link = this.links.get(brokerId);
7740
+ if (link === void 0) continue;
7741
+ const base = bases.get(brokerId) ?? null;
7742
+ const values = projectTrackLifecycle(deviceKey, {
7743
+ deviceId,
7744
+ trackId,
7745
+ phase,
7746
+ classes,
7747
+ ...bestClassName !== null ? { bestClassName } : {},
7748
+ ...label !== null ? { label } : {},
7749
+ ...plateText !== null ? { plateText } : {},
7750
+ lastSeen,
7751
+ zonesVisited,
7752
+ ...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
7753
+ }, base === null ? null : this.mintTrackMediaUrl(trackId, deviceId, base));
7754
+ if (values.length === 0) continue;
7755
+ produced = true;
7756
+ for (const value of values) link.client.publishState(value.topic, value.value);
7705
7757
  }
7706
- this.push(deviceId, values);
7758
+ if (!produced) this.ctx.logger.debug("ha-export: track produced no exportable state", {
7759
+ tags: { deviceId },
7760
+ meta: {
7761
+ trackId,
7762
+ bestClassName,
7763
+ phase,
7764
+ fusedIntoTrackId
7765
+ }
7766
+ });
7707
7767
  }
7708
7768
  onAudioWindow(data) {
7709
7769
  const deviceId = readNumber(data, "deviceId");
@@ -7848,7 +7908,6 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
7848
7908
  await this.refreshBrokers();
7849
7909
  const fullResync = this.resync.take(reason);
7850
7910
  let readsHeld = false;
7851
- await this.refreshMediaBaseUrl();
7852
7911
  await this.refreshComponentSupport();
7853
7912
  const enabled = this.enabledBrokerIds();
7854
7913
  const devices = await this.ctx.api.deviceManager.listAll.query({});
@@ -9325,28 +9384,41 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9325
9384
  * cannot open is worse than no image, because it renders as a broken
9326
9385
  * entity rather than an empty one.
9327
9386
  */
9328
- mintTrackMediaUrl(trackId, deviceId) {
9329
- if (this.mediaBaseUrl === null || this.mediaSecret.length === 0) return null;
9387
+ mintTrackMediaUrl(trackId, deviceId, baseUrl) {
9388
+ if (this.mediaSecret.length === 0) return null;
9330
9389
  return buildMediaUrl({
9331
- baseUrl: this.mediaBaseUrl,
9390
+ baseUrl,
9332
9391
  routePrefix: MEDIA_ROUTE_PREFIX,
9333
9392
  id: `${String(deviceId)}:${trackId}`,
9334
9393
  secret: this.mediaSecret,
9335
9394
  expMs: Date.now() + MEDIA_URL_TTL_MS
9336
9395
  });
9337
9396
  }
9338
- async refreshMediaBaseUrl() {
9339
- if (this.config.publicBaseUrl.length > 0) {
9340
- this.mediaBaseUrl = this.config.publicBaseUrl;
9341
- return;
9342
- }
9397
+ /**
9398
+ * Each broker's media base, read per use: the palette's role the broker chose
9399
+ * (`mediaRole:<brokerId>`, `internal` by default). A role that resolves to nothing
9400
+ * is `null` and says so — the image entities then carry no value, which reads as
9401
+ * empty rather than as a broken link.
9402
+ */
9403
+ async mediaBasesFor(brokerIds, deviceId) {
9404
+ let bases;
9343
9405
  try {
9344
- this.mediaBaseUrl = haMediaBaseUrl(await this.ctx.api.localNetwork.getAddresses.query());
9406
+ bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query());
9345
9407
  } catch (err) {
9346
- this.mediaBaseUrl = null;
9347
- this.ctx.logger.debug("ha-export: could not resolve a media base URL", { meta: { error: errMsg(err) } });
9408
+ this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
9409
+ tags: { deviceId },
9410
+ meta: { error: errMsg(err) }
9411
+ });
9412
+ return new Map(brokerIds.map((brokerId) => [brokerId, null]));
9413
+ }
9414
+ for (const brokerId of brokerIds) {
9415
+ if (bases.get(brokerId) !== null) continue;
9416
+ this.ctx.logger.warn("ha-export: the broker media role resolves to no hub address — image entities carry no value until the Network page names one", {
9417
+ tags: { deviceId },
9418
+ meta: { brokerId }
9419
+ });
9348
9420
  }
9349
- if (this.mediaBaseUrl === null) this.ctx.logger.warn("ha-export: no reachable hub URL — image entities will carry no value until one is set");
9421
+ return bases;
9350
9422
  }
9351
9423
  /**
9352
9424
  * The cap-level expose/unexpose applies to EVERY enabled broker.
@@ -9400,21 +9472,26 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9400
9472
  variant: "warning"
9401
9473
  }]
9402
9474
  },
9403
- {
9475
+ ...this.config.knownBrokers.length > 0 ? [{
9404
9476
  id: "ha-export-transport",
9405
9477
  title: "Connection",
9406
9478
  titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
9479
+ description: "The hub address each Home Assistant instance fetches image links from. Internal is the address flagged Internal on the Network page; choose Public endpoint for an instance outside your network.",
9407
9480
  columns: 1,
9408
- fields: [{
9409
- type: "text",
9410
- key: "publicBaseUrl",
9411
- label: "Hub URL Home Assistant can reach",
9412
- labelKey: "provider-homeassistant:globalSettings.s_ha-export-transport.publicBaseUrl.label",
9413
- description: "Used for the signed image links Home Assistant fetches; leave empty to derive one automatically.",
9414
- descriptionKey: "provider-homeassistant:globalSettings.s_ha-export-transport.publicBaseUrl.description",
9415
- default: ""
9416
- }]
9417
- },
9481
+ fields: this.config.knownBrokers.map((broker) => ({
9482
+ type: "select",
9483
+ key: mediaRoleKey(broker.id),
9484
+ label: `Image links for ${broker.name}`,
9485
+ options: [{
9486
+ value: "internal",
9487
+ label: "Internal (LAN address)"
9488
+ }, {
9489
+ value: "publicEndpoint",
9490
+ label: "Public endpoint (reachable from outside)"
9491
+ }],
9492
+ default: "internal"
9493
+ }))
9494
+ }] : [],
9418
9495
  {
9419
9496
  id: "ha-export-advanced",
9420
9497
  title: "Advanced",
@@ -1,4 +1,4 @@
1
- import { B as consumableKindOf, Et as sendToExportedTarget, K as deviceExportCapability, Kt as isReadingQuantity, Vt as baseUnitOf, Yt as array, _ as PLUG_APPLIANCE_TYPES, bt as runExportedAction, c as COCO_TO_MACRO, d as ConsumableItemSchema, en as object, f as ConsumableStatusSchema, g as MediaPlayerRepeatSchema, gt as pruneDeletedEntities, h as HvacModeSchema, i as AlarmArmModeSchema, in as unknown, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as string, qt as nodePin, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, un as EventCategory, ut as oauthIntegrationCapability, x as addonRoutesCapability, z as consumableFormOf, zt as BaseAddon } from "../ha-broker-source-B6wqfEUz.mjs";
1
+ import { B as consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-iLA_DsW8.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  import "node:zlib";
4
4
  import "node:fs";
@@ -4905,6 +4905,49 @@ async function call(slice, capName, make) {
4905
4905
  }
4906
4906
  return { ok: true };
4907
4907
  }
4908
+ //#endregion
4909
+ //#region src/ha-export/migrations/drop-public-base-url.ts
4910
+ /**
4911
+ * The media base is no longer a setting of this addon (D986 §5): each Home Assistant
4912
+ * instance chooses a hub address ROLE (`mediaRole:<brokerId>`) and the address is read from
4913
+ * the palette on every use. The `publicBaseUrl` key older versions persisted is deleted.
4914
+ * Every other key is left exactly as stored.
4915
+ *
4916
+ * Idempotent: a store without the key yields an empty plan, which writes nothing.
4917
+ */
4918
+ /** The settings scope of this addon (`<addonId>:addon-settings`). */
4919
+ var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
4920
+ var RETIRED_URL_KEYS = ["publicBaseUrl"];
4921
+ async function planDropPublicBaseUrl(ports) {
4922
+ const stored = await ports.readStored();
4923
+ const present = RETIRED_URL_KEYS.filter((key) => key in stored);
4924
+ if (present.length === 0) return { edits: [] };
4925
+ return { edits: [{
4926
+ op: "deleteKeys",
4927
+ scope: HA_EXPORT_SETTINGS_SCOPE,
4928
+ keys: [...present],
4929
+ precondition: preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
4930
+ }] };
4931
+ }
4932
+ //#endregion
4933
+ //#region src/ha-export/migrations/index.ts
4934
+ /** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
4935
+ var ADDON_MIGRATIONS = [{
4936
+ meta: {
4937
+ id: "homeassistant-export/2026-11-02-drop-public-base-url",
4938
+ owner: "homeassistant-export",
4939
+ level: "addon-state",
4940
+ seq: 1,
4941
+ dependsOn: [],
4942
+ store: "settings",
4943
+ scope: "cluster",
4944
+ onFailure: "tolerate",
4945
+ bodyKind: "plan",
4946
+ description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
4947
+ fixtureMark: "the reconcile interval the migration does not know"
4948
+ },
4949
+ plan: planDropPublicBaseUrl
4950
+ }];
4908
4951
  process.platform, process.env["SystemRoot"];
4909
4952
  /**
4910
4953
  * The ONE derivation of "a signed, expiring URL".
@@ -4984,15 +5027,26 @@ function verifyExpiringUrl(input) {
4984
5027
  * an expired link is refused whether or not its signature is valid, so a leaked
4985
5028
  * URL stops working on its own.
4986
5029
  */
5030
+ /** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
5031
+ function mediaRoleKey(brokerId) {
5032
+ return `mediaRole:${brokerId}`;
5033
+ }
5034
+ /** A broker's chosen role; anything stored that is not a known role is `internal`. */
5035
+ function haMediaRoleOf(store, brokerId) {
5036
+ return store[mediaRoleKey(brokerId)] === "publicEndpoint" ? "publicEndpoint" : "internal";
5037
+ }
4987
5038
  /**
4988
- * The base a media link is minted on when the operator left the addon's own
4989
- * field empty: the hub's `internal` role (D734) — the address flagged
4990
- * "Internal" on the Network page, else the detected LAN address. Home
4991
- * Assistant is a LAN device pulling from the hub, so never the public
4992
- * endpoint, and never loopback (`null` when nothing qualifies).
5039
+ * The base a media link is minted on: the hub's role (D734) — `internal` unless the
5040
+ * broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
5041
+ * when the role resolves to nothing): a link Home Assistant cannot open is worse
5042
+ * than no image.
4993
5043
  */
4994
- function haMediaBaseUrl(book) {
4995
- return book.roles.internal.resolved;
5044
+ function haMediaBaseUrl(book, role = "internal") {
5045
+ return book.roles[role].resolved;
5046
+ }
5047
+ /** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
5048
+ function mediaBasesByBroker(store, brokerIds, book) {
5049
+ return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId))]));
4996
5050
  }
4997
5051
  /** How long a minted link stays valid. Long enough for HA to render it. */
4998
5052
  var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
@@ -7133,8 +7187,7 @@ var DEFAULT_CONFIG = {
7133
7187
  exportedEntities: {
7134
7188
  actions: [],
7135
7189
  targets: []
7136
- },
7137
- publicBaseUrl: ""
7190
+ }
7138
7191
  };
7139
7192
  var HaExportAddon = class extends BaseAddon {
7140
7193
  links = /* @__PURE__ */ new Map();
@@ -7143,7 +7196,6 @@ var HaExportAddon = class extends BaseAddon {
7143
7196
  /** numeric deviceId → deviceKey, for the event path. */
7144
7197
  keyByDeviceId = /* @__PURE__ */ new Map();
7145
7198
  mediaSecret = "";
7146
- mediaBaseUrl = null;
7147
7199
  reconcileTimer = null;
7148
7200
  reconcileInFlight = false;
7149
7201
  /** A reason asked for while one was running. Re-run, never dropped. */
@@ -7243,6 +7295,7 @@ var HaExportAddon = class extends BaseAddon {
7243
7295
  super({ ...DEFAULT_CONFIG });
7244
7296
  }
7245
7297
  async onInitialize() {
7298
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, { readStored: async () => this.resolveGlobalStore() }));
7246
7299
  const secret = this.state("haExportMediaSecret", string(), "");
7247
7300
  const existing = await secret.get();
7248
7301
  if (existing.length === 0) {
@@ -7643,32 +7696,39 @@ var HaExportAddon = class extends BaseAddon {
7643
7696
  const lastSeen = readNumber(data, "lastSeen") ?? Date.now();
7644
7697
  const zonesVisited = readStringArray(data, "zonesVisited");
7645
7698
  const fusedIntoTrackId = readString(data, "fusedIntoTrackId");
7646
- const mediaUrl = this.mintTrackMediaUrl(trackId, deviceId);
7647
- const values = projectTrackLifecycle(deviceKey, {
7648
- deviceId,
7649
- trackId,
7650
- phase,
7651
- classes,
7652
- ...bestClassName !== null ? { bestClassName } : {},
7653
- ...label !== null ? { label } : {},
7654
- ...plateText !== null ? { plateText } : {},
7655
- lastSeen,
7656
- zonesVisited,
7657
- ...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
7658
- }, mediaUrl);
7659
- if (values.length === 0) {
7660
- this.ctx.logger.debug("ha-export: track produced no exportable state", {
7661
- tags: { deviceId },
7662
- meta: {
7663
- trackId,
7664
- bestClassName,
7665
- phase,
7666
- fusedIntoTrackId
7667
- }
7668
- });
7669
- return;
7699
+ const exported = this.exported.get(deviceKey);
7700
+ if (exported === void 0) return;
7701
+ const bases = await this.mediaBasesFor(exported.brokerIds, deviceId);
7702
+ let produced = false;
7703
+ for (const brokerId of exported.brokerIds) {
7704
+ const link = this.links.get(brokerId);
7705
+ if (link === void 0) continue;
7706
+ const base = bases.get(brokerId) ?? null;
7707
+ const values = projectTrackLifecycle(deviceKey, {
7708
+ deviceId,
7709
+ trackId,
7710
+ phase,
7711
+ classes,
7712
+ ...bestClassName !== null ? { bestClassName } : {},
7713
+ ...label !== null ? { label } : {},
7714
+ ...plateText !== null ? { plateText } : {},
7715
+ lastSeen,
7716
+ zonesVisited,
7717
+ ...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
7718
+ }, base === null ? null : this.mintTrackMediaUrl(trackId, deviceId, base));
7719
+ if (values.length === 0) continue;
7720
+ produced = true;
7721
+ for (const value of values) link.client.publishState(value.topic, value.value);
7670
7722
  }
7671
- this.push(deviceId, values);
7723
+ if (!produced) this.ctx.logger.debug("ha-export: track produced no exportable state", {
7724
+ tags: { deviceId },
7725
+ meta: {
7726
+ trackId,
7727
+ bestClassName,
7728
+ phase,
7729
+ fusedIntoTrackId
7730
+ }
7731
+ });
7672
7732
  }
7673
7733
  onAudioWindow(data) {
7674
7734
  const deviceId = readNumber(data, "deviceId");
@@ -7813,7 +7873,6 @@ var HaExportAddon = class extends BaseAddon {
7813
7873
  await this.refreshBrokers();
7814
7874
  const fullResync = this.resync.take(reason);
7815
7875
  let readsHeld = false;
7816
- await this.refreshMediaBaseUrl();
7817
7876
  await this.refreshComponentSupport();
7818
7877
  const enabled = this.enabledBrokerIds();
7819
7878
  const devices = await this.ctx.api.deviceManager.listAll.query({});
@@ -9290,28 +9349,41 @@ var HaExportAddon = class extends BaseAddon {
9290
9349
  * cannot open is worse than no image, because it renders as a broken
9291
9350
  * entity rather than an empty one.
9292
9351
  */
9293
- mintTrackMediaUrl(trackId, deviceId) {
9294
- if (this.mediaBaseUrl === null || this.mediaSecret.length === 0) return null;
9352
+ mintTrackMediaUrl(trackId, deviceId, baseUrl) {
9353
+ if (this.mediaSecret.length === 0) return null;
9295
9354
  return buildMediaUrl({
9296
- baseUrl: this.mediaBaseUrl,
9355
+ baseUrl,
9297
9356
  routePrefix: MEDIA_ROUTE_PREFIX,
9298
9357
  id: `${String(deviceId)}:${trackId}`,
9299
9358
  secret: this.mediaSecret,
9300
9359
  expMs: Date.now() + MEDIA_URL_TTL_MS
9301
9360
  });
9302
9361
  }
9303
- async refreshMediaBaseUrl() {
9304
- if (this.config.publicBaseUrl.length > 0) {
9305
- this.mediaBaseUrl = this.config.publicBaseUrl;
9306
- return;
9307
- }
9362
+ /**
9363
+ * Each broker's media base, read per use: the palette's role the broker chose
9364
+ * (`mediaRole:<brokerId>`, `internal` by default). A role that resolves to nothing
9365
+ * is `null` and says so — the image entities then carry no value, which reads as
9366
+ * empty rather than as a broken link.
9367
+ */
9368
+ async mediaBasesFor(brokerIds, deviceId) {
9369
+ let bases;
9308
9370
  try {
9309
- this.mediaBaseUrl = haMediaBaseUrl(await this.ctx.api.localNetwork.getAddresses.query());
9371
+ bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query());
9310
9372
  } catch (err) {
9311
- this.mediaBaseUrl = null;
9312
- this.ctx.logger.debug("ha-export: could not resolve a media base URL", { meta: { error: errMsg(err) } });
9373
+ this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
9374
+ tags: { deviceId },
9375
+ meta: { error: errMsg(err) }
9376
+ });
9377
+ return new Map(brokerIds.map((brokerId) => [brokerId, null]));
9378
+ }
9379
+ for (const brokerId of brokerIds) {
9380
+ if (bases.get(brokerId) !== null) continue;
9381
+ this.ctx.logger.warn("ha-export: the broker media role resolves to no hub address — image entities carry no value until the Network page names one", {
9382
+ tags: { deviceId },
9383
+ meta: { brokerId }
9384
+ });
9313
9385
  }
9314
- if (this.mediaBaseUrl === null) this.ctx.logger.warn("ha-export: no reachable hub URL — image entities will carry no value until one is set");
9386
+ return bases;
9315
9387
  }
9316
9388
  /**
9317
9389
  * The cap-level expose/unexpose applies to EVERY enabled broker.
@@ -9365,21 +9437,26 @@ var HaExportAddon = class extends BaseAddon {
9365
9437
  variant: "warning"
9366
9438
  }]
9367
9439
  },
9368
- {
9440
+ ...this.config.knownBrokers.length > 0 ? [{
9369
9441
  id: "ha-export-transport",
9370
9442
  title: "Connection",
9371
9443
  titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
9444
+ description: "The hub address each Home Assistant instance fetches image links from. Internal is the address flagged Internal on the Network page; choose Public endpoint for an instance outside your network.",
9372
9445
  columns: 1,
9373
- fields: [{
9374
- type: "text",
9375
- key: "publicBaseUrl",
9376
- label: "Hub URL Home Assistant can reach",
9377
- labelKey: "provider-homeassistant:globalSettings.s_ha-export-transport.publicBaseUrl.label",
9378
- description: "Used for the signed image links Home Assistant fetches; leave empty to derive one automatically.",
9379
- descriptionKey: "provider-homeassistant:globalSettings.s_ha-export-transport.publicBaseUrl.description",
9380
- default: ""
9381
- }]
9382
- },
9446
+ fields: this.config.knownBrokers.map((broker) => ({
9447
+ type: "select",
9448
+ key: mediaRoleKey(broker.id),
9449
+ label: `Image links for ${broker.name}`,
9450
+ options: [{
9451
+ value: "internal",
9452
+ label: "Internal (LAN address)"
9453
+ }, {
9454
+ value: "publicEndpoint",
9455
+ label: "Public endpoint (reachable from outside)"
9456
+ }],
9457
+ default: "internal"
9458
+ }))
9459
+ }] : [],
9383
9460
  {
9384
9461
  id: "ha-export-advanced",
9385
9462
  title: "Advanced",
package/i18n/en.json CHANGED
@@ -14,8 +14,6 @@
14
14
  "globalSettings.s_ha-export-brokers.title": "Home Assistant instances",
15
15
  "globalSettings.s_ha-export-brokers.ha-export___no-brokers.label": "No brokers",
16
16
  "globalSettings.s_ha-export-transport.title": "Connection",
17
- "globalSettings.s_ha-export-transport.publicBaseUrl.label": "Hub URL Home Assistant can reach",
18
- "globalSettings.s_ha-export-transport.publicBaseUrl.description": "Used for the signed image links Home Assistant fetches; leave empty to derive one automatically.",
19
17
  "globalSettings.s_ha-export-advanced.title": "Advanced",
20
18
  "globalSettings.s_ha-export-advanced.reconcileIntervalSec.label": "Reconcile interval (s)",
21
19
  "globalSettings.s_ha-export-advanced.reconcileIntervalSec.description": "Full re-announce and re-push cadence.",
package/i18n/it.json CHANGED
@@ -14,8 +14,6 @@
14
14
  "globalSettings.s_ha-export-brokers.title": "Istanze di Home Assistant",
15
15
  "globalSettings.s_ha-export-brokers.ha-export___no-brokers.label": "Nessun broker",
16
16
  "globalSettings.s_ha-export-transport.title": "Connessione",
17
- "globalSettings.s_ha-export-transport.publicBaseUrl.label": "URL dell'hub raggiungibile da Home Assistant",
18
- "globalSettings.s_ha-export-transport.publicBaseUrl.description": "Usato per i link firmati delle immagini che Home Assistant scarica; lascia vuoto per ricavarlo automaticamente.",
19
17
  "globalSettings.s_ha-export-advanced.title": "Avanzate",
20
18
  "globalSettings.s_ha-export-advanced.reconcileIntervalSec.label": "Intervallo di riconciliazione (s)",
21
19
  "globalSettings.s_ha-export-advanced.reconcileIntervalSec.description": "Cadenza di ri-annuncio e ri-invio completo.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.647",
3
+ "version": "1.2.648",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -127,7 +127,7 @@
127
127
  "description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
128
128
  },
129
129
  "requires": {
130
- "server": ">=1.2.731"
130
+ "server": ">=1.2.733"
131
131
  }
132
132
  },
133
133
  "files": [