@camstack/addon-provider-homeassistant 1.2.654 → 1.2.656

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 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";
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,98 @@ 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
+ role: "both",
7851
+ instanceMode: "multiple",
7852
+ kind: "connection",
7853
+ canTest: true,
7854
+ kinds: [{
7855
+ id: HA_INSTANCE_KIND,
7856
+ label: "Home Assistant"
7857
+ }],
7858
+ allowRestoreId: true
7859
+ },
7860
+ formSchema: () => haBrokerSettingsSchema,
7861
+ initialBody: (name) => ({
7862
+ name,
7863
+ authKind: "long-lived"
7864
+ }),
7865
+ mintId: () => deps.mintId(),
7866
+ kindOf: () => HA_INSTANCE_KIND,
7867
+ health: (_body, id) => {
7868
+ const info = deps.infoOf(id);
7869
+ return {
7870
+ status: statusOf(info),
7871
+ ...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
7872
+ };
7873
+ },
7874
+ configured: isHaBodyConfigured,
7875
+ onChanged: deps.onChanged,
7876
+ test: async ({ id, values }) => {
7877
+ if (["accessToken", "supervisorToken"].some((key) => {
7878
+ const v = values[key];
7879
+ return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
7880
+ })) {
7881
+ const r = await deps.probe(values);
7882
+ return r.ok ? {
7883
+ success: true,
7884
+ status: "validated",
7885
+ ...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
7886
+ } : {
7887
+ success: false,
7888
+ status: "rejected",
7889
+ error: r.error ?? "Home Assistant rejected the connection"
7890
+ };
7891
+ }
7892
+ const info = id === null ? null : deps.infoOf(id);
7893
+ if (info === null) return {
7894
+ success: false,
7895
+ status: "inconclusive",
7896
+ error: "Not connected: save the connection first"
7897
+ };
7898
+ return info.status === "connected" ? {
7899
+ success: true,
7900
+ status: "validated"
7901
+ } : {
7902
+ success: false,
7903
+ status: "rejected",
7904
+ error: info.error ?? `status: ${info.status}`
7905
+ };
7906
+ }
7907
+ });
7908
+ }
7909
+ //#endregion
7786
7910
  //#region src/ha-board-key-source.ts
7787
7911
  /**
7788
7912
  * `board-key-source` for Home Assistant: reads an ESPHome board's API
@@ -7956,6 +8080,38 @@ function createHaBoardKeyProvider(source) {
7956
8080
  };
7957
8081
  }
7958
8082
  //#endregion
8083
+ //#region src/migrations/flatten-broker-records.ts
8084
+ /**
8085
+ * The Home Assistant broker records keep their connection flat (planning#408 P3): the nested
8086
+ * `auth` object (`{ kind, baseUrl, accessToken }` or `{ kind, supervisorToken }`) becomes the top
8087
+ * level `authKind`, `baseUrl`, `accessToken` and `supervisorToken`, under the SAME record id, so
8088
+ * the devices that carry `ha_NNN` keep their broker. The work is `flattenNestedRecords`: every
8089
+ * secret is read before anything is written, read back from its new keychain ref before the
8090
+ * nested object goes, and the old refs are deleted last.
8091
+ */
8092
+ var FLATTEN_BROKER_RECORDS_ID = "provider-homeassistant/2026-11-04-flatten-broker-records";
8093
+ function text(value) {
8094
+ return typeof value === "string" ? value : void 0;
8095
+ }
8096
+ async function flattenBrokerRecords(ports) {
8097
+ return flattenNestedRecords(ports, {
8098
+ legacyKey: "auth",
8099
+ flatSecretKeys: ["accessToken", "supervisorToken"],
8100
+ toFlat: (legacy) => {
8101
+ const kind = legacy["kind"] === "supervisor" ? "supervisor" : "long-lived";
8102
+ const baseUrl = text(legacy["baseUrl"]);
8103
+ const accessToken = text(legacy["accessToken"]);
8104
+ const supervisorToken = text(legacy["supervisorToken"]);
8105
+ return {
8106
+ authKind: kind,
8107
+ ...baseUrl !== void 0 ? { baseUrl } : {},
8108
+ ...accessToken !== void 0 ? { accessToken } : {},
8109
+ ...supervisorToken !== void 0 ? { supervisorToken } : {}
8110
+ };
8111
+ }
8112
+ });
8113
+ }
8114
+ //#endregion
7959
8115
  //#region src/migrations/calendar-ids-to-integration.ts
