@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/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as fanControlCapability, A as bestLocationMatch, At as
|
|
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
|
-
/**
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
}),
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
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
|
|
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
|
-
|
|
7198
|
-
|
|
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
|
-
|
|
7723
|
-
|
|
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,
|
|
7795
|
+
const { id, integrationId, ...fields } = entry;
|
|
7763
7796
|
const body = {
|
|
7764
|
-
|
|
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
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
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
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
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
|
-
|
|
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
|
|
9068
|
-
|
|
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
|
|
9082
|
-
|
|
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
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9423
|
+
return entryToSettings(entry);
|
|
9202
9424
|
},
|
|
9203
9425
|
getSettingsSchema: async ({ kind }) => {
|
|
9204
9426
|
if (kind !== HA_KIND) return null;
|