@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.js
CHANGED
|
@@ -101481,7 +101481,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
101481
101481
|
return EventCategory;
|
|
101482
101482
|
}({});
|
|
101483
101483
|
//#endregion
|
|
101484
|
-
//#region ../types/dist/device-type-
|
|
101484
|
+
//#region ../types/dist/device-type-D1fH5kaC.mjs
|
|
101485
101485
|
/**
|
|
101486
101486
|
import { errMsg } from '@camstack/types'
|
|
101487
101487
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -101712,6 +101712,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
101712
101712
|
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
101713
101713
|
/** Media player exposes stop distinct from pause. */
|
|
101714
101714
|
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
101715
|
+
/** Media player exposes volume up / down in steps (an infrared TV has no level). */
|
|
101716
|
+
DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
|
|
101717
|
+
/** Media player exposes channel up / down in steps. */
|
|
101718
|
+
DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
|
|
101719
|
+
/** Media player exposes power on / off. */
|
|
101720
|
+
DeviceFeature["MediaPlayerPower"] = "media-player-power";
|
|
101715
101721
|
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
101716
101722
|
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
101717
101723
|
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
@@ -106538,7 +106544,7 @@ var ZodIssueCode = {
|
|
|
106538
106544
|
var ZodFirstPartyTypeKind;
|
|
106539
106545
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106540
106546
|
//#endregion
|
|
106541
|
-
//#region ../types/dist/sleep-
|
|
106547
|
+
//#region ../types/dist/sleep-Cv4Zmie9.mjs
|
|
106542
106548
|
/**
|
|
106543
106549
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106544
106550
|
* window to float samples (D455).
|
|
@@ -106617,6 +106623,22 @@ function buildAlawTable() {
|
|
|
106617
106623
|
}
|
|
106618
106624
|
buildUlawTable();
|
|
106619
106625
|
buildAlawTable();
|
|
106626
|
+
/**
|
|
106627
|
+
* An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
|
|
106628
|
+
* so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
|
|
106629
|
+
* co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
|
|
106630
|
+
* bundles its own copy of this module, so the class the runner sees is not the one that threw.
|
|
106631
|
+
*/
|
|
106632
|
+
var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
|
|
106633
|
+
var AddonMigrationsBlockedError = class extends Error {
|
|
106634
|
+
/** The ledger ids that did not apply and block the owner. */
|
|
106635
|
+
migrationIds;
|
|
106636
|
+
constructor(migrationIds) {
|
|
106637
|
+
super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
|
|
106638
|
+
this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
|
|
106639
|
+
this.migrationIds = migrationIds;
|
|
106640
|
+
}
|
|
106641
|
+
};
|
|
106620
106642
|
/** Is this field's stored value a credential? */
|
|
106621
106643
|
function isSecretConfigField(field) {
|
|
106622
106644
|
if (field.type === "password") return true;
|
|
@@ -107119,7 +107141,7 @@ var NOT_RUN = {
|
|
|
107119
107141
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
107120
107142
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
107121
107143
|
*/
|
|
107122
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
107144
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
107123
107145
|
const first = migrations[0];
|
|
107124
107146
|
if (first === void 0) return NOT_RUN;
|
|
107125
107147
|
const owner = first.meta.owner;
|
|
@@ -107148,7 +107170,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107148
107170
|
else if (status === "blocked") blocked.push(id);
|
|
107149
107171
|
else skipped.push(id);
|
|
107150
107172
|
};
|
|
107173
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
107151
107174
|
for (const step of declaration.steps) {
|
|
107175
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
107152
107176
|
const bound = byId.get(step.id);
|
|
107153
107177
|
if (bound === void 0 || step.action === "skip") {
|
|
107154
107178
|
skipped.push(step.id);
|
|
@@ -108266,15 +108290,19 @@ var BaseAddon = class {
|
|
|
108266
108290
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
108267
108291
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
108268
108292
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
108293
|
+
*
|
|
108294
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
108295
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
108296
|
+
* omitted applied ids "newer").
|
|
108269
108297
|
*/
|
|
108270
|
-
async runMigrations(migrations) {
|
|
108298
|
+
async runMigrations(migrations, options) {
|
|
108271
108299
|
const api = this.ctx.api.migrations;
|
|
108272
108300
|
const report = await runAddonMigrations({
|
|
108273
108301
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
108274
108302
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
108275
108303
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
108276
|
-
}, migrations, this.ctx.logger);
|
|
108277
|
-
if (report.blockOwner.length > 0) throw new
|
|
108304
|
+
}, migrations, this.ctx.logger, options);
|
|
108305
|
+
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
108278
108306
|
return report;
|
|
108279
108307
|
}
|
|
108280
108308
|
/**
|
|
@@ -108614,6 +108642,10 @@ var BaseAddon = class {
|
|
|
108614
108642
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
108615
108643
|
const patch = {};
|
|
108616
108644
|
for (const key of keys) {
|
|
108645
|
+
if (tree.get(key)?.kind === "array") {
|
|
108646
|
+
patch[key] = [];
|
|
108647
|
+
continue;
|
|
108648
|
+
}
|
|
108617
108649
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
108618
108650
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
108619
108651
|
}
|
|
@@ -109340,7 +109372,8 @@ function isZodSecret(schema) {
|
|
|
109340
109372
|
}
|
|
109341
109373
|
/**
|
|
109342
109374
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
109343
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
109375
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
109376
|
+
* map" (`applianceSecrets{}`). A declared
|
|
109344
109377
|
* object or array is reported as one path and not descended into — everything
|
|
109345
109378
|
* under it is secret.
|
|
109346
109379
|
*/
|
|
@@ -109351,6 +109384,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
109351
109384
|
const def = defOf(layer);
|
|
109352
109385
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
109353
109386
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
109387
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
109354
109388
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
109355
109389
|
else if (def.type === "intersection") {
|
|
109356
109390
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -112229,7 +112263,7 @@ function sleep(ms) {
|
|
|
112229
112263
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
112230
112264
|
}
|
|
112231
112265
|
//#endregion
|
|
112232
|
-
//#region ../types/dist/composition-
|
|
112266
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
112233
112267
|
/**
|
|
112234
112268
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
112235
112269
|
*
|
|
@@ -112992,38 +113026,6 @@ function validateExpressionSource(src) {
|
|
|
112992
113026
|
return null;
|
|
112993
113027
|
}
|
|
112994
113028
|
/**
|
|
112995
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
112996
|
-
*
|
|
112997
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
112998
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
112999
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
113000
|
-
*
|
|
113001
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
113002
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
113003
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
113004
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
113005
|
-
* can move later without moving the others.
|
|
113006
|
-
*/
|
|
113007
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
113008
|
-
var APPLIANCE_KINDS = [
|
|
113009
|
-
"washer",
|
|
113010
|
-
"dryer",
|
|
113011
|
-
"dishwasher"
|
|
113012
|
-
];
|
|
113013
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
113014
|
-
/**
|
|
113015
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
113016
|
-
*
|
|
113017
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
113018
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
113019
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
113020
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
113021
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
113022
|
-
* the smallest radiator.
|
|
113023
|
-
*/
|
|
113024
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
113025
|
-
DeviceType.Heater;
|
|
113026
|
-
/**
|
|
113027
113029
|
* Statistics and integrals over a history read (D944).
|
|
113028
113030
|
*
|
|
113029
113031
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -113167,34 +113169,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
113167
113169
|
/** Actuator groups only. Readings leave this unset. */
|
|
113168
113170
|
active: ActiveCriterionSchema.optional()
|
|
113169
113171
|
}),
|
|
113170
|
-
object({
|
|
113171
|
-
kind: literal("appliance"),
|
|
113172
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
113173
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
113174
|
-
thresholdWatts: number().positive(),
|
|
113175
|
-
startDwellMs: number().int().nonnegative(),
|
|
113176
|
-
stopDwellMs: number().int().nonnegative(),
|
|
113177
|
-
member: HelperMemberSchema,
|
|
113178
|
-
/**
|
|
113179
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
113180
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
113181
|
-
* that plug (power-source link of a NEW-target composition).
|
|
113182
|
-
*/
|
|
113183
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
113184
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
113185
|
-
offSeconds: number().optional()
|
|
113186
|
-
}),
|
|
113187
|
-
object({
|
|
113188
|
-
kind: literal("heater"),
|
|
113189
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
113190
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
113191
|
-
plug: HelperMemberSchema,
|
|
113192
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
113193
|
-
meter: HelperMemberSchema.optional(),
|
|
113194
|
-
thresholdWatts: number().positive(),
|
|
113195
|
-
startDwellMs: number().int().nonnegative(),
|
|
113196
|
-
stopDwellMs: number().int().nonnegative()
|
|
113197
|
-
}),
|
|
113198
113172
|
object({
|
|
113199
113173
|
kind: literal("statistic"),
|
|
113200
113174
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -113366,7 +113340,14 @@ var CompositionForwardCommandSchema = object({
|
|
|
113366
113340
|
method: string().min(1),
|
|
113367
113341
|
args: record(string().min(1), CompositionForwardArgSchema).optional()
|
|
113368
113342
|
});
|
|
113369
|
-
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
113343
|
+
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
113344
|
+
CompositionForwardCommandSchema,
|
|
113345
|
+
CompositionCodeSourceSchema,
|
|
113346
|
+
object({
|
|
113347
|
+
kind: literal("extension"),
|
|
113348
|
+
target: literal("remote")
|
|
113349
|
+
})
|
|
113350
|
+
]);
|
|
113370
113351
|
/** An item key inside an item-array cap (`consumables.items[<key>]`). */
|
|
113371
113352
|
var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
113372
113353
|
/**
|
|
@@ -113411,7 +113392,7 @@ var CompositionSchema = object({
|
|
|
113411
113392
|
kind: literal("existing"),
|
|
113412
113393
|
device: CompositionSourceRefSchema
|
|
113413
113394
|
})]),
|
|
113414
|
-
features: array(CompositionFeatureSchema).min(
|
|
113395
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
113415
113396
|
/**
|
|
113416
113397
|
* Present when the block is a helper template. Absent on a free
|
|
113417
113398
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -114715,7 +114696,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
114715
114696
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
114716
114697
|
* catalogue lists the id (a newer database).
|
|
114717
114698
|
*/
|
|
114718
|
-
meta: MigrationMetaSchema.optional()
|
|
114699
|
+
meta: MigrationMetaSchema.optional(),
|
|
114700
|
+
/**
|
|
114701
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
114702
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
114703
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
114704
|
+
*/
|
|
114705
|
+
ownerNotLoaded: boolean().optional()
|
|
114719
114706
|
});
|
|
114720
114707
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
114721
114708
|
var MigrationPendingEntrySchema = object({
|
|
@@ -114748,6 +114735,146 @@ var MigrationOwnerStepSchema = object({
|
|
|
114748
114735
|
/** The ledger state the engine found: `pending` has no row yet. */
|
|
114749
114736
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
114750
114737
|
});
|
|
114738
|
+
function ordinalOf(id) {
|
|
114739
|
+
const digits = id.replace(/\D+/g, "");
|
|
114740
|
+
return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
|
|
114741
|
+
}
|
|
114742
|
+
/** The lowest-numbered eligible integration, or null. */
|
|
114743
|
+
function defaultIntegrationOf(integrations, eligible) {
|
|
114744
|
+
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;
|
|
114745
|
+
}
|
|
114746
|
+
function unstamped(device) {
|
|
114747
|
+
return device.integrationId === void 0 || device.integrationId === null || device.integrationId === "";
|
|
114748
|
+
}
|
|
114749
|
+
/**
|
|
114750
|
+
* An allocation reservation is not a device (D376): `allocateDeviceId` writes a row of type
|
|
114751
|
+
* `generic`, named by its stable id, that is not registered and holds no configuration. The hub
|
|
114752
|
+
* listing does not carry the `registered` bit, so the shape picks the candidates and the missing
|
|
114753
|
+
* configuration decides.
|
|
114754
|
+
*/
|
|
114755
|
+
async function isReservation(ports, device) {
|
|
114756
|
+
if (device.type !== "generic" || device.name === void 0 || device.name !== device.stableId) return false;
|
|
114757
|
+
return !await ports.hasConfig(device.id);
|
|
114758
|
+
}
|
|
114759
|
+
async function claimDevicesToDefaultIntegration(ports, spec) {
|
|
114760
|
+
const all = await ports.listDevices(spec.addonId);
|
|
114761
|
+
const byId = new Map(all.map((d) => [d.id, d]));
|
|
114762
|
+
const reservations = /* @__PURE__ */ new Set();
|
|
114763
|
+
for (const device of all) if (unstamped(device) && await isReservation(ports, device)) reservations.add(device.id);
|
|
114764
|
+
const orphans = all.filter((d) => unstamped(d) && !reservations.has(d.id));
|
|
114765
|
+
const summaryBase = { reservations: reservations.size };
|
|
114766
|
+
if (orphans.length === 0) return {
|
|
114767
|
+
outcome: "already-done",
|
|
114768
|
+
summary: {
|
|
114769
|
+
claimed: 0,
|
|
114770
|
+
unresolved: 0,
|
|
114771
|
+
...summaryBase
|
|
114772
|
+
}
|
|
114773
|
+
};
|
|
114774
|
+
const fallback = defaultIntegrationOf(await ports.listIntegrations(spec.addonId), spec.eligible);
|
|
114775
|
+
const plan = /* @__PURE__ */ new Map();
|
|
114776
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
114777
|
+
const failed = /* @__PURE__ */ new Set();
|
|
114778
|
+
const resolve = (device, seen) => {
|
|
114779
|
+
if (!unstamped(device)) return device.integrationId ?? null;
|
|
114780
|
+
const planned = plan.get(device.id);
|
|
114781
|
+
if (planned !== void 0) return planned;
|
|
114782
|
+
if (failed.has(device.id)) return null;
|
|
114783
|
+
let target = null;
|
|
114784
|
+
const parentId = device.parentDeviceId ?? null;
|
|
114785
|
+
if (parentId === null) target = fallback?.id ?? null;
|
|
114786
|
+
else {
|
|
114787
|
+
const parent = byId.get(parentId);
|
|
114788
|
+
if (parent !== void 0 && !reservations.has(parentId) && !seen.has(parentId)) target = resolve(parent, new Set([...seen, device.id]));
|
|
114789
|
+
}
|
|
114790
|
+
if (target === null) failed.add(device.id);
|
|
114791
|
+
else plan.set(device.id, target);
|
|
114792
|
+
return target;
|
|
114793
|
+
};
|
|
114794
|
+
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`);
|
|
114795
|
+
for (const device of orphans) if (resolve(device, new Set([device.id])) === null) unresolved.add(device.id);
|
|
114796
|
+
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);
|
|
114797
|
+
for (const [deviceId, integrationId] of plan) await ports.setIntegrationId(deviceId, integrationId);
|
|
114798
|
+
const wrong = [];
|
|
114799
|
+
for (const [deviceId, integrationId] of plan) if (await ports.readIntegrationId(deviceId) !== integrationId) wrong.push(deviceId);
|
|
114800
|
+
if (wrong.length > 0) throw new Error(`device(s) ${wrong.join(", ")} did not read back their integration after the claim`);
|
|
114801
|
+
const claimed = plan.size;
|
|
114802
|
+
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`);
|
|
114803
|
+
return {
|
|
114804
|
+
outcome: claimed > 0 ? "done" : "already-done",
|
|
114805
|
+
summary: {
|
|
114806
|
+
claimed,
|
|
114807
|
+
unresolved: unresolved.size,
|
|
114808
|
+
...summaryBase
|
|
114809
|
+
}
|
|
114810
|
+
};
|
|
114811
|
+
}
|
|
114812
|
+
/** The hub side of the device claim, over `ctx.api`. */
|
|
114813
|
+
function claimDevicesPortsOver(deps) {
|
|
114814
|
+
return {
|
|
114815
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
114816
|
+
id: i.id,
|
|
114817
|
+
name: i.name,
|
|
114818
|
+
info: i.info ?? {}
|
|
114819
|
+
})),
|
|
114820
|
+
listDevices: (addonId) => deps.api.listDevices(addonId),
|
|
114821
|
+
hasConfig: (deviceId) => deps.api.readDeviceHasConfig(deviceId),
|
|
114822
|
+
setIntegrationId: (deviceId, integrationId) => deps.api.setDeviceIntegrationId(deviceId, integrationId),
|
|
114823
|
+
readIntegrationId: (deviceId) => deps.api.readDeviceIntegrationId(deviceId),
|
|
114824
|
+
info: deps.info,
|
|
114825
|
+
warnDevice: deps.warnDevice
|
|
114826
|
+
};
|
|
114827
|
+
}
|
|
114828
|
+
function defaultIntegrationMigrationIds(addonId, date) {
|
|
114829
|
+
return {
|
|
114830
|
+
rowsToRecord: `${addonId}/${date}-integration-rows-to-record`,
|
|
114831
|
+
claimDevices: `${addonId}/${date}-claim-devices-to-default-integration`
|
|
114832
|
+
};
|
|
114833
|
+
}
|
|
114834
|
+
/**
|
|
114835
|
+
* The device claim alone, for an addon that keeps its integration settings where they are for now
|
|
114836
|
+
* (its own wave moves them) but whose persisted devices must name their default integration.
|
|
114837
|
+
*/
|
|
114838
|
+
function claimDevicesMigration(spec) {
|
|
114839
|
+
return {
|
|
114840
|
+
meta: {
|
|
114841
|
+
id: defaultIntegrationMigrationIds(spec.addonId, spec.date).claimDevices,
|
|
114842
|
+
owner: spec.addonId,
|
|
114843
|
+
level: "addon-state",
|
|
114844
|
+
seq: spec.seq,
|
|
114845
|
+
dependsOn: [...spec.dependsOn],
|
|
114846
|
+
store: "settings",
|
|
114847
|
+
scope: "cluster",
|
|
114848
|
+
onFailure: "tolerate",
|
|
114849
|
+
bodyKind: "procedure",
|
|
114850
|
+
description: "Claim the persisted devices that carry no integration to the addon's default integration, keeping their device and stable ids",
|
|
114851
|
+
fixtureMark: "a device config field the claim does not know"
|
|
114852
|
+
},
|
|
114853
|
+
procedure: (ports) => claimDevicesToDefaultIntegration(ports, {
|
|
114854
|
+
addonId: spec.addonId,
|
|
114855
|
+
...spec.eligible !== void 0 ? { eligible: spec.eligible } : {}
|
|
114856
|
+
})
|
|
114857
|
+
};
|
|
114858
|
+
}
|
|
114859
|
+
function defaultIntegrationApiOver(api) {
|
|
114860
|
+
return {
|
|
114861
|
+
list: () => api.integrations.list.query(),
|
|
114862
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
114863
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
114864
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
114865
|
+
addonId,
|
|
114866
|
+
projection: "slim"
|
|
114867
|
+
}),
|
|
114868
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
114869
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
114870
|
+
deviceId,
|
|
114871
|
+
integrationId
|
|
114872
|
+
});
|
|
114873
|
+
},
|
|
114874
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
114875
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
114876
|
+
};
|
|
114877
|
+
}
|
|
114751
114878
|
/**
|
|
114752
114879
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
114753
114880
|
* gets back (cloud training, D805+).
|
|
@@ -120476,6 +120603,48 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
120476
120603
|
scope: "device",
|
|
120477
120604
|
noun: t("Media player", "Lettore multimediale"),
|
|
120478
120605
|
actions: [
|
|
120606
|
+
{
|
|
120607
|
+
id: "media.turnOn",
|
|
120608
|
+
method: "turnOn",
|
|
120609
|
+
label: t("Turn on", "Accendi"),
|
|
120610
|
+
phrase: t("Turn on {device}", "Accendi {device}"),
|
|
120611
|
+
icon: "power"
|
|
120612
|
+
},
|
|
120613
|
+
{
|
|
120614
|
+
id: "media.turnOff",
|
|
120615
|
+
method: "turnOff",
|
|
120616
|
+
label: t("Turn off", "Spegni"),
|
|
120617
|
+
phrase: t("Turn off {device}", "Spegni {device}"),
|
|
120618
|
+
icon: "power"
|
|
120619
|
+
},
|
|
120620
|
+
{
|
|
120621
|
+
id: "media.volumeUp",
|
|
120622
|
+
method: "volumeUp",
|
|
120623
|
+
label: t("Volume up", "Alza il volume"),
|
|
120624
|
+
phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
|
|
120625
|
+
icon: "volume-2"
|
|
120626
|
+
},
|
|
120627
|
+
{
|
|
120628
|
+
id: "media.volumeDown",
|
|
120629
|
+
method: "volumeDown",
|
|
120630
|
+
label: t("Volume down", "Abbassa il volume"),
|
|
120631
|
+
phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
|
|
120632
|
+
icon: "volume-2"
|
|
120633
|
+
},
|
|
120634
|
+
{
|
|
120635
|
+
id: "media.channelUp",
|
|
120636
|
+
method: "channelUp",
|
|
120637
|
+
label: t("Channel up", "Canale successivo"),
|
|
120638
|
+
phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
|
|
120639
|
+
icon: "skip-forward"
|
|
120640
|
+
},
|
|
120641
|
+
{
|
|
120642
|
+
id: "media.channelDown",
|
|
120643
|
+
method: "channelDown",
|
|
120644
|
+
label: t("Channel down", "Canale precedente"),
|
|
120645
|
+
phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
|
|
120646
|
+
icon: "skip-back"
|
|
120647
|
+
},
|
|
120479
120648
|
{
|
|
120480
120649
|
id: "media.play",
|
|
120481
120650
|
method: "play",
|
|
@@ -149996,6 +150165,244 @@ var controlCapability = {
|
|
|
149996
150165
|
*/
|
|
149997
150166
|
history: "none"
|
|
149998
150167
|
};
|
|
150168
|
+
/**
|
|
150169
|
+
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
150170
|
+
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
150171
|
+
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
150172
|
+
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
150173
|
+
*
|
|
150174
|
+
* The cap declares a wide method surface; not every backend supports
|
|
150175
|
+
* every method. Drivers signal sub-capability via the
|
|
150176
|
+
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
150177
|
+
* corresponding method) so the UI gates buttons without a slice
|
|
150178
|
+
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
150179
|
+
*/
|
|
150180
|
+
var MediaPlayerStateSchema = _enum([
|
|
150181
|
+
"off",
|
|
150182
|
+
"on",
|
|
150183
|
+
"idle",
|
|
150184
|
+
"playing",
|
|
150185
|
+
"paused",
|
|
150186
|
+
"buffering",
|
|
150187
|
+
"standby"
|
|
150188
|
+
]);
|
|
150189
|
+
var MediaPlayerRepeatSchema = _enum([
|
|
150190
|
+
"off",
|
|
150191
|
+
"all",
|
|
150192
|
+
"one"
|
|
150193
|
+
]);
|
|
150194
|
+
var MediaInfoSchema = object({
|
|
150195
|
+
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
150196
|
+
* `podcast`, …). */
|
|
150197
|
+
type: string(),
|
|
150198
|
+
/** Human-readable title. */
|
|
150199
|
+
title: string(),
|
|
150200
|
+
/** Optional artist / channel / station label. */
|
|
150201
|
+
artist: string().optional(),
|
|
150202
|
+
/** Optional album / season / show name. */
|
|
150203
|
+
album: string().optional(),
|
|
150204
|
+
/** Optional cover-art / thumbnail URL. */
|
|
150205
|
+
imageUrl: string().optional()
|
|
150206
|
+
});
|
|
150207
|
+
/**
|
|
150208
|
+
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
150209
|
+
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
150210
|
+
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
150211
|
+
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
150212
|
+
* guess, never as a reading.
|
|
150213
|
+
*/
|
|
150214
|
+
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
150215
|
+
/** The fields of a media-player status a driver can only guess. */
|
|
150216
|
+
var MediaPlayerAssumedFieldSchema = _enum([
|
|
150217
|
+
"state",
|
|
150218
|
+
"volumeLevel",
|
|
150219
|
+
"isMuted",
|
|
150220
|
+
"source"
|
|
150221
|
+
]);
|
|
150222
|
+
var MediaPlayerStatusSchema = object({
|
|
150223
|
+
/** Playback lifecycle state. */
|
|
150224
|
+
state: MediaPlayerStateSchema,
|
|
150225
|
+
/**
|
|
150226
|
+
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
150227
|
+
* A slice written before this field existed parses as `reported`.
|
|
150228
|
+
*/
|
|
150229
|
+
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
150230
|
+
/**
|
|
150231
|
+
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
150232
|
+
* this is not empty; a field not listed here is a reading even then (power
|
|
150233
|
+
* from a running-state source, mute guessed from the last send).
|
|
150234
|
+
*/
|
|
150235
|
+
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
150236
|
+
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
150237
|
+
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
150238
|
+
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
150239
|
+
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
150240
|
+
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
150241
|
+
isMuted: boolean().nullable(),
|
|
150242
|
+
/** Active source / input. Empty when no source surface. */
|
|
150243
|
+
source: string(),
|
|
150244
|
+
/** Selectable sources. Empty when no source surface.
|
|
150245
|
+
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
150246
|
+
availableSources: array(string()),
|
|
150247
|
+
/** Currently-playing media info. Null when nothing is playing. */
|
|
150248
|
+
currentMedia: MediaInfoSchema.nullable(),
|
|
150249
|
+
/** Current playback position in ms. Null when not seekable or
|
|
150250
|
+
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
150251
|
+
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
150252
|
+
/** Total duration of the current media in ms. Null when unknown
|
|
150253
|
+
* (live stream) or nothing is playing. */
|
|
150254
|
+
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
150255
|
+
/** Shuffle toggle. Null when no shuffle surface.
|
|
150256
|
+
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
150257
|
+
shuffle: boolean().nullable(),
|
|
150258
|
+
/** Repeat mode. Null when no repeat surface.
|
|
150259
|
+
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
150260
|
+
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
150261
|
+
/** Ms epoch when the slice was last updated. */
|
|
150262
|
+
lastChangedAt: number()
|
|
150263
|
+
});
|
|
150264
|
+
var stepInput = object({
|
|
150265
|
+
deviceId: number().int().nonnegative(),
|
|
150266
|
+
/** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
|
|
150267
|
+
steps: number().int().min(1).max(50).default(1)
|
|
150268
|
+
});
|
|
150269
|
+
var deviceOnly = object({ deviceId: number().int().nonnegative() });
|
|
150270
|
+
var mediaPlayerCapability = {
|
|
150271
|
+
name: "media-player",
|
|
150272
|
+
scope: "device",
|
|
150273
|
+
deviceNative: true,
|
|
150274
|
+
mode: "singleton",
|
|
150275
|
+
deviceTypes: [DeviceType.MediaPlayer],
|
|
150276
|
+
methods: {
|
|
150277
|
+
turnOn: method(deviceOnly, _void(), {
|
|
150278
|
+
kind: "mutation",
|
|
150279
|
+
auth: "protected"
|
|
150280
|
+
}),
|
|
150281
|
+
turnOff: method(deviceOnly, _void(), {
|
|
150282
|
+
kind: "mutation",
|
|
150283
|
+
auth: "protected"
|
|
150284
|
+
}),
|
|
150285
|
+
volumeUp: method(stepInput, _void(), {
|
|
150286
|
+
kind: "mutation",
|
|
150287
|
+
auth: "protected"
|
|
150288
|
+
}),
|
|
150289
|
+
volumeDown: method(stepInput, _void(), {
|
|
150290
|
+
kind: "mutation",
|
|
150291
|
+
auth: "protected"
|
|
150292
|
+
}),
|
|
150293
|
+
channelUp: method(stepInput, _void(), {
|
|
150294
|
+
kind: "mutation",
|
|
150295
|
+
auth: "protected"
|
|
150296
|
+
}),
|
|
150297
|
+
channelDown: method(stepInput, _void(), {
|
|
150298
|
+
kind: "mutation",
|
|
150299
|
+
auth: "protected"
|
|
150300
|
+
}),
|
|
150301
|
+
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150302
|
+
kind: "mutation",
|
|
150303
|
+
auth: "protected"
|
|
150304
|
+
}),
|
|
150305
|
+
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150306
|
+
kind: "mutation",
|
|
150307
|
+
auth: "protected"
|
|
150308
|
+
}),
|
|
150309
|
+
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150310
|
+
kind: "mutation",
|
|
150311
|
+
auth: "protected"
|
|
150312
|
+
}),
|
|
150313
|
+
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150314
|
+
kind: "mutation",
|
|
150315
|
+
auth: "protected"
|
|
150316
|
+
}),
|
|
150317
|
+
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
150318
|
+
kind: "mutation",
|
|
150319
|
+
auth: "protected"
|
|
150320
|
+
}),
|
|
150321
|
+
seek: method(object({
|
|
150322
|
+
deviceId: number().int().nonnegative(),
|
|
150323
|
+
position: number().int().nonnegative()
|
|
150324
|
+
}), _void(), {
|
|
150325
|
+
kind: "mutation",
|
|
150326
|
+
auth: "protected"
|
|
150327
|
+
}),
|
|
150328
|
+
setVolume: method(object({
|
|
150329
|
+
deviceId: number().int().nonnegative(),
|
|
150330
|
+
volumeLevel: number().min(0).max(100)
|
|
150331
|
+
}), _void(), {
|
|
150332
|
+
kind: "mutation",
|
|
150333
|
+
auth: "protected"
|
|
150334
|
+
}),
|
|
150335
|
+
setMute: method(object({
|
|
150336
|
+
deviceId: number().int().nonnegative(),
|
|
150337
|
+
muted: boolean()
|
|
150338
|
+
}), _void(), {
|
|
150339
|
+
kind: "mutation",
|
|
150340
|
+
auth: "protected"
|
|
150341
|
+
}),
|
|
150342
|
+
setShuffle: method(object({
|
|
150343
|
+
deviceId: number().int().nonnegative(),
|
|
150344
|
+
shuffle: boolean()
|
|
150345
|
+
}), _void(), {
|
|
150346
|
+
kind: "mutation",
|
|
150347
|
+
auth: "protected"
|
|
150348
|
+
}),
|
|
150349
|
+
setRepeat: method(object({
|
|
150350
|
+
deviceId: number().int().nonnegative(),
|
|
150351
|
+
repeat: MediaPlayerRepeatSchema
|
|
150352
|
+
}), _void(), {
|
|
150353
|
+
kind: "mutation",
|
|
150354
|
+
auth: "protected"
|
|
150355
|
+
}),
|
|
150356
|
+
selectSource: method(object({
|
|
150357
|
+
deviceId: number().int().nonnegative(),
|
|
150358
|
+
source: string().min(1)
|
|
150359
|
+
}), _void(), {
|
|
150360
|
+
kind: "mutation",
|
|
150361
|
+
auth: "protected"
|
|
150362
|
+
}),
|
|
150363
|
+
playMedia: method(object({
|
|
150364
|
+
deviceId: number().int().nonnegative(),
|
|
150365
|
+
/** Media identifier / URL. */
|
|
150366
|
+
mediaId: string().min(1),
|
|
150367
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
150368
|
+
* passes it through to the upstream service. */
|
|
150369
|
+
mediaType: string().min(1)
|
|
150370
|
+
}), _void(), {
|
|
150371
|
+
kind: "mutation",
|
|
150372
|
+
auth: "protected"
|
|
150373
|
+
})
|
|
150374
|
+
},
|
|
150375
|
+
status: {
|
|
150376
|
+
schema: MediaPlayerStatusSchema,
|
|
150377
|
+
kind: "push"
|
|
150378
|
+
},
|
|
150379
|
+
/**
|
|
150380
|
+
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
150381
|
+
* full slice for live now-playing, volume, and progress updates
|
|
150382
|
+
* without polling.
|
|
150383
|
+
*/
|
|
150384
|
+
runtimeState: MediaPlayerStatusSchema,
|
|
150385
|
+
/**
|
|
150386
|
+
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
150387
|
+
*
|
|
150388
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
150389
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
150390
|
+
*/
|
|
150391
|
+
durability: "session",
|
|
150392
|
+
/**
|
|
150393
|
+
* History (D850): volume (a level an operator or an automation sets; the
|
|
150394
|
+
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
150395
|
+
* `position` is not recorded: it moves continuously while playing.
|
|
150396
|
+
*/
|
|
150397
|
+
history: { fields: {
|
|
150398
|
+
volumeLevel: {
|
|
150399
|
+
kind: "gauge",
|
|
150400
|
+
unit: "%",
|
|
150401
|
+
deadband: 1
|
|
150402
|
+
},
|
|
150403
|
+
isMuted: { kind: "boolean" }
|
|
150404
|
+
} }
|
|
150405
|
+
};
|
|
149999
150406
|
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;
|
|
150000
150407
|
var NUMBER = {
|
|
150001
150408
|
kind: "fixed",
|
|
@@ -150443,117 +150850,6 @@ function actuator(input) {
|
|
|
150443
150850
|
};
|
|
150444
150851
|
}
|
|
150445
150852
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
150446
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
150447
|
-
"no-power-source",
|
|
150448
|
-
"switch-missing",
|
|
150449
|
-
"switch-offline",
|
|
150450
|
-
"switch-unknown",
|
|
150451
|
-
"restart-in-progress",
|
|
150452
|
-
"switch-call-failed"
|
|
150453
|
-
]);
|
|
150454
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
150455
|
-
accepted: literal(false),
|
|
150456
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
150457
|
-
/** One line for the operator, naming the switch. */
|
|
150458
|
-
message: string()
|
|
150459
|
-
})]);
|
|
150460
|
-
var PowerSourceInfoSchema = object({
|
|
150461
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
150462
|
-
linked: boolean(),
|
|
150463
|
-
switchDeviceId: number().nullable(),
|
|
150464
|
-
switchName: string().nullable(),
|
|
150465
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
150466
|
-
switchOn: boolean().nullable(),
|
|
150467
|
-
switchOnline: boolean().nullable(),
|
|
150468
|
-
restartInProgress: boolean(),
|
|
150469
|
-
offSeconds: number().int()
|
|
150470
|
-
});
|
|
150471
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
150472
|
-
var PowerSourceStateSchema = object({
|
|
150473
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
150474
|
-
switchOn: boolean().nullable(),
|
|
150475
|
-
offSeconds: number().int(),
|
|
150476
|
-
lastFetchedAt: number()
|
|
150477
|
-
});
|
|
150478
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
150479
|
-
DeviceType.Gateway,
|
|
150480
|
-
DeviceType.Hub,
|
|
150481
|
-
DeviceType.Camera,
|
|
150482
|
-
DeviceType.Sensor,
|
|
150483
|
-
DeviceType.MediaPlayer,
|
|
150484
|
-
DeviceType.Vacuum,
|
|
150485
|
-
DeviceType.Generic,
|
|
150486
|
-
DeviceType.Washer,
|
|
150487
|
-
DeviceType.Dryer,
|
|
150488
|
-
DeviceType.Dishwasher,
|
|
150489
|
-
DeviceType.PetFountain,
|
|
150490
|
-
DeviceType.Fan,
|
|
150491
|
-
DeviceType.Humidifier,
|
|
150492
|
-
DeviceType.Climate,
|
|
150493
|
-
DeviceType.WaterHeater
|
|
150494
|
-
];
|
|
150495
|
-
var powerSourceCustomize = {
|
|
150496
|
-
version: 1,
|
|
150497
|
-
targets: [{
|
|
150498
|
-
id: "switch",
|
|
150499
|
-
cardinality: "scalar",
|
|
150500
|
-
vocabulary: {
|
|
150501
|
-
cap: "power-source",
|
|
150502
|
-
stateField: "switchOn"
|
|
150503
|
-
},
|
|
150504
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150505
|
-
apply: "add",
|
|
150506
|
-
path: "switchOn",
|
|
150507
|
-
sources: [{
|
|
150508
|
-
kind: "switch",
|
|
150509
|
-
transforms: [{ kind: "identity" }]
|
|
150510
|
-
}],
|
|
150511
|
-
companions: [{
|
|
150512
|
-
kind: "constant",
|
|
150513
|
-
path: "offSeconds",
|
|
150514
|
-
value: 10,
|
|
150515
|
-
when: "add"
|
|
150516
|
-
}, {
|
|
150517
|
-
kind: "clock",
|
|
150518
|
-
path: "lastFetchedAt",
|
|
150519
|
-
when: "always"
|
|
150520
|
-
}],
|
|
150521
|
-
sourceReport: { status: "assignment-status" }
|
|
150522
|
-
}]
|
|
150523
|
-
};
|
|
150524
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
150525
|
-
var powerSourceCapability = {
|
|
150526
|
-
name: "power-source",
|
|
150527
|
-
scope: "device",
|
|
150528
|
-
mode: "singleton",
|
|
150529
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150530
|
-
methods: {
|
|
150531
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
150532
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150533
|
-
kind: "mutation",
|
|
150534
|
-
auth: "admin"
|
|
150535
|
-
}),
|
|
150536
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150537
|
-
kind: "mutation",
|
|
150538
|
-
auth: "admin"
|
|
150539
|
-
}),
|
|
150540
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150541
|
-
kind: "mutation",
|
|
150542
|
-
auth: "admin"
|
|
150543
|
-
})
|
|
150544
|
-
},
|
|
150545
|
-
runtimeState: PowerSourceStateSchema,
|
|
150546
|
-
customize: powerSourceCustomize,
|
|
150547
|
-
/**
|
|
150548
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
150549
|
-
* its next report; a restored value would outlive the reading it names.
|
|
150550
|
-
*
|
|
150551
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
150552
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
150553
|
-
*/
|
|
150554
|
-
durability: "session",
|
|
150555
|
-
history: "none"
|
|
150556
|
-
};
|
|
150557
150853
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
150558
150854
|
DeviceType.Camera;
|
|
150559
150855
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -150581,6 +150877,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
150581
150877
|
}).optional()
|
|
150582
150878
|
})) });
|
|
150583
150879
|
/**
|
|
150880
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
150881
|
+
* `z.infer`, they do not restate the shape.
|
|
150882
|
+
*/
|
|
150883
|
+
var OverrideSourceSchema = object({
|
|
150884
|
+
addonId: string().min(1),
|
|
150885
|
+
stableId: string().min(1),
|
|
150886
|
+
cap: string().min(1),
|
|
150887
|
+
fieldPath: string().min(1)
|
|
150888
|
+
});
|
|
150889
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
150890
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
150891
|
+
var ThresholdTransformDraftSchema = object({
|
|
150892
|
+
kind: literal("threshold"),
|
|
150893
|
+
comparison: _enum(["gte", "lte"]),
|
|
150894
|
+
threshold: number(),
|
|
150895
|
+
startDwellMs: number().nonnegative(),
|
|
150896
|
+
stopDwellMs: number().nonnegative()
|
|
150897
|
+
});
|
|
150898
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
150899
|
+
IdentityTransformDraftSchema,
|
|
150900
|
+
InvertTransformDraftSchema,
|
|
150901
|
+
ThresholdTransformDraftSchema,
|
|
150902
|
+
object({
|
|
150903
|
+
kind: literal("linear"),
|
|
150904
|
+
scale: number(),
|
|
150905
|
+
offset: number(),
|
|
150906
|
+
clampMin: number().optional(),
|
|
150907
|
+
clampMax: number().optional()
|
|
150908
|
+
}),
|
|
150909
|
+
object({
|
|
150910
|
+
kind: literal("enum-map"),
|
|
150911
|
+
mapping: record(string(), string().nullable())
|
|
150912
|
+
}),
|
|
150913
|
+
object({ kind: literal("number") })
|
|
150914
|
+
]);
|
|
150915
|
+
var ScalarOverrideDraftSchema = object({
|
|
150916
|
+
kind: literal("scalar"),
|
|
150917
|
+
cap: string().min(1),
|
|
150918
|
+
targetId: string().min(1),
|
|
150919
|
+
source: OverrideSourceSchema,
|
|
150920
|
+
transform: CustomizeTransformDraftSchema
|
|
150921
|
+
});
|
|
150922
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
150923
|
+
kind: literal("keyedArray"),
|
|
150924
|
+
cap: string().min(1),
|
|
150925
|
+
targetId: string().min(1),
|
|
150926
|
+
key: string().min(1),
|
|
150927
|
+
label: string().min(1),
|
|
150928
|
+
fieldPath: string().min(1),
|
|
150929
|
+
source: OverrideSourceSchema,
|
|
150930
|
+
transform: CustomizeTransformDraftSchema
|
|
150931
|
+
});
|
|
150932
|
+
var PulseOverrideDraftSchema = object({
|
|
150933
|
+
kind: literal("pulse"),
|
|
150934
|
+
cap: string().min(1),
|
|
150935
|
+
targetId: string().min(1),
|
|
150936
|
+
enabled: boolean(),
|
|
150937
|
+
debounceMs: number().nonnegative(),
|
|
150938
|
+
edges: array(_enum([
|
|
150939
|
+
"rising",
|
|
150940
|
+
"button",
|
|
150941
|
+
"native-event"
|
|
150942
|
+
])).min(1).optional(),
|
|
150943
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
150944
|
+
});
|
|
150945
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
150946
|
+
"in-addition",
|
|
150947
|
+
"confirm",
|
|
150948
|
+
"replace"
|
|
150949
|
+
]).optional() });
|
|
150950
|
+
discriminatedUnion("kind", [
|
|
150951
|
+
ScalarOverrideDraftSchema,
|
|
150952
|
+
KeyedArrayOverrideDraftSchema,
|
|
150953
|
+
PulseOverrideDraftSchema,
|
|
150954
|
+
object({
|
|
150955
|
+
kind: literal("span"),
|
|
150956
|
+
cap: string().min(1),
|
|
150957
|
+
targetId: string().min(1),
|
|
150958
|
+
timeoutMs: number().nonnegative(),
|
|
150959
|
+
cooldownMs: number().nonnegative().optional(),
|
|
150960
|
+
sources: array(SpanSourceDraftSchema)
|
|
150961
|
+
})
|
|
150962
|
+
]);
|
|
150963
|
+
object({
|
|
150964
|
+
targetId: string().min(1),
|
|
150965
|
+
configured: boolean(),
|
|
150966
|
+
source: _enum(["native", "assigned"]),
|
|
150967
|
+
state: _enum([
|
|
150968
|
+
"active",
|
|
150969
|
+
"stale",
|
|
150970
|
+
"released",
|
|
150971
|
+
"unavailable"
|
|
150972
|
+
]),
|
|
150973
|
+
reason: string().nullable()
|
|
150974
|
+
});
|
|
150975
|
+
/**
|
|
150976
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
150977
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
150978
|
+
*
|
|
150979
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
150980
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
150981
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
150982
|
+
*/
|
|
150983
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
150984
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
150985
|
+
"high",
|
|
150986
|
+
"medium",
|
|
150987
|
+
"low"
|
|
150988
|
+
]);
|
|
150989
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
150990
|
+
"no-history",
|
|
150991
|
+
"span-too-short",
|
|
150992
|
+
"truncated",
|
|
150993
|
+
"no-idle-floor",
|
|
150994
|
+
"no-runs",
|
|
150995
|
+
"no-separation"
|
|
150996
|
+
]);
|
|
150997
|
+
var RunningProposalReplaySchema = object({
|
|
150998
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
150999
|
+
intervals: number().int().nonnegative(),
|
|
151000
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
151001
|
+
extra: number().int().nonnegative(),
|
|
151002
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
151003
|
+
missed: number().int().nonnegative()
|
|
151004
|
+
});
|
|
151005
|
+
var RunningProposalEvidenceSchema = object({
|
|
151006
|
+
tier: RunningProposalTierSchema,
|
|
151007
|
+
spanMs: number().nonnegative(),
|
|
151008
|
+
from: number(),
|
|
151009
|
+
to: number(),
|
|
151010
|
+
points: number().int().nonnegative(),
|
|
151011
|
+
runsFound: number().int().nonnegative(),
|
|
151012
|
+
pausesFound: number().int().nonnegative(),
|
|
151013
|
+
longestPauseMs: number().nonnegative(),
|
|
151014
|
+
idleFloorW: number(),
|
|
151015
|
+
noiseCeilingW: number(),
|
|
151016
|
+
plateauW: number(),
|
|
151017
|
+
replay: RunningProposalReplaySchema
|
|
151018
|
+
});
|
|
151019
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
151020
|
+
spanMs: number().nonnegative(),
|
|
151021
|
+
points: number().int().nonnegative(),
|
|
151022
|
+
runsFound: number().int().nonnegative()
|
|
151023
|
+
});
|
|
151024
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
151025
|
+
state: literal("proposal"),
|
|
151026
|
+
transform: ThresholdTransformDraftSchema,
|
|
151027
|
+
evidence: RunningProposalEvidenceSchema,
|
|
151028
|
+
confidence: RunningProposalConfidenceSchema,
|
|
151029
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
151030
|
+
notes: array(string())
|
|
151031
|
+
}), object({
|
|
151032
|
+
state: literal("insufficient"),
|
|
151033
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
151034
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
151035
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
151036
|
+
fallback: ThresholdTransformDraftSchema
|
|
151037
|
+
})]);
|
|
151038
|
+
/**
|
|
150584
151039
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
150585
151040
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
150586
151041
|
*
|
|
@@ -150870,6 +151325,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
150870
151325
|
kind: "mutation",
|
|
150871
151326
|
auth: "admin",
|
|
150872
151327
|
access: "view"
|
|
151328
|
+
}), method(object({
|
|
151329
|
+
deviceId: number().int(),
|
|
151330
|
+
cap: string().min(1),
|
|
151331
|
+
fieldPath: string().min(1),
|
|
151332
|
+
deviceType: _enum(DeviceType).optional()
|
|
151333
|
+
}), RunningThresholdProposalSchema, {
|
|
151334
|
+
auth: "admin",
|
|
151335
|
+
access: "view"
|
|
150873
151336
|
}), method(object({
|
|
150874
151337
|
blockId: string().min(1),
|
|
150875
151338
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -151846,7 +152309,27 @@ method(object({
|
|
|
151846
152309
|
* exposes only the COMMON surface: link state, supported device
|
|
151847
152310
|
* kinds (so the UI can filter the device picker), and per-device
|
|
151848
152311
|
* expose/unexpose.
|
|
151849
|
-
|
|
152312
|
+
*
|
|
152313
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
152314
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
152315
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
152316
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
152317
|
+
* default instance's); present = that instance; an id the provider does not
|
|
152318
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
152319
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
152320
|
+
* change always names the target it changes. A single-instance provider
|
|
152321
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
152322
|
+
*/
|
|
152323
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
152324
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
152325
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
152326
|
+
/** One independent export target of an addon. */
|
|
152327
|
+
var ExportInstanceSchema = object({
|
|
152328
|
+
instanceId: InstanceIdSchema$1,
|
|
152329
|
+
name: string(),
|
|
152330
|
+
enabled: boolean(),
|
|
152331
|
+
kind: string().optional()
|
|
152332
|
+
});
|
|
151850
152333
|
var LinkStateSchema = _enum([
|
|
151851
152334
|
"unlinked",
|
|
151852
152335
|
"linked",
|
|
@@ -151897,10 +152380,14 @@ var ExposedDeviceSchema = object({
|
|
|
151897
152380
|
capabilities: array(string()).optional()
|
|
151898
152381
|
});
|
|
151899
152382
|
var ExposeInputSchema = object({
|
|
152383
|
+
instanceId: InstanceIdSchema$1,
|
|
151900
152384
|
deviceId: string(),
|
|
151901
152385
|
capabilities: array(string()).optional()
|
|
151902
152386
|
});
|
|
151903
|
-
var UnexposeInputSchema = object({
|
|
152387
|
+
var UnexposeInputSchema = object({
|
|
152388
|
+
instanceId: InstanceIdSchema$1,
|
|
152389
|
+
deviceId: string()
|
|
152390
|
+
});
|
|
151904
152391
|
/**
|
|
151905
152392
|
* What an exporter may expose besides devices (D968). An action and a
|
|
151906
152393
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -151929,15 +152416,21 @@ var ExposedEntitySchema = object({
|
|
|
151929
152416
|
entity: ExportEntityRefSchema,
|
|
151930
152417
|
exposedAs: string().optional()
|
|
151931
152418
|
});
|
|
151932
|
-
var ExposeEntityInputSchema = object({
|
|
151933
|
-
|
|
151934
|
-
|
|
152419
|
+
var ExposeEntityInputSchema = object({
|
|
152420
|
+
instanceId: InstanceIdSchema$1,
|
|
152421
|
+
entity: ExportEntityRefSchema
|
|
152422
|
+
});
|
|
152423
|
+
var UnexposeEntityInputSchema = object({
|
|
152424
|
+
instanceId: InstanceIdSchema$1,
|
|
152425
|
+
entity: ExportEntityRefSchema
|
|
152426
|
+
});
|
|
152427
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
151935
152428
|
kind: "mutation",
|
|
151936
152429
|
auth: "admin"
|
|
151937
152430
|
}), method(UnexposeInputSchema, _void(), {
|
|
151938
152431
|
kind: "mutation",
|
|
151939
152432
|
auth: "admin"
|
|
151940
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
152433
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
151941
152434
|
kind: "mutation",
|
|
151942
152435
|
auth: "admin"
|
|
151943
152436
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -155560,15 +156053,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
155560
156053
|
items: array(FanCaptureItemSchema),
|
|
155561
156054
|
omitted: array(FanCaptureOmissionSchema)
|
|
155562
156055
|
});
|
|
155563
|
-
var deviceInput$
|
|
156056
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
155564
156057
|
DeviceType.Fan;
|
|
155565
|
-
method(deviceInput$
|
|
156058
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
155566
156059
|
kind: "mutation",
|
|
155567
156060
|
auth: "admin"
|
|
155568
|
-
}), method(deviceInput$
|
|
156061
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
155569
156062
|
kind: "mutation",
|
|
155570
156063
|
auth: "protected"
|
|
155571
|
-
}), method(deviceInput$
|
|
156064
|
+
}), method(deviceInput$2.extend({
|
|
155572
156065
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
155573
156066
|
steps: array(ActionStepSchema).max(20).default([])
|
|
155574
156067
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -156451,6 +156944,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156451
156944
|
}), object({ success: boolean() }), {
|
|
156452
156945
|
kind: "mutation",
|
|
156453
156946
|
auth: "admin"
|
|
156947
|
+
}), method(object({ id: string() }), object({
|
|
156948
|
+
rows: number().int().nonnegative(),
|
|
156949
|
+
secrets: number().int().nonnegative()
|
|
156950
|
+
}), {
|
|
156951
|
+
kind: "mutation",
|
|
156952
|
+
auth: "admin"
|
|
156454
156953
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
156455
156954
|
addonId: string(),
|
|
156456
156955
|
settings: record(string(), unknown())
|
|
@@ -156479,13 +156978,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156479
156978
|
*/
|
|
156480
156979
|
var ConfigSchemaPassthrough$3 = custom();
|
|
156481
156980
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
156981
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
156982
|
+
/**
|
|
156983
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
156984
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
156985
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
156986
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
156987
|
+
* instance id.
|
|
156988
|
+
*/
|
|
156989
|
+
var IntegrationInstanceKindSchema = object({
|
|
156990
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
156991
|
+
id: string().min(1),
|
|
156992
|
+
label: string().min(1),
|
|
156993
|
+
/** The kind signs in through a server-held interactive session. */
|
|
156994
|
+
signIn: object({
|
|
156995
|
+
basePath: string().min(1),
|
|
156996
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
156997
|
+
}).optional(),
|
|
156998
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
156999
|
+
connectPath: string().min(1).optional(),
|
|
157000
|
+
/** Relative hub path to disconnect that account. */
|
|
157001
|
+
disconnectPath: string().min(1).optional()
|
|
157002
|
+
});
|
|
156482
157003
|
var IntegrationInstanceDescriptionSchema = object({
|
|
157004
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
157005
|
+
role: _enum([
|
|
157006
|
+
"devices",
|
|
157007
|
+
"services",
|
|
157008
|
+
"both"
|
|
157009
|
+
]),
|
|
157010
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
157011
|
+
discovery: object({
|
|
157012
|
+
mode: _enum([
|
|
157013
|
+
"none",
|
|
157014
|
+
"lan",
|
|
157015
|
+
"account"
|
|
157016
|
+
]),
|
|
157017
|
+
scopedToInstance: boolean()
|
|
157018
|
+
}).optional(),
|
|
156483
157019
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
156484
|
-
instanceMode:
|
|
157020
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
156485
157021
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
156486
157022
|
kind: string(),
|
|
156487
157023
|
/** True when `testInstance` really probes the remote. */
|
|
156488
|
-
canTest: boolean()
|
|
157024
|
+
canTest: boolean(),
|
|
157025
|
+
/**
|
|
157026
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
157027
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
157028
|
+
*/
|
|
157029
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
157030
|
+
/**
|
|
157031
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
157032
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
157033
|
+
*/
|
|
157034
|
+
allowRestoreId: boolean().optional(),
|
|
157035
|
+
/**
|
|
157036
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
157037
|
+
* send an id (the UI then mints an opaque one).
|
|
157038
|
+
*/
|
|
157039
|
+
mintsId: boolean().optional()
|
|
156489
157040
|
});
|
|
156490
157041
|
var IntegrationInstanceStatusSchema = object({
|
|
156491
157042
|
state: _enum([
|
|
@@ -156494,7 +157045,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
156494
157045
|
"error",
|
|
156495
157046
|
"unknown"
|
|
156496
157047
|
]),
|
|
156497
|
-
detail: string().optional()
|
|
157048
|
+
detail: string().optional(),
|
|
157049
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
157050
|
+
label: string().optional()
|
|
156498
157051
|
});
|
|
156499
157052
|
var IntegrationInstanceSchema = object({
|
|
156500
157053
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156502,13 +157055,18 @@ var IntegrationInstanceSchema = object({
|
|
|
156502
157055
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
156503
157056
|
configured: boolean(),
|
|
156504
157057
|
/**
|
|
156505
|
-
* The instance's own on/off switch (D996)
|
|
156506
|
-
*
|
|
157058
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
157059
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
157060
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
156507
157061
|
*/
|
|
156508
|
-
enabled: boolean(),
|
|
157062
|
+
enabled: boolean().optional(),
|
|
157063
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
157064
|
+
kind: string().optional(),
|
|
156509
157065
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
156510
157066
|
formValues: record(string(), unknown()),
|
|
156511
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
157067
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
157068
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
157069
|
+
lastCheckedAt: number().optional()
|
|
156512
157070
|
});
|
|
156513
157071
|
var IntegrationInstancePatchSchema = object({
|
|
156514
157072
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156517,8 +157075,9 @@ var IntegrationInstancePatchSchema = object({
|
|
|
156517
157075
|
values: record(string(), unknown())
|
|
156518
157076
|
});
|
|
156519
157077
|
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({
|
|
156520
|
-
id: IntegrationInstanceIdSchema,
|
|
156521
|
-
name: string().min(1)
|
|
157078
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
157079
|
+
name: string().min(1),
|
|
157080
|
+
kind: string().min(1).optional()
|
|
156522
157081
|
}), object({ id: string() }), {
|
|
156523
157082
|
kind: "mutation",
|
|
156524
157083
|
auth: "admin"
|
|
@@ -158579,6 +159138,11 @@ var LogEntrySchema = object({
|
|
|
158579
159138
|
* flight, and not asking for the next slice ends the scan.
|
|
158580
159139
|
*/
|
|
158581
159140
|
var LogReadQuerySchema = object({
|
|
159141
|
+
/**
|
|
159142
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
159143
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
159144
|
+
*/
|
|
159145
|
+
instanceId: string().min(1).max(200).optional(),
|
|
158582
159146
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
158583
159147
|
requestId: string().min(1).max(64).optional(),
|
|
158584
159148
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -158641,6 +159205,8 @@ var LogReadResultSchema = object({
|
|
|
158641
159205
|
/** True when `cancelRead` ended this slice early. */
|
|
158642
159206
|
cancelled: boolean().optional()
|
|
158643
159207
|
});
|
|
159208
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
159209
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
158644
159210
|
var LogReadCapabilitiesSchema = object({
|
|
158645
159211
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
158646
159212
|
readable: boolean(),
|
|
@@ -158659,7 +159225,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
158659
159225
|
/** How far back the store reaches, when the provider knows (days). */
|
|
158660
159226
|
retentionDays: number().nullable()
|
|
158661
159227
|
});
|
|
158662
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
159228
|
+
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() }), {
|
|
158663
159229
|
kind: "mutation",
|
|
158664
159230
|
auth: "admin"
|
|
158665
159231
|
});
|
|
@@ -158763,214 +159329,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
|
|
|
158763
159329
|
]);
|
|
158764
159330
|
method(_void(), array(LoginMethodContributionSchema).readonly());
|
|
158765
159331
|
/**
|
|
158766
|
-
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
158767
|
-
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
158768
|
-
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
158769
|
-
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
158770
|
-
*
|
|
158771
|
-
* The cap declares a wide method surface; not every backend supports
|
|
158772
|
-
* every method. Drivers signal sub-capability via the
|
|
158773
|
-
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
158774
|
-
* corresponding method) so the UI gates buttons without a slice
|
|
158775
|
-
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
158776
|
-
*/
|
|
158777
|
-
var MediaPlayerStateSchema = _enum([
|
|
158778
|
-
"off",
|
|
158779
|
-
"on",
|
|
158780
|
-
"idle",
|
|
158781
|
-
"playing",
|
|
158782
|
-
"paused",
|
|
158783
|
-
"buffering",
|
|
158784
|
-
"standby"
|
|
158785
|
-
]);
|
|
158786
|
-
var MediaPlayerRepeatSchema = _enum([
|
|
158787
|
-
"off",
|
|
158788
|
-
"all",
|
|
158789
|
-
"one"
|
|
158790
|
-
]);
|
|
158791
|
-
var MediaInfoSchema = object({
|
|
158792
|
-
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
158793
|
-
* `podcast`, …). */
|
|
158794
|
-
type: string(),
|
|
158795
|
-
/** Human-readable title. */
|
|
158796
|
-
title: string(),
|
|
158797
|
-
/** Optional artist / channel / station label. */
|
|
158798
|
-
artist: string().optional(),
|
|
158799
|
-
/** Optional album / season / show name. */
|
|
158800
|
-
album: string().optional(),
|
|
158801
|
-
/** Optional cover-art / thumbnail URL. */
|
|
158802
|
-
imageUrl: string().optional()
|
|
158803
|
-
});
|
|
158804
|
-
/**
|
|
158805
|
-
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
158806
|
-
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
158807
|
-
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
158808
|
-
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
158809
|
-
* guess, never as a reading.
|
|
158810
|
-
*/
|
|
158811
|
-
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
158812
|
-
/** The fields of a media-player status a driver can only guess. */
|
|
158813
|
-
var MediaPlayerAssumedFieldSchema = _enum([
|
|
158814
|
-
"state",
|
|
158815
|
-
"volumeLevel",
|
|
158816
|
-
"isMuted",
|
|
158817
|
-
"source"
|
|
158818
|
-
]);
|
|
158819
|
-
var MediaPlayerStatusSchema = object({
|
|
158820
|
-
/** Playback lifecycle state. */
|
|
158821
|
-
state: MediaPlayerStateSchema,
|
|
158822
|
-
/**
|
|
158823
|
-
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
158824
|
-
* A slice written before this field existed parses as `reported`.
|
|
158825
|
-
*/
|
|
158826
|
-
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
158827
|
-
/**
|
|
158828
|
-
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
158829
|
-
* this is not empty; a field not listed here is a reading even then (power
|
|
158830
|
-
* from a running-state source, mute guessed from the last send).
|
|
158831
|
-
*/
|
|
158832
|
-
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
158833
|
-
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
158834
|
-
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
158835
|
-
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
158836
|
-
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
158837
|
-
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
158838
|
-
isMuted: boolean().nullable(),
|
|
158839
|
-
/** Active source / input. Empty when no source surface. */
|
|
158840
|
-
source: string(),
|
|
158841
|
-
/** Selectable sources. Empty when no source surface.
|
|
158842
|
-
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
158843
|
-
availableSources: array(string()),
|
|
158844
|
-
/** Currently-playing media info. Null when nothing is playing. */
|
|
158845
|
-
currentMedia: MediaInfoSchema.nullable(),
|
|
158846
|
-
/** Current playback position in ms. Null when not seekable or
|
|
158847
|
-
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
158848
|
-
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
158849
|
-
/** Total duration of the current media in ms. Null when unknown
|
|
158850
|
-
* (live stream) or nothing is playing. */
|
|
158851
|
-
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
158852
|
-
/** Shuffle toggle. Null when no shuffle surface.
|
|
158853
|
-
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
158854
|
-
shuffle: boolean().nullable(),
|
|
158855
|
-
/** Repeat mode. Null when no repeat surface.
|
|
158856
|
-
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
158857
|
-
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
158858
|
-
/** Ms epoch when the slice was last updated. */
|
|
158859
|
-
lastChangedAt: number()
|
|
158860
|
-
});
|
|
158861
|
-
var mediaPlayerCapability = {
|
|
158862
|
-
name: "media-player",
|
|
158863
|
-
scope: "device",
|
|
158864
|
-
deviceNative: true,
|
|
158865
|
-
mode: "singleton",
|
|
158866
|
-
deviceTypes: [DeviceType.MediaPlayer],
|
|
158867
|
-
methods: {
|
|
158868
|
-
play: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158869
|
-
kind: "mutation",
|
|
158870
|
-
auth: "protected"
|
|
158871
|
-
}),
|
|
158872
|
-
pause: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158873
|
-
kind: "mutation",
|
|
158874
|
-
auth: "protected"
|
|
158875
|
-
}),
|
|
158876
|
-
stop: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158877
|
-
kind: "mutation",
|
|
158878
|
-
auth: "protected"
|
|
158879
|
-
}),
|
|
158880
|
-
next: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158881
|
-
kind: "mutation",
|
|
158882
|
-
auth: "protected"
|
|
158883
|
-
}),
|
|
158884
|
-
previous: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
158885
|
-
kind: "mutation",
|
|
158886
|
-
auth: "protected"
|
|
158887
|
-
}),
|
|
158888
|
-
seek: method(object({
|
|
158889
|
-
deviceId: number().int().nonnegative(),
|
|
158890
|
-
position: number().int().nonnegative()
|
|
158891
|
-
}), _void(), {
|
|
158892
|
-
kind: "mutation",
|
|
158893
|
-
auth: "protected"
|
|
158894
|
-
}),
|
|
158895
|
-
setVolume: method(object({
|
|
158896
|
-
deviceId: number().int().nonnegative(),
|
|
158897
|
-
volumeLevel: number().min(0).max(100)
|
|
158898
|
-
}), _void(), {
|
|
158899
|
-
kind: "mutation",
|
|
158900
|
-
auth: "protected"
|
|
158901
|
-
}),
|
|
158902
|
-
setMute: method(object({
|
|
158903
|
-
deviceId: number().int().nonnegative(),
|
|
158904
|
-
muted: boolean()
|
|
158905
|
-
}), _void(), {
|
|
158906
|
-
kind: "mutation",
|
|
158907
|
-
auth: "protected"
|
|
158908
|
-
}),
|
|
158909
|
-
setShuffle: method(object({
|
|
158910
|
-
deviceId: number().int().nonnegative(),
|
|
158911
|
-
shuffle: boolean()
|
|
158912
|
-
}), _void(), {
|
|
158913
|
-
kind: "mutation",
|
|
158914
|
-
auth: "protected"
|
|
158915
|
-
}),
|
|
158916
|
-
setRepeat: method(object({
|
|
158917
|
-
deviceId: number().int().nonnegative(),
|
|
158918
|
-
repeat: MediaPlayerRepeatSchema
|
|
158919
|
-
}), _void(), {
|
|
158920
|
-
kind: "mutation",
|
|
158921
|
-
auth: "protected"
|
|
158922
|
-
}),
|
|
158923
|
-
selectSource: method(object({
|
|
158924
|
-
deviceId: number().int().nonnegative(),
|
|
158925
|
-
source: string().min(1)
|
|
158926
|
-
}), _void(), {
|
|
158927
|
-
kind: "mutation",
|
|
158928
|
-
auth: "protected"
|
|
158929
|
-
}),
|
|
158930
|
-
playMedia: method(object({
|
|
158931
|
-
deviceId: number().int().nonnegative(),
|
|
158932
|
-
/** Media identifier / URL. */
|
|
158933
|
-
mediaId: string().min(1),
|
|
158934
|
-
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
158935
|
-
* passes it through to the upstream service. */
|
|
158936
|
-
mediaType: string().min(1)
|
|
158937
|
-
}), _void(), {
|
|
158938
|
-
kind: "mutation",
|
|
158939
|
-
auth: "protected"
|
|
158940
|
-
})
|
|
158941
|
-
},
|
|
158942
|
-
status: {
|
|
158943
|
-
schema: MediaPlayerStatusSchema,
|
|
158944
|
-
kind: "push"
|
|
158945
|
-
},
|
|
158946
|
-
/**
|
|
158947
|
-
* Runtime-state slice — mirrored by the kernel. UI players read the
|
|
158948
|
-
* full slice for live now-playing, volume, and progress updates
|
|
158949
|
-
* without polling.
|
|
158950
|
-
*/
|
|
158951
|
-
runtimeState: MediaPlayerStatusSchema,
|
|
158952
|
-
/**
|
|
158953
|
-
* Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
|
|
158954
|
-
*
|
|
158955
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
158956
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
158957
|
-
*/
|
|
158958
|
-
durability: "session",
|
|
158959
|
-
/**
|
|
158960
|
-
* History (D850): volume (a level an operator or an automation sets; the
|
|
158961
|
-
* default 10 s floor folds a slider drag into its end value) and mute.
|
|
158962
|
-
* `position` is not recorded: it moves continuously while playing.
|
|
158963
|
-
*/
|
|
158964
|
-
history: { fields: {
|
|
158965
|
-
volumeLevel: {
|
|
158966
|
-
kind: "gauge",
|
|
158967
|
-
unit: "%",
|
|
158968
|
-
deadband: 1
|
|
158969
|
-
},
|
|
158970
|
-
isMuted: { kind: "boolean" }
|
|
158971
|
-
} }
|
|
158972
|
-
};
|
|
158973
|
-
/**
|
|
158974
159332
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
158975
159333
|
*
|
|
158976
159334
|
* Different shape from `network-access` (public tunnels):
|
|
@@ -167749,6 +168107,21 @@ var NcTemplateVarKindSchema = _enum([
|
|
|
167749
168107
|
"sound",
|
|
167750
168108
|
"system"
|
|
167751
168109
|
]);
|
|
168110
|
+
/**
|
|
168111
|
+
* The default title/body a rule of one SITUATION is delivered with when the
|
|
168112
|
+
* operator wrote no template — served so an editor can pre-fill it (and the
|
|
168113
|
+
* operator can rebuild it from placeholders) instead of starting from a blank
|
|
168114
|
+
* box that, once saved, silently replaces it. `id` names the situation inside
|
|
168115
|
+
* a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
|
|
168116
|
+
* appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
|
|
168117
|
+
* the hub's current language and with its overrides applied.
|
|
168118
|
+
*/
|
|
168119
|
+
var NcTemplateDefaultSchema = object({
|
|
168120
|
+
id: _enum(["appliance-cycle"]),
|
|
168121
|
+
kind: NcRuleKindSchema,
|
|
168122
|
+
title: string(),
|
|
168123
|
+
body: string()
|
|
168124
|
+
});
|
|
167752
168125
|
/** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
|
|
167753
168126
|
var NcTemplateFamilySchema = _enum([
|
|
167754
168127
|
"rule",
|
|
@@ -168604,7 +168977,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
168604
168977
|
}), method(object({}), object({
|
|
168605
168978
|
catalog: array(NcConditionDescriptorSchema),
|
|
168606
168979
|
taxonomy: NcTaxonomySchema.optional()
|
|
168607
|
-
})), method(object({}), object({
|
|
168980
|
+
})), method(object({}), object({
|
|
168981
|
+
vars: array(NcTemplateVarDescriptorSchema),
|
|
168982
|
+
/** Per-situation default texts; absent on a hub that predates them. */
|
|
168983
|
+
defaults: array(NcTemplateDefaultSchema)
|
|
168984
|
+
})), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
|
|
168608
168985
|
kind: "mutation",
|
|
168609
168986
|
access: "view"
|
|
168610
168987
|
}), 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 }), {
|
|
@@ -171241,6 +171618,14 @@ var powerMeterCapability = {
|
|
|
171241
171618
|
deviceNative: true,
|
|
171242
171619
|
mode: "singleton",
|
|
171243
171620
|
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
171621
|
+
deviceConfig: { ui: {
|
|
171622
|
+
kind: "widget",
|
|
171623
|
+
widgetId: "host/power-meter-line",
|
|
171624
|
+
tab: "power",
|
|
171625
|
+
placement: "hero",
|
|
171626
|
+
label: "Power",
|
|
171627
|
+
order: 4
|
|
171628
|
+
} },
|
|
171244
171629
|
methods: {},
|
|
171245
171630
|
status: {
|
|
171246
171631
|
schema: PowerMeterStatusSchema,
|
|
@@ -171300,6 +171685,117 @@ var powerMeterCapability = {
|
|
|
171300
171685
|
volatileStateFields: ["lastFetchedAt"],
|
|
171301
171686
|
customize: powerMeterCustomize
|
|
171302
171687
|
};
|
|
171688
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
171689
|
+
"no-power-source",
|
|
171690
|
+
"switch-missing",
|
|
171691
|
+
"switch-offline",
|
|
171692
|
+
"switch-unknown",
|
|
171693
|
+
"restart-in-progress",
|
|
171694
|
+
"switch-call-failed"
|
|
171695
|
+
]);
|
|
171696
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
171697
|
+
accepted: literal(false),
|
|
171698
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
171699
|
+
/** One line for the operator, naming the switch. */
|
|
171700
|
+
message: string()
|
|
171701
|
+
})]);
|
|
171702
|
+
var PowerSourceInfoSchema = object({
|
|
171703
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
171704
|
+
linked: boolean(),
|
|
171705
|
+
switchDeviceId: number().nullable(),
|
|
171706
|
+
switchName: string().nullable(),
|
|
171707
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
171708
|
+
switchOn: boolean().nullable(),
|
|
171709
|
+
switchOnline: boolean().nullable(),
|
|
171710
|
+
restartInProgress: boolean(),
|
|
171711
|
+
offSeconds: number().int()
|
|
171712
|
+
});
|
|
171713
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
171714
|
+
var PowerSourceStateSchema = object({
|
|
171715
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
171716
|
+
switchOn: boolean().nullable(),
|
|
171717
|
+
offSeconds: number().int(),
|
|
171718
|
+
lastFetchedAt: number()
|
|
171719
|
+
});
|
|
171720
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
171721
|
+
DeviceType.Gateway,
|
|
171722
|
+
DeviceType.Hub,
|
|
171723
|
+
DeviceType.Camera,
|
|
171724
|
+
DeviceType.Sensor,
|
|
171725
|
+
DeviceType.MediaPlayer,
|
|
171726
|
+
DeviceType.Vacuum,
|
|
171727
|
+
DeviceType.Generic,
|
|
171728
|
+
DeviceType.Washer,
|
|
171729
|
+
DeviceType.Dryer,
|
|
171730
|
+
DeviceType.Dishwasher,
|
|
171731
|
+
DeviceType.PetFountain,
|
|
171732
|
+
DeviceType.Fan,
|
|
171733
|
+
DeviceType.Humidifier,
|
|
171734
|
+
DeviceType.Climate,
|
|
171735
|
+
DeviceType.WaterHeater
|
|
171736
|
+
];
|
|
171737
|
+
var powerSourceCustomize = {
|
|
171738
|
+
version: 1,
|
|
171739
|
+
targets: [{
|
|
171740
|
+
id: "switch",
|
|
171741
|
+
cardinality: "scalar",
|
|
171742
|
+
vocabulary: {
|
|
171743
|
+
cap: "power-source",
|
|
171744
|
+
stateField: "switchOn"
|
|
171745
|
+
},
|
|
171746
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
171747
|
+
apply: "add",
|
|
171748
|
+
path: "switchOn",
|
|
171749
|
+
sources: [{
|
|
171750
|
+
kind: "switch",
|
|
171751
|
+
transforms: [{ kind: "identity" }]
|
|
171752
|
+
}],
|
|
171753
|
+
companions: [{
|
|
171754
|
+
kind: "constant",
|
|
171755
|
+
path: "offSeconds",
|
|
171756
|
+
value: 10,
|
|
171757
|
+
when: "add"
|
|
171758
|
+
}, {
|
|
171759
|
+
kind: "clock",
|
|
171760
|
+
path: "lastFetchedAt",
|
|
171761
|
+
when: "always"
|
|
171762
|
+
}],
|
|
171763
|
+
sourceReport: { status: "assignment-status" }
|
|
171764
|
+
}]
|
|
171765
|
+
};
|
|
171766
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
171767
|
+
var powerSourceCapability = {
|
|
171768
|
+
name: "power-source",
|
|
171769
|
+
scope: "device",
|
|
171770
|
+
mode: "singleton",
|
|
171771
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
171772
|
+
methods: {
|
|
171773
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
171774
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171775
|
+
kind: "mutation",
|
|
171776
|
+
auth: "admin"
|
|
171777
|
+
}),
|
|
171778
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171779
|
+
kind: "mutation",
|
|
171780
|
+
auth: "admin"
|
|
171781
|
+
}),
|
|
171782
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171783
|
+
kind: "mutation",
|
|
171784
|
+
auth: "admin"
|
|
171785
|
+
})
|
|
171786
|
+
},
|
|
171787
|
+
runtimeState: PowerSourceStateSchema,
|
|
171788
|
+
customize: powerSourceCustomize,
|
|
171789
|
+
/**
|
|
171790
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
171791
|
+
* its next report; a restored value would outlive the reading it names.
|
|
171792
|
+
*
|
|
171793
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
171794
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
171795
|
+
*/
|
|
171796
|
+
durability: "session",
|
|
171797
|
+
history: "none"
|
|
171798
|
+
};
|
|
171303
171799
|
/**
|
|
171304
171800
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
171305
171801
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -175362,7 +175858,13 @@ var RemoteMediaCommandSchema = _enum([
|
|
|
175362
175858
|
"next",
|
|
175363
175859
|
"previous",
|
|
175364
175860
|
"setMute",
|
|
175365
|
-
"selectSource"
|
|
175861
|
+
"selectSource",
|
|
175862
|
+
"turnOn",
|
|
175863
|
+
"turnOff",
|
|
175864
|
+
"volumeUp",
|
|
175865
|
+
"volumeDown",
|
|
175866
|
+
"channelUp",
|
|
175867
|
+
"channelDown"
|
|
175366
175868
|
]);
|
|
175367
175869
|
/** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
|
|
175368
175870
|
var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
|
|
@@ -175383,7 +175885,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
|
|
|
175383
175885
|
}), RemoteSendResultSchema, {
|
|
175384
175886
|
kind: "mutation",
|
|
175385
175887
|
auth: "protected"
|
|
175386
|
-
}), method(deviceInput.extend({
|
|
175888
|
+
}), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
|
|
175387
175889
|
command: RemoteMediaCommandSchema,
|
|
175388
175890
|
muted: boolean().optional(),
|
|
175389
175891
|
source: string().optional(),
|
|
@@ -177733,7 +178235,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
|
|
|
177733
178235
|
* `getSettingsUISchema()` surface.
|
|
177734
178236
|
*/
|
|
177735
178237
|
var SwitchStatusSchema = object({
|
|
177736
|
-
|
|
178238
|
+
/**
|
|
178239
|
+
* `null` = the device has never reported its state (a measurement that
|
|
178240
|
+
* has not happened is unknown, never `false`).
|
|
178241
|
+
*/
|
|
178242
|
+
on: boolean().nullable().meta({ label: "On" }),
|
|
177737
178243
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
177738
178244
|
lastChangedAt: number()
|
|
177739
178245
|
});
|
|
@@ -177758,7 +178264,7 @@ var switchCapability = {
|
|
|
177758
178264
|
}) },
|
|
177759
178265
|
events: { onStateChanged: { data: object({
|
|
177760
178266
|
deviceId: number(),
|
|
177761
|
-
on: boolean(),
|
|
178267
|
+
on: boolean().nullable(),
|
|
177762
178268
|
lastChangedAt: number()
|
|
177763
178269
|
}) } },
|
|
177764
178270
|
status: {
|
|
@@ -181770,6 +182276,26 @@ function buildStreamParamsConfigSchema(options, status) {
|
|
|
181770
182276
|
if (sections.length === 0) return null;
|
|
181771
182277
|
return { sections };
|
|
181772
182278
|
}
|
|
182279
|
+
/**
|
|
182280
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
182281
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
182282
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
182283
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
182284
|
+
* server sends an id, a client maps it to its own icon set.
|
|
182285
|
+
*
|
|
182286
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
182287
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
182288
|
+
* to `icon`.
|
|
182289
|
+
*/
|
|
182290
|
+
function digit(n) {
|
|
182291
|
+
return {
|
|
182292
|
+
label: t(n, n),
|
|
182293
|
+
icon: "hash",
|
|
182294
|
+
legend: n
|
|
182295
|
+
};
|
|
182296
|
+
}
|
|
182297
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
182298
|
+
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
181773
182299
|
var btn = (idSuffix, row, col, init) => ({
|
|
181774
182300
|
idSuffix,
|
|
181775
182301
|
row,
|
|
@@ -181897,6 +182423,34 @@ discriminatedUnion("kind", [object({
|
|
|
181897
182423
|
code: string()
|
|
181898
182424
|
})]);
|
|
181899
182425
|
/**
|
|
182426
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
182427
|
+
*
|
|
182428
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
182429
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
182430
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
182431
|
+
* count of samples.
|
|
182432
|
+
*
|
|
182433
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
182434
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
182435
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
182436
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
182437
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
182438
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
182439
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
182440
|
+
* covers the longest standby blip above T.
|
|
182441
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
182442
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
182443
|
+
*/
|
|
182444
|
+
var SECOND_MS = 1e3;
|
|
182445
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
182446
|
+
24 * (60 * MINUTE_MS$1);
|
|
182447
|
+
10 * MINUTE_MS$1;
|
|
182448
|
+
30 * SECOND_MS;
|
|
182449
|
+
60 * SECOND_MS;
|
|
182450
|
+
20 * MINUTE_MS$1;
|
|
182451
|
+
10 * SECOND_MS;
|
|
182452
|
+
10 * SECOND_MS;
|
|
182453
|
+
/**
|
|
181900
182454
|
* Which of OUR object detections a clip window holds — the ONE overlap rule
|
|
181901
182455
|
* and the ONE label ranking, for every clip source (D923).
|
|
181902
182456
|
*
|
|
@@ -185149,6 +185703,13 @@ Object.freeze({
|
|
|
185149
185703
|
access: "create",
|
|
185150
185704
|
role: "admin"
|
|
185151
185705
|
},
|
|
185706
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
185707
|
+
capName: "core-blocks",
|
|
185708
|
+
capScope: "system",
|
|
185709
|
+
addonId: null,
|
|
185710
|
+
access: "view",
|
|
185711
|
+
role: "admin"
|
|
185712
|
+
},
|
|
185152
185713
|
"coreBlocks.reportAutomationRun": {
|
|
185153
185714
|
capName: "core-blocks",
|
|
185154
185715
|
capScope: "system",
|
|
@@ -185660,6 +186221,13 @@ Object.freeze({
|
|
|
185660
186221
|
access: "view",
|
|
185661
186222
|
role: "admin"
|
|
185662
186223
|
},
|
|
186224
|
+
"deviceExport.listInstances": {
|
|
186225
|
+
capName: "device-export",
|
|
186226
|
+
capScope: "system",
|
|
186227
|
+
addonId: null,
|
|
186228
|
+
access: "view",
|
|
186229
|
+
role: "admin"
|
|
186230
|
+
},
|
|
185663
186231
|
"deviceExport.listSupportedDeviceKinds": {
|
|
185664
186232
|
capName: "device-export",
|
|
185665
186233
|
capScope: "system",
|
|
@@ -187312,6 +187880,13 @@ Object.freeze({
|
|
|
187312
187880
|
access: "create",
|
|
187313
187881
|
role: "admin"
|
|
187314
187882
|
},
|
|
187883
|
+
"integrations.clearSettings": {
|
|
187884
|
+
capName: "integrations",
|
|
187885
|
+
capScope: "system",
|
|
187886
|
+
addonId: null,
|
|
187887
|
+
access: "delete",
|
|
187888
|
+
role: "admin"
|
|
187889
|
+
},
|
|
187315
187890
|
"integrations.create": {
|
|
187316
187891
|
capName: "integrations",
|
|
187317
187892
|
capScope: "system",
|
|
@@ -188096,6 +188671,20 @@ Object.freeze({
|
|
|
188096
188671
|
access: "view",
|
|
188097
188672
|
role: "admin"
|
|
188098
188673
|
},
|
|
188674
|
+
"mediaPlayer.channelDown": {
|
|
188675
|
+
capName: "media-player",
|
|
188676
|
+
capScope: "device",
|
|
188677
|
+
addonId: null,
|
|
188678
|
+
access: "create",
|
|
188679
|
+
role: "viewer"
|
|
188680
|
+
},
|
|
188681
|
+
"mediaPlayer.channelUp": {
|
|
188682
|
+
capName: "media-player",
|
|
188683
|
+
capScope: "device",
|
|
188684
|
+
addonId: null,
|
|
188685
|
+
access: "create",
|
|
188686
|
+
role: "viewer"
|
|
188687
|
+
},
|
|
188099
188688
|
"mediaPlayer.getStatus": {
|
|
188100
188689
|
capName: "media-player",
|
|
188101
188690
|
capScope: "device",
|
|
@@ -188187,6 +188776,34 @@ Object.freeze({
|
|
|
188187
188776
|
access: "create",
|
|
188188
188777
|
role: "viewer"
|
|
188189
188778
|
},
|
|
188779
|
+
"mediaPlayer.turnOff": {
|
|
188780
|
+
capName: "media-player",
|
|
188781
|
+
capScope: "device",
|
|
188782
|
+
addonId: null,
|
|
188783
|
+
access: "create",
|
|
188784
|
+
role: "viewer"
|
|
188785
|
+
},
|
|
188786
|
+
"mediaPlayer.turnOn": {
|
|
188787
|
+
capName: "media-player",
|
|
188788
|
+
capScope: "device",
|
|
188789
|
+
addonId: null,
|
|
188790
|
+
access: "create",
|
|
188791
|
+
role: "viewer"
|
|
188792
|
+
},
|
|
188793
|
+
"mediaPlayer.volumeDown": {
|
|
188794
|
+
capName: "media-player",
|
|
188795
|
+
capScope: "device",
|
|
188796
|
+
addonId: null,
|
|
188797
|
+
access: "create",
|
|
188798
|
+
role: "viewer"
|
|
188799
|
+
},
|
|
188800
|
+
"mediaPlayer.volumeUp": {
|
|
188801
|
+
capName: "media-player",
|
|
188802
|
+
capScope: "device",
|
|
188803
|
+
addonId: null,
|
|
188804
|
+
access: "create",
|
|
188805
|
+
role: "viewer"
|
|
188806
|
+
},
|
|
188190
188807
|
"meshNetwork.getStatus": {
|
|
188191
188808
|
capName: "mesh-network",
|
|
188192
188809
|
capScope: "system",
|
|
@@ -191743,6 +192360,13 @@ Object.freeze({
|
|
|
191743
192360
|
access: "create",
|
|
191744
192361
|
role: "admin"
|
|
191745
192362
|
},
|
|
192363
|
+
"remote.getAssumedStatus": {
|
|
192364
|
+
capName: "remote",
|
|
192365
|
+
capScope: "system",
|
|
192366
|
+
addonId: null,
|
|
192367
|
+
access: "view",
|
|
192368
|
+
role: "admin"
|
|
192369
|
+
},
|
|
191746
192370
|
"remote.getConfig": {
|
|
191747
192371
|
capName: "remote",
|
|
191748
192372
|
capScope: "system",
|
|
@@ -194710,6 +195334,11 @@ Object.freeze({
|
|
|
194710
195334
|
form: "single",
|
|
194711
195335
|
optional: false
|
|
194712
195336
|
}],
|
|
195337
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
195338
|
+
name: "deviceId",
|
|
195339
|
+
form: "single",
|
|
195340
|
+
optional: false
|
|
195341
|
+
}],
|
|
194713
195342
|
"cover.close": [{
|
|
194714
195343
|
name: "deviceId",
|
|
194715
195344
|
form: "single",
|
|
@@ -195561,6 +196190,16 @@ Object.freeze({
|
|
|
195561
196190
|
form: "single",
|
|
195562
196191
|
optional: true
|
|
195563
196192
|
}],
|
|
196193
|
+
"mediaPlayer.channelDown": [{
|
|
196194
|
+
name: "deviceId",
|
|
196195
|
+
form: "single",
|
|
196196
|
+
optional: false
|
|
196197
|
+
}],
|
|
196198
|
+
"mediaPlayer.channelUp": [{
|
|
196199
|
+
name: "deviceId",
|
|
196200
|
+
form: "single",
|
|
196201
|
+
optional: false
|
|
196202
|
+
}],
|
|
195564
196203
|
"mediaPlayer.getStatus": [{
|
|
195565
196204
|
name: "deviceId",
|
|
195566
196205
|
form: "single",
|
|
@@ -195626,6 +196265,26 @@ Object.freeze({
|
|
|
195626
196265
|
form: "single",
|
|
195627
196266
|
optional: false
|
|
195628
196267
|
}],
|
|
196268
|
+
"mediaPlayer.turnOff": [{
|
|
196269
|
+
name: "deviceId",
|
|
196270
|
+
form: "single",
|
|
196271
|
+
optional: false
|
|
196272
|
+
}],
|
|
196273
|
+
"mediaPlayer.turnOn": [{
|
|
196274
|
+
name: "deviceId",
|
|
196275
|
+
form: "single",
|
|
196276
|
+
optional: false
|
|
196277
|
+
}],
|
|
196278
|
+
"mediaPlayer.volumeDown": [{
|
|
196279
|
+
name: "deviceId",
|
|
196280
|
+
form: "single",
|
|
196281
|
+
optional: false
|
|
196282
|
+
}],
|
|
196283
|
+
"mediaPlayer.volumeUp": [{
|
|
196284
|
+
name: "deviceId",
|
|
196285
|
+
form: "single",
|
|
196286
|
+
optional: false
|
|
196287
|
+
}],
|
|
195629
196288
|
"meter.getStatus": [{
|
|
195630
196289
|
name: "deviceId",
|
|
195631
196290
|
form: "single",
|
|
@@ -196791,6 +197450,11 @@ Object.freeze({
|
|
|
196791
197450
|
form: "single",
|
|
196792
197451
|
optional: true
|
|
196793
197452
|
}],
|
|
197453
|
+
"remote.getAssumedStatus": [{
|
|
197454
|
+
name: "deviceId",
|
|
197455
|
+
form: "single",
|
|
197456
|
+
optional: false
|
|
197457
|
+
}],
|
|
196794
197458
|
"remote.getConfig": [{
|
|
196795
197459
|
name: "deviceId",
|
|
196796
197460
|
form: "single",
|
|
@@ -197338,99 +198002,6 @@ Object.freeze({
|
|
|
197338
198002
|
"network-access": "ingress",
|
|
197339
198003
|
"smtp-provider": "email"
|
|
197340
198004
|
});
|
|
197341
|
-
/**
|
|
197342
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
197343
|
-
* `z.infer`, they do not restate the shape.
|
|
197344
|
-
*/
|
|
197345
|
-
var OverrideSourceSchema = object({
|
|
197346
|
-
addonId: string().min(1),
|
|
197347
|
-
stableId: string().min(1),
|
|
197348
|
-
cap: string().min(1),
|
|
197349
|
-
fieldPath: string().min(1)
|
|
197350
|
-
});
|
|
197351
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
197352
|
-
object({ kind: literal("identity") }),
|
|
197353
|
-
object({ kind: literal("invert") }),
|
|
197354
|
-
object({
|
|
197355
|
-
kind: literal("threshold"),
|
|
197356
|
-
comparison: _enum(["gte", "lte"]),
|
|
197357
|
-
threshold: number(),
|
|
197358
|
-
startDwellMs: number().nonnegative(),
|
|
197359
|
-
stopDwellMs: number().nonnegative()
|
|
197360
|
-
}),
|
|
197361
|
-
object({
|
|
197362
|
-
kind: literal("linear"),
|
|
197363
|
-
scale: number(),
|
|
197364
|
-
offset: number(),
|
|
197365
|
-
clampMin: number().optional(),
|
|
197366
|
-
clampMax: number().optional()
|
|
197367
|
-
}),
|
|
197368
|
-
object({
|
|
197369
|
-
kind: literal("enum-map"),
|
|
197370
|
-
mapping: record(string(), string().nullable())
|
|
197371
|
-
}),
|
|
197372
|
-
object({ kind: literal("number") })
|
|
197373
|
-
]);
|
|
197374
|
-
var ScalarOverrideDraftSchema = object({
|
|
197375
|
-
kind: literal("scalar"),
|
|
197376
|
-
cap: string().min(1),
|
|
197377
|
-
targetId: string().min(1),
|
|
197378
|
-
source: OverrideSourceSchema,
|
|
197379
|
-
transform: CustomizeTransformDraftSchema
|
|
197380
|
-
});
|
|
197381
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
197382
|
-
kind: literal("keyedArray"),
|
|
197383
|
-
cap: string().min(1),
|
|
197384
|
-
targetId: string().min(1),
|
|
197385
|
-
key: string().min(1),
|
|
197386
|
-
label: string().min(1),
|
|
197387
|
-
fieldPath: string().min(1),
|
|
197388
|
-
source: OverrideSourceSchema,
|
|
197389
|
-
transform: CustomizeTransformDraftSchema
|
|
197390
|
-
});
|
|
197391
|
-
var PulseOverrideDraftSchema = object({
|
|
197392
|
-
kind: literal("pulse"),
|
|
197393
|
-
cap: string().min(1),
|
|
197394
|
-
targetId: string().min(1),
|
|
197395
|
-
enabled: boolean(),
|
|
197396
|
-
debounceMs: number().nonnegative(),
|
|
197397
|
-
edges: array(_enum([
|
|
197398
|
-
"rising",
|
|
197399
|
-
"button",
|
|
197400
|
-
"native-event"
|
|
197401
|
-
])).min(1).optional(),
|
|
197402
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
197403
|
-
});
|
|
197404
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
197405
|
-
"in-addition",
|
|
197406
|
-
"confirm",
|
|
197407
|
-
"replace"
|
|
197408
|
-
]).optional() });
|
|
197409
|
-
discriminatedUnion("kind", [
|
|
197410
|
-
ScalarOverrideDraftSchema,
|
|
197411
|
-
KeyedArrayOverrideDraftSchema,
|
|
197412
|
-
PulseOverrideDraftSchema,
|
|
197413
|
-
object({
|
|
197414
|
-
kind: literal("span"),
|
|
197415
|
-
cap: string().min(1),
|
|
197416
|
-
targetId: string().min(1),
|
|
197417
|
-
timeoutMs: number().nonnegative(),
|
|
197418
|
-
cooldownMs: number().nonnegative().optional(),
|
|
197419
|
-
sources: array(SpanSourceDraftSchema)
|
|
197420
|
-
})
|
|
197421
|
-
]);
|
|
197422
|
-
object({
|
|
197423
|
-
targetId: string().min(1),
|
|
197424
|
-
configured: boolean(),
|
|
197425
|
-
source: _enum(["native", "assigned"]),
|
|
197426
|
-
state: _enum([
|
|
197427
|
-
"active",
|
|
197428
|
-
"stale",
|
|
197429
|
-
"released",
|
|
197430
|
-
"unavailable"
|
|
197431
|
-
]),
|
|
197432
|
-
reason: string().nullable()
|
|
197433
|
-
});
|
|
197434
198005
|
var G711_SCALE_CORRECTION_DB = {
|
|
197435
198006
|
PCMU: 20 * Math.log10(4),
|
|
197436
198007
|
PCMA: 20 * Math.log10(8)
|
|
@@ -198046,6 +198617,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
198046
198617
|
[...RULE_TEXT],
|
|
198047
198618
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
198048
198619
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
198620
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
198049
198621
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
198050
198622
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
198051
198623
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -208245,7 +208817,7 @@ var PirAccessory = class extends BaseDevice {
|
|
|
208245
208817
|
async applySettingsPatch(patch) {
|
|
208246
208818
|
await this.config.setAll(patch);
|
|
208247
208819
|
if ("pirSensitive" in patch || "pirReduceAlarm" in patch || "pirIntervalS" in patch) try {
|
|
208248
|
-
await this.pushConfigToFirmware(this.switchView().on);
|
|
208820
|
+
await this.pushConfigToFirmware(this.switchView().on === true);
|
|
208249
208821
|
} catch (err) {
|
|
208250
208822
|
this.ctx.logger.warn("pir firmware push failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
208251
208823
|
throw err;
|
|
@@ -219441,6 +220013,285 @@ async function startLegacyWebhookIntake(deps) {
|
|
|
219441
220013
|
};
|
|
219442
220014
|
}
|
|
219443
220015
|
//#endregion
|
|
220016
|
+
//#region src/reolink-discovery-map.ts
|
|
220017
|
+
/**
|
|
220018
|
+
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
220019
|
+
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
220020
|
+
*/
|
|
220021
|
+
function slugifyReolinkHost(host) {
|
|
220022
|
+
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
220023
|
+
}
|
|
220024
|
+
/**
|
|
220025
|
+
* Decide whether a discovered host is a device this provider already owns.
|
|
220026
|
+
*
|
|
220027
|
+
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
220028
|
+
* wins even when the host differs, because the address is the thing that moves.
|
|
220029
|
+
*/
|
|
220030
|
+
function findOnboardedMatch(discovered, onboarded) {
|
|
220031
|
+
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
220032
|
+
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
220033
|
+
if (byUid !== void 0) return byUid;
|
|
220034
|
+
}
|
|
220035
|
+
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
220036
|
+
}
|
|
220037
|
+
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
220038
|
+
var HUB_BAICHUAN_PORT = 9e3;
|
|
220039
|
+
/**
|
|
220040
|
+
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
220041
|
+
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
220042
|
+
* as a camera to add.
|
|
220043
|
+
*/
|
|
220044
|
+
function looksLikeReolinkHub(model) {
|
|
220045
|
+
if (model === void 0) return false;
|
|
220046
|
+
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
220047
|
+
}
|
|
220048
|
+
/**
|
|
220049
|
+
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
220050
|
+
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
220051
|
+
* creates hides this candidate through `originStableId`.
|
|
220052
|
+
*/
|
|
220053
|
+
function reolinkHubStableId(identity) {
|
|
220054
|
+
const uid = identity.uid?.trim() ?? "";
|
|
220055
|
+
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
220056
|
+
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
220057
|
+
}
|
|
220058
|
+
function isKnownHub(d, known) {
|
|
220059
|
+
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
220060
|
+
}
|
|
220061
|
+
/**
|
|
220062
|
+
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
220063
|
+
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
220064
|
+
* first responder for a host wins. Pure: no I/O.
|
|
220065
|
+
*
|
|
220066
|
+
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
220067
|
+
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
220068
|
+
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
220069
|
+
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
220070
|
+
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
220071
|
+
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
220072
|
+
* never a scan result.
|
|
220073
|
+
*
|
|
220074
|
+
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
220075
|
+
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
220076
|
+
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
220077
|
+
* in the first place, so a scan says so instead of offering it again.
|
|
220078
|
+
*/
|
|
220079
|
+
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
220080
|
+
const username = credentials.username?.trim() ?? "";
|
|
220081
|
+
const password = credentials.password ?? "";
|
|
220082
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
220083
|
+
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
220084
|
+
return [...byHost.values()].flatMap((d) => {
|
|
220085
|
+
const displayName = d.name ?? d.model ?? d.host;
|
|
220086
|
+
if (looksLikeReolinkHub(d.model)) {
|
|
220087
|
+
if (isKnownHub(d, hubIntegrations)) return [];
|
|
220088
|
+
return [{
|
|
220089
|
+
stableId: reolinkHubStableId(d),
|
|
220090
|
+
type: DeviceType.Gateway,
|
|
220091
|
+
suggestedName: displayName,
|
|
220092
|
+
prefilledConfig: {
|
|
220093
|
+
host: d.host,
|
|
220094
|
+
port: HUB_BAICHUAN_PORT,
|
|
220095
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220096
|
+
...username ? { username } : {}
|
|
220097
|
+
},
|
|
220098
|
+
setupIntegration: true
|
|
220099
|
+
}];
|
|
220100
|
+
}
|
|
220101
|
+
const match = findOnboardedMatch(d, onboarded);
|
|
220102
|
+
return [{
|
|
220103
|
+
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
220104
|
+
type: DeviceType.Camera,
|
|
220105
|
+
suggestedName: displayName,
|
|
220106
|
+
prefilledConfig: {
|
|
220107
|
+
name: displayName,
|
|
220108
|
+
host: d.host,
|
|
220109
|
+
transport: "auto",
|
|
220110
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220111
|
+
...username ? { username } : {},
|
|
220112
|
+
...password ? { password } : {}
|
|
220113
|
+
},
|
|
220114
|
+
...match !== void 0 ? {
|
|
220115
|
+
alreadyOnboarded: true,
|
|
220116
|
+
onboardedDeviceId: match.deviceId,
|
|
220117
|
+
onboardedName: match.name
|
|
220118
|
+
} : {}
|
|
220119
|
+
}];
|
|
220120
|
+
});
|
|
220121
|
+
}
|
|
220122
|
+
//#endregion
|
|
220123
|
+
//#region src/reolink-hub-migration.ts
|
|
220124
|
+
/**
|
|
220125
|
+
* D985 — boot migration of the old Reolink Hub DEVICE into an INTEGRATION.
|
|
220126
|
+
*
|
|
220127
|
+
* A Hub used to be a device with the NVR's credentials in its config and its channels as
|
|
220128
|
+
* parented child cameras. Now the NVR is an integration (credentials in its keychain-backed
|
|
220129
|
+
* settings) and the channels are top-level cameras stamped with its id. For every Hub row:
|
|
220130
|
+
*
|
|
220131
|
+
* 1. create (or reuse) an integration named after it, config + password moved as settings;
|
|
220132
|
+
* 2. keep every child's deviceId and stableId, stamp it with the integrationId, un-parent it
|
|
220133
|
+
* and make sure its persisted blob can be restored on its own;
|
|
220134
|
+
* 3. only AFTER the children restored and the hub has none left, retire the hub by tombstone.
|
|
220135
|
+
*
|
|
220136
|
+
* Idempotent (a second run finds no Hub rows) and all-or-nothing per hub: a step that throws
|
|
220137
|
+
* leaves the hub device and its children's parentage exactly as they were, and the next boot
|
|
220138
|
+
* starts over. A locked secret is never read as an empty password and no replacement is minted.
|
|
220139
|
+
*/
|
|
220140
|
+
/** The marker an integration carries so a re-run reuses it instead of minting a second one. */
|
|
220141
|
+
var MIGRATED_FROM_HUB_KEY = "migratedFromHubDeviceId";
|
|
220142
|
+
function positivePort(value) {
|
|
220143
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
220144
|
+
}
|
|
220145
|
+
var CONNECTION_FIELDS = [
|
|
220146
|
+
"host",
|
|
220147
|
+
"port",
|
|
220148
|
+
"username",
|
|
220149
|
+
"transport"
|
|
220150
|
+
];
|
|
220151
|
+
function str(value) {
|
|
220152
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
220153
|
+
}
|
|
220154
|
+
function migratedChildConfig(child, hub, integrationId) {
|
|
220155
|
+
const filled = {};
|
|
220156
|
+
for (const key of CONNECTION_FIELDS) {
|
|
220157
|
+
const own = child[key];
|
|
220158
|
+
const inherited = hub[key];
|
|
220159
|
+
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
220160
|
+
}
|
|
220161
|
+
return {
|
|
220162
|
+
...child,
|
|
220163
|
+
...filled,
|
|
220164
|
+
integrationId
|
|
220165
|
+
};
|
|
220166
|
+
}
|
|
220167
|
+
async function migrateOneHub(hub, children, integrations, ports) {
|
|
220168
|
+
const read = readHubConnection(hub.config);
|
|
220169
|
+
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
220170
|
+
const { connection } = read;
|
|
220171
|
+
const uid = str(hub.config["uid"]);
|
|
220172
|
+
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
220173
|
+
addonId: ports.addonId,
|
|
220174
|
+
name: hub.name,
|
|
220175
|
+
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
220176
|
+
originStableId: reolinkHubStableId({
|
|
220177
|
+
...uid !== void 0 ? { uid } : {},
|
|
220178
|
+
host: connection.host
|
|
220179
|
+
}),
|
|
220180
|
+
settings: {
|
|
220181
|
+
host: connection.host,
|
|
220182
|
+
port: connection.port,
|
|
220183
|
+
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
220184
|
+
username: connection.username,
|
|
220185
|
+
password: connection.password,
|
|
220186
|
+
transport: connection.transport,
|
|
220187
|
+
...uid !== void 0 ? { uid } : {},
|
|
220188
|
+
debugGeneral: connection.debugGeneral,
|
|
220189
|
+
debugSocketLogs: connection.debugSocketLogs
|
|
220190
|
+
},
|
|
220191
|
+
secretKeys: ["password"]
|
|
220192
|
+
})).id;
|
|
220193
|
+
const rewritten = [];
|
|
220194
|
+
for (const child of children) {
|
|
220195
|
+
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
220196
|
+
await ports.persistConfig(child.stableId, config);
|
|
220197
|
+
await ports.setIntegrationId(child.id, integrationId);
|
|
220198
|
+
rewritten.push({
|
|
220199
|
+
...child,
|
|
220200
|
+
config,
|
|
220201
|
+
parentDeviceId: null
|
|
220202
|
+
});
|
|
220203
|
+
}
|
|
220204
|
+
return {
|
|
220205
|
+
rows: rewritten,
|
|
220206
|
+
retirement: {
|
|
220207
|
+
hubDeviceId: hub.id,
|
|
220208
|
+
hubName: hub.name,
|
|
220209
|
+
integrationId
|
|
220210
|
+
}
|
|
220211
|
+
};
|
|
220212
|
+
}
|
|
220213
|
+
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
220214
|
+
async function planHubMigration(saved, ports) {
|
|
220215
|
+
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
220216
|
+
if (hubs.length === 0) return {
|
|
220217
|
+
rows: saved,
|
|
220218
|
+
retirements: []
|
|
220219
|
+
};
|
|
220220
|
+
let integrations;
|
|
220221
|
+
try {
|
|
220222
|
+
integrations = await ports.listIntegrations();
|
|
220223
|
+
} catch (err) {
|
|
220224
|
+
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
220225
|
+
return {
|
|
220226
|
+
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
220227
|
+
retirements: []
|
|
220228
|
+
};
|
|
220229
|
+
}
|
|
220230
|
+
const hubIds = new Set(hubs.map((h) => h.id));
|
|
220231
|
+
const rewrittenById = /* @__PURE__ */ new Map();
|
|
220232
|
+
const retirements = [];
|
|
220233
|
+
for (const hub of hubs) {
|
|
220234
|
+
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
220235
|
+
try {
|
|
220236
|
+
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
220237
|
+
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
220238
|
+
retirements.push(done.retirement);
|
|
220239
|
+
ports.logger.info("Reolink hub migrated to an integration", {
|
|
220240
|
+
tags: { deviceId: hub.id },
|
|
220241
|
+
meta: {
|
|
220242
|
+
integrationId: done.retirement.integrationId,
|
|
220243
|
+
children: children.length,
|
|
220244
|
+
name: hub.name
|
|
220245
|
+
}
|
|
220246
|
+
});
|
|
220247
|
+
} catch (err) {
|
|
220248
|
+
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
220249
|
+
tags: { deviceId: hub.id },
|
|
220250
|
+
meta: {
|
|
220251
|
+
error: errMsg$3(err),
|
|
220252
|
+
name: hub.name
|
|
220253
|
+
}
|
|
220254
|
+
});
|
|
220255
|
+
}
|
|
220256
|
+
}
|
|
220257
|
+
return {
|
|
220258
|
+
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
220259
|
+
retirements
|
|
220260
|
+
};
|
|
220261
|
+
}
|
|
220262
|
+
/**
|
|
220263
|
+
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
220264
|
+
* and named: removing it would cascade the camera away with it.
|
|
220265
|
+
*/
|
|
220266
|
+
async function retireMigratedHubs(retirements, ports) {
|
|
220267
|
+
let retired = 0;
|
|
220268
|
+
for (const r of retirements) try {
|
|
220269
|
+
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
220270
|
+
if (remaining.length > 0) {
|
|
220271
|
+
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
220272
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220273
|
+
meta: {
|
|
220274
|
+
remaining: [...remaining],
|
|
220275
|
+
integrationId: r.integrationId
|
|
220276
|
+
}
|
|
220277
|
+
});
|
|
220278
|
+
continue;
|
|
220279
|
+
}
|
|
220280
|
+
await ports.removeDevice(r.hubDeviceId);
|
|
220281
|
+
retired++;
|
|
220282
|
+
ports.logger.info("Reolink hub device retired", {
|
|
220283
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220284
|
+
meta: { integrationId: r.integrationId }
|
|
220285
|
+
});
|
|
220286
|
+
} catch (err) {
|
|
220287
|
+
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
220288
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220289
|
+
meta: { error: errMsg$3(err) }
|
|
220290
|
+
});
|
|
220291
|
+
}
|
|
220292
|
+
return retired;
|
|
220293
|
+
}
|
|
220294
|
+
//#endregion
|
|
219444
220295
|
//#region src/migrations/drop-webhook-intake-settings.ts
|
|
219445
220296
|
/**
|
|
219446
220297
|
* The camera webhook is a plane of the addon's HTTP data plane, not a private listener
|
|
@@ -219565,6 +220416,18 @@ var REOLINK_MIGRATIONS = [{
|
|
|
219565
220416
|
},
|
|
219566
220417
|
plan: planDropWebhookIntakeSettings
|
|
219567
220418
|
}];
|
|
220419
|
+
/**
|
|
220420
|
+
* Every persisted camera that names no integration is claimed to the addon's default one. The
|
|
220421
|
+
* integration a hub migration created is never the default, so it is not eligible. The integration
|
|
220422
|
+
* settings themselves stay where they are until this addon's own wave moves them.
|
|
220423
|
+
*/
|
|
220424
|
+
var REOLINK_CLAIM_MIGRATION = claimDevicesMigration({
|
|
220425
|
+
addonId: "provider-reolink",
|
|
220426
|
+
date: "2026-11-04",
|
|
220427
|
+
seq: 3,
|
|
220428
|
+
dependsOn: [],
|
|
220429
|
+
eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
|
|
220430
|
+
});
|
|
219568
220431
|
//#endregion
|
|
219569
220432
|
//#region src/reolink-adoption-provider.ts
|
|
219570
220433
|
var DEVICES_FILTER = {
|
|
@@ -219730,113 +220593,6 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219730
220593
|
};
|
|
219731
220594
|
}
|
|
219732
220595
|
//#endregion
|
|
219733
|
-
//#region src/reolink-discovery-map.ts
|
|
219734
|
-
/**
|
|
219735
|
-
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
219736
|
-
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
219737
|
-
*/
|
|
219738
|
-
function slugifyReolinkHost(host) {
|
|
219739
|
-
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
219740
|
-
}
|
|
219741
|
-
/**
|
|
219742
|
-
* Decide whether a discovered host is a device this provider already owns.
|
|
219743
|
-
*
|
|
219744
|
-
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
219745
|
-
* wins even when the host differs, because the address is the thing that moves.
|
|
219746
|
-
*/
|
|
219747
|
-
function findOnboardedMatch(discovered, onboarded) {
|
|
219748
|
-
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
219749
|
-
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
219750
|
-
if (byUid !== void 0) return byUid;
|
|
219751
|
-
}
|
|
219752
|
-
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
219753
|
-
}
|
|
219754
|
-
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
219755
|
-
var HUB_BAICHUAN_PORT = 9e3;
|
|
219756
|
-
/**
|
|
219757
|
-
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
219758
|
-
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
219759
|
-
* as a camera to add.
|
|
219760
|
-
*/
|
|
219761
|
-
function looksLikeReolinkHub(model) {
|
|
219762
|
-
if (model === void 0) return false;
|
|
219763
|
-
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
219764
|
-
}
|
|
219765
|
-
/**
|
|
219766
|
-
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
219767
|
-
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
219768
|
-
* creates hides this candidate through `originStableId`.
|
|
219769
|
-
*/
|
|
219770
|
-
function reolinkHubStableId(identity) {
|
|
219771
|
-
const uid = identity.uid?.trim() ?? "";
|
|
219772
|
-
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
219773
|
-
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
219774
|
-
}
|
|
219775
|
-
function isKnownHub(d, known) {
|
|
219776
|
-
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
219777
|
-
}
|
|
219778
|
-
/**
|
|
219779
|
-
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
219780
|
-
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
219781
|
-
* first responder for a host wins. Pure: no I/O.
|
|
219782
|
-
*
|
|
219783
|
-
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
219784
|
-
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
219785
|
-
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
219786
|
-
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
219787
|
-
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
219788
|
-
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
219789
|
-
* never a scan result.
|
|
219790
|
-
*
|
|
219791
|
-
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
219792
|
-
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
219793
|
-
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
219794
|
-
* in the first place, so a scan says so instead of offering it again.
|
|
219795
|
-
*/
|
|
219796
|
-
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
219797
|
-
const username = credentials.username?.trim() ?? "";
|
|
219798
|
-
const password = credentials.password ?? "";
|
|
219799
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
219800
|
-
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
219801
|
-
return [...byHost.values()].flatMap((d) => {
|
|
219802
|
-
const displayName = d.name ?? d.model ?? d.host;
|
|
219803
|
-
if (looksLikeReolinkHub(d.model)) {
|
|
219804
|
-
if (isKnownHub(d, hubIntegrations)) return [];
|
|
219805
|
-
return [{
|
|
219806
|
-
stableId: reolinkHubStableId(d),
|
|
219807
|
-
type: DeviceType.Gateway,
|
|
219808
|
-
suggestedName: displayName,
|
|
219809
|
-
prefilledConfig: {
|
|
219810
|
-
host: d.host,
|
|
219811
|
-
port: HUB_BAICHUAN_PORT,
|
|
219812
|
-
...d.uid ? { uid: d.uid } : {},
|
|
219813
|
-
...username ? { username } : {}
|
|
219814
|
-
},
|
|
219815
|
-
setupIntegration: true
|
|
219816
|
-
}];
|
|
219817
|
-
}
|
|
219818
|
-
const match = findOnboardedMatch(d, onboarded);
|
|
219819
|
-
return [{
|
|
219820
|
-
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
219821
|
-
type: DeviceType.Camera,
|
|
219822
|
-
suggestedName: displayName,
|
|
219823
|
-
prefilledConfig: {
|
|
219824
|
-
name: displayName,
|
|
219825
|
-
host: d.host,
|
|
219826
|
-
transport: "auto",
|
|
219827
|
-
...d.uid ? { uid: d.uid } : {},
|
|
219828
|
-
...username ? { username } : {},
|
|
219829
|
-
...password ? { password } : {}
|
|
219830
|
-
},
|
|
219831
|
-
...match !== void 0 ? {
|
|
219832
|
-
alreadyOnboarded: true,
|
|
219833
|
-
onboardedDeviceId: match.deviceId,
|
|
219834
|
-
onboardedName: match.name
|
|
219835
|
-
} : {}
|
|
219836
|
-
}];
|
|
219837
|
-
});
|
|
219838
|
-
}
|
|
219839
|
-
//#endregion
|
|
219840
220596
|
//#region src/reolink-gateway-device.ts
|
|
219841
220597
|
var reolinkGatewayDeviceSchema = object({
|
|
219842
220598
|
integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
|
|
@@ -219993,158 +220749,6 @@ function createReolinkHubManager(deps, registry) {
|
|
|
219993
220749
|
connect: connectReolinkHub
|
|
219994
220750
|
});
|
|
219995
220751
|
}
|
|
219996
|
-
function positivePort(value) {
|
|
219997
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
219998
|
-
}
|
|
219999
|
-
var CONNECTION_FIELDS = [
|
|
220000
|
-
"host",
|
|
220001
|
-
"port",
|
|
220002
|
-
"username",
|
|
220003
|
-
"transport"
|
|
220004
|
-
];
|
|
220005
|
-
function str(value) {
|
|
220006
|
-
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
220007
|
-
}
|
|
220008
|
-
function migratedChildConfig(child, hub, integrationId) {
|
|
220009
|
-
const filled = {};
|
|
220010
|
-
for (const key of CONNECTION_FIELDS) {
|
|
220011
|
-
const own = child[key];
|
|
220012
|
-
const inherited = hub[key];
|
|
220013
|
-
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
220014
|
-
}
|
|
220015
|
-
return {
|
|
220016
|
-
...child,
|
|
220017
|
-
...filled,
|
|
220018
|
-
integrationId
|
|
220019
|
-
};
|
|
220020
|
-
}
|
|
220021
|
-
async function migrateOneHub(hub, children, integrations, ports) {
|
|
220022
|
-
const read = readHubConnection(hub.config);
|
|
220023
|
-
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
220024
|
-
const { connection } = read;
|
|
220025
|
-
const uid = str(hub.config["uid"]);
|
|
220026
|
-
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
220027
|
-
addonId: ports.addonId,
|
|
220028
|
-
name: hub.name,
|
|
220029
|
-
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
220030
|
-
originStableId: reolinkHubStableId({
|
|
220031
|
-
...uid !== void 0 ? { uid } : {},
|
|
220032
|
-
host: connection.host
|
|
220033
|
-
}),
|
|
220034
|
-
settings: {
|
|
220035
|
-
host: connection.host,
|
|
220036
|
-
port: connection.port,
|
|
220037
|
-
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
220038
|
-
username: connection.username,
|
|
220039
|
-
password: connection.password,
|
|
220040
|
-
transport: connection.transport,
|
|
220041
|
-
...uid !== void 0 ? { uid } : {},
|
|
220042
|
-
debugGeneral: connection.debugGeneral,
|
|
220043
|
-
debugSocketLogs: connection.debugSocketLogs
|
|
220044
|
-
},
|
|
220045
|
-
secretKeys: ["password"]
|
|
220046
|
-
})).id;
|
|
220047
|
-
const rewritten = [];
|
|
220048
|
-
for (const child of children) {
|
|
220049
|
-
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
220050
|
-
await ports.persistConfig(child.stableId, config);
|
|
220051
|
-
await ports.setIntegrationId(child.id, integrationId);
|
|
220052
|
-
rewritten.push({
|
|
220053
|
-
...child,
|
|
220054
|
-
config,
|
|
220055
|
-
parentDeviceId: null
|
|
220056
|
-
});
|
|
220057
|
-
}
|
|
220058
|
-
return {
|
|
220059
|
-
rows: rewritten,
|
|
220060
|
-
retirement: {
|
|
220061
|
-
hubDeviceId: hub.id,
|
|
220062
|
-
hubName: hub.name,
|
|
220063
|
-
integrationId
|
|
220064
|
-
}
|
|
220065
|
-
};
|
|
220066
|
-
}
|
|
220067
|
-
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
220068
|
-
async function planHubMigration(saved, ports) {
|
|
220069
|
-
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
220070
|
-
if (hubs.length === 0) return {
|
|
220071
|
-
rows: saved,
|
|
220072
|
-
retirements: []
|
|
220073
|
-
};
|
|
220074
|
-
let integrations;
|
|
220075
|
-
try {
|
|
220076
|
-
integrations = await ports.listIntegrations();
|
|
220077
|
-
} catch (err) {
|
|
220078
|
-
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
220079
|
-
return {
|
|
220080
|
-
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
220081
|
-
retirements: []
|
|
220082
|
-
};
|
|
220083
|
-
}
|
|
220084
|
-
const hubIds = new Set(hubs.map((h) => h.id));
|
|
220085
|
-
const rewrittenById = /* @__PURE__ */ new Map();
|
|
220086
|
-
const retirements = [];
|
|
220087
|
-
for (const hub of hubs) {
|
|
220088
|
-
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
220089
|
-
try {
|
|
220090
|
-
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
220091
|
-
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
220092
|
-
retirements.push(done.retirement);
|
|
220093
|
-
ports.logger.info("Reolink hub migrated to an integration", {
|
|
220094
|
-
tags: { deviceId: hub.id },
|
|
220095
|
-
meta: {
|
|
220096
|
-
integrationId: done.retirement.integrationId,
|
|
220097
|
-
children: children.length,
|
|
220098
|
-
name: hub.name
|
|
220099
|
-
}
|
|
220100
|
-
});
|
|
220101
|
-
} catch (err) {
|
|
220102
|
-
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
220103
|
-
tags: { deviceId: hub.id },
|
|
220104
|
-
meta: {
|
|
220105
|
-
error: errMsg$3(err),
|
|
220106
|
-
name: hub.name
|
|
220107
|
-
}
|
|
220108
|
-
});
|
|
220109
|
-
}
|
|
220110
|
-
}
|
|
220111
|
-
return {
|
|
220112
|
-
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
220113
|
-
retirements
|
|
220114
|
-
};
|
|
220115
|
-
}
|
|
220116
|
-
/**
|
|
220117
|
-
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
220118
|
-
* and named: removing it would cascade the camera away with it.
|
|
220119
|
-
*/
|
|
220120
|
-
async function retireMigratedHubs(retirements, ports) {
|
|
220121
|
-
let retired = 0;
|
|
220122
|
-
for (const r of retirements) try {
|
|
220123
|
-
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
220124
|
-
if (remaining.length > 0) {
|
|
220125
|
-
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
220126
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220127
|
-
meta: {
|
|
220128
|
-
remaining: [...remaining],
|
|
220129
|
-
integrationId: r.integrationId
|
|
220130
|
-
}
|
|
220131
|
-
});
|
|
220132
|
-
continue;
|
|
220133
|
-
}
|
|
220134
|
-
await ports.removeDevice(r.hubDeviceId);
|
|
220135
|
-
retired++;
|
|
220136
|
-
ports.logger.info("Reolink hub device retired", {
|
|
220137
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220138
|
-
meta: { integrationId: r.integrationId }
|
|
220139
|
-
});
|
|
220140
|
-
} catch (err) {
|
|
220141
|
-
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
220142
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220143
|
-
meta: { error: errMsg$3(err) }
|
|
220144
|
-
});
|
|
220145
|
-
}
|
|
220146
|
-
return retired;
|
|
220147
|
-
}
|
|
220148
220752
|
//#endregion
|
|
220149
220753
|
//#region src/reolink-subnet-scan.ts
|
|
220150
220754
|
/**
|
|
@@ -220963,7 +221567,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220963
221567
|
this.wireClipCatalogRefreshers();
|
|
220964
221568
|
await this.serveClipPlanes(this.clipService);
|
|
220965
221569
|
await this.serveWebhookPlane();
|
|
220966
|
-
await this.runMigrations(defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts())
|
|
221570
|
+
await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations([REOLINK_CLAIM_MIGRATION], claimDevicesPortsOver({
|
|
221571
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
221572
|
+
info: (message) => this.ctx.logger.info(message),
|
|
221573
|
+
warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
|
|
221574
|
+
}))]);
|
|
220967
221575
|
this.wireEmailBridge();
|
|
220968
221576
|
const wireSleepPushResolvers = (event, via) => {
|
|
220969
221577
|
const data = event.data;
|