@camstack/addon-provider-enpal 0.1.15 → 0.1.17
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 +16 -0
- package/README.md +19 -18
- package/dist/addon.js +2466 -662
- package/dist/addon.mjs +2466 -662
- package/i18n/en.json +3 -2
- package/i18n/it.json +3 -2
- package/package.json +9 -5
- package/python/enpal_blazor/LICENSE +21 -0
- package/python/enpal_blazor/__init__.py +0 -0
- package/python/enpal_blazor/protocol.py +186 -0
- package/python/enpal_blazor/render_batch.py +367 -0
- package/python/enpal_worker.py +260 -0
- package/python/fixtures/render-batch-fw852.bin.gz +0 -0
- package/python/requirements.txt +3 -0
- package/python/test_enpal_worker.py +56 -0
package/dist/addon.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let node_child_process = require("node:child_process");
|
|
3
|
+
let node_fs = require("node:fs");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
let node_url = require("node:url");
|
|
2
6
|
//#region ../types/dist/event-category-78tcBemo.mjs
|
|
3
7
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
8
|
EventCategory["SystemBoot"] = "system.boot";
|
|
@@ -802,7 +806,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
802
806
|
return EventCategory;
|
|
803
807
|
}({});
|
|
804
808
|
//#endregion
|
|
805
|
-
//#region ../types/dist/composed-stable-id-
|
|
809
|
+
//#region ../types/dist/composed-stable-id-CPp3votu.mjs
|
|
806
810
|
/**
|
|
807
811
|
import { errMsg } from '@camstack/types'
|
|
808
812
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -815,17 +819,18 @@ function errMsg(err) {
|
|
|
815
819
|
}
|
|
816
820
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
817
821
|
DeviceType["Camera"] = "camera";
|
|
822
|
+
/** Legacy stored value, read only by the hub-to-integration migrations; no live device carries it. */
|
|
818
823
|
DeviceType["Hub"] = "hub";
|
|
819
824
|
DeviceType["Light"] = "light";
|
|
820
825
|
DeviceType["Siren"] = "siren";
|
|
821
826
|
DeviceType["Switch"] = "switch";
|
|
822
827
|
DeviceType["Sensor"] = "sensor";
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
* `climate-control`
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
828
|
+
/** Every climate device: an air conditioner, a room thermostat, a radiator
|
|
829
|
+
* valve, a floor-heating zone, a heat-pump circuit. They share one set of
|
|
830
|
+
* controls (`climate-control`), so they are one type; WHICH kind of climate
|
|
831
|
+
* device it is is its {@link DeviceRole} (`climate-*`), which the UI and the
|
|
832
|
+
* exports branch on. The retired `thermostat` type is read as this one
|
|
833
|
+
* (`canonicalStoredDeviceType`). */
|
|
829
834
|
DeviceType["Climate"] = "climate";
|
|
830
835
|
DeviceType["Button"] = "button";
|
|
831
836
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
@@ -916,10 +921,6 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
916
921
|
/** Washing machine. A plug-built one is running while its power stays
|
|
917
922
|
* above a threshold. No program cap — that surface is the dishwasher's. */
|
|
918
923
|
DeviceType["Washer"] = "washer";
|
|
919
|
-
/** A dumb plug-in stove or heater: a switch (and, with a meter, a power
|
|
920
|
-
* reading) that only runs while the plug is on. Built from a smart plug;
|
|
921
|
-
* the `timed-run` cap switches it off again after a chosen time. */
|
|
922
|
-
DeviceType["Heater"] = "heater";
|
|
923
924
|
/** Tumble dryer. Same plug-built running surface as {@link Washer}. */
|
|
924
925
|
DeviceType["Dryer"] = "dryer";
|
|
925
926
|
/** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
|
|
@@ -1139,6 +1140,27 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
1139
1140
|
/** A smart plug: a `switch` whose source says it is an outlet (HA
|
|
1140
1141
|
* `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
|
|
1141
1142
|
DeviceRole["Outlet"] = "outlet";
|
|
1143
|
+
/** A dumb plug-in stove or heater behind a switch: it runs only while the
|
|
1144
|
+
* switch is on. Offered `timed-run`, which switches it off again after a
|
|
1145
|
+
* chosen time. */
|
|
1146
|
+
DeviceRole["SwitchHeater"] = "switch-heater";
|
|
1147
|
+
/** A plain fan behind a switch (a smart plug powering it): drawn as a fan,
|
|
1148
|
+
* with the switch's on/off only. Speed or oscillation appear only when the
|
|
1149
|
+
* device also has a fan cap (D1033). */
|
|
1150
|
+
DeviceRole["SwitchFan"] = "switch-fan";
|
|
1151
|
+
/** Air conditioner / split unit: cooling, fan speed, louvres. */
|
|
1152
|
+
DeviceRole["ClimateAc"] = "climate-ac";
|
|
1153
|
+
/** Room thermostat: a setpoint and a room temperature. The default. */
|
|
1154
|
+
DeviceRole["ClimateThermostat"] = "climate-thermostat";
|
|
1155
|
+
/** Floor-heating zone actuator. */
|
|
1156
|
+
DeviceRole["ClimateFloor"] = "climate-floor";
|
|
1157
|
+
/** Radiator valve (thermostatic radiator head). */
|
|
1158
|
+
DeviceRole["ClimateRadiator"] = "climate-radiator";
|
|
1159
|
+
/** A heater with its own thermostat (stove, convector). */
|
|
1160
|
+
DeviceRole["ClimateHeater"] = "climate-heater";
|
|
1161
|
+
/** One heating/cooling circuit of a heat pump: a water loop with comfort /
|
|
1162
|
+
* eco setpoints and a heat/cool mode, no room sensor. */
|
|
1163
|
+
DeviceRole["ClimateHeatPumpCircuit"] = "climate-heat-pump-circuit";
|
|
1142
1164
|
/** Generic motion / occupancy / moving event source. Distinct from
|
|
1143
1165
|
* the camera accessory PirSensor role: that one is a camera child;
|
|
1144
1166
|
* this is a standalone HA / 3rd-party motion sensor. */
|
|
@@ -6026,7 +6048,7 @@ var ZodIssueCode = {
|
|
|
6026
6048
|
var ZodFirstPartyTypeKind;
|
|
6027
6049
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6028
6050
|
//#endregion
|
|
6029
|
-
//#region ../types/dist/sleep-
|
|
6051
|
+
//#region ../types/dist/sleep-D0roI_CZ.mjs
|
|
6030
6052
|
/**
|
|
6031
6053
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6032
6054
|
* window to float samples (D455).
|
|
@@ -11803,6 +11825,50 @@ var ACTUATOR_CAP_VOCABULARY = {
|
|
|
11803
11825
|
value: false
|
|
11804
11826
|
},
|
|
11805
11827
|
args: { oscillating: false }
|
|
11828
|
+
},
|
|
11829
|
+
{
|
|
11830
|
+
id: "fan.horizontalOscillateOn",
|
|
11831
|
+
method: "setOscillationAxis",
|
|
11832
|
+
label: t("Horizontal oscillation on", "Oscillazione orizzontale attiva"),
|
|
11833
|
+
phrase: t("Start the horizontal oscillation of {device}", "Attiva l’oscillazione orizzontale di {device}"),
|
|
11834
|
+
icon: "move-horizontal",
|
|
11835
|
+
args: {
|
|
11836
|
+
axis: "horizontal",
|
|
11837
|
+
oscillating: true
|
|
11838
|
+
}
|
|
11839
|
+
},
|
|
11840
|
+
{
|
|
11841
|
+
id: "fan.horizontalOscillateOff",
|
|
11842
|
+
method: "setOscillationAxis",
|
|
11843
|
+
label: t("Horizontal oscillation off", "Oscillazione orizzontale spenta"),
|
|
11844
|
+
phrase: t("Stop the horizontal oscillation of {device}", "Ferma l’oscillazione orizzontale di {device}"),
|
|
11845
|
+
icon: "move-horizontal",
|
|
11846
|
+
args: {
|
|
11847
|
+
axis: "horizontal",
|
|
11848
|
+
oscillating: false
|
|
11849
|
+
}
|
|
11850
|
+
},
|
|
11851
|
+
{
|
|
11852
|
+
id: "fan.verticalOscillateOn",
|
|
11853
|
+
method: "setOscillationAxis",
|
|
11854
|
+
label: t("Vertical oscillation on", "Oscillazione verticale attiva"),
|
|
11855
|
+
phrase: t("Start the vertical oscillation of {device}", "Attiva l’oscillazione verticale di {device}"),
|
|
11856
|
+
icon: "move-vertical",
|
|
11857
|
+
args: {
|
|
11858
|
+
axis: "vertical",
|
|
11859
|
+
oscillating: true
|
|
11860
|
+
}
|
|
11861
|
+
},
|
|
11862
|
+
{
|
|
11863
|
+
id: "fan.verticalOscillateOff",
|
|
11864
|
+
method: "setOscillationAxis",
|
|
11865
|
+
label: t("Vertical oscillation off", "Oscillazione verticale spenta"),
|
|
11866
|
+
phrase: t("Stop the vertical oscillation of {device}", "Ferma l’oscillazione verticale di {device}"),
|
|
11867
|
+
icon: "move-vertical",
|
|
11868
|
+
args: {
|
|
11869
|
+
axis: "vertical",
|
|
11870
|
+
oscillating: false
|
|
11871
|
+
}
|
|
11806
11872
|
}
|
|
11807
11873
|
],
|
|
11808
11874
|
state: [{
|
|
@@ -13892,10 +13958,6 @@ var CAMERA_CAP_VOCABULARY = {
|
|
|
13892
13958
|
scope: "device",
|
|
13893
13959
|
noun: t("Admin link", "Link di amministrazione")
|
|
13894
13960
|
},
|
|
13895
|
-
"device-discovery": {
|
|
13896
|
-
scope: "device",
|
|
13897
|
-
noun: t("Discovery", "Rilevamento dispositivi")
|
|
13898
|
-
},
|
|
13899
13961
|
"device-extension": {
|
|
13900
13962
|
scope: "device",
|
|
13901
13963
|
noun: t("Device extension", "Estensione del dispositivo")
|
|
@@ -15067,6 +15129,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
15067
15129
|
backup: system("system backups — run and restored from the Backups page"),
|
|
15068
15130
|
broker: system("message-broker connections — configured, not actuated"),
|
|
15069
15131
|
"calendar-source": system(INTERNAL),
|
|
15132
|
+
"cluster-update": system(CLUSTER),
|
|
15070
15133
|
calendars: system("calendars feed schedules and conditions, not device actions"),
|
|
15071
15134
|
composer: system("device composition — an authoring tool, not an action"),
|
|
15072
15135
|
"core-blocks": system(INTERNAL),
|
|
@@ -15334,6 +15397,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
15334
15397
|
"camera-streams",
|
|
15335
15398
|
"carbon-monoxide",
|
|
15336
15399
|
"climate-control",
|
|
15400
|
+
"cluster-update",
|
|
15337
15401
|
"color",
|
|
15338
15402
|
"composer",
|
|
15339
15403
|
"connectivity",
|
|
@@ -15351,7 +15415,6 @@ var CAP_NAMES = Object.freeze([
|
|
|
15351
15415
|
"device-admin-link",
|
|
15352
15416
|
"device-adoption",
|
|
15353
15417
|
"device-credentials",
|
|
15354
|
-
"device-discovery",
|
|
15355
15418
|
"device-export",
|
|
15356
15419
|
"device-extension",
|
|
15357
15420
|
"device-history",
|
|
@@ -15973,13 +16036,6 @@ Object.freeze({
|
|
|
15973
16036
|
"valueType": "string"
|
|
15974
16037
|
}
|
|
15975
16038
|
],
|
|
15976
|
-
"device-discovery": [{
|
|
15977
|
-
"path": "lastDiscoveryAt",
|
|
15978
|
-
"valueType": "number"
|
|
15979
|
-
}, {
|
|
15980
|
-
"path": "lastError",
|
|
15981
|
-
"valueType": "string"
|
|
15982
|
-
}],
|
|
15983
16039
|
"device-export": [
|
|
15984
16040
|
{
|
|
15985
16041
|
"path": "linkState",
|
|
@@ -16168,6 +16224,10 @@ Object.freeze({
|
|
|
16168
16224
|
"path": "lastEvent.eventType",
|
|
16169
16225
|
"valueType": "string"
|
|
16170
16226
|
},
|
|
16227
|
+
{
|
|
16228
|
+
"path": "lastEvent.sourceId",
|
|
16229
|
+
"valueType": "string"
|
|
16230
|
+
},
|
|
16171
16231
|
{
|
|
16172
16232
|
"path": "lastEvent.timestamp",
|
|
16173
16233
|
"valueType": "number"
|
|
@@ -17431,7 +17491,7 @@ Object.freeze({
|
|
|
17431
17491
|
}
|
|
17432
17492
|
});
|
|
17433
17493
|
//#endregion
|
|
17434
|
-
//#region ../types/dist/implemented-methods-
|
|
17494
|
+
//#region ../types/dist/implemented-methods-5YMDeE3f.mjs
|
|
17435
17495
|
/**
|
|
17436
17496
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
17437
17497
|
*
|
|
@@ -44092,6 +44152,41 @@ function macroOnly(macro) {
|
|
|
44092
44152
|
};
|
|
44093
44153
|
}
|
|
44094
44154
|
}
|
|
44155
|
+
var EventVocabularyKindSchema = _enum([
|
|
44156
|
+
"press",
|
|
44157
|
+
"long-press",
|
|
44158
|
+
"long-press-start",
|
|
44159
|
+
"release",
|
|
44160
|
+
"repeat",
|
|
44161
|
+
"double-press",
|
|
44162
|
+
"other"
|
|
44163
|
+
]);
|
|
44164
|
+
/** The part of the device an event comes from: a button, a channel. */
|
|
44165
|
+
var EventVocabularySourceSchema = object({
|
|
44166
|
+
id: string().min(1),
|
|
44167
|
+
label: string().min(1)
|
|
44168
|
+
});
|
|
44169
|
+
/** One scalar field of an event's `data`, by dotted path. */
|
|
44170
|
+
var EventVocabularyFieldSchema = object({
|
|
44171
|
+
path: string().min(1),
|
|
44172
|
+
kind: _enum([
|
|
44173
|
+
"string",
|
|
44174
|
+
"number",
|
|
44175
|
+
"boolean",
|
|
44176
|
+
"enum"
|
|
44177
|
+
]),
|
|
44178
|
+
enumValues: array(string()).optional(),
|
|
44179
|
+
label: string().min(1).optional()
|
|
44180
|
+
});
|
|
44181
|
+
var EventVocabularyEntrySchema = object({
|
|
44182
|
+
/** The native token, verbatim. */
|
|
44183
|
+
id: string().min(1),
|
|
44184
|
+
source: EventVocabularySourceSchema.optional(),
|
|
44185
|
+
label: string().min(1),
|
|
44186
|
+
description: string().optional(),
|
|
44187
|
+
kind: EventVocabularyKindSchema.optional(),
|
|
44188
|
+
payload: array(EventVocabularyFieldSchema).optional()
|
|
44189
|
+
});
|
|
44095
44190
|
Math.PI / 180;
|
|
44096
44191
|
/**
|
|
44097
44192
|
* WHEN something happens, as data (D983) — the one time vocabulary of the
|
|
@@ -44144,6 +44239,62 @@ var WeekdayKeySchema = _enum([
|
|
|
44144
44239
|
"sat",
|
|
44145
44240
|
"sun"
|
|
44146
44241
|
]);
|
|
44242
|
+
/**
|
|
44243
|
+
* Hold semantics — "for N seconds" on a leaf of the `when` tree (#498).
|
|
44244
|
+
*
|
|
44245
|
+
* ONE primitive, `for: { ms, mode, tolerance? }`, shared by `device-becomes`,
|
|
44246
|
+
* `device-is` and `expression`:
|
|
44247
|
+
*
|
|
44248
|
+
* - `held`: the condition stays true for `ms`;
|
|
44249
|
+
* - `stable`: the value does not move by more than `tolerance` (absolute, in
|
|
44250
|
+
* the field's base unit) for `ms` while the condition holds.
|
|
44251
|
+
*
|
|
44252
|
+
* {@link HoldCore} is the pure state machine (an input observation → a reading).
|
|
44253
|
+
* {@link HoldTracker} keys cores by a leaf path for the STATE leaves the tree
|
|
44254
|
+
* evaluator judges. {@link BecomesWatch} wraps a core with a timer for the
|
|
44255
|
+
* `device-becomes` EVENT leaf: the deadline is armed when the hold starts and
|
|
44256
|
+
* cancelled when it breaks, and the leaf fires ONCE per episode.
|
|
44257
|
+
*
|
|
44258
|
+
* Hold state is memory only. A hold that is already running when the watcher
|
|
44259
|
+
* first looks (a restart) is INHERITED: nobody knows when it began, so it never
|
|
44260
|
+
* fires for that episode and a state leaf reads it as unknown until the window
|
|
44261
|
+
* has elapsed from the moment the watcher started (D49: not-known is not false).
|
|
44262
|
+
*/
|
|
44263
|
+
var HOLD_MAX_MS = 864e5;
|
|
44264
|
+
var HoldModeSchema = _enum(["held", "stable"]);
|
|
44265
|
+
var HoldSpecSchema = object({
|
|
44266
|
+
/** How long the condition (or the value) must hold. */
|
|
44267
|
+
ms: number$1().int().min(1).max(HOLD_MAX_MS),
|
|
44268
|
+
mode: HoldModeSchema,
|
|
44269
|
+
/** `stable` only: the largest move of the value that still counts as unchanged (absolute, base unit). */
|
|
44270
|
+
tolerance: number$1().min(0).optional()
|
|
44271
|
+
});
|
|
44272
|
+
/** What a trace says about a hold. */
|
|
44273
|
+
var HoldTraceSchema = object({
|
|
44274
|
+
phase: _enum([
|
|
44275
|
+
"pending",
|
|
44276
|
+
"cancelled",
|
|
44277
|
+
"fired",
|
|
44278
|
+
"reseeded"
|
|
44279
|
+
]),
|
|
44280
|
+
ms: number$1().int(),
|
|
44281
|
+
mode: HoldModeSchema,
|
|
44282
|
+
/** When the hold started (ms epoch). */
|
|
44283
|
+
since: number$1().optional(),
|
|
44284
|
+
/** When it completes (ms epoch), for a pending hold. */
|
|
44285
|
+
deadline: number$1().optional()
|
|
44286
|
+
});
|
|
44287
|
+
/** The operators a `device-becomes` leaf may carry: a threshold or a range. */
|
|
44288
|
+
var BecomesOperatorSchema = _enum([
|
|
44289
|
+
"eq",
|
|
44290
|
+
"ne",
|
|
44291
|
+
"gt",
|
|
44292
|
+
"gte",
|
|
44293
|
+
"lt",
|
|
44294
|
+
"lte",
|
|
44295
|
+
"between",
|
|
44296
|
+
"outside"
|
|
44297
|
+
]);
|
|
44147
44298
|
/** How a state leaf compares a device field to a value. Derived from the
|
|
44148
44299
|
* field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
44149
44300
|
var AutomationConditionOperatorSchema = _enum([
|
|
@@ -44157,6 +44308,13 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
44157
44308
|
"in"
|
|
44158
44309
|
]);
|
|
44159
44310
|
/**
|
|
44311
|
+
* The stable name of an event leaf. A path (`when.0.1`) moves when a row is
|
|
44312
|
+
* reordered; the id does not, so a `triggered-by` leaf keeps pointing at the
|
|
44313
|
+
* same row. Optional: a leaf nothing refers to needs none.
|
|
44314
|
+
*/
|
|
44315
|
+
var EVENT_LEAF_ID_RE = /^[A-Za-z0-9_-]{1,40}$/;
|
|
44316
|
+
var eventLeafId = { id: string().regex(EVENT_LEAF_ID_RE).optional() };
|
|
44317
|
+
/**
|
|
44160
44318
|
* D8 compliance, and it is the reason a "becomes" leaf is not merely an event
|
|
44161
44319
|
* subscription: it evaluates against the **state mirror**, which is reconciled,
|
|
44162
44320
|
* and an event only WAKES the evaluation. A dropped event therefore DELAYS a
|
|
@@ -44164,24 +44322,60 @@ var AutomationConditionOperatorSchema = _enum([
|
|
|
44164
44322
|
*/
|
|
44165
44323
|
var DeviceBecomesLeafSchema = object({
|
|
44166
44324
|
kind: literal("device-becomes"),
|
|
44325
|
+
...eventLeafId,
|
|
44167
44326
|
deviceId: number$1().int().nonnegative(),
|
|
44168
44327
|
cap: string().min(1),
|
|
44169
44328
|
fieldPath: string().min(1),
|
|
44170
|
-
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
44329
|
+
/** Fire when the field takes this value. Omit (with no `operator`) to fire on any change. */
|
|
44171
44330
|
becomes: union([
|
|
44172
44331
|
string(),
|
|
44173
44332
|
number$1(),
|
|
44174
44333
|
boolean()
|
|
44175
44334
|
]).optional(),
|
|
44176
|
-
/**
|
|
44335
|
+
/**
|
|
44336
|
+
* A threshold or a range instead of an equality: "crosses above 3000", "enters
|
|
44337
|
+
* 18..24". The leaf fires on the TRANSITION into the predicate, never on a re-report.
|
|
44338
|
+
* `value` is in the field's base unit; `between` / `outside` take `[low, high]`.
|
|
44339
|
+
*/
|
|
44340
|
+
operator: BecomesOperatorSchema.optional(),
|
|
44341
|
+
value: union([
|
|
44342
|
+
number$1(),
|
|
44343
|
+
string(),
|
|
44344
|
+
boolean(),
|
|
44345
|
+
array(number$1()).length(2)
|
|
44346
|
+
]).optional(),
|
|
44347
|
+
/**
|
|
44348
|
+
* Hold: the transition (or, with no predicate, the new value) must stay for
|
|
44349
|
+
* `for.ms` before the leaf fires, ONCE per episode. See `hold.ts`.
|
|
44350
|
+
*/
|
|
44351
|
+
for: HoldSpecSchema.optional(),
|
|
44352
|
+
/** @deprecated every leaf fires on a transition now; read as always on. */
|
|
44177
44353
|
edge: boolean().optional(),
|
|
44178
|
-
/**
|
|
44179
|
-
forMs: number$1().int().min(0).max(
|
|
44354
|
+
/** @deprecated legacy `held` window — read through `holdOf` as `for: { ms, mode: 'held' }`. */
|
|
44355
|
+
forMs: number$1().int().min(0).max(HOLD_MAX_MS).optional(),
|
|
44180
44356
|
/** Collapse a burst into one occurrence. */
|
|
44181
44357
|
debounceMs: number$1().int().min(0).max(6e5).optional()
|
|
44358
|
+
}).superRefine((leaf, ctx) => {
|
|
44359
|
+
if (leaf.becomes !== void 0 && leaf.operator !== void 0) ctx.addIssue({
|
|
44360
|
+
code: "custom",
|
|
44361
|
+
message: "a leaf names `becomes` or `operator`, not both",
|
|
44362
|
+
path: ["operator"]
|
|
44363
|
+
});
|
|
44364
|
+
if (leaf.operator !== void 0 && leaf.value === void 0) ctx.addIssue({
|
|
44365
|
+
code: "custom",
|
|
44366
|
+
message: "an operator needs the value to compare to",
|
|
44367
|
+
path: ["value"]
|
|
44368
|
+
});
|
|
44369
|
+
const range = leaf.operator === "between" || leaf.operator === "outside";
|
|
44370
|
+
if (leaf.operator !== void 0 && range !== Array.isArray(leaf.value)) ctx.addIssue({
|
|
44371
|
+
code: "custom",
|
|
44372
|
+
message: range ? "a range takes [low, high]" : "only a range takes [low, high]",
|
|
44373
|
+
path: ["value"]
|
|
44374
|
+
});
|
|
44182
44375
|
});
|
|
44183
44376
|
var DeviceEventLeafSchema = object({
|
|
44184
44377
|
kind: literal("device-event"),
|
|
44378
|
+
...eventLeafId,
|
|
44185
44379
|
/** An `EventCategory` value. */
|
|
44186
44380
|
category: string().min(1),
|
|
44187
44381
|
deviceId: number$1().int().nonnegative().optional()
|
|
@@ -44194,18 +44388,51 @@ var DeviceEventLeafSchema = object({
|
|
|
44194
44388
|
*/
|
|
44195
44389
|
var ScheduleLeafSchema = object({
|
|
44196
44390
|
kind: literal("schedule"),
|
|
44391
|
+
...eventLeafId,
|
|
44197
44392
|
at: TimeSpecSchema.optional(),
|
|
44198
44393
|
days: array(WeekdayKeySchema).min(1).max(7).optional(),
|
|
44199
44394
|
/** @deprecated legacy cron — new schedules use `at`. */
|
|
44200
44395
|
cron: string().min(1).max(120).optional()
|
|
44201
44396
|
}).refine((leaf) => leaf.at === void 0 !== (leaf.cron === void 0), { message: "a schedule names exactly one of `at` (a time spec) and `cron` (legacy)" }).refine((leaf) => leaf.days === void 0 || leaf.at !== void 0, { message: "`days` goes with `at`; a cron expression carries its own days" });
|
|
44202
|
-
|
|
44397
|
+
/**
|
|
44398
|
+
* One payload predicate of a `device-fired` row: the value at `path` inside
|
|
44399
|
+
* the fire's `data` (dotted) equals `equals`. Compared strictly, no coercion.
|
|
44400
|
+
*/
|
|
44401
|
+
var DeviceFiredMatchSchema = object({
|
|
44402
|
+
path: string().min(1).max(120),
|
|
44403
|
+
equals: union([
|
|
44404
|
+
string(),
|
|
44405
|
+
number$1(),
|
|
44406
|
+
boolean()
|
|
44407
|
+
])
|
|
44408
|
+
});
|
|
44409
|
+
/**
|
|
44410
|
+
* "fires": one device of the `event-emitter` capability reported an event
|
|
44411
|
+
* (D1045). Every list is an OR, the lists together an AND; an absent list does
|
|
44412
|
+
* not narrow. `eventIds` are the device's own tokens, `sourceIds` the parts of
|
|
44413
|
+
* the device (a button), `kinds` the shared press kinds of its vocabulary.
|
|
44414
|
+
*/
|
|
44415
|
+
var DeviceFiredLeafSchema = object({
|
|
44416
|
+
kind: literal("device-fired"),
|
|
44417
|
+
...eventLeafId,
|
|
44418
|
+
deviceId: number$1().int().nonnegative(),
|
|
44419
|
+
eventIds: array(string().min(1)).min(1).max(64).optional(),
|
|
44420
|
+
sourceIds: array(string().min(1)).min(1).max(64).optional(),
|
|
44421
|
+
kinds: array(EventVocabularyKindSchema).min(1).optional(),
|
|
44422
|
+
match: array(DeviceFiredMatchSchema).min(1).max(8).optional()
|
|
44423
|
+
});
|
|
44424
|
+
var ManualLeafSchema = object({
|
|
44425
|
+
kind: literal("manual"),
|
|
44426
|
+
...eventLeafId
|
|
44427
|
+
});
|
|
44203
44428
|
var IntervalLeafSchema = object({
|
|
44204
44429
|
kind: literal("interval"),
|
|
44430
|
+
...eventLeafId,
|
|
44205
44431
|
minutes: number$1().int().min(1).max(1440)
|
|
44206
44432
|
});
|
|
44207
44433
|
var DetectionLeafSchema = object({
|
|
44208
44434
|
kind: literal("detection"),
|
|
44435
|
+
...eventLeafId,
|
|
44209
44436
|
/** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
|
|
44210
44437
|
subjects: array(DetectionSubjectSchema).min(1),
|
|
44211
44438
|
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
@@ -44216,6 +44443,7 @@ var DetectionLeafSchema = object({
|
|
|
44216
44443
|
});
|
|
44217
44444
|
var StationaryLeafSchema = object({
|
|
44218
44445
|
kind: literal("stationary"),
|
|
44446
|
+
...eventLeafId,
|
|
44219
44447
|
deviceIds: array(number$1().int().nonnegative()).min(1),
|
|
44220
44448
|
subjects: array(StationarySubjectSchema).min(1).optional(),
|
|
44221
44449
|
zones: array(string().min(1)).min(1).optional(),
|
|
@@ -44243,7 +44471,9 @@ var DeviceIsLeafSchema = object({
|
|
|
44243
44471
|
QuantityValueSchema
|
|
44244
44472
|
]),
|
|
44245
44473
|
/** The unit the person picked for `value` (display only; `value` is in the base unit). */
|
|
44246
|
-
unit: string().min(1).optional()
|
|
44474
|
+
unit: string().min(1).optional(),
|
|
44475
|
+
/** The comparison must hold (`held`) or the value stay put (`stable`) this long. */
|
|
44476
|
+
for: HoldSpecSchema.optional()
|
|
44247
44477
|
});
|
|
44248
44478
|
/**
|
|
44249
44479
|
* True when the camera currently has a PARKED object matching `subjects` in at
|
|
@@ -44259,6 +44489,41 @@ var StationaryPresentLeafSchema = object({
|
|
|
44259
44489
|
minCount: number$1().int().positive().optional()
|
|
44260
44490
|
});
|
|
44261
44491
|
/**
|
|
44492
|
+
* True while the site-local clock is inside a daily window and/or on one of
|
|
44493
|
+
* the named days. `days` names the local day a window STARTS on, so
|
|
44494
|
+
* 22:00-06:00 on weekdays is still inside at 01:00 Saturday. A one-date spec
|
|
44495
|
+
* (`kind: 'date'`) is refused: only a clock time or a sun event makes a window
|
|
44496
|
+
* that repeats every day.
|
|
44497
|
+
*/
|
|
44498
|
+
var TimeIsLeafSchema = object({
|
|
44499
|
+
kind: literal("time-is"),
|
|
44500
|
+
between: object({
|
|
44501
|
+
from: TimeSpecSchema,
|
|
44502
|
+
to: TimeSpecSchema
|
|
44503
|
+
}).optional(),
|
|
44504
|
+
days: array(WeekdayKeySchema).min(1).max(7).optional()
|
|
44505
|
+
}).superRefine((leaf, ctx) => {
|
|
44506
|
+
if (leaf.between === void 0 && leaf.days === void 0) ctx.addIssue({
|
|
44507
|
+
code: "custom",
|
|
44508
|
+
message: "a time-is leaf needs a between window, days, or both"
|
|
44509
|
+
});
|
|
44510
|
+
for (const end of ["from", "to"]) if (leaf.between?.[end].kind === "date") ctx.addIssue({
|
|
44511
|
+
code: "custom",
|
|
44512
|
+
message: "a one-date time cannot bound a daily window: use a clock time or a sun event",
|
|
44513
|
+
path: ["between", end]
|
|
44514
|
+
});
|
|
44515
|
+
});
|
|
44516
|
+
/**
|
|
44517
|
+
* True for the run that one of the named event leaves started. It reads the
|
|
44518
|
+
* run, not a device: in the tree it is judged against the leaf that fired, in
|
|
44519
|
+
* a step guard against the trigger the run carries. A run no event leaf
|
|
44520
|
+
* started (a state-only tree becoming true) is false.
|
|
44521
|
+
*/
|
|
44522
|
+
var TriggeredByLeafSchema = object({
|
|
44523
|
+
kind: literal("triggered-by"),
|
|
44524
|
+
leafIds: array(string().regex(EVENT_LEAF_ID_RE)).min(1).max(20)
|
|
44525
|
+
});
|
|
44526
|
+
/**
|
|
44262
44527
|
* The escape hatch: the bounded expression engine over named device bindings.
|
|
44263
44528
|
* Declared as a plain object so a discriminated union can read `kind`; the
|
|
44264
44529
|
* author-time validation is the SAME function `ExpressionSourceSchema` runs.
|
|
@@ -44266,7 +44531,9 @@ var StationaryPresentLeafSchema = object({
|
|
|
44266
44531
|
var ExpressionLeafSchema = object({
|
|
44267
44532
|
kind: literal("expression"),
|
|
44268
44533
|
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
44269
|
-
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
44534
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema),
|
|
44535
|
+
/** The expression must stay true this long. An expression has no single value, so only `held`. */
|
|
44536
|
+
for: HoldSpecSchema.optional()
|
|
44270
44537
|
}).superRefine((src, ctx) => {
|
|
44271
44538
|
const err = validateExpressionSource(src);
|
|
44272
44539
|
if (err !== null) ctx.addIssue({
|
|
@@ -44274,6 +44541,11 @@ var ExpressionLeafSchema = object({
|
|
|
44274
44541
|
message: err,
|
|
44275
44542
|
path: ["expr"]
|
|
44276
44543
|
});
|
|
44544
|
+
if (src.for?.mode === "stable") ctx.addIssue({
|
|
44545
|
+
code: "custom",
|
|
44546
|
+
message: "an expression is held, not stable: it has no single value to keep still",
|
|
44547
|
+
path: ["for", "mode"]
|
|
44548
|
+
});
|
|
44277
44549
|
});
|
|
44278
44550
|
var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
44279
44551
|
object({
|
|
@@ -44290,6 +44562,7 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
|
44290
44562
|
}),
|
|
44291
44563
|
DeviceBecomesLeafSchema,
|
|
44292
44564
|
DeviceEventLeafSchema,
|
|
44565
|
+
DeviceFiredLeafSchema,
|
|
44293
44566
|
ScheduleLeafSchema,
|
|
44294
44567
|
ManualLeafSchema,
|
|
44295
44568
|
IntervalLeafSchema,
|
|
@@ -44297,11 +44570,14 @@ var AutomationWhenSchema = lazy(() => discriminatedUnion("kind", [
|
|
|
44297
44570
|
StationaryLeafSchema,
|
|
44298
44571
|
DeviceIsLeafSchema,
|
|
44299
44572
|
StationaryPresentLeafSchema,
|
|
44300
|
-
ExpressionLeafSchema
|
|
44573
|
+
ExpressionLeafSchema,
|
|
44574
|
+
TimeIsLeafSchema,
|
|
44575
|
+
TriggeredByLeafSchema
|
|
44301
44576
|
]));
|
|
44302
44577
|
var EVENT_KINDS = new Set([
|
|
44303
44578
|
"device-becomes",
|
|
44304
44579
|
"device-event",
|
|
44580
|
+
"device-fired",
|
|
44305
44581
|
"schedule",
|
|
44306
44582
|
"manual",
|
|
44307
44583
|
"interval",
|
|
@@ -44351,6 +44627,28 @@ function hasEventLeaves(node) {
|
|
|
44351
44627
|
});
|
|
44352
44628
|
return found;
|
|
44353
44629
|
}
|
|
44630
|
+
/** The event leaves of the tree that carry an id, in tree order. */
|
|
44631
|
+
function identifiedEventLeaves(when) {
|
|
44632
|
+
const found = [];
|
|
44633
|
+
forEachLeaf(when, (leaf, path) => {
|
|
44634
|
+
if (isEventLeaf(leaf) && leaf.id !== void 0) found.push({
|
|
44635
|
+
id: leaf.id,
|
|
44636
|
+
path,
|
|
44637
|
+
leaf
|
|
44638
|
+
});
|
|
44639
|
+
});
|
|
44640
|
+
return found;
|
|
44641
|
+
}
|
|
44642
|
+
/** Does the tree hold a `triggered-by` leaf? */
|
|
44643
|
+
function hasTriggeredByLeaves(when) {
|
|
44644
|
+
let found = false;
|
|
44645
|
+
forEachLeaf(when, (leaf) => {
|
|
44646
|
+
if (leaf.kind === "triggered-by") found = true;
|
|
44647
|
+
});
|
|
44648
|
+
return found;
|
|
44649
|
+
}
|
|
44650
|
+
/** Why a `triggered-by` leaf is refused outside an automation: there is no trigger row to name. */
|
|
44651
|
+
var TRIGGERED_BY_NEEDS_AUTOMATION = "\"triggered by\" reads which trigger started an automation — an action or a scheduler has no trigger rows, so it could never be true here";
|
|
44354
44652
|
function hasEmptyContainer(node) {
|
|
44355
44653
|
if (isLeaf(node)) return false;
|
|
44356
44654
|
if (node.kind === "not") return hasEmptyContainer(node.child);
|
|
@@ -44360,6 +44658,7 @@ function hasEmptyContainer(node) {
|
|
|
44360
44658
|
var LEAF_WORDS = {
|
|
44361
44659
|
"device-becomes": "a device value that becomes",
|
|
44362
44660
|
"device-event": "a device event",
|
|
44661
|
+
"device-fired": "a device firing an event",
|
|
44363
44662
|
schedule: "a schedule",
|
|
44364
44663
|
manual: "a manual run",
|
|
44365
44664
|
interval: "an interval",
|
|
@@ -44409,6 +44708,11 @@ function validateWhenTree(when) {
|
|
|
44409
44708
|
if (depth > 4) return `the rules nest ${depth} levels deep — the limit is 4. "not (any of …)" is the same as "all of (not …)", which is one level shallower`;
|
|
44410
44709
|
const leaves = countWhenLeaves(when);
|
|
44411
44710
|
if (leaves > 32) return `the rules use ${leaves} rows — the limit is 32`;
|
|
44711
|
+
const seen = /* @__PURE__ */ new Set();
|
|
44712
|
+
for (const entry of identifiedEventLeaves(when)) {
|
|
44713
|
+
if (seen.has(entry.id)) return `two trigger rows share the id "${entry.id}" — an id names one row`;
|
|
44714
|
+
seen.add(entry.id);
|
|
44715
|
+
}
|
|
44412
44716
|
const need = eventNeed(when, "when", false);
|
|
44413
44717
|
if ("error" in need) return need.error;
|
|
44414
44718
|
const [first, second] = need.paths;
|
|
@@ -44620,9 +44924,12 @@ function convertLegacyWhen(half) {
|
|
|
44620
44924
|
* every caller — automations, Actions, and the notification centre, which runs
|
|
44621
44925
|
* only Actions (planning#473).
|
|
44622
44926
|
*/
|
|
44927
|
+
/** The longest one `wait` step lasts. It is cut by a restart of the run and does not survive a hub restart. */
|
|
44928
|
+
var WAIT_MAX_SECONDS = 3600;
|
|
44929
|
+
var WAIT_TOO_LONG = `a wait is at most one hour (${WAIT_MAX_SECONDS} s)`;
|
|
44623
44930
|
var AutomationWaitStepSchema = object({
|
|
44624
44931
|
kind: literal("wait"),
|
|
44625
|
-
seconds: number$1().min(0).max(
|
|
44932
|
+
seconds: number$1().min(0).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG)
|
|
44626
44933
|
});
|
|
44627
44934
|
/** The legacy `deviceId` of a cap step: still READ, never written (D983). */
|
|
44628
44935
|
var LegacyStepDeviceSchema = union([
|
|
@@ -44692,28 +44999,227 @@ var elseNeedsIf = {
|
|
|
44692
44999
|
path: ["else"]
|
|
44693
45000
|
};
|
|
44694
45001
|
var GuardedWaitStepSchema = AutomationWaitStepSchema.extend(stepGuardShape).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
|
|
44695
|
-
|
|
45002
|
+
/**
|
|
45003
|
+
* A step of a STANDALONE list (an action, a scheduler point, a preset): the
|
|
45004
|
+
* same two step kinds an automation runs, minus a guard that reads the
|
|
45005
|
+
* automation's trigger, which such a list does not have.
|
|
45006
|
+
*/
|
|
45007
|
+
var ActionStepSchema = discriminatedUnion("kind", [GuardedWaitStepSchema, AutomationCapStepBaseSchema.extend(stepGuardShape).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
|
|
45008
|
+
message: "a cap step needs a target",
|
|
45009
|
+
path: ["target"]
|
|
45010
|
+
}).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
|
|
45011
|
+
message: "a domain target needs the vocabulary action it applies to (actionId)",
|
|
45012
|
+
path: ["actionId"]
|
|
45013
|
+
}).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf)]).superRefine((step, ctx) => {
|
|
45014
|
+
if (step.if !== void 0 && hasTriggeredByLeaves(step.if)) ctx.addIssue({
|
|
45015
|
+
code: "custom",
|
|
45016
|
+
message: TRIGGERED_BY_NEEDS_AUTOMATION,
|
|
45017
|
+
path: ["if"]
|
|
45018
|
+
});
|
|
45019
|
+
});
|
|
45020
|
+
/**
|
|
45021
|
+
* Flow control of an AUTOMATION's list (#500). A standalone list (an action, a
|
|
45022
|
+
* scheduler point, a preset) stays `wait` and `cap`: it has no run of its own
|
|
45023
|
+
* to end and no variable bag to write.
|
|
45024
|
+
*/
|
|
45025
|
+
var continueOnErrorShape = {
|
|
45026
|
+
/** A failure of this step is traced `failed-continued` and the next step runs. */
|
|
45027
|
+
continueOnError: literal(true).optional() };
|
|
45028
|
+
/** An automation's cap step: the shared guarded cap step, plus `continueOnError`. */
|
|
45029
|
+
var AutomationGuardedCapStepSchema = AutomationCapStepBaseSchema.extend({
|
|
45030
|
+
...stepGuardShape,
|
|
45031
|
+
...continueOnErrorShape
|
|
45032
|
+
}).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
|
|
44696
45033
|
message: "a cap step needs a target",
|
|
44697
45034
|
path: ["target"]
|
|
44698
45035
|
}).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
|
|
44699
45036
|
message: "a domain target needs the vocabulary action it applies to (actionId)",
|
|
44700
45037
|
path: ["actionId"]
|
|
44701
45038
|
}).refine((step) => step.else === void 0 || step.if !== void 0, elseNeedsIf);
|
|
44702
|
-
|
|
45039
|
+
/** End the run here, on purpose. `reason` may carry `{{vars.x}}` slots. */
|
|
45040
|
+
var AutomationStopStepSchema = object({
|
|
45041
|
+
kind: literal("stop"),
|
|
45042
|
+
reason: string().min(1, "a stop needs a reason: it is what the run trace shows").max(200),
|
|
45043
|
+
/** `ok`: the run ends as a success. `failed`: it ends as a failure, with the reason. */
|
|
45044
|
+
outcome: _enum(["ok", "failed"]),
|
|
45045
|
+
/** Stop only when this reads true; false or unknown does not stop. */
|
|
45046
|
+
if: AutomationWhenSchema.optional()
|
|
45047
|
+
});
|
|
45048
|
+
var SET_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,39}$/;
|
|
45049
|
+
/** Names the run itself writes and reads (`leaf` is what `triggered-by` judges): a step may not overwrite them. */
|
|
45050
|
+
var RESERVED_RUN_VARS = [
|
|
45051
|
+
"kind",
|
|
45052
|
+
"leaf",
|
|
45053
|
+
"at"
|
|
45054
|
+
];
|
|
45055
|
+
/**
|
|
45056
|
+
* Write variables into the run's bag, for the steps after it. SUBSTITUTION
|
|
45057
|
+
* only, never evaluation: a text value may carry `{{vars.x}}` slots.
|
|
45058
|
+
*/
|
|
45059
|
+
var AutomationSetStepSchema = object({
|
|
45060
|
+
kind: literal("set"),
|
|
45061
|
+
vars: record(string(), union([
|
|
45062
|
+
string().max(500),
|
|
45063
|
+
number$1(),
|
|
45064
|
+
boolean()
|
|
45065
|
+
])).refine((vars) => Object.keys(vars).every((name) => SET_VAR_NAME_RE.test(name)), { message: "a variable name is letters, digits and _, not starting with a digit (max 40)" }).refine((vars) => Object.keys(vars).length >= 1, { message: "set needs at least one variable" }).refine((vars) => Object.keys(vars).length <= 20, { message: `set holds at most 20 variables` }).refine((vars) => Object.keys(vars).every((name) => !RESERVED_RUN_VARS.includes(name)), { message: `the names ${RESERVED_RUN_VARS.join(", ")} belong to the run and cannot be set` }),
|
|
45066
|
+
/** Set only when this reads true; false or unknown leaves the bag as it is. */
|
|
45067
|
+
if: AutomationWhenSchema.optional()
|
|
45068
|
+
});
|
|
45069
|
+
var AutomationRunActionStepSchema = object({
|
|
45070
|
+
kind: literal("action"),
|
|
45071
|
+
/** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
|
|
45072
|
+
actionId: string().min(1),
|
|
45073
|
+
/** Values for the action's declared variables; the others take their default. */
|
|
45074
|
+
variables: record(string(), union([
|
|
45075
|
+
string(),
|
|
45076
|
+
number$1(),
|
|
45077
|
+
boolean()
|
|
45078
|
+
])).optional(),
|
|
45079
|
+
...continueOnErrorShape
|
|
45080
|
+
});
|
|
45081
|
+
/**
|
|
45082
|
+
* A step INSIDE a branch of a `choose`: a flow step with no condition of its
|
|
45083
|
+
* own (the branch is the condition) and no block inside it. Strict, so an `if`
|
|
45084
|
+
* written on such a step is refused rather than dropped.
|
|
45085
|
+
*/
|
|
45086
|
+
/** The longest a `wait-until` may wait: six hours. */
|
|
45087
|
+
var WAIT_UNTIL_TIMEOUT_MAX_S = 21600;
|
|
45088
|
+
/**
|
|
45089
|
+
* Pause the run until a condition is true (#500). The timeout is mandatory: a
|
|
45090
|
+
* run never waits without an end. `onTimeout` says what a wait that ran out
|
|
45091
|
+
* does: `continue` goes on to the next step, `stop` ends the run as a failure.
|
|
45092
|
+
* A condition that cannot be read is not "true": the wait goes on (D49).
|
|
45093
|
+
*/
|
|
45094
|
+
var AutomationWaitUntilStepSchema = object({
|
|
45095
|
+
kind: literal("wait-until"),
|
|
45096
|
+
when: AutomationWhenSchema,
|
|
45097
|
+
timeoutS: number$1().int().min(1, "a wait needs a timeout of at least one second").max(WAIT_UNTIL_TIMEOUT_MAX_S, `a wait for a condition is at most six hours (${WAIT_UNTIL_TIMEOUT_MAX_S} s)`),
|
|
45098
|
+
onTimeout: _enum(["continue", "stop"])
|
|
45099
|
+
}).strict();
|
|
45100
|
+
/** The step kinds that hold steps of their own, or pause on a condition: they nest two levels at most. */
|
|
45101
|
+
var BLOCK_STEP_KINDS = [
|
|
45102
|
+
"choose",
|
|
45103
|
+
"repeat",
|
|
45104
|
+
"parallel",
|
|
45105
|
+
"wait-until"
|
|
45106
|
+
];
|
|
45107
|
+
var NEST_TOO_DEEP = `steps nest two levels at most: a step inside a nested choose or repeat can be a plain step only`;
|
|
45108
|
+
function kindOf(input) {
|
|
45109
|
+
return typeof input === "object" && input !== null ? Reflect.get(input, "kind") : void 0;
|
|
45110
|
+
}
|
|
45111
|
+
/** Where the nesting ends, a block step is refused in words rather than as an unknown kind. */
|
|
45112
|
+
function nestTooDeep(issue) {
|
|
45113
|
+
const kind = kindOf(issue.input);
|
|
45114
|
+
return typeof kind === "string" && BLOCK_STEP_KINDS.includes(kind) ? NEST_TOO_DEEP : void 0;
|
|
45115
|
+
}
|
|
45116
|
+
var plainStepSchemas = [
|
|
45117
|
+
AutomationWaitStepSchema.strict(),
|
|
45118
|
+
AutomationCapStepBaseSchema.extend(continueOnErrorShape).strict().refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
|
|
45119
|
+
message: "a cap step needs a target",
|
|
45120
|
+
path: ["target"]
|
|
45121
|
+
}).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
|
|
45122
|
+
message: "a domain target needs the vocabulary action it applies to (actionId)",
|
|
45123
|
+
path: ["actionId"]
|
|
45124
|
+
}),
|
|
45125
|
+
AutomationStopStepSchema.omit({ if: true }).strict(),
|
|
45126
|
+
AutomationSetStepSchema.omit({ if: true }).strict(),
|
|
45127
|
+
AutomationRunActionStepSchema.strict()
|
|
45128
|
+
];
|
|
45129
|
+
/**
|
|
45130
|
+
* A plain step: a flow step with no condition of its own and no step inside
|
|
45131
|
+
* it. The second (last) level of nesting holds these only.
|
|
45132
|
+
*/
|
|
45133
|
+
var AutomationBranchStepSchema = discriminatedUnion("kind", plainStepSchemas, { error: nestTooDeep });
|
|
45134
|
+
/** A step that can run beside others: it writes no variable and does not end the run. */
|
|
45135
|
+
var AutomationParallelMemberSchema = AutomationBranchStepSchema.refine((step) => step.kind === "cap" || step.kind === "action" || step.kind === "wait", "a parallel holds device commands, actions and waits only: a set or a stop beside other steps has no order");
|
|
45136
|
+
/**
|
|
45137
|
+
* Several steps at once (#500): all are started together and all are awaited.
|
|
45138
|
+
* A failure fails the run once the others finished, as in a sequence; a member
|
|
45139
|
+
* marked "continue if this fails" does not.
|
|
45140
|
+
*/
|
|
45141
|
+
var AutomationParallelStepSchema = object({
|
|
45142
|
+
kind: literal("parallel"),
|
|
45143
|
+
steps: array(AutomationParallelMemberSchema).min(2, "a parallel needs at least two steps").max(5, `a parallel takes at most 5 steps`)
|
|
45144
|
+
}).strict();
|
|
45145
|
+
var REPEAT_NEEDS_MAX = "a repeat on a condition needs a maximum number of iterations";
|
|
45146
|
+
/** A loop on a condition: the bound is mandatory, so a run never loops without an end. */
|
|
45147
|
+
var repeatConditionShape = {
|
|
45148
|
+
when: AutomationWhenSchema,
|
|
45149
|
+
maxIterations: number$1({ error: REPEAT_NEEDS_MAX }).int().min(1, REPEAT_NEEDS_MAX).max(50, `a repeat on a condition runs at most 50 times`),
|
|
45150
|
+
/** A pause between two iterations, never after the last. */
|
|
45151
|
+
waitS: number$1().int().min(1).max(WAIT_MAX_SECONDS, WAIT_TOO_LONG).optional()
|
|
45152
|
+
};
|
|
45153
|
+
/** What decides how many times a `repeat` runs. */
|
|
45154
|
+
var AutomationRepeatLoopSchema = discriminatedUnion("by", [
|
|
45155
|
+
object({
|
|
45156
|
+
by: literal("count"),
|
|
45157
|
+
count: number$1().int().min(1).max(20, `a repeat runs at most 20 times`)
|
|
45158
|
+
}).strict(),
|
|
45159
|
+
object({
|
|
45160
|
+
by: literal("while"),
|
|
45161
|
+
...repeatConditionShape
|
|
45162
|
+
}).strict(),
|
|
45163
|
+
object({
|
|
45164
|
+
by: literal("until"),
|
|
45165
|
+
...repeatConditionShape
|
|
45166
|
+
}).strict(),
|
|
45167
|
+
object({
|
|
45168
|
+
by: literal("forEach"),
|
|
45169
|
+
target: object({
|
|
45170
|
+
kind: literal("devices"),
|
|
45171
|
+
ids: array(number$1().int()).min(1, "a repeat over devices needs at least one device").max(50, `a repeat over devices takes at most 50 devices`)
|
|
45172
|
+
}).strict()
|
|
45173
|
+
}).strict()
|
|
45174
|
+
]);
|
|
45175
|
+
/**
|
|
45176
|
+
* The same steps several times (#500). Every form is bounded at save, so a
|
|
45177
|
+
* run cannot loop without an end. A failure or a `stop` in the steps ends the
|
|
45178
|
+
* loop and the run, as in a sequence.
|
|
45179
|
+
*/
|
|
45180
|
+
function repeatSchemaOf(step) {
|
|
45181
|
+
return object({
|
|
45182
|
+
kind: literal("repeat"),
|
|
45183
|
+
loop: AutomationRepeatLoopSchema,
|
|
45184
|
+
steps: array(step).min(1, "a repeat needs at least one step").max(10, `a repeat takes at most 10 steps`)
|
|
45185
|
+
}).strict();
|
|
45186
|
+
}
|
|
45187
|
+
/**
|
|
45188
|
+
* One step, several ways (#500): the branches are read in order and the FIRST
|
|
45189
|
+
* whose condition is true runs; `else` runs when every branch is false. A
|
|
45190
|
+
* branch that cannot be read ends the choice: nothing runs, not the branches
|
|
45191
|
+
* after it and not the else (D49). The #472 `if` / `else` of a wait or cap
|
|
45192
|
+
* step is the one-branch case and stays as it is stored.
|
|
45193
|
+
*/
|
|
45194
|
+
function chooseSchemaOf(step) {
|
|
45195
|
+
const steps = array(step).max(10);
|
|
45196
|
+
return object({
|
|
45197
|
+
kind: literal("choose"),
|
|
45198
|
+
branches: array(object({
|
|
45199
|
+
if: AutomationWhenSchema,
|
|
45200
|
+
steps: steps.min(1, "a branch needs at least one step")
|
|
45201
|
+
})).min(1, "a choose needs at least one branch").max(6, `a choose takes at most 6 branches`),
|
|
45202
|
+
else: steps.optional()
|
|
45203
|
+
});
|
|
45204
|
+
}
|
|
45205
|
+
var AutomationBodyStepSchema = discriminatedUnion("kind", [
|
|
45206
|
+
...plainStepSchemas,
|
|
45207
|
+
AutomationWaitUntilStepSchema,
|
|
45208
|
+
AutomationParallelStepSchema,
|
|
45209
|
+
chooseSchemaOf(AutomationBranchStepSchema),
|
|
45210
|
+
repeatSchemaOf(AutomationBranchStepSchema)
|
|
45211
|
+
]);
|
|
45212
|
+
var AutomationRepeatStepSchema = repeatSchemaOf(AutomationBodyStepSchema);
|
|
44703
45213
|
var AutomationActionSchema = discriminatedUnion("kind", [
|
|
44704
45214
|
GuardedWaitStepSchema,
|
|
44705
|
-
|
|
44706
|
-
|
|
44707
|
-
|
|
44708
|
-
|
|
44709
|
-
|
|
44710
|
-
|
|
44711
|
-
|
|
44712
|
-
|
|
44713
|
-
number$1(),
|
|
44714
|
-
boolean()
|
|
44715
|
-
])).optional()
|
|
44716
|
-
}),
|
|
45215
|
+
AutomationGuardedCapStepSchema,
|
|
45216
|
+
AutomationStopStepSchema,
|
|
45217
|
+
AutomationSetStepSchema,
|
|
45218
|
+
chooseSchemaOf(AutomationBodyStepSchema),
|
|
45219
|
+
AutomationWaitUntilStepSchema,
|
|
45220
|
+
AutomationParallelStepSchema,
|
|
45221
|
+
AutomationRepeatStepSchema,
|
|
45222
|
+
AutomationRunActionStepSchema,
|
|
44717
45223
|
object({
|
|
44718
45224
|
kind: literal("code"),
|
|
44719
45225
|
/** Compiled into the automation's OWN block by esbuild — not a third
|
|
@@ -44776,11 +45282,6 @@ function convertLegacyRecipe(legacy) {
|
|
|
44776
45282
|
...rest
|
|
44777
45283
|
};
|
|
44778
45284
|
}
|
|
44779
|
-
/**
|
|
44780
|
-
* Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
|
|
44781
|
-
* saved block carries): the new shape as it is, or the old shape converted to
|
|
44782
|
-
* it. The output type is always the new one.
|
|
44783
|
-
*/
|
|
44784
45285
|
var AutomationRecipeSchema = union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
|
|
44785
45286
|
var DETECTION_DESCRIPTIONS = {
|
|
44786
45287
|
deviceId: "the camera that saw it",
|
|
@@ -44796,6 +45297,21 @@ var STATIONARY_DESCRIPTIONS = {
|
|
|
44796
45297
|
phase: "appeared or departed",
|
|
44797
45298
|
entryId: "the stopped object"
|
|
44798
45299
|
};
|
|
45300
|
+
var BECOMES_DESCRIPTIONS = {
|
|
45301
|
+
deviceId: "the device that changed",
|
|
45302
|
+
from: "the value before the change, empty when it is not known",
|
|
45303
|
+
to: "the value it changed to"
|
|
45304
|
+
};
|
|
45305
|
+
/**
|
|
45306
|
+
* A fire's payload is offered too, as `data.<path>` (`{{vars.data.channel}}`):
|
|
45307
|
+
* those names depend on the device, so they are not in this list.
|
|
45308
|
+
*/
|
|
45309
|
+
var FIRED_DESCRIPTIONS = {
|
|
45310
|
+
deviceId: "the device that fired",
|
|
45311
|
+
eventType: "the event, as the device names it",
|
|
45312
|
+
sourceId: "the part of the device that fired (a button), empty when it names none",
|
|
45313
|
+
eventKind: "the kind of press (press, long-press …), empty when the device declares none"
|
|
45314
|
+
};
|
|
44799
45315
|
function describe$1(descriptions) {
|
|
44800
45316
|
return Object.entries(descriptions).map(([name, description]) => ({
|
|
44801
45317
|
name,
|
|
@@ -44804,8 +45320,74 @@ function describe$1(descriptions) {
|
|
|
44804
45320
|
}
|
|
44805
45321
|
describe$1(DETECTION_DESCRIPTIONS);
|
|
44806
45322
|
describe$1(STATIONARY_DESCRIPTIONS);
|
|
45323
|
+
describe$1(FIRED_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
|
|
45324
|
+
...v,
|
|
45325
|
+
hintKey: "firedDeviceId"
|
|
45326
|
+
} : v);
|
|
45327
|
+
describe$1(BECOMES_DESCRIPTIONS).map((v) => v.name === "deviceId" ? {
|
|
45328
|
+
...v,
|
|
45329
|
+
hintKey: "becomesDeviceId"
|
|
45330
|
+
} : v);
|
|
44807
45331
|
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
44808
45332
|
/**
|
|
45333
|
+
* Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
|
|
45334
|
+
* device. Carries the device's EXACT declared event vocabulary verbatim
|
|
45335
|
+
* (NO normalization). Two shapes:
|
|
45336
|
+
* - structured source (HA `event.*` entity) → `vocabulary` is the fixed
|
|
45337
|
+
* declared set (e.g. press_short, press_long, …).
|
|
45338
|
+
* - generic/legacy source (HA bus events, e.g. zha_event) → `vocabulary`
|
|
45339
|
+
* is [] (open) and `lastEvent.data` carries the complex payload.
|
|
45340
|
+
* `vocabulary` (D1044) names each event for a person and types it for a
|
|
45341
|
+
* rule; `eventTypes` is its list of native tokens, kept for older readers.
|
|
45342
|
+
* A fire names the part of the device it came from in `sourceId` when the
|
|
45343
|
+
* vocabulary declares sources (a remote's buttons).
|
|
45344
|
+
* `seq` is monotonic so two identical events still produce distinct slice writes.
|
|
45345
|
+
*/
|
|
45346
|
+
var EventFireSchema = object({
|
|
45347
|
+
deviceId: number$1(),
|
|
45348
|
+
eventType: string(),
|
|
45349
|
+
sourceId: string().optional(),
|
|
45350
|
+
data: record(string(), unknown()).nullable(),
|
|
45351
|
+
timestamp: number$1().meta(readingMeta("none")),
|
|
45352
|
+
seq: number$1().meta(readingMeta("count"))
|
|
45353
|
+
});
|
|
45354
|
+
var EventEmitterStatusSchema = object({
|
|
45355
|
+
eventTypes: array(string()),
|
|
45356
|
+
/**
|
|
45357
|
+
* Required of a provider by the type. A status written before the field
|
|
45358
|
+
* existed still parses, as an empty vocabulary: read it through
|
|
45359
|
+
* `readEventVocabulary`, which then answers with the plain tokens.
|
|
45360
|
+
*/
|
|
45361
|
+
vocabulary: array(EventVocabularyEntrySchema).default([]),
|
|
45362
|
+
lastEvent: EventFireSchema.nullable(),
|
|
45363
|
+
eventCountSinceStart: number$1().meta(readingMeta("count"))
|
|
45364
|
+
});
|
|
45365
|
+
var eventEmitterCapability = {
|
|
45366
|
+
name: "event-emitter",
|
|
45367
|
+
scope: "device",
|
|
45368
|
+
deviceNative: true,
|
|
45369
|
+
mode: "singleton",
|
|
45370
|
+
deviceTypes: [DeviceType.EventEmitter],
|
|
45371
|
+
methods: {},
|
|
45372
|
+
events: { onEvent: { data: EventFireSchema } },
|
|
45373
|
+
status: {
|
|
45374
|
+
schema: EventEmitterStatusSchema,
|
|
45375
|
+
kind: "push"
|
|
45376
|
+
},
|
|
45377
|
+
runtimeState: EventEmitterStatusSchema,
|
|
45378
|
+
/**
|
|
45379
|
+
* Runtime-state durability: **session** — `eventCountSinceStart` names its own scope.
|
|
45380
|
+
*
|
|
45381
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
45382
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
45383
|
+
*/
|
|
45384
|
+
durability: "session",
|
|
45385
|
+
/**
|
|
45386
|
+
* History (D850): none — `eventCountSinceStart` resets with the addon; events live in ledgers.
|
|
45387
|
+
*/
|
|
45388
|
+
history: "none"
|
|
45389
|
+
};
|
|
45390
|
+
/**
|
|
44809
45391
|
* Schedulers (D983) — an entity that performs actions over the day.
|
|
44810
45392
|
*
|
|
44811
45393
|
* A scheduler has profiles; a profile has weekdays and a timeline of POINTS.
|
|
@@ -44828,6 +45410,13 @@ var SchedulerStateTreeSchema = AutomationWhenSchema.superRefine((when, ctx) => {
|
|
|
44828
45410
|
});
|
|
44829
45411
|
return;
|
|
44830
45412
|
}
|
|
45413
|
+
if (hasTriggeredByLeaves(when)) {
|
|
45414
|
+
ctx.addIssue({
|
|
45415
|
+
code: "custom",
|
|
45416
|
+
message: TRIGGERED_BY_NEEDS_AUTOMATION
|
|
45417
|
+
});
|
|
45418
|
+
return;
|
|
45419
|
+
}
|
|
44831
45420
|
const problem = validateWhenTree(when);
|
|
44832
45421
|
if (problem !== null) ctx.addIssue({
|
|
44833
45422
|
code: "custom",
|
|
@@ -44849,12 +45438,16 @@ var HoldOverrideSchema = discriminatedUnion("mode", [object({ mode: literal("res
|
|
|
44849
45438
|
})]);
|
|
44850
45439
|
/** A missed edge up to this late still runs. */
|
|
44851
45440
|
var DEFAULT_MAX_LATE_MS = 10 * 6e4;
|
|
45441
|
+
var SchedulerStepSchema = ActionStepSchema.refine((step) => step.kind !== "wait" || step.seconds <= 300, {
|
|
45442
|
+
message: `a scheduler point waits at most five minutes (300 s); put a longer wait in an action`,
|
|
45443
|
+
path: ["seconds"]
|
|
45444
|
+
});
|
|
44852
45445
|
var SchedulerPointSchema = object({
|
|
44853
45446
|
id: string().min(1).max(64),
|
|
44854
45447
|
at: SchedulerTimeSpecSchema,
|
|
44855
45448
|
mode: SchedulerPointModeSchema,
|
|
44856
45449
|
/** The actions of an `edge` or `hold`; a `release` has none. */
|
|
44857
|
-
steps: array(
|
|
45450
|
+
steps: array(SchedulerStepSchema).max(20).default([]),
|
|
44858
45451
|
/** Checked when the point runs (an edge) or is applied (a hold). */
|
|
44859
45452
|
conditions: SchedulerStateTreeSchema.optional(),
|
|
44860
45453
|
/** Hold only; absent means `respect`. */
|
|
@@ -45033,7 +45626,9 @@ var AutomationTriggerTraceSchema = object({
|
|
|
45033
45626
|
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
45034
45627
|
reason: string(),
|
|
45035
45628
|
/** deviceId, className, trackId, score … — only what the trigger exposes. */
|
|
45036
|
-
vars: record(string(), TraceValueSchema)
|
|
45629
|
+
vars: record(string(), TraceValueSchema),
|
|
45630
|
+
/** A hold (`for`) this occurrence went through: pending, cancelled, fired or re-seeded. */
|
|
45631
|
+
hold: HoldTraceSchema.optional()
|
|
45037
45632
|
});
|
|
45038
45633
|
var AutomationConditionTraceNodeSchema = object({
|
|
45039
45634
|
/** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
|
|
@@ -45046,8 +45641,11 @@ var AutomationConditionTraceNodeSchema = object({
|
|
|
45046
45641
|
"device-is",
|
|
45047
45642
|
"expression",
|
|
45048
45643
|
"stationary-present",
|
|
45644
|
+
"time-is",
|
|
45645
|
+
"triggered-by",
|
|
45049
45646
|
"device-becomes",
|
|
45050
45647
|
"device-event",
|
|
45648
|
+
"device-fired",
|
|
45051
45649
|
"schedule",
|
|
45052
45650
|
"interval",
|
|
45053
45651
|
"manual",
|
|
@@ -45118,18 +45716,46 @@ var StepTraceBaseSchema = object({
|
|
|
45118
45716
|
]).optional(),
|
|
45119
45717
|
/** Why the condition read what it read, in the evaluator's own words. */
|
|
45120
45718
|
conditionReason: string().optional(),
|
|
45121
|
-
/** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. */
|
|
45719
|
+
/** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. `skipped`: its `if` was false (or unknown) and nothing ran in its place. `failed-continued`: it failed and was marked to be passed over (#500). */
|
|
45122
45720
|
outcome: _enum([
|
|
45123
45721
|
"ok",
|
|
45124
45722
|
"error",
|
|
45125
45723
|
"not-run",
|
|
45126
45724
|
"empty",
|
|
45127
|
-
"skipped"
|
|
45725
|
+
"skipped",
|
|
45726
|
+
"failed-continued",
|
|
45727
|
+
"timed-out"
|
|
45128
45728
|
]),
|
|
45129
45729
|
message: string().optional()
|
|
45130
45730
|
});
|
|
45731
|
+
/** What a block step adds to its row: which list ran, what each branch read, how many rounds. */
|
|
45732
|
+
var blockTraceShape = {
|
|
45733
|
+
/** A `choose` step (#500): the list that ran, `branches.<n>` or `else`. Absent when none did. */
|
|
45734
|
+
branch: string().optional(),
|
|
45735
|
+
/** A `choose` step: what each branch read, in order, up to the one that decided. */
|
|
45736
|
+
branchChecks: array(object({
|
|
45737
|
+
condition: _enum([
|
|
45738
|
+
"true",
|
|
45739
|
+
"false",
|
|
45740
|
+
"unknown"
|
|
45741
|
+
]),
|
|
45742
|
+
conditionReason: string().optional()
|
|
45743
|
+
})).optional(),
|
|
45744
|
+
/** A `repeat` step (#500): how many times its steps ran. */
|
|
45745
|
+
iterations: number$1().int().optional()
|
|
45746
|
+
};
|
|
45747
|
+
/** A step one level down: it may be a block step itself, whose own steps are plain rows (the nesting ends there). */
|
|
45748
|
+
var NestedStepTraceSchema = StepTraceBaseSchema.extend({
|
|
45749
|
+
...blockTraceShape,
|
|
45750
|
+
steps: array(StepTraceBaseSchema).optional()
|
|
45751
|
+
});
|
|
45131
45752
|
/** A step trace; the `else` steps that ran are one level of the same shape (the nesting is bounded at one). */
|
|
45132
|
-
var AutomationStepTraceSchema = StepTraceBaseSchema.extend({
|
|
45753
|
+
var AutomationStepTraceSchema = StepTraceBaseSchema.extend({
|
|
45754
|
+
else: array(StepTraceBaseSchema).optional(),
|
|
45755
|
+
...blockTraceShape,
|
|
45756
|
+
/** A `choose` step: the steps of the list that ran. A `parallel`: its members. A `repeat`: the LAST iteration. */
|
|
45757
|
+
steps: array(NestedStepTraceSchema).optional()
|
|
45758
|
+
});
|
|
45133
45759
|
var AutomationRunTraceSchema = object({
|
|
45134
45760
|
runId: string().min(1),
|
|
45135
45761
|
startedAt: number$1(),
|
|
@@ -45379,7 +46005,7 @@ object({
|
|
|
45379
46005
|
* is irrelevant; the array is treated as a set by consumers. */
|
|
45380
46006
|
hiddenChildIds: array(number$1()).readonly()
|
|
45381
46007
|
});
|
|
45382
|
-
DeviceType.Camera, DeviceType.
|
|
46008
|
+
DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Sensor, DeviceType.Climate, DeviceType.Cover, DeviceType.Lock, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Generic, method(object({
|
|
45383
46009
|
deviceId: number$1().int().nonnegative(),
|
|
45384
46010
|
childDeviceId: number$1().int().nonnegative(),
|
|
45385
46011
|
hidden: boolean()
|
|
@@ -45536,7 +46162,9 @@ var AddonHttpRouteSchema = object({
|
|
|
45536
46162
|
]).optional(),
|
|
45537
46163
|
description: string().optional(),
|
|
45538
46164
|
/** The hub limits this route per client address before the handler runs. */
|
|
45539
|
-
rateLimit: _enum(["auth"]).optional()
|
|
46165
|
+
rateLimit: _enum(["auth"]).optional(),
|
|
46166
|
+
/** Served once per integration instance; the handler reads `instanceId`. */
|
|
46167
|
+
perInstance: boolean().optional()
|
|
45540
46168
|
});
|
|
45541
46169
|
/**
|
|
45542
46170
|
* Cross-process route invocation envelope. The hub captures the
|
|
@@ -45553,6 +46181,8 @@ var InvokeRequestSchema = object({
|
|
|
45553
46181
|
query: record(string(), string()),
|
|
45554
46182
|
body: unknown(),
|
|
45555
46183
|
headers: record(string(), string()),
|
|
46184
|
+
/** The instance the hub resolved for a `perInstance` route. */
|
|
46185
|
+
instanceId: string().optional(),
|
|
45556
46186
|
user: object({
|
|
45557
46187
|
id: string(),
|
|
45558
46188
|
username: string(),
|
|
@@ -47677,7 +48307,7 @@ var BATTERY_DEVICE_TYPES = [
|
|
|
47677
48307
|
DeviceType.Switch,
|
|
47678
48308
|
DeviceType.PetFeeder,
|
|
47679
48309
|
DeviceType.Container,
|
|
47680
|
-
DeviceType.
|
|
48310
|
+
DeviceType.Climate,
|
|
47681
48311
|
DeviceType.EventEmitter
|
|
47682
48312
|
];
|
|
47683
48313
|
/** Generic Customize target for the percentage reading (D936). */
|
|
@@ -49440,7 +50070,14 @@ var climateControlCapability = {
|
|
|
49440
50070
|
scope: "device",
|
|
49441
50071
|
deviceNative: true,
|
|
49442
50072
|
mode: "singleton",
|
|
49443
|
-
deviceTypes: [DeviceType.
|
|
50073
|
+
deviceTypes: [DeviceType.Climate],
|
|
50074
|
+
power: {
|
|
50075
|
+
kind: "mode",
|
|
50076
|
+
field: "mode",
|
|
50077
|
+
offValue: "off",
|
|
50078
|
+
set: "setMode",
|
|
50079
|
+
lastField: "lastActiveMode"
|
|
50080
|
+
},
|
|
49444
50081
|
methods: {
|
|
49445
50082
|
setMode: method(object({
|
|
49446
50083
|
deviceId: number$1().int().nonnegative(),
|
|
@@ -49550,6 +50187,140 @@ var climateControlCapability = {
|
|
|
49550
50187
|
} }
|
|
49551
50188
|
};
|
|
49552
50189
|
/**
|
|
50190
|
+
* cluster-update — the ONE flow that brings the whole cluster to a release
|
|
50191
|
+
* (planning#493). Hub-provided (`server-provided`), hub-only: the hub runs
|
|
50192
|
+
* the sequence itself, so the admin UI, the MCP and the CLI all start the same
|
|
50193
|
+
* job and read the same report instead of each driving their own loop.
|
|
50194
|
+
*
|
|
50195
|
+
* 1. backup (`backup.trigger`) — skipped only when the caller says so
|
|
50196
|
+
* 2. the hub's server closure (`applyServerUpdate`); the hub restarts and the
|
|
50197
|
+
* job RESUMES on the new boot, verified against the hub's running version
|
|
50198
|
+
* 3. every online agent behind the target, one at a time, verified: runs the
|
|
50199
|
+
* target, settled, online, and its capabilities registered (D848) — an
|
|
50200
|
+
* agent that comes back capless is a FAILED step, never a done one
|
|
50201
|
+
* 4. every stale addon on the hub (`addons.updatePackage`); agents follow by
|
|
50202
|
+
* reconciliation, which is verified, never pushed
|
|
50203
|
+
*
|
|
50204
|
+
* The job stops on the first failed step with a typed reason (D877). Its state
|
|
50205
|
+
* is durable on the hub and read by RPC (`getClusterUpdateJob`) — events are
|
|
50206
|
+
* telemetry and never carry it (D8, D11).
|
|
50207
|
+
*/
|
|
50208
|
+
/** The steps, in the order they run. */
|
|
50209
|
+
var ClusterUpdateStepKindSchema = _enum([
|
|
50210
|
+
"backup",
|
|
50211
|
+
"hub-server",
|
|
50212
|
+
"agent-server",
|
|
50213
|
+
"addons"
|
|
50214
|
+
]);
|
|
50215
|
+
/**
|
|
50216
|
+
* - `pending` — not reached yet.
|
|
50217
|
+
* - `running` — in progress (for `hub-server`: possibly across the restart).
|
|
50218
|
+
* - `ok` — done AND verified.
|
|
50219
|
+
* - `failed` — did not land; `reason` + `detail` say why. Stops the job.
|
|
50220
|
+
* - `skipped` — not run: nothing to do, opted out, the node is offline, or a
|
|
50221
|
+
* step before it failed. `detail` says which.
|
|
50222
|
+
*/
|
|
50223
|
+
var ClusterUpdateStepStatusSchema = _enum([
|
|
50224
|
+
"pending",
|
|
50225
|
+
"running",
|
|
50226
|
+
"ok",
|
|
50227
|
+
"failed",
|
|
50228
|
+
"skipped"
|
|
50229
|
+
]);
|
|
50230
|
+
/**
|
|
50231
|
+
* Why a step (and with it the job) failed. Stable, greppable; `detail` carries
|
|
50232
|
+
* the prose.
|
|
50233
|
+
*/
|
|
50234
|
+
var ClusterUpdateFailureReasonSchema = _enum([
|
|
50235
|
+
"backup-unavailable",
|
|
50236
|
+
"backup-failed",
|
|
50237
|
+
"server-refused",
|
|
50238
|
+
"server-not-on-target",
|
|
50239
|
+
"node-capless",
|
|
50240
|
+
"status-unreadable",
|
|
50241
|
+
"addon-update-failed",
|
|
50242
|
+
"addons-not-converged",
|
|
50243
|
+
"interrupted"
|
|
50244
|
+
]);
|
|
50245
|
+
var ClusterUpdateStepSchema = object({
|
|
50246
|
+
kind: ClusterUpdateStepKindSchema,
|
|
50247
|
+
/** The node the step acts on; absent for `backup` and `addons`. */
|
|
50248
|
+
nodeId: string().optional(),
|
|
50249
|
+
status: ClusterUpdateStepStatusSchema,
|
|
50250
|
+
/** One line for the operator: what happened, or why not. */
|
|
50251
|
+
detail: string(),
|
|
50252
|
+
reason: ClusterUpdateFailureReasonSchema.optional(),
|
|
50253
|
+
/** `hub-server` / `agent-server`: the version the node ran before. */
|
|
50254
|
+
fromVersion: string().nullable().optional(),
|
|
50255
|
+
/** `addons`: the packages this step updates, `name from -> to`. */
|
|
50256
|
+
packages: array(string()).readonly().optional(),
|
|
50257
|
+
startedAt: number$1().optional(),
|
|
50258
|
+
finishedAt: number$1().optional()
|
|
50259
|
+
});
|
|
50260
|
+
var ClusterUpdateJobStateSchema = _enum([
|
|
50261
|
+
"running",
|
|
50262
|
+
"succeeded",
|
|
50263
|
+
"failed"
|
|
50264
|
+
]);
|
|
50265
|
+
var ClusterUpdateJobSchema = object({
|
|
50266
|
+
id: string(),
|
|
50267
|
+
state: ClusterUpdateJobStateSchema,
|
|
50268
|
+
/** The server version every node is brought to. */
|
|
50269
|
+
targetVersion: string(),
|
|
50270
|
+
/** Who started it (the user name, or `system`). */
|
|
50271
|
+
requestedBy: string(),
|
|
50272
|
+
startedAt: number$1(),
|
|
50273
|
+
finishedAt: number$1().optional(),
|
|
50274
|
+
/** Every step, in run order; later steps stay `pending` while it runs. */
|
|
50275
|
+
steps: array(ClusterUpdateStepSchema).readonly(),
|
|
50276
|
+
/** On `failed`: the step that stopped it (`nodeId` or the step kind). */
|
|
50277
|
+
stoppedAt: string().optional(),
|
|
50278
|
+
reason: ClusterUpdateFailureReasonSchema.optional(),
|
|
50279
|
+
/** Agents offline when the plan was read: never touched. */
|
|
50280
|
+
skippedOffline: array(string()).readonly()
|
|
50281
|
+
});
|
|
50282
|
+
var ClusterUpdateOptionsSchema = object({
|
|
50283
|
+
/** Explicit server target; omitted = the hub's latest registry check. */
|
|
50284
|
+
version: string().optional(),
|
|
50285
|
+
/** Update the addons too (default true). */
|
|
50286
|
+
addons: boolean().optional(),
|
|
50287
|
+
/**
|
|
50288
|
+
* Back up before touching anything (default true). `false` is an explicit
|
|
50289
|
+
* operator opt-out; a missing backup provider is otherwise a failure.
|
|
50290
|
+
*/
|
|
50291
|
+
backup: boolean().optional()
|
|
50292
|
+
});
|
|
50293
|
+
/** Why a plan cannot start. */
|
|
50294
|
+
var ClusterUpdateBlockerSchema = _enum([
|
|
50295
|
+
"no-target",
|
|
50296
|
+
"node-busy",
|
|
50297
|
+
"job-running",
|
|
50298
|
+
"nothing-to-do",
|
|
50299
|
+
"status-unreadable"
|
|
50300
|
+
]);
|
|
50301
|
+
var ClusterUpdatePlanSchema = object({
|
|
50302
|
+
/** `null` when no target could be resolved (`blocker: 'no-target'`). */
|
|
50303
|
+
targetVersion: string().nullable(),
|
|
50304
|
+
/** The steps the job WOULD run, all `pending` (or `skipped` with a reason). */
|
|
50305
|
+
steps: array(ClusterUpdateStepSchema).readonly(),
|
|
50306
|
+
skippedOffline: array(string()).readonly(),
|
|
50307
|
+
/** `null` = the plan can start. */
|
|
50308
|
+
blocker: ClusterUpdateBlockerSchema.nullable(),
|
|
50309
|
+
/** Prose for `blocker`. */
|
|
50310
|
+
blockerDetail: string().nullable()
|
|
50311
|
+
});
|
|
50312
|
+
var StartClusterUpdateResultSchema = object({
|
|
50313
|
+
accepted: boolean(),
|
|
50314
|
+
/** The job to follow with `getClusterUpdateJob`; `null` when refused. */
|
|
50315
|
+
jobId: string().nullable(),
|
|
50316
|
+
blocker: ClusterUpdateBlockerSchema.nullable(),
|
|
50317
|
+
message: string()
|
|
50318
|
+
});
|
|
50319
|
+
method(ClusterUpdateOptionsSchema, ClusterUpdatePlanSchema, { auth: "admin" }), method(ClusterUpdateOptionsSchema, StartClusterUpdateResultSchema, {
|
|
50320
|
+
kind: "mutation",
|
|
50321
|
+
auth: "admin"
|
|
50322
|
+
}), method(object({ jobId: string().optional() }), ClusterUpdateJobSchema.nullable(), { auth: "admin" });
|
|
50323
|
+
/**
|
|
49553
50324
|
* Color-light cap. Coexists with `switch` (on/off) and `brightness`
|
|
49554
50325
|
* (level) on the same device — `switch` toggles the bulb, `brightness`
|
|
49555
50326
|
* sets the dim level, this cap sets the chromaticity.
|
|
@@ -49691,6 +50462,7 @@ var CompositionProblemSchema = object({
|
|
|
49691
50462
|
"nested-field",
|
|
49692
50463
|
"missing-field",
|
|
49693
50464
|
"kind-mismatch",
|
|
50465
|
+
"source-kind-mismatch",
|
|
49694
50466
|
"may-be-null",
|
|
49695
50467
|
"source-missing",
|
|
49696
50468
|
"source-unreadable",
|
|
@@ -49712,7 +50484,8 @@ var CompositionProblemSchema = object({
|
|
|
49712
50484
|
"command-method-unknown",
|
|
49713
50485
|
"command-source-unknown",
|
|
49714
50486
|
"command-input-unknown",
|
|
49715
|
-
"command-input-missing"
|
|
50487
|
+
"command-input-missing",
|
|
50488
|
+
"power-source-is-actuator"
|
|
49716
50489
|
]),
|
|
49717
50490
|
path: string(),
|
|
49718
50491
|
message: string()
|
|
@@ -50750,6 +51523,13 @@ var mediaPlayerCapability = {
|
|
|
50750
51523
|
deviceNative: true,
|
|
50751
51524
|
mode: "singleton",
|
|
50752
51525
|
deviceTypes: [DeviceType.MediaPlayer],
|
|
51526
|
+
power: {
|
|
51527
|
+
kind: "methods",
|
|
51528
|
+
field: "state",
|
|
51529
|
+
offValues: ["off", "standby"],
|
|
51530
|
+
on: "turnOn",
|
|
51531
|
+
off: "turnOff"
|
|
51532
|
+
},
|
|
50753
51533
|
methods: {
|
|
50754
51534
|
turnOn: optionalMethod(deviceOnly, _void(), {
|
|
50755
51535
|
kind: "mutation",
|
|
@@ -50880,8 +51660,8 @@ var mediaPlayerCapability = {
|
|
|
50880
51660
|
isMuted: { kind: "boolean" }
|
|
50881
51661
|
} }
|
|
50882
51662
|
};
|
|
50883
|
-
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.
|
|
50884
|
-
var NUMBER = {
|
|
51663
|
+
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Climate, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, DeviceType.Camera, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, 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.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.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.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Gateway, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.PetFeeder, DeviceType.MediaPlayer;
|
|
51664
|
+
var NUMBER$1 = {
|
|
50885
51665
|
kind: "fixed",
|
|
50886
51666
|
result: ["number"]
|
|
50887
51667
|
};
|
|
@@ -50890,19 +51670,19 @@ var BOOLEAN = {
|
|
|
50890
51670
|
result: ["boolean"]
|
|
50891
51671
|
};
|
|
50892
51672
|
Object.freeze({
|
|
50893
|
-
min: NUMBER,
|
|
50894
|
-
max: NUMBER,
|
|
50895
|
-
abs: NUMBER,
|
|
50896
|
-
floor: NUMBER,
|
|
50897
|
-
ceil: NUMBER,
|
|
50898
|
-
sqrt: NUMBER,
|
|
50899
|
-
round: NUMBER,
|
|
50900
|
-
pow: NUMBER,
|
|
50901
|
-
clamp: NUMBER,
|
|
50902
|
-
avg: NUMBER,
|
|
50903
|
-
sum: NUMBER,
|
|
50904
|
-
age: NUMBER,
|
|
50905
|
-
convert: NUMBER,
|
|
51673
|
+
min: NUMBER$1,
|
|
51674
|
+
max: NUMBER$1,
|
|
51675
|
+
abs: NUMBER$1,
|
|
51676
|
+
floor: NUMBER$1,
|
|
51677
|
+
ceil: NUMBER$1,
|
|
51678
|
+
sqrt: NUMBER$1,
|
|
51679
|
+
round: NUMBER$1,
|
|
51680
|
+
pow: NUMBER$1,
|
|
51681
|
+
clamp: NUMBER$1,
|
|
51682
|
+
avg: NUMBER$1,
|
|
51683
|
+
sum: NUMBER$1,
|
|
51684
|
+
age: NUMBER$1,
|
|
51685
|
+
convert: NUMBER$1,
|
|
50906
51686
|
coalesce: { kind: "coalesce" },
|
|
50907
51687
|
any: BOOLEAN,
|
|
50908
51688
|
all: BOOLEAN,
|
|
@@ -50919,7 +51699,7 @@ Object.freeze({
|
|
|
50919
51699
|
kind: "fixed",
|
|
50920
51700
|
result: ["number", "null"]
|
|
50921
51701
|
},
|
|
50922
|
-
count: NUMBER,
|
|
51702
|
+
count: NUMBER$1,
|
|
50923
51703
|
dwell: {
|
|
50924
51704
|
kind: "fixed",
|
|
50925
51705
|
result: ["boolean", "null"]
|
|
@@ -50945,7 +51725,7 @@ var CHANGED = {
|
|
|
50945
51725
|
fieldPath: "lastChangedAt",
|
|
50946
51726
|
expr: "now"
|
|
50947
51727
|
};
|
|
50948
|
-
function numeric(input) {
|
|
51728
|
+
function numeric$1(input) {
|
|
50949
51729
|
const { alsoFrom, ...base } = input;
|
|
50950
51730
|
return {
|
|
50951
51731
|
...base,
|
|
@@ -50963,7 +51743,7 @@ function numeric(input) {
|
|
|
50963
51743
|
}, ...alsoFrom === void 0 ? [] : [alsoFrom.accept]]
|
|
50964
51744
|
};
|
|
50965
51745
|
}
|
|
50966
|
-
var CLIMATE_DEVICES = [DeviceType.
|
|
51746
|
+
var CLIMATE_DEVICES = [DeviceType.Climate];
|
|
50967
51747
|
function binary(input) {
|
|
50968
51748
|
return {
|
|
50969
51749
|
...input,
|
|
@@ -50980,7 +51760,7 @@ function binary(input) {
|
|
|
50980
51760
|
};
|
|
50981
51761
|
}
|
|
50982
51762
|
var GROUP_TEMPLATES = [
|
|
50983
|
-
numeric({
|
|
51763
|
+
numeric$1({
|
|
50984
51764
|
id: "temperature",
|
|
50985
51765
|
summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
|
|
50986
51766
|
role: DeviceRole.TemperatureSensor,
|
|
@@ -50998,7 +51778,7 @@ var GROUP_TEMPLATES = [
|
|
|
50998
51778
|
}
|
|
50999
51779
|
}
|
|
51000
51780
|
}),
|
|
51001
|
-
numeric({
|
|
51781
|
+
numeric$1({
|
|
51002
51782
|
id: "humidity",
|
|
51003
51783
|
summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
51004
51784
|
role: DeviceRole.HumiditySensor,
|
|
@@ -51016,7 +51796,7 @@ var GROUP_TEMPLATES = [
|
|
|
51016
51796
|
}
|
|
51017
51797
|
}
|
|
51018
51798
|
}),
|
|
51019
|
-
numeric({
|
|
51799
|
+
numeric$1({
|
|
51020
51800
|
id: "pressure",
|
|
51021
51801
|
summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
|
|
51022
51802
|
role: DeviceRole.PressureSensor,
|
|
@@ -51027,7 +51807,7 @@ var GROUP_TEMPLATES = [
|
|
|
51027
51807
|
empty: "null",
|
|
51028
51808
|
companions: [NOW$3]
|
|
51029
51809
|
}),
|
|
51030
|
-
numeric({
|
|
51810
|
+
numeric$1({
|
|
51031
51811
|
id: "illuminance",
|
|
51032
51812
|
summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
|
|
51033
51813
|
role: DeviceRole.AmbientLightSensor,
|
|
@@ -51038,7 +51818,7 @@ var GROUP_TEMPLATES = [
|
|
|
51038
51818
|
empty: "null",
|
|
51039
51819
|
companions: [NOW$3]
|
|
51040
51820
|
}),
|
|
51041
|
-
numeric({
|
|
51821
|
+
numeric$1({
|
|
51042
51822
|
id: "power",
|
|
51043
51823
|
summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
|
|
51044
51824
|
role: DeviceRole.PowerSensor,
|
|
@@ -51049,7 +51829,7 @@ var GROUP_TEMPLATES = [
|
|
|
51049
51829
|
empty: "omit",
|
|
51050
51830
|
companions: [NOW$3]
|
|
51051
51831
|
}),
|
|
51052
|
-
numeric({
|
|
51832
|
+
numeric$1({
|
|
51053
51833
|
id: "energy",
|
|
51054
51834
|
summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
|
|
51055
51835
|
role: DeviceRole.EnergySensor,
|
|
@@ -51060,7 +51840,7 @@ var GROUP_TEMPLATES = [
|
|
|
51060
51840
|
empty: "omit",
|
|
51061
51841
|
companions: [NOW$3]
|
|
51062
51842
|
}),
|
|
51063
|
-
numeric({
|
|
51843
|
+
numeric$1({
|
|
51064
51844
|
id: "battery",
|
|
51065
51845
|
summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
51066
51846
|
role: DeviceRole.BatterySensor,
|
|
@@ -51084,7 +51864,7 @@ var GROUP_TEMPLATES = [
|
|
|
51084
51864
|
}
|
|
51085
51865
|
]
|
|
51086
51866
|
}),
|
|
51087
|
-
numeric({
|
|
51867
|
+
numeric$1({
|
|
51088
51868
|
id: "numeric",
|
|
51089
51869
|
summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
|
|
51090
51870
|
role: DeviceRole.NumericSensor,
|
|
@@ -51285,8 +52065,8 @@ var GROUP_TEMPLATES = [
|
|
|
51285
52065
|
actuator({
|
|
51286
52066
|
id: "climate",
|
|
51287
52067
|
summary: "Running when at least one thermostat or air conditioner is not off, or only when all of them are. Target temperature and fan are offered only when every device has them. A device that reports nothing is left out, never read as off.",
|
|
51288
|
-
deviceType: DeviceType.
|
|
51289
|
-
memberDeviceTypes: [DeviceType.
|
|
52068
|
+
deviceType: DeviceType.Climate,
|
|
52069
|
+
memberDeviceTypes: [DeviceType.Climate],
|
|
51290
52070
|
outputCap: "group-active",
|
|
51291
52071
|
outputField: "active",
|
|
51292
52072
|
member: {
|
|
@@ -51329,6 +52109,118 @@ function actuator(input) {
|
|
|
51329
52109
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
51330
52110
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
51331
52111
|
DeviceType.Camera;
|
|
52112
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
52113
|
+
"no-power-source",
|
|
52114
|
+
"switch-missing",
|
|
52115
|
+
"switch-offline",
|
|
52116
|
+
"switch-unknown",
|
|
52117
|
+
"restart-in-progress",
|
|
52118
|
+
"switch-call-failed",
|
|
52119
|
+
"switch-is-actuator"
|
|
52120
|
+
]);
|
|
52121
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
52122
|
+
accepted: literal(false),
|
|
52123
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
52124
|
+
/** One line for the operator, naming the switch. */
|
|
52125
|
+
message: string()
|
|
52126
|
+
})]);
|
|
52127
|
+
var PowerSourceInfoSchema = object({
|
|
52128
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
52129
|
+
linked: boolean(),
|
|
52130
|
+
switchDeviceId: number$1().nullable(),
|
|
52131
|
+
switchName: string().nullable(),
|
|
52132
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
52133
|
+
switchOn: boolean().nullable(),
|
|
52134
|
+
switchOnline: boolean().nullable(),
|
|
52135
|
+
restartInProgress: boolean(),
|
|
52136
|
+
offSeconds: number$1().int()
|
|
52137
|
+
});
|
|
52138
|
+
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
52139
|
+
var PowerSourceStateSchema = object({
|
|
52140
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
52141
|
+
switchOn: boolean().nullable(),
|
|
52142
|
+
offSeconds: number$1().int(),
|
|
52143
|
+
lastFetchedAt: number$1()
|
|
52144
|
+
});
|
|
52145
|
+
var POWER_SOURCE_DEVICE_TYPES = [
|
|
52146
|
+
DeviceType.Gateway,
|
|
52147
|
+
DeviceType.Camera,
|
|
52148
|
+
DeviceType.Sensor,
|
|
52149
|
+
DeviceType.MediaPlayer,
|
|
52150
|
+
DeviceType.Vacuum,
|
|
52151
|
+
DeviceType.Generic,
|
|
52152
|
+
DeviceType.Washer,
|
|
52153
|
+
DeviceType.Dryer,
|
|
52154
|
+
DeviceType.Dishwasher,
|
|
52155
|
+
DeviceType.PetFountain,
|
|
52156
|
+
DeviceType.Fan,
|
|
52157
|
+
DeviceType.Humidifier,
|
|
52158
|
+
DeviceType.Climate,
|
|
52159
|
+
DeviceType.WaterHeater
|
|
52160
|
+
];
|
|
52161
|
+
var powerSourceCustomize = {
|
|
52162
|
+
version: 1,
|
|
52163
|
+
targets: [{
|
|
52164
|
+
id: "switch",
|
|
52165
|
+
cardinality: "scalar",
|
|
52166
|
+
vocabulary: {
|
|
52167
|
+
cap: "power-source",
|
|
52168
|
+
stateField: "switchOn"
|
|
52169
|
+
},
|
|
52170
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
52171
|
+
apply: "add",
|
|
52172
|
+
path: "switchOn",
|
|
52173
|
+
sources: [{
|
|
52174
|
+
kind: "switch",
|
|
52175
|
+
transforms: [{ kind: "identity" }]
|
|
52176
|
+
}],
|
|
52177
|
+
companions: [{
|
|
52178
|
+
kind: "constant",
|
|
52179
|
+
path: "offSeconds",
|
|
52180
|
+
value: 10,
|
|
52181
|
+
when: "add",
|
|
52182
|
+
formerValues: [0]
|
|
52183
|
+
}, {
|
|
52184
|
+
kind: "clock",
|
|
52185
|
+
path: "lastFetchedAt",
|
|
52186
|
+
when: "always"
|
|
52187
|
+
}],
|
|
52188
|
+
sourceReport: { status: "assignment-status" }
|
|
52189
|
+
}]
|
|
52190
|
+
};
|
|
52191
|
+
var deviceInput$2 = object({ deviceId: number$1().int().nonnegative() });
|
|
52192
|
+
var powerSourceCapability = {
|
|
52193
|
+
name: "power-source",
|
|
52194
|
+
scope: "device",
|
|
52195
|
+
mode: "singleton",
|
|
52196
|
+
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
52197
|
+
methods: {
|
|
52198
|
+
getStatus: systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "protected" }),
|
|
52199
|
+
powerOn: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
52200
|
+
kind: "mutation",
|
|
52201
|
+
auth: "protected"
|
|
52202
|
+
}),
|
|
52203
|
+
powerOff: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
52204
|
+
kind: "mutation",
|
|
52205
|
+
auth: "protected"
|
|
52206
|
+
}),
|
|
52207
|
+
forceRestart: systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
52208
|
+
kind: "mutation",
|
|
52209
|
+
auth: "protected"
|
|
52210
|
+
})
|
|
52211
|
+
},
|
|
52212
|
+
runtimeState: PowerSourceStateSchema,
|
|
52213
|
+
customize: powerSourceCustomize,
|
|
52214
|
+
/**
|
|
52215
|
+
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
52216
|
+
* its next report; a restored value would outlive the reading it names.
|
|
52217
|
+
*
|
|
52218
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
52219
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
52220
|
+
*/
|
|
52221
|
+
durability: "session",
|
|
52222
|
+
history: "none"
|
|
52223
|
+
};
|
|
51332
52224
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
51333
52225
|
/**
|
|
51334
52226
|
* What a composition may BUILD on a device type (G2, D676): the caps a new
|
|
@@ -52373,27 +53265,35 @@ var deviceAdminLinkCapability = {
|
|
|
52373
53265
|
getAdminLink: method(object({ deviceId: number$1().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" }) }
|
|
52374
53266
|
};
|
|
52375
53267
|
/**
|
|
52376
|
-
* device-
|
|
52377
|
-
*
|
|
52378
|
-
*
|
|
52379
|
-
*
|
|
52380
|
-
*
|
|
52381
|
-
*
|
|
52382
|
-
*
|
|
52383
|
-
*
|
|
52384
|
-
*
|
|
52385
|
-
*
|
|
52386
|
-
*
|
|
52387
|
-
*
|
|
52388
|
-
*
|
|
52389
|
-
*
|
|
52390
|
-
*
|
|
52391
|
-
*
|
|
52392
|
-
*
|
|
52393
|
-
|
|
52394
|
-
|
|
52395
|
-
*
|
|
52396
|
-
*
|
|
53268
|
+
* `device-adoption` — generic discovery + adoption surface,
|
|
53269
|
+
* system-scoped collection (one provider per integration addon), routed
|
|
53270
|
+
* by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
|
|
53271
|
+
* pin model as `connection-test`. The admin UI keeps using
|
|
53272
|
+
* `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
|
|
53273
|
+
* successor to `ha-discovery`:
|
|
53274
|
+
* keyed by `integrationId` (the addon resolves integrationId → broker
|
|
53275
|
+
* internally) rather than a connection-specific `brokerId`, and it
|
|
53276
|
+
* OWNS the shared `DiscoveredChildDevice` candidate type
|
|
53277
|
+
* (declared below) so a single shared adoption modal renders any integration's
|
|
53278
|
+
* candidate tree.
|
|
53279
|
+
*
|
|
53280
|
+
* UI flow (Integrations page → integration detail):
|
|
53281
|
+
* 1. `listCandidates({integrationId, page, pageSize, filter})` →
|
|
53282
|
+
* paginated candidate list, each row collapsed.
|
|
53283
|
+
* 2. User expands a row → `getCandidate({integrationId,
|
|
53284
|
+
* childNativeId})` returns the single candidate (with its nested
|
|
53285
|
+
* `children`) for the accordion render.
|
|
53286
|
+
* 3. User clicks "Adopt" on one or more candidates →
|
|
53287
|
+
* `adopt({integrationId, childNativeIds, perCandidate?})`
|
|
53288
|
+
* materialises one parent CamStack device per candidate plus its
|
|
53289
|
+
* accessory children. `perCandidate[childNativeId]` carries an
|
|
53290
|
+
* optional display-name override + a `hiddenChildIds` pre-hide set
|
|
53291
|
+
* (the parent's `accessories.hiddenChildIds` gates UI visibility).
|
|
53292
|
+
* 4. User clicks "Release" → `release({integrationId, camDeviceId})`
|
|
53293
|
+
* removes the parent + every child from the kernel registry.
|
|
53294
|
+
*/
|
|
53295
|
+
/**
|
|
53296
|
+
* Status enum for a discovered device. Generic enough for
|
|
52397
53297
|
* battery cams, awake cams, and unreachable cams alike.
|
|
52398
53298
|
*/
|
|
52399
53299
|
var DiscoveredChildStatusSchema = _enum([
|
|
@@ -52410,12 +53310,12 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
52410
53310
|
/** DeviceType the child should be created with on adopt. */
|
|
52411
53311
|
type: _enum(DeviceType),
|
|
52412
53312
|
status: DiscoveredChildStatusSchema,
|
|
52413
|
-
/** Free-form integration-specific metadata surfaced in the
|
|
53313
|
+
/** Free-form integration-specific metadata surfaced in the adoption modal. */
|
|
52414
53314
|
metadata: object({
|
|
52415
53315
|
model: string().optional(),
|
|
52416
53316
|
serialNumber: string().optional(),
|
|
52417
53317
|
uid: string().optional(),
|
|
52418
|
-
/** Reolink: 0-based channel index inside the parent NVR
|
|
53318
|
+
/** Reolink: 0-based channel index inside the parent NVR. */
|
|
52419
53319
|
rtspChannel: number$1().int().nonnegative().optional(),
|
|
52420
53320
|
isBattery: boolean().optional(),
|
|
52421
53321
|
isDoorbell: boolean().optional(),
|
|
@@ -52431,137 +53331,24 @@ var DiscoveredChildDeviceSchema = lazy(() => object({
|
|
|
52431
53331
|
}).default({}),
|
|
52432
53332
|
/**
|
|
52433
53333
|
* `true` when the framework already created a child device for this
|
|
52434
|
-
* `childNativeId` under the current parent. The
|
|
53334
|
+
* `childNativeId` under the current parent. The adoption modal uses it to
|
|
52435
53335
|
* gate the Add/Remove button and surface the existing `deviceId`.
|
|
52436
53336
|
*/
|
|
52437
53337
|
alreadyAdopted: boolean(),
|
|
52438
53338
|
/** When `alreadyAdopted=true`, the framework-assigned child device id. */
|
|
52439
53339
|
adoptedDeviceId: number$1().int().nonnegative().nullable(),
|
|
52440
|
-
/** Capability names the candidate advertises — integrations populate,
|
|
53340
|
+
/** Capability names the candidate advertises — integrations populate, flat enumerations omit. */
|
|
52441
53341
|
capabilities: array(string()).readonly().optional(),
|
|
52442
|
-
/** Feature flags the candidate advertises — integrations populate,
|
|
53342
|
+
/** Feature flags the candidate advertises — integrations populate, flat enumerations omit. */
|
|
52443
53343
|
features: array(string()).readonly().optional(),
|
|
52444
53344
|
/** Upstream-system identity + rendering hints for the candidate. */
|
|
52445
53345
|
sourceInfo: SourceInfoSchema.optional(),
|
|
52446
|
-
/** Nested entity-children for accordion preview — integrations populate,
|
|
53346
|
+
/** Nested entity-children for accordion preview — integrations populate, flat enumerations omit. */
|
|
52447
53347
|
children: array(DiscoveredChildDeviceSchema).readonly().optional(),
|
|
52448
53348
|
/** `childNativeId` of the gateway candidate in the same list this device sits behind. */
|
|
52449
53349
|
parentStableId: string().optional()
|
|
52450
53350
|
}));
|
|
52451
53351
|
/**
|
|
52452
|
-
* Status block — populated by the integration after each
|
|
52453
|
-
* `refreshDiscovery` (or push-driven update). The runtime-state slice
|
|
52454
|
-
* is what the panel reads via the per-device proxy; `getStatus`
|
|
52455
|
-
* returns the same payload via the codegen'd auto-injection.
|
|
52456
|
-
*/
|
|
52457
|
-
var DeviceDiscoveryStatusSchema = object({
|
|
52458
|
-
discovered: array(DiscoveredChildDeviceSchema),
|
|
52459
|
-
/** Wall-clock ms of the last successful enumeration. */
|
|
52460
|
-
lastDiscoveryAt: number$1().int().nonnegative().nullable(),
|
|
52461
|
-
/** Last error surfaced from the source (rendered as a banner). */
|
|
52462
|
-
lastError: string().nullable()
|
|
52463
|
-
});
|
|
52464
|
-
var deviceDiscoveryCapability = {
|
|
52465
|
-
name: "device-discovery",
|
|
52466
|
-
scope: "device",
|
|
52467
|
-
deviceNative: true,
|
|
52468
|
-
mode: "singleton",
|
|
52469
|
-
deviceTypes: [DeviceType.Hub],
|
|
52470
|
-
status: {
|
|
52471
|
-
schema: DeviceDiscoveryStatusSchema,
|
|
52472
|
-
kind: "poll"
|
|
52473
|
-
},
|
|
52474
|
-
runtimeState: DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number$1().int().nonnegative() }),
|
|
52475
|
-
/**
|
|
52476
|
-
* Runtime-state durability: **session** — 5.7 KB of scan output on the largest device, fully re-derivable by re-scanning.
|
|
52477
|
-
*
|
|
52478
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
52479
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
52480
|
-
*/
|
|
52481
|
-
durability: "session",
|
|
52482
|
-
/**
|
|
52483
|
-
* History (D850): none — inventory.
|
|
52484
|
-
*/
|
|
52485
|
-
history: "none",
|
|
52486
|
-
methods: {
|
|
52487
|
-
/**
|
|
52488
|
-
* Snapshot of the current `discovered` list. Returns the
|
|
52489
|
-
* runtime-state cache — call `refreshDiscovery` first if a
|
|
52490
|
-
* fresh round-trip to the source is required.
|
|
52491
|
-
*/
|
|
52492
|
-
listDiscovered: method(object({ deviceId: number$1().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly()),
|
|
52493
|
-
/**
|
|
52494
|
-
* Force the integration to re-enumerate and update the
|
|
52495
|
-
* runtime-state slice. Returns the freshly-enumerated list (also
|
|
52496
|
-
* available via `listDiscovered` post-call).
|
|
52497
|
-
*/
|
|
52498
|
-
refreshDiscovery: method(object({ deviceId: number$1().int().nonnegative() }), array(DiscoveredChildDeviceSchema).readonly(), {
|
|
52499
|
-
kind: "mutation",
|
|
52500
|
-
auth: "admin"
|
|
52501
|
-
}),
|
|
52502
|
-
/**
|
|
52503
|
-
* Promote a discovered entry to a real child device. The framework
|
|
52504
|
-
* creates the child via `kernel.devices.create()` with
|
|
52505
|
-
* `parentDeviceId = parent.id` and seeds the child's config from
|
|
52506
|
-
* `childInitialConfig` (driver-defined; usually carries channel +
|
|
52507
|
-
* uid + parent reference). Returns the kernel-assigned numeric id.
|
|
52508
|
-
*/
|
|
52509
|
-
adoptDevice: method(object({
|
|
52510
|
-
deviceId: number$1().int().nonnegative(),
|
|
52511
|
-
childNativeId: string(),
|
|
52512
|
-
/** Optional override for the child's display name. */
|
|
52513
|
-
name: string().optional()
|
|
52514
|
-
}), object({
|
|
52515
|
-
deviceId: number$1().int().nonnegative(),
|
|
52516
|
-
stableId: string()
|
|
52517
|
-
}), {
|
|
52518
|
-
kind: "mutation",
|
|
52519
|
-
auth: "admin"
|
|
52520
|
-
}),
|
|
52521
|
-
/**
|
|
52522
|
-
* Inverse of `adoptDevice`: removes the child device from the
|
|
52523
|
-
* kernel registry. The discovered entry remains in the
|
|
52524
|
-
* enumeration (status updates resume) so the operator can re-adopt
|
|
52525
|
-
* it later without a fresh refresh.
|
|
52526
|
-
*/
|
|
52527
|
-
releaseDevice: method(object({
|
|
52528
|
-
deviceId: number$1().int().nonnegative(),
|
|
52529
|
-
childDeviceId: number$1().int().nonnegative()
|
|
52530
|
-
}), _void(), {
|
|
52531
|
-
kind: "mutation",
|
|
52532
|
-
auth: "admin"
|
|
52533
|
-
})
|
|
52534
|
-
}
|
|
52535
|
-
};
|
|
52536
|
-
/**
|
|
52537
|
-
* `device-adoption` — generic discovery + adoption surface,
|
|
52538
|
-
* system-scoped collection (one provider per integration addon), routed
|
|
52539
|
-
* by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
|
|
52540
|
-
* pin model as `connection-test`. The admin UI keeps using
|
|
52541
|
-
* `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
|
|
52542
|
-
* successor to `ha-discovery`:
|
|
52543
|
-
* keyed by `integrationId` (the addon resolves integrationId → broker
|
|
52544
|
-
* internally) rather than a connection-specific `brokerId`, and it
|
|
52545
|
-
* REUSES the shared `DiscoveredChildDevice` candidate type from
|
|
52546
|
-
* `device-discovery.cap.ts` so a single shared adoption panel renders
|
|
52547
|
-
* any integration's candidate tree.
|
|
52548
|
-
*
|
|
52549
|
-
* UI flow (Integrations page → integration detail):
|
|
52550
|
-
* 1. `listCandidates({integrationId, page, pageSize, filter})` →
|
|
52551
|
-
* paginated candidate list, each row collapsed.
|
|
52552
|
-
* 2. User expands a row → `getCandidate({integrationId,
|
|
52553
|
-
* childNativeId})` returns the single candidate (with its nested
|
|
52554
|
-
* `children`) for the accordion render.
|
|
52555
|
-
* 3. User clicks "Adopt" on one or more candidates →
|
|
52556
|
-
* `adopt({integrationId, childNativeIds, perCandidate?})`
|
|
52557
|
-
* materialises one parent CamStack device per candidate plus its
|
|
52558
|
-
* accessory children. `perCandidate[childNativeId]` carries an
|
|
52559
|
-
* optional display-name override + a `hiddenChildIds` pre-hide set
|
|
52560
|
-
* (the parent's `accessories.hiddenChildIds` gates UI visibility).
|
|
52561
|
-
* 4. User clicks "Release" → `release({integrationId, camDeviceId})`
|
|
52562
|
-
* removes the parent + every child from the kernel registry.
|
|
52563
|
-
*/
|
|
52564
|
-
/**
|
|
52565
53352
|
* Provider-declared discovery *granularity* filter (the keystone of the
|
|
52566
53353
|
* single-entity-import design). A provider advertises which discovery
|
|
52567
53354
|
* granularities it supports via `listCandidateFilters`; the operator picks
|
|
@@ -53022,6 +53809,90 @@ var SiteZoneSchema = object({
|
|
|
53022
53809
|
* the ones that most want grouping. */
|
|
53023
53810
|
locationKeys: array(string())
|
|
53024
53811
|
});
|
|
53812
|
+
var DeviceSearchRequiresSchema = object({
|
|
53813
|
+
/** Capability names the field wants a value of. */
|
|
53814
|
+
caps: array(string()).readonly(),
|
|
53815
|
+
/** `any` (default): a device with one of them is compatible. `all`: it must hold every one. */
|
|
53816
|
+
mode: _enum(["any", "all"]).optional()
|
|
53817
|
+
});
|
|
53818
|
+
var DeviceSearchFiltersSchema = object({
|
|
53819
|
+
locations: array(string()).readonly().optional(),
|
|
53820
|
+
integrationIds: array(string()).readonly().optional(),
|
|
53821
|
+
types: array(string()).readonly().optional()
|
|
53822
|
+
});
|
|
53823
|
+
var DeviceSearchInputSchema = object({
|
|
53824
|
+
/** Matches name, location, type, integration (id and label) and id. Case-insensitive. */
|
|
53825
|
+
text: string().optional(),
|
|
53826
|
+
requires: DeviceSearchRequiresSchema.optional(),
|
|
53827
|
+
filters: DeviceSearchFiltersSchema.optional(),
|
|
53828
|
+
/** List the devices that do not provide the required cap too (greyed, not pickable). */
|
|
53829
|
+
includeIncompatible: boolean().optional(),
|
|
53830
|
+
/** The device the field belongs to: its location drives `suggested`. */
|
|
53831
|
+
contextDeviceId: number$1().optional(),
|
|
53832
|
+
/**
|
|
53833
|
+
* Resolve exactly these devices, regardless of every filter, so a picked
|
|
53834
|
+
* value can be rendered. When given, `items` is these devices in the order
|
|
53835
|
+
* asked (no paging, `nextCursor` null) and the ones that do not exist are
|
|
53836
|
+
* listed in `missingIds`.
|
|
53837
|
+
*/
|
|
53838
|
+
ids: array(number$1()).readonly().optional(),
|
|
53839
|
+
cursor: string().nullable().optional(),
|
|
53840
|
+
limit: number$1().int().min(1).max(200).optional()
|
|
53841
|
+
});
|
|
53842
|
+
var DeviceValueOptionSchema = object({
|
|
53843
|
+
cap: string(),
|
|
53844
|
+
/** Status-schema field path inside the cap (same paths as `getWireableFields`). */
|
|
53845
|
+
path: string(),
|
|
53846
|
+
label: string(),
|
|
53847
|
+
/** The current value as text; `null` when it is unknown or the read FAILED — never `0`. */
|
|
53848
|
+
reading: string().nullable(),
|
|
53849
|
+
unit: string().nullable().optional(),
|
|
53850
|
+
/** The field's measured quantity, when the cap declares one, so a client can format the reading. */
|
|
53851
|
+
quantity: ReadingQuantitySchema.optional()
|
|
53852
|
+
});
|
|
53853
|
+
var DeviceSearchItemSchema = object({
|
|
53854
|
+
id: number$1(),
|
|
53855
|
+
name: string(),
|
|
53856
|
+
type: _enum(DeviceType),
|
|
53857
|
+
location: string().nullable(),
|
|
53858
|
+
integrationId: string().nullable(),
|
|
53859
|
+
integrationLabel: string().nullable(),
|
|
53860
|
+
online: boolean().nullable(),
|
|
53861
|
+
disabled: boolean(),
|
|
53862
|
+
compatible: boolean(),
|
|
53863
|
+
/** Only the required caps' fields; empty when nothing was required or the device is incompatible. */
|
|
53864
|
+
values: array(DeviceValueOptionSchema).readonly()
|
|
53865
|
+
});
|
|
53866
|
+
var DeviceSearchFacetValueSchema = object({
|
|
53867
|
+
value: string(),
|
|
53868
|
+
count: number$1()
|
|
53869
|
+
});
|
|
53870
|
+
var DeviceSearchFacetIntegrationSchema = object({
|
|
53871
|
+
id: string(),
|
|
53872
|
+
label: string(),
|
|
53873
|
+
count: number$1()
|
|
53874
|
+
});
|
|
53875
|
+
var DeviceSearchFacetsSchema = object({
|
|
53876
|
+
locations: array(DeviceSearchFacetValueSchema).readonly(),
|
|
53877
|
+
integrations: array(DeviceSearchFacetIntegrationSchema).readonly(),
|
|
53878
|
+
types: array(DeviceSearchFacetValueSchema).readonly()
|
|
53879
|
+
});
|
|
53880
|
+
var DeviceSearchResultSchema = object({
|
|
53881
|
+
items: array(DeviceSearchItemSchema).readonly(),
|
|
53882
|
+
nextCursor: string().nullable(),
|
|
53883
|
+
/** Devices listed by this query across all pages (filters + text applied; incompatible counted only when `includeIncompatible`). */
|
|
53884
|
+
total: number$1(),
|
|
53885
|
+
/** Compatible devices after filters + text. */
|
|
53886
|
+
compatibleTotal: number$1(),
|
|
53887
|
+
/** Every device the caller's fleet holds, before anything is applied. */
|
|
53888
|
+
fleetTotal: number$1(),
|
|
53889
|
+
/** Incompatible devices after filters + text — the K of "show the K that don't". */
|
|
53890
|
+
hiddenIncompatible: number$1(),
|
|
53891
|
+
facets: DeviceSearchFacetsSchema,
|
|
53892
|
+
/** Same location as `contextDeviceId`; first page only. */
|
|
53893
|
+
suggested: array(DeviceSearchItemSchema).readonly(),
|
|
53894
|
+
missingIds: array(number$1()).readonly()
|
|
53895
|
+
});
|
|
53025
53896
|
var ProviderStatusSchema = object({
|
|
53026
53897
|
connected: boolean(),
|
|
53027
53898
|
deviceCount: number$1(),
|
|
@@ -53507,6 +54378,9 @@ var DeviceInfoSchema = object({
|
|
|
53507
54378
|
* named accordion sections (with optional intra-section order). See
|
|
53508
54379
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
53509
54380
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
54381
|
+
/** The children a container stands for, most preferred first, by accessory
|
|
54382
|
+
* `childKey`. Provider-declared. See `DeviceMeta.primaryChildPreference`. */
|
|
54383
|
+
primaryChildPreference: array(string()).readonly().optional(),
|
|
53510
54384
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
53511
54385
|
display: DeviceDisplayOverrideSchema.optional()
|
|
53512
54386
|
});
|
|
@@ -53655,6 +54529,9 @@ var DeviceMetaSchema = object({
|
|
|
53655
54529
|
* accordion sections (with optional intra-section order). See
|
|
53656
54530
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
53657
54531
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
54532
|
+
/** The children a container stands for, most preferred first, by accessory
|
|
54533
|
+
* `childKey`. Provider-declared. See `DeviceMeta.primaryChildPreference`. */
|
|
54534
|
+
primaryChildPreference: array(string()).readonly().optional(),
|
|
53658
54535
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
53659
54536
|
* Optional: only present for accessory children that carry a known role. */
|
|
53660
54537
|
role: string().nullable().optional(),
|
|
@@ -53870,6 +54747,12 @@ method(object({
|
|
|
53870
54747
|
}), _void(), {
|
|
53871
54748
|
kind: "mutation",
|
|
53872
54749
|
auth: "admin"
|
|
54750
|
+
}), method(object({
|
|
54751
|
+
deviceId: number$1(),
|
|
54752
|
+
primaryChildPreference: array(string()).readonly()
|
|
54753
|
+
}), _void(), {
|
|
54754
|
+
kind: "mutation",
|
|
54755
|
+
auth: "admin"
|
|
53873
54756
|
}), method(object({
|
|
53874
54757
|
deviceId: number$1(),
|
|
53875
54758
|
display: DeviceDisplayOverrideSchema.nullable()
|
|
@@ -53893,8 +54776,14 @@ method(object({
|
|
|
53893
54776
|
"string",
|
|
53894
54777
|
"number",
|
|
53895
54778
|
"boolean",
|
|
53896
|
-
"enum"
|
|
54779
|
+
"enum",
|
|
54780
|
+
"union"
|
|
53897
54781
|
]),
|
|
54782
|
+
unionMembers: array(_enum([
|
|
54783
|
+
"string",
|
|
54784
|
+
"number",
|
|
54785
|
+
"boolean"
|
|
54786
|
+
])).optional(),
|
|
53898
54787
|
enumValues: array(string()).optional(),
|
|
53899
54788
|
item: boolean().optional(),
|
|
53900
54789
|
quantity: ReadingQuantitySchema.optional()
|
|
@@ -54007,7 +54896,7 @@ method(object({
|
|
|
54007
54896
|
* it answers today and the caller filters as it already does.
|
|
54008
54897
|
*/
|
|
54009
54898
|
deviceIds: array(number$1()).optional()
|
|
54010
|
-
}), array(DeviceInfoSchema), { role: "viewer" }), method(object({ deviceId: number$1() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number$1() }), array(DeviceInfoSchema), { role: "viewer" }), method(object({ parentDeviceIds: array(number$1()).max(256) }), record(string(), array(DeviceInfoSchema))), method(object({ deviceId: number$1() }), object({
|
|
54899
|
+
}), array(DeviceInfoSchema), { role: "viewer" }), method(DeviceSearchInputSchema, DeviceSearchResultSchema, { role: "viewer" }), method(object({ deviceId: number$1() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number$1() }), array(DeviceInfoSchema), { role: "viewer" }), method(object({ parentDeviceIds: array(number$1()).max(256) }), record(string(), array(DeviceInfoSchema))), method(object({ deviceId: number$1() }), object({
|
|
54011
54900
|
mode: LinkedDevicesModeSchema,
|
|
54012
54901
|
devices: array(LinkedDeviceSchema)
|
|
54013
54902
|
}), { role: "viewer" }), method(object({ deviceIds: array(number$1()) }), array(LinkedDevicesForDeviceSchema), { role: "viewer" }), method(object({ deviceId: number$1() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number$1() }), array(ConfigEntrySchema)), method(object({ deviceId: number$1() }), ConfigUISchemaOutput), method(object({
|
|
@@ -55986,53 +56875,6 @@ var enumSensorCapability = {
|
|
|
55986
56875
|
* whether persisting is worth a SQLite commit. */
|
|
55987
56876
|
volatileStateFields: ["lastFetchedAt", "timestamp"]
|
|
55988
56877
|
};
|
|
55989
|
-
/**
|
|
55990
|
-
* Generic stateless event emitter. Installed on a `DeviceType.EventEmitter`
|
|
55991
|
-
* device. Carries the device's EXACT declared event vocabulary verbatim
|
|
55992
|
-
* (NO normalization). Two shapes:
|
|
55993
|
-
* - structured source (HA `event.*` entity) → `eventTypes` is the fixed
|
|
55994
|
-
* declared vocabulary (e.g. ['press_short','press_long',...]).
|
|
55995
|
-
* - generic/legacy source (HA bus events, e.g. zha_event) → `eventTypes`
|
|
55996
|
-
* is [] (open) and `lastEvent.data` carries the complex payload.
|
|
55997
|
-
* `seq` is monotonic so two identical events still produce distinct slice writes.
|
|
55998
|
-
*/
|
|
55999
|
-
var EventFireSchema = object({
|
|
56000
|
-
deviceId: number$1(),
|
|
56001
|
-
eventType: string(),
|
|
56002
|
-
data: record(string(), unknown()).nullable(),
|
|
56003
|
-
timestamp: number$1().meta(readingMeta("none")),
|
|
56004
|
-
seq: number$1().meta(readingMeta("count"))
|
|
56005
|
-
});
|
|
56006
|
-
var EventEmitterStatusSchema = object({
|
|
56007
|
-
eventTypes: array(string()),
|
|
56008
|
-
lastEvent: EventFireSchema.nullable(),
|
|
56009
|
-
eventCountSinceStart: number$1().meta(readingMeta("count"))
|
|
56010
|
-
});
|
|
56011
|
-
var eventEmitterCapability = {
|
|
56012
|
-
name: "event-emitter",
|
|
56013
|
-
scope: "device",
|
|
56014
|
-
deviceNative: true,
|
|
56015
|
-
mode: "singleton",
|
|
56016
|
-
deviceTypes: [DeviceType.EventEmitter],
|
|
56017
|
-
methods: {},
|
|
56018
|
-
events: { onEvent: { data: EventFireSchema } },
|
|
56019
|
-
status: {
|
|
56020
|
-
schema: EventEmitterStatusSchema,
|
|
56021
|
-
kind: "push"
|
|
56022
|
-
},
|
|
56023
|
-
runtimeState: EventEmitterStatusSchema,
|
|
56024
|
-
/**
|
|
56025
|
-
* Runtime-state durability: **session** — `eventCountSinceStart` names its own scope.
|
|
56026
|
-
*
|
|
56027
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
56028
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
56029
|
-
*/
|
|
56030
|
-
durability: "session",
|
|
56031
|
-
/**
|
|
56032
|
-
* History (D850): none — `eventCountSinceStart` resets with the addon; events live in ledgers.
|
|
56033
|
-
*/
|
|
56034
|
-
history: "none"
|
|
56035
|
-
};
|
|
56036
56878
|
var IdentitySchema = object({
|
|
56037
56879
|
id: string(),
|
|
56038
56880
|
name: string(),
|
|
@@ -56522,8 +57364,85 @@ method(_void(), array(FailureContributionSchema).readonly());
|
|
|
56522
57364
|
* - `FanPreset` — preset selector
|
|
56523
57365
|
* - `FanDirection` — direction toggle (ceiling fans)
|
|
56524
57366
|
* - `FanOscillating` — oscillation toggle
|
|
57367
|
+
*
|
|
57368
|
+
* Per-axis oscillation (`oscillationAxes`, `setOscillationAxis`, D1034): an air
|
|
57369
|
+
* circulator sweeps horizontally and vertically, each axis with its own
|
|
57370
|
+
* switch, fixed angle, sweep bounds and limits. A provider may report them
|
|
57371
|
+
* natively; otherwise the OPERATOR links sibling devices to each field in
|
|
57372
|
+
* Customize (a `replace` over the fields listed in `overridable` — nothing is
|
|
57373
|
+
* linked or inferred by itself), and a write to a linked field is forwarded to
|
|
57374
|
+
* its source. `oscillating` stays the single summary flag: when axes exist it
|
|
57375
|
+
* means "any axis oscillates" (`anyAxisOscillating`).
|
|
56525
57376
|
*/
|
|
56526
57377
|
var FanDirectionSchema = _enum(["forward", "reverse"]);
|
|
57378
|
+
var FAN_OSCILLATION_AXES = ["horizontal", "vertical"];
|
|
57379
|
+
var FanOscillationAxisNameSchema = _enum(FAN_OSCILLATION_AXES);
|
|
57380
|
+
var FanOscillationAxisFieldSchema = _enum([
|
|
57381
|
+
"oscillating",
|
|
57382
|
+
"angle",
|
|
57383
|
+
"sweepFrom",
|
|
57384
|
+
"sweepTo"
|
|
57385
|
+
]);
|
|
57386
|
+
/** The limits of one axis, read from the source that serves its angle. */
|
|
57387
|
+
var FAN_OSCILLATION_AXIS_LIMITS = [
|
|
57388
|
+
"min",
|
|
57389
|
+
"max",
|
|
57390
|
+
"step"
|
|
57391
|
+
];
|
|
57392
|
+
/**
|
|
57393
|
+
* One oscillation axis. Every reading the device has not reported is `null`,
|
|
57394
|
+
* never `0` (D393): `0°` is a real position. Each leaf defaults to `null`, so
|
|
57395
|
+
* a slice that names only the leaves it knows (a Customize link of one field)
|
|
57396
|
+
* is still a whole axis.
|
|
57397
|
+
*/
|
|
57398
|
+
var FanOscillationAxisSchema = object({
|
|
57399
|
+
/** Whether the axis sweeps. */
|
|
57400
|
+
oscillating: boolean().nullable().default(null),
|
|
57401
|
+
/** The fixed position (degrees) the axis holds when it does not sweep. */
|
|
57402
|
+
angle: number$1().nullable().default(null),
|
|
57403
|
+
/** The sweep bounds (degrees): left → right, bottom → top. */
|
|
57404
|
+
sweepFrom: number$1().nullable().default(null),
|
|
57405
|
+
sweepTo: number$1().nullable().default(null),
|
|
57406
|
+
/** The angles the axis accepts. Null when the device states none. */
|
|
57407
|
+
min: number$1().nullable().default(null),
|
|
57408
|
+
max: number$1().nullable().default(null),
|
|
57409
|
+
step: number$1().nullable().default(null)
|
|
57410
|
+
});
|
|
57411
|
+
/** Both axes; an axis the device does not have is `null`. */
|
|
57412
|
+
var FanOscillationAxesSchema = object({
|
|
57413
|
+
horizontal: FanOscillationAxisSchema.nullable().default(null),
|
|
57414
|
+
vertical: FanOscillationAxisSchema.nullable().default(null)
|
|
57415
|
+
});
|
|
57416
|
+
var FanOscillationAxisRefusalSchema = _enum([
|
|
57417
|
+
"nothing-to-set",
|
|
57418
|
+
"axis-not-linked",
|
|
57419
|
+
"out-of-range",
|
|
57420
|
+
"source-gone",
|
|
57421
|
+
"source-unknown",
|
|
57422
|
+
"source-call-failed"
|
|
57423
|
+
]);
|
|
57424
|
+
var FanOscillationAxisResultSchema = discriminatedUnion("status", [object({ status: literal("applied") }), object({
|
|
57425
|
+
status: literal("refused"),
|
|
57426
|
+
reason: FanOscillationAxisRefusalSchema,
|
|
57427
|
+
/** The field at fault, when one is. */
|
|
57428
|
+
field: FanOscillationAxisFieldSchema.optional(),
|
|
57429
|
+
/** One line for the operator, naming the axis, the field and the source. */
|
|
57430
|
+
detail: string(),
|
|
57431
|
+
/** Fields that DID land before the refusal (writes run in order, stop at the first failure). */
|
|
57432
|
+
applied: array(FanOscillationAxisFieldSchema)
|
|
57433
|
+
})]);
|
|
57434
|
+
var SetOscillationAxisInputSchema = object({
|
|
57435
|
+
deviceId: number$1().int().nonnegative(),
|
|
57436
|
+
axis: FanOscillationAxisNameSchema,
|
|
57437
|
+
oscillating: boolean().optional(),
|
|
57438
|
+
angle: number$1().optional(),
|
|
57439
|
+
sweepFrom: number$1().optional(),
|
|
57440
|
+
sweepTo: number$1().optional()
|
|
57441
|
+
});
|
|
57442
|
+
/** The state path of one axis field: `oscillationAxes.<axis>.<field>`. */
|
|
57443
|
+
function fanAxisPath(axis, field) {
|
|
57444
|
+
return `oscillationAxes.${axis}.${field}`;
|
|
57445
|
+
}
|
|
56527
57446
|
var FanControlStatusSchema = object({
|
|
56528
57447
|
/** Active speed as 0..100 inclusive. Null when the device has no
|
|
56529
57448
|
* speed surface (single-speed fan). */
|
|
@@ -56542,9 +57461,102 @@ var FanControlStatusSchema = object({
|
|
|
56542
57461
|
/** Oscillation toggle. Null when the device has no oscillation
|
|
56543
57462
|
* surface. */
|
|
56544
57463
|
oscillating: boolean().nullable(),
|
|
57464
|
+
/**
|
|
57465
|
+
* Per-axis oscillation (air circulators). Absent when the device has no
|
|
57466
|
+
* per-axis surface and nothing is linked in Customize.
|
|
57467
|
+
*/
|
|
57468
|
+
oscillationAxes: FanOscillationAxesSchema.optional(),
|
|
56545
57469
|
/** Ms epoch when the slice was last updated. */
|
|
56546
57470
|
lastChangedAt: number$1()
|
|
56547
57471
|
});
|
|
57472
|
+
var FAN_CUSTOMIZE_DEVICE_TYPES = [DeviceType.Fan];
|
|
57473
|
+
/** The Customize section the axis targets are listed under. */
|
|
57474
|
+
var AXES_SECTION = "oscillation-axes";
|
|
57475
|
+
function axisOscillatingTarget(axis) {
|
|
57476
|
+
return {
|
|
57477
|
+
id: `${axis}-oscillating`,
|
|
57478
|
+
cardinality: "scalar",
|
|
57479
|
+
vocabulary: {
|
|
57480
|
+
cap: "fan-control",
|
|
57481
|
+
stateField: "oscillating"
|
|
57482
|
+
},
|
|
57483
|
+
deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
|
|
57484
|
+
apply: "replace",
|
|
57485
|
+
section: AXES_SECTION,
|
|
57486
|
+
path: fanAxisPath(axis, "oscillating"),
|
|
57487
|
+
sources: [{
|
|
57488
|
+
kind: "switch",
|
|
57489
|
+
transforms: [{ kind: "identity" }]
|
|
57490
|
+
}],
|
|
57491
|
+
sourceReport: { status: "assignment-status" }
|
|
57492
|
+
};
|
|
57493
|
+
}
|
|
57494
|
+
/**
|
|
57495
|
+
* A numeric axis field. The `angle` link also carries the axis limits, read
|
|
57496
|
+
* from the SAME linked control's own range (`min`/`max`/`step`), so the limits
|
|
57497
|
+
* are the source's, never guessed.
|
|
57498
|
+
*/
|
|
57499
|
+
function axisNumericTarget(axis, field) {
|
|
57500
|
+
const base = {
|
|
57501
|
+
id: `${axis}-${field}`,
|
|
57502
|
+
cardinality: "scalar",
|
|
57503
|
+
vocabulary: { cap: "fan-control" },
|
|
57504
|
+
deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
|
|
57505
|
+
apply: "replace",
|
|
57506
|
+
section: AXES_SECTION,
|
|
57507
|
+
path: fanAxisPath(axis, field),
|
|
57508
|
+
sources: [{
|
|
57509
|
+
kind: "numeric",
|
|
57510
|
+
transforms: [{ kind: "identity" }]
|
|
57511
|
+
}],
|
|
57512
|
+
sourceReport: { status: "assignment-status" }
|
|
57513
|
+
};
|
|
57514
|
+
if (field !== "angle") return base;
|
|
57515
|
+
return {
|
|
57516
|
+
...base,
|
|
57517
|
+
companions: FAN_OSCILLATION_AXIS_LIMITS.map((limit) => ({
|
|
57518
|
+
kind: "source-field",
|
|
57519
|
+
path: fanAxisPath(axis, limit),
|
|
57520
|
+
field: limit
|
|
57521
|
+
}))
|
|
57522
|
+
};
|
|
57523
|
+
}
|
|
57524
|
+
var AXIS_TARGETS = FAN_OSCILLATION_AXES.flatMap((axis) => [
|
|
57525
|
+
axisOscillatingTarget(axis),
|
|
57526
|
+
axisNumericTarget(axis, "angle"),
|
|
57527
|
+
axisNumericTarget(axis, "sweepFrom"),
|
|
57528
|
+
axisNumericTarget(axis, "sweepTo")
|
|
57529
|
+
]);
|
|
57530
|
+
/** A select (an enum control) the operator links to the fan's `preset`. */
|
|
57531
|
+
var PRESET_TARGET = {
|
|
57532
|
+
id: "preset",
|
|
57533
|
+
cardinality: "scalar",
|
|
57534
|
+
vocabulary: { cap: "fan-control" },
|
|
57535
|
+
deviceTypes: FAN_CUSTOMIZE_DEVICE_TYPES,
|
|
57536
|
+
apply: "replace",
|
|
57537
|
+
path: "preset",
|
|
57538
|
+
sources: [{
|
|
57539
|
+
kind: "field",
|
|
57540
|
+
valueKinds: ["string"],
|
|
57541
|
+
transforms: [{ kind: "identity" }]
|
|
57542
|
+
}],
|
|
57543
|
+
sourceReport: { status: "assignment-status" }
|
|
57544
|
+
};
|
|
57545
|
+
var FAN_CONTROL_TARGETS = [...AXIS_TARGETS, PRESET_TARGET];
|
|
57546
|
+
/**
|
|
57547
|
+
* Customize (D1034): the fan already serves `fan-control` natively, so every
|
|
57548
|
+
* target is a `replace` over a field listed in `overridable` — the axis leaves
|
|
57549
|
+
* and `preset`, nothing else. Nothing is pre-linked: the operator picks each
|
|
57550
|
+
* source (D356).
|
|
57551
|
+
*/
|
|
57552
|
+
var fanControlCustomize = {
|
|
57553
|
+
version: 1,
|
|
57554
|
+
overridable: FAN_CONTROL_TARGETS.map((target) => ({
|
|
57555
|
+
path: target.path,
|
|
57556
|
+
by: target.id
|
|
57557
|
+
})),
|
|
57558
|
+
targets: FAN_CONTROL_TARGETS
|
|
57559
|
+
};
|
|
56548
57560
|
var fanControlCapability = {
|
|
56549
57561
|
name: "fan-control",
|
|
56550
57562
|
scope: "device",
|
|
@@ -56579,6 +57591,18 @@ var fanControlCapability = {
|
|
|
56579
57591
|
}), _void(), {
|
|
56580
57592
|
kind: "mutation",
|
|
56581
57593
|
auth: "protected"
|
|
57594
|
+
}),
|
|
57595
|
+
/**
|
|
57596
|
+
* Sets one oscillation axis: its switch, fixed angle and/or sweep bounds.
|
|
57597
|
+
* Fields left out are untouched. A field linked in Customize is forwarded
|
|
57598
|
+
* to its linked source (D1034); otherwise the native provider serves it;
|
|
57599
|
+
* neither → `axis-not-linked`. Ranges are checked against the axis limits
|
|
57600
|
+
* before anything is written; writes run in order and stop at the first
|
|
57601
|
+
* failure, which the result names.
|
|
57602
|
+
*/
|
|
57603
|
+
setOscillationAxis: optionalMethod(SetOscillationAxisInputSchema, FanOscillationAxisResultSchema, {
|
|
57604
|
+
kind: "mutation",
|
|
57605
|
+
auth: "protected"
|
|
56582
57606
|
})
|
|
56583
57607
|
},
|
|
56584
57608
|
status: {
|
|
@@ -56590,6 +57614,7 @@ var fanControlCapability = {
|
|
|
56590
57614
|
* sliders read `speed` for live updates.
|
|
56591
57615
|
*/
|
|
56592
57616
|
runtimeState: FanControlStatusSchema,
|
|
57617
|
+
customize: fanControlCustomize,
|
|
56593
57618
|
/**
|
|
56594
57619
|
* Runtime-state durability: **session** — as `brightness`.
|
|
56595
57620
|
*
|
|
@@ -56609,16 +57634,33 @@ var fanControlCapability = {
|
|
|
56609
57634
|
oscillating: { kind: "boolean" }
|
|
56610
57635
|
} }
|
|
56611
57636
|
};
|
|
57637
|
+
/** One axis as a preset wants it; an absent field is left as it is. */
|
|
57638
|
+
var FanPresetAxisSchema = object({
|
|
57639
|
+
oscillating: boolean().optional(),
|
|
57640
|
+
angle: number$1().optional(),
|
|
57641
|
+
sweepFrom: number$1().optional(),
|
|
57642
|
+
sweepTo: number$1().optional()
|
|
57643
|
+
});
|
|
57644
|
+
/** The fan-control state a preset sets; an absent field is left as it is. */
|
|
57645
|
+
var FanPresetStateSchema = object({
|
|
57646
|
+
speed: number$1().min(0).max(100).optional(),
|
|
57647
|
+
/** The fan's own preset mode (or the linked select's option). */
|
|
57648
|
+
preset: string().min(1).optional(),
|
|
57649
|
+
direction: FanDirectionSchema.optional(),
|
|
57650
|
+
oscillationAxes: object({
|
|
57651
|
+
horizontal: FanPresetAxisSchema.optional(),
|
|
57652
|
+
vertical: FanPresetAxisSchema.optional()
|
|
57653
|
+
}).optional()
|
|
57654
|
+
});
|
|
56612
57655
|
var FanPresetSchema = object({
|
|
56613
57656
|
/** Stable key; never reused after a delete within one device's list. */
|
|
56614
57657
|
id: string().min(1).max(64),
|
|
56615
57658
|
/** What the operator and a voice assistant call it. */
|
|
56616
57659
|
name: string().trim().min(1).max(40),
|
|
56617
|
-
|
|
57660
|
+
state: FanPresetStateSchema
|
|
56618
57661
|
});
|
|
56619
57662
|
var FanPresetsRefusalSchema = _enum([
|
|
56620
|
-
"
|
|
56621
|
-
"bad-target",
|
|
57663
|
+
"empty-state",
|
|
56622
57664
|
"duplicate-name",
|
|
56623
57665
|
"duplicate-id",
|
|
56624
57666
|
"too-many",
|
|
@@ -56630,72 +57672,55 @@ var FanPresetsSetResultSchema = discriminatedUnion("status", [object({
|
|
|
56630
57672
|
}), object({
|
|
56631
57673
|
status: literal("refused"),
|
|
56632
57674
|
reason: FanPresetsRefusalSchema,
|
|
56633
|
-
/** Names the preset
|
|
57675
|
+
/** Names the preset at fault. */
|
|
56634
57676
|
detail: string()
|
|
56635
57677
|
})]);
|
|
56636
|
-
var
|
|
56637
|
-
|
|
57678
|
+
var FanPresetFieldResultSchema = object({
|
|
57679
|
+
/** The state path: `speed`, `preset`, `direction`, `oscillationAxes.horizontal.angle`. */
|
|
57680
|
+
field: string(),
|
|
56638
57681
|
outcome: _enum([
|
|
56639
57682
|
"applied",
|
|
56640
57683
|
"failed",
|
|
56641
57684
|
"not-run",
|
|
56642
57685
|
"refused"
|
|
56643
57686
|
]),
|
|
56644
|
-
deviceId: number$1().int().nonnegative().optional(),
|
|
56645
|
-
cap: string().optional(),
|
|
56646
|
-
method: string().optional(),
|
|
56647
57687
|
/** The cause, for every outcome but `applied`. */
|
|
56648
57688
|
message: string().optional()
|
|
56649
57689
|
});
|
|
56650
57690
|
var FanPresetApplyResultSchema = object({
|
|
56651
|
-
/** `applied`: every
|
|
57691
|
+
/** `applied`: every field landed. `partial`: some did, one did not. `refused`: nothing ran. */
|
|
56652
57692
|
status: _enum([
|
|
56653
57693
|
"applied",
|
|
56654
57694
|
"partial",
|
|
56655
57695
|
"refused"
|
|
56656
57696
|
]),
|
|
56657
|
-
|
|
57697
|
+
fields: array(FanPresetFieldResultSchema)
|
|
56658
57698
|
});
|
|
56659
57699
|
var FanCaptureFieldSchema = _enum([
|
|
56660
57700
|
"speed",
|
|
56661
|
-
"oscillating",
|
|
56662
57701
|
"direction",
|
|
56663
|
-
"preset"
|
|
57702
|
+
"preset",
|
|
57703
|
+
"oscillationAxes"
|
|
56664
57704
|
]);
|
|
56665
|
-
var FanCaptureOmissionSchema = object({
|
|
56666
|
-
/** A fan field (`speed`) or `step <n>` for a sibling step. */
|
|
56667
|
-
what: string(),
|
|
56668
|
-
/** Why no value was taken: unknown reading (never stored as 0, D393), or a step that cannot be read. */
|
|
56669
|
-
reason: string()
|
|
56670
|
-
});
|
|
56671
|
-
var FanCaptureItemSchema = object({
|
|
56672
|
-
group: _enum([
|
|
56673
|
-
"fan",
|
|
56674
|
-
"oscillation",
|
|
56675
|
-
"angles"
|
|
56676
|
-
]),
|
|
56677
|
-
/** What the step sets, in the operator's words (a device name, a fan field). */
|
|
56678
|
-
label: string()
|
|
56679
|
-
});
|
|
56680
57705
|
var FanPresetCaptureResultSchema = object({
|
|
56681
|
-
/** The
|
|
56682
|
-
|
|
56683
|
-
|
|
56684
|
-
|
|
56685
|
-
|
|
57706
|
+
/** The fan's CURRENT fan-control state, as a preset; NOT saved. */
|
|
57707
|
+
state: FanPresetStateSchema,
|
|
57708
|
+
omitted: array(object({
|
|
57709
|
+
/** A state path (`speed`, `oscillationAxes.vertical.angle`). */
|
|
57710
|
+
what: string(),
|
|
57711
|
+
/** Why no value was taken: an unknown reading is never stored as 0 (D393). */
|
|
57712
|
+
reason: string()
|
|
57713
|
+
}))
|
|
56686
57714
|
});
|
|
56687
|
-
var deviceInput$
|
|
57715
|
+
var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
|
|
56688
57716
|
DeviceType.Fan;
|
|
56689
|
-
method(deviceInput$
|
|
57717
|
+
method(deviceInput$1, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$1.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
56690
57718
|
kind: "mutation",
|
|
56691
57719
|
auth: "admin"
|
|
56692
|
-
}), method(deviceInput$
|
|
57720
|
+
}), method(deviceInput$1.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
56693
57721
|
kind: "mutation",
|
|
56694
57722
|
auth: "protected"
|
|
56695
|
-
}), method(deviceInput$
|
|
56696
|
-
fields: array(FanCaptureFieldSchema).optional(),
|
|
56697
|
-
steps: array(ActionStepSchema).max(20).default([])
|
|
56698
|
-
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
57723
|
+
}), method(deviceInput$1.extend({ fields: array(FanCaptureFieldSchema).optional() }), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
56699
57724
|
/**
|
|
56700
57725
|
* Per-device feature/identity probe slice. Holds the runtime-resolved
|
|
56701
57726
|
* truth about what a device CAN do — which the kernel uses to:
|
|
@@ -56907,8 +57932,7 @@ var groupActiveCapability = {
|
|
|
56907
57932
|
DeviceType.Valve,
|
|
56908
57933
|
DeviceType.Lock,
|
|
56909
57934
|
DeviceType.MediaPlayer,
|
|
56910
|
-
DeviceType.Climate
|
|
56911
|
-
DeviceType.Thermostat
|
|
57935
|
+
DeviceType.Climate
|
|
56912
57936
|
],
|
|
56913
57937
|
methods: {},
|
|
56914
57938
|
status: {
|
|
@@ -56950,7 +57974,6 @@ var groupCommandsCapability = {
|
|
|
56950
57974
|
DeviceType.Valve,
|
|
56951
57975
|
DeviceType.Lock,
|
|
56952
57976
|
DeviceType.MediaPlayer,
|
|
56953
|
-
DeviceType.Thermostat,
|
|
56954
57977
|
DeviceType.Climate
|
|
56955
57978
|
],
|
|
56956
57979
|
methods: {},
|
|
@@ -57006,7 +58029,6 @@ var groupStatusCapability = {
|
|
|
57006
58029
|
DeviceType.Valve,
|
|
57007
58030
|
DeviceType.Lock,
|
|
57008
58031
|
DeviceType.MediaPlayer,
|
|
57009
|
-
DeviceType.Thermostat,
|
|
57010
58032
|
DeviceType.Climate
|
|
57011
58033
|
],
|
|
57012
58034
|
methods: {},
|
|
@@ -57335,7 +58357,7 @@ var heatingValveCapability = {
|
|
|
57335
58357
|
scope: "device",
|
|
57336
58358
|
deviceNative: true,
|
|
57337
58359
|
mode: "singleton",
|
|
57338
|
-
deviceTypes: [DeviceType.
|
|
58360
|
+
deviceTypes: [DeviceType.Climate],
|
|
57339
58361
|
methods: {},
|
|
57340
58362
|
status: {
|
|
57341
58363
|
schema: HeatingValveStatusSchema,
|
|
@@ -57365,13 +58387,18 @@ var heatingValveCapability = {
|
|
|
57365
58387
|
* retried in a loop - D49); a future one is re-armed.
|
|
57366
58388
|
*
|
|
57367
58389
|
* What a device may be run for, and for how long at most, is per device type
|
|
57368
|
-
* ({@link
|
|
58390
|
+
* and role ({@link timedRunLimitOf}): the maximum is a safety bound, not a preference,
|
|
57369
58391
|
* so a request above it is REFUSED with a typed reason, never clamped.
|
|
57370
58392
|
*/
|
|
57371
58393
|
var TimedRunUnitSchema = _enum(["s", "min"]);
|
|
57372
58394
|
_enum(["switch", "pet-fountain"]);
|
|
57373
|
-
|
|
57374
|
-
|
|
58395
|
+
/**
|
|
58396
|
+
* The device types `timed-run` may be offered on. A `switch` is narrowed by
|
|
58397
|
+
* role ({@link TIMED_RUN_DEVICE_ROLES}): only a plug-in heater runs on a timer.
|
|
58398
|
+
*/
|
|
58399
|
+
var TIMED_RUN_DEVICE_TYPES = [DeviceType.Switch, DeviceType.PetFountain];
|
|
58400
|
+
/** The roles that make a device of these types timed-run-able (D1033). */
|
|
58401
|
+
var TIMED_RUN_DEVICE_ROLES = { [DeviceType.Switch]: [DeviceRole.SwitchHeater] };
|
|
57375
58402
|
/** One duration, as the one-tap run asks for it. */
|
|
57376
58403
|
var TimedRunQuickRunSchema = object({
|
|
57377
58404
|
duration: number$1().positive().finite(),
|
|
@@ -57421,6 +58448,7 @@ var timedRunCapability = {
|
|
|
57421
58448
|
kind: "wrapper",
|
|
57422
58449
|
defaultActive: true,
|
|
57423
58450
|
deviceTypes: TIMED_RUN_DEVICE_TYPES,
|
|
58451
|
+
deviceRoles: TIMED_RUN_DEVICE_ROLES,
|
|
57424
58452
|
deviceConfig: { ui: {
|
|
57425
58453
|
kind: "derived-form",
|
|
57426
58454
|
builderId: "timed-run",
|
|
@@ -57581,8 +58609,8 @@ var HeroSceneSchema = object({
|
|
|
57581
58609
|
/** 0..1: volume, cover position, cycle progress. */
|
|
57582
58610
|
level: number$1().min(0).max(1).nullable()
|
|
57583
58611
|
});
|
|
57584
|
-
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.
|
|
57585
|
-
DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
|
|
58612
|
+
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
|
|
58613
|
+
DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.SwitchHeater, DeviceRole.SwitchFan, DeviceRole.ClimateAc, DeviceRole.ClimateThermostat, DeviceRole.ClimateFloor, DeviceRole.ClimateRadiator, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
|
|
57586
58614
|
/** A string in both languages, as the cap vocabulary writes it (D920): no `t()` in the model. */
|
|
57587
58615
|
var HeroTextSchema = object({
|
|
57588
58616
|
en: string(),
|
|
@@ -57648,13 +58676,11 @@ var HeroConfirmSchema = _enum([
|
|
|
57648
58676
|
"press"
|
|
57649
58677
|
]);
|
|
57650
58678
|
var HeroActionSchema = object({
|
|
57651
|
-
/** Unique within the model; the `gated` list and the specs name it. */
|
|
57652
58679
|
id: string(),
|
|
57653
58680
|
label: HeroTextSchema,
|
|
57654
58681
|
icon: IconKeySchema,
|
|
57655
58682
|
call: HeroCallSchema,
|
|
57656
58683
|
emphasis: _enum(["primary", "normal"]),
|
|
57657
|
-
/** Ask before running it. */
|
|
57658
58684
|
confirm: HeroConfirmSchema.nullable()
|
|
57659
58685
|
});
|
|
57660
58686
|
var HeroControlSchema = discriminatedUnion("kind", [
|
|
@@ -57705,6 +58731,8 @@ var HeroPillSchema = object({
|
|
|
57705
58731
|
});
|
|
57706
58732
|
var HeroRowSchema = object({
|
|
57707
58733
|
id: HeroRowIdSchema,
|
|
58734
|
+
/** `scene`: the row's action and slider controls are drawn on the scene. Absent: an ordinary row. */
|
|
58735
|
+
placement: _enum(["row", "scene"]).optional(),
|
|
57708
58736
|
/** The row's own name when the kind's label would not tell rows of one kind apart (a circuit's label). */
|
|
57709
58737
|
label: HeroTextSchema.optional(),
|
|
57710
58738
|
/** A group header drawn above the row when it differs from the previous row's ("System"). */
|
|
@@ -57735,9 +58763,16 @@ var HeroBadgeSchema = object({
|
|
|
57735
58763
|
value: number$1().nullable(),
|
|
57736
58764
|
label: HeroTextSchema
|
|
57737
58765
|
});
|
|
57738
|
-
var HeroRemoteSlotSchema = object({
|
|
57739
|
-
/** Saved commands; `null` = not known yet (the button still shows). */
|
|
57740
|
-
commands: number$1().int().nullable()
|
|
58766
|
+
var HeroRemoteSlotSchema = object({
|
|
58767
|
+
/** Saved commands; `null` = not known yet (the button still shows). */
|
|
58768
|
+
commands: number$1().int().nullable(),
|
|
58769
|
+
/**
|
|
58770
|
+
* The control a list row draws as the "open the remote" icon. Recognised by its id
|
|
58771
|
+
* ({@link REMOTE_OPEN_ACTION_ID}); it changes no state, so a client opens the remote
|
|
58772
|
+
* instead of running `call`. Absent on a model from an older server.
|
|
58773
|
+
*/
|
|
58774
|
+
open: HeroActionSchema.optional()
|
|
58775
|
+
});
|
|
57741
58776
|
var HeroConsumablesSlotSchema = object({
|
|
57742
58777
|
items: array(object({
|
|
57743
58778
|
key: string(),
|
|
@@ -57905,6 +58940,8 @@ var HeroModelSchema = object({
|
|
|
57905
58940
|
* the FIRST row that has any (a later row's control is never promoted). When
|
|
57906
58941
|
* the model lists that part in `gated`, the row has no inline control: the
|
|
57907
58942
|
* projection never falls through to another control the caller could use.
|
|
58943
|
+
* - a climate or thermostat row's primary is its `climate.mode` control (no power
|
|
58944
|
+
* toggle: "off" is a mode); when the model gates it the rule above applies as before;
|
|
57908
58945
|
* - a timed run, when the model carries the slot, sits beside that primary:
|
|
57909
58946
|
* Run while idle, Stop while armed. Gating the slot hides the control.
|
|
57910
58947
|
* - the status line is the head (what plays, else the state) plus at most
|
|
@@ -57912,6 +58949,13 @@ var HeroModelSchema = object({
|
|
|
57912
58949
|
* time left on an armed timed run;
|
|
57913
58950
|
* - the trailing fact is the first of power, battery, consumables.
|
|
57914
58951
|
*
|
|
58952
|
+
* - `extras` are the further inline controls of the types that carry more than one
|
|
58953
|
+
* ({@link ROW_EXTRAS_BY_TYPE}): climate setpoint, a robot's return to base,
|
|
58954
|
+
* a player's volume and remote. Each is a control the model already has, named by id,
|
|
58955
|
+
* in that order and at most {@link ROW_EXTRAS_MAX}; one the model lacks, or lists in
|
|
58956
|
+
* `gated`, is absent, and the one `primary` already is never repeats. Empty = the
|
|
58957
|
+
* row of an older server: `primary` alone.
|
|
58958
|
+
*
|
|
57915
58959
|
* `projectDeviceRow` is pure and computes nothing: it selects from a
|
|
57916
58960
|
* {@link HeroModel}. A row that needed a value the hero lacks would mean the
|
|
57917
58961
|
* model is missing it, so the fix goes in the model.
|
|
@@ -57943,8 +58987,16 @@ var HeroRowModelSchema = object({
|
|
|
57943
58987
|
* while armed. `null` when there is no slot, or the caller may not act.
|
|
57944
58988
|
*/
|
|
57945
58989
|
timedRun: HeroRowTimedRunSchema.nullable(),
|
|
58990
|
+
/**
|
|
58991
|
+
* The further inline controls after `primary`, in order (see the header). The remote
|
|
58992
|
+
* is an `action` with id `remote.open`: the client opens the remote, it does not run
|
|
58993
|
+
* the call. Absent or empty = `primary` alone.
|
|
58994
|
+
*/
|
|
58995
|
+
extras: array(HeroControlSchema).max(2).optional(),
|
|
57946
58996
|
trailing: HeroFactSchema.nullable()
|
|
57947
58997
|
});
|
|
58998
|
+
DeviceType.Climate;
|
|
58999
|
+
DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
|
|
57948
59000
|
/**
|
|
57949
59001
|
* hero-layout — what a device's hero says (D1023). Read-only: the FIXED
|
|
57950
59002
|
* layouts live in `@camstack/types` and nothing here stores or edits one.
|
|
@@ -57976,7 +59028,10 @@ method(heroInput, HeroModelSchema.nullable(), {
|
|
|
57976
59028
|
}), method(rowModelsInput, array(HeroRowModelSchema.nullable()), {
|
|
57977
59029
|
auth: "protected",
|
|
57978
59030
|
role: "viewer"
|
|
57979
|
-
}), method(object({
|
|
59031
|
+
}), method(object({
|
|
59032
|
+
deviceType: string(),
|
|
59033
|
+
role: string().nullable().optional()
|
|
59034
|
+
}), HeroLayoutRowsSchema, {
|
|
57980
59035
|
auth: "protected",
|
|
57981
59036
|
role: "viewer"
|
|
57982
59037
|
});
|
|
@@ -58023,6 +59078,11 @@ var humidifierCapability = {
|
|
|
58023
59078
|
deviceNative: true,
|
|
58024
59079
|
mode: "singleton",
|
|
58025
59080
|
deviceTypes: [DeviceType.Humidifier],
|
|
59081
|
+
power: {
|
|
59082
|
+
kind: "field",
|
|
59083
|
+
field: "on",
|
|
59084
|
+
set: "setOn"
|
|
59085
|
+
},
|
|
58026
59086
|
methods: {
|
|
58027
59087
|
setOn: method(object({
|
|
58028
59088
|
deviceId: number$1().int().nonnegative(),
|
|
@@ -58096,7 +59156,7 @@ var humiditySensorCapability = {
|
|
|
58096
59156
|
scope: "device",
|
|
58097
59157
|
deviceNative: true,
|
|
58098
59158
|
mode: "singleton",
|
|
58099
|
-
deviceTypes: [DeviceType.Sensor, DeviceType.
|
|
59159
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
|
|
58100
59160
|
methods: {},
|
|
58101
59161
|
status: {
|
|
58102
59162
|
schema: HumiditySensorStatusSchema,
|
|
@@ -58390,6 +59450,12 @@ var IntegrationLiteSchema = object({
|
|
|
58390
59450
|
var AvailableIntegrationTypeSchema = object({
|
|
58391
59451
|
addonId: string(),
|
|
58392
59452
|
name: string(),
|
|
59453
|
+
/**
|
|
59454
|
+
* Product name a new integration is prefilled from (`<label> - Home`).
|
|
59455
|
+
* Absent: the client uses `name`. Set when the catalog title is not the
|
|
59456
|
+
* product (Bosch SHC, MQTT).
|
|
59457
|
+
*/
|
|
59458
|
+
providerLabel: string().optional(),
|
|
58393
59459
|
description: string(),
|
|
58394
59460
|
iconUrl: string().nullable(),
|
|
58395
59461
|
color: string(),
|
|
@@ -69494,13 +70560,23 @@ var NcRuleTargetSchema = object({
|
|
|
69494
70560
|
*/
|
|
69495
70561
|
params: record(string(), unknown()).optional()
|
|
69496
70562
|
});
|
|
70563
|
+
/** Which picture kind one target receives (#487). */
|
|
70564
|
+
var NcTargetAttachmentSchema = _enum([
|
|
70565
|
+
"none",
|
|
70566
|
+
"image",
|
|
70567
|
+
"clip",
|
|
70568
|
+
"gif"
|
|
70569
|
+
]);
|
|
69497
70570
|
var NcTargetOverrideSchema = object({
|
|
69498
70571
|
template: object({
|
|
69499
70572
|
title: string().max(500).optional(),
|
|
69500
70573
|
body: string().max(2e3).optional()
|
|
69501
70574
|
}).optional(),
|
|
69502
70575
|
schedule: NcScheduleSchema.optional(),
|
|
69503
|
-
priority: number$1().int().min(1).max(5).optional()
|
|
70576
|
+
priority: number$1().int().min(1).max(5).optional(),
|
|
70577
|
+
attachment: NcTargetAttachmentSchema.optional(),
|
|
70578
|
+
/** `false` = no buttons for this target; `true` is the same as absent. */
|
|
70579
|
+
actions: boolean().optional()
|
|
69504
70580
|
});
|
|
69505
70581
|
/** Per-target settings keyed by target id (#487). */
|
|
69506
70582
|
var NcTargetOverridesSchema = record(string(), NcTargetOverrideSchema);
|
|
@@ -71268,7 +72344,18 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
71268
72344
|
* The value is baked into the authorization code at `/authorize` and travels
|
|
71269
72345
|
* on the tokens, so editing this field changes FUTURE links only.
|
|
71270
72346
|
*/
|
|
71271
|
-
refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional()
|
|
72347
|
+
refreshTokenTtlSec: union([number$1().int().positive(), literal("never")]).optional(),
|
|
72348
|
+
/**
|
|
72349
|
+
* The integration instances an account can be linked to (planning#415),
|
|
72350
|
+
* beside the default one. `/api/oauth2/authorize?instance=<id>` is accepted
|
|
72351
|
+
* only for an id listed here; with no `instance=` the link is made to the
|
|
72352
|
+
* default instance, which is never listed and always accepted — it is the
|
|
72353
|
+
* address third parties already hold. Omit for a single-instance integration.
|
|
72354
|
+
*/
|
|
72355
|
+
instances: array(object({
|
|
72356
|
+
id: string(),
|
|
72357
|
+
displayName: string()
|
|
72358
|
+
})).optional()
|
|
71272
72359
|
});
|
|
71273
72360
|
method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
|
|
71274
72361
|
/**
|
|
@@ -71691,9 +72778,13 @@ var petFeederCapability = {
|
|
|
71691
72778
|
*/
|
|
71692
72779
|
durability: "session",
|
|
71693
72780
|
/**
|
|
71694
|
-
* History (D850): recorded series.
|
|
72781
|
+
* History (D850): recorded series. `feeding` / `eating` are the meals the
|
|
72782
|
+
* device hero's "Meals" button names — change-only booleans, so a dispense
|
|
72783
|
+
* or a pet at the bowl is one span on the history.
|
|
71695
72784
|
*/
|
|
71696
72785
|
history: { fields: {
|
|
72786
|
+
feeding: { kind: "boolean" },
|
|
72787
|
+
eating: { kind: "boolean" },
|
|
71697
72788
|
foodLevel: { kind: "gauge" },
|
|
71698
72789
|
bowlFood: { kind: "gauge" },
|
|
71699
72790
|
food1: { kind: "gauge" },
|
|
@@ -71754,6 +72845,11 @@ var petFountainCapability = {
|
|
|
71754
72845
|
deviceNative: true,
|
|
71755
72846
|
mode: "singleton",
|
|
71756
72847
|
deviceTypes: [DeviceType.PetFountain],
|
|
72848
|
+
power: {
|
|
72849
|
+
kind: "field",
|
|
72850
|
+
field: "pumpRunning",
|
|
72851
|
+
set: "setPump"
|
|
72852
|
+
},
|
|
71757
72853
|
methods: {
|
|
71758
72854
|
/** Start or stop the pump. */
|
|
71759
72855
|
setPump: method(object({
|
|
@@ -73329,7 +74425,6 @@ var POWER_METER_DEVICE_TYPES = [
|
|
|
73329
74425
|
DeviceType.Light,
|
|
73330
74426
|
DeviceType.Fan,
|
|
73331
74427
|
DeviceType.Climate,
|
|
73332
|
-
DeviceType.Thermostat,
|
|
73333
74428
|
DeviceType.Humidifier,
|
|
73334
74429
|
DeviceType.WaterHeater,
|
|
73335
74430
|
DeviceType.MediaPlayer,
|
|
@@ -73338,8 +74433,7 @@ var POWER_METER_DEVICE_TYPES = [
|
|
|
73338
74433
|
DeviceType.Dishwasher,
|
|
73339
74434
|
DeviceType.Vacuum,
|
|
73340
74435
|
DeviceType.LawnMower,
|
|
73341
|
-
DeviceType.PetFeeder
|
|
73342
|
-
DeviceType.Heater
|
|
74436
|
+
DeviceType.PetFeeder
|
|
73343
74437
|
];
|
|
73344
74438
|
/** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
|
|
73345
74439
|
var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
|
|
@@ -73452,118 +74546,6 @@ var powerMeterCapability = {
|
|
|
73452
74546
|
volatileStateFields: ["lastFetchedAt"],
|
|
73453
74547
|
customize: powerMeterCustomize
|
|
73454
74548
|
};
|
|
73455
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
73456
|
-
"no-power-source",
|
|
73457
|
-
"switch-missing",
|
|
73458
|
-
"switch-offline",
|
|
73459
|
-
"switch-unknown",
|
|
73460
|
-
"restart-in-progress",
|
|
73461
|
-
"switch-call-failed"
|
|
73462
|
-
]);
|
|
73463
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
73464
|
-
accepted: literal(false),
|
|
73465
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
73466
|
-
/** One line for the operator, naming the switch. */
|
|
73467
|
-
message: string()
|
|
73468
|
-
})]);
|
|
73469
|
-
var PowerSourceInfoSchema = object({
|
|
73470
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
73471
|
-
linked: boolean(),
|
|
73472
|
-
switchDeviceId: number$1().nullable(),
|
|
73473
|
-
switchName: string().nullable(),
|
|
73474
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
73475
|
-
switchOn: boolean().nullable(),
|
|
73476
|
-
switchOnline: boolean().nullable(),
|
|
73477
|
-
restartInProgress: boolean(),
|
|
73478
|
-
offSeconds: number$1().int()
|
|
73479
|
-
});
|
|
73480
|
-
/** The composed slice: the switch state the link mirrors, plus the stored off time. */
|
|
73481
|
-
var PowerSourceStateSchema = object({
|
|
73482
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
73483
|
-
switchOn: boolean().nullable(),
|
|
73484
|
-
offSeconds: number$1().int(),
|
|
73485
|
-
lastFetchedAt: number$1()
|
|
73486
|
-
});
|
|
73487
|
-
var POWER_SOURCE_DEVICE_TYPES = [
|
|
73488
|
-
DeviceType.Gateway,
|
|
73489
|
-
DeviceType.Hub,
|
|
73490
|
-
DeviceType.Camera,
|
|
73491
|
-
DeviceType.Sensor,
|
|
73492
|
-
DeviceType.MediaPlayer,
|
|
73493
|
-
DeviceType.Vacuum,
|
|
73494
|
-
DeviceType.Generic,
|
|
73495
|
-
DeviceType.Washer,
|
|
73496
|
-
DeviceType.Dryer,
|
|
73497
|
-
DeviceType.Dishwasher,
|
|
73498
|
-
DeviceType.PetFountain,
|
|
73499
|
-
DeviceType.Fan,
|
|
73500
|
-
DeviceType.Humidifier,
|
|
73501
|
-
DeviceType.Climate,
|
|
73502
|
-
DeviceType.WaterHeater
|
|
73503
|
-
];
|
|
73504
|
-
var powerSourceCustomize = {
|
|
73505
|
-
version: 1,
|
|
73506
|
-
targets: [{
|
|
73507
|
-
id: "switch",
|
|
73508
|
-
cardinality: "scalar",
|
|
73509
|
-
vocabulary: {
|
|
73510
|
-
cap: "power-source",
|
|
73511
|
-
stateField: "switchOn"
|
|
73512
|
-
},
|
|
73513
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
73514
|
-
apply: "add",
|
|
73515
|
-
path: "switchOn",
|
|
73516
|
-
sources: [{
|
|
73517
|
-
kind: "switch",
|
|
73518
|
-
transforms: [{ kind: "identity" }]
|
|
73519
|
-
}],
|
|
73520
|
-
companions: [{
|
|
73521
|
-
kind: "constant",
|
|
73522
|
-
path: "offSeconds",
|
|
73523
|
-
value: 10,
|
|
73524
|
-
when: "add",
|
|
73525
|
-
formerValues: [0]
|
|
73526
|
-
}, {
|
|
73527
|
-
kind: "clock",
|
|
73528
|
-
path: "lastFetchedAt",
|
|
73529
|
-
when: "always"
|
|
73530
|
-
}],
|
|
73531
|
-
sourceReport: { status: "assignment-status" }
|
|
73532
|
-
}]
|
|
73533
|
-
};
|
|
73534
|
-
var deviceInput$1 = object({ deviceId: number$1().int().nonnegative() });
|
|
73535
|
-
var powerSourceCapability = {
|
|
73536
|
-
name: "power-source",
|
|
73537
|
-
scope: "device",
|
|
73538
|
-
mode: "singleton",
|
|
73539
|
-
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
73540
|
-
methods: {
|
|
73541
|
-
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }),
|
|
73542
|
-
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
73543
|
-
kind: "mutation",
|
|
73544
|
-
auth: "admin"
|
|
73545
|
-
}),
|
|
73546
|
-
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
73547
|
-
kind: "mutation",
|
|
73548
|
-
auth: "admin"
|
|
73549
|
-
}),
|
|
73550
|
-
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
73551
|
-
kind: "mutation",
|
|
73552
|
-
auth: "admin"
|
|
73553
|
-
})
|
|
73554
|
-
},
|
|
73555
|
-
runtimeState: PowerSourceStateSchema,
|
|
73556
|
-
customize: powerSourceCustomize,
|
|
73557
|
-
/**
|
|
73558
|
-
* Runtime-state durability: **session** — mirrored from the linked switch on
|
|
73559
|
-
* its next report; a restored value would outlive the reading it names.
|
|
73560
|
-
*
|
|
73561
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
73562
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
73563
|
-
*/
|
|
73564
|
-
durability: "session",
|
|
73565
|
-
history: "none"
|
|
73566
|
-
};
|
|
73567
74549
|
/**
|
|
73568
74550
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
73569
74551
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -77030,9 +78012,7 @@ var RUNNING_CUSTOMIZE_DEVICE_TYPES = [
|
|
|
77030
78012
|
DeviceType.Humidifier,
|
|
77031
78013
|
DeviceType.Fan,
|
|
77032
78014
|
DeviceType.Climate,
|
|
77033
|
-
DeviceType.
|
|
77034
|
-
DeviceType.PetFeeder,
|
|
77035
|
-
DeviceType.Heater
|
|
78015
|
+
DeviceType.PetFeeder
|
|
77036
78016
|
];
|
|
77037
78017
|
var runningStateCapability = {
|
|
77038
78018
|
name: "running-state",
|
|
@@ -78304,6 +79284,13 @@ var SsoBridgeClaimsSchema = object({
|
|
|
78304
79284
|
/** OAuth authorization-code binding — set only on `oauth-code` tokens. */
|
|
78305
79285
|
redirectUri: string().optional(),
|
|
78306
79286
|
integrationId: string().optional(),
|
|
79287
|
+
/**
|
|
79288
|
+
* The integration INSTANCE an OAuth link was made for (planning#415). Set on
|
|
79289
|
+
* `oauth-code`, `oauth-access` and `oauth-refresh` tokens of a link made
|
|
79290
|
+
* with `instance=`; absent on a link to the default instance. A cloud relay
|
|
79291
|
+
* reads it off the access token to route; the hub answers from the session.
|
|
79292
|
+
*/
|
|
79293
|
+
instanceId: string().optional(),
|
|
78307
79294
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
78308
79295
|
jti: string().optional(),
|
|
78309
79296
|
/** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
|
|
@@ -79120,8 +80107,7 @@ var switchCapability = {
|
|
|
79120
80107
|
DeviceType.Switch,
|
|
79121
80108
|
DeviceType.Siren,
|
|
79122
80109
|
DeviceType.Light,
|
|
79123
|
-
DeviceType.Fan
|
|
79124
|
-
DeviceType.Heater
|
|
80110
|
+
DeviceType.Fan
|
|
79125
80111
|
],
|
|
79126
80112
|
methods: { setState: method(object({
|
|
79127
80113
|
deviceId: number$1(),
|
|
@@ -79815,7 +80801,7 @@ var temperatureSensorCapability = {
|
|
|
79815
80801
|
scope: "device",
|
|
79816
80802
|
deviceNative: true,
|
|
79817
80803
|
mode: "singleton",
|
|
79818
|
-
deviceTypes: [DeviceType.Sensor, DeviceType.
|
|
80804
|
+
deviceTypes: [DeviceType.Sensor, DeviceType.Climate],
|
|
79819
80805
|
methods: {},
|
|
79820
80806
|
status: {
|
|
79821
80807
|
schema: TemperatureSensorStatusSchema,
|
|
@@ -80683,6 +81669,8 @@ var OauthSessionSummarySchema = object({
|
|
|
80683
81669
|
userId: string(),
|
|
80684
81670
|
username: string(),
|
|
80685
81671
|
integrationId: string(),
|
|
81672
|
+
/** The instance the link was made for; `null` = the default instance. */
|
|
81673
|
+
instanceId: string().nullable().optional(),
|
|
80686
81674
|
scopes: array(TokenScopeSchema),
|
|
80687
81675
|
createdAt: number$1(),
|
|
80688
81676
|
lastUsedAt: number$1(),
|
|
@@ -80782,6 +81770,9 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
|
|
|
80782
81770
|
access: "view"
|
|
80783
81771
|
}), method(object({
|
|
80784
81772
|
integrationId: string(),
|
|
81773
|
+
/** The integration instance the link is for. Omit for the default
|
|
81774
|
+
* instance — never pass the addon id to mean it. */
|
|
81775
|
+
instanceId: string().optional(),
|
|
80785
81776
|
userId: string(),
|
|
80786
81777
|
username: string(),
|
|
80787
81778
|
scopes: array(TokenScopeSchema),
|
|
@@ -80820,7 +81811,13 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(_void(), ob
|
|
|
80820
81811
|
}), method(object({ token: string() }), object({
|
|
80821
81812
|
userId: string(),
|
|
80822
81813
|
username: string(),
|
|
80823
|
-
scopes: array(TokenScopeSchema)
|
|
81814
|
+
scopes: array(TokenScopeSchema),
|
|
81815
|
+
/** The instance the link was made for, read from the SESSION.
|
|
81816
|
+
* Absent = the default instance. `oauthLinkServesInstance` is the
|
|
81817
|
+
* helper a per-instance route calls to compare it with the instance
|
|
81818
|
+
* of its path; no route declares `perInstance` yet (the Alexa
|
|
81819
|
+
* per-instance route, T3, is the first consumer). */
|
|
81820
|
+
instanceId: string().optional()
|
|
80824
81821
|
}).nullable(), { access: "view" }), method(object({ userId: string() }), array(ExternalIdentitySchema), { auth: "admin" }), method(ExternalIdentityKeySchema, object({
|
|
80825
81822
|
userId: string(),
|
|
80826
81823
|
issuer: string()
|
|
@@ -81462,6 +82459,13 @@ var waterHeaterCapability = {
|
|
|
81462
82459
|
deviceNative: true,
|
|
81463
82460
|
mode: "singleton",
|
|
81464
82461
|
deviceTypes: [DeviceType.WaterHeater],
|
|
82462
|
+
power: {
|
|
82463
|
+
kind: "mode",
|
|
82464
|
+
field: "operationMode",
|
|
82465
|
+
offValue: "off",
|
|
82466
|
+
set: "setOperationMode",
|
|
82467
|
+
lastField: "lastActiveMode"
|
|
82468
|
+
},
|
|
81465
82469
|
methods: {
|
|
81466
82470
|
setTargetTemp: method(object({
|
|
81467
82471
|
deviceId: number$1().int().nonnegative(),
|
|
@@ -83120,7 +84124,6 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
83120
84124
|
control: controlCapability,
|
|
83121
84125
|
cover: coverCapability,
|
|
83122
84126
|
dayNight: dayNightCapability,
|
|
83123
|
-
deviceDiscovery: deviceDiscoveryCapability,
|
|
83124
84127
|
deviceStatus: deviceStatusCapability,
|
|
83125
84128
|
dishwasher: dishwasherCapability,
|
|
83126
84129
|
doNotDisturb: doNotDisturbCapability,
|
|
@@ -84215,15 +85218,16 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
84215
85218
|
});
|
|
84216
85219
|
const meta = await this.ctx.api.deviceManager.loadMeta.query({ deviceId: id });
|
|
84217
85220
|
if (meta === null) throw new Error(`${this.providerName}: no persisted meta for "${stableId}" (id ${id}) — cannot reload`);
|
|
84218
|
-
const
|
|
84219
|
-
|
|
85221
|
+
const Class = this.deviceClassForRow({
|
|
85222
|
+
type: meta.type,
|
|
85223
|
+
stableId
|
|
85224
|
+
});
|
|
84220
85225
|
if (!Class) throw new Error(`${this.providerName}: no device class for type "${meta.type}" — "${stableId}" is an accessory child; reload its parent instead`);
|
|
84221
85226
|
await devices.create(stableId, Class, {}, meta.parentDeviceId ?? null);
|
|
84222
85227
|
const rows = await this.ctx.api.deviceManager.listPersistedByAddon.query({ addonId: this.addonId });
|
|
84223
85228
|
for (const row of rows) {
|
|
84224
85229
|
if (row.parentDeviceId !== id) continue;
|
|
84225
|
-
const
|
|
84226
|
-
const ChildClass = childType !== void 0 ? this.deviceClasses[childType] : void 0;
|
|
85230
|
+
const ChildClass = this.deviceClassForRow(row);
|
|
84227
85231
|
if (!ChildClass) continue;
|
|
84228
85232
|
try {
|
|
84229
85233
|
await devices.create(row.stableId, ChildClass, {}, id);
|
|
@@ -84250,6 +85254,16 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
84250
85254
|
return { deviceId: id };
|
|
84251
85255
|
}
|
|
84252
85256
|
/**
|
|
85257
|
+
* The class a PERSISTED row is rebuilt with. Default: by type, from
|
|
85258
|
+
* `deviceClasses`. A provider whose distinct devices share one type (D1033:
|
|
85259
|
+
* a radiator valve and a room climate are both `climate`) overrides this
|
|
85260
|
+
* and tells them apart by the row's stable id or config.
|
|
85261
|
+
*/
|
|
85262
|
+
deviceClassForRow(row) {
|
|
85263
|
+
const type = Object.values(DeviceType).find((t) => t === row.type);
|
|
85264
|
+
return type !== void 0 ? this.deviceClasses[type] : void 0;
|
|
85265
|
+
}
|
|
85266
|
+
/**
|
|
84253
85267
|
* Restore devices from persisted state. Two-pass:
|
|
84254
85268
|
*
|
|
84255
85269
|
* 1. **Top-level pass** — invokes `kernel.devices.create()` for every
|
|
@@ -84310,7 +85324,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
84310
85324
|
restored.add(saved.id);
|
|
84311
85325
|
return;
|
|
84312
85326
|
}
|
|
84313
|
-
const Class = this.
|
|
85327
|
+
const Class = this.deviceClassForRow(saved);
|
|
84314
85328
|
if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
|
|
84315
85329
|
if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
|
|
84316
85330
|
await this.healSavedConfig(saved, savedDevices);
|
|
@@ -84327,7 +85341,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
84327
85341
|
};
|
|
84328
85342
|
const topLevel = savedDevices.filter((saved) => saved.parentDeviceId === null);
|
|
84329
85343
|
const restoreOne = async (saved) => {
|
|
84330
|
-
if (!this.
|
|
85344
|
+
if (!this.deviceClassForRow(saved)) {
|
|
84331
85345
|
this.ctx.logger.warn("No device class registered for restored type — skipping", {
|
|
84332
85346
|
tags: {
|
|
84333
85347
|
deviceId: saved.id,
|
|
@@ -84366,7 +85380,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
84366
85380
|
const failedTopLevelIds = new Set(failures.map((failure) => failure.saved.id));
|
|
84367
85381
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
84368
85382
|
for (const saved of childRows) {
|
|
84369
|
-
if (!this.
|
|
85383
|
+
if (!this.deviceClassForRow(saved)) continue;
|
|
84370
85384
|
if (saved.parentDeviceId === null) continue;
|
|
84371
85385
|
if (restored.has(saved.parentDeviceId)) {
|
|
84372
85386
|
try {
|
|
@@ -84420,7 +85434,9 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
84420
85434
|
}
|
|
84421
85435
|
};
|
|
84422
85436
|
DeviceType.HeatPump;
|
|
84423
|
-
DeviceType.
|
|
85437
|
+
DeviceType.Climate;
|
|
85438
|
+
DeviceRole.ClimateThermostat, DeviceRole.ClimateAc, DeviceRole.ClimateRadiator, DeviceRole.ClimateFloor, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit;
|
|
85439
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, 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;
|
|
84424
85440
|
new Set(Object.values(DeviceType));
|
|
84425
85441
|
/** Parses the raw slice with the cap's own schema; `undefined` when it does not parse. */
|
|
84426
85442
|
function contribution(cap, schema, build) {
|
|
@@ -86426,7 +87442,7 @@ var HEAT_PUMP_FAMILY_CONTRIBUTIONS = { "heat-pump-status": contribution("heat-pu
|
|
|
86426
87442
|
...Object.values(CLIMATE_FAMILY_CONTRIBUTIONS),
|
|
86427
87443
|
...Object.values(HEAT_PUMP_FAMILY_CONTRIBUTIONS)
|
|
86428
87444
|
];
|
|
86429
|
-
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.
|
|
87445
|
+
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
|
|
86430
87446
|
/**
|
|
86431
87447
|
* Who looks like what (D1023). One look per {@link DeviceType}, refined by a
|
|
86432
87448
|
* {@link DeviceRole} where the role changes how the device reads (a leak
|
|
@@ -87070,9 +88086,9 @@ lookOf({
|
|
|
87070
88086
|
idle: INFO,
|
|
87071
88087
|
standby: MUTED
|
|
87072
88088
|
});
|
|
87073
|
-
DeviceType.Climate
|
|
87074
|
-
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.
|
|
87075
|
-
DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
|
|
88089
|
+
DeviceType.Climate;
|
|
88090
|
+
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
|
|
88091
|
+
DeviceRole.Siren, DeviceRole.Floodlight, DeviceRole.Spotlight, DeviceRole.PirSensor, DeviceRole.Chime, DeviceRole.Autotrack, DeviceRole.Nightvision, DeviceRole.PrivacyMask, DeviceRole.Doorbell, DeviceRole.CoverBlind, DeviceRole.CoverShutter, DeviceRole.CoverAwning, DeviceRole.CoverCurtain, DeviceRole.CoverGarage, DeviceRole.CoverGate, DeviceRole.CoverDoor, DeviceRole.CoverWindow, DeviceRole.BinaryHelper, DeviceRole.Outlet, DeviceRole.SwitchHeater, DeviceRole.SwitchFan, DeviceRole.ClimateAc, DeviceRole.ClimateThermostat, DeviceRole.ClimateFloor, DeviceRole.ClimateRadiator, DeviceRole.ClimateHeater, DeviceRole.ClimateHeatPumpCircuit, DeviceRole.MotionSensor, DeviceRole.ContactSensor, DeviceRole.LeakSensor, DeviceRole.SmokeSensor, DeviceRole.COSensor, DeviceRole.GasSensor, DeviceRole.TamperSensor, DeviceRole.VibrationSensor, DeviceRole.ConnectivitySensor, DeviceRole.SoundSensor, DeviceRole.BinarySensor, DeviceRole.TemperatureSensor, DeviceRole.HumiditySensor, DeviceRole.AmbientLightSensor, DeviceRole.PressureSensor, DeviceRole.WindSensor, DeviceRole.RainSensor, DeviceRole.UvSensor, DeviceRole.SolarRadiationSensor, DeviceRole.SoilMoistureSensor, DeviceRole.PowerSensor, DeviceRole.EnergySensor, DeviceRole.VoltageSensor, DeviceRole.CurrentSensor, DeviceRole.AirQualitySensor, DeviceRole.BatterySensor, DeviceRole.NumericSensor, DeviceRole.EnumSensor, DeviceRole.DateTimeSensor, DeviceRole.GenericSensor, DeviceRole.NumericControl, DeviceRole.SelectControl, DeviceRole.TextControl, DeviceRole.DateTimeControl, DeviceRole.MobilePushNotifier, DeviceRole.MessagingNotifier, DeviceRole.EmailNotifier, DeviceRole.GenericNotifier, DeviceRole.IrTransmitter;
|
|
87076
88092
|
Object.freeze({
|
|
87077
88093
|
"accessories.getStatus": {
|
|
87078
88094
|
capName: "accessories",
|
|
@@ -88299,6 +89315,27 @@ Object.freeze({
|
|
|
88299
89315
|
access: "create",
|
|
88300
89316
|
role: "viewer"
|
|
88301
89317
|
},
|
|
89318
|
+
"clusterUpdate.getClusterUpdateJob": {
|
|
89319
|
+
capName: "cluster-update",
|
|
89320
|
+
capScope: "system",
|
|
89321
|
+
addonId: null,
|
|
89322
|
+
access: "view",
|
|
89323
|
+
role: "admin"
|
|
89324
|
+
},
|
|
89325
|
+
"clusterUpdate.previewClusterUpdate": {
|
|
89326
|
+
capName: "cluster-update",
|
|
89327
|
+
capScope: "system",
|
|
89328
|
+
addonId: null,
|
|
89329
|
+
access: "view",
|
|
89330
|
+
role: "admin"
|
|
89331
|
+
},
|
|
89332
|
+
"clusterUpdate.startClusterUpdate": {
|
|
89333
|
+
capName: "cluster-update",
|
|
89334
|
+
capScope: "system",
|
|
89335
|
+
addonId: null,
|
|
89336
|
+
access: "create",
|
|
89337
|
+
role: "admin"
|
|
89338
|
+
},
|
|
88302
89339
|
"color.getStatus": {
|
|
88303
89340
|
capName: "color",
|
|
88304
89341
|
capScope: "device",
|
|
@@ -88901,41 +89938,6 @@ Object.freeze({
|
|
|
88901
89938
|
access: "create",
|
|
88902
89939
|
role: "admin"
|
|
88903
89940
|
},
|
|
88904
|
-
"deviceDiscovery.adoptDevice": {
|
|
88905
|
-
capName: "device-discovery",
|
|
88906
|
-
capScope: "device",
|
|
88907
|
-
addonId: null,
|
|
88908
|
-
access: "create",
|
|
88909
|
-
role: "admin"
|
|
88910
|
-
},
|
|
88911
|
-
"deviceDiscovery.getStatus": {
|
|
88912
|
-
capName: "device-discovery",
|
|
88913
|
-
capScope: "device",
|
|
88914
|
-
addonId: null,
|
|
88915
|
-
access: "view",
|
|
88916
|
-
role: "viewer"
|
|
88917
|
-
},
|
|
88918
|
-
"deviceDiscovery.listDiscovered": {
|
|
88919
|
-
capName: "device-discovery",
|
|
88920
|
-
capScope: "device",
|
|
88921
|
-
addonId: null,
|
|
88922
|
-
access: "view",
|
|
88923
|
-
role: "viewer"
|
|
88924
|
-
},
|
|
88925
|
-
"deviceDiscovery.refreshDiscovery": {
|
|
88926
|
-
capName: "device-discovery",
|
|
88927
|
-
capScope: "device",
|
|
88928
|
-
addonId: null,
|
|
88929
|
-
access: "create",
|
|
88930
|
-
role: "admin"
|
|
88931
|
-
},
|
|
88932
|
-
"deviceDiscovery.releaseDevice": {
|
|
88933
|
-
capName: "device-discovery",
|
|
88934
|
-
capScope: "device",
|
|
88935
|
-
addonId: null,
|
|
88936
|
-
access: "create",
|
|
88937
|
-
role: "admin"
|
|
88938
|
-
},
|
|
88939
89941
|
"deviceExport.applyDeviceSettingsPatch": {
|
|
88940
89942
|
capName: "device-export",
|
|
88941
89943
|
capScope: "system",
|
|
@@ -89671,6 +90673,13 @@ Object.freeze({
|
|
|
89671
90673
|
access: "create",
|
|
89672
90674
|
role: "admin"
|
|
89673
90675
|
},
|
|
90676
|
+
"deviceManager.search": {
|
|
90677
|
+
capName: "device-manager",
|
|
90678
|
+
capScope: "system",
|
|
90679
|
+
addonId: null,
|
|
90680
|
+
access: "view",
|
|
90681
|
+
role: "viewer"
|
|
90682
|
+
},
|
|
89674
90683
|
"deviceManager.setAlias": {
|
|
89675
90684
|
capName: "device-manager",
|
|
89676
90685
|
capScope: "system",
|
|
@@ -89762,6 +90771,13 @@ Object.freeze({
|
|
|
89762
90771
|
access: "create",
|
|
89763
90772
|
role: "admin"
|
|
89764
90773
|
},
|
|
90774
|
+
"deviceManager.setPrimaryChildPreference": {
|
|
90775
|
+
capName: "device-manager",
|
|
90776
|
+
capScope: "system",
|
|
90777
|
+
addonId: null,
|
|
90778
|
+
access: "create",
|
|
90779
|
+
role: "admin"
|
|
90780
|
+
},
|
|
89765
90781
|
"deviceManager.setRole": {
|
|
89766
90782
|
capName: "device-manager",
|
|
89767
90783
|
capScope: "system",
|
|
@@ -90469,6 +91485,13 @@ Object.freeze({
|
|
|
90469
91485
|
access: "create",
|
|
90470
91486
|
role: "viewer"
|
|
90471
91487
|
},
|
|
91488
|
+
"fanControl.setOscillationAxis": {
|
|
91489
|
+
capName: "fan-control",
|
|
91490
|
+
capScope: "device",
|
|
91491
|
+
addonId: null,
|
|
91492
|
+
access: "create",
|
|
91493
|
+
role: "viewer"
|
|
91494
|
+
},
|
|
90472
91495
|
"fanControl.setPercentage": {
|
|
90473
91496
|
capName: "fan-control",
|
|
90474
91497
|
capScope: "device",
|
|
@@ -94401,28 +95424,28 @@ Object.freeze({
|
|
|
94401
95424
|
capScope: "device",
|
|
94402
95425
|
addonId: null,
|
|
94403
95426
|
access: "create",
|
|
94404
|
-
role: "
|
|
95427
|
+
role: "viewer"
|
|
94405
95428
|
},
|
|
94406
95429
|
"powerSource.getStatus": {
|
|
94407
95430
|
capName: "power-source",
|
|
94408
95431
|
capScope: "device",
|
|
94409
95432
|
addonId: null,
|
|
94410
95433
|
access: "view",
|
|
94411
|
-
role: "
|
|
95434
|
+
role: "viewer"
|
|
94412
95435
|
},
|
|
94413
95436
|
"powerSource.powerOff": {
|
|
94414
95437
|
capName: "power-source",
|
|
94415
95438
|
capScope: "device",
|
|
94416
95439
|
addonId: null,
|
|
94417
95440
|
access: "create",
|
|
94418
|
-
role: "
|
|
95441
|
+
role: "viewer"
|
|
94419
95442
|
},
|
|
94420
95443
|
"powerSource.powerOn": {
|
|
94421
95444
|
capName: "power-source",
|
|
94422
95445
|
capScope: "device",
|
|
94423
95446
|
addonId: null,
|
|
94424
95447
|
access: "create",
|
|
94425
|
-
role: "
|
|
95448
|
+
role: "viewer"
|
|
94426
95449
|
},
|
|
94427
95450
|
"presence.getStatus": {
|
|
94428
95451
|
capName: "presence",
|
|
@@ -97490,7 +98513,7 @@ function projectAction(cap, action) {
|
|
|
97490
98513
|
};
|
|
97491
98514
|
}
|
|
97492
98515
|
Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
|
|
97493
|
-
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.
|
|
98516
|
+
DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, 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.HeatPump, 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;
|
|
97494
98517
|
DeviceFeature.BatteryOperated;
|
|
97495
98518
|
function bound(cap, methods = [], authority = "reported") {
|
|
97496
98519
|
return {
|
|
@@ -97979,35 +99002,6 @@ Object.freeze({
|
|
|
97979
99002
|
form: "single",
|
|
97980
99003
|
optional: false
|
|
97981
99004
|
}],
|
|
97982
|
-
"deviceDiscovery.adoptDevice": [{
|
|
97983
|
-
name: "deviceId",
|
|
97984
|
-
form: "single",
|
|
97985
|
-
optional: false
|
|
97986
|
-
}],
|
|
97987
|
-
"deviceDiscovery.getStatus": [{
|
|
97988
|
-
name: "deviceId",
|
|
97989
|
-
form: "single",
|
|
97990
|
-
optional: false
|
|
97991
|
-
}],
|
|
97992
|
-
"deviceDiscovery.listDiscovered": [{
|
|
97993
|
-
name: "deviceId",
|
|
97994
|
-
form: "single",
|
|
97995
|
-
optional: false
|
|
97996
|
-
}],
|
|
97997
|
-
"deviceDiscovery.refreshDiscovery": [{
|
|
97998
|
-
name: "deviceId",
|
|
97999
|
-
form: "single",
|
|
98000
|
-
optional: false
|
|
98001
|
-
}],
|
|
98002
|
-
"deviceDiscovery.releaseDevice": [{
|
|
98003
|
-
name: "childDeviceId",
|
|
98004
|
-
form: "single",
|
|
98005
|
-
optional: false
|
|
98006
|
-
}, {
|
|
98007
|
-
name: "deviceId",
|
|
98008
|
-
form: "single",
|
|
98009
|
-
optional: false
|
|
98010
|
-
}],
|
|
98011
99005
|
"deviceExport.applyDeviceSettingsPatch": [{
|
|
98012
99006
|
name: "deviceId",
|
|
98013
99007
|
form: "single",
|
|
@@ -98286,6 +99280,11 @@ Object.freeze({
|
|
|
98286
99280
|
form: "single",
|
|
98287
99281
|
optional: false
|
|
98288
99282
|
}],
|
|
99283
|
+
"deviceManager.setPrimaryChildPreference": [{
|
|
99284
|
+
name: "deviceId",
|
|
99285
|
+
form: "single",
|
|
99286
|
+
optional: false
|
|
99287
|
+
}],
|
|
98289
99288
|
"deviceManager.setRole": [{
|
|
98290
99289
|
name: "deviceId",
|
|
98291
99290
|
form: "single",
|
|
@@ -98566,6 +99565,11 @@ Object.freeze({
|
|
|
98566
99565
|
form: "single",
|
|
98567
99566
|
optional: false
|
|
98568
99567
|
}],
|
|
99568
|
+
"fanControl.setOscillationAxis": [{
|
|
99569
|
+
name: "deviceId",
|
|
99570
|
+
form: "single",
|
|
99571
|
+
optional: false
|
|
99572
|
+
}],
|
|
98569
99573
|
"fanControl.setPercentage": [{
|
|
98570
99574
|
name: "deviceId",
|
|
98571
99575
|
form: "single",
|
|
@@ -101754,7 +102758,9 @@ var TimelapseRuleInputSchema = object({
|
|
|
101754
102758
|
/** Classes the notification counts — see {@link ReportClassesField}. */
|
|
101755
102759
|
reportClasses: ReportClassesField.optional(),
|
|
101756
102760
|
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
101757
|
-
priority: PriorityField.default(3)
|
|
102761
|
+
priority: PriorityField.default(3),
|
|
102762
|
+
/** Per-target settings keyed by target id (D1032) — absent = every target follows the rule. */
|
|
102763
|
+
targetOverrides: NcTargetOverridesSchema.optional()
|
|
101758
102764
|
});
|
|
101759
102765
|
object({
|
|
101760
102766
|
name: NameField.optional(),
|
|
@@ -101770,7 +102776,9 @@ object({
|
|
|
101770
102776
|
previewText: PreviewTextField.optional(),
|
|
101771
102777
|
previewMode: PreviewModeField.optional(),
|
|
101772
102778
|
reportClasses: ReportClassesField.optional(),
|
|
101773
|
-
priority: PriorityField.optional()
|
|
102779
|
+
priority: PriorityField.optional(),
|
|
102780
|
+
/** Per-target settings keyed by target id (D1032) — absent = every target follows the rule. */
|
|
102781
|
+
targetOverrides: NcTargetOverridesSchema.optional()
|
|
101774
102782
|
});
|
|
101775
102783
|
TimelapseRuleInputSchema.extend({
|
|
101776
102784
|
id: string(),
|
|
@@ -102036,6 +103044,27 @@ function ingestReading(input) {
|
|
|
102036
103044
|
value: converted
|
|
102037
103045
|
};
|
|
102038
103046
|
}
|
|
103047
|
+
function collapse(value) {
|
|
103048
|
+
return value.trim().replace(/\s+/g, " ");
|
|
103049
|
+
}
|
|
103050
|
+
/** "KLF-200" → "KLF200". A hyphen between words ("Home Hub") stays. */
|
|
103051
|
+
function compactModelToken(value) {
|
|
103052
|
+
return value.replace(/([A-Za-z])-(\d)/g, "$1$2");
|
|
103053
|
+
}
|
|
103054
|
+
/**
|
|
103055
|
+
* A Gateway device: "<Brand> <Model>". An unknown model is the brand alone
|
|
103056
|
+
* (the caller passes the brand-level fallback it already has, e.g. "Velux KLF-200").
|
|
103057
|
+
*/
|
|
103058
|
+
function gatewayDeviceName(brand, model) {
|
|
103059
|
+
const brandPart = compactModelToken(collapse(brand));
|
|
103060
|
+
const raw = collapse(model ?? "");
|
|
103061
|
+
if (raw.length === 0) return brandPart;
|
|
103062
|
+
const modelPart = compactModelToken(raw);
|
|
103063
|
+
const prefix = `${brandPart.toLowerCase()} `;
|
|
103064
|
+
if (modelPart.toLowerCase() === brandPart.toLowerCase()) return brandPart;
|
|
103065
|
+
if (modelPart.toLowerCase().startsWith(prefix)) return modelPart;
|
|
103066
|
+
return `${brandPart} ${modelPart}`;
|
|
103067
|
+
}
|
|
102039
103068
|
/**
|
|
102040
103069
|
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
102041
103070
|
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
@@ -102136,13 +103165,15 @@ object({
|
|
|
102136
103165
|
* settings of the integration itself, not of a device. Every equipment the box reports is
|
|
102137
103166
|
* adopted as a Container from it.
|
|
102138
103167
|
*
|
|
102139
|
-
*
|
|
103168
|
+
* Sources behind `source`:
|
|
103169
|
+
* - `blazor`: the box's own web-page channel (Blazor Server, firmware 8.51 and later),
|
|
103170
|
+
* read by an embedded Python bridge. No token, nothing written to the box.
|
|
102140
103171
|
* - `influx`: the box's InfluxDB 2.x (port 8086, org `enpal`, bucket `solar`).
|
|
102141
103172
|
* Needs the access token Enpal support issues; it is a keychain secret.
|
|
102142
103173
|
* - `page`: the box's local device page (`/deviceMessages`). No token; fewer
|
|
102143
103174
|
* boxes and firmware versions still serve it.
|
|
102144
|
-
* - `auto` (default): InfluxDB when a token is set, the page
|
|
102145
|
-
*
|
|
103175
|
+
* - `auto` (default): `blazor`, then InfluxDB when a token is set, then the page; the first
|
|
103176
|
+
* one that answers wins and the snapshot records why a preferred one was skipped.
|
|
102146
103177
|
*/
|
|
102147
103178
|
/** Strip a pasted scheme and path: `http://192.0.2.10/deviceMessages` -> `192.0.2.10`. */
|
|
102148
103179
|
function cleanHost(value) {
|
|
@@ -102160,6 +103191,7 @@ var FLUX_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
|
102160
103191
|
var ENPAL_SYSTEM = "enpal";
|
|
102161
103192
|
var ENPAL_SOURCES = [
|
|
102162
103193
|
"auto",
|
|
103194
|
+
"blazor",
|
|
102163
103195
|
"influx",
|
|
102164
103196
|
"page"
|
|
102165
103197
|
];
|
|
@@ -102220,7 +103252,7 @@ function buildConnectionFormSchema() {
|
|
|
102220
103252
|
id: "source",
|
|
102221
103253
|
title: "Data source",
|
|
102222
103254
|
titleKey: "provider-enpal:connection.s_source.title",
|
|
102223
|
-
description: "
|
|
103255
|
+
description: "The box's own web-page channel needs no credential and is used first. InfluxDB is used only with the access token Enpal support issues; the device page is the last resort.",
|
|
102224
103256
|
descriptionKey: "provider-enpal:connection.s_source.description",
|
|
102225
103257
|
columns: 2,
|
|
102226
103258
|
fields: [
|
|
@@ -102233,9 +103265,14 @@ function buildConnectionFormSchema() {
|
|
|
102233
103265
|
options: [
|
|
102234
103266
|
{
|
|
102235
103267
|
value: "auto",
|
|
102236
|
-
label: "Automatic (InfluxDB, then device page)",
|
|
103268
|
+
label: "Automatic (box web channel, InfluxDB, then device page)",
|
|
102237
103269
|
labelKey: "provider-enpal:connection.s_source.source.o_auto.label"
|
|
102238
103270
|
},
|
|
103271
|
+
{
|
|
103272
|
+
value: "blazor",
|
|
103273
|
+
label: "Box web channel only (firmware 8.51 and later)",
|
|
103274
|
+
labelKey: "provider-enpal:connection.s_source.source.o_blazor.label"
|
|
103275
|
+
},
|
|
102239
103276
|
{
|
|
102240
103277
|
value: "influx",
|
|
102241
103278
|
label: "InfluxDB only",
|
|
@@ -102351,6 +103388,22 @@ var point = (quantity, ...sources) => ({
|
|
|
102351
103388
|
quantity,
|
|
102352
103389
|
sources: sources.map((s) => typeof s === "string" ? { key: s } : s)
|
|
102353
103390
|
});
|
|
103391
|
+
var numeric = (id, name, quantity, key, role = DeviceRole.NumericSensor) => ({
|
|
103392
|
+
kind: "numeric",
|
|
103393
|
+
id,
|
|
103394
|
+
name,
|
|
103395
|
+
role,
|
|
103396
|
+
quantity,
|
|
103397
|
+
value: point(quantity, key)
|
|
103398
|
+
});
|
|
103399
|
+
var text = (id, name, key, timestamp = false) => ({
|
|
103400
|
+
kind: "text",
|
|
103401
|
+
id,
|
|
103402
|
+
name,
|
|
103403
|
+
role: timestamp ? DeviceRole.DateTimeSensor : DeviceRole.EnumSensor,
|
|
103404
|
+
key,
|
|
103405
|
+
...timestamp ? { timestamp: true } : {}
|
|
103406
|
+
});
|
|
102354
103407
|
/**
|
|
102355
103408
|
* What an Enpal box can show, by the dotted keys its InfluxDB and device page use (the
|
|
102356
103409
|
* names follow the Home Assistant component's key table). A device is offered only when the
|
|
@@ -102433,7 +103486,7 @@ var ENPAL_SPECS = [
|
|
|
102433
103486
|
id: "house",
|
|
102434
103487
|
name: "Consumption",
|
|
102435
103488
|
role: DeviceRole.PowerSensor,
|
|
102436
|
-
power: point("power", "Power.Consumption.Total"
|
|
103489
|
+
power: point("power", "Power.Consumption.Total"),
|
|
102437
103490
|
kwh: point("energy", "Energy.Consumption.Total.Lifetime")
|
|
102438
103491
|
},
|
|
102439
103492
|
{
|
|
@@ -102483,7 +103536,87 @@ var ENPAL_SPECS = [
|
|
|
102483
103536
|
role: DeviceRole.NumericSensor,
|
|
102484
103537
|
quantity: "percentage",
|
|
102485
103538
|
value: point("percentage", "Battery.SOH")
|
|
102486
|
-
}
|
|
103539
|
+
},
|
|
103540
|
+
numeric("pv-string-1-power", "String 1 power", "power", "Power.DC.String.1", DeviceRole.PowerSensor),
|
|
103541
|
+
numeric("pv-string-2-power", "String 2 power", "power", "Power.DC.String.2", DeviceRole.PowerSensor),
|
|
103542
|
+
numeric("pv-energy-day", "Production today", "energy", "Energy.Production.Total.Day", DeviceRole.EnergySensor),
|
|
103543
|
+
numeric("pv-string-1-voltage", "String 1 voltage", "voltage", "Voltage.String.1", DeviceRole.VoltageSensor),
|
|
103544
|
+
numeric("pv-string-2-voltage", "String 2 voltage", "voltage", "Voltage.String.2", DeviceRole.VoltageSensor),
|
|
103545
|
+
numeric("pv-string-1-current", "String 1 current", "current", "Current.String.1", DeviceRole.CurrentSensor),
|
|
103546
|
+
numeric("pv-string-2-current", "String 2 current", "current", "Current.String.2", DeviceRole.CurrentSensor),
|
|
103547
|
+
numeric("battery-stored-energy", "Stored energy", "energy", "Energy.Battery.Charge.Load", DeviceRole.EnergySensor),
|
|
103548
|
+
numeric("battery-charge-day", "Charged today", "energy", "Energy.Battery.Charge.Day", DeviceRole.EnergySensor),
|
|
103549
|
+
numeric("battery-discharge-day", "Discharged today", "energy", "Energy.Battery.Discharge.Day", DeviceRole.EnergySensor),
|
|
103550
|
+
numeric("battery-capacity", "Capacity", "energy", "Energy.Battery.Capacity", DeviceRole.EnergySensor),
|
|
103551
|
+
numeric("battery-power-rating", "Maximum power rating", "power", "Battery.Power.Rating.Maximum", DeviceRole.PowerSensor),
|
|
103552
|
+
numeric("battery-level-max", "Maximum charge level", "percentage", "Battery.ChargeLevel.Max"),
|
|
103553
|
+
numeric("battery-level-min", "Minimum charge level", "percentage", "Battery.ChargeLevel.Min"),
|
|
103554
|
+
numeric("battery-power-set", "Power setpoint", "power", "Power.Battery.Charge.Discharge.Set", DeviceRole.PowerSensor),
|
|
103555
|
+
numeric("battery-charge-power-max", "Maximum charge power", "power", "Power.Battery.Charge.Max", DeviceRole.PowerSensor),
|
|
103556
|
+
numeric("battery-discharge-power-max", "Maximum discharge power", "power", "Power.Battery.Discharge.Max", DeviceRole.PowerSensor),
|
|
103557
|
+
numeric("battery-charge-current-max", "Maximum charge current", "current", "Current.Battery.Charge.Max", DeviceRole.CurrentSensor),
|
|
103558
|
+
numeric("battery-discharge-current-max", "Maximum discharge current", "current", "Current.Battery.Discharge.Max", DeviceRole.CurrentSensor),
|
|
103559
|
+
numeric("grid-phase-a", "Phase A power", "power", "Power.AC.Phase.A", DeviceRole.PowerSensor),
|
|
103560
|
+
numeric("grid-phase-b", "Phase B power", "power", "Power.AC.Phase.B", DeviceRole.PowerSensor),
|
|
103561
|
+
numeric("grid-phase-c", "Phase C power", "power", "Power.AC.Phase.C", DeviceRole.PowerSensor),
|
|
103562
|
+
numeric("grid-import-day", "Imported today", "energy", "Energy.Grid.Import.Day", DeviceRole.EnergySensor),
|
|
103563
|
+
numeric("grid-export-day", "Exported today", "energy", "Energy.Grid.Export.Day", DeviceRole.EnergySensor),
|
|
103564
|
+
numeric("grid-voltage-a", "Phase A voltage", "voltage", "Voltage.Phase.A", DeviceRole.VoltageSensor),
|
|
103565
|
+
numeric("grid-voltage-b", "Phase B voltage", "voltage", "Voltage.Phase.B", DeviceRole.VoltageSensor),
|
|
103566
|
+
numeric("grid-voltage-c", "Phase C voltage", "voltage", "Voltage.Phase.C", DeviceRole.VoltageSensor),
|
|
103567
|
+
{
|
|
103568
|
+
kind: "numeric",
|
|
103569
|
+
id: "grid-frequency",
|
|
103570
|
+
name: "Frequency",
|
|
103571
|
+
role: DeviceRole.NumericSensor,
|
|
103572
|
+
quantity: "index",
|
|
103573
|
+
value: {
|
|
103574
|
+
...point("index", "Frequency.Grid"),
|
|
103575
|
+
ignoreUnit: true
|
|
103576
|
+
}
|
|
103577
|
+
},
|
|
103578
|
+
numeric("grid-feed-max", "Maximum feed-in power", "power", "Power.Grid.Maximum.Feed", DeviceRole.PowerSensor),
|
|
103579
|
+
numeric("house-energy-day", "Consumption today", "energy", "Energy.Consumption.Total.Day", DeviceRole.EnergySensor),
|
|
103580
|
+
text("inverter-state", "System state", "Inverter.System.State"),
|
|
103581
|
+
text("inverter-ems-mode", "EMS mode", "Command.EMS.Mode"),
|
|
103582
|
+
numeric("inverter-ac-power", "AC power", "power", "Power.Inverter.AC.Export", DeviceRole.PowerSensor),
|
|
103583
|
+
text("inverter-start-stop", "Start/stop command", "Command.Start.Stop"),
|
|
103584
|
+
text("inverter-forcible-mode", "Forcible charge/discharge mode", "Mode.Forcible.Charge.Discharge"),
|
|
103585
|
+
numeric("inverter-power-rating", "Maximum power rating", "power", "Inverter.Power.Rating.Maximum", DeviceRole.PowerSensor),
|
|
103586
|
+
text("inverter-phase-type", "Phase type", "Type.Phase.Inverter"),
|
|
103587
|
+
text("inverter-modbus-rtu", "Modbus RTU status", "Inverter.Modbus.Health.Rtu.Status"),
|
|
103588
|
+
text("inverter-modbus-tcp", "Modbus TCP status", "Inverter.Modbus.Health.Tcp.Status"),
|
|
103589
|
+
text("heatpump-hot-water-mode", "Hot water mode", "Heatpump.DomesticHotWater.1.Mode"),
|
|
103590
|
+
text("heatpump-heating-mode", "Heating circuit mode", "Heatpump.HeatingCircuit.1.Mode"),
|
|
103591
|
+
text("heatpump-generator-state", "Generator state", "Heatpump.Generator.State.Running"),
|
|
103592
|
+
text("heatpump-heater-state", "Electric heater state", "Heatpump.ElectricHeater.State.Running"),
|
|
103593
|
+
text("heatpump-energy-management", "Energy management state", "Heatpump.EnergyManagement.State"),
|
|
103594
|
+
text("heatpump-power-desired", "Power requested", "Heatpump.Power.Desired.Enabled"),
|
|
103595
|
+
text("heatpump-pv-optimization", "PV optimisation", "Heatpump.Settings.PvOptimizationStatus"),
|
|
103596
|
+
numeric("heatpump-power-min", "Generator minimum power", "power", "Heatpump.Generator.Power.Min", DeviceRole.PowerSensor),
|
|
103597
|
+
numeric("heatpump-power-max", "Generator maximum power", "power", "Heatpump.Generator.Power.Max", DeviceRole.PowerSensor),
|
|
103598
|
+
text("heatpump-connection", "Local connection", "Heatpump.Local.ConnectionState"),
|
|
103599
|
+
text("box-main-state", "Main state", "IoT.MainState"),
|
|
103600
|
+
numeric("box-cpu-load", "CPU load", "percentage", "Cpu.Load"),
|
|
103601
|
+
numeric("box-memory-usage", "Memory usage", "percentage", "Memory.Usage"),
|
|
103602
|
+
text("box-hardware-check", "Hardware check", "HW.Cronny.Result"),
|
|
103603
|
+
text("eebus-connection", "Connection state", "Eebus.ConnectionState"),
|
|
103604
|
+
text("eebus-discovery", "Discovery state", "Eebus.DiscoveryState"),
|
|
103605
|
+
text("eebus-cert-expiry", "Certificate expiry", "Eebus.CertExpiry", true),
|
|
103606
|
+
text("lte-state", "LTE state", "LTE.State"),
|
|
103607
|
+
text("lte-connection-type", "Connection type", "LTE.Connection.Type"),
|
|
103608
|
+
numeric("lte-quality", "LTE quality", "percentage", "LTE.Quality"),
|
|
103609
|
+
numeric("lte-rsrp", "RSRP", "signal-level", "LTE.RSRP"),
|
|
103610
|
+
numeric("lte-rsrq", "RSRQ", "index", "LTE.RSRQ"),
|
|
103611
|
+
numeric("lte-rssi", "RSSI", "signal-level", "LTE.RSSI"),
|
|
103612
|
+
numeric("lte-snr", "SNR", "index", "LTE.SNR"),
|
|
103613
|
+
text("lte-failover", "Failover result", "LTE.Failover.Result"),
|
|
103614
|
+
text("lte-signal-check", "Signal check", "LTE.Cronny.Result"),
|
|
103615
|
+
text("wifi-state", "Wi-Fi state", "WiFi.State"),
|
|
103616
|
+
numeric("data-lan-down", "LAN download this month", "count", "IoT.Data.Consumption.Lan.Down.Month"),
|
|
103617
|
+
numeric("data-lan-up", "LAN upload this month", "count", "IoT.Data.Consumption.Lan.Up.Month"),
|
|
103618
|
+
numeric("data-lte-down", "LTE download this month", "count", "IoT.Data.Consumption.Lte.Down.Month"),
|
|
103619
|
+
numeric("data-lte-up", "LTE upload this month", "count", "IoT.Data.Consumption.Lte.Up.Month")
|
|
102487
103620
|
];
|
|
102488
103621
|
function findSpec(id) {
|
|
102489
103622
|
return ENPAL_SPECS.find((s) => s.id === id);
|
|
@@ -102495,6 +103628,7 @@ function capsOfSpec(spec) {
|
|
|
102495
103628
|
case "battery": return ["battery", "power-meter"];
|
|
102496
103629
|
case "meter": return ["meter"];
|
|
102497
103630
|
case "numeric": return ["numeric-sensor"];
|
|
103631
|
+
case "text": return ["enum-sensor"];
|
|
102498
103632
|
}
|
|
102499
103633
|
}
|
|
102500
103634
|
function pointsOf(spec) {
|
|
@@ -102508,6 +103642,7 @@ function pointsOf(spec) {
|
|
|
102508
103642
|
];
|
|
102509
103643
|
case "meter":
|
|
102510
103644
|
case "numeric": return [spec.value];
|
|
103645
|
+
case "text": return [];
|
|
102511
103646
|
}
|
|
102512
103647
|
}
|
|
102513
103648
|
function resolve(target, snapshot) {
|
|
@@ -102515,7 +103650,7 @@ function resolve(target, snapshot) {
|
|
|
102515
103650
|
for (const source of target.sources) {
|
|
102516
103651
|
const reading = snapshot.readings.get(source.key);
|
|
102517
103652
|
if (reading === void 0) continue;
|
|
102518
|
-
const converted = toBase(reading.value, reading.unit, target.quantity);
|
|
103653
|
+
const converted = toBase(reading.value, target.ignoreUnit === true ? void 0 : reading.unit, target.quantity);
|
|
102519
103654
|
if (!converted.ok) {
|
|
102520
103655
|
refusal = `${source.key}: ${converted.reason} (unit ${reading.unit ?? "none"})`;
|
|
102521
103656
|
continue;
|
|
@@ -102534,7 +103669,7 @@ function resolve(target, snapshot) {
|
|
|
102534
103669
|
}
|
|
102535
103670
|
/** Specs whose data the box reported: a refused point still counts, the device exists. */
|
|
102536
103671
|
function availableSpecs(snapshot) {
|
|
102537
|
-
return ENPAL_SPECS.filter((spec) => pointsOf(spec).some((p) => resolve(p, snapshot).state !== "absent"));
|
|
103672
|
+
return ENPAL_SPECS.filter((spec) => spec.kind === "text" ? snapshot.texts.has(spec.key) : pointsOf(spec).some((p) => resolve(p, snapshot).state !== "absent"));
|
|
102538
103673
|
}
|
|
102539
103674
|
function numberOf(r) {
|
|
102540
103675
|
return r.state === "value" ? r.value : void 0;
|
|
@@ -102633,6 +103768,17 @@ function computeSlices(spec, snapshot, options, now) {
|
|
|
102633
103768
|
refusals: refusalOf(value)
|
|
102634
103769
|
};
|
|
102635
103770
|
}
|
|
103771
|
+
case "text": {
|
|
103772
|
+
const value = snapshot.texts.get(spec.key);
|
|
103773
|
+
return {
|
|
103774
|
+
...value !== void 0 ? { text: {
|
|
103775
|
+
value,
|
|
103776
|
+
...spec.timestamp === true ? { timestamp: true } : {},
|
|
103777
|
+
lastFetchedAt: now
|
|
103778
|
+
} } : {},
|
|
103779
|
+
refusals: []
|
|
103780
|
+
};
|
|
103781
|
+
}
|
|
102636
103782
|
}
|
|
102637
103783
|
}
|
|
102638
103784
|
//#endregion
|
|
@@ -102730,6 +103876,7 @@ var POWER_CAP = powerMeterCapability.name;
|
|
|
102730
103876
|
var BATTERY_CAP = batteryCapability.name;
|
|
102731
103877
|
var METER_CAP = meterCapability.name;
|
|
102732
103878
|
var NUMERIC_CAP = numericSensorCapability.name;
|
|
103879
|
+
var ENUM_CAP = enumSensorCapability.name;
|
|
102733
103880
|
var POWER_COLD = { lastFetchedAt: 0 };
|
|
102734
103881
|
var BATTERY_COLD = {
|
|
102735
103882
|
level: null,
|
|
@@ -102741,6 +103888,11 @@ var VALUE_COLD = {
|
|
|
102741
103888
|
value: null,
|
|
102742
103889
|
lastFetchedAt: 0
|
|
102743
103890
|
};
|
|
103891
|
+
/** A text has no null: until the box sends one the slice is empty and never read as a state. */
|
|
103892
|
+
var TEXT_COLD = {
|
|
103893
|
+
value: "",
|
|
103894
|
+
lastFetchedAt: 0
|
|
103895
|
+
};
|
|
102744
103896
|
/**
|
|
102745
103897
|
* Persisted config of an Enpal Sensor. `specId` selects the entry of the catalog;
|
|
102746
103898
|
* `integrationId` selects the integration whose box feeds it.
|
|
@@ -102778,7 +103930,8 @@ var EnpalSensorDevice = class extends BaseDevice {
|
|
|
102778
103930
|
this.updateSourceInfo({
|
|
102779
103931
|
id: `${this.integrationId}:${spec.id}`,
|
|
102780
103932
|
system: ENPAL_SYSTEM,
|
|
102781
|
-
...spec.kind === "meter" || spec.kind === "numeric" ? { quantity: spec.quantity } : {}
|
|
103933
|
+
...spec.kind === "meter" || spec.kind === "numeric" ? { quantity: spec.quantity } : {},
|
|
103934
|
+
...spec.kind === "text" && spec.timestamp === true ? { dateTimeShape: "date" } : {}
|
|
102782
103935
|
});
|
|
102783
103936
|
}
|
|
102784
103937
|
async onActivate() {
|
|
@@ -102836,6 +103989,7 @@ var EnpalSensorDevice = class extends BaseDevice {
|
|
|
102836
103989
|
});
|
|
102837
103990
|
}
|
|
102838
103991
|
if (slices.numeric !== void 0) this.runtimeState.setCapState(NUMERIC_CAP, slices.numeric);
|
|
103992
|
+
if (slices.text !== void 0) this.runtimeState.setCapState(ENUM_CAP, slices.text);
|
|
102839
103993
|
}
|
|
102840
103994
|
registerCaps() {
|
|
102841
103995
|
switch (this.spec.kind) {
|
|
@@ -102850,6 +104004,10 @@ var EnpalSensorDevice = class extends BaseDevice {
|
|
|
102850
104004
|
this.ctx.registerNativeCap(meterCapability, { getStatus: async () => this.runtimeState.getCapState(METER_CAP) ?? VALUE_COLD });
|
|
102851
104005
|
this.runtimeState.setCapState(METER_CAP, VALUE_COLD);
|
|
102852
104006
|
return;
|
|
104007
|
+
case "text":
|
|
104008
|
+
this.ctx.registerNativeCap(enumSensorCapability, { getStatus: async () => this.runtimeState.getCapState(ENUM_CAP) ?? TEXT_COLD });
|
|
104009
|
+
this.runtimeState.setCapState(ENUM_CAP, TEXT_COLD);
|
|
104010
|
+
return;
|
|
102853
104011
|
case "numeric":
|
|
102854
104012
|
this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(NUMERIC_CAP) ?? VALUE_COLD });
|
|
102855
104013
|
this.runtimeState.setCapState(NUMERIC_CAP, VALUE_COLD);
|
|
@@ -102879,43 +104037,166 @@ function enpalSensorFactory(ctx) {
|
|
|
102879
104037
|
}
|
|
102880
104038
|
//#endregion
|
|
102881
104039
|
//#region src/enpal-equipment.ts
|
|
102882
|
-
|
|
104040
|
+
var live = (section, ...specIds) => specIds.map((specId) => ({
|
|
104041
|
+
specId,
|
|
104042
|
+
...section !== void 0 ? { section } : {},
|
|
104043
|
+
diagnostic: false
|
|
104044
|
+
}));
|
|
104045
|
+
var diag = (section, ...specIds) => specIds.map((specId) => ({
|
|
104046
|
+
specId,
|
|
104047
|
+
...section !== void 0 ? { section } : {},
|
|
104048
|
+
diagnostic: true
|
|
104049
|
+
}));
|
|
104050
|
+
function equipment(input) {
|
|
104051
|
+
return {
|
|
104052
|
+
...input,
|
|
104053
|
+
specIds: input.children.map((c) => c.specId)
|
|
104054
|
+
};
|
|
104055
|
+
}
|
|
104056
|
+
/**
|
|
104057
|
+
* Three adoptable pieces: the box (the Gateway, with its connection, firmware and
|
|
104058
|
+
* diagnostics), the installation (System: Sungrow inverter, battery, grid, house meter), and
|
|
104059
|
+
* the heat pump as the box reads it. Order is the order the candidates are listed in. Every
|
|
104060
|
+
* catalog entry belongs to one. The ids of the older catalog entries are unchanged.
|
|
104061
|
+
*/
|
|
102883
104062
|
var ENPAL_EQUIPMENT = [
|
|
102884
|
-
{
|
|
102885
|
-
id: "
|
|
102886
|
-
|
|
102887
|
-
|
|
102888
|
-
|
|
102889
|
-
|
|
102890
|
-
|
|
102891
|
-
|
|
102892
|
-
|
|
104063
|
+
equipment({
|
|
104064
|
+
id: "box",
|
|
104065
|
+
kind: "gateway",
|
|
104066
|
+
name: "Enpal Box",
|
|
104067
|
+
children: [
|
|
104068
|
+
...diag("Box", "box-main-state", "box-cpu-load", "box-memory-usage", "box-hardware-check"),
|
|
104069
|
+
...diag("EEBUS", "eebus-connection", "eebus-discovery", "eebus-cert-expiry"),
|
|
104070
|
+
...diag("Connectivity", "lte-state", "lte-connection-type", "lte-quality", "lte-rsrp", "lte-rsrq", "lte-rssi", "lte-snr", "lte-failover", "lte-signal-check", "wifi-state", "data-lan-down", "data-lan-up", "data-lte-down", "data-lte-up")
|
|
104071
|
+
],
|
|
104072
|
+
metadata: [
|
|
104073
|
+
{
|
|
104074
|
+
attribute: "deviceName",
|
|
104075
|
+
key: "Device.Name"
|
|
104076
|
+
},
|
|
104077
|
+
{
|
|
104078
|
+
attribute: "model",
|
|
104079
|
+
key: "IoTEdgeDevice.Device.Type"
|
|
104080
|
+
},
|
|
104081
|
+
{
|
|
104082
|
+
attribute: "serialNumber",
|
|
104083
|
+
key: "IoTEdgeDevice.SerialNumber"
|
|
104084
|
+
},
|
|
104085
|
+
{
|
|
104086
|
+
attribute: "hardware",
|
|
104087
|
+
key: "Generation.HW"
|
|
104088
|
+
},
|
|
104089
|
+
{
|
|
104090
|
+
attribute: "softwareGeneration",
|
|
104091
|
+
key: "Generation.SW"
|
|
104092
|
+
},
|
|
104093
|
+
{
|
|
104094
|
+
attribute: "eebusSki",
|
|
104095
|
+
key: "Eebus.Ski"
|
|
104096
|
+
},
|
|
104097
|
+
{
|
|
104098
|
+
attribute: "modemModel",
|
|
104099
|
+
key: "LTE.Modem.Type"
|
|
104100
|
+
},
|
|
104101
|
+
{
|
|
104102
|
+
attribute: "modemFirmware",
|
|
104103
|
+
key: "LTE.Modem.Firmware.Version"
|
|
104104
|
+
}
|
|
104105
|
+
],
|
|
104106
|
+
primaryChildPreference: []
|
|
104107
|
+
}),
|
|
104108
|
+
equipment({
|
|
104109
|
+
id: "impianto",
|
|
104110
|
+
kind: "container",
|
|
104111
|
+
name: "System",
|
|
104112
|
+
children: [
|
|
104113
|
+
...live("Photovoltaic", "pv", "pv-string-1-power", "pv-string-2-power", "pv-energy-day"),
|
|
104114
|
+
...diag("Photovoltaic", "pv-string-1-voltage", "pv-string-2-voltage", "pv-string-1-current", "pv-string-2-current"),
|
|
104115
|
+
...live("Battery", "battery", "battery-stored-energy", "battery-temperature", "battery-charge-day", "battery-discharge-day", "battery-charged", "battery-discharged"),
|
|
104116
|
+
...diag("Battery", "battery-health", "battery-capacity", "battery-power-rating", "battery-level-max", "battery-level-min", "battery-power-set", "battery-charge-power-max", "battery-discharge-power-max", "battery-charge-current-max", "battery-discharge-current-max"),
|
|
104117
|
+
...live("Grid", "grid-import", "grid-export", "grid-phase-a", "grid-phase-b", "grid-phase-c", "grid-import-day", "grid-export-day"),
|
|
104118
|
+
...diag("Grid", "grid-voltage-a", "grid-voltage-b", "grid-voltage-c", "grid-frequency", "grid-feed-max"),
|
|
104119
|
+
...live("Home", "house", "house-energy-day"),
|
|
104120
|
+
...live("Inverter", "inverter-state", "inverter-ems-mode", "inverter-ac-power"),
|
|
104121
|
+
...diag("Inverter", "inverter-start-stop", "inverter-forcible-mode", "inverter-temperature", "inverter-power-rating", "inverter-phase-type", "inverter-modbus-rtu", "inverter-modbus-tcp")
|
|
104122
|
+
],
|
|
104123
|
+
metadata: [
|
|
104124
|
+
{
|
|
104125
|
+
attribute: "model",
|
|
104126
|
+
key: "Code.Device.Type"
|
|
104127
|
+
},
|
|
104128
|
+
{
|
|
104129
|
+
attribute: "firmware",
|
|
104130
|
+
key: "SoftwareVersion.Service.1"
|
|
104131
|
+
},
|
|
104132
|
+
{
|
|
104133
|
+
attribute: "communicationFirmware",
|
|
104134
|
+
key: "SoftwareVersion.Service.2"
|
|
104135
|
+
},
|
|
104136
|
+
{
|
|
104137
|
+
attribute: "dongleSerialNumber",
|
|
104138
|
+
key: "Dongle.SerialNumber"
|
|
104139
|
+
},
|
|
104140
|
+
{
|
|
104141
|
+
attribute: "batterySerialNumber",
|
|
104142
|
+
key: "SerialNumber.Battery"
|
|
104143
|
+
},
|
|
104144
|
+
{
|
|
104145
|
+
attribute: "batteryFirmware",
|
|
104146
|
+
key: "SoftwareVersion.Service.Battery.Unit.1"
|
|
104147
|
+
}
|
|
104148
|
+
],
|
|
104149
|
+
primaryChildPreference: [
|
|
104150
|
+
"pv",
|
|
102893
104151
|
"battery",
|
|
102894
|
-
"
|
|
102895
|
-
"battery-discharged",
|
|
102896
|
-
"battery-temperature",
|
|
102897
|
-
"battery-health"
|
|
104152
|
+
"house"
|
|
102898
104153
|
]
|
|
102899
|
-
},
|
|
102900
|
-
{
|
|
102901
|
-
id: "grid",
|
|
102902
|
-
name: "Grid",
|
|
102903
|
-
specIds: ["grid-import", "grid-export"]
|
|
102904
|
-
},
|
|
102905
|
-
{
|
|
102906
|
-
id: "house",
|
|
102907
|
-
name: "House",
|
|
102908
|
-
specIds: ["house"]
|
|
102909
|
-
},
|
|
102910
|
-
{
|
|
104154
|
+
}),
|
|
104155
|
+
equipment({
|
|
102911
104156
|
id: "heatpump",
|
|
102912
|
-
|
|
102913
|
-
|
|
104157
|
+
kind: "container",
|
|
104158
|
+
name: "Heat pump (Enpal)",
|
|
104159
|
+
children: [...live(void 0, "heatpump", "heatpump-hot-water", "heatpump-outside", "heatpump-hot-water-mode", "heatpump-heating-mode", "heatpump-generator-state", "heatpump-heater-state"), ...diag(void 0, "heatpump-energy-management", "heatpump-power-desired", "heatpump-pv-optimization", "heatpump-power-min", "heatpump-power-max", "heatpump-connection")],
|
|
104160
|
+
metadata: [
|
|
104161
|
+
{
|
|
104162
|
+
attribute: "model",
|
|
104163
|
+
key: "Heatpump.IndoorUnit.DeviceType"
|
|
104164
|
+
},
|
|
104165
|
+
{
|
|
104166
|
+
attribute: "serialNumber",
|
|
104167
|
+
key: "Heatpump.SerialNumber"
|
|
104168
|
+
},
|
|
104169
|
+
{
|
|
104170
|
+
attribute: "indoorUnitSerialNumber",
|
|
104171
|
+
key: "Heatpump.IndoorUnit.SerialNumber"
|
|
104172
|
+
},
|
|
104173
|
+
{
|
|
104174
|
+
attribute: "firmware",
|
|
104175
|
+
key: "Heatpump.SoftwareVersion"
|
|
104176
|
+
},
|
|
104177
|
+
{
|
|
104178
|
+
attribute: "indoorUnitFirmware",
|
|
104179
|
+
key: "Heatpump.IndoorUnit.SoftwareVersion"
|
|
104180
|
+
},
|
|
104181
|
+
{
|
|
104182
|
+
attribute: "gatewayModel",
|
|
104183
|
+
key: "Heatpump.Gateway.Device.Type"
|
|
104184
|
+
},
|
|
104185
|
+
{
|
|
104186
|
+
attribute: "gatewaySerialNumber",
|
|
104187
|
+
key: "Heatpump.Gateway.SerialNumber"
|
|
104188
|
+
},
|
|
104189
|
+
{
|
|
104190
|
+
attribute: "gatewayFirmware",
|
|
104191
|
+
key: "Heatpump.Gateway.SoftwareVersion"
|
|
104192
|
+
}
|
|
104193
|
+
],
|
|
104194
|
+
primaryChildPreference: [
|
|
102914
104195
|
"heatpump",
|
|
102915
104196
|
"heatpump-hot-water",
|
|
102916
104197
|
"heatpump-outside"
|
|
102917
104198
|
]
|
|
102918
|
-
}
|
|
104199
|
+
})
|
|
102919
104200
|
];
|
|
102920
104201
|
function findEquipment(id) {
|
|
102921
104202
|
return ENPAL_EQUIPMENT.find((e) => e.id === id);
|
|
@@ -102929,24 +104210,61 @@ function childNativeId(equipmentId, specId) {
|
|
|
102929
104210
|
* they stay, whatever the box reports later, so a restart with the box off keeps them.
|
|
102930
104211
|
*/
|
|
102931
104212
|
var enpalEquipmentSchema = object({
|
|
102932
|
-
equipment: string().min(1).describe("Equipment id (pv, battery, grid, house
|
|
104213
|
+
equipment: string().min(1).describe("Equipment id (impianto, heatpump; older adoptions: pv, battery, grid, house)"),
|
|
102933
104214
|
specIds: array(string().min(1)).describe("Catalog entries adopted as children"),
|
|
102934
104215
|
integrationId: string().min(1).describe("Owning integration"),
|
|
102935
104216
|
system: literal(ENPAL_SYSTEM).optional()
|
|
102936
104217
|
});
|
|
102937
|
-
/**
|
|
104218
|
+
/**
|
|
104219
|
+
* The declared primary children of an adopted piece: its preference, limited to the children
|
|
104220
|
+
* it was adopted with. Empty for the gateway and for an equipment id the catalog dropped.
|
|
104221
|
+
*/
|
|
104222
|
+
function primaryChildPreferenceOf(found, specIds) {
|
|
104223
|
+
if (found === void 0) return [];
|
|
104224
|
+
return found.primaryChildPreference.filter((id) => specIds.includes(id));
|
|
104225
|
+
}
|
|
104226
|
+
/**
|
|
104227
|
+
* The accordion layout of an equipment Container, for the children it was adopted with.
|
|
104228
|
+
* Empty for a Container with no sections (the heat pump) or an unknown equipment.
|
|
104229
|
+
*/
|
|
104230
|
+
function childLayoutOf(found, specIds) {
|
|
104231
|
+
if (found === void 0) return [];
|
|
104232
|
+
const entries = [];
|
|
104233
|
+
found.children.forEach((child, order) => {
|
|
104234
|
+
if (child.section === void 0 || !specIds.includes(child.specId)) return;
|
|
104235
|
+
entries.push({
|
|
104236
|
+
childKey: child.specId,
|
|
104237
|
+
section: child.section,
|
|
104238
|
+
order
|
|
104239
|
+
});
|
|
104240
|
+
});
|
|
104241
|
+
return entries;
|
|
104242
|
+
}
|
|
104243
|
+
/** The identity facts the box reported for an equipment, by device attribute. */
|
|
104244
|
+
function metadataOf(found, snapshot) {
|
|
104245
|
+
const out = {};
|
|
104246
|
+
for (const field of found?.metadata ?? []) {
|
|
104247
|
+
const value = snapshot.texts.get(field.key);
|
|
104248
|
+
if (value !== void 0) out[field.attribute] = value;
|
|
104249
|
+
}
|
|
104250
|
+
return out;
|
|
104251
|
+
}
|
|
104252
|
+
/** The Sensor children the kernel spawns under an adopted piece (a Container or the Gateway). */
|
|
102938
104253
|
function accessoryChildSpecs(config) {
|
|
104254
|
+
const found = findEquipment(config.equipment);
|
|
102939
104255
|
const out = [];
|
|
102940
104256
|
for (const specId of config.specIds) {
|
|
102941
104257
|
const spec = findSpec(specId);
|
|
102942
104258
|
if (spec === void 0) continue;
|
|
104259
|
+
const diagnostic = found?.children.find((c) => c.specId === specId)?.diagnostic === true;
|
|
102943
104260
|
out.push({
|
|
102944
104261
|
stableIdSuffix: spec.id,
|
|
102945
104262
|
meta: {
|
|
102946
104263
|
type: DeviceType.Sensor,
|
|
102947
104264
|
name: spec.name,
|
|
102948
104265
|
role: spec.role,
|
|
102949
|
-
integrationId: config.integrationId
|
|
104266
|
+
integrationId: config.integrationId,
|
|
104267
|
+
...diagnostic ? { display: { hidden: true } } : {}
|
|
102950
104268
|
},
|
|
102951
104269
|
config: {
|
|
102952
104270
|
specId: spec.id,
|
|
@@ -102959,18 +104277,87 @@ function accessoryChildSpecs(config) {
|
|
|
102959
104277
|
return out;
|
|
102960
104278
|
}
|
|
102961
104279
|
//#endregion
|
|
104280
|
+
//#region src/devices/enpal-metadata-sync.ts
|
|
104281
|
+
/**
|
|
104282
|
+
* Writes the identity facts a piece reported (model, serial, firmware) as device attributes.
|
|
104283
|
+
* A poll that changed none writes nothing; a failed write is logged and retried next poll.
|
|
104284
|
+
*/
|
|
104285
|
+
var EnpalMetadataSync = class {
|
|
104286
|
+
options;
|
|
104287
|
+
applied = "";
|
|
104288
|
+
constructor(options) {
|
|
104289
|
+
this.options = options;
|
|
104290
|
+
}
|
|
104291
|
+
async apply() {
|
|
104292
|
+
const snapshot = this.options.getSnapshot();
|
|
104293
|
+
if (snapshot === null) return;
|
|
104294
|
+
const attributes = metadataOf(findEquipment(this.options.equipment), snapshot);
|
|
104295
|
+
const fingerprint = JSON.stringify(attributes);
|
|
104296
|
+
if (fingerprint === this.applied || Object.keys(attributes).length === 0) return;
|
|
104297
|
+
try {
|
|
104298
|
+
await this.options.write(attributes);
|
|
104299
|
+
this.applied = fingerprint;
|
|
104300
|
+
} catch (err) {
|
|
104301
|
+
this.options.logger.warn("enpal: could not write the device attributes", {
|
|
104302
|
+
tags: { deviceId: this.options.deviceId },
|
|
104303
|
+
meta: {
|
|
104304
|
+
equipment: this.options.equipment,
|
|
104305
|
+
error: errMsg(err)
|
|
104306
|
+
}
|
|
104307
|
+
});
|
|
104308
|
+
}
|
|
104309
|
+
}
|
|
104310
|
+
};
|
|
104311
|
+
//#endregion
|
|
104312
|
+
//#region src/devices/enpal-preference-backfill.ts
|
|
104313
|
+
/**
|
|
104314
|
+
* Stamp the declared primary-child preference on a container adopted before
|
|
104315
|
+
* the declaration existed. Adoption stamps it through `InitialDeviceMeta`;
|
|
104316
|
+
* a restore passes no initial meta, so an older row would otherwise never get
|
|
104317
|
+
* one and keep being shown by the heuristic's child.
|
|
104318
|
+
*
|
|
104319
|
+
* Only an ABSENT declaration is filled: a stored one is the provider's own and
|
|
104320
|
+
* is left alone. The operator's pick (`primaryChildEntityId`) is a different
|
|
104321
|
+
* column, never written here, and still wins at resolution.
|
|
104322
|
+
*
|
|
104323
|
+
* @returns whether it wrote.
|
|
104324
|
+
*/
|
|
104325
|
+
async function backfillPrimaryChildPreference(input) {
|
|
104326
|
+
const { deviceId, stored, declared, write, logger } = input;
|
|
104327
|
+
if (stored !== void 0 && stored.length > 0) return false;
|
|
104328
|
+
if (declared.length === 0) return false;
|
|
104329
|
+
try {
|
|
104330
|
+
await write(declared);
|
|
104331
|
+
} catch (err) {
|
|
104332
|
+
logger.warn("enpal: could not stamp the primary-child preference", {
|
|
104333
|
+
tags: { deviceId },
|
|
104334
|
+
meta: { error: errMsg(err) }
|
|
104335
|
+
});
|
|
104336
|
+
return false;
|
|
104337
|
+
}
|
|
104338
|
+
logger.info("enpal: stamped the primary-child preference on a container adopted without one", {
|
|
104339
|
+
tags: { deviceId },
|
|
104340
|
+
meta: { primaryChildPreference: declared }
|
|
104341
|
+
});
|
|
104342
|
+
return true;
|
|
104343
|
+
}
|
|
104344
|
+
//#endregion
|
|
102962
104345
|
//#region src/devices/enpal-equipment-device.ts
|
|
102963
104346
|
/**
|
|
102964
|
-
* One
|
|
104347
|
+
* One piece Enpal measures (the System: PV, battery, grid, house; the heat pump) as a
|
|
102965
104348
|
* {@link DeviceType.Container}. It owns no caps: it declares the Sensor children it was
|
|
102966
|
-
* adopted with, and the kernel spawns and restores them
|
|
102967
|
-
*
|
|
104349
|
+
* adopted with, and the kernel spawns and restores them; which child stands for it is the
|
|
104350
|
+
* `primaryChildPreference` stamped at adoption, and re-stamped on activation for a
|
|
104351
|
+
* container adopted before it had one. Its model, serials and firmware are device
|
|
104352
|
+
* attributes, written from the box's text rows whenever they change. Online follows the
|
|
104353
|
+
* integration's box; the integration marks it offline when a poll fails.
|
|
102968
104354
|
*/
|
|
102969
104355
|
var EnpalEquipmentDevice = class extends BaseDevice {
|
|
102970
104356
|
features = [];
|
|
102971
104357
|
equipment;
|
|
102972
104358
|
specIds;
|
|
102973
104359
|
integrationId;
|
|
104360
|
+
snapshotUnsub = null;
|
|
102974
104361
|
constructor(ctx) {
|
|
102975
104362
|
const cfg = enpalEquipmentSchema.parse(ctx.persistedConfig ?? {});
|
|
102976
104363
|
super(ctx, enpalEquipmentSchema, { type: DeviceType.Container });
|
|
@@ -102983,6 +104370,42 @@ var EnpalEquipmentDevice = class extends BaseDevice {
|
|
|
102983
104370
|
system: ENPAL_SYSTEM
|
|
102984
104371
|
});
|
|
102985
104372
|
}
|
|
104373
|
+
async onActivate() {
|
|
104374
|
+
await super.onActivate();
|
|
104375
|
+
await backfillPrimaryChildPreference({
|
|
104376
|
+
deviceId: this.id,
|
|
104377
|
+
stored: this.ctx.deviceMeta.primaryChildPreference,
|
|
104378
|
+
declared: primaryChildPreferenceOf(findEquipment(this.equipment), this.specIds),
|
|
104379
|
+
write: async (keys) => {
|
|
104380
|
+
await this.ctx.api.deviceManager.setPrimaryChildPreference.mutate({
|
|
104381
|
+
deviceId: this.id,
|
|
104382
|
+
primaryChildPreference: keys
|
|
104383
|
+
});
|
|
104384
|
+
},
|
|
104385
|
+
logger: this.ctx.logger
|
|
104386
|
+
});
|
|
104387
|
+
const metadata = new EnpalMetadataSync({
|
|
104388
|
+
equipment: this.equipment,
|
|
104389
|
+
deviceId: this.id,
|
|
104390
|
+
getSnapshot: () => enpalConnections.getSnapshot(this.integrationId),
|
|
104391
|
+
write: async (patch) => {
|
|
104392
|
+
await this.ctx.api.deviceManager.setMetadata.mutate({
|
|
104393
|
+
deviceId: this.id,
|
|
104394
|
+
patch
|
|
104395
|
+
});
|
|
104396
|
+
},
|
|
104397
|
+
logger: this.ctx.logger
|
|
104398
|
+
});
|
|
104399
|
+
this.snapshotUnsub = enpalConnections.onSnapshot(this.integrationId, () => {
|
|
104400
|
+
metadata.apply();
|
|
104401
|
+
});
|
|
104402
|
+
await metadata.apply();
|
|
104403
|
+
}
|
|
104404
|
+
async removeDevice() {
|
|
104405
|
+
this.snapshotUnsub?.();
|
|
104406
|
+
this.snapshotUnsub = null;
|
|
104407
|
+
await super.removeDevice();
|
|
104408
|
+
}
|
|
102986
104409
|
getAccessoryChildren() {
|
|
102987
104410
|
return accessoryChildSpecs({
|
|
102988
104411
|
equipment: this.equipment,
|
|
@@ -102991,33 +104414,56 @@ var EnpalEquipmentDevice = class extends BaseDevice {
|
|
|
102991
104414
|
});
|
|
102992
104415
|
}
|
|
102993
104416
|
};
|
|
104417
|
+
//#endregion
|
|
104418
|
+
//#region src/enpal-discovery.ts
|
|
102994
104419
|
var METADATA = {
|
|
102995
104420
|
manufacturer: "Enpal",
|
|
102996
104421
|
integration: "enpal"
|
|
102997
104422
|
};
|
|
104423
|
+
/** The Sensor candidates of one piece: the catalog entries of it the box reported. */
|
|
104424
|
+
function reportedChildren(equipment, snapshot) {
|
|
104425
|
+
if (snapshot === null) return [];
|
|
104426
|
+
const reported = new Map(availableSpecs(snapshot).map((spec) => [spec.id, spec]));
|
|
104427
|
+
return equipment.specIds.flatMap((id) => {
|
|
104428
|
+
const spec = reported.get(id);
|
|
104429
|
+
if (spec === void 0) return [];
|
|
104430
|
+
return [{
|
|
104431
|
+
childNativeId: childNativeId(equipment.id, spec.id),
|
|
104432
|
+
name: spec.name,
|
|
104433
|
+
type: DeviceType.Sensor,
|
|
104434
|
+
status: "online",
|
|
104435
|
+
metadata: METADATA,
|
|
104436
|
+
alreadyAdopted: false,
|
|
104437
|
+
adoptedDeviceId: null,
|
|
104438
|
+
capabilities: capsOfSpec(spec)
|
|
104439
|
+
}];
|
|
104440
|
+
});
|
|
104441
|
+
}
|
|
102998
104442
|
/**
|
|
102999
|
-
* The box ({@link DeviceType.Gateway}, always offered, offline while it is unreachable
|
|
103000
|
-
*
|
|
103001
|
-
* Sensor children it reported. Pure.
|
|
104443
|
+
* The box ({@link DeviceType.Gateway}, always offered, offline while it is unreachable, with the
|
|
104444
|
+
* diagnostics it reported as children), then one Container candidate per other piece the box
|
|
104445
|
+
* reported at least one value for, with the Sensor children it reported. Pure.
|
|
103002
104446
|
*/
|
|
103003
104447
|
function buildEnpalCandidates(input) {
|
|
103004
104448
|
const { snapshot } = input;
|
|
103005
104449
|
const boxAdopted = input.adopted.get("box") ?? null;
|
|
104450
|
+
const box = findEquipment("box");
|
|
104451
|
+
const boxChildren = box === void 0 ? [] : reportedChildren(box, snapshot);
|
|
103006
104452
|
const out = [{
|
|
103007
104453
|
childNativeId: "box",
|
|
103008
|
-
name:
|
|
104454
|
+
name: gatewayDeviceName("Enpal", "Box"),
|
|
103009
104455
|
type: DeviceType.Gateway,
|
|
103010
104456
|
status: input.box.online ? "online" : "offline",
|
|
103011
104457
|
metadata: METADATA,
|
|
103012
104458
|
alreadyAdopted: boxAdopted !== null,
|
|
103013
104459
|
adoptedDeviceId: boxAdopted,
|
|
103014
|
-
capabilities: [deviceAdminLinkCapability.name]
|
|
104460
|
+
capabilities: [deviceAdminLinkCapability.name],
|
|
104461
|
+
...boxChildren.length > 0 ? { children: boxChildren } : {}
|
|
103015
104462
|
}];
|
|
103016
|
-
if (snapshot === null) return out;
|
|
103017
|
-
const reported = new Map(availableSpecs(snapshot).map((spec) => [spec.id, spec]));
|
|
103018
104463
|
for (const equipment of ENPAL_EQUIPMENT) {
|
|
103019
|
-
|
|
103020
|
-
|
|
104464
|
+
if (equipment.kind !== "container") continue;
|
|
104465
|
+
const children = reportedChildren(equipment, snapshot);
|
|
104466
|
+
if (children.length === 0) continue;
|
|
103021
104467
|
const adoptedId = input.adopted.get(equipment.id) ?? null;
|
|
103022
104468
|
out.push({
|
|
103023
104469
|
childNativeId: equipment.id,
|
|
@@ -103028,16 +104474,7 @@ function buildEnpalCandidates(input) {
|
|
|
103028
104474
|
alreadyAdopted: adoptedId !== null,
|
|
103029
104475
|
adoptedDeviceId: adoptedId,
|
|
103030
104476
|
capabilities: [],
|
|
103031
|
-
children
|
|
103032
|
-
childNativeId: childNativeId(equipment.id, spec.id),
|
|
103033
|
-
name: spec.name,
|
|
103034
|
-
type: DeviceType.Sensor,
|
|
103035
|
-
status: "online",
|
|
103036
|
-
metadata: METADATA,
|
|
103037
|
-
alreadyAdopted: false,
|
|
103038
|
-
adoptedDeviceId: null,
|
|
103039
|
-
capabilities: capsOfSpec(spec)
|
|
103040
|
-
}))
|
|
104477
|
+
children
|
|
103041
104478
|
});
|
|
103042
104479
|
}
|
|
103043
104480
|
return out;
|
|
@@ -103051,7 +104488,12 @@ var DEVICES_FILTER = {
|
|
|
103051
104488
|
};
|
|
103052
104489
|
function buildEnpalAdoptionProvider(deps) {
|
|
103053
104490
|
const { getSnapshot, hasIntegration, listIntegrations, listAdopted, logger } = deps;
|
|
103054
|
-
const boxName = (integrationId) => deps.integrationName(integrationId) ?? "Enpal";
|
|
104491
|
+
const boxName = (integrationId) => deps.integrationName(integrationId) ?? findEquipment("box")?.name ?? "Enpal Box";
|
|
104492
|
+
/** The catalog entries of a piece the offered candidate reports as children. */
|
|
104493
|
+
const reportedSpecIds = (equipment, candidate) => {
|
|
104494
|
+
const reported = new Set((candidate?.children ?? []).map((c) => c.childNativeId));
|
|
104495
|
+
return equipment.specIds.filter((id) => reported.has(`${equipment.id}#${id}`));
|
|
104496
|
+
};
|
|
103055
104497
|
async function adoptedMap(integrationId) {
|
|
103056
104498
|
const map = /* @__PURE__ */ new Map();
|
|
103057
104499
|
for (const device of await listAdopted()) {
|
|
@@ -103127,20 +104569,23 @@ function buildEnpalAdoptionProvider(deps) {
|
|
|
103127
104569
|
const adopted = [];
|
|
103128
104570
|
let failures = 0;
|
|
103129
104571
|
for (const equipmentId of childNativeIds) try {
|
|
103130
|
-
|
|
103131
|
-
|
|
104572
|
+
const candidate = offered.find((c) => c.childNativeId === equipmentId);
|
|
104573
|
+
const equipment = findEquipment(equipmentId);
|
|
104574
|
+
if (equipmentId === "box" && equipment !== void 0) {
|
|
104575
|
+
const specIds = reportedSpecIds(equipment, candidate);
|
|
104576
|
+
const { deviceId, accessoryDeviceIds } = await deps.adoptGateway({
|
|
103132
104577
|
integrationId,
|
|
103133
|
-
name: perCandidate?.[equipmentId]?.name ??
|
|
104578
|
+
name: perCandidate?.[equipmentId]?.name ?? gatewayDeviceName("Enpal", "Box"),
|
|
104579
|
+
specIds,
|
|
104580
|
+
childLayout: childLayoutOf(equipment, specIds)
|
|
103134
104581
|
});
|
|
103135
104582
|
adopted.push({
|
|
103136
104583
|
childNativeId: equipmentId,
|
|
103137
104584
|
parentDeviceId: deviceId,
|
|
103138
|
-
accessoryDeviceIds
|
|
104585
|
+
accessoryDeviceIds
|
|
103139
104586
|
});
|
|
103140
104587
|
continue;
|
|
103141
104588
|
}
|
|
103142
|
-
const candidate = offered.find((c) => c.childNativeId === equipmentId);
|
|
103143
|
-
const equipment = findEquipment(equipmentId);
|
|
103144
104589
|
if (candidate === void 0 || equipment === void 0) {
|
|
103145
104590
|
logger.warn("enpal adopt: the box reports nothing for this equipment — skipping", { meta: {
|
|
103146
104591
|
equipment: equipmentId,
|
|
@@ -103149,13 +104594,14 @@ function buildEnpalAdoptionProvider(deps) {
|
|
|
103149
104594
|
failures++;
|
|
103150
104595
|
continue;
|
|
103151
104596
|
}
|
|
103152
|
-
const
|
|
103153
|
-
const specIds = equipment.specIds.filter((id) => reported.has(`${equipment.id}#${id}`));
|
|
104597
|
+
const specIds = reportedSpecIds(equipment, candidate);
|
|
103154
104598
|
const { deviceId, accessoryDeviceIds } = await deps.adoptDevice({
|
|
103155
104599
|
equipment: equipment.id,
|
|
103156
104600
|
specIds,
|
|
103157
104601
|
integrationId,
|
|
103158
|
-
name: perCandidate?.[equipmentId]?.name ?? equipment.name
|
|
104602
|
+
name: perCandidate?.[equipmentId]?.name ?? equipment.name,
|
|
104603
|
+
childLayout: childLayoutOf(equipment, specIds),
|
|
104604
|
+
primaryChildPreference: primaryChildPreferenceOf(equipment, specIds)
|
|
103159
104605
|
});
|
|
103160
104606
|
adopted.push({
|
|
103161
104607
|
childNativeId: equipmentId,
|
|
@@ -103186,10 +104632,187 @@ function buildEnpalAdoptionProvider(deps) {
|
|
|
103186
104632
|
}
|
|
103187
104633
|
};
|
|
103188
104634
|
}
|
|
104635
|
+
var BlazorRowsSchema = array(object({
|
|
104636
|
+
key: string(),
|
|
104637
|
+
value: string(),
|
|
104638
|
+
unit: string().nullable(),
|
|
104639
|
+
timestamp: string().nullable()
|
|
104640
|
+
}));
|
|
104641
|
+
var BridgeResultSchema = discriminatedUnion("ok", [object({
|
|
104642
|
+
ok: literal(true),
|
|
104643
|
+
rows: BlazorRowsSchema
|
|
104644
|
+
}), object({
|
|
104645
|
+
ok: literal(false),
|
|
104646
|
+
kind: _enum([
|
|
104647
|
+
"unreachable",
|
|
104648
|
+
"bad-response",
|
|
104649
|
+
"empty",
|
|
104650
|
+
"protocol"
|
|
104651
|
+
]),
|
|
104652
|
+
error: string()
|
|
104653
|
+
})]);
|
|
104654
|
+
/** A failed bridge run. The message never carries the bridge's stderr beyond a short tail. */
|
|
104655
|
+
var BlazorBridgeError = class extends Error {
|
|
104656
|
+
kind;
|
|
104657
|
+
constructor(kind, message) {
|
|
104658
|
+
super(message);
|
|
104659
|
+
this.name = "BlazorBridgeError";
|
|
104660
|
+
this.kind = kind;
|
|
104661
|
+
}
|
|
104662
|
+
};
|
|
104663
|
+
/** The worker waits up to 12 s for the box's first render; this leaves room for connect and exit. */
|
|
104664
|
+
var DEFAULT_TIMEOUT_MS = 4e4;
|
|
104665
|
+
var WINDOW_MS = 12e3;
|
|
104666
|
+
/** A real answer is ~25 KB; anything near this is a runaway bridge, not a box. */
|
|
104667
|
+
var MAX_STDOUT_BYTES = 2 * 1024 * 1024;
|
|
104668
|
+
/**
|
|
104669
|
+
* How long `auto` leaves the Blazor channel alone after it failed. An older box that never
|
|
104670
|
+
* serves it would otherwise cost the full read window on every poll.
|
|
104671
|
+
*/
|
|
104672
|
+
var BLAZOR_BACKOFF_MS = 10 * 6e4;
|
|
104673
|
+
/** The slowest a good read takes: connect, the 12 s render window, exit. */
|
|
104674
|
+
var BLAZOR_WORST_CASE_READ_MS = 2e4;
|
|
104675
|
+
/**
|
|
104676
|
+
* The poll interval actually used. A box-web-channel read can take most of
|
|
104677
|
+
* {@link BLAZOR_WORST_CASE_READ_MS}, so a shorter interval is raised to it when that channel
|
|
104678
|
+
* can be the source: ticks would only pile onto the running read.
|
|
104679
|
+
*/
|
|
104680
|
+
function effectivePollIntervalMs(connection) {
|
|
104681
|
+
return connection.source === "auto" || connection.source === "blazor" ? Math.max(connection.pollIntervalMs, BLAZOR_WORST_CASE_READ_MS) : connection.pollIntervalMs;
|
|
104682
|
+
}
|
|
104683
|
+
/** Remembers a failed Blazor read so `auto` skips it for a while. Forced `blazor` ignores it. */
|
|
104684
|
+
var BlazorBackoff = class {
|
|
104685
|
+
#until = 0;
|
|
104686
|
+
#reason = null;
|
|
104687
|
+
/** Why the channel is being skipped now, or null when it may be tried. */
|
|
104688
|
+
skipReason(now) {
|
|
104689
|
+
if (this.#reason === null || now >= this.#until) return null;
|
|
104690
|
+
return {
|
|
104691
|
+
kind: this.#reason.kind,
|
|
104692
|
+
message: `${this.#reason.message}; the box web channel is skipped for ${BLAZOR_BACKOFF_MS / 6e4} minutes after a failure`
|
|
104693
|
+
};
|
|
104694
|
+
}
|
|
104695
|
+
failed(now, reason) {
|
|
104696
|
+
this.#until = now + BLAZOR_BACKOFF_MS;
|
|
104697
|
+
this.#reason = reason;
|
|
104698
|
+
}
|
|
104699
|
+
recovered() {
|
|
104700
|
+
this.#until = 0;
|
|
104701
|
+
this.#reason = null;
|
|
104702
|
+
}
|
|
104703
|
+
};
|
|
104704
|
+
/** One bridge run per poll: spawn, write the request on stdin, parse one JSON answer. */
|
|
104705
|
+
function createBlazorReader(deps) {
|
|
104706
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
104707
|
+
return (target) => new Promise((resolve, reject) => {
|
|
104708
|
+
const child = (0, node_child_process.spawn)(deps.pythonPath, [deps.scriptPath], { stdio: [
|
|
104709
|
+
"pipe",
|
|
104710
|
+
"pipe",
|
|
104711
|
+
"pipe"
|
|
104712
|
+
] });
|
|
104713
|
+
let stdout = "";
|
|
104714
|
+
let stderr = "";
|
|
104715
|
+
let settled = false;
|
|
104716
|
+
const finish = (action) => {
|
|
104717
|
+
if (settled) return;
|
|
104718
|
+
settled = true;
|
|
104719
|
+
clearTimeout(timer);
|
|
104720
|
+
action();
|
|
104721
|
+
};
|
|
104722
|
+
const timer = setTimeout(() => {
|
|
104723
|
+
child.kill("SIGKILL");
|
|
104724
|
+
finish(() => reject(new BlazorBridgeError("timeout", `Enpal box ${target.host} did not answer within ${timeoutMs / 1e3}s`)));
|
|
104725
|
+
}, timeoutMs);
|
|
104726
|
+
child.stdout.on("data", (chunk) => {
|
|
104727
|
+
stdout += chunk.toString("utf8");
|
|
104728
|
+
if (stdout.length > MAX_STDOUT_BYTES) {
|
|
104729
|
+
child.kill("SIGKILL");
|
|
104730
|
+
finish(() => reject(new BlazorBridgeError("protocol", "the Enpal bridge wrote more output than any box answer")));
|
|
104731
|
+
}
|
|
104732
|
+
});
|
|
104733
|
+
child.stderr.on("data", (chunk) => {
|
|
104734
|
+
stderr = (stderr + chunk.toString("utf8")).slice(-16384);
|
|
104735
|
+
});
|
|
104736
|
+
child.on("error", (err) => {
|
|
104737
|
+
finish(() => reject(new BlazorBridgeError("protocol", `cannot start the Enpal bridge: ${err.message}`)));
|
|
104738
|
+
});
|
|
104739
|
+
child.on("close", (code) => {
|
|
104740
|
+
finish(() => {
|
|
104741
|
+
let json;
|
|
104742
|
+
try {
|
|
104743
|
+
json = JSON.parse(stdout.trim());
|
|
104744
|
+
} catch {
|
|
104745
|
+
const tail = stderr.trim().slice(-300) || "no stderr";
|
|
104746
|
+
reject(new BlazorBridgeError("protocol", `the Enpal bridge produced no JSON (exit ${code ?? "null"}): ${tail}`));
|
|
104747
|
+
return;
|
|
104748
|
+
}
|
|
104749
|
+
const parsed = BridgeResultSchema.safeParse(json);
|
|
104750
|
+
if (!parsed.success) {
|
|
104751
|
+
reject(new BlazorBridgeError("protocol", "the Enpal bridge answered an unknown shape"));
|
|
104752
|
+
return;
|
|
104753
|
+
}
|
|
104754
|
+
if (!parsed.data.ok) {
|
|
104755
|
+
reject(new BlazorBridgeError(parsed.data.kind, parsed.data.error));
|
|
104756
|
+
return;
|
|
104757
|
+
}
|
|
104758
|
+
resolve(parsed.data.rows);
|
|
104759
|
+
});
|
|
104760
|
+
});
|
|
104761
|
+
child.stdin.on("error", () => void 0);
|
|
104762
|
+
child.stdin.end(JSON.stringify({
|
|
104763
|
+
host: target.host,
|
|
104764
|
+
port: target.port,
|
|
104765
|
+
windowMs: WINDOW_MS
|
|
104766
|
+
}));
|
|
104767
|
+
});
|
|
104768
|
+
}
|
|
104769
|
+
var KEY_SHAPE$2 = /^[A-Z][A-Za-z0-9]*(\.[A-Za-z0-9-]+)+$/;
|
|
104770
|
+
/** A bare decimal number: `3.6`, `-40`, `0`. A version, a hash or a timestamp is not one. */
|
|
104771
|
+
var NUMBER = /^-?\d+(?:[.,]\d+)?$/;
|
|
104772
|
+
/**
|
|
104773
|
+
* The numeric sensors of the bridge's rows. A text value (a state name, a serial number, the
|
|
104774
|
+
* inverter's markup bitfield) is not a reading, and neither is a dotted string that only
|
|
104775
|
+
* looks like a key (`V1.1.7`, a version).
|
|
104776
|
+
*/
|
|
104777
|
+
function blazorRowsToReadings(rows) {
|
|
104778
|
+
const readings = [];
|
|
104779
|
+
for (const row of rows) {
|
|
104780
|
+
if (!KEY_SHAPE$2.test(row.key)) continue;
|
|
104781
|
+
const text = row.value.trim();
|
|
104782
|
+
if (!NUMBER.test(text)) continue;
|
|
104783
|
+
const value = Number(text.replace(",", "."));
|
|
104784
|
+
if (!Number.isFinite(value)) continue;
|
|
104785
|
+
readings.push({
|
|
104786
|
+
key: row.key,
|
|
104787
|
+
value,
|
|
104788
|
+
unit: normalizeEnpalUnit(row.unit ?? void 0),
|
|
104789
|
+
measurement: void 0
|
|
104790
|
+
});
|
|
104791
|
+
}
|
|
104792
|
+
return readings;
|
|
104793
|
+
}
|
|
104794
|
+
/**
|
|
104795
|
+
* Every row's text as delivered, for state labels, serial numbers and firmware strings. A
|
|
104796
|
+
* row with no value, or whose value is markup (the inverter's `<ul>` bitfield), carries no
|
|
104797
|
+
* text worth showing and is left out.
|
|
104798
|
+
*/
|
|
104799
|
+
function blazorRowsToTexts(rows) {
|
|
104800
|
+
const texts = [];
|
|
104801
|
+
for (const row of rows) {
|
|
104802
|
+
if (!KEY_SHAPE$2.test(row.key)) continue;
|
|
104803
|
+
const value = row.value.trim();
|
|
104804
|
+
if (value.length === 0 || value.startsWith("<")) continue;
|
|
104805
|
+
texts.push({
|
|
104806
|
+
key: row.key,
|
|
104807
|
+
value
|
|
104808
|
+
});
|
|
104809
|
+
}
|
|
104810
|
+
return texts;
|
|
104811
|
+
}
|
|
103189
104812
|
//#endregion
|
|
103190
104813
|
//#region src/enpal-device-page.ts
|
|
103191
|
-
var CARD_HEADER = /<div class="card-header">\s*<h2>([^<]+)<\/h2>/g;
|
|
103192
|
-
var ROW = /<tr
|
|
104814
|
+
var CARD_HEADER = /<div class="card-header[^"]*">\s*<h2>([^<]+)<\/h2>/g;
|
|
104815
|
+
var ROW = /<tr(?:\s[^>]*)?>\s*<td>([^<]+)<\/td>\s*<td>([^<]*)<\/td>\s*<td>[^<]*<\/td>\s*<\/tr>/g;
|
|
103193
104816
|
var KEY_SHAPE$1 = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9-]+)+$/;
|
|
103194
104817
|
/** `10231W`, `-350W`, `1.85kW`, `51°C`, `62.5%`, `3` */
|
|
103195
104818
|
var VALUE = /^(-?\d+(?:[.,]\d+)?)\s*(\S*)$/;
|
|
@@ -103212,7 +104835,7 @@ function cardOffsets(html) {
|
|
|
103212
104835
|
* no zone, so it is not used: the poll time is.
|
|
103213
104836
|
*
|
|
103214
104837
|
* Firmware 8.51 and later pre-render only the "Site Data" card; the device rows then never
|
|
103215
|
-
* appear
|
|
104838
|
+
* appear here (they come through the Blazor circuit, `enpal-blazor.ts`).
|
|
103216
104839
|
*/
|
|
103217
104840
|
function parseDeviceMessages(html) {
|
|
103218
104841
|
const cards = cardOffsets(html);
|
|
@@ -103328,7 +104951,7 @@ function canonicalKey(key) {
|
|
|
103328
104951
|
* Index readings by canonical key. The box writes some keys into two measurements: the first
|
|
103329
104952
|
* occurrence wins.
|
|
103330
104953
|
*/
|
|
103331
|
-
function createSnapshot(source, readings, fetchedAt, fallbackReason) {
|
|
104954
|
+
function createSnapshot(source, readings, fetchedAt, fallbackReason, texts = []) {
|
|
103332
104955
|
const map = /* @__PURE__ */ new Map();
|
|
103333
104956
|
for (const reading of readings) {
|
|
103334
104957
|
const key = canonicalKey(reading.key);
|
|
@@ -103338,9 +104961,15 @@ function createSnapshot(source, readings, fetchedAt, fallbackReason) {
|
|
|
103338
104961
|
key
|
|
103339
104962
|
});
|
|
103340
104963
|
}
|
|
104964
|
+
const textMap = /* @__PURE__ */ new Map();
|
|
104965
|
+
for (const text of texts) {
|
|
104966
|
+
const key = canonicalKey(text.key);
|
|
104967
|
+
if (!textMap.has(key)) textMap.set(key, text.value);
|
|
104968
|
+
}
|
|
103341
104969
|
return {
|
|
103342
104970
|
source,
|
|
103343
104971
|
readings: map,
|
|
104972
|
+
texts: textMap,
|
|
103344
104973
|
fetchedAt,
|
|
103345
104974
|
...fallbackReason !== void 0 ? { fallbackReason } : {}
|
|
103346
104975
|
};
|
|
@@ -103399,14 +105028,42 @@ async function readInflux(input) {
|
|
|
103399
105028
|
}
|
|
103400
105029
|
}, input.fetch));
|
|
103401
105030
|
if (readings.length === 0) throw new EnpalError("empty", "influxdb", `Enpal InfluxDB on ${connection.host} returned no series for bucket "${connection.bucket}" in the last two hours`);
|
|
103402
|
-
return
|
|
105031
|
+
return {
|
|
105032
|
+
readings,
|
|
105033
|
+
texts: []
|
|
105034
|
+
};
|
|
105035
|
+
}
|
|
105036
|
+
async function readBlazor(input) {
|
|
105037
|
+
const { connection } = input;
|
|
105038
|
+
if (input.blazor === void 0) throw new EnpalError("bridge-unavailable", "blazor", "The embedded Python the Enpal bridge needs is not available on this node");
|
|
105039
|
+
let readings;
|
|
105040
|
+
let texts;
|
|
105041
|
+
try {
|
|
105042
|
+
const rows = await input.blazor({
|
|
105043
|
+
host: connection.host,
|
|
105044
|
+
port: connection.pagePort
|
|
105045
|
+
});
|
|
105046
|
+
readings = blazorRowsToReadings(rows);
|
|
105047
|
+
texts = blazorRowsToTexts(rows);
|
|
105048
|
+
} catch (error) {
|
|
105049
|
+
if (!(error instanceof BlazorBridgeError)) throw error;
|
|
105050
|
+
throw new EnpalError(error.kind === "protocol" ? "bad-response" : error.kind, "blazor", error.message);
|
|
105051
|
+
}
|
|
105052
|
+
if (readings.length === 0) throw new EnpalError("empty", "blazor", `Enpal box ${connection.host} sent no numeric sensor rows`);
|
|
105053
|
+
return {
|
|
105054
|
+
readings,
|
|
105055
|
+
texts
|
|
105056
|
+
};
|
|
103403
105057
|
}
|
|
103404
105058
|
async function readPage(input) {
|
|
103405
105059
|
const { connection } = input;
|
|
103406
105060
|
const url = `http://${connection.host}:${connection.pagePort}/deviceMessages`;
|
|
103407
105061
|
const readings = parseDeviceMessages(await request("device-page", connection.host, url, { method: "GET" }, input.fetch));
|
|
103408
105062
|
if (readings.length === 0) throw new EnpalError("empty", "device-page", `The device page of ${connection.host} lists no device rows (firmware 8.51 and later render them only through the web socket)`);
|
|
103409
|
-
return
|
|
105063
|
+
return {
|
|
105064
|
+
readings,
|
|
105065
|
+
texts: []
|
|
105066
|
+
};
|
|
103410
105067
|
}
|
|
103411
105068
|
function reasonOf(error) {
|
|
103412
105069
|
return {
|
|
@@ -103414,28 +105071,64 @@ function reasonOf(error) {
|
|
|
103414
105071
|
message: error.message
|
|
103415
105072
|
};
|
|
103416
105073
|
}
|
|
105074
|
+
var READERS = {
|
|
105075
|
+
blazor: readBlazor,
|
|
105076
|
+
influxdb: readInflux,
|
|
105077
|
+
"device-page": readPage
|
|
105078
|
+
};
|
|
105079
|
+
/** One reason from several: the kind of the first real failure, every message. */
|
|
105080
|
+
function joinReasons(reasons) {
|
|
105081
|
+
if (reasons.length === 0) return void 0;
|
|
105082
|
+
return {
|
|
105083
|
+
kind: reasons.find((r) => r.kind !== "bridge-unavailable")?.kind ?? "bridge-unavailable",
|
|
105084
|
+
message: reasons.map((r) => r.message).join("; ")
|
|
105085
|
+
};
|
|
105086
|
+
}
|
|
105087
|
+
var PYTHON_MISSING = {
|
|
105088
|
+
kind: "bridge-unavailable",
|
|
105089
|
+
message: "Python is not available on this node, so the box web channel (the only complete source on firmware 8.51 and later) is not used"
|
|
105090
|
+
};
|
|
103417
105091
|
/**
|
|
103418
|
-
* Read every data point the box reports, once. `auto` tries
|
|
103419
|
-
*
|
|
103420
|
-
* InfluxDB
|
|
103421
|
-
*
|
|
105092
|
+
* Read every data point the box reports, once. `auto` tries the Blazor channel first (the
|
|
105093
|
+
* only one that carries the device tables on firmware 8.51 and later, no credential), then
|
|
105094
|
+
* InfluxDB when a token is set, then the device page. The snapshot records why a preferred
|
|
105095
|
+
* source was not used: Python missing, the channel skipped after a recent failure, or the
|
|
105096
|
+
* failure itself. A Blazor failure in `auto` is remembered for {@link BLAZOR_BACKOFF_MS}.
|
|
105097
|
+
* When every source fails the FIRST real failure is the one reported. A failed read throws
|
|
105098
|
+
* {@link EnpalError}: it never yields a snapshot of zeros.
|
|
103422
105099
|
*/
|
|
103423
105100
|
async function readEnpalSnapshot(input) {
|
|
103424
|
-
const { source } = input.connection;
|
|
103425
|
-
|
|
103426
|
-
|
|
103427
|
-
|
|
103428
|
-
|
|
103429
|
-
|
|
103430
|
-
|
|
103431
|
-
|
|
103432
|
-
|
|
103433
|
-
|
|
103434
|
-
|
|
103435
|
-
|
|
103436
|
-
|
|
105101
|
+
const { source: mode } = input.connection;
|
|
105102
|
+
const now = input.now();
|
|
105103
|
+
const hasToken = input.tokenLocked !== true && input.connection.token.trim().length > 0;
|
|
105104
|
+
const reasons = [];
|
|
105105
|
+
let first;
|
|
105106
|
+
const order = [];
|
|
105107
|
+
if (mode === "blazor") order.push("blazor");
|
|
105108
|
+
else if (mode === "influx") order.push("influxdb");
|
|
105109
|
+
else if (mode === "page") order.push("device-page");
|
|
105110
|
+
else {
|
|
105111
|
+
if (input.blazor === void 0) reasons.push(PYTHON_MISSING);
|
|
105112
|
+
else {
|
|
105113
|
+
const skip = input.blazorBackoff?.skipReason(now) ?? null;
|
|
105114
|
+
if (skip !== null) reasons.push(skip);
|
|
105115
|
+
else order.push("blazor");
|
|
103437
105116
|
}
|
|
105117
|
+
if (hasToken) order.push("influxdb");
|
|
105118
|
+
order.push("device-page");
|
|
103438
105119
|
}
|
|
105120
|
+
for (const source of order) try {
|
|
105121
|
+
const read = await READERS[source](input);
|
|
105122
|
+
if (source === "blazor") input.blazorBackoff?.recovered();
|
|
105123
|
+
return createSnapshot(source, read.readings, now, joinReasons(reasons), read.texts);
|
|
105124
|
+
} catch (error) {
|
|
105125
|
+
if (!(error instanceof EnpalError)) throw error;
|
|
105126
|
+
if (source === "blazor" && mode === "auto") input.blazorBackoff?.failed(now, reasonOf(error));
|
|
105127
|
+
first ??= error;
|
|
105128
|
+
reasons.push(reasonOf(error));
|
|
105129
|
+
}
|
|
105130
|
+
if (first === void 0) throw new Error("readEnpalSnapshot: no source to read");
|
|
105131
|
+
throw first;
|
|
103439
105132
|
}
|
|
103440
105133
|
//#endregion
|
|
103441
105134
|
//#region src/enpal-integration-manager.ts
|
|
@@ -103450,6 +105143,9 @@ var EnpalIntegrationManager = class {
|
|
|
103450
105143
|
#snapshot = null;
|
|
103451
105144
|
#lastError = null;
|
|
103452
105145
|
#timer = null;
|
|
105146
|
+
#blazorBackoff = new BlazorBackoff();
|
|
105147
|
+
#lastSource = null;
|
|
105148
|
+
#lastFallbackKey = null;
|
|
103453
105149
|
#inFlight = null;
|
|
103454
105150
|
#stopped = true;
|
|
103455
105151
|
#failureLogged = false;
|
|
@@ -103481,7 +105177,7 @@ var EnpalIntegrationManager = class {
|
|
|
103481
105177
|
if (this.#stopped || this.#timer !== null) return;
|
|
103482
105178
|
this.#timer = setInterval(() => {
|
|
103483
105179
|
this.poll();
|
|
103484
|
-
}, this.#options.connection
|
|
105180
|
+
}, effectivePollIntervalMs(this.#options.connection));
|
|
103485
105181
|
this.#timer.unref();
|
|
103486
105182
|
}
|
|
103487
105183
|
async stop() {
|
|
@@ -103501,6 +105197,19 @@ var EnpalIntegrationManager = class {
|
|
|
103501
105197
|
this.#inFlight = run;
|
|
103502
105198
|
return run;
|
|
103503
105199
|
}
|
|
105200
|
+
/**
|
|
105201
|
+
* A preferred source that is not in use is said once per change of source or of reason kind (the back-off text changes every poll, the cause does not), not
|
|
105202
|
+
* once per boot: a box that drops from the web channel to the page mid-run is visible.
|
|
105203
|
+
*/
|
|
105204
|
+
#logSourceChange(source, fallback) {
|
|
105205
|
+
const fallbackMessage = fallback?.message;
|
|
105206
|
+
const fallbackKey = fallback?.kind ?? null;
|
|
105207
|
+
const changed = source !== this.#lastSource || fallbackKey !== this.#lastFallbackKey;
|
|
105208
|
+
if (changed && fallbackMessage !== void 0) this.#options.logger.warn(`Enpal box ${this.name}: preferred source not used (${fallbackMessage}), reading ${source}`);
|
|
105209
|
+
else if (changed && this.#lastSource !== null && source !== this.#lastSource) this.#options.logger.info(`Enpal box ${this.name}: now reading ${source}`);
|
|
105210
|
+
this.#lastSource = source;
|
|
105211
|
+
this.#lastFallbackKey = fallbackKey;
|
|
105212
|
+
}
|
|
103504
105213
|
async #pollOnce() {
|
|
103505
105214
|
const wasConnected = this.#status === "connected";
|
|
103506
105215
|
try {
|
|
@@ -103508,6 +105217,8 @@ var EnpalIntegrationManager = class {
|
|
|
103508
105217
|
connection: this.#options.connection,
|
|
103509
105218
|
tokenLocked: this.#options.tokenLocked,
|
|
103510
105219
|
fetch: this.#fetch,
|
|
105220
|
+
blazor: this.#options.blazor?.(),
|
|
105221
|
+
blazorBackoff: this.#blazorBackoff,
|
|
103511
105222
|
now: this.#options.now ?? Date.now
|
|
103512
105223
|
});
|
|
103513
105224
|
if (this.#stopped) return;
|
|
@@ -103515,7 +105226,7 @@ var EnpalIntegrationManager = class {
|
|
|
103515
105226
|
this.#lastError = null;
|
|
103516
105227
|
this.#status = "connected";
|
|
103517
105228
|
if (this.#failureLogged || !wasConnected) this.#options.logger.info(`Enpal box ${this.name} reachable through ${snapshot.source} (${snapshot.readings.size} data points)`);
|
|
103518
|
-
|
|
105229
|
+
this.#logSourceChange(snapshot.source, snapshot.fallbackReason);
|
|
103519
105230
|
this.#failureLogged = false;
|
|
103520
105231
|
if (!wasConnected) this.#options.onConnected();
|
|
103521
105232
|
this.#options.onPolled();
|
|
@@ -103572,6 +105283,7 @@ var EnpalClientRegistry = class {
|
|
|
103572
105283
|
tokenLocked,
|
|
103573
105284
|
logger: this.#logger.withTags({ integrationId }),
|
|
103574
105285
|
fetch: this.#deps.fetch,
|
|
105286
|
+
blazor: this.#deps.blazor,
|
|
103575
105287
|
now: this.#deps.now,
|
|
103576
105288
|
onConnected: () => {
|
|
103577
105289
|
this.#deps.onConnected(integrationId);
|
|
@@ -103618,6 +105330,26 @@ var EnpalClientRegistry = class {
|
|
|
103618
105330
|
}
|
|
103619
105331
|
};
|
|
103620
105332
|
//#endregion
|
|
105333
|
+
//#region src/enpal-python-paths.ts
|
|
105334
|
+
var WORKER = "enpal_worker.py";
|
|
105335
|
+
/**
|
|
105336
|
+
* Absolute path of the bundled `python/enpal_worker.py`: next to `dist/` in a deployed
|
|
105337
|
+
* package, two levels up from `src/` in the workspace.
|
|
105338
|
+
*/
|
|
105339
|
+
function resolveEnpalWorker(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
105340
|
+
const here = (0, node_path.dirname)((0, node_url.fileURLToPath)(fromModuleUrl));
|
|
105341
|
+
const candidates = [
|
|
105342
|
+
(0, node_path.join)(here, "../python"),
|
|
105343
|
+
(0, node_path.join)(here, "../../python"),
|
|
105344
|
+
(0, node_path.join)(here, "python")
|
|
105345
|
+
];
|
|
105346
|
+
for (const dir of candidates) {
|
|
105347
|
+
const file = (0, node_path.join)(dir, WORKER);
|
|
105348
|
+
if ((0, node_fs.existsSync)(file)) return file;
|
|
105349
|
+
}
|
|
105350
|
+
throw new Error(`provider-enpal: ${WORKER} not found. Searched:\n${candidates.join("\n")}`);
|
|
105351
|
+
}
|
|
105352
|
+
//#endregion
|
|
103621
105353
|
//#region src/enpal-instances.ts
|
|
103622
105354
|
/**
|
|
103623
105355
|
* Enpal integrations as `integration-instances` (planning#416, D1007): one `connections` record
|
|
@@ -103799,23 +105531,29 @@ async function cascadeRemoveDevicesForIntegration(input) {
|
|
|
103799
105531
|
var enpalGatewaySchema = object({
|
|
103800
105532
|
equipment: literal("box"),
|
|
103801
105533
|
integrationId: string().min(1).describe("Owning integration"),
|
|
103802
|
-
system: literal(ENPAL_SYSTEM).optional()
|
|
105534
|
+
system: literal(ENPAL_SYSTEM).optional(),
|
|
105535
|
+
/** The box diagnostics it was adopted with; a box adopted before they existed has none. */
|
|
105536
|
+
specIds: array(string().min(1)).default([])
|
|
103803
105537
|
});
|
|
103804
105538
|
/**
|
|
103805
|
-
* The Enpal box itself, as a {@link DeviceType.Gateway}. Its connection lives on
|
|
103806
|
-
* INTEGRATION; this device is the box's presence in the device list
|
|
103807
|
-
*
|
|
103808
|
-
*
|
|
103809
|
-
*
|
|
103810
|
-
*
|
|
105539
|
+
* The Enpal box itself ("Enpal Box"), as a {@link DeviceType.Gateway}. Its connection lives on
|
|
105540
|
+
* the INTEGRATION; this device is the box's presence in the device list: its identity and
|
|
105541
|
+
* firmware as device attributes, its own diagnostics (CPU, memory, LTE) as hidden Sensor
|
|
105542
|
+
* children, and the one action the box really supports, `device-admin-link`, its local device
|
|
105543
|
+
* page. The box has no reboot and no update. The System and the heat pump are NOT children of
|
|
105544
|
+
* it; each is adopted as its own top-level Container. Online follows the integration's box
|
|
105545
|
+
* (the integration marks it offline when a poll fails).
|
|
103811
105546
|
*/
|
|
103812
105547
|
var EnpalGatewayDevice = class extends BaseDevice {
|
|
103813
105548
|
features = [];
|
|
103814
105549
|
integrationId;
|
|
105550
|
+
specIds;
|
|
105551
|
+
snapshotUnsub = null;
|
|
103815
105552
|
constructor(ctx) {
|
|
103816
105553
|
const cfg = enpalGatewaySchema.parse(ctx.persistedConfig ?? {});
|
|
103817
105554
|
super(ctx, enpalGatewaySchema, { type: DeviceType.Gateway });
|
|
103818
105555
|
this.integrationId = cfg.integrationId;
|
|
105556
|
+
this.specIds = cfg.specIds;
|
|
103819
105557
|
this.online = enpalConnections.getSnapshot(this.integrationId) !== null;
|
|
103820
105558
|
this.registerAdminLinkCap();
|
|
103821
105559
|
this.updateSourceInfo({
|
|
@@ -103823,6 +105561,37 @@ var EnpalGatewayDevice = class extends BaseDevice {
|
|
|
103823
105561
|
system: ENPAL_SYSTEM
|
|
103824
105562
|
});
|
|
103825
105563
|
}
|
|
105564
|
+
async onActivate() {
|
|
105565
|
+
await super.onActivate();
|
|
105566
|
+
const metadata = new EnpalMetadataSync({
|
|
105567
|
+
equipment: "box",
|
|
105568
|
+
deviceId: this.id,
|
|
105569
|
+
getSnapshot: () => enpalConnections.getSnapshot(this.integrationId),
|
|
105570
|
+
write: async (patch) => {
|
|
105571
|
+
await this.ctx.api.deviceManager.setMetadata.mutate({
|
|
105572
|
+
deviceId: this.id,
|
|
105573
|
+
patch
|
|
105574
|
+
});
|
|
105575
|
+
},
|
|
105576
|
+
logger: this.ctx.logger
|
|
105577
|
+
});
|
|
105578
|
+
this.snapshotUnsub = enpalConnections.onSnapshot(this.integrationId, () => {
|
|
105579
|
+
metadata.apply();
|
|
105580
|
+
});
|
|
105581
|
+
await metadata.apply();
|
|
105582
|
+
}
|
|
105583
|
+
async removeDevice() {
|
|
105584
|
+
this.snapshotUnsub?.();
|
|
105585
|
+
this.snapshotUnsub = null;
|
|
105586
|
+
await super.removeDevice();
|
|
105587
|
+
}
|
|
105588
|
+
getAccessoryChildren() {
|
|
105589
|
+
return accessoryChildSpecs({
|
|
105590
|
+
equipment: "box",
|
|
105591
|
+
specIds: this.specIds,
|
|
105592
|
+
integrationId: this.integrationId
|
|
105593
|
+
});
|
|
105594
|
+
}
|
|
103826
105595
|
/** The box's local device page (plain HTTP, the page port); no credentials in the URL. */
|
|
103827
105596
|
registerAdminLinkCap() {
|
|
103828
105597
|
this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
|
|
@@ -103878,10 +105647,11 @@ var ENPAL_MIGRATIONS_DATE = "2026-11-05";
|
|
|
103878
105647
|
/**
|
|
103879
105648
|
* Enpal solar box provider, `mode: account` and broker-less. An integration is one box: its
|
|
103880
105649
|
* host, InfluxDB access and token live on the integration's settings, and the addon polls it
|
|
103881
|
-
* with one manager per `integrationId`. Discovery offers
|
|
103882
|
-
* ({@link DeviceType.Gateway}
|
|
103883
|
-
*
|
|
103884
|
-
*
|
|
105650
|
+
* with one manager per `integrationId`. Discovery offers three pieces, each adopted only when
|
|
105651
|
+
* the operator picks it: the box itself ("Enpal Box", a {@link DeviceType.Gateway} carrying its
|
|
105652
|
+
* firmware, its diagnostics and a link to its device page), the "System" (PV, battery, grid,
|
|
105653
|
+
* house meter) and the "Heat pump (Enpal)", both {@link DeviceType.Container}s. Adopting a
|
|
105654
|
+
* piece adopts its Sensor children; a Container declares which of them stands for it.
|
|
103885
105655
|
*
|
|
103886
105656
|
* The box does not advertise itself on the network, so the integration is added by hand.
|
|
103887
105657
|
*/
|
|
@@ -103895,6 +105665,8 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
103895
105665
|
integrationReconciler = null;
|
|
103896
105666
|
clients = null;
|
|
103897
105667
|
instances = null;
|
|
105668
|
+
/** The Python bridge to the box's Blazor channel; null while the node has no embedded Python. */
|
|
105669
|
+
blazorReader = null;
|
|
103898
105670
|
constructor() {
|
|
103899
105671
|
super({});
|
|
103900
105672
|
}
|
|
@@ -103917,7 +105689,9 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
103917
105689
|
kind: enpalConnectionsEntryKind
|
|
103918
105690
|
}));
|
|
103919
105691
|
this.instances = buildEnpalInstances(records, { onChanged: () => this.reconcileIntegrations() });
|
|
105692
|
+
this.blazorReader = await this.createBlazorBridge();
|
|
103920
105693
|
this.clients = new EnpalClientRegistry(this.ctx.logger, {
|
|
105694
|
+
blazor: () => this.blazorReader ?? void 0,
|
|
103921
105695
|
onConnected: (integrationId) => this.setIntegrationDevicesOnline(integrationId, true),
|
|
103922
105696
|
onDisconnected: (integrationId) => this.setIntegrationDevicesOnline(integrationId, false)
|
|
103923
105697
|
});
|
|
@@ -103942,11 +105716,32 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
103942
105716
|
this.ctx.logger.warn("Enpal shutdown error", { meta: { error: errMsg(err) } });
|
|
103943
105717
|
}
|
|
103944
105718
|
this.clients = null;
|
|
105719
|
+
this.blazorReader = null;
|
|
103945
105720
|
this.integrationReconciler?.dispose();
|
|
103946
105721
|
this.integrationReconciler = null;
|
|
103947
105722
|
enpalConnections.clear();
|
|
103948
105723
|
await super.onShutdown();
|
|
103949
105724
|
}
|
|
105725
|
+
/**
|
|
105726
|
+
* The embedded Python and the bundled worker, or null with a logged reason: the addon then
|
|
105727
|
+
* reads InfluxDB or the device page, which on firmware 8.51 and later hold less.
|
|
105728
|
+
*/
|
|
105729
|
+
async createBlazorBridge() {
|
|
105730
|
+
try {
|
|
105731
|
+
const pythonPath = await this.ctx.deps.ensurePython();
|
|
105732
|
+
if (pythonPath === null) {
|
|
105733
|
+
this.ctx.logger.warn("Enpal: embedded Python is unavailable, the Blazor channel is not used");
|
|
105734
|
+
return null;
|
|
105735
|
+
}
|
|
105736
|
+
return createBlazorReader({
|
|
105737
|
+
pythonPath,
|
|
105738
|
+
scriptPath: resolveEnpalWorker()
|
|
105739
|
+
});
|
|
105740
|
+
} catch (err) {
|
|
105741
|
+
this.ctx.logger.warn("Enpal: the Python bridge could not be prepared", { meta: { error: errMsg(err) } });
|
|
105742
|
+
return null;
|
|
105743
|
+
}
|
|
105744
|
+
}
|
|
103950
105745
|
requireInstances() {
|
|
103951
105746
|
if (!this.instances) throw new Error("Enpal provider not initialised");
|
|
103952
105747
|
return this.instances;
|
|
@@ -104050,7 +105845,7 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
104050
105845
|
}
|
|
104051
105846
|
return out;
|
|
104052
105847
|
},
|
|
104053
|
-
adoptDevice: async ({ equipment, specIds, integrationId, name }) => {
|
|
105848
|
+
adoptDevice: async ({ equipment, specIds, integrationId, name, childLayout, primaryChildPreference }) => {
|
|
104054
105849
|
const devices = this.ctx.kernel.devices;
|
|
104055
105850
|
if (!devices) throw new Error("enpal adopt: kernel.devices unavailable");
|
|
104056
105851
|
const config = {
|
|
@@ -104063,7 +105858,9 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
104063
105858
|
const device = await devices.create(stableId, EnpalEquipmentDevice, config, null, {
|
|
104064
105859
|
type: DeviceType.Container,
|
|
104065
105860
|
name,
|
|
104066
|
-
integrationId
|
|
105861
|
+
integrationId,
|
|
105862
|
+
...childLayout.length > 0 ? { childLayout } : {},
|
|
105863
|
+
...primaryChildPreference.length > 0 ? { primaryChildPreference } : {}
|
|
104067
105864
|
});
|
|
104068
105865
|
const children = await devices.getChildren(device.id);
|
|
104069
105866
|
return {
|
|
@@ -104071,20 +105868,27 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
104071
105868
|
accessoryDeviceIds: children.map((c) => c.id)
|
|
104072
105869
|
};
|
|
104073
105870
|
},
|
|
104074
|
-
adoptGateway: async ({ integrationId, name }) => {
|
|
105871
|
+
adoptGateway: async ({ integrationId, name, specIds, childLayout }) => {
|
|
104075
105872
|
const devices = this.ctx.kernel.devices;
|
|
104076
105873
|
if (!devices) throw new Error("enpal adopt: kernel.devices unavailable");
|
|
104077
105874
|
const config = {
|
|
104078
105875
|
equipment: "box",
|
|
105876
|
+
specIds,
|
|
104079
105877
|
integrationId,
|
|
104080
105878
|
system: ENPAL_SYSTEM
|
|
104081
105879
|
};
|
|
104082
105880
|
const stableId = this.generateStableId(DeviceType.Gateway, config);
|
|
104083
|
-
|
|
105881
|
+
const device = await devices.create(stableId, EnpalGatewayDevice, config, null, {
|
|
104084
105882
|
type: DeviceType.Gateway,
|
|
104085
105883
|
name,
|
|
104086
|
-
integrationId
|
|
104087
|
-
|
|
105884
|
+
integrationId,
|
|
105885
|
+
...childLayout.length > 0 ? { childLayout } : {}
|
|
105886
|
+
});
|
|
105887
|
+
const children = await devices.getChildren(device.id);
|
|
105888
|
+
return {
|
|
105889
|
+
deviceId: device.id,
|
|
105890
|
+
accessoryDeviceIds: children.map((c) => c.id)
|
|
105891
|
+
};
|
|
104088
105892
|
},
|
|
104089
105893
|
removeDevice: async (id) => {
|
|
104090
105894
|
await this.ctx.kernel.devices?.remove(id);
|