@camstack/addon-provider-homeassistant 1.2.654 → 1.2.655

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as fanControlCapability, A as bestLocationMatch, At as scriptRunnerCapability, B as climateControlCapability, Bt as valveCapability, C as accessoriesCapability, Ct as quantityFromUnit, D as ambientLightSensorCapability, Dt as runningFromMediaPlayer, E as alarmPanelCapability, Et as runningFromLawnMower, F as buildAddonRouteProvider, Ft as temperatureSensorCapability, G as contactCapability, Gt as RecordSecretUnavailableError, H as connectivityCapability, Ht as waterHeaterCapability, I as buildDeviceAdminUrl, It as tryConvertDelta, J as deviceAdminLinkCapability, Jt as defineAddonMigrations, K as controlCapability, Kt as baseUnitOf, L as buttonCapability, Lt as tryConvertUnit, M as boardKeySourceCapability, Mt as smokeCapability, N as brightnessCapability, Nt as switchCapability, O as automationControlCapability, Ot as runningFromVacuumState, P as brokerCapability, Pt as tamperCapability, Q as eventEmitterCapability, R as calendarSourceCapability, Rt as updateCapability, S as TargetSchema, St as publicEndpointReaderFrom, T as airQualitySensorCapability, Ut as weatherCapability, V as colorCapability, Vt as vibrationCapability, Xt as defineRecordKinds, Y as deviceAdoptionCapability, Yt as defineEntryKind, Z as enumSensorCapability, Zt as entryBody, _t as preconditionOf, an as number, at as ingestReading, bt as pressureSensorCapability, c as BaseDevice, cn as string, ct as mediaPlayerCapability, dn as url, dt as notificationOutputCapability, en as _enum, et as floodCapability, fn as DeviceFeature, ft as notifierCapability, g as EnumSensorDateTimeFormatSchema, gn as EventCategory, gt as powerMeterCapability, hn as errMsg, ht as parseAdminBase, i as HA_BROKER_KIND, in as literal, it as imageCapability, j as binaryCapability, k as batteryCapability, kt as runningStateCapability, l as BaseDeviceProvider, ln as union, lt as motionCapability, mn as DeviceType, nn as boolean, nt as humidifierCapability, on as object, ot as lawnMowerControlCapability, pn as DeviceRole, pt as numericSensorCapability, q as coverCapability, qt as createEvent, rn as discriminatedUnion, rt as humiditySensorCapability, sn as record, st as lockControlCapability, t as haInstanceSettingsPortsOver, tn as array, tt as gasCapability, un as unknown, ut as normalizeUnit, vt as prepareNotification, w as addonRoutesCapability, wt as resolveAttachmentUrls, x as RUNNING_STATE_COLD, yt as presenceCapability, z as carbonMonoxideCapability, zt as vacuumControlCapability } from "./ha-instance-settings-g5pwb3Fi.mjs";
1
+ import { $ as enumSensorCapability, $t as baseUnitOf, A as automationControlCapability, B as calendarSourceCapability, Bt as tamperCapability, C as RecordIntegrationInstances, Cn as EventCategory, Ct as prepareNotification, D as airQualitySensorCapability, Dt as publicEndpointReaderFrom, E as addonRoutesCapability, F as brightnessCapability, Ft as scriptRunnerCapability, Gt as vacuumControlCapability, H as climateControlCapability, Ht as tryConvertDelta, I as brokerCapability, J as controlCapability, Jt as waterHeaterCapability, Kt as valveCapability, L as buildAddonRouteProvider, Lt as smokeCapability, M as bestLocationMatch, Mt as runningFromMediaPlayer, N as binaryCapability, Nt as runningFromVacuumState, O as alarmPanelCapability, Ot as quantityFromUnit, P as boardKeySourceCapability, Pt as runningStateCapability, Qt as RecordSecretUnavailableError, R as buildDeviceAdminUrl, S as RUNNING_STATE_COLD, Sn as errMsg, St as preconditionOf, T as accessoriesCapability, Tt as pressureSensorCapability, U as colorCapability, Ut as tryConvertUnit, V as carbonMonoxideCapability, Vt as temperatureSensorCapability, W as connectivityCapability, Wt as updateCapability, X as deviceAdminLinkCapability, Y as coverCapability, Yt as weatherCapability, Z as deviceAdoptionCapability, _n as unknown, _t as notifierCapability, at as gasCapability, bn as DeviceRole, bt as parseAdminBase, c as BaseDevice, cn as array, ct as imageCapability, dn as literal, dt as lawnMowerControlCapability, en as createEvent, et as eventEmitterCapability, fn as number, ft as lockControlCapability, g as EnumSensorDateTimeFormatSchema, gn as union, gt as notificationOutputCapability, hn as string, ht as normalizeUnit, i as HA_BROKER_KIND, in as entryBody, it as floodCapability, j as batteryCapability, jt as runningFromLawnMower, k as ambientLightSensorCapability, kt as resolveAttachmentUrls, l as BaseDeviceProvider, ln as boolean, lt as ingestReading, mn as record, mt as motionCapability, nn as defineEntryKind, nt as flattenNestedRecords, ot as humidifierCapability, pn as object, pt as mediaPlayerCapability, q as contactCapability, qt as vibrationCapability, rn as defineRecordKinds, rt as flattenPortsOver, sn as _enum, st as humiditySensorCapability, t as haInstanceSettingsPortsOver, tn as defineAddonMigrations, tt as fanControlCapability, un as discriminatedUnion, ut as integrationInstancesCapability, vn as url, vt as numericSensorCapability, w as TargetSchema, wt as presenceCapability, xn as DeviceType, xt as powerMeterCapability, yn as DeviceFeature, z as buttonCapability, zt as switchCapability } from "./ha-instance-settings-DLDNOmgL.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
@@ -5764,22 +5764,16 @@ var TEST_HANDSHAKE_TIMEOUT_MS = 8e3;
5764
5764
  var HaBrokerEntrySchema = object({
5765
5765
  id: string(),
5766
5766
  name: string(),
5767
- /** Either Supervisor (token from env) or long-lived (persisted). */
5768
- auth: discriminatedUnion("kind", [object({
5769
- kind: literal("supervisor"),
5770
- supervisorToken: string().meta({ secret: true })
5771
- }), object({
5772
- kind: literal("long-lived"),
5773
- baseUrl: string(),
5774
- accessToken: string().meta({ secret: true })
5775
- })]),
5776
- /** Optional FK to the integration record that spawned this broker.
5777
- * Populated when the wizard's "+ New Integration" flow created the
5778
- * broker; null when the operator added it via the Settings panel
5779
- * directly. Used by the integration listener to keep the broker
5780
- * list in sync with the integrations table (cascade-delete on
5781
- * integration removal). */
5782
- integrationId: string().optional()
5767
+ /** Supervisor (token from env) or long-lived (persisted); picks which fields below are used. */
5768
+ authKind: _enum(["long-lived", "supervisor"]),
5769
+ /** Origin URL of the HA instance; long-lived only. */
5770
+ baseUrl: string().optional(),
5771
+ accessToken: string().optional().meta({ secret: true }),
5772
+ supervisorToken: string().optional().meta({ secret: true }),
5773
+ /** Optional FK to the integration record that spawned this broker. */
5774
+ integrationId: string().optional(),
5775
+ /** The instance's own switch (D996); absent means on. */
5776
+ enabled: boolean().optional()
5783
5777
  });
