@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/CHANGELOG.md +14 -0
- package/dist/addon.js +313 -88
- package/dist/addon.mjs +310 -88
- package/dist/ha-export/ha-export.addon.js +528 -275
- package/dist/ha-export/ha-export.addon.mjs +515 -276
- package/dist/{ha-instance-settings-Bn_Trz6p.js → ha-instance-settings-Dm0tU8iv.js} +1045 -340
- package/dist/{ha-instance-settings-g5pwb3Fi.mjs → ha-instance-settings-EQ5Ey3Q-.mjs} +1000 -331
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.656] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Home Assistant export: each instance's export switch, image-link address and exported devices now live in one record per instance; a migration moves the old settings and integration values, and the export no longer reads them
|
|
15
|
+
|
|
16
|
+
## [1.2.655] - 2026-11-04
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Exposing or hiding a device in Home Assistant export changes only the Home Assistant instance it names, not every instance
|
|
21
|
+
- Home Assistant connections are listed on the instances page; their stored connection is flattened once at start-up (same ids and tokens).
|
|
22
|
+
- Connections move to the flat body without stranding keychain refs: the old token refs are released when the nested object goes
|
|
23
|
+
|
|
10
24
|
## [1.2.654] - 2026-11-03
|
|
11
25
|
|
|
12
26
|
### 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-
|
|
2
|
+
const require_ha_instance_settings = require("./ha-instance-settings-Dm0tU8iv.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
|
|
@@ -5765,22 +5765,16 @@ var TEST_HANDSHAKE_TIMEOUT_MS = 8e3;
|
|
|
5765
5765
|
var HaBrokerEntrySchema = require_ha_instance_settings.object({
|
|
5766
5766
|
id: require_ha_instance_settings.string(),
|
|
5767
5767
|
name: require_ha_instance_settings.string(),
|
|
5768
|
-
/**
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
* Populated when the wizard's "+ New Integration" flow created the
|
|
5779
|
-
* broker; null when the operator added it via the Settings panel
|
|
5780
|
-
* directly. Used by the integration listener to keep the broker
|
|
5781
|
-
* list in sync with the integrations table (cascade-delete on
|
|
5782
|
-
* integration removal). */
|
|
5783
|
-
integrationId: require_ha_instance_settings.string().optional()
|
|
5768
|
+
/** Supervisor (token from env) or long-lived (persisted); picks which fields below are used. */
|
|
5769
|
+
authKind: require_ha_instance_settings._enum(["long-lived", "supervisor"]),
|
|
5770
|
+
/** Origin URL of the HA instance; long-lived only. */
|
|
5771
|
+
baseUrl: require_ha_instance_settings.string().optional(),
|
|
5772
|
+
accessToken: require_ha_instance_settings.string().optional().meta({ secret: true }),
|
|
5773
|
+
supervisorToken: require_ha_instance_settings.string().optional().meta({ secret: true }),
|
|
5774
|
+
/** Optional FK to the integration record that spawned this broker. */
|
|
5775
|
+
integrationId: require_ha_instance_settings.string().optional(),
|
|
5776
|
+
/** The instance's own switch (D996); absent means on. */
|
|
5777
|
+
enabled: require_ha_instance_settings.boolean().optional()
|
|
5784
5778
|
});
|
|
5785
5779
|
/**
|
|
5786
5780
|
* Validates a `broker.add` `settings` payload for the HA kind. Accepts:
|
|
@@ -5805,6 +5799,34 @@ function settingsToAuth(settings) {
|
|
|
5805
5799
|
accessToken: parsed.accessToken
|
|
5806
5800
|
};
|
|
5807
5801
|
}
|
|
5802
|
+
/** The flat entry fields of an auth config: the one place the two shapes meet. */
|
|
5803
|
+
function authToEntryFields(auth) {
|
|
5804
|
+
return auth.kind === "supervisor" ? {
|
|
5805
|
+
authKind: "supervisor",
|
|
5806
|
+
supervisorToken: auth.supervisorToken
|
|
5807
|
+
} : {
|
|
5808
|
+
authKind: "long-lived",
|
|
5809
|
+
baseUrl: auth.baseUrl,
|
|
5810
|
+
accessToken: auth.accessToken
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
/** The auth config an entry stands for; null while the entry is not filled in. */
|
|
5814
|
+
function haAuthOf(entry) {
|
|
5815
|
+
const parsed = HaAuthConfigSchema.safeParse(entry.authKind === "supervisor" ? {
|
|
5816
|
+
kind: "supervisor",
|
|
5817
|
+
supervisorToken: entry.supervisorToken ?? ""
|
|
5818
|
+
} : {
|
|
5819
|
+
kind: "long-lived",
|
|
5820
|
+
baseUrl: entry.baseUrl ?? "",
|
|
5821
|
+
accessToken: entry.accessToken ?? ""
|
|
5822
|
+
});
|
|
5823
|
+
return parsed.success ? parsed.data : null;
|
|
5824
|
+
}
|
|
5825
|
+
/** The flat settings payload (`settingsToAuth` shape) of an entry; null while it is not filled in. */
|
|
5826
|
+
function entryToSettings(entry) {
|
|
5827
|
+
const auth = haAuthOf(entry);
|
|
5828
|
+
return auth === null ? null : authToSettings(auth);
|
|
5829
|
+
}
|
|
5808
5830
|
function authToSettings(auth) {
|
|
5809
5831
|
if (auth.kind === "supervisor") return { supervisorToken: auth.supervisorToken };
|
|
5810
5832
|
return {
|
|
@@ -5867,7 +5889,7 @@ var HaBrokerRegistry = class {
|
|
|
5867
5889
|
* started concurrently; transient errors don't block bootstrap. */
|
|
5868
5890
|
async restore(entries) {
|
|
5869
5891
|
this.seedCounterFromEntries(entries);
|
|
5870
|
-
await Promise.all(entries.map(async (entry) => {
|
|
5892
|
+
await Promise.all(entries.filter((entry) => entry.enabled !== false && haAuthOf(entry) !== null).map(async (entry) => {
|
|
5871
5893
|
try {
|
|
5872
5894
|
await this.startManagerForEntry(entry);
|
|
5873
5895
|
} catch (err) {
|
|
@@ -5928,7 +5950,7 @@ var HaBrokerRegistry = class {
|
|
|
5928
5950
|
const entry = {
|
|
5929
5951
|
id: options.id !== void 0 ? this.adoptId(options.id) : this.allocateId(),
|
|
5930
5952
|
name,
|
|
5931
|
-
auth,
|
|
5953
|
+
...authToEntryFields(auth),
|
|
5932
5954
|
...options.integrationId !== void 0 ? { integrationId: options.integrationId } : {}
|
|
5933
5955
|
};
|
|
5934
5956
|
await this.startManagerForEntry(entry);
|
|
@@ -5949,7 +5971,7 @@ var HaBrokerRegistry = class {
|
|
|
5949
5971
|
return {
|
|
5950
5972
|
id,
|
|
5951
5973
|
name: existing.getInfo().name,
|
|
5952
|
-
auth
|
|
5974
|
+
...authToEntryFields(auth)
|
|
5953
5975
|
};
|
|
5954
5976
|
}
|
|
5955
5977
|
/**
|
|
@@ -6132,10 +6154,12 @@ var HaBrokerRegistry = class {
|
|
|
6132
6154
|
this.logger.debug("ha-broker already active — reusing existing connection", { meta: { brokerId: entry.id } });
|
|
6133
6155
|
return;
|
|
6134
6156
|
}
|
|
6157
|
+
const auth = haAuthOf(entry);
|
|
6158
|
+
if (auth === null) throw new Error(`ha-broker '${entry.id}' has no complete credentials`);
|
|
6135
6159
|
const manager = new HaIntegrationManager({
|
|
6136
6160
|
id: entry.id,
|
|
6137
6161
|
name: entry.name,
|
|
6138
|
-
auth
|
|
6162
|
+
auth,
|
|
6139
6163
|
logger: this.logger,
|
|
6140
6164
|
eventBus: this.eventBus,
|
|
6141
6165
|
onConnected: (brokerId) => this.onBrokerConnected?.(brokerId),
|
|
@@ -7196,16 +7220,24 @@ function createHaIconRouteProvider() {
|
|
|
7196
7220
|
//#region src/broker-record-kinds.ts
|
|
7197
7221
|
/**
|
|
7198
7222
|
* Broker entry kind (B2): credentials live in records; integrations keep brokerId.
|
|
7223
|
+
*
|
|
7224
|
+
* The body is FLAT (planning#408 P3): the form the instances page edits is the stored shape,
|
|
7225
|
+
* with the two tokens at the top level where the records door keeps them in the keychain.
|
|
7199
7226
|
*/
|
|
7200
7227
|
var HA_BROKERS_KIND = "brokers";
|
|
7201
7228
|
var HaBrokerBodySchema = require_ha_instance_settings.entryBody({
|
|
7202
|
-
|
|
7203
|
-
|
|
7229
|
+
authKind: HaBrokerEntrySchema.shape.authKind,
|
|
7230
|
+
baseUrl: HaBrokerEntrySchema.shape.baseUrl,
|
|
7231
|
+
accessToken: HaBrokerEntrySchema.shape.accessToken,
|
|
7232
|
+
supervisorToken: HaBrokerEntrySchema.shape.supervisorToken,
|
|
7233
|
+
integrationId: HaBrokerEntrySchema.shape.integrationId,
|
|
7234
|
+
enabled: HaBrokerEntrySchema.shape.enabled
|
|
7204
7235
|
});
|
|
7205
7236
|
var haBrokersEntryKind = require_ha_instance_settings.defineEntryKind(HA_BROKERS_KIND, {
|
|
7206
7237
|
title: "Home Assistant brokers",
|
|
7207
7238
|
schema: HaBrokerBodySchema,
|
|
7208
|
-
writeTrigger: "an operator adds, edits or removes a Home Assistant broker connection"
|
|
7239
|
+
writeTrigger: "an operator adds, edits or removes a Home Assistant broker connection",
|
|
7240
|
+
retiredSecretPaths: ["auth.accessToken", "auth.supervisorToken"]
|
|
7209
7241
|
});
|
|
7210
7242
|
//#endregion
|
|
7211
7243
|
//#region src/record-kinds.ts
|
|
@@ -7737,8 +7769,12 @@ function rowToEntry(id, data) {
|
|
|
7737
7769
|
const parsed = HaBrokerEntrySchema.safeParse({
|
|
7738
7770
|
id,
|
|
7739
7771
|
name: data.name,
|
|
7740
|
-
|
|
7741
|
-
|
|
7772
|
+
authKind: data.authKind,
|
|
7773
|
+
baseUrl: data.baseUrl,
|
|
7774
|
+
accessToken: data.accessToken,
|
|
7775
|
+
supervisorToken: data.supervisorToken,
|
|
7776
|
+
integrationId: data.integrationId,
|
|
7777
|
+
enabled: data.enabled
|
|
7742
7778
|
});
|
|
7743
7779
|
return parsed.success ? parsed.data : null;
|
|
7744
7780
|
}
|
|
@@ -7777,10 +7813,9 @@ var HaBrokerStore = class HaBrokerStore {
|
|
|
7777
7813
|
}
|
|
7778
7814
|
}
|
|
7779
7815
|
async upsert(entry) {
|
|
7780
|
-
const { id,
|
|
7816
|
+
const { id, integrationId, ...fields } = entry;
|
|
7781
7817
|
const body = {
|
|
7782
|
-
|
|
7783
|
-
auth,
|
|
7818
|
+
...Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0)),
|
|
7784
7819
|
...integrationId === void 0 ? {} : { integrationId }
|
|
7785
7820
|
};
|
|
7786
7821
|
const parsed = haBrokersEntryKind.schema.safeParse(body);
|
|
@@ -7801,6 +7836,98 @@ var HaBrokerStore = class HaBrokerStore {
|
|
|
7801
7836
|
}
|
|
7802
7837
|
};
|
|
7803
7838
|
//#endregion
|
|
7839
|
+
//#region src/ha-instances.ts
|
|
7840
|
+
/**
|
|
7841
|
+
* The Home Assistant connections as `integration-instances` (planning#408 P3): one instance per
|
|
7842
|
+
* `brokers` record, over the SAME records and ids (`ha_NNN`) the `broker` cap serves. The body is
|
|
7843
|
+
* flat, so the form the instances page edits is the stored shape.
|
|
7844
|
+
*/
|
|
7845
|
+
var HA_INSTANCE_KIND = "home-assistant";
|
|
7846
|
+
function isHaBodyConfigured(body) {
|
|
7847
|
+
return body.authKind === "supervisor" ? (body.supervisorToken ?? "").length > 0 : (body.baseUrl ?? "").length > 0 && (body.accessToken ?? "").length > 0;
|
|
7848
|
+
}
|
|
7849
|
+
function statusOf(info) {
|
|
7850
|
+
if (info === null) return {
|
|
7851
|
+
state: "unknown",
|
|
7852
|
+
label: "Not connected"
|
|
7853
|
+
};
|
|
7854
|
+
if (info.status === "connected") return {
|
|
7855
|
+
state: "ok",
|
|
7856
|
+
label: "Connected"
|
|
7857
|
+
};
|
|
7858
|
+
if (info.status === "connecting") return {
|
|
7859
|
+
state: "unknown",
|
|
7860
|
+
label: "Connecting"
|
|
7861
|
+
};
|
|
7862
|
+
return {
|
|
7863
|
+
state: "error",
|
|
7864
|
+
label: info.status,
|
|
7865
|
+
...info.error !== null ? { detail: info.error } : {}
|
|
7866
|
+
};
|
|
7867
|
+
}
|
|
7868
|
+
function buildHaInstances(records, deps) {
|
|
7869
|
+
return new require_ha_instance_settings.RecordIntegrationInstances(records, haBrokersEntryKind, {
|
|
7870
|
+
description: {
|
|
7871
|
+
role: "both",
|
|
7872
|
+
instanceMode: "multiple",
|
|
7873
|
+
kind: "connection",
|
|
7874
|
+
canTest: true,
|
|
7875
|
+
kinds: [{
|
|
7876
|
+
id: HA_INSTANCE_KIND,
|
|
7877
|
+
label: "Home Assistant"
|
|
7878
|
+
}],
|
|
7879
|
+
allowRestoreId: true
|
|
7880
|
+
},
|
|
7881
|
+
formSchema: () => haBrokerSettingsSchema,
|
|
7882
|
+
initialBody: (name) => ({
|
|
7883
|
+
name,
|
|
7884
|
+
authKind: "long-lived"
|
|
7885
|
+
}),
|
|
7886
|
+
mintId: () => deps.mintId(),
|
|
7887
|
+
kindOf: () => HA_INSTANCE_KIND,
|
|
7888
|
+
health: (_body, id) => {
|
|
7889
|
+
const info = deps.infoOf(id);
|
|
7890
|
+
return {
|
|
7891
|
+
status: statusOf(info),
|
|
7892
|
+
...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
|
|
7893
|
+
};
|
|
7894
|
+
},
|
|
7895
|
+
configured: isHaBodyConfigured,
|
|
7896
|
+
onChanged: deps.onChanged,
|
|
7897
|
+
test: async ({ id, values }) => {
|
|
7898
|
+
if (["accessToken", "supervisorToken"].some((key) => {
|
|
7899
|
+
const v = values[key];
|
|
7900
|
+
return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
|
|
7901
|
+
})) {
|
|
7902
|
+
const r = await deps.probe(values);
|
|
7903
|
+
return r.ok ? {
|
|
7904
|
+
success: true,
|
|
7905
|
+
status: "validated",
|
|
7906
|
+
...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
|
|
7907
|
+
} : {
|
|
7908
|
+
success: false,
|
|
7909
|
+
status: "rejected",
|
|
7910
|
+
error: r.error ?? "Home Assistant rejected the connection"
|
|
7911
|
+
};
|
|
7912
|
+
}
|
|
7913
|
+
const info = id === null ? null : deps.infoOf(id);
|
|
7914
|
+
if (info === null) return {
|
|
7915
|
+
success: false,
|
|
7916
|
+
status: "inconclusive",
|
|
7917
|
+
error: "Not connected: save the connection first"
|
|
7918
|
+
};
|
|
7919
|
+
return info.status === "connected" ? {
|
|
7920
|
+
success: true,
|
|
7921
|
+
status: "validated"
|
|
7922
|
+
} : {
|
|
7923
|
+
success: false,
|
|
7924
|
+
status: "rejected",
|
|
7925
|
+
error: info.error ?? `status: ${info.status}`
|
|
7926
|
+
};
|
|
7927
|
+
}
|
|
7928
|
+
});
|
|
7929
|
+
}
|
|
7930
|
+
//#endregion
|
|
7804
7931
|
//#region src/ha-board-key-source.ts
|
|
7805
7932
|
/**
|
|
7806
7933
|
* `board-key-source` for Home Assistant: reads an ESPHome board's API
|
|
@@ -7974,6 +8101,38 @@ function createHaBoardKeyProvider(source) {
|
|
|
7974
8101
|
};
|
|
7975
8102
|
}
|
|
7976
8103
|
//#endregion
|
|
8104
|
+
//#region src/migrations/flatten-broker-records.ts
|
|
8105
|
+
/**
|
|
8106
|
+
* The Home Assistant broker records keep their connection flat (planning#408 P3): the nested
|
|
8107
|
+
* `auth` object (`{ kind, baseUrl, accessToken }` or `{ kind, supervisorToken }`) becomes the top
|
|
8108
|
+
* level `authKind`, `baseUrl`, `accessToken` and `supervisorToken`, under the SAME record id, so
|
|
8109
|
+
* the devices that carry `ha_NNN` keep their broker. The work is `flattenNestedRecords`: every
|
|
8110
|
+
* secret is read before anything is written, read back from its new keychain ref before the
|
|
8111
|
+
* nested object goes, and the old refs are deleted last.
|
|
8112
|
+
*/
|
|
8113
|
+
var FLATTEN_BROKER_RECORDS_ID = "provider-homeassistant/2026-11-04-flatten-broker-records";
|
|
8114
|
+
function text(value) {
|
|
8115
|
+
return typeof value === "string" ? value : void 0;
|
|
8116
|
+
}
|
|
8117
|
+
async function flattenBrokerRecords(ports) {
|
|
8118
|
+
return require_ha_instance_settings.flattenNestedRecords(ports, {
|
|
8119
|
+
legacyKey: "auth",
|
|
8120
|
+
flatSecretKeys: ["accessToken", "supervisorToken"],
|
|
8121
|
+
toFlat: (legacy) => {
|
|
8122
|
+
const kind = legacy["kind"] === "supervisor" ? "supervisor" : "long-lived";
|
|
8123
|
+
const baseUrl = text(legacy["baseUrl"]);
|
|
8124
|
+
const accessToken = text(legacy["accessToken"]);
|
|
8125
|
+
const supervisorToken = text(legacy["supervisorToken"]);
|
|
8126
|
+
return {
|
|
8127
|
+
authKind: kind,
|
|
8128
|
+
...baseUrl !== void 0 ? { baseUrl } : {},
|
|
8129
|
+
...accessToken !== void 0 ? { accessToken } : {},
|
|
8130
|
+
...supervisorToken !== void 0 ? { supervisorToken } : {}
|
|
8131
|
+
};
|
|
8132
|
+
}
|
|
8133
|
+
});
|
|
8134
|
+
}
|
|
8135
|
+
//#endregion
|
|
7977
8136
|
//#region src/migrations/calendar-ids-to-integration.ts
|
|
7978
8137
|
/**
|
|
7979
8138
|
* The HA calendars an instance imports are a fact of THAT instance (D985): each Home Assistant
|
|
@@ -8063,37 +8222,56 @@ async function planDropSharedCalendarIds(ports) {
|
|
|
8063
8222
|
//#endregion
|
|
8064
8223
|
//#region src/migrations/index.ts
|
|
8065
8224
|
/** The `provider-homeassistant` catalogue (D987): offered to the engine when the addon initialises. */
|
|
8066
|
-
var ADDON_MIGRATIONS = [
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8225
|
+
var ADDON_MIGRATIONS = [
|
|
8226
|
+
{
|
|
8227
|
+
meta: {
|
|
8228
|
+
id: "provider-homeassistant/2026-11-03-calendar-ids-to-integration",
|
|
8229
|
+
owner: "provider-homeassistant",
|
|
8230
|
+
level: "addon-state",
|
|
8231
|
+
seq: 1,
|
|
8232
|
+
dependsOn: [],
|
|
8233
|
+
store: "settings",
|
|
8234
|
+
scope: "cluster",
|
|
8235
|
+
onFailure: "tolerate",
|
|
8236
|
+
bodyKind: "procedure",
|
|
8237
|
+
description: "Copy the shared calendar set to the settings of every Home Assistant integration that has none",
|
|
8238
|
+
fixtureMark: "the broker link stored beside the calendars of an integration"
|
|
8239
|
+
},
|
|
8240
|
+
procedure: copyCalendarIdsToIntegrations
|
|
8079
8241
|
},
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8242
|
+
{
|
|
8243
|
+
meta: {
|
|
8244
|
+
id: "provider-homeassistant/2026-11-03-drop-shared-calendar-ids",
|
|
8245
|
+
owner: "provider-homeassistant",
|
|
8246
|
+
level: "addon-state",
|
|
8247
|
+
seq: 2,
|
|
8248
|
+
dependsOn: ["provider-homeassistant/2026-11-03-calendar-ids-to-integration"],
|
|
8249
|
+
store: "settings",
|
|
8250
|
+
scope: "cluster",
|
|
8251
|
+
onFailure: "tolerate",
|
|
8252
|
+
bodyKind: "plan",
|
|
8253
|
+
description: "Delete the shared calendar set once every integration shows its own",
|
|
8254
|
+
fixtureMark: "the shared set parked when an integration holds a different one"
|
|
8255
|
+
},
|
|
8256
|
+
plan: planDropSharedCalendarIds
|
|
8094
8257
|
},
|
|
8095
|
-
|
|
8096
|
-
|
|
8258
|
+
{
|
|
8259
|
+
meta: {
|
|
8260
|
+
id: FLATTEN_BROKER_RECORDS_ID,
|
|
8261
|
+
owner: "provider-homeassistant",
|
|
8262
|
+
level: "addon-state",
|
|
8263
|
+
seq: 3,
|
|
8264
|
+
dependsOn: [],
|
|
8265
|
+
store: "settings",
|
|
8266
|
+
scope: "cluster",
|
|
8267
|
+
onFailure: "block-owner",
|
|
8268
|
+
bodyKind: "procedure",
|
|
8269
|
+
description: "Flatten the nested auth of every Home Assistant broker record under its own id, moving each token to a top-level keychain secret",
|
|
8270
|
+
fixtureMark: "a non-secret field stored beside the auth of a broker record"
|
|
8271
|
+
},
|
|
8272
|
+
procedure: flattenBrokerRecords
|
|
8273
|
+
}
|
|
8274
|
+
];
|
|
8097
8275
|
//#endregion
|
|
8098
8276
|
//#region src/ha-calendar-source.ts
|
|
8099
8277
|
function isCalendarEntity(entityId) {
|
|
@@ -8707,10 +8885,7 @@ function planStandaloneReconcile(devices, freshById, brokerId) {
|
|
|
8707
8885
|
var ADOPTION_FILTER_DEVICES = "devices";
|
|
8708
8886
|
var ADOPTION_FILTER_ENTITIES = "entities";
|
|
8709
8887
|
var HA_KIND = "home-assistant";
|
|
8710
|
-
var DEFAULT_CONFIG = {
|
|
8711
|
-
brokers: [],
|
|
8712
|
-
calendarIds: []
|
|
8713
|
-
};
|
|
8888
|
+
var DEFAULT_CONFIG = { calendarIds: [] };
|
|
8714
8889
|
/**
|
|
8715
8890
|
* Home Assistant provider addon.
|
|
8716
8891
|
*
|
|
@@ -8757,6 +8932,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8757
8932
|
}
|
|
8758
8933
|
registry = null;
|
|
8759
8934
|
brokerStore = null;
|
|
8935
|
+
instances = null;
|
|
8760
8936
|
/** Runtime broker rows (records when B2 is live, else settings). */
|
|
8761
8937
|
activeBrokers = [];
|
|
8762
8938
|
/** Periodic re-sync sweep handle (cleared on shutdown). */
|
|
@@ -8771,11 +8947,26 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8771
8947
|
constructor() {
|
|
8772
8948
|
super({ ...DEFAULT_CONFIG });
|
|
8773
8949
|
}
|
|
8950
|
+
flattenPorts(records) {
|
|
8951
|
+
if (this.ctx.secrets === void 0) throw new Error("provider-homeassistant: ctx.secrets is required");
|
|
8952
|
+
return {
|
|
8953
|
+
readStored: (key) => this.readStoredGlobalKey(key),
|
|
8954
|
+
...require_ha_instance_settings.haInstanceSettingsPortsOver(this.ctx.api.integrations),
|
|
8955
|
+
...require_ha_instance_settings.flattenPortsOver(records.kind(haBrokersEntryKind))
|
|
8956
|
+
};
|
|
8957
|
+
}
|
|
8774
8958
|
async onInitialize() {
|
|
8775
8959
|
const records = this.ctx.records;
|
|
8776
8960
|
if (records === void 0) throw new Error("provider-homeassistant: ctx.records is required (hub-only addon)");
|
|
8777
8961
|
await records.declare(haAllRecordKinds);
|
|
8778
8962
|
this.brokerStore = HaBrokerStore.of(records);
|
|
8963
|
+
this.instances = buildHaInstances(records, {
|
|
8964
|
+
mintId: () => this.requireRegistry().allocateId(),
|
|
8965
|
+
infoOf: (id) => this.registry?.get(id) ?? null,
|
|
8966
|
+
onChanged: (id) => this.syncBrokerRuntime(id),
|
|
8967
|
+
probe: (settings) => this.requireRegistry().testSettings(settings)
|
|
8968
|
+
});
|
|
8969
|
+
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
|
|
8779
8970
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
8780
8971
|
this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
|
|
8781
8972
|
this.registry.setOnBrokerConnected((brokerId) => {
|
|
@@ -8792,10 +8983,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8792
8983
|
this.reconcileAllConnectedBrokers();
|
|
8793
8984
|
}, HaProviderAddon.RESYNC_INTERVAL_MS);
|
|
8794
8985
|
this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
|
|
8795
|
-
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS,
|
|
8796
|
-
readStored: (key) => this.readStoredGlobalKey(key),
|
|
8797
|
-
...require_ha_instance_settings.haInstanceSettingsPortsOver(this.ctx.api.integrations)
|
|
8798
|
-
}));
|
|
8986
|
+
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
|
|
8799
8987
|
await this.reconcileIntegrationsToBrokers();
|
|
8800
8988
|
this.subscribeIntegrationLifecycle();
|
|
8801
8989
|
return [
|
|
@@ -8803,6 +8991,10 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8803
8991
|
capability: require_ha_instance_settings.brokerCapability,
|
|
8804
8992
|
provider: this.buildBrokerProvider()
|
|
8805
8993
|
},
|
|
8994
|
+
{
|
|
8995
|
+
capability: require_ha_instance_settings.integrationInstancesCapability,
|
|
8996
|
+
provider: this.requireInstances().toProvider()
|
|
8997
|
+
},
|
|
8806
8998
|
{
|
|
8807
8999
|
capability: require_ha_instance_settings.deviceAdoptionCapability,
|
|
8808
9000
|
provider: this.buildAdoptionProvider()
|
|
@@ -9082,11 +9274,8 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9082
9274
|
for (const entry of persisted) {
|
|
9083
9275
|
if (entry.id && liveIds.has(entry.id)) {
|
|
9084
9276
|
try {
|
|
9085
|
-
const settings = entry
|
|
9086
|
-
|
|
9087
|
-
accessToken: entry.auth.accessToken
|
|
9088
|
-
};
|
|
9089
|
-
await reg.updateEntry(entry.id, settings);
|
|
9277
|
+
const settings = entryToSettings(entry);
|
|
9278
|
+
if (settings !== null) await reg.updateEntry(entry.id, settings);
|
|
9090
9279
|
} catch (err) {
|
|
9091
9280
|
this.ctx.logger.warn("ha onConfigChanged: updateEntry failed", { meta: {
|
|
9092
9281
|
brokerId: entry.id,
|
|
@@ -9096,10 +9285,8 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9096
9285
|
continue;
|
|
9097
9286
|
}
|
|
9098
9287
|
try {
|
|
9099
|
-
const settings = entry
|
|
9100
|
-
|
|
9101
|
-
accessToken: entry.auth.accessToken
|
|
9102
|
-
};
|
|
9288
|
+
const settings = entryToSettings(entry);
|
|
9289
|
+
if (settings === null) continue;
|
|
9103
9290
|
const created = await reg.createEntry(entry.name, settings);
|
|
9104
9291
|
if (created.id !== entry.id) {
|
|
9105
9292
|
const next = persisted.map((b) => b === entry ? {
|
|
@@ -9117,21 +9304,59 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9117
9304
|
}
|
|
9118
9305
|
}
|
|
9119
9306
|
async loadPersistedBrokers() {
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9307
|
+
return this.requireBrokerStore().listResolved();
|
|
9308
|
+
}
|
|
9309
|
+
requireInstances() {
|
|
9310
|
+
if (this.instances === null) throw new Error("provider-homeassistant: instances not ready");
|
|
9311
|
+
return this.instances;
|
|
9312
|
+
}
|
|
9313
|
+
/**
|
|
9314
|
+
* After an instance changed (created, edited, switched, deleted) the runtime follows the record:
|
|
9315
|
+
* a missing record removes the manager and its adopted devices, an off or unfilled one stops the
|
|
9316
|
+
* manager but keeps the devices, a filled one starts or re-authenticates it.
|
|
9317
|
+
*/
|
|
9318
|
+
async syncBrokerRuntime(id) {
|
|
9319
|
+
const reg = this.requireRegistry();
|
|
9320
|
+
const entry = await this.requireBrokerStore().getById(id);
|
|
9321
|
+
this.activeBrokers = await this.loadPersistedBrokers();
|
|
9322
|
+
if (entry === null) {
|
|
9323
|
+
await reg.removeEntry(id);
|
|
9324
|
+
const deviceRegistry = this.ctx.kernel.deviceRegistry;
|
|
9325
|
+
const devices = this.ctx.kernel.devices;
|
|
9326
|
+
if (deviceRegistry && devices) await cascadeRemoveDevicesForBroker({
|
|
9327
|
+
reg: deviceRegistry,
|
|
9328
|
+
devices,
|
|
9329
|
+
addonId: this.ctx.id,
|
|
9330
|
+
brokerId: id,
|
|
9331
|
+
logger: this.ctx.logger
|
|
9332
|
+
});
|
|
9333
|
+
return;
|
|
9124
9334
|
}
|
|
9125
|
-
|
|
9335
|
+
const settings = entry.enabled === false ? null : entryToSettings(entry);
|
|
9336
|
+
if (settings === null) {
|
|
9337
|
+
await reg.removeEntry(id);
|
|
9338
|
+
return;
|
|
9339
|
+
}
|
|
9340
|
+
try {
|
|
9341
|
+
if (reg.get(id) !== null) await reg.updateEntry(id, settings);
|
|
9342
|
+
else await reg.createEntry(entry.name, settings, {
|
|
9343
|
+
id,
|
|
9344
|
+
...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
|
|
9345
|
+
});
|
|
9346
|
+
} catch (err) {
|
|
9347
|
+
this.ctx.logger.warn("ha instance: could not start the broker", { meta: {
|
|
9348
|
+
brokerId: id,
|
|
9349
|
+
error: require_ha_instance_settings.errMsg(err)
|
|
9350
|
+
} });
|
|
9351
|
+
}
|
|
9352
|
+
}
|
|
9353
|
+
requireBrokerStore() {
|
|
9354
|
+
if (this.brokerStore === null) throw new Error("provider-homeassistant: broker store not ready");
|
|
9355
|
+
return this.brokerStore;
|
|
9126
9356
|
}
|
|
9127
9357
|
async persistBrokerEntries(entries) {
|
|
9128
9358
|
this.activeBrokers = [...entries];
|
|
9129
|
-
|
|
9130
|
-
if (store !== null) {
|
|
9131
|
-
await store.replaceAll(entries);
|
|
9132
|
-
return;
|
|
9133
|
-
}
|
|
9134
|
-
await this.updateGlobalSettings({ brokers: entries });
|
|
9359
|
+
await this.requireBrokerStore().replaceAll(entries);
|
|
9135
9360
|
}
|
|
9136
9361
|
buildBrokerProvider() {
|
|
9137
9362
|
const ownerAddonId = this.ctx.id;
|
|
@@ -9205,7 +9430,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9205
9430
|
getSettings: async ({ id }) => {
|
|
9206
9431
|
const entry = this.activeBrokers.find((b) => b.id === id);
|
|
9207
9432
|
if (!entry) return null;
|
|
9208
|
-
return
|
|
9433
|
+
return entryToSettings(entry);
|
|
9209
9434
|
},
|
|
9210
9435
|
setSettings: async ({ id, settings }) => {
|
|
9211
9436
|
if (!owns(id)) return;
|
|
@@ -9216,7 +9441,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9216
9441
|
getBrokerConfig: async ({ id }) => {
|
|
9217
9442
|
const entry = this.activeBrokers.find((b) => b.id === id);
|
|
9218
9443
|
if (!entry) return null;
|
|
9219
|
-
return
|
|
9444
|
+
return entryToSettings(entry);
|
|
9220
9445
|
},
|
|
9221
9446
|
getSettingsSchema: async ({ kind }) => {
|
|
9222
9447
|
if (kind !== HA_KIND) return null;
|