@camstack/addon-provider-homeassistant 1.2.651 → 1.2.653
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 +13 -0
- package/dist/addon.js +199 -1
- package/dist/addon.mjs +199 -1
- package/dist/{ha-broker-source-CCmZ9I6P.mjs → ha-broker-source-CKdH9XHn.mjs} +322 -132
- package/dist/{ha-broker-source-D4_TtF2V.js → ha-broker-source-DnSL-mbs.js} +327 -131
- package/dist/ha-export/ha-export.addon.js +6 -2
- package/dist/ha-export/ha-export.addon.mjs +6 -2
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.653] - 2026-11-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Rebuilt against updated dependencies.
|
|
15
|
+
|
|
16
|
+
## [1.2.652] - 2026-11-02
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Home Assistant can hand an ESPHome board's encryption key from its ESPHome Builder add-on to CamStack, over the existing access token (administrator only).
|
|
21
|
+
- pump-cycle is classified as not exported to Home Assistant
|
|
22
|
+
|
|
10
23
|
## [1.2.651] - 2026-11-02
|
|
11
24
|
|
|
12
25
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_ha_broker_source = require("./ha-broker-source-
|
|
2
|
+
const require_ha_broker_source = require("./ha-broker-source-DnSL-mbs.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
|
|
@@ -5500,6 +5500,10 @@ var HaIntegrationManager = class HaIntegrationManager {
|
|
|
5500
5500
|
this.connection = null;
|
|
5501
5501
|
this.setStatus("disconnected");
|
|
5502
5502
|
}
|
|
5503
|
+
/** The address HA answers on; `null` for a supervisor-token broker, which has none of its own. */
|
|
5504
|
+
getBaseUrl() {
|
|
5505
|
+
return this.opts.auth.kind === "long-lived" ? this.opts.auth.baseUrl : null;
|
|
5506
|
+
}
|
|
5503
5507
|
getInfo() {
|
|
5504
5508
|
return {
|
|
5505
5509
|
id: this.opts.id,
|
|
@@ -7773,6 +7777,179 @@ var HaBrokerStore = class HaBrokerStore {
|
|
|
7773
7777
|
}
|
|
7774
7778
|
};
|
|
7775
7779
|
//#endregion
|
|
7780
|
+
//#region src/ha-board-key-source.ts
|
|
7781
|
+
/**
|
|
7782
|
+
* `board-key-source` for Home Assistant: reads an ESPHome board's API
|
|
7783
|
+
* encryption key from the ESPHome Builder add-on that runs inside HA.
|
|
7784
|
+
*
|
|
7785
|
+
* The path reuses what the broker already holds, the long-lived token on the
|
|
7786
|
+
* WebSocket, and opens no port and asks for no second credential:
|
|
7787
|
+
*
|
|
7788
|
+
* 1. `supervisor/api GET /discovery` -> the add-on that announced `esphome`
|
|
7789
|
+
* 2. `supervisor/api GET /addons/<slug>/info` -> its `ingress_url`
|
|
7790
|
+
* 3. `supervisor/api POST /ingress/session` (admin only) -> a session
|
|
7791
|
+
* 4. `GET <ha><ingress_url>devices`, cookie `ingress_session` -> the boards
|
|
7792
|
+
* 5. `GET <ha><ingress_url>json-config?configuration=<x>.yaml` -> the key
|
|
7793
|
+
*
|
|
7794
|
+
* **What crosses.** The builder's `json-config` body carries EVERY value the
|
|
7795
|
+
* YAML secrets file resolves, the Wi-Fi passphrase included. It is parsed in
|
|
7796
|
+
* memory straight into a schema that names only `api.encryption.key`; Zod
|
|
7797
|
+
* drops every other field at the parse, so the rest never reaches a variable,
|
|
7798
|
+
* a return value or a log line. No body is ever logged, only a reason.
|
|
7799
|
+
*/
|
|
7800
|
+
var INGRESS_PREFIX = "/api/hassio_ingress/";
|
|
7801
|
+
var ESPHOME_SERVICE = "esphome";
|
|
7802
|
+
var DiscoverySchema = require_ha_broker_source.object({ discovery: require_ha_broker_source.array(require_ha_broker_source.object({
|
|
7803
|
+
addon: require_ha_broker_source.string(),
|
|
7804
|
+
service: require_ha_broker_source.string()
|
|
7805
|
+
})) });
|
|
7806
|
+
var AddonInfoSchema = require_ha_broker_source.object({ ingress_url: require_ha_broker_source.string().nullable().optional() });
|
|
7807
|
+
var SessionSchema = require_ha_broker_source.object({ session: require_ha_broker_source.string().min(1) });
|
|
7808
|
+
var DevicesSchema = require_ha_broker_source.object({ configured: require_ha_broker_source.array(require_ha_broker_source.object({
|
|
7809
|
+
name: require_ha_broker_source.string(),
|
|
7810
|
+
configuration: require_ha_broker_source.string(),
|
|
7811
|
+
address: require_ha_broker_source.string().nullable().optional()
|
|
7812
|
+
})) });
|
|
7813
|
+
/** The ONLY part of `json-config` that is read; every other field is dropped by the parse. */
|
|
7814
|
+
var KeyConfigSchema = require_ha_broker_source.object({ api: require_ha_broker_source.object({ encryption: require_ha_broker_source.object({ key: require_ha_broker_source.string().min(1) }).optional() }).optional() });
|
|
7815
|
+
var ADMIN_CODES = new Set([
|
|
7816
|
+
"unauthorized",
|
|
7817
|
+
"forbidden",
|
|
7818
|
+
"not_admin"
|
|
7819
|
+
]);
|
|
7820
|
+
var SourceFailure = class extends Error {
|
|
7821
|
+
reason;
|
|
7822
|
+
detail;
|
|
7823
|
+
constructor(reason, detail) {
|
|
7824
|
+
super(reason);
|
|
7825
|
+
this.reason = reason;
|
|
7826
|
+
this.detail = detail;
|
|
7827
|
+
}
|
|
7828
|
+
};
|
|
7829
|
+
function errorCode(err) {
|
|
7830
|
+
if (typeof err !== "object" || err === null) return null;
|
|
7831
|
+
const code = Reflect.get(err, "code");
|
|
7832
|
+
return typeof code === "string" ? code : null;
|
|
7833
|
+
}
|
|
7834
|
+
var HaBoardKeySource = class {
|
|
7835
|
+
deps;
|
|
7836
|
+
constructor(deps) {
|
|
7837
|
+
this.deps = deps;
|
|
7838
|
+
}
|
|
7839
|
+
listSources() {
|
|
7840
|
+
return this.deps.brokers.list().map((b) => ({
|
|
7841
|
+
sourceId: b.id,
|
|
7842
|
+
name: b.name
|
|
7843
|
+
}));
|
|
7844
|
+
}
|
|
7845
|
+
async listBoards(sourceId) {
|
|
7846
|
+
try {
|
|
7847
|
+
const access = await this.openIngress(sourceId);
|
|
7848
|
+
const raw = await this.getJson(access, "devices");
|
|
7849
|
+
const parsed = DevicesSchema.safeParse(raw);
|
|
7850
|
+
if (!parsed.success) throw new SourceFailure("unreadable-config", "unexpected devices reply");
|
|
7851
|
+
return {
|
|
7852
|
+
ok: true,
|
|
7853
|
+
boards: parsed.data.configured.map((d) => ({
|
|
7854
|
+
configuration: d.configuration,
|
|
7855
|
+
name: d.name,
|
|
7856
|
+
address: d.address ?? null
|
|
7857
|
+
}))
|
|
7858
|
+
};
|
|
7859
|
+
} catch (err) {
|
|
7860
|
+
return this.fail(sourceId, "listBoards", err);
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
async getBoardKey(sourceId, configuration) {
|
|
7864
|
+
try {
|
|
7865
|
+
const access = await this.openIngress(sourceId);
|
|
7866
|
+
const raw = await this.getJson(access, `json-config?configuration=${encodeURIComponent(configuration)}`, configuration);
|
|
7867
|
+
const parsed = KeyConfigSchema.safeParse(raw);
|
|
7868
|
+
if (!parsed.success) throw new SourceFailure("unreadable-config");
|
|
7869
|
+
const key = parsed.data.api?.encryption?.key;
|
|
7870
|
+
if (key === void 0) throw new SourceFailure("no-encryption-key");
|
|
7871
|
+
return {
|
|
7872
|
+
ok: true,
|
|
7873
|
+
key
|
|
7874
|
+
};
|
|
7875
|
+
} catch (err) {
|
|
7876
|
+
return this.fail(sourceId, "getBoardKey", err);
|
|
7877
|
+
}
|
|
7878
|
+
}
|
|
7879
|
+
fail(sourceId, op, err) {
|
|
7880
|
+
const failure = err instanceof SourceFailure ? err : new SourceFailure("unreachable", require_ha_broker_source.errMsg(err));
|
|
7881
|
+
this.deps.logger.warn(`board-key-source ${op} refused`, { meta: {
|
|
7882
|
+
sourceId,
|
|
7883
|
+
reason: failure.reason
|
|
7884
|
+
} });
|
|
7885
|
+
return failure.detail === void 0 ? {
|
|
7886
|
+
ok: false,
|
|
7887
|
+
reason: failure.reason
|
|
7888
|
+
} : {
|
|
7889
|
+
ok: false,
|
|
7890
|
+
reason: failure.reason,
|
|
7891
|
+
detail: failure.detail
|
|
7892
|
+
};
|
|
7893
|
+
}
|
|
7894
|
+
async supervisor(broker, endpoint, method) {
|
|
7895
|
+
try {
|
|
7896
|
+
return await broker.send({
|
|
7897
|
+
type: "supervisor/api",
|
|
7898
|
+
endpoint,
|
|
7899
|
+
method
|
|
7900
|
+
});
|
|
7901
|
+
} catch (err) {
|
|
7902
|
+
const code = errorCode(err);
|
|
7903
|
+
if (code !== null && ADMIN_CODES.has(code)) throw new SourceFailure("not-admin", "the Home Assistant token is not an administrator");
|
|
7904
|
+
throw new SourceFailure("unreachable", require_ha_broker_source.errMsg(err));
|
|
7905
|
+
}
|
|
7906
|
+
}
|
|
7907
|
+
async openIngress(sourceId) {
|
|
7908
|
+
const broker = this.deps.brokers.list().find((b) => b.id === sourceId);
|
|
7909
|
+
if (broker === void 0) throw new SourceFailure("unknown-source");
|
|
7910
|
+
if (broker.baseUrl === null) throw new SourceFailure("no-ingress-url", "this broker has no address of its own");
|
|
7911
|
+
const discovery = DiscoverySchema.safeParse(await this.supervisor(broker, "/discovery", "get"));
|
|
7912
|
+
const slug = discovery.success ? discovery.data.discovery.find((d) => d.service === ESPHOME_SERVICE)?.addon : void 0;
|
|
7913
|
+
if (slug === void 0) throw new SourceFailure("no-builder");
|
|
7914
|
+
const info = AddonInfoSchema.safeParse(await this.supervisor(broker, `/addons/${encodeURIComponent(slug)}/info`, "get"));
|
|
7915
|
+
const ingressUrl = info.success ? info.data.ingress_url : void 0;
|
|
7916
|
+
if (typeof ingressUrl !== "string" || !ingressUrl.startsWith(INGRESS_PREFIX)) throw new SourceFailure("no-ingress-url");
|
|
7917
|
+
const session = SessionSchema.safeParse(await this.supervisor(broker, "/ingress/session", "post"));
|
|
7918
|
+
if (!session.success) throw new SourceFailure("not-admin", "no ingress session was granted");
|
|
7919
|
+
return {
|
|
7920
|
+
root: `${broker.baseUrl.replace(/\/+$/, "")}${ingressUrl.endsWith("/") ? ingressUrl : `${ingressUrl}/`}`,
|
|
7921
|
+
cookie: `ingress_session=${session.data.session}`
|
|
7922
|
+
};
|
|
7923
|
+
}
|
|
7924
|
+
async getJson(access, relative, configuration) {
|
|
7925
|
+
let response;
|
|
7926
|
+
try {
|
|
7927
|
+
response = await this.deps.http(`${access.root}${relative}`, { headers: {
|
|
7928
|
+
Cookie: access.cookie,
|
|
7929
|
+
Accept: "application/json"
|
|
7930
|
+
} });
|
|
7931
|
+
} catch (err) {
|
|
7932
|
+
throw new SourceFailure("unreachable", require_ha_broker_source.errMsg(err));
|
|
7933
|
+
}
|
|
7934
|
+
if (response.status === 404 && configuration !== void 0) throw new SourceFailure("unknown-board", configuration);
|
|
7935
|
+
if (response.status === 401 || response.status === 403) throw new SourceFailure("not-admin", `the builder answered ${response.status}`);
|
|
7936
|
+
if (!response.ok) throw new SourceFailure("unreachable", `the builder answered ${response.status}`);
|
|
7937
|
+
try {
|
|
7938
|
+
return await response.json();
|
|
7939
|
+
} catch {
|
|
7940
|
+
throw new SourceFailure("unreadable-config");
|
|
7941
|
+
}
|
|
7942
|
+
}
|
|
7943
|
+
};
|
|
7944
|
+
/** The cap provider over a {@link HaBoardKeySource}. */
|
|
7945
|
+
function createHaBoardKeyProvider(source) {
|
|
7946
|
+
return {
|
|
7947
|
+
listSources: async () => source.listSources(),
|
|
7948
|
+
listBoards: (input) => source.listBoards(input.sourceId),
|
|
7949
|
+
getBoardKey: (input) => source.getBoardKey(input.sourceId, input.configuration)
|
|
7950
|
+
};
|
|
7951
|
+
}
|
|
7952
|
+
//#endregion
|
|
7776
7953
|
//#region src/ha-calendar-source.ts
|
|
7777
7954
|
function isCalendarEntity(entityId) {
|
|
7778
7955
|
return entityId.startsWith("calendar.");
|
|
@@ -8488,6 +8665,10 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_broker_source.Bas
|
|
|
8488
8665
|
capability: require_ha_broker_source.addonRoutesCapability,
|
|
8489
8666
|
provider: createHaIconRouteProvider()
|
|
8490
8667
|
},
|
|
8668
|
+
{
|
|
8669
|
+
capability: require_ha_broker_source.boardKeySourceCapability,
|
|
8670
|
+
provider: this.buildBoardKeyProvider()
|
|
8671
|
+
},
|
|
8491
8672
|
{
|
|
8492
8673
|
capability: require_ha_broker_source.calendarSourceCapability,
|
|
8493
8674
|
provider: createHaCalendarSource({
|
|
@@ -8498,6 +8679,23 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_broker_source.Bas
|
|
|
8498
8679
|
}
|
|
8499
8680
|
];
|
|
8500
8681
|
}
|
|
8682
|
+
/** `board-key-source`: reads an ESPHome board's key from the builder beside HA, over each broker's own token. */
|
|
8683
|
+
buildBoardKeyProvider() {
|
|
8684
|
+
const registry = this.requireRegistry();
|
|
8685
|
+
return createHaBoardKeyProvider(new HaBoardKeySource({
|
|
8686
|
+
brokers: { list: () => registry.listManagers().map(({ brokerId, manager }) => ({
|
|
8687
|
+
id: brokerId,
|
|
8688
|
+
name: manager.getInfo().name,
|
|
8689
|
+
baseUrl: manager.getBaseUrl(),
|
|
8690
|
+
send: (message) => manager.sendMessage(message)
|
|
8691
|
+
})) },
|
|
8692
|
+
http: (url, init) => fetch(url, {
|
|
8693
|
+
headers: init.headers,
|
|
8694
|
+
signal: AbortSignal.timeout(15e3)
|
|
8695
|
+
}),
|
|
8696
|
+
logger: this.ctx.logger.child("board-key-source")
|
|
8697
|
+
}));
|
|
8698
|
+
}
|
|
8501
8699
|
async buildNotificationOutputProvider() {
|
|
8502
8700
|
const records = this.ctx.records;
|
|
8503
8701
|
if (records === void 0) throw new Error("ha notification-output: ctx.records is required (hub-only addon)");
|
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as humidifierCapability, $t as boolean, A as brightnessCapability, At as switchCapability, Bt as weatherCapability, C as alarmPanelCapability, Ct as runningFromLawnMower, D as bestLocationMatch, Dt as scriptRunnerCapability, E as batteryCapability, Et as runningStateCapability, F as calendarSourceCapability, Ft as updateCapability, G as deviceAdminLinkCapability, H as contactCapability, Ht as RecordSecretUnavailableError, I as carbonMonoxideCapability, It as vacuumControlCapability, J as enumSensorCapability, Jt as entryBody, K as deviceAdoptionCapability, Kt as defineEntryKind, L as climateControlCapability, Lt as valveCapability, M as buildAddonRouteProvider, Mt as temperatureSensorCapability, N as buildDeviceAdminUrl, Nt as tryConvertDelta, O as binaryCapability, P as buttonCapability, Pt as tryConvertUnit, Q as gasCapability, Qt as array, R as colorCapability, Rt as vibrationCapability, S as airQualitySensorCapability, T as automationControlCapability, Tt as runningFromVacuumState, U as controlCapability, Ut as baseUnitOf, W as coverCapability, Wt as createEvent, X as fanControlCapability, Y as eventEmitterCapability, Z as floodCapability, Zt as _enum, _t as pressureSensorCapability, a as BaseDevice, an as string, at as mediaPlayerCapability, b as accessoriesCapability, bt as quantityFromUnit, cn as url, ct as notificationOutputCapability, dn as DeviceType, en as discriminatedUnion, et as humiditySensorCapability, fn as errMsg, ft as parseAdminBase, gt as presenceCapability, ht as prepareNotification, in as record, it as lockControlCapability, j as brokerCapability, jt as tamperCapability, k as boardKeySourceCapability, kt as smokeCapability, ln as DeviceFeature, lt as notifierCapability, nn as number, nt as ingestReading, o as BaseDeviceProvider, on as union, ot as motionCapability, p as EnumSensorDateTimeFormatSchema, pn as EventCategory, pt as powerMeterCapability, qt as defineRecordKinds, rn as object, rt as lawnMowerControlCapability, sn as unknown, st as normalizeUnit, t as HA_BROKER_KIND, tn as literal, tt as imageCapability, un as DeviceRole, ut as numericSensorCapability, v as RUNNING_STATE_COLD, w as ambientLightSensorCapability, wt as runningFromMediaPlayer, x as addonRoutesCapability, xt as resolveAttachmentUrls, y as TargetSchema, yt as publicEndpointReaderFrom, z as connectivityCapability, zt as waterHeaterCapability } from "./ha-broker-source-CKdH9XHn.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
|
|
@@ -5499,6 +5499,10 @@ var HaIntegrationManager = class HaIntegrationManager {
|
|
|
5499
5499
|
this.connection = null;
|
|
5500
5500
|
this.setStatus("disconnected");
|
|
5501
5501
|
}
|
|
5502
|
+
/** The address HA answers on; `null` for a supervisor-token broker, which has none of its own. */
|
|
5503
|
+
getBaseUrl() {
|
|
5504
|
+
return this.opts.auth.kind === "long-lived" ? this.opts.auth.baseUrl : null;
|
|
5505
|
+
}
|
|
5502
5506
|
getInfo() {
|
|
5503
5507
|
return {
|
|
5504
5508
|
id: this.opts.id,
|
|
@@ -7755,6 +7759,179 @@ var HaBrokerStore = class HaBrokerStore {
|
|
|
7755
7759
|
}
|
|
7756
7760
|
};
|
|
7757
7761
|
//#endregion
|
|
7762
|
+
//#region src/ha-board-key-source.ts
|
|
7763
|
+
/**
|
|
7764
|
+
* `board-key-source` for Home Assistant: reads an ESPHome board's API
|
|
7765
|
+
* encryption key from the ESPHome Builder add-on that runs inside HA.
|
|
7766
|
+
*
|
|
7767
|
+
* The path reuses what the broker already holds, the long-lived token on the
|
|
7768
|
+
* WebSocket, and opens no port and asks for no second credential:
|
|
7769
|
+
*
|
|
7770
|
+
* 1. `supervisor/api GET /discovery` -> the add-on that announced `esphome`
|
|
7771
|
+
* 2. `supervisor/api GET /addons/<slug>/info` -> its `ingress_url`
|
|
7772
|
+
* 3. `supervisor/api POST /ingress/session` (admin only) -> a session
|
|
7773
|
+
* 4. `GET <ha><ingress_url>devices`, cookie `ingress_session` -> the boards
|
|
7774
|
+
* 5. `GET <ha><ingress_url>json-config?configuration=<x>.yaml` -> the key
|
|
7775
|
+
*
|
|
7776
|
+
* **What crosses.** The builder's `json-config` body carries EVERY value the
|
|
7777
|
+
* YAML secrets file resolves, the Wi-Fi passphrase included. It is parsed in
|
|
7778
|
+
* memory straight into a schema that names only `api.encryption.key`; Zod
|
|
7779
|
+
* drops every other field at the parse, so the rest never reaches a variable,
|
|
7780
|
+
* a return value or a log line. No body is ever logged, only a reason.
|
|
7781
|
+
*/
|
|
7782
|
+
var INGRESS_PREFIX = "/api/hassio_ingress/";
|
|
7783
|
+
var ESPHOME_SERVICE = "esphome";
|
|
7784
|
+
var DiscoverySchema = object({ discovery: array(object({
|
|
7785
|
+
addon: string(),
|
|
7786
|
+
service: string()
|
|
7787
|
+
})) });
|
|
7788
|
+
var AddonInfoSchema = object({ ingress_url: string().nullable().optional() });
|
|
7789
|
+
var SessionSchema = object({ session: string().min(1) });
|
|
7790
|
+
var DevicesSchema = object({ configured: array(object({
|
|
7791
|
+
name: string(),
|
|
7792
|
+
configuration: string(),
|
|
7793
|
+
address: string().nullable().optional()
|
|
7794
|
+
})) });
|
|
7795
|
+
/** The ONLY part of `json-config` that is read; every other field is dropped by the parse. */
|
|
7796
|
+
var KeyConfigSchema = object({ api: object({ encryption: object({ key: string().min(1) }).optional() }).optional() });
|
|
7797
|
+
var ADMIN_CODES = new Set([
|
|
7798
|
+
"unauthorized",
|
|
7799
|
+
"forbidden",
|
|
7800
|
+
"not_admin"
|
|
7801
|
+
]);
|
|
7802
|
+
var SourceFailure = class extends Error {
|
|
7803
|
+
reason;
|
|
7804
|
+
detail;
|
|
7805
|
+
constructor(reason, detail) {
|
|
7806
|
+
super(reason);
|
|
7807
|
+
this.reason = reason;
|
|
7808
|
+
this.detail = detail;
|
|
7809
|
+
}
|
|
7810
|
+
};
|
|
7811
|
+
function errorCode(err) {
|
|
7812
|
+
if (typeof err !== "object" || err === null) return null;
|
|
7813
|
+
const code = Reflect.get(err, "code");
|
|
7814
|
+
return typeof code === "string" ? code : null;
|
|
7815
|
+
}
|
|
7816
|
+
var HaBoardKeySource = class {
|
|
7817
|
+
deps;
|
|
7818
|
+
constructor(deps) {
|
|
7819
|
+
this.deps = deps;
|
|
7820
|
+
}
|
|
7821
|
+
listSources() {
|
|
7822
|
+
return this.deps.brokers.list().map((b) => ({
|
|
7823
|
+
sourceId: b.id,
|
|
7824
|
+
name: b.name
|
|
7825
|
+
}));
|
|
7826
|
+
}
|
|
7827
|
+
async listBoards(sourceId) {
|
|
7828
|
+
try {
|
|
7829
|
+
const access = await this.openIngress(sourceId);
|
|
7830
|
+
const raw = await this.getJson(access, "devices");
|
|
7831
|
+
const parsed = DevicesSchema.safeParse(raw);
|
|
7832
|
+
if (!parsed.success) throw new SourceFailure("unreadable-config", "unexpected devices reply");
|
|
7833
|
+
return {
|
|
7834
|
+
ok: true,
|
|
7835
|
+
boards: parsed.data.configured.map((d) => ({
|
|
7836
|
+
configuration: d.configuration,
|
|
7837
|
+
name: d.name,
|
|
7838
|
+
address: d.address ?? null
|
|
7839
|
+
}))
|
|
7840
|
+
};
|
|
7841
|
+
} catch (err) {
|
|
7842
|
+
return this.fail(sourceId, "listBoards", err);
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
7845
|
+
async getBoardKey(sourceId, configuration) {
|
|
7846
|
+
try {
|
|
7847
|
+
const access = await this.openIngress(sourceId);
|
|
7848
|
+
const raw = await this.getJson(access, `json-config?configuration=${encodeURIComponent(configuration)}`, configuration);
|
|
7849
|
+
const parsed = KeyConfigSchema.safeParse(raw);
|
|
7850
|
+
if (!parsed.success) throw new SourceFailure("unreadable-config");
|
|
7851
|
+
const key = parsed.data.api?.encryption?.key;
|
|
7852
|
+
if (key === void 0) throw new SourceFailure("no-encryption-key");
|
|
7853
|
+
return {
|
|
7854
|
+
ok: true,
|
|
7855
|
+
key
|
|
7856
|
+
};
|
|
7857
|
+
} catch (err) {
|
|
7858
|
+
return this.fail(sourceId, "getBoardKey", err);
|
|
7859
|
+
}
|
|
7860
|
+
}
|
|
7861
|
+
fail(sourceId, op, err) {
|
|
7862
|
+
const failure = err instanceof SourceFailure ? err : new SourceFailure("unreachable", errMsg(err));
|
|
7863
|
+
this.deps.logger.warn(`board-key-source ${op} refused`, { meta: {
|
|
7864
|
+
sourceId,
|
|
7865
|
+
reason: failure.reason
|
|
7866
|
+
} });
|
|
7867
|
+
return failure.detail === void 0 ? {
|
|
7868
|
+
ok: false,
|
|
7869
|
+
reason: failure.reason
|
|
7870
|
+
} : {
|
|
7871
|
+
ok: false,
|
|
7872
|
+
reason: failure.reason,
|
|
7873
|
+
detail: failure.detail
|
|
7874
|
+
};
|
|
7875
|
+
}
|
|
7876
|
+
async supervisor(broker, endpoint, method) {
|
|
7877
|
+
try {
|
|
7878
|
+
return await broker.send({
|
|
7879
|
+
type: "supervisor/api",
|
|
7880
|
+
endpoint,
|
|
7881
|
+
method
|
|
7882
|
+
});
|
|
7883
|
+
} catch (err) {
|
|
7884
|
+
const code = errorCode(err);
|
|
7885
|
+
if (code !== null && ADMIN_CODES.has(code)) throw new SourceFailure("not-admin", "the Home Assistant token is not an administrator");
|
|
7886
|
+
throw new SourceFailure("unreachable", errMsg(err));
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
async openIngress(sourceId) {
|
|
7890
|
+
const broker = this.deps.brokers.list().find((b) => b.id === sourceId);
|
|
7891
|
+
if (broker === void 0) throw new SourceFailure("unknown-source");
|
|
7892
|
+
if (broker.baseUrl === null) throw new SourceFailure("no-ingress-url", "this broker has no address of its own");
|
|
7893
|
+
const discovery = DiscoverySchema.safeParse(await this.supervisor(broker, "/discovery", "get"));
|
|
7894
|
+
const slug = discovery.success ? discovery.data.discovery.find((d) => d.service === ESPHOME_SERVICE)?.addon : void 0;
|
|
7895
|
+
if (slug === void 0) throw new SourceFailure("no-builder");
|
|
7896
|
+
const info = AddonInfoSchema.safeParse(await this.supervisor(broker, `/addons/${encodeURIComponent(slug)}/info`, "get"));
|
|
7897
|
+
const ingressUrl = info.success ? info.data.ingress_url : void 0;
|
|
7898
|
+
if (typeof ingressUrl !== "string" || !ingressUrl.startsWith(INGRESS_PREFIX)) throw new SourceFailure("no-ingress-url");
|
|
7899
|
+
const session = SessionSchema.safeParse(await this.supervisor(broker, "/ingress/session", "post"));
|
|
7900
|
+
if (!session.success) throw new SourceFailure("not-admin", "no ingress session was granted");
|
|
7901
|
+
return {
|
|
7902
|
+
root: `${broker.baseUrl.replace(/\/+$/, "")}${ingressUrl.endsWith("/") ? ingressUrl : `${ingressUrl}/`}`,
|
|
7903
|
+
cookie: `ingress_session=${session.data.session}`
|
|
7904
|
+
};
|
|
7905
|
+
}
|
|
7906
|
+
async getJson(access, relative, configuration) {
|
|
7907
|
+
let response;
|
|
7908
|
+
try {
|
|
7909
|
+
response = await this.deps.http(`${access.root}${relative}`, { headers: {
|
|
7910
|
+
Cookie: access.cookie,
|
|
7911
|
+
Accept: "application/json"
|
|
7912
|
+
} });
|
|
7913
|
+
} catch (err) {
|
|
7914
|
+
throw new SourceFailure("unreachable", errMsg(err));
|
|
7915
|
+
}
|
|
7916
|
+
if (response.status === 404 && configuration !== void 0) throw new SourceFailure("unknown-board", configuration);
|
|
7917
|
+
if (response.status === 401 || response.status === 403) throw new SourceFailure("not-admin", `the builder answered ${response.status}`);
|
|
7918
|
+
if (!response.ok) throw new SourceFailure("unreachable", `the builder answered ${response.status}`);
|
|
7919
|
+
try {
|
|
7920
|
+
return await response.json();
|
|
7921
|
+
} catch {
|
|
7922
|
+
throw new SourceFailure("unreadable-config");
|
|
7923
|
+
}
|
|
7924
|
+
}
|
|
7925
|
+
};
|
|
7926
|
+
/** The cap provider over a {@link HaBoardKeySource}. */
|
|
7927
|
+
function createHaBoardKeyProvider(source) {
|
|
7928
|
+
return {
|
|
7929
|
+
listSources: async () => source.listSources(),
|
|
7930
|
+
listBoards: (input) => source.listBoards(input.sourceId),
|
|
7931
|
+
getBoardKey: (input) => source.getBoardKey(input.sourceId, input.configuration)
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7934
|
+
//#endregion
|
|
7758
7935
|
//#region src/ha-calendar-source.ts
|
|
7759
7936
|
function isCalendarEntity(entityId) {
|
|
7760
7937
|
return entityId.startsWith("calendar.");
|
|
@@ -8470,6 +8647,10 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8470
8647
|
capability: addonRoutesCapability,
|
|
8471
8648
|
provider: createHaIconRouteProvider()
|
|
8472
8649
|
},
|
|
8650
|
+
{
|
|
8651
|
+
capability: boardKeySourceCapability,
|
|
8652
|
+
provider: this.buildBoardKeyProvider()
|
|
8653
|
+
},
|
|
8473
8654
|
{
|
|
8474
8655
|
capability: calendarSourceCapability,
|
|
8475
8656
|
provider: createHaCalendarSource({
|
|
@@ -8480,6 +8661,23 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8480
8661
|
}
|
|
8481
8662
|
];
|
|
8482
8663
|
}
|
|
8664
|
+
/** `board-key-source`: reads an ESPHome board's key from the builder beside HA, over each broker's own token. */
|
|
8665
|
+
buildBoardKeyProvider() {
|
|
8666
|
+
const registry = this.requireRegistry();
|
|
8667
|
+
return createHaBoardKeyProvider(new HaBoardKeySource({
|
|
8668
|
+
brokers: { list: () => registry.listManagers().map(({ brokerId, manager }) => ({
|
|
8669
|
+
id: brokerId,
|
|
8670
|
+
name: manager.getInfo().name,
|
|
8671
|
+
baseUrl: manager.getBaseUrl(),
|
|
8672
|
+
send: (message) => manager.sendMessage(message)
|
|
8673
|
+
})) },
|
|
8674
|
+
http: (url, init) => fetch(url, {
|
|
8675
|
+
headers: init.headers,
|
|
8676
|
+
signal: AbortSignal.timeout(15e3)
|
|
8677
|
+
}),
|
|
8678
|
+
logger: this.ctx.logger.child("board-key-source")
|
|
8679
|
+
}));
|
|
8680
|
+
}
|
|
8483
8681
|
async buildNotificationOutputProvider() {
|
|
8484
8682
|
const records = this.ctx.records;
|
|
8485
8683
|
if (records === void 0) throw new Error("ha notification-output: ctx.records is required (hub-only addon)");
|