@camstack/addon-provider-homeassistant 1.2.656 → 1.2.658

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.658] - 2026-11-04
11
+
12
+ ### Changed
13
+
14
+ - Fixed the Home Assistant export migration reading every switch as off after the instances moved to typed records; it now reads the legacy integration row and refuses when that row is unreadable.
15
+ - Image links sent to Home Assistant now prefer the public endpoint (falling back to the internal address when none is set), so notifications show images away from home.
16
+
17
+ ## [1.2.657] - 2026-11-04
18
+
19
+ ### Changed
20
+
21
+ - Home Assistant broker migrations no longer read as 'skipped-newer' (and no false 'Database is newer' alert) after the addon boots
22
+
10
23
  ## [1.2.656] - 2026-11-04
11
24
 
12
25
  ### Changed
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_ha_instance_settings = require("./ha-instance-settings-Dm0tU8iv.js");
2
+ const require_ha_instance_settings = require("./ha-instance-settings-CRvQ5rH6.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
@@ -8966,7 +8966,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
8966
8966
  onChanged: (id) => this.syncBrokerRuntime(id),
8967
8967
  probe: (settings) => this.requireRegistry().testSettings(settings)
8968
8968
  });
8969
- await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8969
+ await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
8970
8970
  this.activeBrokers = await this.loadPersistedBrokers();
8971
8971
  this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
8972
8972
  this.registry.setOnBrokerConnected((brokerId) => {
@@ -8983,7 +8983,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
8983
8983
  this.reconcileAllConnectedBrokers();
8984
8984
  }, HaProviderAddon.RESYNC_INTERVAL_MS);
8985
8985
  this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
8986
- await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8986
+ await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
8987
8987
  await this.reconcileIntegrationsToBrokers();
8988
8988
  this.subscribeIntegrationLifecycle();
