@camstack/addon-provider-enpal 0.1.1 → 0.1.2
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/README.md +18 -16
- package/dist/addon.js +925 -552
- package/dist/addon.mjs +913 -545
- package/dist/index.js +13 -8
- package/dist/index.mjs +2 -2
- package/i18n/en.json +0 -1
- package/i18n/it.json +0 -1
- package/package.json +6 -3
package/dist/addon.js
CHANGED
|
@@ -10470,20 +10470,6 @@ object({
|
|
|
10470
10470
|
code: string().regex(/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/),
|
|
10471
10471
|
params: record(string(), union([string(), number()])).optional()
|
|
10472
10472
|
});
|
|
10473
|
-
var CamstackError = class extends Error {
|
|
10474
|
-
i18nCode;
|
|
10475
|
-
i18nParams;
|
|
10476
|
-
constructor(code, params, englishMessage) {
|
|
10477
|
-
super(englishMessage);
|
|
10478
|
-
this.name = "CamstackError";
|
|
10479
|
-
this.i18nCode = code;
|
|
10480
|
-
this.i18nParams = params;
|
|
10481
|
-
}
|
|
10482
|
-
};
|
|
10483
|
-
/** `throw userError('device.not-found', { id }, 'Device 12 not found')` */
|
|
10484
|
-
function userError(code, params, englishFallback) {
|
|
10485
|
-
return new CamstackError(code, params, englishFallback);
|
|
10486
|
-
}
|
|
10487
10473
|
//#endregion
|
|
10488
10474
|
//#region ../types/dist/err-msg-DIorGYeC.mjs
|
|
10489
10475
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
@@ -21033,19 +21019,36 @@ var ResyncResultSchema = object({
|
|
|
21033
21019
|
* provider re-derived the device. 0/absent for a normal incremental re-sync. */
|
|
21034
21020
|
removedChildren: number().int().nonnegative().optional()
|
|
21035
21021
|
});
|
|
21036
|
-
|
|
21037
|
-
|
|
21038
|
-
|
|
21039
|
-
|
|
21040
|
-
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
|
|
21045
|
-
}),
|
|
21046
|
-
|
|
21047
|
-
|
|
21048
|
-
})
|
|
21022
|
+
var deviceAdoptionCapability = {
|
|
21023
|
+
name: "device-adoption",
|
|
21024
|
+
scope: "system",
|
|
21025
|
+
mode: "collection",
|
|
21026
|
+
status: {
|
|
21027
|
+
schema: AdoptionStatusSchema,
|
|
21028
|
+
kind: "poll"
|
|
21029
|
+
},
|
|
21030
|
+
methods: {
|
|
21031
|
+
listCandidateFilters: method(object({ integrationId: string() }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }),
|
|
21032
|
+
listCandidates: method(ListCandidatesInputSchema, ListCandidatesOutputSchema, { auth: "admin" }),
|
|
21033
|
+
getCandidate: method(GetCandidateInputSchema, DiscoveredChildDeviceSchema.nullable(), { auth: "admin" }),
|
|
21034
|
+
refresh: method(object({ integrationId: string() }), AdoptionStatusSchema, {
|
|
21035
|
+
kind: "mutation",
|
|
21036
|
+
auth: "admin"
|
|
21037
|
+
}),
|
|
21038
|
+
adopt: method(AdoptInputSchema, AdoptResultSchema, {
|
|
21039
|
+
kind: "mutation",
|
|
21040
|
+
auth: "admin"
|
|
21041
|
+
}),
|
|
21042
|
+
release: method(ReleaseInputSchema, _void(), {
|
|
21043
|
+
kind: "mutation",
|
|
21044
|
+
auth: "admin"
|
|
21045
|
+
}),
|
|
21046
|
+
resync: method(ResyncInputSchema, ResyncResultSchema, {
|
|
21047
|
+
kind: "mutation",
|
|
21048
|
+
auth: "admin"
|
|
21049
|
+
})
|
|
21050
|
+
}
|
|
21051
|
+
};
|
|
21049
21052
|
/**
|
|
21050
21053
|
* `device-export` — collection cap for addons that export camstack
|
|
21051
21054
|
* devices to external ecosystems (HomeAssistant via MQTT discovery,
|
|
@@ -64361,9 +64364,9 @@ var PowerMeterStatusSchema = object({
|
|
|
64361
64364
|
/** Cumulative energy in kilowatt-hours since the meter was reset. */
|
|
64362
64365
|
kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
|
|
64363
64366
|
/** Voltage in volts. */
|
|
64364
|
-
volts: number().optional().meta(readingMeta("voltage")),
|
|
64367
|
+
volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
|
|
64365
64368
|
/** Current in amperes. */
|
|
64366
|
-
amps: number().optional().meta(readingMeta("current")),
|
|
64369
|
+
amps: number().optional().meta(readingMeta("current", { label: "Current" })),
|
|
64367
64370
|
/** Apparent power in volt-amperes. */
|
|
64368
64371
|
va: number().optional().meta(readingMeta("apparent-power")),
|
|
64369
64372
|
/** Power factor (0..1) if reported. */
|
|
@@ -64371,17 +64374,74 @@ var PowerMeterStatusSchema = object({
|
|
|
64371
64374
|
/** Ms epoch when the slice was last updated. */
|
|
64372
64375
|
lastFetchedAt: number()
|
|
64373
64376
|
});
|
|
64377
|
+
/**
|
|
64378
|
+
* Loads that draw metered mains power (D951). The reading is a feature of the
|
|
64379
|
+
* device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
|
|
64380
|
+
* vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
|
|
64381
|
+
* meter or a standalone energy monitor IS the device. Cover, lock, valve,
|
|
64382
|
+
* camera and the like are left out: their draw is not what anyone meters.
|
|
64383
|
+
*/
|
|
64384
|
+
var POWER_METER_DEVICE_TYPES = [
|
|
64385
|
+
DeviceType.Sensor,
|
|
64386
|
+
DeviceType.Switch,
|
|
64387
|
+
DeviceType.Light,
|
|
64388
|
+
DeviceType.Fan,
|
|
64389
|
+
DeviceType.Climate,
|
|
64390
|
+
DeviceType.Thermostat,
|
|
64391
|
+
DeviceType.Humidifier,
|
|
64392
|
+
DeviceType.WaterHeater,
|
|
64393
|
+
DeviceType.MediaPlayer,
|
|
64394
|
+
DeviceType.Washer,
|
|
64395
|
+
DeviceType.Dryer,
|
|
64396
|
+
DeviceType.Dishwasher,
|
|
64397
|
+
DeviceType.Vacuum,
|
|
64398
|
+
DeviceType.LawnMower,
|
|
64399
|
+
DeviceType.PetFeeder
|
|
64400
|
+
];
|
|
64401
|
+
/** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
|
|
64402
|
+
var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
|
|
64403
|
+
/** One scalar target per reading; each is sourced from a device of its own (D951). */
|
|
64404
|
+
function powerTarget(id, unit) {
|
|
64405
|
+
return {
|
|
64406
|
+
id,
|
|
64407
|
+
cardinality: "scalar",
|
|
64408
|
+
vocabulary: {
|
|
64409
|
+
cap: "power-meter",
|
|
64410
|
+
stateField: id
|
|
64411
|
+
},
|
|
64412
|
+
deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
|
|
64413
|
+
apply: "add",
|
|
64414
|
+
path: id,
|
|
64415
|
+
sources: [{
|
|
64416
|
+
kind: "numeric",
|
|
64417
|
+
requiredUnit: unit,
|
|
64418
|
+
transforms: [{ kind: "identity" }]
|
|
64419
|
+
}],
|
|
64420
|
+
companions: [{
|
|
64421
|
+
kind: "clock",
|
|
64422
|
+
path: "lastFetchedAt",
|
|
64423
|
+
when: "always"
|
|
64424
|
+
}],
|
|
64425
|
+
sourceReport: { status: "assignment-status" },
|
|
64426
|
+
advancedExpression: true
|
|
64427
|
+
};
|
|
64428
|
+
}
|
|
64429
|
+
/** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
|
|
64430
|
+
var powerMeterCustomize = {
|
|
64431
|
+
version: 1,
|
|
64432
|
+
targets: [
|
|
64433
|
+
powerTarget("watts", "W"),
|
|
64434
|
+
powerTarget("kwhTotal", "kWh"),
|
|
64435
|
+
powerTarget("volts", "V"),
|
|
64436
|
+
powerTarget("amps", "A")
|
|
64437
|
+
]
|
|
64438
|
+
};
|
|
64374
64439
|
var powerMeterCapability = {
|
|
64375
64440
|
name: "power-meter",
|
|
64376
64441
|
scope: "device",
|
|
64377
64442
|
deviceNative: true,
|
|
64378
64443
|
mode: "singleton",
|
|
64379
|
-
deviceTypes:
|
|
64380
|
-
DeviceType.Sensor,
|
|
64381
|
-
DeviceType.Washer,
|
|
64382
|
-
DeviceType.Dryer,
|
|
64383
|
-
DeviceType.Dishwasher
|
|
64384
|
-
],
|
|
64444
|
+
deviceTypes: POWER_METER_DEVICE_TYPES,
|
|
64385
64445
|
methods: {},
|
|
64386
64446
|
status: {
|
|
64387
64447
|
schema: PowerMeterStatusSchema,
|
|
@@ -64438,7 +64498,8 @@ var powerMeterCapability = {
|
|
|
64438
64498
|
} },
|
|
64439
64499
|
/** Clock fields: written, but excluded from the compare that decides
|
|
64440
64500
|
* whether persisting is worth a SQLite commit. */
|
|
64441
|
-
volatileStateFields: ["lastFetchedAt"]
|
|
64501
|
+
volatileStateFields: ["lastFetchedAt"],
|
|
64502
|
+
customize: powerMeterCustomize
|
|
64442
64503
|
};
|
|
64443
64504
|
/**
|
|
64444
64505
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
@@ -70209,7 +70270,7 @@ function tryConvertUnit(value, from, to) {
|
|
|
70209
70270
|
return fromCanonical(toCanonical(value, fromSpec), toSpec);
|
|
70210
70271
|
}
|
|
70211
70272
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
70212
|
-
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, 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.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
70273
|
+
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, 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.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
70213
70274
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
70214
70275
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
70215
70276
|
new Set(CAP_NAMES);
|
|
@@ -89643,9 +89704,9 @@ var MB = 1024 * 1024;
|
|
|
89643
89704
|
//#endregion
|
|
89644
89705
|
//#region src/config.ts
|
|
89645
89706
|
/**
|
|
89646
|
-
* Operator-supplied connection
|
|
89647
|
-
*
|
|
89648
|
-
*
|
|
89707
|
+
* Operator-supplied connection of ONE Enpal integration (`mode: account`, broker-less): the
|
|
89708
|
+
* settings of the integration itself, not of a device. Every equipment the box reports is
|
|
89709
|
+
* adopted as a Container from it.
|
|
89649
89710
|
*
|
|
89650
89711
|
* Two sources behind `source`:
|
|
89651
89712
|
* - `influx`: the box's InfluxDB 2.x (port 8086, org `enpal`, bucket `solar`).
|
|
@@ -89666,6 +89727,10 @@ function markerToEmpty(value) {
|
|
|
89666
89727
|
return value === null || value === void 0 ? "" : value;
|
|
89667
89728
|
}
|
|
89668
89729
|
var blankToUndefined = (v) => v === "" || v === null ? void 0 : v;
|
|
89730
|
+
/** Names that are interpolated into a Flux string: no quote, no space, no operator. */
|
|
89731
|
+
var FLUX_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
89732
|
+
/** The device-registry `system` tag every Enpal device carries. */
|
|
89733
|
+
var ENPAL_SYSTEM = "enpal";
|
|
89669
89734
|
var enpalConnectionSchema = object({
|
|
89670
89735
|
host: preprocess(cleanHost, string().min(1).regex(/^[A-Za-z0-9._-]+$/, "host name or IP address only").describe("Enpal box host name or IP address")),
|
|
89671
89736
|
source: _enum([
|
|
@@ -89675,7 +89740,7 @@ var enpalConnectionSchema = object({
|
|
|
89675
89740
|
]).default("auto").describe("Where the readings come from"),
|
|
89676
89741
|
influxPort: preprocess(blankToUndefined, number().int().min(1).max(65535).default(8086)).describe("InfluxDB port"),
|
|
89677
89742
|
org: string().trim().min(1).default("enpal").describe("InfluxDB organisation"),
|
|
89678
|
-
bucket: string().trim().regex(
|
|
89743
|
+
bucket: string().trim().regex(FLUX_NAME, "letters, digits, dot, dash and underscore only").default("solar").describe("InfluxDB bucket"),
|
|
89679
89744
|
token: preprocess(markerToEmpty, string().default("")).describe("InfluxDB access token (issued by Enpal support)").meta({ secret: true }),
|
|
89680
89745
|
pagePort: preprocess(blankToUndefined, number().int().min(1).max(65535).default(80)).describe("Device page HTTP port"),
|
|
89681
89746
|
pollIntervalMs: preprocess(blankToUndefined, number().int().min(1e4).max(36e5).default(3e4)).describe("Poll interval (ms)"),
|
|
@@ -89698,30 +89763,21 @@ function readConnection(settings) {
|
|
|
89698
89763
|
tokenLocked: isSecretMarkerLike(raw["token"])
|
|
89699
89764
|
};
|
|
89700
89765
|
}
|
|
89701
|
-
/**
|
|
89702
|
-
|
|
89703
|
-
|
|
89704
|
-
|
|
89705
|
-
|
|
89706
|
-
|
|
89707
|
-
|
|
89708
|
-
|
|
89709
|
-
|
|
89710
|
-
* `stableId`) in the in-process resolver.
|
|
89711
|
-
*/
|
|
89712
|
-
var enpalSensorSchema = object({
|
|
89713
|
-
specId: string().min(1).describe("Device catalog entry id"),
|
|
89714
|
-
boxId: string().min(1).describe("Parent box stableId"),
|
|
89715
|
-
system: literal("enpal").optional()
|
|
89716
|
-
});
|
|
89717
|
-
/** Slugify a host into a readable, stable row-key fragment. */
|
|
89718
|
-
function slugifyHost(value) {
|
|
89719
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
89766
|
+
/** Parse an integration's settings into a connection, or null while its host is missing. */
|
|
89767
|
+
function connectionFromSettings(settings) {
|
|
89768
|
+
const raw = toRecord(settings);
|
|
89769
|
+
const parsed = enpalConnectionSchema.safeParse(raw);
|
|
89770
|
+
if (!parsed.success) return null;
|
|
89771
|
+
return {
|
|
89772
|
+
connection: parsed.data,
|
|
89773
|
+
tokenLocked: isSecretMarkerLike(raw["token"])
|
|
89774
|
+
};
|
|
89720
89775
|
}
|
|
89721
|
-
/**
|
|
89722
|
-
|
|
89723
|
-
|
|
89724
|
-
|
|
89776
|
+
/** True when two connections would poll the box the same way. */
|
|
89777
|
+
function sameConnection(a, b) {
|
|
89778
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
89779
|
+
}
|
|
89780
|
+
/** The integration's settings form (`globalSettingsSchema`); the integration carries the name. */
|
|
89725
89781
|
function buildConnectionFormSchema() {
|
|
89726
89782
|
return { sections: [
|
|
89727
89783
|
{
|
|
@@ -89730,13 +89786,6 @@ function buildConnectionFormSchema() {
|
|
|
89730
89786
|
titleKey: "provider-enpal:connection.s_identity.title",
|
|
89731
89787
|
columns: 1,
|
|
89732
89788
|
fields: [{
|
|
89733
|
-
type: "text",
|
|
89734
|
-
key: "name",
|
|
89735
|
-
label: "Name",
|
|
89736
|
-
labelKey: "provider-enpal:connection.s_identity.name.label",
|
|
89737
|
-
required: true,
|
|
89738
|
-
placeholder: "Enpal"
|
|
89739
|
-
}, {
|
|
89740
89789
|
type: "text",
|
|
89741
89790
|
key: "host",
|
|
89742
89791
|
label: "Box host / IP",
|
|
@@ -89891,7 +89940,7 @@ var ENPAL_SPECS = [
|
|
|
89891
89940
|
{
|
|
89892
89941
|
kind: "power-meter",
|
|
89893
89942
|
id: "pv",
|
|
89894
|
-
name: "
|
|
89943
|
+
name: "Production",
|
|
89895
89944
|
role: DeviceRole.PowerSensor,
|
|
89896
89945
|
watts: point("power", "Power.DC.Total", "Power.DC.Total.Calculated", "Power.DC.Total.Huawei", "Power.Production.Total"),
|
|
89897
89946
|
kwh: point("energy", "Energy.Production.Total.Lifetime")
|
|
@@ -89910,7 +89959,7 @@ var ENPAL_SPECS = [
|
|
|
89910
89959
|
{
|
|
89911
89960
|
kind: "meter",
|
|
89912
89961
|
id: "battery-charged",
|
|
89913
|
-
name: "
|
|
89962
|
+
name: "Energy charged",
|
|
89914
89963
|
role: DeviceRole.EnergySensor,
|
|
89915
89964
|
quantity: "energy",
|
|
89916
89965
|
value: point("energy", "Energy.Battery.Charge.Lifetime")
|
|
@@ -89918,7 +89967,7 @@ var ENPAL_SPECS = [
|
|
|
89918
89967
|
{
|
|
89919
89968
|
kind: "meter",
|
|
89920
89969
|
id: "battery-discharged",
|
|
89921
|
-
name: "
|
|
89970
|
+
name: "Energy discharged",
|
|
89922
89971
|
role: DeviceRole.EnergySensor,
|
|
89923
89972
|
quantity: "energy",
|
|
89924
89973
|
value: point("energy", "Energy.Battery.Discharge.Lifetime")
|
|
@@ -89926,7 +89975,7 @@ var ENPAL_SPECS = [
|
|
|
89926
89975
|
{
|
|
89927
89976
|
kind: "power-meter",
|
|
89928
89977
|
id: "grid-import",
|
|
89929
|
-
name: "
|
|
89978
|
+
name: "Import",
|
|
89930
89979
|
role: DeviceRole.PowerSensor,
|
|
89931
89980
|
watts: point("power", "Grid.Import.Power.Total.Calculated", {
|
|
89932
89981
|
key: "Power.Grid.Export",
|
|
@@ -89943,7 +89992,7 @@ var ENPAL_SPECS = [
|
|
|
89943
89992
|
{
|
|
89944
89993
|
kind: "power-meter",
|
|
89945
89994
|
id: "grid-export",
|
|
89946
|
-
name: "
|
|
89995
|
+
name: "Export",
|
|
89947
89996
|
role: DeviceRole.PowerSensor,
|
|
89948
89997
|
watts: point("power", {
|
|
89949
89998
|
key: "Power.Grid.Export",
|
|
@@ -89960,7 +90009,7 @@ var ENPAL_SPECS = [
|
|
|
89960
90009
|
{
|
|
89961
90010
|
kind: "power-meter",
|
|
89962
90011
|
id: "house",
|
|
89963
|
-
name: "
|
|
90012
|
+
name: "Consumption",
|
|
89964
90013
|
role: DeviceRole.PowerSensor,
|
|
89965
90014
|
watts: point("power", "Power.Consumption.Total", "Power.Consumption.AC"),
|
|
89966
90015
|
kwh: point("energy", "Energy.Consumption.Total.Lifetime")
|
|
@@ -89968,7 +90017,7 @@ var ENPAL_SPECS = [
|
|
|
89968
90017
|
{
|
|
89969
90018
|
kind: "power-meter",
|
|
89970
90019
|
id: "heatpump",
|
|
89971
|
-
name: "
|
|
90020
|
+
name: "Consumption",
|
|
89972
90021
|
role: DeviceRole.PowerSensor,
|
|
89973
90022
|
watts: point("power", "Heatpump.Power.Consumption.Total"),
|
|
89974
90023
|
kwh: point("energy", "Heatpump.Energy.Consumption.Total.Lifetime")
|
|
@@ -89976,7 +90025,7 @@ var ENPAL_SPECS = [
|
|
|
89976
90025
|
{
|
|
89977
90026
|
kind: "numeric",
|
|
89978
90027
|
id: "heatpump-hot-water",
|
|
89979
|
-
name: "
|
|
90028
|
+
name: "Hot water temperature",
|
|
89980
90029
|
role: DeviceRole.TemperatureSensor,
|
|
89981
90030
|
quantity: "temperature",
|
|
89982
90031
|
value: point("temperature", "Heatpump.DomesticHotWater.Temperature")
|
|
@@ -89984,7 +90033,7 @@ var ENPAL_SPECS = [
|
|
|
89984
90033
|
{
|
|
89985
90034
|
kind: "numeric",
|
|
89986
90035
|
id: "heatpump-outside",
|
|
89987
|
-
name: "
|
|
90036
|
+
name: "Outside temperature",
|
|
89988
90037
|
role: DeviceRole.TemperatureSensor,
|
|
89989
90038
|
quantity: "temperature",
|
|
89990
90039
|
value: point("temperature", "Heatpump.Outside.Temperature")
|
|
@@ -90000,7 +90049,7 @@ var ENPAL_SPECS = [
|
|
|
90000
90049
|
{
|
|
90001
90050
|
kind: "numeric",
|
|
90002
90051
|
id: "battery-temperature",
|
|
90003
|
-
name: "
|
|
90052
|
+
name: "Temperature",
|
|
90004
90053
|
role: DeviceRole.TemperatureSensor,
|
|
90005
90054
|
quantity: "temperature",
|
|
90006
90055
|
value: point("temperature", "Temperature.Battery")
|
|
@@ -90008,7 +90057,7 @@ var ENPAL_SPECS = [
|
|
|
90008
90057
|
{
|
|
90009
90058
|
kind: "numeric",
|
|
90010
90059
|
id: "battery-health",
|
|
90011
|
-
name: "
|
|
90060
|
+
name: "Health",
|
|
90012
90061
|
role: DeviceRole.NumericSensor,
|
|
90013
90062
|
quantity: "percentage",
|
|
90014
90063
|
value: point("percentage", "Battery.SOH")
|
|
@@ -90165,36 +90214,528 @@ function computeSlices(spec, snapshot, options, now) {
|
|
|
90165
90214
|
}
|
|
90166
90215
|
}
|
|
90167
90216
|
//#endregion
|
|
90168
|
-
//#region src/enpal-
|
|
90169
|
-
/**
|
|
90170
|
-
|
|
90171
|
-
|
|
90217
|
+
//#region src/enpal-counter-guard.ts
|
|
90218
|
+
/**
|
|
90219
|
+
* Smaller than the history deadband of the lifetime counters (0.01 kWh): float noise from
|
|
90220
|
+
* rounding or a unit conversion, never a real drop.
|
|
90221
|
+
*/
|
|
90222
|
+
var NOISE = .01;
|
|
90223
|
+
/**
|
|
90224
|
+
* Keeps a lifetime counter monotonic before it reaches a `counter` history series, where a
|
|
90225
|
+
* drop means "the meter was reset" and re-bases every later total.
|
|
90226
|
+
*
|
|
90227
|
+
* The box occasionally reports 0 or a stale lower figure for one poll (a restart, a partly
|
|
90228
|
+
* written window). One such reading is held back: the previous value is published instead.
|
|
90229
|
+
* A lower reading that is then followed by a value at or above it is a genuine reset or a
|
|
90230
|
+
* replaced meter, and is accepted.
|
|
90231
|
+
*/
|
|
90232
|
+
var LifetimeCounterGuard = class {
|
|
90233
|
+
#published = null;
|
|
90234
|
+
#suspect = null;
|
|
90235
|
+
/** The value to publish for the reading `next`. */
|
|
90236
|
+
accept(next) {
|
|
90237
|
+
const published = this.#published;
|
|
90238
|
+
if (published === null || next >= published - NOISE) {
|
|
90239
|
+
this.#published = next;
|
|
90240
|
+
this.#suspect = null;
|
|
90241
|
+
return next;
|
|
90242
|
+
}
|
|
90243
|
+
const suspect = this.#suspect;
|
|
90244
|
+
if (suspect !== null && next >= suspect - NOISE) {
|
|
90245
|
+
this.#published = next;
|
|
90246
|
+
this.#suspect = null;
|
|
90247
|
+
return next;
|
|
90248
|
+
}
|
|
90249
|
+
this.#suspect = next;
|
|
90250
|
+
return published;
|
|
90251
|
+
}
|
|
90252
|
+
};
|
|
90253
|
+
//#endregion
|
|
90254
|
+
//#region src/enpal-resolver.ts
|
|
90255
|
+
/**
|
|
90256
|
+
* Per-integration registry that the adopted devices use to reach the live
|
|
90257
|
+
* {@link EnpalIntegrationManager}. Keyed on the integration id; the client registry publishes
|
|
90258
|
+
* a new manager here whenever the connection changes, and the sensors read through the
|
|
90259
|
+
* resolver so they follow it.
|
|
90260
|
+
*/
|
|
90261
|
+
var EnpalConnectionResolver = class {
|
|
90262
|
+
#managers = /* @__PURE__ */ new Map();
|
|
90263
|
+
#subs = /* @__PURE__ */ new Map();
|
|
90264
|
+
set(integrationId, manager) {
|
|
90265
|
+
if (manager === null) {
|
|
90266
|
+
this.#managers.delete(integrationId);
|
|
90267
|
+
return;
|
|
90268
|
+
}
|
|
90269
|
+
this.#managers.set(integrationId, manager);
|
|
90270
|
+
}
|
|
90271
|
+
getManager(integrationId) {
|
|
90272
|
+
return this.#managers.get(integrationId) ?? null;
|
|
90273
|
+
}
|
|
90274
|
+
/** The latest snapshot, or null when the box is unreachable (never stale readings). */
|
|
90275
|
+
getSnapshot(integrationId) {
|
|
90276
|
+
return this.#managers.get(integrationId)?.getSnapshot() ?? null;
|
|
90277
|
+
}
|
|
90278
|
+
/** Subscribe to poll ticks (successful or not). Returns an unsubscribe function. */
|
|
90279
|
+
onSnapshot(integrationId, cb) {
|
|
90280
|
+
let set = this.#subs.get(integrationId);
|
|
90281
|
+
if (set === void 0) {
|
|
90282
|
+
set = /* @__PURE__ */ new Set();
|
|
90283
|
+
this.#subs.set(integrationId, set);
|
|
90284
|
+
}
|
|
90285
|
+
set.add(cb);
|
|
90286
|
+
return () => {
|
|
90287
|
+
const current = this.#subs.get(integrationId);
|
|
90288
|
+
current?.delete(cb);
|
|
90289
|
+
if (current !== void 0 && current.size === 0) this.#subs.delete(integrationId);
|
|
90290
|
+
};
|
|
90291
|
+
}
|
|
90292
|
+
emitSnapshot(integrationId) {
|
|
90293
|
+
const set = this.#subs.get(integrationId);
|
|
90294
|
+
if (set === void 0) return;
|
|
90295
|
+
for (const cb of set) try {
|
|
90296
|
+
cb();
|
|
90297
|
+
} catch {}
|
|
90298
|
+
}
|
|
90299
|
+
clear() {
|
|
90300
|
+
this.#managers.clear();
|
|
90301
|
+
this.#subs.clear();
|
|
90302
|
+
}
|
|
90303
|
+
};
|
|
90304
|
+
var enpalConnections = new EnpalConnectionResolver();
|
|
90305
|
+
//#endregion
|
|
90306
|
+
//#region src/devices/enpal-sensor-device.ts
|
|
90307
|
+
var POWER_CAP = powerMeterCapability.name;
|
|
90308
|
+
var BATTERY_CAP = batteryCapability.name;
|
|
90309
|
+
var METER_CAP = meterCapability.name;
|
|
90310
|
+
var NUMERIC_CAP = numericSensorCapability.name;
|
|
90311
|
+
var POWER_COLD = { lastFetchedAt: 0 };
|
|
90312
|
+
var BATTERY_COLD = {
|
|
90313
|
+
percentage: null,
|
|
90314
|
+
charging: "unknown",
|
|
90315
|
+
sleeping: false,
|
|
90316
|
+
lastUpdated: 0
|
|
90317
|
+
};
|
|
90318
|
+
var VALUE_COLD = {
|
|
90319
|
+
value: null,
|
|
90320
|
+
lastFetchedAt: 0
|
|
90321
|
+
};
|
|
90322
|
+
/**
|
|
90323
|
+
* Persisted config of an Enpal Sensor. `specId` selects the entry of the catalog;
|
|
90324
|
+
* `integrationId` selects the integration whose box feeds it.
|
|
90325
|
+
*/
|
|
90326
|
+
var enpalSensorSchema = object({
|
|
90327
|
+
specId: string().min(1).describe("Device catalog entry id"),
|
|
90328
|
+
integrationId: string().min(1).describe("Owning integration"),
|
|
90329
|
+
system: literal(ENPAL_SYSTEM).optional()
|
|
90330
|
+
});
|
|
90331
|
+
/**
|
|
90332
|
+
* One Enpal data point group (production, state of charge, grid import, ...) as a
|
|
90333
|
+
* {@link DeviceType.Sensor} child of an equipment Container. The catalog entry in `specId`
|
|
90334
|
+
* says which caps it registers and which box keys feed them; the slices are recomputed from
|
|
90335
|
+
* the integration's snapshot on every poll tick.
|
|
90336
|
+
*
|
|
90337
|
+
* An unreachable box marks the device offline and leaves the slices alone: a failed read is
|
|
90338
|
+
* a gap, never a zero (D393). A lifetime counter passes through {@link LifetimeCounterGuard}
|
|
90339
|
+
* so one stale low reading cannot become a history reset.
|
|
90340
|
+
*/
|
|
90341
|
+
var EnpalSensorDevice = class extends BaseDevice {
|
|
90342
|
+
features = [];
|
|
90343
|
+
spec;
|
|
90344
|
+
integrationId;
|
|
90345
|
+
energyGuard = new LifetimeCounterGuard();
|
|
90346
|
+
meterGuard = new LifetimeCounterGuard();
|
|
90347
|
+
snapshotUnsub = null;
|
|
90348
|
+
constructor(ctx) {
|
|
90349
|
+
const cfg = enpalSensorSchema.parse(ctx.persistedConfig ?? {});
|
|
90350
|
+
super(ctx, enpalSensorSchema, { type: ctx.deviceMeta.type });
|
|
90351
|
+
const spec = findSpec(cfg.specId);
|
|
90352
|
+
if (spec === void 0) throw new Error(`Enpal: unknown device kind "${cfg.specId}"`);
|
|
90353
|
+
this.spec = spec;
|
|
90354
|
+
this.integrationId = cfg.integrationId;
|
|
90355
|
+
this.online = enpalConnections.getSnapshot(this.integrationId) !== null;
|
|
90356
|
+
this.updateSourceInfo({
|
|
90357
|
+
id: `${this.integrationId}:${spec.id}`,
|
|
90358
|
+
system: ENPAL_SYSTEM,
|
|
90359
|
+
...spec.kind === "meter" || spec.kind === "numeric" ? { quantity: spec.quantity } : {}
|
|
90360
|
+
});
|
|
90361
|
+
}
|
|
90362
|
+
async onActivate() {
|
|
90363
|
+
await super.onActivate();
|
|
90364
|
+
this.registerCaps();
|
|
90365
|
+
this.snapshotUnsub = enpalConnections.onSnapshot(this.integrationId, () => {
|
|
90366
|
+
try {
|
|
90367
|
+
this.recompute();
|
|
90368
|
+
} catch (err) {
|
|
90369
|
+
this.ctx.logger.warn("enpal sensor: snapshot handler failed", {
|
|
90370
|
+
tags: { deviceId: this.id },
|
|
90371
|
+
meta: {
|
|
90372
|
+
spec: this.spec.id,
|
|
90373
|
+
error: errMsg(err)
|
|
90374
|
+
}
|
|
90375
|
+
});
|
|
90376
|
+
}
|
|
90377
|
+
});
|
|
90378
|
+
this.recompute();
|
|
90379
|
+
}
|
|
90380
|
+
async removeDevice() {
|
|
90381
|
+
this.snapshotUnsub?.();
|
|
90382
|
+
this.snapshotUnsub = null;
|
|
90383
|
+
await super.removeDevice();
|
|
90384
|
+
}
|
|
90385
|
+
recompute() {
|
|
90386
|
+
const snapshot = enpalConnections.getSnapshot(this.integrationId);
|
|
90387
|
+
if (snapshot === null) {
|
|
90388
|
+
if (this.online) this.markOnline(false);
|
|
90389
|
+
return;
|
|
90390
|
+
}
|
|
90391
|
+
if (!this.online) this.markOnline(true);
|
|
90392
|
+
const invert = enpalConnections.getManager(this.integrationId)?.connection.invertBatteryPower ?? false;
|
|
90393
|
+
const slices = computeSlices(this.spec, snapshot, { invertBatteryPower: invert }, snapshot.fetchedAt);
|
|
90394
|
+
this.publish(slices);
|
|
90395
|
+
for (const line of slices.refusals) this.ctx.logger.warn(`enpal sensor: reading not used, ${line}`, {
|
|
90396
|
+
tags: { deviceId: this.id },
|
|
90397
|
+
meta: { spec: this.spec.id }
|
|
90398
|
+
});
|
|
90399
|
+
}
|
|
90400
|
+
publish(slices) {
|
|
90401
|
+
if (slices.powerMeter !== void 0) {
|
|
90402
|
+
const kwh = slices.powerMeter.kwhTotal;
|
|
90403
|
+
this.runtimeState.setCapState(POWER_CAP, kwh === void 0 ? slices.powerMeter : {
|
|
90404
|
+
...slices.powerMeter,
|
|
90405
|
+
kwhTotal: this.energyGuard.accept(kwh)
|
|
90406
|
+
});
|
|
90407
|
+
}
|
|
90408
|
+
if (slices.battery !== void 0) this.runtimeState.setCapState(BATTERY_CAP, slices.battery);
|
|
90409
|
+
if (slices.meter !== void 0) {
|
|
90410
|
+
const raw = slices.meter.value;
|
|
90411
|
+
this.runtimeState.setCapState(METER_CAP, {
|
|
90412
|
+
...slices.meter,
|
|
90413
|
+
value: raw === null ? null : this.meterGuard.accept(raw)
|
|
90414
|
+
});
|
|
90415
|
+
}
|
|
90416
|
+
if (slices.numeric !== void 0) this.runtimeState.setCapState(NUMERIC_CAP, slices.numeric);
|
|
90417
|
+
}
|
|
90418
|
+
registerCaps() {
|
|
90419
|
+
switch (this.spec.kind) {
|
|
90420
|
+
case "power-meter":
|
|
90421
|
+
this.registerPowerMeter();
|
|
90422
|
+
return;
|
|
90423
|
+
case "battery":
|
|
90424
|
+
this.registerBattery();
|
|
90425
|
+
this.registerPowerMeter();
|
|
90426
|
+
return;
|
|
90427
|
+
case "meter":
|
|
90428
|
+
this.ctx.registerNativeCap(meterCapability, { getStatus: async () => this.runtimeState.getCapState(METER_CAP) ?? VALUE_COLD });
|
|
90429
|
+
this.runtimeState.setCapState(METER_CAP, VALUE_COLD);
|
|
90430
|
+
return;
|
|
90431
|
+
case "numeric":
|
|
90432
|
+
this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(NUMERIC_CAP) ?? VALUE_COLD });
|
|
90433
|
+
this.runtimeState.setCapState(NUMERIC_CAP, VALUE_COLD);
|
|
90434
|
+
return;
|
|
90435
|
+
}
|
|
90436
|
+
}
|
|
90437
|
+
registerPowerMeter() {
|
|
90438
|
+
this.ctx.registerNativeCap(powerMeterCapability, { getStatus: async () => this.runtimeState.getCapState(POWER_CAP) ?? POWER_COLD });
|
|
90439
|
+
this.runtimeState.setCapState(POWER_CAP, POWER_COLD);
|
|
90440
|
+
}
|
|
90441
|
+
registerBattery() {
|
|
90442
|
+
this.ctx.registerNativeCap(batteryCapability, {
|
|
90443
|
+
getStatus: async () => this.runtimeState.getCapState(BATTERY_CAP) ?? BATTERY_COLD,
|
|
90444
|
+
wakeForStream: async () => ({
|
|
90445
|
+
awoke: false,
|
|
90446
|
+
durationMs: 0
|
|
90447
|
+
})
|
|
90448
|
+
});
|
|
90449
|
+
this.runtimeState.setCapState(BATTERY_CAP, BATTERY_COLD);
|
|
90450
|
+
}
|
|
90451
|
+
};
|
|
90452
|
+
//#endregion
|
|
90453
|
+
//#region src/devices/enpal-sensor-factory.ts
|
|
90454
|
+
/** Constructor closure the kernel calls for every Sensor child of an equipment Container. */
|
|
90455
|
+
function enpalSensorFactory(ctx) {
|
|
90456
|
+
return new EnpalSensorDevice(ctx);
|
|
90172
90457
|
}
|
|
90173
|
-
|
|
90174
|
-
|
|
90175
|
-
|
|
90176
|
-
|
|
90177
|
-
|
|
90178
|
-
|
|
90458
|
+
//#endregion
|
|
90459
|
+
//#region src/enpal-equipment.ts
|
|
90460
|
+
/** Order is the order the candidates are listed in. Every catalog entry belongs to one. */
|
|
90461
|
+
var ENPAL_EQUIPMENT = [
|
|
90462
|
+
{
|
|
90463
|
+
id: "pv",
|
|
90464
|
+
name: "Photovoltaic",
|
|
90465
|
+
specIds: ["pv", "inverter-temperature"]
|
|
90466
|
+
},
|
|
90467
|
+
{
|
|
90468
|
+
id: "battery",
|
|
90469
|
+
name: "Battery",
|
|
90470
|
+
specIds: [
|
|
90471
|
+
"battery",
|
|
90472
|
+
"battery-charged",
|
|
90473
|
+
"battery-discharged",
|
|
90474
|
+
"battery-temperature",
|
|
90475
|
+
"battery-health"
|
|
90476
|
+
]
|
|
90477
|
+
},
|
|
90478
|
+
{
|
|
90479
|
+
id: "grid",
|
|
90480
|
+
name: "Grid",
|
|
90481
|
+
specIds: ["grid-import", "grid-export"]
|
|
90482
|
+
},
|
|
90483
|
+
{
|
|
90484
|
+
id: "house",
|
|
90485
|
+
name: "House",
|
|
90486
|
+
specIds: ["house"]
|
|
90487
|
+
},
|
|
90488
|
+
{
|
|
90489
|
+
id: "heatpump",
|
|
90490
|
+
name: "Heat pump",
|
|
90491
|
+
specIds: [
|
|
90492
|
+
"heatpump",
|
|
90493
|
+
"heatpump-hot-water",
|
|
90494
|
+
"heatpump-outside"
|
|
90495
|
+
]
|
|
90496
|
+
}
|
|
90497
|
+
];
|
|
90498
|
+
function findEquipment(id) {
|
|
90499
|
+
return ENPAL_EQUIPMENT.find((e) => e.id === id);
|
|
90179
90500
|
}
|
|
90180
|
-
/**
|
|
90181
|
-
function
|
|
90182
|
-
return
|
|
90183
|
-
|
|
90184
|
-
|
|
90185
|
-
|
|
90186
|
-
|
|
90187
|
-
|
|
90188
|
-
|
|
90189
|
-
|
|
90190
|
-
|
|
90191
|
-
|
|
90501
|
+
/** The `childNativeId` of a child candidate: namespaced by its container. */
|
|
90502
|
+
function childNativeId(equipmentId, specId) {
|
|
90503
|
+
return `${equipmentId}#${specId}`;
|
|
90504
|
+
}
|
|
90505
|
+
/**
|
|
90506
|
+
* Persisted config of an equipment Container. `specIds` are the children it was adopted with:
|
|
90507
|
+
* they stay, whatever the box reports later, so a restart with the box off keeps them.
|
|
90508
|
+
*/
|
|
90509
|
+
var enpalEquipmentSchema = object({
|
|
90510
|
+
equipment: string().min(1).describe("Equipment id (pv, battery, grid, house, heatpump)"),
|
|
90511
|
+
specIds: array(string().min(1)).describe("Catalog entries adopted as children"),
|
|
90512
|
+
integrationId: string().min(1).describe("Owning integration"),
|
|
90513
|
+
system: literal(ENPAL_SYSTEM).optional()
|
|
90514
|
+
});
|
|
90515
|
+
/** The Sensor children the kernel spawns under an equipment Container. */
|
|
90516
|
+
function accessoryChildSpecs(config) {
|
|
90517
|
+
const out = [];
|
|
90518
|
+
for (const specId of config.specIds) {
|
|
90519
|
+
const spec = findSpec(specId);
|
|
90520
|
+
if (spec === void 0) continue;
|
|
90521
|
+
out.push({
|
|
90522
|
+
stableIdSuffix: spec.id,
|
|
90523
|
+
meta: {
|
|
90524
|
+
type: DeviceType.Sensor,
|
|
90525
|
+
name: spec.name,
|
|
90526
|
+
role: spec.role,
|
|
90527
|
+
integrationId: config.integrationId
|
|
90528
|
+
},
|
|
90529
|
+
config: {
|
|
90530
|
+
specId: spec.id,
|
|
90531
|
+
integrationId: config.integrationId,
|
|
90532
|
+
system: ENPAL_SYSTEM
|
|
90192
90533
|
},
|
|
90193
|
-
|
|
90194
|
-
|
|
90195
|
-
|
|
90534
|
+
factory: enpalSensorFactory
|
|
90535
|
+
});
|
|
90536
|
+
}
|
|
90537
|
+
return out;
|
|
90538
|
+
}
|
|
90539
|
+
//#endregion
|
|
90540
|
+
//#region src/devices/enpal-equipment-device.ts
|
|
90541
|
+
/**
|
|
90542
|
+
* One physical piece of the installation (photovoltaic, battery, grid, house, heat pump) as a
|
|
90543
|
+
* {@link DeviceType.Container}. It owns no caps: it declares the Sensor children it was
|
|
90544
|
+
* adopted with, and the kernel spawns and restores them. Online follows the integration's
|
|
90545
|
+
* box; the integration marks it offline when a poll fails.
|
|
90546
|
+
*/
|
|
90547
|
+
var EnpalEquipmentDevice = class extends BaseDevice {
|
|
90548
|
+
features = [];
|
|
90549
|
+
equipment;
|
|
90550
|
+
specIds;
|
|
90551
|
+
integrationId;
|
|
90552
|
+
constructor(ctx) {
|
|
90553
|
+
const cfg = enpalEquipmentSchema.parse(ctx.persistedConfig ?? {});
|
|
90554
|
+
super(ctx, enpalEquipmentSchema, { type: DeviceType.Container });
|
|
90555
|
+
this.equipment = cfg.equipment;
|
|
90556
|
+
this.specIds = cfg.specIds;
|
|
90557
|
+
this.integrationId = cfg.integrationId;
|
|
90558
|
+
this.online = enpalConnections.getSnapshot(this.integrationId) !== null;
|
|
90559
|
+
this.updateSourceInfo({
|
|
90560
|
+
id: `${this.integrationId}:${this.equipment}`,
|
|
90561
|
+
system: ENPAL_SYSTEM
|
|
90562
|
+
});
|
|
90563
|
+
}
|
|
90564
|
+
getAccessoryChildren() {
|
|
90565
|
+
return accessoryChildSpecs({
|
|
90566
|
+
equipment: this.equipment,
|
|
90567
|
+
specIds: this.specIds,
|
|
90568
|
+
integrationId: this.integrationId
|
|
90569
|
+
});
|
|
90570
|
+
}
|
|
90571
|
+
};
|
|
90572
|
+
//#endregion
|
|
90573
|
+
//#region src/enpal-discovery.ts
|
|
90574
|
+
var METADATA = {
|
|
90575
|
+
manufacturer: "Enpal",
|
|
90576
|
+
integration: "enpal"
|
|
90577
|
+
};
|
|
90578
|
+
/**
|
|
90579
|
+
* One Container candidate per equipment the box reported at least one value for, with the
|
|
90580
|
+
* Sensor children it reported. Pure.
|
|
90581
|
+
*/
|
|
90582
|
+
function buildEnpalCandidates(input) {
|
|
90583
|
+
const { snapshot } = input;
|
|
90584
|
+
if (snapshot === null) return [];
|
|
90585
|
+
const reported = new Map(availableSpecs(snapshot).map((spec) => [spec.id, spec]));
|
|
90586
|
+
const out = [];
|
|
90587
|
+
for (const equipment of ENPAL_EQUIPMENT) {
|
|
90588
|
+
const specs = equipment.specIds.flatMap((id) => reported.get(id) ?? []);
|
|
90589
|
+
if (specs.length === 0) continue;
|
|
90590
|
+
const adoptedId = input.adopted.get(equipment.id) ?? null;
|
|
90591
|
+
out.push({
|
|
90592
|
+
childNativeId: equipment.id,
|
|
90593
|
+
name: equipment.name,
|
|
90594
|
+
type: DeviceType.Container,
|
|
90595
|
+
status: "online",
|
|
90596
|
+
metadata: METADATA,
|
|
90597
|
+
alreadyAdopted: adoptedId !== null,
|
|
90598
|
+
adoptedDeviceId: adoptedId,
|
|
90599
|
+
capabilities: [],
|
|
90600
|
+
children: specs.map((spec) => ({
|
|
90601
|
+
childNativeId: childNativeId(equipment.id, spec.id),
|
|
90602
|
+
name: spec.name,
|
|
90603
|
+
type: DeviceType.Sensor,
|
|
90604
|
+
status: "online",
|
|
90605
|
+
metadata: METADATA,
|
|
90606
|
+
alreadyAdopted: false,
|
|
90607
|
+
adoptedDeviceId: null,
|
|
90608
|
+
capabilities: capsOfSpec(spec)
|
|
90609
|
+
}))
|
|
90610
|
+
});
|
|
90611
|
+
}
|
|
90612
|
+
return out;
|
|
90613
|
+
}
|
|
90614
|
+
//#endregion
|
|
90615
|
+
//#region src/enpal-adoption-provider.ts
|
|
90616
|
+
var DEVICES_FILTER = {
|
|
90617
|
+
id: "devices",
|
|
90618
|
+
label: "Devices",
|
|
90619
|
+
isDefault: true
|
|
90620
|
+
};
|
|
90621
|
+
function buildEnpalAdoptionProvider(deps) {
|
|
90622
|
+
const { getSnapshot, hasIntegration, listIntegrations, listAdopted, logger } = deps;
|
|
90623
|
+
async function adoptedMap(integrationId) {
|
|
90624
|
+
const map = /* @__PURE__ */ new Map();
|
|
90625
|
+
for (const device of await listAdopted()) {
|
|
90626
|
+
const cfg = device.config;
|
|
90627
|
+
if (cfg["system"] !== "enpal" || cfg["integrationId"] !== integrationId) continue;
|
|
90628
|
+
const equipment = cfg["equipment"];
|
|
90629
|
+
if (typeof equipment === "string") map.set(equipment, device.id);
|
|
90630
|
+
}
|
|
90631
|
+
return map;
|
|
90632
|
+
}
|
|
90633
|
+
async function candidates(integrationId) {
|
|
90634
|
+
return buildEnpalCandidates({
|
|
90635
|
+
snapshot: getSnapshot(integrationId),
|
|
90636
|
+
adopted: await adoptedMap(integrationId)
|
|
90637
|
+
});
|
|
90638
|
+
}
|
|
90639
|
+
async function status(integrationId) {
|
|
90640
|
+
return {
|
|
90641
|
+
lastDiscoveryAt: Date.now(),
|
|
90642
|
+
candidateCount: (await candidates(integrationId)).length,
|
|
90643
|
+
adoptedCount: (await adoptedMap(integrationId)).size,
|
|
90644
|
+
lastError: null
|
|
90196
90645
|
};
|
|
90197
|
-
}
|
|
90646
|
+
}
|
|
90647
|
+
return {
|
|
90648
|
+
listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
|
|
90649
|
+
listCandidates: async ({ integrationId, page, pageSize }) => {
|
|
90650
|
+
const all = await candidates(integrationId);
|
|
90651
|
+
const start = (page - 1) * pageSize;
|
|
90652
|
+
return {
|
|
90653
|
+
candidates: all.slice(start, start + pageSize),
|
|
90654
|
+
totalCount: all.length,
|
|
90655
|
+
page,
|
|
90656
|
+
pageSize
|
|
90657
|
+
};
|
|
90658
|
+
},
|
|
90659
|
+
getCandidate: async ({ integrationId, childNativeId }) => {
|
|
90660
|
+
return (await candidates(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
|
|
90661
|
+
},
|
|
90662
|
+
getStatus: async () => {
|
|
90663
|
+
try {
|
|
90664
|
+
let candidateCount = 0;
|
|
90665
|
+
let adoptedCount = 0;
|
|
90666
|
+
for (const integrationId of listIntegrations()) {
|
|
90667
|
+
const s = await status(integrationId);
|
|
90668
|
+
candidateCount += s.candidateCount;
|
|
90669
|
+
adoptedCount += s.adoptedCount;
|
|
90670
|
+
}
|
|
90671
|
+
return {
|
|
90672
|
+
lastDiscoveryAt: Date.now(),
|
|
90673
|
+
candidateCount,
|
|
90674
|
+
adoptedCount,
|
|
90675
|
+
lastError: null
|
|
90676
|
+
};
|
|
90677
|
+
} catch (err) {
|
|
90678
|
+
return {
|
|
90679
|
+
lastDiscoveryAt: null,
|
|
90680
|
+
candidateCount: 0,
|
|
90681
|
+
adoptedCount: 0,
|
|
90682
|
+
lastError: errMsg(err)
|
|
90683
|
+
};
|
|
90684
|
+
}
|
|
90685
|
+
},
|
|
90686
|
+
refresh: async ({ integrationId }) => status(integrationId),
|
|
90687
|
+
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
90688
|
+
if (!hasIntegration(integrationId)) throw new Error(`enpal adopt: integration ${integrationId} not found`);
|
|
90689
|
+
const offered = await candidates(integrationId);
|
|
90690
|
+
const adopted = [];
|
|
90691
|
+
let failures = 0;
|
|
90692
|
+
for (const equipmentId of childNativeIds) try {
|
|
90693
|
+
const candidate = offered.find((c) => c.childNativeId === equipmentId);
|
|
90694
|
+
const equipment = findEquipment(equipmentId);
|
|
90695
|
+
if (candidate === void 0 || equipment === void 0) {
|
|
90696
|
+
logger.warn("enpal adopt: the box reports nothing for this equipment — skipping", { meta: {
|
|
90697
|
+
equipment: equipmentId,
|
|
90698
|
+
integrationId
|
|
90699
|
+
} });
|
|
90700
|
+
failures++;
|
|
90701
|
+
continue;
|
|
90702
|
+
}
|
|
90703
|
+
const reported = new Set((candidate.children ?? []).map((c) => c.childNativeId));
|
|
90704
|
+
const specIds = equipment.specIds.filter((id) => reported.has(`${equipment.id}#${id}`));
|
|
90705
|
+
const { deviceId, accessoryDeviceIds } = await deps.adoptDevice({
|
|
90706
|
+
equipment: equipment.id,
|
|
90707
|
+
specIds,
|
|
90708
|
+
integrationId,
|
|
90709
|
+
name: perCandidate?.[equipmentId]?.name ?? equipment.name
|
|
90710
|
+
});
|
|
90711
|
+
adopted.push({
|
|
90712
|
+
childNativeId: equipmentId,
|
|
90713
|
+
parentDeviceId: deviceId,
|
|
90714
|
+
accessoryDeviceIds
|
|
90715
|
+
});
|
|
90716
|
+
} catch (err) {
|
|
90717
|
+
logger.warn("enpal adopt: failed to adopt equipment", { meta: {
|
|
90718
|
+
equipment: equipmentId,
|
|
90719
|
+
integrationId,
|
|
90720
|
+
error: errMsg(err)
|
|
90721
|
+
} });
|
|
90722
|
+
failures++;
|
|
90723
|
+
}
|
|
90724
|
+
if (adopted.length === 0 && failures > 0) throw new Error(`enpal adopt: all ${failures} adopt(s) failed`);
|
|
90725
|
+
return { adopted };
|
|
90726
|
+
},
|
|
90727
|
+
release: async ({ camDeviceId }) => {
|
|
90728
|
+
await deps.removeDevice(camDeviceId);
|
|
90729
|
+
},
|
|
90730
|
+
resync: async ({ camDeviceId }) => {
|
|
90731
|
+
const cfg = await deps.findDeviceConfig(camDeviceId);
|
|
90732
|
+
if (cfg === null || cfg["system"] !== "enpal") throw new Error(`enpal resync: device ${camDeviceId} is not an Enpal device`);
|
|
90733
|
+
return {
|
|
90734
|
+
changed: false,
|
|
90735
|
+
rebuiltChildren: 0
|
|
90736
|
+
};
|
|
90737
|
+
}
|
|
90738
|
+
};
|
|
90198
90739
|
}
|
|
90199
90740
|
//#endregion
|
|
90200
90741
|
//#region src/enpal-device-page.ts
|
|
@@ -90550,505 +91091,337 @@ var EnpalIntegrationManager = class {
|
|
|
90550
91091
|
}
|
|
90551
91092
|
};
|
|
90552
91093
|
//#endregion
|
|
90553
|
-
//#region src/enpal-
|
|
91094
|
+
//#region src/enpal-client-registry.ts
|
|
90554
91095
|
/**
|
|
90555
|
-
*
|
|
90556
|
-
* {@link
|
|
90557
|
-
*
|
|
90558
|
-
* the resolver so they follow it.
|
|
91096
|
+
* Owns one poller per Enpal integration (`Map<integrationId, manager>`) and publishes each on
|
|
91097
|
+
* {@link enpalConnections} so the devices follow it. The caller reconciles it against the
|
|
91098
|
+
* live integration list at boot and on every integration lifecycle event.
|
|
90559
91099
|
*/
|
|
90560
|
-
var
|
|
90561
|
-
#
|
|
90562
|
-
#
|
|
90563
|
-
|
|
90564
|
-
|
|
90565
|
-
|
|
90566
|
-
|
|
91100
|
+
var EnpalClientRegistry = class {
|
|
91101
|
+
#logger;
|
|
91102
|
+
#deps;
|
|
91103
|
+
#entries = /* @__PURE__ */ new Map();
|
|
91104
|
+
constructor(logger, deps) {
|
|
91105
|
+
this.#logger = logger;
|
|
91106
|
+
this.#deps = deps;
|
|
91107
|
+
}
|
|
91108
|
+
/**
|
|
91109
|
+
* Ensure a poller runs for `integrationId` with this connection. An unchanged connection
|
|
91110
|
+
* keeps the running poller; a changed one replaces it. Never throws: a box that is off is
|
|
91111
|
+
* an `error` status, polled again later.
|
|
91112
|
+
*/
|
|
91113
|
+
async upsert(integrationId, name, connection, tokenLocked) {
|
|
91114
|
+
const existing = this.#entries.get(integrationId);
|
|
91115
|
+
if (existing !== void 0) {
|
|
91116
|
+
if (existing.tokenLocked === tokenLocked && sameConnection(existing.connection, connection)) return;
|
|
91117
|
+
await this.remove(integrationId);
|
|
91118
|
+
}
|
|
91119
|
+
const manager = new EnpalIntegrationManager({
|
|
91120
|
+
id: integrationId,
|
|
91121
|
+
name,
|
|
91122
|
+
connection,
|
|
91123
|
+
tokenLocked,
|
|
91124
|
+
logger: this.#logger.withTags({ integrationId }),
|
|
91125
|
+
fetch: this.#deps.fetch,
|
|
91126
|
+
now: this.#deps.now,
|
|
91127
|
+
onConnected: () => {
|
|
91128
|
+
this.#deps.onConnected(integrationId);
|
|
91129
|
+
},
|
|
91130
|
+
onDisconnected: () => {
|
|
91131
|
+
this.#deps.onDisconnected(integrationId);
|
|
91132
|
+
enpalConnections.emitSnapshot(integrationId);
|
|
91133
|
+
},
|
|
91134
|
+
onPolled: () => {
|
|
91135
|
+
enpalConnections.emitSnapshot(integrationId);
|
|
91136
|
+
}
|
|
91137
|
+
});
|
|
91138
|
+
this.#entries.set(integrationId, {
|
|
91139
|
+
manager,
|
|
91140
|
+
connection,
|
|
91141
|
+
tokenLocked
|
|
91142
|
+
});
|
|
91143
|
+
enpalConnections.set(integrationId, manager);
|
|
91144
|
+
await manager.start();
|
|
91145
|
+
if (manager.status === "error") {
|
|
91146
|
+
this.#deps.onDisconnected(integrationId);
|
|
91147
|
+
enpalConnections.emitSnapshot(integrationId);
|
|
90567
91148
|
}
|
|
90568
|
-
this.#managers.set(boxId, manager);
|
|
90569
91149
|
}
|
|
90570
|
-
|
|
90571
|
-
|
|
91150
|
+
async remove(integrationId) {
|
|
91151
|
+
const entry = this.#entries.get(integrationId);
|
|
91152
|
+
if (entry === void 0) return;
|
|
91153
|
+
this.#entries.delete(integrationId);
|
|
91154
|
+
enpalConnections.set(integrationId, null);
|
|
91155
|
+
await entry.manager.stop();
|
|
90572
91156
|
}
|
|
90573
|
-
/**
|
|
90574
|
-
|
|
90575
|
-
|
|
91157
|
+
/** Stop every poller whose integration is not in `keep`. */
|
|
91158
|
+
async retain(keep) {
|
|
91159
|
+
for (const id of [...this.#entries.keys()]) if (!keep.has(id)) await this.remove(id);
|
|
90576
91160
|
}
|
|
90577
|
-
|
|
90578
|
-
|
|
90579
|
-
let set = this.#subs.get(boxId);
|
|
90580
|
-
if (set === void 0) {
|
|
90581
|
-
set = /* @__PURE__ */ new Set();
|
|
90582
|
-
this.#subs.set(boxId, set);
|
|
90583
|
-
}
|
|
90584
|
-
set.add(cb);
|
|
90585
|
-
return () => {
|
|
90586
|
-
const current = this.#subs.get(boxId);
|
|
90587
|
-
current?.delete(cb);
|
|
90588
|
-
if (current !== void 0 && current.size === 0) this.#subs.delete(boxId);
|
|
90589
|
-
};
|
|
91161
|
+
async shutdown() {
|
|
91162
|
+
for (const id of [...this.#entries.keys()]) await this.remove(id);
|
|
90590
91163
|
}
|
|
90591
|
-
|
|
90592
|
-
|
|
90593
|
-
if (set === void 0) return;
|
|
90594
|
-
for (const cb of set) try {
|
|
90595
|
-
cb();
|
|
90596
|
-
} catch {}
|
|
91164
|
+
has(integrationId) {
|
|
91165
|
+
return this.#entries.has(integrationId);
|
|
90597
91166
|
}
|
|
90598
|
-
|
|
90599
|
-
this.#
|
|
90600
|
-
this.#subs.clear();
|
|
91167
|
+
list() {
|
|
91168
|
+
return [...this.#entries.keys()];
|
|
90601
91169
|
}
|
|
90602
91170
|
};
|
|
90603
|
-
var enpalBoxes = new EnpalBoxResolver();
|
|
90604
91171
|
//#endregion
|
|
90605
|
-
//#region src/enpal-
|
|
90606
|
-
/**
|
|
90607
|
-
*
|
|
90608
|
-
*
|
|
90609
|
-
*/
|
|
90610
|
-
|
|
90611
|
-
|
|
90612
|
-
|
|
90613
|
-
|
|
90614
|
-
|
|
90615
|
-
|
|
90616
|
-
|
|
90617
|
-
|
|
90618
|
-
|
|
90619
|
-
|
|
90620
|
-
|
|
90621
|
-
|
|
90622
|
-
|
|
90623
|
-
|
|
90624
|
-
|
|
90625
|
-
|
|
90626
|
-
|
|
90627
|
-
this.#published = next;
|
|
90628
|
-
this.#suspect = null;
|
|
90629
|
-
return next;
|
|
90630
|
-
}
|
|
90631
|
-
const suspect = this.#suspect;
|
|
90632
|
-
if (suspect !== null && next >= suspect - NOISE) {
|
|
90633
|
-
this.#published = next;
|
|
90634
|
-
this.#suspect = null;
|
|
90635
|
-
return next;
|
|
90636
|
-
}
|
|
90637
|
-
this.#suspect = next;
|
|
90638
|
-
return published;
|
|
90639
|
-
}
|
|
90640
|
-
};
|
|
90641
|
-
//#endregion
|
|
90642
|
-
//#region src/devices/enpal-sensor-device.ts
|
|
90643
|
-
var POWER_CAP = powerMeterCapability.name;
|
|
90644
|
-
var BATTERY_CAP = batteryCapability.name;
|
|
90645
|
-
var METER_CAP = meterCapability.name;
|
|
90646
|
-
var NUMERIC_CAP = numericSensorCapability.name;
|
|
90647
|
-
var POWER_COLD = { lastFetchedAt: 0 };
|
|
90648
|
-
var BATTERY_COLD = {
|
|
90649
|
-
percentage: null,
|
|
90650
|
-
charging: "unknown",
|
|
90651
|
-
sleeping: false,
|
|
90652
|
-
lastUpdated: 0
|
|
90653
|
-
};
|
|
90654
|
-
var VALUE_COLD = {
|
|
90655
|
-
value: null,
|
|
90656
|
-
lastFetchedAt: 0
|
|
90657
|
-
};
|
|
90658
|
-
/**
|
|
90659
|
-
* One Enpal data group (PV, battery, grid import, ...) as a {@link DeviceType.Sensor}
|
|
90660
|
-
* child of the box. The catalog entry in `specId` says which caps it registers and which
|
|
90661
|
-
* box keys feed them; the slices are recomputed from the box snapshot on every poll tick.
|
|
90662
|
-
*
|
|
90663
|
-
* An unreachable box marks the device offline and leaves the slices alone: a failed read is
|
|
90664
|
-
* a gap, never a zero (D393). A lifetime counter passes through {@link LifetimeCounterGuard}
|
|
90665
|
-
* so one stale low reading cannot become a history reset.
|
|
90666
|
-
*/
|
|
90667
|
-
var EnpalSensorDevice = class extends BaseDevice {
|
|
90668
|
-
features = [];
|
|
90669
|
-
spec;
|
|
90670
|
-
boxId;
|
|
90671
|
-
energyGuard = new LifetimeCounterGuard();
|
|
90672
|
-
meterGuard = new LifetimeCounterGuard();
|
|
90673
|
-
snapshotUnsub = null;
|
|
90674
|
-
constructor(ctx) {
|
|
90675
|
-
const cfg = enpalSensorSchema.parse(ctx.persistedConfig ?? {});
|
|
90676
|
-
super(ctx, enpalSensorSchema, { type: ctx.deviceMeta.type });
|
|
90677
|
-
const spec = findSpec(cfg.specId);
|
|
90678
|
-
if (spec === void 0) throw new Error(`Enpal: unknown device kind "${cfg.specId}"`);
|
|
90679
|
-
this.spec = spec;
|
|
90680
|
-
this.boxId = cfg.boxId;
|
|
90681
|
-
this.online = enpalBoxes.getSnapshot(this.boxId) !== null;
|
|
90682
|
-
this.updateSourceInfo({
|
|
90683
|
-
id: `${this.boxId}:${spec.id}`,
|
|
90684
|
-
system: "enpal",
|
|
90685
|
-
...spec.kind === "meter" || spec.kind === "numeric" ? { quantity: spec.quantity } : {}
|
|
90686
|
-
});
|
|
90687
|
-
}
|
|
90688
|
-
async onActivate() {
|
|
90689
|
-
await super.onActivate();
|
|
90690
|
-
this.registerCaps();
|
|
90691
|
-
this.snapshotUnsub = enpalBoxes.onSnapshot(this.boxId, () => {
|
|
90692
|
-
try {
|
|
90693
|
-
this.recompute();
|
|
90694
|
-
} catch (err) {
|
|
90695
|
-
this.ctx.logger.warn("enpal sensor: snapshot handler failed", {
|
|
90696
|
-
tags: { deviceId: this.id },
|
|
90697
|
-
meta: {
|
|
90698
|
-
spec: this.spec.id,
|
|
90699
|
-
error: errMsg(err)
|
|
90700
|
-
}
|
|
90701
|
-
});
|
|
90702
|
-
}
|
|
90703
|
-
});
|
|
90704
|
-
this.recompute();
|
|
90705
|
-
}
|
|
90706
|
-
async removeDevice() {
|
|
90707
|
-
this.snapshotUnsub?.();
|
|
90708
|
-
this.snapshotUnsub = null;
|
|
90709
|
-
await super.removeDevice();
|
|
90710
|
-
}
|
|
90711
|
-
recompute() {
|
|
90712
|
-
const snapshot = enpalBoxes.getSnapshot(this.boxId);
|
|
90713
|
-
if (snapshot === null) {
|
|
90714
|
-
if (this.online) this.markOnline(false);
|
|
90715
|
-
return;
|
|
90716
|
-
}
|
|
90717
|
-
if (!this.online) this.markOnline(true);
|
|
90718
|
-
const invert = enpalBoxes.getManager(this.boxId)?.connection.invertBatteryPower ?? false;
|
|
90719
|
-
const slices = computeSlices(this.spec, snapshot, { invertBatteryPower: invert }, snapshot.fetchedAt);
|
|
90720
|
-
this.publish(slices);
|
|
90721
|
-
for (const line of slices.refusals) this.ctx.logger.warn(`enpal sensor: reading not used, ${line}`, {
|
|
90722
|
-
tags: { deviceId: this.id },
|
|
90723
|
-
meta: { spec: this.spec.id }
|
|
90724
|
-
});
|
|
90725
|
-
}
|
|
90726
|
-
publish(slices) {
|
|
90727
|
-
if (slices.powerMeter !== void 0) {
|
|
90728
|
-
const kwh = slices.powerMeter.kwhTotal;
|
|
90729
|
-
this.runtimeState.setCapState(POWER_CAP, kwh === void 0 ? slices.powerMeter : {
|
|
90730
|
-
...slices.powerMeter,
|
|
90731
|
-
kwhTotal: this.energyGuard.accept(kwh)
|
|
90732
|
-
});
|
|
90733
|
-
}
|
|
90734
|
-
if (slices.battery !== void 0) this.runtimeState.setCapState(BATTERY_CAP, slices.battery);
|
|
90735
|
-
if (slices.meter !== void 0) {
|
|
90736
|
-
const raw = slices.meter.value;
|
|
90737
|
-
this.runtimeState.setCapState(METER_CAP, {
|
|
90738
|
-
...slices.meter,
|
|
90739
|
-
value: raw === null ? null : this.meterGuard.accept(raw)
|
|
91172
|
+
//#region src/enpal-integration-device-cascade.ts
|
|
91173
|
+
/**
|
|
91174
|
+
* Remove every equipment Container of an integration that was deleted or disabled; the
|
|
91175
|
+
* kernel takes the Sensor children with it. Best-effort per device; returns the count removed.
|
|
91176
|
+
*/
|
|
91177
|
+
async function cascadeRemoveDevicesForIntegration(input) {
|
|
91178
|
+
const { reg, devices, addonId, integrationId, logger } = input;
|
|
91179
|
+
let removed = 0;
|
|
91180
|
+
for (const d of reg.getAllForAddon(addonId)) {
|
|
91181
|
+
if (d.parentDeviceId !== null) continue;
|
|
91182
|
+
const cfg = await devices.loadConfig(d.id).catch(() => ({}));
|
|
91183
|
+
if (cfg["system"] !== "enpal" || cfg["integrationId"] !== integrationId) continue;
|
|
91184
|
+
try {
|
|
91185
|
+
await devices.remove(d.id);
|
|
91186
|
+
removed += 1;
|
|
91187
|
+
} catch (err) {
|
|
91188
|
+
logger.warn("enpal: integration cascade-remove failed", {
|
|
91189
|
+
tags: { deviceId: d.id },
|
|
91190
|
+
meta: {
|
|
91191
|
+
integrationId,
|
|
91192
|
+
error: errMsg(err)
|
|
91193
|
+
}
|
|
90740
91194
|
});
|
|
90741
91195
|
}
|
|
90742
|
-
if (slices.numeric !== void 0) this.runtimeState.setCapState(NUMERIC_CAP, slices.numeric);
|
|
90743
|
-
}
|
|
90744
|
-
registerCaps() {
|
|
90745
|
-
switch (this.spec.kind) {
|
|
90746
|
-
case "power-meter":
|
|
90747
|
-
this.registerPowerMeter();
|
|
90748
|
-
return;
|
|
90749
|
-
case "battery":
|
|
90750
|
-
this.registerBattery();
|
|
90751
|
-
this.registerPowerMeter();
|
|
90752
|
-
return;
|
|
90753
|
-
case "meter":
|
|
90754
|
-
this.ctx.registerNativeCap(meterCapability, { getStatus: async () => this.runtimeState.getCapState(METER_CAP) ?? VALUE_COLD });
|
|
90755
|
-
this.runtimeState.setCapState(METER_CAP, VALUE_COLD);
|
|
90756
|
-
return;
|
|
90757
|
-
case "numeric":
|
|
90758
|
-
this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(NUMERIC_CAP) ?? VALUE_COLD });
|
|
90759
|
-
this.runtimeState.setCapState(NUMERIC_CAP, VALUE_COLD);
|
|
90760
|
-
return;
|
|
90761
|
-
}
|
|
90762
91196
|
}
|
|
90763
|
-
|
|
90764
|
-
|
|
90765
|
-
|
|
90766
|
-
|
|
90767
|
-
|
|
90768
|
-
|
|
90769
|
-
|
|
90770
|
-
|
|
90771
|
-
|
|
90772
|
-
|
|
90773
|
-
|
|
90774
|
-
|
|
90775
|
-
|
|
91197
|
+
return removed;
|
|
91198
|
+
}
|
|
91199
|
+
//#endregion
|
|
91200
|
+
//#region src/enpal-integration-offline.ts
|
|
91201
|
+
/**
|
|
91202
|
+
* Set every Enpal device of `integrationId` (containers and their sensors) to `online`.
|
|
91203
|
+
* Churn-free: a device already in that state is left alone. Returns how many changed.
|
|
91204
|
+
*/
|
|
91205
|
+
function setIntegrationDevicesOnline(devices, integrationId, online) {
|
|
91206
|
+
let count = 0;
|
|
91207
|
+
for (const dev of devices) {
|
|
91208
|
+
if (dev.config.get("system") !== "enpal") continue;
|
|
91209
|
+
if (dev.config.get("integrationId") !== integrationId) continue;
|
|
91210
|
+
if (dev.online === online) continue;
|
|
91211
|
+
dev.markOnline(online);
|
|
91212
|
+
count += 1;
|
|
90776
91213
|
}
|
|
90777
|
-
|
|
91214
|
+
return count;
|
|
91215
|
+
}
|
|
90778
91216
|
//#endregion
|
|
90779
|
-
//#region src/
|
|
91217
|
+
//#region src/addon.ts
|
|
90780
91218
|
/**
|
|
90781
|
-
*
|
|
90782
|
-
* token
|
|
90783
|
-
*
|
|
90784
|
-
*
|
|
91219
|
+
* Enpal solar box provider, `mode: account` and broker-less. An integration is one box: its
|
|
91220
|
+
* host, InfluxDB access and token live on the integration's settings, and the addon polls it
|
|
91221
|
+
* with one manager per `integrationId`. The box is not a device: discovery offers one
|
|
91222
|
+
* {@link DeviceType.Container} per piece of equipment it reports (photovoltaic, battery,
|
|
91223
|
+
* grid, house, heat pump), and adopting a container adopts its Sensor children.
|
|
91224
|
+
*
|
|
91225
|
+
* The box does not advertise itself on the network, so the integration is added by hand.
|
|
90785
91226
|
*/
|
|
90786
|
-
var
|
|
90787
|
-
|
|
90788
|
-
|
|
90789
|
-
|
|
90790
|
-
|
|
90791
|
-
|
|
90792
|
-
super(
|
|
90793
|
-
this.online = false;
|
|
90794
|
-
this.registerDeviceDiscovery();
|
|
90795
|
-
this.updateSourceInfo({
|
|
90796
|
-
id: this.stableId,
|
|
90797
|
-
system: "enpal"
|
|
90798
|
-
});
|
|
91227
|
+
var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
91228
|
+
addonId = "provider-enpal";
|
|
91229
|
+
providerName = "Enpal";
|
|
91230
|
+
deviceClasses = { [DeviceType.Container]: EnpalEquipmentDevice };
|
|
91231
|
+
clients = null;
|
|
91232
|
+
constructor() {
|
|
91233
|
+
super({});
|
|
90799
91234
|
}
|
|
90800
|
-
async
|
|
90801
|
-
await super.
|
|
90802
|
-
this.
|
|
90803
|
-
this.
|
|
90804
|
-
|
|
90805
|
-
meta: { error: errMsg(err) }
|
|
90806
|
-
});
|
|
91235
|
+
async onInitialize() {
|
|
91236
|
+
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
91237
|
+
this.clients = new EnpalClientRegistry(this.ctx.logger, {
|
|
91238
|
+
onConnected: (integrationId) => this.setIntegrationDevicesOnline(integrationId, true),
|
|
91239
|
+
onDisconnected: (integrationId) => this.setIntegrationDevicesOnline(integrationId, false)
|
|
90807
91240
|
});
|
|
90808
|
-
this.
|
|
90809
|
-
|
|
90810
|
-
|
|
90811
|
-
|
|
91241
|
+
await this.reconcileIntegrations();
|
|
91242
|
+
this.subscribeIntegrationLifecycle();
|
|
91243
|
+
return [...regs, {
|
|
91244
|
+
capability: deviceAdoptionCapability,
|
|
91245
|
+
provider: this.buildAdoptionProvider()
|
|
91246
|
+
}];
|
|
90812
91247
|
}
|
|
90813
|
-
async
|
|
90814
|
-
|
|
90815
|
-
|
|
90816
|
-
|
|
90817
|
-
|
|
91248
|
+
async onShutdown() {
|
|
91249
|
+
try {
|
|
91250
|
+
await this.clients?.shutdown();
|
|
91251
|
+
} catch (err) {
|
|
91252
|
+
this.ctx.logger.warn("Enpal shutdown error", { meta: { error: errMsg(err) } });
|
|
91253
|
+
}
|
|
91254
|
+
this.clients = null;
|
|
91255
|
+
enpalConnections.clear();
|
|
91256
|
+
await super.onShutdown();
|
|
90818
91257
|
}
|
|
90819
|
-
|
|
90820
|
-
if (!("
|
|
90821
|
-
|
|
90822
|
-
connection: patch["connection"],
|
|
90823
|
-
system: "enpal-box"
|
|
90824
|
-
});
|
|
90825
|
-
await this.config.setAll({ connection: parsed.connection });
|
|
90826
|
-
await this.stopManager();
|
|
90827
|
-
await this.ensureManager();
|
|
91258
|
+
requireClients() {
|
|
91259
|
+
if (!this.clients) throw new Error("Enpal provider not initialised");
|
|
91260
|
+
return this.clients;
|
|
90828
91261
|
}
|
|
90829
|
-
|
|
90830
|
-
|
|
90831
|
-
|
|
90832
|
-
|
|
90833
|
-
if (
|
|
91262
|
+
/** One container per equipment per integration. */
|
|
91263
|
+
generateStableId(_type, config) {
|
|
91264
|
+
const integrationId = config?.["integrationId"];
|
|
91265
|
+
const equipment = config?.["equipment"];
|
|
91266
|
+
if (typeof integrationId !== "string" || integrationId.length === 0) throw new Error("enpal generateStableId: integrationId required");
|
|
91267
|
+
if (typeof equipment !== "string" || equipment.length === 0) throw new Error("enpal generateStableId: equipment required");
|
|
91268
|
+
return `enpal:${integrationId}:${equipment}`;
|
|
90834
91269
|
}
|
|
90835
|
-
|
|
90836
|
-
|
|
90837
|
-
|
|
90838
|
-
|
|
90839
|
-
|
|
90840
|
-
|
|
90841
|
-
|
|
90842
|
-
|
|
90843
|
-
|
|
90844
|
-
|
|
90845
|
-
|
|
90846
|
-
this.
|
|
90847
|
-
|
|
90848
|
-
|
|
90849
|
-
|
|
90850
|
-
|
|
90851
|
-
|
|
91270
|
+
/**
|
|
91271
|
+
* Rebuild the pollers from the live integration list: upsert one per enabled integration
|
|
91272
|
+
* with a host, drop the ones that vanished and cascade-remove their devices. Idempotent;
|
|
91273
|
+
* runs at boot and on every integration lifecycle event.
|
|
91274
|
+
*/
|
|
91275
|
+
async reconcileIntegrations() {
|
|
91276
|
+
const reg = this.requireClients();
|
|
91277
|
+
try {
|
|
91278
|
+
const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
|
|
91279
|
+
const surviving = /* @__PURE__ */ new Set();
|
|
91280
|
+
for (const integration of mine) try {
|
|
91281
|
+
const read = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
|
|
91282
|
+
if (read === null) {
|
|
91283
|
+
this.ctx.logger.warn("Enpal integration has no box host — skipping", { meta: { integrationId: integration.id } });
|
|
91284
|
+
continue;
|
|
91285
|
+
}
|
|
91286
|
+
await reg.upsert(integration.id, integration.name, read.connection, read.tokenLocked);
|
|
91287
|
+
surviving.add(integration.id);
|
|
91288
|
+
} catch (err) {
|
|
91289
|
+
this.ctx.logger.warn("Enpal reconcile failed for integration", { meta: {
|
|
91290
|
+
integrationId: integration.id,
|
|
91291
|
+
error: errMsg(err)
|
|
91292
|
+
} });
|
|
90852
91293
|
}
|
|
90853
|
-
|
|
90854
|
-
|
|
90855
|
-
|
|
90856
|
-
|
|
90857
|
-
|
|
90858
|
-
this.setOnline(false);
|
|
90859
|
-
enpalBoxes.emitSnapshot(this.stableId);
|
|
91294
|
+
const vanished = reg.list().filter((id) => !surviving.has(id));
|
|
91295
|
+
await reg.retain(surviving);
|
|
91296
|
+
for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
|
|
91297
|
+
} catch (err) {
|
|
91298
|
+
this.ctx.logger.warn("Enpal integration reconcile failed", { meta: { error: errMsg(err) } });
|
|
90860
91299
|
}
|
|
90861
91300
|
}
|
|
90862
|
-
|
|
90863
|
-
|
|
90864
|
-
|
|
90865
|
-
|
|
91301
|
+
subscribeIntegrationLifecycle() {
|
|
91302
|
+
const handler = (event) => {
|
|
91303
|
+
const addonId = event.data["addonId"];
|
|
91304
|
+
if (typeof addonId === "string" && addonId !== this.ctx.id) return;
|
|
91305
|
+
this.reconcileIntegrations();
|
|
91306
|
+
};
|
|
91307
|
+
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
|
|
91308
|
+
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
|
|
91309
|
+
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
|
|
90866
91310
|
}
|
|
90867
|
-
|
|
90868
|
-
this.ctx.
|
|
90869
|
-
|
|
90870
|
-
|
|
90871
|
-
|
|
91311
|
+
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
91312
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
91313
|
+
const devices = this.ctx.kernel.devices;
|
|
91314
|
+
if (!reg || !devices) return;
|
|
91315
|
+
await cascadeRemoveDevicesForIntegration({
|
|
91316
|
+
reg,
|
|
91317
|
+
devices,
|
|
91318
|
+
addonId: this.addonId,
|
|
91319
|
+
integrationId,
|
|
91320
|
+
logger: this.ctx.logger
|
|
91321
|
+
});
|
|
91322
|
+
}
|
|
91323
|
+
buildAdoptionProvider() {
|
|
91324
|
+
return buildEnpalAdoptionProvider({
|
|
91325
|
+
getSnapshot: (integrationId) => enpalConnections.getSnapshot(integrationId),
|
|
91326
|
+
hasIntegration: (integrationId) => this.requireClients().has(integrationId),
|
|
91327
|
+
listIntegrations: () => this.requireClients().list(),
|
|
91328
|
+
listAdopted: async () => {
|
|
91329
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
91330
|
+
const devices = this.ctx.kernel.devices;
|
|
91331
|
+
if (!reg || !devices) return [];
|
|
91332
|
+
const out = [];
|
|
91333
|
+
for (const d of reg.getAllForAddon(this.addonId)) {
|
|
91334
|
+
if (d.parentDeviceId !== null) continue;
|
|
91335
|
+
const config = await devices.loadConfig(d.id).catch(() => ({}));
|
|
91336
|
+
out.push({
|
|
91337
|
+
id: d.id,
|
|
91338
|
+
config
|
|
91339
|
+
});
|
|
91340
|
+
}
|
|
91341
|
+
return out;
|
|
90872
91342
|
},
|
|
90873
|
-
|
|
90874
|
-
|
|
90875
|
-
|
|
91343
|
+
adoptDevice: async ({ equipment, specIds, integrationId, name }) => {
|
|
91344
|
+
const devices = this.ctx.kernel.devices;
|
|
91345
|
+
if (!devices) throw new Error("enpal adopt: kernel.devices unavailable");
|
|
91346
|
+
const config = {
|
|
91347
|
+
equipment,
|
|
91348
|
+
specIds,
|
|
91349
|
+
integrationId,
|
|
91350
|
+
system: ENPAL_SYSTEM
|
|
91351
|
+
};
|
|
91352
|
+
const stableId = this.generateStableId(DeviceType.Container, config);
|
|
91353
|
+
const device = await devices.create(stableId, EnpalEquipmentDevice, config, null, {
|
|
91354
|
+
type: DeviceType.Container,
|
|
91355
|
+
name,
|
|
91356
|
+
integrationId
|
|
91357
|
+
});
|
|
91358
|
+
const children = await devices.getChildren(device.id);
|
|
91359
|
+
return {
|
|
91360
|
+
deviceId: device.id,
|
|
91361
|
+
accessoryDeviceIds: children.map((c) => c.id)
|
|
91362
|
+
};
|
|
90876
91363
|
},
|
|
90877
|
-
|
|
90878
|
-
|
|
90879
|
-
await this.refreshDiscovery();
|
|
90880
|
-
return this.runtimeState.getCapState("device-discovery")?.discovered ?? [];
|
|
91364
|
+
removeDevice: async (id) => {
|
|
91365
|
+
await this.ctx.kernel.devices?.remove(id);
|
|
90881
91366
|
},
|
|
90882
|
-
|
|
90883
|
-
|
|
90884
|
-
|
|
91367
|
+
findDeviceConfig: async (id) => {
|
|
91368
|
+
const devices = this.ctx.kernel.devices;
|
|
91369
|
+
if (!devices) return null;
|
|
91370
|
+
return devices.loadConfig(id).catch(() => null);
|
|
90885
91371
|
},
|
|
90886
|
-
|
|
90887
|
-
if (deviceId !== this.id) return;
|
|
90888
|
-
await this.release(childDeviceId);
|
|
90889
|
-
}
|
|
90890
|
-
});
|
|
90891
|
-
this.setCapSlice(deviceDiscoveryCapability, {
|
|
90892
|
-
discovered: [],
|
|
90893
|
-
lastDiscoveryAt: null,
|
|
90894
|
-
lastError: null,
|
|
90895
|
-
lastFetchedAt: 0
|
|
91372
|
+
logger: this.ctx.logger
|
|
90896
91373
|
});
|
|
90897
91374
|
}
|
|
90898
|
-
|
|
90899
|
-
|
|
90900
|
-
|
|
90901
|
-
let discovered = [];
|
|
90902
|
-
if (snapshot === null) lastError = enpalBoxes.getManager(this.stableId)?.lastError?.message ?? null;
|
|
90903
|
-
else try {
|
|
90904
|
-
discovered = buildCandidates({
|
|
90905
|
-
boxId: this.stableId,
|
|
90906
|
-
boxName: this.name,
|
|
90907
|
-
specs: availableSpecs(snapshot),
|
|
90908
|
-
adoptedDeviceIdBySpec: await this.adoptedBySpec(),
|
|
90909
|
-
online: true
|
|
90910
|
-
});
|
|
90911
|
-
} catch (err) {
|
|
90912
|
-
lastError = errMsg(err);
|
|
90913
|
-
this.ctx.logger.warn("Enpal box: discovery refresh failed", {
|
|
90914
|
-
tags: { deviceId: this.id },
|
|
90915
|
-
meta: { error: lastError }
|
|
90916
|
-
});
|
|
90917
|
-
}
|
|
90918
|
-
this.runtimeState.setCapState(deviceDiscoveryCapability.name, {
|
|
90919
|
-
discovered,
|
|
90920
|
-
lastDiscoveryAt: Date.now(),
|
|
90921
|
-
lastError,
|
|
90922
|
-
lastFetchedAt: Date.now()
|
|
90923
|
-
});
|
|
90924
|
-
}
|
|
90925
|
-
async adoptedBySpec() {
|
|
90926
|
-
const out = {};
|
|
90927
|
-
const children = await this.ctx.devices.getChildren(this.id).catch(() => []);
|
|
90928
|
-
for (const child of children) {
|
|
90929
|
-
const specId = (await this.ctx.devices.loadConfig(child.id).catch(() => ({})))["specId"];
|
|
90930
|
-
if (typeof specId === "string") out[specId] = child.id;
|
|
90931
|
-
}
|
|
90932
|
-
return out;
|
|
90933
|
-
}
|
|
90934
|
-
async adopt(childNativeId, nameOverride) {
|
|
90935
|
-
const specId = parseSpecNativeId(this.stableId, childNativeId);
|
|
90936
|
-
const spec = specId === null ? void 0 : findSpec(specId);
|
|
90937
|
-
if (spec === void 0) throw new Error(`adoptDevice: ${childNativeId} is not an Enpal device of this box`);
|
|
90938
|
-
const existingId = (await this.adoptedBySpec())[spec.id];
|
|
90939
|
-
if (typeof existingId === "number") {
|
|
90940
|
-
const existing = (await this.ctx.devices.getAll()).find((d) => d.id === existingId);
|
|
90941
|
-
if (existing) return {
|
|
90942
|
-
deviceId: existing.id,
|
|
90943
|
-
stableId: existing.stableId
|
|
90944
|
-
};
|
|
90945
|
-
}
|
|
90946
|
-
const name = nameOverride ?? `${this.name} ${spec.name}`;
|
|
90947
|
-
const stableId = specNativeId(this.stableId, spec.id);
|
|
90948
|
-
const created = await this.ctx.devices.create(stableId, EnpalSensorDevice, {
|
|
90949
|
-
specId: spec.id,
|
|
90950
|
-
boxId: this.stableId,
|
|
90951
|
-
system: "enpal",
|
|
90952
|
-
name
|
|
90953
|
-
}, this.id, {
|
|
90954
|
-
type: DeviceType.Sensor,
|
|
90955
|
-
name,
|
|
90956
|
-
role: spec.role
|
|
90957
|
-
});
|
|
90958
|
-
this.ctx.logger.info("Enpal box: adopted device", {
|
|
90959
|
-
tags: { deviceId: created.id },
|
|
90960
|
-
meta: {
|
|
90961
|
-
spec: spec.id,
|
|
90962
|
-
stableId
|
|
90963
|
-
}
|
|
90964
|
-
});
|
|
90965
|
-
await this.refreshDiscovery().catch(() => void 0);
|
|
90966
|
-
return {
|
|
90967
|
-
deviceId: created.id,
|
|
90968
|
-
stableId
|
|
90969
|
-
};
|
|
90970
|
-
}
|
|
90971
|
-
async release(childDeviceId) {
|
|
90972
|
-
const child = (await this.ctx.devices.getAll()).find((d) => d.id === childDeviceId);
|
|
90973
|
-
if (!child || child.parentDeviceId !== this.id) throw new Error(`releaseDevice: ${childDeviceId} is not a child of box ${this.id}`);
|
|
90974
|
-
await this.ctx.devices.remove(childDeviceId);
|
|
90975
|
-
await this.refreshDiscovery().catch(() => void 0);
|
|
90976
|
-
}
|
|
90977
|
-
};
|
|
90978
|
-
//#endregion
|
|
90979
|
-
//#region src/addon.ts
|
|
90980
|
-
/**
|
|
90981
|
-
* Enpal solar box provider, `mode: standalone` (the VeSync / Ecowitt pattern, no broker).
|
|
90982
|
-
* A box is added as a {@link DeviceType.Hub} through the manual-creation form: its host,
|
|
90983
|
-
* InfluxDB access and token live on the device. The box registers `device-discovery` and
|
|
90984
|
-
* adopts the data groups it reports as Sensor devices. The box does not advertise itself on
|
|
90985
|
-
* the network, so there is no auto-discovery of the box itself.
|
|
90986
|
-
*/
|
|
90987
|
-
var EnpalProviderAddon = class extends BaseDeviceProvider {
|
|
90988
|
-
addonId = "provider-enpal";
|
|
90989
|
-
providerName = "Enpal";
|
|
90990
|
-
deviceClasses = {
|
|
90991
|
-
[DeviceType.Hub]: EnpalBoxDevice,
|
|
90992
|
-
[DeviceType.Sensor]: EnpalSensorDevice
|
|
90993
|
-
};
|
|
90994
|
-
constructor() {
|
|
90995
|
-
super({});
|
|
90996
|
-
}
|
|
90997
|
-
async supportsDiscovery() {
|
|
90998
|
-
return false;
|
|
91375
|
+
/** The integration's settings form; the submitted values are stored per integration. */
|
|
91376
|
+
globalSettingsSchema() {
|
|
91377
|
+
return buildConnectionFormSchema();
|
|
90999
91378
|
}
|
|
91000
91379
|
async supportsManualCreation() {
|
|
91001
|
-
return
|
|
91380
|
+
return false;
|
|
91002
91381
|
}
|
|
91003
|
-
async onGetCreationSchema(
|
|
91004
|
-
return
|
|
91382
|
+
async onGetCreationSchema(_type) {
|
|
91383
|
+
return null;
|
|
91005
91384
|
}
|
|
91006
|
-
async onCreateDevice(
|
|
91007
|
-
|
|
91008
|
-
const name = typeof config["name"] === "string" ? config["name"].trim() : "";
|
|
91009
|
-
if (name.length === 0) throw userError("enpal.name-required", void 0, "Box name is required");
|
|
91010
|
-
const parsed = enpalConnectionSchema.safeParse(config["connection"] ?? config);
|
|
91011
|
-
if (!parsed.success || parsed.data.host.length === 0) throw userError("enpal.host-required", void 0, "The box host or IP is required");
|
|
91012
|
-
return {
|
|
91013
|
-
meta: {
|
|
91014
|
-
type: DeviceType.Hub,
|
|
91015
|
-
name
|
|
91016
|
-
},
|
|
91017
|
-
config: {
|
|
91018
|
-
connection: parsed.data,
|
|
91019
|
-
system: "enpal-box"
|
|
91020
|
-
}
|
|
91021
|
-
};
|
|
91385
|
+
async onCreateDevice(_type, _config) {
|
|
91386
|
+
throw new Error("Enpal devices are adopted from the integration");
|
|
91022
91387
|
}
|
|
91023
|
-
|
|
91024
|
-
|
|
91025
|
-
|
|
91026
|
-
|
|
91027
|
-
|
|
91388
|
+
setIntegrationDevicesOnline(integrationId, online) {
|
|
91389
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
91390
|
+
if (!reg) return;
|
|
91391
|
+
const n = setIntegrationDevicesOnline(reg.getAllForAddon(this.addonId), integrationId, online);
|
|
91392
|
+
if (n > 0) this.ctx.logger.info("Enpal integration devices " + (online ? "online" : "offline"), { meta: {
|
|
91393
|
+
integrationId,
|
|
91394
|
+
count: n
|
|
91395
|
+
} });
|
|
91028
91396
|
}
|
|
91029
91397
|
};
|
|
91030
91398
|
//#endregion
|
|
91399
|
+
exports.ENPAL_EQUIPMENT = ENPAL_EQUIPMENT;
|
|
91031
91400
|
exports.ENPAL_SPECS = ENPAL_SPECS;
|
|
91032
|
-
exports.
|
|
91033
|
-
exports.
|
|
91401
|
+
exports.ENPAL_SYSTEM = ENPAL_SYSTEM;
|
|
91402
|
+
exports.EnpalClientRegistry = EnpalClientRegistry;
|
|
91403
|
+
exports.EnpalConnectionResolver = EnpalConnectionResolver;
|
|
91404
|
+
exports.EnpalEquipmentDevice = EnpalEquipmentDevice;
|
|
91034
91405
|
exports.EnpalError = EnpalError;
|
|
91035
91406
|
exports.EnpalIntegrationManager = EnpalIntegrationManager;
|
|
91036
91407
|
exports.EnpalProviderAddon = EnpalProviderAddon;
|
|
91037
91408
|
exports.EnpalSensorDevice = EnpalSensorDevice;
|
|
91038
91409
|
exports.LifetimeCounterGuard = LifetimeCounterGuard;
|
|
91410
|
+
exports.accessoryChildSpecs = accessoryChildSpecs;
|
|
91039
91411
|
exports.availableSpecs = availableSpecs;
|
|
91040
|
-
exports.buildCandidates = buildCandidates;
|
|
91041
91412
|
exports.buildConnectionFormSchema = buildConnectionFormSchema;
|
|
91413
|
+
exports.buildEnpalAdoptionProvider = buildEnpalAdoptionProvider;
|
|
91414
|
+
exports.buildEnpalCandidates = buildEnpalCandidates;
|
|
91042
91415
|
exports.capsOfSpec = capsOfSpec;
|
|
91043
91416
|
exports.computeSlices = computeSlices;
|
|
91044
|
-
exports.
|
|
91045
|
-
exports.enpalBoxes = enpalBoxes;
|
|
91417
|
+
exports.connectionFromSettings = connectionFromSettings;
|
|
91046
91418
|
exports.enpalConnectionSchema = enpalConnectionSchema;
|
|
91419
|
+
exports.enpalConnections = enpalConnections;
|
|
91420
|
+
exports.enpalEquipmentSchema = enpalEquipmentSchema;
|
|
91047
91421
|
exports.enpalSensorSchema = enpalSensorSchema;
|
|
91422
|
+
exports.findEquipment = findEquipment;
|
|
91048
91423
|
exports.findSpec = findSpec;
|
|
91049
|
-
exports.parseSpecNativeId = parseSpecNativeId;
|
|
91050
91424
|
exports.readConnection = readConnection;
|
|
91051
91425
|
exports.readEnpalSnapshot = readEnpalSnapshot;
|
|
91426
|
+
exports.sameConnection = sameConnection;
|
|
91052
91427
|
exports.settingsToConnection = settingsToConnection;
|
|
91053
|
-
exports.slugifyHost = slugifyHost;
|
|
91054
|
-
exports.specNativeId = specNativeId;
|