@camstack/addon-provider-reolink 1.2.220 → 1.2.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/addon.js +1377 -769
- package/dist/addon.mjs +1377 -769
- package/package.json +2 -2
package/dist/addon.mjs
CHANGED
|
@@ -101473,7 +101473,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
101473
101473
|
return EventCategory;
|
|
101474
101474
|
}({});
|
|
101475
101475
|
//#endregion
|
|
101476
|
-
//#region ../types/dist/device-type-
|
|
101476
|
+
//#region ../types/dist/device-type-D1fH5kaC.mjs
|
|
101477
101477
|
/**
|
|
101478
101478
|
import { errMsg } from '@camstack/types'
|
|
101479
101479
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -101704,6 +101704,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
101704
101704
|
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
101705
101705
|
/** Media player exposes stop distinct from pause. */
|
|
101706
101706
|
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
101707
|
+
/** Media player exposes volume up / down in steps (an infrared TV has no level). */
|
|
101708
|
+
DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
|
|
101709
|
+
/** Media player exposes channel up / down in steps. */
|
|
101710
|
+
DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
|
|
101711
|
+
/** Media player exposes power on / off. */
|
|
101712
|
+
DeviceFeature["MediaPlayerPower"] = "media-player-power";
|
|
101707
101713
|
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
101708
101714
|
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
101709
101715
|
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
@@ -106530,7 +106536,7 @@ var ZodIssueCode = {
|
|
|
106530
106536
|
var ZodFirstPartyTypeKind;
|
|
106531
106537
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106532
106538
|
//#endregion
|
|
106533
|
-
//#region ../types/dist/sleep-
|
|
106539
|
+
//#region ../types/dist/sleep-Cv4Zmie9.mjs
|
|
106534
106540
|
/**
|
|
106535
106541
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106536
106542
|
* window to float samples (D455).
|
|
@@ -106609,6 +106615,22 @@ function buildAlawTable() {
|
|
|
106609
106615
|
}
|
|
106610
106616
|
buildUlawTable();
|
|
106611
106617
|
buildAlawTable();
|
|
106618
|
+
/**
|
|
106619
|
+
* An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
|
|
106620
|
+
* so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
|
|
106621
|
+
* co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
|
|
106622
|
+
* bundles its own copy of this module, so the class the runner sees is not the one that threw.
|
|
106623
|
+
*/
|
|
106624
|
+
var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
|
|
106625
|
+
var AddonMigrationsBlockedError = class extends Error {
|
|
106626
|
+
/** The ledger ids that did not apply and block the owner. */
|
|
106627
|
+
migrationIds;
|
|
106628
|
+
constructor(migrationIds) {
|
|
106629
|
+
super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
|
|
106630
|
+
this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
|
|
106631
|
+
this.migrationIds = migrationIds;
|
|
106632
|
+
}
|
|
106633
|
+
};
|
|
106612
106634
|
/** Is this field's stored value a credential? */
|
|
106613
106635
|
function isSecretConfigField(field) {
|
|
106614
106636
|
if (field.type === "password") return true;
|
|
@@ -107111,7 +107133,7 @@ var NOT_RUN = {
|
|
|
107111
107133
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
107112
107134
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
107113
107135
|
*/
|
|
107114
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
107136
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
107115
107137
|
const first = migrations[0];
|
|
107116
107138
|
if (first === void 0) return NOT_RUN;
|
|
107117
107139
|
const owner = first.meta.owner;
|
|
@@ -107140,7 +107162,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107140
107162
|
else if (status === "blocked") blocked.push(id);
|
|
107141
107163
|
else skipped.push(id);
|
|
107142
107164
|
};
|
|
107165
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
107143
107166
|
for (const step of declaration.steps) {
|
|
107167
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
107144
107168
|
const bound = byId.get(step.id);
|
|
107145
107169
|
if (bound === void 0 || step.action === "skip") {
|
|
107146
107170
|
skipped.push(step.id);
|
|
@@ -108258,15 +108282,19 @@ var BaseAddon = class {
|
|
|
108258
108282
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
108259
108283
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
108260
108284
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
108285
|
+
*
|
|
108286
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
108287
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
108288
|
+
* omitted applied ids "newer").
|
|
108261
108289
|
*/
|
|
108262
|
-
async runMigrations(migrations) {
|
|
108290
|
+
async runMigrations(migrations, options) {
|
|
108263
108291
|
const api = this.ctx.api.migrations;
|
|
108264
108292
|
const report = await runAddonMigrations({
|
|
108265
108293
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
108266
108294
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
108267
108295
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
108268
|
-
}, migrations, this.ctx.logger);
|
|
108269
|
-
if (report.blockOwner.length > 0) throw new
|
|
108296
|
+
}, migrations, this.ctx.logger, options);
|
|
108297
|
+
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
108270
108298
|
return report;
|
|
108271
108299
|
}
|
|
108272
108300
|
/**
|
|
@@ -108606,6 +108634,10 @@ var BaseAddon = class {
|
|
|
108606
108634
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
108607
108635
|
const patch = {};
|
|
108608
108636
|
for (const key of keys) {
|
|
108637
|
+
if (tree.get(key)?.kind === "array") {
|
|
108638
|
+
patch[key] = [];
|
|
108639
|
+
continue;
|
|
108640
|
+
}
|
|
108609
108641
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
108610
108642
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
108611
108643
|
}
|
|
@@ -109332,7 +109364,8 @@ function isZodSecret(schema) {
|
|
|
109332
109364
|
}
|
|
109333
109365
|
/**
|
|
109334
109366
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
109335
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
109367
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
109368
|
+
* map" (`applianceSecrets{}`). A declared
|
|
109336
109369
|
* object or array is reported as one path and not descended into — everything
|
|
109337
109370
|
* under it is secret.
|
|
109338
109371
|
*/
|
|
@@ -109343,6 +109376,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
109343
109376
|
const def = defOf(layer);
|
|
109344
109377
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
109345
109378
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
109379
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
109346
109380
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
109347
109381
|
else if (def.type === "intersection") {
|
|
109348
109382
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -112221,7 +112255,7 @@ function sleep(ms) {
|
|
|
112221
112255
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
112222
112256
|
}
|
|
112223
112257
|
//#endregion
|
|
112224
|
-
//#region ../types/dist/composition-
|
|
112258
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
112225
112259
|
/**
|
|
112226
112260
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
112227
112261
|
*
|
|
@@ -112984,38 +113018,6 @@ function validateExpressionSource(src) {
|
|
|
112984
113018
|
return null;
|
|
112985
113019
|
}
|
|
112986
113020
|
/**
|
|
112987
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
112988
|
-
*
|
|
112989
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
112990
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
112991
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
112992
|
-
*
|
|
112993
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
112994
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
112995
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
112996
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
112997
|
-
* can move later without moving the others.
|
|
112998
|
-
*/
|
|
112999
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
113000
|
-
var APPLIANCE_KINDS = [
|
|
113001
|
-
"washer",
|
|
113002
|
-
"dryer",
|
|
113003
|
-
"dishwasher"
|
|
113004
|
-
];
|
|
113005
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
113006
|
-
/**
|
|
113007
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
113008
|
-
*
|
|
113009
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
113010
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
113011
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
113012
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
113013
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
113014
|
-
* the smallest radiator.
|
|
113015
|
-
*/
|
|
113016
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
113017
|
-
DeviceType.Heater;
|
|
113018
|
-
/**
|
|
113019
113021
|
* Statistics and integrals over a history read (D944).
|
|
113020
113022
|
*
|
|
113021
113023
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -113159,34 +113161,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
113159
113161
|
/** Actuator groups only. Readings leave this unset. */
|
|
113160
113162
|
active: ActiveCriterionSchema.optional()
|
|
113161
113163
|
}),
|
|
113162
|
-
object({
|
|
113163
|
-
kind: literal("appliance"),
|
|
113164
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
113165
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
113166
|
-
thresholdWatts: number().positive(),
|
|
113167
|
-
startDwellMs: number().int().nonnegative(),
|
|
113168
|
-
stopDwellMs: number().int().nonnegative(),
|
|
113169
|
-
member: HelperMemberSchema,
|
|
113170
|
-
/**
|
|
113171
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
113172
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
113173
|
-
* that plug (power-source link of a NEW-target composition).
|
|
113174
|
-
*/
|
|
113175
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
113176
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
113177
|
-
offSeconds: number().optional()
|
|
113178
|
-
}),
|
|
113179
|
-
object({
|
|
113180
|
-
kind: literal("heater"),
|
|
113181
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
113182
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
113183
|
-
plug: HelperMemberSchema,
|
|
113184
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
113185
|
-
meter: HelperMemberSchema.optional(),
|
|
113186
|
-
thresholdWatts: number().positive(),
|
|
113187
|
-
startDwellMs: number().int().nonnegative(),
|
|
113188
|
-
stopDwellMs: number().int().nonnegative()
|
|
113189
|
-
}),
|
|
113190
113164
|
object({
|
|
113191
113165
|
kind: literal("statistic"),
|
|
113192
113166
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -113358,7 +113332,14 @@ var CompositionForwardCommandSchema = object({
|
|
|
113358
113332
|
method: string().min(1),
|
|
113359
113333
|
args: record(string().min(1), CompositionForwardArgSchema).optional()
|
|
113360
113334
|
});
|
|
113361
|
-
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
113335
|
+
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
113336
|
+
CompositionForwardCommandSchema,
|
|
113337
|
+
CompositionCodeSourceSchema,
|
|
113338
|
+
object({
|
|
113339
|
+
kind: literal("extension"),
|
|
113340
|
+
target: literal("remote")
|
|
113341
|
+
})
|
|
113342
|
+
]);
|
|
113362
113343
|
/** An item key inside an item-array cap (`consumables.items[<key>]`). */
|
|
113363
113344
|
var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
113364
113345
|
/**
|
|
@@ -113403,7 +113384,7 @@ var CompositionSchema = object({
|
|
|
113403
113384
|
kind: literal("existing"),
|
|
113404
113385
|
device: CompositionSourceRefSchema
|
|
113405
113386
|
})]),
|
|
113406
|
-
features: array(CompositionFeatureSchema).min(
|
|
113387
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
113407
113388
|
/**
|
|
113408
113389
|
* Present when the block is a helper template. Absent on a free
|
|
113409
113390
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -114707,7 +114688,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
114707
114688
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
114708
114689
|
* catalogue lists the id (a newer database).
|
|
114709
114690
|
*/
|
|
114710
|
-
meta: MigrationMetaSchema.optional()
|
|
114691
|
+
meta: MigrationMetaSchema.optional(),
|
|
114692
|
+
/**
|
|
114693
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
114694
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
114695
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
114696
|
+
*/
|
|
114697
|
+
ownerNotLoaded: boolean().optional()
|
|
114711
114698
|
});
|
|
114712
114699
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
114713
114700
|
var MigrationPendingEntrySchema = object({
|
|
@@ -114740,6 +114727,146 @@ var MigrationOwnerStepSchema = object({
|
|
|
114740
114727
|
/** The ledger state the engine found: `pending` has no row yet. */
|
|
114741
114728
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
114742
114729
|
});
|
|
114730
|
+
function ordinalOf(id) {
|
|
114731
|
+
const digits = id.replace(/\D+/g, "");
|
|
114732
|
+
return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
|
|
114733
|
+
}
|
|
114734
|
+
/** The lowest-numbered eligible integration, or null. */
|
|
114735
|
+
function defaultIntegrationOf(integrations, eligible) {
|
|
114736
|
+
return [...eligible === void 0 ? integrations : integrations.filter(eligible)].sort((a, b) => ordinalOf(a.id) - ordinalOf(b.id) || (a.id < b.id ? -1 : 1))[0] ?? null;
|
|
114737
|
+
}
|
|
114738
|
+
function unstamped(device) {
|
|
114739
|
+
return device.integrationId === void 0 || device.integrationId === null || device.integrationId === "";
|
|
114740
|
+
}
|
|
114741
|
+
/**
|
|
114742
|
+
* An allocation reservation is not a device (D376): `allocateDeviceId` writes a row of type
|
|
114743
|
+
* `generic`, named by its stable id, that is not registered and holds no configuration. The hub
|
|
114744
|
+
* listing does not carry the `registered` bit, so the shape picks the candidates and the missing
|
|
114745
|
+
* configuration decides.
|
|
114746
|
+
*/
|
|
114747
|
+
async function isReservation(ports, device) {
|
|
114748
|
+
if (device.type !== "generic" || device.name === void 0 || device.name !== device.stableId) return false;
|
|
114749
|
+
return !await ports.hasConfig(device.id);
|
|
114750
|
+
}
|
|
114751
|
+
async function claimDevicesToDefaultIntegration(ports, spec) {
|
|
114752
|
+
const all = await ports.listDevices(spec.addonId);
|
|
114753
|
+
const byId = new Map(all.map((d) => [d.id, d]));
|
|
114754
|
+
const reservations = /* @__PURE__ */ new Set();
|
|
114755
|
+
for (const device of all) if (unstamped(device) && await isReservation(ports, device)) reservations.add(device.id);
|
|
114756
|
+
const orphans = all.filter((d) => unstamped(d) && !reservations.has(d.id));
|
|
114757
|
+
const summaryBase = { reservations: reservations.size };
|
|
114758
|
+
if (orphans.length === 0) return {
|
|
114759
|
+
outcome: "already-done",
|
|
114760
|
+
summary: {
|
|
114761
|
+
claimed: 0,
|
|
114762
|
+
unresolved: 0,
|
|
114763
|
+
...summaryBase
|
|
114764
|
+
}
|
|
114765
|
+
};
|
|
114766
|
+
const fallback = defaultIntegrationOf(await ports.listIntegrations(spec.addonId), spec.eligible);
|
|
114767
|
+
const plan = /* @__PURE__ */ new Map();
|
|
114768
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
114769
|
+
const failed = /* @__PURE__ */ new Set();
|
|
114770
|
+
const resolve = (device, seen) => {
|
|
114771
|
+
if (!unstamped(device)) return device.integrationId ?? null;
|
|
114772
|
+
const planned = plan.get(device.id);
|
|
114773
|
+
if (planned !== void 0) return planned;
|
|
114774
|
+
if (failed.has(device.id)) return null;
|
|
114775
|
+
let target = null;
|
|
114776
|
+
const parentId = device.parentDeviceId ?? null;
|
|
114777
|
+
if (parentId === null) target = fallback?.id ?? null;
|
|
114778
|
+
else {
|
|
114779
|
+
const parent = byId.get(parentId);
|
|
114780
|
+
if (parent !== void 0 && !reservations.has(parentId) && !seen.has(parentId)) target = resolve(parent, new Set([...seen, device.id]));
|
|
114781
|
+
}
|
|
114782
|
+
if (target === null) failed.add(device.id);
|
|
114783
|
+
else plan.set(device.id, target);
|
|
114784
|
+
return target;
|
|
114785
|
+
};
|
|
114786
|
+
if (orphans.some((d) => (d.parentDeviceId ?? null) === null) && fallback === null) throw new Error(`${orphans.length} device(s) of '${spec.addonId}' carry no integration and the addon has no integration to claim them to`);
|
|
114787
|
+
for (const device of orphans) if (resolve(device, new Set([device.id])) === null) unresolved.add(device.id);
|
|
114788
|
+
for (const id of unresolved) ports.warnDevice(`device ${id} of '${spec.addonId}' carries no integration and its owner chain does not resolve to one: left unstamped`, id);
|
|
114789
|
+
for (const [deviceId, integrationId] of plan) await ports.setIntegrationId(deviceId, integrationId);
|
|
114790
|
+
const wrong = [];
|
|
114791
|
+
for (const [deviceId, integrationId] of plan) if (await ports.readIntegrationId(deviceId) !== integrationId) wrong.push(deviceId);
|
|
114792
|
+
if (wrong.length > 0) throw new Error(`device(s) ${wrong.join(", ")} did not read back their integration after the claim`);
|
|
114793
|
+
const claimed = plan.size;
|
|
114794
|
+
ports.info(`${claimed} device(s) of '${spec.addonId}' claimed to their owner's integration or the default; ${unresolved.size} unresolved, ${reservations.size} reservation(s) skipped`);
|
|
114795
|
+
return {
|
|
114796
|
+
outcome: claimed > 0 ? "done" : "already-done",
|
|
114797
|
+
summary: {
|
|
114798
|
+
claimed,
|
|
114799
|
+
unresolved: unresolved.size,
|
|
114800
|
+
...summaryBase
|
|
114801
|
+
}
|
|
114802
|
+
};
|
|
114803
|
+
}
|
|
114804
|
+
/** The hub side of the device claim, over `ctx.api`. */
|
|
114805
|
+
function claimDevicesPortsOver(deps) {
|
|
114806
|
+
return {
|
|
114807
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
114808
|
+
id: i.id,
|
|
114809
|
+
name: i.name,
|
|
114810
|
+
info: i.info ?? {}
|
|
114811
|
+
})),
|
|
114812
|
+
listDevices: (addonId) => deps.api.listDevices(addonId),
|
|
114813
|
+
hasConfig: (deviceId) => deps.api.readDeviceHasConfig(deviceId),
|
|
114814
|
+
setIntegrationId: (deviceId, integrationId) => deps.api.setDeviceIntegrationId(deviceId, integrationId),
|
|
114815
|
+
readIntegrationId: (deviceId) => deps.api.readDeviceIntegrationId(deviceId),
|
|
114816
|
+
info: deps.info,
|
|
114817
|
+
warnDevice: deps.warnDevice
|
|
114818
|
+
};
|
|
114819
|
+
}
|
|
114820
|
+
function defaultIntegrationMigrationIds(addonId, date) {
|
|
114821
|
+
return {
|
|
114822
|
+
rowsToRecord: `${addonId}/${date}-integration-rows-to-record`,
|
|
114823
|
+
claimDevices: `${addonId}/${date}-claim-devices-to-default-integration`
|
|
114824
|
+
};
|
|
114825
|
+
}
|
|
114826
|
+
/**
|
|
114827
|
+
* The device claim alone, for an addon that keeps its integration settings where they are for now
|
|
114828
|
+
* (its own wave moves them) but whose persisted devices must name their default integration.
|
|
114829
|
+
*/
|
|
114830
|
+
function claimDevicesMigration(spec) {
|
|
114831
|
+
return {
|
|
114832
|
+
meta: {
|
|
114833
|
+
id: defaultIntegrationMigrationIds(spec.addonId, spec.date).claimDevices,
|
|
114834
|
+
owner: spec.addonId,
|
|
114835
|
+
level: "addon-state",
|
|
114836
|
+
seq: spec.seq,
|
|
114837
|
+
dependsOn: [...spec.dependsOn],
|
|
114838
|
+
store: "settings",
|
|
114839
|
+
scope: "cluster",
|
|
114840
|
+
onFailure: "tolerate",
|
|
114841
|
+
bodyKind: "procedure",
|
|
114842
|
+
description: "Claim the persisted devices that carry no integration to the addon's default integration, keeping their device and stable ids",
|
|
114843
|
+
fixtureMark: "a device config field the claim does not know"
|
|
114844
|
+
},
|
|
114845
|
+
procedure: (ports) => claimDevicesToDefaultIntegration(ports, {
|
|
114846
|
+
addonId: spec.addonId,
|
|
114847
|
+
...spec.eligible !== void 0 ? { eligible: spec.eligible } : {}
|
|
114848
|
+
})
|
|
114849
|
+
};
|
|
114850
|
+
}
|
|
114851
|
+
function defaultIntegrationApiOver(api) {
|
|
114852
|
+
return {
|
|
114853
|
+
list: () => api.integrations.list.query(),
|
|
114854
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
114855
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
114856
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
114857
|
+
addonId,
|
|
114858
|
+
projection: "slim"
|
|
114859
|
+
}),
|
|
114860
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
114861
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
114862
|
+
deviceId,
|
|
114863
|
+
integrationId
|
|
114864
|
+
});
|
|
114865
|
+
},
|
|
114866
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
114867
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
114868
|
+
};
|
|
114869
|
+
}
|
|
114743
114870
|
/**
|
|
114744
114871
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
114745
114872
|
* gets back (cloud training, D805+).
|
|
@@ -120468,6 +120595,48 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
120468
120595
|
scope: "device",
|
|
120469
120596
|
noun: t("Media player", "Lettore multimediale"),
|
|
120470
120597
|
actions: [
|
|
120598
|
+
{
|
|
120599
|
+
id: "media.turnOn",
|
|
120600
|
+
method: "turnOn",
|
|
120601
|
+
label: t("Turn on", "Accendi"),
|
|
120602
|
+
phrase: t("Turn on {device}", "Accendi {device}"),
|
|
120603
|
+
icon: "power"
|
|
120604
|
+
},
|
|
120605
|
+
{
|
|
120606
|
+
id: "media.turnOff",
|
|
120607
|
+
method: "turnOff",
|
|
120608
|
+
label: t("Turn off", "Spegni"),
|
|
120609
|
+
phrase: t("Turn off {device}", "Spegni {device}"),
|
|
120610
|
+
icon: "power"
|
|
120611
|
+
},
|
|
120612
|
+
{
|
|
120613
|
+
id: "media.volumeUp",
|
|
120614
|
+
method: "volumeUp",
|
|
120615
|
+
label: t("Volume up", "Alza il volume"),
|
|
120616
|
+
phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
|
|
120617
|
+
icon: "volume-2"
|
|
120618
|
+
},
|
|
120619
|
+
{
|
|
120620
|
+
id: "media.volumeDown",
|
|
120621
|
+
method: "volumeDown",
|
|
120622
|
+
label: t("Volume down", "Abbassa il volume"),
|
|
120623
|
+
phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
|
|
120624
|
+
icon: "volume-2"
|
|
120625
|
+
},
|
|
120626
|
+
{
|
|
120627
|
+
id: "media.channelUp",
|
|
120628
|
+
method: "channelUp",
|
|
120629
|
+
label: t("Channel up", "Canale successivo"),
|
|
120630
|
+
phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
|
|
120631
|
+
icon: "skip-forward"
|
|
120632
|
+
},
|
|
120633
|
+
{
|
|
120634
|
+
id: "media.channelDown",
|
|
120635
|
+
method: "channelDown",
|
|
120636
|
+
label: t("Channel down", "Canale precedente"),
|
|
120637
|
+
phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
|
|
120638
|
+
icon: "skip-back"
|
|
120639
|
+
},
|
|
120471
120640
|
{
|
|
120472
120641
|
id: "media.play",
|
|
120473
120642
|
method: "play",
|
|
@@ -149988,6 +150157,244 @@ var controlCapability = {
|
|
|
149988
150157
|
*/
|
|
149989
150158
|
history: "none"
|
|
149990
150159
|
};
|
|
150160
|
+
/**
|
|
150161
|
+
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
150162
|
+
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
150163
|
+
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
150164
|
+
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
150165
|
+
*
|
|
150166
|
+
* The cap declares a wide method surface; not every backend supports
|
|
150167
|
+
* every method. Drivers signal sub-capability via the
|
|
150168
|
+
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
150169
|
+
* corresponding method) so the UI gates buttons without a slice
|
|
150170
|
+
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
150171
|
+
*/
|
|
150172
|
+
var MediaPlayerStateSchema = _enum([
|
|
150173
|
+
"off",
|
|
150174
|
+
"on",
|
|
150175
|
+
"idle",
|
|
150176
|
+
"playing",
|
|
150177
|
+
"paused",
|
|
150178
|
+
"buffering",
|
|
150179
|
+
"standby"
|
|
150180
|
+
]);
|
|
150181
|
+
var MediaPlayerRepeatSchema = _enum([
|
|
150182
|
+
"off",
|
|
150183
|
+
"all",
|
|
150184
|
+
"one"
|
|
150185
|
+
]);
|
|
150186
|
+
var MediaInfoSchema = object({
|
|
150187
|
+
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
150188
|
+
* `podcast`, …). */
|
|
150189
|
+
type: string(),
|
|
150190
|
+
/** Human-readable title. */
|
|
150191
|
+
title: string(),
|
|
150192
|
+
/** Optional artist / channel / station label. */
|
|
150193
|
+
artist: string().optional(),
|
|
150194
|
+
/** Optional album / season / show name. */
|
|
150195
|
+
album: string().optional(),
|
|
150196
|
+
/** Optional cover-art / thumbnail URL. */
|
|
150197
|
+
imageUrl: string().optional()
|
|
150198
|
+
});
|
|
150199
|
+
/**
|
|
150200
|
+
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
150201
|
+
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
150202
|
+
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
150203
|
+
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
150204
|
+
* guess, never as a reading.
|
|
150205
|
+
*/
|
|
150206
|
+
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
150207
|
+
/** The fields of a media-player status a driver can only guess. */
|
|
150208
|
+
var MediaPlayerAssumedFieldSchema = _enum([
|
|
150209
|
+
"state",
|
|
150210
|
+
"volumeLevel",
|
|
150211
|
+
"isMuted",
|
|
150212
|
+
"source"
|
|
150213
|
+
]);
|
|
150214
|
+
var MediaPlayerStatusSchema = object({
|
|
150215
|
+
/** Playback lifecycle state. */
|
|
150216
|
+
state: MediaPlayerStateSchema,
|
|
150217
|
+
/**
|
|
150218
|
+
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
150219
|
+
* A slice written before this field existed parses as `reported`.
|
|
150220
|
+
*/
|
|
150221
|
+
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
150222
|
+
/**
|
|
150223
|
+
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
150224
|
+
* this is not empty; a field not listed here is a reading even then (power
|
|
150225
|
+
* from a running-state source, mute guessed from the last send).
|
|
150226
|
+
*/
|
|
150227
|
+
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
150228
|
+
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
150229
|
+
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
150230
|
+
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
150231
|
+
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
150232
|
+
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
150233
|
+
isMuted: boolean().nullable(),
|
|
150234
|
+
/** Active source / input. Empty when no source surface. */
|
|
150235
|
+
source: string(),
|
|
150236
|
+
/** Selectable sources. Empty when no source surface.
|
|
150237
|
+
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
150238
|
+
availableSources: array(string()),
|
|
150239
|
+
/** Currently-playing media info. Null when nothing is playing. */
|
|
150240
|
+
currentMedia: MediaInfoSchema.nullable(),
|
|
150241
|
+
/** Current playback position in ms. Null when not seekable or
|
|
150242
|
+
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
150243
|
+
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
150244
|
+
/** Total duration of the current media in ms. Null when unknown
|
|
150245
|
+
* (live stream) or nothing is playing. */
|
|
150246
|
+
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
150247
|
+
/** Shuffle toggle. Null when no shuffle surface.
|
|
150248
|
+
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
150249
|
+
shuffle: boolean().nullable(),
|
|
150250
|
+
/** Repeat mode. Null when no repeat surface.
|
|
150251
|
+
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
150252
|
+
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
150253
|
+
/** Ms epoch when the slice was last updated. */
|
|
150254
|
+
lastChangedAt: number()
|
|
150255
|
+
});
|
|
150256
|
+
var stepInput = object({
|
|
150257
|
+
deviceId: number().int().nonnegative(),
|
|
150258
|
+
/** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
|
|
150259
|
+
steps: number().int().min(1).max(50).default(1)
|
|
150260
|
+
});
|
|
150261
|
+
var deviceOnly = object({ deviceId: number().int().nonnegative() });
|
|
150262
|
+
var mediaPlayerCapability = {
|
|
150263
|
+
name: "media-player",
|
|
150264
|
+
scope: "device",
|
|
150265
|
+
deviceNative: true,
|
|
150266
|
+
mode: "singleton",
|
|
150267
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
150268
|
+
methods: {
|
|
150269
|
+
turnOn: method(deviceOnly, _void(), {
|
|
150270
|
+
kind: "mutation",
|
|
150271
|
+
auth: "protected"
|
|
150272
|
+
}),
|
|
150273
|
+
turnOff: method(deviceOnly, _void(), {
|
|
150274
|
+
kind: "mutation",
|
|
150275
|
+
auth: "protected"
|
|
150276
|
+
}),
|
|
150277
|
+
volumeUp: method(stepInput, _void(), {
|
|
150278
|
+
kind: "mutation",
|
|
150279
|
+
auth: "protected"
|
|
150280
|
+
}),
|
|
150281
|
+
volumeDown: method(stepInput, _void(), {
|
|
150282
|
+
kind: "mutation",
|
|
150283
|
+
auth: "protected"
|
|
150284
|
+
}),
|
|
150285
|
+
channelUp: method(stepInput, _void(), {
|
|
150286
|
+
kind: "mutation",
|
|
150287
|
+
auth: "protected"
|
|
150288
|
+
}),
|
|
150289
|
+
channelDown: method(stepInput, _void(), {
|
|
150290
|
+
kind: "mutation",
|
|
150291
|
+
auth: "protected"
|
|
150292
|
+
}),
|
|
150293
|
+
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150294
|
+
kind: "mutation",
|
|
150295
|
+
auth: "protected"
|
|
150296
|
+
}),
|
|
150297
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150298
|
+
kind: "mutation",
|
|
150299
|
+
auth: "protected"
|
|
150300
|
+
}),
|
|
150301
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150302
|
+
kind: "mutation",
|
|
150303
|
+
auth: "protected"
|
|
150304
|
+
}),
|
|
150305
|
+
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150306
|
+
kind: "mutation",
|
|
150307
|
+
auth: "protected"
|
|
150308
|
+
}),
|
|
150309
|
+
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150310
|
+
kind: "mutation",
|
|
150311
|
+
auth: "protected"
|
|
150312
|
+
}),
|
|
150313
|
+
seek: method(object({
|
|
150314
|
+
deviceId: number().int().nonnegative(),
|
|
150315
|
+
position: number().int().nonnegative()
|
|
150316
|
+
}), _void(), {
|
|
150317
|
+
kind: "mutation",
|
|
150318
|
+
auth: "protected"
|
|
150319
|
+
}),
|
|
150320
|
+
setVolume: method(object({
|
|
150321
|
+
deviceId: number().int().nonnegative(),
|
|
150322
|
+
volumeLevel: number().min(0).max(100)
|
|
150323
|
+
}), _void(), {
|
|
150324
|
+
kind: "mutation",
|
|
150325
|
+
auth: "protected"
|
|
150326
|
+
}),
|
|
150327
|
+
setMute: method(object({
|
|
150328
|
+
deviceId: number().int().nonnegative(),
|
|
150329
|
+
muted: boolean()
|
|
150330
|
+
}), _void(), {
|
|
150331
|
+
kind: "mutation",
|
|
150332
|
+
auth: "protected"
|
|
150333
|
+
}),
|
|
150334
|
+
setShuffle: method(object({
|
|
150335
|
+
deviceId: number().int().nonnegative(),
|
|
150336
|
+
shuffle: boolean()
|
|
150337
|
+
}), _void(), {
|
|
150338
|
+
kind: "mutation",
|
|
150339
|
+
auth: "protected"
|
|
150340
|
+
}),
|
|
150341
|
+
setRepeat: method(object({
|
|
150342
|
+
deviceId: number().int().nonnegative(),
|
|
150343
|
+
repeat: MediaPlayerRepeatSchema
|
|
150344
|
+
}), _void(), {
|
|
150345
|
+
kind: "mutation",
|
|
150346
|
+
auth: "protected"
|
|
150347
|
+
}),
|
|
150348
|
+
selectSource: method(object({
|
|
150349
|
+
deviceId: number().int().nonnegative(),
|
|
150350
|
+
source: string().min(1)
|
|
150351
|
+
}), _void(), {
|
|
150352
|
+
kind: "mutation",
|
|
150353
|
+
auth: "protected"
|
|
150354
|
+
}),
|
|
150355
|
+
playMedia: method(object({
|
|
150356
|
+
deviceId: number().int().nonnegative(),
|
|
150357
|
+
/** Media identifier / URL. */
|
|
150358
|
+
mediaId: string().min(1),
|
|
150359
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
150360
|
+
* passes it through to the upstream service. */
|
|
150361
|
+
mediaType: string().min(1)
|
|
150362
|
+
}), _void(), {
|
|
150363
|
+
kind: "mutation",
|
|
150364
|
+
auth: "protected"
|
|
150365
|
+
})
|
|
150366
|
+
},
|
|
150367
|
+
status: {
|
|
150368
|
+
schema: MediaPlayerStatusSchema,
|
|
150369
|
+
kind: "push"
|
|
150370
|
+
},
|
|
150371
|
+
/**
|
|
150372
|
+
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
150373
|
+
* full slice for live now-playing, volume, and progress updates
|
|
150374
|
+
* without polling.
|
|
150375
|
+
*/
|
|
150376
|
+
runtimeState: MediaPlayerStatusSchema,
|
|
150377
|
+
/**
|
|
150378
|
+
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
150379
|
+
*
|
|
150380
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
150381
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
150382
|
+
*/
|
|
150383
|
+
durability: "session",
|
|
150384
|
+
/**
|
|
150385
|
+
* History (D850): volume (a level an operator or an automation sets; the
|
|
150386
|
+
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
150387
|
+
* `position` is not recorded: it moves continuously while playing.
|
|
150388
|
+
*/
|
|
150389
|
+
history: { fields: {
|
|
150390
|
+
volumeLevel: {
|
|
150391
|
+
kind: "gauge",
|
|
150392
|
+
unit: "%",
|
|
150393
|
+
deadband: 1
|
|
150394
|
+
},
|
|
150395
|
+
isMuted: { kind: "boolean" }
|
|
150396
|
+
} }
|
|
150397
|
+
};
|
|
149991
150398
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
|
|
149992
150399
|
var NUMBER = {
|
|
149993
150400
|
kind: "fixed",
|
|
@@ -150435,117 +150842,6 @@ function actuator(input) {
|
|
|
150435
150842
|
};
|
|
150436
150843
|
}
|
|
150437
150844
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
150438
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
150439
|
-
"no-power-source",
|
|
150440
|
-
"switch-missing",
|
|
150441
|
-
"switch-offline",
|
|
150442
|
-
"switch-unknown",
|
|
150443
|
-
"restart-in-progress",
|
|
150444
|
-
"switch-call-failed"
|
|
150445
|
-
]);
|
|
150446
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
150447
|
-
accepted: literal(false),
|
|
150448
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
150449
|
-
/** One line for the operator, naming the switch. */
|
|
150450
|
-
message: string()
|
|
150451
|
-
})]);
|
|
150452
|
-
var PowerSourceInfoSchema = object({
|
|
150453
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
150454
|
-
linked: boolean(),
|
|
150455
|
-
switchDeviceId: number().nullable(),
|
|
150456
|
-
switchName: string().nullable(),
|
|
150457
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
150458
|
-
switchOn: boolean().nullable(),
|
|
150459
|
-
switchOnline: boolean().nullable(),
|
|
150460
|
-
restartInProgress: boolean(),
|
|
150461
|
-
offSeconds: number().int()
|
|
150462
|
-
});
|
|
150463
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
150464
|
-
var PowerSourceStateSchema = object({
|
|
150465
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
150466
|
-
switchOn: boolean().nullable(),
|
|
150467
|
-
offSeconds: number().int(),
|
|
150468
|
-
lastFetchedAt: number()
|
|
150469
|
-
});
|
|
150470
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
150471
|
-
DeviceType.Gateway,
|
|
150472
|
-
DeviceType.Hub,
|
|
150473
|
-
DeviceType.Camera,
|
|
150474
|
-
DeviceType.Sensor,
|
|
150475
|
-
DeviceType.MediaPlayer,
|
|
150476
|
-
DeviceType.Vacuum,
|
|
150477
|
-
DeviceType.Generic,
|
|
150478
|
-
DeviceType.Washer,
|
|
150479
|
-
DeviceType.Dryer,
|
|
150480
|
-
DeviceType.Dishwasher,
|
|
150481
|
-
DeviceType.PetFountain,
|
|
150482
|
-
DeviceType.Fan,
|
|
150483
|
-
DeviceType.Humidifier,
|
|
150484
|
-
DeviceType.Climate,
|
|
150485
|
-
DeviceType.WaterHeater
|
|
150486
|
-
];
|
|
150487
|
-
var powerSourceCustomize = {
|
|
150488
|
-
version: 1,
|
|
150489
|
-
targets: [{
|
|
150490
|
-
id: "switch",
|
|
150491
|
-
cardinality: "scalar",
|
|
150492
|
-
vocabulary: {
|
|
150493
|
-
cap: "power-source",
|
|
150494
|
-
stateField: "switchOn"
|
|
150495
|
-
},
|
|
150496
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150497
|
-
apply: "add",
|
|
150498
|
-
path: "switchOn",
|
|
150499
|
-
sources: [{
|
|
150500
|
-
kind: "switch",
|
|
150501
|
-
transforms: [{ kind: "identity" }]
|
|
150502
|
-
}],
|
|
150503
|
-
companions: [{
|
|
150504
|
-
kind: "constant",
|
|
150505
|
-
path: "offSeconds",
|
|
150506
|
-
value: 10,
|
|
150507
|
-
when: "add"
|
|
150508
|
-
}, {
|
|
150509
|
-
kind: "clock",
|
|
150510
|
-
path: "lastFetchedAt",
|
|
150511
|
-
when: "always"
|
|
150512
|
-
}],
|
|
150513
|
-
sourceReport: { status: "assignment-status" }
|
|
150514
|
-
}]
|
|
150515
|
-
};
|
|
150516
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
150517
|
-
var powerSourceCapability = {
|
|
150518
|
-
name: "power-source",
|
|
150519
|
-
scope: "device",
|
|
150520
|
-
mode: "singleton",
|
|
150521
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150522
|
-
methods: {
|
|
150523
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
150524
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150525
|
-
kind: "mutation",
|
|
150526
|
-
auth: "admin"
|
|
150527
|
-
}),
|
|
150528
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150529
|
-
kind: "mutation",
|
|
150530
|
-
auth: "admin"
|
|
150531
|
-
}),
|
|
150532
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150533
|
-
kind: "mutation",
|
|
150534
|
-
auth: "admin"
|
|
150535
|
-
})
|
|
150536
|
-
},
|
|
150537
|
-
runtimeState: PowerSourceStateSchema,
|
|
150538
|
-
customize: powerSourceCustomize,
|
|
150539
|
-
/**
|
|
150540
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
150541
|
-
* its next report; a restored value would outlive the reading it names.
|
|
150542
|
-
*
|
|
150543
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
150544
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
150545
|
-
*/
|
|
150546
|
-
durability: "session",
|
|
150547
|
-
history: "none"
|
|
150548
|
-
};
|
|
150549
150845
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
150550
150846
|
DeviceType.Camera;
|
|
150551
150847
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -150573,6 +150869,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
150573
150869
|
}).optional()
|
|
150574
150870
|
})) });
|
|
150575
150871
|
/**
|
|
150872
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
150873
|
+
* `z.infer`, they do not restate the shape.
|
|
150874
|
+
*/
|
|
150875
|
+
var OverrideSourceSchema = object({
|
|
150876
|
+
addonId: string().min(1),
|
|
150877
|
+
stableId: string().min(1),
|
|
150878
|
+
cap: string().min(1),
|
|
150879
|
+
fieldPath: string().min(1)
|
|
150880
|
+
});
|
|
150881
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
150882
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
150883
|
+
var ThresholdTransformDraftSchema = object({
|
|
150884
|
+
kind: literal("threshold"),
|
|
150885
|
+
comparison: _enum(["gte", "lte"]),
|
|
150886
|
+
threshold: number(),
|
|
150887
|
+
startDwellMs: number().nonnegative(),
|
|
150888
|
+
stopDwellMs: number().nonnegative()
|
|
150889
|
+
});
|
|
150890
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
150891
|
+
IdentityTransformDraftSchema,
|
|
150892
|
+
InvertTransformDraftSchema,
|
|
150893
|
+
ThresholdTransformDraftSchema,
|
|
150894
|
+
object({
|
|
150895
|
+
kind: literal("linear"),
|
|
150896
|
+
scale: number(),
|
|
150897
|
+
offset: number(),
|
|
150898
|
+
clampMin: number().optional(),
|
|
150899
|
+
clampMax: number().optional()
|
|
150900
|
+
}),
|
|
150901
|
+
object({
|
|
150902
|
+
kind: literal("enum-map"),
|
|
150903
|
+
mapping: record(string(), string().nullable())
|
|
150904
|
+
}),
|
|
150905
|
+
object({ kind: literal("number") })
|
|
150906
|
+
]);
|
|
150907
|
+
var ScalarOverrideDraftSchema = object({
|
|
150908
|
+
kind: literal("scalar"),
|
|
150909
|
+
cap: string().min(1),
|
|
150910
|
+
targetId: string().min(1),
|
|
150911
|
+
source: OverrideSourceSchema,
|
|
150912
|
+
transform: CustomizeTransformDraftSchema
|
|
150913
|
+
});
|
|
150914
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
150915
|
+
kind: literal("keyedArray"),
|
|
150916
|
+
cap: string().min(1),
|
|
150917
|
+
targetId: string().min(1),
|
|
150918
|
+
key: string().min(1),
|
|
150919
|
+
label: string().min(1),
|
|
150920
|
+
fieldPath: string().min(1),
|
|
150921
|
+
source: OverrideSourceSchema,
|
|
150922
|
+
transform: CustomizeTransformDraftSchema
|
|
150923
|
+
});
|
|
150924
|
+
var PulseOverrideDraftSchema = object({
|
|
150925
|
+
kind: literal("pulse"),
|
|
150926
|
+
cap: string().min(1),
|
|
150927
|
+
targetId: string().min(1),
|
|
150928
|
+
enabled: boolean(),
|
|
150929
|
+
debounceMs: number().nonnegative(),
|
|
150930
|
+
edges: array(_enum([
|
|
150931
|
+
"rising",
|
|
150932
|
+
"button",
|
|
150933
|
+
"native-event"
|
|
150934
|
+
])).min(1).optional(),
|
|
150935
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
150936
|
+
});
|
|
150937
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
150938
|
+
"in-addition",
|
|
150939
|
+
"confirm",
|
|
150940
|
+
"replace"
|
|
150941
|
+
]).optional() });
|
|
150942
|
+
discriminatedUnion("kind", [
|
|
150943
|
+
ScalarOverrideDraftSchema,
|
|
150944
|
+
KeyedArrayOverrideDraftSchema,
|
|
150945
|
+
PulseOverrideDraftSchema,
|
|
150946
|
+
object({
|
|
150947
|
+
kind: literal("span"),
|
|
150948
|
+
cap: string().min(1),
|
|
150949
|
+
targetId: string().min(1),
|
|
150950
|
+
timeoutMs: number().nonnegative(),
|
|
150951
|
+
cooldownMs: number().nonnegative().optional(),
|
|
150952
|
+
sources: array(SpanSourceDraftSchema)
|
|
150953
|
+
})
|
|
150954
|
+
]);
|
|
150955
|
+
object({
|
|
150956
|
+
targetId: string().min(1),
|
|
150957
|
+
configured: boolean(),
|
|
150958
|
+
source: _enum(["native", "assigned"]),
|
|
150959
|
+
state: _enum([
|
|
150960
|
+
"active",
|
|
150961
|
+
"stale",
|
|
150962
|
+
"released",
|
|
150963
|
+
"unavailable"
|
|
150964
|
+
]),
|
|
150965
|
+
reason: string().nullable()
|
|
150966
|
+
});
|
|
150967
|
+
/**
|
|
150968
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
150969
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
150970
|
+
*
|
|
150971
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
150972
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
150973
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
150974
|
+
*/
|
|
150975
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
150976
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
150977
|
+
"high",
|
|
150978
|
+
"medium",
|
|
150979
|
+
"low"
|
|
150980
|
+
]);
|
|
150981
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
150982
|
+
"no-history",
|
|
150983
|
+
"span-too-short",
|
|
150984
|
+
"truncated",
|
|
150985
|
+
"no-idle-floor",
|
|
150986
|
+
"no-runs",
|
|
150987
|
+
"no-separation"
|
|
150988
|
+
]);
|
|
150989
|
+
var RunningProposalReplaySchema = object({
|
|
150990
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
150991
|
+
intervals: number().int().nonnegative(),
|
|
150992
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
150993
|
+
extra: number().int().nonnegative(),
|
|
150994
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
150995
|
+
missed: number().int().nonnegative()
|
|
150996
|
+
});
|
|
150997
|
+
var RunningProposalEvidenceSchema = object({
|
|
150998
|
+
tier: RunningProposalTierSchema,
|
|
150999
|
+
spanMs: number().nonnegative(),
|
|
151000
|
+
from: number(),
|
|
151001
|
+
to: number(),
|
|
151002
|
+
points: number().int().nonnegative(),
|
|
151003
|
+
runsFound: number().int().nonnegative(),
|
|
151004
|
+
pausesFound: number().int().nonnegative(),
|
|
151005
|
+
longestPauseMs: number().nonnegative(),
|
|
151006
|
+
idleFloorW: number(),
|
|
151007
|
+
noiseCeilingW: number(),
|
|
151008
|
+
plateauW: number(),
|
|
151009
|
+
replay: RunningProposalReplaySchema
|
|
151010
|
+
});
|
|
151011
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
151012
|
+
spanMs: number().nonnegative(),
|
|
151013
|
+
points: number().int().nonnegative(),
|
|
151014
|
+
runsFound: number().int().nonnegative()
|
|
151015
|
+
});
|
|
151016
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
151017
|
+
state: literal("proposal"),
|
|
151018
|
+
transform: ThresholdTransformDraftSchema,
|
|
151019
|
+
evidence: RunningProposalEvidenceSchema,
|
|
151020
|
+
confidence: RunningProposalConfidenceSchema,
|
|
151021
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
151022
|
+
notes: array(string())
|
|
151023
|
+
}), object({
|
|
151024
|
+
state: literal("insufficient"),
|
|
151025
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
151026
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
151027
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
151028
|
+
fallback: ThresholdTransformDraftSchema
|
|
151029
|
+
})]);
|
|
151030
|
+
/**
|
|
150576
151031
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
150577
151032
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
150578
151033
|
*
|
|
@@ -150862,6 +151317,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
150862
151317
|
kind: "mutation",
|
|
150863
151318
|
auth: "admin",
|
|
150864
151319
|
access: "view"
|
|
151320
|
+
}), method(object({
|
|
151321
|
+
deviceId: number().int(),
|
|
151322
|
+
cap: string().min(1),
|
|
151323
|
+
fieldPath: string().min(1),
|
|
151324
|
+
deviceType: _enum(DeviceType).optional()
|
|
151325
|
+
}), RunningThresholdProposalSchema, {
|
|
151326
|
+
auth: "admin",
|
|
151327
|
+
access: "view"
|
|
150865
151328
|
}), method(object({
|
|
150866
151329
|
blockId: string().min(1),
|
|
150867
151330
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -151838,7 +152301,27 @@ method(object({
|
|
|
151838
152301
|
* exposes only the COMMON surface: link state, supported device
|
|
151839
152302
|
* kinds (so the UI can filter the device picker), and per-device
|
|
151840
152303
|
* expose/unexpose.
|
|
151841
|
-
|
|
152304
|
+
*
|
|
152305
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
152306
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
152307
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
152308
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
152309
|
+
* default instance's); present = that instance; an id the provider does not
|
|
152310
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
152311
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
152312
|
+
* change always names the target it changes. A single-instance provider
|
|
152313
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
152314
|
+
*/
|
|
152315
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
152316
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
152317
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
152318
|
+
/** One independent export target of an addon. */
|
|
152319
|
+
var ExportInstanceSchema = object({
|
|
152320
|
+
instanceId: InstanceIdSchema$1,
|
|
152321
|
+
name: string(),
|
|
152322
|
+
enabled: boolean(),
|
|
152323
|
+
kind: string().optional()
|
|
152324
|
+
});
|
|
151842
152325
|
var LinkStateSchema = _enum([
|
|
151843
152326
|
"unlinked",
|
|
151844
152327
|
"linked",
|
|
@@ -151889,10 +152372,14 @@ var ExposedDeviceSchema = object({
|
|
|
151889
152372
|
capabilities: array(string()).optional()
|
|
151890
152373
|
});
|
|
151891
152374
|
var ExposeInputSchema = object({
|
|
152375
|
+
instanceId: InstanceIdSchema$1,
|
|
151892
152376
|
deviceId: string(),
|
|
151893
152377
|
capabilities: array(string()).optional()
|
|
151894
152378
|
});
|
|
151895
|
-
var UnexposeInputSchema = object({
|
|
152379
|
+
var UnexposeInputSchema = object({
|
|
152380
|
+
instanceId: InstanceIdSchema$1,
|
|
152381
|
+
deviceId: string()
|
|
152382
|
+
});
|
|
151896
152383
|
/**
|
|
151897
152384
|
* What an exporter may expose besides devices (D968). An action and a
|
|
151898
152385
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -151921,15 +152408,21 @@ var ExposedEntitySchema = object({
|
|
|
151921
152408
|
entity: ExportEntityRefSchema,
|
|
151922
152409
|
exposedAs: string().optional()
|
|
151923
152410
|
});
|
|
151924
|
-
var ExposeEntityInputSchema = object({
|
|
151925
|
-
|
|
151926
|
-
|
|
152411
|
+
var ExposeEntityInputSchema = object({
|
|
152412
|
+
instanceId: InstanceIdSchema$1,
|
|
152413
|
+
entity: ExportEntityRefSchema
|
|
152414
|
+
});
|
|
152415
|
+
var UnexposeEntityInputSchema = object({
|
|
152416
|
+
instanceId: InstanceIdSchema$1,
|
|
152417
|
+
entity: ExportEntityRefSchema
|
|
152418
|
+
});
|
|
152419
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
151927
152420
|
kind: "mutation",
|
|
151928
152421
|
auth: "admin"
|
|
151929
152422
|
}), method(UnexposeInputSchema, _void(), {
|
|
151930
152423
|
kind: "mutation",
|
|
151931
152424
|
auth: "admin"
|
|
151932
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
152425
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
151933
152426
|
kind: "mutation",
|
|
151934
152427
|
auth: "admin"
|
|
151935
152428
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -155552,15 +156045,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
155552
156045
|
items: array(FanCaptureItemSchema),
|
|
155553
156046
|
omitted: array(FanCaptureOmissionSchema)
|
|
155554
156047
|
});
|
|
155555
|
-
var deviceInput$
|
|
156048
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
155556
156049
|
DeviceType.Fan;
|
|
155557
|
-
method(deviceInput$
|
|
156050
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
155558
156051
|
kind: "mutation",
|
|
155559
156052
|
auth: "admin"
|
|
155560
|
-
}), method(deviceInput$
|
|
156053
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
155561
156054
|
kind: "mutation",
|
|
155562
156055
|
auth: "protected"
|
|
155563
|
-
}), method(deviceInput$
|
|
156056
|
+
}), method(deviceInput$2.extend({
|
|
155564
156057
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
155565
156058
|
steps: array(ActionStepSchema).max(20).default([])
|
|
155566
156059
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -156443,6 +156936,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156443
156936
|
}), object({ success: boolean() }), {
|
|
156444
156937
|
kind: "mutation",
|
|
156445
156938
|
auth: "admin"
|
|
156939
|
+
}), method(object({ id: string() }), object({
|
|
156940
|
+
rows: number().int().nonnegative(),
|
|
156941
|
+
secrets: number().int().nonnegative()
|
|
156942
|
+
}), {
|
|
156943
|
+
kind: "mutation",
|
|
156944
|
+
auth: "admin"
|
|
156446
156945
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
156447
156946
|
addonId: string(),
|
|
156448
156947
|
settings: record(string(), unknown())
|
|
@@ -156471,13 +156970,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156471
156970
|
*/
|
|
156472
156971
|
var ConfigSchemaPassthrough$3 = custom();
|
|
156473
156972
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
156973
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
156974
|
+
/**
|
|
156975
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
156976
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
156977
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
156978
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
156979
|
+
* instance id.
|
|
156980
|
+
*/
|
|
156981
|
+
var IntegrationInstanceKindSchema = object({
|
|
156982
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
156983
|
+
id: string().min(1),
|
|
156984
|
+
label: string().min(1),
|
|
156985
|
+
/** The kind signs in through a server-held interactive session. */
|
|
156986
|
+
signIn: object({
|
|
156987
|
+
basePath: string().min(1),
|
|
156988
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
156989
|
+
}).optional(),
|
|
156990
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
156991
|
+
connectPath: string().min(1).optional(),
|
|
156992
|
+
/** Relative hub path to disconnect that account. */
|
|
156993
|
+
disconnectPath: string().min(1).optional()
|
|
156994
|
+
});
|
|
156474
156995
|
var IntegrationInstanceDescriptionSchema = object({
|
|
156996
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
156997
|
+
role: _enum([
|
|
156998
|
+
"devices",
|
|
156999
|
+
"services",
|
|
157000
|
+
"both"
|
|
157001
|
+
]),
|
|
157002
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
157003
|
+
discovery: object({
|
|
157004
|
+
mode: _enum([
|
|
157005
|
+
"none",
|
|
157006
|
+
"lan",
|
|
157007
|
+
"account"
|
|
157008
|
+
]),
|
|
157009
|
+
scopedToInstance: boolean()
|
|
157010
|
+
}).optional(),
|
|
156475
157011
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
156476
|
-
instanceMode:
|
|
157012
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
156477
157013
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
156478
157014
|
kind: string(),
|
|
156479
157015
|
/** True when `testInstance` really probes the remote. */
|
|
156480
|
-
canTest: boolean()
|
|
157016
|
+
canTest: boolean(),
|
|
157017
|
+
/**
|
|
157018
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
157019
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
157020
|
+
*/
|
|
157021
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
157022
|
+
/**
|
|
157023
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
157024
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
157025
|
+
*/
|
|
157026
|
+
allowRestoreId: boolean().optional(),
|
|
157027
|
+
/**
|
|
157028
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
157029
|
+
* send an id (the UI then mints an opaque one).
|
|
157030
|
+
*/
|
|
157031
|
+
mintsId: boolean().optional()
|
|
156481
157032
|
});
|
|
156482
157033
|
var IntegrationInstanceStatusSchema = object({
|
|
156483
157034
|
state: _enum([
|
|
@@ -156486,7 +157037,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
156486
157037
|
"error",
|
|
156487
157038
|
"unknown"
|
|
156488
157039
|
]),
|
|
156489
|
-
detail: string().optional()
|
|
157040
|
+
detail: string().optional(),
|
|
157041
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
157042
|
+
label: string().optional()
|
|
156490
157043
|
});
|
|
156491
157044
|
var IntegrationInstanceSchema = object({
|
|
156492
157045
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156494,13 +157047,18 @@ var IntegrationInstanceSchema = object({
|
|
|
156494
157047
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
156495
157048
|
configured: boolean(),
|
|
156496
157049
|
/**
|
|
156497
|
-
* The instance's own on/off switch (D996)
|
|
156498
|
-
*
|
|
157050
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
157051
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
157052
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
156499
157053
|
*/
|
|
156500
|
-
enabled: boolean(),
|
|
157054
|
+
enabled: boolean().optional(),
|
|
157055
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
157056
|
+
kind: string().optional(),
|
|
156501
157057
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
156502
157058
|
formValues: record(string(), unknown()),
|
|
156503
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
157059
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
157060
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
157061
|
+
lastCheckedAt: number().optional()
|
|
156504
157062
|
});
|
|
156505
157063
|
var IntegrationInstancePatchSchema = object({
|
|
156506
157064
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156509,8 +157067,9 @@ var IntegrationInstancePatchSchema = object({
|
|
|
156509
157067
|
values: record(string(), unknown())
|
|
156510
157068
|
});
|
|
156511
157069
|
method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
|
|
156512
|
-
id: IntegrationInstanceIdSchema,
|
|
156513
|
-
name: string().min(1)
|
|
157070
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
157071
|
+
name: string().min(1),
|
|
157072
|
+
kind: string().min(1).optional()
|
|
156514
157073
|
}), object({ id: string() }), {
|
|
156515
157074
|
kind: "mutation",
|
|
156516
157075
|
auth: "admin"
|
|
@@ -158571,6 +159130,11 @@ var LogEntrySchema = object({
|
|
|
158571
159130
|
* flight, and not asking for the next slice ends the scan.
|
|
158572
159131
|
*/
|
|
158573
159132
|
var LogReadQuerySchema = object({
|
|
159133
|
+
/**
|
|
159134
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
159135
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
159136
|
+
*/
|
|
159137
|
+
instanceId: string().min(1).max(200).optional(),
|
|
158574
159138
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
158575
159139
|
requestId: string().min(1).max(64).optional(),
|
|
158576
159140
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -158633,6 +159197,8 @@ var LogReadResultSchema = object({
|
|
|
158633
159197
|
/** True when `cancelRead` ended this slice early. */
|
|
158634
159198
|
cancelled: boolean().optional()
|
|
158635
159199
|
});
|
|
159200
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
159201
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
158636
159202
|
var LogReadCapabilitiesSchema = object({
|
|
158637
159203
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
158638
159204
|
readable: boolean(),
|
|
@@ -158651,7 +159217,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
158651
159217
|
/** How far back the store reaches, when the provider knows (days). */
|
|
158652
159218
|
retentionDays: number().nullable()
|
|
158653
159219
|
});
|
|
158654
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
159220
|
+
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
|
|
158655
159221
|
kind: "mutation",
|
|
158656
159222
|
auth: "admin"
|
|
158657
159223
|
});
|
|
@@ -158755,214 +159321,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
|
|
|
158755
159321
|
]);
|
|
158756
159322
|
method(_void(), array(LoginMethodContributionSchema).readonly());
|
|
158757
159323
|
/**
|
|
158758
|
-
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
158759
|
-
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
158760
|
-
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
158761
|
-
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
158762
|
-
*
|
|
158763
|
-
* The cap declares a wide method surface; not every backend supports
|
|
158764
|
-
* every method. Drivers signal sub-capability via the
|
|
158765
|
-
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
158766
|
-
* corresponding method) so the UI gates buttons without a slice
|
|
158767
|
-
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
158768
|
-
*/
|
|
158769
|
-
var MediaPlayerStateSchema = _enum([
|
|
158770
|
-
"off",
|
|
158771
|
-
"on",
|
|
158772
|
-
"idle",
|
|
158773
|
-
"playing",
|
|
158774
|
-
"paused",
|
|
158775
|
-
"buffering",
|
|
158776
|
-
"standby"
|
|
158777
|
-
]);
|
|
158778
|
-
var MediaPlayerRepeatSchema = _enum([
|
|
158779
|
-
"off",
|
|
158780
|
-
"all",
|
|
158781
|
-
"one"
|
|
158782
|
-
]);
|
|
158783
|
-
var MediaInfoSchema = object({
|
|
158784
|
-
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
158785
|
-
* `podcast`, …). */
|
|
158786
|
-
type: string(),
|
|
158787
|
-
/** Human-readable title. */
|
|
158788
|
-
title: string(),
|
|
158789
|
-
/** Optional artist / channel / station label. */
|
|
158790
|
-
artist: string().optional(),
|
|
158791
|
-
/** Optional album / season / show name. */
|
|
158792
|
-
album: string().optional(),
|
|
158793
|
-
/** Optional cover-art / thumbnail URL. */
|
|
158794
|
-
imageUrl: string().optional()
|
|
158795
|
-
});
|
|
158796
|
-
/**
|
|
158797
|
-
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
158798
|
-
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
158799
|
-
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
158800
|
-
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
158801
|
-
* guess, never as a reading.
|
|
158802
|
-
*/
|
|
158803
|
-
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
158804
|
-
/** The fields of a media-player status a driver can only guess. */
|
|
158805
|
-
var MediaPlayerAssumedFieldSchema = _enum([
|
|
158806
|
-
"state",
|
|
158807
|
-
"volumeLevel",
|
|
158808
|
-
"isMuted",
|
|
158809
|
-
"source"
|
|
158810
|
-
]);
|
|
158811
|
-
var MediaPlayerStatusSchema = object({
|
|
158812
|
-
/** Playback lifecycle state. */
|
|
158813
|
-
state: MediaPlayerStateSchema,
|
|
158814
|
-
/**
|
|
158815
|
-
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
158816
|
-
* A slice written before this field existed parses as `reported`.
|
|
158817
|
-
*/
|
|
158818
|
-
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
158819
|
-
/**
|
|
158820
|
-
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
158821
|
-
* this is not empty; a field not listed here is a reading even then (power
|
|
158822
|
-
* from a running-state source, mute guessed from the last send).
|
|
158823
|
-
*/
|
|
158824
|
-
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
158825
|
-
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
158826
|
-
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
158827
|
-
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
158828
|
-
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
158829
|
-
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
158830
|
-
isMuted: boolean().nullable(),
|
|
158831
|
-
/** Active source / input. Empty when no source surface. */
|
|
158832
|
-
source: string(),
|
|
158833
|
-
/** Selectable sources. Empty when no source surface.
|
|
158834
|
-
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
158835
|
-
availableSources: array(string()),
|
|
158836
|
-
/** Currently-playing media info. Null when nothing is playing. */
|
|
158837
|
-
currentMedia: MediaInfoSchema.nullable(),
|
|
158838
|
-
/** Current playback position in ms. Null when not seekable or
|
|
158839
|
-
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
158840
|
-
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
158841
|
-
/** Total duration of the current media in ms. Null when unknown
|
|
158842
|
-
* (live stream) or nothing is playing. */
|
|
158843
|
-
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
158844
|
-
/** Shuffle toggle. Null when no shuffle surface.
|
|
158845
|
-
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
158846
|
-
shuffle: boolean().nullable(),
|
|
158847
|
-
/** Repeat mode. Null when no repeat surface.
|
|
158848
|
-
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
158849
|
-
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
158850
|
-
/** Ms epoch when the slice was last updated. */
|
|
158851
|
-
lastChangedAt: number()
|
|
158852
|
-
});
|
|
158853
|
-
var mediaPlayerCapability = {
|
|
158854
|
-
name: "media-player",
|
|
158855
|
-
scope: "device",
|
|
158856
|
-
deviceNative: true,
|
|
158857
|
-
mode: "singleton",
|
|
158858
|
-
deviceTypes: [DeviceType.MediaPlayer],
|
|
158859
|
-
methods: {
|
|
158860
|
-
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158861
|
-
kind: "mutation",
|
|
158862
|
-
auth: "protected"
|
|
158863
|
-
}),
|
|
158864
|
-
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158865
|
-
kind: "mutation",
|
|
158866
|
-
auth: "protected"
|
|
158867
|
-
}),
|
|
158868
|
-
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158869
|
-
kind: "mutation",
|
|
158870
|
-
auth: "protected"
|
|
158871
|
-
}),
|
|
158872
|
-
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158873
|
-
kind: "mutation",
|
|
158874
|
-
auth: "protected"
|
|
158875
|
-
}),
|
|
158876
|
-
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158877
|
-
kind: "mutation",
|
|
158878
|
-
auth: "protected"
|
|
158879
|
-
}),
|
|
158880
|
-
seek: method(object({
|
|
158881
|
-
deviceId: number().int().nonnegative(),
|
|
158882
|
-
position: number().int().nonnegative()
|
|
158883
|
-
}), _void(), {
|
|
158884
|
-
kind: "mutation",
|
|
158885
|
-
auth: "protected"
|
|
158886
|
-
}),
|
|
158887
|
-
setVolume: method(object({
|
|
158888
|
-
deviceId: number().int().nonnegative(),
|
|
158889
|
-
volumeLevel: number().min(0).max(100)
|
|
158890
|
-
}), _void(), {
|
|
158891
|
-
kind: "mutation",
|
|
158892
|
-
auth: "protected"
|
|
158893
|
-
}),
|
|
158894
|
-
setMute: method(object({
|
|
158895
|
-
deviceId: number().int().nonnegative(),
|
|
158896
|
-
muted: boolean()
|
|
158897
|
-
}), _void(), {
|
|
158898
|
-
kind: "mutation",
|
|
158899
|
-
auth: "protected"
|
|
158900
|
-
}),
|
|
158901
|
-
setShuffle: method(object({
|
|
158902
|
-
deviceId: number().int().nonnegative(),
|
|
158903
|
-
shuffle: boolean()
|
|
158904
|
-
}), _void(), {
|
|
158905
|
-
kind: "mutation",
|
|
158906
|
-
auth: "protected"
|
|
158907
|
-
}),
|
|
158908
|
-
setRepeat: method(object({
|
|
158909
|
-
deviceId: number().int().nonnegative(),
|
|
158910
|
-
repeat: MediaPlayerRepeatSchema
|
|
158911
|
-
}), _void(), {
|
|
158912
|
-
kind: "mutation",
|
|
158913
|
-
auth: "protected"
|
|
158914
|
-
}),
|
|
158915
|
-
selectSource: method(object({
|
|
158916
|
-
deviceId: number().int().nonnegative(),
|
|
158917
|
-
source: string().min(1)
|
|
158918
|
-
}), _void(), {
|
|
158919
|
-
kind: "mutation",
|
|
158920
|
-
auth: "protected"
|
|
158921
|
-
}),
|
|
158922
|
-
playMedia: method(object({
|
|
158923
|
-
deviceId: number().int().nonnegative(),
|
|
158924
|
-
/** Media identifier / URL. */
|
|
158925
|
-
mediaId: string().min(1),
|
|
158926
|
-
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
158927
|
-
* passes it through to the upstream service. */
|
|
158928
|
-
mediaType: string().min(1)
|
|
158929
|
-
}), _void(), {
|
|
158930
|
-
kind: "mutation",
|
|
158931
|
-
auth: "protected"
|
|
158932
|
-
})
|
|
158933
|
-
},
|
|
158934
|
-
status: {
|
|
158935
|
-
schema: MediaPlayerStatusSchema,
|
|
158936
|
-
kind: "push"
|
|
158937
|
-
},
|
|
158938
|
-
/**
|
|
158939
|
-
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
158940
|
-
* full slice for live now-playing, volume, and progress updates
|
|
158941
|
-
* without polling.
|
|
158942
|
-
*/
|
|
158943
|
-
runtimeState: MediaPlayerStatusSchema,
|
|
158944
|
-
/**
|
|
158945
|
-
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
158946
|
-
*
|
|
158947
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
158948
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
158949
|
-
*/
|
|
158950
|
-
durability: "session",
|
|
158951
|
-
/**
|
|
158952
|
-
* History (D850): volume (a level an operator or an automation sets; the
|
|
158953
|
-
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
158954
|
-
* `position` is not recorded: it moves continuously while playing.
|
|
158955
|
-
*/
|
|
158956
|
-
history: { fields: {
|
|
158957
|
-
volumeLevel: {
|
|
158958
|
-
kind: "gauge",
|
|
158959
|
-
unit: "%",
|
|
158960
|
-
deadband: 1
|
|
158961
|
-
},
|
|
158962
|
-
isMuted: { kind: "boolean" }
|
|
158963
|
-
} }
|
|
158964
|
-
};
|
|
158965
|
-
/**
|
|
158966
159324
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
158967
159325
|
*
|
|
158968
159326
|
* Different shape from `network-access` (public tunnels):
|
|
@@ -167741,6 +168099,21 @@ var NcTemplateVarKindSchema = _enum([
|
|
|
167741
168099
|
"sound",
|
|
167742
168100
|
"system"
|
|
167743
168101
|
]);
|
|
168102
|
+
/**
|
|
168103
|
+
* The default title/body a rule of one SITUATION is delivered with when the
|
|
168104
|
+
* operator wrote no template — served so an editor can pre-fill it (and the
|
|
168105
|
+
* operator can rebuild it from placeholders) instead of starting from a blank
|
|
168106
|
+
* box that, once saved, silently replaces it. `id` names the situation inside
|
|
168107
|
+
* a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
|
|
168108
|
+
* appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
|
|
168109
|
+
* the hub's current language and with its overrides applied.
|
|
168110
|
+
*/
|
|
168111
|
+
var NcTemplateDefaultSchema = object({
|
|
168112
|
+
id: _enum(["appliance-cycle"]),
|
|
168113
|
+
kind: NcRuleKindSchema,
|
|
168114
|
+
title: string(),
|
|
168115
|
+
body: string()
|
|
168116
|
+
});
|
|
167744
168117
|
/** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
|
|
167745
168118
|
var NcTemplateFamilySchema = _enum([
|
|
167746
168119
|
"rule",
|
|
@@ -168596,7 +168969,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
168596
168969
|
}), method(object({}), object({
|
|
168597
168970
|
catalog: array(NcConditionDescriptorSchema),
|
|
168598
168971
|
taxonomy: NcTaxonomySchema.optional()
|
|
168599
|
-
})), method(object({}), object({
|
|
168972
|
+
})), method(object({}), object({
|
|
168973
|
+
vars: array(NcTemplateVarDescriptorSchema),
|
|
168974
|
+
/** Per-situation default texts; absent on a hub that predates them. */
|
|
168975
|
+
defaults: array(NcTemplateDefaultSchema)
|
|
168976
|
+
})), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
|
|
168600
168977
|
kind: "mutation",
|
|
168601
168978
|
access: "view"
|
|
168602
168979
|
}), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
|
|
@@ -171233,6 +171610,14 @@ var powerMeterCapability = {
|
|
|
171233
171610
|
deviceNative: true,
|
|
171234
171611
|
mode: "singleton",
|
|
171235
171612
|
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
171613
|
+
deviceConfig: { ui: {
|
|
171614
|
+
kind: "widget",
|
|
171615
|
+
widgetId: "host/power-meter-line",
|
|
171616
|
+
tab: "power",
|
|
171617
|
+
placement: "hero",
|
|
171618
|
+
label: "Power",
|
|
171619
|
+
order: 4
|
|
171620
|
+
} },
|
|
171236
171621
|
methods: {},
|
|
171237
171622
|
status: {
|
|
171238
171623
|
schema: PowerMeterStatusSchema,
|
|
@@ -171292,6 +171677,117 @@ var powerMeterCapability = {
|
|
|
171292
171677
|
volatileStateFields: ["lastFetchedAt"],
|
|
171293
171678
|
customize: powerMeterCustomize
|
|
171294
171679
|
};
|
|
171680
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
171681
|
+
"no-power-source",
|
|
171682
|
+
"switch-missing",
|
|
171683
|
+
"switch-offline",
|
|
171684
|
+
"switch-unknown",
|
|
171685
|
+
"restart-in-progress",
|
|
171686
|
+
"switch-call-failed"
|
|
171687
|
+
]);
|
|
171688
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
171689
|
+
accepted: literal(false),
|
|
171690
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
171691
|
+
/** One line for the operator, naming the switch. */
|
|
171692
|
+
message: string()
|
|
171693
|
+
})]);
|
|
171694
|
+
var PowerSourceInfoSchema = object({
|
|
171695
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
171696
|
+
linked: boolean(),
|
|
171697
|
+
switchDeviceId: number().nullable(),
|
|
171698
|
+
switchName: string().nullable(),
|
|
171699
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
171700
|
+
switchOn: boolean().nullable(),
|
|
171701
|
+
switchOnline: boolean().nullable(),
|
|
171702
|
+
restartInProgress: boolean(),
|
|
171703
|
+
offSeconds: number().int()
|
|
171704
|
+
});
|
|
171705
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
171706
|
+
var PowerSourceStateSchema = object({
|
|
171707
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
171708
|
+
switchOn: boolean().nullable(),
|
|
171709
|
+
offSeconds: number().int(),
|
|
171710
|
+
lastFetchedAt: number()
|
|
171711
|
+
});
|
|
171712
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
171713
|
+
DeviceType.Gateway,
|
|
171714
|
+
DeviceType.Hub,
|
|
171715
|
+
DeviceType.Camera,
|
|
171716
|
+
DeviceType.Sensor,
|
|
171717
|
+
DeviceType.MediaPlayer,
|
|
171718
|
+
DeviceType.Vacuum,
|
|
171719
|
+
DeviceType.Generic,
|
|
171720
|
+
DeviceType.Washer,
|
|
171721
|
+
DeviceType.Dryer,
|
|
171722
|
+
DeviceType.Dishwasher,
|
|
171723
|
+
DeviceType.PetFountain,
|
|
171724
|
+
DeviceType.Fan,
|
|
171725
|
+
DeviceType.Humidifier,
|
|
171726
|
+
DeviceType.Climate,
|
|
171727
|
+
DeviceType.WaterHeater
|
|
171728
|
+
];
|
|
171729
|
+
var powerSourceCustomize = {
|
|
171730
|
+
version: 1,
|
|
171731
|
+
targets: [{
|
|
171732
|
+
id: "switch",
|
|
171733
|
+
cardinality: "scalar",
|
|
171734
|
+
vocabulary: {
|
|
171735
|
+
cap: "power-source",
|
|
171736
|
+
stateField: "switchOn"
|
|
171737
|
+
},
|
|
171738
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
171739
|
+
apply: "add",
|
|
171740
|
+
path: "switchOn",
|
|
171741
|
+
sources: [{
|
|
171742
|
+
kind: "switch",
|
|
171743
|
+
transforms: [{ kind: "identity" }]
|
|
171744
|
+
}],
|
|
171745
|
+
companions: [{
|
|
171746
|
+
kind: "constant",
|
|
171747
|
+
path: "offSeconds",
|
|
171748
|
+
value: 10,
|
|
171749
|
+
when: "add"
|
|
171750
|
+
}, {
|
|
171751
|
+
kind: "clock",
|
|
171752
|
+
path: "lastFetchedAt",
|
|
171753
|
+
when: "always"
|
|
171754
|
+
}],
|
|
171755
|
+
sourceReport: { status: "assignment-status" }
|
|
171756
|
+
}]
|
|
171757
|
+
};
|
|
171758
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
171759
|
+
var powerSourceCapability = {
|
|
171760
|
+
name: "power-source",
|
|
171761
|
+
scope: "device",
|
|
171762
|
+
mode: "singleton",
|
|
171763
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
171764
|
+
methods: {
|
|
171765
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
171766
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171767
|
+
kind: "mutation",
|
|
171768
|
+
auth: "admin"
|
|
171769
|
+
}),
|
|
171770
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171771
|
+
kind: "mutation",
|
|
171772
|
+
auth: "admin"
|
|
171773
|
+
}),
|
|
171774
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171775
|
+
kind: "mutation",
|
|
171776
|
+
auth: "admin"
|
|
171777
|
+
})
|
|
171778
|
+
},
|
|
171779
|
+
runtimeState: PowerSourceStateSchema,
|
|
171780
|
+
customize: powerSourceCustomize,
|
|
171781
|
+
/**
|
|
171782
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
171783
|
+
* its next report; a restored value would outlive the reading it names.
|
|
171784
|
+
*
|
|
171785
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
171786
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
171787
|
+
*/
|
|
171788
|
+
durability: "session",
|
|
171789
|
+
history: "none"
|
|
171790
|
+
};
|
|
171295
171791
|
/**
|
|
171296
171792
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
171297
171793
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -175354,7 +175850,13 @@ var RemoteMediaCommandSchema = _enum([
|
|
|
175354
175850
|
"next",
|
|
175355
175851
|
"previous",
|
|
175356
175852
|
"setMute",
|
|
175357
|
-
"selectSource"
|
|
175853
|
+
"selectSource",
|
|
175854
|
+
"turnOn",
|
|
175855
|
+
"turnOff",
|
|
175856
|
+
"volumeUp",
|
|
175857
|
+
"volumeDown",
|
|
175858
|
+
"channelUp",
|
|
175859
|
+
"channelDown"
|
|
175358
175860
|
]);
|
|
175359
175861
|
/** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
|
|
175360
175862
|
var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
|
|
@@ -175375,7 +175877,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
|
|
|
175375
175877
|
}), RemoteSendResultSchema, {
|
|
175376
175878
|
kind: "mutation",
|
|
175377
175879
|
auth: "protected"
|
|
175378
|
-
}), method(deviceInput.extend({
|
|
175880
|
+
}), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
|
|
175379
175881
|
command: RemoteMediaCommandSchema,
|
|
175380
175882
|
muted: boolean().optional(),
|
|
175381
175883
|
source: string().optional(),
|
|
@@ -177725,7 +178227,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
|
|
|
177725
178227
|
* `getSettingsUISchema()` surface.
|
|
177726
178228
|
*/
|
|
177727
178229
|
var SwitchStatusSchema = object({
|
|
177728
|
-
|
|
178230
|
+
/**
|
|
178231
|
+
* `null` = the device has never reported its state (a measurement that
|
|
178232
|
+
* has not happened is unknown, never `false`).
|
|
178233
|
+
*/
|
|
178234
|
+
on: boolean().nullable().meta({ label: "On" }),
|
|
177729
178235
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
177730
178236
|
lastChangedAt: number()
|
|
177731
178237
|
});
|
|
@@ -177750,7 +178256,7 @@ var switchCapability = {
|
|
|
177750
178256
|
}) },
|
|
177751
178257
|
events: { onStateChanged: { data: object({
|
|
177752
178258
|
deviceId: number(),
|
|
177753
|
-
on: boolean(),
|
|
178259
|
+
on: boolean().nullable(),
|
|
177754
178260
|
lastChangedAt: number()
|
|
177755
178261
|
}) } },
|
|
177756
178262
|
status: {
|
|
@@ -181762,6 +182268,26 @@ function buildStreamParamsConfigSchema(options, status) {
|
|
|
181762
182268
|
if (sections.length === 0) return null;
|
|
181763
182269
|
return { sections };
|
|
181764
182270
|
}
|
|
182271
|
+
/**
|
|
182272
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
182273
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
182274
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
182275
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
182276
|
+
* server sends an id, a client maps it to its own icon set.
|
|
182277
|
+
*
|
|
182278
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
182279
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
182280
|
+
* to `icon`.
|
|
182281
|
+
*/
|
|
182282
|
+
function digit(n) {
|
|
182283
|
+
return {
|
|
182284
|
+
label: t(n, n),
|
|
182285
|
+
icon: "hash",
|
|
182286
|
+
legend: n
|
|
182287
|
+
};
|
|
182288
|
+
}
|
|
182289
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
182290
|
+
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
181765
182291
|
var btn = (idSuffix, row, col, init) => ({
|
|
181766
182292
|
idSuffix,
|
|
181767
182293
|
row,
|
|
@@ -181889,6 +182415,34 @@ discriminatedUnion("kind", [object({
|
|
|
181889
182415
|
code: string()
|
|
181890
182416
|
})]);
|
|
181891
182417
|
/**
|
|
182418
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
182419
|
+
*
|
|
182420
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
182421
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
182422
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
182423
|
+
* count of samples.
|
|
182424
|
+
*
|
|
182425
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
182426
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
182427
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
182428
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
182429
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
182430
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
182431
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
182432
|
+
* covers the longest standby blip above T.
|
|
182433
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
182434
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
182435
|
+
*/
|
|
182436
|
+
var SECOND_MS = 1e3;
|
|
182437
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
182438
|
+
24 * (60 * MINUTE_MS$1);
|
|
182439
|
+
10 * MINUTE_MS$1;
|
|
182440
|
+
30 * SECOND_MS;
|
|
182441
|
+
60 * SECOND_MS;
|
|
182442
|
+
20 * MINUTE_MS$1;
|
|
182443
|
+
10 * SECOND_MS;
|
|
182444
|
+
10 * SECOND_MS;
|
|
182445
|
+
/**
|
|
181892
182446
|
* Which of OUR object detections a clip window holds — the ONE overlap rule
|
|
181893
182447
|
* and the ONE label ranking, for every clip source (D923).
|
|
181894
182448
|
*
|
|
@@ -185141,6 +185695,13 @@ Object.freeze({
|
|
|
185141
185695
|
access: "create",
|
|
185142
185696
|
role: "admin"
|
|
185143
185697
|
},
|
|
185698
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
185699
|
+
capName: "core-blocks",
|
|
185700
|
+
capScope: "system",
|
|
185701
|
+
addonId: null,
|
|
185702
|
+
access: "view",
|
|
185703
|
+
role: "admin"
|
|
185704
|
+
},
|
|
185144
185705
|
"coreBlocks.reportAutomationRun": {
|
|
185145
185706
|
capName: "core-blocks",
|
|
185146
185707
|
capScope: "system",
|
|
@@ -185652,6 +186213,13 @@ Object.freeze({
|
|
|
185652
186213
|
access: "view",
|
|
185653
186214
|
role: "admin"
|
|
185654
186215
|
},
|
|
186216
|
+
"deviceExport.listInstances": {
|
|
186217
|
+
capName: "device-export",
|
|
186218
|
+
capScope: "system",
|
|
186219
|
+
addonId: null,
|
|
186220
|
+
access: "view",
|
|
186221
|
+
role: "admin"
|
|
186222
|
+
},
|
|
185655
186223
|
"deviceExport.listSupportedDeviceKinds": {
|
|
185656
186224
|
capName: "device-export",
|
|
185657
186225
|
capScope: "system",
|
|
@@ -187304,6 +187872,13 @@ Object.freeze({
|
|
|
187304
187872
|
access: "create",
|
|
187305
187873
|
role: "admin"
|
|
187306
187874
|
},
|
|
187875
|
+
"integrations.clearSettings": {
|
|
187876
|
+
capName: "integrations",
|
|
187877
|
+
capScope: "system",
|
|
187878
|
+
addonId: null,
|
|
187879
|
+
access: "delete",
|
|
187880
|
+
role: "admin"
|
|
187881
|
+
},
|
|
187307
187882
|
"integrations.create": {
|
|
187308
187883
|
capName: "integrations",
|
|
187309
187884
|
capScope: "system",
|
|
@@ -188088,6 +188663,20 @@ Object.freeze({
|
|
|
188088
188663
|
access: "view",
|
|
188089
188664
|
role: "admin"
|
|
188090
188665
|
},
|
|
188666
|
+
"mediaPlayer.channelDown": {
|
|
188667
|
+
capName: "media-player",
|
|
188668
|
+
capScope: "device",
|
|
188669
|
+
addonId: null,
|
|
188670
|
+
access: "create",
|
|
188671
|
+
role: "viewer"
|
|
188672
|
+
},
|
|
188673
|
+
"mediaPlayer.channelUp": {
|
|
188674
|
+
capName: "media-player",
|
|
188675
|
+
capScope: "device",
|
|
188676
|
+
addonId: null,
|
|
188677
|
+
access: "create",
|
|
188678
|
+
role: "viewer"
|
|
188679
|
+
},
|
|
188091
188680
|
"mediaPlayer.getStatus": {
|
|
188092
188681
|
capName: "media-player",
|
|
188093
188682
|
capScope: "device",
|
|
@@ -188179,6 +188768,34 @@ Object.freeze({
|
|
|
188179
188768
|
access: "create",
|
|
188180
188769
|
role: "viewer"
|
|
188181
188770
|
},
|
|
188771
|
+
"mediaPlayer.turnOff": {
|
|
188772
|
+
capName: "media-player",
|
|
188773
|
+
capScope: "device",
|
|
188774
|
+
addonId: null,
|
|
188775
|
+
access: "create",
|
|
188776
|
+
role: "viewer"
|
|
188777
|
+
},
|
|
188778
|
+
"mediaPlayer.turnOn": {
|
|
188779
|
+
capName: "media-player",
|
|
188780
|
+
capScope: "device",
|
|
188781
|
+
addonId: null,
|
|
188782
|
+
access: "create",
|
|
188783
|
+
role: "viewer"
|
|
188784
|
+
},
|
|
188785
|
+
"mediaPlayer.volumeDown": {
|
|
188786
|
+
capName: "media-player",
|
|
188787
|
+
capScope: "device",
|
|
188788
|
+
addonId: null,
|
|
188789
|
+
access: "create",
|
|
188790
|
+
role: "viewer"
|
|
188791
|
+
},
|
|
188792
|
+
"mediaPlayer.volumeUp": {
|
|
188793
|
+
capName: "media-player",
|
|
188794
|
+
capScope: "device",
|
|
188795
|
+
addonId: null,
|
|
188796
|
+
access: "create",
|
|
188797
|
+
role: "viewer"
|
|
188798
|
+
},
|
|
188182
188799
|
"meshNetwork.getStatus": {
|
|
188183
188800
|
capName: "mesh-network",
|
|
188184
188801
|
capScope: "system",
|
|
@@ -191735,6 +192352,13 @@ Object.freeze({
|
|
|
191735
192352
|
access: "create",
|
|
191736
192353
|
role: "admin"
|
|
191737
192354
|
},
|
|
192355
|
+
"remote.getAssumedStatus": {
|
|
192356
|
+
capName: "remote",
|
|
192357
|
+
capScope: "system",
|
|
192358
|
+
addonId: null,
|
|
192359
|
+
access: "view",
|
|
192360
|
+
role: "admin"
|
|
192361
|
+
},
|
|
191738
192362
|
"remote.getConfig": {
|
|
191739
192363
|
capName: "remote",
|
|
191740
192364
|
capScope: "system",
|
|
@@ -194702,6 +195326,11 @@ Object.freeze({
|
|
|
194702
195326
|
form: "single",
|
|
194703
195327
|
optional: false
|
|
194704
195328
|
}],
|
|
195329
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
195330
|
+
name: "deviceId",
|
|
195331
|
+
form: "single",
|
|
195332
|
+
optional: false
|
|
195333
|
+
}],
|
|
194705
195334
|
"cover.close": [{
|
|
194706
195335
|
name: "deviceId",
|
|
194707
195336
|
form: "single",
|
|
@@ -195553,6 +196182,16 @@ Object.freeze({
|
|
|
195553
196182
|
form: "single",
|
|
195554
196183
|
optional: true
|
|
195555
196184
|
}],
|
|
196185
|
+
"mediaPlayer.channelDown": [{
|
|
196186
|
+
name: "deviceId",
|
|
196187
|
+
form: "single",
|
|
196188
|
+
optional: false
|
|
196189
|
+
}],
|
|
196190
|
+
"mediaPlayer.channelUp": [{
|
|
196191
|
+
name: "deviceId",
|
|
196192
|
+
form: "single",
|
|
196193
|
+
optional: false
|
|
196194
|
+
}],
|
|
195556
196195
|
"mediaPlayer.getStatus": [{
|
|
195557
196196
|
name: "deviceId",
|
|
195558
196197
|
form: "single",
|
|
@@ -195618,6 +196257,26 @@ Object.freeze({
|
|
|
195618
196257
|
form: "single",
|
|
195619
196258
|
optional: false
|
|
195620
196259
|
}],
|
|
196260
|
+
"mediaPlayer.turnOff": [{
|
|
196261
|
+
name: "deviceId",
|
|
196262
|
+
form: "single",
|
|
196263
|
+
optional: false
|
|
196264
|
+
}],
|
|
196265
|
+
"mediaPlayer.turnOn": [{
|
|
196266
|
+
name: "deviceId",
|
|
196267
|
+
form: "single",
|
|
196268
|
+
optional: false
|
|
196269
|
+
}],
|
|
196270
|
+
"mediaPlayer.volumeDown": [{
|
|
196271
|
+
name: "deviceId",
|
|
196272
|
+
form: "single",
|
|
196273
|
+
optional: false
|
|
196274
|
+
}],
|
|
196275
|
+
"mediaPlayer.volumeUp": [{
|
|
196276
|
+
name: "deviceId",
|
|
196277
|
+
form: "single",
|
|
196278
|
+
optional: false
|
|
196279
|
+
}],
|
|
195621
196280
|
"meter.getStatus": [{
|
|
195622
196281
|
name: "deviceId",
|
|
195623
196282
|
form: "single",
|
|
@@ -196783,6 +197442,11 @@ Object.freeze({
|
|
|
196783
197442
|
form: "single",
|
|
196784
197443
|
optional: true
|
|
196785
197444
|
}],
|
|
197445
|
+
"remote.getAssumedStatus": [{
|
|
197446
|
+
name: "deviceId",
|
|
197447
|
+
form: "single",
|
|
197448
|
+
optional: false
|
|
197449
|
+
}],
|
|
196786
197450
|
"remote.getConfig": [{
|
|
196787
197451
|
name: "deviceId",
|
|
196788
197452
|
form: "single",
|
|
@@ -197330,99 +197994,6 @@ Object.freeze({
|
|
|
197330
197994
|
"network-access": "ingress",
|
|
197331
197995
|
"smtp-provider": "email"
|
|
197332
197996
|
});
|
|
197333
|
-
/**
|
|
197334
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
197335
|
-
* `z.infer`, they do not restate the shape.
|
|
197336
|
-
*/
|
|
197337
|
-
var OverrideSourceSchema = object({
|
|
197338
|
-
addonId: string().min(1),
|
|
197339
|
-
stableId: string().min(1),
|
|
197340
|
-
cap: string().min(1),
|
|
197341
|
-
fieldPath: string().min(1)
|
|
197342
|
-
});
|
|
197343
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
197344
|
-
object({ kind: literal("identity") }),
|
|
197345
|
-
object({ kind: literal("invert") }),
|
|
197346
|
-
object({
|
|
197347
|
-
kind: literal("threshold"),
|
|
197348
|
-
comparison: _enum(["gte", "lte"]),
|
|
197349
|
-
threshold: number(),
|
|
197350
|
-
startDwellMs: number().nonnegative(),
|
|
197351
|
-
stopDwellMs: number().nonnegative()
|
|
197352
|
-
}),
|
|
197353
|
-
object({
|
|
197354
|
-
kind: literal("linear"),
|
|
197355
|
-
scale: number(),
|
|
197356
|
-
offset: number(),
|
|
197357
|
-
clampMin: number().optional(),
|
|
197358
|
-
clampMax: number().optional()
|
|
197359
|
-
}),
|
|
197360
|
-
object({
|
|
197361
|
-
kind: literal("enum-map"),
|
|
197362
|
-
mapping: record(string(), string().nullable())
|
|
197363
|
-
}),
|
|
197364
|
-
object({ kind: literal("number") })
|
|
197365
|
-
]);
|
|
197366
|
-
var ScalarOverrideDraftSchema = object({
|
|
197367
|
-
kind: literal("scalar"),
|
|
197368
|
-
cap: string().min(1),
|
|
197369
|
-
targetId: string().min(1),
|
|
197370
|
-
source: OverrideSourceSchema,
|
|
197371
|
-
transform: CustomizeTransformDraftSchema
|
|
197372
|
-
});
|
|
197373
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
197374
|
-
kind: literal("keyedArray"),
|
|
197375
|
-
cap: string().min(1),
|
|
197376
|
-
targetId: string().min(1),
|
|
197377
|
-
key: string().min(1),
|
|
197378
|
-
label: string().min(1),
|
|
197379
|
-
fieldPath: string().min(1),
|
|
197380
|
-
source: OverrideSourceSchema,
|
|
197381
|
-
transform: CustomizeTransformDraftSchema
|
|
197382
|
-
});
|
|
197383
|
-
var PulseOverrideDraftSchema = object({
|
|
197384
|
-
kind: literal("pulse"),
|
|
197385
|
-
cap: string().min(1),
|
|
197386
|
-
targetId: string().min(1),
|
|
197387
|
-
enabled: boolean(),
|
|
197388
|
-
debounceMs: number().nonnegative(),
|
|
197389
|
-
edges: array(_enum([
|
|
197390
|
-
"rising",
|
|
197391
|
-
"button",
|
|
197392
|
-
"native-event"
|
|
197393
|
-
])).min(1).optional(),
|
|
197394
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
197395
|
-
});
|
|
197396
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
197397
|
-
"in-addition",
|
|
197398
|
-
"confirm",
|
|
197399
|
-
"replace"
|
|
197400
|
-
]).optional() });
|
|
197401
|
-
discriminatedUnion("kind", [
|
|
197402
|
-
ScalarOverrideDraftSchema,
|
|
197403
|
-
KeyedArrayOverrideDraftSchema,
|
|
197404
|
-
PulseOverrideDraftSchema,
|
|
197405
|
-
object({
|
|
197406
|
-
kind: literal("span"),
|
|
197407
|
-
cap: string().min(1),
|
|
197408
|
-
targetId: string().min(1),
|
|
197409
|
-
timeoutMs: number().nonnegative(),
|
|
197410
|
-
cooldownMs: number().nonnegative().optional(),
|
|
197411
|
-
sources: array(SpanSourceDraftSchema)
|
|
197412
|
-
})
|
|
197413
|
-
]);
|
|
197414
|
-
object({
|
|
197415
|
-
targetId: string().min(1),
|
|
197416
|
-
configured: boolean(),
|
|
197417
|
-
source: _enum(["native", "assigned"]),
|
|
197418
|
-
state: _enum([
|
|
197419
|
-
"active",
|
|
197420
|
-
"stale",
|
|
197421
|
-
"released",
|
|
197422
|
-
"unavailable"
|
|
197423
|
-
]),
|
|
197424
|
-
reason: string().nullable()
|
|
197425
|
-
});
|
|
197426
197997
|
var G711_SCALE_CORRECTION_DB = {
|
|
197427
197998
|
PCMU: 20 * Math.log10(4),
|
|
197428
197999
|
PCMA: 20 * Math.log10(8)
|
|
@@ -198038,6 +198609,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
198038
198609
|
[...RULE_TEXT],
|
|
198039
198610
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
198040
198611
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
198612
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
198041
198613
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
198042
198614
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
198043
198615
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -208237,7 +208809,7 @@ var PirAccessory = class extends BaseDevice {
|
|
|
208237
208809
|
async applySettingsPatch(patch) {
|
|
208238
208810
|
await this.config.setAll(patch);
|
|
208239
208811
|
if ("pirSensitive" in patch || "pirReduceAlarm" in patch || "pirIntervalS" in patch) try {
|
|
208240
|
-
await this.pushConfigToFirmware(this.switchView().on);
|
|
208812
|
+
await this.pushConfigToFirmware(this.switchView().on === true);
|
|
208241
208813
|
} catch (err) {
|
|
208242
208814
|
this.ctx.logger.warn("pir firmware push failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
208243
208815
|
throw err;
|
|
@@ -219433,6 +220005,285 @@ async function startLegacyWebhookIntake(deps) {
|
|
|
219433
220005
|
};
|
|
219434
220006
|
}
|
|
219435
220007
|
//#endregion
|
|
220008
|
+
//#region src/reolink-discovery-map.ts
|
|
220009
|
+
/**
|
|
220010
|
+
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
220011
|
+
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
220012
|
+
*/
|
|
220013
|
+
function slugifyReolinkHost(host) {
|
|
220014
|
+
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
220015
|
+
}
|
|
220016
|
+
/**
|
|
220017
|
+
* Decide whether a discovered host is a device this provider already owns.
|
|
220018
|
+
*
|
|
220019
|
+
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
220020
|
+
* wins even when the host differs, because the address is the thing that moves.
|
|
220021
|
+
*/
|
|
220022
|
+
function findOnboardedMatch(discovered, onboarded) {
|
|
220023
|
+
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
220024
|
+
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
220025
|
+
if (byUid !== void 0) return byUid;
|
|
220026
|
+
}
|
|
220027
|
+
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
220028
|
+
}
|
|
220029
|
+
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
220030
|
+
var HUB_BAICHUAN_PORT = 9e3;
|
|
220031
|
+
/**
|
|
220032
|
+
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
220033
|
+
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
220034
|
+
* as a camera to add.
|
|
220035
|
+
*/
|
|
220036
|
+
function looksLikeReolinkHub(model) {
|
|
220037
|
+
if (model === void 0) return false;
|
|
220038
|
+
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
220039
|
+
}
|
|
220040
|
+
/**
|
|
220041
|
+
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
220042
|
+
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
220043
|
+
* creates hides this candidate through `originStableId`.
|
|
220044
|
+
*/
|
|
220045
|
+
function reolinkHubStableId(identity) {
|
|
220046
|
+
const uid = identity.uid?.trim() ?? "";
|
|
220047
|
+
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
220048
|
+
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
220049
|
+
}
|
|
220050
|
+
function isKnownHub(d, known) {
|
|
220051
|
+
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
220052
|
+
}
|
|
220053
|
+
/**
|
|
220054
|
+
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
220055
|
+
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
220056
|
+
* first responder for a host wins. Pure: no I/O.
|
|
220057
|
+
*
|
|
220058
|
+
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
220059
|
+
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
220060
|
+
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
220061
|
+
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
220062
|
+
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
220063
|
+
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
220064
|
+
* never a scan result.
|
|
220065
|
+
*
|
|
220066
|
+
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
220067
|
+
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
220068
|
+
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
220069
|
+
* in the first place, so a scan says so instead of offering it again.
|
|
220070
|
+
*/
|
|
220071
|
+
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
220072
|
+
const username = credentials.username?.trim() ?? "";
|
|
220073
|
+
const password = credentials.password ?? "";
|
|
220074
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
220075
|
+
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
220076
|
+
return [...byHost.values()].flatMap((d) => {
|
|
220077
|
+
const displayName = d.name ?? d.model ?? d.host;
|
|
220078
|
+
if (looksLikeReolinkHub(d.model)) {
|
|
220079
|
+
if (isKnownHub(d, hubIntegrations)) return [];
|
|
220080
|
+
return [{
|
|
220081
|
+
stableId: reolinkHubStableId(d),
|
|
220082
|
+
type: DeviceType.Gateway,
|
|
220083
|
+
suggestedName: displayName,
|
|
220084
|
+
prefilledConfig: {
|
|
220085
|
+
host: d.host,
|
|
220086
|
+
port: HUB_BAICHUAN_PORT,
|
|
220087
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220088
|
+
...username ? { username } : {}
|
|
220089
|
+
},
|
|
220090
|
+
setupIntegration: true
|
|
220091
|
+
}];
|
|
220092
|
+
}
|
|
220093
|
+
const match = findOnboardedMatch(d, onboarded);
|
|
220094
|
+
return [{
|
|
220095
|
+
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
220096
|
+
type: DeviceType.Camera,
|
|
220097
|
+
suggestedName: displayName,
|
|
220098
|
+
prefilledConfig: {
|
|
220099
|
+
name: displayName,
|
|
220100
|
+
host: d.host,
|
|
220101
|
+
transport: "auto",
|
|
220102
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220103
|
+
...username ? { username } : {},
|
|
220104
|
+
...password ? { password } : {}
|
|
220105
|
+
},
|
|
220106
|
+
...match !== void 0 ? {
|
|
220107
|
+
alreadyOnboarded: true,
|
|
220108
|
+
onboardedDeviceId: match.deviceId,
|
|
220109
|
+
onboardedName: match.name
|
|
220110
|
+
} : {}
|
|
220111
|
+
}];
|
|
220112
|
+
});
|
|
220113
|
+
}
|
|
220114
|
+
//#endregion
|
|
220115
|
+
//#region src/reolink-hub-migration.ts
|
|
220116
|
+
/**
|
|
220117
|
+
* D985 — boot migration of the old Reolink Hub DEVICE into an INTEGRATION.
|
|
220118
|
+
*
|
|
220119
|
+
* A Hub used to be a device with the NVR's credentials in its config and its channels as
|
|
220120
|
+
* parented child cameras. Now the NVR is an integration (credentials in its keychain-backed
|
|
220121
|
+
* settings) and the channels are top-level cameras stamped with its id. For every Hub row:
|
|
220122
|
+
*
|
|
220123
|
+
* 1. create (or reuse) an integration named after it, config + password moved as settings;
|
|
220124
|
+
* 2. keep every child's deviceId and stableId, stamp it with the integrationId, un-parent it
|
|
220125
|
+
* and make sure its persisted blob can be restored on its own;
|
|
220126
|
+
* 3. only AFTER the children restored and the hub has none left, retire the hub by tombstone.
|
|
220127
|
+
*
|
|
220128
|
+
* Idempotent (a second run finds no Hub rows) and all-or-nothing per hub: a step that throws
|
|
220129
|
+
* leaves the hub device and its children's parentage exactly as they were, and the next boot
|
|
220130
|
+
* starts over. A locked secret is never read as an empty password and no replacement is minted.
|
|
220131
|
+
*/
|
|
220132
|
+
/** The marker an integration carries so a re-run reuses it instead of minting a second one. */
|
|
220133
|
+
var MIGRATED_FROM_HUB_KEY = "migratedFromHubDeviceId";
|
|
220134
|
+
function positivePort(value) {
|
|
220135
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
220136
|
+
}
|
|
220137
|
+
var CONNECTION_FIELDS = [
|
|
220138
|
+
"host",
|
|
220139
|
+
"port",
|
|
220140
|
+
"username",
|
|
220141
|
+
"transport"
|
|
220142
|
+
];
|
|
220143
|
+
function str(value) {
|
|
220144
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
220145
|
+
}
|
|
220146
|
+
function migratedChildConfig(child, hub, integrationId) {
|
|
220147
|
+
const filled = {};
|
|
220148
|
+
for (const key of CONNECTION_FIELDS) {
|
|
220149
|
+
const own = child[key];
|
|
220150
|
+
const inherited = hub[key];
|
|
220151
|
+
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
220152
|
+
}
|
|
220153
|
+
return {
|
|
220154
|
+
...child,
|
|
220155
|
+
...filled,
|
|
220156
|
+
integrationId
|
|
220157
|
+
};
|
|
220158
|
+
}
|
|
220159
|
+
async function migrateOneHub(hub, children, integrations, ports) {
|
|
220160
|
+
const read = readHubConnection(hub.config);
|
|
220161
|
+
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
220162
|
+
const { connection } = read;
|
|
220163
|
+
const uid = str(hub.config["uid"]);
|
|
220164
|
+
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
220165
|
+
addonId: ports.addonId,
|
|
220166
|
+
name: hub.name,
|
|
220167
|
+
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
220168
|
+
originStableId: reolinkHubStableId({
|
|
220169
|
+
...uid !== void 0 ? { uid } : {},
|
|
220170
|
+
host: connection.host
|
|
220171
|
+
}),
|
|
220172
|
+
settings: {
|
|
220173
|
+
host: connection.host,
|
|
220174
|
+
port: connection.port,
|
|
220175
|
+
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
220176
|
+
username: connection.username,
|
|
220177
|
+
password: connection.password,
|
|
220178
|
+
transport: connection.transport,
|
|
220179
|
+
...uid !== void 0 ? { uid } : {},
|
|
220180
|
+
debugGeneral: connection.debugGeneral,
|
|
220181
|
+
debugSocketLogs: connection.debugSocketLogs
|
|
220182
|
+
},
|
|
220183
|
+
secretKeys: ["password"]
|
|
220184
|
+
})).id;
|
|
220185
|
+
const rewritten = [];
|
|
220186
|
+
for (const child of children) {
|
|
220187
|
+
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
220188
|
+
await ports.persistConfig(child.stableId, config);
|
|
220189
|
+
await ports.setIntegrationId(child.id, integrationId);
|
|
220190
|
+
rewritten.push({
|
|
220191
|
+
...child,
|
|
220192
|
+
config,
|
|
220193
|
+
parentDeviceId: null
|
|
220194
|
+
});
|
|
220195
|
+
}
|
|
220196
|
+
return {
|
|
220197
|
+
rows: rewritten,
|
|
220198
|
+
retirement: {
|
|
220199
|
+
hubDeviceId: hub.id,
|
|
220200
|
+
hubName: hub.name,
|
|
220201
|
+
integrationId
|
|
220202
|
+
}
|
|
220203
|
+
};
|
|
220204
|
+
}
|
|
220205
|
+
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
220206
|
+
async function planHubMigration(saved, ports) {
|
|
220207
|
+
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
220208
|
+
if (hubs.length === 0) return {
|
|
220209
|
+
rows: saved,
|
|
220210
|
+
retirements: []
|
|
220211
|
+
};
|
|
220212
|
+
let integrations;
|
|
220213
|
+
try {
|
|
220214
|
+
integrations = await ports.listIntegrations();
|
|
220215
|
+
} catch (err) {
|
|
220216
|
+
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
220217
|
+
return {
|
|
220218
|
+
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
220219
|
+
retirements: []
|
|
220220
|
+
};
|
|
220221
|
+
}
|
|
220222
|
+
const hubIds = new Set(hubs.map((h) => h.id));
|
|
220223
|
+
const rewrittenById = /* @__PURE__ */ new Map();
|
|
220224
|
+
const retirements = [];
|
|
220225
|
+
for (const hub of hubs) {
|
|
220226
|
+
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
220227
|
+
try {
|
|
220228
|
+
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
220229
|
+
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
220230
|
+
retirements.push(done.retirement);
|
|
220231
|
+
ports.logger.info("Reolink hub migrated to an integration", {
|
|
220232
|
+
tags: { deviceId: hub.id },
|
|
220233
|
+
meta: {
|
|
220234
|
+
integrationId: done.retirement.integrationId,
|
|
220235
|
+
children: children.length,
|
|
220236
|
+
name: hub.name
|
|
220237
|
+
}
|
|
220238
|
+
});
|
|
220239
|
+
} catch (err) {
|
|
220240
|
+
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
220241
|
+
tags: { deviceId: hub.id },
|
|
220242
|
+
meta: {
|
|
220243
|
+
error: errMsg$3(err),
|
|
220244
|
+
name: hub.name
|
|
220245
|
+
}
|
|
220246
|
+
});
|
|
220247
|
+
}
|
|
220248
|
+
}
|
|
220249
|
+
return {
|
|
220250
|
+
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
220251
|
+
retirements
|
|
220252
|
+
};
|
|
220253
|
+
}
|
|
220254
|
+
/**
|
|
220255
|
+
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
220256
|
+
* and named: removing it would cascade the camera away with it.
|
|
220257
|
+
*/
|
|
220258
|
+
async function retireMigratedHubs(retirements, ports) {
|
|
220259
|
+
let retired = 0;
|
|
220260
|
+
for (const r of retirements) try {
|
|
220261
|
+
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
220262
|
+
if (remaining.length > 0) {
|
|
220263
|
+
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
220264
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220265
|
+
meta: {
|
|
220266
|
+
remaining: [...remaining],
|
|
220267
|
+
integrationId: r.integrationId
|
|
220268
|
+
}
|
|
220269
|
+
});
|
|
220270
|
+
continue;
|
|
220271
|
+
}
|
|
220272
|
+
await ports.removeDevice(r.hubDeviceId);
|
|
220273
|
+
retired++;
|
|
220274
|
+
ports.logger.info("Reolink hub device retired", {
|
|
220275
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220276
|
+
meta: { integrationId: r.integrationId }
|
|
220277
|
+
});
|
|
220278
|
+
} catch (err) {
|
|
220279
|
+
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
220280
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220281
|
+
meta: { error: errMsg$3(err) }
|
|
220282
|
+
});
|
|
220283
|
+
}
|
|
220284
|
+
return retired;
|
|
220285
|
+
}
|
|
220286
|
+
//#endregion
|
|
219436
220287
|
//#region src/migrations/drop-webhook-intake-settings.ts
|
|
219437
220288
|
/**
|
|
219438
220289
|
* The camera webhook is a plane of the addon's HTTP data plane, not a private listener
|
|
@@ -219557,6 +220408,18 @@ var REOLINK_MIGRATIONS = [{
|
|
|
219557
220408
|
},
|
|
219558
220409
|
plan: planDropWebhookIntakeSettings
|
|
219559
220410
|
}];
|
|
220411
|
+
/**
|
|
220412
|
+
* Every persisted camera that names no integration is claimed to the addon's default one. The
|
|
220413
|
+
* integration a hub migration created is never the default, so it is not eligible. The integration
|
|
220414
|
+
* settings themselves stay where they are until this addon's own wave moves them.
|
|
220415
|
+
*/
|
|
220416
|
+
var REOLINK_CLAIM_MIGRATION = claimDevicesMigration({
|
|
220417
|
+
addonId: "provider-reolink",
|
|
220418
|
+
date: "2026-11-04",
|
|
220419
|
+
seq: 3,
|
|
220420
|
+
dependsOn: [],
|
|
220421
|
+
eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
|
|
220422
|
+
});
|
|
219560
220423
|
//#endregion
|
|
219561
220424
|
//#region src/reolink-adoption-provider.ts
|
|
219562
220425
|
var DEVICES_FILTER = {
|
|
@@ -219722,113 +220585,6 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219722
220585
|
};
|
|
219723
220586
|
}
|
|
219724
220587
|
//#endregion
|
|
219725
|
-
//#region src/reolink-discovery-map.ts
|
|
219726
|
-
/**
|
|
219727
|
-
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
219728
|
-
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
219729
|
-
*/
|
|
219730
|
-
function slugifyReolinkHost(host) {
|
|
219731
|
-
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
219732
|
-
}
|
|
219733
|
-
/**
|
|
219734
|
-
* Decide whether a discovered host is a device this provider already owns.
|
|
219735
|
-
*
|
|
219736
|
-
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
219737
|
-
* wins even when the host differs, because the address is the thing that moves.
|
|
219738
|
-
*/
|
|
219739
|
-
function findOnboardedMatch(discovered, onboarded) {
|
|
219740
|
-
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
219741
|
-
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
219742
|
-
if (byUid !== void 0) return byUid;
|
|
219743
|
-
}
|
|
219744
|
-
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
219745
|
-
}
|
|
219746
|
-
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
219747
|
-
var HUB_BAICHUAN_PORT = 9e3;
|
|
219748
|
-
/**
|
|
219749
|
-
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
219750
|
-
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
219751
|
-
* as a camera to add.
|
|
219752
|
-
*/
|
|
219753
|
-
function looksLikeReolinkHub(model) {
|
|
219754
|
-
if (model === void 0) return false;
|
|
219755
|
-
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
219756
|
-
}
|
|
219757
|
-
/**
|
|
219758
|
-
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
219759
|
-
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
219760
|
-
* creates hides this candidate through `originStableId`.
|
|
219761
|
-
*/
|
|
219762
|
-
function reolinkHubStableId(identity) {
|
|
219763
|
-
const uid = identity.uid?.trim() ?? "";
|
|
219764
|
-
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
219765
|
-
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
219766
|
-
}
|
|
219767
|
-
function isKnownHub(d, known) {
|
|
219768
|
-
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
219769
|
-
}
|
|
219770
|
-
/**
|
|
219771
|
-
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
219772
|
-
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
219773
|
-
* first responder for a host wins. Pure: no I/O.
|
|
219774
|
-
*
|
|
219775
|
-
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
219776
|
-
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
219777
|
-
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
219778
|
-
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
219779
|
-
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
219780
|
-
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
219781
|
-
* never a scan result.
|
|
219782
|
-
*
|
|
219783
|
-
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
219784
|
-
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
219785
|
-
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
219786
|
-
* in the first place, so a scan says so instead of offering it again.
|
|
219787
|
-
*/
|
|
219788
|
-
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
219789
|
-
const username = credentials.username?.trim() ?? "";
|
|
219790
|
-
const password = credentials.password ?? "";
|
|
219791
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
219792
|
-
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
219793
|
-
return [...byHost.values()].flatMap((d) => {
|
|
219794
|
-
const displayName = d.name ?? d.model ?? d.host;
|
|
219795
|
-
if (looksLikeReolinkHub(d.model)) {
|
|
219796
|
-
if (isKnownHub(d, hubIntegrations)) return [];
|
|
219797
|
-
return [{
|
|
219798
|
-
stableId: reolinkHubStableId(d),
|
|
219799
|
-
type: DeviceType.Gateway,
|
|
219800
|
-
suggestedName: displayName,
|
|
219801
|
-
prefilledConfig: {
|
|
219802
|
-
host: d.host,
|
|
219803
|
-
port: HUB_BAICHUAN_PORT,
|
|
219804
|
-
...d.uid ? { uid: d.uid } : {},
|
|
219805
|
-
...username ? { username } : {}
|
|
219806
|
-
},
|
|
219807
|
-
setupIntegration: true
|
|
219808
|
-
}];
|
|
219809
|
-
}
|
|
219810
|
-
const match = findOnboardedMatch(d, onboarded);
|
|
219811
|
-
return [{
|
|
219812
|
-
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
219813
|
-
type: DeviceType.Camera,
|
|
219814
|
-
suggestedName: displayName,
|
|
219815
|
-
prefilledConfig: {
|
|
219816
|
-
name: displayName,
|
|
219817
|
-
host: d.host,
|
|
219818
|
-
transport: "auto",
|
|
219819
|
-
...d.uid ? { uid: d.uid } : {},
|
|
219820
|
-
...username ? { username } : {},
|
|
219821
|
-
...password ? { password } : {}
|
|
219822
|
-
},
|
|
219823
|
-
...match !== void 0 ? {
|
|
219824
|
-
alreadyOnboarded: true,
|
|
219825
|
-
onboardedDeviceId: match.deviceId,
|
|
219826
|
-
onboardedName: match.name
|
|
219827
|
-
} : {}
|
|
219828
|
-
}];
|
|
219829
|
-
});
|
|
219830
|
-
}
|
|
219831
|
-
//#endregion
|
|
219832
220588
|
//#region src/reolink-gateway-device.ts
|
|
219833
220589
|
var reolinkGatewayDeviceSchema = object({
|
|
219834
220590
|
integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
|
|
@@ -219985,158 +220741,6 @@ function createReolinkHubManager(deps, registry) {
|
|
|
219985
220741
|
connect: connectReolinkHub
|
|
219986
220742
|
});
|
|
219987
220743
|
}
|
|
219988
|
-
function positivePort(value) {
|
|
219989
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
219990
|
-
}
|
|
219991
|
-
var CONNECTION_FIELDS = [
|
|
219992
|
-
"host",
|
|
219993
|
-
"port",
|
|
219994
|
-
"username",
|
|
219995
|
-
"transport"
|
|
219996
|
-
];
|
|
219997
|
-
function str(value) {
|
|
219998
|
-
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
219999
|
-
}
|
|
220000
|
-
function migratedChildConfig(child, hub, integrationId) {
|
|
220001
|
-
const filled = {};
|
|
220002
|
-
for (const key of CONNECTION_FIELDS) {
|
|
220003
|
-
const own = child[key];
|
|
220004
|
-
const inherited = hub[key];
|
|
220005
|
-
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
220006
|
-
}
|
|
220007
|
-
return {
|
|
220008
|
-
...child,
|
|
220009
|
-
...filled,
|
|
220010
|
-
integrationId
|
|
220011
|
-
};
|
|
220012
|
-
}
|
|
220013
|
-
async function migrateOneHub(hub, children, integrations, ports) {
|
|
220014
|
-
const read = readHubConnection(hub.config);
|
|
220015
|
-
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
220016
|
-
const { connection } = read;
|
|
220017
|
-
const uid = str(hub.config["uid"]);
|
|
220018
|
-
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
220019
|
-
addonId: ports.addonId,
|
|
220020
|
-
name: hub.name,
|
|
220021
|
-
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
220022
|
-
originStableId: reolinkHubStableId({
|
|
220023
|
-
...uid !== void 0 ? { uid } : {},
|
|
220024
|
-
host: connection.host
|
|
220025
|
-
}),
|
|
220026
|
-
settings: {
|
|
220027
|
-
host: connection.host,
|
|
220028
|
-
port: connection.port,
|
|
220029
|
-
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
220030
|
-
username: connection.username,
|
|
220031
|
-
password: connection.password,
|
|
220032
|
-
transport: connection.transport,
|
|
220033
|
-
...uid !== void 0 ? { uid } : {},
|
|
220034
|
-
debugGeneral: connection.debugGeneral,
|
|
220035
|
-
debugSocketLogs: connection.debugSocketLogs
|
|
220036
|
-
},
|
|
220037
|
-
secretKeys: ["password"]
|
|
220038
|
-
})).id;
|
|
220039
|
-
const rewritten = [];
|
|
220040
|
-
for (const child of children) {
|
|
220041
|
-
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
220042
|
-
await ports.persistConfig(child.stableId, config);
|
|
220043
|
-
await ports.setIntegrationId(child.id, integrationId);
|
|
220044
|
-
rewritten.push({
|
|
220045
|
-
...child,
|
|
220046
|
-
config,
|
|
220047
|
-
parentDeviceId: null
|
|
220048
|
-
});
|
|
220049
|
-
}
|
|
220050
|
-
return {
|
|
220051
|
-
rows: rewritten,
|
|
220052
|
-
retirement: {
|
|
220053
|
-
hubDeviceId: hub.id,
|
|
220054
|
-
hubName: hub.name,
|
|
220055
|
-
integrationId
|
|
220056
|
-
}
|
|
220057
|
-
};
|
|
220058
|
-
}
|
|
220059
|
-
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
220060
|
-
async function planHubMigration(saved, ports) {
|
|
220061
|
-
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
220062
|
-
if (hubs.length === 0) return {
|
|
220063
|
-
rows: saved,
|
|
220064
|
-
retirements: []
|
|
220065
|
-
};
|
|
220066
|
-
let integrations;
|
|
220067
|
-
try {
|
|
220068
|
-
integrations = await ports.listIntegrations();
|
|
220069
|
-
} catch (err) {
|
|
220070
|
-
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
220071
|
-
return {
|
|
220072
|
-
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
220073
|
-
retirements: []
|
|
220074
|
-
};
|
|
220075
|
-
}
|
|
220076
|
-
const hubIds = new Set(hubs.map((h) => h.id));
|
|
220077
|
-
const rewrittenById = /* @__PURE__ */ new Map();
|
|
220078
|
-
const retirements = [];
|
|
220079
|
-
for (const hub of hubs) {
|
|
220080
|
-
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
220081
|
-
try {
|
|
220082
|
-
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
220083
|
-
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
220084
|
-
retirements.push(done.retirement);
|
|
220085
|
-
ports.logger.info("Reolink hub migrated to an integration", {
|
|
220086
|
-
tags: { deviceId: hub.id },
|
|
220087
|
-
meta: {
|
|
220088
|
-
integrationId: done.retirement.integrationId,
|
|
220089
|
-
children: children.length,
|
|
220090
|
-
name: hub.name
|
|
220091
|
-
}
|
|
220092
|
-
});
|
|
220093
|
-
} catch (err) {
|
|
220094
|
-
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
220095
|
-
tags: { deviceId: hub.id },
|
|
220096
|
-
meta: {
|
|
220097
|
-
error: errMsg$3(err),
|
|
220098
|
-
name: hub.name
|
|
220099
|
-
}
|
|
220100
|
-
});
|
|
220101
|
-
}
|
|
220102
|
-
}
|
|
220103
|
-
return {
|
|
220104
|
-
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
220105
|
-
retirements
|
|
220106
|
-
};
|
|
220107
|
-
}
|
|
220108
|
-
/**
|
|
220109
|
-
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
220110
|
-
* and named: removing it would cascade the camera away with it.
|
|
220111
|
-
*/
|
|
220112
|
-
async function retireMigratedHubs(retirements, ports) {
|
|
220113
|
-
let retired = 0;
|
|
220114
|
-
for (const r of retirements) try {
|
|
220115
|
-
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
220116
|
-
if (remaining.length > 0) {
|
|
220117
|
-
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
220118
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220119
|
-
meta: {
|
|
220120
|
-
remaining: [...remaining],
|
|
220121
|
-
integrationId: r.integrationId
|
|
220122
|
-
}
|
|
220123
|
-
});
|
|
220124
|
-
continue;
|
|
220125
|
-
}
|
|
220126
|
-
await ports.removeDevice(r.hubDeviceId);
|
|
220127
|
-
retired++;
|
|
220128
|
-
ports.logger.info("Reolink hub device retired", {
|
|
220129
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220130
|
-
meta: { integrationId: r.integrationId }
|
|
220131
|
-
});
|
|
220132
|
-
} catch (err) {
|
|
220133
|
-
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
220134
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220135
|
-
meta: { error: errMsg$3(err) }
|
|
220136
|
-
});
|
|
220137
|
-
}
|
|
220138
|
-
return retired;
|
|
220139
|
-
}
|
|
220140
220744
|
//#endregion
|
|
220141
220745
|
//#region src/reolink-subnet-scan.ts
|
|
220142
220746
|
/**
|
|
@@ -220955,7 +221559,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220955
221559
|
this.wireClipCatalogRefreshers();
|
|
220956
221560
|
await this.serveClipPlanes(this.clipService);
|
|
220957
221561
|
await this.serveWebhookPlane();
|
|
220958
|
-
await this.runMigrations(defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts())
|
|
221562
|
+
await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations([REOLINK_CLAIM_MIGRATION], claimDevicesPortsOver({
|
|
221563
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
221564
|
+
info: (message) => this.ctx.logger.info(message),
|
|
221565
|
+
warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
|
|
221566
|
+
}))]);
|
|
220959
221567
|
this.wireEmailBridge();
|
|
220960
221568
|
const wireSleepPushResolvers = (event, via) => {
|
|
220961
221569
|
const data = event.data;
|