7960
8116
  /**
7961
8117
  * The HA calendars an instance imports are a fact of THAT instance (D985): each Home Assistant
@@ -8045,37 +8201,56 @@ async function planDropSharedCalendarIds(ports) {
8045
8201
  //#endregion
8046
8202
  //#region src/migrations/index.ts
8047
8203
  /** 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"
8204
+ var ADDON_MIGRATIONS = [
8205
+ {
8206
+ meta: {
8207
+ id: "provider-homeassistant/2026-11-03-calendar-ids-to-integration",
8208
+ owner: "provider-homeassistant",
8209
+ level: "addon-state",
8210
+ seq: 1,
8211
+ dependsOn: [],
8212
+ store: "settings",
8213
+ scope: "cluster",
8214
+ onFailure: "tolerate",
8215
+ bodyKind: "procedure",
8216
+ description: "Copy the shared calendar set to the settings of every Home Assistant integration that has none",
8217
+ fixtureMark: "the broker link stored beside the calendars of an integration"
8218
+ },
8219
+ procedure: copyCalendarIdsToIntegrations
8061
8220
  },
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"
8221
+ {
8222
+ meta: {
8223
+ id: "provider-homeassistant/2026-11-03-drop-shared-calendar-ids",
8224
+ owner: "provider-homeassistant",
8225
+ level: "addon-state",
8226
+ seq: 2,
8227
+ dependsOn: ["provider-homeassistant/2026-11-03-calendar-ids-to-integration"],
8228
+ store: "settings",
8229
+ scope: "cluster",
8230
+ onFailure: "tolerate",
8231
+ bodyKind: "plan",
8232
+ description: "Delete the shared calendar set once every integration shows its own",
8233
+ fixtureMark: "the shared set parked when an integration holds a different one"
8234
+ },
8235
+ plan: planDropSharedCalendarIds
8076
8236
  },
8077
- plan: planDropSharedCalendarIds
8078
- }];
8237
+ {
8238
+ meta: {
8239
+ id: FLATTEN_BROKER_RECORDS_ID,
8240
+ owner: "provider-homeassistant",
8241
+ level: "addon-state",
8242
+ seq: 3,
8243
+ dependsOn: [],
8244
+ store: "settings",
8245
+ scope: "cluster",
8246
+ onFailure: "block-owner",
8247
+ bodyKind: "procedure",
8248
+ description: "Flatten the nested auth of every Home Assistant broker record under its own id, moving each token to a top-level keychain secret",
8249
+ fixtureMark: "a non-secret field stored beside the auth of a broker record"
8250
+ },
8251
+ procedure: flattenBrokerRecords
8252
+ }
8253
+ ];
8079
8254
  //#endregion
8080
8255
  //#region src/ha-calendar-source.ts
8081
8256
  function isCalendarEntity(entityId) {
@@ -8689,10 +8864,7 @@ function planStandaloneReconcile(devices, freshById, brokerId) {
8689
8864
  var ADOPTION_FILTER_DEVICES = "devices";
8690
8865
  var ADOPTION_FILTER_ENTITIES = "entities";
8691
8866
  var HA_KIND = "home-assistant";
8692
- var DEFAULT_CONFIG = {
8693
- brokers: [],
8694
- calendarIds: []
8695
- };
8867
+ var DEFAULT_CONFIG = { calendarIds: [] };
8696
8868
  /**
8697
8869
  * Home Assistant provider addon.
8698
8870
  *
@@ -8739,6 +8911,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8739
8911
  }
8740
8912
  registry = null;
8741
8913
  brokerStore = null;
8914
+ instances = null;
8742
8915
  /** Runtime broker rows (records when B2 is live, else settings). */
