@camstack/addon-provider-homeassistant 1.2.16 → 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 +52 -1
- package/dist/addon.mjs +52 -1
- package/dist/{dist-DQdO_V3C.js → dist-DuKJzmrJ.js} +283 -2
- package/dist/{dist-B04tcTnS.mjs → dist-UfT_fD3-.mjs} +278 -3
- 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
|
|
@@ -7824,11 +7824,39 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
|
|
|
7824
7824
|
const haDevices = await this.fetchHaDiscoveredDevices(brokerId);
|
|
7825
7825
|
const haDeviceMap = new Map(haDevices.map((d) => [d.haDeviceId, d]));
|
|
7826
7826
|
let knownLocations = importLocations === true ? await this.ctx.api.deviceManager.listLocations.query() : [];
|
|
7827
|
+
const existingByStableId = /* @__PURE__ */ new Map();
|
|
7828
|
+
for (const dev of await this.ctx.kernel.devices.getAll()) {
|
|
7829
|
+
if (dev.parentDeviceId !== null) continue;
|
|
7830
|
+
existingByStableId.set(dev.stableId, dev);
|
|
7831
|
+
}
|
|
7827
7832
|
const adopted = [];
|
|
7828
7833
|
let failures = 0;
|
|
7834
|
+
let reused = 0;
|
|
7829
7835
|
for (const haDeviceId of childNativeIds) try {
|
|
7830
7836
|
const haDevice = haDeviceMap.get(haDeviceId);
|
|
7831
7837
|
const deviceName = (perCandidate?.[haDeviceId])?.name ?? haDevice?.name ?? haDeviceId;
|
|
7838
|
+
const existing = existingByStableId.get(`ha:${brokerId}:dev:${haDeviceId}`);
|
|
7839
|
+
if (existing) {
|
|
7840
|
+
const kids = await this.ctx.kernel.devices.getChildren(existing.id);
|
|
7841
|
+
adopted.push({
|
|
7842
|
+
childNativeId: haDeviceId,
|
|
7843
|
+
parentDeviceId: existing.id,
|
|
7844
|
+
accessoryDeviceIds: kids.map((c) => c.id)
|
|
7845
|
+
});
|
|
7846
|
+
reused += 1;
|
|
7847
|
+
this.ctx.logger.info("ha-adoption: candidate already adopted — returning existing", {
|
|
7848
|
+
tags: {
|
|
7849
|
+
deviceId: existing.id,
|
|
7850
|
+
integrationId
|
|
7851
|
+
},
|
|
7852
|
+
meta: {
|
|
7853
|
+
brokerId,
|
|
7854
|
+
haDeviceId,
|
|
7855
|
+
accessories: kids.length
|
|
7856
|
+
}
|
|
7857
|
+
});
|
|
7858
|
+
continue;
|
|
7859
|
+
}
|
|
7832
7860
|
const containerConfig = {
|
|
7833
7861
|
system: "homeassistant",
|
|
7834
7862
|
brokerId,
|
|
@@ -7848,6 +7876,19 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
|
|
|
7848
7876
|
parentDeviceId: summary.id,
|
|
7849
7877
|
accessoryDeviceIds
|
|
7850
7878
|
});
|
|
7879
|
+
this.ctx.logger.info("ha-adoption: candidate adopted", {
|
|
7880
|
+
tags: {
|
|
7881
|
+
deviceId: summary.id,
|
|
7882
|
+
integrationId
|
|
7883
|
+
},
|
|
7884
|
+
meta: {
|
|
7885
|
+
brokerId,
|
|
7886
|
+
haDeviceId,
|
|
7887
|
+
accessories: accessoryDeviceIds.length,
|
|
7888
|
+
index: adopted.length,
|
|
7889
|
+
of: childNativeIds.length
|
|
7890
|
+
}
|
|
7891
|
+
});
|
|
7851
7892
|
} catch (err) {
|
|
7852
7893
|
failures += 1;
|
|
7853
7894
|
this.ctx.logger.warn("ha-adoption: adopt failed for candidate (skipped)", {
|
|
@@ -7859,6 +7900,16 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
|
|
|
7859
7900
|
}
|
|
7860
7901
|
});
|
|
7861
7902
|
}
|
|
7903
|
+
this.ctx.logger.info("ha-adoption: adopt batch finished", {
|
|
7904
|
+
tags: { integrationId },
|
|
7905
|
+
meta: {
|
|
7906
|
+
brokerId,
|
|
7907
|
+
requested: childNativeIds.length,
|
|
7908
|
+
created: adopted.length - reused,
|
|
7909
|
+
alreadyAdopted: reused,
|
|
7910
|
+
failed: failures
|
|
7911
|
+
}
|
|
7912
|
+
});
|
|
7862
7913
|
if (failures > 0 && adopted.length === 0) throw new Error(`ha-adoption: all ${failures} adopt(s) failed for integration '${integrationId}'`);
|
|
7863
7914
|
return { adopted };
|
|
7864
7915
|
},
|
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
|
|
@@ -7823,11 +7823,39 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
7823
7823
|
const haDevices = await this.fetchHaDiscoveredDevices(brokerId);
|
|
7824
7824
|
const haDeviceMap = new Map(haDevices.map((d) => [d.haDeviceId, d]));
|
|
7825
7825
|
let knownLocations = importLocations === true ? await this.ctx.api.deviceManager.listLocations.query() : [];
|
|
7826
|
+
const existingByStableId = /* @__PURE__ */ new Map();
|
|
7827
|
+
for (const dev of await this.ctx.kernel.devices.getAll()) {
|
|
7828
|
+
if (dev.parentDeviceId !== null) continue;
|
|
7829
|
+
existingByStableId.set(dev.stableId, dev);
|
|
7830
|
+
}
|
|
7826
7831
|
const adopted = [];
|
|
7827
7832
|
let failures = 0;
|
|
7833
|
+
let reused = 0;
|
|
7828
7834
|
for (const haDeviceId of childNativeIds) try {
|
|
7829
7835
|
const haDevice = haDeviceMap.get(haDeviceId);
|
|
7830
7836
|
const deviceName = (perCandidate?.[haDeviceId])?.name ?? haDevice?.name ?? haDeviceId;
|
|
7837
|
+
const existing = existingByStableId.get(`ha:${brokerId}:dev:${haDeviceId}`);
|
|
7838
|
+
if (existing) {
|
|
7839
|
+
const kids = await this.ctx.kernel.devices.getChildren(existing.id);
|
|
7840
|
+
adopted.push({
|
|
7841
|
+
childNativeId: haDeviceId,
|
|
7842
|
+
parentDeviceId: existing.id,
|
|
7843
|
+
accessoryDeviceIds: kids.map((c) => c.id)
|
|
7844
|
+
});
|
|
7845
|
+
reused += 1;
|
|
7846
|
+
this.ctx.logger.info("ha-adoption: candidate already adopted — returning existing", {
|
|
7847
|
+
tags: {
|
|
7848
|
+
deviceId: existing.id,
|
|
7849
|
+
integrationId
|
|
7850
|
+
},
|
|
7851
|
+
meta: {
|
|
7852
|
+
brokerId,
|
|
7853
|
+
haDeviceId,
|
|
7854
|
+
accessories: kids.length
|
|
7855
|
+
}
|
|
7856
|
+
});
|
|
7857
|
+
continue;
|
|
7858
|
+
}
|
|
7831
7859
|
const containerConfig = {
|
|
7832
7860
|
system: "homeassistant",
|
|
7833
7861
|
brokerId,
|
|
@@ -7847,6 +7875,19 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
7847
7875
|
parentDeviceId: summary.id,
|
|
7848
7876
|
accessoryDeviceIds
|
|
7849
7877
|
});
|
|
7878
|
+
this.ctx.logger.info("ha-adoption: candidate adopted", {
|
|
7879
|
+
tags: {
|
|
7880
|
+
deviceId: summary.id,
|
|
7881
|
+
integrationId
|
|
7882
|
+
},
|
|
7883
|
+
meta: {
|
|
7884
|
+
brokerId,
|
|
7885
|
+
haDeviceId,
|
|
7886
|
+
accessories: accessoryDeviceIds.length,
|
|
7887
|
+
index: adopted.length,
|
|
7888
|
+
of: childNativeIds.length
|
|
7889
|
+
}
|
|
7890
|
+
});
|
|
7850
7891
|
} catch (err) {
|
|
7851
7892
|
failures += 1;
|
|
7852
7893
|
this.ctx.logger.warn("ha-adoption: adopt failed for candidate (skipped)", {
|
|
@@ -7858,6 +7899,16 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
7858
7899
|
}
|
|
7859
7900
|
});
|
|
7860
7901
|
}
|
|
7902
|
+
this.ctx.logger.info("ha-adoption: adopt batch finished", {
|
|
7903
|
+
tags: { integrationId },
|
|
7904
|
+
meta: {
|
|
7905
|
+
brokerId,
|
|
7906
|
+
requested: childNativeIds.length,
|
|
7907
|
+
created: adopted.length - reused,
|
|
7908
|
+
alreadyAdopted: reused,
|
|
7909
|
+
failed: failures
|
|
7910
|
+
}
|
|
7911
|
+
});
|
|
7861
7912
|
if (failures > 0 && adopted.length === 0) throw new Error(`ha-adoption: all ${failures} adopt(s) failed for integration '${integrationId}'`);
|
|
7862
7913
|
return { adopted };
|
|
7863
7914
|
},
|
|
@@ -7218,6 +7218,104 @@ object({
|
|
|
7218
7218
|
})
|
|
7219
7219
|
});
|
|
7220
7220
|
/**
|
|
7221
|
+
* Adoption job — the background form of `device-adoption.adopt`.
|
|
7222
|
+
*
|
|
7223
|
+
* ## Why this exists
|
|
7224
|
+
*
|
|
7225
|
+
* `adopt({childNativeIds: [...]})` materialises one CamStack device per
|
|
7226
|
+
* candidate PLUS every accessory child, and the whole array shares ONE UDS
|
|
7227
|
+
* request deadline (60s). Measured on the live hub against Home Assistant:
|
|
7228
|
+
* each device the kernel creates costs ~450 ms — `devices.create` pre-seeds
|
|
7229
|
+
* meta with up to eleven SEQUENTIAL round trips (`setName`, `setType`,
|
|
7230
|
+
* `setRole`, … `persistConfig`) before the class is constructed — and an
|
|
7231
|
+
* accessory child costs the same as its parent. So the real unit of work is
|
|
7232
|
+
* the CHILD, not the candidate:
|
|
7233
|
+
*
|
|
7234
|
+
* - 25 candidates averaging 6 children → ~150 devices → **>60s, times out**
|
|
7235
|
+
* - ONE candidate with 217 children → ~217 devices → **>60s, times out**
|
|
7236
|
+
*
|
|
7237
|
+
* That second line is why this is a job and not a smaller batch. No chunking,
|
|
7238
|
+
* no bounded concurrency over candidates and no per-call tuning can fix a
|
|
7239
|
+
* shape where **N=1 already exceeds the deadline** — the count that blows the
|
|
7240
|
+
* budget is the source system's accessory fan-out, which the operator does not
|
|
7241
|
+
* choose and cannot see. A design that only works below some N is the same bug
|
|
7242
|
+
* deferred.
|
|
7243
|
+
*
|
|
7244
|
+
* ## What the timeout did NOT do
|
|
7245
|
+
*
|
|
7246
|
+
* It did not stop the work. The UDS deadline ends the CALLER's wait; the
|
|
7247
|
+
* provider's loop runs to completion. Measured: a 25-candidate adopt that
|
|
7248
|
+
* "failed" at 60s had adopted 17 by 87s and all 25 by ~130s. The operator saw
|
|
7249
|
+
* an error and had no way to learn that. Every field below exists so that
|
|
7250
|
+
* question has an answer.
|
|
7251
|
+
*
|
|
7252
|
+
* ## Idempotency
|
|
7253
|
+
*
|
|
7254
|
+
* Jobs are in-RAM; a restart forgets them. That is safe here because adoption
|
|
7255
|
+
* is keyed by a stable id (`ha:<broker>:dev:<nativeId>` and equivalents), so
|
|
7256
|
+
* re-running a job re-adopts nothing: an already-adopted candidate is SKIPPED
|
|
7257
|
+
* by the engine before any provider call and lands in `alreadyAdopted`. It is
|
|
7258
|
+
* never a duplicate device, and never an error the operator has to interpret.
|
|
7259
|
+
*/
|
|
7260
|
+
var AdoptionJobStateSchema = _enum([
|
|
7261
|
+
"running",
|
|
7262
|
+
"done",
|
|
7263
|
+
"failed",
|
|
7264
|
+
"cancelled"
|
|
7265
|
+
]);
|
|
7266
|
+
/**
|
|
7267
|
+
* Per-candidate result. Every candidate the job was asked to adopt ends in
|
|
7268
|
+
* exactly one of these buckets — there is no silent drop, and the operator can
|
|
7269
|
+
* always answer "which of my 25 landed?".
|
|
7270
|
+
*
|
|
7271
|
+
* - `adopted` — created now by this job.
|
|
7272
|
+
* - `already-adopted` — a device for this candidate existed before the job
|
|
7273
|
+
* reached it (a re-run, or a retry after a timeout). Not an error.
|
|
7274
|
+
* - `failed` — the provider threw; `error` carries the message.
|
|
7275
|
+
* - `cancelled` — the operator cancelled before this candidate was reached.
|
|
7276
|
+
*/
|
|
7277
|
+
var AdoptionOutcomeSchema = _enum([
|
|
7278
|
+
"adopted",
|
|
7279
|
+
"already-adopted",
|
|
7280
|
+
"failed",
|
|
7281
|
+
"cancelled"
|
|
7282
|
+
]);
|
|
7283
|
+
var AdoptionCandidateResultSchema = object({
|
|
7284
|
+
childNativeId: string(),
|
|
7285
|
+
outcome: AdoptionOutcomeSchema,
|
|
7286
|
+
/** The materialised parent device id — null for `failed` / `cancelled`. */
|
|
7287
|
+
parentDeviceId: number().int().nonnegative().nullable(),
|
|
7288
|
+
/** Accessory children created for this candidate. */
|
|
7289
|
+
accessoryCount: number().int().nonnegative(),
|
|
7290
|
+
/** Failure message; null unless `outcome === 'failed'`. */
|
|
7291
|
+
error: string().nullable()
|
|
7292
|
+
});
|
|
7293
|
+
var AdoptionJobSchema = object({
|
|
7294
|
+
jobId: string(),
|
|
7295
|
+
/** The integration provider this job adopts through (the `addonId` pin). */
|
|
7296
|
+
addonId: string(),
|
|
7297
|
+
integrationId: string(),
|
|
7298
|
+
state: AdoptionJobStateSchema,
|
|
7299
|
+
/** Candidates the job was asked to adopt. Known up front, so never null. */
|
|
7300
|
+
total: number().int().nonnegative(),
|
|
7301
|
+
/** Candidates that have reached a terminal bucket. */
|
|
7302
|
+
processed: number().int().nonnegative(),
|
|
7303
|
+
adopted: number().int().nonnegative(),
|
|
7304
|
+
alreadyAdopted: number().int().nonnegative(),
|
|
7305
|
+
failed: number().int().nonnegative(),
|
|
7306
|
+
/** Accessory child devices created across every candidate — the real unit
|
|
7307
|
+
* of work, surfaced so a slow job is legible rather than mysterious. */
|
|
7308
|
+
accessoriesCreated: number().int().nonnegative(),
|
|
7309
|
+
/** The candidate currently being adopted; null when idle or finished. */
|
|
7310
|
+
currentChildNativeId: string().nullable(),
|
|
7311
|
+
/** One entry per candidate, in the order they were processed. */
|
|
7312
|
+
results: array(AdoptionCandidateResultSchema).readonly(),
|
|
7313
|
+
startedAt: number(),
|
|
7314
|
+
finishedAt: number().nullable(),
|
|
7315
|
+
/** Set only when the job itself broke (not a per-candidate failure). */
|
|
7316
|
+
error: string().nullable()
|
|
7317
|
+
});
|
|
7318
|
+
/**
|
|
7221
7319
|
* Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
|
|
7222
7320
|
* pipeline functions an operator thinks in terms of.
|
|
7223
7321
|
*
|
|
@@ -12289,6 +12387,15 @@ method(object({
|
|
|
12289
12387
|
}), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
|
|
12290
12388
|
kind: "mutation",
|
|
12291
12389
|
auth: "admin"
|
|
12390
|
+
}), method(AdoptInputSchema.extend({ addonId: string() }), object({ jobId: string() }), {
|
|
12391
|
+
kind: "mutation",
|
|
12392
|
+
auth: "admin"
|
|
12393
|
+
}), method(object({
|
|
12394
|
+
addonId: string(),
|
|
12395
|
+
integrationId: string().optional()
|
|
12396
|
+
}), array(AdoptionJobSchema).readonly(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
12397
|
+
kind: "mutation",
|
|
12398
|
+
auth: "admin"
|
|
12292
12399
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
12293
12400
|
kind: "mutation",
|
|
12294
12401
|
auth: "admin"
|
|
@@ -15210,6 +15317,65 @@ object({
|
|
|
15210
15317
|
* Each provider returns a static descriptor; the core enumerates them
|
|
15211
15318
|
* to validate the `integration=` query param and resolve the consent
|
|
15212
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.
|
|
15213
15379
|
*/
|
|
15214
15380
|
var OauthIntegrationDescriptorSchema = object({
|
|
15215
15381
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -15242,7 +15408,13 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15242
15408
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15243
15409
|
hubUrl: string().optional()
|
|
15244
15410
|
});
|
|
15245
|
-
|
|
15411
|
+
var oauthIntegrationCapability = {
|
|
15412
|
+
name: "oauth-integration",
|
|
15413
|
+
scope: "system",
|
|
15414
|
+
mode: "collection",
|
|
15415
|
+
internal: true,
|
|
15416
|
+
methods: { getDescriptor: method(_void(), OauthIntegrationDescriptorSchema) }
|
|
15417
|
+
};
|
|
15246
15418
|
/**
|
|
15247
15419
|
* pipeline-analytics — device-scoped wrapper cap. Refines raw
|
|
15248
15420
|
* per-frame detections emitted by the pipeline runner into tracked
|
|
@@ -20451,6 +20623,37 @@ onColorChanged: { data: object({
|
|
|
20451
20623
|
*/
|
|
20452
20624
|
runtimeState: ColorStatusSchema
|
|
20453
20625
|
};
|
|
20626
|
+
var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
|
|
20627
|
+
object({
|
|
20628
|
+
outcome: literal("validated"),
|
|
20629
|
+
/** Round-trip of the sign-in, when the provider measured it. */
|
|
20630
|
+
latencyMs: number().nonnegative().optional(),
|
|
20631
|
+
/** Optional human detail worth showing next to the tick
|
|
20632
|
+
* ("3 devices visible on this account"). */
|
|
20633
|
+
detail: string().optional()
|
|
20634
|
+
}).strict(),
|
|
20635
|
+
object({
|
|
20636
|
+
outcome: literal("rejected"),
|
|
20637
|
+
error: string()
|
|
20638
|
+
}).strict(),
|
|
20639
|
+
object({
|
|
20640
|
+
outcome: literal("inconclusive"),
|
|
20641
|
+
error: string()
|
|
20642
|
+
}).strict()
|
|
20643
|
+
]);
|
|
20644
|
+
var ConnectionTestInputSchema = object({
|
|
20645
|
+
/** Candidate integration settings, exactly as the create form collected them. */
|
|
20646
|
+
settings: record(string(), unknown()) });
|
|
20647
|
+
/**
|
|
20648
|
+
* What the provider's test actually DOES, so the UI can say it in words before
|
|
20649
|
+
* the operator presses the button ("Signs in to the Dreo cloud"). Purely
|
|
20650
|
+
* descriptive — it never changes routing.
|
|
20651
|
+
*/
|
|
20652
|
+
var ConnectionTestDescriptorSchema = object({ label: string() });
|
|
20653
|
+
method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
|
|
20654
|
+
kind: "mutation",
|
|
20655
|
+
auth: "admin"
|
|
20656
|
+
}), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
|
|
20454
20657
|
/**
|
|
20455
20658
|
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
20456
20659
|
* which is the kernel-managed online/offline flag for the device's
|
|
@@ -21812,15 +22015,57 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
21812
22015
|
* flow can import (e.g. HA areas). Drives the adopt modal's "import
|
|
21813
22016
|
* locations" checkbox. Provider-declared in the addon manifest. */
|
|
21814
22017
|
supportsLocationImport: boolean(),
|
|
22018
|
+
/**
|
|
22019
|
+
* True when this integration DECLARES a pre-creation test (the
|
|
22020
|
+
* `connection-test` cap, or a broker whose settings it stores). Drives the
|
|
22021
|
+
* Test button: an integration that cannot be tested must say so up front
|
|
22022
|
+
* rather than offering a button that always answers the same nonsense.
|
|
22023
|
+
*/
|
|
22024
|
+
canTest: boolean(),
|
|
21815
22025
|
existingInstances: array(object({
|
|
21816
22026
|
id: string(),
|
|
21817
22027
|
name: string()
|
|
21818
22028
|
})),
|
|
21819
22029
|
canAdd: boolean()
|
|
21820
22030
|
});
|
|
22031
|
+
/**
|
|
22032
|
+
* Why a test could not be answered as a plain boolean.
|
|
22033
|
+
*
|
|
22034
|
+
* `success` alone collapsed four different situations into one red box, and the
|
|
22035
|
+
* one that mattered most — "nobody ever asked the remote anything" — looked
|
|
22036
|
+
* exactly like "the remote said no". The status is the discriminator:
|
|
22037
|
+
*
|
|
22038
|
+
* - `validated` — a provider-declared test ran and the remote ACCEPTED.
|
|
22039
|
+
* - `rejected` — a provider-declared test ran and the remote REFUSED.
|
|
22040
|
+
* The only status that blocks `integrations.create`.
|
|
22041
|
+
* - `inconclusive` — a test IS declared but could not complete (timeout,
|
|
22042
|
+
* DNS, 5xx). Nothing was observed; not a failure.
|
|
22043
|
+
* - `unsupported` — this integration declares NO test. Nothing was
|
|
22044
|
+
* observed either; not a failure, and not a pass.
|
|
22045
|
+
*
|
|
22046
|
+
* `unsupported` and `inconclusive` both carry `success: false` so an older
|
|
22047
|
+
* client can never read them as a green tick, and both carry an `error` string
|
|
22048
|
+
* that SAYS the test did not run rather than inventing a failure.
|
|
22049
|
+
*/
|
|
22050
|
+
var TestConnectionStatusEnum = _enum([
|
|
22051
|
+
"validated",
|
|
22052
|
+
"rejected",
|
|
22053
|
+
"inconclusive",
|
|
22054
|
+
"unsupported"
|
|
22055
|
+
]);
|
|
21821
22056
|
var TestConnectionResultSchema$1 = object({
|
|
22057
|
+
/** True ONLY for `validated`. Never true for a test that did not run. */
|
|
21822
22058
|
success: boolean(),
|
|
21823
|
-
error: string().optional()
|
|
22059
|
+
error: string().optional(),
|
|
22060
|
+
/** Optional for wire back-compat with clients built before the tri-state;
|
|
22061
|
+
* the server always sets it. */
|
|
22062
|
+
status: TestConnectionStatusEnum.optional(),
|
|
22063
|
+
/** Addon id whose declared test answered — `null` when none did. Lets the UI
|
|
22064
|
+
* attribute a result instead of blaming "the integration". */
|
|
22065
|
+
testedBy: string().nullable().optional(),
|
|
22066
|
+
latencyMs: number().nonnegative().optional(),
|
|
22067
|
+
/** Human detail from a `validated` result ("3 devices on this account"). */
|
|
22068
|
+
detail: string().optional()
|
|
21824
22069
|
});
|
|
21825
22070
|
var CreateIntegrationInputSchema = object({
|
|
21826
22071
|
addonId: string(),
|
|
@@ -28043,6 +28288,18 @@ Object.freeze({
|
|
|
28043
28288
|
addonId: null,
|
|
28044
28289
|
access: "create"
|
|
28045
28290
|
},
|
|
28291
|
+
"connectionTest.describeTest": {
|
|
28292
|
+
capName: "connection-test",
|
|
28293
|
+
capScope: "system",
|
|
28294
|
+
addonId: null,
|
|
28295
|
+
access: "view"
|
|
28296
|
+
},
|
|
28297
|
+
"connectionTest.testSettings": {
|
|
28298
|
+
capName: "connection-test",
|
|
28299
|
+
capScope: "system",
|
|
28300
|
+
addonId: null,
|
|
28301
|
+
access: "create"
|
|
28302
|
+
},
|
|
28046
28303
|
"consumables.reset": {
|
|
28047
28304
|
capName: "consumables",
|
|
28048
28305
|
capScope: "device",
|
|
@@ -28433,6 +28690,12 @@ Object.freeze({
|
|
|
28433
28690
|
addonId: null,
|
|
28434
28691
|
access: "create"
|
|
28435
28692
|
},
|
|
28693
|
+
"deviceManager.adoptionCancelJob": {
|
|
28694
|
+
capName: "device-manager",
|
|
28695
|
+
capScope: "system",
|
|
28696
|
+
addonId: null,
|
|
28697
|
+
access: "create"
|
|
28698
|
+
},
|
|
28436
28699
|
"deviceManager.adoptionListCandidateFilters": {
|
|
28437
28700
|
capName: "device-manager",
|
|
28438
28701
|
capScope: "system",
|
|
@@ -28445,6 +28708,12 @@ Object.freeze({
|
|
|
28445
28708
|
addonId: null,
|
|
28446
28709
|
access: "view"
|
|
28447
28710
|
},
|
|
28711
|
+
"deviceManager.adoptionListJobs": {
|
|
28712
|
+
capName: "device-manager",
|
|
28713
|
+
capScope: "system",
|
|
28714
|
+
addonId: null,
|
|
28715
|
+
access: "view"
|
|
28716
|
+
},
|
|
28448
28717
|
"deviceManager.adoptionRefresh": {
|
|
28449
28718
|
capName: "device-manager",
|
|
28450
28719
|
capScope: "system",
|
|
@@ -28463,6 +28732,12 @@ Object.freeze({
|
|
|
28463
28732
|
addonId: null,
|
|
28464
28733
|
access: "create"
|
|
28465
28734
|
},
|
|
28735
|
+
"deviceManager.adoptionStartJob": {
|
|
28736
|
+
capName: "device-manager",
|
|
28737
|
+
capScope: "system",
|
|
28738
|
+
addonId: null,
|
|
28739
|
+
access: "create"
|
|
28740
|
+
},
|
|
28466
28741
|
"deviceManager.allocateDeviceId": {
|
|
28467
28742
|
capName: "device-manager",
|
|
28468
28743
|
capScope: "system",
|
|
@@ -33477,6 +33752,12 @@ Object.defineProperty(exports, "numericSensorCapability", {
|
|
|
33477
33752
|
return numericSensorCapability;
|
|
33478
33753
|
}
|
|
33479
33754
|
});
|
|
33755
|
+
Object.defineProperty(exports, "oauthIntegrationCapability", {
|
|
33756
|
+
enumerable: true,
|
|
33757
|
+
get: function() {
|
|
33758
|
+
return oauthIntegrationCapability;
|
|
33759
|
+
}
|
|
33760
|
+
});
|
|
33480
33761
|
Object.defineProperty(exports, "object", {
|
|
33481
33762
|
enumerable: true,
|
|
33482
33763
|
get: function() {
|
|
@@ -7218,6 +7218,104 @@ object({
|
|
|
7218
7218
|
})
|
|
7219
7219
|
});
|
|
7220
7220
|
/**
|
|
7221
|
+
* Adoption job — the background form of `device-adoption.adopt`.
|
|
7222
|
+
*
|
|
7223
|
+
* ## Why this exists
|
|
7224
|
+
*
|
|
7225
|
+
* `adopt({childNativeIds: [...]})` materialises one CamStack device per
|
|
7226
|
+
* candidate PLUS every accessory child, and the whole array shares ONE UDS
|
|
7227
|
+
* request deadline (60s). Measured on the live hub against Home Assistant:
|
|
7228
|
+
* each device the kernel creates costs ~450 ms — `devices.create` pre-seeds
|
|
7229
|
+
* meta with up to eleven SEQUENTIAL round trips (`setName`, `setType`,
|
|
7230
|
+
* `setRole`, … `persistConfig`) before the class is constructed — and an
|
|
7231
|
+
* accessory child costs the same as its parent. So the real unit of work is
|
|
7232
|
+
* the CHILD, not the candidate:
|
|
7233
|
+
*
|
|
7234
|
+
* - 25 candidates averaging 6 children → ~150 devices → **>60s, times out**
|
|
7235
|
+
* - ONE candidate with 217 children → ~217 devices → **>60s, times out**
|
|
7236
|
+
*
|
|
7237
|
+
* That second line is why this is a job and not a smaller batch. No chunking,
|
|
7238
|
+
* no bounded concurrency over candidates and no per-call tuning can fix a
|
|
7239
|
+
* shape where **N=1 already exceeds the deadline** — the count that blows the
|
|
7240
|
+
* budget is the source system's accessory fan-out, which the operator does not
|
|
7241
|
+
* choose and cannot see. A design that only works below some N is the same bug
|
|
7242
|
+
* deferred.
|
|
7243
|
+
*
|
|
7244
|
+
* ## What the timeout did NOT do
|
|
7245
|
+
*
|
|
7246
|
+
* It did not stop the work. The UDS deadline ends the CALLER's wait; the
|
|
7247
|
+
* provider's loop runs to completion. Measured: a 25-candidate adopt that
|
|
7248
|
+
* "failed" at 60s had adopted 17 by 87s and all 25 by ~130s. The operator saw
|
|
7249
|
+
* an error and had no way to learn that. Every field below exists so that
|
|
7250
|
+
* question has an answer.
|
|
7251
|
+
*
|
|
7252
|
+
* ## Idempotency
|
|
7253
|
+
*
|
|
7254
|
+
* Jobs are in-RAM; a restart forgets them. That is safe here because adoption
|
|
7255
|
+
* is keyed by a stable id (`ha:<broker>:dev:<nativeId>` and equivalents), so
|
|
7256
|
+
* re-running a job re-adopts nothing: an already-adopted candidate is SKIPPED
|
|
7257
|
+
* by the engine before any provider call and lands in `alreadyAdopted`. It is
|
|
7258
|
+
* never a duplicate device, and never an error the operator has to interpret.
|
|
7259
|
+
*/
|
|
7260
|
+
var AdoptionJobStateSchema = _enum([
|
|
7261
|
+
"running",
|
|
7262
|
+
"done",
|
|
7263
|
+
"failed",
|
|
7264
|
+
"cancelled"
|
|
7265
|
+
]);
|
|
7266
|
+
/**
|
|
7267
|
+
* Per-candidate result. Every candidate the job was asked to adopt ends in
|
|
7268
|
+
* exactly one of these buckets — there is no silent drop, and the operator can
|
|
7269
|
+
* always answer "which of my 25 landed?".
|
|
7270
|
+
*
|
|
7271
|
+
* - `adopted` — created now by this job.
|
|
7272
|
+
* - `already-adopted` — a device for this candidate existed before the job
|
|
7273
|
+
* reached it (a re-run, or a retry after a timeout). Not an error.
|
|
7274
|
+
* - `failed` — the provider threw; `error` carries the message.
|
|
7275
|
+
* - `cancelled` — the operator cancelled before this candidate was reached.
|
|
7276
|
+
*/
|
|
7277
|
+
var AdoptionOutcomeSchema = _enum([
|
|
7278
|
+
"adopted",
|
|
7279
|
+
"already-adopted",
|
|
7280
|
+
"failed",
|
|
7281
|
+
"cancelled"
|
|
7282
|
+
]);
|
|
7283
|
+
var AdoptionCandidateResultSchema = object({
|
|
7284
|
+
childNativeId: string(),
|
|
7285
|
+
outcome: AdoptionOutcomeSchema,
|
|
7286
|
+
/** The materialised parent device id — null for `failed` / `cancelled`. */
|
|
7287
|
+
parentDeviceId: number().int().nonnegative().nullable(),
|
|
7288
|
+
/** Accessory children created for this candidate. */
|
|
7289
|
+
accessoryCount: number().int().nonnegative(),
|
|
7290
|
+
/** Failure message; null unless `outcome === 'failed'`. */
|
|
7291
|
+
error: string().nullable()
|
|
7292
|
+
});
|
|
7293
|
+
var AdoptionJobSchema = object({
|
|
7294
|
+
jobId: string(),
|
|
7295
|
+
/** The integration provider this job adopts through (the `addonId` pin). */
|
|
7296
|
+
addonId: string(),
|
|
7297
|
+
integrationId: string(),
|
|
7298
|
+
state: AdoptionJobStateSchema,
|
|
7299
|
+
/** Candidates the job was asked to adopt. Known up front, so never null. */
|
|
7300
|
+
total: number().int().nonnegative(),
|
|
7301
|
+
/** Candidates that have reached a terminal bucket. */
|
|
7302
|
+
processed: number().int().nonnegative(),
|
|
7303
|
+
adopted: number().int().nonnegative(),
|
|
7304
|
+
alreadyAdopted: number().int().nonnegative(),
|
|
7305
|
+
failed: number().int().nonnegative(),
|
|
7306
|
+
/** Accessory child devices created across every candidate — the real unit
|
|
7307
|
+
* of work, surfaced so a slow job is legible rather than mysterious. */
|
|
7308
|
+
accessoriesCreated: number().int().nonnegative(),
|
|
7309
|
+
/** The candidate currently being adopted; null when idle or finished. */
|
|
7310
|
+
currentChildNativeId: string().nullable(),
|
|
7311
|
+
/** One entry per candidate, in the order they were processed. */
|
|
7312
|
+
results: array(AdoptionCandidateResultSchema).readonly(),
|
|
7313
|
+
startedAt: number(),
|
|
7314
|
+
finishedAt: number().nullable(),
|
|
7315
|
+
/** Set only when the job itself broke (not a per-candidate failure). */
|
|
7316
|
+
error: string().nullable()
|
|
7317
|
+
});
|
|
7318
|
+
/**
|
|
7221
7319
|
* Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
|
|
7222
7320
|
* pipeline functions an operator thinks in terms of.
|
|
7223
7321
|
*
|
|
@@ -12289,6 +12387,15 @@ method(object({
|
|
|
12289
12387
|
}), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
|
|
12290
12388
|
kind: "mutation",
|
|
12291
12389
|
auth: "admin"
|
|
12390
|
+
}), method(AdoptInputSchema.extend({ addonId: string() }), object({ jobId: string() }), {
|
|
12391
|
+
kind: "mutation",
|
|
12392
|
+
auth: "admin"
|
|
12393
|
+
}), method(object({
|
|
12394
|
+
addonId: string(),
|
|
12395
|
+
integrationId: string().optional()
|
|
12396
|
+
}), array(AdoptionJobSchema).readonly(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
12397
|
+
kind: "mutation",
|
|
12398
|
+
auth: "admin"
|
|
12292
12399
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
12293
12400
|
kind: "mutation",
|
|
12294
12401
|
auth: "admin"
|
|
@@ -15210,6 +15317,65 @@ object({
|
|
|
15210
15317
|
* Each provider returns a static descriptor; the core enumerates them
|
|
15211
15318
|
* to validate the `integration=` query param and resolve the consent
|
|
15212
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.
|
|
15213
15379
|
*/
|
|
15214
15380
|
var OauthIntegrationDescriptorSchema = object({
|
|
15215
15381
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -15242,7 +15408,13 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15242
15408
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15243
15409
|
hubUrl: string().optional()
|
|
15244
15410
|
});
|
|
15245
|
-
|
|
15411
|
+
var oauthIntegrationCapability = {
|
|
15412
|
+
name: "oauth-integration",
|
|
15413
|
+
scope: "system",
|
|
15414
|
+
mode: "collection",
|
|
15415
|
+
internal: true,
|
|
15416
|
+
methods: { getDescriptor: method(_void(), OauthIntegrationDescriptorSchema) }
|
|
15417
|
+
};
|
|
15246
15418
|
/**
|
|
15247
15419
|
* pipeline-analytics — device-scoped wrapper cap. Refines raw
|
|
15248
15420
|
* per-frame detections emitted by the pipeline runner into tracked
|
|
@@ -20451,6 +20623,37 @@ onColorChanged: { data: object({
|
|
|
20451
20623
|
*/
|
|
20452
20624
|
runtimeState: ColorStatusSchema
|
|
20453
20625
|
};
|
|
20626
|
+
var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
|
|
20627
|
+
object({
|
|
20628
|
+
outcome: literal("validated"),
|
|
20629
|
+
/** Round-trip of the sign-in, when the provider measured it. */
|
|
20630
|
+
latencyMs: number().nonnegative().optional(),
|
|
20631
|
+
/** Optional human detail worth showing next to the tick
|
|
20632
|
+
* ("3 devices visible on this account"). */
|
|
20633
|
+
detail: string().optional()
|
|
20634
|
+
}).strict(),
|
|
20635
|
+
object({
|
|
20636
|
+
outcome: literal("rejected"),
|
|
20637
|
+
error: string()
|
|
20638
|
+
}).strict(),
|
|
20639
|
+
object({
|
|
20640
|
+
outcome: literal("inconclusive"),
|
|
20641
|
+
error: string()
|
|
20642
|
+
}).strict()
|
|
20643
|
+
]);
|
|
20644
|
+
var ConnectionTestInputSchema = object({
|
|
20645
|
+
/** Candidate integration settings, exactly as the create form collected them. */
|
|
20646
|
+
settings: record(string(), unknown()) });
|
|
20647
|
+
/**
|
|
20648
|
+
* What the provider's test actually DOES, so the UI can say it in words before
|
|
20649
|
+
* the operator presses the button ("Signs in to the Dreo cloud"). Purely
|
|
20650
|
+
* descriptive — it never changes routing.
|
|
20651
|
+
*/
|
|
20652
|
+
var ConnectionTestDescriptorSchema = object({ label: string() });
|
|
20653
|
+
method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
|
|
20654
|
+
kind: "mutation",
|
|
20655
|
+
auth: "admin"
|
|
20656
|
+
}), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
|
|
20454
20657
|
/**
|
|
20455
20658
|
* Upstream-system connectivity sensor — distinct from `device-status`,
|
|
20456
20659
|
* which is the kernel-managed online/offline flag for the device's
|
|
@@ -21812,15 +22015,57 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
21812
22015
|
* flow can import (e.g. HA areas). Drives the adopt modal's "import
|
|
21813
22016
|
* locations" checkbox. Provider-declared in the addon manifest. */
|
|
21814
22017
|
supportsLocationImport: boolean(),
|
|
22018
|
+
/**
|
|
22019
|
+
* True when this integration DECLARES a pre-creation test (the
|
|
22020
|
+
* `connection-test` cap, or a broker whose settings it stores). Drives the
|
|
22021
|
+
* Test button: an integration that cannot be tested must say so up front
|
|
22022
|
+
* rather than offering a button that always answers the same nonsense.
|
|
22023
|
+
*/
|
|
22024
|
+
canTest: boolean(),
|
|
21815
22025
|
existingInstances: array(object({
|
|
21816
22026
|
id: string(),
|
|
21817
22027
|
name: string()
|
|
21818
22028
|
})),
|
|
21819
22029
|
canAdd: boolean()
|
|
21820
22030
|
});
|
|
22031
|
+
/**
|
|
22032
|
+
* Why a test could not be answered as a plain boolean.
|
|
22033
|
+
*
|
|
22034
|
+
* `success` alone collapsed four different situations into one red box, and the
|
|
22035
|
+
* one that mattered most — "nobody ever asked the remote anything" — looked
|
|
22036
|
+
* exactly like "the remote said no". The status is the discriminator:
|
|
22037
|
+
*
|
|
22038
|
+
* - `validated` — a provider-declared test ran and the remote ACCEPTED.
|
|
22039
|
+
* - `rejected` — a provider-declared test ran and the remote REFUSED.
|
|
22040
|
+
* The only status that blocks `integrations.create`.
|
|
22041
|
+
* - `inconclusive` — a test IS declared but could not complete (timeout,
|
|
22042
|
+
* DNS, 5xx). Nothing was observed; not a failure.
|
|
22043
|
+
* - `unsupported` — this integration declares NO test. Nothing was
|
|
22044
|
+
* observed either; not a failure, and not a pass.
|
|
22045
|
+
*
|
|
22046
|
+
* `unsupported` and `inconclusive` both carry `success: false` so an older
|
|
22047
|
+
* client can never read them as a green tick, and both carry an `error` string
|
|
22048
|
+
* that SAYS the test did not run rather than inventing a failure.
|
|
22049
|
+
*/
|
|
22050
|
+
var TestConnectionStatusEnum = _enum([
|
|
22051
|
+
"validated",
|
|
22052
|
+
"rejected",
|
|
22053
|
+
"inconclusive",
|
|
22054
|
+
"unsupported"
|
|
22055
|
+
]);
|
|
21821
22056
|
var TestConnectionResultSchema$1 = object({
|
|
22057
|
+
/** True ONLY for `validated`. Never true for a test that did not run. */
|
|
21822
22058
|
success: boolean(),
|
|
21823
|
-
error: string().optional()
|
|
22059
|
+
error: string().optional(),
|
|
22060
|
+
/** Optional for wire back-compat with clients built before the tri-state;
|
|
22061
|
+
* the server always sets it. */
|
|
22062
|
+
status: TestConnectionStatusEnum.optional(),
|
|
22063
|
+
/** Addon id whose declared test answered — `null` when none did. Lets the UI
|
|
22064
|
+
* attribute a result instead of blaming "the integration". */
|
|
22065
|
+
testedBy: string().nullable().optional(),
|
|
22066
|
+
latencyMs: number().nonnegative().optional(),
|
|
22067
|
+
/** Human detail from a `validated` result ("3 devices on this account"). */
|
|
22068
|
+
detail: string().optional()
|
|
21824
22069
|
});
|
|
21825
22070
|
var CreateIntegrationInputSchema = object({
|
|
21826
22071
|
addonId: string(),
|
|
@@ -28043,6 +28288,18 @@ Object.freeze({
|
|
|
28043
28288
|
addonId: null,
|
|
28044
28289
|
access: "create"
|
|
28045
28290
|
},
|
|
28291
|
+
"connectionTest.describeTest": {
|
|
28292
|
+
capName: "connection-test",
|
|
28293
|
+
capScope: "system",
|
|
28294
|
+
addonId: null,
|
|
28295
|
+
access: "view"
|
|
28296
|
+
},
|
|
28297
|
+
"connectionTest.testSettings": {
|
|
28298
|
+
capName: "connection-test",
|
|
28299
|
+
capScope: "system",
|
|
28300
|
+
addonId: null,
|
|
28301
|
+
access: "create"
|
|
28302
|
+
},
|
|
28046
28303
|
"consumables.reset": {
|
|
28047
28304
|
capName: "consumables",
|
|
28048
28305
|
capScope: "device",
|
|
@@ -28433,6 +28690,12 @@ Object.freeze({
|
|
|
28433
28690
|
addonId: null,
|
|
28434
28691
|
access: "create"
|
|
28435
28692
|
},
|
|
28693
|
+
"deviceManager.adoptionCancelJob": {
|
|
28694
|
+
capName: "device-manager",
|
|
28695
|
+
capScope: "system",
|
|
28696
|
+
addonId: null,
|
|
28697
|
+
access: "create"
|
|
28698
|
+
},
|
|
28436
28699
|
"deviceManager.adoptionListCandidateFilters": {
|
|
28437
28700
|
capName: "device-manager",
|
|
28438
28701
|
capScope: "system",
|
|
@@ -28445,6 +28708,12 @@ Object.freeze({
|
|
|
28445
28708
|
addonId: null,
|
|
28446
28709
|
access: "view"
|
|
28447
28710
|
},
|
|
28711
|
+
"deviceManager.adoptionListJobs": {
|
|
28712
|
+
capName: "device-manager",
|
|
28713
|
+
capScope: "system",
|
|
28714
|
+
addonId: null,
|
|
28715
|
+
access: "view"
|
|
28716
|
+
},
|
|
28448
28717
|
"deviceManager.adoptionRefresh": {
|
|
28449
28718
|
capName: "device-manager",
|
|
28450
28719
|
capScope: "system",
|
|
@@ -28463,6 +28732,12 @@ Object.freeze({
|
|
|
28463
28732
|
addonId: null,
|
|
28464
28733
|
access: "create"
|
|
28465
28734
|
},
|
|
28735
|
+
"deviceManager.adoptionStartJob": {
|
|
28736
|
+
capName: "device-manager",
|
|
28737
|
+
capScope: "system",
|
|
28738
|
+
addonId: null,
|
|
28739
|
+
access: "create"
|
|
28740
|
+
},
|
|
28466
28741
|
"deviceManager.allocateDeviceId": {
|
|
28467
28742
|
capName: "device-manager",
|
|
28468
28743
|
capScope: "system",
|
|
@@ -33141,4 +33416,4 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
|
|
|
33141
33416
|
return best;
|
|
33142
33417
|
}
|
|
33143
33418
|
//#endregion
|
|
33144
|
-
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
|
}
|