@enyo-energy/energy-app-sdk 1.43.0 → 1.45.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 (70) hide show
  1. package/README.md +176 -3
  2. package/dist/cjs/energy-app-package-definition.d.cts +46 -1
  3. package/dist/cjs/energy-app-permission.type.cjs +2 -0
  4. package/dist/cjs/energy-app-permission.type.d.cts +3 -1
  5. package/dist/cjs/energy-app.cjs +26 -0
  6. package/dist/cjs/energy-app.d.cts +24 -0
  7. package/dist/cjs/enyo-energy-app-sdk.d.cts +6 -0
  8. package/dist/cjs/implementations/heatpump/heatpump-curve-optimization-rooms.cjs +96 -0
  9. package/dist/cjs/implementations/heatpump/heatpump-curve-optimization-rooms.d.cts +65 -0
  10. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +77 -19
  11. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +4 -2
  12. package/dist/cjs/implementations/subscriptions/define-subscription-plan.cjs +48 -0
  13. package/dist/cjs/implementations/subscriptions/define-subscription-plan.d.cts +40 -0
  14. package/dist/cjs/implementations/subscriptions/subscription-offer-validators.cjs +301 -0
  15. package/dist/cjs/implementations/subscriptions/subscription-offer-validators.d.cts +54 -0
  16. package/dist/cjs/implementations/subscriptions/subscription-plan-validators.cjs +8 -0
  17. package/dist/cjs/implementations/subscriptions/subscription-plan-validators.d.cts +8 -3
  18. package/dist/cjs/index.cjs +7 -0
  19. package/dist/cjs/index.d.cts +7 -0
  20. package/dist/cjs/packages/energy-app-room-temperatures.cjs +2 -0
  21. package/dist/cjs/packages/energy-app-room-temperatures.d.cts +51 -0
  22. package/dist/cjs/packages/energy-app-room.cjs +2 -0
  23. package/dist/cjs/packages/energy-app-room.d.cts +59 -0
  24. package/dist/cjs/types/enyo-authentication.d.cts +11 -0
  25. package/dist/cjs/types/enyo-heatpump-appliance.cjs +35 -2
  26. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +98 -39
  27. package/dist/cjs/types/enyo-room-temperature.cjs +2 -0
  28. package/dist/cjs/types/enyo-room-temperature.d.cts +102 -0
  29. package/dist/cjs/types/enyo-room.cjs +37 -0
  30. package/dist/cjs/types/enyo-room.d.cts +60 -0
  31. package/dist/cjs/types/enyo-settings.d.cts +22 -1
  32. package/dist/cjs/types/enyo-subscription-offer.cjs +113 -0
  33. package/dist/cjs/types/enyo-subscription-offer.d.cts +358 -0
  34. package/dist/cjs/version.cjs +1 -1
  35. package/dist/cjs/version.d.cts +1 -1
  36. package/dist/energy-app-package-definition.d.ts +46 -1
  37. package/dist/energy-app-permission.type.d.ts +3 -1
  38. package/dist/energy-app-permission.type.js +2 -0
  39. package/dist/energy-app.d.ts +24 -0
  40. package/dist/energy-app.js +26 -0
  41. package/dist/enyo-energy-app-sdk.d.ts +6 -0
  42. package/dist/implementations/heatpump/heatpump-curve-optimization-rooms.d.ts +65 -0
  43. package/dist/implementations/heatpump/heatpump-curve-optimization-rooms.js +89 -0
  44. package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +4 -2
  45. package/dist/implementations/heatpump/heatpump-metadata-validators.js +78 -20
  46. package/dist/implementations/subscriptions/define-subscription-plan.d.ts +40 -0
  47. package/dist/implementations/subscriptions/define-subscription-plan.js +45 -0
  48. package/dist/implementations/subscriptions/subscription-offer-validators.d.ts +54 -0
  49. package/dist/implementations/subscriptions/subscription-offer-validators.js +298 -0
  50. package/dist/implementations/subscriptions/subscription-plan-validators.d.ts +8 -3
  51. package/dist/implementations/subscriptions/subscription-plan-validators.js +8 -0
  52. package/dist/index.d.ts +7 -0
  53. package/dist/index.js +7 -0
  54. package/dist/packages/energy-app-room-temperatures.d.ts +51 -0
  55. package/dist/packages/energy-app-room-temperatures.js +1 -0
  56. package/dist/packages/energy-app-room.d.ts +59 -0
  57. package/dist/packages/energy-app-room.js +1 -0
  58. package/dist/types/enyo-authentication.d.ts +11 -0
  59. package/dist/types/enyo-heatpump-appliance.d.ts +98 -39
  60. package/dist/types/enyo-heatpump-appliance.js +34 -1
  61. package/dist/types/enyo-room-temperature.d.ts +102 -0
  62. package/dist/types/enyo-room-temperature.js +1 -0
  63. package/dist/types/enyo-room.d.ts +60 -0
  64. package/dist/types/enyo-room.js +34 -0
  65. package/dist/types/enyo-settings.d.ts +22 -1
  66. package/dist/types/enyo-subscription-offer.d.ts +358 -0
  67. package/dist/types/enyo-subscription-offer.js +110 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +1 -1
package/README.md CHANGED
@@ -409,6 +409,8 @@ Energy Apps use a granular permissions system to control access to system resour
409
409
  - **`GridFeeRegister`** / **`GridFeeUse`**: Publish / resolve dynamic grid fees (time-variable network charges)
410
410
  - **`CommandLog`**: Record and read back the commands apps issued to appliances
411
411
  - **`Subscriptions`**: Read the subscriptions bought for the app on this device