8743
8916
  activeBrokers = [];
8744
8917
  /** Periodic re-sync sweep handle (cleared on shutdown). */
@@ -8753,11 +8926,26 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8753
8926
  constructor() {
8754
8927
  super({ ...DEFAULT_CONFIG });
8755
8928
  }
8929
+ flattenPorts(records) {
8930
+ if (this.ctx.secrets === void 0) throw new Error("provider-homeassistant: ctx.secrets is required");
8931
+ return {
8932
+ readStored: (key) => this.readStoredGlobalKey(key),
8933
+ ...haInstanceSettingsPortsOver(this.ctx.api.integrations),
8934
+ ...flattenPortsOver(records.kind(haBrokersEntryKind))
8935
+ };
8936
+ }
8756
8937
  async onInitialize() {
8757
8938
  const records = this.ctx.records;
8758
8939
  if (records === void 0) throw new Error("provider-homeassistant: ctx.records is required (hub-only addon)");
8759
8940
  await records.declare(haAllRecordKinds);
8760
8941
  this.brokerStore = HaBrokerStore.of(records);
8942
+ this.instances = buildHaInstances(records, {
8943
+ mintId: () => this.requireRegistry().allocateId(),
8944
+ infoOf: (id) => this.registry?.get(id) ?? null,
8945
+ onChanged: (id) => this.syncBrokerRuntime(id),
8946
+ probe: (settings) => this.requireRegistry().testSettings(settings)
8947
+ });
8948
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8761
8949
  this.activeBrokers = await this.loadPersistedBrokers();
8762
8950
  this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
8763
8951
  this.registry.setOnBrokerConnected((brokerId) => {
@@ -8774,10 +8962,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8774
8962
  this.reconcileAllConnectedBrokers();
8775
8963
  }, HaProviderAddon.RESYNC_INTERVAL_MS);
8776
8964
  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
- }));
8965
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
8781
8966
  await this.reconcileIntegrationsToBrokers();
8782
8967
  this.subscribeIntegrationLifecycle();
8783
8968
  return [
@@ -8785,6 +8970,10 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8785
8970
  capability: brokerCapability,
8786
8971
  provider: this.buildBrokerProvider()
8787
8972
  },
