@enyo-energy/energy-app-sdk 1.26.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.
Files changed (58) hide show
  1. package/README.md +162 -3
  2. package/dist/cjs/energy-app-model-feature.enum.cjs +2 -0
  3. package/dist/cjs/energy-app-model-feature.enum.d.cts +2 -0
  4. package/dist/cjs/energy-app.cjs +17 -0
  5. package/dist/cjs/energy-app.d.cts +16 -0
  6. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  7. package/dist/cjs/implementations/appliances/appliance-manager.cjs +72 -0
  8. package/dist/cjs/implementations/appliances/appliance-manager.d.cts +37 -1
  9. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +329 -0
  10. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +105 -0
  11. package/dist/cjs/index.cjs +3 -0
  12. package/dist/cjs/index.d.cts +3 -0
  13. package/dist/cjs/integrations/heatpump-integration-energy-app.cjs +29 -1
  14. package/dist/cjs/integrations/heatpump-integration-energy-app.d.cts +24 -2
  15. package/dist/cjs/packages/energy-app-cascade.cjs +2 -0
  16. package/dist/cjs/packages/energy-app-cascade.d.cts +374 -0
  17. package/dist/cjs/packages/energy-app-electricity-tariff.d.cts +41 -3
  18. package/dist/cjs/types/enyo-appliance.cjs +8 -0
  19. package/dist/cjs/types/enyo-appliance.d.cts +9 -1
  20. package/dist/cjs/types/enyo-data-bus-value.cjs +4 -0
  21. package/dist/cjs/types/enyo-data-bus-value.d.cts +184 -4
  22. package/dist/cjs/types/enyo-electricity-tariff.cjs +12 -1
  23. package/dist/cjs/types/enyo-electricity-tariff.d.cts +42 -0
  24. package/dist/cjs/types/enyo-heatpump-appliance.cjs +69 -1
  25. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +246 -3
  26. package/dist/cjs/types/enyo-meter-cascade.cjs +49 -0
  27. package/dist/cjs/types/enyo-meter-cascade.d.cts +229 -0
  28. package/dist/cjs/version.cjs +1 -1
  29. package/dist/cjs/version.d.cts +1 -1
  30. package/dist/energy-app-model-feature.enum.d.ts +2 -0
  31. package/dist/energy-app-model-feature.enum.js +2 -0
  32. package/dist/energy-app.d.ts +16 -0
  33. package/dist/energy-app.js +17 -0
  34. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  35. package/dist/implementations/appliances/appliance-manager.d.ts +37 -1
  36. package/dist/implementations/appliances/appliance-manager.js +72 -0
  37. package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +105 -0
  38. package/dist/implementations/heatpump/heatpump-metadata-validators.js +320 -0
  39. package/dist/index.d.ts +3 -0
  40. package/dist/index.js +3 -0
  41. package/dist/integrations/heatpump-integration-energy-app.d.ts +24 -2
  42. package/dist/integrations/heatpump-integration-energy-app.js +30 -2
  43. package/dist/packages/energy-app-cascade.d.ts +374 -0
  44. package/dist/packages/energy-app-cascade.js +1 -0
  45. package/dist/packages/energy-app-electricity-tariff.d.ts +41 -3
  46. package/dist/types/enyo-appliance.d.ts +9 -1
  47. package/dist/types/enyo-appliance.js +8 -0
  48. package/dist/types/enyo-data-bus-value.d.ts +184 -4
  49. package/dist/types/enyo-data-bus-value.js +4 -0
  50. package/dist/types/enyo-electricity-tariff.d.ts +42 -0
  51. package/dist/types/enyo-electricity-tariff.js +11 -0
  52. package/dist/types/enyo-heatpump-appliance.d.ts +246 -3
  53. package/dist/types/enyo-heatpump-appliance.js +68 -0
  54. package/dist/types/enyo-meter-cascade.d.ts +229 -0
  55. package/dist/types/enyo-meter-cascade.js +46 -0
  56. package/dist/version.d.ts +1 -1
  57. package/dist/version.js +1 -1
  58. 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) |
@@ -1715,6 +1716,18 @@ const prices = await tariffs.getPrices(EnyoTariffDirectionEnum.Consumption, { fr
1715
1716
  const needsGridFee = !prices?.includes.includes(EnyoPriceComponentEnum.GridFee);
1716
1717
  ```
1717
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
+
1718
1731
  The app that integrates a provider owns the other side. It answers when the user picks it, sets the
1719
1732
  tariff once it is actually usable, and pushes prices as they arrive:
1720
1733
 
@@ -1785,6 +1798,115 @@ const fees = await gridFee.getGridFeeValues({ fromIso, untilIso });
1785
1798
 
1786
1799
  Publishers need `GridFeeRegister`; consumers need `GridFeeUse`.
1787
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
+
1788
1910
  #### `useWeatherForecasting(): EnergyAppWeatherForecasting`
1789
1911
 
1790
1912
  Register a weather-forecast provider (e.g. wraps an external API) and / or consume forecasts by zip code or coordinates.
@@ -2971,11 +3093,48 @@ class MyHeatpumpApp extends HeatpumpIntegrationEnergyApp {
2971
3093
 
2972
3094
  Drives a heatpump. Manages building / DHW overheating commands and grid-power-availability announcements.
2973
3095
 
2974
- - **Subscribed commands:** `HeatpumpOverheatingV1`, `HeatpumpAvailablePowerAnnouncementV1`, `GridOperatorPowerLimitationV1` (broadcast)
2975
- - **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`.
2976
3099
  - **Publish helpers:**
2977
3100
  - `publishHeatpumpValuesUpdate(applianceId, values)` — operation mode, electrical and thermal power, energies.
2978
- - `publishHeatpumpTemperatures(applianceId, temperatures)` — outdoor, flow, return, DHW tanks, heating circuits, buffer tank.
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 }`.
2979
3138
 
2980
3139
  ### WallboxIntegrationEnergyApp
2981
3140
 
@@ -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 */
@@ -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