@enyo-energy/energy-app-sdk 1.42.0 → 1.43.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 (38) hide show
  1. package/README.md +43 -1
  2. package/dist/cjs/implementations/appliances/appliance-manager.cjs +56 -0
  3. package/dist/cjs/implementations/appliances/appliance-manager.d.cts +34 -1
  4. package/dist/cjs/implementations/heatpump/heatpump-control-validators.cjs +61 -0
  5. package/dist/cjs/implementations/heatpump/heatpump-control-validators.d.cts +38 -1
  6. package/dist/cjs/implementations/heatpump/heatpump-heating-curve-offset.cjs +119 -0
  7. package/dist/cjs/implementations/heatpump/heatpump-heating-curve-offset.d.cts +77 -0
  8. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +72 -0
  9. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +3 -0
  10. package/dist/cjs/index.cjs +1 -0
  11. package/dist/cjs/index.d.cts +1 -0
  12. package/dist/cjs/integrations/heatpump-integration-energy-app.cjs +89 -0
  13. package/dist/cjs/integrations/heatpump-integration-energy-app.d.cts +75 -1
  14. package/dist/cjs/types/enyo-data-bus-value.cjs +2 -0
  15. package/dist/cjs/types/enyo-data-bus-value.d.cts +60 -0
  16. package/dist/cjs/types/enyo-heatpump-appliance.cjs +27 -1
  17. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +151 -1
  18. package/dist/cjs/version.cjs +1 -1
  19. package/dist/cjs/version.d.cts +1 -1
  20. package/dist/implementations/appliances/appliance-manager.d.ts +34 -1
  21. package/dist/implementations/appliances/appliance-manager.js +56 -0
  22. package/dist/implementations/heatpump/heatpump-control-validators.d.ts +38 -1
  23. package/dist/implementations/heatpump/heatpump-control-validators.js +60 -0
  24. package/dist/implementations/heatpump/heatpump-heating-curve-offset.d.ts +77 -0
  25. package/dist/implementations/heatpump/heatpump-heating-curve-offset.js +113 -0
  26. package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +3 -0
  27. package/dist/implementations/heatpump/heatpump-metadata-validators.js +72 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +1 -0
  30. package/dist/integrations/heatpump-integration-energy-app.d.ts +75 -1
  31. package/dist/integrations/heatpump-integration-energy-app.js +90 -1
  32. package/dist/types/enyo-data-bus-value.d.ts +60 -0
  33. package/dist/types/enyo-data-bus-value.js +2 -0
  34. package/dist/types/enyo-heatpump-appliance.d.ts +151 -1
  35. package/dist/types/enyo-heatpump-appliance.js +26 -0
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -3666,12 +3666,13 @@ Drives a heatpump. Manages building / DHW overheating commands and grid-power-av
3666
3666
 
3667
3667
  > **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).
3668
3668
 
3669
- - **Subscribed commands:** `HeatpumpControlV3`, `HeatpumpControlCancelV3`, `HeatpumpProgramSwitchV3`, `HeatpumpOverheatingV1` (deprecated), `HeatpumpAvailablePowerAnnouncementV1` (deprecated), `SetHeatpumpAvailablePowerV2` (deprecated), `SetHeatpumpRoomTemperatureV1`, `GridOperatorPowerLimitationV1` (broadcast)
3669
+ - **Subscribed commands:** `HeatpumpControlV3`, `HeatpumpControlCancelV3`, `HeatpumpProgramSwitchV3`, `SetHeatpumpHeatingCurveOffsetV1`, `HeatpumpOverheatingV1` (deprecated), `HeatpumpAvailablePowerAnnouncementV1` (deprecated), `SetHeatpumpAvailablePowerV2` (deprecated), `SetHeatpumpRoomTemperatureV1`, `GridOperatorPowerLimitationV1` (broadcast)
3670
3670
  - **Implement:** `handleHeatpumpOverheating`, `handleHeatpumpAvailablePowerAnnouncement`, `handleSetHeatpumpAvailablePower`, `handleGridOperatorPowerLimitation`
3671
3671
  - **Optional overrides:**
3672
3672
  - `handleSetHeatpumpRoomTemperature` — write a measured room temperature into a heating circuit. Answers `NotSupported` by default; override it when the heatpump declares `RoomTemperatureInput`.
3673
3673
  - `handleHeatpumpControl` — support `HeatpumpControlV3` (see below). Answers `NotSupported` by default; override it and declare `ControlV3`.
3674
3674
  - `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`.
3675
+ - `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`.
3675
3676
  - `handleHeatpumpControlCancel` — withdraw a cancelled session on the device. By default `Accepted` for heatpumps declaring `ControlV3`, `NotSupported` otherwise.
