@enyo-energy/energy-app-sdk 1.14.0 → 1.16.0
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 +109 -11
- package/dist/cjs/implementations/appliances/appliance-manager.cjs +2 -1
- package/dist/cjs/implementations/appliances/appliance-manager.d.cts +12 -0
- package/dist/cjs/implementations/appliances/in-memory-appliance-manager.cjs +3 -1
- package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.cjs +41 -3
- package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.d.cts +6 -3
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/packages/energy-app-mqtt.d.cts +32 -1
- package/dist/cjs/packages/energy-app-vehicle.d.cts +45 -2
- package/dist/cjs/types/enyo-appliance.d.cts +8 -0
- package/dist/cjs/types/enyo-data-bus-value.cjs +89 -5
- package/dist/cjs/types/enyo-data-bus-value.d.cts +215 -7
- package/dist/cjs/types/enyo-energy-manager-settings.cjs +62 -5
- package/dist/cjs/types/enyo-energy-manager-settings.d.cts +100 -12
- package/dist/cjs/types/enyo-mqtt.cjs +16 -1
- package/dist/cjs/types/enyo-mqtt.d.cts +49 -0
- package/dist/cjs/types/enyo-vehicle.d.cts +127 -1
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/appliances/appliance-manager.d.ts +12 -0
- package/dist/implementations/appliances/appliance-manager.js +2 -1
- package/dist/implementations/appliances/in-memory-appliance-manager.js +3 -1
- package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.d.ts +6 -3
- package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.js +42 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/packages/energy-app-mqtt.d.ts +32 -1
- package/dist/packages/energy-app-vehicle.d.ts +45 -2
- package/dist/types/enyo-appliance.d.ts +8 -0
- package/dist/types/enyo-data-bus-value.d.ts +215 -7
- package/dist/types/enyo-data-bus-value.js +88 -4
- package/dist/types/enyo-energy-manager-settings.d.ts +100 -12
- package/dist/types/enyo-energy-manager-settings.js +62 -5
- package/dist/types/enyo-mqtt.d.ts +49 -0
- package/dist/types/enyo-mqtt.js +15 -0
- package/dist/types/enyo-vehicle.d.ts +127 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -779,25 +779,102 @@ await appliances.removeById(applianceId);
|
|
|
779
779
|
|
|
780
780
|
#### `useVehicle(): EnergyAppVehicle`
|
|
781
781
|
|
|
782
|
-
Access electric
|
|
782
|
+
Access electric vehicles and the charging behaviour configured on them:
|
|
783
783
|
|
|
784
784
|
```typescript
|
|
785
785
|
const vehicles = energyApp.useVehicle();
|
|
786
786
|
|
|
787
787
|
// Get all vehicles
|
|
788
|
-
const vehicleList = await vehicles.
|
|
788
|
+
const vehicleList = await vehicles.list();
|
|
789
789
|
|
|
790
790
|
// Get vehicle details
|
|
791
|
-
const vehicle = await vehicles.
|
|
791
|
+
const vehicle = await vehicles.getById(vehicleId);
|
|
792
|
+
|
|
793
|
+
// How this car wants to be charged — the behaviour travels with the car,
|
|
794
|
+
// not with the wallbox it happens to be plugged into
|
|
795
|
+
vehicle?.defaultChargeMode; // EnyoChargeModeEnum
|
|
796
|
+
vehicle?.defaultChargeLimitPercent; // target SoC for every session
|
|
797
|
+
vehicle?.priceLimitMode; // how the ceiling is expressed, or none
|
|
798
|
+
vehicle?.priceLimitCtPerKwh; // ct/kWh ceiling, under 'ct-per-kwh'
|
|
799
|
+
vehicle?.priceLimitSharePercent; // cheapest n % of the day, under 'cheapest-share'
|
|
800
|
+
vehicle?.immediateReserveKwh; // full-power reserve before any mode
|
|
801
|
+
vehicle?.departureTimeHHmm; // daily deadline, wall-clock "07:30"
|
|
802
|
+
vehicle?.departureTimezone; // IANA zone the time is read in
|
|
803
|
+
|
|
804
|
+
// State of charge is a reading with an age, not a property of the car
|
|
805
|
+
const soc = await vehicles.getSoc(vehicleId);
|
|
806
|
+
if (soc && Date.now() - Date.parse(soc.measuredAtIso) < 60 * 60 * 1000) {
|
|
807
|
+
console.log(`${soc.socPercent} % as of ${soc.measuredAtIso}`);
|
|
808
|
+
}
|
|
809
|
+
```
|
|
792
810
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
811
|
+
`EnergyAppVehicle` is read-only — there is no `update()`. An app that can read
|
|
812
|
+
the car (ISO 15118, a manufacturer integration, OCPP meter values carrying SoC)
|
|
813
|
+
**writes a state of charge by publishing `VehicleSocUpdateV1` on the data bus**,
|
|
814
|
+
and the host makes it readable through `getSoc()`:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
energyApp.useDataBus().sendMessage([{
|
|
818
|
+
type: 'message',
|
|
819
|
+
message: 'VehicleSocUpdateV1',
|
|
820
|
+
data: {
|
|
821
|
+
vehicleId,
|
|
822
|
+
socPercent: 62,
|
|
823
|
+
// omit only when the reading came straight off the wire
|
|
824
|
+
measuredAtIso: new Date().toISOString(),
|
|
825
|
+
},
|
|
826
|
+
}]);
|
|
799
827
|
```
|
|
800
828
|
|
|
829
|
+
That message is the only write path for a state of charge; it needs the
|
|
830
|
+
`SendDataBusValues` permission. `getSoc()` resolves to `undefined` when nothing
|
|
831
|
+
is known — an ordinary answer for a car with no SoC source, not an error.
|
|
832
|
+
|
|
833
|
+
##### Charge modes
|
|
834
|
+
|
|
835
|
+
Three modes, one meaning each, identical with and without a dynamic tariff — a
|
|
836
|
+
tariff changes only what happens *inside* a mode:
|
|
837
|
+
|
|
838
|
+
| UI (German) | `EnyoChargeModeEnum` | Behaviour |
|
|
839
|
+
|---|---|---|
|
|
840
|
+
| **Sofort laden** | `Immediate` | Full power, no optimisation, no deadline. |
|
|
841
|
+
| **Nur Sonne** | `PriceLimit` | Strictly PV surplus. Never imports, has no deadline, never has to finish "in time". A price ceiling is meaningless here and is ignored. |
|
|
842
|
+
| **Sonne zuerst** | `CostOptimized` | PV first; whatever is missing by the deadline comes from the grid — in the cheapest hours with a dynamic tariff, as late as possible without one. Takes an **optional** ceiling. |
|
|
843
|
+
|
|
844
|
+
- **The ceiling belongs to `CostOptimized`**, the only mode that imports at all,
|
|
845
|
+
and it is optional there: "Preisgrenze: Aus" is a normal answer, not a missing
|
|
846
|
+
value — spelled as `priceLimitMode` being absent.
|
|
847
|
+
- **A ceiling has two spellings**, picked by `priceLimitMode`
|
|
848
|
+
(`EnyoPriceLimitModeEnum`) and mutually exclusive:
|
|
849
|
+
`ct-per-kwh` reads `priceLimitCtPerKwh` ("never above 25 ct/kWh"), while
|
|
850
|
+
`cheapest-share` reads `priceLimitSharePercent` ("only the cheapest 25 % of
|
|
851
|
+
the day"). An absolute ceiling says exactly what the user pays but may select
|
|
852
|
+
no hours at all on a uniformly expensive day; a relative one always selects
|
|
853
|
+
some hours but says nothing about what they cost. A relative ceiling ranks the
|
|
854
|
+
intervals *known* for the day, so its effective threshold moves as later
|
|
855
|
+
prices publish — re-evaluate it instead of resolving it once at session start.
|
|
856
|
+
- **`PriceLimit` without a ceiling is strict PV-only** — an instruction, not an
|
|
857
|
+
under-specified state. It exists as its own mode rather than as "`CostOptimized`
|
|
858
|
+
at 0 ct" because the 0 ct spelling breaks the moment wholesale prices go
|
|
859
|
+
negative, which is exactly when the customer would be glad to import.
|
|
860
|
+
- **An immediate reserve runs ahead of every mode.** With
|
|
861
|
+
`immediateReserveKwh > 0` on the vehicle, the session charges at full power
|
|
862
|
+
until that much energy has gone in — counted from the SoC at plug-in — and
|
|
863
|
+
only then hands over to the selected mode. That includes "Nur Sonne": the
|
|
864
|
+
driver may have to leave unexpectedly and the sun is not a guarantee.
|
|
865
|
+
|
|
866
|
+
The values reach an app already resolved, per session, on `StartChargeV1`
|
|
867
|
+
(`startSocPercent`, `targetSocPercent`, `priceLimitMode` + the ceiling it names)
|
|
868
|
+
and on `ChangeChargeModeV1` (the ceiling only — the plug-in SoC is fixed for the
|
|
869
|
+
duration of a session and a mode change must never move it). The host merges
|
|
870
|
+
session, vehicle and wallbox first, so an app never reproduces the precedence
|
|
871
|
+
rules.
|
|
872
|
+
|
|
873
|
+
The house-wide `EnergyManagerSettingEnum.DefaultChargeMode`, `PriceLimitMode`,
|
|
874
|
+
`PriceLimitCtPerKwh`, `PriceLimitSharePercent` and `CostOptimizedTarget` are
|
|
875
|
+
being retired — the default mode becomes per-wallbox, the ceiling and the
|
|
876
|
+
deadline per-vehicle — but keep honouring them until the migration has run.
|
|
877
|
+
|
|
801
878
|
#### `useCharge(): EnergyAppCharge`
|
|
802
879
|
|
|
803
880
|
Manage charging sessions:
|
|
@@ -1152,6 +1229,24 @@ await client.publish('control/pump', 'on', /* qos */ 1, /* retain */ false);
|
|
|
1152
1229
|
|
|
1153
1230
|
For external brokers use `connectToExternalBroker(brokerUrl, options)`. Requires the `Mqtt` permission.
|
|
1154
1231
|
|
|
1232
|
+
The SDK broker may be switched off until an app needs it. Ask for it before connecting:
|
|
1233
|
+
|
|
1234
|
+
```typescript
|
|
1235
|
+
const result = await mqtt.requestBrokerEnable({
|
|
1236
|
+
reason: 'Publish inverter readings to the local home automation system',
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
if (!result.enabled) {
|
|
1240
|
+
// `pending` (awaiting confirmation) or `rejected` (policy / permission) — not an error
|
|
1241
|
+
console.warn(`Local broker unavailable: ${result.status}`);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
console.log(`Broker ready at ${result.connection?.url}`);
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
The call is idempotent — an already running broker resolves with `already-enabled`.
|
|
1249
|
+
|
|
1155
1250
|
#### `useBluetooth(): EnergyAppBluetooth`
|
|
1156
1251
|
|
|
1157
1252
|
Scan for BLE peripherals and perform GATT read / write / notify against them.
|
|
@@ -1269,7 +1364,9 @@ A closed, SDK-defined set of user-facing controls the active energy manager hono
|
|
|
1269
1364
|
| `HeatingRodMode` | `pv-surplus-only` \| `boost` |
|
|
1270
1365
|
| `ChargerControl` | boolean |
|
|
1271
1366
|
| `DefaultChargeMode` | `EnyoChargeModeEnum` |
|
|
1272
|
-
| `
|
|
1367
|
+
| `PriceLimitMode` | `ct-per-kwh` \| `cheapest-share` (absent = no ceiling) |
|
|
1368
|
+
| `PriceLimitCtPerKwh` | number, **ct/kWh** (`7` = 7 ct/kWh; only under `ct-per-kwh`) |
|
|
1369
|
+
| `PriceLimitSharePercent` | integer **%** of the day (`25` = cheapest quarter; only under `cheapest-share`) |
|
|
1273
1370
|
| `CostOptimizedTarget` | wall-clock `"07:30"` + IANA timezone |
|
|
1274
1371
|
|
|
1275
1372
|
An energy manager declares what it honours, next to `registerFeatures()`:
|
|
@@ -1310,8 +1407,9 @@ Three things to get right:
|
|
|
1310
1407
|
|
|
1311
1408
|
- **Absent is not `false`.** `undefined` means unsupported or never chosen; `false` means the user switched it off. Check `supported` first, then the value — collapsing the two steers hardware someone deliberately disabled.
|
|
1312
1409
|
- **How the battery may feed a charging EV is one mode plus its parameter.** `BatteryEvDischargeMode` picks the strategy; `BatteryEvDischargeFixedWh` and `BatteryEvDischargeSocLimitPercent` are read only under their own mode. `block-discharge` (actively hold the battery back) and `unmanaged` (do not intervene either way) are **not** synonyms.
|
|
1313
|
-
- **
|
|
1410
|
+
- **Ten settings are gated by another** (`heatingRodMode` needs `heatingRodControl`, `priceLimitCtPerKwh` only applies under `priceLimitMode: 'ct-per-kwh'`, …). The tree is exported as `ENERGY_MANAGER_SETTING_DEPENDENCIES`, with two helpers over it: `getEnergyManagerSettingDependency(setting)` returns the direct gate and the value it must hold (or `null` for an ungated root), and `isEnergyManagerSettingActive(setting, values)` walks the whole chain — the gates are now two deep, so a Wh budget is live only when the mode is `fixed-wh` *and* battery control is on.
|
|
1314
1411
|
- **`priceLimitCtPerKwh` is in cents**, unlike the SDK's machine-readable price fields (`electricityPricePerKwh`, EUR/kWh). Divide by 100 when comparing. `validateEnergyManagerSettingsState()` warns on values outside a plausible ct band, which catches the mix-up.
|
|
1412
|
+
- **A price ceiling is one mode plus its parameter**, the same shape as the battery-to-EV one. `PriceLimitMode` picks how the ceiling is expressed — an absolute `ct-per-kwh` or a relative `cheapest-share` — and each value is read only under its own mode; the mode being absent means no ceiling at all. A relative ceiling ranks the price intervals *known* for the day and treats the cheapest `n` % as importable (the same rule as the `cheapest-share-of-day` automation trigger), so its effective threshold moves when tomorrow's prices publish — re-evaluate it rather than resolving it once.
|
|
1315
1413
|
|
|
1316
1414
|
#### `useElectricityTariff(): EnergyAppElectricityTariff`
|
|
1317
1415
|
|
|
@@ -211,7 +211,7 @@ class ApplianceManager {
|
|
|
211
211
|
airConditioning: appliance.airConditioning,
|
|
212
212
|
heatingRod: appliance.heatingRod,
|
|
213
213
|
smartPlug: appliance.smartPlug,
|
|
214
|
-
// Conditionally spread the
|
|
214
|
+
// Conditionally spread the optional top-level fields that are NOT
|
|
215
215
|
// covered by MERGEABLE_METADATA_KEYS. If they were always materialized
|
|
216
216
|
// as explicit keys, an omitted (undefined) value would clobber the
|
|
217
217
|
// stored value during mergeApplianceData's `{...existing, ...update}`
|
|
@@ -219,6 +219,7 @@ class ApplianceManager {
|
|
|
219
219
|
// while dropping omitted fields so the existing value is preserved.
|
|
220
220
|
...(appliance.cloudPackageId !== undefined && { cloudPackageId: appliance.cloudPackageId }),
|
|
221
221
|
...(appliance.availableFeatures !== undefined && { availableFeatures: appliance.availableFeatures }),
|
|
222
|
+
...(appliance.compatibilityModes !== undefined && { compatibilityModes: appliance.compatibilityModes }),
|
|
222
223
|
};
|
|
223
224
|
let applianceData = newApplianceData;
|
|
224
225
|
if (existingApplianceId) {
|
|
@@ -65,6 +65,13 @@ export interface ApplianceConfig {
|
|
|
65
65
|
heatingRod?: EnyoHeatingRodApplianceMetadata;
|
|
66
66
|
smartPlug?: EnyoSmartPlugApplianceMetadata;
|
|
67
67
|
availableFeatures?: EnyoApplianceAvailableFeaturesEnum[];
|
|
68
|
+
/**
|
|
69
|
+
* Optional vendor- or integration-specific compatibility modes. Forwarded to
|
|
70
|
+
* {@link EnyoAppliance.compatibilityModes} when the appliance is created or
|
|
71
|
+
* updated. Omit to keep the stored value on an update; pass an explicit `[]`
|
|
72
|
+
* to clear it.
|
|
73
|
+
*/
|
|
74
|
+
compatibilityModes?: string[];
|
|
68
75
|
/**
|
|
69
76
|
* Optional identifier of the cloud-deployed energy app package that manages
|
|
70
77
|
* this appliance. Forwarded to {@link EnyoAppliance.cloudPackageId} when the
|
|
@@ -443,6 +450,11 @@ export interface PartialEnyoAppliance {
|
|
|
443
450
|
smartPlug?: Partial<EnyoSmartPlugApplianceMetadata>;
|
|
444
451
|
/** Optional custom name for the appliance, defined by the user */
|
|
445
452
|
customName?: string;
|
|
453
|
+
/**
|
|
454
|
+
* Optional list of vendor- or integration-specific compatibility modes that
|
|
455
|
+
* are active for this appliance. Mirrors {@link EnyoAppliance.compatibilityModes}.
|
|
456
|
+
*/
|
|
457
|
+
compatibilityModes?: string[];
|
|
446
458
|
/**
|
|
447
459
|
* Optional identifier of the cloud-deployed energy app package that manages
|
|
448
460
|
* this appliance. Mirrors {@link EnyoAppliance.cloudPackageId}.
|
|
@@ -54,7 +54,8 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
54
54
|
};
|
|
55
55
|
// Build the incoming appliance data covering every ApplianceConfig field.
|
|
56
56
|
// The two optional top-level fields that are NOT shallow-merged by
|
|
57
|
-
// mergeApplianceData (`cloudPackageId`, `availableFeatures
|
|
57
|
+
// mergeApplianceData (`cloudPackageId`, `availableFeatures`,
|
|
58
|
+
// `compatibilityModes`) are only
|
|
58
59
|
// materialized when provided so an omitted value cannot clobber the stored
|
|
59
60
|
// one during an update; pass an explicit value (e.g. `[]`) to clear them.
|
|
60
61
|
const newApplianceData = {
|
|
@@ -73,6 +74,7 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
73
74
|
heatingRod: appliance.heatingRod,
|
|
74
75
|
...(appliance.cloudPackageId !== undefined && { cloudPackageId: appliance.cloudPackageId }),
|
|
75
76
|
...(appliance.availableFeatures !== undefined && { availableFeatures: appliance.availableFeatures }),
|
|
77
|
+
...(appliance.compatibilityModes !== undefined && { compatibilityModes: appliance.compatibilityModes }),
|
|
76
78
|
};
|
|
77
79
|
// On update, merge onto the stored appliance so type-specific metadata and
|
|
78
80
|
// top-level fields that were not supplied in this call are preserved.
|
package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.cjs
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* an IANA zone, a price that is not a finite number. These fail at planning
|
|
10
10
|
* time, in the dark, on the night the charge was supposed to happen.
|
|
11
11
|
* 2. **Values that can never take effect.** A heating-rod mode stored while rod
|
|
12
|
-
* control is off, a
|
|
13
|
-
*
|
|
14
|
-
* to a user as "the app ignored me".
|
|
12
|
+
* control is off, a ct/kWh ceiling while the price limit is expressed as a
|
|
13
|
+
* share of the day. Nothing rejects these — they simply do nothing, which
|
|
14
|
+
* reads to a user as "the app ignored me".
|
|
15
15
|
*
|
|
16
16
|
* The second class is why {@link ENERGY_MANAGER_SETTING_DEPENDENCIES} exists as
|
|
17
17
|
* data: the gate tree is checked here rather than restated by every surface.
|
|
@@ -73,6 +73,20 @@ const EV_DISCHARGE_MODES = new Set(Object.values(enyo_energy_manager_settings_js
|
|
|
73
73
|
const BATTERY_CHARGING_MODES = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerBatteryChargingModeEnum));
|
|
74
74
|
const HEATING_ROD_MODES = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerHeatingRodModeEnum));
|
|
75
75
|
const CHARGE_MODES = new Set(Object.values(enyo_data_bus_value_js_1.EnyoChargeModeEnum));
|
|
76
|
+
const PRICE_LIMIT_MODES = new Set(Object.values(enyo_data_bus_value_js_1.EnyoPriceLimitModeEnum));
|
|
77
|
+
/**
|
|
78
|
+
* The band a relative price ceiling must lie in, in percent of the day.
|
|
79
|
+
*
|
|
80
|
+
* Matches the automation validators' `AUTOMATION_MIN/MAX_CHEAPEST_SHARE_PERCENT`
|
|
81
|
+
* on purpose: "the cheapest 25 % of the day" means the same thing whether it
|
|
82
|
+
* drives a charging session or an automation, and a user who learns one band
|
|
83
|
+
* should not meet a different one elsewhere.
|
|
84
|
+
*
|
|
85
|
+
* `0` is excluded because it selects no hours at all — that is "never import",
|
|
86
|
+
* which is {@link EnyoChargeModeEnum.PriceLimit}, not a ceiling.
|
|
87
|
+
*/
|
|
88
|
+
const PRICE_LIMIT_SHARE_MIN_PERCENT = 1;
|
|
89
|
+
const PRICE_LIMIT_SHARE_MAX_PERCENT = 100;
|
|
76
90
|
/**
|
|
77
91
|
* True when `timezone` is an IANA zone this runtime recognises.
|
|
78
92
|
*
|
|
@@ -116,6 +130,9 @@ function gateValue(values, setting) {
|
|
|
116
130
|
* zone the runtime resolves (**error**).
|
|
117
131
|
* - `priceLimitCtPerKwh` is a finite number (**error**), and lies in a plausible
|
|
118
132
|
* ct/kWh band (**warning** — catches a EUR/kWh value in a ct/kWh field).
|
|
133
|
+
* - `priceLimitMode` holds an enum member (**error**).
|
|
134
|
+
* - `priceLimitSharePercent` is an integer in 1…100 (**error**), and not `100`
|
|
135
|
+
* (**warning** — the whole day counts as cheap, so it is no ceiling at all).
|
|
119
136
|
* - `batteryEvDischargeMode` holds an enum member (**error**).
|
|
120
137
|
* - `batteryEvDischargeFixedWh` is a finite non-negative number (**error**), and
|
|
121
138
|
* is neither `0` nor a negligible positive figure (**warning** — the first
|
|
@@ -194,6 +211,27 @@ function validateEnergyManagerSettingsState(state) {
|
|
|
194
211
|
'cents per kWh, not EUR per kWh.');
|
|
195
212
|
}
|
|
196
213
|
}
|
|
214
|
+
if (values.priceLimitMode !== undefined && !PRICE_LIMIT_MODES.has(values.priceLimitMode)) {
|
|
215
|
+
errors.push(`\`priceLimitMode\` "${values.priceLimitMode}" is not an EnyoPriceLimitModeEnum member.`);
|
|
216
|
+
}
|
|
217
|
+
if (values.priceLimitSharePercent !== undefined) {
|
|
218
|
+
const share = values.priceLimitSharePercent;
|
|
219
|
+
if (typeof share !== 'number' || !Number.isFinite(share)) {
|
|
220
|
+
errors.push('`priceLimitSharePercent` must be a finite number when set.');
|
|
221
|
+
}
|
|
222
|
+
else if (!Number.isInteger(share)) {
|
|
223
|
+
errors.push(`\`priceLimitSharePercent\` is ${share}; a share of the day is a whole ` +
|
|
224
|
+
'percentage and must be an integer.');
|
|
225
|
+
}
|
|
226
|
+
else if (share < PRICE_LIMIT_SHARE_MIN_PERCENT || share > PRICE_LIMIT_SHARE_MAX_PERCENT) {
|
|
227
|
+
errors.push(`\`priceLimitSharePercent\` is ${share}; it is a percentage of the day and must ` +
|
|
228
|
+
`lie between ${PRICE_LIMIT_SHARE_MIN_PERCENT} and ${PRICE_LIMIT_SHARE_MAX_PERCENT}.`);
|
|
229
|
+
}
|
|
230
|
+
else if (share === PRICE_LIMIT_SHARE_MAX_PERCENT) {
|
|
231
|
+
warnings.push('`priceLimitSharePercent` is 100, which treats the whole day as cheap and so ' +
|
|
232
|
+
'imposes no ceiling at all. Leave `priceLimitMode` unset to say that outright.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
197
235
|
if (values.batteryEvDischargeMode !== undefined
|
|
198
236
|
&& !EV_DISCHARGE_MODES.has(values.batteryEvDischargeMode)) {
|
|
199
237
|
errors.push(`\`batteryEvDischargeMode\` "${values.batteryEvDischargeMode}" is not an ` +
|
package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.d.cts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* an IANA zone, a price that is not a finite number. These fail at planning
|
|
9
9
|
* time, in the dark, on the night the charge was supposed to happen.
|
|
10
10
|
* 2. **Values that can never take effect.** A heating-rod mode stored while rod
|
|
11
|
-
* control is off, a
|
|
12
|
-
*
|
|
13
|
-
* to a user as "the app ignored me".
|
|
11
|
+
* control is off, a ct/kWh ceiling while the price limit is expressed as a
|
|
12
|
+
* share of the day. Nothing rejects these — they simply do nothing, which
|
|
13
|
+
* reads to a user as "the app ignored me".
|
|
14
14
|
*
|
|
15
15
|
* The second class is why {@link ENERGY_MANAGER_SETTING_DEPENDENCIES} exists as
|
|
16
16
|
* data: the gate tree is checked here rather than restated by every surface.
|
|
@@ -56,6 +56,9 @@ export interface EnergyManagerSettingsValidationResult {
|
|
|
56
56
|
* zone the runtime resolves (**error**).
|
|
57
57
|
* - `priceLimitCtPerKwh` is a finite number (**error**), and lies in a plausible
|
|
58
58
|
* ct/kWh band (**warning** — catches a EUR/kWh value in a ct/kWh field).
|
|
59
|
+
* - `priceLimitMode` holds an enum member (**error**).
|
|
60
|
+
* - `priceLimitSharePercent` is an integer in 1…100 (**error**), and not `100`
|
|
61
|
+
* (**warning** — the whole day counts as cheap, so it is no ceiling at all).
|
|
59
62
|
* - `batteryEvDischargeMode` holds an enum member (**error**).
|
|
60
63
|
* - `batteryEvDischargeFixedWh` is a finite non-negative number (**error**), and
|
|
61
64
|
* is neither `0` nor a negligible positive figure (**warning** — the first
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -94,6 +94,8 @@ __exportStar(require("./types/enyo-charging-card.cjs"), exports);
|
|
|
94
94
|
__exportStar(require("./packages/energy-app-charging-card.cjs"), exports);
|
|
95
95
|
__exportStar(require("./types/enyo-charge.cjs"), exports);
|
|
96
96
|
__exportStar(require("./packages/energy-app-charge.cjs"), exports);
|
|
97
|
+
__exportStar(require("./types/enyo-vehicle.cjs"), exports);
|
|
98
|
+
__exportStar(require("./packages/energy-app-vehicle.cjs"), exports);
|
|
97
99
|
__exportStar(require("./types/enyo-battery-appliance.cjs"), exports);
|
|
98
100
|
__exportStar(require("./types/enyo-heatpump-appliance.cjs"), exports);
|
|
99
101
|
__exportStar(require("./types/enyo-inverter-appliance.cjs"), exports);
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -78,6 +78,8 @@ export * from './types/enyo-charging-card.cjs';
|
|
|
78
78
|
export * from './packages/energy-app-charging-card.cjs';
|
|
79
79
|
export * from './types/enyo-charge.cjs';
|
|
80
80
|
export * from './packages/energy-app-charge.cjs';
|
|
81
|
+
export * from './types/enyo-vehicle.cjs';
|
|
82
|
+
export * from './packages/energy-app-vehicle.cjs';
|
|
81
83
|
export * from './types/enyo-battery-appliance.cjs';
|
|
82
84
|
export * from './types/enyo-heatpump-appliance.cjs';
|
|
83
85
|
export * from './types/enyo-inverter-appliance.cjs';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MqttConnectOptions, MqttExternalConnectOptions, MqttConnectionStatus, MqttSubscribeOptions, MqttPublishOptions, MqttMessage, EnyoMqttAvailableConnectionDetails } from "../types/enyo-mqtt.cjs";
|
|
1
|
+
import { MqttConnectOptions, MqttExternalConnectOptions, MqttConnectionStatus, MqttSubscribeOptions, MqttPublishOptions, MqttMessage, EnyoMqttAvailableConnectionDetails, EnyoMqttBrokerEnableOptions, EnyoMqttBrokerEnableResult } from "../types/enyo-mqtt.cjs";
|
|
2
2
|
/**
|
|
3
3
|
* Interface for MQTT communication in enyo packages.
|
|
4
4
|
* Provides MQTT client functionality with support for both the SDK-provided
|
|
@@ -79,6 +79,37 @@ export interface EnergyAppMqtt {
|
|
|
79
79
|
* ```
|
|
80
80
|
*/
|
|
81
81
|
getAvailableConnectionDetails: () => Promise<EnyoMqttAvailableConnectionDetails>;
|
|
82
|
+
/**
|
|
83
|
+
* Request that the SDK-provided local MQTT broker be enabled.
|
|
84
|
+
*
|
|
85
|
+
* The local broker is not guaranteed to be running: the host may keep it
|
|
86
|
+
* switched off until an installed app actually needs it. Call this before
|
|
87
|
+
* {@link connectInternal} or {@link getAvailableConnectionDetails} when the
|
|
88
|
+
* app depends on the local broker, and treat a non-enabled outcome as a
|
|
89
|
+
* normal runtime state rather than an error — the host may require a user
|
|
90
|
+
* or installer confirmation, or refuse the request by policy.
|
|
91
|
+
*
|
|
92
|
+
* The call is idempotent: if the broker is already running it resolves with
|
|
93
|
+
* {@link EnyoMqttBrokerEnableStatus.AlreadyEnabled} without restarting it.
|
|
94
|
+
*
|
|
95
|
+
* @param options - Optional reason shown with the request and a wait timeout
|
|
96
|
+
* @returns The outcome of the request, including the broker connection details when it is running
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const result = await mqtt.requestBrokerEnable({
|
|
101
|
+
* reason: 'Publish inverter readings to the local home automation system',
|
|
102
|
+
* });
|
|
103
|
+
*
|
|
104
|
+
* if (!result.enabled) {
|
|
105
|
+
* console.warn(`Local broker not available (${result.status}): ${result.message ?? ''}`);
|
|
106
|
+
* return;
|
|
107
|
+
* }
|
|
108
|
+
*
|
|
109
|
+
* const client = await mqtt.connectInternal();
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
requestBrokerEnable: (options?: EnyoMqttBrokerEnableOptions) => Promise<EnyoMqttBrokerEnableResult>;
|
|
82
113
|
}
|
|
83
114
|
/**
|
|
84
115
|
* Represents an active MQTT client connection.
|
|
@@ -1,11 +1,54 @@
|
|
|
1
|
-
import { EnyoVehicle } from "../types/enyo-vehicle.cjs";
|
|
1
|
+
import { EnyoVehicle, EnyoVehicleSoc } from "../types/enyo-vehicle.cjs";
|
|
2
2
|
/**
|
|
3
3
|
* Interface for managing vehicles in enyo packages.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Reading is **read-only by design**: a vehicle's identity and its charging
|
|
6
|
+
* behaviour are configured by the user in the enyo app, and an energy app
|
|
7
|
+
* plans against them rather than changing them. There is no `update()` — if
|
|
8
|
+
* you are looking for one, see the note on the state of charge below.
|
|
9
|
+
*
|
|
10
|
+
* ## Writing a state of charge
|
|
11
|
+
*
|
|
12
|
+
* An app that can read the car — a wallbox speaking ISO 15118, a manufacturer
|
|
13
|
+
* cloud integration, an OCPP charge point reporting SoC in its meter values —
|
|
14
|
+
* publishes it on the data bus as `EnyoDataBusVehicleSocUpdateV1`. The host
|
|
15
|
+
* ingests the message and it becomes readable through {@link getSoc}:
|
|
16
|
+
*
|
|
17
|
+
* ```typescript
|
|
18
|
+
* energyApp.useDataBus().sendMessage([{
|
|
19
|
+
* type: 'message',
|
|
20
|
+
* message: 'VehicleSocUpdateV1',
|
|
21
|
+
* data: {
|
|
22
|
+
* vehicleId,
|
|
23
|
+
* socPercent: 62,
|
|
24
|
+
* measuredAtIso: new Date().toISOString(),
|
|
25
|
+
* },
|
|
26
|
+
* }]);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* That message is the only write path for a state of charge; publishing it
|
|
30
|
+
* requires the `SendDataBusValues` permission.
|
|
5
31
|
*/
|
|
6
32
|
export interface EnergyAppVehicle {
|
|
7
33
|
/** Get a list of all registered vehicles */
|
|
8
34
|
list: () => Promise<EnyoVehicle[]>;
|
|
9
35
|
/** Get a specific vehicle by its ID */
|
|
10
36
|
getById: (id: string) => Promise<EnyoVehicle | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Get the most recent state-of-charge reading for a vehicle.
|
|
39
|
+
*
|
|
40
|
+
* Resolves to `undefined` when nothing is known about the car's charge —
|
|
41
|
+
* no source has ever reported one, or the host does not keep readings
|
|
42
|
+
* this old. `undefined` is an ordinary answer, not an error: plenty of
|
|
43
|
+
* vehicles have no SoC source at all, and a planner must cope without one
|
|
44
|
+
* rather than assume a number.
|
|
45
|
+
*
|
|
46
|
+
* The reading carries {@link EnyoVehicleSoc.measuredAtIso}, so decide for
|
|
47
|
+
* yourself how old is too old before showing it to a user or sizing a
|
|
48
|
+
* session against it.
|
|
49
|
+
*
|
|
50
|
+
* @param id - ID of the vehicle to read.
|
|
51
|
+
* @returns The reading and its age, or `undefined` when none is known.
|
|
52
|
+
*/
|
|
53
|
+
getSoc: (id: string) => Promise<EnyoVehicleSoc | undefined>;
|
|
11
54
|
}
|
|
@@ -277,6 +277,14 @@ export interface EnyoAppliance {
|
|
|
277
277
|
smartPlug?: EnyoSmartPlugApplianceMetadata;
|
|
278
278
|
/** Optional custom name for the appliance, defined by the user */
|
|
279
279
|
customName?: string;
|
|
280
|
+
/**
|
|
281
|
+
* Optional list of vendor- or integration-specific compatibility modes that
|
|
282
|
+
* are active for this appliance. Each entry is a free-form, non-localized
|
|
283
|
+
* identifier describing a behavioural deviation the appliance requires
|
|
284
|
+
* (e.g. a firmware quirk workaround or a legacy protocol dialect).
|
|
285
|
+
* Consumers that do not know a given mode should ignore it.
|
|
286
|
+
*/
|
|
287
|
+
compatibilityModes?: string[];
|
|
280
288
|
/**
|
|
281
289
|
* Optional identifier of the cloud-deployed energy app package that manages
|
|
282
290
|
* this appliance. Set when the appliance is provisioned and operated by a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EnyoPowerSourceEnum = exports.EnyoHeatpumpHeatSourceEnum = exports.EnyoHeatpumpControlPurposeEnum = exports.EnyoChargingProfileTypeEnum = exports.EnyoCommandAcknowledgeAnswerEnum = exports.EnyoStorageControlDirectionEnum = exports.EnyoStorageControlModeEnum = exports.EnyoStorageScheduleDirectionEnum = exports.EnyoStorageScheduleModeEnum = exports.EnyoChargingLimitRequestResultEnum = exports.EnyoDataBusMessageEnum = exports.EnyoChargeInitiatorEnum = exports.EnyoChargeModeEnum = exports.EnyoChargingStopReason = exports.EnyoChargingMeterValueContext = exports.EnyoStringStateEnum = exports.EnyoHeatingRodStateEnum = exports.EnyoInverterStateEnum = exports.EnyoBatteryStateEnum = exports.EnyoGridOperatorLimitTypeEnum = exports.EnyoDataBusCommandReasonCategoryEnum = exports.EnyoDataBusCommandReasonTypeEnum = void 0;
|
|
3
|
+
exports.EnyoPowerSourceEnum = exports.EnyoHeatpumpHeatSourceEnum = exports.EnyoHeatpumpControlPurposeEnum = exports.EnyoChargingProfileTypeEnum = exports.EnyoCommandAcknowledgeAnswerEnum = exports.EnyoStorageControlDirectionEnum = exports.EnyoStorageControlModeEnum = exports.EnyoStorageScheduleDirectionEnum = exports.EnyoStorageScheduleModeEnum = exports.EnyoChargingLimitRequestResultEnum = exports.EnyoDataBusMessageEnum = exports.EnyoChargeInitiatorEnum = exports.EnyoPriceLimitModeEnum = exports.EnyoChargeModeEnum = exports.EnyoChargingStopReason = exports.EnyoChargingMeterValueContext = exports.EnyoStringStateEnum = exports.EnyoHeatingRodStateEnum = exports.EnyoInverterStateEnum = exports.EnyoBatteryStateEnum = exports.EnyoGridOperatorLimitTypeEnum = exports.EnyoDataBusCommandReasonCategoryEnum = exports.EnyoDataBusCommandReasonTypeEnum = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Enum representing the reason type for why a data bus command was issued.
|
|
6
6
|
* Used to attach context to commands for logging, debugging, and UI display.
|
|
@@ -205,17 +205,101 @@ var EnyoChargingStopReason;
|
|
|
205
205
|
EnyoChargingStopReason["Other"] = "Other";
|
|
206
206
|
})(EnyoChargingStopReason || (exports.EnyoChargingStopReason = EnyoChargingStopReason = {}));
|
|
207
207
|
/**
|
|
208
|
-
* Mode for charging session
|
|
208
|
+
* Mode for a charging session.
|
|
209
|
+
*
|
|
210
|
+
* Three modes, one meaning each, identical with and without a dynamic tariff —
|
|
211
|
+
* the tariff changes only what happens *inside* a mode, never which modes
|
|
212
|
+
* exist. The user-facing German names map one to one:
|
|
213
|
+
*
|
|
214
|
+
* | UI | Member | Behaviour |
|
|
215
|
+
* |---|---|---|
|
|
216
|
+
* | Sofort laden | {@link Immediate} | full power, no optimisation |
|
|
217
|
+
* | Nur Sonne | {@link PriceLimit} | strictly PV surplus, never imports |
|
|
218
|
+
* | Sonne zuerst | {@link CostOptimized} | PV first, grid tops up by the deadline |
|
|
219
|
+
*
|
|
220
|
+
* **An immediate reserve runs ahead of every mode.** When the vehicle carries
|
|
221
|
+
* `immediateReserveKwh > 0` (see `EnyoVehicle`), the session first charges at
|
|
222
|
+
* full power until that much energy has gone in — counted from the state of
|
|
223
|
+
* charge at plug-in — and only then hands over to the selected mode. This
|
|
224
|
+
* applies to {@link PriceLimit} too: the point of the reserve is that the
|
|
225
|
+
* driver may have to leave unexpectedly, and the sun is not a guarantee.
|
|
209
226
|
*/
|
|
210
227
|
var EnyoChargeModeEnum;
|
|
211
228
|
(function (EnyoChargeModeEnum) {
|
|
212
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* "Sofort laden" — charge at the highest power the car and the wallbox
|
|
231
|
+
* jointly allow, immediately, with no optimisation and no deadline.
|
|
232
|
+
*/
|
|
213
233
|
EnyoChargeModeEnum["Immediate"] = "immediate";
|
|
214
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* "Sonne zuerst" — PV surplus first; whatever is still missing by
|
|
236
|
+
* `completeChargeAtIso` is imported from the grid, in the cheapest hours
|
|
237
|
+
* with a dynamic tariff and as late as possible without one.
|
|
238
|
+
*
|
|
239
|
+
* This is the only mode that imports, so it is the only one a price
|
|
240
|
+
* ceiling applies to: it takes an **optional**
|
|
241
|
+
* {@link EnyoDataBusStartChargeV1.data.priceLimitCtPerKwh}, and no ceiling
|
|
242
|
+
* is a normal answer rather than a missing one.
|
|
243
|
+
*/
|
|
215
244
|
EnyoChargeModeEnum["CostOptimized"] = "cost-optimized";
|
|
216
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* "Nur Sonne" — strictly PV surplus. No grid energy, ever, for this
|
|
247
|
+
* session. There is no deadline and the session never has to finish "in
|
|
248
|
+
* time"; it charges what the sun delivers and stops when it does not.
|
|
249
|
+
*
|
|
250
|
+
* A price ceiling is meaningless here and must be ignored if one is sent.
|
|
251
|
+
*
|
|
252
|
+
* Strict PV-only is its own answer rather than "{@link CostOptimized} at
|
|
253
|
+
* 0 ct" because the 0 ct spelling fails the moment wholesale prices go
|
|
254
|
+
* negative — which is precisely when the customer would be glad to import.
|
|
255
|
+
*/
|
|
217
256
|
EnyoChargeModeEnum["PriceLimit"] = "price-limit";
|
|
218
257
|
})(EnyoChargeModeEnum || (exports.EnyoChargeModeEnum = EnyoChargeModeEnum = {}));
|
|
258
|
+
/**
|
|
259
|
+
* How a price ceiling for grid energy is expressed.
|
|
260
|
+
*
|
|
261
|
+
* The two spellings answer the same question — "which grid energy is cheap
|
|
262
|
+
* enough to charge from?" — in the two ways a user thinks about it, and they
|
|
263
|
+
* are mutually exclusive: the mode says which of the accompanying values is
|
|
264
|
+
* read, exactly as {@link EnergyManagerBatteryEvDischargeModeEnum} does for the
|
|
265
|
+
* battery-to-vehicle parameters.
|
|
266
|
+
*
|
|
267
|
+
* No mode at all means **no ceiling**, which is a deliberate and common answer
|
|
268
|
+
* ("Preisgrenze: Aus") rather than a missing value.
|
|
269
|
+
*
|
|
270
|
+
* Both spellings resolve to the same thing at planning time: an absolute price
|
|
271
|
+
* per kWh above which the session does not import. A relative ceiling simply
|
|
272
|
+
* resolves late, against the prices actually known for the day.
|
|
273
|
+
*
|
|
274
|
+
* A ceiling only applies under {@link EnyoChargeModeEnum.CostOptimized} — the
|
|
275
|
+
* only mode that imports at all.
|
|
276
|
+
*/
|
|
277
|
+
var EnyoPriceLimitModeEnum;
|
|
278
|
+
(function (EnyoPriceLimitModeEnum) {
|
|
279
|
+
/**
|
|
280
|
+
* Absolute ceiling: never import above `priceLimitCtPerKwh` cents per kWh.
|
|
281
|
+
*
|
|
282
|
+
* Says exactly what the user pays at most, and is independent of how the
|
|
283
|
+
* rest of the day looks — on a uniformly expensive day it may mean the car
|
|
284
|
+
* does not charge from the grid at all.
|
|
285
|
+
*/
|
|
286
|
+
EnyoPriceLimitModeEnum["CtPerKwh"] = "ct-per-kwh";
|
|
287
|
+
/**
|
|
288
|
+
* Relative ceiling: import only during the cheapest `priceLimitSharePercent`
|
|
289
|
+
* of the day — `25` is the cheapest quarter.
|
|
290
|
+
*
|
|
291
|
+
* The intervals known for the day are ranked and the cheapest share is
|
|
292
|
+
* treated as importable, the same rule
|
|
293
|
+
* {@link EnyoAutomationCheapestShareOfDayTrigger} applies. Note "the day"
|
|
294
|
+
* is the prices *known* at evaluation time: before the next day's prices
|
|
295
|
+
* publish, the share is taken over a shorter horizon and the resulting
|
|
296
|
+
* threshold moves when they arrive.
|
|
297
|
+
*
|
|
298
|
+
* Always yields some importable hours, which an absolute ceiling does not —
|
|
299
|
+
* but it says nothing about what those hours cost.
|
|
300
|
+
*/
|
|
301
|
+
EnyoPriceLimitModeEnum["CheapestShare"] = "cheapest-share";
|
|
302
|
+
})(EnyoPriceLimitModeEnum || (exports.EnyoPriceLimitModeEnum = EnyoPriceLimitModeEnum = {}));
|
|
219
303
|
/**
|
|
220
304
|
* Identifies who initiated a charging session.
|
|
221
305
|
*/
|