@camstack/addon-provider-homeassistant 1.2.18 → 1.2.19
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/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/{dist-UfT_fD3-.mjs → dist-BSQpoIVg.mjs} +73 -16
- package/dist/{dist-DuKJzmrJ.js → dist-Bz5ENMVc.js} +73 -16
- package/dist/ha-export/ha-export.addon.js +117 -16
- package/dist/ha-export/ha-export.addon.mjs +117 -16
- package/package.json +2 -3
package/dist/addon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_dist = require("./dist-
|
|
2
|
+
const require_dist = require("./dist-Bz5ENMVc.js");
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
let node_zlib = require("node:zlib");
|
|
5
5
|
//#region src/ha-device-source.ts
|
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as tamperCapability, A as eventEmitterCapability, B as motionCapability, C as connectivityCapability, Ct as url, D as deviceAdoptionCapability, E as coverCapability, F as humiditySensorCapability, H as notificationOutputCapability, I as imageCapability, J as presenceCapability, K as powerMeterCapability, L as lawnMowerControlCapability, M as floodCapability, N as gasCapability, P as humidifierCapability, Q as switchCapability, R as lockControlCapability, S as colorCapability, St as unknown, T as controlCapability, U as notifierCapability, V as normalizeUnit, W as numericSensorCapability, X as scriptRunnerCapability, Y as pressureSensorCapability, Z as smokeCapability, _ as brokerCapability, _t as number, a as EnumSensorDateTimeFormatSchema, at as waterHeaterCapability, b as carbonMonoxideCapability, bt as string, c as addonRoutesCapability, d as ambientLightSensorCapability, dt as DeviceType, et as temperatureSensorCapability, f as automationControlCapability, ft as createEvent, g as brightnessCapability, gt as literal, h as binaryCapability, ht as discriminatedUnion, it as vibrationCapability, j as fanControlCapability, k as enumSensorCapability, l as airQualitySensorCapability, lt as DeviceFeature, m as bestLocationMatch, mt as array, n as BaseDeviceProvider, nt as vacuumControlCapability, o as TargetSchema, ot as weatherCapability, p as batteryCapability, pt as _enum, q as prepareNotification, rt as valveCapability, s as accessoriesCapability, st as errMsg, t as BaseDevice, tt as updateCapability, u as alarmPanelCapability, ut as DeviceRole, v as buildAddonRouteProvider, vt as object, w as contactCapability, wt as EventCategory, x as climateControlCapability, xt as union, y as buttonCapability, yt as record, z as mediaPlayerCapability } from "./dist-
|
|
1
|
+
import { $ as tamperCapability, A as eventEmitterCapability, B as motionCapability, C as connectivityCapability, Ct as url, D as deviceAdoptionCapability, E as coverCapability, F as humiditySensorCapability, H as notificationOutputCapability, I as imageCapability, J as presenceCapability, K as powerMeterCapability, L as lawnMowerControlCapability, M as floodCapability, N as gasCapability, P as humidifierCapability, Q as switchCapability, R as lockControlCapability, S as colorCapability, St as unknown, T as controlCapability, U as notifierCapability, V as normalizeUnit, W as numericSensorCapability, X as scriptRunnerCapability, Y as pressureSensorCapability, Z as smokeCapability, _ as brokerCapability, _t as number, a as EnumSensorDateTimeFormatSchema, at as waterHeaterCapability, b as carbonMonoxideCapability, bt as string, c as addonRoutesCapability, d as ambientLightSensorCapability, dt as DeviceType, et as temperatureSensorCapability, f as automationControlCapability, ft as createEvent, g as brightnessCapability, gt as literal, h as binaryCapability, ht as discriminatedUnion, it as vibrationCapability, j as fanControlCapability, k as enumSensorCapability, l as airQualitySensorCapability, lt as DeviceFeature, m as bestLocationMatch, mt as array, n as BaseDeviceProvider, nt as vacuumControlCapability, o as TargetSchema, ot as weatherCapability, p as batteryCapability, pt as _enum, q as prepareNotification, rt as valveCapability, s as accessoriesCapability, st as errMsg, t as BaseDevice, tt as updateCapability, u as alarmPanelCapability, ut as DeviceRole, v as buildAddonRouteProvider, vt as object, w as contactCapability, wt as EventCategory, x as climateControlCapability, xt as union, y as buttonCapability, yt as record, z as mediaPlayerCapability } from "./dist-BSQpoIVg.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
|
|
4
4
|
//#region src/ha-device-source.ts
|
|
@@ -15351,21 +15351,32 @@ object({
|
|
|
15351
15351
|
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
15352
15352
|
* `/api/oauth2/integrations` are built from this collection alone.
|
|
15353
15353
|
*
|
|
15354
|
-
* **Scopes
|
|
15355
|
-
*
|
|
15356
|
-
*
|
|
15357
|
-
*
|
|
15358
|
-
*
|
|
15354
|
+
* **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
|
|
15355
|
+
* NEEDS to function.** Not a blast radius, not a conservative
|
|
15356
|
+
* under-declaration, not a description of some other path the addon happens to
|
|
15357
|
+
* have. Derive it from what the client actually calls **with this token** —
|
|
15358
|
+
* every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
|
|
15359
|
+
* addon HTTP route it posts to — and write the call that justifies each entry
|
|
15360
|
+
* next to it. Two integrations once used this field to mean two different
|
|
15361
|
+
* things; the operator ruled there is one meaning, and any third integration
|
|
15362
|
+
* inherits it (2026-08-09).
|
|
15363
|
+
*
|
|
15364
|
+
* This is not documentation, it is the ENFORCEMENT INPUT. Since
|
|
15365
|
+
* [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
|
|
15366
|
+
* checks an integration token's grant before letting it reach an
|
|
15367
|
+
* `access: 'authenticated'` route, so an **under-declaration is an integration
|
|
15368
|
+
* that stops working** — a missing `addon:` entry means `403 Token scope
|
|
15369
|
+
* mismatch` on every control the client tries to actuate. Widen the descriptor
|
|
15370
|
+
* honestly rather than weakening a check to make a route pass.
|
|
15371
|
+
*
|
|
15372
|
+
* Prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
15373
|
+
* genuinely needs a whole family; a category scope grants every future member
|
|
15359
15374
|
* of that category too. `category:system [create]` has been rejected once and
|
|
15360
15375
|
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
15361
15376
|
*
|
|
15362
|
-
*
|
|
15363
|
-
*
|
|
15364
|
-
*
|
|
15365
|
-
* the addon does the work, while the Home Assistant component calls tRPC
|
|
15366
|
-
* directly with the token. So `requestedScopes` describes the blast radius of
|
|
15367
|
-
* the GRANT, not the reach of the integration; do not widen one to describe the
|
|
15368
|
-
* other.
|
|
15377
|
+
* Calls the ADDON itself makes over `ctx.api` run as the addon and are not
|
|
15378
|
+
* scope-checked, so they are not what this field describes — but reaching the
|
|
15379
|
+
* addon's route in the first place IS, and that is the entry to declare.
|
|
15369
15380
|
*
|
|
15370
15381
|
* **The boot window.** An addon registers its provider after its runner forks
|
|
15371
15382
|
* and initialises, so between hub start and that moment this collection is
|
|
@@ -15406,7 +15417,30 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15406
15417
|
* present, /api/oauth2/authorize bakes THIS into the code instead of the
|
|
15407
15418
|
* hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
|
|
15408
15419
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15409
|
-
hubUrl: string().optional()
|
|
15420
|
+
hubUrl: string().optional(),
|
|
15421
|
+
/**
|
|
15422
|
+
* How long a REFRESH token issued for this integration lives — seconds, or
|
|
15423
|
+
* `'never'` for a token minted with no `exp` claim at all. Omit to keep the
|
|
15424
|
+
* 30-day default, which is what every link used before this field existed.
|
|
15425
|
+
*
|
|
15426
|
+
* Declared here for the same reason `requestedScopes` is: the integration
|
|
15427
|
+
* knows what it needs. Amazon's account linking and a Home Assistant config
|
|
15428
|
+
* entry are both meant to survive indefinitely, and re-linking is a manual
|
|
15429
|
+
* user action, so a 30-day expiry silently unlinks a working integration.
|
|
15430
|
+
*
|
|
15431
|
+
* **The security posture, stated so it is owned deliberately.** A refresh
|
|
15432
|
+
* token that never expires is permanent access if it leaks. What bounds it is
|
|
15433
|
+
* revocation, not time: `oauthRefresh` re-reads the session on every use and
|
|
15434
|
+
* returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
|
|
15435
|
+
* The one gap is the ACCESS token — it is a plain signed JWT that nothing
|
|
15436
|
+
* re-checks against the session on the `/trpc` and `/addon/*` paths, so
|
|
15437
|
+
* revoking a link takes effect there only after its remaining hour. That hour
|
|
15438
|
+
* is why the access TTL is not configurable.
|
|
15439
|
+
*
|
|
15440
|
+
* The value is baked into the authorization code at `/authorize` and travels
|
|
15441
|
+
* on the tokens, so editing this field changes FUTURE links only.
|
|
15442
|
+
*/
|
|
15443
|
+
refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
15410
15444
|
});
|
|
15411
15445
|
var oauthIntegrationCapability = {
|
|
15412
15446
|
name: "oauth-integration",
|
|
@@ -18549,11 +18583,29 @@ var SsoBridgeClaimsSchema = object({
|
|
|
18549
18583
|
codeChallenge: string().optional(),
|
|
18550
18584
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
18551
18585
|
* tokens so the verify path can check the session is not revoked. */
|
|
18552
|
-
sessionId: string().optional()
|
|
18586
|
+
sessionId: string().optional(),
|
|
18587
|
+
/**
|
|
18588
|
+
* The refresh lifetime this LINK was created with, in seconds, or `'never'`.
|
|
18589
|
+
* Baked into the code at `/authorize` from the integration's descriptor and
|
|
18590
|
+
* carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
|
|
18591
|
+
* on the token rather than being re-read from the descriptor on purpose:
|
|
18592
|
+
* editing a descriptor must not retroactively extend or shorten a link the
|
|
18593
|
+
* operator already consented to.
|
|
18594
|
+
*/
|
|
18595
|
+
refreshTtl: union([number().int().positive(), literal("never")]).optional()
|
|
18553
18596
|
});
|
|
18554
18597
|
method(object({
|
|
18555
18598
|
claims: SsoBridgeClaimsSchema,
|
|
18556
|
-
|
|
18599
|
+
/**
|
|
18600
|
+
* Seconds, or `'never'` for a token minted with NO `exp` claim.
|
|
18601
|
+
*
|
|
18602
|
+
* `'never'` is a literal rather than `undefined`/`0` because omitting
|
|
18603
|
+
* this field already means "the 5-minute SSO hand-off default", and
|
|
18604
|
+
* `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
|
|
18605
|
+
* went through the numeric path would fail at mint time and break
|
|
18606
|
+
* linking rather than produce an eternal token.
|
|
18607
|
+
*/
|
|
18608
|
+
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
18557
18609
|
}), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
|
|
18558
18610
|
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
18559
18611
|
providerId: string().min(1),
|
|
@@ -25834,7 +25886,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
25834
25886
|
hubUrl: string(),
|
|
25835
25887
|
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
25836
25888
|
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
25837
|
-
codeChallenge: string().optional()
|
|
25889
|
+
codeChallenge: string().optional(),
|
|
25890
|
+
/** The integration's declared refresh lifetime — seconds, or `'never'`.
|
|
25891
|
+
* From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
|
|
25892
|
+
* code so the link carries its own lifetime; omit for the 30-day
|
|
25893
|
+
* default. */
|
|
25894
|
+
refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
25838
25895
|
}), object({ code: string() }), {
|
|
25839
25896
|
kind: "mutation",
|
|
25840
25897
|
access: "create"
|
|
@@ -15351,21 +15351,32 @@ object({
|
|
|
15351
15351
|
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
15352
15352
|
* `/api/oauth2/integrations` are built from this collection alone.
|
|
15353
15353
|
*
|
|
15354
|
-
* **Scopes
|
|
15355
|
-
*
|
|
15356
|
-
*
|
|
15357
|
-
*
|
|
15358
|
-
*
|
|
15354
|
+
* **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
|
|
15355
|
+
* NEEDS to function.** Not a blast radius, not a conservative
|
|
15356
|
+
* under-declaration, not a description of some other path the addon happens to
|
|
15357
|
+
* have. Derive it from what the client actually calls **with this token** —
|
|
15358
|
+
* every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
|
|
15359
|
+
* addon HTTP route it posts to — and write the call that justifies each entry
|
|
15360
|
+
* next to it. Two integrations once used this field to mean two different
|
|
15361
|
+
* things; the operator ruled there is one meaning, and any third integration
|
|
15362
|
+
* inherits it (2026-08-09).
|
|
15363
|
+
*
|
|
15364
|
+
* This is not documentation, it is the ENFORCEMENT INPUT. Since
|
|
15365
|
+
* [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
|
|
15366
|
+
* checks an integration token's grant before letting it reach an
|
|
15367
|
+
* `access: 'authenticated'` route, so an **under-declaration is an integration
|
|
15368
|
+
* that stops working** — a missing `addon:` entry means `403 Token scope
|
|
15369
|
+
* mismatch` on every control the client tries to actuate. Widen the descriptor
|
|
15370
|
+
* honestly rather than weakening a check to make a route pass.
|
|
15371
|
+
*
|
|
15372
|
+
* Prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
15373
|
+
* genuinely needs a whole family; a category scope grants every future member
|
|
15359
15374
|
* of that category too. `category:system [create]` has been rejected once and
|
|
15360
15375
|
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
15361
15376
|
*
|
|
15362
|
-
*
|
|
15363
|
-
*
|
|
15364
|
-
*
|
|
15365
|
-
* the addon does the work, while the Home Assistant component calls tRPC
|
|
15366
|
-
* directly with the token. So `requestedScopes` describes the blast radius of
|
|
15367
|
-
* the GRANT, not the reach of the integration; do not widen one to describe the
|
|
15368
|
-
* other.
|
|
15377
|
+
* Calls the ADDON itself makes over `ctx.api` run as the addon and are not
|
|
15378
|
+
* scope-checked, so they are not what this field describes — but reaching the
|
|
15379
|
+
* addon's route in the first place IS, and that is the entry to declare.
|
|
15369
15380
|
*
|
|
15370
15381
|
* **The boot window.** An addon registers its provider after its runner forks
|
|
15371
15382
|
* and initialises, so between hub start and that moment this collection is
|
|
@@ -15406,7 +15417,30 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15406
15417
|
* present, /api/oauth2/authorize bakes THIS into the code instead of the
|
|
15407
15418
|
* hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
|
|
15408
15419
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15409
|
-
hubUrl: string().optional()
|
|
15420
|
+
hubUrl: string().optional(),
|
|
15421
|
+
/**
|
|
15422
|
+
* How long a REFRESH token issued for this integration lives — seconds, or
|
|
15423
|
+
* `'never'` for a token minted with no `exp` claim at all. Omit to keep the
|
|
15424
|
+
* 30-day default, which is what every link used before this field existed.
|
|
15425
|
+
*
|
|
15426
|
+
* Declared here for the same reason `requestedScopes` is: the integration
|
|
15427
|
+
* knows what it needs. Amazon's account linking and a Home Assistant config
|
|
15428
|
+
* entry are both meant to survive indefinitely, and re-linking is a manual
|
|
15429
|
+
* user action, so a 30-day expiry silently unlinks a working integration.
|
|
15430
|
+
*
|
|
15431
|
+
* **The security posture, stated so it is owned deliberately.** A refresh
|
|
15432
|
+
* token that never expires is permanent access if it leaks. What bounds it is
|
|
15433
|
+
* revocation, not time: `oauthRefresh` re-reads the session on every use and
|
|
15434
|
+
* returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
|
|
15435
|
+
* The one gap is the ACCESS token — it is a plain signed JWT that nothing
|
|
15436
|
+
* re-checks against the session on the `/trpc` and `/addon/*` paths, so
|
|
15437
|
+
* revoking a link takes effect there only after its remaining hour. That hour
|
|
15438
|
+
* is why the access TTL is not configurable.
|
|
15439
|
+
*
|
|
15440
|
+
* The value is baked into the authorization code at `/authorize` and travels
|
|
15441
|
+
* on the tokens, so editing this field changes FUTURE links only.
|
|
15442
|
+
*/
|
|
15443
|
+
refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
15410
15444
|
});
|
|
15411
15445
|
var oauthIntegrationCapability = {
|
|
15412
15446
|
name: "oauth-integration",
|
|
@@ -18549,11 +18583,29 @@ var SsoBridgeClaimsSchema = object({
|
|
|
18549
18583
|
codeChallenge: string().optional(),
|
|
18550
18584
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
18551
18585
|
* tokens so the verify path can check the session is not revoked. */
|
|
18552
|
-
sessionId: string().optional()
|
|
18586
|
+
sessionId: string().optional(),
|
|
18587
|
+
/**
|
|
18588
|
+
* The refresh lifetime this LINK was created with, in seconds, or `'never'`.
|
|
18589
|
+
* Baked into the code at `/authorize` from the integration's descriptor and
|
|
18590
|
+
* carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
|
|
18591
|
+
* on the token rather than being re-read from the descriptor on purpose:
|
|
18592
|
+
* editing a descriptor must not retroactively extend or shorten a link the
|
|
18593
|
+
* operator already consented to.
|
|
18594
|
+
*/
|
|
18595
|
+
refreshTtl: union([number().int().positive(), literal("never")]).optional()
|
|
18553
18596
|
});
|
|
18554
18597
|
method(object({
|
|
18555
18598
|
claims: SsoBridgeClaimsSchema,
|
|
18556
|
-
|
|
18599
|
+
/**
|
|
18600
|
+
* Seconds, or `'never'` for a token minted with NO `exp` claim.
|
|
18601
|
+
*
|
|
18602
|
+
* `'never'` is a literal rather than `undefined`/`0` because omitting
|
|
18603
|
+
* this field already means "the 5-minute SSO hand-off default", and
|
|
18604
|
+
* `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
|
|
18605
|
+
* went through the numeric path would fail at mint time and break
|
|
18606
|
+
* linking rather than produce an eternal token.
|
|
18607
|
+
*/
|
|
18608
|
+
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
18557
18609
|
}), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
|
|
18558
18610
|
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
18559
18611
|
providerId: string().min(1),
|
|
@@ -25834,7 +25886,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
25834
25886
|
hubUrl: string(),
|
|
25835
25887
|
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
25836
25888
|
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
25837
|
-
codeChallenge: string().optional()
|
|
25889
|
+
codeChallenge: string().optional(),
|
|
25890
|
+
/** The integration's declared refresh lifetime — seconds, or `'never'`.
|
|
25891
|
+
* From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
|
|
25892
|
+
* code so the link carries its own lifetime; omit for the 30-day
|
|
25893
|
+
* default. */
|
|
25894
|
+
refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
25838
25895
|
}), object({ code: string() }), {
|
|
25839
25896
|
kind: "mutation",
|
|
25840
25897
|
access: "create"
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
//#endregion
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-Bz5ENMVc.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
//#region src/ha-export/topics.ts
|
|
9
9
|
/**
|
|
@@ -872,9 +872,38 @@ function pruneBrokers(membership, knownBrokerIds) {
|
|
|
872
872
|
return next;
|
|
873
873
|
}
|
|
874
874
|
/**
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
875
|
+
* The brokers the operator has switched ON, read from the RAW addon store.
|
|
876
|
+
*
|
|
877
|
+
* `exportTo:<brokerId>` is a **dynamic** settings key — one per broker the
|
|
878
|
+
* hub happens to know about — so it can never appear in the addon's
|
|
879
|
+
* `DEFAULT_CONFIG`. `BaseAddon.resolveConfig` copies only keys that are
|
|
880
|
+
* present in the defaults ("the store can contain extra keys … without
|
|
881
|
+
* polluting the typed config"), so every `exportTo:*` key is dropped on the
|
|
882
|
+
* way into `this.config` and reading one there yields `undefined` for ever.
|
|
883
|
+
*
|
|
884
|
+
* That is not theoretical. On 2026-08-09 the switch was on
|
|
885
|
+
* (`getGlobalSettings` returned `exportTo:ha_001 → value: true`, because the
|
|
886
|
+
* settings FORM reads the store) and every reconcile still logged
|
|
887
|
+
* `brokers=0`: nothing was ever exported, the per-device Export tab rendered
|
|
888
|
+
* its "Not configured" card, and no operator action could have changed
|
|
889
|
+
* either. The store is the single authority — read it, do not mirror it into
|
|
890
|
+
* a typed field that would then be a second one (D62).
|
|
891
|
+
*
|
|
892
|
+
* A broker that is no longer known is not enabled, whatever the store says:
|
|
893
|
+
* a stale `exportTo:` key for a deleted instance must not resurrect it.
|
|
894
|
+
*/
|
|
895
|
+
function enabledBrokerIds(store, knownBrokerIds) {
|
|
896
|
+
return knownBrokerIds.filter((brokerId) => store[`exportTo:${brokerId}`] === true);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* What a linked Home Assistant NEEDS to function — one entry per thing it
|
|
900
|
+
* actually calls with this token, each named. Not a blast radius, not a
|
|
901
|
+
* conservative under-declaration: `requestedScopes` has exactly one meaning
|
|
902
|
+
* across every integration, and since D103 it is also the ENFORCEMENT input, so
|
|
903
|
+
* an under-declaration is an integration that stops working.
|
|
904
|
+
*
|
|
905
|
+
* `category:system [create]` was rejected and stays rejected: it would hand
|
|
906
|
+
* `addons.installPackage` to a home-automation bridge.
|
|
878
907
|
*/
|
|
879
908
|
var HOME_ASSISTANT_OAUTH_INTEGRATION = {
|
|
880
909
|
integrationId: "homeassistant",
|
|
@@ -899,11 +928,54 @@ var HOME_ASSISTANT_OAUTH_INTEGRATION = {
|
|
|
899
928
|
type: "capability",
|
|
900
929
|
target: "pipeline-orchestrator",
|
|
901
930
|
access: ["view", "create"]
|
|
902
|
-
}
|
|
931
|
+
},
|
|
932
|
+
(
|
|
933
|
+
/**
|
|
934
|
+
* `deviceExport.listExposedDevices` — the membership read that decides WHICH
|
|
935
|
+
* devices the component imports. It is pinned to this addon so the unpinned
|
|
936
|
+
* aggregate (which merges Alexa's and HomeKit's exposed sets, D12) cannot
|
|
937
|
+
* leak another exporter's devices into Home Assistant.
|
|
938
|
+
*
|
|
939
|
+
* Missing until 2026-08-09, which broke setup outright: the component's very
|
|
940
|
+
* first authenticated call is this one, and the hub answered "rejected the
|
|
941
|
+
* token" — reported by Home Assistant as `Configurazione non riuscita:
|
|
942
|
+
* deviceExport.listExposedDevices: hub rejected the token`. The call was
|
|
943
|
+
* introduced with the export filter and the descriptor was never widened for
|
|
944
|
+
* it. `system` scope, so a capability grant is what it needs — the `addon:`
|
|
945
|
+
* grant below covers the HTTP command route and does NOT cover this.
|
|
946
|
+
*/
|
|
947
|
+
{
|
|
948
|
+
type: "capability",
|
|
949
|
+
target: "device-export",
|
|
950
|
+
access: ["view"]
|
|
951
|
+
}),
|
|
952
|
+
(
|
|
953
|
+
/**
|
|
954
|
+
* `POST /addon/homeassistant-export/command` — the ONLY path by which an
|
|
955
|
+
* actuated Home Assistant entity (a camera switch, a PTZ button, reboot,
|
|
956
|
+
* snooze) reaches CamStack. Added when D103 made the addon-route gate check
|
|
957
|
+
* the grant instead of accepting any valid hub JWT; without it every HA
|
|
958
|
+
* control entity answers `403 Token scope mismatch`. `view` is here for the
|
|
959
|
+
* same addon's future GET routes, so a read does not force a re-link.
|
|
960
|
+
*/
|
|
961
|
+
{
|
|
962
|
+
type: "addon",
|
|
963
|
+
target: "homeassistant-export",
|
|
964
|
+
access: ["view", "create"]
|
|
965
|
+
})
|
|
903
966
|
],
|
|
904
967
|
allowedRedirectPrefixes: ["https://my.home-assistant.io/redirect/oauth"],
|
|
905
968
|
allowedPrivateHostPaths: ["/auth/external/callback"],
|
|
906
|
-
requiresPkce: true
|
|
969
|
+
requiresPkce: true,
|
|
970
|
+
/**
|
|
971
|
+
* One year. A Home Assistant config entry is meant to survive indefinitely
|
|
972
|
+
* and re-linking is a manual trip through the UI, so the 30-day default
|
|
973
|
+
* silently unlinked a working integration a month after setup. Finite rather
|
|
974
|
+
* than `'never'` (which Alexa uses): an HA instance refreshes on a timer
|
|
975
|
+
* while it is running, so a year is far beyond any realistic downtime, and a
|
|
976
|
+
* bounded token is the better default where nothing is lost by it.
|
|
977
|
+
*/
|
|
978
|
+
refreshTokenTtlSec: 365 * 24 * 60 * 60
|
|
907
979
|
};
|
|
908
980
|
//#endregion
|
|
909
981
|
//#region src/ha-export/push-client.ts
|
|
@@ -1299,6 +1371,19 @@ function projectCameraSwitches(deviceKey, switches) {
|
|
|
1299
1371
|
value: bool(sw.enabled)
|
|
1300
1372
|
}));
|
|
1301
1373
|
}
|
|
1374
|
+
/** The `BrokerInfo.kind` tag the Home Assistant provider stamps. */
|
|
1375
|
+
var HA_BROKER_KIND = "home-assistant";
|
|
1376
|
+
/**
|
|
1377
|
+
* Every Home Assistant broker currently registered, cluster-wide.
|
|
1378
|
+
*
|
|
1379
|
+
* Unpinned by design (see the module docblock) and filtered by `kind`, which is
|
|
1380
|
+
* the contract check that matters: whatever mix of providers answered the
|
|
1381
|
+
* union, only `home-assistant` brokers are ever exported to over Home
|
|
1382
|
+
* Assistant's transport.
|
|
1383
|
+
*/
|
|
1384
|
+
async function listHomeAssistantBrokers(query) {
|
|
1385
|
+
return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
|
|
1386
|
+
}
|
|
1302
1387
|
//#endregion
|
|
1303
1388
|
//#region src/ha-export/ha-export.addon.ts
|
|
1304
1389
|
/**
|
|
@@ -1336,8 +1421,6 @@ function projectCameraSwitches(deviceKey, switches) {
|
|
|
1336
1421
|
* different sets of cameras, and the per-device Export panel shows
|
|
1337
1422
|
* one switch per broker over the single membership store.
|
|
1338
1423
|
*/
|
|
1339
|
-
/** The addon that owns the import direction — never export back to it. */
|
|
1340
|
-
var HA_PROVIDER_ADDON_ID = "provider-homeassistant";
|
|
1341
1424
|
var ADDON_ID = "homeassistant-export";
|
|
1342
1425
|
/** Where the custom component registers its push view inside HA. */
|
|
1343
1426
|
var PUSH_PATH = "/api/camstack/push";
|
|
@@ -1699,7 +1782,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
1699
1782
|
* exported back to it: HA would mirror its own entity across the
|
|
1700
1783
|
* bridge, and any automation touching it would round-trip.
|
|
1701
1784
|
*/
|
|
1702
|
-
if (device.addonId ===
|
|
1785
|
+
if (device.addonId === "provider-homeassistant") {
|
|
1703
1786
|
this.ctx.logger.warn("ha-export: refusing to export a device imported from Home Assistant", { tags: { deviceId: numericId } });
|
|
1704
1787
|
continue;
|
|
1705
1788
|
}
|
|
@@ -1919,13 +2002,14 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
1919
2002
|
* ones the operator ticked.
|
|
1920
2003
|
*
|
|
1921
2004
|
* Credentials come through the `broker` cap — the sanctioned no-import
|
|
1922
|
-
* path — and the owning `addonId
|
|
1923
|
-
* COLLECTION cap, so an unpinned
|
|
1924
|
-
* first-registered provider,
|
|
1925
|
-
*
|
|
2005
|
+
* path — and EVERY call carries the owning `addonId`: `broker` is a
|
|
2006
|
+
* COLLECTION cap, so an unpinned call is answered by the
|
|
2007
|
+
* first-registered provider (Homematic, on the operator's hub), which
|
|
2008
|
+
* knows nothing about `ha_*`. `broker-source.ts` holds the pin and the
|
|
2009
|
+
* reasoning.
|
|
1926
2010
|
*/
|
|
1927
2011
|
async refreshBrokers() {
|
|
1928
|
-
const haBrokers =
|
|
2012
|
+
const haBrokers = await listHomeAssistantBrokers((input) => this.ctx.api.broker.list.query(input));
|
|
1929
2013
|
const known = haBrokers.map((broker) => ({
|
|
1930
2014
|
id: broker.id,
|
|
1931
2015
|
name: broker.name
|
|
@@ -1935,6 +2019,7 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
1935
2019
|
knownBrokers: known,
|
|
1936
2020
|
membership: pruned
|
|
1937
2021
|
});
|
|
2022
|
+
await this.refreshEnabledBrokers(known);
|
|
1938
2023
|
const wanted = new Set(this.enabledBrokerIds());
|
|
1939
2024
|
for (const [brokerId, link] of this.links) {
|
|
1940
2025
|
if (wanted.has(brokerId)) continue;
|
|
@@ -2006,8 +2091,23 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
2006
2091
|
return null;
|
|
2007
2092
|
}
|
|
2008
2093
|
}
|
|
2094
|
+
/**
|
|
2095
|
+
* The brokers switched on, as of the last {@link refreshEnabledBrokers}.
|
|
2096
|
+
*
|
|
2097
|
+
* NOT read from `this.config`: `exportTo:<brokerId>` is a dynamic key and
|
|
2098
|
+
* `BaseAddon.resolveConfig` copies only keys present in `DEFAULT_CONFIG`, so
|
|
2099
|
+
* it is `undefined` there for ever — see `enabledBrokerIds` in
|
|
2100
|
+
* `membership.ts` for what that cost. The store is the authority; this is a
|
|
2101
|
+
* read-through cache of it, refreshed at the top of every reconcile pass and
|
|
2102
|
+
* before the per-device panel renders, never written to.
|
|
2103
|
+
*/
|
|
2104
|
+
enabled = [];
|
|
2105
|
+
async refreshEnabledBrokers(known) {
|
|
2106
|
+
const store = await this.resolveGlobalStore();
|
|
2107
|
+
this.enabled = enabledBrokerIds(store, known.map((broker) => broker.id));
|
|
2108
|
+
}
|
|
2009
2109
|
enabledBrokerIds() {
|
|
2010
|
-
return this.
|
|
2110
|
+
return this.enabled;
|
|
2011
2111
|
}
|
|
2012
2112
|
brokerName(brokerId) {
|
|
2013
2113
|
return this.config.knownBrokers.find((broker) => broker.id === brokerId)?.name ?? brokerId;
|
|
@@ -2346,7 +2446,8 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
2346
2446
|
* exporters' identically-named fields and the operator sees another
|
|
2347
2447
|
* exporter's value in this one's toggle.
|
|
2348
2448
|
*/
|
|
2349
|
-
buildContribution(deviceId) {
|
|
2449
|
+
async buildContribution(deviceId) {
|
|
2450
|
+
await this.refreshEnabledBrokers(this.config.knownBrokers);
|
|
2350
2451
|
const idStr = String(deviceId);
|
|
2351
2452
|
const enabled = this.enabledBrokerIds();
|
|
2352
2453
|
const fields = enabled.map((brokerId) => ({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as oauthIntegrationCapability, O as deviceExportCapability, bt as string, c as addonRoutesCapability, ct as BaseAddon, i as CameraSwitchIdSchema, r as COCO_TO_MACRO, v as buildAddonRouteProvider, wt as EventCategory } from "../dist-
|
|
1
|
+
import { G as oauthIntegrationCapability, O as deviceExportCapability, bt as string, c as addonRoutesCapability, ct as BaseAddon, i as CameraSwitchIdSchema, r as COCO_TO_MACRO, v as buildAddonRouteProvider, wt as EventCategory } from "../dist-BSQpoIVg.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
//#region src/ha-export/topics.ts
|
|
4
4
|
/**
|
|
@@ -867,9 +867,38 @@ function pruneBrokers(membership, knownBrokerIds) {
|
|
|
867
867
|
return next;
|
|
868
868
|
}
|
|
869
869
|
/**
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
870
|
+
* The brokers the operator has switched ON, read from the RAW addon store.
|
|
871
|
+
*
|
|
872
|
+
* `exportTo:<brokerId>` is a **dynamic** settings key — one per broker the
|
|
873
|
+
* hub happens to know about — so it can never appear in the addon's
|
|
874
|
+
* `DEFAULT_CONFIG`. `BaseAddon.resolveConfig` copies only keys that are
|
|
875
|
+
* present in the defaults ("the store can contain extra keys … without
|
|
876
|
+
* polluting the typed config"), so every `exportTo:*` key is dropped on the
|
|
877
|
+
* way into `this.config` and reading one there yields `undefined` for ever.
|
|
878
|
+
*
|
|
879
|
+
* That is not theoretical. On 2026-08-09 the switch was on
|
|
880
|
+
* (`getGlobalSettings` returned `exportTo:ha_001 → value: true`, because the
|
|
881
|
+
* settings FORM reads the store) and every reconcile still logged
|
|
882
|
+
* `brokers=0`: nothing was ever exported, the per-device Export tab rendered
|
|
883
|
+
* its "Not configured" card, and no operator action could have changed
|
|
884
|
+
* either. The store is the single authority — read it, do not mirror it into
|
|
885
|
+
* a typed field that would then be a second one (D62).
|
|
886
|
+
*
|
|
887
|
+
* A broker that is no longer known is not enabled, whatever the store says:
|
|
888
|
+
* a stale `exportTo:` key for a deleted instance must not resurrect it.
|
|
889
|
+
*/
|
|
890
|
+
function enabledBrokerIds(store, knownBrokerIds) {
|
|
891
|
+
return knownBrokerIds.filter((brokerId) => store[`exportTo:${brokerId}`] === true);
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* What a linked Home Assistant NEEDS to function — one entry per thing it
|
|
895
|
+
* actually calls with this token, each named. Not a blast radius, not a
|
|
896
|
+
* conservative under-declaration: `requestedScopes` has exactly one meaning
|
|
897
|
+
* across every integration, and since D103 it is also the ENFORCEMENT input, so
|
|
898
|
+
* an under-declaration is an integration that stops working.
|
|
899
|
+
*
|
|
900
|
+
* `category:system [create]` was rejected and stays rejected: it would hand
|
|
901
|
+
* `addons.installPackage` to a home-automation bridge.
|
|
873
902
|
*/
|
|
874
903
|
var HOME_ASSISTANT_OAUTH_INTEGRATION = {
|
|
875
904
|
integrationId: "homeassistant",
|
|
@@ -894,11 +923,54 @@ var HOME_ASSISTANT_OAUTH_INTEGRATION = {
|
|
|
894
923
|
type: "capability",
|
|
895
924
|
target: "pipeline-orchestrator",
|
|
896
925
|
access: ["view", "create"]
|
|
897
|
-
}
|
|
926
|
+
},
|
|
927
|
+
(
|
|
928
|
+
/**
|
|
929
|
+
* `deviceExport.listExposedDevices` — the membership read that decides WHICH
|
|
930
|
+
* devices the component imports. It is pinned to this addon so the unpinned
|
|
931
|
+
* aggregate (which merges Alexa's and HomeKit's exposed sets, D12) cannot
|
|
932
|
+
* leak another exporter's devices into Home Assistant.
|
|
933
|
+
*
|
|
934
|
+
* Missing until 2026-08-09, which broke setup outright: the component's very
|
|
935
|
+
* first authenticated call is this one, and the hub answered "rejected the
|
|
936
|
+
* token" — reported by Home Assistant as `Configurazione non riuscita:
|
|
937
|
+
* deviceExport.listExposedDevices: hub rejected the token`. The call was
|
|
938
|
+
* introduced with the export filter and the descriptor was never widened for
|
|
939
|
+
* it. `system` scope, so a capability grant is what it needs — the `addon:`
|
|
940
|
+
* grant below covers the HTTP command route and does NOT cover this.
|
|
941
|
+
*/
|
|
942
|
+
{
|
|
943
|
+
type: "capability",
|
|
944
|
+
target: "device-export",
|
|
945
|
+
access: ["view"]
|
|
946
|
+
}),
|
|
947
|
+
(
|
|
948
|
+
/**
|
|
949
|
+
* `POST /addon/homeassistant-export/command` — the ONLY path by which an
|
|
950
|
+
* actuated Home Assistant entity (a camera switch, a PTZ button, reboot,
|
|
951
|
+
* snooze) reaches CamStack. Added when D103 made the addon-route gate check
|
|
952
|
+
* the grant instead of accepting any valid hub JWT; without it every HA
|
|
953
|
+
* control entity answers `403 Token scope mismatch`. `view` is here for the
|
|
954
|
+
* same addon's future GET routes, so a read does not force a re-link.
|
|
955
|
+
*/
|
|
956
|
+
{
|
|
957
|
+
type: "addon",
|
|
958
|
+
target: "homeassistant-export",
|
|
959
|
+
access: ["view", "create"]
|
|
960
|
+
})
|
|
898
961
|
],
|
|
899
962
|
allowedRedirectPrefixes: ["https://my.home-assistant.io/redirect/oauth"],
|
|
900
963
|
allowedPrivateHostPaths: ["/auth/external/callback"],
|
|
901
|
-
requiresPkce: true
|
|
964
|
+
requiresPkce: true,
|
|
965
|
+
/**
|
|
966
|
+
* One year. A Home Assistant config entry is meant to survive indefinitely
|
|
967
|
+
* and re-linking is a manual trip through the UI, so the 30-day default
|
|
968
|
+
* silently unlinked a working integration a month after setup. Finite rather
|
|
969
|
+
* than `'never'` (which Alexa uses): an HA instance refreshes on a timer
|
|
970
|
+
* while it is running, so a year is far beyond any realistic downtime, and a
|
|
971
|
+
* bounded token is the better default where nothing is lost by it.
|
|
972
|
+
*/
|
|
973
|
+
refreshTokenTtlSec: 365 * 24 * 60 * 60
|
|
902
974
|
};
|
|
903
975
|
//#endregion
|
|
904
976
|
//#region src/ha-export/push-client.ts
|
|
@@ -1294,6 +1366,19 @@ function projectCameraSwitches(deviceKey, switches) {
|
|
|
1294
1366
|
value: bool(sw.enabled)
|
|
1295
1367
|
}));
|
|
1296
1368
|
}
|
|
1369
|
+
/** The `BrokerInfo.kind` tag the Home Assistant provider stamps. */
|
|
1370
|
+
var HA_BROKER_KIND = "home-assistant";
|
|
1371
|
+
/**
|
|
1372
|
+
* Every Home Assistant broker currently registered, cluster-wide.
|
|
1373
|
+
*
|
|
1374
|
+
* Unpinned by design (see the module docblock) and filtered by `kind`, which is
|
|
1375
|
+
* the contract check that matters: whatever mix of providers answered the
|
|
1376
|
+
* union, only `home-assistant` brokers are ever exported to over Home
|
|
1377
|
+
* Assistant's transport.
|
|
1378
|
+
*/
|
|
1379
|
+
async function listHomeAssistantBrokers(query) {
|
|
1380
|
+
return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
|
|
1381
|
+
}
|
|
1297
1382
|
//#endregion
|
|
1298
1383
|
//#region src/ha-export/ha-export.addon.ts
|
|
1299
1384
|
/**
|
|
@@ -1331,8 +1416,6 @@ function projectCameraSwitches(deviceKey, switches) {
|
|
|
1331
1416
|
* different sets of cameras, and the per-device Export panel shows
|
|
1332
1417
|
* one switch per broker over the single membership store.
|
|
1333
1418
|
*/
|
|
1334
|
-
/** The addon that owns the import direction — never export back to it. */
|
|
1335
|
-
var HA_PROVIDER_ADDON_ID = "provider-homeassistant";
|
|
1336
1419
|
var ADDON_ID = "homeassistant-export";
|
|
1337
1420
|
/** Where the custom component registers its push view inside HA. */
|
|
1338
1421
|
var PUSH_PATH = "/api/camstack/push";
|
|
@@ -1694,7 +1777,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
1694
1777
|
* exported back to it: HA would mirror its own entity across the
|
|
1695
1778
|
* bridge, and any automation touching it would round-trip.
|
|
1696
1779
|
*/
|
|
1697
|
-
if (device.addonId ===
|
|
1780
|
+
if (device.addonId === "provider-homeassistant") {
|
|
1698
1781
|
this.ctx.logger.warn("ha-export: refusing to export a device imported from Home Assistant", { tags: { deviceId: numericId } });
|
|
1699
1782
|
continue;
|
|
1700
1783
|
}
|
|
@@ -1914,13 +1997,14 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
1914
1997
|
* ones the operator ticked.
|
|
1915
1998
|
*
|
|
1916
1999
|
* Credentials come through the `broker` cap — the sanctioned no-import
|
|
1917
|
-
* path — and the owning `addonId
|
|
1918
|
-
* COLLECTION cap, so an unpinned
|
|
1919
|
-
* first-registered provider,
|
|
1920
|
-
*
|
|
2000
|
+
* path — and EVERY call carries the owning `addonId`: `broker` is a
|
|
2001
|
+
* COLLECTION cap, so an unpinned call is answered by the
|
|
2002
|
+
* first-registered provider (Homematic, on the operator's hub), which
|
|
2003
|
+
* knows nothing about `ha_*`. `broker-source.ts` holds the pin and the
|
|
2004
|
+
* reasoning.
|
|
1921
2005
|
*/
|
|
1922
2006
|
async refreshBrokers() {
|
|
1923
|
-
const haBrokers =
|
|
2007
|
+
const haBrokers = await listHomeAssistantBrokers((input) => this.ctx.api.broker.list.query(input));
|
|
1924
2008
|
const known = haBrokers.map((broker) => ({
|
|
1925
2009
|
id: broker.id,
|
|
1926
2010
|
name: broker.name
|
|
@@ -1930,6 +2014,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
1930
2014
|
knownBrokers: known,
|
|
1931
2015
|
membership: pruned
|
|
1932
2016
|
});
|
|
2017
|
+
await this.refreshEnabledBrokers(known);
|
|
1933
2018
|
const wanted = new Set(this.enabledBrokerIds());
|
|
1934
2019
|
for (const [brokerId, link] of this.links) {
|
|
1935
2020
|
if (wanted.has(brokerId)) continue;
|
|
@@ -2001,8 +2086,23 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
2001
2086
|
return null;
|
|
2002
2087
|
}
|
|
2003
2088
|
}
|
|
2089
|
+
/**
|
|
2090
|
+
* The brokers switched on, as of the last {@link refreshEnabledBrokers}.
|
|
2091
|
+
*
|
|
2092
|
+
* NOT read from `this.config`: `exportTo:<brokerId>` is a dynamic key and
|
|
2093
|
+
* `BaseAddon.resolveConfig` copies only keys present in `DEFAULT_CONFIG`, so
|
|
2094
|
+
* it is `undefined` there for ever — see `enabledBrokerIds` in
|
|
2095
|
+
* `membership.ts` for what that cost. The store is the authority; this is a
|
|
2096
|
+
* read-through cache of it, refreshed at the top of every reconcile pass and
|
|
2097
|
+
* before the per-device panel renders, never written to.
|
|
2098
|
+
*/
|
|
2099
|
+
enabled = [];
|
|
2100
|
+
async refreshEnabledBrokers(known) {
|
|
2101
|
+
const store = await this.resolveGlobalStore();
|
|
2102
|
+
this.enabled = enabledBrokerIds(store, known.map((broker) => broker.id));
|
|
2103
|
+
}
|
|
2004
2104
|
enabledBrokerIds() {
|
|
2005
|
-
return this.
|
|
2105
|
+
return this.enabled;
|
|
2006
2106
|
}
|
|
2007
2107
|
brokerName(brokerId) {
|
|
2008
2108
|
return this.config.knownBrokers.find((broker) => broker.id === brokerId)?.name ?? brokerId;
|
|
@@ -2341,7 +2441,8 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
2341
2441
|
* exporters' identically-named fields and the operator sees another
|
|
2342
2442
|
* exporter's value in this one's toggle.
|
|
2343
2443
|
*/
|
|
2344
|
-
buildContribution(deviceId) {
|
|
2444
|
+
async buildContribution(deviceId) {
|
|
2445
|
+
await this.refreshEnabledBrokers(this.config.knownBrokers);
|
|
2345
2446
|
const idStr = String(deviceId);
|
|
2346
2447
|
const enabled = this.enabledBrokerIds();
|
|
2347
2448
|
const fields = enabled.map((brokerId) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-homeassistant",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.19",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -46,8 +46,7 @@
|
|
|
46
46
|
"brokerKind": "home-assistant",
|
|
47
47
|
"supportsLocationImport": true,
|
|
48
48
|
"execution": {
|
|
49
|
-
"placement": "hub-only"
|
|
50
|
-
"group": "notifiers"
|
|
49
|
+
"placement": "hub-only"
|
|
51
50
|
},
|
|
52
51
|
"capabilities": [
|
|
53
52
|
{
|