5784
5778
  /**
5785
5779
  * Validates a `broker.add` `settings` payload for the HA kind. Accepts:
@@ -5804,6 +5798,34 @@ function settingsToAuth(settings) {
5804
5798
  accessToken: parsed.accessToken
5805
5799
  };
5806
5800
  }
5801
+ /** The flat entry fields of an auth config: the one place the two shapes meet. */
5802
+ function authToEntryFields(auth) {
5803
+ return auth.kind === "supervisor" ? {
5804
+ authKind: "supervisor",
5805
+ supervisorToken: auth.supervisorToken
5806
+ } : {
5807
+ authKind: "long-lived",
5808
+ baseUrl: auth.baseUrl,
5809
+ accessToken: auth.accessToken
5810
+ };
5811
+ }
5812
+ /** The auth config an entry stands for; null while the entry is not filled in. */
5813
+ function haAuthOf(entry) {
5814
+ const parsed = HaAuthConfigSchema.safeParse(entry.authKind === "supervisor" ? {
5815
+ kind: "supervisor",
5816
+ supervisorToken: entry.supervisorToken ?? ""
5817
+ } : {
5818
+ kind: "long-lived",
5819
+ baseUrl: entry.baseUrl ?? "",
5820
+ accessToken: entry.accessToken ?? ""
5821
+ });
5822
+ return parsed.success ? parsed.data : null;
5823
+ }
5824
+ /** The flat settings payload (`settingsToAuth` shape) of an entry; null while it is not filled in. */
5825
+ function entryToSettings(entry) {
5826
+ const auth = haAuthOf(entry);
5827
+ return auth === null ? null : authToSettings(auth);
5828
+ }
5807
5829
  function authToSettings(auth) {
5808
5830
  if (auth.kind === "supervisor") return { supervisorToken: auth.supervisorToken };
5809
5831
  return {
@@ -5866,7 +5888,7 @@ var HaBrokerRegistry = class {
5866
5888
  * started concurrently; transient errors don't block bootstrap. */
5867
5889
  async restore(entries) {
5868
5890
  this.seedCounterFromEntries(entries);
5869
- await Promise.all(entries.map(async (entry) => {
5891
+ await Promise.all(entries.filter((entry) => entry.enabled !== false && haAuthOf(entry) !== null).map(async (entry) => {
5870
5892
  try {
5871
5893
  await this.startManagerForEntry(entry);
5872
5894
  } catch (err) {
@@ -5927,7 +5949,7 @@ var HaBrokerRegistry = class {
5927
5949
  const entry = {
5928
5950
  id: options.id !== void 0 ? this.adoptId(options.id) : this.allocateId(),
5929
5951
  name,
5930
- auth,
5952
+ ...authToEntryFields(auth),
5931
5953
  ...options.integrationId !== void 0 ? { integrationId: options.integrationId } : {}
5932
5954
  };
5933
5955
  await this.startManagerForEntry(entry);
@@ -5948,7 +5970,7 @@ var HaBrokerRegistry = class {
5948
5970
  return {
5949
5971
  id,
5950
5972
  name: existing.getInfo().name,
5951
- auth
5973
+ ...authToEntryFields(auth)
5952
5974
  };
5953
5975
  }
5954
5976
  /**
@@ -6131,10 +6153,12 @@ var HaBrokerRegistry = class {
6131
6153
  this.logger.debug("ha-broker already active — reusing existing connection", { meta: { brokerId: entry.id } });
6132
6154
  return;
6133
6155
  }
6156
+ const auth = haAuthOf(entry);
6157
+ if (auth === null) throw new Error(`ha-broker '${entry.id}' has no complete credentials`);
6134
6158
  const manager = new HaIntegrationManager({
6135
6159
  id: entry.id,
6136
6160
  name: entry.name,
6137
- auth: entry.auth,
6161
+ auth,
6138
6162
  logger: this.logger,
6139
6163
  eventBus: this.eventBus,
6140
6164
  onConnected: (brokerId) => this.onBrokerConnected?.(brokerId),
@@ -7194,10 +7218,15 @@ function createHaIconRouteProvider() {
7194
7218
  var haBrokersEntryKind = defineEntryKind("brokers", {
7195
7219
  title: "Home Assistant brokers",
7196
7220
  schema: entryBody({
7197
- auth: HaBrokerEntrySchema.shape.auth,
7198
- integrationId: string().optional()
7221
+ authKind: HaBrokerEntrySchema.shape.authKind,
7222
+ baseUrl: HaBrokerEntrySchema.shape.baseUrl,
7223
+ accessToken: HaBrokerEntrySchema.shape.accessToken,
7224
+ supervisorToken: HaBrokerEntrySchema.shape.supervisorToken,
7225
+ integrationId: HaBrokerEntrySchema.shape.integrationId,
7226
+ enabled: HaBrokerEntrySchema.shape.enabled
7199
7227
  }),
7200
- writeTrigger: "an operator adds, edits or removes a Home Assistant broker connection"
7228
+ writeTrigger: "an operator adds, edits or removes a Home Assistant broker connection",
7229
+ retiredSecretPaths: ["auth.accessToken", "auth.supervisorToken"]
7201
7230
  });
7202
7231
  var haRecordKinds = defineRecordKinds({
7203
7232
  /**
@@ -7719,8 +7748,12 @@ function rowToEntry(id, data) {
7719
7748
  const parsed = HaBrokerEntrySchema.safeParse({
7720
7749
  id,
7721
7750
  name: data.name,
7722
- auth: data.auth,
7723
- integrationId: data.integrationId
7751
+ authKind: data.authKind,
7752
+ baseUrl: data.baseUrl,
7753
+ accessToken: data.accessToken,
7754
+ supervisorToken: data.supervisorToken,
7755
+ integrationId: data.integrationId,
7756
+ enabled: data.enabled
7724
7757
  });
7725
7758
  return parsed.success ? parsed.data : null;
7726
7759
  }
@@ -7759,10 +7792,9 @@ var HaBrokerStore = class HaBrokerStore {
7759
7792
  }
7760
7793
  }
7761
7794
  async upsert(entry) {
7762
- const { id, name, auth, integrationId } = entry;
7795
+ const { id, integrationId, ...fields } = entry;
7763
7796
  const body = {
7764
- name,
7765
- auth,
7797
+ ...Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0)),
7766
7798
  ...integrationId === void 0 ? {} : { integrationId }
7767
7799
  };
7768
7800
  const parsed = haBrokersEntryKind.schema.safeParse(body);
@@ -7783,6 +7815,97 @@ var HaBrokerStore = class HaBrokerStore {
7783
7815
  }
7784
7816
  };
7785
7817
  //#endregion
7818
+ //#region src/ha-instances.ts
7819
+ /**
7820
+ * The Home Assistant connections as `integration-instances` (planning#408 P3): one instance per
7821
+ * `brokers` record, over the SAME records and ids (`ha_NNN`) the `broker` cap serves. The body is
7822
+ * flat, so the form the instances page edits is the stored shape.
7823
+ */
7824
+ var HA_INSTANCE_KIND = "home-assistant";
7825
+ function isHaBodyConfigured(body) {
7826
+ return body.authKind === "supervisor" ? (body.supervisorToken ?? "").length > 0 : (body.baseUrl ?? "").length > 0 && (body.accessToken ?? "").length > 0;
7827
+ }
7828
+ function statusOf(info) {
7829
+ if (info === null) return {
7830
+ state: "unknown",
7831
+ label: "Not connected"
7832
+ };
7833
+ if (info.status === "connected") return {
7834
+ state: "ok",
7835
+ label: "Connected"
7836
+ };
7837
+ if (info.status === "connecting") return {
7838
+ state: "unknown",
7839
+ label: "Connecting"
7840
+ };
7841
+ return {
7842
+ state: "error",
7843
+ label: info.status,
7844
+ ...info.error !== null ? { detail: info.error } : {}
7845
+ };
7846
+ }
7847
+ function buildHaInstances(records, deps) {
7848
+ return new RecordIntegrationInstances(records, haBrokersEntryKind, {
7849
+ description: {
7850
+ instanceMode: "multiple",
7851
+ kind: "connection",
7852
+ canTest: true,
7853
+ kinds: [{
7854
+ id: HA_INSTANCE_KIND,
7855
+ label: "Home Assistant"
7856
+ }],
7857
+ allowRestoreId: true
7858
+ },
7859
+ formSchema: () => haBrokerSettingsSchema,
7860
+ initialBody: (name) => ({
7861
+ name,
7862
+ authKind: "long-lived"
7863
+ }),
7864
+ mintId: () => deps.mintId(),
7865
+ kindOf: () => HA_INSTANCE_KIND,
7866
+ health: (_body, id) => {
7867
+ const info = deps.infoOf(id);
7868
+ return {
7869
+ status: statusOf(info),
7870
+ ...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
7871
+ };
7872
+ },
7873
+ configured: isHaBodyConfigured,
7874
+ onChanged: deps.onChanged,
7875
+ test: async ({ id, values }) => {
7876
+ if (["accessToken", "supervisorToken"].some((key) => {
7877
+ const v = values[key];
7878
+ return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
7879
+ })) {
7880
+ const r = await deps.probe(values);
7881
+ return r.ok ? {
7882
+ success: true,
7883
+ status: "validated",
7884
+ ...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
7885
+ } : {
7886
+ success: false,
7887
+ status: "rejected",
7888
+ error: r.error ?? "Home Assistant rejected the connection"
7889
+ };
7890
+ }
7891
+ const info = id === null ? null : deps.infoOf(id);
7892
+ if (info === null) return {
7893
+ success: false,
7894
+ status: "inconclusive",
7895
+ error: "Not connected: save the connection first"
7896
+ };
7897
+ return info.status === "connected" ? {
7898
+ success: true,
7899
+ status: "validated"
7900
+ } : {
7901
+ success: false,
7902
+ status: "rejected",
7903
+ error: info.error ?? `status: ${info.status}`
7904
+ };
7905
+ }
7906
+ });
7907
+ }
7908
+ //#endregion
7786
7909
  //#region src/ha-board-key-source.ts
7787
7910
  /**
7788
7911
  * `board-key-source` for Home Assistant: reads an ESPHome board's API
@@ -7956,6 +8079,38 @@ function createHaBoardKeyProvider(source) {
7956
8079
  };
7957
8080
  }
7958
8081
  //#endregion
8082
+ //#region src/migrations/flatten-broker-records.ts
8083
+ /**
8084
+ * The Home Assistant broker records keep their connection flat (planning#408 P3): the nested
8085
+ * `auth` object (`{ kind, baseUrl, accessToken }` or `{ kind, supervisorToken }`) becomes the top
8086
+ * level `authKind`, `baseUrl`, `accessToken` and `supervisorToken`, under the SAME record id, so
8087
+ * the devices that carry `ha_NNN` keep their broker. The work is `flattenNestedRecords`: every
8088
+ * secret is read before anything is written, read back from its new keychain ref before the
8089
+ * nested object goes, and the old refs are deleted last.
8090
+ */
8091
+ var FLATTEN_BROKER_RECORDS_ID = "provider-homeassistant/2026-11-04-flatten-broker-records";
8092
+ function text(value) {
8093
+ return typeof value === "string" ? value : void 0;
8094
+ }
8095
+ async function flattenBrokerRecords(ports) {
8096
+ return flattenNestedRecords(ports, {
8097
+ legacyKey: "auth",
8098
+ flatSecretKeys: ["accessToken", "supervisorToken"],
8099
+ toFlat: (legacy) => {
8100
+ const kind = legacy["kind"] === "supervisor" ? "supervisor" : "long-lived";
8101
+ const baseUrl = text(legacy["baseUrl"]);
8102
+ const accessToken = text(legacy["accessToken"]);
8103
+ const supervisorToken = text(legacy["supervisorToken"]);
8104
+ return {
8105
+ authKind: kind,
8106
+ ...baseUrl !== void 0 ? { baseUrl } : {},
8107
+ ...accessToken !== void 0 ? { accessToken } : {},
8108
+ ...supervisorToken !== void 0 ? { supervisorToken } : {}
8109
+ };
8110
+ }
8111
+ });
8112
+ }
8113
+ //#endregion
7959
8114
  //#region src/migrations/calendar-ids-to-integration.ts
7960
8115
  /**
7961
8116
  * The HA calendars an instance imports are a fact of THAT instance (D985): each Home Assistant
@@ -8045,37 +8200,56 @@ async function planDropSharedCalendarIds(ports) {
8045
8200
  //#endregion
8046
8201
  //#region src/migrations/index.ts
8047
8202
  /** The `provider-homeassistant` catalogue (D987): offered to the engine when the addon initialises. */
8048
- var ADDON_MIGRATIONS = [{
8049
- meta: {
8050
- id: "provider-homeassistant/2026-11-03-calendar-ids-to-integration",
8051
- owner: "provider-homeassistant",
8052
- level: "addon-state",
8053
- seq: 1,
8054
- dependsOn: [],
8055
- store: "settings",
8056
- scope: "cluster",
8057
- onFailure: "tolerate",
8058
- bodyKind: "procedure",
8059
- description: "Copy the shared calendar set to the settings of every Home Assistant integration that has none",
8060
- fixtureMark: "the broker link stored beside the calendars of an integration"
8203
+ var ADDON_MIGRATIONS = [
8204
+ {
8205
+ meta: {
8206
+ id: "provider-homeassistant/2026-11-03-calendar-ids-to-integration",
8207
+ owner: "provider-homeassistant",
8208
+ level: "addon-state",
8209
+ seq: 1,
8210
+ dependsOn: [],
8211
+ store: "settings",
8212
+ scope: "cluster",
8213
+ onFailure: "tolerate",
8214
+ bodyKind: "procedure",
8215
+ description: "Copy the shared calendar set to the settings of every Home Assistant integration that has none",
8216
+ fixtureMark: "the broker link stored beside the calendars of an integration"
8217
+ },
8218
+ procedure: copyCalendarIdsToIntegrations
8061
8219
  },
8062
- procedure: copyCalendarIdsToIntegrations
8063
- }, {
8064
- meta: {
8065
- id: "provider-homeassistant/2026-11-03-drop-shared-calendar-ids",
8066
- owner: "provider-homeassistant",
8067
- level: "addon-state",
8068
- seq: 2,
8069
- dependsOn: ["provider-homeassistant/2026-11-03-calendar-ids-to-integration"],
8070
- store: "settings",
8071
- scope: "cluster",
8072
- onFailure: "tolerate",
8073
- bodyKind: "plan",
8074
- description: "Delete the shared calendar set once every integration shows its own",
8075
- fixtureMark: "the shared set parked when an integration holds a different one"
8220
+ {
8221
+ meta: {
8222
+ id: "provider-homeassistant/2026-11-03-drop-shared-calendar-ids",
8223
+ owner: "provider-homeassistant",
8224
+ level: "addon-state",
8225
+ seq: 2,
8226
+ dependsOn: ["provider-homeassistant/2026-11-03-calendar-ids-to-integration"],
8227
+ store: "settings",
8228
+ scope: "cluster",
8229
+ onFailure: "tolerate",
8230
+ bodyKind: "plan",
8231
+ description: "Delete the shared calendar set once every integration shows its own",
8232
+ fixtureMark: "the shared set parked when an integration holds a different one"
8233
+ },
8234
+ plan: planDropSharedCalendarIds
8076
8235
  },
8077
- plan: planDropSharedCalendarIds
8078
- }];
8236
+ {
8237
+ meta: {
8238
+ id: FLATTEN_BROKER_RECORDS_ID,
8239
+ owner: "provider-homeassistant",
8240
+ level: "addon-state",
8241
+ seq: 3,
8242
+ dependsOn: [],
8243
+ store: "settings",
8244
+ scope: "cluster",
8245
+ onFailure: "block-owner",
8246
+ bodyKind: "procedure",
8247
+ description: "Flatten the nested auth of every Home Assistant broker record under its own id, moving each token to a top-level keychain secret",
8248
+ fixtureMark: "a non-secret field stored beside the auth of a broker record"
8249
+ },
8250
+ procedure: flattenBrokerRecords
8251
+ }
8252
+ ];
8079
8253
  //#endregion
8080
8254
  //#region src/ha-calendar-source.ts
8081
8255
  function isCalendarEntity(entityId) {
@@ -8689,10 +8863,7 @@ function planStandaloneReconcile(devices, freshById, brokerId) {
8689
8863
  var ADOPTION_FILTER_DEVICES = "devices";
8690
8864
  var ADOPTION_FILTER_ENTITIES = "entities";
8691
8865
  var HA_KIND = "home-assistant";
8692
- var DEFAULT_CONFIG = {
8693
- brokers: [],
8694
- calendarIds: []
8695
- };
8866
+ var DEFAULT_CONFIG = { calendarIds: [] };
8696
8867
  /**
8697
8868
  * Home Assistant provider addon.
8698
8869
  *
@@ -8739,6 +8910,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8739
8910
  }
8740
8911
  registry = null;
8741
8912
  brokerStore = null;
8913
+ instances = null;
8742
8914
  /** Runtime broker rows (records when B2 is live, else settings). */
8743
8915
  activeBrokers = [];
8744
8916
  /** Periodic re-sync sweep handle (cleared on shutdown). */
@@ -8753,11 +8925,26 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8753
8925
  constructor() {
8754
8926
  super({ ...DEFAULT_CONFIG });
8755
8927
  }
8928
+ flattenPorts(records) {
8929
+ if (this.ctx.secrets === void 0) throw new Error("provider-homeassistant: ctx.secrets is required");
8930
+ return {
8931
+ readStored: (key) => this.readStoredGlobalKey(key),
8932
+ ...haInstanceSettingsPortsOver(this.ctx.api.integrations),
8933
+ ...flattenPortsOver(records.kind(haBrokersEntryKind))
8934
+ };
8935
+ }
8756
8936
  async onInitialize() {
8757
8937
  const records = this.ctx.records;
8758
8938
  if (records === void 0) throw new Error("provider-homeassistant: ctx.records is required (hub-only addon)");
8759
8939
  await records.declare(haAllRecordKinds);
8760
8940
  this.brokerStore = HaBrokerStore.of(records);
8941
+ this.instances = buildHaInstances(records, {
8942
+ mintId: () => this.requireRegistry().allocateId(),
8943
+ infoOf: (id) => this.registry?.get(id) ?? null,
8944
+ onChanged: (id) => this.syncBrokerRuntime(id),
8945
+ probe: (settings) => this.requireRegistry().testSettings(settings)
8946
+ });
8947
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8761
8948
  this.activeBrokers = await this.loadPersistedBrokers();
8762
8949
  this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
8763
8950
  this.registry.setOnBrokerConnected((brokerId) => {
@@ -8774,10 +8961,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8774
8961
  this.reconcileAllConnectedBrokers();
8775
8962
  }, HaProviderAddon.RESYNC_INTERVAL_MS);
8776
8963
  this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
8777
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, {
8778
- readStored: (key) => this.readStoredGlobalKey(key),
8779
- ...haInstanceSettingsPortsOver(this.ctx.api.integrations)
8780
- }));
8964
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8781
8965
  await this.reconcileIntegrationsToBrokers();
8782
8966
  this.subscribeIntegrationLifecycle();
8783
8967
  return [
@@ -8785,6 +8969,10 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8785
8969
  capability: brokerCapability,
8786
8970
  provider: this.buildBrokerProvider()
8787
8971
  },
8972
+ {
8973
+ capability: integrationInstancesCapability,
8974
+ provider: this.requireInstances().toProvider()
8975
+ },
8788
8976
  {
8789
8977
  capability: deviceAdoptionCapability,
8790
8978
  provider: this.buildAdoptionProvider()
@@ -9064,11 +9252,8 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9064
9252
  for (const entry of persisted) {
9065
9253
  if (entry.id && liveIds.has(entry.id)) {
9066
9254
  try {
9067
- const settings = entry.auth.kind === "supervisor" ? { supervisorToken: entry.auth.supervisorToken } : {
9068
- baseUrl: entry.auth.baseUrl,
9069
- accessToken: entry.auth.accessToken
9070
- };
9071
- await reg.updateEntry(entry.id, settings);
9255
+ const settings = entryToSettings(entry);
9256
+ if (settings !== null) await reg.updateEntry(entry.id, settings);
9072
9257
  } catch (err) {
9073
9258
  this.ctx.logger.warn("ha onConfigChanged: updateEntry failed", { meta: {
9074
9259
  brokerId: entry.id,
@@ -9078,10 +9263,8 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9078
9263
  continue;
9079
9264
  }
9080
9265
  try {
9081
- const settings = entry.auth.kind === "supervisor" ? { supervisorToken: entry.auth.supervisorToken } : {
9082
- baseUrl: entry.auth.baseUrl,
9083
- accessToken: entry.auth.accessToken
9084
- };
9266
+ const settings = entryToSettings(entry);
9267
+ if (settings === null) continue;
9085
9268
  const created = await reg.createEntry(entry.name, settings);
9086
9269
  if (created.id !== entry.id) {
9087
9270
  const next = persisted.map((b) => b === entry ? {
@@ -9099,21 +9282,59 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9099
9282
  }
9100
9283
  }
9101
9284
  async loadPersistedBrokers() {
9102
- const store = this.brokerStore;
9103
- if (store !== null) {
9104
- const fromRecords = await store.listResolved();
9105
- if (fromRecords.length > 0) return fromRecords;
9285
+ return this.requireBrokerStore().listResolved();
9286
+ }
9287
+ requireInstances() {
9288
+ if (this.instances === null) throw new Error("provider-homeassistant: instances not ready");
9289
+ return this.instances;
9290
+ }
9291
+ /**
9292
+ * After an instance changed (created, edited, switched, deleted) the runtime follows the record:
9293
+ * a missing record removes the manager and its adopted devices, an off or unfilled one stops the
9294
+ * manager but keeps the devices, a filled one starts or re-authenticates it.
9295
+ */
9296
+ async syncBrokerRuntime(id) {
9297
+ const reg = this.requireRegistry();
9298
+ const entry = await this.requireBrokerStore().getById(id);
9299
+ this.activeBrokers = await this.loadPersistedBrokers();
9300
+ if (entry === null) {
9301
+ await reg.removeEntry(id);
9302
+ const deviceRegistry = this.ctx.kernel.deviceRegistry;
9303
+ const devices = this.ctx.kernel.devices;
9304
+ if (deviceRegistry && devices) await cascadeRemoveDevicesForBroker({
9305
+ reg: deviceRegistry,
9306
+ devices,
9307
+ addonId: this.ctx.id,
9308
+ brokerId: id,
9309
+ logger: this.ctx.logger
9310
+ });
9311
+ return;
9106
9312
  }
9107
- return this.config.brokers;
9313
+ const settings = entry.enabled === false ? null : entryToSettings(entry);
9314
+ if (settings === null) {
9315
+ await reg.removeEntry(id);
9316
+ return;
9317
+ }
9318
+ try {
9319
+ if (reg.get(id) !== null) await reg.updateEntry(id, settings);
9320
+ else await reg.createEntry(entry.name, settings, {
9321
+ id,
9322
+ ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
9323
+ });
9324
+ } catch (err) {
9325
+ this.ctx.logger.warn("ha instance: could not start the broker", { meta: {
9326
+ brokerId: id,
9327
+ error: errMsg(err)
9328
+ } });
9329
+ }
9330
+ }
9331
+ requireBrokerStore() {
9332
+ if (this.brokerStore === null) throw new Error("provider-homeassistant: broker store not ready");
9333
+ return this.brokerStore;
9108
9334
  }
9109
9335
  async persistBrokerEntries(entries) {
9110
9336
  this.activeBrokers = [...entries];
9111
- const store = this.brokerStore;
9112
- if (store !== null) {
9113
- await store.replaceAll(entries);
9114
- return;
9115
- }
9116
- await this.updateGlobalSettings({ brokers: entries });
9337
+ await this.requireBrokerStore().replaceAll(entries);
9117
9338
  }
9118
9339
  buildBrokerProvider() {
9119
9340
  const ownerAddonId = this.ctx.id;
@@ -9187,7 +9408,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9187
9408
  getSettings: async ({ id }) => {
9188
9409
  const entry = this.activeBrokers.find((b) => b.id === id);
9189
9410
  if (!entry) return null;
9190
- return authToSettings(entry.auth);
9411
+ return entryToSettings(entry);
9191
9412
  },
9192
9413
  setSettings: async ({ id, settings }) => {
9193
9414
  if (!owns(id)) return;
@@ -9198,7 +9419,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9198
9419
  getBrokerConfig: async ({ id }) => {
9199
9420
  const entry = this.activeBrokers.find((b) => b.id === id);
9200
9421
  if (!entry) return null;
9201
- return authToSettings(entry.auth);
9422
+ return entryToSettings(entry);
9202
9423
  },
9203
9424
  getSettingsSchema: async ({ kind }) => {
9204
9425
  if (kind !== HA_KIND) return null;