@enyo-energy/energy-app-sdk 1.21.0 → 1.23.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 (48) hide show
  1. package/README.md +231 -6
  2. package/dist/cjs/energy-app-package-definition.cjs +19 -1
  3. package/dist/cjs/energy-app-package-definition.d.cts +37 -0
  4. package/dist/cjs/implementations/appliance-command-forecast/appliance-command-forecast-validators.cjs +134 -0
  5. package/dist/cjs/implementations/appliance-command-forecast/appliance-command-forecast-validators.d.cts +51 -1
  6. package/dist/cjs/implementations/energy-distribution/energy-distribution-progress.cjs +5 -1
  7. package/dist/cjs/implementations/energy-distribution/energy-distribution-progress.d.cts +9 -1
  8. package/dist/cjs/implementations/energy-distribution/energy-distribution-snapshot-builder.cjs +27 -6
  9. package/dist/cjs/implementations/energy-distribution/energy-distribution-snapshot-builder.d.cts +36 -4
  10. package/dist/cjs/implementations/energy-distribution/energy-distribution-validators.cjs +134 -6
  11. package/dist/cjs/implementations/energy-distribution/energy-distribution-validators.d.cts +14 -2
  12. package/dist/cjs/implementations/firmware/firmware-validators.cjs +16 -2
  13. package/dist/cjs/packages/energy-app-appliance-energy-manager-forecast.d.cts +44 -2
  14. package/dist/cjs/packages/energy-app-energy-manager.d.cts +18 -0
  15. package/dist/cjs/packages/energy-app-modbus-rtu.d.cts +31 -2
  16. package/dist/cjs/types/enyo-appliance-command-forecast.cjs +18 -2
  17. package/dist/cjs/types/enyo-appliance-command-forecast.d.cts +237 -2
  18. package/dist/cjs/types/enyo-charger-appliance.d.cts +21 -0
  19. package/dist/cjs/types/enyo-data-bus-value.cjs +26 -0
  20. package/dist/cjs/types/enyo-data-bus-value.d.cts +26 -0
  21. package/dist/cjs/types/enyo-energy-distribution.cjs +27 -0
  22. package/dist/cjs/types/enyo-energy-distribution.d.cts +93 -0
  23. package/dist/cjs/version.cjs +1 -1
  24. package/dist/cjs/version.d.cts +1 -1
  25. package/dist/energy-app-package-definition.d.ts +37 -0
  26. package/dist/energy-app-package-definition.js +18 -0
  27. package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.d.ts +51 -1
  28. package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.js +128 -0
  29. package/dist/implementations/energy-distribution/energy-distribution-progress.d.ts +9 -1
  30. package/dist/implementations/energy-distribution/energy-distribution-progress.js +5 -1
  31. package/dist/implementations/energy-distribution/energy-distribution-snapshot-builder.d.ts +36 -4
  32. package/dist/implementations/energy-distribution/energy-distribution-snapshot-builder.js +27 -6
  33. package/dist/implementations/energy-distribution/energy-distribution-validators.d.ts +14 -2
  34. package/dist/implementations/energy-distribution/energy-distribution-validators.js +134 -6
  35. package/dist/implementations/firmware/firmware-validators.js +16 -2
  36. package/dist/packages/energy-app-appliance-energy-manager-forecast.d.ts +44 -2
  37. package/dist/packages/energy-app-energy-manager.d.ts +18 -0
  38. package/dist/packages/energy-app-modbus-rtu.d.ts +31 -2
  39. package/dist/types/enyo-appliance-command-forecast.d.ts +237 -2
  40. package/dist/types/enyo-appliance-command-forecast.js +18 -2
  41. package/dist/types/enyo-charger-appliance.d.ts +21 -0
  42. package/dist/types/enyo-data-bus-value.d.ts +26 -0
  43. package/dist/types/enyo-data-bus-value.js +26 -0
  44. package/dist/types/enyo-energy-distribution.d.ts +93 -0
  45. package/dist/types/enyo-energy-distribution.js +27 -0
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -63,10 +63,14 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
63
63
  - [ChargerForecast](#chargerforecast)
64
64
  - [BatteryCommandForecast](#batterycommandforecast)
65
65
  - [HeatpumpForecast](#heatpumpforecast)
66
+ - [HeatingRodForecast](#heatingrodforecast)
67
+ - [SmartPlugForecast](#smartplugforecast)
68
+ - [AirConditioningForecast](#airconditioningforecast)
66
69
  - [Validators](#validators)
67
70
  - [Energy Distribution Snapshot](#energy-distribution-snapshot)
68
71
  - [What the snapshot states](#what-the-snapshot-states)
69
72
  - [Stating the goal on an announcement](#stating-the-goal-on-an-announcement)
73
+ - [What a row states for the waterfall view](#what-a-row-states-for-the-waterfall-view)
70
74
  - [Publishing a snapshot](#publishing-a-snapshot)
71
75
  - [Consuming a snapshot](#consuming-a-snapshot)
72
76
  - [Helpers and validation](#helpers-and-validation)
@@ -171,7 +175,7 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
171
175
  | Forecast heatpump DHW tank temperature | [`HeatpumpDhwTemperatureForecast`](#heatpumpdhwtemperatureforecast) |
172
176
  | Forecast air conditioning electrical consumption | [`AirConditioningConsumptionForecast`](#airconditioningconsumptionforecast) |
173
177
  | Forecast air conditioning room temperature | [`AirConditioningRoomTemperatureForecast`](#airconditioningroomtemperatureforecast) |
174
- | Announce a charger / battery / heatpump command plan you **intend to apply** | [`useApplianceEnergyManagerForecast()`](#useapplianceenergymanagerforecast-energyappapplianceenergymanagerforecast) |
178
+ | Announce a charger / battery / heatpump / heating-rod / smart-plug / air-conditioning command plan you **intend to apply** | [`useApplianceEnergyManagerForecast()`](#useapplianceenergymanagerforecast-energyappapplianceenergymanagerforecast) |
175
179
  | Talk to an EEBUS / SHIP / SPINE device | [`useEebus()`](#useeebus-energyappeebus) |
176
180
  | Speak MQTT (SDK broker or external) | [`useMqtt()`](#usemqtt-energyappmqtt) |
177
181
  | Scan or talk to Bluetooth LE peripherals | [`useBluetooth()`](#usebluetooth-energyappbluetooth) |
@@ -3234,7 +3238,7 @@ energyApp.onShutdown(async () => {
3234
3238
 
3235
3239
  ## Appliance Energy-Manager Forecast
3236
3240
 
3237
- The [Forecasting](#forecasting) module above predicts what an appliance will **do** based on history. The Appliance Energy-Manager Forecast package goes the other way: it lets an energy-manager app declare what it **intends to command** each appliance to do over the upcoming horizon, plus the temperature trajectories its commands are expected to produce. Three appliance families are supported today — chargers, batteries, and heatpumps — and the heatpump payload can carry any combination of DHW boost, room pre-heating, buffer-tank boost, and a relative power-announcement schedule in one call.
3241
+ The [Forecasting](#forecasting) module above predicts what an appliance will **do** based on history. The Appliance Energy-Manager Forecast package goes the other way: it lets an energy-manager app declare what it **intends to command** each appliance to do over the upcoming horizon, plus the temperature trajectories its commands are expected to produce. Six appliance families are supported today — chargers, batteries, heatpumps, heating rods, smart plugs, and air conditioning units — and the heatpump payload can carry any combination of DHW boost, room pre-heating, buffer-tank boost, and a relative power-announcement schedule in one call.
3238
3242
 
3239
3243
  How the runtime fans these forecasts out to subscribers (data bus, RPC, …) is an internal implementation detail of the SDK runtime — apps just call `publish*` and the SDK takes care of the rest.
3240
3244
 
@@ -3251,6 +3255,9 @@ const forecasts = energyApp.useApplianceEnergyManagerForecast();
3251
3255
  | `publishChargerForecast(applianceId, forecast: ChargerForecast)` | Publish the planned phase / power schedule for a charger. |
3252
3256
  | `publishBatteryForecast(applianceId, forecast: BatteryCommandForecast)` | Publish the planned charge / discharge / auto cadence for a battery. |
3253
3257
  | `publishHeatpumpForecast(applianceId, forecast: HeatpumpForecast)` | Publish any combination of DHW boost / room pre-heating / buffer-tank boost / power-announcement schedule for a heatpump. |
3258
+ | `publishHeatingRodForecast(applianceId, forecast: HeatingRodForecast)` | Publish the planned target temperature / heating / available-power schedule for a heating rod. |
3259
+ | `publishSmartPlugForecast(applianceId, forecast: SmartPlugForecast)` | Publish the planned on/off schedule for a smart plug, including the trigger type behind each slot. |
3260
+ | `publishAirConditioningForecast(applianceId, forecast: AirConditioningForecast)` | Publish the planned mode / target-temperature / available-power schedule for an air conditioning unit. |
3254
3261
 
3255
3262
  Every call validates the payload first and rejects with `ApplianceCommandForecastValidationError` if any invariant is broken — `publish*` never goes through the runtime with malformed data.
3256
3263
 
@@ -3298,6 +3305,14 @@ Per-entry invariants:
3298
3305
  - `seconds`: finite, non-negative; first entry `= 0`; subsequent entries strictly increasing.
3299
3306
  - `powerW`: finite, non-negative (`0` means "pause").
3300
3307
  - `numberOfPhases`: optional; if set, must be `1`, `2`, or `3`.
3308
+ - `priceCtPerKwh`: optional; finite — **may be negative**, since a negative-price hour is exactly when a plan wants to charge. It is the expected grid price for that slot, so each planned window can explain itself („02:00 – 03:40 · 14 ct") instead of a consumer re-joining the schedule against a price stream and rounding the boundaries differently. `estimatedSavings` stays what it was: one figure for the plan as a whole.
3309
+
3310
+ Two charger facts belong on the appliance rather than on a forecast, and `EnyoChargerApplianceMetadata` now states them alongside `maxChargingPowerKw`:
3311
+
3312
+ - `minChargingPowerKw` — the lowest power a session can be held at (typically ≈ 1.4 kW at 6 A on one phase). Below it a plan can only pause, not trickle; the EMS must not issue a non-zero setpoint under it.
3313
+ - `phaseSwitchThresholdKw` — the surplus at which the EMS switches this charger to three phases (typically ≈ 4.1 kW). Absent on chargers that cannot switch.
3314
+
3315
+ Both are for marking on the same power scale a consumer already draws from `maxChargingPowerKw`, instead of hard-coding thresholds only the EMS knows.
3301
3316
 
3302
3317
  ### BatteryCommandForecast
3303
3318
 
@@ -3386,6 +3401,141 @@ Per-family invariants:
3386
3401
  - **`powerAnnouncementSchedule`** — relative schedule (seconds-since-effective), first entry at `seconds = 0`, strictly increasing thereafter; per-entry `powerW` finite and non-negative.
3387
3402
  - **Temperature trajectories** — strictly increasing `timestampIso`; `temperatureC ∈ [−50, 150]`.
3388
3403
 
3404
+ ### HeatingRodForecast
3405
+
3406
+ A **single** relative schedule whose entries pack every per-slot decision at once: the forecasted target temperature, whether the rod is planned to heat, and the announced available power. One entry per slot keeps the temperature trajectory and the heating decisions aligned by construction.
3407
+
3408
+ ```typescript
3409
+ import {
3410
+ ApplianceForecastResolutionEnum,
3411
+ HeatingRodForecast,
3412
+ } from '@enyo-energy/energy-app-sdk';
3413
+
3414
+ const forecast: HeatingRodForecast = {
3415
+ resolution: ApplianceForecastResolutionEnum.FifteenMinutes,
3416
+ relativeSchedule: [
3417
+ { seconds: 0, powerW: 2000, temperatureC: 48, heatingActive: true, availablePowerActive: true },
3418
+ { seconds: 900, powerW: 3000, temperatureC: 55, heatingActive: true, availablePowerActive: true },
3419
+ { seconds: 1800, powerW: 0, temperatureC: 60, heatingActive: false },
3420
+ ],
3421
+ estimatedSavings: { costSavings: 0.27, currency: 'EUR' },
3422
+ };
3423
+
3424
+ await forecasts.publishHeatingRodForecast('heating-rod-1', forecast);
3425
+ ```
3426
+
3427
+ Per-entry invariants:
3428
+
3429
+ - `seconds`: finite, non-negative; first entry `= 0`; consecutive entries spaced by exactly `resolution` (60s / 900s).
3430
+ - `powerW`: optional; finite and non-negative when present. It is the power the energy manager makes **available** — the rod may consume less.
3431
+ - `temperatureC`: optional; `∈ [−50, 150]`.
3432
+ - `heatingActive` / `availablePowerActive`: optional and independent of each other — both may be `true` for the same slot.
3433
+
3434
+ ### SmartPlugForecast
3435
+
3436
+ A smart plug is a pure on/off load, so its forecast is a **single** relative schedule of planned switching slots. Every entry carries the planned relay `state` **plus the trigger that motivates it** — the same trigger vocabulary the user composes automations from ([`EnyoAutomationTriggerTypeEnum`](#-the-model)) — and the id of the automation the decision came from. A consumer can therefore render *"off until 13:00, then on for two hours because PV surplus is above 2000 W"* without re-deriving the reasoning.
3437
+
3438
+ ```typescript
3439
+ import {
3440
+ ApplianceForecastResolutionEnum,
3441
+ EnyoAutomationTriggerTypeEnum,
3442
+ EnyoSmartPlugApplianceStateEnum,
3443
+ SmartPlugForecast,
3444
+ } from '@enyo-energy/energy-app-sdk';
3445
+
3446
+ const forecast: SmartPlugForecast = {
3447
+ resolution: ApplianceForecastResolutionEnum.FifteenMinutes,
3448
+ // Optional: which socket of a multi-channel plug this plan is for.
3449
+ channelIndex: 0,
3450
+ relativeSchedule: [
3451
+ // Right now: off — surplus is still below the user's threshold.
3452
+ { seconds: 0, state: EnyoSmartPlugApplianceStateEnum.Off },
3453
+ // In 15 minutes: on, because PV surplus is forecasted above the threshold.
3454
+ {
3455
+ seconds: 900,
3456
+ state: EnyoSmartPlugApplianceStateEnum.On,
3457
+ triggerType: EnyoAutomationTriggerTypeEnum.PvSurplusThreshold,
3458
+ automationId: 'pool-pump-on-surplus',
3459
+ powerW: 1200,
3460
+ },
3461
+ // In 30 minutes: still on — only to honour the 10-minute minimum runtime.
3462
+ {
3463
+ seconds: 1800,
3464
+ state: EnyoSmartPlugApplianceStateEnum.On,
3465
+ triggerType: EnyoAutomationTriggerTypeEnum.PvSurplusThreshold,
3466
+ automationId: 'pool-pump-on-surplus',
3467
+ powerW: 1200,
3468
+ minDurationHold: true,
3469
+ },
3470
+ ],
3471
+ estimatedSavings: { costSavings: 0.12, currency: 'EUR' },
3472
+ };
3473
+
3474
+ await forecasts.publishSmartPlugForecast('smart-plug-1', forecast);
3475
+ ```
3476
+
3477
+ Per-entry invariants:
3478
+
3479
+ - `seconds`: finite, non-negative; first entry `= 0`; consecutive entries spaced by exactly `resolution` (60s / 900s).
3480
+ - `state`: **required** on every entry — `On` or `Off`. Unlike the modulating appliances there is no "carry the previous value" fallback for the relay itself.
3481
+ - `triggerType`: optional; must be a member of `EnyoAutomationTriggerTypeEnum` (`pv-surplus-threshold`, `pv-surplus-below-threshold`, `below-price-limit`, `cheapest-share-of-day`, `schedule`). Explanatory metadata only — the authoritative command for the slot is `state`.
3482
+ - `automationId`: optional non-empty string — *which* configured rule produced the slot (`triggerType` says what *kind* of condition drives it).
3483
+ - `powerW`: optional; finite and non-negative — the expected draw of the connected load while `On`. Omit it rather than sending `0` for an `On` slot whose consumption is unknown.
3484
+ - `minDurationHold`: optional boolean — `true` marks a "tail" slot kept on only to honour the automation's `minDurationMinutes`.
3485
+
3486
+ Forecast-level `channelIndex` is optional and must be a non-negative integer.
3487
+
3488
+ ### AirConditioningForecast
3489
+
3490
+ A **single** relative schedule whose entries pack the planned operating mode, the optimization mode the plan was built for, the target and forecasted room temperatures, and the available-power announcement. Multi-split units are forecasted **one room at a time** — set `roomIndex` to the room index reported on the appliance metadata and publish one forecast per room.
3491
+
3492
+ ```typescript
3493
+ import {
3494
+ AirConditioningForecast,
3495
+ ApplianceForecastResolutionEnum,
3496
+ EnyoAirConditioningApplianceModeEnum,
3497
+ EnyoAirConditioningOptimizationModeEnum,
3498
+ } from '@enyo-energy/energy-app-sdk';
3499
+
3500
+ const forecast: AirConditioningForecast = {
3501
+ resolution: ApplianceForecastResolutionEnum.FifteenMinutes,
3502
+ roomIndex: 0,
3503
+ relativeSchedule: [
3504
+ // Right now: pre-cool the living room on PV surplus.
3505
+ {
3506
+ seconds: 0,
3507
+ powerW: 1500,
3508
+ mode: EnyoAirConditioningApplianceModeEnum.Cooling,
3509
+ optimizationMode: EnyoAirConditioningOptimizationModeEnum.PvSurplus,
3510
+ targetTemperatureC: 22,
3511
+ roomTemperatureC: 26,
3512
+ availablePowerActive: true,
3513
+ },
3514
+ // In 15 minutes: target reached, coast.
3515
+ {
3516
+ seconds: 900,
3517
+ powerW: 0,
3518
+ mode: EnyoAirConditioningApplianceModeEnum.Idle,
3519
+ roomTemperatureC: 22,
3520
+ },
3521
+ ],
3522
+ estimatedSavings: { costSavings: 0.48, currency: 'EUR', co2SavingsGrams: 140 },
3523
+ };
3524
+
3525
+ await forecasts.publishAirConditioningForecast('air-conditioning-1', forecast);
3526
+ ```
3527
+
3528
+ Per-entry invariants:
3529
+
3530
+ - `seconds`: finite, non-negative; first entry `= 0`; consecutive entries spaced by exactly `resolution` (60s / 900s).
3531
+ - `powerW`: optional; finite and non-negative — the power the energy manager makes **available**; the unit may consume less.
3532
+ - `mode`: optional; `Idle` / `Cooling` / `Heating`.
3533
+ - `optimizationMode`: optional; `PvSurplus` / `Boost`. Independent of `mode`.
3534
+ - `targetTemperatureC` / `roomTemperatureC`: optional; `∈ [−50, 150]`.
3535
+ - `availablePowerActive`: optional boolean; independent of `mode`.
3536
+
3537
+ Forecast-level `roomIndex` is optional and must be a non-negative integer.
3538
+
3389
3539
  ### Validators
3390
3540
 
3391
3541
  The validators that `publish*` runs internally are exported as standalone pure functions so apps can validate forecasts while building them — for instance, to surface user-facing errors in a planning UI before holding the forecast in state.
@@ -3395,6 +3545,9 @@ import {
3395
3545
  validateChargerForecast,
3396
3546
  validateBatteryCommandForecast,
3397
3547
  validateHeatpumpForecast,
3548
+ validateHeatingRodForecast,
3549
+ validateSmartPlugForecast,
3550
+ validateAirConditioningForecast,
3398
3551
  ApplianceCommandForecastValidationError,
3399
3552
  } from '@enyo-energy/energy-app-sdk';
3400
3553
 
@@ -3407,7 +3560,7 @@ try {
3407
3560
  }
3408
3561
  ```
3409
3562
 
3410
- Granular helpers are exported alongside the top-level validators: `validateChargerSchedule`, `validateBatterySchedule`, `validateDhwBoostWindows`, `validateRoomPreHeatingWindows`, `validateBufferTankBoostWindows`, `validatePowerAnnouncementSchedule`, `validateTemperatureForecast`.
3563
+ Granular helpers are exported alongside the top-level validators: `validateChargerSchedule`, `validateBatterySchedule`, `validateDhwBoostWindows`, `validateRoomPreHeatingWindows`, `validateBufferTankBoostWindows`, `validatePowerAnnouncementSchedule`, `validateTemperatureForecast`, `validateHeatingRodSchedule`, `validateHeatingRodScheduleEntry`, `validateSmartPlugSchedule`, `validateSmartPlugScheduleEntry`, `validateAirConditioningSchedule`, `validateAirConditioningScheduleEntry`.
3411
3564
 
3412
3565
  ## Automations
3413
3566
 
@@ -3685,6 +3838,78 @@ context: {
3685
3838
 
3686
3839
  `start` is the bar's zero, and it matters everywhere except energy: without it, a cold tank at 20 °C heading for 48 °C draws a 42 % bar before anything has happened.
3687
3840
 
3841
+ ### What a row states for the waterfall view
3842
+
3843
+ The „Wasserfall" screen draws one row per participant with a time track and a detail box under it. Six optional fields carry the facts only the energy manager holds — each one replaces something a consumer would otherwise have to guess:
3844
+
3845
+ | Field | On | What it makes exact |
3846
+ |---|---|---|
3847
+ | `plannedStartIso` | any row | „wartet · ab 11:05", and the card's „Als Nächstes" header — which is simply the earliest `plannedStartIso` among the rows that are not running. Absent = not planned again today. |
3848
+ | `waitingForRank` | any row | „wartet auf Speicher" — the `rank` of the row this one is queued behind, instead of parsing it out of the reason sentence. |
3849
+ | `offerEndsAtIso` | `Offered` rows | when a released offer lapses. |
3850
+ | `progress.targetReachedAtIso` | rows with a goal | „erreicht ca. 11:05" / „fertig ca. 12:40". The remaining distance can be subtracted from the triple; the *time* follows from the planned power over the coming slots, which is the planner's own arithmetic. |
3851
+ | `pvPowerW` / `gridPowerW` | drawing rows | „4,8 kW Sonne · 6,2 kW Netz". Unsolvable from the totals: one signed `powerW` per row plus a single site-level `GridImport` row cannot be split back out when two appliances draw while the site imports. |
3852
+
3853
+ `plannedStartIso` is stated on the **participant**, not on the appliance's command forecast, precisely so the rows that have no forecast can use it — a `FeedIn` row that expects to export from 17:40 says so there.
3854
+
3855
+ Two states join the enum, and both are stated rather than derived:
3856
+
3857
+ | State | Means | Why it cannot be derived |
3858
+ |---|---|---|
3859
+ | `DrawingOutsidePlan` | Drawing power the plan did not allocate — a defrost cycle, a manual start, the appliance's own comfort logic. The manager re-plans around it and the rows below get less until it stops. | Comparing measured power against a command forecast is guesswork twice over: an appliance whose app publishes no forecast could never be shown as off-plan, and a forecast one cycle stale paints a perfectly planned run as a deviation. |
3860
+ | `Offered` | Power was released for it to use at its own discretion, and it is not using it yet — „freigegeben, sie entscheidet selbst, wann". A good state, not a warning. | It used to be `NotAsking` plus a sentence, with the *window* recovered from `HeatpumpForecast.availablePowerActive` — so only a heatpump could ever show one. Now a heating rod, a smart plug or a charger can. |
3861
+
3862
+ `DrawingOutsidePlan` carries positive power like `Drawing`; `Offered` carries `powerW = 0` (an offer is not a draw) and may carry `offerEndsAtIso`. Both are mutually exclusive with `Drawing` on purpose: a row is either following the allocation or it is not, so a consumer needs no second rule to tell them apart.
3863
+
3864
+ Each new state needs its **sentence** like every other: enrich `reason.translation` per language before publishing. Two reason types exist for them — `ApplianceInitiatedDraw` (set `reason.powerW` to the draw being planned around) and `PowerOffered` (set `reason.powerW` to the power on offer).
3865
+
3866
+ ```typescript
3867
+ const snapshot = new EnergyDistributionSnapshotBuilder()
3868
+ .addAppliance({
3869
+ rank: 0,
3870
+ applianceId: 'heatpump-1',
3871
+ applianceType: EnyoApplianceTypeEnum.Heatpump,
3872
+ name: 'Wärmepumpe',
3873
+ state: EnyoDistributionParticipantStateEnum.DrawingOutsidePlan,
3874
+ powerW: 1800,
3875
+ reason: enrich({
3876
+ type: EnyoDataBusCommandReasonTypeEnum.ApplianceInitiatedDraw,
3877
+ powerW: 1800,
3878
+ }),
3879
+ pvPowerW: 1800,
3880
+ gridPowerW: 0,
3881
+ })
3882
+ .addAppliance({
3883
+ rank: 1,
3884
+ applianceId: 'heating-rod-1',
3885
+ applianceType: EnyoApplianceTypeEnum.HeatingRod,
3886
+ name: 'Heizstab',
3887
+ state: EnyoDistributionParticipantStateEnum.Offered,
3888
+ powerW: 0, // an offer is not a draw
3889
+ reason: enrich({type: EnyoDataBusCommandReasonTypeEnum.PowerOffered, powerW: 2000}),
3890
+ offerEndsAtIso: '2026-09-20T14:00:00.000Z',
3891
+ })
3892
+ .addAppliance({
3893
+ rank: 2,
3894
+ applianceId: 'charger-1',
3895
+ applianceType: EnyoApplianceTypeEnum.Charger,
3896
+ name: 'Wallbox Garage',
3897
+ state: EnyoDistributionParticipantStateEnum.NotAsking,
3898
+ powerW: 0,
3899
+ reason: enrich({type: EnyoDataBusCommandReasonTypeEnum.OtherApplianceTurn}),
3900
+ plannedStartIso: '2026-09-20T11:05:00.000Z', // "wartet · ab 11:05"
3901
+ waitingForRank: 1, // "wartet auf Heizstab"
3902
+ })
3903
+ .addFeedIn({
3904
+ powerW: 0,
3905
+ name: 'Einspeisung',
3906
+ plannedStartIso: '2026-09-20T17:40:00.000Z', // "kurz ab 17:40"
3907
+ })
3908
+ .build({slotStartMs: slotStart});
3909
+ ```
3910
+
3911
+ The validator holds these to the same standard as the rest: `plannedStartIso` may not predate the slot it is stated in (a stale plan rendered as an imminent one), `waitingForRank` must name another row of the same snapshot and never itself, `offerEndsAtIso` only appears on an `Offered` row, and a stated PV / grid split must add up to the row's own `powerW` — state one half alone if the other is unknown, because an unknown share is not zero.
3912
+
3688
3913
  ### Publishing a snapshot
3689
3914
 
3690
3915
  ```typescript
@@ -3747,13 +3972,13 @@ Each event carries the **complete** picture of the slot — render it as it arri
3747
3972
 
3748
3973
  | Helper | What it does |
3749
3974
  |---|---|
3750
- | `makeProgress({unit, start?, current, target})` | Builds an `EnyoDistributionProgress` with `percent` computed — `(current − start) / (target − start)`, clamped to 0–100. The single definition of how full a bar is, so no two consumers disagree by a rounding rule. |
3975
+ | `makeProgress({unit, start?, current, target, targetReachedAtIso?})` | Builds an `EnyoDistributionProgress` with `percent` computed — `(current − start) / (target − start)`, clamped to 0–100. The single definition of how full a bar is, so no two consumers disagree by a rounding rule. |
3751
3976
  | `progressPercent(input)` | The same arithmetic on its own, for a caller that already holds a stated triple. |
3752
3977
  | `progressFromAnnouncement(stated)` | Turns a manager's announced goal into a participant's progress, adding nothing but the percentage. |
3753
3978
  | `EnergyDistributionSnapshotBuilder` | Assembles the snapshot: orders rows by stated rank, stamps the timestamps, and keeps the measured rows structurally free of a bar. |
3754
- | `validateEnergyDistributionSnapshot(snapshot)` | Throws `EnergyDistributionValidationError` on the first violated invariant — duplicate ranks, a bar on a household row, a `Complete` without a stated `SessionComplete`, a percentage that does not follow from its own numbers, a "skipped" row still drawing power. |
3979
+ | `validateEnergyDistributionSnapshot(snapshot)` | Throws `EnergyDistributionValidationError` on the first violated invariant — duplicate ranks, a bar on a household row, a `Complete` without a stated `SessionComplete`, a percentage that does not follow from its own numbers, a "skipped" row still drawing power, a next start in the past, a dependency on a rank nobody holds, a PV / grid split that does not add up. |
3755
3980
 
3756
- New reason types came with this surface, so a skipped row can say *why* instead of falling back to a generic "scheduled optimization": `SessionComplete`, `AppliancePaused`, `NothingConnected`, `DeadlinePassed`, `WaitingForCheaperSlot`, `AboveOwnPriceLimit`, `BelowMinPower`, `OtherApplianceTurn`, `SupplyExhausted`, `OutsideSchedule` — grouped by the new `SessionState` and `Contention` reason categories.
3981
+ New reason types came with this surface, so a skipped row can say *why* instead of falling back to a generic "scheduled optimization": `SessionComplete`, `AppliancePaused`, `NothingConnected`, `DeadlinePassed`, `WaitingForCheaperSlot`, `AboveOwnPriceLimit`, `BelowMinPower`, `OtherApplianceTurn`, `SupplyExhausted`, `OutsideSchedule` — grouped by the new `SessionState` and `Contention` reason categories. `ApplianceInitiatedDraw` and `PowerOffered` came with the waterfall states above.
3757
3982
 
3758
3983
  ## Dynamic Grid Fees & Tariff Bonuses
3759
3984
 
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EnergyAppPackageFirmwareModeEnum = exports.EnergyAppPackageCategory = void 0;
3
+ exports.EnergyAppPackageFirmwareModeEnum = exports.EnergyAppPackageCompatibilityStatus = exports.EnergyAppPackageCategory = void 0;
4
4
  exports.defineEnergyAppPackage = defineEnergyAppPackage;
5
5
  const version_js_1 = require("./version.cjs");
6
6
  var EnergyAppPackageCategory;
@@ -27,6 +27,24 @@ var EnergyAppPackageCategory;
27
27
  EnergyAppPackageCategory["Vehicle"] = "vehicle";
28
28
  EnergyAppPackageCategory["Other"] = "other";
29
29
  })(EnergyAppPackageCategory || (exports.EnergyAppPackageCategory = EnergyAppPackageCategory = {}));
30
+ /**
31
+ * Whether a declared compatibility entry means "this works" or "this is known
32
+ * not to work".
33
+ *
34
+ * Used by {@link EnergyAppPackageCompatibilityVendor.status} and
35
+ * {@link EnergyAppPackageCompatibilityModel.status} so a package can
36
+ * list a vendor or model it has explicitly tested and found unsupported,
37
+ * instead of silently leaving it out. The enyo Store and onboarding flows can
38
+ * then tell the user "this device is not supported by this app" rather than
39
+ * showing nothing at all.
40
+ */
41
+ var EnergyAppPackageCompatibilityStatus;
42
+ (function (EnergyAppPackageCompatibilityStatus) {
43
+ /** The package supports this vendor or model. */
44
+ EnergyAppPackageCompatibilityStatus["Compatible"] = "compatible";
45
+ /** The package has been verified **not** to work with this vendor or model. */
46
+ EnergyAppPackageCompatibilityStatus["NotCompatible"] = "not-compatible";
47
+ })(EnergyAppPackageCompatibilityStatus || (exports.EnergyAppPackageCompatibilityStatus = EnergyAppPackageCompatibilityStatus = {}));
30
48
  /**
31
49
  * Enum form of {@link EnergyAppPackageFirmwareMode} for use in package
32
50
  * definitions.
@@ -213,6 +213,23 @@ export interface EnergyAppPackagePermission {
213
213
  /** Internal documentation describing what this permission is used for */
214
214
  internalComment: string;
215
215
  }
216
+ /**
217
+ * Whether a declared compatibility entry means "this works" or "this is known
218
+ * not to work".
219
+ *
220
+ * Used by {@link EnergyAppPackageCompatibilityVendor.status} and
221
+ * {@link EnergyAppPackageCompatibilityModel.status} so a package can
222
+ * list a vendor or model it has explicitly tested and found unsupported,
223
+ * instead of silently leaving it out. The enyo Store and onboarding flows can
224
+ * then tell the user "this device is not supported by this app" rather than
225
+ * showing nothing at all.
226
+ */
227
+ export declare enum EnergyAppPackageCompatibilityStatus {
228
+ /** The package supports this vendor or model. */
229
+ Compatible = "compatible",
230
+ /** The package has been verified **not** to work with this vendor or model. */
231
+ NotCompatible = "not-compatible"
232
+ }
216
233
  /**
217
234
  * A specific device model supported by an Energy App package.
218
235
  * the concrete models the package has been verified to work with.
@@ -249,6 +266,16 @@ export interface EnergyAppPackageCompatibilityModel {
249
266
  category?: EnergyAppPackageCategory;
250
267
  /** Optional internal note explaining model-specific caveats or limitations */
251
268
  internalComment?: string;
269
+ /**
270
+ * Whether this model is supported or explicitly unsupported.
271
+ *
272
+ * Defaults to {@link EnergyAppPackageCompatibilityStatus.Compatible} when
273
+ * omitted, so existing definitions keep their meaning. Set it to
274
+ * {@link EnergyAppPackageCompatibilityStatus.NotCompatible} to document a
275
+ * model you have tested and found not to work — use
276
+ * {@link internalComment} to say why.
277
+ */
278
+ status?: EnergyAppPackageCompatibilityStatus;
252
279
  /**
253
280
  * Important capabilities this specific model supports (e.g. charging the
254
281
  * battery from grid, limiting the charge, or forcing a heat pump DHW boost).
@@ -277,6 +304,16 @@ export interface EnergyAppPackageCompatibilityVendor {
277
304
  * worse than listing none.
278
305
  */
279
306
  models: EnergyAppPackageCompatibilityModel[];
307
+ /**
308
+ * Whether this vendor is supported or explicitly unsupported.
309
+ *
310
+ * Defaults to {@link EnergyAppPackageCompatibilityStatus.Compatible} when
311
+ * omitted. Set it to
312
+ * {@link EnergyAppPackageCompatibilityStatus.NotCompatible} to declare a
313
+ * brand the package is known not to work with; individual models may still
314
+ * override it with their own {@link EnergyAppPackageCompatibilityModel.status}.
315
+ */
316
+ status?: EnergyAppPackageCompatibilityStatus;
280
317
  /**
281
318
  * Marks this package as the default Energy App for the vendor when no
282
319
  * concrete model has been selected.
@@ -11,8 +11,17 @@ exports.validateHeatpumpScheduleEntry = validateHeatpumpScheduleEntry;
11
11
  exports.validateHeatingRodForecast = validateHeatingRodForecast;
12
12
  exports.validateHeatingRodSchedule = validateHeatingRodSchedule;
13
13
  exports.validateHeatingRodScheduleEntry = validateHeatingRodScheduleEntry;
14
+ exports.validateSmartPlugForecast = validateSmartPlugForecast;
15
+ exports.validateSmartPlugSchedule = validateSmartPlugSchedule;
16
+ exports.validateSmartPlugScheduleEntry = validateSmartPlugScheduleEntry;
17
+ exports.validateAirConditioningForecast = validateAirConditioningForecast;
18
+ exports.validateAirConditioningSchedule = validateAirConditioningSchedule;
19
+ exports.validateAirConditioningScheduleEntry = validateAirConditioningScheduleEntry;
14
20
  const enyo_appliance_command_forecast_js_1 = require("../../types/enyo-appliance-command-forecast.cjs");
21
+ const enyo_air_conditioning_appliance_js_1 = require("../../types/enyo-air-conditioning-appliance.cjs");
22
+ const enyo_automation_js_1 = require("../../types/enyo-automation.cjs");
15
23
  const enyo_data_bus_value_js_1 = require("../../types/enyo-data-bus-value.cjs");
24
+ const enyo_smart_plug_appliance_js_1 = require("../../types/enyo-smart-plug-appliance.cjs");
16
25
  /**
17
26
  * Thrown when a forecast payload passed to one of the validators (or to
18
27
  * {@link EnergyAppApplianceEnergyManagerForecast.publishChargerForecast}
@@ -107,6 +116,9 @@ function validateChargerSchedule(entries, resolution) {
107
116
  const entry = entries[i];
108
117
  validateSecondsField(entry.seconds, `relativeSchedule[${i}].seconds`);
109
118
  validatePowerW(entry.powerW, `relativeSchedule[${i}].powerW`);
119
+ if (entry.priceCtPerKwh !== undefined && !Number.isFinite(entry.priceCtPerKwh)) {
120
+ throw new ApplianceCommandForecastValidationError(`relativeSchedule[${i}].priceCtPerKwh=${entry.priceCtPerKwh} must be a finite number when provided (a negative price is legitimate; a non-number is not).`);
121
+ }
110
122
  if (entry.numberOfPhases !== undefined && ![1, 2, 3].includes(entry.numberOfPhases)) {
111
123
  throw new ApplianceCommandForecastValidationError(`relativeSchedule[${i}].numberOfPhases must be 1, 2, or 3; got ${entry.numberOfPhases}.`);
112
124
  }
@@ -219,6 +231,109 @@ function validateHeatingRodScheduleEntry(entry, fieldName) {
219
231
  validateBooleanField(entry.heatingActive, `${fieldName}.heatingActive`);
220
232
  validateBooleanField(entry.availablePowerActive, `${fieldName}.availablePowerActive`);
221
233
  }
234
+ /**
235
+ * Validates a {@link SmartPlugForecast}. Throws on the first violation —
236
+ * the error message names the offending field / index.
237
+ *
238
+ * The forecast carries a single relative schedule of on/off slots; every
239
+ * entry is validated by {@link validateSmartPlugScheduleEntry}.
240
+ */
241
+ function validateSmartPlugForecast(forecast) {
242
+ if (!forecast || typeof forecast !== 'object') {
243
+ throw new ApplianceCommandForecastValidationError('SmartPlugForecast must be an object.');
244
+ }
245
+ validateMetadata(forecast);
246
+ validateChannelOrRoomIndex(forecast.channelIndex, 'SmartPlugForecast.channelIndex');
247
+ validateSmartPlugSchedule(forecast.relativeSchedule, forecast.resolution);
248
+ }
249
+ /**
250
+ * Validates a smart plug relative schedule (the schedule used by
251
+ * {@link SmartPlugForecast.relativeSchedule}). Enforces that the
252
+ * schedule is non-empty, starts at `seconds = 0`, has entries spaced by
253
+ * exactly `resolution`, and that every per-entry value satisfies the
254
+ * invariants documented on {@link SmartPlugForecastScheduleEntry}.
255
+ */
256
+ function validateSmartPlugSchedule(entries, resolution) {
257
+ const stepSeconds = resolveResolutionSeconds(resolution);
258
+ validateNonEmptySchedule(entries, 'relativeSchedule');
259
+ for (let i = 0; i < entries.length; i++) {
260
+ validateSmartPlugScheduleEntry(entries[i], `relativeSchedule[${i}]`);
261
+ }
262
+ validateFirstEntryStartsAtZero(entries[0].seconds, 'relativeSchedule');
263
+ validateSecondsMatchResolution(entries.map((e) => e.seconds), stepSeconds, 'relativeSchedule');
264
+ }
265
+ /**
266
+ * Validates a single {@link SmartPlugForecastScheduleEntry}. Used by
267
+ * {@link validateSmartPlugSchedule} and exposed for callers that build
268
+ * entries incrementally.
269
+ *
270
+ * `state` is mandatory (a plug slot without a relay state carries no
271
+ * command); `triggerType`, when present, must be one of the
272
+ * {@link EnyoAutomationTriggerTypeEnum} members so consumers can switch
273
+ * on it exhaustively.
274
+ */
275
+ function validateSmartPlugScheduleEntry(entry, fieldName) {
276
+ validateSecondsField(entry.seconds, `${fieldName}.seconds`);
277
+ validateEnumField(entry.state, Object.values(enyo_smart_plug_appliance_js_1.EnyoSmartPlugApplianceStateEnum), `${fieldName}.state`, true);
278
+ validateEnumField(entry.triggerType, Object.values(enyo_automation_js_1.EnyoAutomationTriggerTypeEnum), `${fieldName}.triggerType`, false);
279
+ if (entry.automationId !== undefined) {
280
+ if (typeof entry.automationId !== 'string' || entry.automationId.length === 0) {
281
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.automationId must be a non-empty string when provided.`);
282
+ }
283
+ }
284
+ if (entry.powerW !== undefined) {
285
+ validatePowerW(entry.powerW, `${fieldName}.powerW`);
286
+ }
287
+ validateBooleanField(entry.minDurationHold, `${fieldName}.minDurationHold`);
288
+ }
289
+ /**
290
+ * Validates an {@link AirConditioningForecast}. Throws on the first
291
+ * violation — the error message names the offending field / index.
292
+ *
293
+ * The forecast carries a single relative schedule; every entry is
294
+ * validated by {@link validateAirConditioningScheduleEntry}.
295
+ */
296
+ function validateAirConditioningForecast(forecast) {
297
+ if (!forecast || typeof forecast !== 'object') {
298
+ throw new ApplianceCommandForecastValidationError('AirConditioningForecast must be an object.');
299
+ }
300
+ validateMetadata(forecast);
301
+ validateChannelOrRoomIndex(forecast.roomIndex, 'AirConditioningForecast.roomIndex');
302
+ validateAirConditioningSchedule(forecast.relativeSchedule, forecast.resolution);
303
+ }
304
+ /**
305
+ * Validates an air conditioning relative schedule (the schedule used by
306
+ * {@link AirConditioningForecast.relativeSchedule}). Enforces that the
307
+ * schedule is non-empty, starts at `seconds = 0`, has entries spaced by
308
+ * exactly `resolution`, and that every per-entry value falls in the
309
+ * plausible range documented on
310
+ * {@link AirConditioningForecastScheduleEntry}.
311
+ */
312
+ function validateAirConditioningSchedule(entries, resolution) {
313
+ const stepSeconds = resolveResolutionSeconds(resolution);
314
+ validateNonEmptySchedule(entries, 'relativeSchedule');
315
+ for (let i = 0; i < entries.length; i++) {
316
+ validateAirConditioningScheduleEntry(entries[i], `relativeSchedule[${i}]`);
317
+ }
318
+ validateFirstEntryStartsAtZero(entries[0].seconds, 'relativeSchedule');
319
+ validateSecondsMatchResolution(entries.map((e) => e.seconds), stepSeconds, 'relativeSchedule');
320
+ }
321
+ /**
322
+ * Validates a single {@link AirConditioningForecastScheduleEntry}. Used
323
+ * by {@link validateAirConditioningSchedule} and exposed for callers
324
+ * that build entries incrementally.
325
+ */
326
+ function validateAirConditioningScheduleEntry(entry, fieldName) {
327
+ validateSecondsField(entry.seconds, `${fieldName}.seconds`);
328
+ if (entry.powerW !== undefined) {
329
+ validatePowerW(entry.powerW, `${fieldName}.powerW`);
330
+ }
331
+ validateEnumField(entry.mode, Object.values(enyo_air_conditioning_appliance_js_1.EnyoAirConditioningApplianceModeEnum), `${fieldName}.mode`, false);
332
+ validateEnumField(entry.optimizationMode, Object.values(enyo_air_conditioning_appliance_js_1.EnyoAirConditioningOptimizationModeEnum), `${fieldName}.optimizationMode`, false);
333
+ validateTemperatureField(entry.targetTemperatureC, `${fieldName}.targetTemperatureC`);
334
+ validateTemperatureField(entry.roomTemperatureC, `${fieldName}.roomTemperatureC`);
335
+ validateBooleanField(entry.availablePowerActive, `${fieldName}.availablePowerActive`);
336
+ }
222
337
  function validateMetadata(forecast) {
223
338
  if (!(forecast.resolution in RESOLUTION_SECONDS)) {
224
339
  throw new ApplianceCommandForecastValidationError(`resolution is invalid: ${forecast.resolution}. Allowed values: ${Object.values(enyo_appliance_command_forecast_js_1.ApplianceForecastResolutionEnum).join(', ')}.`);
@@ -286,3 +401,22 @@ function validateSecondsMatchResolution(secondsList, stepSeconds, fieldName) {
286
401
  }
287
402
  }
288
403
  }
404
+ function validateEnumField(value, allowedValues, fieldName, required) {
405
+ if (value === undefined) {
406
+ if (required) {
407
+ throw new ApplianceCommandForecastValidationError(`${fieldName} is required. Allowed values: ${allowedValues.join(', ')}.`);
408
+ }
409
+ return;
410
+ }
411
+ if (!allowedValues.includes(value)) {
412
+ throw new ApplianceCommandForecastValidationError(`${fieldName} is invalid: ${value}. Allowed values: ${allowedValues.join(', ')}.`);
413
+ }
414
+ }
415
+ function validateChannelOrRoomIndex(value, fieldName) {
416
+ if (value === undefined) {
417
+ return;
418
+ }
419
+ if (!Number.isInteger(value) || value < 0) {
420
+ throw new ApplianceCommandForecastValidationError(`${fieldName}=${value} must be a non-negative integer when provided.`);
421
+ }
422
+ }
@@ -1,4 +1,4 @@
1
- import { ApplianceForecastResolutionEnum, BatteryCommandForecast, BatteryCommandForecastScheduleEntry, ChargerForecast, ChargerForecastScheduleEntry, HeatingRodForecast, HeatingRodForecastScheduleEntry, HeatpumpForecast, HeatpumpForecastScheduleEntry } from '../../types/enyo-appliance-command-forecast.cjs';
1
+ import { AirConditioningForecast, AirConditioningForecastScheduleEntry, ApplianceForecastResolutionEnum, BatteryCommandForecast, BatteryCommandForecastScheduleEntry, ChargerForecast, ChargerForecastScheduleEntry, HeatingRodForecast, HeatingRodForecastScheduleEntry, HeatpumpForecast, HeatpumpForecastScheduleEntry, SmartPlugForecast, SmartPlugForecastScheduleEntry } from '../../types/enyo-appliance-command-forecast.cjs';
2
2
  /**
3
3
  * Thrown when a forecast payload passed to one of the validators (or to
4
4
  * {@link EnergyAppApplianceEnergyManagerForecast.publishChargerForecast}
@@ -87,3 +87,53 @@ export declare function validateHeatingRodSchedule(entries: HeatingRodForecastSc
87
87
  * entries incrementally.
88
88
  */
89
89
  export declare function validateHeatingRodScheduleEntry(entry: HeatingRodForecastScheduleEntry, fieldName: string): void;
90
+ /**
91
+ * Validates a {@link SmartPlugForecast}. Throws on the first violation —
92
+ * the error message names the offending field / index.
93
+ *
94
+ * The forecast carries a single relative schedule of on/off slots; every
95
+ * entry is validated by {@link validateSmartPlugScheduleEntry}.
96
+ */
97
+ export declare function validateSmartPlugForecast(forecast: SmartPlugForecast): void;
98
+ /**
99
+ * Validates a smart plug relative schedule (the schedule used by
100
+ * {@link SmartPlugForecast.relativeSchedule}). Enforces that the
101
+ * schedule is non-empty, starts at `seconds = 0`, has entries spaced by
102
+ * exactly `resolution`, and that every per-entry value satisfies the
103
+ * invariants documented on {@link SmartPlugForecastScheduleEntry}.
104
+ */
105
+ export declare function validateSmartPlugSchedule(entries: SmartPlugForecastScheduleEntry[], resolution: ApplianceForecastResolutionEnum): void;
106
+ /**
107
+ * Validates a single {@link SmartPlugForecastScheduleEntry}. Used by
108
+ * {@link validateSmartPlugSchedule} and exposed for callers that build
109
+ * entries incrementally.
110
+ *
111
+ * `state` is mandatory (a plug slot without a relay state carries no
112
+ * command); `triggerType`, when present, must be one of the
113
+ * {@link EnyoAutomationTriggerTypeEnum} members so consumers can switch
114
+ * on it exhaustively.
115
+ */
116
+ export declare function validateSmartPlugScheduleEntry(entry: SmartPlugForecastScheduleEntry, fieldName: string): void;
117
+ /**
118
+ * Validates an {@link AirConditioningForecast}. Throws on the first
119
+ * violation — the error message names the offending field / index.
120
+ *
121
+ * The forecast carries a single relative schedule; every entry is
122
+ * validated by {@link validateAirConditioningScheduleEntry}.
123
+ */
124
+ export declare function validateAirConditioningForecast(forecast: AirConditioningForecast): void;
125
+ /**
126
+ * Validates an air conditioning relative schedule (the schedule used by
127
+ * {@link AirConditioningForecast.relativeSchedule}). Enforces that the
128
+ * schedule is non-empty, starts at `seconds = 0`, has entries spaced by
129
+ * exactly `resolution`, and that every per-entry value falls in the
130
+ * plausible range documented on
131
+ * {@link AirConditioningForecastScheduleEntry}.
132
+ */
133
+ export declare function validateAirConditioningSchedule(entries: AirConditioningForecastScheduleEntry[], resolution: ApplianceForecastResolutionEnum): void;
134
+ /**
135
+ * Validates a single {@link AirConditioningForecastScheduleEntry}. Used
136
+ * by {@link validateAirConditioningSchedule} and exposed for callers
137
+ * that build entries incrementally.
138
+ */
139
+ export declare function validateAirConditioningScheduleEntry(entry: AirConditioningForecastScheduleEntry, fieldName: string): void;
@@ -23,7 +23,8 @@ exports.progressFromAnnouncement = progressFromAnnouncement;
23
23
  * A full bar is NOT the same as a finished run: completion is stated with
24
24
  * {@link EnyoDataBusCommandReasonTypeEnum.SessionComplete}, never inferred from this number.
25
25
  *
26
- * @param input - The stated facts: unit, optional start, current and target.
26
+ * @param input - The stated facts: unit, optional start, current and target, and optionally
27
+ * when the plan expects the target to be reached.
27
28
  * @returns The progress object, ready to put on a participant.
28
29
  * @throws {RangeError} When `target` equals `start`, or when any value is not finite — a bar
29
30
  * with no distance to travel has no meaningful fill, and silently reporting `0` or `100` for
@@ -59,6 +60,9 @@ function makeProgress(input) {
59
60
  };
60
61
  if (input.start !== undefined)
61
62
  progress.start = input.start;
63
+ if (input.targetReachedAtIso !== undefined) {
64
+ progress.targetReachedAtIso = input.targetReachedAtIso;
65
+ }
62
66
  return progress;
63
67
  }
64
68
  /**