3676
3677
  - `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.
3677
3678
  - `resolveHeatpumpMetadata(applianceId)` — the metadata actions are validated against. Defaults to the stored appliance's `heatpump` metadata.
@@ -3732,6 +3733,44 @@ Switches one heating circuit or DHW zone to its **comfort** or **eco** setpoint
3732
3733
  - **Owner's consent for price-driven eco.** An energy manager switches zones to eco for price reasons only while the general setting `heatpumpEcoModeInExpensiveTimes` (and `heatpumpControl`) is `true` — see [General settings](#general-settings).
3733
3734
  - **Validation** (run by the base class before the handler, or `validateHeatpumpProgramSwitch(data, metadata)` directly): target `Room` / `Dhw`, a known mode, a valid `untilIso` for comfort / eco (`Rejected`); `ProgramSwitch` declared and the mode listed for the zone (`NotSupported`); a known zone index (`Rejected`).
3734
3735
 
3736
+ #### SetHeatpumpHeatingCurveOffsetV1
3737
+
3738
+ Shifts the heating curve of one heating circuit in parallel — **permanently**, until the next message. The energy manager uses it to lower the curve as far as the measured room temperature allows (less flow temperature, roughly 2.5 % less heating electricity per K).
3739
+
3740
+ ```typescript
3741
+ {
3742
+ heatingCircuitIndex: 0,
3743
+ offsetK: -1.5, // K of flow temperature, relative to the initial curve; 0 restores it
3744
+ }
3745
+ ```
3746
+
3747
+ - **Relative to the initial curve, never to the current one.** Before the first offset other than `0`, the integration stores the curve set on the device as `heatingCircuits[].initialHeatingCurve` (`ApplianceManager.rememberHeatpumpInitialHeatingCurve` keeps an already stored one). The same message may therefore arrive twice.
3748
+ - **`offsetK: 0` restores** the initial curve exactly, including vendor parameters, then clears it (`ApplianceManager.clearHeatpumpInitialHeatingCurve`, which also sets `activeHeatingCurveOffsetK` to `0`).
3749
+ - **No expiry.** If the hub is offline nobody could reset it anyway; the stored initial curve is the safety net.
3750
+ - **Vendor units.** Convert to whatever the device shifts in (K of room temperature, steps, …) and report the effective shift in K of flow temperature as `heatingCircuits[].activeHeatingCurveOffsetK`, read back from the device. The energy manager waits for it before the next change.
3751
+ - **Changed by hand.** If the curve read from the device differs from `initialHeatingCurve` shifted by `activeHeatingCurveOffsetK` by more than 0.5 K at any point (`isHeatingCurveManuallyChanged(initial, offsetK, read)`), store the read curve as the new `initialHeatingCurve`, set `activeHeatingCurveOffsetK` to `0` and log it in a plain sentence.
3752
+ - **Alongside `HeatpumpControlV3` and `HeatpumpProgramSwitchV3`.** Boost and eco act on top; the offset is the new base.
3753
+ - **Validation** (run by the base class before the handler via `checkHeatingCurveOffset`, or `validateHeatpumpHeatingCurveOffset(data, metadata)` directly): a valid index and finite `offsetK` (`Rejected`); `HeatingCurveOffset` declared (`NotSupported`); a known circuit and — unless `offsetK` is `0` — within `heatingCurveOffsetRangeK` (`Rejected`).
3754
+ - **Customer settings.** Whether to optimise at all, and which rooms of the home the circuit heats (`roomIds`, whether or not a room has a sensor), live in `heatingCircuits[].curveOptimization`. The host resolves them into `rooms`: per room its target temperature (set in the enyo app, `HEATPUMP_CURVE_OPTIMIZATION_DEFAULT_TARGET_C` = 21 °C until then) and the temperature sensors assigned to it. Rooms without a sensor are left out; the room furthest below its target counts, and the curve is lowered by at most `HEATPUMP_CURVE_OPTIMIZATION_MAX_LOWERING_K` (3 K). Integrations only read them.
3755
+
3756
+ ```typescript
3757
+ protected async handleHeatpumpHeatingCurveOffset(message: EnyoDataBusSetHeatpumpHeatingCurveOffsetV1) {
3758
+ const {heatingCircuitIndex: index, offsetK} = message.data;
3759
+ if (offsetK !== 0) {
3760
+ await this.appliances.rememberHeatpumpInitialHeatingCurve(message.applianceId, index, await this.readCurve(index));
3761
+ }
3762
+ await this.device.writeCurveShift(index, offsetK);
3763
+ if (offsetK === 0) {
3764
+ await this.appliances.clearHeatpumpInitialHeatingCurve(message.applianceId, index);
3765
+ } else {
3766
+ await this.appliances.updateHeatpumpHeatingCircuit(message.applianceId, index, {
3767
+ activeHeatingCurveOffsetK: await this.readEffectiveShiftK(index),
3768
+ });
3769
+ }
3770
+ return {answer: EnyoCommandAcknowledgeAnswerEnum.Accepted};
3771
+ }
3772
+ ```
3773
+
3735
3774
  #### HeatpumpFlexibilityAnnouncementV3
3736
3775
 
3737
3776
  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`.
@@ -3767,6 +3806,9 @@ Beyond tanks and circuits, the heatpump metadata (`EnyoHeatpumpApplianceMetadata
3767
3806
  | `ProgramSwitch` | `heatingCircuits[].supportedProgramModes` / `programModeTemperaturesC` / `activeProgramMode` and the same on `domesticHotWater[]` — which zones can be switched to comfort / eco with `HeatpumpProgramSwitchV3`, at which setpoints, and the mode they run in now |
3768
3807
  | `RoomTemperature` | `heatingCircuits[].roomTemperatureC` in `HeatpumpTemperaturesUpdateV1`; where it is measured goes in `heatingCircuits[].roomTemperatureSource` (`None` / `Internal` / `External`) and, for an enyo sensor, `roomTemperatureSensor` |
3769
3808
  | `RoomTemperatureInput` | The heatpump accepts measured room temperatures via `SetHeatpumpRoomTemperatureV1` |
3809
+ | `HeatingCurveOffset` | The heatpump's app shifts a circuit's curve via `SetHeatpumpHeatingCurveOffsetV1`. `heatingCircuits[].heatingCurveOffsetRangeK` (allowed range, must contain 0), `activeHeatingCurveOffsetK` (effective shift read back) and `initialHeatingCurve` (the curve before the first shift) |
3810
+ | — | `heatingCircuits[].curveOptimization` — the customer's choice for the room-temperature based curve optimisation: `enabled` (off by default), `roomIds` (the rooms the circuit heats) and, resolved by the host, `rooms` — per room `roomId`, `name`, its `targetRoomTemperatureC` (16–26 °C) and the `sensors` assigned to it. Customer-owned; integrations only read it |
3811
+ | — | `building.recommendedHeatingCurve` — the curve the building actually needs, learned from the days the room held its target |
3770
3812
  | `PvSurplusSignal` | The heatpump takes `availablePowerW` of `HeatpumpControlV3` as a PV surplus signal (e.g. Lambda register 102). Supersedes the deprecated `AvailablePowerAnnouncement` |
3771
3813
  | `ControlV3` | The heatpump's app handles `HeatpumpControlV3` / `HeatpumpControlCancelV3` and reports `HeatpumpControlStatusV3`. The energy manager sends V3 only to heatpumps declaring it. Needs at least one overheating feature to be actionable |
3772
3814
  | `FlexibilityAnnouncementV3` | The heatpump publishes `HeatpumpFlexibilityAnnouncementV3` offers; the energy manager plans from them instead of deriving the headroom from metadata. Needs at least one overheating feature to be actionable |
@@ -618,6 +618,62 @@ class ApplianceManager {
618
618
  await this.energyApp.useAppliances().save(this.mergeApplianceData(appliance, { heatpump: { domesticHotWater: zones } }), applianceId);
619
619
  await this.primeCacheFromSdk(applianceId);
620
620
  }
621
+ /**
622
+ * Remembers the heating curve a circuit had before the first
623
+ * `SetHeatpumpHeatingCurveOffsetV1` changed it, as the circuit's
624
+ * `initialHeatingCurve` — the curve `offsetK: 0` restores. Call it before
625
+ * writing the first offset other than `0`.
626
+ *
627
+ * Keeps an already stored initial curve: every later offset is relative to
628
+ * the curve before the *first* change, so it must not be overwritten with
629
+ * an already shifted one. Works like {@link updateHeatpumpHeatingCircuit}
630
+ * otherwise.
631
+ *
632
+ * Does nothing when the appliance does not exist.
633
+ *
634
+ * @param applianceId The ID of the heatpump appliance
635
+ * @param index The `index` of the heating circuit
636
+ * @param curve The curve currently set on the device
637
+ * @returns `true` when the curve was stored, `false` when an initial curve
638
+ * was already stored or the appliance does not exist
639
+ * @throws {ApplianceManagerDisposedError} when called after {@link dispose}
640
+ */
641
+ async rememberHeatpumpInitialHeatingCurve(applianceId, index, curve) {
642
+ this.throwIfDisposed();
643
+ const appliance = await this.energyApp.useAppliances().getById(applianceId);
644
+ if (!appliance)
645
+ return false;
646
+ const circuit = appliance.heatpump?.heatingCircuits?.find(c => c.index === index);
647
+ if (circuit?.initialHeatingCurve)
648
+ return false;
649
+ await this.updateHeatpumpHeatingCircuit(applianceId, index, { initialHeatingCurve: curve });
650
+ return true;
651
+ }
652
+ /**
653
+ * Forgets a circuit's `initialHeatingCurve` after the integration restored
654
+ * it on the device (`offsetK: 0`), and sets `activeHeatingCurveOffsetK` to
655
+ * `0`. Other circuits and fields are left untouched.
656
+ *
657
+ * Does nothing when the appliance or the circuit does not exist.
658
+ *
659
+ * @param applianceId The ID of the heatpump appliance
660
+ * @param index The `index` of the heating circuit
661
+ * @throws {ApplianceManagerDisposedError} when called after {@link dispose}
662
+ */
663
+ async clearHeatpumpInitialHeatingCurve(applianceId, index) {
664
+ this.throwIfDisposed();
665
+ const appliance = await this.energyApp.useAppliances().getById(applianceId);
666
+ if (!appliance)
667
+ return;
668
+ const circuits = [...(appliance.heatpump?.heatingCircuits ?? [])];
669
+ const position = circuits.findIndex(c => c.index === index);
670
+ if (position < 0)
671
+ return;
672
+ const { initialHeatingCurve: _removed, ...rest } = circuits[position];
673
+ circuits[position] = { ...rest, activeHeatingCurveOffsetK: 0 };
674
+ await this.energyApp.useAppliances().save(this.mergeApplianceData(appliance, { heatpump: { heatingCircuits: circuits } }), applianceId);
675
+ await this.primeCacheFromSdk(applianceId);
676
+ }
621
677
  /**
622
678
  * Refetches an appliance from the SDK and reflects the result in the
623
679
  * cache. Used by mutating methods to keep cache reads consistent with
@@ -3,7 +3,7 @@ import type { EnyoNetworkDevice } from "../../types/enyo-network-device.cjs";
3
3
  import { EnyoAppliance, EnyoApplianceAvailableFeaturesEnum, EnyoApplianceConnectionType, EnyoApplianceGroup, EnyoApplianceMetadata, EnyoApplianceName, EnyoApplianceStateEnum, EnyoApplianceTopology, EnyoApplianceTypeEnum } from "../../types/enyo-appliance.cjs";
4
4
  import type { EnyoApplianceCreatedFilter } from "../../packages/energy-app-appliance.cjs";
5
5
  import type { EnyoChargerApplianceMetadata } from "../../types/enyo-charger-appliance.cjs";
6
- import type { EnyoHeatpumpApplianceDomesticHotWater, EnyoHeatpumpApplianceHeatingCircuit, EnyoHeatpumpApplianceMetadata } from "../../types/enyo-heatpump-appliance.cjs";
6
+ import type { EnyoHeatpumpApplianceDomesticHotWater, EnyoHeatpumpApplianceHeatingCircuit, EnyoHeatpumpApplianceHeatingCurve, EnyoHeatpumpApplianceMetadata } from "../../types/enyo-heatpump-appliance.cjs";
7
7
  import type { EnyoBatteryApplianceMetadata } from "../../types/enyo-battery-appliance.cjs";
8
8
  import type { EnyoInverterApplianceMetadata } from "../../types/enyo-inverter-appliance.cjs";
9
9
  import type { EnyoMeterAppliance } from "../../types/enyo-meter-appliance.cjs";
@@ -401,6 +401,39 @@ export declare class ApplianceManager {
401
401
  * @throws {Error} when the zone does not exist and `patch` lacks `targetTemperatureC`
402
402
  */
403
403
  updateHeatpumpDomesticHotWater(applianceId: string, index: number, patch: Partial<Omit<EnyoHeatpumpApplianceDomesticHotWater, 'index'>>): Promise<void>;
404
+ /**
405
+ * Remembers the heating curve a circuit had before the first
406
+ * `SetHeatpumpHeatingCurveOffsetV1` changed it, as the circuit's
407
+ * `initialHeatingCurve` — the curve `offsetK: 0` restores. Call it before
408
+ * writing the first offset other than `0`.
409
+ *
410
+ * Keeps an already stored initial curve: every later offset is relative to
411
+ * the curve before the *first* change, so it must not be overwritten with
412
+ * an already shifted one. Works like {@link updateHeatpumpHeatingCircuit}
413
+ * otherwise.
414
+ *
415
+ * Does nothing when the appliance does not exist.
416
+ *
417
+ * @param applianceId The ID of the heatpump appliance
418
+ * @param index The `index` of the heating circuit
419
+ * @param curve The curve currently set on the device
420
+ * @returns `true` when the curve was stored, `false` when an initial curve
421
+ * was already stored or the appliance does not exist
422
+ * @throws {ApplianceManagerDisposedError} when called after {@link dispose}
423
+ */
424
+ rememberHeatpumpInitialHeatingCurve(applianceId: string, index: number, curve: EnyoHeatpumpApplianceHeatingCurve): Promise<boolean>;
425
+ /**
426
+ * Forgets a circuit's `initialHeatingCurve` after the integration restored
427
+ * it on the device (`offsetK: 0`), and sets `activeHeatingCurveOffsetK` to
428
+ * `0`. Other circuits and fields are left untouched.
429
+ *
430
+ * Does nothing when the appliance or the circuit does not exist.
431
+ *
432
+ * @param applianceId The ID of the heatpump appliance
433
+ * @param index The `index` of the heating circuit
434
+ * @throws {ApplianceManagerDisposedError} when called after {@link dispose}
435
+ */
436
+ clearHeatpumpInitialHeatingCurve(applianceId: string, index: number): Promise<void>;
404
437
  /**
405
438
  * Refetches an appliance from the SDK and reflects the result in the
406
439
  * cache. Used by mutating methods to keep cache reads consistent with
@@ -5,6 +5,7 @@ exports.isPvOnlyPowerSources = isPvOnlyPowerSources;
5
5
  exports.validateHeatpumpControlAction = validateHeatpumpControlAction;
6
6
  exports.computeHeatpumpSetpointRaise = computeHeatpumpSetpointRaise;
7
7
  exports.validateHeatpumpProgramSwitch = validateHeatpumpProgramSwitch;
8
+ exports.validateHeatpumpHeatingCurveOffset = validateHeatpumpHeatingCurveOffset;
8
9
  const enyo_data_bus_value_js_1 = require("../../types/enyo-data-bus-value.cjs");
9
10
  const enyo_heatpump_appliance_js_1 = require("../../types/enyo-heatpump-appliance.cjs");
10
11
  /**
@@ -284,3 +285,63 @@ function validateHeatpumpProgramSwitch(data, metadata, nowMs = Date.now()) {
284
285
  }
285
286
  return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
286
287
  }
288
+ /**
289
+ * Validates a `SetHeatpumpHeatingCurveOffsetV1` against the heatpump's
290
+ * metadata and returns the answer it deserves.
291
+ *
292
+ * Used by `HeatpumpIntegrationEnergyApp` before it calls
293
+ * `handleHeatpumpHeatingCurveOffset`; an integration handling the message
294
+ * itself can call it too.
295
+ *
296
+ * Rules, in order — the first that fails decides:
297
+ * 1. `heatingCircuitIndex` is an integer ≥ 0 and `offsetK` a finite number
298
+ * (**Rejected**).
299
+ * 2. The heatpump does not declare `HeatingCurveOffset` (**NotSupported**).
300
+ * 3. No heating circuit has the message's index (**Rejected**).
301
+ * 4. `offsetK` is not `0` and the circuit has no `heatingCurveOffsetRangeK`
302
+ * or `offsetK` lies outside it (**Rejected**). `0` — restoring the initial
303
+ * curve — is always allowed.
304
+ *
305
+ * Without metadata only rule 1 can be checked.
306
+ *
307
+ * @param data - The message's data.
308
+ * @param metadata - The heatpump's metadata, if known.
309
+ * @returns The answer: `Accepted` when every rule holds.
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * const result = validateHeatpumpHeatingCurveOffset(message.data, appliance.heatpump);
314
+ * if (result.answer !== EnyoCommandAcknowledgeAnswerEnum.Accepted) console.warn(result.rejectionReason);
315
+ * ```
316
+ */
317
+ function validateHeatpumpHeatingCurveOffset(data, metadata) {
318
+ const rejected = (rejectionReason) => ({ answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Rejected, rejectionReason });
319
+ if (!Number.isInteger(data.heatingCircuitIndex) || data.heatingCircuitIndex < 0) {
320
+ return rejected(`heatingCircuitIndex must be an integer ≥ 0; got ${data.heatingCircuitIndex}.`);
321
+ }
322
+ if (typeof data.offsetK !== 'number' || !Number.isFinite(data.offsetK)) {
323
+ return rejected(`offsetK must be a finite number; got ${data.offsetK}.`);
324
+ }
325
+ if (!metadata)
326
+ return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
327
+ if (!(metadata.availableFeatures ?? []).includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpApplianceAvailableFeaturesEnum.HeatingCurveOffset)) {
328
+ return {
329
+ answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.NotSupported,
330
+ rejectionReason: 'The heatpump does not support heating curve offsets (no HeatingCurveOffset).',
331
+ };
332
+ }
333
+ const circuit = (metadata.heatingCircuits ?? []).find((c) => c.index === data.heatingCircuitIndex);
334
+ if (!circuit) {
335
+ return rejected(`No heating circuit with index ${data.heatingCircuitIndex}.`);
336
+ }
337
+ if (data.offsetK === 0)
338
+ return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
339
+ const range = circuit.heatingCurveOffsetRangeK;
340
+ if (!range) {
341
+ return rejected(`Heating circuit ${circuit.index} declares no heatingCurveOffsetRangeK; only offsetK 0 is accepted.`);
342
+ }
343
+ if (data.offsetK < range.minK || data.offsetK > range.maxK) {
344
+ return rejected(`offsetK ${data.offsetK} K is outside the range ${range.minK}…${range.maxK} K of heating circuit ${circuit.index}.`);
345
+ }
346
+ return { answer: enyo_data_bus_value_js_1.EnyoCommandAcknowledgeAnswerEnum.Accepted };
347
+ }
@@ -1,4 +1,4 @@
1
- import { EnyoCommandAcknowledgeActionResult, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusHeatpumpFlexibilityAnnouncementV3, EnyoDataBusHeatpumpProgramSwitchV3, EnyoHeatpumpControlAction, EnyoHeatpumpControlTargetEnum, EnyoPowerSourceShare } from '../../types/enyo-data-bus-value.cjs';
1
+ import { EnyoCommandAcknowledgeActionResult, EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusHeatpumpFlexibilityAnnouncementV3, EnyoDataBusHeatpumpProgramSwitchV3, EnyoDataBusSetHeatpumpHeatingCurveOffsetV1, 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
@@ -175,3 +175,40 @@ export interface HeatpumpProgramSwitchValidationResult {
175
175
  * ```
176
176
  */
177
177
  export declare function validateHeatpumpProgramSwitch(data: EnyoDataBusHeatpumpProgramSwitchV3['data'], metadata: EnyoHeatpumpApplianceMetadata | undefined, nowMs?: number): HeatpumpProgramSwitchValidationResult;
178
+ /** The answer {@link validateHeatpumpHeatingCurveOffset} gives a `SetHeatpumpHeatingCurveOffsetV1`. */
179
+ export interface HeatpumpHeatingCurveOffsetValidationResult {
180
+ /** `Accepted` when every rule holds. */
181
+ answer: EnyoCommandAcknowledgeAnswerEnum;
182
+ /** Why the offset was rejected or is not supported. */
183
+ rejectionReason?: string;
184
+ }
185
+ /**
186
+ * Validates a `SetHeatpumpHeatingCurveOffsetV1` against the heatpump's
187
+ * metadata and returns the answer it deserves.
188
+ *
189
+ * Used by `HeatpumpIntegrationEnergyApp` before it calls
190
+ * `handleHeatpumpHeatingCurveOffset`; an integration handling the message
191
+ * itself can call it too.
192
+ *
193
+ * Rules, in order — the first that fails decides:
194
+ * 1. `heatingCircuitIndex` is an integer ≥ 0 and `offsetK` a finite number
195
+ * (**Rejected**).
196
+ * 2. The heatpump does not declare `HeatingCurveOffset` (**NotSupported**).
197
+ * 3. No heating circuit has the message's index (**Rejected**).
198
+ * 4. `offsetK` is not `0` and the circuit has no `heatingCurveOffsetRangeK`
199
+ * or `offsetK` lies outside it (**Rejected**). `0` — restoring the initial
200
+ * curve — is always allowed.
201
+ *
202
+ * Without metadata only rule 1 can be checked.
203
+ *
204
+ * @param data - The message's data.
205
+ * @param metadata - The heatpump's metadata, if known.
206
+ * @returns The answer: `Accepted` when every rule holds.
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const result = validateHeatpumpHeatingCurveOffset(message.data, appliance.heatpump);
211
+ * if (result.answer !== EnyoCommandAcknowledgeAnswerEnum.Accepted) console.warn(result.rejectionReason);
212
+ * ```
213
+ */
214
+ export declare function validateHeatpumpHeatingCurveOffset(data: EnyoDataBusSetHeatpumpHeatingCurveOffsetV1['data'], metadata: EnyoHeatpumpApplianceMetadata | undefined): HeatpumpHeatingCurveOffsetValidationResult;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Pure helpers for `SetHeatpumpHeatingCurveOffsetV1`: shifting a heating curve
4
+ * by an offset, reading a flow temperature off a curve, and telling whether
5
+ * someone changed the curve on the device by hand.
6
+ *
7
+ * No I/O — persisting the initial curve is done with
8
+ * `ApplianceManager.rememberHeatpumpInitialHeatingCurve` /
9
+ * `ApplianceManager.clearHeatpumpInitialHeatingCurve`.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.HEATPUMP_HEATING_CURVE_MANUAL_CHANGE_TOLERANCE_K = void 0;
13
+ exports.interpolateHeatingCurve = interpolateHeatingCurve;
14
+ exports.shiftHeatingCurve = shiftHeatingCurve;
15
+ exports.isHeatingCurveManuallyChanged = isHeatingCurveManuallyChanged;
16
+ /**
17
+ * How far, in Kelvin of flow temperature, the curve read from the device may
18
+ * differ from `initialHeatingCurve + activeHeatingCurveOffsetK` at any point
19
+ * before it counts as changed by hand.
20
+ */
21
+ exports.HEATPUMP_HEATING_CURVE_MANUAL_CHANGE_TOLERANCE_K = 0.5;
22
+ /**
23
+ * Reads the target flow temperature off a curve at an outdoor temperature:
24
+ * linear interpolation between neighbouring points, clamped to the first /
25
+ * last point outside the covered range — the way every consumer of
26
+ * {@link EnyoHeatpumpApplianceHeatingCurve.points} reads it.
27
+ *
28
+ * @param points - The curve points, sorted by increasing outdoor temperature.
29
+ * @param outdoorTemperatureC - The outdoor temperature in °C.
30
+ * @returns The flow temperature in °C, or `undefined` for an empty curve.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * interpolateHeatingCurve(curve.points, 7.5); // 30.0 between (5 → 31.4) and (10 → 28.5)
35
+ * ```
36
+ */
37
+ function interpolateHeatingCurve(points, outdoorTemperatureC) {
38
+ if (points.length === 0)
39
+ return undefined;
40
+ const first = points[0];
41
+ const last = points[points.length - 1];
42
+ if (outdoorTemperatureC <= first.outdoorTemperatureC)
43
+ return first.flowTemperatureC;
44
+ if (outdoorTemperatureC >= last.outdoorTemperatureC)
45
+ return last.flowTemperatureC;
46
+ for (let i = 1; i < points.length; i++) {
47
+ const right = points[i];
48
+ if (outdoorTemperatureC <= right.outdoorTemperatureC) {
49
+ const left = points[i - 1];
50
+ const share = (outdoorTemperatureC - left.outdoorTemperatureC)
51
+ / (right.outdoorTemperatureC - left.outdoorTemperatureC);
52
+ return left.flowTemperatureC + share * (right.flowTemperatureC - left.flowTemperatureC);
53
+ }
54
+ }
55
+ return last.flowTemperatureC;
56
+ }
57
+ /**
58
+ * Shifts a heating curve in parallel: every point's flow temperature (and
59
+ * every cooling point's) moves by `offsetK`. The vendor parameters and the
60
+ * flow temperature bounds are kept as they are — the device still clamps to
61
+ * them.
62
+ *
63
+ * @param curve - The curve to shift, typically the circuit's `initialHeatingCurve`.
64
+ * @param offsetK - The shift in Kelvin of flow temperature; negative lowers.
65
+ * @returns A new curve; `curve` is not modified.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const expected = shiftHeatingCurve(circuit.initialHeatingCurve, -1.5);
70
+ * ```
71
+ */
72
+ function shiftHeatingCurve(curve, offsetK) {
73
+ const shift = (points) => points.map((p) => ({ ...p, flowTemperatureC: p.flowTemperatureC + offsetK }));
74
+ return {
75
+ ...curve,
76
+ points: shift(curve.points),
77
+ ...(curve.coolingPoints ? { coolingPoints: shift(curve.coolingPoints) } : {}),
78
+ };
79
+ }
80
+ /**
81
+ * Whether the heating curve read from the device was changed by hand — i.e.
82
+ * it differs from `initialCurve` shifted by `activeOffsetK` by more than
83
+ * `toleranceK` at any point of either curve.
84
+ *
85
+ * When it returns `true`, the integration stores `readCurve` as the new
86
+ * `initialHeatingCurve`, sets `activeHeatingCurveOffsetK` to `0` and logs it
87
+ * in a plain sentence; the energy manager sees the jump to `0` and restarts
88
+ * its controller.
89
+ *
90
+ * @param initialCurve - The circuit's stored `initialHeatingCurve`.
91
+ * @param activeOffsetK - The offset in K the integration believes is in effect.
92
+ * @param readCurve - The curve just read from the device.
93
+ * @param toleranceK - Allowed difference per point, in K. Defaults to
94
+ * {@link HEATPUMP_HEATING_CURVE_MANUAL_CHANGE_TOLERANCE_K}.
95
+ * @returns `true` when the curve was changed by hand.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * if (circuit.initialHeatingCurve
100
+ * && isHeatingCurveManuallyChanged(circuit.initialHeatingCurve, circuit.activeHeatingCurveOffsetK ?? 0, read)) {
101
+ * console.info('The heating curve was changed on the device; it is the new starting point.');
102
+ * await applianceManager.updateHeatpumpHeatingCircuit(id, 0, {
103
+ * heatingCurve: read, initialHeatingCurve: read, activeHeatingCurveOffsetK: 0,
104
+ * });
105
+ * }
106
+ * ```
107
+ */
108
+ function isHeatingCurveManuallyChanged(initialCurve, activeOffsetK, readCurve, toleranceK = exports.HEATPUMP_HEATING_CURVE_MANUAL_CHANGE_TOLERANCE_K) {
109
+ const expected = shiftHeatingCurve(initialCurve, activeOffsetK).points;
110
+ const read = readCurve.points;
111
+ if (expected.length === 0 || read.length === 0)
112
+ return expected.length !== read.length;
113
+ const outdoorTemperatures = [...expected, ...read].map((p) => p.outdoorTemperatureC);
114
+ return outdoorTemperatures.some((outdoorC) => {
115
+ const a = interpolateHeatingCurve(expected, outdoorC);
116
+ const b = interpolateHeatingCurve(read, outdoorC);
117
+ return Math.abs(a - b) > toleranceK;
118
+ });
119
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Pure helpers for `SetHeatpumpHeatingCurveOffsetV1`: shifting a heating curve
3
+ * by an offset, reading a flow temperature off a curve, and telling whether
4
+ * someone changed the curve on the device by hand.
5
+ *
6
+ * No I/O — persisting the initial curve is done with
7
+ * `ApplianceManager.rememberHeatpumpInitialHeatingCurve` /
8
+ * `ApplianceManager.clearHeatpumpInitialHeatingCurve`.
9
+ */
10
+ import type { EnyoHeatpumpApplianceHeatingCurve, EnyoHeatpumpApplianceHeatingCurvePoint } from '../../types/enyo-heatpump-appliance.cjs';
11
+ /**
12
+ * How far, in Kelvin of flow temperature, the curve read from the device may
13
+ * differ from `initialHeatingCurve + activeHeatingCurveOffsetK` at any point
14
+ * before it counts as changed by hand.
15
+ */
16
+ export declare const HEATPUMP_HEATING_CURVE_MANUAL_CHANGE_TOLERANCE_K = 0.5;
17
+ /**
18
+ * Reads the target flow temperature off a curve at an outdoor temperature:
19
+ * linear interpolation between neighbouring points, clamped to the first /
20
+ * last point outside the covered range — the way every consumer of
21
+ * {@link EnyoHeatpumpApplianceHeatingCurve.points} reads it.
22
+ *
23
+ * @param points - The curve points, sorted by increasing outdoor temperature.
24
+ * @param outdoorTemperatureC - The outdoor temperature in °C.
25
+ * @returns The flow temperature in °C, or `undefined` for an empty curve.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * interpolateHeatingCurve(curve.points, 7.5); // 30.0 between (5 → 31.4) and (10 → 28.5)
30
+ * ```
31
+ */
32
+ export declare function interpolateHeatingCurve(points: EnyoHeatpumpApplianceHeatingCurvePoint[], outdoorTemperatureC: number): number | undefined;
33
+ /**
34
+ * Shifts a heating curve in parallel: every point's flow temperature (and
35
+ * every cooling point's) moves by `offsetK`. The vendor parameters and the
36
+ * flow temperature bounds are kept as they are — the device still clamps to
37
+ * them.
38
+ *
39
+ * @param curve - The curve to shift, typically the circuit's `initialHeatingCurve`.
40
+ * @param offsetK - The shift in Kelvin of flow temperature; negative lowers.
41
+ * @returns A new curve; `curve` is not modified.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const expected = shiftHeatingCurve(circuit.initialHeatingCurve, -1.5);
46
+ * ```
47
+ */
48
+ export declare function shiftHeatingCurve(curve: EnyoHeatpumpApplianceHeatingCurve, offsetK: number): EnyoHeatpumpApplianceHeatingCurve;
49
+ /**
50
+ * Whether the heating curve read from the device was changed by hand — i.e.
51
+ * it differs from `initialCurve` shifted by `activeOffsetK` by more than
52
+ * `toleranceK` at any point of either curve.
53
+ *
54
+ * When it returns `true`, the integration stores `readCurve` as the new
55
+ * `initialHeatingCurve`, sets `activeHeatingCurveOffsetK` to `0` and logs it
56
+ * in a plain sentence; the energy manager sees the jump to `0` and restarts
57
+ * its controller.
58
+ *
59
+ * @param initialCurve - The circuit's stored `initialHeatingCurve`.
60
+ * @param activeOffsetK - The offset in K the integration believes is in effect.
61
+ * @param readCurve - The curve just read from the device.
62
+ * @param toleranceK - Allowed difference per point, in K. Defaults to
63
+ * {@link HEATPUMP_HEATING_CURVE_MANUAL_CHANGE_TOLERANCE_K}.
64
+ * @returns `true` when the curve was changed by hand.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * if (circuit.initialHeatingCurve
69
+ * && isHeatingCurveManuallyChanged(circuit.initialHeatingCurve, circuit.activeHeatingCurveOffsetK ?? 0, read)) {
70
+ * console.info('The heating curve was changed on the device; it is the new starting point.');
71
+ * await applianceManager.updateHeatpumpHeatingCircuit(id, 0, {
72
+ * heatingCurve: read, initialHeatingCurve: read, activeHeatingCurveOffsetK: 0,
73
+ * });
74
+ * }
75
+ * ```
76
+ */
77
+ export declare function isHeatingCurveManuallyChanged(initialCurve: EnyoHeatpumpApplianceHeatingCurve, activeOffsetK: number, readCurve: EnyoHeatpumpApplianceHeatingCurve, toleranceK?: number): boolean;
@@ -80,6 +80,67 @@ function checkProgramModes(zone, prefix, errors, warnings) {
80
80
  errors.push(`${prefix}: activeProgramMode "${zone.activeProgramMode}" is not a valid value`);
81
81
  }
82
82
  }
83
+ /** Range the customer may choose for a room's `targetRoomTemperatureC`, in °C. */
84
+ const CURVE_OPTIMIZATION_TARGET_RANGE_C = { min: 16, max: 26 };
85
+ /**
86
+ * Validates the heating-curve-offset fields of one heating circuit
87
+ * (`heatingCurveOffsetRangeK`, `activeHeatingCurveOffsetK`,
88
+ * `curveOptimization`) and appends the problems to `errors`.
89
+ *
90
+ * Errors: the range is not finite or does not contain `0`; the active offset
91
+ * is not finite or lies outside the range; a room id is empty; a resolved room
92
+ * lacks its `roomId`, a sensor lacks its `applianceId` / `sensorId`, or its
93
+ * target lies outside 16…26 °C.
94
+ *
95
+ * @param circuit - The heating circuit.
96
+ * @param prefix - Path prefix for the messages, e.g. `heatingCircuits[index=0]`.
97
+ * @param errors - Receives blocking problems.
98
+ */
99
+ function checkHeatingCurveOffset(circuit, prefix, errors) {
100
+ const range = circuit.heatingCurveOffsetRangeK;
101
+ const rangeValid = range !== undefined && isFiniteNumber(range.minK) && isFiniteNumber(range.maxK);
102
+ if (range !== undefined) {
103
+ if (!rangeValid) {
104
+ errors.push(`${prefix}: heatingCurveOffsetRangeK.minK and maxK must be finite numbers`);
105
+ }
106
+ else if (!(range.minK <= 0 && range.maxK >= 0)) {
107
+ errors.push(`${prefix}: heatingCurveOffsetRangeK ${range.minK}…${range.maxK} K must contain 0`);
108
+ }
109
+ }
110
+ const active = circuit.activeHeatingCurveOffsetK;
111
+ if (active !== undefined) {
112
+ if (!isFiniteNumber(active)) {
113
+ errors.push(`${prefix}: activeHeatingCurveOffsetK must be a finite number`);
114
+ }
115
+ else if (rangeValid && (active < range.minK || active > range.maxK)) {
116
+ errors.push(`${prefix}: activeHeatingCurveOffsetK ${active} K is outside heatingCurveOffsetRangeK ${range.minK}…${range.maxK} K`);
117
+ }
118
+ }
119
+ const settings = circuit.curveOptimization;
120
+ if (!settings)
121
+ return;
122
+ (settings.roomIds ?? []).forEach((roomId, i) => {
123
+ if (typeof roomId !== 'string' || roomId.length === 0) {
124
+ errors.push(`${prefix}: curveOptimization.roomIds[${i}] must be a non-empty string`);
125
+ }
126
+ });
127
+ (settings.rooms ?? []).forEach((room, i) => {
128
+ if (!room?.roomId) {
129
+ errors.push(`${prefix}: curveOptimization.rooms[${i}] needs a roomId`);
130
+ }
131
+ (room?.sensors ?? []).forEach((sensor, j) => {
132
+ if (!sensor?.applianceId || !sensor?.sensorId) {
133
+ errors.push(`${prefix}: curveOptimization.rooms[${i}].sensors[${j}] needs applianceId and sensorId`);
134
+ }
135
+ });
136
+ const target = room?.targetRoomTemperatureC;
137
+ if (!(isFiniteNumber(target)
138
+ && target >= CURVE_OPTIMIZATION_TARGET_RANGE_C.min && target <= CURVE_OPTIMIZATION_TARGET_RANGE_C.max)) {
139
+ errors.push(`${prefix}: curveOptimization.rooms[${i}].targetRoomTemperatureC must lie within ` +
140
+ `${CURVE_OPTIMIZATION_TARGET_RANGE_C.min}…${CURVE_OPTIMIZATION_TARGET_RANGE_C.max} °C; got ${target}`);
141
+ }
142
+ });
143
+ }
83
144
  /**
84
145
  * Parses an `HH:mm` time of day into minutes since midnight.
85
146
  *
@@ -330,6 +391,9 @@ function assertValidHeatpumpTimeProgram(program) {
330
391
  * `EnyoHeatpumpProgramModeEnum` member. Warning: eco is above comfort.
331
392
  * - Warning: `ProgramSwitch` is declared but no zone lists
332
393
  * `supportedProgramModes`, or a zone lists them without `ProgramSwitch`.
394
+ * - Heating curve offset: errors from {@link checkHeatingCurveOffset}.
395
+ * Warning: `HeatingCurveOffset` is declared but no circuit has a
396
+ * `heatingCurveOffsetRangeK` — no offset other than `0` could be accepted.
333
397
  *
334
398
  * @param metadata - The heatpump metadata to validate.
335
399
  * @returns The validation result.
@@ -380,10 +444,18 @@ function validateHeatpumpAdvancedMetadata(metadata) {
380
444
  if (declaresProgramSwitch && !switchableZones.some(({ zone }) => (zone.supportedProgramModes ?? []).length > 0)) {
381
445
  warnings.push('ProgramSwitch is declared but no zone lists supportedProgramModes — no switch could ever be accepted');
382
446
  }
447
+ if (features.includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpApplianceAvailableFeaturesEnum.HeatingCurveOffset)
448
+ && !circuits.some((c) => c.heatingCurveOffsetRangeK)) {
449
+ warnings.push('HeatingCurveOffset is declared but no heating circuit has a heatingCurveOffsetRangeK — only offset 0 could be accepted');
450
+ }
383
451
  for (const circuit of circuits) {
384
452
  const prefix = `heatingCircuits[index=${circuit.index}]`;
385
453
  if (circuit.heatingCurve)
386
454
  merge(`${prefix}.heatingCurve`, validateHeatpumpHeatingCurve(circuit.heatingCurve));
455
+ checkHeatingCurveOffset(circuit, prefix, errors);
456
+ if (circuit.initialHeatingCurve) {
457
+ merge(`${prefix}.initialHeatingCurve`, validateHeatpumpHeatingCurve(circuit.initialHeatingCurve));
458
+ }
387
459
  if (circuit.timeProgram)
388
460
  merge(`${prefix}.timeProgram`, validateHeatpumpTimeProgram(circuit.timeProgram));
389
461
  const isExternal = circuit.roomTemperatureSource === enyo_heatpump_appliance_js_1.EnyoHeatpumpRoomTemperatureSourceEnum.External;
@@ -113,6 +113,9 @@ export declare function assertValidHeatpumpTimeProgram(program: EnyoHeatpumpTime
113
113
  * `EnyoHeatpumpProgramModeEnum` member. Warning: eco is above comfort.
114
114
  * - Warning: `ProgramSwitch` is declared but no zone lists
115
115
  * `supportedProgramModes`, or a zone lists them without `ProgramSwitch`.
116
+ * - Heating curve offset: errors from {@link checkHeatingCurveOffset}.
117
+ * Warning: `HeatingCurveOffset` is declared but no circuit has a
118
+ * `heatingCurveOffsetRangeK` — no offset other than `0` could be accepted.
116
119
  *
117
120
  * @param metadata - The heatpump metadata to validate.
118
121
  * @returns The validation result.
@@ -109,6 +109,7 @@ __exportStar(require("./implementations/heatpump/heatpump-metadata-validators.cj
109
109
  __exportStar(require("./implementations/heatpump/heatpump-control-validators.cjs"), exports);
110
110
  __exportStar(require("./implementations/heatpump/heatpump-control-session.cjs"), exports);
111
111
  __exportStar(require("./implementations/heatpump/heatpump-flexibility-validators.cjs"), exports);
112
+ __exportStar(require("./implementations/heatpump/heatpump-heating-curve-offset.cjs"), exports);
112
113
  __exportStar(require("./types/enyo-inverter-appliance.cjs"), exports);
113
114
  __exportStar(require("./types/enyo-onboarding.cjs"), exports);
114
115
  __exportStar(require("./packages/energy-app-onboarding.cjs"), exports);
@@ -93,6 +93,7 @@ export * from './implementations/heatpump/heatpump-metadata-validators.cjs';
93
93
  export * from './implementations/heatpump/heatpump-control-validators.cjs';
94
94
  export * from './implementations/heatpump/heatpump-control-session.cjs';
95
95
  export * from './implementations/heatpump/heatpump-flexibility-validators.cjs';
96
+ export * from './implementations/heatpump/heatpump-heating-curve-offset.cjs';
96
97
  export * from './types/enyo-inverter-appliance.cjs';
97
98
  export * from './types/enyo-onboarding.cjs';
98
99
  export * from './packages/energy-app-onboarding.cjs';