8989
8989
  return [
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as fanControlCapability, $t as createEvent, A as bestLocationMatch, At as runningFromMediaPlayer, B as climateControlCapability, Bt as tryConvertDelta, C as accessoriesCapability, Ct as pressureSensorCapability, D as ambientLightSensorCapability, Dt as resolveAttachmentUrls, E as alarmPanelCapability, Et as quantityFromUnit, F as buildAddonRouteProvider, Ft as smokeCapability, G as contactCapability, Gt as vibrationCapability, H as connectivityCapability, Ht as updateCapability, I as buildDeviceAdminUrl, J as deviceAdminLinkCapability, K as controlCapability, Kt as waterHeaterCapability, L as buttonCapability, Lt as switchCapability, M as boardKeySourceCapability, Mt as runningStateCapability, N as brightnessCapability, Nt as scriptRunnerCapability, O as automationControlCapability, P as brokerCapability, Q as eventEmitterCapability, Qt as baseUnitOf, R as calendarSourceCapability, Rt as tamperCapability, S as TargetSchema, Sn as EventCategory, St as presenceCapability, T as airQualitySensorCapability, Tt as publicEndpointReaderFrom, Ut as vacuumControlCapability, V as colorCapability, Vt as tryConvertUnit, Wt as valveCapability, Xt as RecordSecretUnavailableError, Y as deviceAdoptionCapability, Z as enumSensorCapability, _n as url, at as humiditySensorCapability, b as RUNNING_STATE_COLD, bn as DeviceType, bt as preconditionOf, cn as boolean, ct as integrationInstancesCapability, dn as number, dt as mediaPlayerCapability, en as defineAddonMigrations, et as flattenNestedRecords, fn as object, ft as motionCapability, gn as unknown, gt as numericSensorCapability, hn as union, ht as notifierCapability, it as humidifierCapability, j as binaryCapability, jt as runningFromVacuumState, k as batteryCapability, kt as runningFromLawnMower, ln as discriminatedUnion, lt as lawnMowerControlCapability, m as EnumSensorDateTimeFormatSchema, mn as string, mt as notificationOutputCapability, n as HA_BROKER_KIND, nn as defineRecordKinds, nt as floodCapability, o as BaseDevice, on as _enum, ot as imageCapability, pn as record, pt as normalizeUnit, q as coverCapability, qt as weatherCapability, rn as entryBody, rt as gasCapability, s as BaseDeviceProvider, sn as array, st as ingestReading, t as haInstanceSettingsPortsOver, tn as defineEntryKind, tt as flattenPortsOver, un as literal, ut as lockControlCapability, vn as DeviceFeature, vt as parseAdminBase, w as addonRoutesCapability, x as RecordIntegrationInstances, xn as errMsg, xt as prepareNotification, yn as DeviceRole, yt as powerMeterCapability, z as carbonMonoxideCapability, zt as temperatureSensorCapability } from "./ha-instance-settings-EQ5Ey3Q-.mjs";
1
+ import { $ as fanControlCapability, $t as createEvent, A as bestLocationMatch, At as runningFromMediaPlayer, B as climateControlCapability, Bt as tryConvertDelta, C as accessoriesCapability, Ct as pressureSensorCapability, D as ambientLightSensorCapability, Dt as resolveAttachmentUrls, E as alarmPanelCapability, Et as quantityFromUnit, F as buildAddonRouteProvider, Ft as smokeCapability, G as contactCapability, Gt as vibrationCapability, H as connectivityCapability, Ht as updateCapability, I as buildDeviceAdminUrl, J as deviceAdminLinkCapability, K as controlCapability, Kt as waterHeaterCapability, L as buttonCapability, Lt as switchCapability, M as boardKeySourceCapability, Mt as runningStateCapability, N as brightnessCapability, Nt as scriptRunnerCapability, O as automationControlCapability, P as brokerCapability, Q as eventEmitterCapability, Qt as baseUnitOf, R as calendarSourceCapability, Rt as tamperCapability, S as TargetSchema, Sn as EventCategory, St as presenceCapability, T as airQualitySensorCapability, Tt as publicEndpointReaderFrom, Ut as vacuumControlCapability, V as colorCapability, Vt as tryConvertUnit, Wt as valveCapability, Xt as RecordSecretUnavailableError, Y as deviceAdoptionCapability, Z as enumSensorCapability, _n as url, at as humiditySensorCapability, b as RUNNING_STATE_COLD, bn as DeviceType, bt as preconditionOf, cn as boolean, ct as integrationInstancesCapability, dn as number, dt as mediaPlayerCapability, en as defineAddonMigrations, et as flattenNestedRecords, fn as object, ft as motionCapability, gn as unknown, gt as numericSensorCapability, hn as union, ht as notifierCapability, it as humidifierCapability, j as binaryCapability, jt as runningFromVacuumState, k as batteryCapability, kt as runningFromLawnMower, ln as discriminatedUnion, lt as lawnMowerControlCapability, m as EnumSensorDateTimeFormatSchema, mn as string, mt as notificationOutputCapability, n as HA_BROKER_KIND, nn as defineRecordKinds, nt as floodCapability, o as BaseDevice, on as _enum, ot as imageCapability, pn as record, pt as normalizeUnit, q as coverCapability, qt as weatherCapability, rn as entryBody, rt as gasCapability, s as BaseDeviceProvider, sn as array, st as ingestReading, t as haInstanceSettingsPortsOver, tn as defineEntryKind, tt as flattenPortsOver, un as literal, ut as lockControlCapability, vn as DeviceFeature, vt as parseAdminBase, w as addonRoutesCapability, x as RecordIntegrationInstances, xn as errMsg, xt as prepareNotification, yn as DeviceRole, yt as powerMeterCapability, z as carbonMonoxideCapability, zt as temperatureSensorCapability } from "./ha-instance-settings-qhCNJbbn.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
@@ -8945,7 +8945,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8945
8945
  onChanged: (id) => this.syncBrokerRuntime(id),
8946
8946
  probe: (settings) => this.requireRegistry().testSettings(settings)
8947
8947
  });
8948
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8948
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
8949
8949
  this.activeBrokers = await this.loadPersistedBrokers();
8950
8950
  this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
8951
8951
  this.registry.setOnBrokerConnected((brokerId) => {
@@ -8962,7 +8962,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8962
8962
  this.reconcileAllConnectedBrokers();
8963
8963
  }, HaProviderAddon.RESYNC_INTERVAL_MS);
8964
8964
  this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
8965
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8965
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
8966
8966
  await this.reconcileIntegrationsToBrokers();
8967
8967
  this.subscribeIntegrationLifecycle();
