@camstack/addon-provider-reolink 1.2.221 → 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 +6 -0
- package/dist/addon.js +944 -552
- package/dist/addon.mjs +944 -552
- package/package.json +2 -2
package/dist/addon.mjs
CHANGED
|
@@ -106536,7 +106536,7 @@ var ZodIssueCode = {
|
|
|
106536
106536
|
var ZodFirstPartyTypeKind;
|
|
106537
106537
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106538
106538
|
//#endregion
|
|
106539
|
-
//#region ../types/dist/sleep-
|
|
106539
|
+
//#region ../types/dist/sleep-Cv4Zmie9.mjs
|
|
106540
106540
|
/**
|
|
106541
106541
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106542
106542
|
* window to float samples (D455).
|
|
@@ -107133,7 +107133,7 @@ var NOT_RUN = {
|
|
|
107133
107133
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
107134
107134
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
107135
107135
|
*/
|
|
107136
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
107136
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
107137
107137
|
const first = migrations[0];
|
|
107138
107138
|
if (first === void 0) return NOT_RUN;
|
|
107139
107139
|
const owner = first.meta.owner;
|
|
@@ -107162,7 +107162,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107162
107162
|
else if (status === "blocked") blocked.push(id);
|
|
107163
107163
|
else skipped.push(id);
|
|
107164
107164
|
};
|
|
107165
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
107165
107166
|
for (const step of declaration.steps) {
|
|
107167
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
107166
107168
|
const bound = byId.get(step.id);
|
|
107167
107169
|
if (bound === void 0 || step.action === "skip") {
|
|
107168
107170
|
skipped.push(step.id);
|
|
@@ -108280,14 +108282,18 @@ var BaseAddon = class {
|
|
|
108280
108282
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
108281
108283
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
108282
108284
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
108285
|
+
*
|
|
108286
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
108287
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
108288
|
+
* omitted applied ids "newer").
|
|
108283
108289
|
*/
|
|
108284
|
-
async runMigrations(migrations) {
|
|
108290
|
+
async runMigrations(migrations, options) {
|
|
108285
108291
|
const api = this.ctx.api.migrations;
|
|
108286
108292
|
const report = await runAddonMigrations({
|
|
108287
108293
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
108288
108294
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
108289
108295
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
108290
|
-
}, migrations, this.ctx.logger);
|
|
108296
|
+
}, migrations, this.ctx.logger, options);
|
|
108291
108297
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
108292
108298
|
return report;
|
|
108293
108299
|
}
|
|
@@ -108628,6 +108634,10 @@ var BaseAddon = class {
|
|
|
108628
108634
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
108629
108635
|
const patch = {};
|
|
108630
108636
|
for (const key of keys) {
|
|
108637
|
+
if (tree.get(key)?.kind === "array") {
|
|
108638
|
+
patch[key] = [];
|
|
108639
|
+
continue;
|
|
108640
|
+
}
|
|
108631
108641
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
108632
108642
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
108633
108643
|
}
|
|
@@ -109354,7 +109364,8 @@ function isZodSecret(schema) {
|
|
|
109354
109364
|
}
|
|
109355
109365
|
/**
|
|
109356
109366
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
109357
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
109367
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
109368
|
+
* map" (`applianceSecrets{}`). A declared
|
|
109358
109369
|
* object or array is reported as one path and not descended into — everything
|
|
109359
109370
|
* under it is secret.
|
|
109360
109371
|
*/
|
|
@@ -109365,6 +109376,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
109365
109376
|
const def = defOf(layer);
|
|
109366
109377
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
109367
109378
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
109379
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
109368
109380
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
109369
109381
|
else if (def.type === "intersection") {
|
|
109370
109382
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -112243,7 +112255,7 @@ function sleep(ms) {
|
|
|
112243
112255
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
112244
112256
|
}
|
|
112245
112257
|
//#endregion
|
|
112246
|
-
//#region ../types/dist/composition-
|
|
112258
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
112247
112259
|
/**
|
|
112248
112260
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
112249
112261
|
*
|
|
@@ -113006,38 +113018,6 @@ function validateExpressionSource(src) {
|
|
|
113006
113018
|
return null;
|
|
113007
113019
|
}
|
|
113008
113020
|
/**
|
|
113009
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
113010
|
-
*
|
|
113011
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
113012
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
113013
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
113014
|
-
*
|
|
113015
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
113016
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
113017
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
113018
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
113019
|
-
* can move later without moving the others.
|
|
113020
|
-
*/
|
|
113021
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
113022
|
-
var APPLIANCE_KINDS = [
|
|
113023
|
-
"washer",
|
|
113024
|
-
"dryer",
|
|
113025
|
-
"dishwasher"
|
|
113026
|
-
];
|
|
113027
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
113028
|
-
/**
|
|
113029
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
113030
|
-
*
|
|
113031
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
113032
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
113033
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
113034
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
113035
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
113036
|
-
* the smallest radiator.
|
|
113037
|
-
*/
|
|
113038
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
113039
|
-
DeviceType.Heater;
|
|
113040
|
-
/**
|
|
113041
113021
|
* Statistics and integrals over a history read (D944).
|
|
113042
113022
|
*
|
|
113043
113023
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -113181,34 +113161,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
113181
113161
|
/** Actuator groups only. Readings leave this unset. */
|
|
113182
113162
|
active: ActiveCriterionSchema.optional()
|
|
113183
113163
|
}),
|
|
113184
|
-
object({
|
|
113185
|
-
kind: literal("appliance"),
|
|
113186
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
113187
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
113188
|
-
thresholdWatts: number().positive(),
|
|
113189
|
-
startDwellMs: number().int().nonnegative(),
|
|
113190
|
-
stopDwellMs: number().int().nonnegative(),
|
|
113191
|
-
member: HelperMemberSchema,
|
|
113192
|
-
/**
|
|
113193
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
113194
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
113195
|
-
* that plug (power-source link of a NEW-target composition).
|
|
113196
|
-
*/
|
|
113197
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
113198
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
113199
|
-
offSeconds: number().optional()
|
|
113200
|
-
}),
|
|
113201
|
-
object({
|
|
113202
|
-
kind: literal("heater"),
|
|
113203
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
113204
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
113205
|
-
plug: HelperMemberSchema,
|
|
113206
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
113207
|
-
meter: HelperMemberSchema.optional(),
|
|
113208
|
-
thresholdWatts: number().positive(),
|
|
113209
|
-
startDwellMs: number().int().nonnegative(),
|
|
113210
|
-
stopDwellMs: number().int().nonnegative()
|
|
113211
|
-
}),
|
|
113212
113164
|
object({
|
|
113213
113165
|
kind: literal("statistic"),
|
|
113214
113166
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -113432,7 +113384,7 @@ var CompositionSchema = object({
|
|
|
113432
113384
|
kind: literal("existing"),
|
|
113433
113385
|
device: CompositionSourceRefSchema
|
|
113434
113386
|
})]),
|
|
113435
|
-
features: array(CompositionFeatureSchema).min(
|
|
113387
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
113436
113388
|
/**
|
|
113437
113389
|
* Present when the block is a helper template. Absent on a free
|
|
113438
113390
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -114736,7 +114688,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
114736
114688
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
114737
114689
|
* catalogue lists the id (a newer database).
|
|
114738
114690
|
*/
|
|
114739
|
-
meta: MigrationMetaSchema.optional()
|
|
114691
|
+
meta: MigrationMetaSchema.optional(),
|
|
114692
|
+
/**
|
|
114693
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
114694
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
114695
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
114696
|
+
*/
|
|
114697
|
+
ownerNotLoaded: boolean().optional()
|
|
114740
114698
|
});
|
|
114741
114699
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
114742
114700
|
var MigrationPendingEntrySchema = object({
|
|
@@ -114769,6 +114727,146 @@ var MigrationOwnerStepSchema = object({
|
|
|
114769
114727
|
/** The ledger state the engine found: `pending` has no row yet. */
|
|
114770
114728
|
state: union([literal("pending"), MigrationStatusSchema])
|
|
114771
114729
|
});
|
|
114730
|
+
function ordinalOf(id) {
|
|
114731
|
+
const digits = id.replace(/\D+/g, "");
|
|
114732
|
+
return digits.length > 0 ? Number(digits) : Number.POSITIVE_INFINITY;
|
|
114733
|
+
}
|
|
114734
|
+
/** The lowest-numbered eligible integration, or null. */
|
|
114735
|
+
function defaultIntegrationOf(integrations, eligible) {
|
|
114736
|
+
return [...eligible === void 0 ? integrations : integrations.filter(eligible)].sort((a, b) => ordinalOf(a.id) - ordinalOf(b.id) || (a.id < b.id ? -1 : 1))[0] ?? null;
|
|
114737
|
+
}
|
|
114738
|
+
function unstamped(device) {
|
|
114739
|
+
return device.integrationId === void 0 || device.integrationId === null || device.integrationId === "";
|
|
114740
|
+
}
|
|
114741
|
+
/**
|
|
114742
|
+
* An allocation reservation is not a device (D376): `allocateDeviceId` writes a row of type
|
|
114743
|
+
* `generic`, named by its stable id, that is not registered and holds no configuration. The hub
|
|
114744
|
+
* listing does not carry the `registered` bit, so the shape picks the candidates and the missing
|
|
114745
|
+
* configuration decides.
|
|
114746
|
+
*/
|
|
114747
|
+
async function isReservation(ports, device) {
|
|
114748
|
+
if (device.type !== "generic" || device.name === void 0 || device.name !== device.stableId) return false;
|
|
114749
|
+
return !await ports.hasConfig(device.id);
|
|
114750
|
+
}
|
|
114751
|
+
async function claimDevicesToDefaultIntegration(ports, spec) {
|
|
114752
|
+
const all = await ports.listDevices(spec.addonId);
|
|
114753
|
+
const byId = new Map(all.map((d) => [d.id, d]));
|
|
114754
|
+
const reservations = /* @__PURE__ */ new Set();
|
|
114755
|
+
for (const device of all) if (unstamped(device) && await isReservation(ports, device)) reservations.add(device.id);
|
|
114756
|
+
const orphans = all.filter((d) => unstamped(d) && !reservations.has(d.id));
|
|
114757
|
+
const summaryBase = { reservations: reservations.size };
|
|
114758
|
+
if (orphans.length === 0) return {
|
|
114759
|
+
outcome: "already-done",
|
|
114760
|
+
summary: {
|
|
114761
|
+
claimed: 0,
|
|
114762
|
+
unresolved: 0,
|
|
114763
|
+
...summaryBase
|
|
114764
|
+
}
|
|
114765
|
+
};
|
|
114766
|
+
const fallback = defaultIntegrationOf(await ports.listIntegrations(spec.addonId), spec.eligible);
|
|
114767
|
+
const plan = /* @__PURE__ */ new Map();
|
|
114768
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
114769
|
+
const failed = /* @__PURE__ */ new Set();
|
|
114770
|
+
const resolve = (device, seen) => {
|
|
114771
|
+
if (!unstamped(device)) return device.integrationId ?? null;
|
|
114772
|
+
const planned = plan.get(device.id);
|
|
114773
|
+
if (planned !== void 0) return planned;
|
|
114774
|
+
if (failed.has(device.id)) return null;
|
|
114775
|
+
let target = null;
|
|
114776
|
+
const parentId = device.parentDeviceId ?? null;
|
|
114777
|
+
if (parentId === null) target = fallback?.id ?? null;
|
|
114778
|
+
else {
|
|
114779
|
+
const parent = byId.get(parentId);
|
|
114780
|
+
if (parent !== void 0 && !reservations.has(parentId) && !seen.has(parentId)) target = resolve(parent, new Set([...seen, device.id]));
|
|
114781
|
+
}
|
|
114782
|
+
if (target === null) failed.add(device.id);
|
|
114783
|
+
else plan.set(device.id, target);
|
|
114784
|
+
return target;
|
|
114785
|
+
};
|
|
114786
|
+
if (orphans.some((d) => (d.parentDeviceId ?? null) === null) && fallback === null) throw new Error(`${orphans.length} device(s) of '${spec.addonId}' carry no integration and the addon has no integration to claim them to`);
|
|
114787
|
+
for (const device of orphans) if (resolve(device, new Set([device.id])) === null) unresolved.add(device.id);
|
|
114788
|
+
for (const id of unresolved) ports.warnDevice(`device ${id} of '${spec.addonId}' carries no integration and its owner chain does not resolve to one: left unstamped`, id);
|
|
114789
|
+
for (const [deviceId, integrationId] of plan) await ports.setIntegrationId(deviceId, integrationId);
|
|
114790
|
+
const wrong = [];
|
|
114791
|
+
for (const [deviceId, integrationId] of plan) if (await ports.readIntegrationId(deviceId) !== integrationId) wrong.push(deviceId);
|
|
114792
|
+
if (wrong.length > 0) throw new Error(`device(s) ${wrong.join(", ")} did not read back their integration after the claim`);
|
|
114793
|
+
const claimed = plan.size;
|
|
114794
|
+
ports.info(`${claimed} device(s) of '${spec.addonId}' claimed to their owner's integration or the default; ${unresolved.size} unresolved, ${reservations.size} reservation(s) skipped`);
|
|
114795
|
+
return {
|
|
114796
|
+
outcome: claimed > 0 ? "done" : "already-done",
|
|
114797
|
+
summary: {
|
|
114798
|
+
claimed,
|
|
114799
|
+
unresolved: unresolved.size,
|
|
114800
|
+
...summaryBase
|
|
114801
|
+
}
|
|
114802
|
+
};
|
|
114803
|
+
}
|
|
114804
|
+
/** The hub side of the device claim, over `ctx.api`. */
|
|
114805
|
+
function claimDevicesPortsOver(deps) {
|
|
114806
|
+
return {
|
|
114807
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
114808
|
+
id: i.id,
|
|
114809
|
+
name: i.name,
|
|
114810
|
+
info: i.info ?? {}
|
|
114811
|
+
})),
|
|
114812
|
+
listDevices: (addonId) => deps.api.listDevices(addonId),
|
|
114813
|
+
hasConfig: (deviceId) => deps.api.readDeviceHasConfig(deviceId),
|
|
114814
|
+
setIntegrationId: (deviceId, integrationId) => deps.api.setDeviceIntegrationId(deviceId, integrationId),
|
|
114815
|
+
readIntegrationId: (deviceId) => deps.api.readDeviceIntegrationId(deviceId),
|
|
114816
|
+
info: deps.info,
|
|
114817
|
+
warnDevice: deps.warnDevice
|
|
114818
|
+
};
|
|
114819
|
+
}
|
|
114820
|
+
function defaultIntegrationMigrationIds(addonId, date) {
|
|
114821
|
+
return {
|
|
114822
|
+
rowsToRecord: `${addonId}/${date}-integration-rows-to-record`,
|
|
114823
|
+
claimDevices: `${addonId}/${date}-claim-devices-to-default-integration`
|
|
114824
|
+
};
|
|
114825
|
+
}
|
|
114826
|
+
/**
|
|
114827
|
+
* The device claim alone, for an addon that keeps its integration settings where they are for now
|
|
114828
|
+
* (its own wave moves them) but whose persisted devices must name their default integration.
|
|
114829
|
+
*/
|
|
114830
|
+
function claimDevicesMigration(spec) {
|
|
114831
|
+
return {
|
|
114832
|
+
meta: {
|
|
114833
|
+
id: defaultIntegrationMigrationIds(spec.addonId, spec.date).claimDevices,
|
|
114834
|
+
owner: spec.addonId,
|
|
114835
|
+
level: "addon-state",
|
|
114836
|
+
seq: spec.seq,
|
|
114837
|
+
dependsOn: [...spec.dependsOn],
|
|
114838
|
+
store: "settings",
|
|
114839
|
+
scope: "cluster",
|
|
114840
|
+
onFailure: "tolerate",
|
|
114841
|
+
bodyKind: "procedure",
|
|
114842
|
+
description: "Claim the persisted devices that carry no integration to the addon's default integration, keeping their device and stable ids",
|
|
114843
|
+
fixtureMark: "a device config field the claim does not know"
|
|
114844
|
+
},
|
|
114845
|
+
procedure: (ports) => claimDevicesToDefaultIntegration(ports, {
|
|
114846
|
+
addonId: spec.addonId,
|
|
114847
|
+
...spec.eligible !== void 0 ? { eligible: spec.eligible } : {}
|
|
114848
|
+
})
|
|
114849
|
+
};
|
|
114850
|
+
}
|
|
114851
|
+
function defaultIntegrationApiOver(api) {
|
|
114852
|
+
return {
|
|
114853
|
+
list: () => api.integrations.list.query(),
|
|
114854
|
+
getSettings: (input) => api.integrations.getSettings.query(input),
|
|
114855
|
+
clearSettings: (input) => api.integrations.clearSettings.mutate(input),
|
|
114856
|
+
listDevices: (addonId) => api.deviceManager.listAll.query({
|
|
114857
|
+
addonId,
|
|
114858
|
+
projection: "slim"
|
|
114859
|
+
}),
|
|
114860
|
+
setDeviceIntegrationId: async (deviceId, integrationId) => {
|
|
114861
|
+
await api.deviceManager.setIntegrationId.mutate({
|
|
114862
|
+
deviceId,
|
|
114863
|
+
integrationId
|
|
114864
|
+
});
|
|
114865
|
+
},
|
|
114866
|
+
readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
|
|
114867
|
+
readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
|
|
114868
|
+
};
|
|
114869
|
+
}
|
|
114772
114870
|
/**
|
|
114773
114871
|
* The trainer contract — what the hub hands the cloud trainer and what it
|
|
114774
114872
|
* gets back (cloud training, D805+).
|
|
@@ -150744,117 +150842,6 @@ function actuator(input) {
|
|
|
150744
150842
|
};
|
|
150745
150843
|
}
|
|
150746
150844
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
150747
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
150748
|
-
"no-power-source",
|
|
150749
|
-
"switch-missing",
|
|
150750
|
-
"switch-offline",
|
|
150751
|
-
"switch-unknown",
|
|
150752
|
-
"restart-in-progress",
|
|
150753
|
-
"switch-call-failed"
|
|
150754
|
-
]);
|
|
150755
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
150756
|
-
accepted: literal(false),
|
|
150757
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
150758
|
-
/** One line for the operator, naming the switch. */
|
|
150759
|
-
message: string()
|
|
150760
|
-
})]);
|
|
150761
|
-
var PowerSourceInfoSchema = object({
|
|
150762
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
150763
|
-
linked: boolean(),
|
|
150764
|
-
switchDeviceId: number().nullable(),
|
|
150765
|
-
switchName: string().nullable(),
|
|
150766
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
150767
|
-
switchOn: boolean().nullable(),
|
|
150768
|
-
switchOnline: boolean().nullable(),
|
|
150769
|
-
restartInProgress: boolean(),
|
|
150770
|
-
offSeconds: number().int()
|
|
150771
|
-
});
|
|
150772
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
150773
|
-
var PowerSourceStateSchema = object({
|
|
150774
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
150775
|
-
switchOn: boolean().nullable(),
|
|
150776
|
-
offSeconds: number().int(),
|
|
150777
|
-
lastFetchedAt: number()
|
|
150778
|
-
});
|
|
150779
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
150780
|
-
DeviceType.Gateway,
|
|
150781
|
-
DeviceType.Hub,
|
|
150782
|
-
DeviceType.Camera,
|
|
150783
|
-
DeviceType.Sensor,
|
|
150784
|
-
DeviceType.MediaPlayer,
|
|
150785
|
-
DeviceType.Vacuum,
|
|
150786
|
-
DeviceType.Generic,
|
|
150787
|
-
DeviceType.Washer,
|
|
150788
|
-
DeviceType.Dryer,
|
|
150789
|
-
DeviceType.Dishwasher,
|
|
150790
|
-
DeviceType.PetFountain,
|
|
150791
|
-
DeviceType.Fan,
|
|
150792
|
-
DeviceType.Humidifier,
|
|
150793
|
-
DeviceType.Climate,
|
|
150794
|
-
DeviceType.WaterHeater
|
|
150795
|
-
];
|
|
150796
|
-
var powerSourceCustomize = {
|
|
150797
|
-
version: 1,
|
|
150798
|
-
targets: [{
|
|
150799
|
-
id: "switch",
|
|
150800
|
-
cardinality: "scalar",
|
|
150801
|
-
vocabulary: {
|
|
150802
|
-
cap: "power-source",
|
|
150803
|
-
stateField: "switchOn"
|
|
150804
|
-
},
|
|
150805
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150806
|
-
apply: "add",
|
|
150807
|
-
path: "switchOn",
|
|
150808
|
-
sources: [{
|
|
150809
|
-
kind: "switch",
|
|
150810
|
-
transforms: [{ kind: "identity" }]
|
|
150811
|
-
}],
|
|
150812
|
-
companions: [{
|
|
150813
|
-
kind: "constant",
|
|
150814
|
-
path: "offSeconds",
|
|
150815
|
-
value: 10,
|
|
150816
|
-
when: "add"
|
|
150817
|
-
}, {
|
|
150818
|
-
kind: "clock",
|
|
150819
|
-
path: "lastFetchedAt",
|
|
150820
|
-
when: "always"
|
|
150821
|
-
}],
|
|
150822
|
-
sourceReport: { status: "assignment-status" }
|
|
150823
|
-
}]
|
|
150824
|
-
};
|
|
150825
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
150826
|
-
var powerSourceCapability = {
|
|
150827
|
-
name: "power-source",
|
|
150828
|
-
scope: "device",
|
|
150829
|
-
mode: "singleton",
|
|
150830
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
150831
|
-
methods: {
|
|
150832
|
-
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }),
|
|
150833
|
-
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150834
|
-
kind: "mutation",
|
|
150835
|
-
auth: "admin"
|
|
150836
|
-
}),
|
|
150837
|
-
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150838
|
-
kind: "mutation",
|
|
150839
|
-
auth: "admin"
|
|
150840
|
-
}),
|
|
150841
|
-
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
150842
|
-
kind: "mutation",
|
|
150843
|
-
auth: "admin"
|
|
150844
|
-
})
|
|
150845
|
-
},
|
|
150846
|
-
runtimeState: PowerSourceStateSchema,
|
|
150847
|
-
customize: powerSourceCustomize,
|
|
150848
|
-
/**
|
|
150849
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
150850
|
-
* its next report; a restored value would outlive the reading it names.
|
|
150851
|
-
*
|
|
150852
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
150853
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
150854
|
-
*/
|
|
150855
|
-
durability: "session",
|
|
150856
|
-
history: "none"
|
|
150857
|
-
};
|
|
150858
150845
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
150859
150846
|
DeviceType.Camera;
|
|
150860
150847
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -150882,6 +150869,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
150882
150869
|
}).optional()
|
|
150883
150870
|
})) });
|
|
150884
150871
|
/**
|
|
150872
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
150873
|
+
* `z.infer`, they do not restate the shape.
|
|
150874
|
+
*/
|
|
150875
|
+
var OverrideSourceSchema = object({
|
|
150876
|
+
addonId: string().min(1),
|
|
150877
|
+
stableId: string().min(1),
|
|
150878
|
+
cap: string().min(1),
|
|
150879
|
+
fieldPath: string().min(1)
|
|
150880
|
+
});
|
|
150881
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
150882
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
150883
|
+
var ThresholdTransformDraftSchema = object({
|
|
150884
|
+
kind: literal("threshold"),
|
|
150885
|
+
comparison: _enum(["gte", "lte"]),
|
|
150886
|
+
threshold: number(),
|
|
150887
|
+
startDwellMs: number().nonnegative(),
|
|
150888
|
+
stopDwellMs: number().nonnegative()
|
|
150889
|
+
});
|
|
150890
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
150891
|
+
IdentityTransformDraftSchema,
|
|
150892
|
+
InvertTransformDraftSchema,
|
|
150893
|
+
ThresholdTransformDraftSchema,
|
|
150894
|
+
object({
|
|
150895
|
+
kind: literal("linear"),
|
|
150896
|
+
scale: number(),
|
|
150897
|
+
offset: number(),
|
|
150898
|
+
clampMin: number().optional(),
|
|
150899
|
+
clampMax: number().optional()
|
|
150900
|
+
}),
|
|
150901
|
+
object({
|
|
150902
|
+
kind: literal("enum-map"),
|
|
150903
|
+
mapping: record(string(), string().nullable())
|
|
150904
|
+
}),
|
|
150905
|
+
object({ kind: literal("number") })
|
|
150906
|
+
]);
|
|
150907
|
+
var ScalarOverrideDraftSchema = object({
|
|
150908
|
+
kind: literal("scalar"),
|
|
150909
|
+
cap: string().min(1),
|
|
150910
|
+
targetId: string().min(1),
|
|
150911
|
+
source: OverrideSourceSchema,
|
|
150912
|
+
transform: CustomizeTransformDraftSchema
|
|
150913
|
+
});
|
|
150914
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
150915
|
+
kind: literal("keyedArray"),
|
|
150916
|
+
cap: string().min(1),
|
|
150917
|
+
targetId: string().min(1),
|
|
150918
|
+
key: string().min(1),
|
|
150919
|
+
label: string().min(1),
|
|
150920
|
+
fieldPath: string().min(1),
|
|
150921
|
+
source: OverrideSourceSchema,
|
|
150922
|
+
transform: CustomizeTransformDraftSchema
|
|
150923
|
+
});
|
|
150924
|
+
var PulseOverrideDraftSchema = object({
|
|
150925
|
+
kind: literal("pulse"),
|
|
150926
|
+
cap: string().min(1),
|
|
150927
|
+
targetId: string().min(1),
|
|
150928
|
+
enabled: boolean(),
|
|
150929
|
+
debounceMs: number().nonnegative(),
|
|
150930
|
+
edges: array(_enum([
|
|
150931
|
+
"rising",
|
|
150932
|
+
"button",
|
|
150933
|
+
"native-event"
|
|
150934
|
+
])).min(1).optional(),
|
|
150935
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
150936
|
+
});
|
|
150937
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
150938
|
+
"in-addition",
|
|
150939
|
+
"confirm",
|
|
150940
|
+
"replace"
|
|
150941
|
+
]).optional() });
|
|
150942
|
+
discriminatedUnion("kind", [
|
|
150943
|
+
ScalarOverrideDraftSchema,
|
|
150944
|
+
KeyedArrayOverrideDraftSchema,
|
|
150945
|
+
PulseOverrideDraftSchema,
|
|
150946
|
+
object({
|
|
150947
|
+
kind: literal("span"),
|
|
150948
|
+
cap: string().min(1),
|
|
150949
|
+
targetId: string().min(1),
|
|
150950
|
+
timeoutMs: number().nonnegative(),
|
|
150951
|
+
cooldownMs: number().nonnegative().optional(),
|
|
150952
|
+
sources: array(SpanSourceDraftSchema)
|
|
150953
|
+
})
|
|
150954
|
+
]);
|
|
150955
|
+
object({
|
|
150956
|
+
targetId: string().min(1),
|
|
150957
|
+
configured: boolean(),
|
|
150958
|
+
source: _enum(["native", "assigned"]),
|
|
150959
|
+
state: _enum([
|
|
150960
|
+
"active",
|
|
150961
|
+
"stale",
|
|
150962
|
+
"released",
|
|
150963
|
+
"unavailable"
|
|
150964
|
+
]),
|
|
150965
|
+
reason: string().nullable()
|
|
150966
|
+
});
|
|
150967
|
+
/**
|
|
150968
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
150969
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
150970
|
+
*
|
|
150971
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
150972
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
150973
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
150974
|
+
*/
|
|
150975
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
150976
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
150977
|
+
"high",
|
|
150978
|
+
"medium",
|
|
150979
|
+
"low"
|
|
150980
|
+
]);
|
|
150981
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
150982
|
+
"no-history",
|
|
150983
|
+
"span-too-short",
|
|
150984
|
+
"truncated",
|
|
150985
|
+
"no-idle-floor",
|
|
150986
|
+
"no-runs",
|
|
150987
|
+
"no-separation"
|
|
150988
|
+
]);
|
|
150989
|
+
var RunningProposalReplaySchema = object({
|
|
150990
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
150991
|
+
intervals: number().int().nonnegative(),
|
|
150992
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
150993
|
+
extra: number().int().nonnegative(),
|
|
150994
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
150995
|
+
missed: number().int().nonnegative()
|
|
150996
|
+
});
|
|
150997
|
+
var RunningProposalEvidenceSchema = object({
|
|
150998
|
+
tier: RunningProposalTierSchema,
|
|
150999
|
+
spanMs: number().nonnegative(),
|
|
151000
|
+
from: number(),
|
|
151001
|
+
to: number(),
|
|
151002
|
+
points: number().int().nonnegative(),
|
|
151003
|
+
runsFound: number().int().nonnegative(),
|
|
151004
|
+
pausesFound: number().int().nonnegative(),
|
|
151005
|
+
longestPauseMs: number().nonnegative(),
|
|
151006
|
+
idleFloorW: number(),
|
|
151007
|
+
noiseCeilingW: number(),
|
|
151008
|
+
plateauW: number(),
|
|
151009
|
+
replay: RunningProposalReplaySchema
|
|
151010
|
+
});
|
|
151011
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
151012
|
+
spanMs: number().nonnegative(),
|
|
151013
|
+
points: number().int().nonnegative(),
|
|
151014
|
+
runsFound: number().int().nonnegative()
|
|
151015
|
+
});
|
|
151016
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
151017
|
+
state: literal("proposal"),
|
|
151018
|
+
transform: ThresholdTransformDraftSchema,
|
|
151019
|
+
evidence: RunningProposalEvidenceSchema,
|
|
151020
|
+
confidence: RunningProposalConfidenceSchema,
|
|
151021
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
151022
|
+
notes: array(string())
|
|
151023
|
+
}), object({
|
|
151024
|
+
state: literal("insufficient"),
|
|
151025
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
151026
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
151027
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
151028
|
+
fallback: ThresholdTransformDraftSchema
|
|
151029
|
+
})]);
|
|
151030
|
+
/**
|
|
150885
151031
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
150886
151032
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
150887
151033
|
*
|
|
@@ -151171,6 +151317,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
151171
151317
|
kind: "mutation",
|
|
151172
151318
|
auth: "admin",
|
|
151173
151319
|
access: "view"
|
|
151320
|
+
}), method(object({
|
|
151321
|
+
deviceId: number().int(),
|
|
151322
|
+
cap: string().min(1),
|
|
151323
|
+
fieldPath: string().min(1),
|
|
151324
|
+
deviceType: _enum(DeviceType).optional()
|
|
151325
|
+
}), RunningThresholdProposalSchema, {
|
|
151326
|
+
auth: "admin",
|
|
151327
|
+
access: "view"
|
|
151174
151328
|
}), method(object({
|
|
151175
151329
|
blockId: string().min(1),
|
|
151176
151330
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -152147,7 +152301,27 @@ method(object({
|
|
|
152147
152301
|
* exposes only the COMMON surface: link state, supported device
|
|
152148
152302
|
* kinds (so the UI can filter the device picker), and per-device
|
|
152149
152303
|
* expose/unexpose.
|
|
152150
|
-
|
|
152304
|
+
*
|
|
152305
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
152306
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
152307
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
152308
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
152309
|
+
* default instance's); present = that instance; an id the provider does not
|
|
152310
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
152311
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
152312
|
+
* change always names the target it changes. A single-instance provider
|
|
152313
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
152314
|
+
*/
|
|
152315
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
152316
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
152317
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
152318
|
+
/** One independent export target of an addon. */
|
|
152319
|
+
var ExportInstanceSchema = object({
|
|
152320
|
+
instanceId: InstanceIdSchema$1,
|
|
152321
|
+
name: string(),
|
|
152322
|
+
enabled: boolean(),
|
|
152323
|
+
kind: string().optional()
|
|
152324
|
+
});
|
|
152151
152325
|
var LinkStateSchema = _enum([
|
|
152152
152326
|
"unlinked",
|
|
152153
152327
|
"linked",
|
|
@@ -152198,10 +152372,14 @@ var ExposedDeviceSchema = object({
|
|
|
152198
152372
|
capabilities: array(string()).optional()
|
|
152199
152373
|
});
|
|
152200
152374
|
var ExposeInputSchema = object({
|
|
152375
|
+
instanceId: InstanceIdSchema$1,
|
|
152201
152376
|
deviceId: string(),
|
|
152202
152377
|
capabilities: array(string()).optional()
|
|
152203
152378
|
});
|
|
152204
|
-
var UnexposeInputSchema = object({
|
|
152379
|
+
var UnexposeInputSchema = object({
|
|
152380
|
+
instanceId: InstanceIdSchema$1,
|
|
152381
|
+
deviceId: string()
|
|
152382
|
+
});
|
|
152205
152383
|
/**
|
|
152206
152384
|
* What an exporter may expose besides devices (D968). An action and a
|
|
152207
152385
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -152230,15 +152408,21 @@ var ExposedEntitySchema = object({
|
|
|
152230
152408
|
entity: ExportEntityRefSchema,
|
|
152231
152409
|
exposedAs: string().optional()
|
|
152232
152410
|
});
|
|
152233
|
-
var ExposeEntityInputSchema = object({
|
|
152234
|
-
|
|
152235
|
-
|
|
152411
|
+
var ExposeEntityInputSchema = object({
|
|
152412
|
+
instanceId: InstanceIdSchema$1,
|
|
152413
|
+
entity: ExportEntityRefSchema
|
|
152414
|
+
});
|
|
152415
|
+
var UnexposeEntityInputSchema = object({
|
|
152416
|
+
instanceId: InstanceIdSchema$1,
|
|
152417
|
+
entity: ExportEntityRefSchema
|
|
152418
|
+
});
|
|
152419
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
152236
152420
|
kind: "mutation",
|
|
152237
152421
|
auth: "admin"
|
|
152238
152422
|
}), method(UnexposeInputSchema, _void(), {
|
|
152239
152423
|
kind: "mutation",
|
|
152240
152424
|
auth: "admin"
|
|
152241
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
152425
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
152242
152426
|
kind: "mutation",
|
|
152243
152427
|
auth: "admin"
|
|
152244
152428
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -155861,15 +156045,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
155861
156045
|
items: array(FanCaptureItemSchema),
|
|
155862
156046
|
omitted: array(FanCaptureOmissionSchema)
|
|
155863
156047
|
});
|
|
155864
|
-
var deviceInput$
|
|
156048
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
155865
156049
|
DeviceType.Fan;
|
|
155866
|
-
method(deviceInput$
|
|
156050
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
155867
156051
|
kind: "mutation",
|
|
155868
156052
|
auth: "admin"
|
|
155869
|
-
}), method(deviceInput$
|
|
156053
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
155870
156054
|
kind: "mutation",
|
|
155871
156055
|
auth: "protected"
|
|
155872
|
-
}), method(deviceInput$
|
|
156056
|
+
}), method(deviceInput$2.extend({
|
|
155873
156057
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
155874
156058
|
steps: array(ActionStepSchema).max(20).default([])
|
|
155875
156059
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -156752,6 +156936,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156752
156936
|
}), object({ success: boolean() }), {
|
|
156753
156937
|
kind: "mutation",
|
|
156754
156938
|
auth: "admin"
|
|
156939
|
+
}), method(object({ id: string() }), object({
|
|
156940
|
+
rows: number().int().nonnegative(),
|
|
156941
|
+
secrets: number().int().nonnegative()
|
|
156942
|
+
}), {
|
|
156943
|
+
kind: "mutation",
|
|
156944
|
+
auth: "admin"
|
|
156755
156945
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
156756
156946
|
addonId: string(),
|
|
156757
156947
|
settings: record(string(), unknown())
|
|
@@ -156780,13 +156970,65 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156780
156970
|
*/
|
|
156781
156971
|
var ConfigSchemaPassthrough$3 = custom();
|
|
156782
156972
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
156973
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
156974
|
+
/**
|
|
156975
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
156976
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
156977
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
156978
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
156979
|
+
* instance id.
|
|
156980
|
+
*/
|
|
156981
|
+
var IntegrationInstanceKindSchema = object({
|
|
156982
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
156983
|
+
id: string().min(1),
|
|
156984
|
+
label: string().min(1),
|
|
156985
|
+
/** The kind signs in through a server-held interactive session. */
|
|
156986
|
+
signIn: object({
|
|
156987
|
+
basePath: string().min(1),
|
|
156988
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
156989
|
+
}).optional(),
|
|
156990
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
156991
|
+
connectPath: string().min(1).optional(),
|
|
156992
|
+
/** Relative hub path to disconnect that account. */
|
|
156993
|
+
disconnectPath: string().min(1).optional()
|
|
156994
|
+
});
|
|
156783
156995
|
var IntegrationInstanceDescriptionSchema = object({
|
|
156996
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
156997
|
+
role: _enum([
|
|
156998
|
+
"devices",
|
|
156999
|
+
"services",
|
|
157000
|
+
"both"
|
|
157001
|
+
]),
|
|
157002
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
157003
|
+
discovery: object({
|
|
157004
|
+
mode: _enum([
|
|
157005
|
+
"none",
|
|
157006
|
+
"lan",
|
|
157007
|
+
"account"
|
|
157008
|
+
]),
|
|
157009
|
+
scopedToInstance: boolean()
|
|
157010
|
+
}).optional(),
|
|
156784
157011
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
156785
|
-
instanceMode:
|
|
157012
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
156786
157013
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
156787
157014
|
kind: string(),
|
|
156788
157015
|
/** True when `testInstance` really probes the remote. */
|
|
156789
|
-
canTest: boolean()
|
|
157016
|
+
canTest: boolean(),
|
|
157017
|
+
/**
|
|
157018
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
157019
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
157020
|
+
*/
|
|
157021
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
157022
|
+
/**
|
|
157023
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
157024
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
157025
|
+
*/
|
|
157026
|
+
allowRestoreId: boolean().optional(),
|
|
157027
|
+
/**
|
|
157028
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
157029
|
+
* send an id (the UI then mints an opaque one).
|
|
157030
|
+
*/
|
|
157031
|
+
mintsId: boolean().optional()
|
|
156790
157032
|
});
|
|
156791
157033
|
var IntegrationInstanceStatusSchema = object({
|
|
156792
157034
|
state: _enum([
|
|
@@ -156795,7 +157037,9 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
156795
157037
|
"error",
|
|
156796
157038
|
"unknown"
|
|
156797
157039
|
]),
|
|
156798
|
-
detail: string().optional()
|
|
157040
|
+
detail: string().optional(),
|
|
157041
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
157042
|
+
label: string().optional()
|
|
156799
157043
|
});
|
|
156800
157044
|
var IntegrationInstanceSchema = object({
|
|
156801
157045
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156803,13 +157047,18 @@ var IntegrationInstanceSchema = object({
|
|
|
156803
157047
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
156804
157048
|
configured: boolean(),
|
|
156805
157049
|
/**
|
|
156806
|
-
* The instance's own on/off switch (D996)
|
|
156807
|
-
*
|
|
157050
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
157051
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
157052
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
156808
157053
|
*/
|
|
156809
|
-
enabled: boolean(),
|
|
157054
|
+
enabled: boolean().optional(),
|
|
157055
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
157056
|
+
kind: string().optional(),
|
|
156810
157057
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
156811
157058
|
formValues: record(string(), unknown()),
|
|
156812
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
157059
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
157060
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
157061
|
+
lastCheckedAt: number().optional()
|
|
156813
157062
|
});
|
|
156814
157063
|
var IntegrationInstancePatchSchema = object({
|
|
156815
157064
|
id: IntegrationInstanceIdSchema,
|
|
@@ -156818,8 +157067,9 @@ var IntegrationInstancePatchSchema = object({
|
|
|
156818
157067
|
values: record(string(), unknown())
|
|
156819
157068
|
});
|
|
156820
157069
|
method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
|
|
156821
|
-
id: IntegrationInstanceIdSchema,
|
|
156822
|
-
name: string().min(1)
|
|
157070
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
157071
|
+
name: string().min(1),
|
|
157072
|
+
kind: string().min(1).optional()
|
|
156823
157073
|
}), object({ id: string() }), {
|
|
156824
157074
|
kind: "mutation",
|
|
156825
157075
|
auth: "admin"
|
|
@@ -158880,6 +159130,11 @@ var LogEntrySchema = object({
|
|
|
158880
159130
|
* flight, and not asking for the next slice ends the scan.
|
|
158881
159131
|
*/
|
|
158882
159132
|
var LogReadQuerySchema = object({
|
|
159133
|
+
/**
|
|
159134
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
159135
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
159136
|
+
*/
|
|
159137
|
+
instanceId: string().min(1).max(200).optional(),
|
|
158883
159138
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
158884
159139
|
requestId: string().min(1).max(64).optional(),
|
|
158885
159140
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -158942,6 +159197,8 @@ var LogReadResultSchema = object({
|
|
|
158942
159197
|
/** True when `cancelRead` ended this slice early. */
|
|
158943
159198
|
cancelled: boolean().optional()
|
|
158944
159199
|
});
|
|
159200
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
159201
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
158945
159202
|
var LogReadCapabilitiesSchema = object({
|
|
158946
159203
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
158947
159204
|
readable: boolean(),
|
|
@@ -158960,7 +159217,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
158960
159217
|
/** How far back the store reaches, when the provider knows (days). */
|
|
158961
159218
|
retentionDays: number().nullable()
|
|
158962
159219
|
});
|
|
158963
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
159220
|
+
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
|
|
158964
159221
|
kind: "mutation",
|
|
158965
159222
|
auth: "admin"
|
|
158966
159223
|
});
|
|
@@ -171353,6 +171610,14 @@ var powerMeterCapability = {
|
|
|
171353
171610
|
deviceNative: true,
|
|
171354
171611
|
mode: "singleton",
|
|
171355
171612
|
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
171613
|
+
deviceConfig: { ui: {
|
|
171614
|
+
kind: "widget",
|
|
171615
|
+
widgetId: "host/power-meter-line",
|
|
171616
|
+
tab: "power",
|
|
171617
|
+
placement: "hero",
|
|
171618
|
+
label: "Power",
|
|
171619
|
+
order: 4
|
|
171620
|
+
} },
|
|
171356
171621
|
methods: {},
|
|
171357
171622
|
status: {
|
|
171358
171623
|
schema: PowerMeterStatusSchema,
|
|
@@ -171412,6 +171677,117 @@ var powerMeterCapability = {
|
|
|
171412
171677
|
volatileStateFields: ["lastFetchedAt"],
|
|
171413
171678
|
customize: powerMeterCustomize
|
|
171414
171679
|
};
|
|
171680
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
171681
|
+
"no-power-source",
|
|
171682
|
+
"switch-missing",
|
|
171683
|
+
"switch-offline",
|
|
171684
|
+
"switch-unknown",
|
|
171685
|
+
"restart-in-progress",
|
|
171686
|
+
"switch-call-failed"
|
|
171687
|
+
]);
|
|
171688
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
171689
|
+
accepted: literal(false),
|
|
171690
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
171691
|
+
/** One line for the operator, naming the switch. */
|
|
171692
|
+
message: string()
|
|
171693
|
+
})]);
|
|
171694
|
+
var PowerSourceInfoSchema = object({
|
|
171695
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
171696
|
+
linked: boolean(),
|
|
171697
|
+
switchDeviceId: number().nullable(),
|
|
171698
|
+
switchName: string().nullable(),
|
|
171699
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
171700
|
+
switchOn: boolean().nullable(),
|
|
171701
|
+
switchOnline: boolean().nullable(),
|
|
171702
|
+
restartInProgress: boolean(),
|
|
171703
|
+
offSeconds: number().int()
|
|
171704
|
+
});
|
|
171705
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
171706
|
+
var PowerSourceStateSchema = object({
|
|
171707
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
171708
|
+
switchOn: boolean().nullable(),
|
|
171709
|
+
offSeconds: number().int(),
|
|
171710
|
+
lastFetchedAt: number()
|
|
171711
|
+
});
|
|
171712
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
171713
|
+
DeviceType.Gateway,
|
|
171714
|
+
DeviceType.Hub,
|
|
171715
|
+
DeviceType.Camera,
|
|
171716
|
+
DeviceType.Sensor,
|
|
171717
|
+
DeviceType.MediaPlayer,
|
|
171718
|
+
DeviceType.Vacuum,
|
|
171719
|
+
DeviceType.Generic,
|
|
171720
|
+
DeviceType.Washer,
|
|
171721
|
+
DeviceType.Dryer,
|
|
171722
|
+
DeviceType.Dishwasher,
|
|
171723
|
+
DeviceType.PetFountain,
|
|
171724
|
+
DeviceType.Fan,
|
|
171725
|
+
DeviceType.Humidifier,
|
|
171726
|
+
DeviceType.Climate,
|
|
171727
|
+
DeviceType.WaterHeater
|
|
171728
|
+
];
|
|
171729
|
+
var powerSourceCustomize = {
|
|
171730
|
+
version: 1,
|
|
171731
|
+
targets: [{
|
|
171732
|
+
id: "switch",
|
|
171733
|
+
cardinality: "scalar",
|
|
171734
|
+
vocabulary: {
|
|
171735
|
+
cap: "power-source",
|
|
171736
|
+
stateField: "switchOn"
|
|
171737
|
+
},
|
|
171738
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
171739
|
+
apply: "add",
|
|
171740
|
+
path: "switchOn",
|
|
171741
|
+
sources: [{
|
|
171742
|
+
kind: "switch",
|
|
171743
|
+
transforms: [{ kind: "identity" }]
|
|
171744
|
+
}],
|
|
171745
|
+
companions: [{
|
|
171746
|
+
kind: "constant",
|
|
171747
|
+
path: "offSeconds",
|
|
171748
|
+
value: 10,
|
|
171749
|
+
when: "add"
|
|
171750
|
+
}, {
|
|
171751
|
+
kind: "clock",
|
|
171752
|
+
path: "lastFetchedAt",
|
|
171753
|
+
when: "always"
|
|
171754
|
+
}],
|
|
171755
|
+
sourceReport: { status: "assignment-status" }
|
|
171756
|
+
}]
|
|
171757
|
+
};
|
|
171758
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
171759
|
+
var powerSourceCapability = {
|
|
171760
|
+
name: "power-source",
|
|
171761
|
+
scope: "device",
|
|
171762
|
+
mode: "singleton",
|
|
171763
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
171764
|
+
methods: {
|
|
171765
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
171766
|
+
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171767
|
+
kind: "mutation",
|
|
171768
|
+
auth: "admin"
|
|
171769
|
+
}),
|
|
171770
|
+
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171771
|
+
kind: "mutation",
|
|
171772
|
+
auth: "admin"
|
|
171773
|
+
}),
|
|
171774
|
+
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
171775
|
+
kind: "mutation",
|
|
171776
|
+
auth: "admin"
|
|
171777
|
+
})
|
|
171778
|
+
},
|
|
171779
|
+
runtimeState: PowerSourceStateSchema,
|
|
171780
|
+
customize: powerSourceCustomize,
|
|
171781
|
+
/**
|
|
171782
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
171783
|
+
* its next report; a restored value would outlive the reading it names.
|
|
171784
|
+
*
|
|
171785
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
171786
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
171787
|
+
*/
|
|
171788
|
+
durability: "session",
|
|
171789
|
+
history: "none"
|
|
171790
|
+
};
|
|
171415
171791
|
/**
|
|
171416
171792
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
171417
171793
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -181892,6 +182268,25 @@ function buildStreamParamsConfigSchema(options, status) {
|
|
|
181892
182268
|
if (sections.length === 0) return null;
|
|
181893
182269
|
return { sections };
|
|
181894
182270
|
}
|
|
182271
|
+
/**
|
|
182272
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
182273
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
182274
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
182275
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
182276
|
+
* server sends an id, a client maps it to its own icon set.
|
|
182277
|
+
*
|
|
182278
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
182279
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
182280
|
+
* to `icon`.
|
|
182281
|
+
*/
|
|
182282
|
+
function digit(n) {
|
|
182283
|
+
return {
|
|
182284
|
+
label: t(n, n),
|
|
182285
|
+
icon: "hash",
|
|
182286
|
+
legend: n
|
|
182287
|
+
};
|
|
182288
|
+
}
|
|
182289
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
181895
182290
|
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
181896
182291
|
var btn = (idSuffix, row, col, init) => ({
|
|
181897
182292
|
idSuffix,
|
|
@@ -182020,6 +182415,34 @@ discriminatedUnion("kind", [object({
|
|
|
182020
182415
|
code: string()
|
|
182021
182416
|
})]);
|
|
182022
182417
|
/**
|
|
182418
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
182419
|
+
*
|
|
182420
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
182421
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
182422
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
182423
|
+
* count of samples.
|
|
182424
|
+
*
|
|
182425
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
182426
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
182427
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
182428
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
182429
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
182430
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
182431
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
182432
|
+
* covers the longest standby blip above T.
|
|
182433
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
182434
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
182435
|
+
*/
|
|
182436
|
+
var SECOND_MS = 1e3;
|
|
182437
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
182438
|
+
24 * (60 * MINUTE_MS$1);
|
|
182439
|
+
10 * MINUTE_MS$1;
|
|
182440
|
+
30 * SECOND_MS;
|
|
182441
|
+
60 * SECOND_MS;
|
|
182442
|
+
20 * MINUTE_MS$1;
|
|
182443
|
+
10 * SECOND_MS;
|
|
182444
|
+
10 * SECOND_MS;
|
|
182445
|
+
/**
|
|
182023
182446
|
* Which of OUR object detections a clip window holds — the ONE overlap rule
|
|
182024
182447
|
* and the ONE label ranking, for every clip source (D923).
|
|
182025
182448
|
*
|
|
@@ -185272,6 +185695,13 @@ Object.freeze({
|
|
|
185272
185695
|
access: "create",
|
|
185273
185696
|
role: "admin"
|
|
185274
185697
|
},
|
|
185698
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
185699
|
+
capName: "core-blocks",
|
|
185700
|
+
capScope: "system",
|
|
185701
|
+
addonId: null,
|
|
185702
|
+
access: "view",
|
|
185703
|
+
role: "admin"
|
|
185704
|
+
},
|
|
185275
185705
|
"coreBlocks.reportAutomationRun": {
|
|
185276
185706
|
capName: "core-blocks",
|
|
185277
185707
|
capScope: "system",
|
|
@@ -185783,6 +186213,13 @@ Object.freeze({
|
|
|
185783
186213
|
access: "view",
|
|
185784
186214
|
role: "admin"
|
|
185785
186215
|
},
|
|
186216
|
+
"deviceExport.listInstances": {
|
|
186217
|
+
capName: "device-export",
|
|
186218
|
+
capScope: "system",
|
|
186219
|
+
addonId: null,
|
|
186220
|
+
access: "view",
|
|
186221
|
+
role: "admin"
|
|
186222
|
+
},
|
|
185786
186223
|
"deviceExport.listSupportedDeviceKinds": {
|
|
185787
186224
|
capName: "device-export",
|
|
185788
186225
|
capScope: "system",
|
|
@@ -187435,6 +187872,13 @@ Object.freeze({
|
|
|
187435
187872
|
access: "create",
|
|
187436
187873
|
role: "admin"
|
|
187437
187874
|
},
|
|
187875
|
+
"integrations.clearSettings": {
|
|
187876
|
+
capName: "integrations",
|
|
187877
|
+
capScope: "system",
|
|
187878
|
+
addonId: null,
|
|
187879
|
+
access: "delete",
|
|
187880
|
+
role: "admin"
|
|
187881
|
+
},
|
|
187438
187882
|
"integrations.create": {
|
|
187439
187883
|
capName: "integrations",
|
|
187440
187884
|
capScope: "system",
|
|
@@ -194882,6 +195326,11 @@ Object.freeze({
|
|
|
194882
195326
|
form: "single",
|
|
194883
195327
|
optional: false
|
|
194884
195328
|
}],
|
|
195329
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
195330
|
+
name: "deviceId",
|
|
195331
|
+
form: "single",
|
|
195332
|
+
optional: false
|
|
195333
|
+
}],
|
|
194885
195334
|
"cover.close": [{
|
|
194886
195335
|
name: "deviceId",
|
|
194887
195336
|
form: "single",
|
|
@@ -197545,99 +197994,6 @@ Object.freeze({
|
|
|
197545
197994
|
"network-access": "ingress",
|
|
197546
197995
|
"smtp-provider": "email"
|
|
197547
197996
|
});
|
|
197548
|
-
/**
|
|
197549
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
197550
|
-
* `z.infer`, they do not restate the shape.
|
|
197551
|
-
*/
|
|
197552
|
-
var OverrideSourceSchema = object({
|
|
197553
|
-
addonId: string().min(1),
|
|
197554
|
-
stableId: string().min(1),
|
|
197555
|
-
cap: string().min(1),
|
|
197556
|
-
fieldPath: string().min(1)
|
|
197557
|
-
});
|
|
197558
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
197559
|
-
object({ kind: literal("identity") }),
|
|
197560
|
-
object({ kind: literal("invert") }),
|
|
197561
|
-
object({
|
|
197562
|
-
kind: literal("threshold"),
|
|
197563
|
-
comparison: _enum(["gte", "lte"]),
|
|
197564
|
-
threshold: number(),
|
|
197565
|
-
startDwellMs: number().nonnegative(),
|
|
197566
|
-
stopDwellMs: number().nonnegative()
|
|
197567
|
-
}),
|
|
197568
|
-
object({
|
|
197569
|
-
kind: literal("linear"),
|
|
197570
|
-
scale: number(),
|
|
197571
|
-
offset: number(),
|
|
197572
|
-
clampMin: number().optional(),
|
|
197573
|
-
clampMax: number().optional()
|
|
197574
|
-
}),
|
|
197575
|
-
object({
|
|
197576
|
-
kind: literal("enum-map"),
|
|
197577
|
-
mapping: record(string(), string().nullable())
|
|
197578
|
-
}),
|
|
197579
|
-
object({ kind: literal("number") })
|
|
197580
|
-
]);
|
|
197581
|
-
var ScalarOverrideDraftSchema = object({
|
|
197582
|
-
kind: literal("scalar"),
|
|
197583
|
-
cap: string().min(1),
|
|
197584
|
-
targetId: string().min(1),
|
|
197585
|
-
source: OverrideSourceSchema,
|
|
197586
|
-
transform: CustomizeTransformDraftSchema
|
|
197587
|
-
});
|
|
197588
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
197589
|
-
kind: literal("keyedArray"),
|
|
197590
|
-
cap: string().min(1),
|
|
197591
|
-
targetId: string().min(1),
|
|
197592
|
-
key: string().min(1),
|
|
197593
|
-
label: string().min(1),
|
|
197594
|
-
fieldPath: string().min(1),
|
|
197595
|
-
source: OverrideSourceSchema,
|
|
197596
|
-
transform: CustomizeTransformDraftSchema
|
|
197597
|
-
});
|
|
197598
|
-
var PulseOverrideDraftSchema = object({
|
|
197599
|
-
kind: literal("pulse"),
|
|
197600
|
-
cap: string().min(1),
|
|
197601
|
-
targetId: string().min(1),
|
|
197602
|
-
enabled: boolean(),
|
|
197603
|
-
debounceMs: number().nonnegative(),
|
|
197604
|
-
edges: array(_enum([
|
|
197605
|
-
"rising",
|
|
197606
|
-
"button",
|
|
197607
|
-
"native-event"
|
|
197608
|
-
])).min(1).optional(),
|
|
197609
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
197610
|
-
});
|
|
197611
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
197612
|
-
"in-addition",
|
|
197613
|
-
"confirm",
|
|
197614
|
-
"replace"
|
|
197615
|
-
]).optional() });
|
|
197616
|
-
discriminatedUnion("kind", [
|
|
197617
|
-
ScalarOverrideDraftSchema,
|
|
197618
|
-
KeyedArrayOverrideDraftSchema,
|
|
197619
|
-
PulseOverrideDraftSchema,
|
|
197620
|
-
object({
|
|
197621
|
-
kind: literal("span"),
|
|
197622
|
-
cap: string().min(1),
|
|
197623
|
-
targetId: string().min(1),
|
|
197624
|
-
timeoutMs: number().nonnegative(),
|
|
197625
|
-
cooldownMs: number().nonnegative().optional(),
|
|
197626
|
-
sources: array(SpanSourceDraftSchema)
|
|
197627
|
-
})
|
|
197628
|
-
]);
|
|
197629
|
-
object({
|
|
197630
|
-
targetId: string().min(1),
|
|
197631
|
-
configured: boolean(),
|
|
197632
|
-
source: _enum(["native", "assigned"]),
|
|
197633
|
-
state: _enum([
|
|
197634
|
-
"active",
|
|
197635
|
-
"stale",
|
|
197636
|
-
"released",
|
|
197637
|
-
"unavailable"
|
|
197638
|
-
]),
|
|
197639
|
-
reason: string().nullable()
|
|
197640
|
-
});
|
|
197641
197997
|
var G711_SCALE_CORRECTION_DB = {
|
|
197642
197998
|
PCMU: 20 * Math.log10(4),
|
|
197643
197999
|
PCMA: 20 * Math.log10(8)
|
|
@@ -219649,6 +220005,285 @@ async function startLegacyWebhookIntake(deps) {
|
|
|
219649
220005
|
};
|
|
219650
220006
|
}
|
|
219651
220007
|
//#endregion
|
|
220008
|
+
//#region src/reolink-discovery-map.ts
|
|
220009
|
+
/**
|
|
220010
|
+
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
220011
|
+
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
220012
|
+
*/
|
|
220013
|
+
function slugifyReolinkHost(host) {
|
|
220014
|
+
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
220015
|
+
}
|
|
220016
|
+
/**
|
|
220017
|
+
* Decide whether a discovered host is a device this provider already owns.
|
|
220018
|
+
*
|
|
220019
|
+
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
220020
|
+
* wins even when the host differs, because the address is the thing that moves.
|
|
220021
|
+
*/
|
|
220022
|
+
function findOnboardedMatch(discovered, onboarded) {
|
|
220023
|
+
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
220024
|
+
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
220025
|
+
if (byUid !== void 0) return byUid;
|
|
220026
|
+
}
|
|
220027
|
+
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
220028
|
+
}
|
|
220029
|
+
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
220030
|
+
var HUB_BAICHUAN_PORT = 9e3;
|
|
220031
|
+
/**
|
|
220032
|
+
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
220033
|
+
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
220034
|
+
* as a camera to add.
|
|
220035
|
+
*/
|
|
220036
|
+
function looksLikeReolinkHub(model) {
|
|
220037
|
+
if (model === void 0) return false;
|
|
220038
|
+
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
220039
|
+
}
|
|
220040
|
+
/**
|
|
220041
|
+
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
220042
|
+
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
220043
|
+
* creates hides this candidate through `originStableId`.
|
|
220044
|
+
*/
|
|
220045
|
+
function reolinkHubStableId(identity) {
|
|
220046
|
+
const uid = identity.uid?.trim() ?? "";
|
|
220047
|
+
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
220048
|
+
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
220049
|
+
}
|
|
220050
|
+
function isKnownHub(d, known) {
|
|
220051
|
+
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
220052
|
+
}
|
|
220053
|
+
/**
|
|
220054
|
+
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
220055
|
+
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
220056
|
+
* first responder for a host wins. Pure: no I/O.
|
|
220057
|
+
*
|
|
220058
|
+
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
220059
|
+
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
220060
|
+
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
220061
|
+
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
220062
|
+
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
220063
|
+
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
220064
|
+
* never a scan result.
|
|
220065
|
+
*
|
|
220066
|
+
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
220067
|
+
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
220068
|
+
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
220069
|
+
* in the first place, so a scan says so instead of offering it again.
|
|
220070
|
+
*/
|
|
220071
|
+
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
220072
|
+
const username = credentials.username?.trim() ?? "";
|
|
220073
|
+
const password = credentials.password ?? "";
|
|
220074
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
220075
|
+
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
220076
|
+
return [...byHost.values()].flatMap((d) => {
|
|
220077
|
+
const displayName = d.name ?? d.model ?? d.host;
|
|
220078
|
+
if (looksLikeReolinkHub(d.model)) {
|
|
220079
|
+
if (isKnownHub(d, hubIntegrations)) return [];
|
|
220080
|
+
return [{
|
|
220081
|
+
stableId: reolinkHubStableId(d),
|
|
220082
|
+
type: DeviceType.Gateway,
|
|
220083
|
+
suggestedName: displayName,
|
|
220084
|
+
prefilledConfig: {
|
|
220085
|
+
host: d.host,
|
|
220086
|
+
port: HUB_BAICHUAN_PORT,
|
|
220087
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220088
|
+
...username ? { username } : {}
|
|
220089
|
+
},
|
|
220090
|
+
setupIntegration: true
|
|
220091
|
+
}];
|
|
220092
|
+
}
|
|
220093
|
+
const match = findOnboardedMatch(d, onboarded);
|
|
220094
|
+
return [{
|
|
220095
|
+
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
220096
|
+
type: DeviceType.Camera,
|
|
220097
|
+
suggestedName: displayName,
|
|
220098
|
+
prefilledConfig: {
|
|
220099
|
+
name: displayName,
|
|
220100
|
+
host: d.host,
|
|
220101
|
+
transport: "auto",
|
|
220102
|
+
...d.uid ? { uid: d.uid } : {},
|
|
220103
|
+
...username ? { username } : {},
|
|
220104
|
+
...password ? { password } : {}
|
|
220105
|
+
},
|
|
220106
|
+
...match !== void 0 ? {
|
|
220107
|
+
alreadyOnboarded: true,
|
|
220108
|
+
onboardedDeviceId: match.deviceId,
|
|
220109
|
+
onboardedName: match.name
|
|
220110
|
+
} : {}
|
|
220111
|
+
}];
|
|
220112
|
+
});
|
|
220113
|
+
}
|
|
220114
|
+
//#endregion
|
|
220115
|
+
//#region src/reolink-hub-migration.ts
|
|
220116
|
+
/**
|
|
220117
|
+
* D985 — boot migration of the old Reolink Hub DEVICE into an INTEGRATION.
|
|
220118
|
+
*
|
|
220119
|
+
* A Hub used to be a device with the NVR's credentials in its config and its channels as
|
|
220120
|
+
* parented child cameras. Now the NVR is an integration (credentials in its keychain-backed
|
|
220121
|
+
* settings) and the channels are top-level cameras stamped with its id. For every Hub row:
|
|
220122
|
+
*
|
|
220123
|
+
* 1. create (or reuse) an integration named after it, config + password moved as settings;
|
|
220124
|
+
* 2. keep every child's deviceId and stableId, stamp it with the integrationId, un-parent it
|
|
220125
|
+
* and make sure its persisted blob can be restored on its own;
|
|
220126
|
+
* 3. only AFTER the children restored and the hub has none left, retire the hub by tombstone.
|
|
220127
|
+
*
|
|
220128
|
+
* Idempotent (a second run finds no Hub rows) and all-or-nothing per hub: a step that throws
|
|
220129
|
+
* leaves the hub device and its children's parentage exactly as they were, and the next boot
|
|
220130
|
+
* starts over. A locked secret is never read as an empty password and no replacement is minted.
|
|
220131
|
+
*/
|
|
220132
|
+
/** The marker an integration carries so a re-run reuses it instead of minting a second one. */
|
|
220133
|
+
var MIGRATED_FROM_HUB_KEY = "migratedFromHubDeviceId";
|
|
220134
|
+
function positivePort(value) {
|
|
220135
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
220136
|
+
}
|
|
220137
|
+
var CONNECTION_FIELDS = [
|
|
220138
|
+
"host",
|
|
220139
|
+
"port",
|
|
220140
|
+
"username",
|
|
220141
|
+
"transport"
|
|
220142
|
+
];
|
|
220143
|
+
function str(value) {
|
|
220144
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
220145
|
+
}
|
|
220146
|
+
function migratedChildConfig(child, hub, integrationId) {
|
|
220147
|
+
const filled = {};
|
|
220148
|
+
for (const key of CONNECTION_FIELDS) {
|
|
220149
|
+
const own = child[key];
|
|
220150
|
+
const inherited = hub[key];
|
|
220151
|
+
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
220152
|
+
}
|
|
220153
|
+
return {
|
|
220154
|
+
...child,
|
|
220155
|
+
...filled,
|
|
220156
|
+
integrationId
|
|
220157
|
+
};
|
|
220158
|
+
}
|
|
220159
|
+
async function migrateOneHub(hub, children, integrations, ports) {
|
|
220160
|
+
const read = readHubConnection(hub.config);
|
|
220161
|
+
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
220162
|
+
const { connection } = read;
|
|
220163
|
+
const uid = str(hub.config["uid"]);
|
|
220164
|
+
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
220165
|
+
addonId: ports.addonId,
|
|
220166
|
+
name: hub.name,
|
|
220167
|
+
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
220168
|
+
originStableId: reolinkHubStableId({
|
|
220169
|
+
...uid !== void 0 ? { uid } : {},
|
|
220170
|
+
host: connection.host
|
|
220171
|
+
}),
|
|
220172
|
+
settings: {
|
|
220173
|
+
host: connection.host,
|
|
220174
|
+
port: connection.port,
|
|
220175
|
+
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
220176
|
+
username: connection.username,
|
|
220177
|
+
password: connection.password,
|
|
220178
|
+
transport: connection.transport,
|
|
220179
|
+
...uid !== void 0 ? { uid } : {},
|
|
220180
|
+
debugGeneral: connection.debugGeneral,
|
|
220181
|
+
debugSocketLogs: connection.debugSocketLogs
|
|
220182
|
+
},
|
|
220183
|
+
secretKeys: ["password"]
|
|
220184
|
+
})).id;
|
|
220185
|
+
const rewritten = [];
|
|
220186
|
+
for (const child of children) {
|
|
220187
|
+
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
220188
|
+
await ports.persistConfig(child.stableId, config);
|
|
220189
|
+
await ports.setIntegrationId(child.id, integrationId);
|
|
220190
|
+
rewritten.push({
|
|
220191
|
+
...child,
|
|
220192
|
+
config,
|
|
220193
|
+
parentDeviceId: null
|
|
220194
|
+
});
|
|
220195
|
+
}
|
|
220196
|
+
return {
|
|
220197
|
+
rows: rewritten,
|
|
220198
|
+
retirement: {
|
|
220199
|
+
hubDeviceId: hub.id,
|
|
220200
|
+
hubName: hub.name,
|
|
220201
|
+
integrationId
|
|
220202
|
+
}
|
|
220203
|
+
};
|
|
220204
|
+
}
|
|
220205
|
+
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
220206
|
+
async function planHubMigration(saved, ports) {
|
|
220207
|
+
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
220208
|
+
if (hubs.length === 0) return {
|
|
220209
|
+
rows: saved,
|
|
220210
|
+
retirements: []
|
|
220211
|
+
};
|
|
220212
|
+
let integrations;
|
|
220213
|
+
try {
|
|
220214
|
+
integrations = await ports.listIntegrations();
|
|
220215
|
+
} catch (err) {
|
|
220216
|
+
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
220217
|
+
return {
|
|
220218
|
+
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
220219
|
+
retirements: []
|
|
220220
|
+
};
|
|
220221
|
+
}
|
|
220222
|
+
const hubIds = new Set(hubs.map((h) => h.id));
|
|
220223
|
+
const rewrittenById = /* @__PURE__ */ new Map();
|
|
220224
|
+
const retirements = [];
|
|
220225
|
+
for (const hub of hubs) {
|
|
220226
|
+
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
220227
|
+
try {
|
|
220228
|
+
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
220229
|
+
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
220230
|
+
retirements.push(done.retirement);
|
|
220231
|
+
ports.logger.info("Reolink hub migrated to an integration", {
|
|
220232
|
+
tags: { deviceId: hub.id },
|
|
220233
|
+
meta: {
|
|
220234
|
+
integrationId: done.retirement.integrationId,
|
|
220235
|
+
children: children.length,
|
|
220236
|
+
name: hub.name
|
|
220237
|
+
}
|
|
220238
|
+
});
|
|
220239
|
+
} catch (err) {
|
|
220240
|
+
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
220241
|
+
tags: { deviceId: hub.id },
|
|
220242
|
+
meta: {
|
|
220243
|
+
error: errMsg$3(err),
|
|
220244
|
+
name: hub.name
|
|
220245
|
+
}
|
|
220246
|
+
});
|
|
220247
|
+
}
|
|
220248
|
+
}
|
|
220249
|
+
return {
|
|
220250
|
+
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
220251
|
+
retirements
|
|
220252
|
+
};
|
|
220253
|
+
}
|
|
220254
|
+
/**
|
|
220255
|
+
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
220256
|
+
* and named: removing it would cascade the camera away with it.
|
|
220257
|
+
*/
|
|
220258
|
+
async function retireMigratedHubs(retirements, ports) {
|
|
220259
|
+
let retired = 0;
|
|
220260
|
+
for (const r of retirements) try {
|
|
220261
|
+
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
220262
|
+
if (remaining.length > 0) {
|
|
220263
|
+
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
220264
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220265
|
+
meta: {
|
|
220266
|
+
remaining: [...remaining],
|
|
220267
|
+
integrationId: r.integrationId
|
|
220268
|
+
}
|
|
220269
|
+
});
|
|
220270
|
+
continue;
|
|
220271
|
+
}
|
|
220272
|
+
await ports.removeDevice(r.hubDeviceId);
|
|
220273
|
+
retired++;
|
|
220274
|
+
ports.logger.info("Reolink hub device retired", {
|
|
220275
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220276
|
+
meta: { integrationId: r.integrationId }
|
|
220277
|
+
});
|
|
220278
|
+
} catch (err) {
|
|
220279
|
+
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
220280
|
+
tags: { deviceId: r.hubDeviceId },
|
|
220281
|
+
meta: { error: errMsg$3(err) }
|
|
220282
|
+
});
|
|
220283
|
+
}
|
|
220284
|
+
return retired;
|
|
220285
|
+
}
|
|
220286
|
+
//#endregion
|
|
219652
220287
|
//#region src/migrations/drop-webhook-intake-settings.ts
|
|
219653
220288
|
/**
|
|
219654
220289
|
* The camera webhook is a plane of the addon's HTTP data plane, not a private listener
|
|
@@ -219773,6 +220408,18 @@ var REOLINK_MIGRATIONS = [{
|
|
|
219773
220408
|
},
|
|
219774
220409
|
plan: planDropWebhookIntakeSettings
|
|
219775
220410
|
}];
|
|
220411
|
+
/**
|
|
220412
|
+
* Every persisted camera that names no integration is claimed to the addon's default one. The
|
|
220413
|
+
* integration a hub migration created is never the default, so it is not eligible. The integration
|
|
220414
|
+
* settings themselves stay where they are until this addon's own wave moves them.
|
|
220415
|
+
*/
|
|
220416
|
+
var REOLINK_CLAIM_MIGRATION = claimDevicesMigration({
|
|
220417
|
+
addonId: "provider-reolink",
|
|
220418
|
+
date: "2026-11-04",
|
|
220419
|
+
seq: 3,
|
|
220420
|
+
dependsOn: [],
|
|
220421
|
+
eligible: (integration) => integration.info[MIGRATED_FROM_HUB_KEY] === void 0
|
|
220422
|
+
});
|
|
219776
220423
|
//#endregion
|
|
219777
220424
|
//#region src/reolink-adoption-provider.ts
|
|
219778
220425
|
var DEVICES_FILTER = {
|
|
@@ -219938,113 +220585,6 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219938
220585
|
};
|
|
219939
220586
|
}
|
|
219940
220587
|
//#endregion
|
|
219941
|
-
//#region src/reolink-discovery-map.ts
|
|
219942
|
-
/**
|
|
219943
|
-
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
219944
|
-
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
|
|
219945
|
-
*/
|
|
219946
|
-
function slugifyReolinkHost(host) {
|
|
219947
|
-
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
219948
|
-
}
|
|
219949
|
-
/**
|
|
219950
|
-
* Decide whether a discovered host is a device this provider already owns.
|
|
219951
|
-
*
|
|
219952
|
-
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
219953
|
-
* wins even when the host differs, because the address is the thing that moves.
|
|
219954
|
-
*/
|
|
219955
|
-
function findOnboardedMatch(discovered, onboarded) {
|
|
219956
|
-
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
219957
|
-
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
219958
|
-
if (byUid !== void 0) return byUid;
|
|
219959
|
-
}
|
|
219960
|
-
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
219961
|
-
}
|
|
219962
|
-
/** Baichuan control port of an NVR / Home Hub (the scan only ever reports the HTTP/ONVIF one). */
|
|
219963
|
-
var HUB_BAICHUAN_PORT = 9e3;
|
|
219964
|
-
/**
|
|
219965
|
-
* `true` for a model string that names an NVR or a Home Hub: a device that fronts channels
|
|
219966
|
-
* rather than being one camera. Such a device is offered as an INTEGRATION to set up (D985), never
|
|
219967
|
-
* as a camera to add.
|
|
219968
|
-
*/
|
|
219969
|
-
function looksLikeReolinkHub(model) {
|
|
219970
|
-
if (model === void 0) return false;
|
|
219971
|
-
return /\b(nvr|rln\d*|home\s*hub|hub)\b/i.test(model);
|
|
219972
|
-
}
|
|
219973
|
-
/**
|
|
219974
|
-
* The stableId of an NVR / Home Hub candidate. UID first (it survives a DHCP lease change), host
|
|
219975
|
-
* second. The boot migration derives the SAME id for an old Hub device, so the integration it
|
|
219976
|
-
* creates hides this candidate through `originStableId`.
|
|
219977
|
-
*/
|
|
219978
|
-
function reolinkHubStableId(identity) {
|
|
219979
|
-
const uid = identity.uid?.trim() ?? "";
|
|
219980
|
-
if (uid !== "") return `reolink-hub-uid-${slugifyReolinkHost(uid)}`;
|
|
219981
|
-
return `reolink-hub-host-${slugifyReolinkHost(identity.host)}`;
|
|
219982
|
-
}
|
|
219983
|
-
function isKnownHub(d, known) {
|
|
219984
|
-
return known.some((k) => k.uid !== void 0 && k.uid !== "" && k.uid === d.uid || k.host !== void 0 && k.host !== "" && k.host === d.host);
|
|
219985
|
-
}
|
|
219986
|
-
/**
|
|
219987
|
-
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
219988
|
-
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
219989
|
-
* first responder for a host wins. Pure: no I/O.
|
|
219990
|
-
*
|
|
219991
|
-
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
219992
|
-
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
219993
|
-
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
219994
|
-
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
219995
|
-
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
219996
|
-
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
219997
|
-
* never a scan result.
|
|
219998
|
-
*
|
|
219999
|
-
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
220000
|
-
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
220001
|
-
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
220002
|
-
* in the first place, so a scan says so instead of offering it again.
|
|
220003
|
-
*/
|
|
220004
|
-
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = [], hubIntegrations = []) {
|
|
220005
|
-
const username = credentials.username?.trim() ?? "";
|
|
220006
|
-
const password = credentials.password ?? "";
|
|
220007
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
220008
|
-
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
220009
|
-
return [...byHost.values()].flatMap((d) => {
|
|
220010
|
-
const displayName = d.name ?? d.model ?? d.host;
|
|
220011
|
-
if (looksLikeReolinkHub(d.model)) {
|
|
220012
|
-
if (isKnownHub(d, hubIntegrations)) return [];
|
|
220013
|
-
return [{
|
|
220014
|
-
stableId: reolinkHubStableId(d),
|
|
220015
|
-
type: DeviceType.Gateway,
|
|
220016
|
-
suggestedName: displayName,
|
|
220017
|
-
prefilledConfig: {
|
|
220018
|
-
host: d.host,
|
|
220019
|
-
port: HUB_BAICHUAN_PORT,
|
|
220020
|
-
...d.uid ? { uid: d.uid } : {},
|
|
220021
|
-
...username ? { username } : {}
|
|
220022
|
-
},
|
|
220023
|
-
setupIntegration: true
|
|
220024
|
-
}];
|
|
220025
|
-
}
|
|
220026
|
-
const match = findOnboardedMatch(d, onboarded);
|
|
220027
|
-
return [{
|
|
220028
|
-
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
220029
|
-
type: DeviceType.Camera,
|
|
220030
|
-
suggestedName: displayName,
|
|
220031
|
-
prefilledConfig: {
|
|
220032
|
-
name: displayName,
|
|
220033
|
-
host: d.host,
|
|
220034
|
-
transport: "auto",
|
|
220035
|
-
...d.uid ? { uid: d.uid } : {},
|
|
220036
|
-
...username ? { username } : {},
|
|
220037
|
-
...password ? { password } : {}
|
|
220038
|
-
},
|
|
220039
|
-
...match !== void 0 ? {
|
|
220040
|
-
alreadyOnboarded: true,
|
|
220041
|
-
onboardedDeviceId: match.deviceId,
|
|
220042
|
-
onboardedName: match.name
|
|
220043
|
-
} : {}
|
|
220044
|
-
}];
|
|
220045
|
-
});
|
|
220046
|
-
}
|
|
220047
|
-
//#endregion
|
|
220048
220588
|
//#region src/reolink-gateway-device.ts
|
|
220049
220589
|
var reolinkGatewayDeviceSchema = object({
|
|
220050
220590
|
integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
|
|
@@ -220201,158 +220741,6 @@ function createReolinkHubManager(deps, registry) {
|
|
|
220201
220741
|
connect: connectReolinkHub
|
|
220202
220742
|
});
|
|
220203
220743
|
}
|
|
220204
|
-
function positivePort(value) {
|
|
220205
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
220206
|
-
}
|
|
220207
|
-
var CONNECTION_FIELDS = [
|
|
220208
|
-
"host",
|
|
220209
|
-
"port",
|
|
220210
|
-
"username",
|
|
220211
|
-
"transport"
|
|
220212
|
-
];
|
|
220213
|
-
function str(value) {
|
|
220214
|
-
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
220215
|
-
}
|
|
220216
|
-
function migratedChildConfig(child, hub, integrationId) {
|
|
220217
|
-
const filled = {};
|
|
220218
|
-
for (const key of CONNECTION_FIELDS) {
|
|
220219
|
-
const own = child[key];
|
|
220220
|
-
const inherited = hub[key];
|
|
220221
|
-
if ((own === void 0 || own === null || own === "") && inherited !== void 0) filled[key] = inherited;
|
|
220222
|
-
}
|
|
220223
|
-
return {
|
|
220224
|
-
...child,
|
|
220225
|
-
...filled,
|
|
220226
|
-
integrationId
|
|
220227
|
-
};
|
|
220228
|
-
}
|
|
220229
|
-
async function migrateOneHub(hub, children, integrations, ports) {
|
|
220230
|
-
const read = readHubConnection(hub.config);
|
|
220231
|
-
if (read.kind !== "ok") throw new Error(`hub connection unusable (${read.kind}) — left in place`);
|
|
220232
|
-
const { connection } = read;
|
|
220233
|
-
const uid = str(hub.config["uid"]);
|
|
220234
|
-
const integrationId = integrations.find((i) => i.addonId === ports.addonId && i.info["migratedFromHubDeviceId"] === hub.id)?.id ?? (await ports.createIntegration({
|
|
220235
|
-
addonId: ports.addonId,
|
|
220236
|
-
name: hub.name,
|
|
220237
|
-
info: { ["migratedFromHubDeviceId"]: hub.id },
|
|
220238
|
-
originStableId: reolinkHubStableId({
|
|
220239
|
-
...uid !== void 0 ? { uid } : {},
|
|
220240
|
-
host: connection.host
|
|
220241
|
-
}),
|
|
220242
|
-
settings: {
|
|
220243
|
-
host: connection.host,
|
|
220244
|
-
port: connection.port,
|
|
220245
|
-
rtspPort: positivePort(hub.config["rtspPort"]) ?? connection.rtspPort,
|
|
220246
|
-
username: connection.username,
|
|
220247
|
-
password: connection.password,
|
|
220248
|
-
transport: connection.transport,
|
|
220249
|
-
...uid !== void 0 ? { uid } : {},
|
|
220250
|
-
debugGeneral: connection.debugGeneral,
|
|
220251
|
-
debugSocketLogs: connection.debugSocketLogs
|
|
220252
|
-
},
|
|
220253
|
-
secretKeys: ["password"]
|
|
220254
|
-
})).id;
|
|
220255
|
-
const rewritten = [];
|
|
220256
|
-
for (const child of children) {
|
|
220257
|
-
const config = migratedChildConfig(child.config, hub.config, integrationId);
|
|
220258
|
-
await ports.persistConfig(child.stableId, config);
|
|
220259
|
-
await ports.setIntegrationId(child.id, integrationId);
|
|
220260
|
-
rewritten.push({
|
|
220261
|
-
...child,
|
|
220262
|
-
config,
|
|
220263
|
-
parentDeviceId: null
|
|
220264
|
-
});
|
|
220265
|
-
}
|
|
220266
|
-
return {
|
|
220267
|
-
rows: rewritten,
|
|
220268
|
-
retirement: {
|
|
220269
|
-
hubDeviceId: hub.id,
|
|
220270
|
-
hubName: hub.name,
|
|
220271
|
-
integrationId
|
|
220272
|
-
}
|
|
220273
|
-
};
|
|
220274
|
-
}
|
|
220275
|
-
/** Plan and persist the migration of every Hub row in `saved`. Never throws. */
|
|
220276
|
-
async function planHubMigration(saved, ports) {
|
|
220277
|
-
const hubs = saved.filter((row) => row.type === DeviceType.Hub);
|
|
220278
|
-
if (hubs.length === 0) return {
|
|
220279
|
-
rows: saved,
|
|
220280
|
-
retirements: []
|
|
220281
|
-
};
|
|
220282
|
-
let integrations;
|
|
220283
|
-
try {
|
|
220284
|
-
integrations = await ports.listIntegrations();
|
|
220285
|
-
} catch (err) {
|
|
220286
|
-
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
220287
|
-
return {
|
|
220288
|
-
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
220289
|
-
retirements: []
|
|
220290
|
-
};
|
|
220291
|
-
}
|
|
220292
|
-
const hubIds = new Set(hubs.map((h) => h.id));
|
|
220293
|
-
const rewrittenById = /* @__PURE__ */ new Map();
|
|
220294
|
-
const retirements = [];
|
|
220295
|
-
for (const hub of hubs) {
|
|
220296
|
-
const children = saved.filter((row) => row.parentDeviceId === hub.id);
|
|
220297
|
-
try {
|
|
220298
|
-
const done = await migrateOneHub(hub, children, integrations, ports);
|
|
220299
|
-
for (const row of done.rows) rewrittenById.set(row.id, row);
|
|
220300
|
-
retirements.push(done.retirement);
|
|
220301
|
-
ports.logger.info("Reolink hub migrated to an integration", {
|
|
220302
|
-
tags: { deviceId: hub.id },
|
|
220303
|
-
meta: {
|
|
220304
|
-
integrationId: done.retirement.integrationId,
|
|
220305
|
-
children: children.length,
|
|
220306
|
-
name: hub.name
|
|
220307
|
-
}
|
|
220308
|
-
});
|
|
220309
|
-
} catch (err) {
|
|
220310
|
-
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
220311
|
-
tags: { deviceId: hub.id },
|
|
220312
|
-
meta: {
|
|
220313
|
-
error: errMsg$3(err),
|
|
220314
|
-
name: hub.name
|
|
220315
|
-
}
|
|
220316
|
-
});
|
|
220317
|
-
}
|
|
220318
|
-
}
|
|
220319
|
-
return {
|
|
220320
|
-
rows: saved.filter((row) => !hubIds.has(row.id)).map((row) => rewrittenById.get(row.id) ?? row),
|
|
220321
|
-
retirements
|
|
220322
|
-
};
|
|
220323
|
-
}
|
|
220324
|
-
/**
|
|
220325
|
-
* Retire the hubs whose children all restored. A hub that still has a child in the store is left
|
|
220326
|
-
* and named: removing it would cascade the camera away with it.
|
|
220327
|
-
*/
|
|
220328
|
-
async function retireMigratedHubs(retirements, ports) {
|
|
220329
|
-
let retired = 0;
|
|
220330
|
-
for (const r of retirements) try {
|
|
220331
|
-
const remaining = await ports.childIdsOf(r.hubDeviceId);
|
|
220332
|
-
if (remaining.length > 0) {
|
|
220333
|
-
ports.logger.warn("Reolink hub kept — a child did not restore, retiring it would remove it", {
|
|
220334
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220335
|
-
meta: {
|
|
220336
|
-
remaining: [...remaining],
|
|
220337
|
-
integrationId: r.integrationId
|
|
220338
|
-
}
|
|
220339
|
-
});
|
|
220340
|
-
continue;
|
|
220341
|
-
}
|
|
220342
|
-
await ports.removeDevice(r.hubDeviceId);
|
|
220343
|
-
retired++;
|
|
220344
|
-
ports.logger.info("Reolink hub device retired", {
|
|
220345
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220346
|
-
meta: { integrationId: r.integrationId }
|
|
220347
|
-
});
|
|
220348
|
-
} catch (err) {
|
|
220349
|
-
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
220350
|
-
tags: { deviceId: r.hubDeviceId },
|
|
220351
|
-
meta: { error: errMsg$3(err) }
|
|
220352
|
-
});
|
|
220353
|
-
}
|
|
220354
|
-
return retired;
|
|
220355
|
-
}
|
|
220356
220744
|
//#endregion
|
|
220357
220745
|
//#region src/reolink-subnet-scan.ts
|
|
220358
220746
|
/**
|
|
@@ -221171,7 +221559,11 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221171
221559
|
this.wireClipCatalogRefreshers();
|
|
221172
221560
|
await this.serveClipPlanes(this.clipService);
|
|
221173
221561
|
await this.serveWebhookPlane();
|
|
221174
|
-
await this.runMigrations(defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts())
|
|
221562
|
+
await this.runMigrations([...defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()), ...defineAddonMigrations([REOLINK_CLAIM_MIGRATION], claimDevicesPortsOver({
|
|
221563
|
+
api: defaultIntegrationApiOver(this.ctx.api),
|
|
221564
|
+
info: (message) => this.ctx.logger.info(message),
|
|
221565
|
+
warnDevice: (message, deviceId) => this.ctx.logger.warn(message, { tags: { deviceId } })
|
|
221566
|
+
}))]);
|
|
221175
221567
|
this.wireEmailBridge();
|
|
221176
221568
|
const wireSleepPushResolvers = (event, via) => {
|
|
221177
221569
|
const data = event.data;
|