@camstack/addon-provider-homeassistant 1.2.639 → 1.2.640
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 +7 -0
- package/dist/addon.js +529 -460
- package/dist/addon.mjs +85 -16
- package/dist/{dist-C_XoeULr.js → ha-broker-source-0HGmZj7D.js} +2225 -853
- package/dist/{dist-Cvz76AD1.mjs → ha-broker-source-FHFpUlgq.mjs} +2208 -854
- package/dist/ha-export/ha-export.addon.js +209 -86
- package/dist/ha-export/ha-export.addon.mjs +164 -41
- package/i18n/en.json +2 -2
- package/i18n/it.json +2 -2
- package/package.json +1 -1
|
@@ -5588,7 +5588,7 @@ var ZodIssueCode = {
|
|
|
5588
5588
|
var ZodFirstPartyTypeKind;
|
|
5589
5589
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5590
5590
|
//#endregion
|
|
5591
|
-
//#region ../types/dist/sleep-
|
|
5591
|
+
//#region ../types/dist/sleep-CeyhrcOk.mjs
|
|
5592
5592
|
/**
|
|
5593
5593
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5594
5594
|
* window to float samples (D455).
|
|
@@ -7655,6 +7655,7 @@ var BaseAddon = class {
|
|
|
7655
7655
|
* a warning and falls back rather than crashing boot.
|
|
7656
7656
|
*/
|
|
7657
7657
|
state(key, schema, fallback) {
|
|
7658
|
+
this._declaredStateKeys.add(key);
|
|
7658
7659
|
return createDurableState({
|
|
7659
7660
|
key,
|
|
7660
7661
|
schema,
|
|
@@ -7670,6 +7671,96 @@ var BaseAddon = class {
|
|
|
7670
7671
|
} })
|
|
7671
7672
|
});
|
|
7672
7673
|
}
|
|
7674
|
+
/** Durable-state keys this process registered through {@link state}. */
|
|
7675
|
+
_declaredStateKeys = /* @__PURE__ */ new Set();
|
|
7676
|
+
/**
|
|
7677
|
+
* Stale-data audit: every global-settings key this addon still declares —
|
|
7678
|
+
* constructor defaults, settings-schema fields (layout containers included),
|
|
7679
|
+
* durable-state keys and the root of each `secretStatePaths` entry. Answered
|
|
7680
|
+
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
7681
|
+
*/
|
|
7682
|
+
async getDeclaredSettingsKeys() {
|
|
7683
|
+
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
7684
|
+
}
|
|
7685
|
+
declaredKeySet() {
|
|
7686
|
+
const keys = new Set(Object.keys(this.defaults));
|
|
7687
|
+
const schema = this.globalSettingsSchema();
|
|
7688
|
+
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
7689
|
+
for (const key of this._declaredStateKeys) keys.add(key);
|
|
7690
|
+
for (const path of this.secretStatePaths) {
|
|
7691
|
+
const root = path.split(/[.[]/, 1)[0];
|
|
7692
|
+
if (root) keys.add(root);
|
|
7693
|
+
}
|
|
7694
|
+
return keys;
|
|
7695
|
+
}
|
|
7696
|
+
/**
|
|
7697
|
+
* Stale-data audit: remove stored global-settings keys the addon no longer
|
|
7698
|
+
* declares, and clear the keychain values their markers held — except a value
|
|
7699
|
+
* a surviving key still points at. A declared or absent key is refused.
|
|
7700
|
+
*/
|
|
7701
|
+
async unsetStoredSettingsKeys(keys) {
|
|
7702
|
+
const settings = this._ctx?.settings;
|
|
7703
|
+
const raw = this.rawSettings();
|
|
7704
|
+
if (!settings || !raw) return {
|
|
7705
|
+
removed: [],
|
|
7706
|
+
refused: keys.map((key) => ({
|
|
7707
|
+
key,
|
|
7708
|
+
reason: "no settings store"
|
|
7709
|
+
})),
|
|
7710
|
+
secretsCleared: 0
|
|
7711
|
+
};
|
|
7712
|
+
let stored;
|
|
7713
|
+
try {
|
|
7714
|
+
stored = await raw.readAddonStore();
|
|
7715
|
+
} catch (error) {
|
|
7716
|
+
const reason = `store unreadable: ${error instanceof Error ? error.message : "unknown"}`;
|
|
7717
|
+
return {
|
|
7718
|
+
removed: [],
|
|
7719
|
+
refused: keys.map((key) => ({
|
|
7720
|
+
key,
|
|
7721
|
+
reason
|
|
7722
|
+
})),
|
|
7723
|
+
secretsCleared: 0
|
|
7724
|
+
};
|
|
7725
|
+
}
|
|
7726
|
+
const declared = this.declaredKeySet();
|
|
7727
|
+
const removed = [];
|
|
7728
|
+
const refused = [];
|
|
7729
|
+
for (const key of keys) {
|
|
7730
|
+
const base = key.split("@", 1)[0] ?? key;
|
|
7731
|
+
if (declared.has(base)) refused.push({
|
|
7732
|
+
key,
|
|
7733
|
+
reason: "still declared by the addon"
|
|
7734
|
+
});
|
|
7735
|
+
else if (!(key in stored)) refused.push({
|
|
7736
|
+
key,
|
|
7737
|
+
reason: "not stored"
|
|
7738
|
+
});
|
|
7739
|
+
else removed.push(key);
|
|
7740
|
+
}
|
|
7741
|
+
if (removed.length === 0) return {
|
|
7742
|
+
removed,
|
|
7743
|
+
refused,
|
|
7744
|
+
secretsCleared: 0
|
|
7745
|
+
};
|
|
7746
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7747
|
+
const held = /* @__PURE__ */ new Set();
|
|
7748
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
7749
|
+
const target = removed.includes(key) ? held : kept;
|
|
7750
|
+
for (const ref of collectSecretMarkerRefs(value)) target.add(ref);
|
|
7751
|
+
}
|
|
7752
|
+
const refsToClear = [...held].filter((ref) => !kept.has(ref));
|
|
7753
|
+
const patch = {};
|
|
7754
|
+
for (const key of removed) patch[key] = void 0;
|
|
7755
|
+
await settings.writeAddonStore(patch);
|
|
7756
|
+
const secretsCleared = refsToClear.length > 0 && this._ctx?.secrets ? await this._ctx.secrets.delete(refsToClear) : 0;
|
|
7757
|
+
await this.resolveConfig();
|
|
7758
|
+
return {
|
|
7759
|
+
removed,
|
|
7760
|
+
refused,
|
|
7761
|
+
secretsCleared
|
|
7762
|
+
};
|
|
7763
|
+
}
|
|
7673
7764
|
/** Per-device variant of {@link state}, backed by the per-device store. */
|
|
7674
7765
|
deviceState(deviceId, key, schema, fallback) {
|
|
7675
7766
|
return createDurableState({
|
|
@@ -7940,6 +8031,21 @@ function dropPerNodeFields(fields) {
|
|
|
7940
8031
|
}
|
|
7941
8032
|
return kept;
|
|
7942
8033
|
}
|
|
8034
|
+
function collectFieldKeys(fields) {
|
|
8035
|
+
const collected = [];
|
|
8036
|
+
for (const field of fields) {
|
|
8037
|
+
if (field.type === "group") {
|
|
8038
|
+
collected.push(...collectFieldKeys(field.fields));
|
|
8039
|
+
continue;
|
|
8040
|
+
}
|
|
8041
|
+
if (field.type === "sub-tabs") {
|
|
8042
|
+
for (const tab of field.tabs) collected.push(...collectFieldKeys(tab.fields));
|
|
8043
|
+
continue;
|
|
8044
|
+
}
|
|
8045
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
8046
|
+
}
|
|
8047
|
+
return collected;
|
|
8048
|
+
}
|
|
7943
8049
|
function collectPerNodeFieldKeys(fields) {
|
|
7944
8050
|
const collected = [];
|
|
7945
8051
|
for (const field of fields) {
|
|
@@ -9172,6 +9278,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9172
9278
|
"recording-signal",
|
|
9173
9279
|
"records",
|
|
9174
9280
|
"remote",
|
|
9281
|
+
"remote-layout-components",
|
|
9175
9282
|
"running-state",
|
|
9176
9283
|
"scene-monitor",
|
|
9177
9284
|
"script-runner",
|
|
@@ -11017,7 +11124,7 @@ object({
|
|
|
11017
11124
|
params: record(string(), union([string(), number()])).optional()
|
|
11018
11125
|
});
|
|
11019
11126
|
//#endregion
|
|
11020
|
-
//#region ../types/dist/err-msg-
|
|
11127
|
+
//#region ../types/dist/err-msg-BD5buxvT.mjs
|
|
11021
11128
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
11022
11129
|
DeviceType["Camera"] = "camera";
|
|
11023
11130
|
DeviceType["Hub"] = "hub";
|
|
@@ -11123,6 +11230,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
11123
11230
|
DeviceType["Washer"] = "washer";
|
|
11124
11231
|
/** Tumble dryer. Same plug-built running surface as {@link Washer}. */
|
|
11125
11232
|
DeviceType["Dryer"] = "dryer";
|
|
11233
|
+
/** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
|
|
11234
|
+
* other radio-to-IP bridges). It carries the box's own facts (firmware,
|
|
11235
|
+
* address, connection state) and the actions the box really supports
|
|
11236
|
+
* (`reboot`, `device-admin-link`). Products are NOT its children — they are
|
|
11237
|
+
* adopted beside it — and its connection settings stay on the integration. */
|
|
11238
|
+
DeviceType["Gateway"] = "gateway";
|
|
11126
11239
|
return DeviceType;
|
|
11127
11240
|
}({});
|
|
11128
11241
|
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
@@ -11383,7 +11496,7 @@ function errMsg(err) {
|
|
|
11383
11496
|
return String(err);
|
|
11384
11497
|
}
|
|
11385
11498
|
//#endregion
|
|
11386
|
-
//#region ../types/dist/composition-
|
|
11499
|
+
//#region ../types/dist/composition-C7WdThmP.mjs
|
|
11387
11500
|
/**
|
|
11388
11501
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11389
11502
|
*
|
|
@@ -16044,246 +16157,630 @@ object({
|
|
|
16044
16157
|
path: ["expr"]
|
|
16045
16158
|
});
|
|
16046
16159
|
});
|
|
16047
|
-
/** How a leaf compares a device field to a value. Derived from the field's
|
|
16048
|
-
* `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16049
|
-
var AutomationConditionOperatorSchema = _enum([
|
|
16050
|
-
"eq",
|
|
16051
|
-
"ne",
|
|
16052
|
-
"gt",
|
|
16053
|
-
"gte",
|
|
16054
|
-
"lt",
|
|
16055
|
-
"lte",
|
|
16056
|
-
"contains",
|
|
16057
|
-
"in"
|
|
16058
|
-
]);
|
|
16059
|
-
var AutomationConditionLeafSchema = object({
|
|
16060
|
-
kind: literal("condition"),
|
|
16061
|
-
deviceId: number().int().nonnegative(),
|
|
16062
|
-
cap: string().min(1),
|
|
16063
|
-
fieldPath: string().min(1),
|
|
16064
|
-
operator: AutomationConditionOperatorSchema,
|
|
16065
|
-
value: union([
|
|
16066
|
-
string(),
|
|
16067
|
-
number(),
|
|
16068
|
-
boolean(),
|
|
16069
|
-
array(union([string(), number()]))
|
|
16070
|
-
])
|
|
16071
|
-
});
|
|
16072
|
-
var AutomationConditionStationaryPresentSchema = object({
|
|
16073
|
-
kind: literal("stationary-present"),
|
|
16074
|
-
deviceId: number().int().nonnegative(),
|
|
16075
|
-
/** Class names; a macro class matches every raw class that maps to it. */
|
|
16076
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16077
|
-
/** The parked object must be in at least one of these zones. */
|
|
16078
|
-
zones: array(string().min(1)).min(1).optional()
|
|
16079
|
-
});
|
|
16080
16160
|
/**
|
|
16081
|
-
*
|
|
16082
|
-
*
|
|
16083
|
-
*
|
|
16084
|
-
*
|
|
16085
|
-
*
|
|
16161
|
+
* Identity envelope for a device's upstream-system metadata.
|
|
16162
|
+
*
|
|
16163
|
+
* Two jobs:
|
|
16164
|
+
*
|
|
16165
|
+
* 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
|
|
16166
|
+
* uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
|
|
16167
|
+
* WS-Discovery endpoint UUID, …). Provider code keeps a
|
|
16168
|
+
* `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
|
|
16169
|
+
* without round-tripping through `stableId` / `id` mappings.
|
|
16170
|
+
*
|
|
16171
|
+
* 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
|
|
16172
|
+
* one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
|
|
16173
|
+
* for vendors where MAC is the natural key). For systems where the
|
|
16174
|
+
* live dispatch key can be renamed (HA `entity_id` can be edited),
|
|
16175
|
+
* providers detect renames by matching `uniqueId` and patch `id`
|
|
16176
|
+
* in-place so dispatch keeps working.
|
|
16177
|
+
*
|
|
16178
|
+
* **Display unit and precision are NOT stored here.** They are a read-time
|
|
16179
|
+
* preference (D944). A polymorphic reading stores its `quantity` here once,
|
|
16180
|
+
* at adopt, and a date/time value stores `dateTimeShape` the same way —
|
|
16181
|
+
* neither is repeated on each sample. The `device_class` tag is an
|
|
16182
|
+
* HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
|
|
16183
|
+
* to derive the CamStack `DeviceRole`; it is never stored after that point.
|
|
16184
|
+
*
|
|
16185
|
+
* The `raw` escape hatch carries provider-specific attributes that don't
|
|
16186
|
+
* fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
|
|
16187
|
+
* details, …). Providers control what lands here.
|
|
16086
16188
|
*/
|
|
16087
|
-
var
|
|
16088
|
-
|
|
16089
|
-
|
|
16090
|
-
|
|
16091
|
-
|
|
16092
|
-
|
|
16093
|
-
|
|
16094
|
-
|
|
16095
|
-
|
|
16096
|
-
|
|
16097
|
-
|
|
16189
|
+
var SourceInfoSchema = object({
|
|
16190
|
+
/** Live dispatch key — mutable when the source system allows rename. */
|
|
16191
|
+
id: string(),
|
|
16192
|
+
/** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
|
|
16193
|
+
system: string(),
|
|
16194
|
+
/** Immutable upstream identifier when available (HA `unique_id`, … ).
|
|
16195
|
+
* Used to detect rename when `id` changes. */
|
|
16196
|
+
uniqueId: string().optional(),
|
|
16197
|
+
/** Quantity of a polymorphic reading, written once at adopt (D944). */
|
|
16198
|
+
quantity: ReadingQuantitySchema.optional(),
|
|
16199
|
+
/** Date / time / datetime shape of a string reading, written once at adopt. */
|
|
16200
|
+
dateTimeShape: _enum([
|
|
16201
|
+
"date",
|
|
16202
|
+
"time",
|
|
16203
|
+
"datetime"
|
|
16204
|
+
]).optional(),
|
|
16205
|
+
/** Provider-specific extras that don't fit the structured slots.
|
|
16206
|
+
* Whitelist what goes here per provider — do NOT dump entire HA
|
|
16207
|
+
* attribute blobs (would explode the meta JSON column). */
|
|
16208
|
+
raw: record(string(), unknown()).optional()
|
|
16098
16209
|
});
|
|
16099
|
-
var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
16100
|
-
object({
|
|
16101
|
-
kind: literal("all"),
|
|
16102
|
-
children: array(AutomationConditionSchema)
|
|
16103
|
-
}),
|
|
16104
|
-
object({
|
|
16105
|
-
kind: literal("any"),
|
|
16106
|
-
children: array(AutomationConditionSchema)
|
|
16107
|
-
}),
|
|
16108
|
-
object({
|
|
16109
|
-
kind: literal("not"),
|
|
16110
|
-
child: AutomationConditionSchema
|
|
16111
|
-
}),
|
|
16112
|
-
AutomationConditionLeafSchema,
|
|
16113
|
-
AutomationConditionExpressionSchema,
|
|
16114
|
-
AutomationConditionStationaryPresentSchema
|
|
16115
|
-
]));
|
|
16116
16210
|
/**
|
|
16117
|
-
*
|
|
16118
|
-
*
|
|
16119
|
-
*
|
|
16120
|
-
*
|
|
16121
|
-
*
|
|
16122
|
-
*
|
|
16123
|
-
* already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
|
|
16124
|
-
* not.
|
|
16211
|
+
* Build the synthetic SourceInfo every existing provider falls back to
|
|
16212
|
+
* when no upstream value has been persisted. Keeps non-migrated providers
|
|
16213
|
+
* (Reolink / Hikvision / ONVIF / RTSP) functional without code
|
|
16214
|
+
* changes — `id` reuses the CamStack stableId, `system` reuses the addon
|
|
16215
|
+
* id. Real upstream identity replaces this once a provider migrates and
|
|
16216
|
+
* calls `updateSourceInfo()` with concrete values.
|
|
16125
16217
|
*/
|
|
16126
|
-
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
|
|
16132
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16133
|
-
becomes: union([
|
|
16134
|
-
string(),
|
|
16135
|
-
number(),
|
|
16136
|
-
boolean()
|
|
16137
|
-
]).optional(),
|
|
16138
|
-
/** Only on a CHANGE of value, not on every re-report. */
|
|
16139
|
-
edge: boolean().optional(),
|
|
16140
|
-
/** The condition must hold this long before the run starts. */
|
|
16141
|
-
forMs: number().int().min(0).max(864e5).optional(),
|
|
16142
|
-
/** Collapse a burst into one run. */
|
|
16143
|
-
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16144
|
-
}),
|
|
16145
|
-
object({
|
|
16146
|
-
kind: literal("device-event"),
|
|
16147
|
-
/** An `EventCategory` value. */
|
|
16148
|
-
category: string().min(1),
|
|
16149
|
-
deviceId: number().int().nonnegative().optional()
|
|
16150
|
-
}),
|
|
16151
|
-
object({
|
|
16152
|
-
kind: literal("schedule"),
|
|
16153
|
-
cron: string().min(1).max(120)
|
|
16154
|
-
}),
|
|
16155
|
-
object({ kind: literal("manual") }),
|
|
16156
|
-
object({
|
|
16157
|
-
kind: literal("detection"),
|
|
16158
|
-
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
16159
|
-
* matches every raw class that maps to it; any other name matches
|
|
16160
|
-
* exactly. */
|
|
16161
|
-
classes: array(string().min(1)).min(1),
|
|
16162
|
-
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16163
|
-
/** The track must have visited at least one of these zones. */
|
|
16164
|
-
zones: array(string().min(1)).min(1).optional(),
|
|
16165
|
-
/** Best-so-far confidence the track must reach. */
|
|
16166
|
-
minScore: number().min(0).max(1).optional()
|
|
16167
|
-
}),
|
|
16168
|
-
object({
|
|
16169
|
-
kind: literal("stationary"),
|
|
16170
|
-
deviceIds: array(number().int().nonnegative()).min(1),
|
|
16171
|
-
/** Class names; a macro class matches like the detection trigger. */
|
|
16172
|
-
classes: array(string().min(1)).min(1).optional(),
|
|
16173
|
-
/** The parked object must be in at least one of these zones. */
|
|
16174
|
-
zones: array(string().min(1)).min(1).optional(),
|
|
16175
|
-
/** `appeared`: it became parked. `departed`: the registry retired it.
|
|
16176
|
-
* The vocabulary of `PipelineAnalyticsStationaryChanged`. */
|
|
16177
|
-
phase: _enum(["appeared", "departed"])
|
|
16178
|
-
}),
|
|
16179
|
-
object({
|
|
16180
|
-
kind: literal("interval"),
|
|
16181
|
-
minutes: number().int().min(1).max(1440)
|
|
16182
|
-
})
|
|
16183
|
-
]);
|
|
16218
|
+
function synthesizeSourceInfo(input) {
|
|
16219
|
+
return {
|
|
16220
|
+
id: input.stableId,
|
|
16221
|
+
system: input.addonId
|
|
16222
|
+
};
|
|
16223
|
+
}
|
|
16184
16224
|
/**
|
|
16185
|
-
*
|
|
16225
|
+
* Shallow-merge a partial patch over a prior SourceInfo. `undefined`
|
|
16226
|
+
* patch values are ignored (no field clobber); to clear a field
|
|
16227
|
+
* explicitly, providers omit it from the patch and rebuild from scratch
|
|
16228
|
+
* via `updateSourceInfo({})` cycles — uncommon enough that we don't
|
|
16229
|
+
* bother with a `null`-means-delete sentinel.
|
|
16186
16230
|
*
|
|
16187
|
-
* `
|
|
16188
|
-
*
|
|
16189
|
-
|
|
16190
|
-
|
|
16231
|
+
* The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
|
|
16232
|
+
* merges over existing `raw` keys instead of replacing the whole bag.
|
|
16233
|
+
*/
|
|
16234
|
+
function mergeSourceInfo(prev, patch) {
|
|
16235
|
+
const next = { ...prev };
|
|
16236
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
16237
|
+
if (v === void 0) continue;
|
|
16238
|
+
if (k === "raw" && typeof v === "object" && v !== null) {
|
|
16239
|
+
next.raw = {
|
|
16240
|
+
...prev.raw,
|
|
16241
|
+
...v
|
|
16242
|
+
};
|
|
16243
|
+
continue;
|
|
16244
|
+
}
|
|
16245
|
+
next[k] = v;
|
|
16246
|
+
}
|
|
16247
|
+
return next;
|
|
16248
|
+
}
|
|
16249
|
+
/**
|
|
16250
|
+
* Coerce common unit-of-measurement string variants to a canonical form
|
|
16251
|
+
* so the UI's display templates can rely on stable strings. Pure
|
|
16252
|
+
* passthrough for unknown units — kept conservative on purpose, the goal
|
|
16253
|
+
* is to normalise the long tail of HA `unit_of_measurement` values that
|
|
16254
|
+
* have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
|
|
16191
16255
|
*
|
|
16192
|
-
*
|
|
16193
|
-
*
|
|
16194
|
-
*
|
|
16195
|
-
* NC action can never target the device that triggered it — which is fine for
|
|
16196
|
-
* the NC (its rules already scope to a device) and fatal for an automation
|
|
16197
|
-
* ("sound the siren of the camera that saw the person"). `deviceId` therefore
|
|
16198
|
-
* also accepts `{ $var }`, resolved from the run's `vars` bag BEFORE the runner
|
|
16199
|
-
* is called. The runner still receives a number and is untouched; the
|
|
16200
|
-
* resolution is the recipe's job, not the runner's.
|
|
16256
|
+
* Extend the map as we encounter real-world data. Returning the raw
|
|
16257
|
+
* input for unknown values is intentional: providers should never
|
|
16258
|
+
* lose information silently here.
|
|
16201
16259
|
*/
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
|
|
16205
|
-
|
|
16206
|
-
|
|
16207
|
-
|
|
16208
|
-
|
|
16209
|
-
|
|
16210
|
-
|
|
16211
|
-
|
|
16212
|
-
|
|
16213
|
-
|
|
16214
|
-
|
|
16215
|
-
|
|
16216
|
-
|
|
16217
|
-
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16260
|
+
function normalizeUnit(raw) {
|
|
16261
|
+
if (raw == null) return void 0;
|
|
16262
|
+
const trimmed = raw.trim();
|
|
16263
|
+
if (trimmed === "") return void 0;
|
|
16264
|
+
const lower = trimmed.toLowerCase();
|
|
16265
|
+
if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
|
|
16266
|
+
if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
|
|
16267
|
+
if (lower === "k" || lower === "kelvin") return "K";
|
|
16268
|
+
if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
|
|
16269
|
+
if (lower === "lx" || lower === "lux") return "lx";
|
|
16270
|
+
if (lower === "hpa" || lower === "hectopascal") return "hPa";
|
|
16271
|
+
if (lower === "kpa") return "kPa";
|
|
16272
|
+
if (lower === "pa") return "Pa";
|
|
16273
|
+
if (lower === "mbar" || lower === "millibar") return "mbar";
|
|
16274
|
+
if (lower === "w" || lower === "watt") return "W";
|
|
16275
|
+
if (lower === "kw" || lower === "kilowatt") return "kW";
|
|
16276
|
+
if (lower === "wh" || lower === "watt-hour") return "Wh";
|
|
16277
|
+
if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
|
|
16278
|
+
if (lower === "v" || lower === "volt") return "V";
|
|
16279
|
+
if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
|
|
16280
|
+
if (lower === "ma" || lower === "milliampere") return "mA";
|
|
16281
|
+
if (lower === "va") return "VA";
|
|
16282
|
+
if (lower === "ppm") return "ppm";
|
|
16283
|
+
if (lower === "ppb") return "ppb";
|
|
16284
|
+
if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
|
|
16285
|
+
if (lower === "km/h" || lower === "kph") return "km/h";
|
|
16286
|
+
if (lower === "mph") return "mph";
|
|
16287
|
+
if (lower === "m/s") return "m/s";
|
|
16288
|
+
if (lower === "m") return "m";
|
|
16289
|
+
if (lower === "m²" || lower === "m2") return "m²";
|
|
16290
|
+
if (lower === "mm") return "mm";
|
|
16291
|
+
if (lower === "cm") return "cm";
|
|
16292
|
+
if (lower === "km") return "km";
|
|
16293
|
+
if (lower === "in" || lower === "inch" || lower === "inches") return "in";
|
|
16294
|
+
if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
|
|
16295
|
+
if (lower === "kg") return "kg";
|
|
16296
|
+
if (lower === "g") return "g";
|
|
16297
|
+
if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
|
|
16298
|
+
if (lower === "ml") return "mL";
|
|
16299
|
+
if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
|
|
16300
|
+
if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
|
|
16301
|
+
if (lower === "ms") return "ms";
|
|
16302
|
+
if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
|
|
16303
|
+
if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
|
|
16304
|
+
if (lower === "d" || lower === "day" || lower === "days") return "d";
|
|
16305
|
+
if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
|
|
16306
|
+
if (lower === "kib") return "KiB";
|
|
16307
|
+
if (lower === "mib") return "MiB";
|
|
16308
|
+
if (lower === "gib") return "GiB";
|
|
16309
|
+
if (lower === "tib") return "TiB";
|
|
16310
|
+
if (lower === "kb") return "kB";
|
|
16311
|
+
if (lower === "mb") return "MB";
|
|
16312
|
+
if (lower === "gb") return "GB";
|
|
16313
|
+
if (lower === "dbm") return "dBm";
|
|
16314
|
+
if (lower === "dbfs") return "dBFS";
|
|
16315
|
+
if (lower === "inhg") return "inHg";
|
|
16316
|
+
if (lower === "mm/h") return "mm/h";
|
|
16317
|
+
if (lower === "w/m²" || lower === "w/m2") return "W/m²";
|
|
16318
|
+
if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
|
|
16319
|
+
if (lower === "kbps") return "kbps";
|
|
16320
|
+
if (lower === "fps") return "fps";
|
|
16321
|
+
return trimmed;
|
|
16322
|
+
}
|
|
16225
16323
|
/**
|
|
16226
|
-
*
|
|
16227
|
-
*
|
|
16324
|
+
* Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
|
|
16325
|
+
* blob is a free-form `Record<string, unknown>` shared with hardware-
|
|
16326
|
+
* identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
|
|
16327
|
+
* lives under a single nested key to keep it composable with those.
|
|
16228
16328
|
*/
|
|
16229
|
-
var
|
|
16230
|
-
var AutomationActionSchema = discriminatedUnion("kind", [
|
|
16231
|
-
AutomationWaitStepSchema,
|
|
16232
|
-
AutomationCapStepSchema,
|
|
16233
|
-
object({
|
|
16234
|
-
kind: literal("action"),
|
|
16235
|
-
/** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
|
|
16236
|
-
actionId: string().min(1),
|
|
16237
|
-
/** Values for the action's declared variables; the others take their default. */
|
|
16238
|
-
variables: record(string(), union([
|
|
16239
|
-
string(),
|
|
16240
|
-
number(),
|
|
16241
|
-
boolean()
|
|
16242
|
-
])).optional()
|
|
16243
|
-
}),
|
|
16244
|
-
object({
|
|
16245
|
-
kind: literal("code"),
|
|
16246
|
-
/** Compiled into the automation's OWN block by esbuild — not a third
|
|
16247
|
-
* runtime, not a `vm`, and not dynamically evaluated. */
|
|
16248
|
-
code: string().min(1).max(2e4)
|
|
16249
|
-
})
|
|
16250
|
-
]);
|
|
16329
|
+
var SOURCE_INFO_METADATA_KEY = "sourceInfo";
|
|
16251
16330
|
/**
|
|
16252
|
-
*
|
|
16253
|
-
*
|
|
16254
|
-
*
|
|
16331
|
+
* Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
|
|
16332
|
+
* with Zod. Returns `null` when the blob is missing or the nested
|
|
16333
|
+
* `sourceInfo` key fails validation — callers fall back to the
|
|
16334
|
+
* synthetic default in that case (see `synthesizeSourceInfo`).
|
|
16335
|
+
*/
|
|
16336
|
+
function extractSourceInfoFromMetadata(metadata) {
|
|
16337
|
+
if (!metadata) return null;
|
|
16338
|
+
const raw = metadata[SOURCE_INFO_METADATA_KEY];
|
|
16339
|
+
if (!raw) return null;
|
|
16340
|
+
const parsed = SourceInfoSchema.safeParse(raw);
|
|
16341
|
+
if (!parsed.success) return null;
|
|
16342
|
+
return parsed.data;
|
|
16343
|
+
}
|
|
16344
|
+
/**
|
|
16345
|
+
* Frozen unit → spec table. `•` in the doc marks each dimension's canonical
|
|
16346
|
+
* unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
|
|
16347
|
+
* `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
|
|
16348
|
+
* decimals). K derives from `°C = K − 273.15`.
|
|
16349
|
+
*/
|
|
16350
|
+
var UNIT_TABLE = {
|
|
16351
|
+
"°C": {
|
|
16352
|
+
dimension: "temperature",
|
|
16353
|
+
factor: 1,
|
|
16354
|
+
offset: 0
|
|
16355
|
+
},
|
|
16356
|
+
"°F": {
|
|
16357
|
+
dimension: "temperature",
|
|
16358
|
+
factor: 5 / 9,
|
|
16359
|
+
offset: -160 / 9
|
|
16360
|
+
},
|
|
16361
|
+
K: {
|
|
16362
|
+
dimension: "temperature",
|
|
16363
|
+
factor: 1,
|
|
16364
|
+
offset: -273.15
|
|
16365
|
+
},
|
|
16366
|
+
hPa: {
|
|
16367
|
+
dimension: "pressure",
|
|
16368
|
+
factor: 1,
|
|
16369
|
+
offset: 0
|
|
16370
|
+
},
|
|
16371
|
+
kPa: {
|
|
16372
|
+
dimension: "pressure",
|
|
16373
|
+
factor: 10,
|
|
16374
|
+
offset: 0
|
|
16375
|
+
},
|
|
16376
|
+
Pa: {
|
|
16377
|
+
dimension: "pressure",
|
|
16378
|
+
factor: .01,
|
|
16379
|
+
offset: 0
|
|
16380
|
+
},
|
|
16381
|
+
mbar: {
|
|
16382
|
+
dimension: "pressure",
|
|
16383
|
+
factor: 1,
|
|
16384
|
+
offset: 0
|
|
16385
|
+
},
|
|
16386
|
+
bar: {
|
|
16387
|
+
dimension: "pressure",
|
|
16388
|
+
factor: 1e3,
|
|
16389
|
+
offset: 0
|
|
16390
|
+
},
|
|
16391
|
+
inHg: {
|
|
16392
|
+
dimension: "pressure",
|
|
16393
|
+
factor: 33.8639,
|
|
16394
|
+
offset: 0
|
|
16395
|
+
},
|
|
16396
|
+
mmHg: {
|
|
16397
|
+
dimension: "pressure",
|
|
16398
|
+
factor: 1.33322,
|
|
16399
|
+
offset: 0
|
|
16400
|
+
},
|
|
16401
|
+
psi: {
|
|
16402
|
+
dimension: "pressure",
|
|
16403
|
+
factor: 68.9476,
|
|
16404
|
+
offset: 0
|
|
16405
|
+
},
|
|
16406
|
+
"m/s": {
|
|
16407
|
+
dimension: "speed",
|
|
16408
|
+
factor: 1,
|
|
16409
|
+
offset: 0
|
|
16410
|
+
},
|
|
16411
|
+
"km/h": {
|
|
16412
|
+
dimension: "speed",
|
|
16413
|
+
factor: 1 / 3.6,
|
|
16414
|
+
offset: 0
|
|
16415
|
+
},
|
|
16416
|
+
mph: {
|
|
16417
|
+
dimension: "speed",
|
|
16418
|
+
factor: .44704,
|
|
16419
|
+
offset: 0
|
|
16420
|
+
},
|
|
16421
|
+
kn: {
|
|
16422
|
+
dimension: "speed",
|
|
16423
|
+
factor: .514444,
|
|
16424
|
+
offset: 0
|
|
16425
|
+
},
|
|
16426
|
+
"mm/h": {
|
|
16427
|
+
dimension: "precipitation-rate",
|
|
16428
|
+
factor: 1,
|
|
16429
|
+
offset: 0
|
|
16430
|
+
},
|
|
16431
|
+
"in/h": {
|
|
16432
|
+
dimension: "precipitation-rate",
|
|
16433
|
+
factor: 25.4,
|
|
16434
|
+
offset: 0
|
|
16435
|
+
},
|
|
16436
|
+
m: {
|
|
16437
|
+
dimension: "length",
|
|
16438
|
+
factor: 1,
|
|
16439
|
+
offset: 0
|
|
16440
|
+
},
|
|
16441
|
+
mm: {
|
|
16442
|
+
dimension: "length",
|
|
16443
|
+
factor: .001,
|
|
16444
|
+
offset: 0
|
|
16445
|
+
},
|
|
16446
|
+
cm: {
|
|
16447
|
+
dimension: "length",
|
|
16448
|
+
factor: .01,
|
|
16449
|
+
offset: 0
|
|
16450
|
+
},
|
|
16451
|
+
km: {
|
|
16452
|
+
dimension: "length",
|
|
16453
|
+
factor: 1e3,
|
|
16454
|
+
offset: 0
|
|
16455
|
+
},
|
|
16456
|
+
in: {
|
|
16457
|
+
dimension: "length",
|
|
16458
|
+
factor: .0254,
|
|
16459
|
+
offset: 0
|
|
16460
|
+
},
|
|
16461
|
+
ft: {
|
|
16462
|
+
dimension: "length",
|
|
16463
|
+
factor: .3048,
|
|
16464
|
+
offset: 0
|
|
16465
|
+
},
|
|
16466
|
+
mi: {
|
|
16467
|
+
dimension: "length",
|
|
16468
|
+
factor: 1609.344,
|
|
16469
|
+
offset: 0
|
|
16470
|
+
},
|
|
16471
|
+
"m²": {
|
|
16472
|
+
dimension: "area",
|
|
16473
|
+
factor: 1,
|
|
16474
|
+
offset: 0
|
|
16475
|
+
},
|
|
16476
|
+
lx: {
|
|
16477
|
+
dimension: "illuminance",
|
|
16478
|
+
factor: 1,
|
|
16479
|
+
offset: 0
|
|
16480
|
+
},
|
|
16481
|
+
"W/m²": {
|
|
16482
|
+
dimension: "irradiance",
|
|
16483
|
+
factor: 1,
|
|
16484
|
+
offset: 0
|
|
16485
|
+
},
|
|
16486
|
+
W: {
|
|
16487
|
+
dimension: "power",
|
|
16488
|
+
factor: 1,
|
|
16489
|
+
offset: 0
|
|
16490
|
+
},
|
|
16491
|
+
kW: {
|
|
16492
|
+
dimension: "power",
|
|
16493
|
+
factor: 1e3,
|
|
16494
|
+
offset: 0
|
|
16495
|
+
},
|
|
16496
|
+
VA: {
|
|
16497
|
+
dimension: "apparent-power",
|
|
16498
|
+
factor: 1,
|
|
16499
|
+
offset: 0
|
|
16500
|
+
},
|
|
16501
|
+
Wh: {
|
|
16502
|
+
dimension: "energy",
|
|
16503
|
+
factor: 1,
|
|
16504
|
+
offset: 0
|
|
16505
|
+
},
|
|
16506
|
+
kWh: {
|
|
16507
|
+
dimension: "energy",
|
|
16508
|
+
factor: 1e3,
|
|
16509
|
+
offset: 0
|
|
16510
|
+
},
|
|
16511
|
+
MWh: {
|
|
16512
|
+
dimension: "energy",
|
|
16513
|
+
factor: 1e6,
|
|
16514
|
+
offset: 0
|
|
16515
|
+
},
|
|
16516
|
+
V: {
|
|
16517
|
+
dimension: "voltage",
|
|
16518
|
+
factor: 1,
|
|
16519
|
+
offset: 0
|
|
16520
|
+
},
|
|
16521
|
+
mV: {
|
|
16522
|
+
dimension: "voltage",
|
|
16523
|
+
factor: .001,
|
|
16524
|
+
offset: 0
|
|
16525
|
+
},
|
|
16526
|
+
A: {
|
|
16527
|
+
dimension: "current",
|
|
16528
|
+
factor: 1,
|
|
16529
|
+
offset: 0
|
|
16530
|
+
},
|
|
16531
|
+
mA: {
|
|
16532
|
+
dimension: "current",
|
|
16533
|
+
factor: .001,
|
|
16534
|
+
offset: 0
|
|
16535
|
+
},
|
|
16536
|
+
ppm: {
|
|
16537
|
+
dimension: "concentration-volume",
|
|
16538
|
+
factor: 1,
|
|
16539
|
+
offset: 0
|
|
16540
|
+
},
|
|
16541
|
+
ppb: {
|
|
16542
|
+
dimension: "concentration-volume",
|
|
16543
|
+
factor: .001,
|
|
16544
|
+
offset: 0
|
|
16545
|
+
},
|
|
16546
|
+
"µg/m³": {
|
|
16547
|
+
dimension: "concentration-mass",
|
|
16548
|
+
factor: 1,
|
|
16549
|
+
offset: 0
|
|
16550
|
+
},
|
|
16551
|
+
"mg/m³": {
|
|
16552
|
+
dimension: "concentration-mass",
|
|
16553
|
+
factor: 1e3,
|
|
16554
|
+
offset: 0
|
|
16555
|
+
},
|
|
16556
|
+
kg: {
|
|
16557
|
+
dimension: "mass",
|
|
16558
|
+
factor: 1,
|
|
16559
|
+
offset: 0
|
|
16560
|
+
},
|
|
16561
|
+
g: {
|
|
16562
|
+
dimension: "mass",
|
|
16563
|
+
factor: .001,
|
|
16564
|
+
offset: 0
|
|
16565
|
+
},
|
|
16566
|
+
lb: {
|
|
16567
|
+
dimension: "mass",
|
|
16568
|
+
factor: .453592,
|
|
16569
|
+
offset: 0
|
|
16570
|
+
},
|
|
16571
|
+
oz: {
|
|
16572
|
+
dimension: "mass",
|
|
16573
|
+
factor: .0283495,
|
|
16574
|
+
offset: 0
|
|
16575
|
+
},
|
|
16576
|
+
"%": {
|
|
16577
|
+
dimension: "percentage",
|
|
16578
|
+
factor: 1,
|
|
16579
|
+
offset: 0
|
|
16580
|
+
},
|
|
16581
|
+
L: {
|
|
16582
|
+
dimension: "volume",
|
|
16583
|
+
factor: 1,
|
|
16584
|
+
offset: 0
|
|
16585
|
+
},
|
|
16586
|
+
mL: {
|
|
16587
|
+
dimension: "volume",
|
|
16588
|
+
factor: .001,
|
|
16589
|
+
offset: 0
|
|
16590
|
+
},
|
|
16591
|
+
gal: {
|
|
16592
|
+
dimension: "volume",
|
|
16593
|
+
factor: 3.785411784,
|
|
16594
|
+
offset: 0
|
|
16595
|
+
},
|
|
16596
|
+
s: {
|
|
16597
|
+
dimension: "duration",
|
|
16598
|
+
factor: 1,
|
|
16599
|
+
offset: 0
|
|
16600
|
+
},
|
|
16601
|
+
ms: {
|
|
16602
|
+
dimension: "duration",
|
|
16603
|
+
factor: .001,
|
|
16604
|
+
offset: 0
|
|
16605
|
+
},
|
|
16606
|
+
min: {
|
|
16607
|
+
dimension: "duration",
|
|
16608
|
+
factor: 60,
|
|
16609
|
+
offset: 0
|
|
16610
|
+
},
|
|
16611
|
+
h: {
|
|
16612
|
+
dimension: "duration",
|
|
16613
|
+
factor: 3600,
|
|
16614
|
+
offset: 0
|
|
16615
|
+
},
|
|
16616
|
+
d: {
|
|
16617
|
+
dimension: "duration",
|
|
16618
|
+
factor: 86400,
|
|
16619
|
+
offset: 0
|
|
16620
|
+
},
|
|
16621
|
+
B: {
|
|
16622
|
+
dimension: "information",
|
|
16623
|
+
factor: 1,
|
|
16624
|
+
offset: 0
|
|
16625
|
+
},
|
|
16626
|
+
KiB: {
|
|
16627
|
+
dimension: "information",
|
|
16628
|
+
factor: 1024,
|
|
16629
|
+
offset: 0
|
|
16630
|
+
},
|
|
16631
|
+
MiB: {
|
|
16632
|
+
dimension: "information",
|
|
16633
|
+
factor: 1024 ** 2,
|
|
16634
|
+
offset: 0
|
|
16635
|
+
},
|
|
16636
|
+
GiB: {
|
|
16637
|
+
dimension: "information",
|
|
16638
|
+
factor: 1024 ** 3,
|
|
16639
|
+
offset: 0
|
|
16640
|
+
},
|
|
16641
|
+
TiB: {
|
|
16642
|
+
dimension: "information",
|
|
16643
|
+
factor: 1024 ** 4,
|
|
16644
|
+
offset: 0
|
|
16645
|
+
},
|
|
16646
|
+
kB: {
|
|
16647
|
+
dimension: "information",
|
|
16648
|
+
factor: 1e3,
|
|
16649
|
+
offset: 0
|
|
16650
|
+
},
|
|
16651
|
+
MB: {
|
|
16652
|
+
dimension: "information",
|
|
16653
|
+
factor: 1e6,
|
|
16654
|
+
offset: 0
|
|
16655
|
+
},
|
|
16656
|
+
GB: {
|
|
16657
|
+
dimension: "information",
|
|
16658
|
+
factor: 1e9,
|
|
16659
|
+
offset: 0
|
|
16660
|
+
},
|
|
16661
|
+
"°": {
|
|
16662
|
+
dimension: "angle",
|
|
16663
|
+
factor: 1,
|
|
16664
|
+
offset: 0
|
|
16665
|
+
},
|
|
16666
|
+
dBFS: {
|
|
16667
|
+
dimension: "sound-level",
|
|
16668
|
+
factor: 1,
|
|
16669
|
+
offset: 0
|
|
16670
|
+
},
|
|
16671
|
+
dBm: {
|
|
16672
|
+
dimension: "signal-level",
|
|
16673
|
+
factor: 1,
|
|
16674
|
+
offset: 0
|
|
16675
|
+
},
|
|
16676
|
+
mired: {
|
|
16677
|
+
dimension: "colour-temperature",
|
|
16678
|
+
factor: 1,
|
|
16679
|
+
offset: 0
|
|
16680
|
+
},
|
|
16681
|
+
kbps: {
|
|
16682
|
+
dimension: "bitrate",
|
|
16683
|
+
factor: 1,
|
|
16684
|
+
offset: 0
|
|
16685
|
+
},
|
|
16686
|
+
Mbps: {
|
|
16687
|
+
dimension: "bitrate",
|
|
16688
|
+
factor: 1e3,
|
|
16689
|
+
offset: 0
|
|
16690
|
+
},
|
|
16691
|
+
fps: {
|
|
16692
|
+
dimension: "rate",
|
|
16693
|
+
factor: 1,
|
|
16694
|
+
offset: 0
|
|
16695
|
+
}
|
|
16696
|
+
};
|
|
16697
|
+
/** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
|
|
16698
|
+
function specFor(unit) {
|
|
16699
|
+
return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
|
|
16700
|
+
}
|
|
16701
|
+
/** The dimension a unit belongs to, or `undefined` for an unknown unit. */
|
|
16702
|
+
function unitDimension(unit) {
|
|
16703
|
+
return specFor(unit)?.dimension;
|
|
16704
|
+
}
|
|
16705
|
+
/** Value in `spec`'s unit → its dimension's canonical unit. */
|
|
16706
|
+
function toCanonical(value, spec) {
|
|
16707
|
+
return value * spec.factor + spec.offset;
|
|
16708
|
+
}
|
|
16709
|
+
/** Canonical-unit value → the unit described by `spec`. */
|
|
16710
|
+
function fromCanonical(canonical, spec) {
|
|
16711
|
+
return (canonical - spec.offset) / spec.factor;
|
|
16712
|
+
}
|
|
16713
|
+
/**
|
|
16714
|
+
* Graceful variant for the render path: returns the converted value, or
|
|
16715
|
+
* `undefined` when the conversion is refused (unknown unit / cross-dimension).
|
|
16716
|
+
* Never throws — a render caller falls back to the raw value + source unit.
|
|
16717
|
+
*/
|
|
16718
|
+
function tryConvertUnit(value, from, to) {
|
|
16719
|
+
if (from === to) return value;
|
|
16720
|
+
const fromSpec = specFor(from);
|
|
16721
|
+
if (!fromSpec) return void 0;
|
|
16722
|
+
const toSpec = specFor(to);
|
|
16723
|
+
if (!toSpec) return void 0;
|
|
16724
|
+
if (fromSpec.dimension !== toSpec.dimension) return void 0;
|
|
16725
|
+
return fromCanonical(toCanonical(value, fromSpec), toSpec);
|
|
16726
|
+
}
|
|
16727
|
+
/**
|
|
16728
|
+
* Every known unit spelling in a dimension (table insertion order — canonical
|
|
16729
|
+
* unit first). Powers the UI's same-dimension unit picker.
|
|
16730
|
+
*/
|
|
16731
|
+
/**
|
|
16732
|
+
* Convert a difference, not a reading. Affine units (°F) shift an absolute
|
|
16733
|
+
* value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
|
|
16734
|
+
*/
|
|
16735
|
+
function tryConvertDelta(value, from, to) {
|
|
16736
|
+
if (from === to) return value;
|
|
16737
|
+
const origin = tryConvertUnit(0, from, to);
|
|
16738
|
+
const shifted = tryConvertUnit(value, from, to);
|
|
16739
|
+
if (origin === void 0 || shifted === void 0) return void 0;
|
|
16740
|
+
return shifted - origin;
|
|
16741
|
+
}
|
|
16742
|
+
/**
|
|
16743
|
+
* A number a person types for a physical quantity — the input side of the
|
|
16744
|
+
* value + unit contract (D972).
|
|
16255
16745
|
*
|
|
16256
|
-
*
|
|
16257
|
-
*
|
|
16258
|
-
*
|
|
16259
|
-
*
|
|
16260
|
-
*
|
|
16261
|
-
* STARTED; persisted across a restart. A manual trigger bypasses only this
|
|
16262
|
-
* wait, never the single/queued limits.
|
|
16746
|
+
* Readings are stored in the base unit of their quantity (D944); a cap method
|
|
16747
|
+
* param is stored in the unit its input schema DECLARES (`paramMeta`). The
|
|
16748
|
+
* wire pair {@link QuantityValue} says "this number is expressed in this
|
|
16749
|
+
* unit"; {@link convertQuantityInput} brings it to the stored unit and refuses,
|
|
16750
|
+
* with a typed {@link QuantityUnitError}, a unit that cannot be that quantity.
|
|
16263
16751
|
*/
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
|
|
16274
|
-
|
|
16275
|
-
|
|
16276
|
-
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
16280
|
-
|
|
16281
|
-
|
|
16282
|
-
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16752
|
+
/** A unit that cannot be applied to the number it was given with. */
|
|
16753
|
+
var QuantityUnitError = class extends Error {
|
|
16754
|
+
code;
|
|
16755
|
+
constructor(code, message) {
|
|
16756
|
+
super(`[${code}] ${message}`);
|
|
16757
|
+
this.name = "QuantityUnitError";
|
|
16758
|
+
this.code = code;
|
|
16759
|
+
}
|
|
16760
|
+
};
|
|
16761
|
+
/** The wire pair: `value` is expressed in `unit`. */
|
|
16762
|
+
var QuantityValueSchema = object({
|
|
16763
|
+
value: number(),
|
|
16764
|
+
unit: string().min(1)
|
|
16765
|
+
});
|
|
16766
|
+
/**
|
|
16767
|
+
* Zod `.meta()` bag for a cap METHOD param that is a physical quantity — the
|
|
16768
|
+
* input-side twin of `readingMeta`. The value is carried in `unit` (a gram
|
|
16769
|
+
* portion stays in grams); the unit must belong to the quantity, checked once
|
|
16770
|
+
* when the cap is declared.
|
|
16771
|
+
*/
|
|
16772
|
+
function paramMeta(declared) {
|
|
16773
|
+
const base = baseUnitOf(declared.quantity);
|
|
16774
|
+
if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
|
|
16775
|
+
if (tryConvertUnit(1, declared.unit, base) === void 0) throw new QuantityUnitError("cross-dimension", `"${declared.unit}" is not a unit of ${declared.quantity}`);
|
|
16776
|
+
return {
|
|
16777
|
+
quantity: declared.quantity,
|
|
16778
|
+
unit: declared.unit
|
|
16779
|
+
};
|
|
16780
|
+
}
|
|
16781
|
+
object({
|
|
16782
|
+
quantity: ReadingQuantitySchema,
|
|
16783
|
+
unit: string().min(1)
|
|
16287
16784
|
});
|
|
16288
16785
|
new Set([
|
|
16289
16786
|
{
|
|
@@ -16330,16 +16827,610 @@ var COCO_TO_MACRO = {
|
|
|
16330
16827
|
},
|
|
16331
16828
|
preserveOriginal: false
|
|
16332
16829
|
};
|
|
16830
|
+
/**
|
|
16831
|
+
* WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
|
|
16832
|
+
* animal, package) plus an OPTIONAL label that narrows it.
|
|
16833
|
+
*
|
|
16834
|
+
* The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
|
|
16835
|
+
* second list. The label that fits depends on the macro class:
|
|
16836
|
+
*
|
|
16837
|
+
* - `animal` → a sub-class (`dog`, `cat`, …);
|
|
16838
|
+
* - `person` → a known face (an identity of the `face-gallery`);
|
|
16839
|
+
* - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
|
|
16840
|
+
* - `package` → a sub-class.
|
|
16841
|
+
*
|
|
16842
|
+
* `other` exists only so a class the taxonomy does not know (a custom model's
|
|
16843
|
+
* `forklift`) survives the conversion of a stored recipe without widening.
|
|
16844
|
+
*
|
|
16845
|
+
* A label is decided LATE (D683): face recognition and LPR land after the
|
|
16846
|
+
* detection starts. The track-lifecycle `update` event carries the newly
|
|
16847
|
+
* resolved `label` / `plateText`, which is why a leaf with a label fires when
|
|
16848
|
+
* the subject GETS it — not at the first detection.
|
|
16849
|
+
*/
|
|
16850
|
+
var SubClassLabelSchema = object({
|
|
16851
|
+
kind: literal("sub-class"),
|
|
16852
|
+
className: string().min(1)
|
|
16853
|
+
});
|
|
16854
|
+
var IdentityLabelSchema = object({
|
|
16855
|
+
kind: literal("identity"),
|
|
16856
|
+
/** The `face-gallery` identity id (stable across a rename). */
|
|
16857
|
+
identityId: string().min(1),
|
|
16858
|
+
/** The identity's name — what a track-lifecycle event carries as `label`. */
|
|
16859
|
+
name: string().min(1)
|
|
16860
|
+
});
|
|
16861
|
+
var PlateLabelSchema = object({
|
|
16862
|
+
kind: literal("plate"),
|
|
16863
|
+
text: string().min(1)
|
|
16864
|
+
});
|
|
16865
|
+
function macroOf$1(className) {
|
|
16866
|
+
return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
|
|
16867
|
+
}
|
|
16868
|
+
/** A sub-class label must belong to the macro it is under. */
|
|
16869
|
+
function subClassBelongs(macro, label) {
|
|
16870
|
+
return label === void 0 || macroOf$1(label.className) === macro;
|
|
16871
|
+
}
|
|
16872
|
+
var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
|
|
16873
|
+
var DetectionSubjectSchema = discriminatedUnion("macro", [
|
|
16874
|
+
object({
|
|
16875
|
+
macro: literal("animal"),
|
|
16876
|
+
label: subClassOf("animal").optional()
|
|
16877
|
+
}),
|
|
16878
|
+
object({
|
|
16879
|
+
macro: literal("person"),
|
|
16880
|
+
label: IdentityLabelSchema.optional()
|
|
16881
|
+
}),
|
|
16882
|
+
object({
|
|
16883
|
+
macro: literal("vehicle"),
|
|
16884
|
+
label: discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
|
|
16885
|
+
}),
|
|
16886
|
+
object({
|
|
16887
|
+
macro: literal("package"),
|
|
16888
|
+
label: subClassOf("package").optional()
|
|
16889
|
+
}),
|
|
16890
|
+
object({
|
|
16891
|
+
macro: literal("other"),
|
|
16892
|
+
className: string().min(1)
|
|
16893
|
+
})
|
|
16894
|
+
]);
|
|
16895
|
+
/**
|
|
16896
|
+
* The subject of a PARKED object. The stationary registry's event carries a
|
|
16897
|
+
* class and a zone, no face and no plate — an identity or plate label there
|
|
16898
|
+
* could never match, so the schema refuses it instead of letting it sit dead.
|
|
16899
|
+
*/
|
|
16900
|
+
var StationarySubjectSchema = DetectionSubjectSchema.refine((s) => s.macro === "other" || s.label === void 0 || s.label.kind === "sub-class", { message: "a parked object carries no face or plate — only a class can narrow it" });
|
|
16901
|
+
/**
|
|
16902
|
+
* A stored class string as a subject: a macro name stays a macro; a raw class
|
|
16903
|
+
* the taxonomy files under a macro becomes that macro plus the sub-class label
|
|
16904
|
+
* (`dog` → animal / dog); anything else is `other`, kept verbatim.
|
|
16905
|
+
*/
|
|
16906
|
+
function subjectFromClassName(className) {
|
|
16907
|
+
const name = className.trim().toLowerCase();
|
|
16908
|
+
const macro = COCO_TO_MACRO.mapping[name];
|
|
16909
|
+
if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
|
|
16910
|
+
if (macro === void 0) return {
|
|
16911
|
+
macro: "other",
|
|
16912
|
+
className
|
|
16913
|
+
};
|
|
16914
|
+
const label = {
|
|
16915
|
+
kind: "sub-class",
|
|
16916
|
+
className: name
|
|
16917
|
+
};
|
|
16918
|
+
switch (macro) {
|
|
16919
|
+
case "animal": return {
|
|
16920
|
+
macro: "animal",
|
|
16921
|
+
label
|
|
16922
|
+
};
|
|
16923
|
+
case "vehicle": return {
|
|
16924
|
+
macro: "vehicle",
|
|
16925
|
+
label
|
|
16926
|
+
};
|
|
16927
|
+
case "package": return {
|
|
16928
|
+
macro: "package",
|
|
16929
|
+
label
|
|
16930
|
+
};
|
|
16931
|
+
default: return {
|
|
16932
|
+
macro: "other",
|
|
16933
|
+
className
|
|
16934
|
+
};
|
|
16935
|
+
}
|
|
16936
|
+
}
|
|
16937
|
+
function macroOnly(macro) {
|
|
16938
|
+
switch (macro) {
|
|
16939
|
+
case "animal": return { macro: "animal" };
|
|
16940
|
+
case "person": return { macro: "person" };
|
|
16941
|
+
case "vehicle": return { macro: "vehicle" };
|
|
16942
|
+
case "package": return { macro: "package" };
|
|
16943
|
+
default: return {
|
|
16944
|
+
macro: "other",
|
|
16945
|
+
className: macro
|
|
16946
|
+
};
|
|
16947
|
+
}
|
|
16948
|
+
}
|
|
16949
|
+
/** How a state leaf compares a device field to a value. Derived from the
|
|
16950
|
+
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
16951
|
+
var AutomationConditionOperatorSchema = _enum([
|
|
16952
|
+
"eq",
|
|
16953
|
+
"ne",
|
|
16954
|
+
"gt",
|
|
16955
|
+
"gte",
|
|
16956
|
+
"lt",
|
|
16957
|
+
"lte",
|
|
16958
|
+
"contains",
|
|
16959
|
+
"in"
|
|
16960
|
+
]);
|
|
16961
|
+
/**
|
|
16962
|
+
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
16963
|
+
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
16964
|
+
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
16965
|
+
* fire; it does not lose it.
|
|
16966
|
+
*/
|
|
16967
|
+
var DeviceBecomesLeafSchema = object({
|
|
16968
|
+
kind: literal("device-becomes"),
|
|
16969
|
+
deviceId: number().int().nonnegative(),
|
|
16970
|
+
cap: string().min(1),
|
|
16971
|
+
fieldPath: string().min(1),
|
|
16972
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
16973
|
+
becomes: union([
|
|
16974
|
+
string(),
|
|
16975
|
+
number(),
|
|
16976
|
+
boolean()
|
|
16977
|
+
]).optional(),
|
|
16978
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
16979
|
+
edge: boolean().optional(),
|
|
16980
|
+
/** The new value must hold this long before it counts. */
|
|
16981
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
16982
|
+
/** Collapse a burst into one occurrence. */
|
|
16983
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
16984
|
+
});
|
|
16985
|
+
var DeviceEventLeafSchema = object({
|
|
16986
|
+
kind: literal("device-event"),
|
|
16987
|
+
/** An `EventCategory` value. */
|
|
16988
|
+
category: string().min(1),
|
|
16989
|
+
deviceId: number().int().nonnegative().optional()
|
|
16990
|
+
});
|
|
16991
|
+
var ScheduleLeafSchema = object({
|
|
16992
|
+
kind: literal("schedule"),
|
|
16993
|
+
cron: string().min(1).max(120)
|
|
16994
|
+
});
|
|
16995
|
+
var ManualLeafSchema = object({ kind: literal("manual") });
|
|
16996
|
+
var IntervalLeafSchema = object({
|
|
16997
|
+
kind: literal("interval"),
|
|
16998
|
+
minutes: number().int().min(1).max(1440)
|
|
16999
|
+
});
|
|
17000
|
+
var DetectionLeafSchema = object({
|
|
17001
|
+
kind: literal("detection"),
|
|
17002
|
+
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
17003
|
+
subjects: array(DetectionSubjectSchema).min(1),
|
|
17004
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
17005
|
+
/** The track must have visited at least one of these zones. */
|
|
17006
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
17007
|
+
/** Best-so-far confidence the track must reach. */
|
|
17008
|
+
minScore: number().min(0).max(1).optional()
|
|
17009
|
+
});
|
|
17010
|
+
var StationaryLeafSchema = object({
|
|
17011
|
+
kind: literal("stationary"),
|
|
17012
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
17013
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
17014
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
17015
|
+
/** `appeared`: it became parked. `departed`: the registry retired it. */
|
|
17016
|
+
phase: _enum(["appeared", "departed"])
|
|
17017
|
+
});
|
|
17018
|
+
/** "is": one device field compared to one value, NOW. */
|
|
17019
|
+
var DeviceIsLeafSchema = object({
|
|
17020
|
+
kind: literal("device-is"),
|
|
17021
|
+
deviceId: number().int().nonnegative(),
|
|
17022
|
+
cap: string().min(1),
|
|
17023
|
+
fieldPath: string().min(1),
|
|
17024
|
+
operator: AutomationConditionOperatorSchema,
|
|
17025
|
+
/**
|
|
17026
|
+
* The value. A number on a quantity field is stored in the field's base unit
|
|
17027
|
+
* (D944); a quantity value is the WRITE form, which the save boundary
|
|
17028
|
+
* replaces with the converted number and `unit`. A leaf that still carries
|
|
17029
|
+
* one is never evaluated (it fails).
|
|
17030
|
+
*/
|
|
17031
|
+
value: union([
|
|
17032
|
+
string(),
|
|
17033
|
+
number(),
|
|
17034
|
+
boolean(),
|
|
17035
|
+
array(union([string(), number()])),
|
|
17036
|
+
QuantityValueSchema
|
|
17037
|
+
]),
|
|
17038
|
+
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
17039
|
+
unit: string().min(1).optional()
|
|
17040
|
+
});
|
|
17041
|
+
/**
|
|
17042
|
+
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
17043
|
+
* least one of `zones`. Reads the zone-analytics snapshot through the same
|
|
17044
|
+
* mirror as every other state leaf, so a dropped event never decides it (D49).
|
|
17045
|
+
*/
|
|
17046
|
+
var StationaryPresentLeafSchema = object({
|
|
17047
|
+
kind: literal("stationary-present"),
|
|
17048
|
+
deviceId: number().int().nonnegative(),
|
|
17049
|
+
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
17050
|
+
zones: array(string().min(1)).min(1).optional()
|
|
17051
|
+
});
|
|
17052
|
+
/**
|
|
17053
|
+
* The escape hatch: the bounded expression engine over named device bindings.
|
|
17054
|
+
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
17055
|
+
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
17056
|
+
*/
|
|
17057
|
+
var ExpressionLeafSchema = object({
|
|
17058
|
+
kind: literal("expression"),
|
|
17059
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
17060
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
17061
|
+
}).superRefine((src, ctx) => {
|
|
17062
|
+
const err = validateExpressionSource(src);
|
|
17063
|
+
if (err !== null) ctx.addIssue({
|
|
17064
|
+
code: "custom",
|
|
17065
|
+
message: err,
|
|
17066
|
+
path: ["expr"]
|
|
17067
|
+
});
|
|
17068
|
+
});
|
|
17069
|
+
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
17070
|
+
object({
|
|
17071
|
+
kind: literal("all"),
|
|
17072
|
+
children: array(AutomationWhenSchema)
|
|
17073
|
+
}),
|
|
17074
|
+
object({
|
|
17075
|
+
kind: literal("any"),
|
|
17076
|
+
children: array(AutomationWhenSchema)
|
|
17077
|
+
}),
|
|
17078
|
+
object({
|
|
17079
|
+
kind: literal("not"),
|
|
17080
|
+
child: AutomationWhenSchema
|
|
17081
|
+
}),
|
|
17082
|
+
DeviceBecomesLeafSchema,
|
|
17083
|
+
DeviceEventLeafSchema,
|
|
17084
|
+
ScheduleLeafSchema,
|
|
17085
|
+
ManualLeafSchema,
|
|
17086
|
+
IntervalLeafSchema,
|
|
17087
|
+
DetectionLeafSchema,
|
|
17088
|
+
StationaryLeafSchema,
|
|
17089
|
+
DeviceIsLeafSchema,
|
|
17090
|
+
StationaryPresentLeafSchema,
|
|
17091
|
+
ExpressionLeafSchema
|
|
17092
|
+
]));
|
|
17093
|
+
/**
|
|
17094
|
+
* The recipe shape before the single `when` tree: a flat list of triggers
|
|
17095
|
+
* (OR'd, each an instant) and an optional condition tree. Stored recipes —
|
|
17096
|
+
* the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
|
|
17097
|
+
* older client still posts — are in this shape, so it stays READABLE here, and
|
|
17098
|
+
* {@link convertLegacyRecipe} turns it into the new one, losslessly and one
|
|
17099
|
+
* way. Nothing writes this shape any more.
|
|
17100
|
+
*
|
|
17101
|
+
* `{ triggers: [t1…tn], conditions?: C }`
|
|
17102
|
+
* → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
|
|
17103
|
+
* a single trigger is not wrapped in `any`.
|
|
17104
|
+
* a `condition` leaf → a `device-is` leaf (the "is" verb);
|
|
17105
|
+
* a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
|
|
17106
|
+
* `classes: ['dog']` → the macro `animal` with the label `dog`.
|
|
17107
|
+
*/
|
|
17108
|
+
var LegacyTriggerSchema = discriminatedUnion("kind", [
|
|
17109
|
+
object({
|
|
17110
|
+
kind: literal("device-state"),
|
|
17111
|
+
deviceId: number().int().nonnegative(),
|
|
17112
|
+
cap: string().min(1),
|
|
17113
|
+
fieldPath: string().min(1),
|
|
17114
|
+
becomes: union([
|
|
17115
|
+
string(),
|
|
17116
|
+
number(),
|
|
17117
|
+
boolean()
|
|
17118
|
+
]).optional(),
|
|
17119
|
+
edge: boolean().optional(),
|
|
17120
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
17121
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
17122
|
+
}),
|
|
17123
|
+
object({
|
|
17124
|
+
kind: literal("device-event"),
|
|
17125
|
+
category: string().min(1),
|
|
17126
|
+
deviceId: number().int().nonnegative().optional()
|
|
17127
|
+
}),
|
|
17128
|
+
object({
|
|
17129
|
+
kind: literal("schedule"),
|
|
17130
|
+
cron: string().min(1).max(120)
|
|
17131
|
+
}),
|
|
17132
|
+
object({ kind: literal("manual") }),
|
|
17133
|
+
object({
|
|
17134
|
+
kind: literal("detection"),
|
|
17135
|
+
classes: array(string().min(1)).min(1),
|
|
17136
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
17137
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
17138
|
+
minScore: number().min(0).max(1).optional()
|
|
17139
|
+
}),
|
|
17140
|
+
object({
|
|
17141
|
+
kind: literal("stationary"),
|
|
17142
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
17143
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
17144
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
17145
|
+
phase: _enum(["appeared", "departed"])
|
|
17146
|
+
}),
|
|
17147
|
+
object({
|
|
17148
|
+
kind: literal("interval"),
|
|
17149
|
+
minutes: number().int().min(1).max(1440)
|
|
17150
|
+
})
|
|
17151
|
+
]);
|
|
17152
|
+
var LegacyConditionLeafSchema = object({
|
|
17153
|
+
kind: literal("condition"),
|
|
17154
|
+
deviceId: number().int().nonnegative(),
|
|
17155
|
+
cap: string().min(1),
|
|
17156
|
+
fieldPath: string().min(1),
|
|
17157
|
+
operator: AutomationConditionOperatorSchema,
|
|
17158
|
+
value: union([
|
|
17159
|
+
string(),
|
|
17160
|
+
number(),
|
|
17161
|
+
boolean(),
|
|
17162
|
+
array(union([string(), number()])),
|
|
17163
|
+
QuantityValueSchema
|
|
17164
|
+
]),
|
|
17165
|
+
unit: string().min(1).optional()
|
|
17166
|
+
});
|
|
17167
|
+
var LegacyStationaryPresentSchema = object({
|
|
17168
|
+
kind: literal("stationary-present"),
|
|
17169
|
+
deviceId: number().int().nonnegative(),
|
|
17170
|
+
classes: array(string().min(1)).min(1).optional(),
|
|
17171
|
+
zones: array(string().min(1)).min(1).optional()
|
|
17172
|
+
});
|
|
17173
|
+
var LegacyExpressionSchema = object({
|
|
17174
|
+
kind: literal("expression"),
|
|
17175
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
17176
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
17177
|
+
}).superRefine((src, ctx) => {
|
|
17178
|
+
const err = validateExpressionSource(src);
|
|
17179
|
+
if (err !== null) ctx.addIssue({
|
|
17180
|
+
code: "custom",
|
|
17181
|
+
message: err,
|
|
17182
|
+
path: ["expr"]
|
|
17183
|
+
});
|
|
17184
|
+
});
|
|
17185
|
+
var LegacyConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
17186
|
+
object({
|
|
17187
|
+
kind: literal("all"),
|
|
17188
|
+
children: array(LegacyConditionSchema)
|
|
17189
|
+
}),
|
|
17190
|
+
object({
|
|
17191
|
+
kind: literal("any"),
|
|
17192
|
+
children: array(LegacyConditionSchema)
|
|
17193
|
+
}),
|
|
17194
|
+
object({
|
|
17195
|
+
kind: literal("not"),
|
|
17196
|
+
child: LegacyConditionSchema
|
|
17197
|
+
}),
|
|
17198
|
+
LegacyConditionLeafSchema,
|
|
17199
|
+
LegacyExpressionSchema,
|
|
17200
|
+
LegacyStationaryPresentSchema
|
|
17201
|
+
]));
|
|
17202
|
+
function convertTrigger(trigger) {
|
|
17203
|
+
switch (trigger.kind) {
|
|
17204
|
+
case "device-state": return {
|
|
17205
|
+
...trigger,
|
|
17206
|
+
kind: "device-becomes"
|
|
17207
|
+
};
|
|
17208
|
+
case "detection": {
|
|
17209
|
+
const { classes, ...rest } = trigger;
|
|
17210
|
+
return {
|
|
17211
|
+
...rest,
|
|
17212
|
+
subjects: classes.map(subjectFromClassName)
|
|
17213
|
+
};
|
|
17214
|
+
}
|
|
17215
|
+
case "stationary": {
|
|
17216
|
+
const { classes, ...rest } = trigger;
|
|
17217
|
+
return {
|
|
17218
|
+
...rest,
|
|
17219
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
17220
|
+
};
|
|
17221
|
+
}
|
|
17222
|
+
default: return trigger;
|
|
17223
|
+
}
|
|
17224
|
+
}
|
|
17225
|
+
/** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
|
|
17226
|
+
function convertLegacyCondition(condition) {
|
|
17227
|
+
switch (condition.kind) {
|
|
17228
|
+
case "condition": return {
|
|
17229
|
+
...condition,
|
|
17230
|
+
kind: "device-is"
|
|
17231
|
+
};
|
|
17232
|
+
case "stationary-present": {
|
|
17233
|
+
const { classes, ...rest } = condition;
|
|
17234
|
+
return {
|
|
17235
|
+
...rest,
|
|
17236
|
+
...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
|
|
17237
|
+
};
|
|
17238
|
+
}
|
|
17239
|
+
case "expression": return condition;
|
|
17240
|
+
case "not": return {
|
|
17241
|
+
kind: "not",
|
|
17242
|
+
child: convertLegacyCondition(condition.child)
|
|
17243
|
+
};
|
|
17244
|
+
case "all":
|
|
17245
|
+
case "any": return {
|
|
17246
|
+
kind: condition.kind,
|
|
17247
|
+
children: condition.children.map(convertLegacyCondition)
|
|
17248
|
+
};
|
|
17249
|
+
}
|
|
17250
|
+
}
|
|
17251
|
+
/** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
|
|
17252
|
+
function convertLegacyWhen(half) {
|
|
17253
|
+
const leaves = half.triggers.map(convertTrigger);
|
|
17254
|
+
const first = leaves[0];
|
|
17255
|
+
const triggers = leaves.length === 1 && first !== void 0 ? first : {
|
|
17256
|
+
kind: "any",
|
|
17257
|
+
children: leaves
|
|
17258
|
+
};
|
|
17259
|
+
if (half.conditions === void 0) return triggers;
|
|
17260
|
+
return {
|
|
17261
|
+
kind: "all",
|
|
17262
|
+
children: [triggers, convertLegacyCondition(half.conditions)]
|
|
17263
|
+
};
|
|
17264
|
+
}
|
|
17265
|
+
/**
|
|
17266
|
+
* The automation RECIPE — what a visually-authored automation is, before it
|
|
17267
|
+
* becomes code.
|
|
17268
|
+
*
|
|
17269
|
+
* This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
|
|
17270
|
+
* Phase 6. A recipe is not an engine and never evaluates anything: it is a
|
|
17271
|
+
* declarative description that {@link generateAutomationBlock} compiles to the
|
|
17272
|
+
* TypeScript of an ordinary core block. There is therefore no second rule
|
|
17273
|
+
* engine, no second store and no second runtime — the block runner is the
|
|
17274
|
+
* runtime, `core_blocks` is the store (§2.1).
|
|
17275
|
+
*
|
|
17276
|
+
* Two parts:
|
|
17277
|
+
*
|
|
17278
|
+
* - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
|
|
17279
|
+
* (instants) and state leaves (durations). It replaces the flat trigger list
|
|
17280
|
+
* plus the separate condition tree; the firing rule is in that file and in
|
|
17281
|
+
* ADR "one tree".
|
|
17282
|
+
* - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
|
|
17283
|
+
* fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
|
|
17284
|
+
* this had to diverge from "verbatim", and why.
|
|
17285
|
+
*
|
|
17286
|
+
* A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
|
|
17287
|
+
* {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
|
|
17288
|
+
*/
|
|
17289
|
+
/**
|
|
17290
|
+
* One action step.
|
|
17291
|
+
*
|
|
17292
|
+
* `wait` and `cap` are `NcRuleActionSchema`'s two members, kept structurally
|
|
17293
|
+
* identical so `NcRuleActionRunner` runs them unchanged — its device-scope
|
|
17294
|
+
* check, stop-at-first-failure and per-sequence throttle are the whole reason
|
|
17295
|
+
* to reuse it, and none of them are re-implemented here.
|
|
17296
|
+
*
|
|
17297
|
+
* **The one divergence, and it is forced.** `NcRuleActionSchema.cap.deviceId` is
|
|
17298
|
+
* a literal `z.number().int()`, and the NC runner's own `RunSequencesInput`
|
|
17299
|
+
* documents its subject device as *"for the log tag, never for routing"*. So an
|
|
17300
|
+
* NC action can never target the device that triggered it — which is fine for
|
|
17301
|
+
* the NC (its rules already scope to a device) and fatal for an automation
|
|
17302
|
+
* ("sound the siren of the camera that saw the person"). `deviceId` therefore
|
|
17303
|
+
* also accepts `{ $var }`, resolved from the run's `vars` bag BEFORE the runner
|
|
17304
|
+
* is called. The runner still receives a number and is untouched; the
|
|
17305
|
+
* resolution is the recipe's job, not the runner's.
|
|
17306
|
+
*/
|
|
17307
|
+
var AutomationWaitStepSchema = object({
|
|
17308
|
+
kind: literal("wait"),
|
|
17309
|
+
seconds: number().min(0).max(300)
|
|
17310
|
+
});
|
|
17311
|
+
var AutomationCapStepSchema = object({
|
|
17312
|
+
kind: literal("cap"),
|
|
17313
|
+
/**
|
|
17314
|
+
* One device, several same-family devices (locks / climate family / …),
|
|
17315
|
+
* or `{ $var }` for the device that triggered the run. An array is
|
|
17316
|
+
* fan-out at codegen — each id gets the same cap.method call.
|
|
17317
|
+
*/
|
|
17318
|
+
deviceId: union([
|
|
17319
|
+
number().int(),
|
|
17320
|
+
array(number().int()).min(1),
|
|
17321
|
+
object({ $var: string().min(1) })
|
|
17322
|
+
]),
|
|
17323
|
+
cap: string().min(1),
|
|
17324
|
+
method: string().min(1),
|
|
17325
|
+
/** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
|
|
17326
|
+
* SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
|
|
17327
|
+
* the expression leaf. */
|
|
17328
|
+
args: record(string(), unknown()).optional(),
|
|
17329
|
+
/**
|
|
17330
|
+
* The unit the person picked per arg (display only). `args` always hold the
|
|
17331
|
+
* value in the unit the method's input DECLARES (`paramMeta`); on a write an
|
|
17332
|
+
* arg may be a {@link QuantityValue}, which the save boundary converts.
|
|
17333
|
+
*/
|
|
17334
|
+
argUnits: record(string(), string().min(1)).optional()
|
|
17335
|
+
});
|
|
17336
|
+
/**
|
|
17337
|
+
* The steps an internal Action may contain: cap and wait, and nothing that can
|
|
17338
|
+
* call back into an Action — so an Action can never reach itself (no cycles).
|
|
17339
|
+
*/
|
|
17340
|
+
var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
|
|
17341
|
+
var AutomationActionSchema = discriminatedUnion("kind", [
|
|
17342
|
+
AutomationWaitStepSchema,
|
|
17343
|
+
AutomationCapStepSchema,
|
|
17344
|
+
object({
|
|
17345
|
+
kind: literal("action"),
|
|
17346
|
+
/** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
|
|
17347
|
+
actionId: string().min(1),
|
|
17348
|
+
/** Values for the action's declared variables; the others take their default. */
|
|
17349
|
+
variables: record(string(), union([
|
|
17350
|
+
string(),
|
|
17351
|
+
number(),
|
|
17352
|
+
boolean()
|
|
17353
|
+
])).optional()
|
|
17354
|
+
}),
|
|
17355
|
+
object({
|
|
17356
|
+
kind: literal("code"),
|
|
17357
|
+
/** Compiled into the automation's OWN block by esbuild — not a third
|
|
17358
|
+
* runtime, not a `vm`, and not dynamically evaluated. */
|
|
17359
|
+
code: string().min(1).max(2e4)
|
|
17360
|
+
})
|
|
17361
|
+
]);
|
|
17362
|
+
/**
|
|
17363
|
+
* How a trigger that arrives while the automation is busy (or cooling down) is
|
|
17364
|
+
* handled. One of the automation's execution modes, never a property of its
|
|
17365
|
+
* actions. Absent means `single`.
|
|
17366
|
+
*
|
|
17367
|
+
* - `single` — drop the trigger while a run is in progress (traced + logged).
|
|
17368
|
+
* - `restart` — abort the run in progress (at its next step boundary), start fresh.
|
|
17369
|
+
* - `queued` — queue up to `max` triggers, drop the rest (traced).
|
|
17370
|
+
* - `parallel` — up to `max` runs at once, drop the rest (traced).
|
|
17371
|
+
* - `cooldown` — `single`, and no new run until `minutes` after the last run
|
|
17372
|
+
* STARTED; persisted across a restart. A manual trigger bypasses only this
|
|
17373
|
+
* wait, never the single/queued limits.
|
|
17374
|
+
*/
|
|
17375
|
+
var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
17376
|
+
object({ mode: literal("single") }),
|
|
17377
|
+
object({ mode: literal("restart") }),
|
|
17378
|
+
object({
|
|
17379
|
+
mode: literal("queued"),
|
|
17380
|
+
max: number().int().min(1).max(10)
|
|
17381
|
+
}),
|
|
17382
|
+
object({
|
|
17383
|
+
mode: literal("parallel"),
|
|
17384
|
+
max: number().int().min(1).max(10)
|
|
17385
|
+
}),
|
|
17386
|
+
object({
|
|
17387
|
+
mode: literal("cooldown"),
|
|
17388
|
+
minutes: number().int().min(1).max(1440)
|
|
17389
|
+
})
|
|
17390
|
+
]);
|
|
17391
|
+
var CurrentAutomationRecipeSchema = object({
|
|
17392
|
+
when: AutomationWhenSchema,
|
|
17393
|
+
actions: array(AutomationActionSchema),
|
|
17394
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
17395
|
+
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
17396
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
17397
|
+
});
|
|
17398
|
+
/** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
|
|
17399
|
+
var LegacyAutomationRecipeSchema = object({
|
|
17400
|
+
triggers: array(LegacyTriggerSchema),
|
|
17401
|
+
conditions: LegacyConditionSchema.optional(),
|
|
17402
|
+
actions: array(AutomationActionSchema),
|
|
17403
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
17404
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
17405
|
+
});
|
|
17406
|
+
/** The old shape as the new one, lossless. */
|
|
17407
|
+
function convertLegacyRecipe(legacy) {
|
|
17408
|
+
const { triggers, conditions, ...rest } = legacy;
|
|
17409
|
+
return {
|
|
17410
|
+
when: convertLegacyWhen({
|
|
17411
|
+
triggers,
|
|
17412
|
+
conditions
|
|
17413
|
+
}),
|
|
17414
|
+
...rest
|
|
17415
|
+
};
|
|
17416
|
+
}
|
|
17417
|
+
/**
|
|
17418
|
+
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
17419
|
+
* saved block carries): the new shape as it is, or the old shape converted to
|
|
17420
|
+
* it. The output type is always the new one.
|
|
17421
|
+
*/
|
|
17422
|
+
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
16333
17423
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
16334
17424
|
/**
|
|
16335
17425
|
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
16336
17426
|
* so the editor can paint every block green or red with its reason.
|
|
16337
17427
|
*
|
|
16338
|
-
* A trace is data, not a log: it covers the WHOLE tree — the
|
|
16339
|
-
* every
|
|
16340
|
-
* short-circuit never reached), the run-mode verdict and every action
|
|
16341
|
-
* each addressed by a stable path (`
|
|
16342
|
-
* for an Action run).
|
|
17428
|
+
* A trace is data, not a log: it covers the WHOLE tree — the event leaf that
|
|
17429
|
+
* fired, every node of the `when` tree (containers and leaves, including the
|
|
17430
|
+
* ones a short-circuit never reached), the run-mode verdict and every action
|
|
17431
|
+
* step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
|
|
17432
|
+
* for an Action run). Traces written before the single tree carry
|
|
17433
|
+
* `conditions.*` paths and still parse. Skipped
|
|
16343
17434
|
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
16344
17435
|
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
16345
17436
|
*
|
|
@@ -16371,6 +17462,8 @@ var TraceNodeOutcomeSchema = _enum([
|
|
|
16371
17462
|
]);
|
|
16372
17463
|
var AutomationTriggerTraceSchema = object({
|
|
16373
17464
|
kind: string().min(1),
|
|
17465
|
+
/** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
|
|
17466
|
+
path: string().min(1).optional(),
|
|
16374
17467
|
outcome: _enum(["pass", "fail"]),
|
|
16375
17468
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
16376
17469
|
reason: string(),
|
|
@@ -16378,15 +17471,23 @@ var AutomationTriggerTraceSchema = object({
|
|
|
16378
17471
|
vars: record(string(), TraceValueSchema)
|
|
16379
17472
|
});
|
|
16380
17473
|
var AutomationConditionTraceNodeSchema = object({
|
|
16381
|
-
/** `
|
|
17474
|
+
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
16382
17475
|
path: string().min(1),
|
|
16383
17476
|
kind: _enum([
|
|
16384
17477
|
"all",
|
|
16385
17478
|
"any",
|
|
16386
17479
|
"not",
|
|
16387
17480
|
"condition",
|
|
17481
|
+
"device-is",
|
|
16388
17482
|
"expression",
|
|
16389
|
-
"stationary-present"
|
|
17483
|
+
"stationary-present",
|
|
17484
|
+
"device-becomes",
|
|
17485
|
+
"device-event",
|
|
17486
|
+
"schedule",
|
|
17487
|
+
"interval",
|
|
17488
|
+
"manual",
|
|
17489
|
+
"detection",
|
|
17490
|
+
"stationary"
|
|
16390
17491
|
]),
|
|
16391
17492
|
outcome: TraceNodeOutcomeSchema,
|
|
16392
17493
|
reason: string(),
|
|
@@ -16420,6 +17521,12 @@ var AutomationStepTraceSchema = object({
|
|
|
16420
17521
|
deviceId: number().int().optional(),
|
|
16421
17522
|
startedAt: number().optional(),
|
|
16422
17523
|
endedAt: number().optional(),
|
|
17524
|
+
/**
|
|
17525
|
+
* The parameters the step SENT, after `{{vars.x}}` substitution — what the
|
|
17526
|
+
* device was actually asked. Bounded like trigger `vars`; a secret parameter
|
|
17527
|
+
* is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
|
|
17528
|
+
*/
|
|
17529
|
+
args: record(string(), TraceValueSchema).optional(),
|
|
16423
17530
|
outcome: _enum([
|
|
16424
17531
|
"ok",
|
|
16425
17532
|
"error",
|
|
@@ -16600,6 +17707,16 @@ object({
|
|
|
16600
17707
|
lastRunAt: number().nullable(),
|
|
16601
17708
|
lastError: string().nullable()
|
|
16602
17709
|
});
|
|
17710
|
+
/** A bare number accepted as a base unit — the save succeeded, say so. */
|
|
17711
|
+
var UnitWarningSchema = object({
|
|
17712
|
+
/** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
|
|
17713
|
+
path: string(),
|
|
17714
|
+
deviceId: number().int().optional(),
|
|
17715
|
+
field: string(),
|
|
17716
|
+
value: number(),
|
|
17717
|
+
assumedUnit: string(),
|
|
17718
|
+
message: string()
|
|
17719
|
+
});
|
|
16603
17720
|
/**
|
|
16604
17721
|
* notification-output — canonical, capability-gated notification delivery.
|
|
16605
17722
|
*
|
|
@@ -17338,6 +18455,23 @@ method(object({
|
|
|
17338
18455
|
addonId: string(),
|
|
17339
18456
|
nodeId: string().optional()
|
|
17340
18457
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
18458
|
+
addonId: string(),
|
|
18459
|
+
nodeId: string().optional()
|
|
18460
|
+
}), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
|
|
18461
|
+
addonId: string(),
|
|
18462
|
+
nodeId: string().optional(),
|
|
18463
|
+
keys: array(string())
|
|
18464
|
+
}), object({
|
|
18465
|
+
removed: array(string()),
|
|
18466
|
+
refused: array(object({
|
|
18467
|
+
key: string(),
|
|
18468
|
+
reason: string()
|
|
18469
|
+
})),
|
|
18470
|
+
secretsCleared: number()
|
|
18471
|
+
}), {
|
|
18472
|
+
kind: "mutation",
|
|
18473
|
+
auth: "admin"
|
|
18474
|
+
}), method(object({
|
|
17341
18475
|
addonId: string(),
|
|
17342
18476
|
deviceId: number(),
|
|
17343
18477
|
nodeId: string().optional()
|
|
@@ -18773,8 +19907,21 @@ var BrokerProviderInfoSchema = object({
|
|
|
18773
19907
|
* (`{ sessionId }`) drops the session. The dialog that edits a broker
|
|
18774
19908
|
* offers it as the default path; the fields the session fills are
|
|
18775
19909
|
* marked `filledBySignIn` in the kind's settings schema.
|
|
19910
|
+
*
|
|
19911
|
+
* `methods` lists the ways the session can sign in, recommended first:
|
|
19912
|
+
* `'device-code'` adds `POST {basePath}/code/start` (body: the form's
|
|
19913
|
+
* settings) answering `{ sessionId, userCode, verificationUrl,
|
|
19914
|
+
* expiresAt }` — the operator types `userCode` at `verificationUrl` on
|
|
19915
|
+
* any device, and the same `status` / `commit` / `cancel` routes follow
|
|
19916
|
+
* (status then carries `method`, `userCode`, `verificationUrl`,
|
|
19917
|
+
* `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
|
|
19918
|
+
* `'proxy'` is the `{basePath}/start` flow above. Absent means
|
|
19919
|
+
* `['proxy']`.
|
|
18776
19920
|
*/
|
|
18777
|
-
signIn: object({
|
|
19921
|
+
signIn: object({
|
|
19922
|
+
basePath: string().min(1),
|
|
19923
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
19924
|
+
}).optional()
|
|
18778
19925
|
}))
|
|
18779
19926
|
});
|
|
18780
19927
|
var ListInputSchema = object({
|
|
@@ -21403,6 +22550,8 @@ var StorageKindSchema = object({
|
|
|
21403
22550
|
"orphaned",
|
|
21404
22551
|
"dropped"
|
|
21405
22552
|
]),
|
|
22553
|
+
/** An owner declared this kind since the engine started. */
|
|
22554
|
+
declaredThisBoot: boolean(),
|
|
21406
22555
|
/** `null` when the caller asked for no counts (`counts: false`). */
|
|
21407
22556
|
rows: number().int().nonnegative().nullable(),
|
|
21408
22557
|
/** Rows x the average size of a sample; null for an empty kind. */
|
|
@@ -21414,6 +22563,51 @@ var StorageKindSchema = object({
|
|
|
21414
22563
|
/** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
|
|
21415
22564
|
declaration: RecordKindDeclarationSchema
|
|
21416
22565
|
});
|
|
22566
|
+
_enum([
|
|
22567
|
+
"orphaned",
|
|
22568
|
+
"stale",
|
|
22569
|
+
"unknown"
|
|
22570
|
+
]);
|
|
22571
|
+
/** A record kind nobody declared since its owner started, or whose owner is gone. */
|
|
22572
|
+
var StaleKindFindingSchema = object({
|
|
22573
|
+
owner: string(),
|
|
22574
|
+
kind: string(),
|
|
22575
|
+
/** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
|
|
22576
|
+
verdict: _enum(["orphaned", "unknown"]),
|
|
22577
|
+
reason: string(),
|
|
22578
|
+
rows: number().int().nonnegative().nullable(),
|
|
22579
|
+
/** local-auth's tables are never dropped by this audit. */
|
|
22580
|
+
protected: boolean()
|
|
22581
|
+
});
|
|
22582
|
+
/** A stored global-settings key the running addon no longer declares. */
|
|
22583
|
+
var StaleSettingsKeyFindingSchema = object({
|
|
22584
|
+
addonId: string(),
|
|
22585
|
+
/** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
|
|
22586
|
+
key: string(),
|
|
22587
|
+
/**
|
|
22588
|
+
* Keychain refs whose ONLY marker sits under this key: in use by stale data,
|
|
22589
|
+
* not orphans yet. Removing the key releases them.
|
|
22590
|
+
*/
|
|
22591
|
+
secretRefs: array(string())
|
|
22592
|
+
});
|
|
22593
|
+
/** An addon the audit could not judge, with the reason. Never reported as stale. */
|
|
22594
|
+
var StaleUnknownSchema = object({
|
|
22595
|
+
subject: string(),
|
|
22596
|
+
reason: string()
|
|
22597
|
+
});
|
|
22598
|
+
var StaleDataReportSchema = object({
|
|
22599
|
+
/** Epoch ms of the last finished scan; `null` before the first. */
|
|
22600
|
+
scannedAt: number().nullable(),
|
|
22601
|
+
scanning: boolean(),
|
|
22602
|
+
trigger: _enum([
|
|
22603
|
+
"boot",
|
|
22604
|
+
"interval",
|
|
22605
|
+
"manual"
|
|
22606
|
+
]).nullable(),
|
|
22607
|
+
kinds: array(StaleKindFindingSchema),
|
|
22608
|
+
settingsKeys: array(StaleSettingsKeyFindingSchema),
|
|
22609
|
+
unknown: array(StaleUnknownSchema)
|
|
22610
|
+
});
|
|
21417
22611
|
method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
|
|
21418
22612
|
owner: string().min(1).optional(),
|
|
21419
22613
|
/** `false` skips the per-kind row count (a catalog read only). Default true. */
|
|
@@ -21421,7 +22615,30 @@ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: arr
|
|
|
21421
22615
|
}), object({ kinds: array(StorageKindSchema) }), { auth: "admin" }), method(RecordQueryInputSchema.extend({
|
|
21422
22616
|
owner: string().min(1),
|
|
21423
22617
|
kind: RecordKindNameSchema
|
|
21424
|
-
}), RecordQueryResultSchema, { auth: "admin" })
|
|
22618
|
+
}), RecordQueryResultSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, { auth: "admin" }), method(object({}), StaleDataReportSchema, {
|
|
22619
|
+
kind: "mutation",
|
|
22620
|
+
auth: "admin"
|
|
22621
|
+
}), method(object({
|
|
22622
|
+
owner: string().min(1),
|
|
22623
|
+
kind: RecordKindNameSchema
|
|
22624
|
+
}), object({
|
|
22625
|
+
deleted: number().int().nonnegative(),
|
|
22626
|
+
secretsReleased: number().int().nonnegative()
|
|
22627
|
+
}), {
|
|
22628
|
+
kind: "mutation",
|
|
22629
|
+
auth: "admin",
|
|
22630
|
+
caller: "required"
|
|
22631
|
+
}), method(object({
|
|
22632
|
+
addonId: string().min(1),
|
|
22633
|
+
key: string().min(1)
|
|
22634
|
+
}), object({
|
|
22635
|
+
removed: boolean(),
|
|
22636
|
+
secretsCleared: number().int().nonnegative()
|
|
22637
|
+
}), {
|
|
22638
|
+
kind: "mutation",
|
|
22639
|
+
auth: "admin",
|
|
22640
|
+
caller: "required"
|
|
22641
|
+
});
|
|
21425
22642
|
/**
|
|
21426
22643
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
21427
22644
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
@@ -21560,6 +22777,12 @@ method(object({
|
|
|
21560
22777
|
}), method(object({ deviceId: number().int() }), object({ refs: array(string()).readonly() }), {
|
|
21561
22778
|
kind: "mutation",
|
|
21562
22779
|
auth: "admin"
|
|
22780
|
+
}), method(object({
|
|
22781
|
+
refs: RefListSchema,
|
|
22782
|
+
onBehalfOf: string().min(1)
|
|
22783
|
+
}), object({ released: number().int() }), {
|
|
22784
|
+
kind: "mutation",
|
|
22785
|
+
auth: "admin"
|
|
21563
22786
|
}), method(object({}), array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }), method(object({}), KeychainStateSchema, { auth: "admin" });
|
|
21564
22787
|
/**
|
|
21565
22788
|
* device-history — the ONE mechanism for numeric history (D850).
|
|
@@ -22425,190 +23648,6 @@ function parseAdminBase(baseUrl) {
|
|
|
22425
23648
|
};
|
|
22426
23649
|
}
|
|
22427
23650
|
/**
|
|
22428
|
-
* Identity envelope for a device's upstream-system metadata.
|
|
22429
|
-
*
|
|
22430
|
-
* Two jobs:
|
|
22431
|
-
*
|
|
22432
|
-
* 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
|
|
22433
|
-
* uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
|
|
22434
|
-
* WS-Discovery endpoint UUID, …). Provider code keeps a
|
|
22435
|
-
* `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
|
|
22436
|
-
* without round-tripping through `stableId` / `id` mappings.
|
|
22437
|
-
*
|
|
22438
|
-
* 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
|
|
22439
|
-
* one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
|
|
22440
|
-
* for vendors where MAC is the natural key). For systems where the
|
|
22441
|
-
* live dispatch key can be renamed (HA `entity_id` can be edited),
|
|
22442
|
-
* providers detect renames by matching `uniqueId` and patch `id`
|
|
22443
|
-
* in-place so dispatch keeps working.
|
|
22444
|
-
*
|
|
22445
|
-
* **Display unit and precision are NOT stored here.** They are a read-time
|
|
22446
|
-
* preference (D944). A polymorphic reading stores its `quantity` here once,
|
|
22447
|
-
* at adopt, and a date/time value stores `dateTimeShape` the same way —
|
|
22448
|
-
* neither is repeated on each sample. The `device_class` tag is an
|
|
22449
|
-
* HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
|
|
22450
|
-
* to derive the CamStack `DeviceRole`; it is never stored after that point.
|
|
22451
|
-
*
|
|
22452
|
-
* The `raw` escape hatch carries provider-specific attributes that don't
|
|
22453
|
-
* fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
|
|
22454
|
-
* details, …). Providers control what lands here.
|
|
22455
|
-
*/
|
|
22456
|
-
var SourceInfoSchema = object({
|
|
22457
|
-
/** Live dispatch key — mutable when the source system allows rename. */
|
|
22458
|
-
id: string(),
|
|
22459
|
-
/** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
|
|
22460
|
-
system: string(),
|
|
22461
|
-
/** Immutable upstream identifier when available (HA `unique_id`, … ).
|
|
22462
|
-
* Used to detect rename when `id` changes. */
|
|
22463
|
-
uniqueId: string().optional(),
|
|
22464
|
-
/** Quantity of a polymorphic reading, written once at adopt (D944). */
|
|
22465
|
-
quantity: ReadingQuantitySchema.optional(),
|
|
22466
|
-
/** Date / time / datetime shape of a string reading, written once at adopt. */
|
|
22467
|
-
dateTimeShape: _enum([
|
|
22468
|
-
"date",
|
|
22469
|
-
"time",
|
|
22470
|
-
"datetime"
|
|
22471
|
-
]).optional(),
|
|
22472
|
-
/** Provider-specific extras that don't fit the structured slots.
|
|
22473
|
-
* Whitelist what goes here per provider — do NOT dump entire HA
|
|
22474
|
-
* attribute blobs (would explode the meta JSON column). */
|
|
22475
|
-
raw: record(string(), unknown()).optional()
|
|
22476
|
-
});
|
|
22477
|
-
/**
|
|
22478
|
-
* Build the synthetic SourceInfo every existing provider falls back to
|
|
22479
|
-
* when no upstream value has been persisted. Keeps non-migrated providers
|
|
22480
|
-
* (Reolink / Hikvision / ONVIF / RTSP) functional without code
|
|
22481
|
-
* changes — `id` reuses the CamStack stableId, `system` reuses the addon
|
|
22482
|
-
* id. Real upstream identity replaces this once a provider migrates and
|
|
22483
|
-
* calls `updateSourceInfo()` with concrete values.
|
|
22484
|
-
*/
|
|
22485
|
-
function synthesizeSourceInfo(input) {
|
|
22486
|
-
return {
|
|
22487
|
-
id: input.stableId,
|
|
22488
|
-
system: input.addonId
|
|
22489
|
-
};
|
|
22490
|
-
}
|
|
22491
|
-
/**
|
|
22492
|
-
* Shallow-merge a partial patch over a prior SourceInfo. `undefined`
|
|
22493
|
-
* patch values are ignored (no field clobber); to clear a field
|
|
22494
|
-
* explicitly, providers omit it from the patch and rebuild from scratch
|
|
22495
|
-
* via `updateSourceInfo({})` cycles — uncommon enough that we don't
|
|
22496
|
-
* bother with a `null`-means-delete sentinel.
|
|
22497
|
-
*
|
|
22498
|
-
* The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
|
|
22499
|
-
* merges over existing `raw` keys instead of replacing the whole bag.
|
|
22500
|
-
*/
|
|
22501
|
-
function mergeSourceInfo(prev, patch) {
|
|
22502
|
-
const next = { ...prev };
|
|
22503
|
-
for (const [k, v] of Object.entries(patch)) {
|
|
22504
|
-
if (v === void 0) continue;
|
|
22505
|
-
if (k === "raw" && typeof v === "object" && v !== null) {
|
|
22506
|
-
next.raw = {
|
|
22507
|
-
...prev.raw,
|
|
22508
|
-
...v
|
|
22509
|
-
};
|
|
22510
|
-
continue;
|
|
22511
|
-
}
|
|
22512
|
-
next[k] = v;
|
|
22513
|
-
}
|
|
22514
|
-
return next;
|
|
22515
|
-
}
|
|
22516
|
-
/**
|
|
22517
|
-
* Coerce common unit-of-measurement string variants to a canonical form
|
|
22518
|
-
* so the UI's display templates can rely on stable strings. Pure
|
|
22519
|
-
* passthrough for unknown units — kept conservative on purpose, the goal
|
|
22520
|
-
* is to normalise the long tail of HA `unit_of_measurement` values that
|
|
22521
|
-
* have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
|
|
22522
|
-
*
|
|
22523
|
-
* Extend the map as we encounter real-world data. Returning the raw
|
|
22524
|
-
* input for unknown values is intentional: providers should never
|
|
22525
|
-
* lose information silently here.
|
|
22526
|
-
*/
|
|
22527
|
-
function normalizeUnit(raw) {
|
|
22528
|
-
if (raw == null) return void 0;
|
|
22529
|
-
const trimmed = raw.trim();
|
|
22530
|
-
if (trimmed === "") return void 0;
|
|
22531
|
-
const lower = trimmed.toLowerCase();
|
|
22532
|
-
if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
|
|
22533
|
-
if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
|
|
22534
|
-
if (lower === "k" || lower === "kelvin") return "K";
|
|
22535
|
-
if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
|
|
22536
|
-
if (lower === "lx" || lower === "lux") return "lx";
|
|
22537
|
-
if (lower === "hpa" || lower === "hectopascal") return "hPa";
|
|
22538
|
-
if (lower === "kpa") return "kPa";
|
|
22539
|
-
if (lower === "pa") return "Pa";
|
|
22540
|
-
if (lower === "mbar" || lower === "millibar") return "mbar";
|
|
22541
|
-
if (lower === "w" || lower === "watt") return "W";
|
|
22542
|
-
if (lower === "kw" || lower === "kilowatt") return "kW";
|
|
22543
|
-
if (lower === "wh" || lower === "watt-hour") return "Wh";
|
|
22544
|
-
if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
|
|
22545
|
-
if (lower === "v" || lower === "volt") return "V";
|
|
22546
|
-
if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
|
|
22547
|
-
if (lower === "ma" || lower === "milliampere") return "mA";
|
|
22548
|
-
if (lower === "va") return "VA";
|
|
22549
|
-
if (lower === "ppm") return "ppm";
|
|
22550
|
-
if (lower === "ppb") return "ppb";
|
|
22551
|
-
if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
|
|
22552
|
-
if (lower === "km/h" || lower === "kph") return "km/h";
|
|
22553
|
-
if (lower === "mph") return "mph";
|
|
22554
|
-
if (lower === "m/s") return "m/s";
|
|
22555
|
-
if (lower === "m") return "m";
|
|
22556
|
-
if (lower === "m²" || lower === "m2") return "m²";
|
|
22557
|
-
if (lower === "mm") return "mm";
|
|
22558
|
-
if (lower === "cm") return "cm";
|
|
22559
|
-
if (lower === "km") return "km";
|
|
22560
|
-
if (lower === "in" || lower === "inch" || lower === "inches") return "in";
|
|
22561
|
-
if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
|
|
22562
|
-
if (lower === "kg") return "kg";
|
|
22563
|
-
if (lower === "g") return "g";
|
|
22564
|
-
if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
|
|
22565
|
-
if (lower === "ml") return "mL";
|
|
22566
|
-
if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
|
|
22567
|
-
if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
|
|
22568
|
-
if (lower === "ms") return "ms";
|
|
22569
|
-
if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
|
|
22570
|
-
if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
|
|
22571
|
-
if (lower === "d" || lower === "day" || lower === "days") return "d";
|
|
22572
|
-
if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
|
|
22573
|
-
if (lower === "kib") return "KiB";
|
|
22574
|
-
if (lower === "mib") return "MiB";
|
|
22575
|
-
if (lower === "gib") return "GiB";
|
|
22576
|
-
if (lower === "tib") return "TiB";
|
|
22577
|
-
if (lower === "kb") return "kB";
|
|
22578
|
-
if (lower === "mb") return "MB";
|
|
22579
|
-
if (lower === "gb") return "GB";
|
|
22580
|
-
if (lower === "dbm") return "dBm";
|
|
22581
|
-
if (lower === "dbfs") return "dBFS";
|
|
22582
|
-
if (lower === "inhg") return "inHg";
|
|
22583
|
-
if (lower === "mm/h") return "mm/h";
|
|
22584
|
-
if (lower === "w/m²" || lower === "w/m2") return "W/m²";
|
|
22585
|
-
if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
|
|
22586
|
-
if (lower === "kbps") return "kbps";
|
|
22587
|
-
if (lower === "fps") return "fps";
|
|
22588
|
-
return trimmed;
|
|
22589
|
-
}
|
|
22590
|
-
/**
|
|
22591
|
-
* Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
|
|
22592
|
-
* blob is a free-form `Record<string, unknown>` shared with hardware-
|
|
22593
|
-
* identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
|
|
22594
|
-
* lives under a single nested key to keep it composable with those.
|
|
22595
|
-
*/
|
|
22596
|
-
var SOURCE_INFO_METADATA_KEY = "sourceInfo";
|
|
22597
|
-
/**
|
|
22598
|
-
* Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
|
|
22599
|
-
* with Zod. Returns `null` when the blob is missing or the nested
|
|
22600
|
-
* `sourceInfo` key fails validation — callers fall back to the
|
|
22601
|
-
* synthetic default in that case (see `synthesizeSourceInfo`).
|
|
22602
|
-
*/
|
|
22603
|
-
function extractSourceInfoFromMetadata(metadata) {
|
|
22604
|
-
if (!metadata) return null;
|
|
22605
|
-
const raw = metadata[SOURCE_INFO_METADATA_KEY];
|
|
22606
|
-
if (!raw) return null;
|
|
22607
|
-
const parsed = SourceInfoSchema.safeParse(raw);
|
|
22608
|
-
if (!parsed.success) return null;
|
|
22609
|
-
return parsed.data;
|
|
22610
|
-
}
|
|
22611
|
-
/**
|
|
22612
23651
|
* device-discovery — device-scoped capability for parents that host /
|
|
22613
23652
|
* enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
|
|
22614
23653
|
* integrations). The parent advertises a list of discoverable children,
|
|
@@ -22680,7 +23719,9 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
22680
23719
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
22681
23720
|
sourceInfo: SourceInfoSchema.optional(),
|
|
22682
23721
|
/** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
|
|
22683
|
-
children: array(DiscoveredChildDeviceSchema).readonly().optional()
|
|
23722
|
+
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
23723
|
+
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
23724
|
+
parentStableId: string().optional()
|
|
22684
23725
|
}));
|
|
22685
23726
|
/**
|
|
22686
23727
|
* Status block — populated by the integration after each
|
|
@@ -43092,6 +44133,79 @@ object({
|
|
|
43092
44133
|
metadata: record(string(), unknown()).optional()
|
|
43093
44134
|
});
|
|
43094
44135
|
/**
|
|
44136
|
+
* HOW a provider can be found on the network. The provider only DECLARES the
|
|
44137
|
+
* mode; the `local-network` scan engine runs it once for every provider that
|
|
44138
|
+
* asked (one multicast browse, one host sweep) and hands the raw hits back to
|
|
44139
|
+
* `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
|
|
44140
|
+
* except through a `custom` mode.
|
|
44141
|
+
*/
|
|
44142
|
+
var DiscoveryModeSchema = discriminatedUnion("kind", [
|
|
44143
|
+
object({
|
|
44144
|
+
kind: literal("mdns"),
|
|
44145
|
+
/** DNS-SD service type, e.g. `_esphomelib._tcp`. */
|
|
44146
|
+
serviceType: string().min(1),
|
|
44147
|
+
/** Regular expression an instance name must match (case-insensitive). */
|
|
44148
|
+
instancePattern: string().optional()
|
|
44149
|
+
}),
|
|
44150
|
+
object({
|
|
44151
|
+
kind: literal("ws-discovery"),
|
|
44152
|
+
/** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
|
|
44153
|
+
types: array(string()).optional()
|
|
44154
|
+
}),
|
|
44155
|
+
object({
|
|
44156
|
+
kind: literal("ssdp"),
|
|
44157
|
+
searchTarget: string().min(1)
|
|
44158
|
+
}),
|
|
44159
|
+
object({
|
|
44160
|
+
kind: literal("udp-broadcast"),
|
|
44161
|
+
port: number().int().min(1).max(65535),
|
|
44162
|
+
payloadHex: string().regex(/^([0-9a-fA-F]{2})*$/),
|
|
44163
|
+
/** Port replies arrive on, when the device answers somewhere other than the source port. */
|
|
44164
|
+
listenPort: number().int().min(1).max(65535).optional()
|
|
44165
|
+
}),
|
|
44166
|
+
object({
|
|
44167
|
+
kind: literal("http-probe"),
|
|
44168
|
+
port: number().int().min(1).max(65535),
|
|
44169
|
+
path: string().startsWith("/"),
|
|
44170
|
+
method: _enum(["GET", "POST"]).default("GET"),
|
|
44171
|
+
bodyText: string().optional(),
|
|
44172
|
+
contentType: string().optional(),
|
|
44173
|
+
tls: boolean().default(false),
|
|
44174
|
+
/** Status codes that count as a hit. Omitted = any HTTP answer. */
|
|
44175
|
+
acceptStatus: array(number().int()).optional()
|
|
44176
|
+
}),
|
|
44177
|
+
object({
|
|
44178
|
+
kind: literal("custom"),
|
|
44179
|
+
/** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
|
|
44180
|
+
reason: string().min(1)
|
|
44181
|
+
})
|
|
44182
|
+
]);
|
|
44183
|
+
/** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
|
|
44184
|
+
var DiscoveryHitSchema = object({
|
|
44185
|
+
/** Index into the provider's own `getDiscoveryModes()` array. */
|
|
44186
|
+
modeIndex: number().int().min(0),
|
|
44187
|
+
host: string(),
|
|
44188
|
+
port: number().int(),
|
|
44189
|
+
http: object({
|
|
44190
|
+
status: number().int(),
|
|
44191
|
+
headers: record(string(), string()),
|
|
44192
|
+
/** At most 16 KiB. */
|
|
44193
|
+
bodyText: string()
|
|
44194
|
+
}).optional(),
|
|
44195
|
+
mdns: object({
|
|
44196
|
+
instance: string(),
|
|
44197
|
+
host: string(),
|
|
44198
|
+
port: number().int(),
|
|
44199
|
+
txt: record(string(), string())
|
|
44200
|
+
}).optional(),
|
|
44201
|
+
udp: object({ payloadHex: string() }).optional(),
|
|
44202
|
+
wsd: object({
|
|
44203
|
+
xaddrs: array(string()),
|
|
44204
|
+
types: array(string()),
|
|
44205
|
+
scopes: array(string())
|
|
44206
|
+
}).optional()
|
|
44207
|
+
});
|
|
44208
|
+
/**
|
|
43095
44209
|
* Candidate handed back from discovery and accepted by
|
|
43096
44210
|
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
43097
44211
|
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
@@ -43126,7 +44240,17 @@ var DiscoveryCandidateSchema = object({
|
|
|
43126
44240
|
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
43127
44241
|
onboardedDeviceId: number().optional(),
|
|
43128
44242
|
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
43129
|
-
onboardedName: string().optional()
|
|
44243
|
+
onboardedName: string().optional(),
|
|
44244
|
+
/**
|
|
44245
|
+
* This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
|
|
44246
|
+
* not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
|
|
44247
|
+
* which is still read for one release.
|
|
44248
|
+
*/
|
|
44249
|
+
setupIntegration: boolean().optional(),
|
|
44250
|
+
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
44251
|
+
parentStableId: string().optional(),
|
|
44252
|
+
/** `false` = shown for context but not pickable. Default true. */
|
|
44253
|
+
selectable: boolean().optional()
|
|
43130
44254
|
});
|
|
43131
44255
|
/**
|
|
43132
44256
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -43223,6 +44347,24 @@ var deviceProviderCapability = {
|
|
|
43223
44347
|
auth: "admin"
|
|
43224
44348
|
}),
|
|
43225
44349
|
/**
|
|
44350
|
+
* How this provider is found on the network. `[]` = the provider has no
|
|
44351
|
+
* declarative mode and keeps answering `discoverDevices` itself.
|
|
44352
|
+
*/
|
|
44353
|
+
getDiscoveryModes: method(object({}), array(DiscoveryModeSchema)),
|
|
44354
|
+
/**
|
|
44355
|
+
* Turn the engine's raw hits into candidates. The provider may make
|
|
44356
|
+
* follow-up UNICAST requests to a hit host (an authenticated call, an
|
|
44357
|
+
* identity check) and never sweeps a subnet. `params` is the per-scan form
|
|
44358
|
+
* from `getDiscoveryParamsSchema`.
|
|
44359
|
+
*/
|
|
44360
|
+
classifyDiscoveryHits: method(object({
|
|
44361
|
+
hits: array(DiscoveryHitSchema),
|
|
44362
|
+
params: record(string(), unknown()).optional()
|
|
44363
|
+
}), array(DiscoveryCandidateSchema), {
|
|
44364
|
+
kind: "mutation",
|
|
44365
|
+
auth: "admin"
|
|
44366
|
+
}),
|
|
44367
|
+
/**
|
|
43226
44368
|
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
43227
44369
|
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
43228
44370
|
* `null` when the provider takes no extra scan params — the generic
|
|
@@ -43987,17 +45129,23 @@ method(object({
|
|
|
43987
45129
|
}), method(object({}), object({ providers: array(object({
|
|
43988
45130
|
addonId: string(),
|
|
43989
45131
|
label: string()
|
|
43990
|
-
})).readonly() }), { auth: "admin" }), method(
|
|
43991
|
-
|
|
43992
|
-
|
|
43993
|
-
|
|
43994
|
-
|
|
43995
|
-
|
|
43996
|
-
|
|
43997
|
-
|
|
43998
|
-
}),
|
|
45132
|
+
})).readonly() }), { auth: "admin" }), method(
|
|
45133
|
+
/** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
|
|
45134
|
+
object({ subnets: array(string()).optional() }),
|
|
45135
|
+
object({ groups: array(object({
|
|
45136
|
+
addonId: string(),
|
|
45137
|
+
label: string(),
|
|
45138
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
45139
|
+
error: string().nullable()
|
|
45140
|
+
})).readonly() }),
|
|
45141
|
+
{
|
|
45142
|
+
kind: "mutation",
|
|
45143
|
+
auth: "admin"
|
|
45144
|
+
}
|
|
45145
|
+
), method(object({
|
|
43999
45146
|
addonId: string(),
|
|
44000
|
-
params: record(string(), unknown()).optional()
|
|
45147
|
+
params: record(string(), unknown()).optional(),
|
|
45148
|
+
subnets: array(string()).optional()
|
|
44001
45149
|
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
44002
45150
|
kind: "mutation",
|
|
44003
45151
|
auth: "admin"
|
|
@@ -55637,11 +56785,13 @@ var ImageContractSchema = object({
|
|
|
55637
56785
|
* The shell verdict (see {@link WrapperKindSchema}) with the proofs that
|
|
55638
56786
|
* produced it.
|
|
55639
56787
|
*
|
|
55640
|
-
* There is deliberately NO `canSelfUpdate`
|
|
55641
|
-
*
|
|
55642
|
-
*
|
|
55643
|
-
*
|
|
55644
|
-
*
|
|
56788
|
+
* There is deliberately NO `canSelfUpdate` boolean: a wrapper update is an
|
|
56789
|
+
* ANNOUNCEMENT. The notification body carries the exact command for a `docker`
|
|
56790
|
+
* shell and says the app restarts into the new build for `electron`, and that
|
|
56791
|
+
* sentence is chosen from `kind` alone — a boolean nothing reads is the defect
|
|
56792
|
+
* this repo keeps paying for. The one shell that CAN be acted on is the
|
|
56793
|
+
* Electron app, and only through `installDesktopUpdate` (see
|
|
56794
|
+
* {@link DesktopAppStatusSchema} for what the node reads from the app first).
|
|
55645
56795
|
*/
|
|
55646
56796
|
var WrapperIdentitySchema = object({
|
|
55647
56797
|
kind: _enum([
|
|
@@ -55689,6 +56839,49 @@ var ServerRollbackInfoSchema = object({
|
|
|
55689
56839
|
reason: string()
|
|
55690
56840
|
});
|
|
55691
56841
|
/**
|
|
56842
|
+
* What the native desktop app (Electron) that hosts this node says about
|
|
56843
|
+
* itself, read from `<dataDir>/server-root/desktop-app-state.json` (ADR-0971).
|
|
56844
|
+
* Absent on the status = no desktop app, or it never wrote the file.
|
|
56845
|
+
*
|
|
56846
|
+
* `stale` is the honest answer to an app that stopped writing (closed, hung,
|
|
56847
|
+
* crashed): the update facts are then the LAST ones known, not current ones,
|
|
56848
|
+
* and a consumer must not read `update.available: false` from a stale record
|
|
56849
|
+
* as "no update". `stateAtMs` is when the app last wrote.
|
|
56850
|
+
*/
|
|
56851
|
+
var DesktopAppStatusSchema = object({
|
|
56852
|
+
/** The Electron app's own version (not the node closure's). */
|
|
56853
|
+
version: string(),
|
|
56854
|
+
update: object({
|
|
56855
|
+
available: boolean(),
|
|
56856
|
+
/** The new build is downloaded and `installDesktopUpdate` can apply it. */
|
|
56857
|
+
downloaded: boolean(),
|
|
56858
|
+
version: string().nullable(),
|
|
56859
|
+
checking: boolean(),
|
|
56860
|
+
/** Why the app's last update check produced no answer; `null` when it did. */
|
|
56861
|
+
error: string().nullable()
|
|
56862
|
+
}),
|
|
56863
|
+
stateAtMs: number(),
|
|
56864
|
+
stale: boolean()
|
|
56865
|
+
});
|
|
56866
|
+
/**
|
|
56867
|
+
* Why `installDesktopUpdate` did nothing. Stable names a UI maps to a sentence:
|
|
56868
|
+
* - `not-electron` — this node does not run inside the desktop app.
|
|
56869
|
+
* - `no-app-state` — the app never published its state (or the file is unreadable).
|
|
56870
|
+
* - `app-state-stale` — the app stopped writing; it is probably not running.
|
|
56871
|
+
* - `not-downloaded` — no update is downloaded yet; nothing to install.
|
|
56872
|
+
*/
|
|
56873
|
+
var InstallDesktopUpdateReasonSchema = _enum([
|
|
56874
|
+
"not-electron",
|
|
56875
|
+
"no-app-state",
|
|
56876
|
+
"app-state-stale",
|
|
56877
|
+
"not-downloaded"
|
|
56878
|
+
]);
|
|
56879
|
+
var InstallDesktopUpdateResultSchema = object({
|
|
56880
|
+
accepted: boolean(),
|
|
56881
|
+
/** Non-null exactly when `accepted` is false. */
|
|
56882
|
+
reason: InstallDesktopUpdateReasonSchema.nullable()
|
|
56883
|
+
});
|
|
56884
|
+
/**
|
|
55692
56885
|
* The runner boot check (D877): the runners this node PLANNED (hub: one per
|
|
55693
56886
|
* `resolveRunnerId` over its forked addons; agent: the runners its process
|
|
55694
56887
|
* service holds, because only the hub plans) against the states the process
|
|
@@ -55792,7 +56985,12 @@ var ServerPackageStatusSchema = object({
|
|
|
55792
56985
|
* on a provider that predates it. Absent is "not checked yet", never "all
|
|
55793
56986
|
* well": a consumer that needs the answer treats it as pending.
|
|
55794
56987
|
*/
|
|
55795
|
-
runners: RunnerBootCheckSchema.optional()
|
|
56988
|
+
runners: RunnerBootCheckSchema.optional(),
|
|
56989
|
+
/**
|
|
56990
|
+
* The desktop app hosting this node (see {@link DesktopAppStatusSchema}).
|
|
56991
|
+
* Optional for version skew AND because most nodes have no desktop app.
|
|
56992
|
+
*/
|
|
56993
|
+
desktopApp: DesktopAppStatusSchema.optional()
|
|
55796
56994
|
});
|
|
55797
56995
|
var ServerUpdateCheckResultSchema = object({
|
|
55798
56996
|
packageName: string(),
|
|
@@ -55897,6 +57095,9 @@ method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), S
|
|
|
55897
57095
|
kind: "mutation",
|
|
55898
57096
|
auth: "admin",
|
|
55899
57097
|
timeoutMs: 16 * 6e4
|
|
57098
|
+
}), method(_void(), InstallDesktopUpdateResultSchema, {
|
|
57099
|
+
kind: "mutation",
|
|
57100
|
+
auth: "admin"
|
|
55900
57101
|
}), method(_void(), ServerUpdateActionResultSchema, {
|
|
55901
57102
|
kind: "mutation",
|
|
55902
57103
|
auth: "admin"
|
|
@@ -56627,6 +57828,12 @@ var TerminalInstanceInfoSchema = object({
|
|
|
56627
57828
|
instanceId: string(),
|
|
56628
57829
|
cameraStableId: string(),
|
|
56629
57830
|
nodeId: string(),
|
|
57831
|
+
/**
|
|
57832
|
+
* False while the node the instance is bound to is not in the cluster
|
|
57833
|
+
* topology (forgotten, renamed, re-paired). The instance is kept; the
|
|
57834
|
+
* operator re-points it with `updateInstance({ targetNodeId })`.
|
|
57835
|
+
*/
|
|
57836
|
+
nodeInCluster: boolean(),
|
|
56630
57837
|
profileId: string(),
|
|
56631
57838
|
profileLabel: string(),
|
|
56632
57839
|
name: string(),
|
|
@@ -56676,6 +57883,8 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
56676
57883
|
auth: "admin"
|
|
56677
57884
|
}), method(object({
|
|
56678
57885
|
instanceId: string().min(1),
|
|
57886
|
+
/** Re-point the instance to another node; the profile must exist there. */
|
|
57887
|
+
targetNodeId: string().min(1).max(256).optional(),
|
|
56679
57888
|
name: string().trim().min(1).max(160).optional(),
|
|
56680
57889
|
executable: string().max(1024).optional(),
|
|
56681
57890
|
args: array(string().max(2048)).max(64).optional(),
|
|
@@ -59085,13 +60294,19 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
|
59085
60294
|
}), method(ActionIdInput, _void(), {
|
|
59086
60295
|
kind: "mutation",
|
|
59087
60296
|
auth: "admin"
|
|
59088
|
-
}), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({
|
|
60297
|
+
}), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({
|
|
60298
|
+
action: ActionEntrySchema,
|
|
60299
|
+
warnings: array(UnitWarningSchema).optional()
|
|
60300
|
+
}), {
|
|
59089
60301
|
kind: "mutation",
|
|
59090
60302
|
auth: "admin"
|
|
59091
60303
|
}), method(object({
|
|
59092
60304
|
actionId: string().min(1),
|
|
59093
60305
|
action: ActionInputSchema.partial()
|
|
59094
|
-
}), object({
|
|
60306
|
+
}), object({
|
|
60307
|
+
action: ActionEntrySchema,
|
|
60308
|
+
warnings: array(UnitWarningSchema).optional()
|
|
60309
|
+
}), {
|
|
59095
60310
|
kind: "mutation",
|
|
59096
60311
|
auth: "admin"
|
|
59097
60312
|
}), method(ActionIdInput, _void(), {
|
|
@@ -59673,6 +60888,30 @@ object({
|
|
|
59673
60888
|
at: number()
|
|
59674
60889
|
}).nullable()
|
|
59675
60890
|
});
|
|
60891
|
+
var RemoteButtonShapeSchema = _enum([
|
|
60892
|
+
"rect",
|
|
60893
|
+
"rounded",
|
|
60894
|
+
"round",
|
|
60895
|
+
"pill"
|
|
60896
|
+
]);
|
|
60897
|
+
var RemoteButtonToneSchema = _enum([
|
|
60898
|
+
"neutral",
|
|
60899
|
+
"accent",
|
|
60900
|
+
"danger",
|
|
60901
|
+
"red",
|
|
60902
|
+
"green",
|
|
60903
|
+
"yellow",
|
|
60904
|
+
"blue"
|
|
60905
|
+
]);
|
|
60906
|
+
var RemoteGroupFrameSchema = _enum([
|
|
60907
|
+
"none",
|
|
60908
|
+
"panel",
|
|
60909
|
+
"divider",
|
|
60910
|
+
"dpad",
|
|
60911
|
+
"rocker-v",
|
|
60912
|
+
"rocker-h",
|
|
60913
|
+
"keypad"
|
|
60914
|
+
]);
|
|
59676
60915
|
var RemoteLayoutButtonSchema = object({
|
|
59677
60916
|
id: string().min(1).max(40),
|
|
59678
60917
|
/** 0-based grid position. */
|
|
@@ -59682,12 +60921,94 @@ var RemoteLayoutButtonSchema = object({
|
|
|
59682
60921
|
colSpan: number().int().min(1).max(24),
|
|
59683
60922
|
icon: string().min(1).max(60).optional(),
|
|
59684
60923
|
label: string().min(1).max(40).optional(),
|
|
59685
|
-
role: RemoteRoleSchema
|
|
60924
|
+
role: RemoteRoleSchema,
|
|
60925
|
+
/** Absent = the plain rendering every layout had before shapes existed. */
|
|
60926
|
+
shape: RemoteButtonShapeSchema.optional(),
|
|
60927
|
+
tone: RemoteButtonToneSchema.optional(),
|
|
60928
|
+
/** The group this button belongs to; it must lie inside that group's rectangle. */
|
|
60929
|
+
groupId: string().min(1).max(40).optional()
|
|
59686
60930
|
});
|
|
60931
|
+
/**
|
|
60932
|
+
* A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
|
|
60933
|
+
* titled panel, a section divider, a D-pad ring. `componentId` names the palette
|
|
60934
|
+
* entry it came from (informational: the layout is self-contained).
|
|
60935
|
+
*/
|
|
60936
|
+
var RemoteLayoutGroupSchema = object({
|
|
60937
|
+
id: string().min(1).max(40),
|
|
60938
|
+
componentId: string().min(1).max(80),
|
|
60939
|
+
frame: RemoteGroupFrameSchema,
|
|
60940
|
+
row: number().int().min(0).max(24),
|
|
60941
|
+
col: number().int().min(0).max(24),
|
|
60942
|
+
rowSpan: number().int().min(1).max(24),
|
|
60943
|
+
colSpan: number().int().min(1).max(24),
|
|
60944
|
+
title: string().min(1).max(40).optional()
|
|
60945
|
+
});
|
|
60946
|
+
function rectsOverlap(a, b) {
|
|
60947
|
+
return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
|
|
60948
|
+
}
|
|
60949
|
+
function rectContains(outer, inner) {
|
|
60950
|
+
return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
|
|
60951
|
+
}
|
|
60952
|
+
/**
|
|
60953
|
+
* The group rules of a layout: inside the grid, no two groups overlap, a
|
|
60954
|
+
* member lies inside its group, a divider holds nothing, and a button that is
|
|
60955
|
+
* not a member never sits inside someone else's rectangle (a group moves as
|
|
60956
|
+
* one thing, so a stray key under it would be left behind).
|
|
60957
|
+
*/
|
|
60958
|
+
function refineGroups(grid, ctx) {
|
|
60959
|
+
const groups = grid.groups ?? [];
|
|
60960
|
+
const byId = /* @__PURE__ */ new Map();
|
|
60961
|
+
const fail = (message, path) => {
|
|
60962
|
+
ctx.addIssue({
|
|
60963
|
+
code: "custom",
|
|
60964
|
+
message,
|
|
60965
|
+
path
|
|
60966
|
+
});
|
|
60967
|
+
};
|
|
60968
|
+
groups.forEach((group, index) => {
|
|
60969
|
+
if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
|
|
60970
|
+
"groups",
|
|
60971
|
+
index,
|
|
60972
|
+
"id"
|
|
60973
|
+
]);
|
|
60974
|
+
byId.set(group.id, group);
|
|
60975
|
+
if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
|
|
60976
|
+
for (let other = 0; other < index; other += 1) {
|
|
60977
|
+
const earlier = groups[other];
|
|
60978
|
+
if (earlier !== void 0 && rectsOverlap(earlier, group)) {
|
|
60979
|
+
fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
|
|
60980
|
+
return;
|
|
60981
|
+
}
|
|
60982
|
+
}
|
|
60983
|
+
});
|
|
60984
|
+
grid.buttons.forEach((button, index) => {
|
|
60985
|
+
if (button.groupId !== void 0) {
|
|
60986
|
+
const group = byId.get(button.groupId);
|
|
60987
|
+
if (group === void 0) {
|
|
60988
|
+
fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
|
|
60989
|
+
"buttons",
|
|
60990
|
+
index,
|
|
60991
|
+
"groupId"
|
|
60992
|
+
]);
|
|
60993
|
+
return;
|
|
60994
|
+
}
|
|
60995
|
+
if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
|
|
60996
|
+
"buttons",
|
|
60997
|
+
index,
|
|
60998
|
+
"groupId"
|
|
60999
|
+
]);
|
|
61000
|
+
if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
|
|
61001
|
+
return;
|
|
61002
|
+
}
|
|
61003
|
+
const foreign = groups.find((group) => rectsOverlap(group, button));
|
|
61004
|
+
if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
|
|
61005
|
+
});
|
|
61006
|
+
}
|
|
59687
61007
|
object({
|
|
59688
61008
|
columns: number().int().min(1).max(24),
|
|
59689
61009
|
rows: number().int().min(1).max(24),
|
|
59690
|
-
buttons: array(RemoteLayoutButtonSchema).max(200)
|
|
61010
|
+
buttons: array(RemoteLayoutButtonSchema).max(200),
|
|
61011
|
+
groups: array(RemoteLayoutGroupSchema).max(50).optional()
|
|
59691
61012
|
}).superRefine((grid, ctx) => {
|
|
59692
61013
|
const ids = /* @__PURE__ */ new Set();
|
|
59693
61014
|
const taken = /* @__PURE__ */ new Map();
|
|
@@ -59724,6 +61045,7 @@ object({
|
|
|
59724
61045
|
taken.set(cell, button.id);
|
|
59725
61046
|
}
|
|
59726
61047
|
});
|
|
61048
|
+
refineGroups(grid, ctx);
|
|
59727
61049
|
});
|
|
59728
61050
|
var RemoteBoundCommandSchema = object({
|
|
59729
61051
|
role: RemoteRoleSchema,
|
|
@@ -59855,6 +61177,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
|
|
|
59855
61177
|
kind: "mutation",
|
|
59856
61178
|
auth: "admin"
|
|
59857
61179
|
});
|
|
61180
|
+
var RemoteComponentCategorySchema = _enum([
|
|
61181
|
+
"power",
|
|
61182
|
+
"navigation",
|
|
61183
|
+
"volume",
|
|
61184
|
+
"numbers",
|
|
61185
|
+
"media",
|
|
61186
|
+
"colour",
|
|
61187
|
+
"apps",
|
|
61188
|
+
"layout"
|
|
61189
|
+
]);
|
|
61190
|
+
/** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
|
|
61191
|
+
var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
|
|
61192
|
+
var RemoteComponentButtonSchema = object({
|
|
61193
|
+
/** Joined to the instance id when the component is inserted. */
|
|
61194
|
+
idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
|
|
61195
|
+
row: number().int().min(0).max(24),
|
|
61196
|
+
col: number().int().min(0).max(24),
|
|
61197
|
+
rowSpan: number().int().min(1).max(24),
|
|
61198
|
+
colSpan: number().int().min(1).max(24),
|
|
61199
|
+
role: RemoteRoleSchema,
|
|
61200
|
+
icon: string().min(1).max(60).optional(),
|
|
61201
|
+
label: string().min(1).max(40).optional(),
|
|
61202
|
+
shape: RemoteButtonShapeSchema.optional(),
|
|
61203
|
+
tone: RemoteButtonToneSchema.optional()
|
|
61204
|
+
});
|
|
61205
|
+
var RemoteLayoutComponentSchema = object({
|
|
61206
|
+
id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
|
|
61207
|
+
name: string().min(1).max(60),
|
|
61208
|
+
nameI18n: object({
|
|
61209
|
+
en: string().min(1).max(60),
|
|
61210
|
+
it: string().min(1).max(60)
|
|
61211
|
+
}).optional(),
|
|
61212
|
+
/** A display icon key, resolved by the UI like a button icon. */
|
|
61213
|
+
icon: string().min(1).max(60),
|
|
61214
|
+
category: RemoteComponentCategorySchema,
|
|
61215
|
+
size: object({
|
|
61216
|
+
rows: number().int().min(1).max(24),
|
|
61217
|
+
cols: number().int().min(1).max(24)
|
|
61218
|
+
}),
|
|
61219
|
+
/** The frame drawn around the whole component, or null for loose buttons. */
|
|
61220
|
+
group: object({
|
|
61221
|
+
frame: RemoteGroupFrameSchema,
|
|
61222
|
+
title: string().min(1).max(40).optional()
|
|
61223
|
+
}).nullable(),
|
|
61224
|
+
buttons: array(RemoteComponentButtonSchema).max(40)
|
|
61225
|
+
}).superRefine((component, ctx) => {
|
|
61226
|
+
const suffixes = /* @__PURE__ */ new Set();
|
|
61227
|
+
component.buttons.forEach((button, index) => {
|
|
61228
|
+
if (suffixes.has(button.idSuffix)) ctx.addIssue({
|
|
61229
|
+
code: "custom",
|
|
61230
|
+
message: `button "${button.idSuffix}" is listed twice`,
|
|
61231
|
+
path: [
|
|
61232
|
+
"buttons",
|
|
61233
|
+
index,
|
|
61234
|
+
"idSuffix"
|
|
61235
|
+
]
|
|
61236
|
+
});
|
|
61237
|
+
suffixes.add(button.idSuffix);
|
|
61238
|
+
if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
|
|
61239
|
+
ctx.addIssue({
|
|
61240
|
+
code: "custom",
|
|
61241
|
+
message: `button "${button.idSuffix}" leaves the component`,
|
|
61242
|
+
path: ["buttons", index]
|
|
61243
|
+
});
|
|
61244
|
+
return;
|
|
61245
|
+
}
|
|
61246
|
+
for (let other = 0; other < index; other += 1) {
|
|
61247
|
+
const earlier = component.buttons[other];
|
|
61248
|
+
if (earlier !== void 0 && rectsOverlap(earlier, button)) {
|
|
61249
|
+
ctx.addIssue({
|
|
61250
|
+
code: "custom",
|
|
61251
|
+
message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
|
|
61252
|
+
path: ["buttons", index]
|
|
61253
|
+
});
|
|
61254
|
+
return;
|
|
61255
|
+
}
|
|
61256
|
+
}
|
|
61257
|
+
});
|
|
61258
|
+
if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
|
|
61259
|
+
code: "custom",
|
|
61260
|
+
message: "a divider component holds no buttons",
|
|
61261
|
+
path: ["buttons"]
|
|
61262
|
+
});
|
|
61263
|
+
});
|
|
61264
|
+
method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
|
|
59858
61265
|
/**
|
|
59859
61266
|
* button — device-scoped capability for HA `button.*` / `input_button.*`
|
|
59860
61267
|
* fire-only actuators. A button entity has no readable state — pressing it
|
|
@@ -63219,7 +64626,9 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
63219
64626
|
id: string(),
|
|
63220
64627
|
settings: record(string(), unknown()),
|
|
63221
64628
|
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
63222
|
-
secretKeys: array(string()).optional()
|
|
64629
|
+
secretKeys: array(string()).optional(),
|
|
64630
|
+
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
64631
|
+
skipRestart: boolean().optional()
|
|
63223
64632
|
}), object({ success: boolean() }), {
|
|
63224
64633
|
kind: "mutation",
|
|
63225
64634
|
auth: "admin"
|
|
@@ -63847,6 +65256,39 @@ var TlsStatusSchema = object({
|
|
|
63847
65256
|
hasCa: boolean()
|
|
63848
65257
|
}).nullable()
|
|
63849
65258
|
});
|
|
65259
|
+
/** One provider's ask of the scan engine: the modes it declared. */
|
|
65260
|
+
var ScanRequestSchema = object({
|
|
65261
|
+
providerId: string().min(1),
|
|
65262
|
+
modes: array(DiscoveryModeSchema)
|
|
65263
|
+
});
|
|
65264
|
+
var ScanErrorSchema = object({
|
|
65265
|
+
/** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
|
|
65266
|
+
providerId: string().optional(),
|
|
65267
|
+
/** The mode family that failed (`mdns`, `http-probe`, …). */
|
|
65268
|
+
mode: string().optional(),
|
|
65269
|
+
message: string()
|
|
65270
|
+
});
|
|
65271
|
+
/** What one scan did, per mode family; the engine logs the same numbers once. */
|
|
65272
|
+
var ScanStatsSchema = object({
|
|
65273
|
+
hostsProbed: number().int(),
|
|
65274
|
+
hits: number().int(),
|
|
65275
|
+
timeouts: number().int(),
|
|
65276
|
+
byMode: record(string(), object({
|
|
65277
|
+
probed: number().int(),
|
|
65278
|
+
hits: number().int(),
|
|
65279
|
+
timeouts: number().int()
|
|
65280
|
+
}))
|
|
65281
|
+
});
|
|
65282
|
+
var ScanNetworkResultSchema = object({
|
|
65283
|
+
hits: array(object({
|
|
65284
|
+
providerId: string(),
|
|
65285
|
+
hit: DiscoveryHitSchema
|
|
65286
|
+
})),
|
|
65287
|
+
/** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
|
|
65288
|
+
subnetsScanned: array(string()),
|
|
65289
|
+
errors: array(ScanErrorSchema),
|
|
65290
|
+
stats: ScanStatsSchema
|
|
65291
|
+
});
|
|
63850
65292
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
63851
65293
|
/**
|
|
63852
65294
|
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
@@ -63901,6 +65343,16 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
63901
65343
|
}), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
|
|
63902
65344
|
kind: "mutation",
|
|
63903
65345
|
auth: "admin"
|
|
65346
|
+
}), method(object({
|
|
65347
|
+
requests: array(ScanRequestSchema),
|
|
65348
|
+
subnets: array(string()).optional()
|
|
65349
|
+
}), ScanNetworkResultSchema, {
|
|
65350
|
+
kind: "mutation",
|
|
65351
|
+
auth: "admin",
|
|
65352
|
+
timeoutMs: 9e4
|
|
65353
|
+
}), method(_void(), object({ subnets: array(string()) }), { auth: "admin" }), method(object({ subnets: array(string()) }), object({ subnets: array(string()) }), {
|
|
65354
|
+
kind: "mutation",
|
|
65355
|
+
auth: "admin"
|
|
63904
65356
|
}), method(_void(), TlsStatusSchema, {
|
|
63905
65357
|
kind: "mutation",
|
|
63906
65358
|
auth: "admin"
|
|
@@ -65988,6 +67440,14 @@ var PetFeederDeviceStatusSchema = _enum([
|
|
|
65988
67440
|
"on_batteries"
|
|
65989
67441
|
]);
|
|
65990
67442
|
var gramsPortion = number().int().min(4).max(200);
|
|
67443
|
+
/** A portion REQUESTED of the feeder is a mass the method takes in GRAMS (the
|
|
67444
|
+
* hardware's own unit). The bounds are grams; a caller entering another unit
|
|
67445
|
+
* converts to grams first (`paramMeta`, D972). The STATUS fields above stay
|
|
67446
|
+
* plain grams — they are not readings in the kilogram base. */
|
|
67447
|
+
var gramsParam = gramsPortion.meta(paramMeta({
|
|
67448
|
+
quantity: "mass",
|
|
67449
|
+
unit: "g"
|
|
67450
|
+
}));
|
|
65991
67451
|
var PetFeederStatusSchema = object({
|
|
65992
67452
|
/** Food in the TANK (hopper), as the feeder's own sensor reports it
|
|
65993
67453
|
* (`state.food`). A coarse level code, not grams — `0` is empty. The
|
|
@@ -66076,9 +67536,9 @@ var petFeederCapability = {
|
|
|
66076
67536
|
*/
|
|
66077
67537
|
feed: method(object({
|
|
66078
67538
|
deviceId: number().int().nonnegative(),
|
|
66079
|
-
grams:
|
|
66080
|
-
hopper1:
|
|
66081
|
-
hopper2:
|
|
67539
|
+
grams: gramsParam.optional(),
|
|
67540
|
+
hopper1: gramsParam.optional(),
|
|
67541
|
+
hopper2: gramsParam.optional()
|
|
66082
67542
|
}), _void(), {
|
|
66083
67543
|
kind: "mutation",
|
|
66084
67544
|
auth: "protected"
|
|
@@ -67620,7 +69080,7 @@ var runningStateCapability = {
|
|
|
67620
69080
|
volatileStateFields: ["lastFetchedAt"],
|
|
67621
69081
|
customize: runningCustomize
|
|
67622
69082
|
};
|
|
67623
|
-
DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
|
|
69083
|
+
DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
|
|
67624
69084
|
kind: "mutation",
|
|
67625
69085
|
auth: "admin"
|
|
67626
69086
|
});
|
|
@@ -72233,6 +73693,124 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
|
|
|
72233
73693
|
kind: "mutation",
|
|
72234
73694
|
auth: "admin"
|
|
72235
73695
|
});
|
|
73696
|
+
var btn = (idSuffix, row, col, init) => ({
|
|
73697
|
+
idSuffix,
|
|
73698
|
+
row,
|
|
73699
|
+
col,
|
|
73700
|
+
rowSpan: 1,
|
|
73701
|
+
colSpan: 1,
|
|
73702
|
+
...init
|
|
73703
|
+
});
|
|
73704
|
+
function rocker(id, name, it, frame, title, buttons, size) {
|
|
73705
|
+
return {
|
|
73706
|
+
id,
|
|
73707
|
+
name,
|
|
73708
|
+
nameI18n: {
|
|
73709
|
+
en: name,
|
|
73710
|
+
it
|
|
73711
|
+
},
|
|
73712
|
+
icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
|
|
73713
|
+
category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
|
|
73714
|
+
size,
|
|
73715
|
+
group: title === null ? { frame } : {
|
|
73716
|
+
frame,
|
|
73717
|
+
title
|
|
73718
|
+
},
|
|
73719
|
+
buttons
|
|
73720
|
+
};
|
|
73721
|
+
}
|
|
73722
|
+
btn("power", 0, 0, {
|
|
73723
|
+
role: "power",
|
|
73724
|
+
shape: "round",
|
|
73725
|
+
tone: "danger"
|
|
73726
|
+
}), btn("up", 0, 1, {
|
|
73727
|
+
role: "up",
|
|
73728
|
+
icon: "chevron-up"
|
|
73729
|
+
}), btn("left", 1, 0, {
|
|
73730
|
+
role: "left",
|
|
73731
|
+
icon: "chevron-left"
|
|
73732
|
+
}), btn("ok", 1, 1, {
|
|
73733
|
+
role: "ok",
|
|
73734
|
+
shape: "round",
|
|
73735
|
+
tone: "accent"
|
|
73736
|
+
}), btn("right", 1, 2, {
|
|
73737
|
+
role: "right",
|
|
73738
|
+
icon: "chevron-right"
|
|
73739
|
+
}), btn("down", 2, 1, {
|
|
73740
|
+
role: "down",
|
|
73741
|
+
icon: "chevron-down"
|
|
73742
|
+
}), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
|
|
73743
|
+
role: "volume_up",
|
|
73744
|
+
icon: "plus",
|
|
73745
|
+
shape: "pill"
|
|
73746
|
+
}), btn("down", 1, 0, {
|
|
73747
|
+
role: "volume_down",
|
|
73748
|
+
icon: "minus",
|
|
73749
|
+
shape: "pill"
|
|
73750
|
+
})], {
|
|
73751
|
+
rows: 2,
|
|
73752
|
+
cols: 1
|
|
73753
|
+
}), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
|
|
73754
|
+
role: "channel_up",
|
|
73755
|
+
icon: "chevron-up",
|
|
73756
|
+
shape: "pill"
|
|
73757
|
+
}), btn("down", 1, 0, {
|
|
73758
|
+
role: "channel_down",
|
|
73759
|
+
icon: "chevron-down",
|
|
73760
|
+
shape: "pill"
|
|
73761
|
+
})], {
|
|
73762
|
+
rows: 2,
|
|
73763
|
+
cols: 1
|
|
73764
|
+
}), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
|
|
73765
|
+
role: "custom:minus",
|
|
73766
|
+
label: "Minus",
|
|
73767
|
+
icon: "minus",
|
|
73768
|
+
shape: "pill"
|
|
73769
|
+
}), btn("plus", 0, 1, {
|
|
73770
|
+
role: "custom:plus",
|
|
73771
|
+
label: "Plus",
|
|
73772
|
+
icon: "plus",
|
|
73773
|
+
shape: "pill"
|
|
73774
|
+
})], {
|
|
73775
|
+
rows: 1,
|
|
73776
|
+
cols: 2
|
|
73777
|
+
}), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
|
|
73778
|
+
role: "custom:color_red",
|
|
73779
|
+
label: "Red",
|
|
73780
|
+
tone: "red"
|
|
73781
|
+
}), btn("green", 0, 1, {
|
|
73782
|
+
role: "custom:color_green",
|
|
73783
|
+
label: "Green",
|
|
73784
|
+
tone: "green"
|
|
73785
|
+
}), btn("yellow", 0, 2, {
|
|
73786
|
+
role: "custom:color_yellow",
|
|
73787
|
+
label: "Yellow",
|
|
73788
|
+
tone: "yellow"
|
|
73789
|
+
}), btn("blue", 0, 3, {
|
|
73790
|
+
role: "custom:color_blue",
|
|
73791
|
+
label: "Blue",
|
|
73792
|
+
tone: "blue"
|
|
73793
|
+
}), btn("rewind", 0, 0, {
|
|
73794
|
+
role: "rewind",
|
|
73795
|
+
icon: "rewind"
|
|
73796
|
+
}), btn("play", 0, 1, {
|
|
73797
|
+
role: "play",
|
|
73798
|
+
icon: "play"
|
|
73799
|
+
}), btn("pause", 0, 2, {
|
|
73800
|
+
role: "pause",
|
|
73801
|
+
icon: "pause"
|
|
73802
|
+
}), btn("forward", 0, 3, {
|
|
73803
|
+
role: "fast_forward",
|
|
73804
|
+
icon: "fast-forward"
|
|
73805
|
+
}), btn("stop", 0, 4, {
|
|
73806
|
+
role: "stop",
|
|
73807
|
+
icon: "square"
|
|
73808
|
+
}), [
|
|
73809
|
+
1,
|
|
73810
|
+
2,
|
|
73811
|
+
3,
|
|
73812
|
+
4
|
|
73813
|
+
].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
|
|
72236
73814
|
discriminatedUnion("kind", [object({
|
|
72237
73815
|
kind: literal("raw"),
|
|
72238
73816
|
timings: array(number().int()),
|
|
@@ -72279,406 +73857,8 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
|
|
|
72279
73857
|
return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
|
|
72280
73858
|
}
|
|
72281
73859
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
72282
|
-
/**
|
|
72283
|
-
* Frozen unit → spec table. `•` in the doc marks each dimension's canonical
|
|
72284
|
-
* unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
|
|
72285
|
-
* `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
|
|
72286
|
-
* decimals). K derives from `°C = K − 273.15`.
|
|
72287
|
-
*/
|
|
72288
|
-
var UNIT_TABLE = {
|
|
72289
|
-
"°C": {
|
|
72290
|
-
dimension: "temperature",
|
|
72291
|
-
factor: 1,
|
|
72292
|
-
offset: 0
|
|
72293
|
-
},
|
|
72294
|
-
"°F": {
|
|
72295
|
-
dimension: "temperature",
|
|
72296
|
-
factor: 5 / 9,
|
|
72297
|
-
offset: -160 / 9
|
|
72298
|
-
},
|
|
72299
|
-
K: {
|
|
72300
|
-
dimension: "temperature",
|
|
72301
|
-
factor: 1,
|
|
72302
|
-
offset: -273.15
|
|
72303
|
-
},
|
|
72304
|
-
hPa: {
|
|
72305
|
-
dimension: "pressure",
|
|
72306
|
-
factor: 1,
|
|
72307
|
-
offset: 0
|
|
72308
|
-
},
|
|
72309
|
-
kPa: {
|
|
72310
|
-
dimension: "pressure",
|
|
72311
|
-
factor: 10,
|
|
72312
|
-
offset: 0
|
|
72313
|
-
},
|
|
72314
|
-
Pa: {
|
|
72315
|
-
dimension: "pressure",
|
|
72316
|
-
factor: .01,
|
|
72317
|
-
offset: 0
|
|
72318
|
-
},
|
|
72319
|
-
mbar: {
|
|
72320
|
-
dimension: "pressure",
|
|
72321
|
-
factor: 1,
|
|
72322
|
-
offset: 0
|
|
72323
|
-
},
|
|
72324
|
-
bar: {
|
|
72325
|
-
dimension: "pressure",
|
|
72326
|
-
factor: 1e3,
|
|
72327
|
-
offset: 0
|
|
72328
|
-
},
|
|
72329
|
-
inHg: {
|
|
72330
|
-
dimension: "pressure",
|
|
72331
|
-
factor: 33.8639,
|
|
72332
|
-
offset: 0
|
|
72333
|
-
},
|
|
72334
|
-
mmHg: {
|
|
72335
|
-
dimension: "pressure",
|
|
72336
|
-
factor: 1.33322,
|
|
72337
|
-
offset: 0
|
|
72338
|
-
},
|
|
72339
|
-
psi: {
|
|
72340
|
-
dimension: "pressure",
|
|
72341
|
-
factor: 68.9476,
|
|
72342
|
-
offset: 0
|
|
72343
|
-
},
|
|
72344
|
-
"m/s": {
|
|
72345
|
-
dimension: "speed",
|
|
72346
|
-
factor: 1,
|
|
72347
|
-
offset: 0
|
|
72348
|
-
},
|
|
72349
|
-
"km/h": {
|
|
72350
|
-
dimension: "speed",
|
|
72351
|
-
factor: 1 / 3.6,
|
|
72352
|
-
offset: 0
|
|
72353
|
-
},
|
|
72354
|
-
mph: {
|
|
72355
|
-
dimension: "speed",
|
|
72356
|
-
factor: .44704,
|
|
72357
|
-
offset: 0
|
|
72358
|
-
},
|
|
72359
|
-
kn: {
|
|
72360
|
-
dimension: "speed",
|
|
72361
|
-
factor: .514444,
|
|
72362
|
-
offset: 0
|
|
72363
|
-
},
|
|
72364
|
-
"mm/h": {
|
|
72365
|
-
dimension: "precipitation-rate",
|
|
72366
|
-
factor: 1,
|
|
72367
|
-
offset: 0
|
|
72368
|
-
},
|
|
72369
|
-
"in/h": {
|
|
72370
|
-
dimension: "precipitation-rate",
|
|
72371
|
-
factor: 25.4,
|
|
72372
|
-
offset: 0
|
|
72373
|
-
},
|
|
72374
|
-
m: {
|
|
72375
|
-
dimension: "length",
|
|
72376
|
-
factor: 1,
|
|
72377
|
-
offset: 0
|
|
72378
|
-
},
|
|
72379
|
-
mm: {
|
|
72380
|
-
dimension: "length",
|
|
72381
|
-
factor: .001,
|
|
72382
|
-
offset: 0
|
|
72383
|
-
},
|
|
72384
|
-
cm: {
|
|
72385
|
-
dimension: "length",
|
|
72386
|
-
factor: .01,
|
|
72387
|
-
offset: 0
|
|
72388
|
-
},
|
|
72389
|
-
km: {
|
|
72390
|
-
dimension: "length",
|
|
72391
|
-
factor: 1e3,
|
|
72392
|
-
offset: 0
|
|
72393
|
-
},
|
|
72394
|
-
in: {
|
|
72395
|
-
dimension: "length",
|
|
72396
|
-
factor: .0254,
|
|
72397
|
-
offset: 0
|
|
72398
|
-
},
|
|
72399
|
-
ft: {
|
|
72400
|
-
dimension: "length",
|
|
72401
|
-
factor: .3048,
|
|
72402
|
-
offset: 0
|
|
72403
|
-
},
|
|
72404
|
-
mi: {
|
|
72405
|
-
dimension: "length",
|
|
72406
|
-
factor: 1609.344,
|
|
72407
|
-
offset: 0
|
|
72408
|
-
},
|
|
72409
|
-
"m²": {
|
|
72410
|
-
dimension: "area",
|
|
72411
|
-
factor: 1,
|
|
72412
|
-
offset: 0
|
|
72413
|
-
},
|
|
72414
|
-
lx: {
|
|
72415
|
-
dimension: "illuminance",
|
|
72416
|
-
factor: 1,
|
|
72417
|
-
offset: 0
|
|
72418
|
-
},
|
|
72419
|
-
"W/m²": {
|
|
72420
|
-
dimension: "irradiance",
|
|
72421
|
-
factor: 1,
|
|
72422
|
-
offset: 0
|
|
72423
|
-
},
|
|
72424
|
-
W: {
|
|
72425
|
-
dimension: "power",
|
|
72426
|
-
factor: 1,
|
|
72427
|
-
offset: 0
|
|
72428
|
-
},
|
|
72429
|
-
kW: {
|
|
72430
|
-
dimension: "power",
|
|
72431
|
-
factor: 1e3,
|
|
72432
|
-
offset: 0
|
|
72433
|
-
},
|
|
72434
|
-
VA: {
|
|
72435
|
-
dimension: "apparent-power",
|
|
72436
|
-
factor: 1,
|
|
72437
|
-
offset: 0
|
|
72438
|
-
},
|
|
72439
|
-
Wh: {
|
|
72440
|
-
dimension: "energy",
|
|
72441
|
-
factor: 1,
|
|
72442
|
-
offset: 0
|
|
72443
|
-
},
|
|
72444
|
-
kWh: {
|
|
72445
|
-
dimension: "energy",
|
|
72446
|
-
factor: 1e3,
|
|
72447
|
-
offset: 0
|
|
72448
|
-
},
|
|
72449
|
-
MWh: {
|
|
72450
|
-
dimension: "energy",
|
|
72451
|
-
factor: 1e6,
|
|
72452
|
-
offset: 0
|
|
72453
|
-
},
|
|
72454
|
-
V: {
|
|
72455
|
-
dimension: "voltage",
|
|
72456
|
-
factor: 1,
|
|
72457
|
-
offset: 0
|
|
72458
|
-
},
|
|
72459
|
-
mV: {
|
|
72460
|
-
dimension: "voltage",
|
|
72461
|
-
factor: .001,
|
|
72462
|
-
offset: 0
|
|
72463
|
-
},
|
|
72464
|
-
A: {
|
|
72465
|
-
dimension: "current",
|
|
72466
|
-
factor: 1,
|
|
72467
|
-
offset: 0
|
|
72468
|
-
},
|
|
72469
|
-
mA: {
|
|
72470
|
-
dimension: "current",
|
|
72471
|
-
factor: .001,
|
|
72472
|
-
offset: 0
|
|
72473
|
-
},
|
|
72474
|
-
ppm: {
|
|
72475
|
-
dimension: "concentration-volume",
|
|
72476
|
-
factor: 1,
|
|
72477
|
-
offset: 0
|
|
72478
|
-
},
|
|
72479
|
-
ppb: {
|
|
72480
|
-
dimension: "concentration-volume",
|
|
72481
|
-
factor: .001,
|
|
72482
|
-
offset: 0
|
|
72483
|
-
},
|
|
72484
|
-
"µg/m³": {
|
|
72485
|
-
dimension: "concentration-mass",
|
|
72486
|
-
factor: 1,
|
|
72487
|
-
offset: 0
|
|
72488
|
-
},
|
|
72489
|
-
"mg/m³": {
|
|
72490
|
-
dimension: "concentration-mass",
|
|
72491
|
-
factor: 1e3,
|
|
72492
|
-
offset: 0
|
|
72493
|
-
},
|
|
72494
|
-
kg: {
|
|
72495
|
-
dimension: "mass",
|
|
72496
|
-
factor: 1,
|
|
72497
|
-
offset: 0
|
|
72498
|
-
},
|
|
72499
|
-
g: {
|
|
72500
|
-
dimension: "mass",
|
|
72501
|
-
factor: .001,
|
|
72502
|
-
offset: 0
|
|
72503
|
-
},
|
|
72504
|
-
lb: {
|
|
72505
|
-
dimension: "mass",
|
|
72506
|
-
factor: .453592,
|
|
72507
|
-
offset: 0
|
|
72508
|
-
},
|
|
72509
|
-
oz: {
|
|
72510
|
-
dimension: "mass",
|
|
72511
|
-
factor: .0283495,
|
|
72512
|
-
offset: 0
|
|
72513
|
-
},
|
|
72514
|
-
"%": {
|
|
72515
|
-
dimension: "percentage",
|
|
72516
|
-
factor: 1,
|
|
72517
|
-
offset: 0
|
|
72518
|
-
},
|
|
72519
|
-
L: {
|
|
72520
|
-
dimension: "volume",
|
|
72521
|
-
factor: 1,
|
|
72522
|
-
offset: 0
|
|
72523
|
-
},
|
|
72524
|
-
mL: {
|
|
72525
|
-
dimension: "volume",
|
|
72526
|
-
factor: .001,
|
|
72527
|
-
offset: 0
|
|
72528
|
-
},
|
|
72529
|
-
gal: {
|
|
72530
|
-
dimension: "volume",
|
|
72531
|
-
factor: 3.785411784,
|
|
72532
|
-
offset: 0
|
|
72533
|
-
},
|
|
72534
|
-
s: {
|
|
72535
|
-
dimension: "duration",
|
|
72536
|
-
factor: 1,
|
|
72537
|
-
offset: 0
|
|
72538
|
-
},
|
|
72539
|
-
ms: {
|
|
72540
|
-
dimension: "duration",
|
|
72541
|
-
factor: .001,
|
|
72542
|
-
offset: 0
|
|
72543
|
-
},
|
|
72544
|
-
min: {
|
|
72545
|
-
dimension: "duration",
|
|
72546
|
-
factor: 60,
|
|
72547
|
-
offset: 0
|
|
72548
|
-
},
|
|
72549
|
-
h: {
|
|
72550
|
-
dimension: "duration",
|
|
72551
|
-
factor: 3600,
|
|
72552
|
-
offset: 0
|
|
72553
|
-
},
|
|
72554
|
-
d: {
|
|
72555
|
-
dimension: "duration",
|
|
72556
|
-
factor: 86400,
|
|
72557
|
-
offset: 0
|
|
72558
|
-
},
|
|
72559
|
-
B: {
|
|
72560
|
-
dimension: "information",
|
|
72561
|
-
factor: 1,
|
|
72562
|
-
offset: 0
|
|
72563
|
-
},
|
|
72564
|
-
KiB: {
|
|
72565
|
-
dimension: "information",
|
|
72566
|
-
factor: 1024,
|
|
72567
|
-
offset: 0
|
|
72568
|
-
},
|
|
72569
|
-
MiB: {
|
|
72570
|
-
dimension: "information",
|
|
72571
|
-
factor: 1024 ** 2,
|
|
72572
|
-
offset: 0
|
|
72573
|
-
},
|
|
72574
|
-
GiB: {
|
|
72575
|
-
dimension: "information",
|
|
72576
|
-
factor: 1024 ** 3,
|
|
72577
|
-
offset: 0
|
|
72578
|
-
},
|
|
72579
|
-
TiB: {
|
|
72580
|
-
dimension: "information",
|
|
72581
|
-
factor: 1024 ** 4,
|
|
72582
|
-
offset: 0
|
|
72583
|
-
},
|
|
72584
|
-
kB: {
|
|
72585
|
-
dimension: "information",
|
|
72586
|
-
factor: 1e3,
|
|
72587
|
-
offset: 0
|
|
72588
|
-
},
|
|
72589
|
-
MB: {
|
|
72590
|
-
dimension: "information",
|
|
72591
|
-
factor: 1e6,
|
|
72592
|
-
offset: 0
|
|
72593
|
-
},
|
|
72594
|
-
GB: {
|
|
72595
|
-
dimension: "information",
|
|
72596
|
-
factor: 1e9,
|
|
72597
|
-
offset: 0
|
|
72598
|
-
},
|
|
72599
|
-
"°": {
|
|
72600
|
-
dimension: "angle",
|
|
72601
|
-
factor: 1,
|
|
72602
|
-
offset: 0
|
|
72603
|
-
},
|
|
72604
|
-
dBFS: {
|
|
72605
|
-
dimension: "sound-level",
|
|
72606
|
-
factor: 1,
|
|
72607
|
-
offset: 0
|
|
72608
|
-
},
|
|
72609
|
-
dBm: {
|
|
72610
|
-
dimension: "signal-level",
|
|
72611
|
-
factor: 1,
|
|
72612
|
-
offset: 0
|
|
72613
|
-
},
|
|
72614
|
-
mired: {
|
|
72615
|
-
dimension: "colour-temperature",
|
|
72616
|
-
factor: 1,
|
|
72617
|
-
offset: 0
|
|
72618
|
-
},
|
|
72619
|
-
kbps: {
|
|
72620
|
-
dimension: "bitrate",
|
|
72621
|
-
factor: 1,
|
|
72622
|
-
offset: 0
|
|
72623
|
-
},
|
|
72624
|
-
Mbps: {
|
|
72625
|
-
dimension: "bitrate",
|
|
72626
|
-
factor: 1e3,
|
|
72627
|
-
offset: 0
|
|
72628
|
-
},
|
|
72629
|
-
fps: {
|
|
72630
|
-
dimension: "rate",
|
|
72631
|
-
factor: 1,
|
|
72632
|
-
offset: 0
|
|
72633
|
-
}
|
|
72634
|
-
};
|
|
72635
|
-
/** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
|
|
72636
|
-
function specFor(unit) {
|
|
72637
|
-
return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
|
|
72638
|
-
}
|
|
72639
|
-
/** The dimension a unit belongs to, or `undefined` for an unknown unit. */
|
|
72640
|
-
function unitDimension(unit) {
|
|
72641
|
-
return specFor(unit)?.dimension;
|
|
72642
|
-
}
|
|
72643
|
-
/** Value in `spec`'s unit → its dimension's canonical unit. */
|
|
72644
|
-
function toCanonical(value, spec) {
|
|
72645
|
-
return value * spec.factor + spec.offset;
|
|
72646
|
-
}
|
|
72647
|
-
/** Canonical-unit value → the unit described by `spec`. */
|
|
72648
|
-
function fromCanonical(canonical, spec) {
|
|
72649
|
-
return (canonical - spec.offset) / spec.factor;
|
|
72650
|
-
}
|
|
72651
|
-
/**
|
|
72652
|
-
* Graceful variant for the render path: returns the converted value, or
|
|
72653
|
-
* `undefined` when the conversion is refused (unknown unit / cross-dimension).
|
|
72654
|
-
* Never throws — a render caller falls back to the raw value + source unit.
|
|
72655
|
-
*/
|
|
72656
|
-
function tryConvertUnit(value, from, to) {
|
|
72657
|
-
if (from === to) return value;
|
|
72658
|
-
const fromSpec = specFor(from);
|
|
72659
|
-
if (!fromSpec) return void 0;
|
|
72660
|
-
const toSpec = specFor(to);
|
|
72661
|
-
if (!toSpec) return void 0;
|
|
72662
|
-
if (fromSpec.dimension !== toSpec.dimension) return void 0;
|
|
72663
|
-
return fromCanonical(toCanonical(value, fromSpec), toSpec);
|
|
72664
|
-
}
|
|
72665
|
-
/**
|
|
72666
|
-
* Every known unit spelling in a dimension (table insertion order — canonical
|
|
72667
|
-
* unit first). Powers the UI's same-dimension unit picker.
|
|
72668
|
-
*/
|
|
72669
|
-
/**
|
|
72670
|
-
* Convert a difference, not a reading. Affine units (°F) shift an absolute
|
|
72671
|
-
* value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
|
|
72672
|
-
*/
|
|
72673
|
-
function tryConvertDelta(value, from, to) {
|
|
72674
|
-
if (from === to) return value;
|
|
72675
|
-
const origin = tryConvertUnit(0, from, to);
|
|
72676
|
-
const shifted = tryConvertUnit(value, from, to);
|
|
72677
|
-
if (origin === void 0 || shifted === void 0) return void 0;
|
|
72678
|
-
return shifted - origin;
|
|
72679
|
-
}
|
|
72680
73860
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
72681
|
-
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
73861
|
+
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
72682
73862
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
72683
73863
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
72684
73864
|
new Set(CAP_NAMES);
|
|
@@ -73800,6 +74980,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
73800
74980
|
async discoverDevices(_input) {
|
|
73801
74981
|
return [];
|
|
73802
74982
|
}
|
|
74983
|
+
/**
|
|
74984
|
+
* How the scan engine finds this provider. Default `[]`: the provider is
|
|
74985
|
+
* not migrated and keeps answering `discoverDevices` itself.
|
|
74986
|
+
*/
|
|
74987
|
+
async getDiscoveryModes() {
|
|
74988
|
+
return [];
|
|
74989
|
+
}
|
|
74990
|
+
/** Turn the scan engine's raw hits into candidates. Default: none. */
|
|
74991
|
+
async classifyDiscoveryHits(_input) {
|
|
74992
|
+
return [];
|
|
74993
|
+
}
|
|
73803
74994
|
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
73804
74995
|
* Null = no extra params. Override in providers that support scoped scans. */
|
|
73805
74996
|
async getDiscoveryParamsSchema() {
|
|
@@ -74280,7 +75471,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
74280
75471
|
return toDeviceSummary(device, this.addonId);
|
|
74281
75472
|
}
|
|
74282
75473
|
};
|
|
74283
|
-
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
|
|
75474
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
|
|
74284
75475
|
new Set(Object.values(DeviceType));
|
|
74285
75476
|
Object.freeze({
|
|
74286
75477
|
"accessories.getStatus": {
|
|
@@ -74668,6 +75859,13 @@ Object.freeze({
|
|
|
74668
75859
|
access: "create",
|
|
74669
75860
|
role: "admin"
|
|
74670
75861
|
},
|
|
75862
|
+
"addonSettings.getDeclaredSettingsKeys": {
|
|
75863
|
+
capName: "addon-settings",
|
|
75864
|
+
capScope: "system",
|
|
75865
|
+
addonId: null,
|
|
75866
|
+
access: "view",
|
|
75867
|
+
role: "admin"
|
|
75868
|
+
},
|
|
74671
75869
|
"addonSettings.getDeviceSettings": {
|
|
74672
75870
|
capName: "addon-settings",
|
|
74673
75871
|
capScope: "system",
|
|
@@ -74689,6 +75887,13 @@ Object.freeze({
|
|
|
74689
75887
|
access: "view",
|
|
74690
75888
|
role: "admin"
|
|
74691
75889
|
},
|
|
75890
|
+
"addonSettings.unsetStoredSettingsKeys": {
|
|
75891
|
+
capName: "addon-settings",
|
|
75892
|
+
capScope: "system",
|
|
75893
|
+
addonId: null,
|
|
75894
|
+
access: "create",
|
|
75895
|
+
role: "admin"
|
|
75896
|
+
},
|
|
74692
75897
|
"addonSettings.updateDeviceSettings": {
|
|
74693
75898
|
capName: "addon-settings",
|
|
74694
75899
|
capScope: "system",
|
|
@@ -77104,6 +78309,13 @@ Object.freeze({
|
|
|
77104
78309
|
access: "create",
|
|
77105
78310
|
role: "admin"
|
|
77106
78311
|
},
|
|
78312
|
+
"deviceProvider.classifyDiscoveryHits": {
|
|
78313
|
+
capName: "device-provider",
|
|
78314
|
+
capScope: "system",
|
|
78315
|
+
addonId: null,
|
|
78316
|
+
access: "create",
|
|
78317
|
+
role: "admin"
|
|
78318
|
+
},
|
|
77107
78319
|
"deviceProvider.createDevice": {
|
|
77108
78320
|
capName: "device-provider",
|
|
77109
78321
|
capScope: "system",
|
|
@@ -77132,6 +78344,13 @@ Object.freeze({
|
|
|
77132
78344
|
access: "view",
|
|
77133
78345
|
role: "admin"
|
|
77134
78346
|
},
|
|
78347
|
+
"deviceProvider.getDiscoveryModes": {
|
|
78348
|
+
capName: "device-provider",
|
|
78349
|
+
capScope: "system",
|
|
78350
|
+
addonId: null,
|
|
78351
|
+
access: "view",
|
|
78352
|
+
role: "admin"
|
|
78353
|
+
},
|
|
77135
78354
|
"deviceProvider.getDiscoveryParamsSchema": {
|
|
77136
78355
|
capName: "device-provider",
|
|
77137
78356
|
capScope: "system",
|
|
@@ -77909,6 +79128,13 @@ Object.freeze({
|
|
|
77909
79128
|
access: "create",
|
|
77910
79129
|
role: "admin"
|
|
77911
79130
|
},
|
|
79131
|
+
"keychain.releaseRecordRefs": {
|
|
79132
|
+
capName: "keychain",
|
|
79133
|
+
capScope: "system",
|
|
79134
|
+
addonId: null,
|
|
79135
|
+
access: "create",
|
|
79136
|
+
role: "admin"
|
|
79137
|
+
},
|
|
77912
79138
|
"keychain.resolve": {
|
|
77913
79139
|
capName: "keychain",
|
|
77914
79140
|
capScope: "system",
|
|
@@ -78329,6 +79555,13 @@ Object.freeze({
|
|
|
78329
79555
|
access: "view",
|
|
78330
79556
|
role: "admin"
|
|
78331
79557
|
},
|
|
79558
|
+
"localNetwork.getScanSubnets": {
|
|
79559
|
+
capName: "local-network",
|
|
79560
|
+
capScope: "system",
|
|
79561
|
+
addonId: null,
|
|
79562
|
+
access: "view",
|
|
79563
|
+
role: "admin"
|
|
79564
|
+
},
|
|
78332
79565
|
"localNetwork.getTlsStatus": {
|
|
78333
79566
|
capName: "local-network",
|
|
78334
79567
|
capScope: "system",
|
|
@@ -78378,6 +79611,13 @@ Object.freeze({
|
|
|
78378
79611
|
access: "create",
|
|
78379
79612
|
role: "admin"
|
|
78380
79613
|
},
|
|
79614
|
+
"localNetwork.scanNetwork": {
|
|
79615
|
+
capName: "local-network",
|
|
79616
|
+
capScope: "system",
|
|
79617
|
+
addonId: null,
|
|
79618
|
+
access: "create",
|
|
79619
|
+
role: "admin"
|
|
79620
|
+
},
|
|
78381
79621
|
"localNetwork.setAddressRole": {
|
|
78382
79622
|
capName: "local-network",
|
|
78383
79623
|
capScope: "system",
|
|
@@ -78399,6 +79639,13 @@ Object.freeze({
|
|
|
78399
79639
|
access: "create",
|
|
78400
79640
|
role: "admin"
|
|
78401
79641
|
},
|
|
79642
|
+
"localNetwork.setScanSubnets": {
|
|
79643
|
+
capName: "local-network",
|
|
79644
|
+
capScope: "system",
|
|
79645
|
+
addonId: null,
|
|
79646
|
+
access: "create",
|
|
79647
|
+
role: "admin"
|
|
79648
|
+
},
|
|
78402
79649
|
"localNetwork.setViewerEndpoints": {
|
|
78403
79650
|
capName: "local-network",
|
|
78404
79651
|
capScope: "system",
|
|
@@ -82081,6 +83328,13 @@ Object.freeze({
|
|
|
82081
83328
|
access: "create",
|
|
82082
83329
|
role: "admin"
|
|
82083
83330
|
},
|
|
83331
|
+
"remoteLayoutComponents.listComponents": {
|
|
83332
|
+
capName: "remote-layout-components",
|
|
83333
|
+
capScope: "system",
|
|
83334
|
+
addonId: null,
|
|
83335
|
+
access: "view",
|
|
83336
|
+
role: "admin"
|
|
83337
|
+
},
|
|
82084
83338
|
"runningState.getStatus": {
|
|
82085
83339
|
capName: "running-state",
|
|
82086
83340
|
capScope: "device",
|
|
@@ -82207,6 +83461,13 @@ Object.freeze({
|
|
|
82207
83461
|
access: "view",
|
|
82208
83462
|
role: "admin"
|
|
82209
83463
|
},
|
|
83464
|
+
"serverManagement.installDesktopUpdate": {
|
|
83465
|
+
capName: "server-management",
|
|
83466
|
+
capScope: "system",
|
|
83467
|
+
addonId: null,
|
|
83468
|
+
access: "create",
|
|
83469
|
+
role: "admin"
|
|
83470
|
+
},
|
|
82210
83471
|
"serverManagement.restartServer": {
|
|
82211
83472
|
capName: "server-management",
|
|
82212
83473
|
capScope: "system",
|
|
@@ -82606,6 +83867,13 @@ Object.freeze({
|
|
|
82606
83867
|
access: "create",
|
|
82607
83868
|
role: "admin"
|
|
82608
83869
|
},
|
|
83870
|
+
"storageAdmin.dropOrphanedKind": {
|
|
83871
|
+
capName: "storage-admin",
|
|
83872
|
+
capScope: "system",
|
|
83873
|
+
addonId: null,
|
|
83874
|
+
access: "delete",
|
|
83875
|
+
role: "admin"
|
|
83876
|
+
},
|
|
82609
83877
|
"storageAdmin.listKinds": {
|
|
82610
83878
|
capName: "storage-admin",
|
|
82611
83879
|
capScope: "system",
|
|
@@ -82627,6 +83895,27 @@ Object.freeze({
|
|
|
82627
83895
|
access: "view",
|
|
82628
83896
|
role: "admin"
|
|
82629
83897
|
},
|
|
83898
|
+
"storageAdmin.scanStaleData": {
|
|
83899
|
+
capName: "storage-admin",
|
|
83900
|
+
capScope: "system",
|
|
83901
|
+
addonId: null,
|
|
83902
|
+
access: "create",
|
|
83903
|
+
role: "admin"
|
|
83904
|
+
},
|
|
83905
|
+
"storageAdmin.staleDataReport": {
|
|
83906
|
+
capName: "storage-admin",
|
|
83907
|
+
capScope: "system",
|
|
83908
|
+
addonId: null,
|
|
83909
|
+
access: "view",
|
|
83910
|
+
role: "admin"
|
|
83911
|
+
},
|
|
83912
|
+
"storageAdmin.unsetStaleSettingsKey": {
|
|
83913
|
+
capName: "storage-admin",
|
|
83914
|
+
capScope: "system",
|
|
83915
|
+
addonId: null,
|
|
83916
|
+
access: "create",
|
|
83917
|
+
role: "admin"
|
|
83918
|
+
},
|
|
82630
83919
|
"storageEvictable.evict": {
|
|
82631
83920
|
capName: "storage-evictable",
|
|
82632
83921
|
capScope: "system",
|
|
@@ -86117,7 +87406,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
86117
87406
|
min: 4,
|
|
86118
87407
|
max: 200,
|
|
86119
87408
|
step: 1,
|
|
86120
|
-
unit: "g"
|
|
87409
|
+
unit: "g",
|
|
87410
|
+
quantity: "mass"
|
|
86121
87411
|
},
|
|
86122
87412
|
optional: true
|
|
86123
87413
|
}]
|
|
@@ -86136,7 +87426,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
86136
87426
|
min: 4,
|
|
86137
87427
|
max: 200,
|
|
86138
87428
|
step: 1,
|
|
86139
|
-
unit: "g"
|
|
87429
|
+
unit: "g",
|
|
87430
|
+
quantity: "mass"
|
|
86140
87431
|
},
|
|
86141
87432
|
optional: true
|
|
86142
87433
|
}, {
|
|
@@ -86147,7 +87438,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
86147
87438
|
min: 4,
|
|
86148
87439
|
max: 200,
|
|
86149
87440
|
step: 1,
|
|
86150
|
-
unit: "g"
|
|
87441
|
+
unit: "g",
|
|
87442
|
+
quantity: "mass"
|
|
86151
87443
|
},
|
|
86152
87444
|
optional: true
|
|
86153
87445
|
}]
|
|
@@ -88247,6 +89539,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
88247
89539
|
"recording-export": system("recording exports — requested from the timeline, not an automation step"),
|
|
88248
89540
|
records: system(INTERNAL),
|
|
88249
89541
|
remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
|
|
89542
|
+
"remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
|
|
88250
89543
|
"server-management": system(CLUSTER),
|
|
88251
89544
|
"settings-store": system(INTERNAL),
|
|
88252
89545
|
"smtp-provider": system(INTERNAL),
|
|
@@ -88319,7 +89612,7 @@ function projectAction(cap, action) {
|
|
|
88319
89612
|
};
|
|
88320
89613
|
}
|
|
88321
89614
|
Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
|
|
88322
|
-
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
|
|
89615
|
+
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Gateway;
|
|
88323
89616
|
DeviceFeature.BatteryOperated;
|
|
88324
89617
|
/**
|
|
88325
89618
|
* How a washer, dryer or dishwasher leaves CamStack.
|
|
@@ -92918,6 +94211,67 @@ object({
|
|
|
92918
94211
|
commands: record(string(), unknown())
|
|
92919
94212
|
});
|
|
92920
94213
|
//#endregion
|
|
94214
|
+
//#region src/ha-broker-source.ts
|
|
94215
|
+
/**
|
|
94216
|
+
* The export half's ONLY read of the `broker` registry — and the record of why
|
|
94217
|
+
* it is NOT pinned, which is the opposite of what this file said for one deploy.
|
|
94218
|
+
*
|
|
94219
|
+
* ── What a pin would buy, and why it cannot be paid for here ───────────────
|
|
94220
|
+
* `broker` is a `mode:'collection'` capability: several addons register a
|
|
94221
|
+
* provider, each owning a DISJOINT set of brokers (`provider-homematic` owns
|
|
94222
|
+
* `hm_*`, `provider-homeassistant` owns `ha_*`). For an ID-KEYED method that is
|
|
94223
|
+
* decisive, and the export half's `getBrokerConfig` call IS pinned for exactly
|
|
94224
|
+
* that reason — measured on the live hub:
|
|
94225
|
+
*
|
|
94226
|
+
* broker.getBrokerConfig { id: 'ha_001' } → null
|
|
94227
|
+
* broker.getBrokerConfig { id: 'ha_001', addonId: 'provider-…' } → the config
|
|
94228
|
+
*
|
|
94229
|
+
* `list` is different, and the difference is not cosmetic. Pinning it makes the
|
|
94230
|
+
* read depend on `provider-homeassistant` being REGISTERED at the moment of the
|
|
94231
|
+
* call — and that provider registers its capabilities only at the very END of
|
|
94232
|
+
* its `initialize()`, after restoring every HA device. Pinned, the export half
|
|
94233
|
+
* hard-fails through that whole window with
|
|
94234
|
+
*
|
|
94235
|
+
* BAD_REQUEST: Capability "broker" has no provider with addonId
|
|
94236
|
+
* "provider-homeassistant"
|
|
94237
|
+
*
|
|
94238
|
+
* which is strictly worse than the union: unpinned, `list` is fanned across
|
|
94239
|
+
* every registered provider, so it returns `ha_001` as soon as the provider is
|
|
94240
|
+
* up and simply omits it while it is not. Shipped pinned on 2026-08-09 and it
|
|
94241
|
+
* took the operator from "one instance, switched off" to "no instance at all".
|
|
94242
|
+
*
|
|
94243
|
+
* ── The rule this leaves behind ────────────────────────────────────────────
|
|
94244
|
+
* Pin a call that NAMES a resource (`{ id }`, `{ brokerId }`) — only one
|
|
94245
|
+
* provider can answer it and a wrong answer is `null`, indistinguishable from
|
|
94246
|
+
* "no such resource". Do NOT pin an enumeration: its contract is already the
|
|
94247
|
+
* union, and a pin converts a provider's absence from "returns fewer rows" into
|
|
94248
|
+
* "throws". `scripts/check-collection-cap-pin.ts` enforces exactly that split.
|
|
94249
|
+
*
|
|
94250
|
+
* ── The other half of the lesson: never pin a cap you provide yourself ─────
|
|
94251
|
+
* The provider half's own `broker.*` calls (subscribe / getState / publish)
|
|
94252
|
+
* must stay unpinned too, and for a sharper reason: they run DURING
|
|
94253
|
+
* `registry.restore()`, i.e. before `initialize()` returns the provider that
|
|
94254
|
+
* would satisfy the pin. Pinning them deadlocks the addon's own boot — the cap
|
|
94255
|
+
* cannot register until restore finishes, and restore cannot finish until the
|
|
94256
|
+
* cap resolves. In-process they need no pin anyway: `localProviderLink`
|
|
94257
|
+
* short-circuits any cap the calling process already provides.
|
|
94258
|
+
*/
|
|
94259
|
+
/** The addon that owns the import direction, and every `ha_*` broker. */
|
|
94260
|
+
var HA_PROVIDER_ADDON_ID = "provider-homeassistant";
|
|
94261
|
+
/** The `BrokerInfo.kind` tag the Home Assistant provider stamps. */
|
|
94262
|
+
var HA_BROKER_KIND = "home-assistant";
|
|
94263
|
+
/**
|
|
94264
|
+
* Every Home Assistant broker currently registered, cluster-wide.
|
|
94265
|
+
*
|
|
94266
|
+
* Unpinned by design (see the module docblock) and filtered by `kind`, which is
|
|
94267
|
+
* the contract check that matters: whatever mix of providers answered the
|
|
94268
|
+
* union, only `home-assistant` brokers are ever exported to over Home
|
|
94269
|
+
* Assistant's transport.
|
|
94270
|
+
*/
|
|
94271
|
+
async function listHomeAssistantBrokers(query) {
|
|
94272
|
+
return (await query({})).filter((broker) => broker.kind === HA_BROKER_KIND);
|
|
94273
|
+
}
|
|
94274
|
+
//#endregion
|
|
92921
94275
|
Object.defineProperty(exports, "AlarmArmModeSchema", {
|
|
92922
94276
|
enumerable: true,
|
|
92923
94277
|
get: function() {
|
|
@@ -93014,6 +94368,18 @@ Object.defineProperty(exports, "FanDirectionSchema", {
|
|
|
93014
94368
|
return FanDirectionSchema;
|
|
93015
94369
|
}
|
|
93016
94370
|
});
|
|
94371
|
+
Object.defineProperty(exports, "HA_BROKER_KIND", {
|
|
94372
|
+
enumerable: true,
|
|
94373
|
+
get: function() {
|
|
94374
|
+
return HA_BROKER_KIND;
|
|
94375
|
+
}
|
|
94376
|
+
});
|
|
94377
|
+
Object.defineProperty(exports, "HA_PROVIDER_ADDON_ID", {
|
|
94378
|
+
enumerable: true,
|
|
94379
|
+
get: function() {
|
|
94380
|
+
return HA_PROVIDER_ADDON_ID;
|
|
94381
|
+
}
|
|
94382
|
+
});
|
|
93017
94383
|
Object.defineProperty(exports, "HvacModeSchema", {
|
|
93018
94384
|
enumerable: true,
|
|
93019
94385
|
get: function() {
|
|
@@ -93338,6 +94704,12 @@ Object.defineProperty(exports, "lawnMowerControlCapability", {
|
|
|
93338
94704
|
return lawnMowerControlCapability;
|
|
93339
94705
|
}
|
|
93340
94706
|
});
|
|
94707
|
+
Object.defineProperty(exports, "listHomeAssistantBrokers", {
|
|
94708
|
+
enumerable: true,
|
|
94709
|
+
get: function() {
|
|
94710
|
+
return listHomeAssistantBrokers;
|
|
94711
|
+
}
|
|
94712
|
+
});
|
|
93341
94713
|
Object.defineProperty(exports, "literal", {
|
|
93342
94714
|
enumerable: true,
|
|
93343
94715
|
get: function() {
|