@enyo-energy/energy-app-sdk 1.25.0 → 1.27.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 +207 -3
- package/dist/cjs/energy-app-model-feature.enum.cjs +2 -0
- package/dist/cjs/energy-app-model-feature.enum.d.cts +2 -0
- package/dist/cjs/energy-app-package-definition.cjs +29 -1
- package/dist/cjs/energy-app-package-definition.d.cts +77 -0
- package/dist/cjs/energy-app.cjs +17 -0
- package/dist/cjs/energy-app.d.cts +16 -0
- package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
- package/dist/cjs/implementations/appliances/appliance-manager.cjs +72 -0
- package/dist/cjs/implementations/appliances/appliance-manager.d.cts +37 -1
- package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +329 -0
- package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +105 -0
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.d.cts +3 -0
- package/dist/cjs/integrations/heatpump-integration-energy-app.cjs +29 -1
- package/dist/cjs/integrations/heatpump-integration-energy-app.d.cts +24 -2
- package/dist/cjs/integrations/integration-energy-app.cjs +36 -0
- package/dist/cjs/integrations/integration-energy-app.d.cts +26 -1
- package/dist/cjs/packages/energy-app-cascade.cjs +2 -0
- package/dist/cjs/packages/energy-app-cascade.d.cts +374 -0
- package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +41 -3
- package/dist/cjs/types/enyo-appliance.cjs +8 -0
- package/dist/cjs/types/enyo-appliance.d.cts +9 -1
- package/dist/cjs/types/enyo-data-bus-value.cjs +112 -2
- package/dist/cjs/types/enyo-data-bus-value.d.cts +518 -6
- package/dist/cjs/types/enyo-electricity-tariff.cjs +12 -1
- package/dist/cjs/types/enyo-electricity-tariff.d.cts +42 -0
- package/dist/cjs/types/enyo-forecasting.d.cts +36 -5
- package/dist/cjs/types/enyo-heatpump-appliance.cjs +69 -1
- package/dist/cjs/types/enyo-heatpump-appliance.d.cts +246 -3
- package/dist/cjs/types/enyo-meter-cascade.cjs +49 -0
- package/dist/cjs/types/enyo-meter-cascade.d.cts +229 -0
- package/dist/cjs/types/enyo-weather-history.d.cts +35 -4
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app-model-feature.enum.d.ts +2 -0
- package/dist/energy-app-model-feature.enum.js +2 -0
- package/dist/energy-app-package-definition.d.ts +77 -0
- package/dist/energy-app-package-definition.js +28 -0
- package/dist/energy-app.d.ts +16 -0
- package/dist/energy-app.js +17 -0
- package/dist/enyo-energy-app-sdk.d.ts +3 -0
- package/dist/implementations/appliances/appliance-manager.d.ts +37 -1
- package/dist/implementations/appliances/appliance-manager.js +72 -0
- package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +105 -0
- package/dist/implementations/heatpump/heatpump-metadata-validators.js +320 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/integrations/heatpump-integration-energy-app.d.ts +24 -2
- package/dist/integrations/heatpump-integration-energy-app.js +30 -2
- package/dist/integrations/integration-energy-app.d.ts +26 -1
- package/dist/integrations/integration-energy-app.js +36 -0
- package/dist/packages/energy-app-cascade.d.ts +374 -0
- package/dist/packages/energy-app-cascade.js +1 -0
- package/dist/packages/energy-app-electricity-tariff.d.ts +41 -3
- package/dist/types/enyo-appliance.d.ts +9 -1
- package/dist/types/enyo-appliance.js +8 -0
- package/dist/types/enyo-data-bus-value.d.ts +518 -6
- package/dist/types/enyo-data-bus-value.js +111 -1
- package/dist/types/enyo-electricity-tariff.d.ts +42 -0
- package/dist/types/enyo-electricity-tariff.js +11 -0
- package/dist/types/enyo-forecasting.d.ts +36 -5
- package/dist/types/enyo-heatpump-appliance.d.ts +246 -3
- package/dist/types/enyo-heatpump-appliance.js +68 -0
- package/dist/types/enyo-meter-cascade.d.ts +229 -0
- package/dist/types/enyo-meter-cascade.js +46 -0
- package/dist/types/enyo-weather-history.d.ts +35 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -191,6 +191,7 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
|
|
|
191
191
|
| Read EPEX SPOT wholesale prices (incl. negative-price windows) | [`useEpexSpotPrices()`](#useepexspotprices-energyappepexspotprice) |
|
|
192
192
|
| Manage electricity tariffs (default tariff, price per kWh) | [`useElectricityTariff()`](#useelectricitytariff-energyappelectricitytariff) |
|
|
193
193
|
| Publish or resolve time-variable grid fees (§14a HT/NT) | [`useGridFee()`](#usegridfee-energyappgridfee) |
|
|
194
|
+
| Price appliances behind a cascade meter (own tariff, prices and grid fee) | [`useCascade()`](#usecascade-energyappcascade) |
|
|
194
195
|
| Register a PV system (kWp, DC strings, orientation) | [`usePvSystem()`](#usepvsystem-energyapppvsystem) |
|
|
195
196
|
| Discover capabilities of the active energy manager | [`useEnergyManager()`](#useenergymanager-energyappenergymanager) |
|
|
196
197
|
| Serve your v2 onboarding guides when the host asks for them | [`useOnboardingV2()`](#useonboardingv2-energyapponboardingv2) |
|
|
@@ -249,6 +250,7 @@ Every Energy App must be defined using `defineEnergyAppPackage()`:
|
|
|
249
250
|
import {
|
|
250
251
|
defineEnergyAppPackage,
|
|
251
252
|
EnergyAppPackageCategory,
|
|
253
|
+
EnergyAppPackageOptionsDeviceDetectionModbusModeEnum,
|
|
252
254
|
EnergyAppPermissionTypeEnum
|
|
253
255
|
} from '@enyo-energy/energy-app-sdk';
|
|
254
256
|
|
|
@@ -291,11 +293,22 @@ const packageDef = defineEnergyAppPackage({
|
|
|
291
293
|
},
|
|
292
294
|
deviceDetection: {
|
|
293
295
|
modbus: [{
|
|
296
|
+
// Optional, only on the first entry: how the entries and their matching
|
|
297
|
+
// values combine. Default: RegistersOr_MatchingValuesOr.
|
|
298
|
+
mode: EnergyAppPackageOptionsDeviceDetectionModbusModeEnum.RegistersAnd_MatchingValuesOr,
|
|
294
299
|
unitIds: [1],
|
|
295
300
|
registerAddress: 40001,
|
|
296
301
|
registerSize: 2,
|
|
297
302
|
type: 'string',
|
|
298
303
|
matchingValues: ['SolarMax', 'SMA']
|
|
304
|
+
}, {
|
|
305
|
+
unitIds: [1],
|
|
306
|
+
// `registerType` defaults to 'holding'; use 'input' for function code 4.
|
|
307
|
+
registerType: 'input',
|
|
308
|
+
registerAddress: 30053,
|
|
309
|
+
registerSize: 2,
|
|
310
|
+
type: 'UInt32BE',
|
|
311
|
+
matchingValues: ['9401', '9402']
|
|
299
312
|
}],
|
|
300
313
|
mdns: [{
|
|
301
314
|
// The Envoy advertises under a vendor-specific service type; without
|
|
@@ -1703,6 +1716,18 @@ const prices = await tariffs.getPrices(EnyoTariffDirectionEnum.Consumption, { fr
|
|
|
1703
1716
|
const needsGridFee = !prices?.includes.includes(EnyoPriceComponentEnum.GridFee);
|
|
1704
1717
|
```
|
|
1705
1718
|
|
|
1719
|
+
To price one device, ask for the prices that actually bill it. This works for every appliance: the
|
|
1720
|
+
site's consumption prices, or — for an appliance behind an active [meter cascade](#usecascade-energyappcascade)
|
|
1721
|
+
— the cascade's, filled per 15-minute slot. The prices are effective prices (grid fee and taxes
|
|
1722
|
+
included, see `includes`), so never add a grid fee on top:
|
|
1723
|
+
|
|
1724
|
+
```typescript
|
|
1725
|
+
const billed = await tariffs.getPricesForAppliance('heatpump-1', { fromIso, untilIso });
|
|
1726
|
+
// billed.billingMeter: 'primary' | 'cascade'
|
|
1727
|
+
// entry.fallback: cascade had no price for this slot, the site's was used
|
|
1728
|
+
// billed.inheritedFromSite: cascade has no tariff of its own yet
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1706
1731
|
The app that integrates a provider owns the other side. It answers when the user picks it, sets the
|
|
1707
1732
|
tariff once it is actually usable, and pushes prices as they arrive:
|
|
1708
1733
|
|
|
@@ -1773,6 +1798,115 @@ const fees = await gridFee.getGridFeeValues({ fromIso, untilIso });
|
|
|
1773
1798
|
|
|
1774
1799
|
Publishers need `GridFeeRegister`; consumers need `GridFeeUse`.
|
|
1775
1800
|
|
|
1801
|
+
#### `useCascade(): EnergyAppCascade`
|
|
1802
|
+
|
|
1803
|
+
A **meter cascade** (Kaskadenschaltung) is a second billing meter (Z2) installed *behind* the primary
|
|
1804
|
+
meter (Z1) — typically for a heatpump or wallbox on a dedicated tariff with a reduced (§14a EnWG) grid
|
|
1805
|
+
fee. Appliances behind an **active** Z2 are billed on the cascade's tariff and grid fee; everything else
|
|
1806
|
+
on `useElectricityTariff()` and `useGridFee()`. The site has at most one cascade, and Z2 has a
|
|
1807
|
+
**consumption tariff only** — all feed-in leaves through Z1.
|
|
1808
|
+
|
|
1809
|
+
**Split rule.** PV and battery cover Z2 first; Z2 only pays the cascade tariff for what Z1 imports at the
|
|
1810
|
+
same moment. Z1 importing 2 kW, battery 1 kW, Z2 drawing 2 kW → Z2 grid share 1 kW, Z2 self-consumed
|
|
1811
|
+
1 kW, household grid share 1 kW.
|
|
1812
|
+
|
|
1813
|
+
Topology — is there a cascade, is it working, who is behind it:
|
|
1814
|
+
|
|
1815
|
+
```typescript
|
|
1816
|
+
const cascade = energyApp.useCascade();
|
|
1817
|
+
|
|
1818
|
+
const details = await cascade.getCascade(); // active, status, estimated, meterApplianceId?, applianceIds
|
|
1819
|
+
if (details?.active && details.status !== EnyoCascadeStatusEnum.Live) {
|
|
1820
|
+
// 'noCascadeSource' (nothing assigned yet) or 'noGridReading' (no Z1 reading)
|
|
1821
|
+
}
|
|
1822
|
+
cascade.onCascadeChanged(event => reassignPrices(event.cascade));
|
|
1823
|
+
```
|
|
1824
|
+
|
|
1825
|
+
With `estimated: true`, Z2 is the sum of its appliances and has no meter (`meterApplianceId` absent).
|
|
1826
|
+
A physical Z2 meter carries the topology feature `CascadeSubMeter` — skip it when looking for the grid
|
|
1827
|
+
meter.
|
|
1828
|
+
|
|
1829
|
+
Prices — to price a device, use `useElectricityTariff().getPricesForAppliance(applianceId, range)`. It
|
|
1830
|
+
works for every appliance and picks Z2 or Z1 itself, returning the **effective** price per 15-minute slot,
|
|
1831
|
+
grid fee and taxes included. `cascade.getPrices(range)` returns Z2's effective prices directly. Never add
|
|
1832
|
+
a grid fee on top:
|
|
1833
|
+
|
|
1834
|
+
```typescript
|
|
1835
|
+
const prices = await energyApp.useElectricityTariff().getPricesForAppliance('heatpump-1', { fromIso, untilIso });
|
|
1836
|
+
// prices.billingMeter: 'cascade' | 'primary'
|
|
1837
|
+
// entry.fallback: Z2 had no price for that slot, the site's was used
|
|
1838
|
+
// prices.inheritedFromSite: Z2 has no tariff of its own yet
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
Until Z2 has its own tariff or grid fee, the hub prices it with the site's — and so do `getTariff`,
|
|
1842
|
+
`getPrices`, `getGridFee` and `getGridFeeValues`: they return the site's values with
|
|
1843
|
+
`inheritedFromSite: true` rather than `null`. The grid fee methods are informational (to show the fee
|
|
1844
|
+
separately).
|
|
1845
|
+
|
|
1846
|
+
What Z2 is actually drawing and costing — live via the `CascadeSplitUpdateV1` data bus message
|
|
1847
|
+
(`householdGridW`, `cascadeGridW`, `cascadeSelfConsumedW`, `subMeterPowerW`, `status`, `estimated` — the
|
|
1848
|
+
same values the cockpit shows), and as history:
|
|
1849
|
+
|
|
1850
|
+
```typescript
|
|
1851
|
+
energyApp.useDataBus().listenForMessages([EnyoDataBusMessageEnum.CascadeSplitUpdateV1], msg => {
|
|
1852
|
+
const { cascadeGridW, status } = (msg as EnyoDataBusCascadeSplitUpdateV1).data;
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
const history = await cascade.getCascadeTimeseries({ fromIso, untilIso, resolution: '1d' });
|
|
1856
|
+
// resolution: '1m' (kept 31 days) | '15m' | '1d' | '1mo'
|
|
1857
|
+
// per bucket: householdGridKwh, cascadeGridKwh, cascadeSelfConsumedKwh,
|
|
1858
|
+
// householdCostCt, cascadeCostCt, cascadePriceCt, estimated
|
|
1859
|
+
```
|
|
1860
|
+
|
|
1861
|
+
History costs are in **ct**, priced when read with the tariffs then in force.
|
|
1862
|
+
|
|
1863
|
+
**The core owns the cascade tariff by default** — the user enters it in the hub. The cascade **grid fee
|
|
1864
|
+
comes with the tariff**: it is provided by the core, or by the app that owns the cascade tariff, and goes
|
|
1865
|
+
away with that app's ownership. An app that provides a
|
|
1866
|
+
**dynamic** tariff can offer itself for Z2 and take it over once the user selects it, then publish its
|
|
1867
|
+
prices:
|
|
1868
|
+
|
|
1869
|
+
```typescript
|
|
1870
|
+
// Registering the handler is what lists this app in the hub's cascade tariff selection.
|
|
1871
|
+
cascade.onTariffSelected(async () => {
|
|
1872
|
+
if (!await isAuthenticated()) {
|
|
1873
|
+
return {
|
|
1874
|
+
status: EnyoTariffActivationStatusEnum.AuthenticationRequired,
|
|
1875
|
+
authenticationUrl: buildOAuthUrl('cascade'),
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
await cascade.setTariff({ // takes over the slot from the core
|
|
1879
|
+
name: 'Tibber Wärmepumpe',
|
|
1880
|
+
vendorName: 'Tibber',
|
|
1881
|
+
currency: EnyoCurrencyEnum.EUR,
|
|
1882
|
+
pricing: { type: EnyoTariffPricingTypeEnum.Dynamic }, // only dynamic tariffs can be app-provided
|
|
1883
|
+
externalTariffId: contract.id,
|
|
1884
|
+
});
|
|
1885
|
+
return { status: EnyoTariffActivationStatusEnum.Success };
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
await cascade.publishPrices({ includes: [], entries }); // only while this app owns Z2's tariff
|
|
1889
|
+
await cascade.registerGridFee(reducedGridFee); // optional: the Z2 grid fee comes with the tariff
|
|
1890
|
+
|
|
1891
|
+
cascade.onTariffChanged(event => {
|
|
1892
|
+
if (event.tariff?.externalTariffId !== contract.id) stopPublishing(); // lost ownership
|
|
1893
|
+
});
|
|
1894
|
+
```
|
|
1895
|
+
|
|
1896
|
+
| Group | Methods | Permission |
|
|
1897
|
+
|---|---|---|
|
|
1898
|
+
| Topology | `isActive`, `getCascade`, `getApplianceIds`, `isApplianceBehindCascade`, `onCascadeChanged` | none |
|
|
1899
|
+
| Prices & tariff (read) | `getTariff`, `getPrices`, `onTariffChanged` | none |
|
|
1900
|
+
| History | `getCascadeTimeseries` | `Timeseries` |
|
|
1901
|
+
| Grid fee | `getGridFee`, `getGridFeeValues`, `onGridFeeChanged` / `registerGridFee`, `removeGridFee` (cascade tariff owner only) | `GridFeeUse` / `GridFeeRegister` |
|
|
1902
|
+
| Dynamic tariff provider | `onTariffSelected`, `setTariff`, `publishPrices` | `ElectricityTariff` |
|
|
1903
|
+
| Data bus | `CascadeSplitUpdateV1`, `AggregatedStateUpdateV1.data.cascade` | `SubscribeDataBus` |
|
|
1904
|
+
|
|
1905
|
+
Without a cascade, topology getters resolve to `null` / `false` / `[]`, cascade price and grid fee
|
|
1906
|
+
getters to `null`, and writes reject; `useElectricityTariff().getPricesForAppliance` returns the site's
|
|
1907
|
+
prices. Z1 also counts
|
|
1908
|
+
everything that passes Z2, so never add the two meter readings together.
|
|
1909
|
+
|
|
1776
1910
|
#### `useWeatherForecasting(): EnergyAppWeatherForecasting`
|
|
1777
1911
|
|
|
1778
1912
|
Register a weather-forecast provider (e.g. wraps an external API) and / or consume forecasts by zip code or coordinates.
|
|
@@ -1860,6 +1994,11 @@ forecast series concatenate into one timeline without translation. Every measure
|
|
|
1860
1994
|
optional: you get what the provider holds and you asked for, and a measure it does not hold is simply
|
|
1861
1995
|
absent rather than an error.
|
|
1862
1996
|
|
|
1997
|
+
`timestampIso` is the **start** of the bucket a reading or forecast entry covers. The irradiance
|
|
1998
|
+
fields are the mean over `[timestampIso, timestampIso + resolution)` — not the value at that moment,
|
|
1999
|
+
and not the mean of the preceding hour. Temperature, wind and cloud cover are the value at the start
|
|
2000
|
+
of the bucket for an hourly source, and the time-weighted mean over the bucket at coarser resolutions.
|
|
2001
|
+
|
|
1863
2002
|
Aggregates per measure live in `statistics`, keyed by `WeatherHistoryMeasureEnum`. `averageValue` is
|
|
1864
2003
|
time-weighted; for the irradiance measures it is a mean power density in W/m², so multiply by the
|
|
1865
2004
|
covered duration in hours to get received energy in Wh/m². `symbol` is categorical and therefore
|
|
@@ -2954,11 +3093,48 @@ class MyHeatpumpApp extends HeatpumpIntegrationEnergyApp {
|
|
|
2954
3093
|
|
|
2955
3094
|
Drives a heatpump. Manages building / DHW overheating commands and grid-power-availability announcements.
|
|
2956
3095
|
|
|
2957
|
-
- **Subscribed commands:** `HeatpumpOverheatingV1`, `HeatpumpAvailablePowerAnnouncementV1`, `GridOperatorPowerLimitationV1` (broadcast)
|
|
2958
|
-
- **Implement:** `handleHeatpumpOverheating`, `handleHeatpumpAvailablePowerAnnouncement`, `handleGridOperatorPowerLimitation`
|
|
3096
|
+
- **Subscribed commands:** `HeatpumpOverheatingV1`, `HeatpumpAvailablePowerAnnouncementV1`, `SetHeatpumpAvailablePowerV2`, `SetHeatpumpRoomTemperatureV1`, `GridOperatorPowerLimitationV1` (broadcast)
|
|
3097
|
+
- **Implement:** `handleHeatpumpOverheating`, `handleHeatpumpAvailablePowerAnnouncement`, `handleSetHeatpumpAvailablePower`, `handleGridOperatorPowerLimitation`
|
|
3098
|
+
- **Optional override:** `handleSetHeatpumpRoomTemperature` — write a measured room temperature into a heating circuit. Answers `NotSupported` by default; override it when the heatpump declares `RoomTemperatureInput`.
|
|
2959
3099
|
- **Publish helpers:**
|
|
2960
3100
|
- `publishHeatpumpValuesUpdate(applianceId, values)` — operation mode, electrical and thermal power, energies.
|
|
2961
|
-
- `publishHeatpumpTemperatures(applianceId, temperatures)` — outdoor, flow
|
|
3101
|
+
- `publishHeatpumpTemperatures(applianceId, temperatures)` — outdoor, flow / target flow / return, DHW tanks, heating circuits (room, flow, target flow, return), buffer tank.
|
|
3102
|
+
|
|
3103
|
+
#### Advanced heatpump metadata
|
|
3104
|
+
|
|
3105
|
+
Beyond tanks and circuits, the heatpump metadata (`EnyoHeatpumpApplianceMetadata`) can carry what the device is configured to do. Declare each part with its feature flag in `availableFeatures` so consumers know to expect it:
|
|
3106
|
+
|
|
3107
|
+
| Feature flag | Where the data lives |
|
|
3108
|
+
|---|---|
|
|
3109
|
+
| `HeatingCurve` | `heatingCircuits[].heatingCurve` — outdoor → flow temperature `points` (vendor-neutral, linearly interpolated), plus raw `slope` / `parallelShiftK`, min / max flow and an optional cooling curve |
|
|
3110
|
+
| `TimeProgram` | `heatingCircuits[].timeProgram` and `domesticHotWater[].timeProgram` — weekly periods (`daysOfWeek` 0 = Sunday, local `HH:mm` times, may wrap past midnight) switching between `Comfort` / `Reduced` / `Off` / `Boost` |
|
|
3111
|
+
| `RoomTemperature` | `heatingCircuits[].roomTemperatureC` in `HeatpumpTemperaturesUpdateV1`; where it is measured goes in `heatingCircuits[].roomTemperatureSource` (`None` / `Internal` / `External`) and, for an enyo sensor, `roomTemperatureSensor` |
|
|
3112
|
+
| `RoomTemperatureInput` | The heatpump accepts measured room temperatures via `SetHeatpumpRoomTemperatureV1` |
|
|
3113
|
+
| `FlowTemperature` / `ReturnTemperature` | `heatpumpFlowTemperatureC`, `heatpumpTargetFlowTemperatureC`, `heatpumpReturnTemperatureC` and per-circuit `flowTemperatureC`, `targetFlowTemperatureC`, `returnTemperatureC` in `HeatpumpTemperaturesUpdateV1` |
|
|
3114
|
+
|
|
3115
|
+
`updateAppliance` replaces the whole `heatingCircuits` / `domesticHotWater` array, so use the `ApplianceManager` helpers to change a single entry:
|
|
3116
|
+
|
|
3117
|
+
```typescript
|
|
3118
|
+
await applianceManager.updateHeatpumpHeatingCircuit(applianceId, 0, {
|
|
3119
|
+
heatingCurve: {
|
|
3120
|
+
points: [
|
|
3121
|
+
{ outdoorTemperatureC: -15, flowTemperatureC: 48 },
|
|
3122
|
+
{ outdoorTemperatureC: 0, flowTemperatureC: 38 },
|
|
3123
|
+
{ outdoorTemperatureC: 15, flowTemperatureC: 27 },
|
|
3124
|
+
],
|
|
3125
|
+
slope: 0.8,
|
|
3126
|
+
parallelShiftK: 0,
|
|
3127
|
+
},
|
|
3128
|
+
timeProgram: {
|
|
3129
|
+
defaultLevel: EnyoHeatpumpTimeProgramLevelEnum.Reduced,
|
|
3130
|
+
levelTemperaturesC: { Comfort: 21, Reduced: 18 },
|
|
3131
|
+
periods: [{ daysOfWeek: [1, 2, 3, 4, 5], startTimeOfDay: '06:00', endTimeOfDay: '22:00', level: EnyoHeatpumpTimeProgramLevelEnum.Comfort }],
|
|
3132
|
+
},
|
|
3133
|
+
});
|
|
3134
|
+
await applianceManager.updateHeatpumpDomesticHotWater(applianceId, 0, { timeProgram: dhwProgram });
|
|
3135
|
+
```
|
|
3136
|
+
|
|
3137
|
+
Validate before publishing with `validateHeatpumpHeatingCurve`, `validateHeatpumpTimeProgram` or `validateHeatpumpAdvancedMetadata` (or their `assertValid*` counterparts). Each returns `{ ok, errors, warnings }`.
|
|
2962
3138
|
|
|
2963
3139
|
### WallboxIntegrationEnergyApp
|
|
2964
3140
|
|
|
@@ -3993,6 +4169,12 @@ Each event carries the **complete** picture of the slot — render it as it arri
|
|
|
3993
4169
|
|
|
3994
4170
|
New reason types came with this surface, so a skipped row can say *why* instead of falling back to a generic "scheduled optimization": `SessionComplete`, `AppliancePaused`, `NothingConnected`, `DeadlinePassed`, `WaitingForCheaperSlot`, `AboveOwnPriceLimit`, `BelowMinPower`, `OtherApplianceTurn`, `SupplyExhausted`, `OutsideSchedule` — grouped by the new `SessionState` and `Contention` reason categories. `ApplianceInitiatedDraw` and `PowerOffered` came with the waterfall states above.
|
|
3995
4171
|
|
|
4172
|
+
**Battery vs. charging car.** While a car charges, the house battery is either held out of it or allowed to help, according to the owner's `batteryEvDischargeMode`. Two reason types (category `BatteryState`) say which: `BatteryReservedFromEv` for the hold (`Discharge 0`) and `BatterySupportsEvCharging` for the release (`mode=Auto`). Both carry `context.batteryToEv` (`EnyoDataBusCommandReasonBatteryToEvContext`) with the owner's `mode`, the hold `trigger` (`EnyoBatteryToEvHoldTriggerEnum`: `OwnerBlocked`, `SocLimitReached`, `AllowanceSpent`, `NoMeasurement`), `socLimitPercent`, `allowanceWh` and `remainingWh`. The energy manager re-checks a hold periodically by briefly releasing the battery; that re-check sets `batteryToEv.probe: true` and should not be listed as its own entry in a command history.
|
|
4173
|
+
|
|
4174
|
+
**Short-cycling protection.** When an appliance is kept running (or kept off) to honour its minimum on/off time, state `DeviceProtection` with `context.switching` (`EnyoDataBusCommandReasonSwitchingContext`): `hold` (`EnyoSwitchingProtectionHoldEnum.KeptOn` / `KeptOff`), `minOnSeconds`, `minOffSeconds` and `untilIso`. The text can then say "Kept running for another 4 minutes to protect the compressor" instead of a generic "Protecting the device".
|
|
4175
|
+
|
|
4176
|
+
A hold that keeps an appliance **off** after a stop has its own type, `RestartDelay` (category `DeviceProtection`): set `context.switching` with `hold: KeptOff`, `minOffSeconds` and `untilIso` (when it may restart), and `powerW` to the power waiting for it — e.g. "Short pause after switching off — charging resumes at 13:15 so the car isn't switched on and off too often."
|
|
4177
|
+
|
|
3996
4178
|
## Dynamic Grid Fees & Tariff Bonuses
|
|
3997
4179
|
|
|
3998
4180
|
An electricity price is rarely one number. It is the energy price, plus the grid operator's network
|
|
@@ -4560,6 +4742,28 @@ Notes:
|
|
|
4560
4742
|
- V1 is unchanged and remains fully supported. V2 is an additive sibling, not a
|
|
4561
4743
|
migration.
|
|
4562
4744
|
|
|
4745
|
+
#### Forecasting When a Heat Pump Runs
|
|
4746
|
+
|
|
4747
|
+
A heat pump integration can publish its own forecast of when the heat pump will run with
|
|
4748
|
+
`HeatpumpOperationForecastV1` (or `publishHeatpumpOperationForecast()` on `IntegrationEnergyApp`).
|
|
4749
|
+
It is a prediction, not a request for power — to offer power, send `ApplianceFlexibilityAnnouncementV2`.
|
|
4750
|
+
|
|
4751
|
+
Each entry covers one slot of `resolution`, starting at `timestampIso`. `outdoorTemperatureC` and
|
|
4752
|
+
`running` are always present; `flowTemperatureC`, `generatedHeatWh`, `consumptionWh` and
|
|
4753
|
+
`averagePowerW` are optional. Energy values are per slot, and `averagePowerW` is averaged over the
|
|
4754
|
+
whole slot. Each message replaces the previous forecast for the appliance.
|
|
4755
|
+
|
|
4756
|
+
```typescript
|
|
4757
|
+
this.publishHeatpumpOperationForecast('heatpump-1', {
|
|
4758
|
+
resolution: ForecastResolutionEnum.OneHour,
|
|
4759
|
+
entries: [
|
|
4760
|
+
{timestampIso: '2026-10-06T06:00:00Z', outdoorTemperatureC: 4.5, running: true,
|
|
4761
|
+
flowTemperatureC: 38, generatedHeatWh: 5200, consumptionWh: 1600, averagePowerW: 1600},
|
|
4762
|
+
{timestampIso: '2026-10-06T07:00:00Z', outdoorTemperatureC: 5.0, running: false},
|
|
4763
|
+
],
|
|
4764
|
+
});
|
|
4765
|
+
```
|
|
4766
|
+
|
|
4563
4767
|
#### Explaining Why a Command Was Issued
|
|
4564
4768
|
|
|
4565
4769
|
Every data bus command can carry an `EnyoDataBusCommandReason`. Its `type`
|
|
@@ -41,6 +41,8 @@ var EnergyAppModelFeatureEnum;
|
|
|
41
41
|
EnergyAppModelFeatureEnum["HeatpumpPowerModulation"] = "heatpump-power-modulation";
|
|
42
42
|
/** The model supports the SG Ready interface for smart-grid signalling */
|
|
43
43
|
EnergyAppModelFeatureEnum["HeatpumpSgReady"] = "heatpump-sg-ready";
|
|
44
|
+
/** The heat pump accepts measured room temperatures from an external sensor as control input */
|
|
45
|
+
EnergyAppModelFeatureEnum["HeatpumpRoomTemperatureInput"] = "heatpump-room-temperature-input";
|
|
44
46
|
// Climate Control / Air Conditioning
|
|
45
47
|
/** The target temperature setpoint can be controlled */
|
|
46
48
|
EnergyAppModelFeatureEnum["ClimateTemperatureSetpoint"] = "climate-temperature-setpoint";
|
|
@@ -33,6 +33,8 @@ export declare enum EnergyAppModelFeatureEnum {
|
|
|
33
33
|
HeatpumpPowerModulation = "heatpump-power-modulation",
|
|
34
34
|
/** The model supports the SG Ready interface for smart-grid signalling */
|
|
35
35
|
HeatpumpSgReady = "heatpump-sg-ready",
|
|
36
|
+
/** The heat pump accepts measured room temperatures from an external sensor as control input */
|
|
37
|
+
HeatpumpRoomTemperatureInput = "heatpump-room-temperature-input",
|
|
36
38
|
/** The target temperature setpoint can be controlled */
|
|
37
39
|
ClimateTemperatureSetpoint = "climate-temperature-setpoint",
|
|
38
40
|
/** The operating mode (heat / cool / fan / off) can be controlled */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EnergyAppPackageFirmwareModeEnum = exports.EnergyAppPackageCompatibilityStatus = exports.EnergyAppPackageCategory = void 0;
|
|
3
|
+
exports.EnergyAppPackageFirmwareModeEnum = exports.EnergyAppPackageCompatibilityStatus = exports.EnergyAppPackageOptionsDeviceDetectionModbusModeEnum = exports.EnergyAppPackageCategory = void 0;
|
|
4
4
|
exports.defineEnergyAppPackage = defineEnergyAppPackage;
|
|
5
5
|
const version_js_1 = require("./version.cjs");
|
|
6
6
|
var EnergyAppPackageCategory;
|
|
@@ -27,6 +27,34 @@ var EnergyAppPackageCategory;
|
|
|
27
27
|
EnergyAppPackageCategory["Vehicle"] = "vehicle";
|
|
28
28
|
EnergyAppPackageCategory["Other"] = "other";
|
|
29
29
|
})(EnergyAppPackageCategory || (exports.EnergyAppPackageCategory = EnergyAppPackageCategory = {}));
|
|
30
|
+
/**
|
|
31
|
+
* How the Modbus device detection rules in
|
|
32
|
+
* {@link EnergyAppPackageOptionsDeviceDetection.modbus} are combined.
|
|
33
|
+
*
|
|
34
|
+
* Each name has two parts:
|
|
35
|
+
* - `Registers…`: how the entries (registers) of the `modbus` list combine.
|
|
36
|
+
* `Or` means one matching register is enough; `And` means every register has
|
|
37
|
+
* to match, all on the same unit id.
|
|
38
|
+
* - `MatchingValues…`: how the `matchingValues` within one register combine.
|
|
39
|
+
* `Or` means the register's value has to match one of them; `And` means it
|
|
40
|
+
* has to match all of them.
|
|
41
|
+
*
|
|
42
|
+
* Only read from the **first** entry of the `modbus` list and applies to the
|
|
43
|
+
* whole list; set it there and nowhere else. Defaults to
|
|
44
|
+
* {@link RegistersOr_MatchingValuesOr} when omitted, which is the behaviour of
|
|
45
|
+
* all existing rules.
|
|
46
|
+
*/
|
|
47
|
+
var EnergyAppPackageOptionsDeviceDetectionModbusModeEnum;
|
|
48
|
+
(function (EnergyAppPackageOptionsDeviceDetectionModbusModeEnum) {
|
|
49
|
+
/** One register has to match, with one of its matching values. The default. */
|
|
50
|
+
EnergyAppPackageOptionsDeviceDetectionModbusModeEnum["RegistersOr_MatchingValuesOr"] = "RegistersOr_MatchingValuesOr";
|
|
51
|
+
/** One register has to match, with all of its matching values. */
|
|
52
|
+
EnergyAppPackageOptionsDeviceDetectionModbusModeEnum["RegistersOr_MatchingValuesAnd"] = "RegistersOr_MatchingValuesAnd";
|
|
53
|
+
/** Every register has to match, each with one of its matching values. */
|
|
54
|
+
EnergyAppPackageOptionsDeviceDetectionModbusModeEnum["RegistersAnd_MatchingValuesOr"] = "RegistersAnd_MatchingValuesOr";
|
|
55
|
+
/** Every register has to match, each with all of its matching values. */
|
|
56
|
+
EnergyAppPackageOptionsDeviceDetectionModbusModeEnum["RegistersAnd_MatchingValuesAnd"] = "RegistersAnd_MatchingValuesAnd";
|
|
57
|
+
})(EnergyAppPackageOptionsDeviceDetectionModbusModeEnum || (exports.EnergyAppPackageOptionsDeviceDetectionModbusModeEnum = EnergyAppPackageOptionsDeviceDetectionModbusModeEnum = {}));
|
|
30
58
|
/**
|
|
31
59
|
* Whether a declared compatibility entry means "this works" or "this is known
|
|
32
60
|
* not to work".
|
|
@@ -40,8 +40,85 @@ export interface EnergyAppPackageOptionsDeviceDetectionHostname {
|
|
|
40
40
|
operation: 'eq' | 'startsWith';
|
|
41
41
|
matchingValue: string;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Which Modbus register bank a device detection rule reads.
|
|
45
|
+
* - `'holding'`: holding registers, function code 3
|
|
46
|
+
* - `'input'`: input registers, function code 4
|
|
47
|
+
*
|
|
48
|
+
* The two banks are separate address spaces, so the same address may hold
|
|
49
|
+
* different data (or nothing) in each.
|
|
50
|
+
*/
|
|
51
|
+
export type EnergyAppPackageOptionsDeviceDetectionModbusRegisterType = 'holding' | 'input';
|
|
52
|
+
/**
|
|
53
|
+
* How the Modbus device detection rules in
|
|
54
|
+
* {@link EnergyAppPackageOptionsDeviceDetection.modbus} are combined.
|
|
55
|
+
*
|
|
56
|
+
* Each name has two parts:
|
|
57
|
+
* - `Registers…`: how the entries (registers) of the `modbus` list combine.
|
|
58
|
+
* `Or` means one matching register is enough; `And` means every register has
|
|
59
|
+
* to match, all on the same unit id.
|
|
60
|
+
* - `MatchingValues…`: how the `matchingValues` within one register combine.
|
|
61
|
+
* `Or` means the register's value has to match one of them; `And` means it
|
|
62
|
+
* has to match all of them.
|
|
63
|
+
*
|
|
64
|
+
* Only read from the **first** entry of the `modbus` list and applies to the
|
|
65
|
+
* whole list; set it there and nowhere else. Defaults to
|
|
66
|
+
* {@link RegistersOr_MatchingValuesOr} when omitted, which is the behaviour of
|
|
67
|
+
* all existing rules.
|
|
68
|
+
*/
|
|
69
|
+
export declare enum EnergyAppPackageOptionsDeviceDetectionModbusModeEnum {
|
|
70
|
+
/** One register has to match, with one of its matching values. The default. */
|
|
71
|
+
RegistersOr_MatchingValuesOr = "RegistersOr_MatchingValuesOr",
|
|
72
|
+
/** One register has to match, with all of its matching values. */
|
|
73
|
+
RegistersOr_MatchingValuesAnd = "RegistersOr_MatchingValuesAnd",
|
|
74
|
+
/** Every register has to match, each with one of its matching values. */
|
|
75
|
+
RegistersAnd_MatchingValuesOr = "RegistersAnd_MatchingValuesOr",
|
|
76
|
+
/** Every register has to match, each with all of its matching values. */
|
|
77
|
+
RegistersAnd_MatchingValuesAnd = "RegistersAnd_MatchingValuesAnd"
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Optional device detection configuration for Modbus TCP register matching:
|
|
81
|
+
* read a register range and compare its decoded value against the matching
|
|
82
|
+
* values.
|
|
83
|
+
*
|
|
84
|
+
* How several entries and several matching values combine is set by
|
|
85
|
+
* {@link mode} on the first entry of the list.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Vendor name in holding registers AND model id in an input register:
|
|
89
|
+
* modbus: [
|
|
90
|
+
* {
|
|
91
|
+
* mode: EnergyAppPackageOptionsDeviceDetectionModbusModeEnum.RegistersAnd_MatchingValuesOr,
|
|
92
|
+
* unitIds: [1],
|
|
93
|
+
* registerAddress: 40001,
|
|
94
|
+
* registerSize: 2,
|
|
95
|
+
* type: 'string',
|
|
96
|
+
* matchingValues: ['SMA'],
|
|
97
|
+
* },
|
|
98
|
+
* {
|
|
99
|
+
* unitIds: [1],
|
|
100
|
+
* registerType: 'input',
|
|
101
|
+
* registerAddress: 30053,
|
|
102
|
+
* registerSize: 2,
|
|
103
|
+
* type: 'UInt32BE',
|
|
104
|
+
* matchingValues: ['9401', '9402'],
|
|
105
|
+
* },
|
|
106
|
+
* ]
|
|
107
|
+
*/
|
|
43
108
|
export interface EnergyAppPackageOptionsDeviceDetectionModbus {
|
|
109
|
+
/**
|
|
110
|
+
* How all entries of the `modbus` list and their matching values combine.
|
|
111
|
+
* Only read from the first entry of the list — set it there and leave it
|
|
112
|
+
* out on the others. Defaults to
|
|
113
|
+
* {@link EnergyAppPackageOptionsDeviceDetectionModbusModeEnum.RegistersOr_MatchingValuesOr}.
|
|
114
|
+
*/
|
|
115
|
+
mode?: EnergyAppPackageOptionsDeviceDetectionModbusModeEnum;
|
|
44
116
|
unitIds: number[];
|
|
117
|
+
/**
|
|
118
|
+
* Which register bank to read. Defaults to `'holding'` when omitted,
|
|
119
|
+
* which is the behaviour of all existing rules.
|
|
120
|
+
*/
|
|
121
|
+
registerType?: EnergyAppPackageOptionsDeviceDetectionModbusRegisterType;
|
|
45
122
|
/** Register address, for example 30001 */
|
|
46
123
|
registerAddress: number;
|
|
47
124
|
/** Register size, for example 2 for 30001 - 30002 */
|
package/dist/cjs/energy-app.cjs
CHANGED
|
@@ -506,6 +506,23 @@ class EnergyApp {
|
|
|
506
506
|
useCommandLog() {
|
|
507
507
|
return this.energyAppSdk.useCommandLog();
|
|
508
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Gets the Cascade API for the site's meter cascade: a second meter behind
|
|
511
|
+
* the primary meter (e.g. for a heatpump on a dedicated tariff) with its
|
|
512
|
+
* own electricity tariff, prices and grid fee.
|
|
513
|
+
*
|
|
514
|
+
* Use it to find out whether a cascade is active and which appliances are
|
|
515
|
+
* behind it, and to read or supply the cascade's tariff, prices and grid
|
|
516
|
+
* fee — appliances behind an active cascade are billed on those instead of
|
|
517
|
+
* on {@link useElectricityTariff} and {@link useGridFee}.
|
|
518
|
+
* @returns The Cascade API instance
|
|
519
|
+
* @throws {EnergyAppPermissionNotGrantedError} If the `ElectricityTariff`,
|
|
520
|
+
* `GridFeeRegister` or `GridFeeUse` permission required by the
|
|
521
|
+
* called method is not granted.
|
|
522
|
+
*/
|
|
523
|
+
useCascade() {
|
|
524
|
+
return this.energyAppSdk.useCascade();
|
|
525
|
+
}
|
|
509
526
|
/**
|
|
510
527
|
* Gets the current SDK version.
|
|
511
528
|
* @returns The semantic version string of the SDK
|
|
@@ -50,6 +50,7 @@ import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
|
|
|
50
50
|
import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
|
|
51
51
|
import { EnergyAppGridFee } from "./packages/energy-app-grid-fee.cjs";
|
|
52
52
|
import { EnergyAppCommandLog } from "./packages/energy-app-command-log.cjs";
|
|
53
|
+
import { EnergyAppCascade } from "./packages/energy-app-cascade.cjs";
|
|
53
54
|
import { UseFetchOptions } from "./types/enyo-fetch.cjs";
|
|
54
55
|
/**
|
|
55
56
|
* Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
|
|
@@ -426,6 +427,21 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
|
|
|
426
427
|
* permission is not granted.
|
|
427
428
|
*/
|
|
428
429
|
useCommandLog(): EnergyAppCommandLog;
|
|
430
|
+
/**
|
|
431
|
+
* Gets the Cascade API for the site's meter cascade: a second meter behind
|
|
432
|
+
* the primary meter (e.g. for a heatpump on a dedicated tariff) with its
|
|
433
|
+
* own electricity tariff, prices and grid fee.
|
|
434
|
+
*
|
|
435
|
+
* Use it to find out whether a cascade is active and which appliances are
|
|
436
|
+
* behind it, and to read or supply the cascade's tariff, prices and grid
|
|
437
|
+
* fee — appliances behind an active cascade are billed on those instead of
|
|
438
|
+
* on {@link useElectricityTariff} and {@link useGridFee}.
|
|
439
|
+
* @returns The Cascade API instance
|
|
440
|
+
* @throws {EnergyAppPermissionNotGrantedError} If the `ElectricityTariff`,
|
|
441
|
+
* `GridFeeRegister` or `GridFeeUse` permission required by the
|
|
442
|
+
* called method is not granted.
|
|
443
|
+
*/
|
|
444
|
+
useCascade(): EnergyAppCascade;
|
|
429
445
|
/**
|
|
430
446
|
* Gets the current SDK version.
|
|
431
447
|
* @returns The semantic version string of the SDK
|
|
@@ -49,6 +49,7 @@ import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
|
|
|
49
49
|
import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
|
|
50
50
|
import { EnergyAppGridFee } from "./packages/energy-app-grid-fee.cjs";
|
|
51
51
|
import { EnergyAppCommandLog } from "./packages/energy-app-command-log.cjs";
|
|
52
|
+
import { EnergyAppCascade } from "./packages/energy-app-cascade.cjs";
|
|
52
53
|
import { UseFetchOptions } from "./types/enyo-fetch.cjs";
|
|
53
54
|
export declare enum EnergyAppStateEnum {
|
|
54
55
|
Launching = "launching",
|
|
@@ -181,4 +182,6 @@ export interface EnyoEnergyAppSdk {
|
|
|
181
182
|
useGridFee: () => EnergyAppGridFee;
|
|
182
183
|
/** Get the Command Log API for recording which register, configuration key or message this app wrote to an appliance, and reading those commands back */
|
|
183
184
|
useCommandLog: () => EnergyAppCommandLog;
|
|
185
|
+
/** Get the Cascade API for the meter behind the primary meter: whether it is active, which appliances are behind it, and its own electricity tariff, prices and grid fee */
|
|
186
|
+
useCascade: () => EnergyAppCascade;
|
|
184
187
|
}
|
|
@@ -531,6 +531,78 @@ class ApplianceManager {
|
|
|
531
531
|
await this.energyApp.useAppliances().save(updated, applianceId);
|
|
532
532
|
await this.primeCacheFromSdk(applianceId);
|
|
533
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Patches a single heating circuit of a heatpump — e.g. to publish a changed
|
|
536
|
+
* heating curve or time program — without resending the whole
|
|
537
|
+
* `heatingCircuits` array.
|
|
538
|
+
*
|
|
539
|
+
* {@link updateAppliance} merges `heatpump` metadata only one level deep,
|
|
540
|
+
* so passing `heatingCircuits` there replaces every circuit. This helper
|
|
541
|
+
* reads the stored array, shallow-merges `patch` into the circuit with the
|
|
542
|
+
* given `index` (appending a new circuit when none exists), keeps the
|
|
543
|
+
* array sorted by index, and saves. Other circuits are left untouched.
|
|
544
|
+
*
|
|
545
|
+
* Does nothing when the appliance does not exist.
|
|
546
|
+
*
|
|
547
|
+
* @param applianceId The ID of the heatpump appliance
|
|
548
|
+
* @param index The `index` of the heating circuit to patch
|
|
549
|
+
* @param patch The circuit fields to set; fields not given keep their stored value
|
|
550
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
551
|
+
*/
|
|
552
|
+
async updateHeatpumpHeatingCircuit(applianceId, index, patch) {
|
|
553
|
+
this.throwIfDisposed();
|
|
554
|
+
const appliance = await this.energyApp.useAppliances().getById(applianceId);
|
|
555
|
+
if (!appliance)
|
|
556
|
+
return;
|
|
557
|
+
const circuits = [...(appliance.heatpump?.heatingCircuits ?? [])];
|
|
558
|
+
const position = circuits.findIndex(c => c.index === index);
|
|
559
|
+
if (position >= 0) {
|
|
560
|
+
circuits[position] = { ...circuits[position], ...patch, index };
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
circuits.push({ ...patch, index });
|
|
564
|
+
circuits.sort((a, b) => a.index - b.index);
|
|
565
|
+
}
|
|
566
|
+
await this.energyApp.useAppliances().save(this.mergeApplianceData(appliance, { heatpump: { heatingCircuits: circuits } }), applianceId);
|
|
567
|
+
await this.primeCacheFromSdk(applianceId);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Patches a single domestic-hot-water zone of a heatpump — e.g. to publish
|
|
571
|
+
* a changed time program — without resending the whole `domesticHotWater`
|
|
572
|
+
* array. Works like {@link updateHeatpumpHeatingCircuit}.
|
|
573
|
+
*
|
|
574
|
+
* Appending a zone that does not exist yet requires `targetTemperatureC`
|
|
575
|
+
* in `patch`, because a DHW zone cannot be stored without it.
|
|
576
|
+
*
|
|
577
|
+
* Does nothing when the appliance does not exist.
|
|
578
|
+
*
|
|
579
|
+
* @param applianceId The ID of the heatpump appliance
|
|
580
|
+
* @param index The `index` of the DHW zone to patch
|
|
581
|
+
* @param patch The zone fields to set; fields not given keep their stored value
|
|
582
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
583
|
+
* @throws {Error} when the zone does not exist and `patch` lacks `targetTemperatureC`
|
|
584
|
+
*/
|
|
585
|
+
async updateHeatpumpDomesticHotWater(applianceId, index, patch) {
|
|
586
|
+
this.throwIfDisposed();
|
|
587
|
+
const appliance = await this.energyApp.useAppliances().getById(applianceId);
|
|
588
|
+
if (!appliance)
|
|
589
|
+
return;
|
|
590
|
+
const zones = [...(appliance.heatpump?.domesticHotWater ?? [])];
|
|
591
|
+
const position = zones.findIndex(z => z.index === index);
|
|
592
|
+
if (position >= 0) {
|
|
593
|
+
zones[position] = { ...zones[position], ...patch, index };
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
const { targetTemperatureC } = patch;
|
|
597
|
+
if (targetTemperatureC === undefined) {
|
|
598
|
+
throw new Error(`Cannot add domestic hot water zone ${index} to appliance ${applianceId} without targetTemperatureC`);
|
|
599
|
+
}
|
|
600
|
+
zones.push({ ...patch, index, targetTemperatureC });
|
|
601
|
+
zones.sort((a, b) => a.index - b.index);
|
|
602
|
+
}
|
|
603
|
+
await this.energyApp.useAppliances().save(this.mergeApplianceData(appliance, { heatpump: { domesticHotWater: zones } }), applianceId);
|
|
604
|
+
await this.primeCacheFromSdk(applianceId);
|
|
605
|
+
}
|
|
534
606
|
/**
|
|
535
607
|
* Refetches an appliance from the SDK and reflects the result in the
|
|
536
608
|
* 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, 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 { EnyoHeatpumpApplianceMetadata } from "../../types/enyo-heatpump-appliance.cjs";
|
|
6
|
+
import type { EnyoHeatpumpApplianceDomesticHotWater, EnyoHeatpumpApplianceHeatingCircuit, 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";
|
|
@@ -329,6 +329,42 @@ export declare class ApplianceManager {
|
|
|
329
329
|
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
330
330
|
*/
|
|
331
331
|
updateAppliance(applianceId: string, attributes: Partial<PartialEnyoAppliance>): Promise<void>;
|
|
332
|
+
/**
|
|
333
|
+
* Patches a single heating circuit of a heatpump — e.g. to publish a changed
|
|
334
|
+
* heating curve or time program — without resending the whole
|
|
335
|
+
* `heatingCircuits` array.
|
|
336
|
+
*
|
|
337
|
+
* {@link updateAppliance} merges `heatpump` metadata only one level deep,
|
|
338
|
+
* so passing `heatingCircuits` there replaces every circuit. This helper
|
|
339
|
+
* reads the stored array, shallow-merges `patch` into the circuit with the
|
|
340
|
+
* given `index` (appending a new circuit when none exists), keeps the
|
|
341
|
+
* array sorted by index, and saves. Other circuits are left untouched.
|
|
342
|
+
*
|
|
343
|
+
* Does nothing when the appliance does not exist.
|
|
344
|
+
*
|
|
345
|
+
* @param applianceId The ID of the heatpump appliance
|
|
346
|
+
* @param index The `index` of the heating circuit to patch
|
|
347
|
+
* @param patch The circuit fields to set; fields not given keep their stored value
|
|
348
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
349
|
+
*/
|
|
350
|
+
updateHeatpumpHeatingCircuit(applianceId: string, index: number, patch: Partial<Omit<EnyoHeatpumpApplianceHeatingCircuit, 'index'>>): Promise<void>;
|
|
351
|
+
/**
|
|
352
|
+
* Patches a single domestic-hot-water zone of a heatpump — e.g. to publish
|
|
353
|
+
* a changed time program — without resending the whole `domesticHotWater`
|
|
354
|
+
* array. Works like {@link updateHeatpumpHeatingCircuit}.
|
|
355
|
+
*
|
|
356
|
+
* Appending a zone that does not exist yet requires `targetTemperatureC`
|
|
357
|
+
* in `patch`, because a DHW zone cannot be stored without it.
|
|
358
|
+
*
|
|
359
|
+
* Does nothing when the appliance does not exist.
|
|
360
|
+
*
|
|
361
|
+
* @param applianceId The ID of the heatpump appliance
|
|
362
|
+
* @param index The `index` of the DHW zone to patch
|
|
363
|
+
* @param patch The zone fields to set; fields not given keep their stored value
|
|
364
|
+
* @throws {ApplianceManagerDisposedError} when called after {@link dispose}
|
|
365
|
+
* @throws {Error} when the zone does not exist and `patch` lacks `targetTemperatureC`
|
|
366
|
+
*/
|
|
367
|
+
updateHeatpumpDomesticHotWater(applianceId: string, index: number, patch: Partial<Omit<EnyoHeatpumpApplianceDomesticHotWater, 'index'>>): Promise<void>;
|
|
332
368
|
/**
|
|
333
369
|
* Refetches an appliance from the SDK and reflects the result in the
|
|
334
370
|
* cache. Used by mutating methods to keep cache reads consistent with
|