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