@enyo-energy/energy-app-sdk 1.50.0 → 1.52.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 +45 -1
  2. package/dist/cjs/energy-app.cjs +13 -0
  3. package/dist/cjs/energy-app.d.cts +12 -0
  4. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  5. package/dist/cjs/implementations/heatpump/heatpump-control-validators.cjs +126 -0
  6. package/dist/cjs/implementations/heatpump/heatpump-control-validators.d.cts +62 -1
  7. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +19 -0
  8. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +4 -1
  9. package/dist/cjs/index.cjs +2 -0
  10. package/dist/cjs/index.d.cts +2 -0
  11. package/dist/cjs/integrations/heatpump-integration-energy-app.cjs +121 -0
  12. package/dist/cjs/integrations/heatpump-integration-energy-app.d.cts +97 -1
  13. package/dist/cjs/packages/energy-app-vacations.cjs +2 -0
  14. package/dist/cjs/packages/energy-app-vacations.d.cts +48 -0
  15. package/dist/cjs/types/enyo-authentication.cjs +5 -1
  16. package/dist/cjs/types/enyo-authentication.d.cts +16 -0
  17. package/dist/cjs/types/enyo-data-bus-value.cjs +27 -1
  18. package/dist/cjs/types/enyo-data-bus-value.d.cts +166 -0
  19. package/dist/cjs/types/enyo-heatpump-appliance.cjs +36 -0
  20. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +58 -1
  21. package/dist/cjs/types/enyo-vacation.cjs +22 -0
  22. package/dist/cjs/types/enyo-vacation.d.cts +72 -0
  23. package/dist/cjs/version.cjs +1 -1
  24. package/dist/cjs/version.d.cts +1 -1
  25. package/dist/energy-app.d.ts +12 -0
  26. package/dist/energy-app.js +13 -0
  27. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  28. package/dist/implementations/heatpump/heatpump-control-validators.d.ts +62 -1
  29. package/dist/implementations/heatpump/heatpump-control-validators.js +125 -1
  30. package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +4 -1
  31. package/dist/implementations/heatpump/heatpump-metadata-validators.js +19 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +2 -0
  34. package/dist/integrations/heatpump-integration-energy-app.d.ts +97 -1
  35. package/dist/integrations/heatpump-integration-energy-app.js +122 -1
  36. package/dist/packages/energy-app-vacations.d.ts +48 -0
  37. package/dist/packages/energy-app-vacations.js +1 -0
  38. package/dist/types/enyo-authentication.d.ts +16 -0
  39. package/dist/types/enyo-authentication.js +4 -0
  40. package/dist/types/enyo-data-bus-value.d.ts +166 -0
  41. package/dist/types/enyo-data-bus-value.js +26 -0
  42. package/dist/types/enyo-heatpump-appliance.d.ts +58 -1
  43. package/dist/types/enyo-heatpump-appliance.js +36 -0
  44. package/dist/types/enyo-vacation.d.ts +72 -0
  45. package/dist/types/enyo-vacation.js +19 -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
