@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/CHANGELOG.md +8 -0
- package/dist/addon.js +312 -88
- package/dist/addon.mjs +309 -88
- package/dist/ha-export/ha-export.addon.js +71 -31
- package/dist/ha-export/ha-export.addon.mjs +71 -31
- package/dist/{ha-instance-settings-Bn_Trz6p.js → ha-instance-settings-5Ms7yRju.js} +940 -309
- package/dist/{ha-instance-settings-g5pwb3Fi.mjs → ha-instance-settings-DLDNOmgL.mjs} +899 -310
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.655] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Exposing or hiding a device in Home Assistant export changes only the Home Assistant instance it names, not every instance
|
|
15
|
+
- Home Assistant connections are listed on the instances page; their stored connection is flattened once at start-up (same ids and tokens).
|
|
16
|
+
- Connections move to the flat body without stranding keychain refs: the old token refs are released when the nested object goes
|
|
17
|
+
|
|
10
18
|
## [1.2.654] - 2026-11-03
|
|
11
19
|
|
|
12
20
|
### 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-5Ms7yRju.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,97 @@ 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
|
+
instanceMode: "multiple",
|
|
7872
|
+
kind: "connection",
|
|
7873
|
+
canTest: true,
|
|
7874
|
+
kinds: [{
|
|
7875
|
+
id: HA_INSTANCE_KIND,
|
|
7876
|
+
label: "Home Assistant"
|
|
7877
|
+
}],
|
|
7878
|
+
allowRestoreId: true
|
|
7879
|
+
},
|
|
7880
|
+
formSchema: () => haBrokerSettingsSchema,
|
|
7881
|
+
initialBody: (name) => ({
|
|
7882
|
+
name,
|
|
7883
|
+
authKind: "long-lived"
|
|
7884
|
+
}),
|
|
7885
|
+
mintId: () => deps.mintId(),
|
|
7886
|
+
kindOf: () => HA_INSTANCE_KIND,
|
|
7887
|
+
health: (_body, id) => {
|
|
7888
|
+
const info = deps.infoOf(id);
|
|
7889
|
+
return {
|
|
7890
|
+
status: statusOf(info),
|
|
7891
|
+
...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
|
|
7892
|
+
};
|
|
7893
|
+
},
|
|
7894
|
+
configured: isHaBodyConfigured,
|
|
7895
|
+
onChanged: deps.onChanged,
|
|
7896
|
+
test: async ({ id, values }) => {
|
|
7897
|
+
if (["accessToken", "supervisorToken"].some((key) => {
|
|
7898
|
+
const v = values[key];
|
|
7899
|
+
return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
|
|
7900
|
+
})) {
|
|
7901
|
+
const r = await deps.probe(values);
|
|
7902
|
+
return r.ok ? {
|
|
7903
|
+
success: true,
|
|
7904
|
+
status: "validated",
|
|
7905
|
+
...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
|
|
7906
|
+
} : {
|
|
7907
|
+
success: false,
|
|
7908
|
+
status: "rejected",
|
|
7909
|
+
error: r.error ?? "Home Assistant rejected the connection"
|
|
7910
|
+
};
|
|
7911
|
+
}
|
|
7912
|
+
const info = id === null ? null : deps.infoOf(id);
|
|
7913
|
+
if (info === null) return {
|
|
7914
|
+
success: false,
|
|
7915
|
+
status: "inconclusive",
|
|
7916
|
+
error: "Not connected: save the connection first"
|
|
7917
|
+
};
|
|
7918
|
+
return info.status === "connected" ? {
|
|
7919
|
+
success: true,
|
|
7920
|
+
status: "validated"
|
|
7921
|
+
} : {
|
|
7922
|
+
success: false,
|
|
7923
|
+
status: "rejected",
|
|
7924
|
+
error: info.error ?? `status: ${info.status}`
|
|
7925
|
+
};
|
|
7926
|
+
}
|
|
7927
|
+
});
|
|
7928
|
+
}
|
|
7929
|
+
//#endregion
|
|
7804
7930
|
//#region src/ha-board-key-source.ts
|
|
7805
7931
|
/**
|
|
7806
7932
|
* `board-key-source` for Home Assistant: reads an ESPHome board's API
|
|
@@ -7974,6 +8100,38 @@ function createHaBoardKeyProvider(source) {
|
|
|
7974
8100
|
};
|
|
7975
8101
|
}
|
|
7976
8102
|
//#endregion
|
|
8103
|
+
//#region src/migrations/flatten-broker-records.ts
|
|
8104
|
+
/**
|
|
8105
|
+
* The Home Assistant broker records keep their connection flat (planning#408 P3): the nested
|
|
8106
|
+
* `auth` object (`{ kind, baseUrl, accessToken }` or `{ kind, supervisorToken }`) becomes the top
|
|
8107
|
+
* level `authKind`, `baseUrl`, `accessToken` and `supervisorToken`, under the SAME record id, so
|
|
8108
|
+
* the devices that carry `ha_NNN` keep their broker. The work is `flattenNestedRecords`: every
|
|
8109
|
+
* secret is read before anything is written, read back from its new keychain ref before the
|
|
8110
|
+
* nested object goes, and the old refs are deleted last.
|
|
8111
|
+
*/
|
|
8112
|
+
var FLATTEN_BROKER_RECORDS_ID = "provider-homeassistant/2026-11-04-flatten-broker-records";
|
|
8113
|
+
function text(value) {
|
|
8114
|
+
return typeof value === "string" ? value : void 0;
|
|
8115
|
+
}
|
|
8116
|
+
async function flattenBrokerRecords(ports) {
|
|
8117
|
+
return require_ha_instance_settings.flattenNestedRecords(ports, {
|
|
8118
|
+
legacyKey: "auth",
|
|
8119
|
+
flatSecretKeys: ["accessToken", "supervisorToken"],
|
|
8120
|
+
toFlat: (legacy) => {
|
|
8121
|
+
const kind = legacy["kind"] === "supervisor" ? "supervisor" : "long-lived";
|
|
8122
|
+
const baseUrl = text(legacy["baseUrl"]);
|
|
8123
|
+
const accessToken = text(legacy["accessToken"]);
|
|
8124
|
+
const supervisorToken = text(legacy["supervisorToken"]);
|
|
8125
|
+
return {
|
|
8126
|
+
authKind: kind,
|
|
8127
|
+
...baseUrl !== void 0 ? { baseUrl } : {},
|
|
8128
|
+
...accessToken !== void 0 ? { accessToken } : {},
|
|
8129
|
+
...supervisorToken !== void 0 ? { supervisorToken } : {}
|
|
8130
|
+
};
|
|
8131
|
+
}
|
|
8132
|
+
});
|
|
8133
|
+
}
|
|
8134
|
+
//#endregion
|
|
7977
8135
|
//#region src/migrations/calendar-ids-to-integration.ts
|
|
7978
8136
|
/**
|
|
7979
8137
|
* The HA calendars an instance imports are a fact of THAT instance (D985): each Home Assistant
|
|
@@ -8063,37 +8221,56 @@ async function planDropSharedCalendarIds(ports) {
|
|
|
8063
8221
|
//#endregion
|
|
8064
8222
|
//#region src/migrations/index.ts
|
|
8065
8223
|
/** 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
|
-
|
|
8224
|
+
var ADDON_MIGRATIONS = [
|
|
8225
|
+
{
|
|
8226
|
+
meta: {
|
|
8227
|
+
id: "provider-homeassistant/2026-11-03-calendar-ids-to-integration",
|
|
8228
|
+
owner: "provider-homeassistant",
|
|
8229
|
+
level: "addon-state",
|
|
8230
|
+
seq: 1,
|
|
8231
|
+
dependsOn: [],
|
|
8232
|
+
store: "settings",
|
|
8233
|
+
scope: "cluster",
|
|
8234
|
+
onFailure: "tolerate",
|
|
8235
|
+
bodyKind: "procedure",
|
|
8236
|
+
description: "Copy the shared calendar set to the settings of every Home Assistant integration that has none",
|
|
8237
|
+
fixtureMark: "the broker link stored beside the calendars of an integration"
|
|
8238
|
+
},
|
|
8239
|
+
procedure: copyCalendarIdsToIntegrations
|
|
8079
8240
|
},
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8241
|
+
{
|
|
8242
|
+
meta: {
|
|
8243
|
+
id: "provider-homeassistant/2026-11-03-drop-shared-calendar-ids",
|
|
8244
|
+
owner: "provider-homeassistant",
|
|
8245
|
+
level: "addon-state",
|
|
8246
|
+
seq: 2,
|
|
8247
|
+
dependsOn: ["provider-homeassistant/2026-11-03-calendar-ids-to-integration"],
|
|
8248
|
+
store: "settings",
|
|
8249
|
+
scope: "cluster",
|
|
8250
|
+
onFailure: "tolerate",
|
|
8251
|
+
bodyKind: "plan",
|
|
8252
|
+
description: "Delete the shared calendar set once every integration shows its own",
|
|
8253
|
+
fixtureMark: "the shared set parked when an integration holds a different one"
|
|
8254
|
+
},
|
|
8255
|
+
plan: planDropSharedCalendarIds
|
|
8094
8256
|
},
|
|
8095
|
-
|
|
8096
|
-
|
|
8257
|
+
{
|
|
8258
|
+
meta: {
|
|
8259
|
+
id: FLATTEN_BROKER_RECORDS_ID,
|
|
8260
|
+
owner: "provider-homeassistant",
|
|
8261
|
+
level: "addon-state",
|
|
8262
|
+
seq: 3,
|
|
8263
|
+
dependsOn: [],
|
|
8264
|
+
store: "settings",
|
|
8265
|
+
scope: "cluster",
|
|
8266
|
+
onFailure: "block-owner",
|
|
8267
|
+
bodyKind: "procedure",
|
|
8268
|
+
description: "Flatten the nested auth of every Home Assistant broker record under its own id, moving each token to a top-level keychain secret",
|
|
8269
|
+
fixtureMark: "a non-secret field stored beside the auth of a broker record"
|
|
8270
|
+
},
|
|
8271
|
+
procedure: flattenBrokerRecords
|
|
8272
|
+
}
|
|
8273
|
+
];
|
|
8097
8274
|
//#endregion
|
|
8098
8275
|
//#region src/ha-calendar-source.ts
|
|
8099
8276
|
function isCalendarEntity(entityId) {
|
|
@@ -8707,10 +8884,7 @@ function planStandaloneReconcile(devices, freshById, brokerId) {
|
|
|
8707
8884
|
var ADOPTION_FILTER_DEVICES = "devices";
|
|
8708
8885
|
var ADOPTION_FILTER_ENTITIES = "entities";
|
|
8709
8886
|
var HA_KIND = "home-assistant";
|
|
8710
|
-
var DEFAULT_CONFIG = {
|
|
8711
|
-
brokers: [],
|
|
8712
|
-
calendarIds: []
|
|
8713
|
-
};
|
|
8887
|
+
var DEFAULT_CONFIG = { calendarIds: [] };
|
|
8714
8888
|
/**
|
|
8715
8889
|
* Home Assistant provider addon.
|
|
8716
8890
|
*
|
|
@@ -8757,6 +8931,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8757
8931
|
}
|
|
8758
8932
|
registry = null;
|
|
8759
8933
|
brokerStore = null;
|
|
8934
|
+
instances = null;
|
|
8760
8935
|
/** Runtime broker rows (records when B2 is live, else settings). */
|
|
8761
8936
|
activeBrokers = [];
|
|
8762
8937
|
/** Periodic re-sync sweep handle (cleared on shutdown). */
|
|
@@ -8771,11 +8946,26 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8771
8946
|
constructor() {
|
|
8772
8947
|
super({ ...DEFAULT_CONFIG });
|
|
8773
8948
|
}
|
|
8949
|
+
flattenPorts(records) {
|
|
8950
|
+
if (this.ctx.secrets === void 0) throw new Error("provider-homeassistant: ctx.secrets is required");
|
|
8951
|
+
return {
|
|
8952
|
+
readStored: (key) => this.readStoredGlobalKey(key),
|
|
8953
|
+
...require_ha_instance_settings.haInstanceSettingsPortsOver(this.ctx.api.integrations),
|
|
8954
|
+
...require_ha_instance_settings.flattenPortsOver(records.kind(haBrokersEntryKind))
|
|
8955
|
+
};
|
|
8956
|
+
}
|
|
8774
8957
|
async onInitialize() {
|
|
8775
8958
|
const records = this.ctx.records;
|
|
8776
8959
|
if (records === void 0) throw new Error("provider-homeassistant: ctx.records is required (hub-only addon)");
|
|
8777
8960
|
await records.declare(haAllRecordKinds);
|
|
8778
8961
|
this.brokerStore = HaBrokerStore.of(records);
|
|
8962
|
+
this.instances = buildHaInstances(records, {
|
|
8963
|
+
mintId: () => this.requireRegistry().allocateId(),
|
|
8964
|
+
infoOf: (id) => this.registry?.get(id) ?? null,
|
|
8965
|
+
onChanged: (id) => this.syncBrokerRuntime(id),
|
|
8966
|
+
probe: (settings) => this.requireRegistry().testSettings(settings)
|
|
8967
|
+
});
|
|
8968
|
+
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
|
|
8779
8969
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
8780
8970
|
this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
|
|
8781
8971
|
this.registry.setOnBrokerConnected((brokerId) => {
|
|
@@ -8792,10 +8982,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8792
8982
|
this.reconcileAllConnectedBrokers();
|
|
8793
8983
|
}, HaProviderAddon.RESYNC_INTERVAL_MS);
|
|
8794
8984
|
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
|
-
}));
|
|
8985
|
+
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
|
|
8799
8986
|
await this.reconcileIntegrationsToBrokers();
|
|
8800
8987
|
this.subscribeIntegrationLifecycle();
|
|
8801
8988
|
return [
|
|
@@ -8803,6 +8990,10 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8803
8990
|
capability: require_ha_instance_settings.brokerCapability,
|
|
8804
8991
|
provider: this.buildBrokerProvider()
|
|
8805
8992
|
},
|
|
8993
|
+
{
|
|
8994
|
+
capability: require_ha_instance_settings.integrationInstancesCapability,
|
|
8995
|
+
provider: this.requireInstances().toProvider()
|
|
8996
|
+
},
|
|
8806
8997
|
{
|
|
8807
8998
|
capability: require_ha_instance_settings.deviceAdoptionCapability,
|
|
8808
8999
|
provider: this.buildAdoptionProvider()
|
|
@@ -9082,11 +9273,8 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9082
9273
|
for (const entry of persisted) {
|
|
9083
9274
|
if (entry.id && liveIds.has(entry.id)) {
|
|
9084
9275
|
try {
|
|
9085
|
-
const settings = entry
|
|
9086
|
-
|
|
9087
|
-
accessToken: entry.auth.accessToken
|
|
9088
|
-
};
|
|
9089
|
-
await reg.updateEntry(entry.id, settings);
|
|
9276
|
+
const settings = entryToSettings(entry);
|
|
9277
|
+
if (settings !== null) await reg.updateEntry(entry.id, settings);
|
|
9090
9278
|
} catch (err) {
|
|
9091
9279
|
this.ctx.logger.warn("ha onConfigChanged: updateEntry failed", { meta: {
|
|
9092
9280
|
brokerId: entry.id,
|
|
@@ -9096,10 +9284,8 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9096
9284
|
continue;
|
|
9097
9285
|
}
|
|
9098
9286
|
try {
|
|
9099
|
-
const settings = entry
|
|
9100
|
-
|
|
9101
|
-
accessToken: entry.auth.accessToken
|
|
9102
|
-
};
|
|
9287
|
+
const settings = entryToSettings(entry);
|
|
9288
|
+
if (settings === null) continue;
|
|
9103
9289
|
const created = await reg.createEntry(entry.name, settings);
|
|
9104
9290
|
if (created.id !== entry.id) {
|
|
9105
9291
|
const next = persisted.map((b) => b === entry ? {
|
|
@@ -9117,21 +9303,59 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9117
9303
|
}
|
|
9118
9304
|
}
|
|
9119
9305
|
async loadPersistedBrokers() {
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9306
|
+
return this.requireBrokerStore().listResolved();
|
|
9307
|
+
}
|
|
9308
|
+
requireInstances() {
|
|
9309
|
+
if (this.instances === null) throw new Error("provider-homeassistant: instances not ready");
|
|
9310
|
+
return this.instances;
|
|
9311
|
+
}
|
|
9312
|
+
/**
|
|
9313
|
+
* After an instance changed (created, edited, switched, deleted) the runtime follows the record:
|
|
9314
|
+
* a missing record removes the manager and its adopted devices, an off or unfilled one stops the
|
|
9315
|
+
* manager but keeps the devices, a filled one starts or re-authenticates it.
|
|
9316
|
+
*/
|
|
9317
|
+
async syncBrokerRuntime(id) {
|
|
9318
|
+
const reg = this.requireRegistry();
|
|
9319
|
+
const entry = await this.requireBrokerStore().getById(id);
|
|
9320
|
+
this.activeBrokers = await this.loadPersistedBrokers();
|
|
9321
|
+
if (entry === null) {
|
|
9322
|
+
await reg.removeEntry(id);
|
|
9323
|
+
const deviceRegistry = this.ctx.kernel.deviceRegistry;
|
|
9324
|
+
const devices = this.ctx.kernel.devices;
|
|
9325
|
+
if (deviceRegistry && devices) await cascadeRemoveDevicesForBroker({
|
|
9326
|
+
reg: deviceRegistry,
|
|
9327
|
+
devices,
|
|
9328
|
+
addonId: this.ctx.id,
|
|
9329
|
+
brokerId: id,
|
|
9330
|
+
logger: this.ctx.logger
|
|
9331
|
+
});
|
|
9332
|
+
return;
|
|
9124
9333
|
}
|
|
9125
|
-
|
|
9334
|
+
const settings = entry.enabled === false ? null : entryToSettings(entry);
|
|
9335
|
+
if (settings === null) {
|
|
9336
|
+
await reg.removeEntry(id);
|
|
9337
|
+
return;
|
|
9338
|
+
}
|
|
9339
|
+
try {
|
|
9340
|
+
if (reg.get(id) !== null) await reg.updateEntry(id, settings);
|
|
9341
|
+
else await reg.createEntry(entry.name, settings, {
|
|
9342
|
+
id,
|
|
9343
|
+
...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
|
|
9344
|
+
});
|
|
9345
|
+
} catch (err) {
|
|
9346
|
+
this.ctx.logger.warn("ha instance: could not start the broker", { meta: {
|
|
9347
|
+
brokerId: id,
|
|
9348
|
+
error: require_ha_instance_settings.errMsg(err)
|
|
9349
|
+
} });
|
|
9350
|
+
}
|
|
9351
|
+
}
|
|
9352
|
+
requireBrokerStore() {
|
|
9353
|
+
if (this.brokerStore === null) throw new Error("provider-homeassistant: broker store not ready");
|
|
9354
|
+
return this.brokerStore;
|
|
9126
9355
|
}
|
|
9127
9356
|
async persistBrokerEntries(entries) {
|
|
9128
9357
|
this.activeBrokers = [...entries];
|
|
9129
|
-
|
|
9130
|
-
if (store !== null) {
|
|
9131
|
-
await store.replaceAll(entries);
|
|
9132
|
-
return;
|
|
9133
|
-
}
|
|
9134
|
-
await this.updateGlobalSettings({ brokers: entries });
|
|
9358
|
+
await this.requireBrokerStore().replaceAll(entries);
|
|
9135
9359
|
}
|
|
9136
9360
|
buildBrokerProvider() {
|
|
9137
9361
|
const ownerAddonId = this.ctx.id;
|
|
@@ -9205,7 +9429,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9205
9429
|
getSettings: async ({ id }) => {
|
|
9206
9430
|
const entry = this.activeBrokers.find((b) => b.id === id);
|
|
9207
9431
|
if (!entry) return null;
|
|
9208
|
-
return
|
|
9432
|
+
return entryToSettings(entry);
|
|
9209
9433
|
},
|
|
9210
9434
|
setSettings: async ({ id, settings }) => {
|
|
9211
9435
|
if (!owns(id)) return;
|
|
@@ -9216,7 +9440,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
9216
9440
|
getBrokerConfig: async ({ id }) => {
|
|
9217
9441
|
const entry = this.activeBrokers.find((b) => b.id === id);
|
|
9218
9442
|
if (!entry) return null;
|
|
9219
|
-
return
|
|
9443
|
+
return entryToSettings(entry);
|
|
9220
9444
|
},
|
|
9221
9445
|
getSettingsSchema: async ({ kind }) => {
|
|
9222
9446
|
if (kind !== HA_KIND) return null;
|