@camstack/addon-provider-enpal 0.1.15 → 0.1.16
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 +10 -0
- package/README.md +19 -18
- package/dist/addon.js +1219 -152
- package/dist/addon.mjs +1219 -152
- 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";
|
|
@@ -6026,7 +6030,7 @@ var ZodIssueCode = {
|
|
|
6026
6030
|
var ZodFirstPartyTypeKind;
|
|
6027
6031
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6028
6032
|
//#endregion
|
|
6029
|
-
//#region ../types/dist/sleep-
|
|
6033
|
+
//#region ../types/dist/sleep-Cw1uUXvl.mjs
|
|
6030
6034
|
/**
|
|
6031
6035
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6032
6036
|
* window to float samples (D455).
|
|
@@ -15067,6 +15071,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
15067
15071
|
backup: system("system backups — run and restored from the Backups page"),
|
|
15068
15072
|
broker: system("message-broker connections — configured, not actuated"),
|
|
15069
15073
|
"calendar-source": system(INTERNAL),
|
|
15074
|
+
"cluster-update": system(CLUSTER),
|
|
15070
15075
|
calendars: system("calendars feed schedules and conditions, not device actions"),
|
|
15071
15076
|
composer: system("device composition — an authoring tool, not an action"),
|
|
15072
15077
|
"core-blocks": system(INTERNAL),
|
|
@@ -15334,6 +15339,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
15334
15339
|
"camera-streams",
|
|
15335
15340
|
"carbon-monoxide",
|
|
15336
15341
|
"climate-control",
|
|
15342
|
+
"cluster-update",
|
|
15337
15343
|
"color",
|
|
15338
15344
|
"composer",
|
|
15339
15345
|
"connectivity",
|
|
@@ -49550,6 +49556,140 @@ var climateControlCapability = {
|
|
|
49550
49556
|
} }
|
|
49551
49557
|
};
|
|
49552
49558
|
/**
|
|
49559
|
+
* cluster-update — the ONE flow that brings the whole cluster to a release
|
|
49560
|
+
* (planning#493). Hub-provided (`server-provided`), hub-only: the hub runs
|
|
49561
|
+
* the sequence itself, so the admin UI, the MCP and the CLI all start the same
|
|
49562
|
+
* job and read the same report instead of each driving their own loop.
|
|
49563
|
+
*
|
|
49564
|
+
* 1. backup (`backup.trigger`) — skipped only when the caller says so
|
|
49565
|
+
* 2. the hub's server closure (`applyServerUpdate`); the hub restarts and the
|
|
49566
|
+
* job RESUMES on the new boot, verified against the hub's running version
|
|
49567
|
+
* 3. every online agent behind the target, one at a time, verified: runs the
|
|
49568
|
+
* target, settled, online, and its capabilities registered (D848) — an
|
|
49569
|
+
* agent that comes back capless is a FAILED step, never a done one
|
|
49570
|
+
* 4. every stale addon on the hub (`addons.updatePackage`); agents follow by
|
|
49571
|
+
* reconciliation, which is verified, never pushed
|
|
49572
|
+
*
|
|
49573
|
+
* The job stops on the first failed step with a typed reason (D877). Its state
|
|
49574
|
+
* is durable on the hub and read by RPC (`getClusterUpdateJob`) — events are
|
|
49575
|
+
* telemetry and never carry it (D8, D11).
|
|
49576
|
+
*/
|
|
49577
|
+
/** The steps, in the order they run. */
|
|
49578
|
+
var ClusterUpdateStepKindSchema = _enum([
|
|
49579
|
+
"backup",
|
|
49580
|
+
"hub-server",
|
|
49581
|
+
"agent-server",
|
|
49582
|
+
"addons"
|
|
49583
|
+
]);
|
|
49584
|
+
/**
|
|
49585
|
+
* - `pending` — not reached yet.
|
|
49586
|
+
* - `running` — in progress (for `hub-server`: possibly across the restart).
|
|
49587
|
+
* - `ok` — done AND verified.
|
|
49588
|
+
* - `failed` — did not land; `reason` + `detail` say why. Stops the job.
|
|
49589
|
+
* - `skipped` — not run: nothing to do, opted out, the node is offline, or a
|
|
49590
|
+
* step before it failed. `detail` says which.
|
|
49591
|
+
*/
|
|
49592
|
+
var ClusterUpdateStepStatusSchema = _enum([
|
|
49593
|
+
"pending",
|
|
49594
|
+
"running",
|
|
49595
|
+
"ok",
|
|
49596
|
+
"failed",
|
|
49597
|
+
"skipped"
|
|
49598
|
+
]);
|
|
49599
|
+
/**
|
|
49600
|
+
* Why a step (and with it the job) failed. Stable, greppable; `detail` carries
|
|
49601
|
+
* the prose.
|
|
49602
|
+
*/
|
|
49603
|
+
var ClusterUpdateFailureReasonSchema = _enum([
|
|
49604
|
+
"backup-unavailable",
|
|
49605
|
+
"backup-failed",
|
|
49606
|
+
"server-refused",
|
|
49607
|
+
"server-not-on-target",
|
|
49608
|
+
"node-capless",
|
|
49609
|
+
"status-unreadable",
|
|
49610
|
+
"addon-update-failed",
|
|
49611
|
+
"addons-not-converged",
|
|
49612
|
+
"interrupted"
|
|
49613
|
+
]);
|
|
49614
|
+
var ClusterUpdateStepSchema = object({
|
|
49615
|
+
kind: ClusterUpdateStepKindSchema,
|
|
49616
|
+
/** The node the step acts on; absent for `backup` and `addons`. */
|
|
49617
|
+
nodeId: string().optional(),
|
|
49618
|
+
status: ClusterUpdateStepStatusSchema,
|
|
49619
|
+
/** One line for the operator: what happened, or why not. */
|
|
49620
|
+
detail: string(),
|
|
49621
|
+
reason: ClusterUpdateFailureReasonSchema.optional(),
|
|
49622
|
+
/** `hub-server` / `agent-server`: the version the node ran before. */
|
|
49623
|
+
fromVersion: string().nullable().optional(),
|
|
49624
|
+
/** `addons`: the packages this step updates, `name from -> to`. */
|
|
49625
|
+
packages: array(string()).readonly().optional(),
|
|
49626
|
+
startedAt: number$1().optional(),
|
|
49627
|
+
finishedAt: number$1().optional()
|
|
49628
|
+
});
|
|
49629
|
+
var ClusterUpdateJobStateSchema = _enum([
|
|
49630
|
+
"running",
|
|
49631
|
+
"succeeded",
|
|
49632
|
+
"failed"
|
|
49633
|
+
]);
|
|
49634
|
+
var ClusterUpdateJobSchema = object({
|
|
49635
|
+
id: string(),
|
|
49636
|
+
state: ClusterUpdateJobStateSchema,
|
|
49637
|
+
/** The server version every node is brought to. */
|
|
49638
|
+
targetVersion: string(),
|
|
49639
|
+
/** Who started it (the user name, or `system`). */
|
|
49640
|
+
requestedBy: string(),
|
|
49641
|
+
startedAt: number$1(),
|
|
49642
|
+
finishedAt: number$1().optional(),
|
|
49643
|
+
/** Every step, in run order; later steps stay `pending` while it runs. */
|
|
49644
|
+
steps: array(ClusterUpdateStepSchema).readonly(),
|
|
49645
|
+
/** On `failed`: the step that stopped it (`nodeId` or the step kind). */
|
|
49646
|
+
stoppedAt: string().optional(),
|
|
49647
|
+
reason: ClusterUpdateFailureReasonSchema.optional(),
|
|
49648
|
+
/** Agents offline when the plan was read: never touched. */
|
|
49649
|
+
skippedOffline: array(string()).readonly()
|
|
49650
|
+
});
|
|
49651
|
+
var ClusterUpdateOptionsSchema = object({
|
|
49652
|
+
/** Explicit server target; omitted = the hub's latest registry check. */
|
|
49653
|
+
version: string().optional(),
|
|
49654
|
+
/** Update the addons too (default true). */
|
|
49655
|
+
addons: boolean().optional(),
|
|
49656
|
+
/**
|
|
49657
|
+
* Back up before touching anything (default true). `false` is an explicit
|
|
49658
|
+
* operator opt-out; a missing backup provider is otherwise a failure.
|
|
49659
|
+
*/
|
|
49660
|
+
backup: boolean().optional()
|
|
49661
|
+
});
|
|
49662
|
+
/** Why a plan cannot start. */
|
|
49663
|
+
var ClusterUpdateBlockerSchema = _enum([
|
|
49664
|
+
"no-target",
|
|
49665
|
+
"node-busy",
|
|
49666
|
+
"job-running",
|
|
49667
|
+
"nothing-to-do",
|
|
49668
|
+
"status-unreadable"
|
|
49669
|
+
]);
|
|
49670
|
+
var ClusterUpdatePlanSchema = object({
|
|
49671
|
+
/** `null` when no target could be resolved (`blocker: 'no-target'`). */
|
|
49672
|
+
targetVersion: string().nullable(),
|
|
49673
|
+
/** The steps the job WOULD run, all `pending` (or `skipped` with a reason). */
|
|
49674
|
+
steps: array(ClusterUpdateStepSchema).readonly(),
|
|
49675
|
+
skippedOffline: array(string()).readonly(),
|
|
49676
|
+
/** `null` = the plan can start. */
|
|
49677
|
+
blocker: ClusterUpdateBlockerSchema.nullable(),
|
|
49678
|
+
/** Prose for `blocker`. */
|
|
49679
|
+
blockerDetail: string().nullable()
|
|
49680
|
+
});
|
|
49681
|
+
var StartClusterUpdateResultSchema = object({
|
|
49682
|
+
accepted: boolean(),
|
|
49683
|
+
/** The job to follow with `getClusterUpdateJob`; `null` when refused. */
|
|
49684
|
+
jobId: string().nullable(),
|
|
49685
|
+
blocker: ClusterUpdateBlockerSchema.nullable(),
|
|
49686
|
+
message: string()
|
|
49687
|
+
});
|
|
49688
|
+
method(ClusterUpdateOptionsSchema, ClusterUpdatePlanSchema, { auth: "admin" }), method(ClusterUpdateOptionsSchema, StartClusterUpdateResultSchema, {
|
|
49689
|
+
kind: "mutation",
|
|
49690
|
+
auth: "admin"
|
|
49691
|
+
}), method(object({ jobId: string().optional() }), ClusterUpdateJobSchema.nullable(), { auth: "admin" });
|
|
49692
|
+
/**
|
|
49553
49693
|
* Color-light cap. Coexists with `switch` (on/off) and `brightness`
|
|
49554
49694
|
* (level) on the same device — `switch` toggles the bulb, `brightness`
|
|
49555
49695
|
* sets the dim level, this cap sets the chromaticity.
|
|
@@ -50881,7 +51021,7 @@ var mediaPlayerCapability = {
|
|
|
50881
51021
|
} }
|
|
50882
51022
|
};
|
|
50883
51023
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.HeatPump, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.MediaPlayer;
|
|
50884
|
-
var NUMBER = {
|
|
51024
|
+
var NUMBER$1 = {
|
|
50885
51025
|
kind: "fixed",
|
|
50886
51026
|
result: ["number"]
|
|
50887
51027
|
};
|
|
@@ -50890,19 +51030,19 @@ var BOOLEAN = {
|
|
|
50890
51030
|
result: ["boolean"]
|
|
50891
51031
|
};
|
|
50892
51032
|
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,
|
|
51033
|
+
min: NUMBER$1,
|
|
51034
|
+
max: NUMBER$1,
|
|
51035
|
+
abs: NUMBER$1,
|
|
51036
|
+
floor: NUMBER$1,
|
|
51037
|
+
ceil: NUMBER$1,
|
|
51038
|
+
sqrt: NUMBER$1,
|
|
51039
|
+
round: NUMBER$1,
|
|
51040
|
+
pow: NUMBER$1,
|
|
51041
|
+
clamp: NUMBER$1,
|
|
51042
|
+
avg: NUMBER$1,
|
|
51043
|
+
sum: NUMBER$1,
|
|
51044
|
+
age: NUMBER$1,
|
|
51045
|
+
convert: NUMBER$1,
|
|
50906
51046
|
coalesce: { kind: "coalesce" },
|
|
50907
51047
|
any: BOOLEAN,
|
|
50908
51048
|
all: BOOLEAN,
|
|
@@ -50919,7 +51059,7 @@ Object.freeze({
|
|
|
50919
51059
|
kind: "fixed",
|
|
50920
51060
|
result: ["number", "null"]
|
|
50921
51061
|
},
|
|
50922
|
-
count: NUMBER,
|
|
51062
|
+
count: NUMBER$1,
|
|
50923
51063
|
dwell: {
|
|
50924
51064
|
kind: "fixed",
|
|
50925
51065
|
result: ["boolean", "null"]
|
|
@@ -50945,7 +51085,7 @@ var CHANGED = {
|
|
|
50945
51085
|
fieldPath: "lastChangedAt",
|
|
50946
51086
|
expr: "now"
|
|
50947
51087
|
};
|
|
50948
|
-
function numeric(input) {
|
|
51088
|
+
function numeric$1(input) {
|
|
50949
51089
|
const { alsoFrom, ...base } = input;
|
|
50950
51090
|
return {
|
|
50951
51091
|
...base,
|
|
@@ -50980,7 +51120,7 @@ function binary(input) {
|
|
|
50980
51120
|
};
|
|
50981
51121
|
}
|
|
50982
51122
|
var GROUP_TEMPLATES = [
|
|
50983
|
-
numeric({
|
|
51123
|
+
numeric$1({
|
|
50984
51124
|
id: "temperature",
|
|
50985
51125
|
summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
|
|
50986
51126
|
role: DeviceRole.TemperatureSensor,
|
|
@@ -50998,7 +51138,7 @@ var GROUP_TEMPLATES = [
|
|
|
50998
51138
|
}
|
|
50999
51139
|
}
|
|
51000
51140
|
}),
|
|
51001
|
-
numeric({
|
|
51141
|
+
numeric$1({
|
|
51002
51142
|
id: "humidity",
|
|
51003
51143
|
summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
51004
51144
|
role: DeviceRole.HumiditySensor,
|
|
@@ -51016,7 +51156,7 @@ var GROUP_TEMPLATES = [
|
|
|
51016
51156
|
}
|
|
51017
51157
|
}
|
|
51018
51158
|
}),
|
|
51019
|
-
numeric({
|
|
51159
|
+
numeric$1({
|
|
51020
51160
|
id: "pressure",
|
|
51021
51161
|
summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
|
|
51022
51162
|
role: DeviceRole.PressureSensor,
|
|
@@ -51027,7 +51167,7 @@ var GROUP_TEMPLATES = [
|
|
|
51027
51167
|
empty: "null",
|
|
51028
51168
|
companions: [NOW$3]
|
|
51029
51169
|
}),
|
|
51030
|
-
numeric({
|
|
51170
|
+
numeric$1({
|
|
51031
51171
|
id: "illuminance",
|
|
51032
51172
|
summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
|
|
51033
51173
|
role: DeviceRole.AmbientLightSensor,
|
|
@@ -51038,7 +51178,7 @@ var GROUP_TEMPLATES = [
|
|
|
51038
51178
|
empty: "null",
|
|
51039
51179
|
companions: [NOW$3]
|
|
51040
51180
|
}),
|
|
51041
|
-
numeric({
|
|
51181
|
+
numeric$1({
|
|
51042
51182
|
id: "power",
|
|
51043
51183
|
summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
|
|
51044
51184
|
role: DeviceRole.PowerSensor,
|
|
@@ -51049,7 +51189,7 @@ var GROUP_TEMPLATES = [
|
|
|
51049
51189
|
empty: "omit",
|
|
51050
51190
|
companions: [NOW$3]
|
|
51051
51191
|
}),
|
|
51052
|
-
numeric({
|
|
51192
|
+
numeric$1({
|
|
51053
51193
|
id: "energy",
|
|
51054
51194
|
summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
|
|
51055
51195
|
role: DeviceRole.EnergySensor,
|
|
@@ -51060,7 +51200,7 @@ var GROUP_TEMPLATES = [
|
|
|
51060
51200
|
empty: "omit",
|
|
51061
51201
|
companions: [NOW$3]
|
|
51062
51202
|
}),
|
|
51063
|
-
numeric({
|
|
51203
|
+
numeric$1({
|
|
51064
51204
|
id: "battery",
|
|
51065
51205
|
summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
|
|
51066
51206
|
role: DeviceRole.BatterySensor,
|
|
@@ -51084,7 +51224,7 @@ var GROUP_TEMPLATES = [
|
|
|
51084
51224
|
}
|
|
51085
51225
|
]
|
|
51086
51226
|
}),
|
|
51087
|
-
numeric({
|
|
51227
|
+
numeric$1({
|
|
51088
51228
|
id: "numeric",
|
|
51089
51229
|
summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
|
|
51090
51230
|
role: DeviceRole.NumericSensor,
|
|
@@ -53022,6 +53162,90 @@ var SiteZoneSchema = object({
|
|
|
53022
53162
|
* the ones that most want grouping. */
|
|
53023
53163
|
locationKeys: array(string())
|
|
53024
53164
|
});
|
|
53165
|
+
var DeviceSearchRequiresSchema = object({
|
|
53166
|
+
/** Capability names the field wants a value of. */
|
|
53167
|
+
caps: array(string()).readonly(),
|
|
53168
|
+
/** `any` (default): a device with one of them is compatible. `all`: it must hold every one. */
|
|
53169
|
+
mode: _enum(["any", "all"]).optional()
|
|
53170
|
+
});
|
|
53171
|
+
var DeviceSearchFiltersSchema = object({
|
|
53172
|
+
locations: array(string()).readonly().optional(),
|
|
53173
|
+
integrationIds: array(string()).readonly().optional(),
|
|
53174
|
+
types: array(string()).readonly().optional()
|
|
53175
|
+
});
|
|
53176
|
+
var DeviceSearchInputSchema = object({
|
|
53177
|
+
/** Matches name, location, type, integration (id and label) and id. Case-insensitive. */
|
|
53178
|
+
text: string().optional(),
|
|
53179
|
+
requires: DeviceSearchRequiresSchema.optional(),
|
|
53180
|
+
filters: DeviceSearchFiltersSchema.optional(),
|
|
53181
|
+
/** List the devices that do not provide the required cap too (greyed, not pickable). */
|
|
53182
|
+
includeIncompatible: boolean().optional(),
|
|
53183
|
+
/** The device the field belongs to: its location drives `suggested`. */
|
|
53184
|
+
contextDeviceId: number$1().optional(),
|
|
53185
|
+
/**
|
|
53186
|
+
* Resolve exactly these devices, regardless of every filter, so a picked
|
|
53187
|
+
* value can be rendered. When given, `items` is these devices in the order
|
|
53188
|
+
* asked (no paging, `nextCursor` null) and the ones that do not exist are
|
|
53189
|
+
* listed in `missingIds`.
|
|
53190
|
+
*/
|
|
53191
|
+
ids: array(number$1()).readonly().optional(),
|
|
53192
|
+
cursor: string().nullable().optional(),
|
|
53193
|
+
limit: number$1().int().min(1).max(200).optional()
|
|
53194
|
+
});
|
|
53195
|
+
var DeviceValueOptionSchema = object({
|
|
53196
|
+
cap: string(),
|
|
53197
|
+
/** Status-schema field path inside the cap (same paths as `getWireableFields`). */
|
|
53198
|
+
path: string(),
|
|
53199
|
+
label: string(),
|
|
53200
|
+
/** The current value as text; `null` when it is unknown or the read FAILED — never `0`. */
|
|
53201
|
+
reading: string().nullable(),
|
|
53202
|
+
unit: string().nullable().optional(),
|
|
53203
|
+
/** The field's measured quantity, when the cap declares one, so a client can format the reading. */
|
|
53204
|
+
quantity: ReadingQuantitySchema.optional()
|
|
53205
|
+
});
|
|
53206
|
+
var DeviceSearchItemSchema = object({
|
|
53207
|
+
id: number$1(),
|
|
53208
|
+
name: string(),
|
|
53209
|
+
type: _enum(DeviceType),
|
|
53210
|
+
location: string().nullable(),
|
|
53211
|
+
integrationId: string().nullable(),
|
|
53212
|
+
integrationLabel: string().nullable(),
|
|
53213
|
+
online: boolean().nullable(),
|
|
53214
|
+
disabled: boolean(),
|
|
53215
|
+
compatible: boolean(),
|
|
53216
|
+
/** Only the required caps' fields; empty when nothing was required or the device is incompatible. */
|
|
53217
|
+
values: array(DeviceValueOptionSchema).readonly()
|
|
53218
|
+
});
|
|
53219
|
+
var DeviceSearchFacetValueSchema = object({
|
|
53220
|
+
value: string(),
|
|
53221
|
+
count: number$1()
|
|
53222
|
+
});
|
|
53223
|
+
var DeviceSearchFacetIntegrationSchema = object({
|
|
53224
|
+
id: string(),
|
|
53225
|
+
label: string(),
|
|
53226
|
+
count: number$1()
|
|
53227
|
+
});
|
|
53228
|
+
var DeviceSearchFacetsSchema = object({
|
|
53229
|
+
locations: array(DeviceSearchFacetValueSchema).readonly(),
|
|
53230
|
+
integrations: array(DeviceSearchFacetIntegrationSchema).readonly(),
|
|
53231
|
+
types: array(DeviceSearchFacetValueSchema).readonly()
|
|
53232
|
+
});
|
|
53233
|
+
var DeviceSearchResultSchema = object({
|
|
53234
|
+
items: array(DeviceSearchItemSchema).readonly(),
|
|
53235
|
+
nextCursor: string().nullable(),
|
|
53236
|
+
/** Devices listed by this query across all pages (filters + text applied; incompatible counted only when `includeIncompatible`). */
|
|
53237
|
+
total: number$1(),
|
|
53238
|
+
/** Compatible devices after filters + text. */
|
|
53239
|
+
compatibleTotal: number$1(),
|
|
53240
|
+
/** Every device the caller's fleet holds, before anything is applied. */
|
|
53241
|
+
fleetTotal: number$1(),
|
|
53242
|
+
/** Incompatible devices after filters + text — the K of "show the K that don't". */
|
|
53243
|
+
hiddenIncompatible: number$1(),
|
|
53244
|
+
facets: DeviceSearchFacetsSchema,
|
|
53245
|
+
/** Same location as `contextDeviceId`; first page only. */
|
|
53246
|
+
suggested: array(DeviceSearchItemSchema).readonly(),
|
|
53247
|
+
missingIds: array(number$1()).readonly()
|
|
53248
|
+
});
|
|
53025
53249
|
var ProviderStatusSchema = object({
|
|
53026
53250
|
connected: boolean(),
|
|
53027
53251
|
deviceCount: number$1(),
|
|
@@ -53507,6 +53731,9 @@ var DeviceInfoSchema = object({
|
|
|
53507
53731
|
* named accordion sections (with optional intra-section order). See
|
|
53508
53732
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
53509
53733
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
53734
|
+
/** The children a container stands for, most preferred first, by accessory
|
|
53735
|
+
* `childKey`. Provider-declared. See `DeviceMeta.primaryChildPreference`. */
|
|
53736
|
+
primaryChildPreference: array(string()).readonly().optional(),
|
|
53510
53737
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
53511
53738
|
display: DeviceDisplayOverrideSchema.optional()
|
|
53512
53739
|
});
|
|
@@ -53655,6 +53882,9 @@ var DeviceMetaSchema = object({
|
|
|
53655
53882
|
* accordion sections (with optional intra-section order). See
|
|
53656
53883
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
53657
53884
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
53885
|
+
/** The children a container stands for, most preferred first, by accessory
|
|
53886
|
+
* `childKey`. Provider-declared. See `DeviceMeta.primaryChildPreference`. */
|
|
53887
|
+
primaryChildPreference: array(string()).readonly().optional(),
|
|
53658
53888
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
53659
53889
|
* Optional: only present for accessory children that carry a known role. */
|
|
53660
53890
|
role: string().nullable().optional(),
|
|
@@ -53870,6 +54100,12 @@ method(object({
|
|
|
53870
54100
|
}), _void(), {
|
|
53871
54101
|
kind: "mutation",
|
|
53872
54102
|
auth: "admin"
|
|
54103
|
+
}), method(object({
|
|
54104
|
+
deviceId: number$1(),
|
|
54105
|
+
primaryChildPreference: array(string()).readonly()
|
|
54106
|
+
}), _void(), {
|
|
54107
|
+
kind: "mutation",
|
|
54108
|
+
auth: "admin"
|
|
53873
54109
|
}), method(object({
|
|
53874
54110
|
deviceId: number$1(),
|
|
53875
54111
|
display: DeviceDisplayOverrideSchema.nullable()
|
|
@@ -54007,7 +54243,7 @@ method(object({
|
|
|
54007
54243
|
* it answers today and the caller filters as it already does.
|
|
54008
54244
|
*/
|
|
54009
54245
|
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({
|
|
54246
|
+
}), 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
54247
|
mode: LinkedDevicesModeSchema,
|
|
54012
54248
|
devices: array(LinkedDeviceSchema)
|
|
54013
54249
|
}), { 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({
|
|
@@ -57705,6 +57941,8 @@ var HeroPillSchema = object({
|
|
|
57705
57941
|
});
|
|
57706
57942
|
var HeroRowSchema = object({
|
|
57707
57943
|
id: HeroRowIdSchema,
|
|
57944
|
+
/** `scene`: the row's action and slider controls are drawn on the scene. Absent: an ordinary row. */
|
|
57945
|
+
placement: _enum(["row", "scene"]).optional(),
|
|
57708
57946
|
/** The row's own name when the kind's label would not tell rows of one kind apart (a circuit's label). */
|
|
57709
57947
|
label: HeroTextSchema.optional(),
|
|
57710
57948
|
/** A group header drawn above the row when it differs from the previous row's ("System"). */
|
|
@@ -69494,13 +69732,23 @@ var NcRuleTargetSchema = object({
|
|
|
69494
69732
|
*/
|
|
69495
69733
|
params: record(string(), unknown()).optional()
|
|
69496
69734
|
});
|
|
69735
|
+
/** Which picture kind one target receives (#487). */
|
|
69736
|
+
var NcTargetAttachmentSchema = _enum([
|
|
69737
|
+
"none",
|
|
69738
|
+
"image",
|
|
69739
|
+
"clip",
|
|
69740
|
+
"gif"
|
|
69741
|
+
]);
|
|
69497
69742
|
var NcTargetOverrideSchema = object({
|
|
69498
69743
|
template: object({
|
|
69499
69744
|
title: string().max(500).optional(),
|
|
69500
69745
|
body: string().max(2e3).optional()
|
|
69501
69746
|
}).optional(),
|
|
69502
69747
|
schedule: NcScheduleSchema.optional(),
|
|
69503
|
-
priority: number$1().int().min(1).max(5).optional()
|
|
69748
|
+
priority: number$1().int().min(1).max(5).optional(),
|
|
69749
|
+
attachment: NcTargetAttachmentSchema.optional(),
|
|
69750
|
+
/** `false` = no buttons for this target; `true` is the same as absent. */
|
|
69751
|
+
actions: boolean().optional()
|
|
69504
69752
|
});
|
|
69505
69753
|
/** Per-target settings keyed by target id (#487). */
|
|
69506
69754
|
var NcTargetOverridesSchema = record(string(), NcTargetOverrideSchema);
|
|
@@ -73538,18 +73786,18 @@ var powerSourceCapability = {
|
|
|
73538
73786
|
mode: "singleton",
|
|
73539
73787
|
deviceTypes: POWER_SOURCE_DEVICE_TYPES,
|
|
73540
73788
|
methods: {
|
|
73541
|
-
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "
|
|
73789
|
+
getStatus: systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "protected" }),
|
|
73542
73790
|
powerOn: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
73543
73791
|
kind: "mutation",
|
|
73544
|
-
auth: "
|
|
73792
|
+
auth: "protected"
|
|
73545
73793
|
}),
|
|
73546
73794
|
powerOff: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
73547
73795
|
kind: "mutation",
|
|
73548
|
-
auth: "
|
|
73796
|
+
auth: "protected"
|
|
73549
73797
|
}),
|
|
73550
73798
|
forceRestart: systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
73551
73799
|
kind: "mutation",
|
|
73552
|
-
auth: "
|
|
73800
|
+
auth: "protected"
|
|
73553
73801
|
})
|
|
73554
73802
|
},
|
|
73555
73803
|
runtimeState: PowerSourceStateSchema,
|
|
@@ -88299,6 +88547,27 @@ Object.freeze({
|
|
|
88299
88547
|
access: "create",
|
|
88300
88548
|
role: "viewer"
|
|
88301
88549
|
},
|
|
88550
|
+
"clusterUpdate.getClusterUpdateJob": {
|
|
88551
|
+
capName: "cluster-update",
|
|
88552
|
+
capScope: "system",
|
|
88553
|
+
addonId: null,
|
|
88554
|
+
access: "view",
|
|
88555
|
+
role: "admin"
|
|
88556
|
+
},
|
|
88557
|
+
"clusterUpdate.previewClusterUpdate": {
|
|
88558
|
+
capName: "cluster-update",
|
|
88559
|
+
capScope: "system",
|
|
88560
|
+
addonId: null,
|
|
88561
|
+
access: "view",
|
|
88562
|
+
role: "admin"
|
|
88563
|
+
},
|
|
88564
|
+
"clusterUpdate.startClusterUpdate": {
|
|
88565
|
+
capName: "cluster-update",
|
|
88566
|
+
capScope: "system",
|
|
88567
|
+
addonId: null,
|
|
88568
|
+
access: "create",
|
|
88569
|
+
role: "admin"
|
|
88570
|
+
},
|
|
88302
88571
|
"color.getStatus": {
|
|
88303
88572
|
capName: "color",
|
|
88304
88573
|
capScope: "device",
|
|
@@ -89671,6 +89940,13 @@ Object.freeze({
|
|
|
89671
89940
|
access: "create",
|
|
89672
89941
|
role: "admin"
|
|
89673
89942
|
},
|
|
89943
|
+
"deviceManager.search": {
|
|
89944
|
+
capName: "device-manager",
|
|
89945
|
+
capScope: "system",
|
|
89946
|
+
addonId: null,
|
|
89947
|
+
access: "view",
|
|
89948
|
+
role: "viewer"
|
|
89949
|
+
},
|
|
89674
89950
|
"deviceManager.setAlias": {
|
|
89675
89951
|
capName: "device-manager",
|
|
89676
89952
|
capScope: "system",
|
|
@@ -89762,6 +90038,13 @@ Object.freeze({
|
|
|
89762
90038
|
access: "create",
|
|
89763
90039
|
role: "admin"
|
|
89764
90040
|
},
|
|
90041
|
+
"deviceManager.setPrimaryChildPreference": {
|
|
90042
|
+
capName: "device-manager",
|
|
90043
|
+
capScope: "system",
|
|
90044
|
+
addonId: null,
|
|
90045
|
+
access: "create",
|
|
90046
|
+
role: "admin"
|
|
90047
|
+
},
|
|
89765
90048
|
"deviceManager.setRole": {
|
|
89766
90049
|
capName: "device-manager",
|
|
89767
90050
|
capScope: "system",
|
|
@@ -94401,28 +94684,28 @@ Object.freeze({
|
|
|
94401
94684
|
capScope: "device",
|
|
94402
94685
|
addonId: null,
|
|
94403
94686
|
access: "create",
|
|
94404
|
-
role: "
|
|
94687
|
+
role: "viewer"
|
|
94405
94688
|
},
|
|
94406
94689
|
"powerSource.getStatus": {
|
|
94407
94690
|
capName: "power-source",
|
|
94408
94691
|
capScope: "device",
|
|
94409
94692
|
addonId: null,
|
|
94410
94693
|
access: "view",
|
|
94411
|
-
role: "
|
|
94694
|
+
role: "viewer"
|
|
94412
94695
|
},
|
|
94413
94696
|
"powerSource.powerOff": {
|
|
94414
94697
|
capName: "power-source",
|
|
94415
94698
|
capScope: "device",
|
|
94416
94699
|
addonId: null,
|
|
94417
94700
|
access: "create",
|
|
94418
|
-
role: "
|
|
94701
|
+
role: "viewer"
|
|
94419
94702
|
},
|
|
94420
94703
|
"powerSource.powerOn": {
|
|
94421
94704
|
capName: "power-source",
|
|
94422
94705
|
capScope: "device",
|
|
94423
94706
|
addonId: null,
|
|
94424
94707
|
access: "create",
|
|
94425
|
-
role: "
|
|
94708
|
+
role: "viewer"
|
|
94426
94709
|
},
|
|
94427
94710
|
"presence.getStatus": {
|
|
94428
94711
|
capName: "presence",
|
|
@@ -98286,6 +98569,11 @@ Object.freeze({
|
|
|
98286
98569
|
form: "single",
|
|
98287
98570
|
optional: false
|
|
98288
98571
|
}],
|
|
98572
|
+
"deviceManager.setPrimaryChildPreference": [{
|
|
98573
|
+
name: "deviceId",
|
|
98574
|
+
form: "single",
|
|
98575
|
+
optional: false
|
|
98576
|
+
}],
|
|
98289
98577
|
"deviceManager.setRole": [{
|
|
98290
98578
|
name: "deviceId",
|
|
98291
98579
|
form: "single",
|
|
@@ -101754,7 +102042,9 @@ var TimelapseRuleInputSchema = object({
|
|
|
101754
102042
|
/** Classes the notification counts — see {@link ReportClassesField}. */
|
|
101755
102043
|
reportClasses: ReportClassesField.optional(),
|
|
101756
102044
|
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
101757
|
-
priority: PriorityField.default(3)
|
|
102045
|
+
priority: PriorityField.default(3),
|
|
102046
|
+
/** Per-target settings keyed by target id (D1032) — absent = every target follows the rule. */
|
|
102047
|
+
targetOverrides: NcTargetOverridesSchema.optional()
|
|
101758
102048
|
});
|
|
101759
102049
|
object({
|
|
101760
102050
|
name: NameField.optional(),
|
|
@@ -101770,7 +102060,9 @@ object({
|
|
|
101770
102060
|
previewText: PreviewTextField.optional(),
|
|
101771
102061
|
previewMode: PreviewModeField.optional(),
|
|
101772
102062
|
reportClasses: ReportClassesField.optional(),
|
|
101773
|
-
priority: PriorityField.optional()
|
|
102063
|
+
priority: PriorityField.optional(),
|
|
102064
|
+
/** Per-target settings keyed by target id (D1032) — absent = every target follows the rule. */
|
|
102065
|
+
targetOverrides: NcTargetOverridesSchema.optional()
|
|
101774
102066
|
});
|
|
101775
102067
|
TimelapseRuleInputSchema.extend({
|
|
101776
102068
|
id: string(),
|
|
@@ -102136,13 +102428,15 @@ object({
|
|
|
102136
102428
|
* settings of the integration itself, not of a device. Every equipment the box reports is
|
|
102137
102429
|
* adopted as a Container from it.
|
|
102138
102430
|
*
|
|
102139
|
-
*
|
|
102431
|
+
* Sources behind `source`:
|
|
102432
|
+
* - `blazor`: the box's own web-page channel (Blazor Server, firmware 8.51 and later),
|
|
102433
|
+
* read by an embedded Python bridge. No token, nothing written to the box.
|
|
102140
102434
|
* - `influx`: the box's InfluxDB 2.x (port 8086, org `enpal`, bucket `solar`).
|
|
102141
102435
|
* Needs the access token Enpal support issues; it is a keychain secret.
|
|
102142
102436
|
* - `page`: the box's local device page (`/deviceMessages`). No token; fewer
|
|
102143
102437
|
* boxes and firmware versions still serve it.
|
|
102144
|
-
* - `auto` (default): InfluxDB when a token is set, the page
|
|
102145
|
-
*
|
|
102438
|
+
* - `auto` (default): `blazor`, then InfluxDB when a token is set, then the page; the first
|
|
102439
|
+
* one that answers wins and the snapshot records why a preferred one was skipped.
|
|
102146
102440
|
*/
|
|
102147
102441
|
/** Strip a pasted scheme and path: `http://192.0.2.10/deviceMessages` -> `192.0.2.10`. */
|
|
102148
102442
|
function cleanHost(value) {
|
|
@@ -102160,6 +102454,7 @@ var FLUX_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
|
102160
102454
|
var ENPAL_SYSTEM = "enpal";
|
|
102161
102455
|
var ENPAL_SOURCES = [
|
|
102162
102456
|
"auto",
|
|
102457
|
+
"blazor",
|
|
102163
102458
|
"influx",
|
|
102164
102459
|
"page"
|
|
102165
102460
|
];
|
|
@@ -102220,7 +102515,7 @@ function buildConnectionFormSchema() {
|
|
|
102220
102515
|
id: "source",
|
|
102221
102516
|
title: "Data source",
|
|
102222
102517
|
titleKey: "provider-enpal:connection.s_source.title",
|
|
102223
|
-
description: "
|
|
102518
|
+
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
102519
|
descriptionKey: "provider-enpal:connection.s_source.description",
|
|
102225
102520
|
columns: 2,
|
|
102226
102521
|
fields: [
|
|
@@ -102233,9 +102528,14 @@ function buildConnectionFormSchema() {
|
|
|
102233
102528
|
options: [
|
|
102234
102529
|
{
|
|
102235
102530
|
value: "auto",
|
|
102236
|
-
label: "Automatic (InfluxDB, then device page)",
|
|
102531
|
+
label: "Automatic (box web channel, InfluxDB, then device page)",
|
|
102237
102532
|
labelKey: "provider-enpal:connection.s_source.source.o_auto.label"
|
|
102238
102533
|
},
|
|
102534
|
+
{
|
|
102535
|
+
value: "blazor",
|
|
102536
|
+
label: "Box web channel only (firmware 8.51 and later)",
|
|
102537
|
+
labelKey: "provider-enpal:connection.s_source.source.o_blazor.label"
|
|
102538
|
+
},
|
|
102239
102539
|
{
|
|
102240
102540
|
value: "influx",
|
|
102241
102541
|
label: "InfluxDB only",
|
|
@@ -102351,6 +102651,22 @@ var point = (quantity, ...sources) => ({
|
|
|
102351
102651
|
quantity,
|
|
102352
102652
|
sources: sources.map((s) => typeof s === "string" ? { key: s } : s)
|
|
102353
102653
|
});
|
|
102654
|
+
var numeric = (id, name, quantity, key, role = DeviceRole.NumericSensor) => ({
|
|
102655
|
+
kind: "numeric",
|
|
102656
|
+
id,
|
|
102657
|
+
name,
|
|
102658
|
+
role,
|
|
102659
|
+
quantity,
|
|
102660
|
+
value: point(quantity, key)
|
|
102661
|
+
});
|
|
102662
|
+
var text = (id, name, key, timestamp = false) => ({
|
|
102663
|
+
kind: "text",
|
|
102664
|
+
id,
|
|
102665
|
+
name,
|
|
102666
|
+
role: timestamp ? DeviceRole.DateTimeSensor : DeviceRole.EnumSensor,
|
|
102667
|
+
key,
|
|
102668
|
+
...timestamp ? { timestamp: true } : {}
|
|
102669
|
+
});
|
|
102354
102670
|
/**
|
|
102355
102671
|
* What an Enpal box can show, by the dotted keys its InfluxDB and device page use (the
|
|
102356
102672
|
* names follow the Home Assistant component's key table). A device is offered only when the
|
|
@@ -102433,7 +102749,7 @@ var ENPAL_SPECS = [
|
|
|
102433
102749
|
id: "house",
|
|
102434
102750
|
name: "Consumption",
|
|
102435
102751
|
role: DeviceRole.PowerSensor,
|
|
102436
|
-
power: point("power", "Power.Consumption.Total"
|
|
102752
|
+
power: point("power", "Power.Consumption.Total"),
|
|
102437
102753
|
kwh: point("energy", "Energy.Consumption.Total.Lifetime")
|
|
102438
102754
|
},
|
|
102439
102755
|
{
|
|
@@ -102483,7 +102799,87 @@ var ENPAL_SPECS = [
|
|
|
102483
102799
|
role: DeviceRole.NumericSensor,
|
|
102484
102800
|
quantity: "percentage",
|
|
102485
102801
|
value: point("percentage", "Battery.SOH")
|
|
102486
|
-
}
|
|
102802
|
+
},
|
|
102803
|
+
numeric("pv-string-1-power", "String 1 power", "power", "Power.DC.String.1", DeviceRole.PowerSensor),
|
|
102804
|
+
numeric("pv-string-2-power", "String 2 power", "power", "Power.DC.String.2", DeviceRole.PowerSensor),
|
|
102805
|
+
numeric("pv-energy-day", "Production today", "energy", "Energy.Production.Total.Day", DeviceRole.EnergySensor),
|
|
102806
|
+
numeric("pv-string-1-voltage", "String 1 voltage", "voltage", "Voltage.String.1", DeviceRole.VoltageSensor),
|
|
102807
|
+
numeric("pv-string-2-voltage", "String 2 voltage", "voltage", "Voltage.String.2", DeviceRole.VoltageSensor),
|
|
102808
|
+
numeric("pv-string-1-current", "String 1 current", "current", "Current.String.1", DeviceRole.CurrentSensor),
|
|
102809
|
+
numeric("pv-string-2-current", "String 2 current", "current", "Current.String.2", DeviceRole.CurrentSensor),
|
|
102810
|
+
numeric("battery-stored-energy", "Stored energy", "energy", "Energy.Battery.Charge.Load", DeviceRole.EnergySensor),
|
|
102811
|
+
numeric("battery-charge-day", "Charged today", "energy", "Energy.Battery.Charge.Day", DeviceRole.EnergySensor),
|
|
102812
|
+
numeric("battery-discharge-day", "Discharged today", "energy", "Energy.Battery.Discharge.Day", DeviceRole.EnergySensor),
|
|
102813
|
+
numeric("battery-capacity", "Capacity", "energy", "Energy.Battery.Capacity", DeviceRole.EnergySensor),
|
|
102814
|
+
numeric("battery-power-rating", "Maximum power rating", "power", "Battery.Power.Rating.Maximum", DeviceRole.PowerSensor),
|
|
102815
|
+
numeric("battery-level-max", "Maximum charge level", "percentage", "Battery.ChargeLevel.Max"),
|
|
102816
|
+
numeric("battery-level-min", "Minimum charge level", "percentage", "Battery.ChargeLevel.Min"),
|
|
102817
|
+
numeric("battery-power-set", "Power setpoint", "power", "Power.Battery.Charge.Discharge.Set", DeviceRole.PowerSensor),
|
|
102818
|
+
numeric("battery-charge-power-max", "Maximum charge power", "power", "Power.Battery.Charge.Max", DeviceRole.PowerSensor),
|
|
102819
|
+
numeric("battery-discharge-power-max", "Maximum discharge power", "power", "Power.Battery.Discharge.Max", DeviceRole.PowerSensor),
|
|
102820
|
+
numeric("battery-charge-current-max", "Maximum charge current", "current", "Current.Battery.Charge.Max", DeviceRole.CurrentSensor),
|
|
102821
|
+
numeric("battery-discharge-current-max", "Maximum discharge current", "current", "Current.Battery.Discharge.Max", DeviceRole.CurrentSensor),
|
|
102822
|
+
numeric("grid-phase-a", "Phase A power", "power", "Power.AC.Phase.A", DeviceRole.PowerSensor),
|
|
102823
|
+
numeric("grid-phase-b", "Phase B power", "power", "Power.AC.Phase.B", DeviceRole.PowerSensor),
|
|
102824
|
+
numeric("grid-phase-c", "Phase C power", "power", "Power.AC.Phase.C", DeviceRole.PowerSensor),
|
|
102825
|
+
numeric("grid-import-day", "Imported today", "energy", "Energy.Grid.Import.Day", DeviceRole.EnergySensor),
|
|
102826
|
+
numeric("grid-export-day", "Exported today", "energy", "Energy.Grid.Export.Day", DeviceRole.EnergySensor),
|
|
102827
|
+
numeric("grid-voltage-a", "Phase A voltage", "voltage", "Voltage.Phase.A", DeviceRole.VoltageSensor),
|
|
102828
|
+
numeric("grid-voltage-b", "Phase B voltage", "voltage", "Voltage.Phase.B", DeviceRole.VoltageSensor),
|
|
102829
|
+
numeric("grid-voltage-c", "Phase C voltage", "voltage", "Voltage.Phase.C", DeviceRole.VoltageSensor),
|
|
102830
|
+
{
|
|
102831
|
+
kind: "numeric",
|
|
102832
|
+
id: "grid-frequency",
|
|
102833
|
+
name: "Frequency",
|
|
102834
|
+
role: DeviceRole.NumericSensor,
|
|
102835
|
+
quantity: "index",
|
|
102836
|
+
value: {
|
|
102837
|
+
...point("index", "Frequency.Grid"),
|
|
102838
|
+
ignoreUnit: true
|
|
102839
|
+
}
|
|
102840
|
+
},
|
|
102841
|
+
numeric("grid-feed-max", "Maximum feed-in power", "power", "Power.Grid.Maximum.Feed", DeviceRole.PowerSensor),
|
|
102842
|
+
numeric("house-energy-day", "Consumption today", "energy", "Energy.Consumption.Total.Day", DeviceRole.EnergySensor),
|
|
102843
|
+
text("inverter-state", "System state", "Inverter.System.State"),
|
|
102844
|
+
text("inverter-ems-mode", "EMS mode", "Command.EMS.Mode"),
|
|
102845
|
+
numeric("inverter-ac-power", "AC power", "power", "Power.Inverter.AC.Export", DeviceRole.PowerSensor),
|
|
102846
|
+
text("inverter-start-stop", "Start/stop command", "Command.Start.Stop"),
|
|
102847
|
+
text("inverter-forcible-mode", "Forcible charge/discharge mode", "Mode.Forcible.Charge.Discharge"),
|
|
102848
|
+
numeric("inverter-power-rating", "Maximum power rating", "power", "Inverter.Power.Rating.Maximum", DeviceRole.PowerSensor),
|
|
102849
|
+
text("inverter-phase-type", "Phase type", "Type.Phase.Inverter"),
|
|
102850
|
+
text("inverter-modbus-rtu", "Modbus RTU status", "Inverter.Modbus.Health.Rtu.Status"),
|
|
102851
|
+
text("inverter-modbus-tcp", "Modbus TCP status", "Inverter.Modbus.Health.Tcp.Status"),
|
|
102852
|
+
text("heatpump-hot-water-mode", "Hot water mode", "Heatpump.DomesticHotWater.1.Mode"),
|
|
102853
|
+
text("heatpump-heating-mode", "Heating circuit mode", "Heatpump.HeatingCircuit.1.Mode"),
|
|
102854
|
+
text("heatpump-generator-state", "Generator state", "Heatpump.Generator.State.Running"),
|
|
102855
|
+
text("heatpump-heater-state", "Electric heater state", "Heatpump.ElectricHeater.State.Running"),
|
|
102856
|
+
text("heatpump-energy-management", "Energy management state", "Heatpump.EnergyManagement.State"),
|
|
102857
|
+
text("heatpump-power-desired", "Power requested", "Heatpump.Power.Desired.Enabled"),
|
|
102858
|
+
text("heatpump-pv-optimization", "PV optimisation", "Heatpump.Settings.PvOptimizationStatus"),
|
|
102859
|
+
numeric("heatpump-power-min", "Generator minimum power", "power", "Heatpump.Generator.Power.Min", DeviceRole.PowerSensor),
|
|
102860
|
+
numeric("heatpump-power-max", "Generator maximum power", "power", "Heatpump.Generator.Power.Max", DeviceRole.PowerSensor),
|
|
102861
|
+
text("heatpump-connection", "Local connection", "Heatpump.Local.ConnectionState"),
|
|
102862
|
+
text("box-main-state", "Main state", "IoT.MainState"),
|
|
102863
|
+
numeric("box-cpu-load", "CPU load", "percentage", "Cpu.Load"),
|
|
102864
|
+
numeric("box-memory-usage", "Memory usage", "percentage", "Memory.Usage"),
|
|
102865
|
+
text("box-hardware-check", "Hardware check", "HW.Cronny.Result"),
|
|
102866
|
+
text("eebus-connection", "Connection state", "Eebus.ConnectionState"),
|
|
102867
|
+
text("eebus-discovery", "Discovery state", "Eebus.DiscoveryState"),
|
|
102868
|
+
text("eebus-cert-expiry", "Certificate expiry", "Eebus.CertExpiry", true),
|
|
102869
|
+
text("lte-state", "LTE state", "LTE.State"),
|
|
102870
|
+
text("lte-connection-type", "Connection type", "LTE.Connection.Type"),
|
|
102871
|
+
numeric("lte-quality", "LTE quality", "percentage", "LTE.Quality"),
|
|
102872
|
+
numeric("lte-rsrp", "RSRP", "signal-level", "LTE.RSRP"),
|
|
102873
|
+
numeric("lte-rsrq", "RSRQ", "index", "LTE.RSRQ"),
|
|
102874
|
+
numeric("lte-rssi", "RSSI", "signal-level", "LTE.RSSI"),
|
|
102875
|
+
numeric("lte-snr", "SNR", "index", "LTE.SNR"),
|
|
102876
|
+
text("lte-failover", "Failover result", "LTE.Failover.Result"),
|
|
102877
|
+
text("lte-signal-check", "Signal check", "LTE.Cronny.Result"),
|
|
102878
|
+
text("wifi-state", "Wi-Fi state", "WiFi.State"),
|
|
102879
|
+
numeric("data-lan-down", "LAN download this month", "count", "IoT.Data.Consumption.Lan.Down.Month"),
|
|
102880
|
+
numeric("data-lan-up", "LAN upload this month", "count", "IoT.Data.Consumption.Lan.Up.Month"),
|
|
102881
|
+
numeric("data-lte-down", "LTE download this month", "count", "IoT.Data.Consumption.Lte.Down.Month"),
|
|
102882
|
+
numeric("data-lte-up", "LTE upload this month", "count", "IoT.Data.Consumption.Lte.Up.Month")
|
|
102487
102883
|
];
|
|
102488
102884
|
function findSpec(id) {
|
|
102489
102885
|
return ENPAL_SPECS.find((s) => s.id === id);
|
|
@@ -102495,6 +102891,7 @@ function capsOfSpec(spec) {
|
|
|
102495
102891
|
case "battery": return ["battery", "power-meter"];
|
|
102496
102892
|
case "meter": return ["meter"];
|
|
102497
102893
|
case "numeric": return ["numeric-sensor"];
|
|
102894
|
+
case "text": return ["enum-sensor"];
|
|
102498
102895
|
}
|
|
102499
102896
|
}
|
|
102500
102897
|
function pointsOf(spec) {
|
|
@@ -102508,6 +102905,7 @@ function pointsOf(spec) {
|
|
|
102508
102905
|
];
|
|
102509
102906
|
case "meter":
|
|
102510
102907
|
case "numeric": return [spec.value];
|
|
102908
|
+
case "text": return [];
|
|
102511
102909
|
}
|
|
102512
102910
|
}
|
|
102513
102911
|
function resolve(target, snapshot) {
|
|
@@ -102515,7 +102913,7 @@ function resolve(target, snapshot) {
|
|
|
102515
102913
|
for (const source of target.sources) {
|
|
102516
102914
|
const reading = snapshot.readings.get(source.key);
|
|
102517
102915
|
if (reading === void 0) continue;
|
|
102518
|
-
const converted = toBase(reading.value, reading.unit, target.quantity);
|
|
102916
|
+
const converted = toBase(reading.value, target.ignoreUnit === true ? void 0 : reading.unit, target.quantity);
|
|
102519
102917
|
if (!converted.ok) {
|
|
102520
102918
|
refusal = `${source.key}: ${converted.reason} (unit ${reading.unit ?? "none"})`;
|
|
102521
102919
|
continue;
|
|
@@ -102534,7 +102932,7 @@ function resolve(target, snapshot) {
|
|
|
102534
102932
|
}
|
|
102535
102933
|
/** Specs whose data the box reported: a refused point still counts, the device exists. */
|
|
102536
102934
|
function availableSpecs(snapshot) {
|
|
102537
|
-
return ENPAL_SPECS.filter((spec) => pointsOf(spec).some((p) => resolve(p, snapshot).state !== "absent"));
|
|
102935
|
+
return ENPAL_SPECS.filter((spec) => spec.kind === "text" ? snapshot.texts.has(spec.key) : pointsOf(spec).some((p) => resolve(p, snapshot).state !== "absent"));
|
|
102538
102936
|
}
|
|
102539
102937
|
function numberOf(r) {
|
|
102540
102938
|
return r.state === "value" ? r.value : void 0;
|
|
@@ -102633,6 +103031,17 @@ function computeSlices(spec, snapshot, options, now) {
|
|
|
102633
103031
|
refusals: refusalOf(value)
|
|
102634
103032
|
};
|
|
102635
103033
|
}
|
|
103034
|
+
case "text": {
|
|
103035
|
+
const value = snapshot.texts.get(spec.key);
|
|
103036
|
+
return {
|
|
103037
|
+
...value !== void 0 ? { text: {
|
|
103038
|
+
value,
|
|
103039
|
+
...spec.timestamp === true ? { timestamp: true } : {},
|
|
103040
|
+
lastFetchedAt: now
|
|
103041
|
+
} } : {},
|
|
103042
|
+
refusals: []
|
|
103043
|
+
};
|
|
103044
|
+
}
|
|
102636
103045
|
}
|
|
102637
103046
|
}
|
|
102638
103047
|
//#endregion
|
|
@@ -102730,6 +103139,7 @@ var POWER_CAP = powerMeterCapability.name;
|
|
|
102730
103139
|
var BATTERY_CAP = batteryCapability.name;
|
|
102731
103140
|
var METER_CAP = meterCapability.name;
|
|
102732
103141
|
var NUMERIC_CAP = numericSensorCapability.name;
|
|
103142
|
+
var ENUM_CAP = enumSensorCapability.name;
|
|
102733
103143
|
var POWER_COLD = { lastFetchedAt: 0 };
|
|
102734
103144
|
var BATTERY_COLD = {
|
|
102735
103145
|
level: null,
|
|
@@ -102741,6 +103151,11 @@ var VALUE_COLD = {
|
|
|
102741
103151
|
value: null,
|
|
102742
103152
|
lastFetchedAt: 0
|
|
102743
103153
|
};
|
|
103154
|
+
/** A text has no null: until the box sends one the slice is empty and never read as a state. */
|
|
103155
|
+
var TEXT_COLD = {
|
|
103156
|
+
value: "",
|
|
103157
|
+
lastFetchedAt: 0
|
|
103158
|
+
};
|
|
102744
103159
|
/**
|
|
102745
103160
|
* Persisted config of an Enpal Sensor. `specId` selects the entry of the catalog;
|
|
102746
103161
|
* `integrationId` selects the integration whose box feeds it.
|
|
@@ -102778,7 +103193,8 @@ var EnpalSensorDevice = class extends BaseDevice {
|
|
|
102778
103193
|
this.updateSourceInfo({
|
|
102779
103194
|
id: `${this.integrationId}:${spec.id}`,
|
|
102780
103195
|
system: ENPAL_SYSTEM,
|
|
102781
|
-
...spec.kind === "meter" || spec.kind === "numeric" ? { quantity: spec.quantity } : {}
|
|
103196
|
+
...spec.kind === "meter" || spec.kind === "numeric" ? { quantity: spec.quantity } : {},
|
|
103197
|
+
...spec.kind === "text" && spec.timestamp === true ? { dateTimeShape: "date" } : {}
|
|
102782
103198
|
});
|
|
102783
103199
|
}
|
|
102784
103200
|
async onActivate() {
|
|
@@ -102836,6 +103252,7 @@ var EnpalSensorDevice = class extends BaseDevice {
|
|
|
102836
103252
|
});
|
|
102837
103253
|
}
|
|
102838
103254
|
if (slices.numeric !== void 0) this.runtimeState.setCapState(NUMERIC_CAP, slices.numeric);
|
|
103255
|
+
if (slices.text !== void 0) this.runtimeState.setCapState(ENUM_CAP, slices.text);
|
|
102839
103256
|
}
|
|
102840
103257
|
registerCaps() {
|
|
102841
103258
|
switch (this.spec.kind) {
|
|
@@ -102850,6 +103267,10 @@ var EnpalSensorDevice = class extends BaseDevice {
|
|
|
102850
103267
|
this.ctx.registerNativeCap(meterCapability, { getStatus: async () => this.runtimeState.getCapState(METER_CAP) ?? VALUE_COLD });
|
|
102851
103268
|
this.runtimeState.setCapState(METER_CAP, VALUE_COLD);
|
|
102852
103269
|
return;
|
|
103270
|
+
case "text":
|
|
103271
|
+
this.ctx.registerNativeCap(enumSensorCapability, { getStatus: async () => this.runtimeState.getCapState(ENUM_CAP) ?? TEXT_COLD });
|
|
103272
|
+
this.runtimeState.setCapState(ENUM_CAP, TEXT_COLD);
|
|
103273
|
+
return;
|
|
102853
103274
|
case "numeric":
|
|
102854
103275
|
this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(NUMERIC_CAP) ?? VALUE_COLD });
|
|
102855
103276
|
this.runtimeState.setCapState(NUMERIC_CAP, VALUE_COLD);
|
|
@@ -102879,43 +103300,166 @@ function enpalSensorFactory(ctx) {
|
|
|
102879
103300
|
}
|
|
102880
103301
|
//#endregion
|
|
102881
103302
|
//#region src/enpal-equipment.ts
|
|
102882
|
-
|
|
103303
|
+
var live = (section, ...specIds) => specIds.map((specId) => ({
|
|
103304
|
+
specId,
|
|
103305
|
+
...section !== void 0 ? { section } : {},
|
|
103306
|
+
diagnostic: false
|
|
103307
|
+
}));
|
|
103308
|
+
var diag = (section, ...specIds) => specIds.map((specId) => ({
|
|
103309
|
+
specId,
|
|
103310
|
+
...section !== void 0 ? { section } : {},
|
|
103311
|
+
diagnostic: true
|
|
103312
|
+
}));
|
|
103313
|
+
function equipment(input) {
|
|
103314
|
+
return {
|
|
103315
|
+
...input,
|
|
103316
|
+
specIds: input.children.map((c) => c.specId)
|
|
103317
|
+
};
|
|
103318
|
+
}
|
|
103319
|
+
/**
|
|
103320
|
+
* Three adoptable pieces: the box (the Gateway, with its connection, firmware and
|
|
103321
|
+
* diagnostics), the installation (System: Sungrow inverter, battery, grid, house meter), and
|
|
103322
|
+
* the heat pump as the box reads it. Order is the order the candidates are listed in. Every
|
|
103323
|
+
* catalog entry belongs to one. The ids of the older catalog entries are unchanged.
|
|
103324
|
+
*/
|
|
102883
103325
|
var ENPAL_EQUIPMENT = [
|
|
102884
|
-
{
|
|
102885
|
-
id: "
|
|
102886
|
-
|
|
102887
|
-
|
|
102888
|
-
|
|
102889
|
-
|
|
102890
|
-
|
|
102891
|
-
|
|
102892
|
-
|
|
103326
|
+
equipment({
|
|
103327
|
+
id: "box",
|
|
103328
|
+
kind: "gateway",
|
|
103329
|
+
name: "Enpal Box",
|
|
103330
|
+
children: [
|
|
103331
|
+
...diag("Box", "box-main-state", "box-cpu-load", "box-memory-usage", "box-hardware-check"),
|
|
103332
|
+
...diag("EEBUS", "eebus-connection", "eebus-discovery", "eebus-cert-expiry"),
|
|
103333
|
+
...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")
|
|
103334
|
+
],
|
|
103335
|
+
metadata: [
|
|
103336
|
+
{
|
|
103337
|
+
attribute: "deviceName",
|
|
103338
|
+
key: "Device.Name"
|
|
103339
|
+
},
|
|
103340
|
+
{
|
|
103341
|
+
attribute: "model",
|
|
103342
|
+
key: "IoTEdgeDevice.Device.Type"
|
|
103343
|
+
},
|
|
103344
|
+
{
|
|
103345
|
+
attribute: "serialNumber",
|
|
103346
|
+
key: "IoTEdgeDevice.SerialNumber"
|
|
103347
|
+
},
|
|
103348
|
+
{
|
|
103349
|
+
attribute: "hardware",
|
|
103350
|
+
key: "Generation.HW"
|
|
103351
|
+
},
|
|
103352
|
+
{
|
|
103353
|
+
attribute: "softwareGeneration",
|
|
103354
|
+
key: "Generation.SW"
|
|
103355
|
+
},
|
|
103356
|
+
{
|
|
103357
|
+
attribute: "eebusSki",
|
|
103358
|
+
key: "Eebus.Ski"
|
|
103359
|
+
},
|
|
103360
|
+
{
|
|
103361
|
+
attribute: "modemModel",
|
|
103362
|
+
key: "LTE.Modem.Type"
|
|
103363
|
+
},
|
|
103364
|
+
{
|
|
103365
|
+
attribute: "modemFirmware",
|
|
103366
|
+
key: "LTE.Modem.Firmware.Version"
|
|
103367
|
+
}
|
|
103368
|
+
],
|
|
103369
|
+
primaryChildPreference: []
|
|
103370
|
+
}),
|
|
103371
|
+
equipment({
|
|
103372
|
+
id: "impianto",
|
|
103373
|
+
kind: "container",
|
|
103374
|
+
name: "System",
|
|
103375
|
+
children: [
|
|
103376
|
+
...live("Photovoltaic", "pv", "pv-string-1-power", "pv-string-2-power", "pv-energy-day"),
|
|
103377
|
+
...diag("Photovoltaic", "pv-string-1-voltage", "pv-string-2-voltage", "pv-string-1-current", "pv-string-2-current"),
|
|
103378
|
+
...live("Battery", "battery", "battery-stored-energy", "battery-temperature", "battery-charge-day", "battery-discharge-day", "battery-charged", "battery-discharged"),
|
|
103379
|
+
...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"),
|
|
103380
|
+
...live("Grid", "grid-import", "grid-export", "grid-phase-a", "grid-phase-b", "grid-phase-c", "grid-import-day", "grid-export-day"),
|
|
103381
|
+
...diag("Grid", "grid-voltage-a", "grid-voltage-b", "grid-voltage-c", "grid-frequency", "grid-feed-max"),
|
|
103382
|
+
...live("Home", "house", "house-energy-day"),
|
|
103383
|
+
...live("Inverter", "inverter-state", "inverter-ems-mode", "inverter-ac-power"),
|
|
103384
|
+
...diag("Inverter", "inverter-start-stop", "inverter-forcible-mode", "inverter-temperature", "inverter-power-rating", "inverter-phase-type", "inverter-modbus-rtu", "inverter-modbus-tcp")
|
|
103385
|
+
],
|
|
103386
|
+
metadata: [
|
|
103387
|
+
{
|
|
103388
|
+
attribute: "model",
|
|
103389
|
+
key: "Code.Device.Type"
|
|
103390
|
+
},
|
|
103391
|
+
{
|
|
103392
|
+
attribute: "firmware",
|
|
103393
|
+
key: "SoftwareVersion.Service.1"
|
|
103394
|
+
},
|
|
103395
|
+
{
|
|
103396
|
+
attribute: "communicationFirmware",
|
|
103397
|
+
key: "SoftwareVersion.Service.2"
|
|
103398
|
+
},
|
|
103399
|
+
{
|
|
103400
|
+
attribute: "dongleSerialNumber",
|
|
103401
|
+
key: "Dongle.SerialNumber"
|
|
103402
|
+
},
|
|
103403
|
+
{
|
|
103404
|
+
attribute: "batterySerialNumber",
|
|
103405
|
+
key: "SerialNumber.Battery"
|
|
103406
|
+
},
|
|
103407
|
+
{
|
|
103408
|
+
attribute: "batteryFirmware",
|
|
103409
|
+
key: "SoftwareVersion.Service.Battery.Unit.1"
|
|
103410
|
+
}
|
|
103411
|
+
],
|
|
103412
|
+
primaryChildPreference: [
|
|
103413
|
+
"pv",
|
|
102893
103414
|
"battery",
|
|
102894
|
-
"
|
|
102895
|
-
"battery-discharged",
|
|
102896
|
-
"battery-temperature",
|
|
102897
|
-
"battery-health"
|
|
103415
|
+
"house"
|
|
102898
103416
|
]
|
|
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
|
-
{
|
|
103417
|
+
}),
|
|
103418
|
+
equipment({
|
|
102911
103419
|
id: "heatpump",
|
|
102912
|
-
|
|
102913
|
-
|
|
103420
|
+
kind: "container",
|
|
103421
|
+
name: "Heat pump (Enpal)",
|
|
103422
|
+
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")],
|
|
103423
|
+
metadata: [
|
|
103424
|
+
{
|
|
103425
|
+
attribute: "model",
|
|
103426
|
+
key: "Heatpump.IndoorUnit.DeviceType"
|
|
103427
|
+
},
|
|
103428
|
+
{
|
|
103429
|
+
attribute: "serialNumber",
|
|
103430
|
+
key: "Heatpump.SerialNumber"
|
|
103431
|
+
},
|
|
103432
|
+
{
|
|
103433
|
+
attribute: "indoorUnitSerialNumber",
|
|
103434
|
+
key: "Heatpump.IndoorUnit.SerialNumber"
|
|
103435
|
+
},
|
|
103436
|
+
{
|
|
103437
|
+
attribute: "firmware",
|
|
103438
|
+
key: "Heatpump.SoftwareVersion"
|
|
103439
|
+
},
|
|
103440
|
+
{
|
|
103441
|
+
attribute: "indoorUnitFirmware",
|
|
103442
|
+
key: "Heatpump.IndoorUnit.SoftwareVersion"
|
|
103443
|
+
},
|
|
103444
|
+
{
|
|
103445
|
+
attribute: "gatewayModel",
|
|
103446
|
+
key: "Heatpump.Gateway.Device.Type"
|
|
103447
|
+
},
|
|
103448
|
+
{
|
|
103449
|
+
attribute: "gatewaySerialNumber",
|
|
103450
|
+
key: "Heatpump.Gateway.SerialNumber"
|
|
103451
|
+
},
|
|
103452
|
+
{
|
|
103453
|
+
attribute: "gatewayFirmware",
|
|
103454
|
+
key: "Heatpump.Gateway.SoftwareVersion"
|
|
103455
|
+
}
|
|
103456
|
+
],
|
|
103457
|
+
primaryChildPreference: [
|
|
102914
103458
|
"heatpump",
|
|
102915
103459
|
"heatpump-hot-water",
|
|
102916
103460
|
"heatpump-outside"
|
|
102917
103461
|
]
|
|
102918
|
-
}
|
|
103462
|
+
})
|
|
102919
103463
|
];
|
|
102920
103464
|
function findEquipment(id) {
|
|
102921
103465
|
return ENPAL_EQUIPMENT.find((e) => e.id === id);
|
|
@@ -102929,24 +103473,61 @@ function childNativeId(equipmentId, specId) {
|
|
|
102929
103473
|
* they stay, whatever the box reports later, so a restart with the box off keeps them.
|
|
102930
103474
|
*/
|
|
102931
103475
|
var enpalEquipmentSchema = object({
|
|
102932
|
-
equipment: string().min(1).describe("Equipment id (pv, battery, grid, house
|
|
103476
|
+
equipment: string().min(1).describe("Equipment id (impianto, heatpump; older adoptions: pv, battery, grid, house)"),
|
|
102933
103477
|
specIds: array(string().min(1)).describe("Catalog entries adopted as children"),
|
|
102934
103478
|
integrationId: string().min(1).describe("Owning integration"),
|
|
102935
103479
|
system: literal(ENPAL_SYSTEM).optional()
|
|
102936
103480
|
});
|
|
102937
|
-
/**
|
|
103481
|
+
/**
|
|
103482
|
+
* The declared primary children of an adopted piece: its preference, limited to the children
|
|
103483
|
+
* it was adopted with. Empty for the gateway and for an equipment id the catalog dropped.
|
|
103484
|
+
*/
|
|
103485
|
+
function primaryChildPreferenceOf(found, specIds) {
|
|
103486
|
+
if (found === void 0) return [];
|
|
103487
|
+
return found.primaryChildPreference.filter((id) => specIds.includes(id));
|
|
103488
|
+
}
|
|
103489
|
+
/**
|
|
103490
|
+
* The accordion layout of an equipment Container, for the children it was adopted with.
|
|
103491
|
+
* Empty for a Container with no sections (the heat pump) or an unknown equipment.
|
|
103492
|
+
*/
|
|
103493
|
+
function childLayoutOf(found, specIds) {
|
|
103494
|
+
if (found === void 0) return [];
|
|
103495
|
+
const entries = [];
|
|
103496
|
+
found.children.forEach((child, order) => {
|
|
103497
|
+
if (child.section === void 0 || !specIds.includes(child.specId)) return;
|
|
103498
|
+
entries.push({
|
|
103499
|
+
childKey: child.specId,
|
|
103500
|
+
section: child.section,
|
|
103501
|
+
order
|
|
103502
|
+
});
|
|
103503
|
+
});
|
|
103504
|
+
return entries;
|
|
103505
|
+
}
|
|
103506
|
+
/** The identity facts the box reported for an equipment, by device attribute. */
|
|
103507
|
+
function metadataOf(found, snapshot) {
|
|
103508
|
+
const out = {};
|
|
103509
|
+
for (const field of found?.metadata ?? []) {
|
|
103510
|
+
const value = snapshot.texts.get(field.key);
|
|
103511
|
+
if (value !== void 0) out[field.attribute] = value;
|
|
103512
|
+
}
|
|
103513
|
+
return out;
|
|
103514
|
+
}
|
|
103515
|
+
/** The Sensor children the kernel spawns under an adopted piece (a Container or the Gateway). */
|
|
102938
103516
|
function accessoryChildSpecs(config) {
|
|
103517
|
+
const found = findEquipment(config.equipment);
|
|
102939
103518
|
const out = [];
|
|
102940
103519
|
for (const specId of config.specIds) {
|
|
102941
103520
|
const spec = findSpec(specId);
|
|
102942
103521
|
if (spec === void 0) continue;
|
|
103522
|
+
const diagnostic = found?.children.find((c) => c.specId === specId)?.diagnostic === true;
|
|
102943
103523
|
out.push({
|
|
102944
103524
|
stableIdSuffix: spec.id,
|
|
102945
103525
|
meta: {
|
|
102946
103526
|
type: DeviceType.Sensor,
|
|
102947
103527
|
name: spec.name,
|
|
102948
103528
|
role: spec.role,
|
|
102949
|
-
integrationId: config.integrationId
|
|
103529
|
+
integrationId: config.integrationId,
|
|
103530
|
+
...diagnostic ? { display: { hidden: true } } : {}
|
|
102950
103531
|
},
|
|
102951
103532
|
config: {
|
|
102952
103533
|
specId: spec.id,
|
|
@@ -102959,18 +103540,87 @@ function accessoryChildSpecs(config) {
|
|
|
102959
103540
|
return out;
|
|
102960
103541
|
}
|
|
102961
103542
|
//#endregion
|
|
103543
|
+
//#region src/devices/enpal-metadata-sync.ts
|
|
103544
|
+
/**
|
|
103545
|
+
* Writes the identity facts a piece reported (model, serial, firmware) as device attributes.
|
|
103546
|
+
* A poll that changed none writes nothing; a failed write is logged and retried next poll.
|
|
103547
|
+
*/
|
|
103548
|
+
var EnpalMetadataSync = class {
|
|
103549
|
+
options;
|
|
103550
|
+
applied = "";
|
|
103551
|
+
constructor(options) {
|
|
103552
|
+
this.options = options;
|
|
103553
|
+
}
|
|
103554
|
+
async apply() {
|
|
103555
|
+
const snapshot = this.options.getSnapshot();
|
|
103556
|
+
if (snapshot === null) return;
|
|
103557
|
+
const attributes = metadataOf(findEquipment(this.options.equipment), snapshot);
|
|
103558
|
+
const fingerprint = JSON.stringify(attributes);
|
|
103559
|
+
if (fingerprint === this.applied || Object.keys(attributes).length === 0) return;
|
|
103560
|
+
try {
|
|
103561
|
+
await this.options.write(attributes);
|
|
103562
|
+
this.applied = fingerprint;
|
|
103563
|
+
} catch (err) {
|
|
103564
|
+
this.options.logger.warn("enpal: could not write the device attributes", {
|
|
103565
|
+
tags: { deviceId: this.options.deviceId },
|
|
103566
|
+
meta: {
|
|
103567
|
+
equipment: this.options.equipment,
|
|
103568
|
+
error: errMsg(err)
|
|
103569
|
+
}
|
|
103570
|
+
});
|
|
103571
|
+
}
|
|
103572
|
+
}
|
|
103573
|
+
};
|
|
103574
|
+
//#endregion
|
|
103575
|
+
//#region src/devices/enpal-preference-backfill.ts
|
|
103576
|
+
/**
|
|
103577
|
+
* Stamp the declared primary-child preference on a container adopted before
|
|
103578
|
+
* the declaration existed. Adoption stamps it through `InitialDeviceMeta`;
|
|
103579
|
+
* a restore passes no initial meta, so an older row would otherwise never get
|
|
103580
|
+
* one and keep being shown by the heuristic's child.
|
|
103581
|
+
*
|
|
103582
|
+
* Only an ABSENT declaration is filled: a stored one is the provider's own and
|
|
103583
|
+
* is left alone. The operator's pick (`primaryChildEntityId`) is a different
|
|
103584
|
+
* column, never written here, and still wins at resolution.
|
|
103585
|
+
*
|
|
103586
|
+
* @returns whether it wrote.
|
|
103587
|
+
*/
|
|
103588
|
+
async function backfillPrimaryChildPreference(input) {
|
|
103589
|
+
const { deviceId, stored, declared, write, logger } = input;
|
|
103590
|
+
if (stored !== void 0 && stored.length > 0) return false;
|
|
103591
|
+
if (declared.length === 0) return false;
|
|
103592
|
+
try {
|
|
103593
|
+
await write(declared);
|
|
103594
|
+
} catch (err) {
|
|
103595
|
+
logger.warn("enpal: could not stamp the primary-child preference", {
|
|
103596
|
+
tags: { deviceId },
|
|
103597
|
+
meta: { error: errMsg(err) }
|
|
103598
|
+
});
|
|
103599
|
+
return false;
|
|
103600
|
+
}
|
|
103601
|
+
logger.info("enpal: stamped the primary-child preference on a container adopted without one", {
|
|
103602
|
+
tags: { deviceId },
|
|
103603
|
+
meta: { primaryChildPreference: declared }
|
|
103604
|
+
});
|
|
103605
|
+
return true;
|
|
103606
|
+
}
|
|
103607
|
+
//#endregion
|
|
102962
103608
|
//#region src/devices/enpal-equipment-device.ts
|
|
102963
103609
|
/**
|
|
102964
|
-
* One
|
|
103610
|
+
* One piece Enpal measures (the System: PV, battery, grid, house; the heat pump) as a
|
|
102965
103611
|
* {@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
|
-
*
|
|
103612
|
+
* adopted with, and the kernel spawns and restores them; which child stands for it is the
|
|
103613
|
+
* `primaryChildPreference` stamped at adoption, and re-stamped on activation for a
|
|
103614
|
+
* container adopted before it had one. Its model, serials and firmware are device
|
|
103615
|
+
* attributes, written from the box's text rows whenever they change. Online follows the
|
|
103616
|
+
* integration's box; the integration marks it offline when a poll fails.
|
|
102968
103617
|
*/
|
|
102969
103618
|
var EnpalEquipmentDevice = class extends BaseDevice {
|
|
102970
103619
|
features = [];
|
|
102971
103620
|
equipment;
|
|
102972
103621
|
specIds;
|
|
102973
103622
|
integrationId;
|
|
103623
|
+
snapshotUnsub = null;
|
|
102974
103624
|
constructor(ctx) {
|
|
102975
103625
|
const cfg = enpalEquipmentSchema.parse(ctx.persistedConfig ?? {});
|
|
102976
103626
|
super(ctx, enpalEquipmentSchema, { type: DeviceType.Container });
|
|
@@ -102983,6 +103633,42 @@ var EnpalEquipmentDevice = class extends BaseDevice {
|
|
|
102983
103633
|
system: ENPAL_SYSTEM
|
|
102984
103634
|
});
|
|
102985
103635
|
}
|
|
103636
|
+
async onActivate() {
|
|
103637
|
+
await super.onActivate();
|
|
103638
|
+
await backfillPrimaryChildPreference({
|
|
103639
|
+
deviceId: this.id,
|
|
103640
|
+
stored: this.ctx.deviceMeta.primaryChildPreference,
|
|
103641
|
+
declared: primaryChildPreferenceOf(findEquipment(this.equipment), this.specIds),
|
|
103642
|
+
write: async (keys) => {
|
|
103643
|
+
await this.ctx.api.deviceManager.setPrimaryChildPreference.mutate({
|
|
103644
|
+
deviceId: this.id,
|
|
103645
|
+
primaryChildPreference: keys
|
|
103646
|
+
});
|
|
103647
|
+
},
|
|
103648
|
+
logger: this.ctx.logger
|
|
103649
|
+
});
|
|
103650
|
+
const metadata = new EnpalMetadataSync({
|
|
103651
|
+
equipment: this.equipment,
|
|
103652
|
+
deviceId: this.id,
|
|
103653
|
+
getSnapshot: () => enpalConnections.getSnapshot(this.integrationId),
|
|
103654
|
+
write: async (patch) => {
|
|
103655
|
+
await this.ctx.api.deviceManager.setMetadata.mutate({
|
|
103656
|
+
deviceId: this.id,
|
|
103657
|
+
patch
|
|
103658
|
+
});
|
|
103659
|
+
},
|
|
103660
|
+
logger: this.ctx.logger
|
|
103661
|
+
});
|
|
103662
|
+
this.snapshotUnsub = enpalConnections.onSnapshot(this.integrationId, () => {
|
|
103663
|
+
metadata.apply();
|
|
103664
|
+
});
|
|
103665
|
+
await metadata.apply();
|
|
103666
|
+
}
|
|
103667
|
+
async removeDevice() {
|
|
103668
|
+
this.snapshotUnsub?.();
|
|
103669
|
+
this.snapshotUnsub = null;
|
|
103670
|
+
await super.removeDevice();
|
|
103671
|
+
}
|
|
102986
103672
|
getAccessoryChildren() {
|
|
102987
103673
|
return accessoryChildSpecs({
|
|
102988
103674
|
equipment: this.equipment,
|
|
@@ -102991,18 +103677,41 @@ var EnpalEquipmentDevice = class extends BaseDevice {
|
|
|
102991
103677
|
});
|
|
102992
103678
|
}
|
|
102993
103679
|
};
|
|
103680
|
+
//#endregion
|
|
103681
|
+
//#region src/enpal-discovery.ts
|
|
102994
103682
|
var METADATA = {
|
|
102995
103683
|
manufacturer: "Enpal",
|
|
102996
103684
|
integration: "enpal"
|
|
102997
103685
|
};
|
|
103686
|
+
/** The Sensor candidates of one piece: the catalog entries of it the box reported. */
|
|
103687
|
+
function reportedChildren(equipment, snapshot) {
|
|
103688
|
+
if (snapshot === null) return [];
|
|
103689
|
+
const reported = new Map(availableSpecs(snapshot).map((spec) => [spec.id, spec]));
|
|
103690
|
+
return equipment.specIds.flatMap((id) => {
|
|
103691
|
+
const spec = reported.get(id);
|
|
103692
|
+
if (spec === void 0) return [];
|
|
103693
|
+
return [{
|
|
103694
|
+
childNativeId: childNativeId(equipment.id, spec.id),
|
|
103695
|
+
name: spec.name,
|
|
103696
|
+
type: DeviceType.Sensor,
|
|
103697
|
+
status: "online",
|
|
103698
|
+
metadata: METADATA,
|
|
103699
|
+
alreadyAdopted: false,
|
|
103700
|
+
adoptedDeviceId: null,
|
|
103701
|
+
capabilities: capsOfSpec(spec)
|
|
103702
|
+
}];
|
|
103703
|
+
});
|
|
103704
|
+
}
|
|
102998
103705
|
/**
|
|
102999
|
-
* The box ({@link DeviceType.Gateway}, always offered, offline while it is unreachable
|
|
103000
|
-
*
|
|
103001
|
-
* Sensor children it reported. Pure.
|
|
103706
|
+
* The box ({@link DeviceType.Gateway}, always offered, offline while it is unreachable, with the
|
|
103707
|
+
* diagnostics it reported as children), then one Container candidate per other piece the box
|
|
103708
|
+
* reported at least one value for, with the Sensor children it reported. Pure.
|
|
103002
103709
|
*/
|
|
103003
103710
|
function buildEnpalCandidates(input) {
|
|
103004
103711
|
const { snapshot } = input;
|
|
103005
103712
|
const boxAdopted = input.adopted.get("box") ?? null;
|
|
103713
|
+
const box = findEquipment("box");
|
|
103714
|
+
const boxChildren = box === void 0 ? [] : reportedChildren(box, snapshot);
|
|
103006
103715
|
const out = [{
|
|
103007
103716
|
childNativeId: "box",
|
|
103008
103717
|
name: input.box.name,
|
|
@@ -103011,13 +103720,13 @@ function buildEnpalCandidates(input) {
|
|
|
103011
103720
|
metadata: METADATA,
|
|
103012
103721
|
alreadyAdopted: boxAdopted !== null,
|
|
103013
103722
|
adoptedDeviceId: boxAdopted,
|
|
103014
|
-
capabilities: [deviceAdminLinkCapability.name]
|
|
103723
|
+
capabilities: [deviceAdminLinkCapability.name],
|
|
103724
|
+
...boxChildren.length > 0 ? { children: boxChildren } : {}
|
|
103015
103725
|
}];
|
|
103016
|
-
if (snapshot === null) return out;
|
|
103017
|
-
const reported = new Map(availableSpecs(snapshot).map((spec) => [spec.id, spec]));
|
|
103018
103726
|
for (const equipment of ENPAL_EQUIPMENT) {
|
|
103019
|
-
|
|
103020
|
-
|
|
103727
|
+
if (equipment.kind !== "container") continue;
|
|
103728
|
+
const children = reportedChildren(equipment, snapshot);
|
|
103729
|
+
if (children.length === 0) continue;
|
|
103021
103730
|
const adoptedId = input.adopted.get(equipment.id) ?? null;
|
|
103022
103731
|
out.push({
|
|
103023
103732
|
childNativeId: equipment.id,
|
|
@@ -103028,16 +103737,7 @@ function buildEnpalCandidates(input) {
|
|
|
103028
103737
|
alreadyAdopted: adoptedId !== null,
|
|
103029
103738
|
adoptedDeviceId: adoptedId,
|
|
103030
103739
|
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
|
-
}))
|
|
103740
|
+
children
|
|
103041
103741
|
});
|
|
103042
103742
|
}
|
|
103043
103743
|
return out;
|
|
@@ -103051,7 +103751,12 @@ var DEVICES_FILTER = {
|
|
|
103051
103751
|
};
|
|
103052
103752
|
function buildEnpalAdoptionProvider(deps) {
|
|
103053
103753
|
const { getSnapshot, hasIntegration, listIntegrations, listAdopted, logger } = deps;
|
|
103054
|
-
const boxName = (integrationId) => deps.integrationName(integrationId) ?? "Enpal";
|
|
103754
|
+
const boxName = (integrationId) => deps.integrationName(integrationId) ?? findEquipment("box")?.name ?? "Enpal Box";
|
|
103755
|
+
/** The catalog entries of a piece the offered candidate reports as children. */
|
|
103756
|
+
const reportedSpecIds = (equipment, candidate) => {
|
|
103757
|
+
const reported = new Set((candidate?.children ?? []).map((c) => c.childNativeId));
|
|
103758
|
+
return equipment.specIds.filter((id) => reported.has(`${equipment.id}#${id}`));
|
|
103759
|
+
};
|
|
103055
103760
|
async function adoptedMap(integrationId) {
|
|
103056
103761
|
const map = /* @__PURE__ */ new Map();
|
|
103057
103762
|
for (const device of await listAdopted()) {
|
|
@@ -103127,20 +103832,23 @@ function buildEnpalAdoptionProvider(deps) {
|
|
|
103127
103832
|
const adopted = [];
|
|
103128
103833
|
let failures = 0;
|
|
103129
103834
|
for (const equipmentId of childNativeIds) try {
|
|
103130
|
-
|
|
103131
|
-
|
|
103835
|
+
const candidate = offered.find((c) => c.childNativeId === equipmentId);
|
|
103836
|
+
const equipment = findEquipment(equipmentId);
|
|
103837
|
+
if (equipmentId === "box" && equipment !== void 0) {
|
|
103838
|
+
const specIds = reportedSpecIds(equipment, candidate);
|
|
103839
|
+
const { deviceId, accessoryDeviceIds } = await deps.adoptGateway({
|
|
103132
103840
|
integrationId,
|
|
103133
|
-
name: perCandidate?.[equipmentId]?.name ?? boxName(integrationId)
|
|
103841
|
+
name: perCandidate?.[equipmentId]?.name ?? boxName(integrationId),
|
|
103842
|
+
specIds,
|
|
103843
|
+
childLayout: childLayoutOf(equipment, specIds)
|
|
103134
103844
|
});
|
|
103135
103845
|
adopted.push({
|
|
103136
103846
|
childNativeId: equipmentId,
|
|
103137
103847
|
parentDeviceId: deviceId,
|
|
103138
|
-
accessoryDeviceIds
|
|
103848
|
+
accessoryDeviceIds
|
|
103139
103849
|
});
|
|
103140
103850
|
continue;
|
|
103141
103851
|
}
|
|
103142
|
-
const candidate = offered.find((c) => c.childNativeId === equipmentId);
|
|
103143
|
-
const equipment = findEquipment(equipmentId);
|
|
103144
103852
|
if (candidate === void 0 || equipment === void 0) {
|
|
103145
103853
|
logger.warn("enpal adopt: the box reports nothing for this equipment — skipping", { meta: {
|
|
103146
103854
|
equipment: equipmentId,
|
|
@@ -103149,13 +103857,14 @@ function buildEnpalAdoptionProvider(deps) {
|
|
|
103149
103857
|
failures++;
|
|
103150
103858
|
continue;
|
|
103151
103859
|
}
|
|
103152
|
-
const
|
|
103153
|
-
const specIds = equipment.specIds.filter((id) => reported.has(`${equipment.id}#${id}`));
|
|
103860
|
+
const specIds = reportedSpecIds(equipment, candidate);
|
|
103154
103861
|
const { deviceId, accessoryDeviceIds } = await deps.adoptDevice({
|
|
103155
103862
|
equipment: equipment.id,
|
|
103156
103863
|
specIds,
|
|
103157
103864
|
integrationId,
|
|
103158
|
-
name: perCandidate?.[equipmentId]?.name ?? equipment.name
|
|
103865
|
+
name: perCandidate?.[equipmentId]?.name ?? equipment.name,
|
|
103866
|
+
childLayout: childLayoutOf(equipment, specIds),
|
|
103867
|
+
primaryChildPreference: primaryChildPreferenceOf(equipment, specIds)
|
|
103159
103868
|
});
|
|
103160
103869
|
adopted.push({
|
|
103161
103870
|
childNativeId: equipmentId,
|
|
@@ -103186,10 +103895,187 @@ function buildEnpalAdoptionProvider(deps) {
|
|
|
103186
103895
|
}
|
|
103187
103896
|
};
|
|
103188
103897
|
}
|
|
103898
|
+
var BlazorRowsSchema = array(object({
|
|
103899
|
+
key: string(),
|
|
103900
|
+
value: string(),
|
|
103901
|
+
unit: string().nullable(),
|
|
103902
|
+
timestamp: string().nullable()
|
|
103903
|
+
}));
|
|
103904
|
+
var BridgeResultSchema = discriminatedUnion("ok", [object({
|
|
103905
|
+
ok: literal(true),
|
|
103906
|
+
rows: BlazorRowsSchema
|
|
103907
|
+
}), object({
|
|
103908
|
+
ok: literal(false),
|
|
103909
|
+
kind: _enum([
|
|
103910
|
+
"unreachable",
|
|
103911
|
+
"bad-response",
|
|
103912
|
+
"empty",
|
|
103913
|
+
"protocol"
|
|
103914
|
+
]),
|
|
103915
|
+
error: string()
|
|
103916
|
+
})]);
|
|
103917
|
+
/** A failed bridge run. The message never carries the bridge's stderr beyond a short tail. */
|
|
103918
|
+
var BlazorBridgeError = class extends Error {
|
|
103919
|
+
kind;
|
|
103920
|
+
constructor(kind, message) {
|
|
103921
|
+
super(message);
|
|
103922
|
+
this.name = "BlazorBridgeError";
|
|
103923
|
+
this.kind = kind;
|
|
103924
|
+
}
|
|
103925
|
+
};
|
|
103926
|
+
/** The worker waits up to 12 s for the box's first render; this leaves room for connect and exit. */
|
|
103927
|
+
var DEFAULT_TIMEOUT_MS = 4e4;
|
|
103928
|
+
var WINDOW_MS = 12e3;
|
|
103929
|
+
/** A real answer is ~25 KB; anything near this is a runaway bridge, not a box. */
|
|
103930
|
+
var MAX_STDOUT_BYTES = 2 * 1024 * 1024;
|
|
103931
|
+
/**
|
|
103932
|
+
* How long `auto` leaves the Blazor channel alone after it failed. An older box that never
|
|
103933
|
+
* serves it would otherwise cost the full read window on every poll.
|
|
103934
|
+
*/
|
|
103935
|
+
var BLAZOR_BACKOFF_MS = 10 * 6e4;
|
|
103936
|
+
/** The slowest a good read takes: connect, the 12 s render window, exit. */
|
|
103937
|
+
var BLAZOR_WORST_CASE_READ_MS = 2e4;
|
|
103938
|
+
/**
|
|
103939
|
+
* The poll interval actually used. A box-web-channel read can take most of
|
|
103940
|
+
* {@link BLAZOR_WORST_CASE_READ_MS}, so a shorter interval is raised to it when that channel
|
|
103941
|
+
* can be the source: ticks would only pile onto the running read.
|
|
103942
|
+
*/
|
|
103943
|
+
function effectivePollIntervalMs(connection) {
|
|
103944
|
+
return connection.source === "auto" || connection.source === "blazor" ? Math.max(connection.pollIntervalMs, BLAZOR_WORST_CASE_READ_MS) : connection.pollIntervalMs;
|
|
103945
|
+
}
|
|
103946
|
+
/** Remembers a failed Blazor read so `auto` skips it for a while. Forced `blazor` ignores it. */
|
|
103947
|
+
var BlazorBackoff = class {
|
|
103948
|
+
#until = 0;
|
|
103949
|
+
#reason = null;
|
|
103950
|
+
/** Why the channel is being skipped now, or null when it may be tried. */
|
|
103951
|
+
skipReason(now) {
|
|
103952
|
+
if (this.#reason === null || now >= this.#until) return null;
|
|
103953
|
+
return {
|
|
103954
|
+
kind: this.#reason.kind,
|
|
103955
|
+
message: `${this.#reason.message}; the box web channel is skipped for ${BLAZOR_BACKOFF_MS / 6e4} minutes after a failure`
|
|
103956
|
+
};
|
|
103957
|
+
}
|
|
103958
|
+
failed(now, reason) {
|
|
103959
|
+
this.#until = now + BLAZOR_BACKOFF_MS;
|
|
103960
|
+
this.#reason = reason;
|
|
103961
|
+
}
|
|
103962
|
+
recovered() {
|
|
103963
|
+
this.#until = 0;
|
|
103964
|
+
this.#reason = null;
|
|
103965
|
+
}
|
|
103966
|
+
};
|
|
103967
|
+
/** One bridge run per poll: spawn, write the request on stdin, parse one JSON answer. */
|
|
103968
|
+
function createBlazorReader(deps) {
|
|
103969
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
103970
|
+
return (target) => new Promise((resolve, reject) => {
|
|
103971
|
+
const child = (0, node_child_process.spawn)(deps.pythonPath, [deps.scriptPath], { stdio: [
|
|
103972
|
+
"pipe",
|
|
103973
|
+
"pipe",
|
|
103974
|
+
"pipe"
|
|
103975
|
+
] });
|
|
103976
|
+
let stdout = "";
|
|
103977
|
+
let stderr = "";
|
|
103978
|
+
let settled = false;
|
|
103979
|
+
const finish = (action) => {
|
|
103980
|
+
if (settled) return;
|
|
103981
|
+
settled = true;
|
|
103982
|
+
clearTimeout(timer);
|
|
103983
|
+
action();
|
|
103984
|
+
};
|
|
103985
|
+
const timer = setTimeout(() => {
|
|
103986
|
+
child.kill("SIGKILL");
|
|
103987
|
+
finish(() => reject(new BlazorBridgeError("timeout", `Enpal box ${target.host} did not answer within ${timeoutMs / 1e3}s`)));
|
|
103988
|
+
}, timeoutMs);
|
|
103989
|
+
child.stdout.on("data", (chunk) => {
|
|
103990
|
+
stdout += chunk.toString("utf8");
|
|
103991
|
+
if (stdout.length > MAX_STDOUT_BYTES) {
|
|
103992
|
+
child.kill("SIGKILL");
|
|
103993
|
+
finish(() => reject(new BlazorBridgeError("protocol", "the Enpal bridge wrote more output than any box answer")));
|
|
103994
|
+
}
|
|
103995
|
+
});
|
|
103996
|
+
child.stderr.on("data", (chunk) => {
|
|
103997
|
+
stderr = (stderr + chunk.toString("utf8")).slice(-16384);
|
|
103998
|
+
});
|
|
103999
|
+
child.on("error", (err) => {
|
|
104000
|
+
finish(() => reject(new BlazorBridgeError("protocol", `cannot start the Enpal bridge: ${err.message}`)));
|
|
104001
|
+
});
|
|
104002
|
+
child.on("close", (code) => {
|
|
104003
|
+
finish(() => {
|
|
104004
|
+
let json;
|
|
104005
|
+
try {
|
|
104006
|
+
json = JSON.parse(stdout.trim());
|
|
104007
|
+
} catch {
|
|
104008
|
+
const tail = stderr.trim().slice(-300) || "no stderr";
|
|
104009
|
+
reject(new BlazorBridgeError("protocol", `the Enpal bridge produced no JSON (exit ${code ?? "null"}): ${tail}`));
|
|
104010
|
+
return;
|
|
104011
|
+
}
|
|
104012
|
+
const parsed = BridgeResultSchema.safeParse(json);
|
|
104013
|
+
if (!parsed.success) {
|
|
104014
|
+
reject(new BlazorBridgeError("protocol", "the Enpal bridge answered an unknown shape"));
|
|
104015
|
+
return;
|
|
104016
|
+
}
|
|
104017
|
+
if (!parsed.data.ok) {
|
|
104018
|
+
reject(new BlazorBridgeError(parsed.data.kind, parsed.data.error));
|
|
104019
|
+
return;
|
|
104020
|
+
}
|
|
104021
|
+
resolve(parsed.data.rows);
|
|
104022
|
+
});
|
|
104023
|
+
});
|
|
104024
|
+
child.stdin.on("error", () => void 0);
|
|
104025
|
+
child.stdin.end(JSON.stringify({
|
|
104026
|
+
host: target.host,
|
|
104027
|
+
port: target.port,
|
|
104028
|
+
windowMs: WINDOW_MS
|
|
104029
|
+
}));
|
|
104030
|
+
});
|
|
104031
|
+
}
|
|
104032
|
+
var KEY_SHAPE$2 = /^[A-Z][A-Za-z0-9]*(\.[A-Za-z0-9-]+)+$/;
|
|
104033
|
+
/** A bare decimal number: `3.6`, `-40`, `0`. A version, a hash or a timestamp is not one. */
|
|
104034
|
+
var NUMBER = /^-?\d+(?:[.,]\d+)?$/;
|
|
104035
|
+
/**
|
|
104036
|
+
* The numeric sensors of the bridge's rows. A text value (a state name, a serial number, the
|
|
104037
|
+
* inverter's markup bitfield) is not a reading, and neither is a dotted string that only
|
|
104038
|
+
* looks like a key (`V1.1.7`, a version).
|
|
104039
|
+
*/
|
|
104040
|
+
function blazorRowsToReadings(rows) {
|
|
104041
|
+
const readings = [];
|
|
104042
|
+
for (const row of rows) {
|
|
104043
|
+
if (!KEY_SHAPE$2.test(row.key)) continue;
|
|
104044
|
+
const text = row.value.trim();
|
|
104045
|
+
if (!NUMBER.test(text)) continue;
|
|
104046
|
+
const value = Number(text.replace(",", "."));
|
|
104047
|
+
if (!Number.isFinite(value)) continue;
|
|
104048
|
+
readings.push({
|
|
104049
|
+
key: row.key,
|
|
104050
|
+
value,
|
|
104051
|
+
unit: normalizeEnpalUnit(row.unit ?? void 0),
|
|
104052
|
+
measurement: void 0
|
|
104053
|
+
});
|
|
104054
|
+
}
|
|
104055
|
+
return readings;
|
|
104056
|
+
}
|
|
104057
|
+
/**
|
|
104058
|
+
* Every row's text as delivered, for state labels, serial numbers and firmware strings. A
|
|
104059
|
+
* row with no value, or whose value is markup (the inverter's `<ul>` bitfield), carries no
|
|
104060
|
+
* text worth showing and is left out.
|
|
104061
|
+
*/
|
|
104062
|
+
function blazorRowsToTexts(rows) {
|
|
104063
|
+
const texts = [];
|
|
104064
|
+
for (const row of rows) {
|
|
104065
|
+
if (!KEY_SHAPE$2.test(row.key)) continue;
|
|
104066
|
+
const value = row.value.trim();
|
|
104067
|
+
if (value.length === 0 || value.startsWith("<")) continue;
|
|
104068
|
+
texts.push({
|
|
104069
|
+
key: row.key,
|
|
104070
|
+
value
|
|
104071
|
+
});
|
|
104072
|
+
}
|
|
104073
|
+
return texts;
|
|
104074
|
+
}
|
|
103189
104075
|
//#endregion
|
|
103190
104076
|
//#region src/enpal-device-page.ts
|
|
103191
|
-
var CARD_HEADER = /<div class="card-header">\s*<h2>([^<]+)<\/h2>/g;
|
|
103192
|
-
var ROW = /<tr
|
|
104077
|
+
var CARD_HEADER = /<div class="card-header[^"]*">\s*<h2>([^<]+)<\/h2>/g;
|
|
104078
|
+
var ROW = /<tr(?:\s[^>]*)?>\s*<td>([^<]+)<\/td>\s*<td>([^<]*)<\/td>\s*<td>[^<]*<\/td>\s*<\/tr>/g;
|
|
103193
104079
|
var KEY_SHAPE$1 = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9-]+)+$/;
|
|
103194
104080
|
/** `10231W`, `-350W`, `1.85kW`, `51°C`, `62.5%`, `3` */
|
|
103195
104081
|
var VALUE = /^(-?\d+(?:[.,]\d+)?)\s*(\S*)$/;
|
|
@@ -103212,7 +104098,7 @@ function cardOffsets(html) {
|
|
|
103212
104098
|
* no zone, so it is not used: the poll time is.
|
|
103213
104099
|
*
|
|
103214
104100
|
* Firmware 8.51 and later pre-render only the "Site Data" card; the device rows then never
|
|
103215
|
-
* appear
|
|
104101
|
+
* appear here (they come through the Blazor circuit, `enpal-blazor.ts`).
|
|
103216
104102
|
*/
|
|
103217
104103
|
function parseDeviceMessages(html) {
|
|
103218
104104
|
const cards = cardOffsets(html);
|
|
@@ -103328,7 +104214,7 @@ function canonicalKey(key) {
|
|
|
103328
104214
|
* Index readings by canonical key. The box writes some keys into two measurements: the first
|
|
103329
104215
|
* occurrence wins.
|
|
103330
104216
|
*/
|
|
103331
|
-
function createSnapshot(source, readings, fetchedAt, fallbackReason) {
|
|
104217
|
+
function createSnapshot(source, readings, fetchedAt, fallbackReason, texts = []) {
|
|
103332
104218
|
const map = /* @__PURE__ */ new Map();
|
|
103333
104219
|
for (const reading of readings) {
|
|
103334
104220
|
const key = canonicalKey(reading.key);
|
|
@@ -103338,9 +104224,15 @@ function createSnapshot(source, readings, fetchedAt, fallbackReason) {
|
|
|
103338
104224
|
key
|
|
103339
104225
|
});
|
|
103340
104226
|
}
|
|
104227
|
+
const textMap = /* @__PURE__ */ new Map();
|
|
104228
|
+
for (const text of texts) {
|
|
104229
|
+
const key = canonicalKey(text.key);
|
|
104230
|
+
if (!textMap.has(key)) textMap.set(key, text.value);
|
|
104231
|
+
}
|
|
103341
104232
|
return {
|
|
103342
104233
|
source,
|
|
103343
104234
|
readings: map,
|
|
104235
|
+
texts: textMap,
|
|
103344
104236
|
fetchedAt,
|
|
103345
104237
|
...fallbackReason !== void 0 ? { fallbackReason } : {}
|
|
103346
104238
|
};
|
|
@@ -103399,14 +104291,42 @@ async function readInflux(input) {
|
|
|
103399
104291
|
}
|
|
103400
104292
|
}, input.fetch));
|
|
103401
104293
|
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
|
|
104294
|
+
return {
|
|
104295
|
+
readings,
|
|
104296
|
+
texts: []
|
|
104297
|
+
};
|
|
104298
|
+
}
|
|
104299
|
+
async function readBlazor(input) {
|
|
104300
|
+
const { connection } = input;
|
|
104301
|
+
if (input.blazor === void 0) throw new EnpalError("bridge-unavailable", "blazor", "The embedded Python the Enpal bridge needs is not available on this node");
|
|
104302
|
+
let readings;
|
|
104303
|
+
let texts;
|
|
104304
|
+
try {
|
|
104305
|
+
const rows = await input.blazor({
|
|
104306
|
+
host: connection.host,
|
|
104307
|
+
port: connection.pagePort
|
|
104308
|
+
});
|
|
104309
|
+
readings = blazorRowsToReadings(rows);
|
|
104310
|
+
texts = blazorRowsToTexts(rows);
|
|
104311
|
+
} catch (error) {
|
|
104312
|
+
if (!(error instanceof BlazorBridgeError)) throw error;
|
|
104313
|
+
throw new EnpalError(error.kind === "protocol" ? "bad-response" : error.kind, "blazor", error.message);
|
|
104314
|
+
}
|
|
104315
|
+
if (readings.length === 0) throw new EnpalError("empty", "blazor", `Enpal box ${connection.host} sent no numeric sensor rows`);
|
|
104316
|
+
return {
|
|
104317
|
+
readings,
|
|
104318
|
+
texts
|
|
104319
|
+
};
|
|
103403
104320
|
}
|
|
103404
104321
|
async function readPage(input) {
|
|
103405
104322
|
const { connection } = input;
|
|
103406
104323
|
const url = `http://${connection.host}:${connection.pagePort}/deviceMessages`;
|
|
103407
104324
|
const readings = parseDeviceMessages(await request("device-page", connection.host, url, { method: "GET" }, input.fetch));
|
|
103408
104325
|
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
|
|
104326
|
+
return {
|
|
104327
|
+
readings,
|
|
104328
|
+
texts: []
|
|
104329
|
+
};
|
|
103410
104330
|
}
|
|
103411
104331
|
function reasonOf(error) {
|
|
103412
104332
|
return {
|
|
@@ -103414,28 +104334,64 @@ function reasonOf(error) {
|
|
|
103414
104334
|
message: error.message
|
|
103415
104335
|
};
|
|
103416
104336
|
}
|
|
104337
|
+
var READERS = {
|
|
104338
|
+
blazor: readBlazor,
|
|
104339
|
+
influxdb: readInflux,
|
|
104340
|
+
"device-page": readPage
|
|
104341
|
+
};
|
|
104342
|
+
/** One reason from several: the kind of the first real failure, every message. */
|
|
104343
|
+
function joinReasons(reasons) {
|
|
104344
|
+
if (reasons.length === 0) return void 0;
|
|
104345
|
+
return {
|
|
104346
|
+
kind: reasons.find((r) => r.kind !== "bridge-unavailable")?.kind ?? "bridge-unavailable",
|
|
104347
|
+
message: reasons.map((r) => r.message).join("; ")
|
|
104348
|
+
};
|
|
104349
|
+
}
|
|
104350
|
+
var PYTHON_MISSING = {
|
|
104351
|
+
kind: "bridge-unavailable",
|
|
104352
|
+
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"
|
|
104353
|
+
};
|
|
103417
104354
|
/**
|
|
103418
|
-
* Read every data point the box reports, once. `auto` tries
|
|
103419
|
-
*
|
|
103420
|
-
* InfluxDB
|
|
103421
|
-
*
|
|
104355
|
+
* Read every data point the box reports, once. `auto` tries the Blazor channel first (the
|
|
104356
|
+
* only one that carries the device tables on firmware 8.51 and later, no credential), then
|
|
104357
|
+
* InfluxDB when a token is set, then the device page. The snapshot records why a preferred
|
|
104358
|
+
* source was not used: Python missing, the channel skipped after a recent failure, or the
|
|
104359
|
+
* failure itself. A Blazor failure in `auto` is remembered for {@link BLAZOR_BACKOFF_MS}.
|
|
104360
|
+
* When every source fails the FIRST real failure is the one reported. A failed read throws
|
|
104361
|
+
* {@link EnpalError}: it never yields a snapshot of zeros.
|
|
103422
104362
|
*/
|
|
103423
104363
|
async function readEnpalSnapshot(input) {
|
|
103424
|
-
const { source } = input.connection;
|
|
103425
|
-
|
|
103426
|
-
|
|
103427
|
-
|
|
103428
|
-
|
|
103429
|
-
|
|
103430
|
-
|
|
103431
|
-
|
|
103432
|
-
|
|
103433
|
-
|
|
103434
|
-
|
|
103435
|
-
|
|
103436
|
-
|
|
104364
|
+
const { source: mode } = input.connection;
|
|
104365
|
+
const now = input.now();
|
|
104366
|
+
const hasToken = input.tokenLocked !== true && input.connection.token.trim().length > 0;
|
|
104367
|
+
const reasons = [];
|
|
104368
|
+
let first;
|
|
104369
|
+
const order = [];
|
|
104370
|
+
if (mode === "blazor") order.push("blazor");
|
|
104371
|
+
else if (mode === "influx") order.push("influxdb");
|
|
104372
|
+
else if (mode === "page") order.push("device-page");
|
|
104373
|
+
else {
|
|
104374
|
+
if (input.blazor === void 0) reasons.push(PYTHON_MISSING);
|
|
104375
|
+
else {
|
|
104376
|
+
const skip = input.blazorBackoff?.skipReason(now) ?? null;
|
|
104377
|
+
if (skip !== null) reasons.push(skip);
|
|
104378
|
+
else order.push("blazor");
|
|
103437
104379
|
}
|
|
104380
|
+
if (hasToken) order.push("influxdb");
|
|
104381
|
+
order.push("device-page");
|
|
103438
104382
|
}
|
|
104383
|
+
for (const source of order) try {
|
|
104384
|
+
const read = await READERS[source](input);
|
|
104385
|
+
if (source === "blazor") input.blazorBackoff?.recovered();
|
|
104386
|
+
return createSnapshot(source, read.readings, now, joinReasons(reasons), read.texts);
|
|
104387
|
+
} catch (error) {
|
|
104388
|
+
if (!(error instanceof EnpalError)) throw error;
|
|
104389
|
+
if (source === "blazor" && mode === "auto") input.blazorBackoff?.failed(now, reasonOf(error));
|
|
104390
|
+
first ??= error;
|
|
104391
|
+
reasons.push(reasonOf(error));
|
|
104392
|
+
}
|
|
104393
|
+
if (first === void 0) throw new Error("readEnpalSnapshot: no source to read");
|
|
104394
|
+
throw first;
|
|
103439
104395
|
}
|
|
103440
104396
|
//#endregion
|
|
103441
104397
|
//#region src/enpal-integration-manager.ts
|
|
@@ -103450,6 +104406,9 @@ var EnpalIntegrationManager = class {
|
|
|
103450
104406
|
#snapshot = null;
|
|
103451
104407
|
#lastError = null;
|
|
103452
104408
|
#timer = null;
|
|
104409
|
+
#blazorBackoff = new BlazorBackoff();
|
|
104410
|
+
#lastSource = null;
|
|
104411
|
+
#lastFallbackKey = null;
|
|
103453
104412
|
#inFlight = null;
|
|
103454
104413
|
#stopped = true;
|
|
103455
104414
|
#failureLogged = false;
|
|
@@ -103481,7 +104440,7 @@ var EnpalIntegrationManager = class {
|
|
|
103481
104440
|
if (this.#stopped || this.#timer !== null) return;
|
|
103482
104441
|
this.#timer = setInterval(() => {
|
|
103483
104442
|
this.poll();
|
|
103484
|
-
}, this.#options.connection
|
|
104443
|
+
}, effectivePollIntervalMs(this.#options.connection));
|
|
103485
104444
|
this.#timer.unref();
|
|
103486
104445
|
}
|
|
103487
104446
|
async stop() {
|
|
@@ -103501,6 +104460,19 @@ var EnpalIntegrationManager = class {
|
|
|
103501
104460
|
this.#inFlight = run;
|
|
103502
104461
|
return run;
|
|
103503
104462
|
}
|
|
104463
|
+
/**
|
|
104464
|
+
* 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
|
|
104465
|
+
* once per boot: a box that drops from the web channel to the page mid-run is visible.
|
|
104466
|
+
*/
|
|
104467
|
+
#logSourceChange(source, fallback) {
|
|
104468
|
+
const fallbackMessage = fallback?.message;
|
|
104469
|
+
const fallbackKey = fallback?.kind ?? null;
|
|
104470
|
+
const changed = source !== this.#lastSource || fallbackKey !== this.#lastFallbackKey;
|
|
104471
|
+
if (changed && fallbackMessage !== void 0) this.#options.logger.warn(`Enpal box ${this.name}: preferred source not used (${fallbackMessage}), reading ${source}`);
|
|
104472
|
+
else if (changed && this.#lastSource !== null && source !== this.#lastSource) this.#options.logger.info(`Enpal box ${this.name}: now reading ${source}`);
|
|
104473
|
+
this.#lastSource = source;
|
|
104474
|
+
this.#lastFallbackKey = fallbackKey;
|
|
104475
|
+
}
|
|
103504
104476
|
async #pollOnce() {
|
|
103505
104477
|
const wasConnected = this.#status === "connected";
|
|
103506
104478
|
try {
|
|
@@ -103508,6 +104480,8 @@ var EnpalIntegrationManager = class {
|
|
|
103508
104480
|
connection: this.#options.connection,
|
|
103509
104481
|
tokenLocked: this.#options.tokenLocked,
|
|
103510
104482
|
fetch: this.#fetch,
|
|
104483
|
+
blazor: this.#options.blazor?.(),
|
|
104484
|
+
blazorBackoff: this.#blazorBackoff,
|
|
103511
104485
|
now: this.#options.now ?? Date.now
|
|
103512
104486
|
});
|
|
103513
104487
|
if (this.#stopped) return;
|
|
@@ -103515,7 +104489,7 @@ var EnpalIntegrationManager = class {
|
|
|
103515
104489
|
this.#lastError = null;
|
|
103516
104490
|
this.#status = "connected";
|
|
103517
104491
|
if (this.#failureLogged || !wasConnected) this.#options.logger.info(`Enpal box ${this.name} reachable through ${snapshot.source} (${snapshot.readings.size} data points)`);
|
|
103518
|
-
|
|
104492
|
+
this.#logSourceChange(snapshot.source, snapshot.fallbackReason);
|
|
103519
104493
|
this.#failureLogged = false;
|
|
103520
104494
|
if (!wasConnected) this.#options.onConnected();
|
|
103521
104495
|
this.#options.onPolled();
|
|
@@ -103572,6 +104546,7 @@ var EnpalClientRegistry = class {
|
|
|
103572
104546
|
tokenLocked,
|
|
103573
104547
|
logger: this.#logger.withTags({ integrationId }),
|
|
103574
104548
|
fetch: this.#deps.fetch,
|
|
104549
|
+
blazor: this.#deps.blazor,
|
|
103575
104550
|
now: this.#deps.now,
|
|
103576
104551
|
onConnected: () => {
|
|
103577
104552
|
this.#deps.onConnected(integrationId);
|
|
@@ -103618,6 +104593,26 @@ var EnpalClientRegistry = class {
|
|
|
103618
104593
|
}
|
|
103619
104594
|
};
|
|
103620
104595
|
//#endregion
|
|
104596
|
+
//#region src/enpal-python-paths.ts
|
|
104597
|
+
var WORKER = "enpal_worker.py";
|
|
104598
|
+
/**
|
|
104599
|
+
* Absolute path of the bundled `python/enpal_worker.py`: next to `dist/` in a deployed
|
|
104600
|
+
* package, two levels up from `src/` in the workspace.
|
|
104601
|
+
*/
|
|
104602
|
+
function resolveEnpalWorker(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
104603
|
+
const here = (0, node_path.dirname)((0, node_url.fileURLToPath)(fromModuleUrl));
|
|
104604
|
+
const candidates = [
|
|
104605
|
+
(0, node_path.join)(here, "../python"),
|
|
104606
|
+
(0, node_path.join)(here, "../../python"),
|
|
104607
|
+
(0, node_path.join)(here, "python")
|
|
104608
|
+
];
|
|
104609
|
+
for (const dir of candidates) {
|
|
104610
|
+
const file = (0, node_path.join)(dir, WORKER);
|
|
104611
|
+
if ((0, node_fs.existsSync)(file)) return file;
|
|
104612
|
+
}
|
|
104613
|
+
throw new Error(`provider-enpal: ${WORKER} not found. Searched:\n${candidates.join("\n")}`);
|
|
104614
|
+
}
|
|
104615
|
+
//#endregion
|
|
103621
104616
|
//#region src/enpal-instances.ts
|
|
103622
104617
|
/**
|
|
103623
104618
|
* Enpal integrations as `integration-instances` (planning#416, D1007): one `connections` record
|
|
@@ -103799,23 +104794,29 @@ async function cascadeRemoveDevicesForIntegration(input) {
|
|
|
103799
104794
|
var enpalGatewaySchema = object({
|
|
103800
104795
|
equipment: literal("box"),
|
|
103801
104796
|
integrationId: string().min(1).describe("Owning integration"),
|
|
103802
|
-
system: literal(ENPAL_SYSTEM).optional()
|
|
104797
|
+
system: literal(ENPAL_SYSTEM).optional(),
|
|
104798
|
+
/** The box diagnostics it was adopted with; a box adopted before they existed has none. */
|
|
104799
|
+
specIds: array(string().min(1)).default([])
|
|
103803
104800
|
});
|
|
103804
104801
|
/**
|
|
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
|
-
*
|
|
104802
|
+
* The Enpal box itself ("Enpal Box"), as a {@link DeviceType.Gateway}. Its connection lives on
|
|
104803
|
+
* the INTEGRATION; this device is the box's presence in the device list: its identity and
|
|
104804
|
+
* firmware as device attributes, its own diagnostics (CPU, memory, LTE) as hidden Sensor
|
|
104805
|
+
* children, and the one action the box really supports, `device-admin-link`, its local device
|
|
104806
|
+
* page. The box has no reboot and no update. The System and the heat pump are NOT children of
|
|
104807
|
+
* it; each is adopted as its own top-level Container. Online follows the integration's box
|
|
104808
|
+
* (the integration marks it offline when a poll fails).
|
|
103811
104809
|
*/
|
|
103812
104810
|
var EnpalGatewayDevice = class extends BaseDevice {
|
|
103813
104811
|
features = [];
|
|
103814
104812
|
integrationId;
|
|
104813
|
+
specIds;
|
|
104814
|
+
snapshotUnsub = null;
|
|
103815
104815
|
constructor(ctx) {
|
|
103816
104816
|
const cfg = enpalGatewaySchema.parse(ctx.persistedConfig ?? {});
|
|
103817
104817
|
super(ctx, enpalGatewaySchema, { type: DeviceType.Gateway });
|
|
103818
104818
|
this.integrationId = cfg.integrationId;
|
|
104819
|
+
this.specIds = cfg.specIds;
|
|
103819
104820
|
this.online = enpalConnections.getSnapshot(this.integrationId) !== null;
|
|
103820
104821
|
this.registerAdminLinkCap();
|
|
103821
104822
|
this.updateSourceInfo({
|
|
@@ -103823,6 +104824,37 @@ var EnpalGatewayDevice = class extends BaseDevice {
|
|
|
103823
104824
|
system: ENPAL_SYSTEM
|
|
103824
104825
|
});
|
|
103825
104826
|
}
|
|
104827
|
+
async onActivate() {
|
|
104828
|
+
await super.onActivate();
|
|
104829
|
+
const metadata = new EnpalMetadataSync({
|
|
104830
|
+
equipment: "box",
|
|
104831
|
+
deviceId: this.id,
|
|
104832
|
+
getSnapshot: () => enpalConnections.getSnapshot(this.integrationId),
|
|
104833
|
+
write: async (patch) => {
|
|
104834
|
+
await this.ctx.api.deviceManager.setMetadata.mutate({
|
|
104835
|
+
deviceId: this.id,
|
|
104836
|
+
patch
|
|
104837
|
+
});
|
|
104838
|
+
},
|
|
104839
|
+
logger: this.ctx.logger
|
|
104840
|
+
});
|
|
104841
|
+
this.snapshotUnsub = enpalConnections.onSnapshot(this.integrationId, () => {
|
|
104842
|
+
metadata.apply();
|
|
104843
|
+
});
|
|
104844
|
+
await metadata.apply();
|
|
104845
|
+
}
|
|
104846
|
+
async removeDevice() {
|
|
104847
|
+
this.snapshotUnsub?.();
|
|
104848
|
+
this.snapshotUnsub = null;
|
|
104849
|
+
await super.removeDevice();
|
|
104850
|
+
}
|
|
104851
|
+
getAccessoryChildren() {
|
|
104852
|
+
return accessoryChildSpecs({
|
|
104853
|
+
equipment: "box",
|
|
104854
|
+
specIds: this.specIds,
|
|
104855
|
+
integrationId: this.integrationId
|
|
104856
|
+
});
|
|
104857
|
+
}
|
|
103826
104858
|
/** The box's local device page (plain HTTP, the page port); no credentials in the URL. */
|
|
103827
104859
|
registerAdminLinkCap() {
|
|
103828
104860
|
this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
|
|
@@ -103878,10 +104910,11 @@ var ENPAL_MIGRATIONS_DATE = "2026-11-05";
|
|
|
103878
104910
|
/**
|
|
103879
104911
|
* Enpal solar box provider, `mode: account` and broker-less. An integration is one box: its
|
|
103880
104912
|
* 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
|
-
*
|
|
104913
|
+
* with one manager per `integrationId`. Discovery offers three pieces, each adopted only when
|
|
104914
|
+
* the operator picks it: the box itself ("Enpal Box", a {@link DeviceType.Gateway} carrying its
|
|
104915
|
+
* firmware, its diagnostics and a link to its device page), the "System" (PV, battery, grid,
|
|
104916
|
+
* house meter) and the "Heat pump (Enpal)", both {@link DeviceType.Container}s. Adopting a
|
|
104917
|
+
* piece adopts its Sensor children; a Container declares which of them stands for it.
|
|
103885
104918
|
*
|
|
103886
104919
|
* The box does not advertise itself on the network, so the integration is added by hand.
|
|
103887
104920
|
*/
|
|
@@ -103895,6 +104928,8 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
103895
104928
|
integrationReconciler = null;
|
|
103896
104929
|
clients = null;
|
|
103897
104930
|
instances = null;
|
|
104931
|
+
/** The Python bridge to the box's Blazor channel; null while the node has no embedded Python. */
|
|
104932
|
+
blazorReader = null;
|
|
103898
104933
|
constructor() {
|
|
103899
104934
|
super({});
|
|
103900
104935
|
}
|
|
@@ -103917,7 +104952,9 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
103917
104952
|
kind: enpalConnectionsEntryKind
|
|
103918
104953
|
}));
|
|
103919
104954
|
this.instances = buildEnpalInstances(records, { onChanged: () => this.reconcileIntegrations() });
|
|
104955
|
+
this.blazorReader = await this.createBlazorBridge();
|
|
103920
104956
|
this.clients = new EnpalClientRegistry(this.ctx.logger, {
|
|
104957
|
+
blazor: () => this.blazorReader ?? void 0,
|
|
103921
104958
|
onConnected: (integrationId) => this.setIntegrationDevicesOnline(integrationId, true),
|
|
103922
104959
|
onDisconnected: (integrationId) => this.setIntegrationDevicesOnline(integrationId, false)
|
|
103923
104960
|
});
|
|
@@ -103942,11 +104979,32 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
103942
104979
|
this.ctx.logger.warn("Enpal shutdown error", { meta: { error: errMsg(err) } });
|
|
103943
104980
|
}
|
|
103944
104981
|
this.clients = null;
|
|
104982
|
+
this.blazorReader = null;
|
|
103945
104983
|
this.integrationReconciler?.dispose();
|
|
103946
104984
|
this.integrationReconciler = null;
|
|
103947
104985
|
enpalConnections.clear();
|
|
103948
104986
|
await super.onShutdown();
|
|
103949
104987
|
}
|
|
104988
|
+
/**
|
|
104989
|
+
* The embedded Python and the bundled worker, or null with a logged reason: the addon then
|
|
104990
|
+
* reads InfluxDB or the device page, which on firmware 8.51 and later hold less.
|
|
104991
|
+
*/
|
|
104992
|
+
async createBlazorBridge() {
|
|
104993
|
+
try {
|
|
104994
|
+
const pythonPath = await this.ctx.deps.ensurePython();
|
|
104995
|
+
if (pythonPath === null) {
|
|
104996
|
+
this.ctx.logger.warn("Enpal: embedded Python is unavailable, the Blazor channel is not used");
|
|
104997
|
+
return null;
|
|
104998
|
+
}
|
|
104999
|
+
return createBlazorReader({
|
|
105000
|
+
pythonPath,
|
|
105001
|
+
scriptPath: resolveEnpalWorker()
|
|
105002
|
+
});
|
|
105003
|
+
} catch (err) {
|
|
105004
|
+
this.ctx.logger.warn("Enpal: the Python bridge could not be prepared", { meta: { error: errMsg(err) } });
|
|
105005
|
+
return null;
|
|
105006
|
+
}
|
|
105007
|
+
}
|
|
103950
105008
|
requireInstances() {
|
|
103951
105009
|
if (!this.instances) throw new Error("Enpal provider not initialised");
|
|
103952
105010
|
return this.instances;
|
|
@@ -104050,7 +105108,7 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
104050
105108
|
}
|
|
104051
105109
|
return out;
|
|
104052
105110
|
},
|
|
104053
|
-
adoptDevice: async ({ equipment, specIds, integrationId, name }) => {
|
|
105111
|
+
adoptDevice: async ({ equipment, specIds, integrationId, name, childLayout, primaryChildPreference }) => {
|
|
104054
105112
|
const devices = this.ctx.kernel.devices;
|
|
104055
105113
|
if (!devices) throw new Error("enpal adopt: kernel.devices unavailable");
|
|
104056
105114
|
const config = {
|
|
@@ -104063,7 +105121,9 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
104063
105121
|
const device = await devices.create(stableId, EnpalEquipmentDevice, config, null, {
|
|
104064
105122
|
type: DeviceType.Container,
|
|
104065
105123
|
name,
|
|
104066
|
-
integrationId
|
|
105124
|
+
integrationId,
|
|
105125
|
+
...childLayout.length > 0 ? { childLayout } : {},
|
|
105126
|
+
...primaryChildPreference.length > 0 ? { primaryChildPreference } : {}
|
|
104067
105127
|
});
|
|
104068
105128
|
const children = await devices.getChildren(device.id);
|
|
104069
105129
|
return {
|
|
@@ -104071,20 +105131,27 @@ var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
|
104071
105131
|
accessoryDeviceIds: children.map((c) => c.id)
|
|
104072
105132
|
};
|
|
104073
105133
|
},
|
|
104074
|
-
adoptGateway: async ({ integrationId, name }) => {
|
|
105134
|
+
adoptGateway: async ({ integrationId, name, specIds, childLayout }) => {
|
|
104075
105135
|
const devices = this.ctx.kernel.devices;
|
|
104076
105136
|
if (!devices) throw new Error("enpal adopt: kernel.devices unavailable");
|
|
104077
105137
|
const config = {
|
|
104078
105138
|
equipment: "box",
|
|
105139
|
+
specIds,
|
|
104079
105140
|
integrationId,
|
|
104080
105141
|
system: ENPAL_SYSTEM
|
|
104081
105142
|
};
|
|
104082
105143
|
const stableId = this.generateStableId(DeviceType.Gateway, config);
|
|
104083
|
-
|
|
105144
|
+
const device = await devices.create(stableId, EnpalGatewayDevice, config, null, {
|
|
104084
105145
|
type: DeviceType.Gateway,
|
|
104085
105146
|
name,
|
|
104086
|
-
integrationId
|
|
104087
|
-
|
|
105147
|
+
integrationId,
|
|
105148
|
+
...childLayout.length > 0 ? { childLayout } : {}
|
|
105149
|
+
});
|
|
105150
|
+
const children = await devices.getChildren(device.id);
|
|
105151
|
+
return {
|
|
105152
|
+
deviceId: device.id,
|
|
105153
|
+
accessoryDeviceIds: children.map((c) => c.id)
|
|
105154
|
+
};
|
|
104088
105155
|
},
|
|
104089
105156
|
removeDevice: async (id) => {
|
|
104090
105157
|
await this.ctx.kernel.devices?.remove(id);
|