@@ -1864,6 +1864,19 @@ const history = await roomTemperatures.getRoomTemperatureHistory({
1864
1864
 
1865
1865
  Requires the `RoomTemperatures` permission.
1866
1866
 
1867
+ #### `useVacations(): EnergyAppVacations`
1868
+
1869
+ Read the customer's absences (vacation mode), as planned in the enyo app — every absence that is not over yet, planned or running; they never overlap. Each `EnyoVacation` carries `startIso` / `endIso` and the customer's choices per appliance category: `heatpump.heating` — `mode` while away (`EnyoVacationHeatingModeEnum`: `FrostProtection` = `VACATION_FROST_PROTECTION_C` 7 °C, `Lowered` to `loweredTemperatureC` between `VACATION_LOWERED_MIN_C` and `VACATION_LOWERED_MAX_C` (10–20 °C), or `Normal`) and `preheatBeforeReturn` —, `heatpump.domesticHotWater.pause`, and `battery.gridServing` / `battery.minSocAtReturnPercent`. An early return moves `endIso` to that moment and fires the listener. Read-only. The energy manager turns an absence into [VacationStartedV1 / VacationStoppedV1](#vacationstartedv1--vacationstoppedv1).
1870
+
1871
+ ```typescript
1872
+ const vacations = energyApp.useVacations();
1873
+
1874
+ applyVacations(await vacations.getVacations());
1875
+ vacations.listenForVacationsChanged((all) => applyVacations(all));
1876
+ ```
1877
+
1878
+ Requires the `EnergyManager` permission.
1879
+
1867
1880
  #### `useGridConnectionPoint(): EnergyAppGridConnectionPoint`
1868
1881
 
1869
1882
  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.
@@ -3891,13 +3904,14 @@ Drives a heatpump. Manages building / DHW overheating commands and grid-power-av
3891
3904
 
3892
3905
  > **Preferred: control heatpumps with the V3 messages.** `HeatpumpControlV3` (power, power sources, which zones to heat now, until when), `HeatpumpControlCancelV3` (cancel a session) and `HeatpumpControlStatusV3` (progress) are the preferred way to control a heatpump — for energy managers sending commands and for integrations receiving them. Use them for all new work. `HeatpumpOverheatingV1`, `SetHeatpumpAvailablePowerV2` and `HeatpumpAvailablePowerAnnouncementV1` are deprecated and only remain for heatpumps that do not declare `ControlV3`. See [HeatpumpControlV3](#heatpumpcontrolv3). Likewise, a heatpump offers its flexibility with `HeatpumpFlexibilityAnnouncementV3` rather than the generic `ApplianceFlexibilityAnnouncementV1` / `V2` — see [HeatpumpFlexibilityAnnouncementV3](#heatpumpflexibilityannouncementv3).
3893
3906
 
3894
- - **Subscribed commands:** `HeatpumpControlV3`, `HeatpumpControlCancelV3`, `HeatpumpProgramSwitchV3`, `SetHeatpumpHeatingCurveOffsetV1`, `HeatpumpOverheatingV1` (deprecated), `HeatpumpAvailablePowerAnnouncementV1` (deprecated), `SetHeatpumpAvailablePowerV2` (deprecated), `SetHeatpumpRoomTemperatureV1`, `GridOperatorPowerLimitationV1` (broadcast)
3907
+ - **Subscribed commands:** `HeatpumpControlV3`, `HeatpumpControlCancelV3`, `HeatpumpProgramSwitchV3`, `SetHeatpumpHeatingCurveOffsetV1`, `VacationStartedV1`, `VacationStoppedV1`, `HeatpumpOverheatingV1` (deprecated), `HeatpumpAvailablePowerAnnouncementV1` (deprecated), `SetHeatpumpAvailablePowerV2` (deprecated), `SetHeatpumpRoomTemperatureV1`, `GridOperatorPowerLimitationV1` (broadcast)
3895
3908
  - **Implement:** `handleHeatpumpOverheating`, `handleHeatpumpAvailablePowerAnnouncement`, `handleSetHeatpumpAvailablePower`, `handleGridOperatorPowerLimitation`
3896
3909
  - **Optional overrides:**
3897
3910
  - `handleSetHeatpumpRoomTemperature` — write a measured room temperature into a heating circuit. Answers `NotSupported` by default; override it when the heatpump declares `RoomTemperatureInput`.
3898
3911
  - `handleHeatpumpControl` — support `HeatpumpControlV3` (see below). Answers `NotSupported` by default; override it and declare `ControlV3`.
3899
3912
  - `handleHeatpumpProgramSwitch(request)` — switch a zone to comfort or eco, or back to its time program (see [HeatpumpProgramSwitchV3](#heatpumpprogramswitchv3)). Answers `NotSupported` by default; override it and declare `ProgramSwitch`.
3900
3913
  - `handleHeatpumpHeatingCurveOffset(message)` — shift a heating circuit's curve permanently, relative to its initial curve (see [SetHeatpumpHeatingCurveOffsetV1](#setheatpumpheatingcurveoffsetv1)). Answers `NotSupported` by default; override it and declare `HeatingCurveOffset`.
3914
+ - `handleVacationStarted(message)` / `handleVacationStopped(message)` — the customer is away: heat with frost protection or lowered and / or pause hot water, then restore everything (see [VacationStartedV1 / VacationStoppedV1](#vacationstartedv1--vacationstoppedv1)). Answer `NotSupported` by default; override both and declare `VacationFrostProtection`, `VacationLowered` and / or `VacationHotWaterPause`.
3901
3915
  - `handleHeatpumpControlCancel` — withdraw a cancelled session on the device. By default `Accepted` for heatpumps declaring `ControlV3`, `NotSupported` otherwise.
3902
3916
  - `onHeatpumpControlEnded(applianceId, cause)` — return the heatpump to its own behaviour when a session ends; `cause` is `HeatpumpControlEndCauseEnum.Expired`, `Ended` or `Cancelled`. Does nothing by default.
3903
3917
  - `resolveHeatpumpMetadata(applianceId)` — the metadata actions are validated against. Defaults to the stored appliance's `heatpump` metadata.
@@ -3996,6 +4010,33 @@ protected async handleHeatpumpHeatingCurveOffset(message: EnyoDataBusSetHeatpump
3996
4010
  }
3997
4011
  ```
3998
4012
 
4013
+ #### VacationStartedV1 / VacationStoppedV1
4014
+
4015
+ The customer is away (`useVacations()`): `VacationStartedV1` carries the customer's preferences, nested by appliance type — `heatpump` (heating while away, whether hot water is paused) and `battery` — and the integration decides how to realise them, ideally with the device's own holiday / absence mode. `VacationStoppedV1` ends the vacation (return, early return or removed): restore everything so the zones follow their own time program again. Both are answered with the usual `CommandAcknowledgeV1`.
4016
+
4017
+ ```typescript
4018
+ // VacationStartedV1
4019
+ {
4020
+ vacationId: 'vacation-1',
4021
+ returnIso: '2026-12-14T18:00:00.000Z', // planned return; safety net: end everything by then on your own
4022
+ heatpump: {
4023
+ heating: { mode: EnyoVacationHeatingEnum.FrostProtection, targetTemperatureC: 7 }, // absent = heat as usual
4024
+ domesticHotWater: { pause: true }, // absent or pause: false = hot water as usual; legionella protection keeps running
4025
+ },
4026
+ // battery: { gridServing: true, minSocAtReturnPercent: 50 }, // for batteries with their own vacation handling; not sent by the enyo energy manager today
4027
+ reason: { type: EnyoDataBusCommandReasonTypeEnum.UserRequest },
4028
+ }
4029
+ // VacationStoppedV1
4030
+ { vacationId: 'vacation-1' }
4031
+ ```
4032
+
4033
+ - **Only to heatpumps declaring a vacation feature.** `heatpump.heating.mode` `FrostProtection` (`EnyoVacationHeatingEnum`, the device's own frost protection ≈ `VACATION_FROST_PROTECTION_C` 7 °C) needs `VacationFrostProtection`; `Lowered` needs `VacationLowered` and a `targetTemperatureC` inside the appliance-level `vacationLoweredRangeC` (the setpoint the device can hold for all its circuits); `heatpump.domesticHotWater.pause: true` needs `VacationHotWaterPause`. A heatpump without any vacation feature gets the eco fallback via `HeatpumpProgramSwitchV3`.
4034
+ - **Idempotent, the complete wanted state.** The energy manager re-sends `VacationStartedV1` whenever the wanted state changes and at least hourly. Pre-heating for the return stays with the energy manager: when heating resumes it re-sends the message without `heatpump.heating`, when hot water resumes with `domesticHotWater.pause: false`, and when nothing is left it sends `VacationStoppedV1`.
4035
+ - **Ends by itself.** Write `returnIso` to the device as the end of its holiday mode, or end everything yourself at `returnIso` if no `VacationStoppedV1` arrives.
4036
+ - **Stop is always accepted.** Answer `VacationStoppedV1` `Accepted` even when nothing runs.
4037
+ - **Precedence.** While a vacation heating state is active the energy manager sends no `HeatpumpProgramSwitchV3` and no `HeatpumpControlV3` room action for the heatpump; while `heatpump.domesticHotWater.pause` is set, no hot water boosts.
4038
+ - **Validation** (run by the base class before the handlers, or `validateVacationStarted(data, metadata)` / `validateVacationStopped(data)` directly): malformed data is `Rejected`; a `heatpump.heating` mode whose feature is missing, a lowered target outside `vacationLoweredRangeC` or `heatpump.domesticHotWater.pause: true` without `VacationHotWaterPause` is `NotSupported`. A stop is only rejected when malformed.
4039
+
3999
4040
  #### HeatpumpFlexibilityAnnouncementV3
4000
4041
 
4001
4042
  The heatpump's offer, in the vocabulary of `HeatpumpControlV3`: per zone (`target` + `index`), how far it can be raised right now (`maxDeltaK` above `setpointC`, room in Kelvin of room temperature) and the smallest raise that actually starts it (`minDeltaK`, e.g. the remaining hysteresis), with which heat source at what power (`heatSources`: compressor and heating rod as separate entries, each with a `power` band and an optional `estimatedElectricalKwh` for the full raise), and when (`earliestStartIso` / `latestStartIso`, `minDurationMinutes`). An optional `required: {deltaK, byIso}` turns a "may" into a "must, by then" — e.g. hot water dropping below comfort at 18:00. Declare `FlexibilityAnnouncementV3` on the heatpump; for such heatpumps it supersedes the generic `ApplianceFlexibilityAnnouncementV1` / `V2`.
@@ -4032,6 +4073,9 @@ Beyond tanks and circuits, the heatpump metadata (`EnyoHeatpumpApplianceMetadata
4032
4073
  | `RoomTemperature` | `heatingCircuits[].roomTemperatureC` in `HeatpumpTemperaturesUpdateV1`; where it is measured goes in `heatingCircuits[].roomTemperatureSource` (`None` / `Internal` / `External`) and, for an enyo sensor, `roomTemperatureSensor` |
4033
4074
  | `RoomTemperatureInput` | The heatpump accepts measured room temperatures via `SetHeatpumpRoomTemperatureV1` |
4034
4075
  | `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) |
4076
+ | `VacationFrostProtection` | The heatpump's app handles `VacationStartedV1` with `heatpump.heating.mode` `FrostProtection` — the device's own frost protection while the customer is away |
4077
+ | `VacationLowered` | The heatpump's app handles `VacationStartedV1` with `heatpump.heating.mode` `Lowered`. `vacationLoweredRangeC` (required) — the setpoints (°C) the device can hold for all its circuits |
4078
+ | `VacationHotWaterPause` | The heatpump's app handles `VacationStartedV1` with `heatpump.domesticHotWater.pause: true`. Declare it only if legionella protection keeps running |
4035
4079
  | — | `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 |
4036
4080
  | — | `building.recommendedHeatingCurve` — the curve the building actually needs, learned from the days the room held its target |
4037
4081
  | `PvSurplusSignal` | The heatpump takes `availablePowerW` of `HeatpumpControlV3` as a PV surplus signal (e.g. Lambda register 102). Supersedes the deprecated `AvailablePowerAnnouncement` |
@@ -578,6 +578,19 @@ class EnergyApp {
578
578
  useRoomTemperatures() {
579
579
  return this.energyAppSdk.useRoomTemperatures();
580
580
  }
581
+ /**
582
+ * Gets the Vacations API for reading the customer's absences (vacation
583
+ * mode): departure and return, what the heatpump heats to meanwhile,
584
+ * whether hot water pauses, whether to pre-heat for the return and how the
585
+ * battery is used. Read them with `getVacations()` and react to changes
586
+ * with `listenForVacationsChanged()`.
587
+ * @returns The Vacations API instance
588
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EnergyManager`
589
+ * permission is not granted.
590
+ */
591
+ useVacations() {
592
+ return this.energyAppSdk.useVacations();
593
+ }
581
594
  /**
582
595
  * Gets the Timeseries Store API for storing the app's own timeseries —
583
596
  * numbers and text — and querying them back raw or aggregated by the host.
@@ -55,6 +55,7 @@ import { EnergyAppWebhook } from "./packages/energy-app-webhook.cjs";
55
55
  import { EnergyAppSubscriptions } from "./packages/energy-app-subscriptions.cjs";
56
56
  import { EnergyAppRoom } from "./packages/energy-app-room.cjs";
57
57
  import { EnergyAppRoomTemperatures } from "./packages/energy-app-room-temperatures.cjs";
58
+ import { EnergyAppVacations } from "./packages/energy-app-vacations.cjs";
58
59
  import { EnergyAppTimeseriesStore } from "./packages/energy-app-timeseries-store.cjs";
59
60
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
60
61
  /**
@@ -494,6 +495,17 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
494
495
  * permission is not granted.
495
496
  */
496
497
  useRoomTemperatures(): EnergyAppRoomTemperatures;
498
+ /**
499
+ * Gets the Vacations API for reading the customer's absences (vacation
500
+ * mode): departure and return, what the heatpump heats to meanwhile,
501
+ * whether hot water pauses, whether to pre-heat for the return and how the
502
+ * battery is used. Read them with `getVacations()` and react to changes
503
+ * with `listenForVacationsChanged()`.
504
+ * @returns The Vacations API instance
505
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EnergyManager`
506
+ * permission is not granted.
507
+ */
508
+ useVacations(): EnergyAppVacations;
497
509
  /**
498
510
  * Gets the Timeseries Store API for storing the app's own timeseries —
499
511
  * numbers and text — and querying them back raw or aggregated by the host.
@@ -54,6 +54,7 @@ import { EnergyAppWebhook } from "./packages/energy-app-webhook.cjs";
54
54
  import { EnergyAppSubscriptions } from "./packages/energy-app-subscriptions.cjs";
55
55
  import { EnergyAppRoom } from "./packages/energy-app-room.cjs";
56
56
  import { EnergyAppRoomTemperatures } from "./packages/energy-app-room-temperatures.cjs";
57
+ import { EnergyAppVacations } from "./packages/energy-app-vacations.cjs";
57
58
  import { EnergyAppTimeseriesStore } from "./packages/energy-app-timeseries-store.cjs";
58
59
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
59
60
  export declare enum EnergyAppStateEnum {
@@ -205,6 +206,8 @@ export interface EnyoEnergyAppSdk {
205
206
  useRoom: () => EnergyAppRoom;
206
207
  /** Get the Room Temperatures API for reading the current and past temperatures measured in the rooms of the home */
207
208
  useRoomTemperatures: () => EnergyAppRoomTemperatures;
209
+ /** Get the Vacations API for reading the absences (vacation mode) the customer planned in the enyo app */
210
+ useVacations: () => EnergyAppVacations;
208
211
  /** Get the Timeseries Store API for storing the app's own number and text timeseries and querying them raw or aggregated */
209
212
  useTimeseriesStore: () => EnergyAppTimeseriesStore;
210
213
  }
@@ -6,6 +6,8 @@ exports.validateHeatpumpControlAction = validateHeatpumpControlAction;
6
6
  exports.computeHeatpumpSetpointRaise = computeHeatpumpSetpointRaise;
7
7
  exports.validateHeatpumpProgramSwitch = validateHeatpumpProgramSwitch;
8
8
  exports.validateHeatpumpHeatingCurveOffset = validateHeatpumpHeatingCurveOffset;
9
+ exports.validateVacationStarted = validateVacationStarted;
10
+ exports.validateVacationStopped = validateVacationStopped;
9
11
  const enyo_data_bus_value_js_1 = require("../../types/enyo-data-bus-value.cjs");
10
12
  const enyo_heatpump_appliance_js_1 = require("../../types/enyo-heatpump-appliance.cjs");
11
13
  /**
@@ -345,3 +347,127 @@ function validateHeatpumpHeatingCurveOffset(data, metadata) {
345
347
  }
346
348
  return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
347
349
  }
350
+ /**
351
+ * Whether a value is a parsable ISO 8601 timestamp.
352
+ *
353
+ * @param value - The value to check.
354
+ * @returns `true` for a string `Date.parse` understands.
355
+ */
356
+ function isIsoTimestamp(value) {
357
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
358
+ }
359
+ /**
360
+ * Validates a `VacationStartedV1` against the heatpump's metadata and returns
361
+ * the answer it deserves.
362
+ *
363
+ * Used by `HeatpumpIntegrationEnergyApp` before it calls
364
+ * `handleVacationStarted`; an integration handling the message itself can call
365
+ * it too, and the energy manager can use it to decide between the vacation
366
+ * message and the eco fallback.
367
+ *
368
+ * Rules, in order — the first that fails decides:
369
+ * 1. Malformed data (**Rejected**): `vacationId` is not a non-empty string,
370
+ * `returnIso` is not an ISO 8601 timestamp, `heatpump.heating` has an
371
+ * unknown `mode` or a non-finite `targetTemperatureC`,
372
+ * `heatpump.domesticHotWater.pause` is not a boolean, or `battery` has a
373
+ * non-boolean `gridServing` or a `minSocAtReturnPercent` outside 0…100.
374
+ * 2. `heatpump.heating` asks for a mode whose feature is missing —
375
+ * `VacationFrostProtection` or `VacationLowered` (**NotSupported**).
376
+ * 3. A lowered `targetTemperatureC` outside `vacationLoweredRangeC`, or no
377
+ * range declared (**NotSupported**).
378
+ * 4. `heatpump.domesticHotWater.pause: true` without `VacationHotWaterPause`
379
+ * (**NotSupported**).
380
+ *
381
+ * The `battery` part is only checked for its shape — heatpumps ignore it.
382
+ *
383
+ * Without metadata only rule 1 can be checked.
384
+ *
385
+ * @param data - The message's data.
386
+ * @param metadata - The heatpump's metadata, if known.
387
+ * @returns The answer: `Accepted` when every rule holds.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * const result = validateVacationStarted(message.data, appliance.heatpump);
392
+ * if (result.answer !== EnyoCommandAcknowledgeAnswerEnum.Accepted) console.warn(result.rejectionReason);
393
+ * ```
394
+ */
395
+ function validateVacationStarted(data, metadata) {
396
+ const rejected = (rejectionReason) => ({ answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, rejectionReason });
397
+ const notSupported = (rejectionReason) => ({ answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported, rejectionReason });
398
+ if (typeof data.vacationId !== 'string' || data.vacationId.length === 0) {
399
+ return rejected(`vacationId must be a non-empty string; got ${data.vacationId}.`);
400
+ }
401
+ if (!isIsoTimestamp(data.returnIso))
402
+ return rejected(`returnIso must be an ISO 8601 timestamp; got ${data.returnIso}.`);
403
+ const heating = data.heatpump?.heating;
404
+ if (heating) {
405
+ if (!Object.values(enyo_data_bus_value_js_1.EnyoVacationHeatingEnum).includes(heating.mode)) {
406
+ return rejected(`heatpump.heating.mode must be one of ${Object.values(enyo_data_bus_value_js_1.EnyoVacationHeatingEnum).join(', ')}; got ${heating.mode}.`);
407
+ }
408
+ if (typeof heating.targetTemperatureC !== 'number' || !Number.isFinite(heating.targetTemperatureC)) {
409
+ return rejected(`heatpump.heating.targetTemperatureC must be a finite number; got ${heating.targetTemperatureC}.`);
410
+ }
411
+ }
412
+ const hotWater = data.heatpump?.domesticHotWater;
413
+ if (hotWater && typeof hotWater.pause !== 'boolean') {
414
+ return rejected(`heatpump.domesticHotWater.pause must be a boolean; got ${hotWater.pause}.`);
415
+ }
416
+ const battery = data.battery;
417
+ if (battery) {
418
+ if (typeof battery.gridServing !== 'boolean') {
419
+ return rejected(`battery.gridServing must be a boolean; got ${battery.gridServing}.`);
420
+ }
421
+ const soc = battery.minSocAtReturnPercent;
422
+ if (typeof soc !== 'number' || !Number.isFinite(soc) || soc < 0 || soc > 100) {
423
+ return rejected(`battery.minSocAtReturnPercent must be a number between 0 and 100; got ${soc}.`);
424
+ }
425
+ }
426
+ if (!metadata)
427
+ return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
428
+ const features = metadata.availableFeatures ?? [];
429
+ if (heating?.mode === enyo_data_bus_value_js_1.EnyoVacationHeatingEnum.FrostProtection
430
+ && !features.includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationFrostProtection)) {
431
+ return notSupported('The heatpump does not support vacation frost protection (no VacationFrostProtection).');
432
+ }
433
+ if (heating?.mode === enyo_data_bus_value_js_1.EnyoVacationHeatingEnum.Lowered) {
434
+ if (!features.includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationLowered)) {
435
+ return notSupported('The heatpump does not support a lowered vacation temperature (no VacationLowered).');
436
+ }
437
+ const range = metadata.vacationLoweredRangeC;
438
+ if (!range)
439
+ return notSupported('The heatpump declares no vacationLoweredRangeC.');
440
+ if (heating.targetTemperatureC < range.minC || heating.targetTemperatureC > range.maxC) {
441
+ return notSupported(`heatpump.heating.targetTemperatureC ${heating.targetTemperatureC} °C is outside vacationLoweredRangeC ${range.minC}…${range.maxC} °C.`);
442
+ }
443
+ }
444
+ if (hotWater?.pause && !features.includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationHotWaterPause)) {
445
+ return notSupported('The heatpump does not support pausing hot water while away (no VacationHotWaterPause).');
446
+ }
447
+ return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
448
+ }
449
+ /**
450
+ * Validates a `VacationStoppedV1` and returns the answer it deserves. It is
451
+ * only `Rejected` when malformed (`vacationId` not a non-empty string) — a
452
+ * stop must be accepted even when nothing runs, so it needs no metadata.
453
+ *
454
+ * Used by `HeatpumpIntegrationEnergyApp` before it calls
455
+ * `handleVacationStopped`.
456
+ *
457
+ * @param data - The message's data.
458
+ * @returns The answer: `Accepted` unless the data is malformed.
459
+ *
460
+ * @example
461
+ * ```typescript
462
+ * const result = validateVacationStopped(message.data);
463
+ * ```
464
+ */
465
+ function validateVacationStopped(data) {
466
+ if (typeof data.vacationId !== 'string' || data.vacationId.length === 0) {
467
+ return {
468
+ answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected,
469
+ rejectionReason: `vacationId must be a non-empty string; got ${data.vacationId}.`,
470
+ };
471
+ }
472
+ return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
473
+ }
@@ -1,4 +1,4 @@
1
- import { EnyoCommandAcknowledgeActionResult, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusHeatpumpFlexibilityAnnouncementV3, EnyoDataBusHeatpumpProgramSwitchV3, EnyoDataBusSetHeatpumpHeatingCurveOffsetV1, EnyoHeatpumpControlAction, EnyoHeatpumpControlTargetEnum, EnyoPowerSourceShare } from '../../types/enyo-data-bus-value.cjs';
1
+ import { EnyoCommandAcknowledgeActionResult, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusHeatpumpFlexibilityAnnouncementV3, EnyoDataBusHeatpumpProgramSwitchV3, EnyoDataBusSetHeatpumpHeatingCurveOffsetV1, EnyoDataBusVacationStartedV1, EnyoDataBusVacationStoppedV1, EnyoHeatpumpControlAction, EnyoHeatpumpControlTargetEnum, EnyoPowerSourceShare } from '../../types/enyo-data-bus-value.cjs';
2
2
  import { EnyoHeatpumpApplianceAvailableFeaturesEnum, EnyoHeatpumpApplianceMetadata } from '../../types/enyo-heatpump-appliance.cjs';
3
3
  /**
4
4
  * The overheating feature each `HeatpumpControlV3` target needs — and that a
@@ -212,3 +212,64 @@ export interface HeatpumpHeatingCurveOffsetValidationResult {
212
212
  * ```
213
213
  */
214
214
  export declare function validateHeatpumpHeatingCurveOffset(data: EnyoDataBusSetHeatpumpHeatingCurveOffsetV1['data'], metadata: EnyoHeatpumpApplianceMetadata | undefined): HeatpumpHeatingCurveOffsetValidationResult;
215
+ /** The answer {@link validateVacationStarted} or {@link validateVacationStopped} gives. */
216
+ export interface HeatpumpVacationValidationResult {
217
+ /** `Accepted` when every rule holds. */
218
+ answer: EnyoCommandAcknowledgeAnswerEnum;
219
+ /** Why the command was rejected or is not supported. */
220
+ rejectionReason?: string;
221
+ }
222
+ /**
223
+ * Validates a `VacationStartedV1` against the heatpump's metadata and returns
224
+ * the answer it deserves.
225
+ *
226
+ * Used by `HeatpumpIntegrationEnergyApp` before it calls
227
+ * `handleVacationStarted`; an integration handling the message itself can call
228
+ * it too, and the energy manager can use it to decide between the vacation
229
+ * message and the eco fallback.
230
+ *
231
+ * Rules, in order — the first that fails decides:
232
+ * 1. Malformed data (**Rejected**): `vacationId` is not a non-empty string,
233
+ * `returnIso` is not an ISO 8601 timestamp, `heatpump.heating` has an
234
+ * unknown `mode` or a non-finite `targetTemperatureC`,
235
+ * `heatpump.domesticHotWater.pause` is not a boolean, or `battery` has a
236
+ * non-boolean `gridServing` or a `minSocAtReturnPercent` outside 0…100.
237
+ * 2. `heatpump.heating` asks for a mode whose feature is missing —
238
+ * `VacationFrostProtection` or `VacationLowered` (**NotSupported**).
239
+ * 3. A lowered `targetTemperatureC` outside `vacationLoweredRangeC`, or no
240
+ * range declared (**NotSupported**).
241
+ * 4. `heatpump.domesticHotWater.pause: true` without `VacationHotWaterPause`
242
+ * (**NotSupported**).
243
+ *
244
+ * The `battery` part is only checked for its shape — heatpumps ignore it.
245
+ *
246
+ * Without metadata only rule 1 can be checked.
247
+ *
248
+ * @param data - The message's data.
249
+ * @param metadata - The heatpump's metadata, if known.
250
+ * @returns The answer: `Accepted` when every rule holds.
251
+ *
252
+ * @example
253
+ * ```typescript
254
+ * const result = validateVacationStarted(message.data, appliance.heatpump);
255
+ * if (result.answer !== EnyoCommandAcknowledgeAnswerEnum.Accepted) console.warn(result.rejectionReason);
256
+ * ```
257
+ */
258
+ export declare function validateVacationStarted(data: EnyoDataBusVacationStartedV1['data'], metadata: EnyoHeatpumpApplianceMetadata | undefined): HeatpumpVacationValidationResult;
259
+ /**
260
+ * Validates a `VacationStoppedV1` and returns the answer it deserves. It is
261
+ * only `Rejected` when malformed (`vacationId` not a non-empty string) — a
262
+ * stop must be accepted even when nothing runs, so it needs no metadata.
263
+ *
264
+ * Used by `HeatpumpIntegrationEnergyApp` before it calls
265
+ * `handleVacationStopped`.
266
+ *
267
+ * @param data - The message's data.
268
+ * @returns The answer: `Accepted` unless the data is malformed.
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * const result = validateVacationStopped(message.data);
273
+ * ```
274
+ */
275
+ export declare function validateVacationStopped(data: EnyoDataBusVacationStoppedV1['data']): HeatpumpVacationValidationResult;
@@ -450,6 +450,9 @@ function assertValidHeatpumpTimeProgram(program) {
450
450
  * - Heating curve offset: errors from {@link checkHeatingCurveOffset}.
451
451
  * Warning: `HeatingCurveOffset` is declared but no circuit has a
452
452
  * `heatingCurveOffsetRangeK` — no offset other than `0` could be accepted.
453
+ * - Error: `VacationLowered` is declared without a `vacationLoweredRangeC`,
454
+ * a `vacationLoweredRangeC` is set without `VacationLowered`, or the range
455
+ * is not finite or has `minC` above `maxC`.
453
456
  * - SG Ready relays: errors and warnings from {@link checkSgReadyRelays}.
454
457
  *
455
458
  * @param metadata - The heatpump metadata to validate.
@@ -505,6 +508,22 @@ function validateHeatpumpAdvancedMetadata(metadata) {
505
508
  && !circuits.some((c) => c.heatingCurveOffsetRangeK)) {
506
509
  warnings.push('HeatingCurveOffset is declared but no heating circuit has a heatingCurveOffsetRangeK — only offset 0 could be accepted');
507
510
  }
511
+ const declaresVacationLowered = features.includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationLowered);
512
+ const vacationRange = metadata.vacationLoweredRangeC;
513
+ if (declaresVacationLowered && !vacationRange) {
514
+ errors.push('VacationLowered is declared but vacationLoweredRangeC is missing');
515
+ }
516
+ if (vacationRange) {
517
+ if (!declaresVacationLowered) {
518
+ errors.push('vacationLoweredRangeC is set but the heatpump does not declare VacationLowered');
519
+ }
520
+ if (!isFiniteNumber(vacationRange.minC) || !isFiniteNumber(vacationRange.maxC)) {
521
+ errors.push('vacationLoweredRangeC.minC and maxC must be finite numbers');
522
+ }
523
+ else if (vacationRange.minC > vacationRange.maxC) {
524
+ errors.push(`vacationLoweredRangeC ${vacationRange.minC}…${vacationRange.maxC} °C has minC above maxC`);
525
+ }
526
+ }
508
527
  for (const circuit of circuits) {
509
528
  const prefix = `heatingCircuits[index=${circuit.index}]`;
510
529
  if (circuit.heatingCurve)
@@ -117,9 +117,12 @@ export declare function assertValidHeatpumpTimeProgram(program: EnyoHeatpumpTime
117
117
  * - Heating curve offset: errors from {@link checkHeatingCurveOffset}.
118
118
  * Warning: `HeatingCurveOffset` is declared but no circuit has a
119
119
  * `heatingCurveOffsetRangeK` — no offset other than `0` could be accepted.
120
+ * - Error: `VacationLowered` is declared without a `vacationLoweredRangeC`,
121
+ * a `vacationLoweredRangeC` is set without `VacationLowered`, or the range
122
+ * is not finite or has `minC` above `maxC`.
120
123
  * - SG Ready relays: errors and warnings from {@link checkSgReadyRelays}.
121
124
  *
122
125
  * @param metadata - The heatpump metadata to validate.
123
126
  * @returns The validation result.
124
127
  */
125
- export declare function validateHeatpumpAdvancedMetadata(metadata: Pick<EnyoHeatpumpApplianceMetadata, 'availableFeatures' | 'heatingCircuits' | 'domesticHotWater' | 'building'> & Partial<Pick<EnyoHeatpumpApplianceMetadata, 'sgReadyRelays' | 'additionalDevices'>>): HeatpumpMetadataValidationResult;
128
+ export declare function validateHeatpumpAdvancedMetadata(metadata: Pick<EnyoHeatpumpApplianceMetadata, 'availableFeatures' | 'heatingCircuits' | 'domesticHotWater' | 'building'> & Partial<Pick<EnyoHeatpumpApplianceMetadata, 'sgReadyRelays' | 'additionalDevices' | 'vacationLoweredRangeC'>>): HeatpumpMetadataValidationResult;
@@ -205,6 +205,8 @@ __exportStar(require("./types/enyo-room.cjs"), exports);
205
205
  __exportStar(require("./packages/energy-app-room.cjs"), exports);
206
206
  __exportStar(require("./types/enyo-room-temperature.cjs"), exports);
207
207
  __exportStar(require("./packages/energy-app-room-temperatures.cjs"), exports);
208
+ __exportStar(require("./types/enyo-vacation.cjs"), exports);
209
+ __exportStar(require("./packages/energy-app-vacations.cjs"), exports);
208
210
  __exportStar(require("./implementations/subscriptions/define-subscription-plan.cjs"), exports);
209
211
  __exportStar(require("./implementations/subscriptions/subscription-plan-validators.cjs"), exports);
210
212
  __exportStar(require("./implementations/subscriptions/subscription-offer-validators.cjs"), exports);
@@ -189,6 +189,8 @@ export * from './types/enyo-room.cjs';
189
189
  export * from './packages/energy-app-room.cjs';
190
190
  export * from './types/enyo-room-temperature.cjs';
191
191
  export * from './packages/energy-app-room-temperatures.cjs';
192
+ export * from './types/enyo-vacation.cjs';
193
+ export * from './packages/energy-app-vacations.cjs';
192
194
  export * from './implementations/subscriptions/define-subscription-plan.cjs';
193
195
  export * from './implementations/subscriptions/subscription-plan-validators.cjs';
194
196
  export * from './implementations/subscriptions/subscription-offer-validators.cjs';
@@ -78,6 +78,15 @@ function combineHeatpumpControlAnswers(answers) {
78
78
  * {@link handleHeatpumpHeatingCurveOffset} and declare
79
79
  * {@link EnyoHeatpumpApplianceAvailableFeaturesEnum.HeatingCurveOffset}).
80
80
  * The base class validates it with {@link checkHeatingCurveOffset} first.
81
+ * - `VacationStartedV1` / `VacationStoppedV1` — the customer is away: heat
82
+ * with frost protection or lowered and / or pause hot water, then restore
83
+ * everything (optional: override {@link handleVacationStarted} /
84
+ * {@link handleVacationStopped} and declare
85
+ * {@link EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationFrostProtection},
86
+ * {@link EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationLowered} and / or
87
+ * {@link EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationHotWaterPause}).
88
+ * The base class validates them with {@link validateVacationStarted} /
89
+ * {@link validateVacationStopped} first.
81
90
  * - `GridOperatorPowerLimitationV1` — §14a EnWG broadcast (handled in base).
82
91
  *
83
92
  * Publishes `HeatpumpFlexibilityAnnouncementV3` offers via
@@ -138,6 +147,8 @@ class HeatpumpIntegrationEnergyApp extends integration_energy_app_js_1.Integrati
138
147
  this.registerCommandHandler(enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.HeatpumpControlCancelV3, (msg) => this.dispatchHeatpumpControlCancel(msg));
139
148
  this.registerCommandHandler(enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.HeatpumpProgramSwitchV3, (msg) => this.dispatchHeatpumpProgramSwitch(msg));
140
149
  this.registerCommandHandler(enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetHeatpumpHeatingCurveOffsetV1, (msg) => this.dispatchHeatpumpHeatingCurveOffset(msg));
150
+ this.registerCommandHandler(enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.VacationStartedV1, (msg) => this.dispatchVacationStarted(msg));
151
+ this.registerCommandHandler(enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.VacationStoppedV1, (msg) => this.dispatchVacationStopped(msg));
141
152
  }
142
153
  /**
143
154
  * Stops the integration, drops every running `HeatpumpControlV3` session
@@ -558,6 +569,99 @@ class HeatpumpIntegrationEnergyApp extends integration_energy_app_js_1.Integrati
558
569
  const metadata = await this.resolveHeatpumpMetadata(message.applianceId);
559
570
  return (0, heatpump_control_validators_js_1.validateHeatpumpHeatingCurveOffset)(message.data, metadata);
560
571
  }
572
+ /**
573
+ * Handles a `VacationStartedV1` command: the customer is away, so bring the
574
+ * heatpump into the wanted state — heat with frost protection or at a
575
+ * lowered room temperature (`message.data.heatpump.heating`; absent = heat
576
+ * as usual) and pause hot water
577
+ * (`message.data.heatpump.domesticHotWater.pause`). How to realise it is up
578
+ * to the integration; the `battery` part is not meant for heatpumps. Answers `NotSupported` by default. Override
579
+ * it together with {@link handleVacationStopped} and declare the vacation
580
+ * features the device supports
581
+ * ({@link EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationFrostProtection},
582
+ * {@link EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationLowered} with
583
+ * `vacationLoweredRangeC`,
584
+ * {@link EnyoHeatpumpApplianceAvailableFeaturesEnum.VacationHotWaterPause}).
585
+ *
586
+ * What an implementation must do:
587
+ * - Prefer the device's own holiday / absence mode and write
588
+ * `message.data.returnIso` as its end, so everything ends even when the
589
+ * app or the energy manager is not running. Without one, write the
590
+ * setpoints and restore them yourself at `returnIso` if no
591
+ * `VacationStoppedV1` arrives.
592
+ * - Treat the message as idempotent and as the complete wanted state: the
593
+ * energy manager re-sends it whenever the wanted state changes and at
594
+ * least hourly. A message without `heatpump.heating` means heating runs
595
+ * as usual again (e.g. pre-heating for the return); without
596
+ * `heatpump.domesticHotWater` or with `pause: false`, hot water is
597
+ * reheated again.
598
+ * - Keep legionella protection running while hot water is paused.
599
+ * - Answer `Rejected` (with `rejectionReason`) when the state cannot be
600
+ * written right now, e.g. the device is in a manual mode.
601
+ *
602
+ * The base class has already validated the message with
603
+ * {@link validateVacationStarted}: the data is well-formed, the features
604
+ * for the requested heating mode and hot water pause are declared and a
605
+ * lowered target lies inside `vacationLoweredRangeC`.
606
+ *
607
+ * @param _message - The vacation started command.
608
+ * @returns `Accepted` once the state is written, `Rejected` (with
609
+ * `rejectionReason`) when it cannot be written right now,
610
+ * `NotSupported` when the integration does not support vacations.
611
+ *
612
+ * @example
613
+ * ```ts
614
+ * protected async handleVacationStarted(message: EnyoDataBusVacationStartedV1) {
615
+ * const {heatpump, returnIso} = message.data;
616
+ * await this.device.writeHolidayMode({
617
+ * roomSetpointC: heatpump?.heating?.targetTemperatureC,
618
+ * pauseHotWater: heatpump?.domesticHotWater?.pause ?? false,
619
+ * endIso: returnIso,
620
+ * });
621
+ * return {answer: EnyoCommandAcknowledgeAnswerEnum.Accepted};
622
+ * }
623
+ * ```
624
+ */
625
+ async handleVacationStarted(_message) {
626
+ return {
627
+ answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported,
628
+ rejectionReason: 'Heatpump integration does not support VacationStartedV1',
629
+ };
630
+ }
631
+ /**
632
+ * Handles a `VacationStoppedV1` command: the vacation is over (return,
633
+ * early return or removed), so restore everything
634
+ * {@link handleVacationStarted} changed and let the heatpump follow its
635
+ * own time program again. Answers `NotSupported` by default; override it
636
+ * together with {@link handleVacationStarted}.
637
+ *
638
+ * An implementation must answer `Accepted` even when nothing runs (e.g.
639
+ * the device already ended its holiday mode at `returnIso`), and
640
+ * `Rejected` (with `rejectionReason`) only when the device cannot be
641
+ * restored right now.
642
+ *
643
+ * The base class has already validated the message with
644
+ * {@link validateVacationStopped}.
645
+ *
646
+ * @param _message - The vacation stopped command.
647
+ * @returns `Accepted` once everything is restored (or nothing ran),
648
+ * `Rejected` (with `rejectionReason`) when it cannot be restored right
649
+ * now, `NotSupported` when the integration does not support vacations.
650
+ *
651
+ * @example
652
+ * ```ts
653
+ * protected async handleVacationStopped(_message: EnyoDataBusVacationStoppedV1) {
654
+ * await this.device.endHolidayMode();
655
+ * return {answer: EnyoCommandAcknowledgeAnswerEnum.Accepted};
656
+ * }
657
+ * ```
658
+ */
659
+ async handleVacationStopped(_message) {
660
+ return {
661
+ answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported,
662
+ rejectionReason: 'Heatpump integration does not support VacationStoppedV1',
663
+ };
664
+ }
561
665
  /**
562
666
  * Publishes a `HeatpumpValuesUpdateV1` message reporting the heatpump's
563
667
  * current operation mode and energy values to the data bus.
@@ -721,6 +825,23 @@ class HeatpumpIntegrationEnergyApp extends integration_energy_app_js_1.Integrati
721
825
  }
722
826
  return this.handleHeatpumpHeatingCurveOffset(message);
723
827
  }
828
+ /** Validates a `VacationStartedV1` against the metadata, then calls {@link handleVacationStarted}. */
829
+ async dispatchVacationStarted(message) {
830
+ const metadata = await this.resolveHeatpumpMetadata(message.applianceId);
831
+ const validation = (0, heatpump_control_validators_js_1.validateVacationStarted)(message.data, metadata);
832
+ if (validation.answer !== enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted) {
833
+ return validation;
834
+ }
835
+ return this.handleVacationStarted(message);
836
+ }
837
+ /** Validates a `VacationStoppedV1`, then calls {@link handleVacationStopped}. */
838
+ async dispatchVacationStopped(message) {
839
+ const validation = (0, heatpump_control_validators_js_1.validateVacationStopped)(message.data);
840
+ if (validation.answer !== enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted) {
841
+ return validation;
842
+ }
843
+ return this.handleVacationStopped(message);
844
+ }
724
845
  async dispatchHeatpumpControlCancel(message) {
725
846
  const response = await this.handleHeatpumpControlCancel(message);
726
847
  if (response.answer !== enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported