@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.
Files changed (40) hide show
  1. package/README.md +109 -11
  2. package/dist/cjs/implementations/appliances/appliance-manager.cjs +2 -1
  3. package/dist/cjs/implementations/appliances/appliance-manager.d.cts +12 -0
  4. package/dist/cjs/implementations/appliances/in-memory-appliance-manager.cjs +3 -1
  5. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.cjs +41 -3
  6. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.d.cts +6 -3
  7. package/dist/cjs/index.cjs +2 -0
  8. package/dist/cjs/index.d.cts +2 -0
  9. package/dist/cjs/packages/energy-app-mqtt.d.cts +32 -1
  10. package/dist/cjs/packages/energy-app-vehicle.d.cts +45 -2
  11. package/dist/cjs/types/enyo-appliance.d.cts +8 -0
  12. package/dist/cjs/types/enyo-data-bus-value.cjs +89 -5
  13. package/dist/cjs/types/enyo-data-bus-value.d.cts +215 -7
  14. package/dist/cjs/types/enyo-energy-manager-settings.cjs +62 -5
  15. package/dist/cjs/types/enyo-energy-manager-settings.d.cts +100 -12
  16. package/dist/cjs/types/enyo-mqtt.cjs +16 -1
  17. package/dist/cjs/types/enyo-mqtt.d.cts +49 -0
  18. package/dist/cjs/types/enyo-vehicle.d.cts +127 -1
  19. package/dist/cjs/version.cjs +1 -1
  20. package/dist/cjs/version.d.cts +1 -1
  21. package/dist/implementations/appliances/appliance-manager.d.ts +12 -0
  22. package/dist/implementations/appliances/appliance-manager.js +2 -1
  23. package/dist/implementations/appliances/in-memory-appliance-manager.js +3 -1
  24. package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.d.ts +6 -3
  25. package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.js +42 -4
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +2 -0
  28. package/dist/packages/energy-app-mqtt.d.ts +32 -1
  29. package/dist/packages/energy-app-vehicle.d.ts +45 -2
  30. package/dist/types/enyo-appliance.d.ts +8 -0
  31. package/dist/types/enyo-data-bus-value.d.ts +215 -7
  32. package/dist/types/enyo-data-bus-value.js +88 -4
  33. package/dist/types/enyo-energy-manager-settings.d.ts +100 -12
  34. package/dist/types/enyo-energy-manager-settings.js +62 -5
  35. package/dist/types/enyo-mqtt.d.ts +49 -0
  36. package/dist/types/enyo-mqtt.js +15 -0
  37. package/dist/types/enyo-vehicle.d.ts +127 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. 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 vehicle information:
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.getVehicles();
788
+ const vehicleList = await vehicles.list();
789
789
 
790
790
  // Get vehicle details
791
- const vehicle = await vehicles.getVehicleById(vehicleId);
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
- // Update vehicle state
794
- await vehicles.updateVehicleState(vehicleId, {
795
- batteryLevel: 80,
796
- isPluggedIn: true,
797
- estimatedRange: 320
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
- | `PriceLimitCtPerKwh` | number, **ct/kWh** (`7` = 7 ct/kWh) |
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
- - **Eight settings are gated by another** (`heatingRodMode` needs `heatingRodControl`, `priceLimitCtPerKwh` only applies under `price-limit`, …). 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.
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 two optional top-level fields that are NOT
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`) are only
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.
@@ -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 price limit while the default charge mode is
13
- * cost-optimized. Nothing rejects these — they simply do nothing, which reads
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 ` +
@@ -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 price limit while the default charge mode is
12
- * cost-optimized. Nothing rejects these — they simply do nothing, which reads
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
@@ -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);
@@ -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
- * Provides read-only operations for vehicle information.
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
- /** Start charging immediately at maximum rate */
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
- /** Optimize charging schedule for lowest cost */
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
- /** Optimize charging schedule for a maximum price limit, for example 7 ct grid or pv production */
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
  */