@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.
- package/README.md +43 -1
- package/dist/cjs/implementations/appliances/appliance-manager.cjs +56 -0
- package/dist/cjs/implementations/appliances/appliance-manager.d.cts +34 -1
- package/dist/cjs/implementations/heatpump/heatpump-control-validators.cjs +61 -0
- package/dist/cjs/implementations/heatpump/heatpump-control-validators.d.cts +38 -1
- package/dist/cjs/implementations/heatpump/heatpump-heating-curve-offset.cjs +119 -0
- package/dist/cjs/implementations/heatpump/heatpump-heating-curve-offset.d.cts +77 -0
- package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +72 -0
- package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +3 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/integrations/heatpump-integration-energy-app.cjs +89 -0
- package/dist/cjs/integrations/heatpump-integration-energy-app.d.cts +75 -1
- package/dist/cjs/types/enyo-data-bus-value.cjs +2 -0
- package/dist/cjs/types/enyo-data-bus-value.d.cts +60 -0
- package/dist/cjs/types/enyo-heatpump-appliance.cjs +27 -1
- package/dist/cjs/types/enyo-heatpump-appliance.d.cts +151 -1
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/appliances/appliance-manager.d.ts +34 -1
- package/dist/implementations/appliances/appliance-manager.js +56 -0
- package/dist/implementations/heatpump/heatpump-control-validators.d.ts +38 -1
- package/dist/implementations/heatpump/heatpump-control-validators.js +60 -0
- package/dist/implementations/heatpump/heatpump-heating-curve-offset.d.ts +77 -0
- package/dist/implementations/heatpump/heatpump-heating-curve-offset.js +113 -0
- package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +3 -0
- package/dist/implementations/heatpump/heatpump-metadata-validators.js +72 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integrations/heatpump-integration-energy-app.d.ts +75 -1
- package/dist/integrations/heatpump-integration-energy-app.js +90 -1
- package/dist/types/enyo-data-bus-value.d.ts +60 -0
- package/dist/types/enyo-data-bus-value.js +2 -0
- package/dist/types/enyo-heatpump-appliance.d.ts +151 -1
- package/dist/types/enyo-heatpump-appliance.js +26 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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);
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -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';
|