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