8973
+ {
8974
+ capability: integrationInstancesCapability,
8975
+ provider: this.requireInstances().toProvider()
8976
+ },
8788
8977
  {
8789
8978
  capability: deviceAdoptionCapability,
8790
8979
  provider: this.buildAdoptionProvider()
@@ -9064,11 +9253,8 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9064
9253
  for (const entry of persisted) {
9065
9254
  if (entry.id && liveIds.has(entry.id)) {
9066
9255
  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);
9256
+ const settings = entryToSettings(entry);
9257
+ if (settings !== null) await reg.updateEntry(entry.id, settings);
9072
9258
  } catch (err) {
9073
9259
  this.ctx.logger.warn("ha onConfigChanged: updateEntry failed", { meta: {
9074
9260
  brokerId: entry.id,
@@ -9078,10 +9264,8 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9078
9264
  continue;
9079
9265
  }
9080
9266
  try {
9081
- const settings = entry.auth.kind === "supervisor" ? { supervisorToken: entry.auth.supervisorToken } : {
9082
- baseUrl: entry.auth.baseUrl,
9083
- accessToken: entry.auth.accessToken
9084
- };
9267
+ const settings = entryToSettings(entry);
9268
+ if (settings === null) continue;
9085
9269
  const created = await reg.createEntry(entry.name, settings);
9086
9270
  if (created.id !== entry.id) {
9087
9271
  const next = persisted.map((b) => b === entry ? {
@@ -9099,21 +9283,59 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9099
9283
  }
9100
9284
  }
9101
9285
  async loadPersistedBrokers() {
9102
- const store = this.brokerStore;
9103
- if (store !== null) {
9104
- const fromRecords = await store.listResolved();
9105
- if (fromRecords.length > 0) return fromRecords;
9286
+ return this.requireBrokerStore().listResolved();
9287
+ }
9288
+ requireInstances() {
9289
+ if (this.instances === null) throw new Error("provider-homeassistant: instances not ready");
9290
+ return this.instances;
9291
+ }
9292
+ /**
9293
+ * After an instance changed (created, edited, switched, deleted) the runtime follows the record:
9294
+ * a missing record removes the manager and its adopted devices, an off or unfilled one stops the
9295
+ * manager but keeps the devices, a filled one starts or re-authenticates it.
9296
+ */
9297
+ async syncBrokerRuntime(id) {
9298
+ const reg = this.requireRegistry();
9299
+ const entry = await this.requireBrokerStore().getById(id);
9300
+ this.activeBrokers = await this.loadPersistedBrokers();
9301
+ if (entry === null) {
9302
+ await reg.removeEntry(id);
9303
+ const deviceRegistry = this.ctx.kernel.deviceRegistry;
9304
+ const devices = this.ctx.kernel.devices;
9305
+ if (deviceRegistry && devices) await cascadeRemoveDevicesForBroker({
9306
+ reg: deviceRegistry,
9307
+ devices,
9308
+ addonId: this.ctx.id,
9309
+ brokerId: id,
9310
+ logger: this.ctx.logger
9311
+ });
9312
+ return;
9106
9313
  }
9107
- return this.config.brokers;
9314
+ const settings = entry.enabled === false ? null : entryToSettings(entry);
9315
+ if (settings === null) {
9316
+ await reg.removeEntry(id);
9317
+ return;
9318
+ }
9319
+ try {
9320
+ if (reg.get(id) !== null) await reg.updateEntry(id, settings);
9321
+ else await reg.createEntry(entry.name, settings, {
9322
+ id,
9323
+ ...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
9324
+ });
9325
+ } catch (err) {
9326
+ this.ctx.logger.warn("ha instance: could not start the broker", { meta: {
9327
+ brokerId: id,
9328
+ error: errMsg(err)
9329
+ } });
9330
+ }
9331
+ }
9332
+ requireBrokerStore() {
9333
+ if (this.brokerStore === null) throw new Error("provider-homeassistant: broker store not ready");
9334
+ return this.brokerStore;
9108
9335
  }
9109
9336
  async persistBrokerEntries(entries) {
9110
9337
  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 });
9338
+ await this.requireBrokerStore().replaceAll(entries);
9117
9339
  }
9118
9340
  buildBrokerProvider() {
9119
9341
  const ownerAddonId = this.ctx.id;
@@ -9187,7 +9409,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9187
9409
  getSettings: async ({ id }) => {
9188
9410
  const entry = this.activeBrokers.find((b) => b.id === id);
9189
9411
  if (!entry) return null;
9190
- return authToSettings(entry.auth);
9412
+ return entryToSettings(entry);
9191
9413
  },
9192
9414
  setSettings: async ({ id, settings }) => {
9193
9415
  if (!owns(id)) return;
@@ -9198,7 +9420,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
9198
9420
  getBrokerConfig: async ({ id }) => {
9199
9421
  const entry = this.activeBrokers.find((b) => b.id === id);
9200
9422
  if (!entry) return null;
9201
- return authToSettings(entry.auth);
9423
+ return entryToSettings(entry);
9202
9424
  },
9203
9425
  getSettingsSchema: async ({ kind }) => {
9204
9426
  if (kind !== HA_KIND) return null;