@camstack/addon-provider-homeassistant 1.2.653 → 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 +15 -0
- package/dist/addon.js +938 -540
- package/dist/addon.mjs +481 -86
- package/dist/ha-export/ha-export.addon.js +452 -160
- package/dist/ha-export/ha-export.addon.mjs +406 -114
- package/dist/{ha-broker-source-DnSL-mbs.js → ha-instance-settings-5Ms7yRju.js} +1402 -525
- package/dist/{ha-broker-source-CKdH9XHn.mjs → ha-instance-settings-DLDNOmgL.mjs} +1342 -525
- package/package.json +6 -2
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as enumSensorCapability, $t as baseUnitOf, A as automationControlCapability, B as calendarSourceCapability, Bt as tamperCapability, C as RecordIntegrationInstances, Cn as EventCategory, Ct as prepareNotification, D as airQualitySensorCapability, Dt as publicEndpointReaderFrom, E as addonRoutesCapability, F as brightnessCapability, Ft as scriptRunnerCapability, Gt as vacuumControlCapability, H as climateControlCapability, Ht as tryConvertDelta, I as brokerCapability, J as controlCapability, Jt as waterHeaterCapability, Kt as valveCapability, L as buildAddonRouteProvider, Lt as smokeCapability, M as bestLocationMatch, Mt as runningFromMediaPlayer, N as binaryCapability, Nt as runningFromVacuumState, O as alarmPanelCapability, Ot as quantityFromUnit, P as boardKeySourceCapability, Pt as runningStateCapability, Qt as RecordSecretUnavailableError, R as buildDeviceAdminUrl, S as RUNNING_STATE_COLD, Sn as errMsg, St as preconditionOf, T as accessoriesCapability, Tt as pressureSensorCapability, U as colorCapability, Ut as tryConvertUnit, V as carbonMonoxideCapability, Vt as temperatureSensorCapability, W as connectivityCapability, Wt as updateCapability, X as deviceAdminLinkCapability, Y as coverCapability, Yt as weatherCapability, Z as deviceAdoptionCapability, _n as unknown, _t as notifierCapability, at as gasCapability, bn as DeviceRole, bt as parseAdminBase, c as BaseDevice, cn as array, ct as imageCapability, dn as literal, dt as lawnMowerControlCapability, en as createEvent, et as eventEmitterCapability, fn as number, ft as lockControlCapability, g as EnumSensorDateTimeFormatSchema, gn as union, gt as notificationOutputCapability, hn as string, ht as normalizeUnit, i as HA_BROKER_KIND, in as entryBody, it as floodCapability, j as batteryCapability, jt as runningFromLawnMower, k as ambientLightSensorCapability, kt as resolveAttachmentUrls, l as BaseDeviceProvider, ln as boolean, lt as ingestReading, mn as record, mt as motionCapability, nn as defineEntryKind, nt as flattenNestedRecords, ot as humidifierCapability, pn as object, pt as mediaPlayerCapability, q as contactCapability, qt as vibrationCapability, rn as defineRecordKinds, rt as flattenPortsOver, sn as _enum, st as humiditySensorCapability, t as haInstanceSettingsPortsOver, tn as defineAddonMigrations, tt as fanControlCapability, un as discriminatedUnion, ut as integrationInstancesCapability, vn as url, vt as numericSensorCapability, w as TargetSchema, wt as presenceCapability, xn as DeviceType, xt as powerMeterCapability, yn as DeviceFeature, z as buttonCapability, zt as switchCapability } from "./ha-instance-settings-DLDNOmgL.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
|
|
4
4
|
//#region src/ha-admin-link.ts
|
|
@@ -2496,11 +2496,33 @@ var HaMediaPlayerDevice = class extends HaBaseChildDevice {
|
|
|
2496
2496
|
registerRunningCap() {
|
|
2497
2497
|
this.ctx.registerNativeCap(runningStateCapability, { getStatus: async () => this.runtimeState.getCapState("running-state") ?? RUNNING_STATE_COLD });
|
|
2498
2498
|
}
|
|
2499
|
+
/** One service call per step, in order: HA has no "N steps" form. */
|
|
2500
|
+
async repeatHaService(service, steps) {
|
|
2501
|
+
for (let step = 0; step < steps; step += 1) await this.callHaService("media_player", service);
|
|
2502
|
+
}
|
|
2499
2503
|
registerMediaPlayerCap() {
|
|
2500
2504
|
this.ctx.registerNativeCap(mediaPlayerCapability, {
|
|
2501
2505
|
getStatus: async () => {
|
|
2502
2506
|
return this.runtimeState.getCapState("media-player") ?? COLD_START$10;
|
|
2503
2507
|
},
|
|
2508
|
+
turnOn: async () => {
|
|
2509
|
+
await this.callHaService("media_player", "turn_on");
|
|
2510
|
+
},
|
|
2511
|
+
turnOff: async () => {
|
|
2512
|
+
await this.callHaService("media_player", "turn_off");
|
|
2513
|
+
},
|
|
2514
|
+
volumeUp: async ({ steps }) => {
|
|
2515
|
+
await this.repeatHaService("volume_up", steps);
|
|
2516
|
+
},
|
|
2517
|
+
volumeDown: async ({ steps }) => {
|
|
2518
|
+
await this.repeatHaService("volume_down", steps);
|
|
2519
|
+
},
|
|
2520
|
+
channelUp: async ({ steps }) => {
|
|
2521
|
+
await this.repeatHaService("media_next_track", steps);
|
|
2522
|
+
},
|
|
2523
|
+
channelDown: async ({ steps }) => {
|
|
2524
|
+
await this.repeatHaService("media_previous_track", steps);
|
|
2525
|
+
},
|
|
2504
2526
|
play: async () => {
|
|
2505
2527
|
await this.callHaService("media_player", "media_play");
|
|
2506
2528
|
},
|
|
@@ -5742,22 +5764,16 @@ var TEST_HANDSHAKE_TIMEOUT_MS = 8e3;
|
|
|
5742
5764
|
var HaBrokerEntrySchema = object({
|
|
5743
5765
|
id: string(),
|
|
5744
5766
|
name: string(),
|
|
5745
|
-
/**
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
}),
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
* Populated when the wizard's "+ New Integration" flow created the
|
|
5756
|
-
* broker; null when the operator added it via the Settings panel
|
|
5757
|
-
* directly. Used by the integration listener to keep the broker
|
|
5758
|
-
* list in sync with the integrations table (cascade-delete on
|
|
5759
|
-
* integration removal). */
|
|
5760
|
-
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()
|
|
5761
5777
|
});
|
|
5762
5778
|
/**
|
|
5763
5779
|
* Validates a `broker.add` `settings` payload for the HA kind. Accepts:
|
|
@@ -5782,6 +5798,34 @@ function settingsToAuth(settings) {
|
|
|
5782
5798
|
accessToken: parsed.accessToken
|
|
5783
5799
|
};
|
|
5784
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
|
+
}
|
|
5785
5829
|
function authToSettings(auth) {
|
|
5786
5830
|
if (auth.kind === "supervisor") return { supervisorToken: auth.supervisorToken };
|
|
5787
5831
|
return {
|
|
@@ -5844,7 +5888,7 @@ var HaBrokerRegistry = class {
|
|
|
5844
5888
|
* started concurrently; transient errors don't block bootstrap. */
|
|
5845
5889
|
async restore(entries) {
|
|
5846
5890
|
this.seedCounterFromEntries(entries);
|
|
5847
|
-
await Promise.all(entries.map(async (entry) => {
|
|
5891
|
+
await Promise.all(entries.filter((entry) => entry.enabled !== false && haAuthOf(entry) !== null).map(async (entry) => {
|
|
5848
5892
|
try {
|
|
5849
5893
|
await this.startManagerForEntry(entry);
|
|
5850
5894
|
} catch (err) {
|
|
@@ -5905,7 +5949,7 @@ var HaBrokerRegistry = class {
|
|
|
5905
5949
|
const entry = {
|
|
5906
5950
|
id: options.id !== void 0 ? this.adoptId(options.id) : this.allocateId(),
|
|
5907
5951
|
name,
|
|
5908
|
-
auth,
|
|
5952
|
+
...authToEntryFields(auth),
|
|
5909
5953
|
...options.integrationId !== void 0 ? { integrationId: options.integrationId } : {}
|
|
5910
5954
|
};
|
|
5911
5955
|
await this.startManagerForEntry(entry);
|
|
@@ -5926,7 +5970,7 @@ var HaBrokerRegistry = class {
|
|
|
5926
5970
|
return {
|
|
5927
5971
|
id,
|
|
5928
5972
|
name: existing.getInfo().name,
|
|
5929
|
-
auth
|
|
5973
|
+
...authToEntryFields(auth)
|
|
5930
5974
|
};
|
|
5931
5975
|
}
|
|
5932
5976
|
/**
|
|
@@ -6109,10 +6153,12 @@ var HaBrokerRegistry = class {
|
|
|
6109
6153
|
this.logger.debug("ha-broker already active — reusing existing connection", { meta: { brokerId: entry.id } });
|
|
6110
6154
|
return;
|
|
6111
6155
|
}
|
|
6156
|
+
const auth = haAuthOf(entry);
|
|
6157
|
+
if (auth === null) throw new Error(`ha-broker '${entry.id}' has no complete credentials`);
|
|
6112
6158
|
const manager = new HaIntegrationManager({
|
|
6113
6159
|
id: entry.id,
|
|
6114
6160
|
name: entry.name,
|
|
6115
|
-
auth
|
|
6161
|
+
auth,
|
|
6116
6162
|
logger: this.logger,
|
|
6117
6163
|
eventBus: this.eventBus,
|
|
6118
6164
|
onConnected: (brokerId) => this.onBrokerConnected?.(brokerId),
|
|
@@ -6439,6 +6485,8 @@ function mapMediaPlayer(attributes) {
|
|
|
6439
6485
|
if ((sf & 8) !== 0) features.push(DeviceFeature.MediaPlayerMute);
|
|
6440
6486
|
if ((sf & 16) !== 0) features.push(DeviceFeature.MediaPlayerPrevious);
|
|
6441
6487
|
if ((sf & 32) !== 0) features.push(DeviceFeature.MediaPlayerNext);
|
|
6488
|
+
if ((sf & 384) === 384) features.push(DeviceFeature.MediaPlayerPower);
|
|
6489
|
+
if ((sf & 1024) !== 0) features.push(DeviceFeature.MediaPlayerVolumeStep);
|
|
6442
6490
|
if ((sf & 512) !== 0) features.push(DeviceFeature.MediaPlayerPlayMedia);
|
|
6443
6491
|
if ((sf & 2048) !== 0) features.push(DeviceFeature.MediaPlayerSelectSource);
|
|
6444
6492
|
if ((sf & 4096) !== 0) features.push(DeviceFeature.MediaPlayerStop);
|
|
@@ -7170,10 +7218,15 @@ function createHaIconRouteProvider() {
|
|
|
7170
7218
|
var haBrokersEntryKind = defineEntryKind("brokers", {
|
|
7171
7219
|
title: "Home Assistant brokers",
|
|
7172
7220
|
schema: entryBody({
|
|
7173
|
-
|
|
7174
|
-
|
|
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
|
|
7175
7227
|
}),
|
|
7176
|
-
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"]
|
|
7177
7230
|
});
|
|
7178
7231
|
var haRecordKinds = defineRecordKinds({
|
|
7179
7232
|
/**
|
|
@@ -7695,8 +7748,12 @@ function rowToEntry(id, data) {
|
|
|
7695
7748
|
const parsed = HaBrokerEntrySchema.safeParse({
|
|
7696
7749
|
id,
|
|
7697
7750
|
name: data.name,
|
|
7698
|
-
|
|
7699
|
-
|
|
7751
|
+
authKind: data.authKind,
|
|
7752
|
+
baseUrl: data.baseUrl,
|
|
7753
|
+
accessToken: data.accessToken,
|
|
7754
|
+
supervisorToken: data.supervisorToken,
|
|
7755
|
+
integrationId: data.integrationId,
|
|
7756
|
+
enabled: data.enabled
|
|
7700
7757
|
});
|
|
7701
7758
|
return parsed.success ? parsed.data : null;
|
|
7702
7759
|
}
|
|
@@ -7735,10 +7792,9 @@ var HaBrokerStore = class HaBrokerStore {
|
|
|
7735
7792
|
}
|
|
7736
7793
|
}
|
|
7737
7794
|
async upsert(entry) {
|
|
7738
|
-
const { id,
|
|
7795
|
+
const { id, integrationId, ...fields } = entry;
|
|
7739
7796
|
const body = {
|
|
7740
|
-
|
|
7741
|
-
auth,
|
|
7797
|
+
...Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0)),
|
|
7742
7798
|
...integrationId === void 0 ? {} : { integrationId }
|
|
7743
7799
|
};
|
|
7744
7800
|
const parsed = haBrokersEntryKind.schema.safeParse(body);
|
|
@@ -7759,6 +7815,97 @@ var HaBrokerStore = class HaBrokerStore {
|
|
|
7759
7815
|
}
|
|
7760
7816
|
};
|
|
7761
7817
|
//#endregion
|
|
7818
|
+
//#region src/ha-instances.ts
|
|
7819
|
+
/**
|
|
7820
|
+
* The Home Assistant connections as `integration-instances` (planning#408 P3): one instance per
|
|
7821
|
+
* `brokers` record, over the SAME records and ids (`ha_NNN`) the `broker` cap serves. The body is
|
|
7822
|
+
* flat, so the form the instances page edits is the stored shape.
|
|
7823
|
+
*/
|
|
7824
|
+
var HA_INSTANCE_KIND = "home-assistant";
|
|
7825
|
+
function isHaBodyConfigured(body) {
|
|
7826
|
+
return body.authKind === "supervisor" ? (body.supervisorToken ?? "").length > 0 : (body.baseUrl ?? "").length > 0 && (body.accessToken ?? "").length > 0;
|
|
7827
|
+
}
|
|
7828
|
+
function statusOf(info) {
|
|
7829
|
+
if (info === null) return {
|
|
7830
|
+
state: "unknown",
|
|
7831
|
+
label: "Not connected"
|
|
7832
|
+
};
|
|
7833
|
+
if (info.status === "connected") return {
|
|
7834
|
+
state: "ok",
|
|
7835
|
+
label: "Connected"
|
|
7836
|
+
};
|
|
7837
|
+
if (info.status === "connecting") return {
|
|
7838
|
+
state: "unknown",
|
|
7839
|
+
label: "Connecting"
|
|
7840
|
+
};
|
|
7841
|
+
return {
|
|
7842
|
+
state: "error",
|
|
7843
|
+
label: info.status,
|
|
7844
|
+
...info.error !== null ? { detail: info.error } : {}
|
|
7845
|
+
};
|
|
7846
|
+
}
|
|
7847
|
+
function buildHaInstances(records, deps) {
|
|
7848
|
+
return new RecordIntegrationInstances(records, haBrokersEntryKind, {
|
|
7849
|
+
description: {
|
|
7850
|
+
instanceMode: "multiple",
|
|
7851
|
+
kind: "connection",
|
|
7852
|
+
canTest: true,
|
|
7853
|
+
kinds: [{
|
|
7854
|
+
id: HA_INSTANCE_KIND,
|
|
7855
|
+
label: "Home Assistant"
|
|
7856
|
+
}],
|
|
7857
|
+
allowRestoreId: true
|
|
7858
|
+
},
|
|
7859
|
+
formSchema: () => haBrokerSettingsSchema,
|
|
7860
|
+
initialBody: (name) => ({
|
|
7861
|
+
name,
|
|
7862
|
+
authKind: "long-lived"
|
|
7863
|
+
}),
|
|
7864
|
+
mintId: () => deps.mintId(),
|
|
7865
|
+
kindOf: () => HA_INSTANCE_KIND,
|
|
7866
|
+
health: (_body, id) => {
|
|
7867
|
+
const info = deps.infoOf(id);
|
|
7868
|
+
return {
|
|
7869
|
+
status: statusOf(info),
|
|
7870
|
+
...info?.lastCheckedAt != null ? { lastCheckedAt: info.lastCheckedAt } : {}
|
|
7871
|
+
};
|
|
7872
|
+
},
|
|
7873
|
+
configured: isHaBodyConfigured,
|
|
7874
|
+
onChanged: deps.onChanged,
|
|
7875
|
+
test: async ({ id, values }) => {
|
|
7876
|
+
if (["accessToken", "supervisorToken"].some((key) => {
|
|
7877
|
+
const v = values[key];
|
|
7878
|
+
return typeof v === "string" && v.length > 0 && v !== "__camstack_redacted__";
|
|
7879
|
+
})) {
|
|
7880
|
+
const r = await deps.probe(values);
|
|
7881
|
+
return r.ok ? {
|
|
7882
|
+
success: true,
|
|
7883
|
+
status: "validated",
|
|
7884
|
+
...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
|
|
7885
|
+
} : {
|
|
7886
|
+
success: false,
|
|
7887
|
+
status: "rejected",
|
|
7888
|
+
error: r.error ?? "Home Assistant rejected the connection"
|
|
7889
|
+
};
|
|
7890
|
+
}
|
|
7891
|
+
const info = id === null ? null : deps.infoOf(id);
|
|
7892
|
+
if (info === null) return {
|
|
7893
|
+
success: false,
|
|
7894
|
+
status: "inconclusive",
|
|
7895
|
+
error: "Not connected: save the connection first"
|
|
7896
|
+
};
|
|
7897
|
+
return info.status === "connected" ? {
|
|
7898
|
+
success: true,
|
|
7899
|
+
status: "validated"
|
|
7900
|
+
} : {
|
|
7901
|
+
success: false,
|
|
7902
|
+
status: "rejected",
|
|
7903
|
+
error: info.error ?? `status: ${info.status}`
|
|
7904
|
+
};
|
|
7905
|
+
}
|
|
7906
|
+
});
|
|
7907
|
+
}
|
|
7908
|
+
//#endregion
|
|
7762
7909
|
//#region src/ha-board-key-source.ts
|
|
7763
7910
|
/**
|
|
7764
7911
|
* `board-key-source` for Home Assistant: reads an ESPHome board's API
|
|
@@ -7932,6 +8079,178 @@ function createHaBoardKeyProvider(source) {
|
|
|
7932
8079
|
};
|
|
7933
8080
|
}
|
|
7934
8081
|
//#endregion
|
|
8082
|
+
//#region src/migrations/flatten-broker-records.ts
|
|
8083
|
+
/**
|
|
8084
|
+
* The Home Assistant broker records keep their connection flat (planning#408 P3): the nested
|
|
8085
|
+
* `auth` object (`{ kind, baseUrl, accessToken }` or `{ kind, supervisorToken }`) becomes the top
|
|
8086
|
+
* level `authKind`, `baseUrl`, `accessToken` and `supervisorToken`, under the SAME record id, so
|
|
8087
|
+
* the devices that carry `ha_NNN` keep their broker. The work is `flattenNestedRecords`: every
|
|
8088
|
+
* secret is read before anything is written, read back from its new keychain ref before the
|
|
8089
|
+
* nested object goes, and the old refs are deleted last.
|
|
8090
|
+
*/
|
|
8091
|
+
var FLATTEN_BROKER_RECORDS_ID = "provider-homeassistant/2026-11-04-flatten-broker-records";
|
|
8092
|
+
function text(value) {
|
|
8093
|
+
return typeof value === "string" ? value : void 0;
|
|
8094
|
+
}
|
|
8095
|
+
async function flattenBrokerRecords(ports) {
|
|
8096
|
+
return flattenNestedRecords(ports, {
|
|
8097
|
+
legacyKey: "auth",
|
|
8098
|
+
flatSecretKeys: ["accessToken", "supervisorToken"],
|
|
8099
|
+
toFlat: (legacy) => {
|
|
8100
|
+
const kind = legacy["kind"] === "supervisor" ? "supervisor" : "long-lived";
|
|
8101
|
+
const baseUrl = text(legacy["baseUrl"]);
|
|
8102
|
+
const accessToken = text(legacy["accessToken"]);
|
|
8103
|
+
const supervisorToken = text(legacy["supervisorToken"]);
|
|
8104
|
+
return {
|
|
8105
|
+
authKind: kind,
|
|
8106
|
+
...baseUrl !== void 0 ? { baseUrl } : {},
|
|
8107
|
+
...accessToken !== void 0 ? { accessToken } : {},
|
|
8108
|
+
...supervisorToken !== void 0 ? { supervisorToken } : {}
|
|
8109
|
+
};
|
|
8110
|
+
}
|
|
8111
|
+
});
|
|
8112
|
+
}
|
|
8113
|
+
//#endregion
|
|
8114
|
+
//#region src/migrations/calendar-ids-to-integration.ts
|
|
8115
|
+
/**
|
|
8116
|
+
* The HA calendars an instance imports are a fact of THAT instance (D985): each Home Assistant
|
|
8117
|
+
* integration carries its own `calendarIds` in its settings, and the addon-level `calendarIds`
|
|
8118
|
+
* setting (one set of `calendar.*` entity ids for every broker) goes away.
|
|
8119
|
+
*
|
|
8120
|
+
* Two migrations, in this order:
|
|
8121
|
+
*
|
|
8122
|
+
* 1. `calendar-ids-to-integration` (procedure) copies the shared set to EVERY integration that
|
|
8123
|
+
* has no `calendarIds` yet. An integration that already has one is never overwritten; one
|
|
8124
|
+
* deleted meanwhile is skipped, never recreated; a stored value that is not a list of strings
|
|
8125
|
+
* is an ambiguity and throws (nothing is guessed).
|
|
8126
|
+
* 2. `drop-shared-calendar-ids` (plan) deletes the shared setting after a read-back: every
|
|
8127
|
+
* integration must show its own set. When one holds a set that differs from the shared one,
|
|
8128
|
+
* the shared value is the loser and stays PARKED in place, never deleted.
|
|
8129
|
+
*
|
|
8130
|
+
* Both are idempotent.
|
|
8131
|
+
*/
|
|
8132
|
+
var HA_PROVIDER_ID = "provider-homeassistant";
|
|
8133
|
+
/** The canonical settings table: one row per key, id `<addonId>.<key>`. */
|
|
8134
|
+
var ADDON_SETTINGS_TABLE = "addon-settings";
|
|
8135
|
+
var SHARED_CALENDAR_IDS_KEY = "calendarIds";
|
|
8136
|
+
async function readSharedSet(ports) {
|
|
8137
|
+
const answer = await ports.readStored(SHARED_CALENDAR_IDS_KEY);
|
|
8138
|
+
if (answer.kind === "unreadable") throw new Error(`provider-homeassistant: the shared calendarIds cannot be read (${answer.reason})`);
|
|
8139
|
+
if (answer.kind === "absent") return null;
|
|
8140
|
+
const value = answer.value;
|
|
8141
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new Error("provider-homeassistant: the shared calendarIds is not a list of entity ids — nothing copied");
|
|
8142
|
+
return value;
|
|
8143
|
+
}
|
|
8144
|
+
async function copyCalendarIdsToIntegrations(ports) {
|
|
8145
|
+
const shared = await readSharedSet(ports);
|
|
8146
|
+
if (shared === null) return {
|
|
8147
|
+
outcome: "already-done",
|
|
8148
|
+
summary: { reason: "no-shared-set" }
|
|
8149
|
+
};
|
|
8150
|
+
let copied = 0;
|
|
8151
|
+
let kept = 0;
|
|
8152
|
+
let gone = 0;
|
|
8153
|
+
for (const instance of await ports.listInstances()) {
|
|
8154
|
+
if ("calendarIds" in instance.settings) {
|
|
8155
|
+
kept += 1;
|
|
8156
|
+
continue;
|
|
8157
|
+
}
|
|
8158
|
+
if (await ports.patchInstanceSettings(instance.integrationId, { calendarIds: [...shared] })) copied += 1;
|
|
8159
|
+
else gone += 1;
|
|
8160
|
+
}
|
|
8161
|
+
return {
|
|
8162
|
+
outcome: copied === 0 ? "already-done" : "done",
|
|
8163
|
+
summary: {
|
|
8164
|
+
copied,
|
|
8165
|
+
kept,
|
|
8166
|
+
gone
|
|
8167
|
+
}
|
|
8168
|
+
};
|
|
8169
|
+
}
|
|
8170
|
+
var asIds = (value) => Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
8171
|
+
var sameSet = (a, b) => {
|
|
8172
|
+
const left = new Set(a);
|
|
8173
|
+
const right = new Set(b);
|
|
8174
|
+
return left.size === right.size && [...left].every((id) => right.has(id));
|
|
8175
|
+
};
|
|
8176
|
+
async function planDropSharedCalendarIds(ports) {
|
|
8177
|
+
const shared = await readSharedSet(ports);
|
|
8178
|
+
if (shared === null) return { edits: [] };
|
|
8179
|
+
const instances = await ports.listInstances();
|
|
8180
|
+
if (instances.length === 0) return { edits: [] };
|
|
8181
|
+
const own = instances.map((instance) => ({
|
|
8182
|
+
integrationId: instance.integrationId,
|
|
8183
|
+
ids: asIds(instance.settings[SHARED_CALENDAR_IDS_KEY])
|
|
8184
|
+
}));
|
|
8185
|
+
const missing = own.filter((entry) => entry.ids === null);
|
|
8186
|
+
if (missing.length > 0) throw new Error(`provider-homeassistant: the shared calendarIds is not dropped, ${missing.map((m) => m.integrationId).join(", ")} has no set of its own`);
|
|
8187
|
+
if (!own.every((entry) => sameSet(entry.ids ?? [], shared))) return { edits: [] };
|
|
8188
|
+
const rowId = `${HA_PROVIDER_ID}.${SHARED_CALENDAR_IDS_KEY}`;
|
|
8189
|
+
return { edits: [{
|
|
8190
|
+
op: "deleteKeys",
|
|
8191
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
8192
|
+
keys: [rowId],
|
|
8193
|
+
precondition: preconditionOf({ [rowId]: {
|
|
8194
|
+
addonId: HA_PROVIDER_ID,
|
|
8195
|
+
key: SHARED_CALENDAR_IDS_KEY,
|
|
8196
|
+
value: shared
|
|
8197
|
+
} })
|
|
8198
|
+
}] };
|
|
8199
|
+
}
|
|
8200
|
+
//#endregion
|
|
8201
|
+
//#region src/migrations/index.ts
|
|
8202
|
+
/** The `provider-homeassistant` catalogue (D987): offered to the engine when the addon initialises. */
|
|
8203
|
+
var ADDON_MIGRATIONS = [
|
|
8204
|
+
{
|
|
8205
|
+
meta: {
|
|
8206
|
+
id: "provider-homeassistant/2026-11-03-calendar-ids-to-integration",
|
|
8207
|
+
owner: "provider-homeassistant",
|
|
8208
|
+
level: "addon-state",
|
|
8209
|
+
seq: 1,
|
|
8210
|
+
dependsOn: [],
|
|
8211
|
+
store: "settings",
|
|
8212
|
+
scope: "cluster",
|
|
8213
|
+
onFailure: "tolerate",
|
|
8214
|
+
bodyKind: "procedure",
|
|
8215
|
+
description: "Copy the shared calendar set to the settings of every Home Assistant integration that has none",
|
|
8216
|
+
fixtureMark: "the broker link stored beside the calendars of an integration"
|
|
8217
|
+
},
|
|
8218
|
+
procedure: copyCalendarIdsToIntegrations
|
|
8219
|
+
},
|
|
8220
|
+
{
|
|
8221
|
+
meta: {
|
|
8222
|
+
id: "provider-homeassistant/2026-11-03-drop-shared-calendar-ids",
|
|
8223
|
+
owner: "provider-homeassistant",
|
|
8224
|
+
level: "addon-state",
|
|
8225
|
+
seq: 2,
|
|
8226
|
+
dependsOn: ["provider-homeassistant/2026-11-03-calendar-ids-to-integration"],
|
|
8227
|
+
store: "settings",
|
|
8228
|
+
scope: "cluster",
|
|
8229
|
+
onFailure: "tolerate",
|
|
8230
|
+
bodyKind: "plan",
|
|
8231
|
+
description: "Delete the shared calendar set once every integration shows its own",
|
|
8232
|
+
fixtureMark: "the shared set parked when an integration holds a different one"
|
|
8233
|
+
},
|
|
8234
|
+
plan: planDropSharedCalendarIds
|
|
8235
|
+
},
|
|
8236
|
+
{
|
|
8237
|
+
meta: {
|
|
8238
|
+
id: FLATTEN_BROKER_RECORDS_ID,
|
|
8239
|
+
owner: "provider-homeassistant",
|
|
8240
|
+
level: "addon-state",
|
|
8241
|
+
seq: 3,
|
|
8242
|
+
dependsOn: [],
|
|
8243
|
+
store: "settings",
|
|
8244
|
+
scope: "cluster",
|
|
8245
|
+
onFailure: "block-owner",
|
|
8246
|
+
bodyKind: "procedure",
|
|
8247
|
+
description: "Flatten the nested auth of every Home Assistant broker record under its own id, moving each token to a top-level keychain secret",
|
|
8248
|
+
fixtureMark: "a non-secret field stored beside the auth of a broker record"
|
|
8249
|
+
},
|
|
8250
|
+
procedure: flattenBrokerRecords
|
|
8251
|
+
}
|
|
8252
|
+
];
|
|
8253
|
+
//#endregion
|
|
7935
8254
|
//#region src/ha-calendar-source.ts
|
|
7936
8255
|
function isCalendarEntity(entityId) {
|
|
7937
8256
|
return entityId.startsWith("calendar.");
|
|
@@ -8042,9 +8361,9 @@ function isSelected(entityId, selected) {
|
|
|
8042
8361
|
function createHaCalendarSource(deps) {
|
|
8043
8362
|
return {
|
|
8044
8363
|
listCalendars: async () => {
|
|
8045
|
-
const selected = deps.selectedCalendarIds();
|
|
8046
8364
|
const out = [];
|
|
8047
|
-
for (const { manager } of deps.brokers.listManagers()) {
|
|
8365
|
+
for (const { brokerId, manager } of deps.brokers.listManagers()) {
|
|
8366
|
+
const selected = deps.selectedCalendarIds(brokerId);
|
|
8048
8367
|
const states = manager.snapshotAllStates();
|
|
8049
8368
|
for (const [entityId, raw] of Object.entries(states)) {
|
|
8050
8369
|
if (!isCalendarEntity(entityId)) continue;
|
|
@@ -8064,10 +8383,10 @@ function createHaCalendarSource(deps) {
|
|
|
8064
8383
|
listEvents: async (input) => {
|
|
8065
8384
|
const startIso = new Date(input.startMs).toISOString();
|
|
8066
8385
|
const endIso = new Date(input.endMs).toISOString();
|
|
8067
|
-
const selected = deps.selectedCalendarIds();
|
|
8068
8386
|
const wanted = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? new Set(input.calendarIds) : null;
|
|
8069
8387
|
const out = [];
|
|
8070
8388
|
for (const { brokerId, manager } of deps.brokers.listManagers()) {
|
|
8389
|
+
const selected = deps.selectedCalendarIds(brokerId);
|
|
8071
8390
|
const states = manager.snapshotAllStates();
|
|
8072
8391
|
const entityIds = Object.keys(states).filter((id) => {
|
|
8073
8392
|
if (!isCalendarEntity(id)) return false;
|
|
@@ -8091,10 +8410,11 @@ function createHaCalendarSource(deps) {
|
|
|
8091
8410
|
};
|
|
8092
8411
|
}
|
|
8093
8412
|
/** Discover every HA calendar entity (ignores the import filter) — for settings catalog. */
|
|
8094
|
-
function listHaCalendarCatalog(brokers) {
|
|
8413
|
+
function listHaCalendarCatalog(brokers, onlyBrokerId) {
|
|
8095
8414
|
const out = [];
|
|
8096
8415
|
const seen = /* @__PURE__ */ new Set();
|
|
8097
|
-
for (const { manager } of brokers.listManagers()) {
|
|
8416
|
+
for (const { brokerId, manager } of brokers.listManagers()) {
|
|
8417
|
+
if (onlyBrokerId !== void 0 && brokerId !== onlyBrokerId) continue;
|
|
8098
8418
|
const states = manager.snapshotAllStates();
|
|
8099
8419
|
for (const [entityId, raw] of Object.entries(states)) {
|
|
8100
8420
|
if (!isCalendarEntity(entityId) || seen.has(entityId)) continue;
|
|
@@ -8543,10 +8863,7 @@ function planStandaloneReconcile(devices, freshById, brokerId) {
|
|
|
8543
8863
|
var ADOPTION_FILTER_DEVICES = "devices";
|
|
8544
8864
|
var ADOPTION_FILTER_ENTITIES = "entities";
|
|
8545
8865
|
var HA_KIND = "home-assistant";
|
|
8546
|
-
var DEFAULT_CONFIG = {
|
|
8547
|
-
brokers: [],
|
|
8548
|
-
calendarIds: []
|
|
8549
|
-
};
|
|
8866
|
+
var DEFAULT_CONFIG = { calendarIds: [] };
|
|
8550
8867
|
/**
|
|
8551
8868
|
* Home Assistant provider addon.
|
|
8552
8869
|
*
|
|
@@ -8593,6 +8910,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8593
8910
|
}
|
|
8594
8911
|
registry = null;
|
|
8595
8912
|
brokerStore = null;
|
|
8913
|
+
instances = null;
|
|
8596
8914
|
/** Runtime broker rows (records when B2 is live, else settings). */
|
|
8597
8915
|
activeBrokers = [];
|
|
8598
8916
|
/** Periodic re-sync sweep handle (cleared on shutdown). */
|
|
@@ -8607,11 +8925,26 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8607
8925
|
constructor() {
|
|
8608
8926
|
super({ ...DEFAULT_CONFIG });
|
|
8609
8927
|
}
|
|
8928
|
+
flattenPorts(records) {
|
|
8929
|
+
if (this.ctx.secrets === void 0) throw new Error("provider-homeassistant: ctx.secrets is required");
|
|
8930
|
+
return {
|
|
8931
|
+
readStored: (key) => this.readStoredGlobalKey(key),
|
|
8932
|
+
...haInstanceSettingsPortsOver(this.ctx.api.integrations),
|
|
8933
|
+
...flattenPortsOver(records.kind(haBrokersEntryKind))
|
|
8934
|
+
};
|
|
8935
|
+
}
|
|
8610
8936
|
async onInitialize() {
|
|
8611
8937
|
const records = this.ctx.records;
|
|
8612
8938
|
if (records === void 0) throw new Error("provider-homeassistant: ctx.records is required (hub-only addon)");
|
|
8613
8939
|
await records.declare(haAllRecordKinds);
|
|
8614
8940
|
this.brokerStore = HaBrokerStore.of(records);
|
|
8941
|
+
this.instances = buildHaInstances(records, {
|
|
8942
|
+
mintId: () => this.requireRegistry().allocateId(),
|
|
8943
|
+
infoOf: (id) => this.registry?.get(id) ?? null,
|
|
8944
|
+
onChanged: (id) => this.syncBrokerRuntime(id),
|
|
8945
|
+
probe: (settings) => this.requireRegistry().testSettings(settings)
|
|
8946
|
+
});
|
|
8947
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id === FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
|
|
8615
8948
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
8616
8949
|
this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
|
|
8617
8950
|
this.registry.setOnBrokerConnected((brokerId) => {
|
|
@@ -8628,6 +8961,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8628
8961
|
this.reconcileAllConnectedBrokers();
|
|
8629
8962
|
}, HaProviderAddon.RESYNC_INTERVAL_MS);
|
|
8630
8963
|
this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
|
|
8964
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.filter((m) => m.meta.id !== FLATTEN_BROKER_RECORDS_ID), this.flattenPorts(records)));
|
|
8631
8965
|
await this.reconcileIntegrationsToBrokers();
|
|
8632
8966
|
this.subscribeIntegrationLifecycle();
|
|
8633
8967
|
return [
|
|
@@ -8635,6 +8969,10 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8635
8969
|
capability: brokerCapability,
|
|
8636
8970
|
provider: this.buildBrokerProvider()
|
|
8637
8971
|
},
|
|
8972
|
+
{
|
|
8973
|
+
capability: integrationInstancesCapability,
|
|
8974
|
+
provider: this.requireInstances().toProvider()
|
|
8975
|
+
},
|
|
8638
8976
|
{
|
|
8639
8977
|
capability: deviceAdoptionCapability,
|
|
8640
8978
|
provider: this.buildAdoptionProvider()
|
|
@@ -8656,7 +8994,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8656
8994
|
provider: createHaCalendarSource({
|
|
8657
8995
|
brokers: this.requireRegistry(),
|
|
8658
8996
|
logger: this.ctx.logger.child("calendar-source"),
|
|
8659
|
-
selectedCalendarIds: () =>
|
|
8997
|
+
selectedCalendarIds: (brokerId) => this.calendarIdsOf(brokerId)
|
|
8660
8998
|
})
|
|
8661
8999
|
}
|
|
8662
9000
|
];
|
|
@@ -8754,6 +9092,20 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8754
9092
|
*
|
|
8755
9093
|
* Idempotent — runs on boot + on every integration.* event.
|
|
8756
9094
|
*/
|
|
9095
|
+
/**
|
|
9096
|
+
* Calendars to import, per broker (D985): the integration's own selection, filled by the
|
|
9097
|
+
* reconcile that already reads each integration's settings. A broker whose integration has no
|
|
9098
|
+
* selection yet (the move has not run) uses the legacy shared set, so a boot mid-move never
|
|
9099
|
+
* changes what is imported.
|
|
9100
|
+
*/
|
|
9101
|
+
calendarIdsByBroker = /* @__PURE__ */ new Map();
|
|
9102
|
+
noteInstanceCalendars(brokerId, raw) {
|
|
9103
|
+
if (Array.isArray(raw) && raw.every((id) => typeof id === "string")) this.calendarIdsByBroker.set(brokerId, new Set(raw));
|
|
9104
|
+
else this.calendarIdsByBroker.delete(brokerId);
|
|
9105
|
+
}
|
|
9106
|
+
calendarIdsOf(brokerId) {
|
|
9107
|
+
return this.calendarIdsByBroker.get(brokerId) ?? new Set(this.config.calendarIds ?? []);
|
|
9108
|
+
}
|
|
8757
9109
|
async reconcileIntegrationsToBrokers() {
|
|
8758
9110
|
const api = this.ctx.api;
|
|
8759
9111
|
if (!api.integrations?.list?.query || !api.integrations.getSettings?.query) return;
|
|
@@ -8765,6 +9117,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8765
9117
|
const settings = await api.integrations.getSettings.query({ id: integration.id });
|
|
8766
9118
|
const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : null;
|
|
8767
9119
|
if (!brokerId) continue;
|
|
9120
|
+
this.noteInstanceCalendars(brokerId, settings["calendarIds"]);
|
|
8768
9121
|
if (!this.activeBrokers.find((b) => b.id === brokerId)) {
|
|
8769
9122
|
this.ctx.logger.warn("integration→broker: linked broker not found", { meta: {
|
|
8770
9123
|
integrationId: integration.id,
|
|
@@ -8804,23 +9157,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8804
9157
|
eb.subscribe({ category: "integration.disabled" }, handler);
|
|
8805
9158
|
eb.subscribe({ category: "integration.deleted" }, handler);
|
|
8806
9159
|
}
|
|
8807
|
-
globalSettingsSchema(
|
|
8808
|
-
if (cap === "calendar-source") return this.schema({ sections: [{
|
|
8809
|
-
id: "ha-calendars-import",
|
|
8810
|
-
title: "Calendars to import",
|
|
8811
|
-
titleKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.title",
|
|
8812
|
-
columns: 1,
|
|
8813
|
-
fields: [{
|
|
8814
|
-
type: "multiselect",
|
|
8815
|
-
key: "calendarIds",
|
|
8816
|
-
label: "Calendars",
|
|
8817
|
-
labelKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.label",
|
|
8818
|
-
options: [],
|
|
8819
|
-
optionsFrom: "_calendarCatalog",
|
|
8820
|
-
description: "Discovered from connected HA brokers. Select at least one to import; leave empty to import none.",
|
|
8821
|
-
descriptionKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.description"
|
|
8822
|
-
}]
|
|
8823
|
-
}] });
|
|
9160
|
+
globalSettingsSchema(_cap) {
|
|
8824
9161
|
return this.schema({ sections: [{
|
|
8825
9162
|
id: "ha-broker",
|
|
8826
9163
|
title: "Home Assistant broker",
|
|
@@ -8835,14 +9172,39 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8835
9172
|
}]
|
|
8836
9173
|
}] });
|
|
8837
9174
|
}
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
9175
|
+
/**
|
|
9176
|
+
* The calendars of ONE Home Assistant instance (D985): picked on its integration, offered from
|
|
9177
|
+
* that broker's own `calendar.*` entities.
|
|
9178
|
+
*/
|
|
9179
|
+
async integrationSettingsSchema(instance) {
|
|
9180
|
+
if (instance.integrationId === null || !this.registry) return null;
|
|
9181
|
+
let brokerId;
|
|
9182
|
+
try {
|
|
9183
|
+
brokerId = (await this.ctx.api.integrations.getSettings.query({ id: instance.integrationId }))["brokerId"];
|
|
9184
|
+
} catch (err) {
|
|
9185
|
+
this.ctx.logger.warn("HA integration form: instance lookup failed — broker form shown", { meta: {
|
|
9186
|
+
integrationId: instance.integrationId,
|
|
9187
|
+
error: errMsg(err)
|
|
9188
|
+
} });
|
|
9189
|
+
return null;
|
|
9190
|
+
}
|
|
9191
|
+
if (typeof brokerId !== "string") return null;
|
|
9192
|
+
const options = listHaCalendarCatalog(this.registry, brokerId);
|
|
9193
|
+
return this.schema({ sections: [{
|
|
9194
|
+
id: "ha-calendars-import",
|
|
9195
|
+
title: "Calendars to import",
|
|
9196
|
+
titleKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.title",
|
|
9197
|
+
columns: 1,
|
|
9198
|
+
fields: [{
|
|
9199
|
+
type: "multiselect",
|
|
9200
|
+
key: "calendarIds",
|
|
9201
|
+
label: "Calendars",
|
|
9202
|
+
labelKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.label",
|
|
9203
|
+
options,
|
|
9204
|
+
description: "Discovered from this Home Assistant instance. Select at least one to import; leave empty to import none.",
|
|
9205
|
+
descriptionKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.description"
|
|
9206
|
+
}]
|
|
9207
|
+
}] });
|
|
8846
9208
|
}
|
|
8847
9209
|
async onShutdown() {
|
|
8848
9210
|
if (this.resyncTimer) {
|
|
@@ -8890,11 +9252,8 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8890
9252
|
for (const entry of persisted) {
|
|
8891
9253
|
if (entry.id && liveIds.has(entry.id)) {
|
|
8892
9254
|
try {
|
|
8893
|
-
const settings = entry
|
|
8894
|
-
|
|
8895
|
-
accessToken: entry.auth.accessToken
|
|
8896
|
-
};
|
|
8897
|
-
await reg.updateEntry(entry.id, settings);
|
|
9255
|
+
const settings = entryToSettings(entry);
|
|
9256
|
+
if (settings !== null) await reg.updateEntry(entry.id, settings);
|
|
8898
9257
|
} catch (err) {
|
|
8899
9258
|
this.ctx.logger.warn("ha onConfigChanged: updateEntry failed", { meta: {
|
|
8900
9259
|
brokerId: entry.id,
|
|
@@ -8904,10 +9263,8 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8904
9263
|
continue;
|
|
8905
9264
|
}
|
|
8906
9265
|
try {
|
|
8907
|
-
const settings = entry
|
|
8908
|
-
|
|
8909
|
-
accessToken: entry.auth.accessToken
|
|
8910
|
-
};
|
|
9266
|
+
const settings = entryToSettings(entry);
|
|
9267
|
+
if (settings === null) continue;
|
|
8911
9268
|
const created = await reg.createEntry(entry.name, settings);
|
|
8912
9269
|
if (created.id !== entry.id) {
|
|
8913
9270
|
const next = persisted.map((b) => b === entry ? {
|
|
@@ -8925,21 +9282,59 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8925
9282
|
}
|
|
8926
9283
|
}
|
|
8927
9284
|
async loadPersistedBrokers() {
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
9285
|
+
return this.requireBrokerStore().listResolved();
|
|
9286
|
+
}
|
|
9287
|
+
requireInstances() {
|
|
9288
|
+
if (this.instances === null) throw new Error("provider-homeassistant: instances not ready");
|
|
9289
|
+
return this.instances;
|
|
9290
|
+
}
|
|
9291
|
+
/**
|
|
9292
|
+
* After an instance changed (created, edited, switched, deleted) the runtime follows the record:
|
|
9293
|
+
* a missing record removes the manager and its adopted devices, an off or unfilled one stops the
|
|
9294
|
+
* manager but keeps the devices, a filled one starts or re-authenticates it.
|
|
9295
|
+
*/
|
|
9296
|
+
async syncBrokerRuntime(id) {
|
|
9297
|
+
const reg = this.requireRegistry();
|
|
9298
|
+
const entry = await this.requireBrokerStore().getById(id);
|
|
9299
|
+
this.activeBrokers = await this.loadPersistedBrokers();
|
|
9300
|
+
if (entry === null) {
|
|
9301
|
+
await reg.removeEntry(id);
|
|
9302
|
+
const deviceRegistry = this.ctx.kernel.deviceRegistry;
|
|
9303
|
+
const devices = this.ctx.kernel.devices;
|
|
9304
|
+
if (deviceRegistry && devices) await cascadeRemoveDevicesForBroker({
|
|
9305
|
+
reg: deviceRegistry,
|
|
9306
|
+
devices,
|
|
9307
|
+
addonId: this.ctx.id,
|
|
9308
|
+
brokerId: id,
|
|
9309
|
+
logger: this.ctx.logger
|
|
9310
|
+
});
|
|
9311
|
+
return;
|
|
9312
|
+
}
|
|
9313
|
+
const settings = entry.enabled === false ? null : entryToSettings(entry);
|
|
9314
|
+
if (settings === null) {
|
|
9315
|
+
await reg.removeEntry(id);
|
|
9316
|
+
return;
|
|
8932
9317
|
}
|
|
8933
|
-
|
|
9318
|
+
try {
|
|
9319
|
+
if (reg.get(id) !== null) await reg.updateEntry(id, settings);
|
|
9320
|
+
else await reg.createEntry(entry.name, settings, {
|
|
9321
|
+
id,
|
|
9322
|
+
...entry.integrationId !== void 0 ? { integrationId: entry.integrationId } : {}
|
|
9323
|
+
});
|
|
9324
|
+
} catch (err) {
|
|
9325
|
+
this.ctx.logger.warn("ha instance: could not start the broker", { meta: {
|
|
9326
|
+
brokerId: id,
|
|
9327
|
+
error: errMsg(err)
|
|
9328
|
+
} });
|
|
9329
|
+
}
|
|
9330
|
+
}
|
|
9331
|
+
requireBrokerStore() {
|
|
9332
|
+
if (this.brokerStore === null) throw new Error("provider-homeassistant: broker store not ready");
|
|
9333
|
+
return this.brokerStore;
|
|
8934
9334
|
}
|
|
8935
9335
|
async persistBrokerEntries(entries) {
|
|
8936
9336
|
this.activeBrokers = [...entries];
|
|
8937
|
-
|
|
8938
|
-
if (store !== null) {
|
|
8939
|
-
await store.replaceAll(entries);
|
|
8940
|
-
return;
|
|
8941
|
-
}
|
|
8942
|
-
await this.updateGlobalSettings({ brokers: entries });
|
|
9337
|
+
await this.requireBrokerStore().replaceAll(entries);
|
|
8943
9338
|
}
|
|
8944
9339
|
buildBrokerProvider() {
|
|
8945
9340
|
const ownerAddonId = this.ctx.id;
|
|
@@ -9013,7 +9408,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
9013
9408
|
getSettings: async ({ id }) => {
|
|
9014
9409
|
const entry = this.activeBrokers.find((b) => b.id === id);
|
|
9015
9410
|
if (!entry) return null;
|
|
9016
|
-
return
|
|
9411
|
+
return entryToSettings(entry);
|
|
9017
9412
|
},
|
|
9018
9413
|
setSettings: async ({ id, settings }) => {
|
|
9019
9414
|
if (!owns(id)) return;
|
|
@@ -9024,7 +9419,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
9024
9419
|
getBrokerConfig: async ({ id }) => {
|
|
9025
9420
|
const entry = this.activeBrokers.find((b) => b.id === id);
|
|
9026
9421
|
if (!entry) return null;
|
|
9027
|
-
return
|
|
9422
|
+
return entryToSettings(entry);
|
|
9028
9423
|
},
|
|
9029
9424
|
getSettingsSchema: async ({ kind }) => {
|
|
9030
9425
|
if (kind !== HA_KIND) return null;
|