412
+ - **`Room`**: Read the rooms of the home the customer set up in the enyo app
413
+ - **`RoomTemperatures`**: Read the current and past temperatures measured in those rooms
412
414
 
413
415
  #### Site & Identity Permissions
414
416
 
@@ -1253,6 +1255,33 @@ settings.listenForSettingsChanges((settingName, newValue) => {
1253
1255
  const allSettings = await settings.getSettingsConfig();
1254
1256
  ```
1255
1257
 
1258
+ ##### Action settings (buttons)
1259
+
1260
+ A setting of type `'action'` renders as a button for a one-off command (restart a device, start a self-test, …). Every tap writes the current time as an ISO 8601 string to the setting, so each tap arrives as a change event — react to the event, not to the stored value.
1261
+
1262
+ Use an action only for something that *happens*. Anything that has a state the user turns on or off (e.g. "charging cable locked") is a `checkbox`, not an action.
1263
+
1264
+ ```typescript
1265
+ await settings.addSettingConfig({
1266
+ name: `restart-${applianceId}`,
1267
+ applianceId,
1268
+ type: 'action',
1269
+ required: false,
1270
+ fieldName: [{ language: 'en', value: 'Restart wallbox' }, { language: 'de', value: 'Wallbox neu starten' }],
1271
+ actionOptions: {
1272
+ buttonCaption: [{ language: 'en', value: 'Restart now' }, { language: 'de', value: 'Jetzt neu starten' }],
1273
+ // optional: ask before triggering
1274
+ confirmationText: [{ language: 'en', value: 'Restart the wallbox?' }, { language: 'de', value: 'Wallbox neu starten?' }],
1275
+ },
1276
+ });
1277
+
1278
+ settings.listenForSettingsChanges((event) => {
1279
+ if (event.settingName === `restart-${applianceId}`) {
1280
+ void restartWallbox();
1281
+ }
1282
+ });
1283
+ ```
1284
+
1256
1285
  #### `useConfigurationManager(): EnergyAppConfigurationManager`
1257
1286
 
1258
1287
  Register **internal**, non user-facing configurations for your package and react to value changes. Unlike `useSettings()`, configurations registered here are NOT rendered in the Energy App UI — they are intended for values the package itself reads and writes at runtime (e.g. internal feature toggles, tuning parameters, calibration values) and need to be persisted across restarts.
@@ -1768,6 +1797,37 @@ const full = await location.getLocation(); // requires LocationC
1768
1797
  if (full) console.log(`lat=${full.latitude} lon=${full.longitude}`);
1769
1798
  ```
1770
1799
 
1800
+ #### `useRoom(): EnergyAppRoom`
1801
+
1802
+ Read the rooms of the home, as the customer set them up in the enyo app: name, the appliances in each, the temperature the customer wants there (`targetTemperatureC`, optional — use `HEATPUMP_CURVE_OPTIMIZATION_DEFAULT_TARGET_C` = 21 °C until set) and, for the heating curve optimisation, the room's `warmth` (`EnyoRoomWarmthEnum`) and `thermostat` (`EnyoRoomThermostatEnum`). Read-only.
1803
+
1804
+ ```typescript
1805
+ const rooms = energyApp.useRoom();
1806
+
1807
+ applyRooms(await rooms.getRooms());
1808
+ const circuitRooms = await rooms.getRoomsById(circuit.curveOptimization?.roomIds ?? []);
1809
+ rooms.listenForRoomsChanged((all) => applyRooms(all));
1810
+ ```
1811
+
1812
+ Requires the `Room` permission.
1813
+
1814
+ #### `useRoomTemperatures(): EnergyAppRoomTemperatures`
1815
+
1816
+ Read the temperatures measured in the rooms, per sensor — a room can have several. `getCurrentRoomTemperatures(roomIds)` returns the latest reading of each sensor, `getRoomTemperatureHistory({roomIds, startDateIso, endDateIso, resolution})` the past ones in `'1m'` or `'15m'` buckets (average, min, max). There is no listener: follow the sensors named in the current values (`applianceId`, `sensorId`) via `TemperatureSensorValuesUpdateV1` on the data bus.
1817
+
1818
+ ```typescript
1819
+ const roomTemperatures = energyApp.useRoomTemperatures();
1820
+
1821
+ const current = await roomTemperatures.getCurrentRoomTemperatures(['living', 'bath']);
1822
+ const history = await roomTemperatures.getRoomTemperatureHistory({
1823
+ roomIds: ['living', 'bath'],
1824
+ startDateIso: '2026-10-27T00:00:00Z',
1825
+ endDateIso: '2026-10-28T00:00:00Z',
1826
+ });
1827
+ ```
1828
+
1829
+ Requires the `RoomTemperatures` permission.
1830
+
1771
1831
  #### `useGridConnectionPoint(): EnergyAppGridConnectionPoint`
1772
1832
 
1773
1833
  Read the site's grid connection details — main fuse rating, number of phases, the maximum allowed grid power, and the total power that load balancing may allocate to EV chargers. Use this to size dispatch envelopes and avoid violating the contractual cap.
@@ -3147,7 +3207,9 @@ const packageDef = defineEnergyAppPackage({
3147
3207
  | `pricingModel` | `per-package` (default): one price for the package on the device. `per-unit`: the price is charged per unit — see [Per-unit plans](#per-unit-plans). |
3148
3208
  | `prices` | One net price per currency. `amountWithoutTax` is always the **total for one interval** — per month, per year, or the single one-time amount; per unit for `per-unit` plans — in the currency (not cent), at most two decimals; enyo adds VAT. |
3149
3209
  | `trialDays` | Optional free trial in days. Not allowed for `one-time` plans. |
3150
- | `storeEntry` | Title, short description and optional benefits; `de` and `en` are required. |
3210
+ | `storeEntry` | Title, short description and optional benefits; `de` and `en` are required. Prefer `includedFeatures` over `benefits`. |
3211
+ | `includedFeatures` | Optional feature keys from `subscriptionFeatures`, or `{ featureKey, value }` with a translated value — see [Offer screen](#offer-screen). |
3212
+ | `highlight` | Optional badge: `recommended`, `best-value` or `most-popular`. At most one plan per package. |
3151
3213
  | `internalComment` | Optional note, never shown to users. |
3152
3214
 
3153
3215
  What a plan unlocks is decided by the app: check `isPlanActive(subscriptions, planKey)` at runtime (see
@@ -3176,6 +3238,97 @@ In tests, `InMemorySubscriptions` takes the mode as its third constructor argume
3176
3238
  throws when an `optional` package reports `SubscriptionRequired`. The same check is available as
3177
3239
  `assertEnergyAppStateMatchesSubscriptionMode(mode, state)`.
3178
3240
 
3241
+ ### Offer screen
3242
+
3243
+ Promote the plans on the screen where enyo sells them: declare a **feature catalog** once, say which features the
3244
+ **free tier** and each plan include, and build the screen from **blocks**. All three are optional. Without an offer
3245
+ screen, enyo shows only the plan cards.
3246
+
3247
+ ```typescript
3248
+ import {
3249
+ defineSubscriptionFeature,
3250
+ defineSubscriptionOfferScreen,
3251
+ EnyoSubscriptionOfferHintKindEnum,
3252
+ EnyoSubscriptionOfferIconEnum,
3253
+ EnyoSubscriptionPlanHighlightEnum
3254
+ } from '@enyo-energy/energy-app-sdk';
3255
+
3256
+ defineEnergyAppPackage({
3257
+ // ...
3258
+ subscriptionMode: EnyoSubscriptionModeEnum.Optional,
3259
+ subscriptionFeatures: [
3260
+ defineSubscriptionFeature({
3261
+ featureKey: 'pv-charging',
3262
+ icon: EnyoSubscriptionOfferIconEnum.Sun,
3263
+ storeEntry: [
3264
+ { language: 'en', title: 'PV surplus charging' },
3265
+ { language: 'de', title: 'PV-Überschussladen' }
3266
+ ]
3267
+ }),
3268
+ defineSubscriptionFeature({
3269
+ featureKey: 'price-optimisation',
3270
+ icon: EnyoSubscriptionOfferIconEnum.PiggyBank,
3271
+ storeEntry: [
3272
+ { language: 'en', title: 'Dynamic price optimisation', description: 'Charges when electricity is cheapest.' },
3273
+ { language: 'de', title: 'Optimierung nach dynamischen Preisen', description: 'Lädt, wenn Strom am günstigsten ist.' }
3274
+ ]
3275
+ }),
3276
+ defineSubscriptionFeature({
3277
+ featureKey: 'vehicles',
3278
+ icon: EnyoSubscriptionOfferIconEnum.Car,
3279
+ storeEntry: [{ language: 'en', title: 'Connected vehicles' }, { language: 'de', title: 'Verbundene Fahrzeuge' }]
3280
+ })
3281
+ ],
3282
+ subscriptionFreeTier: {
3283
+ storeEntry: [{ language: 'en', title: 'Basic' }, { language: 'de', title: 'Basis' }],
3284
+ includedFeatures: ['pv-charging']
3285
+ },
3286
+ subscriptionPlans: [
3287
+ defineSubscriptionPlan({
3288
+ planKey: 'premium',
3289
+ highlight: EnyoSubscriptionPlanHighlightEnum.Recommended,
3290
+ includedFeatures: [
3291
+ 'pv-charging',
3292
+ 'price-optimisation',
3293
+ { featureKey: 'vehicles', value: [{ language: 'en', value: 'up to 5' }, { language: 'de', value: 'bis zu 5' }] }
3294
+ ],
3295
+ // paymentInterval, prices, storeEntry ...
3296
+ })
3297
+ ],
3298
+ subscriptionOfferScreen: defineSubscriptionOfferScreen({
3299
+ blocks: [
3300
+ { type: 'hero', icon: 'piggy-bank', storeEntry: [
3301
+ { language: 'en', title: 'Charge for less', text: 'Smart charging with dynamic prices.' },
3302
+ { language: 'de', title: 'Günstiger laden', text: 'Intelligentes Laden mit dynamischen Preisen.' }
3303
+ ] },
3304
+ { type: 'comparison' }, // Basic vs. Premium, one row per feature
3305
+ { type: 'hint', kind: EnyoSubscriptionOfferHintKindEnum.PricingReason, icon: 'handshake', storeEntry: [
3306
+ { language: 'en', title: 'Why does this cost money?', text: 'The car vendors charge us for every connected vehicle.' },
3307
+ { language: 'de', title: 'Warum kostet das Geld?', text: 'Die Fahrzeughersteller berechnen uns jedes verbundene Fahrzeug.' }
3308
+ ] },
3309
+ { type: 'plans' }
3310
+ ]
3311
+ })
3312
+ });
3313
+ ```
3314
+
3315
+ | Block | Shows | Notes |
3316
+ |---|---|---|
3317
+ | `hero` | A large headline with optional text and icon | `title` is required. |
3318
+ | `features` | Features with icon and description | `featureKeys` picks and orders them; omit it to list all. |
3319
+ | `comparison` | A table: one row per feature, one column per tier | The free column is shown when a free tier is declared (`includeFreeTier` overrides this). `planKeys` and `featureKeys` pick the columns and rows. A cell shows a check mark, the feature's `value`, or nothing. Needs at least two columns. |
3320
+ | `hint` | A highlighted note | `kind`: `pricing-reason` (why it costs money), `requirement`, `info`, `legal`. `text` is required. |
3321
+ | `plans` | The plan cards and checkout | At most one. Without it, the plans follow the last block. |
3322
+
3323
+ - **Icons** come from a fixed set, `EnyoSubscriptionOfferIconEnum` (`piggy-bank`, `sun`, `car`, `wallbox`, `heat-pump`,
3324
+ `handshake`, …), so every app's offer screen looks the same.
3325
+ - **A plan's `includedFeatures` should list everything the customer gets with it**, including the free tier's
3326
+ features. The comparison table takes each column straight from the tier's list.
3327
+ - **`subscriptionFreeTier` is only allowed with `subscriptionMode: 'optional'`**.
3328
+ - Every text needs `de` and `en`. `validatePackageSubscriptionPlans()` checks all of this as well (it calls
3329
+ `validatePackageSubscriptionOffer()`). It also warns about features that no tier includes and comparison rows
3330
+ that stay empty.
3331
+
3179
3332
  ### Per-unit plans
3180
3333
 
3181
3334
  For a `per-unit` plan the price is charged once per unit — 3 units on a yearly plan for 10 € cost 30 € per year
@@ -3751,7 +3904,7 @@ Shifts the heating curve of one heating circuit in parallel — **permanently**,
3751
3904
  - **Changed by hand.** If the curve read from the device differs from `initialHeatingCurve` shifted by `activeHeatingCurveOffsetK` by more than 0.5 K at any point (`isHeatingCurveManuallyChanged(initial, offsetK, read)`), store the read curve as the new `initialHeatingCurve`, set `activeHeatingCurveOffsetK` to `0` and log it in a plain sentence.
3752
3905
  - **Alongside `HeatpumpControlV3` and `HeatpumpProgramSwitchV3`.** Boost and eco act on top; the offset is the new base.
3753
3906
  - **Validation** (run by the base class before the handler via `checkHeatingCurveOffset`, or `validateHeatpumpHeatingCurveOffset(data, metadata)` directly): a valid index and finite `offsetK` (`Rejected`); `HeatingCurveOffset` declared (`NotSupported`); a known circuit and — unless `offsetK` is `0` — within `heatingCurveOffsetRangeK` (`Rejected`).
3754
- - **Customer settings.** Whether to optimise at all, and which rooms of the home the circuit heats (`roomIds`, whether or not a room has a sensor), live in `heatingCircuits[].curveOptimization`. The host resolves them into `rooms`: per room its target temperature (set in the enyo app, `HEATPUMP_CURVE_OPTIMIZATION_DEFAULT_TARGET_C` = 21 °C until then) and the temperature sensors assigned to it. Rooms without a sensor are left out; the room furthest below its target counts, and the curve is lowered by at most `HEATPUMP_CURVE_OPTIMIZATION_MAX_LOWERING_K` (3 K). Integrations only read them.
3907
+ - **Customer settings.** Whether to optimise at all, and which rooms of the home the circuit heats (`roomIds`, whether or not a room has a sensor), live in `heatingCircuits[].curveOptimization`. The rooms themselves come from `useRoom().getRoomsById(roomIds)`, their measured temperatures from `useRoomTemperatures()`; `curveOptimizationRooms(curveOptimization, rooms)` picks the ones the curve is judged by (the warmest rooms behind a manual thermostat, and every room with a self-regulating thermostat or none). A room at its target means the curve is just right when its thermostat is open, but only a room falling below its target shows the curve went too far behind a self-regulating one. Rooms without a sensor are left out; the room furthest below its target counts, and the curve is lowered by at most `HEATPUMP_CURVE_OPTIMIZATION_MAX_LOWERING_K` (3 K). Integrations only read them.
3755
3908
 
3756
3909
  ```typescript
3757
3910
  protected async handleHeatpumpHeatingCurveOffset(message: EnyoDataBusSetHeatpumpHeatingCurveOffsetV1) {
@@ -3807,7 +3960,7 @@ Beyond tanks and circuits, the heatpump metadata (`EnyoHeatpumpApplianceMetadata
3807
3960
  | `RoomTemperature` | `heatingCircuits[].roomTemperatureC` in `HeatpumpTemperaturesUpdateV1`; where it is measured goes in `heatingCircuits[].roomTemperatureSource` (`None` / `Internal` / `External`) and, for an enyo sensor, `roomTemperatureSensor` |
3808
3961
  | `RoomTemperatureInput` | The heatpump accepts measured room temperatures via `SetHeatpumpRoomTemperatureV1` |
3809
3962
  | `HeatingCurveOffset` | The heatpump's app shifts a circuit's curve via `SetHeatpumpHeatingCurveOffsetV1`. `heatingCircuits[].heatingCurveOffsetRangeK` (allowed range, must contain 0), `activeHeatingCurveOffsetK` (effective shift read back) and `initialHeatingCurve` (the curve before the first shift) |
3810
- | — | `heatingCircuits[].curveOptimization` — the customer's choice for the room-temperature based curve optimisation: `enabled` (off by default), `roomIds` (the rooms the circuit heats) and, resolved by the host, `rooms` — per room `roomId`, `name`, its `targetRoomTemperatureC` (16–26 °C) and the `sensors` assigned to it. Customer-owned; integrations only read it |
3963
+ | — | `heatingCircuits[].curveOptimization` — the customer's choice for the room-temperature based curve optimisation: `enabled` (off by default) and `roomIds` (the rooms the circuit heats, see `useRoom()`). Customer-owned; integrations only read it |
3811
3964
  | — | `building.recommendedHeatingCurve` — the curve the building actually needs, learned from the days the room held its target |
3812
3965
  | `PvSurplusSignal` | The heatpump takes `availablePowerW` of `HeatpumpControlV3` as a PV surplus signal (e.g. Lambda register 102). Supersedes the deprecated `AvailablePowerAnnouncement` |
3813
3966
  | `ControlV3` | The heatpump's app handles `HeatpumpControlV3` / `HeatpumpControlCancelV3` and reports `HeatpumpControlStatusV3`. The energy manager sends V3 only to heatpumps declaring it. Needs at least one overheating feature to be actionable |
@@ -3840,6 +3993,26 @@ await applianceManager.updateHeatpumpDomesticHotWater(applianceId, 0, { timeProg
3840
3993
 
3841
3994
  Validate before publishing with `validateHeatpumpHeatingCurve`, `validateHeatpumpTimeProgram` or `validateHeatpumpAdvancedMetadata` (or their `assertValid*` counterparts). Each returns `{ ok, errors, warnings }`.
3842
3995
 
3996
+ **SG Ready relays.** You can declare 0–n SG Ready inputs in `metadata.heatpump.sgReadyRelays`. Each one has a
3997
+ `function` that says what it acts on: `heating` (a heating circuit), `dhw-heatpump` (DHW heated by the compressor)
3998
+ or `dhw-heating-rod` (DHW heated by the heating rod). `zoneIndex` names the heating circuit or DHW zone; omit it for
3999
+ the first one. If only some of the four states (`blocked`, `normal`, `recommended`, `forced`) are wired, list them
4000
+ in `supportedStates`. `activeState` reports the current state. This works independently of `connectionType`.
4001
+
4002
+ ```typescript
4003
+ sgReadyRelays: [
4004
+ { index: 0, function: EnyoHeatpumpSgReadyFunctionEnum.Heating, zoneIndex: 0, name: 'K1/K2' },
4005
+ { index: 1, function: EnyoHeatpumpSgReadyFunctionEnum.DhwHeatpump, name: 'K3/K4' },
4006
+ { index: 2, function: EnyoHeatpumpSgReadyFunctionEnum.DhwHeatingRod,
4007
+ supportedStates: [EnyoHeatpumpSgReadyStateEnum.Normal, EnyoHeatpumpSgReadyStateEnum.Forced] },
4008
+ ]
4009
+ ```
4010
+
4011
+ `validateHeatpumpAdvancedMetadata` checks the relays too. It rejects duplicate indexes, unknown functions or
4012
+ states, and an `activeState` that is not in `supportedStates`. It warns about unknown zones, two relays with the
4013
+ same function on one zone, a relay without `normal`, and a `dhw-heating-rod` relay when `additionalDevices` has
4014
+ no `HeatingRod`.
4015
+
3843
4016
  ### WallboxIntegrationEnergyApp
3844
4017
 
3845
4018
  Drives an EV wallbox / charger. Has the richest command surface of all integrations.
@@ -4,6 +4,7 @@ import type { EnyoFileTranslation } from "./types/enyo-file.cjs";
4
4
  import type { EnyoOnboardingV2Guide } from "./types/enyo-onboarding-v2.cjs";
5
5
  import type { EnyoOnboardingTranslatedContent } from "./types/enyo-onboarding.cjs";
6
6
  import type { EnyoSubscriptionMode, EnyoSubscriptionPaymentInterval, EnyoSubscriptionPrice, EnyoSubscriptionPricingModel } from "./types/enyo-subscription.cjs";
7
+ import type { EnyoSubscriptionFeature, EnyoSubscriptionFreeTier, EnyoSubscriptionIncludedFeature, EnyoSubscriptionOfferScreen, EnyoSubscriptionPlanHighlight } from "./types/enyo-subscription-offer.cjs";
7
8
  export type EnergyAppPackageLanguage = 'de' | 'en';
8
9
  export declare enum EnergyAppPackageCategory {
9
10
  Inverter = "inverter",
@@ -294,7 +295,13 @@ export interface EnergyAppPackageSubscriptionPlanStoreEntry {
294
295
  title: string;
295
296
  /** One sentence describing what the plan unlocks */
296
297
  shortDescription: string;
297
- /** Optional list of benefits, shown as bullet points on the offer screen */
298
+ /**
299
+ * Optional list of benefits, shown as bullet points on the plan card.
300
+ *
301
+ * Prefer {@link EnergyAppPackageSubscriptionPlan.includedFeatures}: features
302
+ * carry icons and descriptions and feed the comparison table. The host
303
+ * shows `benefits` only for plans without `includedFeatures`.
304
+ */
298
305
  benefits?: string[];
299
306
  }
300
307
  /**
@@ -358,6 +365,21 @@ export interface EnergyAppPackageSubscriptionPlan {
358
365
  trialDays?: number;
359
366
  /** Localized store information. Entries for `de` and `en` are required. */
360
367
  storeEntry: EnergyAppPackageSubscriptionPlanStoreEntry[];
368
+ /**
369
+ * The features this plan includes, as keys of
370
+ * {@link EnergyAppPackageDefinition.subscriptionFeatures} — or with a
371
+ * translated value such as "up to 5 vehicles" — in display order. Shown on
372
+ * the plan card and in the comparison table.
373
+ *
374
+ * List every feature the customer gets with the plan, including those of
375
+ * the free tier, so the comparison table reads correctly.
376
+ */
377
+ includedFeatures?: EnyoSubscriptionIncludedFeature[];
378
+ /**
379
+ * Optional badge that makes this plan stand out on the offer screen, e.g.
380
+ * `recommended`. At most one plan of a package may be highlighted.
381
+ */
382
+ highlight?: EnyoSubscriptionPlanHighlight;
361
383
  /** Optional internal note explaining this plan; never shown to users. */
362
384
  internalComment?: string;
363
385
  }
@@ -1064,6 +1086,29 @@ export interface EnergyAppPackageDefinition {
1064
1086
  * Omit for packages that are free.
1065
1087
  */
1066
1088
  subscriptionPlans?: EnergyAppPackageSubscriptionPlan[];
1089
+ /**
1090
+ * The package's feature catalog: every feature that the plans and the free
1091
+ * tier refer to, declared once with its icon and translations. enyo builds
1092
+ * the feature lists and the comparison table of the offer screen from it.
1093
+ *
1094
+ * Only used together with {@link subscriptionPlans}. Validate it with
1095
+ * `validatePackageSubscriptionPlans()`.
1096
+ */
1097
+ subscriptionFeatures?: EnyoSubscriptionFeature[];
1098
+ /**
1099
+ * What the app offers without a plan, shown as the free column of the
1100
+ * comparison table. Only allowed with `subscriptionMode: 'optional'`.
1101
+ */
1102
+ subscriptionFreeTier?: EnyoSubscriptionFreeTier;
1103
+ /**
1104
+ * The blocks of the screen on which enyo offers the plans — a headline,
1105
+ * feature lists, a comparison table, hints such as why the app costs
1106
+ * money, and the plan cards — in display order.
1107
+ *
1108
+ * Omit to show only the plan cards. Validate it with
1109
+ * `validatePackageSubscriptionPlans()`.
1110
+ */
1111
+ subscriptionOfferScreen?: EnyoSubscriptionOfferScreen;
1067
1112
  /**
1068
1113
  * Whether the app needs one of its plans to work at all.
1069
1114
  *
@@ -69,4 +69,6 @@ var EnergyAppPermissionTypeEnum;
69
69
  EnergyAppPermissionTypeEnum["VehicleIntegration"] = "VehicleIntegration";
70
70
  EnergyAppPermissionTypeEnum["Webhook"] = "Webhook";
71
71
  EnergyAppPermissionTypeEnum["Subscriptions"] = "Subscriptions";
72
+ EnergyAppPermissionTypeEnum["Room"] = "Room";
73
+ EnergyAppPermissionTypeEnum["RoomTemperatures"] = "RoomTemperatures";
72
74
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -64,7 +64,9 @@ export declare enum EnergyAppPermissionTypeEnum {
64
64
  Calibration = "Calibration",
65
65
  VehicleIntegration = "VehicleIntegration",
66
66
  Webhook = "Webhook",
67
- Subscriptions = "Subscriptions"
67
+ Subscriptions = "Subscriptions",
68
+ Room = "Room",
69
+ RoomTemperatures = "RoomTemperatures"
68
70
  }
69
71
  /**
70
72
  * String union of every permission name, derived from
@@ -552,6 +552,32 @@ class EnergyApp {
552
552
  useSubscriptions() {
553
553
  return this.energyAppSdk.useSubscriptions();
554
554
  }
555
+ /**
556
+ * Gets the Room API for reading the rooms of the home: their name, the
557
+ * appliances in them, the temperature the customer wants there and, for
558
+ * the heating curve optimisation, their warmth and thermostat. Read the
559
+ * rooms with `getRooms()` or `getRoomsById()` and react to changes with
560
+ * `listenForRoomsChanged()`.
561
+ * @returns The Room API instance
562
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Room` permission is
563
+ * not granted.
564
+ */
565
+ useRoom() {
566
+ return this.energyAppSdk.useRoom();
567
+ }
568
+ /**
569
+ * Gets the Room Temperatures API for reading the temperatures measured in
570
+ * the rooms of the home, per sensor: the current values with
571
+ * `getCurrentRoomTemperatures()` and the past ones with
572
+ * `getRoomTemperatureHistory()`. Follow changes via the
573
+ * `TemperatureSensorValuesUpdateV1` messages on the data bus.
574
+ * @returns The Room Temperatures API instance
575
+ * @throws {EnergyAppPermissionNotGrantedError} If the `RoomTemperatures`
576
+ * permission is not granted.
577
+ */
578
+ useRoomTemperatures() {
579
+ return this.energyAppSdk.useRoomTemperatures();
580
+ }
555
581
  /**
556
582
  * Gets the current SDK version.
557
583
  * @returns The semantic version string of the SDK
@@ -53,6 +53,8 @@ import { EnergyAppCommandLog } from "./packages/energy-app-command-log.cjs";
53
53
  import { EnergyAppCascade } from "./packages/energy-app-cascade.cjs";
54
54
  import { EnergyAppWebhook } from "./packages/energy-app-webhook.cjs";
55
55
  import { EnergyAppSubscriptions } from "./packages/energy-app-subscriptions.cjs";
56
+ import { EnergyAppRoom } from "./packages/energy-app-room.cjs";
57
+ import { EnergyAppRoomTemperatures } from "./packages/energy-app-room-temperatures.cjs";
56
58
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
57
59
  /**
58
60
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -469,6 +471,28 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
469
471
  * permission is not granted.
470
472
  */
471
473
  useSubscriptions(): EnergyAppSubscriptions;
474
+ /**
475
+ * Gets the Room API for reading the rooms of the home: their name, the
476
+ * appliances in them, the temperature the customer wants there and, for
477
+ * the heating curve optimisation, their warmth and thermostat. Read the
478
+ * rooms with `getRooms()` or `getRoomsById()` and react to changes with
479
+ * `listenForRoomsChanged()`.
480
+ * @returns The Room API instance
481
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Room` permission is
482
+ * not granted.
483
+ */
484
+ useRoom(): EnergyAppRoom;
485
+ /**
486
+ * Gets the Room Temperatures API for reading the temperatures measured in
487
+ * the rooms of the home, per sensor: the current values with
488
+ * `getCurrentRoomTemperatures()` and the past ones with
489
+ * `getRoomTemperatureHistory()`. Follow changes via the
490
+ * `TemperatureSensorValuesUpdateV1` messages on the data bus.
491
+ * @returns The Room Temperatures API instance
492
+ * @throws {EnergyAppPermissionNotGrantedError} If the `RoomTemperatures`
493
+ * permission is not granted.
494
+ */
495
+ useRoomTemperatures(): EnergyAppRoomTemperatures;
472
496
  /**
473
497
  * Gets the current SDK version.
474
498
  * @returns The semantic version string of the SDK
@@ -52,6 +52,8 @@ import { EnergyAppCommandLog } from "./packages/energy-app-command-log.cjs";
52
52
  import { EnergyAppCascade } from "./packages/energy-app-cascade.cjs";
53
53
  import { EnergyAppWebhook } from "./packages/energy-app-webhook.cjs";
54
54
  import { EnergyAppSubscriptions } from "./packages/energy-app-subscriptions.cjs";
55
+ import { EnergyAppRoom } from "./packages/energy-app-room.cjs";
56
+ import { EnergyAppRoomTemperatures } from "./packages/energy-app-room-temperatures.cjs";
55
57
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
56
58
  export declare enum EnergyAppStateEnum {
57
59
  Launching = "launching",
@@ -198,4 +200,8 @@ export interface EnyoEnergyAppSdk {
198
200
  useWebhook: () => EnergyAppWebhook;
199
201
  /** Get the Subscriptions API for reading the subscriptions bought for this app on this device and listening for new or changed subscriptions */
200
202
  useSubscriptions: () => EnergyAppSubscriptions;
203
+ /** Get the Room API for reading the rooms of the home the customer set up in the enyo app */
204
+ useRoom: () => EnergyAppRoom;
205
+ /** Get the Room Temperatures API for reading the current and past temperatures measured in the rooms of the home */
206
+ useRoomTemperatures: () => EnergyAppRoomTemperatures;
201
207
  }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /**
3
+ * Which rooms of a heating circuit the room-temperature based heating curve
4
+ * optimisation is judged by. Shared by the energy manager, which lowers the
5
+ * curve, and the host's app, which tells the customer how to set each room's
6
+ * thermostat — both must agree on it.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.curveWarmthRank = curveWarmthRank;
10
+ exports.isManualRoomThermostat = isManualRoomThermostat;
11
+ exports.curveTopManualRank = curveTopManualRank;
12
+ exports.curveJudgedRoomIds = curveJudgedRoomIds;
13
+ exports.curveOptimizationRooms = curveOptimizationRooms;
14
+ const enyo_room_js_1 = require("../../types/enyo-room.cjs");
15
+ const WARMTH_RANK = {
16
+ [enyo_room_js_1.EnyoRoomWarmthEnum.Cool]: 0,
17
+ [enyo_room_js_1.EnyoRoomWarmthEnum.Normal]: 1,
18
+ [enyo_room_js_1.EnyoRoomWarmthEnum.Warm]: 2,
19
+ };
20
+ const MANUAL_THERMOSTATS = new Set([
21
+ enyo_room_js_1.EnyoRoomThermostatEnum.Steps5,
22
+ enyo_room_js_1.EnyoRoomThermostatEnum.Steps6,
23
+ enyo_room_js_1.EnyoRoomThermostatEnum.CelsiusDial,
24
+ ]);
25
+ /**
26
+ * How much warmth a room asks for compared with the other rooms of its
27
+ * circuit: 0 for `cool`, 1 for `normal` (also when absent), 2 for `warm`.
28
+ *
29
+ * @param traits - The room's warmth, if known.
30
+ * @returns The rank.
31
+ */
32
+ function curveWarmthRank(traits) {
33
+ return WARMTH_RANK[traits?.warmth ?? enyo_room_js_1.EnyoRoomWarmthEnum.Normal];
34
+ }
35
+ /**
36
+ * Whether a thermostat is set by hand: a dial with steps or a dial in °C.
37
+ * Absent counts as the common dial with steps 1–5.
38
+ *
39
+ * @param thermostat - The room's thermostat.
40
+ * @returns `true` for a manual thermostat.
41
+ */
42
+ function isManualRoomThermostat(thermostat) {
43
+ return thermostat === undefined || MANUAL_THERMOSTATS.has(thermostat);
44
+ }
45
+ /**
46
+ * How much warmth the most demanding room behind a manual thermostat asks for.
47
+ *
48
+ * @param roomIds - The rooms of the circuit.
49
+ * @param traitsOf - A room's warmth and thermostat.
50
+ * @returns The highest rank, or `undefined` when no room has a manual thermostat.
51
+ */
52
+ function curveTopManualRank(roomIds, traitsOf) {
53
+ const ranks = roomIds
54
+ .filter((roomId) => isManualRoomThermostat(traitsOf(roomId)?.thermostat))
55
+ .map((roomId) => curveWarmthRank(traitsOf(roomId)));
56
+ return ranks.length > 0 ? Math.max(...ranks) : undefined;
57
+ }
58
+ /**
59
+ * The rooms of a heating circuit the curve is judged by. One flow temperature
60
+ * heats every room of the circuit, so the curve has to serve the warmest wish:
61
+ * of the rooms with a manual thermostat, those asking for the most warmth keep
62
+ * it fully open and show what the curve does. Every other one is held back by
63
+ * its thermostat, reads the thermostat rather than the curve, and would mislead
64
+ * the controller. A room with no thermostat follows the curve, and one with a
65
+ * self-regulating thermostat holds its own target — both are always judged.
66
+ *
67
+ * @param roomIds - The rooms of the circuit.
68
+ * @param traitsOf - A room's warmth and thermostat.
69
+ * @returns The judged room IDs, in the order given.
70
+ */
71
+ function curveJudgedRoomIds(roomIds, traitsOf) {
72
+ const top = curveTopManualRank(roomIds, traitsOf);
73
+ return roomIds.filter((roomId) => {
74
+ const traits = traitsOf(roomId);
75
+ return !isManualRoomThermostat(traits?.thermostat) || curveWarmthRank(traits) === top;
76
+ });
77
+ }
78
+ /**
79
+ * The rooms a heating circuit's curve optimisation is judged by, resolved
80
+ * against the rooms of the home ({@link EnergyAppRoom.getRooms} or
81
+ * {@link EnergyAppRoom.getRoomsById}). A room that no longer exists is
82
+ * dropped. Whether a judged room can be measured is up to
83
+ * {@link EnergyAppRoomTemperatures}.
84
+ *
85
+ * @param settings - The circuit's {@link EnyoHeatpumpApplianceHeatingCircuit.curveOptimization}.
86
+ * @param rooms - Every room of the home.
87
+ * @returns The judged rooms, in the order of `settings.roomIds`.
88
+ */
89
+ function curveOptimizationRooms(settings, rooms) {
90
+ const live = (settings?.roomIds ?? []).flatMap((roomId) => {
91
+ const room = rooms.find((candidate) => candidate.id === roomId);
92
+ return room ? [room] : [];
93
+ });
94
+ const judged = curveJudgedRoomIds(live.map((room) => room.id), (roomId) => live.find((room) => room.id === roomId));
95
+ return live.filter((room) => judged.includes(room.id));
96
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Which rooms of a heating circuit the room-temperature based heating curve
3
+ * optimisation is judged by. Shared by the energy manager, which lowers the
4
+ * curve, and the host's app, which tells the customer how to set each room's
5
+ * thermostat — both must agree on it.
6
+ */
7
+ import type { EnyoHeatpumpHeatingCurveOptimizationSettings } from '../../types/enyo-heatpump-appliance.cjs';
8
+ import { type EnyoRoom, EnyoRoomThermostatEnum, EnyoRoomWarmthEnum } from '../../types/enyo-room.cjs';
9
+ /** What the curve rules need to know about a room. */
10
+ export interface CurveRoomTraits {
11
+ /** Absent: {@link EnyoRoomWarmthEnum.Normal}. */
12
+ warmth?: EnyoRoomWarmthEnum;
13
+ /** Absent: {@link EnyoRoomThermostatEnum.Steps5}. */
14
+ thermostat?: EnyoRoomThermostatEnum;
15
+ }
16
+ /**
17
+ * How much warmth a room asks for compared with the other rooms of its
18
+ * circuit: 0 for `cool`, 1 for `normal` (also when absent), 2 for `warm`.
19
+ *
20
+ * @param traits - The room's warmth, if known.
21
+ * @returns The rank.
22
+ */
23
+ export declare function curveWarmthRank(traits: CurveRoomTraits | undefined): number;
24
+ /**
25
+ * Whether a thermostat is set by hand: a dial with steps or a dial in °C.
26
+ * Absent counts as the common dial with steps 1–5.
27
+ *
28
+ * @param thermostat - The room's thermostat.
29
+ * @returns `true` for a manual thermostat.
30
+ */
31
+ export declare function isManualRoomThermostat(thermostat: EnyoRoomThermostatEnum | undefined): boolean;
32
+ /**
33
+ * How much warmth the most demanding room behind a manual thermostat asks for.
34
+ *
35
+ * @param roomIds - The rooms of the circuit.
36
+ * @param traitsOf - A room's warmth and thermostat.
37
+ * @returns The highest rank, or `undefined` when no room has a manual thermostat.
38
+ */
39
+ export declare function curveTopManualRank(roomIds: readonly string[], traitsOf: (roomId: string) => CurveRoomTraits | undefined): number | undefined;
40
+ /**
41
+ * The rooms of a heating circuit the curve is judged by. One flow temperature
42
+ * heats every room of the circuit, so the curve has to serve the warmest wish:
43
+ * of the rooms with a manual thermostat, those asking for the most warmth keep
44
+ * it fully open and show what the curve does. Every other one is held back by
45
+ * its thermostat, reads the thermostat rather than the curve, and would mislead
46
+ * the controller. A room with no thermostat follows the curve, and one with a
47
+ * self-regulating thermostat holds its own target — both are always judged.
48
+ *
49
+ * @param roomIds - The rooms of the circuit.
50
+ * @param traitsOf - A room's warmth and thermostat.
51
+ * @returns The judged room IDs, in the order given.
52
+ */
53
+ export declare function curveJudgedRoomIds(roomIds: readonly string[], traitsOf: (roomId: string) => CurveRoomTraits | undefined): string[];
54
+ /**
55
+ * The rooms a heating circuit's curve optimisation is judged by, resolved
56
+ * against the rooms of the home ({@link EnergyAppRoom.getRooms} or
57
+ * {@link EnergyAppRoom.getRoomsById}). A room that no longer exists is
58
+ * dropped. Whether a judged room can be measured is up to
59
+ * {@link EnergyAppRoomTemperatures}.
60
+ *
61
+ * @param settings - The circuit's {@link EnyoHeatpumpApplianceHeatingCircuit.curveOptimization}.
62
+ * @param rooms - Every room of the home.
63
+ * @returns The judged rooms, in the order of `settings.roomIds`.
64
+ */
65
+ export declare function curveOptimizationRooms(settings: EnyoHeatpumpHeatingCurveOptimizationSettings | undefined, rooms: readonly EnyoRoom[]): EnyoRoom[];