8968
8968
  return [
@@ -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_instance_settings = require("../ha-instance-settings-Dm0tU8iv.js");
27
+ const require_ha_instance_settings = require("../ha-instance-settings-CRvQ5rH6.js");
28
28
  let node_crypto = require("node:crypto");
29
29
  require("node:zlib");
30
30
  let node_fs = require("node:fs");
@@ -5090,13 +5090,21 @@ function verifyExpiringUrl(input) {
5090
5090
  /** The hub address roles a Home Assistant media link may be minted on. */
5091
5091
  var HA_MEDIA_ROLES = ["internal", "publicEndpoint"];
5092
5092
  /**
5093
- * The base a media link is minted on: the hub's role (D734) — `internal` unless the
5094
- * broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
5095
- * when the role resolves to nothing): a link Home Assistant cannot open is worse
5096
- * than no image.
5093
+ * The base a media link is minted on: the hub's role (D734) the broker chose. Links reach
5094
+ * Home Assistant notifications on phones away from home, so a new broker chooses `publicEndpoint`;
5095
+ * when that resolves to nothing the `internal` address answers instead (a LAN Home Assistant still
5096
+ * gets an image). A broker that chose `internal` never falls back to public. Ranked by
5097
+ * `local-network`, never loopback (`null` when nothing resolves): a link Home Assistant cannot
5098
+ * open is worse than no image.
5097
5099
  */
5098
- function haMediaBaseUrl(book, role = "internal") {
5099
- return book.roles[role].resolved;
5100
+ function haMediaBaseUrl(book, role = "publicEndpoint") {
5101
+ const chosen = book.roles[role].resolved;
5102
+ if (chosen !== null || role !== "publicEndpoint") return chosen;
5103
+ return book.roles.internal.resolved;
5104
+ }
5105
+ /** True when `haMediaBaseUrl` answered with the internal address because the public endpoint was chosen but unset. */
5106
+ function usedInternalFallback(book, role) {
5107
+ return role === "publicEndpoint" && book.roles.publicEndpoint.resolved === null && book.roles.internal.resolved !== null;
5100
5108
  }
5101
5109
  /** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
5102
5110
  function mediaBasesByBroker(brokerIds, book, roleOf) {
@@ -5279,6 +5287,8 @@ function integrationByBroker(rows, warn) {
5279
5287
  /** Every broker the old shape has something for, with the value the old reader would have used. */
5280
5288
  function legacyLinks(stored, rows, warn) {
5281
5289
  const membership = membershipOf$1(stored[MEMBERSHIP_KEY]);
5290
+ const unreadable = rows.find((row) => Object.keys(row.settings).length === 0);
5291
+ if (unreadable !== void 0 && membership.size > 0) throw new Error(`the settings of integration ${unreadable.integrationId} read as empty; the export switch cannot be carried from an unreadable source, nothing was written`);
5282
5292
  const integrations = integrationByBroker(rows, warn);
5283
5293
  const brokers = new Set(membership.keys());
5284
5294
  for (const key of Object.keys(stored)) for (const prefix of [EXPORT_PREFIX, ROLE_PREFIX]) if (key.startsWith(prefix) && key.length > prefix.length) brokers.add(key.slice(prefix.length));
@@ -5472,12 +5482,12 @@ var haExportLinksKind = require_ha_instance_settings.defineEntryKind(HA_EXPORT_L
5472
5482
  schema: HaExportLinksBodySchema,
5473
5483
  writeTrigger: "an operator switches the export of a Home Assistant instance, picks its image-link address, or exports a device to it"
5474
5484
  });
5475
- /** A link the operator has not touched: off, on the internal address, no device. */
5485
+ /** A link the operator has not touched: off, on the public endpoint, no device. */
5476
5486
  function defaultLinks(name) {
5477
5487
  return {
5478
5488
  name,
5479
5489
  exportTo: false,
5480
- mediaRole: "internal",
5490
+ mediaRole: "publicEndpoint",
5481
5491
  devices: []
5482
5492
  };
5483
5493
  }
@@ -5521,7 +5531,7 @@ var ExportLinks = class ExportLinks {
5521
5531
  return this.rows.get(brokerId)?.exportTo === true;
5522
5532
  }
5523
5533
  mediaRole(brokerId) {
5524
- return this.rows.get(brokerId)?.mediaRole ?? "internal";
5534
+ return this.rows.get(brokerId)?.mediaRole ?? "publicEndpoint";
5525
5535
  }
5526
5536
  /** brokerId → exported device ids, for the brokers that export at least one. */
5527
5537
  membership() {
@@ -8247,6 +8257,7 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
8247
8257
  }
8248
8258
  /** deviceId:cap:field already reported. Bounded by devices × mapped caps. */
8249
8259
  warnedCapFields = /* @__PURE__ */ new Set();
8260
+ warnedMediaFallback = /* @__PURE__ */ new Set();
8250
8261
  warnOnce(deviceId, capName, field, line) {
8251
8262
  const key = `${deviceId}:${capName}:${field}`;
8252
8263
  if (this.warnedCapFields.has(key)) return;
@@ -10023,14 +10034,24 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
10023
10034
  }
10024
10035
  /**
10025
10036
  * Each broker's media base, read per use: the palette's role the broker chose
10026
- * (the `ha-export-links` record's `mediaRole`, `internal` by default). A role that resolves to nothing
10037
+ * (the `ha-export-links` record's `mediaRole`, `publicEndpoint` by default, internal when none is set). A role that resolves to nothing
10027
10038
  * is `null` and says so — the image entities then carry no value, which reads as
10028
10039
  * empty rather than as a broken link.
10029
10040
  */
10030
10041
  async mediaBasesFor(brokerIds, deviceId) {
10031
10042
  let bases;
10032
10043
  try {
10033
- bases = mediaBasesByBroker(brokerIds, await this.ctx.api.localNetwork.getAddresses.query(), (brokerId) => this.book().mediaRole(brokerId));
10044
+ const answer = await this.ctx.api.localNetwork.getAddresses.query();
10045
+ bases = mediaBasesByBroker(brokerIds, answer, (brokerId) => this.book().mediaRole(brokerId));
10046
+ for (const brokerId of brokerIds) {
10047
+ if (this.warnedMediaFallback.has(brokerId)) continue;
10048
+ if (!usedInternalFallback(answer, this.book().mediaRole(brokerId))) continue;
10049
+ this.warnedMediaFallback.add(brokerId);
10050
+ this.ctx.logger.info("ha-export: no public endpoint is set, so image links use the internal address", {
10051
+ tags: { deviceId },
10052
+ meta: { brokerId }
10053
+ });
10054
+ }
10034
10055
  } catch (err) {
10035
10056
  this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
10036
10057
  tags: { deviceId },
@@ -10127,7 +10148,7 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
10127
10148
  id: "ha-export-transport",
10128
10149
  title: "Connection",
10129
10150
  titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
10130
- 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.",
10151
+ description: "The hub address each Home Assistant instance fetches image links from. Public endpoint is the default, so notifications show images away from home; when none is set the internal address is used. Choose Internal to never use the public endpoint.",
10131
10152
  columns: 1,
10132
10153
  fields: this.config.knownBrokers.map((broker) => ({
10133
10154
  type: "select",
@@ -10140,7 +10161,7 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
10140
10161
  value: "publicEndpoint",
10141
10162
  label: "Public endpoint (reachable from outside)"
10142
10163
  }],
10143
- default: "internal"
10164
+ default: "publicEndpoint"
10144
10165
  }))
10145
10166
  }] : [],
10146
10167
  {
@@ -1,4 +1,4 @@
1
- import { F as buildAddonRouteProvider, It as statusInstanceOf, Jt as BaseAddon, Ot as runExportedAction, Pt as sendToExportedTarget, Qt as baseUnitOf, Sn as EventCategory, U as consumableFormOf, W as consumableKindOf, X as deviceExportCapability, Zt as RevConflictError, _ as HvacModeSchema, _t as oauthIntegrationCapability, a as AlarmArmModeSchema, an as nodePin, bt as preconditionOf, c as CAMERA_SWITCH_ORDER, cn as boolean, d as ConsumableFormSchema, en as defineAddonMigrations, f as ConsumableItemSchema, fn as object, g as FanDirectionSchema, gn as unknown, h as ExportInstanceUnknownError, i as listHomeAssistantBrokers, in as isReadingQuantity, l as COCO_TO_MACRO, mn as string, on as _enum, p as ConsumableStatusSchema, sn as array, t as haInstanceSettingsPortsOver, tn as defineEntryKind, u as CameraSwitchIdSchema, v as MediaPlayerRepeatSchema, w as addonRoutesCapability, wt as pruneDeletedEntities, y as PLUG_APPLIANCE_TYPES } from "../ha-instance-settings-EQ5Ey3Q-.mjs";
1
+ import { F as buildAddonRouteProvider, It as statusInstanceOf, Jt as BaseAddon, Ot as runExportedAction, Pt as sendToExportedTarget, Qt as baseUnitOf, Sn as EventCategory, U as consumableFormOf, W as consumableKindOf, X as deviceExportCapability, Zt as RevConflictError, _ as HvacModeSchema, _t as oauthIntegrationCapability, a as AlarmArmModeSchema, an as nodePin, bt as preconditionOf, c as CAMERA_SWITCH_ORDER, cn as boolean, d as ConsumableFormSchema, en as defineAddonMigrations, f as ConsumableItemSchema, fn as object, g as FanDirectionSchema, gn as unknown, h as ExportInstanceUnknownError, i as listHomeAssistantBrokers, in as isReadingQuantity, l as COCO_TO_MACRO, mn as string, on as _enum, p as ConsumableStatusSchema, sn as array, t as haInstanceSettingsPortsOver, tn as defineEntryKind, u as CameraSwitchIdSchema, v as MediaPlayerRepeatSchema, w as addonRoutesCapability, wt as pruneDeletedEntities, y as PLUG_APPLIANCE_TYPES } from "../ha-instance-settings-qhCNJbbn.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  import "node:zlib";
4
4
  import "node:fs";
@@ -5055,13 +5055,21 @@ function verifyExpiringUrl(input) {
5055
5055
  /** The hub address roles a Home Assistant media link may be minted on. */
5056
5056
  var HA_MEDIA_ROLES = ["internal", "publicEndpoint"];
5057
5057
  /**
5058
- * The base a media link is minted on: the hub's role (D734) — `internal` unless the
5059
- * broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
5060
- * when the role resolves to nothing): a link Home Assistant cannot open is worse
5061
- * than no image.
5058
+ * The base a media link is minted on: the hub's role (D734) the broker chose. Links reach
5059
+ * Home Assistant notifications on phones away from home, so a new broker chooses `publicEndpoint`;
5060
+ * when that resolves to nothing the `internal` address answers instead (a LAN Home Assistant still
5061
+ * gets an image). A broker that chose `internal` never falls back to public. Ranked by
5062
+ * `local-network`, never loopback (`null` when nothing resolves): a link Home Assistant cannot
5063
+ * open is worse than no image.
5062
5064
  */
5063
- function haMediaBaseUrl(book, role = "internal") {
5064
- return book.roles[role].resolved;
5065
+ function haMediaBaseUrl(book, role = "publicEndpoint") {
5066
+ const chosen = book.roles[role].resolved;
5067
+ if (chosen !== null || role !== "publicEndpoint") return chosen;
5068
+ return book.roles.internal.resolved;
5069
+ }
5070
+ /** True when `haMediaBaseUrl` answered with the internal address because the public endpoint was chosen but unset. */
5071
+ function usedInternalFallback(book, role) {
5072
+ return role === "publicEndpoint" && book.roles.publicEndpoint.resolved === null && book.roles.internal.resolved !== null;
5065
5073
  }
5066
5074
  /** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
5067
5075
  function mediaBasesByBroker(brokerIds, book, roleOf) {
@@ -5244,6 +5252,8 @@ function integrationByBroker(rows, warn) {
5244
5252
  /** Every broker the old shape has something for, with the value the old reader would have used. */
5245
5253
  function legacyLinks(stored, rows, warn) {
5246
5254
  const membership = membershipOf$1(stored[MEMBERSHIP_KEY]);
5255
+ const unreadable = rows.find((row) => Object.keys(row.settings).length === 0);
5256
+ if (unreadable !== void 0 && membership.size > 0) throw new Error(`the settings of integration ${unreadable.integrationId} read as empty; the export switch cannot be carried from an unreadable source, nothing was written`);
5247
5257
  const integrations = integrationByBroker(rows, warn);
5248
5258
  const brokers = new Set(membership.keys());
5249
5259
  for (const key of Object.keys(stored)) for (const prefix of [EXPORT_PREFIX, ROLE_PREFIX]) if (key.startsWith(prefix) && key.length > prefix.length) brokers.add(key.slice(prefix.length));
@@ -5423,12 +5433,12 @@ var haExportLinksKind = defineEntryKind("ha-export-links", {
5423
5433
  }),
5424
5434
  writeTrigger: "an operator switches the export of a Home Assistant instance, picks its image-link address, or exports a device to it"
5425
5435
  });
5426
- /** A link the operator has not touched: off, on the internal address, no device. */
5436
+ /** A link the operator has not touched: off, on the public endpoint, no device. */
5427
5437
  function defaultLinks(name) {
5428
5438
  return {
5429
5439
  name,
5430
5440
  exportTo: false,
5431
- mediaRole: "internal",
5441
+ mediaRole: "publicEndpoint",
5432
5442
  devices: []
5433
5443
  };
5434
5444
  }
@@ -5472,7 +5482,7 @@ var ExportLinks = class ExportLinks {
5472
5482
  return this.rows.get(brokerId)?.exportTo === true;
5473
5483
  }
5474
5484
  mediaRole(brokerId) {
5475
- return this.rows.get(brokerId)?.mediaRole ?? "internal";
5485
+ return this.rows.get(brokerId)?.mediaRole ?? "publicEndpoint";
5476
5486
  }
5477
5487
  /** brokerId → exported device ids, for the brokers that export at least one. */
5478
5488
  membership() {
@@ -8198,6 +8208,7 @@ var HaExportAddon = class extends BaseAddon {
8198
8208
  }
8199
8209
  /** deviceId:cap:field already reported. Bounded by devices × mapped caps. */
8200
8210
  warnedCapFields = /* @__PURE__ */ new Set();
8211
+ warnedMediaFallback = /* @__PURE__ */ new Set();
8201
8212
  warnOnce(deviceId, capName, field, line) {
8202
8213
  const key = `${deviceId}:${capName}:${field}`;
8203
8214
  if (this.warnedCapFields.has(key)) return;
@@ -9974,14 +9985,24 @@ var HaExportAddon = class extends BaseAddon {
9974
9985
  }
9975
9986
  /**
9976
9987
  * Each broker's media base, read per use: the palette's role the broker chose
9977
- * (the `ha-export-links` record's `mediaRole`, `internal` by default). A role that resolves to nothing
9988
+ * (the `ha-export-links` record's `mediaRole`, `publicEndpoint` by default, internal when none is set). A role that resolves to nothing
9978
9989
  * is `null` and says so — the image entities then carry no value, which reads as
9979
9990
  * empty rather than as a broken link.
9980
9991
  */
9981
9992
  async mediaBasesFor(brokerIds, deviceId) {
9982
9993
  let bases;
9983
9994
  try {
9984
- bases = mediaBasesByBroker(brokerIds, await this.ctx.api.localNetwork.getAddresses.query(), (brokerId) => this.book().mediaRole(brokerId));
9995
+ const answer = await this.ctx.api.localNetwork.getAddresses.query();
9996
+ bases = mediaBasesByBroker(brokerIds, answer, (brokerId) => this.book().mediaRole(brokerId));
9997
+ for (const brokerId of brokerIds) {
9998
+ if (this.warnedMediaFallback.has(brokerId)) continue;
9999
+ if (!usedInternalFallback(answer, this.book().mediaRole(brokerId))) continue;
10000
+ this.warnedMediaFallback.add(brokerId);
10001
+ this.ctx.logger.info("ha-export: no public endpoint is set, so image links use the internal address", {
10002
+ tags: { deviceId },
10003
+ meta: { brokerId }
10004
+ });
10005
+ }
9985
10006
  } catch (err) {
9986
10007
  this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
9987
10008
  tags: { deviceId },
@@ -10078,7 +10099,7 @@ var HaExportAddon = class extends BaseAddon {
10078
10099
  id: "ha-export-transport",
10079
10100
  title: "Connection",
10080
10101
  titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
10081
- 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.",
10102
+ description: "The hub address each Home Assistant instance fetches image links from. Public endpoint is the default, so notifications show images away from home; when none is set the internal address is used. Choose Internal to never use the public endpoint.",
10082
10103
  columns: 1,
10083
10104
  fields: this.config.knownBrokers.map((broker) => ({
10084
10105
  type: "select",
@@ -10091,7 +10112,7 @@ var HaExportAddon = class extends BaseAddon {
10091
10112
  value: "publicEndpoint",
10092
10113
  label: "Public endpoint (reachable from outside)"
10093
10114
  }],
10094
- default: "internal"
10115
+ default: "publicEndpoint"
10095
10116
  }))
10096
10117
  }] : [],
10097
10118
  {
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
5981
5981
  var ZodFirstPartyTypeKind;
5982
5982
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5983
5983
  //#endregion
5984
- //#region ../types/dist/sleep-BaWhXZFm.mjs
5984
+ //#region ../types/dist/sleep-D1jjWAr8.mjs
5985
5985
  /**
5986
5986
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5987
5987
  * window to float samples (D455).
@@ -6591,7 +6591,7 @@ var NOT_RUN = {
6591
6591
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6592
6592
  * because the hub (and this addon's boot) must not hang on a migration.
6593
6593
  */
6594
- async function runAddonMigrations(host, migrations, logger) {
6594
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6595
6595
  const first = migrations[0];
6596
6596
  if (first === void 0) return NOT_RUN;
6597
6597
  const owner = first.meta.owner;
@@ -6620,7 +6620,9 @@ async function runAddonMigrations(host, migrations, logger) {
6620
6620
  else if (status === "blocked") blocked.push(id);
6621
6621
  else skipped.push(id);
6622
6622
  };
6623
+ const selected = options.only === void 0 ? null : new Set(options.only);
6623
6624
  for (const step of declaration.steps) {
6625
+ if (selected !== null && !selected.has(step.id)) continue;
6624
6626
  const bound = byId.get(step.id);
6625
6627
  if (bound === void 0 || step.action === "skip") {
6626
6628
  skipped.push(step.id);
@@ -7780,14 +7782,18 @@ var BaseAddon = class {
7780
7782
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7781
7783
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7782
7784
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7785
+ *
7786
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7787
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7788
+ * omitted applied ids "newer").
7783
7789
  */
7784
- async runMigrations(migrations) {
7790
+ async runMigrations(migrations, options) {
7785
7791
  const api = this.ctx.api.migrations;
7786
7792
  const report = await runAddonMigrations({
7787
7793
  declareOwner: (input) => api.declareOwner.mutate(input),
7788
7794
  startMigration: (input) => api.startMigration.mutate(input),
7789
7795
  finishMigration: (input) => api.finishMigration.mutate(input)
7790
- }, migrations, this.ctx.logger);
7796
+ }, migrations, this.ctx.logger, options);
7791
7797
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7792
7798
  return report;
7793
7799
  }
@@ -9259,6 +9265,7 @@ function defineEntryKind(name, spec) {
9259
9265
  class: "config",
9260
9266
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9261
9267
  retention: { none: "an entry goes only when the operator deletes it" },
9268
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9262
9269
  ...spec.database === void 0 ? {} : { database: spec.database },
9263
9270
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9264
9271
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -14725,7 +14732,13 @@ var MigrationLedgerEntrySchema = object({
14725
14732
  * joined by the engine from the catalogues it knows this boot. Absent when no
14726
14733
  * catalogue lists the id (a newer database).
14727
14734
  */
14728
- meta: MigrationMetaSchema.optional()
14735
+ meta: MigrationMetaSchema.optional(),
14736
+ /**
14737
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14738
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14739
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14740
+ */
14741
+ ownerNotLoaded: boolean().optional()
14729
14742
  });
14730
14743
  /** A catalogued migration that is not applied, with where it stands. */
14731
14744
  var MigrationPendingEntrySchema = object({
@@ -56803,6 +56816,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56803
56816
  }), object({ success: boolean() }), {
56804
56817
  kind: "mutation",
56805
56818
  auth: "admin"
56819
+ }), method(object({ id: string() }), object({
56820
+ rows: number().int().nonnegative(),
56821
+ secrets: number().int().nonnegative()
56822
+ }), {
56823
+ kind: "mutation",
56824
+ auth: "admin"
56806
56825
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56807
56826
  addonId: string(),
56808
56827
  settings: record(string(), unknown())
@@ -86591,6 +86610,13 @@ Object.freeze({
86591
86610
  access: "create",
86592
86611
  role: "admin"
86593
86612
  },
86613
+ "integrations.clearSettings": {
86614
+ capName: "integrations",
86615
+ capScope: "system",
86616
+ addonId: null,
86617
+ access: "delete",
86618
+ role: "admin"
86619
+ },
86594
86620
  "integrations.create": {
86595
86621
  capName: "integrations",
86596
86622
  capScope: "system",
@@ -5980,7 +5980,7 @@ var ZodIssueCode = {
5980
5980
  var ZodFirstPartyTypeKind;
5981
5981
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5982
5982
  //#endregion
5983
- //#region ../types/dist/sleep-BaWhXZFm.mjs
5983
+ //#region ../types/dist/sleep-D1jjWAr8.mjs
5984
5984
  /**
5985
5985
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5986
5986
  * window to float samples (D455).
@@ -6590,7 +6590,7 @@ var NOT_RUN = {
6590
6590
  * throws: an unreachable or refusing engine is logged and reported `declared: false`,
6591
6591
  * because the hub (and this addon's boot) must not hang on a migration.
6592
6592
  */
6593
- async function runAddonMigrations(host, migrations, logger) {
6593
+ async function runAddonMigrations(host, migrations, logger, options = {}) {
6594
6594
  const first = migrations[0];
6595
6595
  if (first === void 0) return NOT_RUN;
6596
6596
  const owner = first.meta.owner;
@@ -6619,7 +6619,9 @@ async function runAddonMigrations(host, migrations, logger) {
6619
6619
  else if (status === "blocked") blocked.push(id);
6620
6620
  else skipped.push(id);
6621
6621
  };
6622
+ const selected = options.only === void 0 ? null : new Set(options.only);
6622
6623
  for (const step of declaration.steps) {
6624
+ if (selected !== null && !selected.has(step.id)) continue;
6623
6625
  const bound = byId.get(step.id);
6624
6626
  if (bound === void 0 || step.action === "skip") {
6625
6627
  skipped.push(step.id);
@@ -7779,14 +7781,18 @@ var BaseAddon = class {
7779
7781
  * nothing; the boot continues. A migration that FAILED (or was blocked by one)
7780
7782
  * and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
7781
7783
  * serve data whose shape it cannot trust; `tolerate` migrations never do.
7784
+ *
7785
+ * ALWAYS pass the whole catalogue; a phase that must run only some of it passes
7786
+ * `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
7787
+ * omitted applied ids "newer").
7782
7788
  */
7783
- async runMigrations(migrations) {
7789
+ async runMigrations(migrations, options) {
7784
7790
  const api = this.ctx.api.migrations;
7785
7791
  const report = await runAddonMigrations({
7786
7792
  declareOwner: (input) => api.declareOwner.mutate(input),
7787
7793
  startMigration: (input) => api.startMigration.mutate(input),
7788
7794
  finishMigration: (input) => api.finishMigration.mutate(input)
7789
- }, migrations, this.ctx.logger);
7795
+ }, migrations, this.ctx.logger, options);
7790
7796
  if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7791
7797
  return report;
7792
7798
  }
@@ -9258,6 +9264,7 @@ function defineEntryKind(name, spec) {
9258
9264
  class: "config",
9259
9265
  writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9260
9266
  retention: { none: "an entry goes only when the operator deletes it" },
9267
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9261
9268
  ...spec.database === void 0 ? {} : { database: spec.database },
9262
9269
  ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9263
9270
  ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
@@ -14724,7 +14731,13 @@ var MigrationLedgerEntrySchema = object({
14724
14731
  * joined by the engine from the catalogues it knows this boot. Absent when no
14725
14732
  * catalogue lists the id (a newer database).
14726
14733
  */
14727
- meta: MigrationMetaSchema.optional()
14734
+ meta: MigrationMetaSchema.optional(),
14735
+ /**
14736
+ * Set when this is an applied row of an addon owner whose catalogue the answering
14737
+ * process has not loaded (the owner has not declared yet): the row is applied, the
14738
+ * process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
14739
+ */
14740
+ ownerNotLoaded: boolean().optional()
14728
14741
  });
14729
14742
  /** A catalogued migration that is not applied, with where it stands. */
14730
14743
  var MigrationPendingEntrySchema = object({
@@ -56802,6 +56815,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
56802
56815
  }), object({ success: boolean() }), {
56803
56816
  kind: "mutation",
56804
56817
  auth: "admin"
56818
+ }), method(object({ id: string() }), object({
56819
+ rows: number().int().nonnegative(),
56820
+ secrets: number().int().nonnegative()
56821
+ }), {
56822
+ kind: "mutation",
56823
+ auth: "admin"
56805
56824
  }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
56806
56825
  addonId: string(),
56807
56826
  settings: record(string(), unknown())
@@ -86590,6 +86609,13 @@ Object.freeze({
86590
86609
  access: "create",
86591
86610
  role: "admin"
86592
86611
  },
86612
+ "integrations.clearSettings": {
86613
+ capName: "integrations",
86614
+ capScope: "system",
86615
+ addonId: null,
86616
+ access: "delete",
86617
+ role: "admin"
86618
+ },
86593
86619
  "integrations.create": {
86594
86620
  capName: "integrations",
86595
86621
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.656",
3
+ "version": "1.2.658",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -134,7 +134,7 @@
134
134
  "description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
135
135
  },
136
136
  "requires": {
137
- "server": ">=1.2.762"
137
+ "server": ">=1.2.766"
138
138
  }
139
139
  },
140
140
  "files": [