@camstack/addon-provider-homeassistant 1.2.17 → 1.2.18
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-DIa87XAf.js → dist-DuKJzmrJ.js} +72 -1
- package/dist/{dist-reK3lvnr.mjs → dist-UfT_fD3-.mjs} +67 -2
- package/dist/ha-export/ha-export.addon.js +70 -31
- package/dist/ha-export/ha-export.addon.mjs +70 -31
- package/package.json +4 -1
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-DuKJzmrJ.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
|
|
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-UfT_fD3-.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
|
|
@@ -15317,6 +15317,65 @@ object({
|
|
|
15317
15317
|
* Each provider returns a static descriptor; the core enumerates them
|
|
15318
15318
|
* to validate the `integration=` query param and resolve the consent
|
|
15319
15319
|
* label + the scopes baked into the issued token.
|
|
15320
|
+
*
|
|
15321
|
+
* ## Declaring one
|
|
15322
|
+
*
|
|
15323
|
+
* An OAuth client is integration-specific knowledge — who the client is, what
|
|
15324
|
+
* it may ask for, where it may be sent — so it is declared by the ADDON that
|
|
15325
|
+
* owns the integration, never by the kernel and never as a branch inside
|
|
15326
|
+
* `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
|
|
15327
|
+
* steps, no others:
|
|
15328
|
+
*
|
|
15329
|
+
* 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
|
|
15330
|
+
* manifest entry. This is also what tells the hub, at addon-LOAD time, that
|
|
15331
|
+
* a descriptor is owed — see "the boot window" below.
|
|
15332
|
+
* 2. Return a provider from `onInitialize()`:
|
|
15333
|
+
*
|
|
15334
|
+
* ```ts
|
|
15335
|
+
* const provider: IOauthIntegrationProvider = {
|
|
15336
|
+
* getDescriptor: async () => ({
|
|
15337
|
+
* integrationId: 'my-thing', // the `integration=` query param
|
|
15338
|
+
* displayName: 'My Thing',
|
|
15339
|
+
* requestedScopes: [ … ], // see below
|
|
15340
|
+
* allowedRedirectPrefixes: ['https://callback.example/'],
|
|
15341
|
+
* }),
|
|
15342
|
+
* }
|
|
15343
|
+
* return [{ capability: oauthIntegrationCapability, provider }]
|
|
15344
|
+
* ```
|
|
15345
|
+
*
|
|
15346
|
+
* The descriptor must be **static** — it is read on the authorize path, so
|
|
15347
|
+
* never put an await on network or disk behind it, and never register it
|
|
15348
|
+
* behind one either (a provider is registered only once `onInitialize`
|
|
15349
|
+
* RETURNS, so anything awaited before the return delays linking).
|
|
15350
|
+
* 3. Nothing else. There is no allow-list to join, no id to register with the
|
|
15351
|
+
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
15352
|
+
* `/api/oauth2/integrations` are built from this collection alone.
|
|
15353
|
+
*
|
|
15354
|
+
* **Scopes.** `requestedScopes` is baked into every token this integration is
|
|
15355
|
+
* ever issued and the operator consents to it once. Derive it from the tRPC
|
|
15356
|
+
* paths the client calls **with that token**, against `METHOD_ACCESS_MAP`, and
|
|
15357
|
+
* prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
15358
|
+
* genuinely needs a whole family. A category scope grants every future member
|
|
15359
|
+
* of that category too. `category:system [create]` has been rejected once and
|
|
15360
|
+
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
15361
|
+
*
|
|
15362
|
+
* What it does NOT cover: calls the ADDON makes over `ctx.api`, which run as
|
|
15363
|
+
* the addon and are not scope-checked. Alexa's descriptor is narrower than
|
|
15364
|
+
* Home Assistant's for exactly that reason — its Lambda posts directives and
|
|
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.
|
|
15369
|
+
*
|
|
15370
|
+
* **The boot window.** An addon registers its provider after its runner forks
|
|
15371
|
+
* and initialises, so between hub start and that moment this collection is
|
|
15372
|
+
* incomplete and an `integrationId` can be legitimately absent. The core does
|
|
15373
|
+
* not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
|
|
15374
|
+
* it compares the manifest declarers against the registered providers and
|
|
15375
|
+
* answers `503 temporarily_unavailable` (with `Retry-After` and the pending
|
|
15376
|
+
* addon ids) instead of `400 unknown integration`, and reports
|
|
15377
|
+
* `complete: false` on `GET /api/oauth2/integrations`. A client should retry
|
|
15378
|
+
* while the list is incomplete rather than conclude the hub cannot do OAuth.
|
|
15320
15379
|
*/
|
|
15321
15380
|
var OauthIntegrationDescriptorSchema = object({
|
|
15322
15381
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -15349,7 +15408,13 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15349
15408
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15350
15409
|
hubUrl: string().optional()
|
|
15351
15410
|
});
|
|
15352
|
-
|
|
15411
|
+
var oauthIntegrationCapability = {
|
|
15412
|
+
name: "oauth-integration",
|
|
15413
|
+
scope: "system",
|
|
15414
|
+
mode: "collection",
|
|
15415
|
+
internal: true,
|
|
15416
|
+
methods: { getDescriptor: method(_void(), OauthIntegrationDescriptorSchema) }
|
|
15417
|
+
};
|
|
15353
15418
|
/**
|
|
15354
15419
|
* pipeline-analytics — device-scoped wrapper cap. Refines raw
|
|
15355
15420
|
* per-frame detections emitted by the pipeline runner into tracked
|
|
@@ -33687,6 +33752,12 @@ Object.defineProperty(exports, "numericSensorCapability", {
|
|
|
33687
33752
|
return numericSensorCapability;
|
|
33688
33753
|
}
|
|
33689
33754
|
});
|
|
33755
|
+
Object.defineProperty(exports, "oauthIntegrationCapability", {
|
|
33756
|
+
enumerable: true,
|
|
33757
|
+
get: function() {
|
|
33758
|
+
return oauthIntegrationCapability;
|
|
33759
|
+
}
|
|
33760
|
+
});
|
|
33690
33761
|
Object.defineProperty(exports, "object", {
|
|
33691
33762
|
enumerable: true,
|
|
33692
33763
|
get: function() {
|
|
@@ -15317,6 +15317,65 @@ object({
|
|
|
15317
15317
|
* Each provider returns a static descriptor; the core enumerates them
|
|
15318
15318
|
* to validate the `integration=` query param and resolve the consent
|
|
15319
15319
|
* label + the scopes baked into the issued token.
|
|
15320
|
+
*
|
|
15321
|
+
* ## Declaring one
|
|
15322
|
+
*
|
|
15323
|
+
* An OAuth client is integration-specific knowledge — who the client is, what
|
|
15324
|
+
* it may ask for, where it may be sent — so it is declared by the ADDON that
|
|
15325
|
+
* owns the integration, never by the kernel and never as a branch inside
|
|
15326
|
+
* `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
|
|
15327
|
+
* steps, no others:
|
|
15328
|
+
*
|
|
15329
|
+
* 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
|
|
15330
|
+
* manifest entry. This is also what tells the hub, at addon-LOAD time, that
|
|
15331
|
+
* a descriptor is owed — see "the boot window" below.
|
|
15332
|
+
* 2. Return a provider from `onInitialize()`:
|
|
15333
|
+
*
|
|
15334
|
+
* ```ts
|
|
15335
|
+
* const provider: IOauthIntegrationProvider = {
|
|
15336
|
+
* getDescriptor: async () => ({
|
|
15337
|
+
* integrationId: 'my-thing', // the `integration=` query param
|
|
15338
|
+
* displayName: 'My Thing',
|
|
15339
|
+
* requestedScopes: [ … ], // see below
|
|
15340
|
+
* allowedRedirectPrefixes: ['https://callback.example/'],
|
|
15341
|
+
* }),
|
|
15342
|
+
* }
|
|
15343
|
+
* return [{ capability: oauthIntegrationCapability, provider }]
|
|
15344
|
+
* ```
|
|
15345
|
+
*
|
|
15346
|
+
* The descriptor must be **static** — it is read on the authorize path, so
|
|
15347
|
+
* never put an await on network or disk behind it, and never register it
|
|
15348
|
+
* behind one either (a provider is registered only once `onInitialize`
|
|
15349
|
+
* RETURNS, so anything awaited before the return delays linking).
|
|
15350
|
+
* 3. Nothing else. There is no allow-list to join, no id to register with the
|
|
15351
|
+
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
15352
|
+
* `/api/oauth2/integrations` are built from this collection alone.
|
|
15353
|
+
*
|
|
15354
|
+
* **Scopes.** `requestedScopes` is baked into every token this integration is
|
|
15355
|
+
* ever issued and the operator consents to it once. Derive it from the tRPC
|
|
15356
|
+
* paths the client calls **with that token**, against `METHOD_ACCESS_MAP`, and
|
|
15357
|
+
* prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
15358
|
+
* genuinely needs a whole family. A category scope grants every future member
|
|
15359
|
+
* of that category too. `category:system [create]` has been rejected once and
|
|
15360
|
+
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
15361
|
+
*
|
|
15362
|
+
* What it does NOT cover: calls the ADDON makes over `ctx.api`, which run as
|
|
15363
|
+
* the addon and are not scope-checked. Alexa's descriptor is narrower than
|
|
15364
|
+
* Home Assistant's for exactly that reason — its Lambda posts directives and
|
|
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.
|
|
15369
|
+
*
|
|
15370
|
+
* **The boot window.** An addon registers its provider after its runner forks
|
|
15371
|
+
* and initialises, so between hub start and that moment this collection is
|
|
15372
|
+
* incomplete and an `integrationId` can be legitimately absent. The core does
|
|
15373
|
+
* not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
|
|
15374
|
+
* it compares the manifest declarers against the registered providers and
|
|
15375
|
+
* answers `503 temporarily_unavailable` (with `Retry-After` and the pending
|
|
15376
|
+
* addon ids) instead of `400 unknown integration`, and reports
|
|
15377
|
+
* `complete: false` on `GET /api/oauth2/integrations`. A client should retry
|
|
15378
|
+
* while the list is incomplete rather than conclude the hub cannot do OAuth.
|
|
15320
15379
|
*/
|
|
15321
15380
|
var OauthIntegrationDescriptorSchema = object({
|
|
15322
15381
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -15349,7 +15408,13 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15349
15408
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15350
15409
|
hubUrl: string().optional()
|
|
15351
15410
|
});
|
|
15352
|
-
|
|
15411
|
+
var oauthIntegrationCapability = {
|
|
15412
|
+
name: "oauth-integration",
|
|
15413
|
+
scope: "system",
|
|
15414
|
+
mode: "collection",
|
|
15415
|
+
internal: true,
|
|
15416
|
+
methods: { getDescriptor: method(_void(), OauthIntegrationDescriptorSchema) }
|
|
15417
|
+
};
|
|
15353
15418
|
/**
|
|
15354
15419
|
* pipeline-analytics — device-scoped wrapper cap. Refines raw
|
|
15355
15420
|
* per-frame detections emitted by the pipeline runner into tracked
|
|
@@ -33351,4 +33416,4 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
|
|
|
33351
33416
|
return best;
|
|
33352
33417
|
}
|
|
33353
33418
|
//#endregion
|
|
33354
|
-
export {
|
|
33419
|
+
export { tamperCapability as $, eventEmitterCapability as A, motionCapability as B, connectivityCapability as C, url as Ct, deviceAdoptionCapability as D, coverCapability as E, humiditySensorCapability as F, oauthIntegrationCapability as G, notificationOutputCapability as H, imageCapability as I, presenceCapability as J, powerMeterCapability as K, lawnMowerControlCapability as L, floodCapability as M, gasCapability as N, deviceExportCapability as O, humidifierCapability as P, switchCapability as Q, lockControlCapability as R, colorCapability as S, unknown as St, controlCapability as T, notifierCapability as U, normalizeUnit as V, numericSensorCapability as W, scriptRunnerCapability as X, pressureSensorCapability as Y, smokeCapability as Z, brokerCapability as _, number as _t, EnumSensorDateTimeFormatSchema as a, waterHeaterCapability as at, carbonMonoxideCapability as b, string as bt, addonRoutesCapability as c, BaseAddon as ct, ambientLightSensorCapability as d, DeviceType as dt, temperatureSensorCapability as et, automationControlCapability as f, createEvent as ft, brightnessCapability as g, literal as gt, binaryCapability as h, discriminatedUnion as ht, CameraSwitchIdSchema as i, vibrationCapability as it, fanControlCapability as j, enumSensorCapability as k, airQualitySensorCapability as l, DeviceFeature as lt, bestLocationMatch as m, array as mt, BaseDeviceProvider as n, vacuumControlCapability as nt, TargetSchema as o, weatherCapability as ot, batteryCapability as p, _enum as pt, prepareNotification as q, COCO_TO_MACRO as r, valveCapability as rt, accessoriesCapability as s, errMsg as st, BaseDevice as t, updateCapability as tt, alarmPanelCapability as u, DeviceRole as ut, buildAddonRouteProvider as v, object as vt, contactCapability as w, EventCategory as wt, climateControlCapability as x, union as xt, buttonCapability as y, record as yt, mediaPlayerCapability as z };
|
|
@@ -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-DuKJzmrJ.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
//#region src/ha-export/topics.ts
|
|
9
9
|
/**
|
|
@@ -871,6 +871,40 @@ function pruneBrokers(membership, knownBrokerIds) {
|
|
|
871
871
|
for (const brokerId of kept) next[brokerId] = membership[brokerId] ?? [];
|
|
872
872
|
return next;
|
|
873
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* Scopes granted to a linked Home Assistant, derived from the tRPC paths the
|
|
876
|
+
* component actually calls. `category:system [create]` was rejected: it would
|
|
877
|
+
* hand `addons.installPackage` to a home-automation bridge.
|
|
878
|
+
*/
|
|
879
|
+
var HOME_ASSISTANT_OAUTH_INTEGRATION = {
|
|
880
|
+
integrationId: "homeassistant",
|
|
881
|
+
displayName: "Home Assistant",
|
|
882
|
+
requestedScopes: [
|
|
883
|
+
{
|
|
884
|
+
type: "category",
|
|
885
|
+
target: "device",
|
|
886
|
+
access: ["view", "create"]
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
type: "capability",
|
|
890
|
+
target: "device-manager",
|
|
891
|
+
access: ["view"]
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
type: "capability",
|
|
895
|
+
target: "device-state",
|
|
896
|
+
access: ["view"]
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
type: "capability",
|
|
900
|
+
target: "pipeline-orchestrator",
|
|
901
|
+
access: ["view", "create"]
|
|
902
|
+
}
|
|
903
|
+
],
|
|
904
|
+
allowedRedirectPrefixes: ["https://my.home-assistant.io/redirect/oauth"],
|
|
905
|
+
allowedPrivateHostPaths: ["/auth/external/callback"],
|
|
906
|
+
requiresPkce: true
|
|
907
|
+
};
|
|
874
908
|
//#endregion
|
|
875
909
|
//#region src/ha-export/push-client.ts
|
|
876
910
|
/** How long state updates accumulate before one POST carries them all. */
|
|
@@ -1374,12 +1408,10 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
1374
1408
|
} else this.mediaSecret = existing;
|
|
1375
1409
|
this.wireListeners();
|
|
1376
1410
|
await this.serveMediaPlane();
|
|
1377
|
-
|
|
1378
|
-
await this.reconcile("boot");
|
|
1379
|
-
} catch (err) {
|
|
1411
|
+
this.reconcile("boot").catch((err) => {
|
|
1380
1412
|
this.lastError = errMsg(err);
|
|
1381
1413
|
this.ctx.logger.warn("ha-export: initial reconcile failed", { meta: { error: this.lastError } });
|
|
1382
|
-
}
|
|
1414
|
+
});
|
|
1383
1415
|
this.startTimer();
|
|
1384
1416
|
this.ctx.addDisposer(async () => {
|
|
1385
1417
|
this.stopTimer();
|
|
@@ -1387,33 +1419,40 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
1387
1419
|
for (const link of this.links.values()) await link.client.dispose();
|
|
1388
1420
|
this.links.clear();
|
|
1389
1421
|
});
|
|
1390
|
-
return [
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
deviceId
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
}, {
|
|
1406
|
-
capability: require_dist.addonRoutesCapability,
|
|
1407
|
-
provider: require_dist.buildAddonRouteProvider(ADDON_ID, [{
|
|
1408
|
-
method: "POST",
|
|
1409
|
-
path: "/command",
|
|
1410
|
-
access: "authenticated",
|
|
1411
|
-
description: "Home Assistant → CamStack command: {topic, value}",
|
|
1412
|
-
handler: async (request, reply) => {
|
|
1413
|
-
await this.handleCommandRoute(request.body, reply);
|
|
1422
|
+
return [
|
|
1423
|
+
{
|
|
1424
|
+
capability: require_dist.deviceExportCapability,
|
|
1425
|
+
provider: {
|
|
1426
|
+
getStatus: async () => this.buildStatus(),
|
|
1427
|
+
listSupportedDeviceKinds: async () => [...SUPPORTED_DEVICE_KINDS],
|
|
1428
|
+
listExposedDevices: async () => exposedDeviceIds(this.config.membership, this.enabledBrokerIds()).map((deviceId) => ({
|
|
1429
|
+
deviceId,
|
|
1430
|
+
exposedAs: brokersExposing(this.config.membership, deviceId).map((brokerId) => this.brokerName(brokerId)).join(", ")
|
|
1431
|
+
})),
|
|
1432
|
+
exposeDevice: async ({ deviceId }) => this.setMembershipEverywhere(deviceId, true),
|
|
1433
|
+
unexposeDevice: async ({ deviceId }) => this.setMembershipEverywhere(deviceId, false),
|
|
1434
|
+
getDeviceSettingsContribution: async ({ deviceId }) => this.buildContribution(deviceId),
|
|
1435
|
+
getDeviceLiveContribution: async () => null,
|
|
1436
|
+
applyDeviceSettingsPatch: async ({ deviceId, patch }) => this.applyDeviceSettingsPatch(deviceId, patch)
|
|
1414
1437
|
}
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
capability: require_dist.addonRoutesCapability,
|
|
1441
|
+
provider: require_dist.buildAddonRouteProvider(ADDON_ID, [{
|
|
1442
|
+
method: "POST",
|
|
1443
|
+
path: "/command",
|
|
1444
|
+
access: "authenticated",
|
|
1445
|
+
description: "Home Assistant → CamStack command: {topic, value}",
|
|
1446
|
+
handler: async (request, reply) => {
|
|
1447
|
+
await this.handleCommandRoute(request.body, reply);
|
|
1448
|
+
}
|
|
1449
|
+
}])
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
capability: require_dist.oauthIntegrationCapability,
|
|
1453
|
+
provider: { getDescriptor: async () => HOME_ASSISTANT_OAUTH_INTEGRATION }
|
|
1454
|
+
}
|
|
1455
|
+
];
|
|
1417
1456
|
}
|
|
1418
1457
|
async onConfigChanged() {
|
|
1419
1458
|
await this.reconcile("config-changed");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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-UfT_fD3-.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
//#region src/ha-export/topics.ts
|
|
4
4
|
/**
|
|
@@ -866,6 +866,40 @@ function pruneBrokers(membership, knownBrokerIds) {
|
|
|
866
866
|
for (const brokerId of kept) next[brokerId] = membership[brokerId] ?? [];
|
|
867
867
|
return next;
|
|
868
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Scopes granted to a linked Home Assistant, derived from the tRPC paths the
|
|
871
|
+
* component actually calls. `category:system [create]` was rejected: it would
|
|
872
|
+
* hand `addons.installPackage` to a home-automation bridge.
|
|
873
|
+
*/
|
|
874
|
+
var HOME_ASSISTANT_OAUTH_INTEGRATION = {
|
|
875
|
+
integrationId: "homeassistant",
|
|
876
|
+
displayName: "Home Assistant",
|
|
877
|
+
requestedScopes: [
|
|
878
|
+
{
|
|
879
|
+
type: "category",
|
|
880
|
+
target: "device",
|
|
881
|
+
access: ["view", "create"]
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
type: "capability",
|
|
885
|
+
target: "device-manager",
|
|
886
|
+
access: ["view"]
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
type: "capability",
|
|
890
|
+
target: "device-state",
|
|
891
|
+
access: ["view"]
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
type: "capability",
|
|
895
|
+
target: "pipeline-orchestrator",
|
|
896
|
+
access: ["view", "create"]
|
|
897
|
+
}
|
|
898
|
+
],
|
|
899
|
+
allowedRedirectPrefixes: ["https://my.home-assistant.io/redirect/oauth"],
|
|
900
|
+
allowedPrivateHostPaths: ["/auth/external/callback"],
|
|
901
|
+
requiresPkce: true
|
|
902
|
+
};
|
|
869
903
|
//#endregion
|
|
870
904
|
//#region src/ha-export/push-client.ts
|
|
871
905
|
/** How long state updates accumulate before one POST carries them all. */
|
|
@@ -1369,12 +1403,10 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
1369
1403
|
} else this.mediaSecret = existing;
|
|
1370
1404
|
this.wireListeners();
|
|
1371
1405
|
await this.serveMediaPlane();
|
|
1372
|
-
|
|
1373
|
-
await this.reconcile("boot");
|
|
1374
|
-
} catch (err) {
|
|
1406
|
+
this.reconcile("boot").catch((err) => {
|
|
1375
1407
|
this.lastError = errMsg(err);
|
|
1376
1408
|
this.ctx.logger.warn("ha-export: initial reconcile failed", { meta: { error: this.lastError } });
|
|
1377
|
-
}
|
|
1409
|
+
});
|
|
1378
1410
|
this.startTimer();
|
|
1379
1411
|
this.ctx.addDisposer(async () => {
|
|
1380
1412
|
this.stopTimer();
|
|
@@ -1382,33 +1414,40 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
1382
1414
|
for (const link of this.links.values()) await link.client.dispose();
|
|
1383
1415
|
this.links.clear();
|
|
1384
1416
|
});
|
|
1385
|
-
return [
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
deviceId
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}, {
|
|
1401
|
-
capability: addonRoutesCapability,
|
|
1402
|
-
provider: buildAddonRouteProvider(ADDON_ID, [{
|
|
1403
|
-
method: "POST",
|
|
1404
|
-
path: "/command",
|
|
1405
|
-
access: "authenticated",
|
|
1406
|
-
description: "Home Assistant → CamStack command: {topic, value}",
|
|
1407
|
-
handler: async (request, reply) => {
|
|
1408
|
-
await this.handleCommandRoute(request.body, reply);
|
|
1417
|
+
return [
|
|
1418
|
+
{
|
|
1419
|
+
capability: deviceExportCapability,
|
|
1420
|
+
provider: {
|
|
1421
|
+
getStatus: async () => this.buildStatus(),
|
|
1422
|
+
listSupportedDeviceKinds: async () => [...SUPPORTED_DEVICE_KINDS],
|
|
1423
|
+
listExposedDevices: async () => exposedDeviceIds(this.config.membership, this.enabledBrokerIds()).map((deviceId) => ({
|
|
1424
|
+
deviceId,
|
|
1425
|
+
exposedAs: brokersExposing(this.config.membership, deviceId).map((brokerId) => this.brokerName(brokerId)).join(", ")
|
|
1426
|
+
})),
|
|
1427
|
+
exposeDevice: async ({ deviceId }) => this.setMembershipEverywhere(deviceId, true),
|
|
1428
|
+
unexposeDevice: async ({ deviceId }) => this.setMembershipEverywhere(deviceId, false),
|
|
1429
|
+
getDeviceSettingsContribution: async ({ deviceId }) => this.buildContribution(deviceId),
|
|
1430
|
+
getDeviceLiveContribution: async () => null,
|
|
1431
|
+
applyDeviceSettingsPatch: async ({ deviceId, patch }) => this.applyDeviceSettingsPatch(deviceId, patch)
|
|
1409
1432
|
}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
capability: addonRoutesCapability,
|
|
1436
|
+
provider: buildAddonRouteProvider(ADDON_ID, [{
|
|
1437
|
+
method: "POST",
|
|
1438
|
+
path: "/command",
|
|
1439
|
+
access: "authenticated",
|
|
1440
|
+
description: "Home Assistant → CamStack command: {topic, value}",
|
|
1441
|
+
handler: async (request, reply) => {
|
|
1442
|
+
await this.handleCommandRoute(request.body, reply);
|
|
1443
|
+
}
|
|
1444
|
+
}])
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
capability: oauthIntegrationCapability,
|
|
1448
|
+
provider: { getDescriptor: async () => HOME_ASSISTANT_OAUTH_INTEGRATION }
|
|
1449
|
+
}
|
|
1450
|
+
];
|
|
1412
1451
|
}
|
|
1413
1452
|
async onConfigChanged() {
|
|
1414
1453
|
await this.reconcile("config-changed");
|
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.18",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -106,6 +106,9 @@
|
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"name": "addon-routes"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "oauth-integration"
|
|
109
112
|
}
|
|
110
113
|
]
|
|
111
114
|
}
|