@enyo-energy/energy-app-sdk 0.0.185 → 0.0.186

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 (34) hide show
  1. package/README.md +38 -0
  2. package/dist/cjs/energy-app-permission.type.cjs +1 -0
  3. package/dist/cjs/energy-app-permission.type.d.cts +2 -1
  4. package/dist/cjs/energy-app.cjs +16 -0
  5. package/dist/cjs/energy-app.d.cts +15 -0
  6. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  7. package/dist/cjs/index.cjs +2 -0
  8. package/dist/cjs/index.d.cts +2 -0
  9. package/dist/cjs/packages/energy-app-epex-spot-price.cjs +2 -0
  10. package/dist/cjs/packages/energy-app-epex-spot-price.d.cts +123 -0
  11. package/dist/cjs/types/enyo-data-bus-value.cjs +18 -1
  12. package/dist/cjs/types/enyo-data-bus-value.d.cts +44 -0
  13. package/dist/cjs/types/enyo-epex-spot-price.cjs +2 -0
  14. package/dist/cjs/types/enyo-epex-spot-price.d.cts +77 -0
  15. package/dist/cjs/types/enyo-inverter-appliance.d.cts +15 -0
  16. package/dist/cjs/version.cjs +1 -1
  17. package/dist/cjs/version.d.cts +1 -1
  18. package/dist/energy-app-permission.type.d.ts +2 -1
  19. package/dist/energy-app-permission.type.js +1 -0
  20. package/dist/energy-app.d.ts +15 -0
  21. package/dist/energy-app.js +16 -0
  22. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/packages/energy-app-epex-spot-price.d.ts +123 -0
  26. package/dist/packages/energy-app-epex-spot-price.js +1 -0
  27. package/dist/types/enyo-data-bus-value.d.ts +44 -0
  28. package/dist/types/enyo-data-bus-value.js +17 -0
  29. package/dist/types/enyo-epex-spot-price.d.ts +77 -0
  30. package/dist/types/enyo-epex-spot-price.js +1 -0
  31. package/dist/types/enyo-inverter-appliance.d.ts +15 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -167,6 +167,7 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
167
167
  | Retrieve secrets from the developer org secret store | [`useSecretManager()`](#usesecretmanager-energyappsecretmanager) |
168
168
  | Submit energy-manager diagnostics | [`useDiagnostics()`](#usediagnostics-energyappdiagnostics) |
169
169
  | Register a weather / PV / dynamic-price forecast provider | [`useWeatherForecasting()`](#useweatherforecasting-energyappweatherforecasting) / [`usePvForecasting()`](#usepvforecasting-energyapppvforecasting) / [`useDynamicPriceForecast()`](#usedynamicpriceforecast-energyappdynamicpriceforecast) |
170
+ | Read EPEX SPOT wholesale prices (incl. negative-price windows) | [`useEpexSpotPrices()`](#useepexspotprices-energyappepexspotprice) |
170
171
  | Manage electricity tariffs (default tariff, price per kWh) | [`useElectricityTariff()`](#useelectricitytariff-energyappelectricitytariff) |
171
172
  | Register a PV system (kWp, DC strings, orientation) | [`usePvSystem()`](#usepvsystem-energyapppvsystem) |
172
173
  | Discover capabilities of the active energy manager | [`useEnergyManager()`](#useenergymanager-energyappenergymanager) |
@@ -353,6 +354,7 @@ Energy Apps use a granular permissions system to control access to system resour
353
354
  - **`DynamicPriceForecastRegister`** / **`DynamicPriceForecastUse`**: Publish / consume dynamic-price forecasts
354
355
  - **`PvSystemRegister`** / **`PvSystemUse`**: Register / read PV system configuration
355
356
  - **`Savings`**: Publish and read back day-scoped savings reports
357
+ - **`EpexSpotPrices`**: Read EPEX SPOT day-ahead wholesale market prices
356
358
 
357
359
  #### Site & Identity Permissions
358
360
 
@@ -1139,6 +1141,42 @@ dpf.onForecastPublished((forecast) => console.log('new forecast', forecast.forec
1139
1141
 
1140
1142
  Publishers need `DynamicPriceForecastRegister`; consumers need `DynamicPriceForecastUse`.
1141
1143
 
1144
+ #### `useEpexSpotPrices(): EnergyAppEpexSpotPrice`
1145
+
1146
+ Read the EPEX SPOT day-ahead wholesale prices the host caches for this device, so an energy manager can decide when to charge, when to run a flexible load, and when to stop exporting PV.
1147
+
1148
+ These are **raw market prices** — no grid fees, levies, taxes or supplier margin — and they go **negative** when supply outruns demand. For what the customer is billed use [`useElectricityPrices()`](#useelectricityprices-energyappenergyprices); for forecasts published by other apps use [`useDynamicPriceForecast()`](#usedynamicpriceforecast-energyappdynamicpriceforecast).
1149
+
1150
+ ```typescript
1151
+ const epex = energyApp.useEpexSpotPrices();
1152
+
1153
+ const now = await epex.getCurrentSpotPrice();
1154
+ if (now && now.pricePerKwh < 0) {
1155
+ // feeding in costs money right now
1156
+ }
1157
+
1158
+ const tomorrow = await epex.getSpotPrices({
1159
+ fromIso: '2026-08-13T00:00:00Z',
1160
+ untilIso: '2026-08-14T00:00:00Z'
1161
+ });
1162
+
1163
+ // Pre-grouped runs of sub-zero periods — the shape curtailment logic wants.
1164
+ const windows = await epex.getNegativePriceWindows();
1165
+
1166
+ // Tomorrow's auction clears around 14:00 CET; re-plan when it lands.
1167
+ epex.onSpotPricesUpdated(prices => scheduler.replan(prices.entries));
1168
+ ```
1169
+
1170
+ Notes worth respecting:
1171
+
1172
+ - **Read `resolution`, don't assume it.** EPEX SPOT day-ahead moved to 15-minute periods in 2025, but older data is still hourly.
1173
+ - **The series may be shorter than you asked for.** Before the day-ahead auction clears (14:00 CET/CEST), only today exists — check the last entry's `endTimestampIso`.
1174
+ - **`retrievedAtIso` tells you how stale the cache is** after the device has been offline.
1175
+ - Prices come both as `pricePerMwh` (the exchange's own unit) and `pricePerKwh` (the SDK's convention).
1176
+ - Inverter appliances carry a `blockFeedInOnNegativePrices` flag in their metadata (`EnyoInverterApplianceMetadata`). It is configuration, not state: whoever controls the inverter is responsible for curtailing export to 0 W while the price is negative and lifting the curtailment afterwards.
1177
+
1178
+ Requires the `EpexSpotPrices` permission.
1179
+
1142
1180
  #### `usePvSystem(): EnergyAppPvSystem`
1143
1181
 
1144
1182
  Register a PV system's structural configuration (kWp, DC string orientations, associated appliances) so other apps can reason about expected production.
@@ -51,5 +51,6 @@ var EnergyAppPermissionTypeEnum;
51
51
  EnergyAppPermissionTypeEnum["ProvidedFiles"] = "ProvidedFiles";
52
52
  EnergyAppPermissionTypeEnum["Automation"] = "Automation";
53
53
  EnergyAppPermissionTypeEnum["Savings"] = "Savings";
54
+ EnergyAppPermissionTypeEnum["EpexSpotPrices"] = "EpexSpotPrices";
54
55
  EnergyAppPermissionTypeEnum["FirmwareRegistry"] = "FirmwareRegistry";
55
56
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -1,4 +1,4 @@
1
- export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation' | 'Savings' | 'FirmwareRegistry';
1
+ export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation' | 'Savings' | 'EpexSpotPrices' | 'FirmwareRegistry';
2
2
  export declare enum EnergyAppPermissionTypeEnum {
3
3
  RestrictedInternetAccess = "RestrictedInternetAccess",
4
4
  NetworkDeviceDiscovery = "NetworkDeviceDiscovery",
@@ -48,5 +48,6 @@ export declare enum EnergyAppPermissionTypeEnum {
48
48
  ProvidedFiles = "ProvidedFiles",
49
49
  Automation = "Automation",
50
50
  Savings = "Savings",
51
+ EpexSpotPrices = "EpexSpotPrices",
51
52
  FirmwareRegistry = "FirmwareRegistry"
52
53
  }
@@ -397,6 +397,22 @@ class EnergyApp {
397
397
  useDeviceTest() {
398
398
  return this.energyAppSdk.useDeviceTest();
399
399
  }
400
+ /**
401
+ * Gets the EPEX SPOT Price API for reading the cleared day-ahead
402
+ * wholesale electricity prices that apply to this device.
403
+ *
404
+ * These are raw market prices — excluding grid fees, levies, taxes and
405
+ * supplier margin, and negative when supply outruns demand — intended for
406
+ * energy-manager decisions such as when to charge, when to run a flexible
407
+ * load, and when to curtail PV feed-in. For customer-billed pricing use
408
+ * {@link useElectricityPrices} instead.
409
+ * @returns The EPEX SPOT Price API instance
410
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
411
+ * permission is not granted.
412
+ */
413
+ useEpexSpotPrices() {
414
+ return this.energyAppSdk.useEpexSpotPrices();
415
+ }
400
416
  /**
401
417
  * Gets the current SDK version.
402
418
  * @returns The semantic version string of the SDK
@@ -42,6 +42,7 @@ import { EnergyAppFirmwareRegistry } from "./packages/energy-app-firmware-regist
42
42
  import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
43
43
  import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
44
44
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
45
+ import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
45
46
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
46
47
  /**
47
48
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -323,6 +324,20 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
323
324
  * @returns The Device Test API instance
324
325
  */
325
326
  useDeviceTest(): EnergyAppDeviceTest;
327
+ /**
328
+ * Gets the EPEX SPOT Price API for reading the cleared day-ahead
329
+ * wholesale electricity prices that apply to this device.
330
+ *
331
+ * These are raw market prices — excluding grid fees, levies, taxes and
332
+ * supplier margin, and negative when supply outruns demand — intended for
333
+ * energy-manager decisions such as when to charge, when to run a flexible
334
+ * load, and when to curtail PV feed-in. For customer-billed pricing use
335
+ * {@link useElectricityPrices} instead.
336
+ * @returns The EPEX SPOT Price API instance
337
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
338
+ * permission is not granted.
339
+ */
340
+ useEpexSpotPrices(): EnergyAppEpexSpotPrice;
326
341
  /**
327
342
  * Gets the current SDK version.
328
343
  * @returns The semantic version string of the SDK
@@ -41,6 +41,7 @@ import { EnergyAppFirmwareRegistry } from "./packages/energy-app-firmware-regist
41
41
  import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
42
42
  import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
43
43
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
44
+ import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
44
45
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
45
46
  export declare enum EnergyAppStateEnum {
46
47
  Launching = "launching",
@@ -154,4 +155,6 @@ export interface EnyoEnergyAppSdk {
154
155
  useSavings: () => EnergyAppSavings;
155
156
  /** Get the Device Test API for answering the host's requests to test detected network devices and report whether appliances were found or created */
156
157
  useDeviceTest: () => EnergyAppDeviceTest;
158
+ /** Get the EPEX SPOT Price API for reading the cleared day-ahead wholesale electricity prices that apply to this device */
159
+ useEpexSpotPrices: () => EnergyAppEpexSpotPrice;
157
160
  }
@@ -128,6 +128,8 @@ __exportStar(require("./packages/energy-app-device-test.cjs"), exports);
128
128
  __exportStar(require("./implementations/device-test/device-test-validators.cjs"), exports);
129
129
  __exportStar(require("./types/enyo-savings.cjs"), exports);
130
130
  __exportStar(require("./packages/energy-app-savings.cjs"), exports);
131
+ __exportStar(require("./types/enyo-epex-spot-price.cjs"), exports);
132
+ __exportStar(require("./packages/energy-app-epex-spot-price.cjs"), exports);
131
133
  __exportStar(require("./types/enyo-automation.cjs"), exports);
132
134
  __exportStar(require("./packages/energy-app-automation.cjs"), exports);
133
135
  __exportStar(require("./implementations/automation/automation-validators.cjs"), exports);
@@ -112,6 +112,8 @@ export * from './packages/energy-app-device-test.cjs';
112
112
  export * from './implementations/device-test/device-test-validators.cjs';
113
113
  export * from './types/enyo-savings.cjs';
114
114
  export * from './packages/energy-app-savings.cjs';
115
+ export * from './types/enyo-epex-spot-price.cjs';
116
+ export * from './packages/energy-app-epex-spot-price.cjs';
115
117
  export * from './types/enyo-automation.cjs';
116
118
  export * from './packages/energy-app-automation.cjs';
117
119
  export * from './implementations/automation/automation-validators.cjs';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,123 @@
1
+ import { EnyoEpexNegativePriceWindow, EnyoEpexSpotPriceEntry, EnyoEpexSpotPriceFilter, EnyoEpexSpotPrices } from "../types/enyo-epex-spot-price.cjs";
2
+ /**
3
+ * Interface for reading EPEX SPOT day-ahead electricity prices.
4
+ *
5
+ * The host fetches the cleared prices that apply to this device and caches
6
+ * them locally, so every app on the device shares one source of truth instead
7
+ * of each one calling a price API itself. An energy manager can use the series
8
+ * to decide when to charge a battery, when to run a flexible load, and — via
9
+ * {@link EnyoInverterApplianceMetadata.blockFeedInOnNegativePrices} — when to
10
+ * stop exporting PV to the grid.
11
+ *
12
+ * What this API returns is **market data, not customer pricing**: the raw
13
+ * exchange result, excluding grid fees, levies, taxes and supplier margin, and
14
+ * negative whenever supply outruns demand. For what the customer is actually
15
+ * billed, use {@link EnergyAppEnergyPrices}; for forward-looking price
16
+ * forecasts published by other apps, use
17
+ * {@link EnergyAppDynamicPriceForecast}.
18
+ *
19
+ * Prices for the following day become known once the auction clears (14:00
20
+ * CET/CEST) — before that, only the current day is available, so an app must
21
+ * always tolerate a series that ends earlier than it would like.
22
+ *
23
+ * Access to this API requires the `EpexSpotPrices` permission
24
+ * ({@link EnergyAppPermissionType}); {@link EnergyApp.useEpexSpotPrices} throws
25
+ * when it has not been granted.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const epex = energyApp.useEpexSpotPrices();
30
+ *
31
+ * const now = await epex.getCurrentSpotPrice();
32
+ * if (now && now.pricePerKwh < 0) {
33
+ * // exporting costs money right now
34
+ * }
35
+ *
36
+ * // Re-plan whenever tomorrow's auction result arrives.
37
+ * epex.onSpotPricesUpdated(prices => scheduler.replan(prices.entries));
38
+ * ```
39
+ */
40
+ export interface EnergyAppEpexSpotPrice {
41
+ /**
42
+ * Retrieves the cached EPEX SPOT price series for a time range.
43
+ *
44
+ * Returns `null` when the host holds no prices at all (for example on a
45
+ * device that has never been online). A series that is merely shorter than
46
+ * the requested range is returned as-is with the entries that are known —
47
+ * check the last entry's `endTimestampIso` before planning against it.
48
+ *
49
+ * @param filter - Optional time range. Defaults to the current delivery
50
+ * period through the end of the known series.
51
+ * @returns Promise resolving to the price series, or `null` if none is available.
52
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
53
+ * permission is not granted.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const prices = await epex.getSpotPrices({
58
+ * fromIso: '2026-08-12T00:00:00Z',
59
+ * untilIso: '2026-08-13T00:00:00Z'
60
+ * });
61
+ * const cheapest = prices?.entries
62
+ * .reduce((min, e) => e.pricePerKwh < min.pricePerKwh ? e : min);
63
+ * ```
64
+ */
65
+ getSpotPrices(filter?: EnyoEpexSpotPriceFilter): Promise<EnyoEpexSpotPrices | null>;
66
+ /**
67
+ * Retrieves the price of the delivery period that contains "now".
68
+ *
69
+ * Convenience wrapper around {@link getSpotPrices} for control loops that
70
+ * only need the price they are currently exposed to.
71
+ *
72
+ * @returns Promise resolving to the current entry, or `null` when the
73
+ * current period is not covered by the cached series.
74
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
75
+ * permission is not granted.
76
+ */
77
+ getCurrentSpotPrice(): Promise<EnyoEpexSpotPriceEntry | null>;
78
+ /**
79
+ * Retrieves the contiguous runs of delivery periods priced below zero
80
+ * within the requested range.
81
+ *
82
+ * This is the same information as {@link getSpotPrices}, pre-grouped for
83
+ * the decisions that care about it: curtailing PV feed-in, and pulling
84
+ * consumption into hours the market is paying for. Windows are returned in
85
+ * chronological order; an empty array means no negative prices are known
86
+ * for the range.
87
+ *
88
+ * @param filter - Optional time range; same defaults as {@link getSpotPrices}.
89
+ * @returns Promise resolving to the negative-price windows in the range.
90
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
91
+ * permission is not granted.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const windows = await epex.getNegativePriceWindows();
96
+ * for (const w of windows) {
97
+ * console.log(`negative from ${w.startIso} for ${w.durationMinutes} min`);
98
+ * }
99
+ * ```
100
+ */
101
+ getNegativePriceWindows(filter?: EnyoEpexSpotPriceFilter): Promise<EnyoEpexNegativePriceWindow[]>;
102
+ /**
103
+ * Registers a listener invoked whenever the host has refreshed the cached
104
+ * series — most importantly when the day-ahead auction for the next day
105
+ * clears, which is the moment a scheduler can plan a full 24 hours ahead.
106
+ *
107
+ * The listener receives the complete refreshed series, not just the
108
+ * changed entries.
109
+ *
110
+ * @param listener - Callback invoked with the newly cached price series.
111
+ * @returns A unique listener id for {@link offSpotPricesUpdated}.
112
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
113
+ * permission is not granted.
114
+ */
115
+ onSpotPricesUpdated(listener: (prices: EnyoEpexSpotPrices) => void | Promise<void>): string;
116
+ /**
117
+ * Removes a previously registered price-update listener. Unknown listener
118
+ * ids are ignored.
119
+ *
120
+ * @param listenerId - The id returned from {@link onSpotPricesUpdated}.
121
+ */
122
+ offSpotPricesUpdated(listenerId: string): void;
123
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EnyoPowerSourceEnum = exports.EnyoHeatpumpControlPurposeEnum = exports.EnyoChargingProfileTypeEnum = exports.EnyoCommandAcknowledgeAnswerEnum = exports.EnyoStorageControlDirectionEnum = exports.EnyoStorageControlModeEnum = exports.EnyoStorageScheduleDirectionEnum = exports.EnyoStorageScheduleModeEnum = exports.EnyoDataBusMessageEnum = exports.EnyoChargeInitiatorEnum = exports.EnyoChargeModeEnum = exports.EnyoChargingStopReason = exports.EnyoChargingMeterValueContext = exports.EnyoStringStateEnum = exports.EnyoInverterStateEnum = exports.EnyoBatteryStateEnum = exports.EnyoGridOperatorLimitTypeEnum = exports.EnyoDataBusCommandReasonCategoryEnum = exports.EnyoDataBusCommandReasonTypeEnum = void 0;
3
+ exports.EnyoPowerSourceEnum = exports.EnyoHeatpumpControlPurposeEnum = exports.EnyoChargingProfileTypeEnum = exports.EnyoCommandAcknowledgeAnswerEnum = exports.EnyoStorageControlDirectionEnum = exports.EnyoStorageControlModeEnum = exports.EnyoStorageScheduleDirectionEnum = exports.EnyoStorageScheduleModeEnum = exports.EnyoDataBusMessageEnum = exports.EnyoChargeInitiatorEnum = exports.EnyoChargeModeEnum = exports.EnyoChargingStopReason = exports.EnyoChargingMeterValueContext = exports.EnyoStringStateEnum = exports.EnyoHeatingRodStateEnum = exports.EnyoInverterStateEnum = exports.EnyoBatteryStateEnum = exports.EnyoGridOperatorLimitTypeEnum = exports.EnyoDataBusCommandReasonCategoryEnum = exports.EnyoDataBusCommandReasonTypeEnum = void 0;
4
4
  /**
5
5
  * Enum representing the reason type for why a data bus command was issued.
6
6
  * Used to attach context to commands for logging, debugging, and UI display.
@@ -100,6 +100,22 @@ var EnyoInverterStateEnum;
100
100
  EnyoInverterStateEnum["Fault"] = "fault";
101
101
  EnyoInverterStateEnum["Standby"] = "standby";
102
102
  })(EnyoInverterStateEnum || (exports.EnyoInverterStateEnum = EnyoInverterStateEnum = {}));
103
+ /**
104
+ * Operating state of a heating rod as reported on the data bus via
105
+ * {@link EnyoDataBusHeatingRodValuesV1}.
106
+ *
107
+ * This is the *observed* state of the element, not the configured mode: the
108
+ * appliance metadata's {@link EnyoHeatingRodApplianceModeEnum} describes how the
109
+ * heating rod is set up, while this enum says whether it is drawing power right
110
+ * now.
111
+ */
112
+ var EnyoHeatingRodStateEnum;
113
+ (function (EnyoHeatingRodStateEnum) {
114
+ /** The heating element is not energized and consumes (close to) no power */
115
+ EnyoHeatingRodStateEnum["Idle"] = "idle";
116
+ /** The heating element is energized and heating */
117
+ EnyoHeatingRodStateEnum["Running"] = "running";
118
+ })(EnyoHeatingRodStateEnum || (exports.EnyoHeatingRodStateEnum = EnyoHeatingRodStateEnum = {}));
103
119
  var EnyoStringStateEnum;
104
120
  (function (EnyoStringStateEnum) {
105
121
  EnyoStringStateEnum["Off"] = "off";
@@ -180,6 +196,7 @@ var EnyoDataBusMessageEnum;
180
196
  EnyoDataBusMessageEnum["ApplianceFlexibilityAnnouncementV1"] = "ApplianceFlexibilityAnnouncementV1";
181
197
  EnyoDataBusMessageEnum["ApplianceStateUpdateV1"] = "ApplianceStateUpdateV1";
182
198
  EnyoDataBusMessageEnum["HeatpumpValuesUpdateV1"] = "HeatpumpValuesUpdateV1";
199
+ EnyoDataBusMessageEnum["HeatingRodValuesUpdateV1"] = "HeatingRodValuesUpdateV1";
183
200
  EnyoDataBusMessageEnum["ChargingStartedV1"] = "ChargingStartedV1";
184
201
  EnyoDataBusMessageEnum["ChargingMeterValuesUpdateV1"] = "ChargingMeterValuesUpdateV1";
185
202
  EnyoDataBusMessageEnum["ChargingStoppedV1"] = "ChargingStoppedV1";
@@ -153,6 +153,21 @@ export declare enum EnyoInverterStateEnum {
153
153
  Fault = "fault",
154
154
  Standby = "standby"
155
155
  }
156
+ /**
157
+ * Operating state of a heating rod as reported on the data bus via
158
+ * {@link EnyoDataBusHeatingRodValuesV1}.
159
+ *
160
+ * This is the *observed* state of the element, not the configured mode: the
161
+ * appliance metadata's {@link EnyoHeatingRodApplianceModeEnum} describes how the
162
+ * heating rod is set up, while this enum says whether it is drawing power right
163
+ * now.
164
+ */
165
+ export declare enum EnyoHeatingRodStateEnum {
166
+ /** The heating element is not energized and consumes (close to) no power */
167
+ Idle = "idle",
168
+ /** The heating element is energized and heating */
169
+ Running = "running"
170
+ }
156
171
  export declare enum EnyoStringStateEnum {
157
172
  Off = "off",
158
173
  Sleeping = "sleeping",
@@ -235,6 +250,7 @@ export declare enum EnyoDataBusMessageEnum {
235
250
  ApplianceFlexibilityAnnouncementV1 = "ApplianceFlexibilityAnnouncementV1",
236
251
  ApplianceStateUpdateV1 = "ApplianceStateUpdateV1",
237
252
  HeatpumpValuesUpdateV1 = "HeatpumpValuesUpdateV1",
253
+ HeatingRodValuesUpdateV1 = "HeatingRodValuesUpdateV1",
238
254
  ChargingStartedV1 = "ChargingStartedV1",
239
255
  ChargingMeterValuesUpdateV1 = "ChargingMeterValuesUpdateV1",
240
256
  ChargingStoppedV1 = "ChargingStoppedV1",
@@ -418,6 +434,34 @@ export interface EnyoDataBusHeatpumpValuesV1 extends EnyoDataBusMessage {
418
434
  gridOperatorLimit?: EnyoGridOperatorLimit;
419
435
  };
420
436
  }
437
+ /**
438
+ * Live values of a heating rod (immersion / electric resistive heating
439
+ * element), published by the integration that owns the appliance.
440
+ *
441
+ * Send this whenever the power draw or the operating state changes, so an
442
+ * energy manager can account for the load and decide how much power to
443
+ * announce back via {@link EnyoDataBusSetHeatingRodAvailablePowerV2}.
444
+ */
445
+ export interface EnyoDataBusHeatingRodValuesV1 extends EnyoDataBusMessage {
446
+ type: 'message';
447
+ message: EnyoDataBusMessageEnum.HeatingRodValuesUpdateV1;
448
+ /** ID of the appliance that delivered these values */
449
+ applianceId: string;
450
+ data: {
451
+ /**
452
+ * Current electrical power consumption of the heating rod in Watt.
453
+ * Always positive (a heating rod only consumes). Omit when the
454
+ * integration cannot measure it — do not send `0` as a stand-in for
455
+ * "unknown", since a consumer cannot tell that apart from "off".
456
+ */
457
+ powerW?: number;
458
+ /**
459
+ * Current operating state of the heating element. Omit when the
460
+ * integration cannot determine it.
461
+ */
462
+ state?: EnyoHeatingRodStateEnum;
463
+ };
464
+ }
421
465
  export interface EnyoDataBusBatteryValuesUpdateV1 extends EnyoDataBusMessage {
422
466
  type: 'message';
423
467
  message: EnyoDataBusMessageEnum.BatteryValuesUpdateV1;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,77 @@
1
+ import { EnyoCurrencyEnum } from "./enyo-currency.cjs";
2
+ import { ForecastResolutionEnum } from "./enyo-forecasting.cjs";
3
+ /**
4
+ * A single cleared market price for one delivery period.
5
+ *
6
+ * Prices are the raw exchange result: they exclude grid fees, levies, taxes
7
+ * and any supplier margin, and they **can be negative** when generation
8
+ * exceeds demand.
9
+ */
10
+ export interface EnyoEpexSpotPriceEntry {
11
+ /** Start of the delivery period in ISO format (inclusive) */
12
+ timestampIso: string;
13
+ /** End of the delivery period in ISO format (exclusive) */
14
+ endTimestampIso: string;
15
+ /**
16
+ * Cleared price per megawatt hour, in the series' currency. This is the
17
+ * unit the exchange publishes (e.g. `-14.2` for EUR -14.20/MWh).
18
+ */
19
+ pricePerMwh: number;
20
+ /**
21
+ * The same price expressed per kilowatt hour (`pricePerMwh / 1000`), for
22
+ * consistency with the rest of the SDK, which prices energy per kWh.
23
+ */
24
+ pricePerKwh: number;
25
+ }
26
+ /**
27
+ * A contiguous run of delivery periods whose spot price is below zero.
28
+ *
29
+ * Convenience shape for the common decision "should PV feed-in be curtailed /
30
+ * should a flexible load be pulled forward?" — see
31
+ * {@link EnyoInverterApplianceMetadata.blockFeedInOnNegativePrices}.
32
+ */
33
+ export interface EnyoEpexNegativePriceWindow {
34
+ /** Start of the window in ISO format (inclusive) */
35
+ startIso: string;
36
+ /** End of the window in ISO format (exclusive) */
37
+ endIso: string;
38
+ /** Length of the window in minutes */
39
+ durationMinutes: number;
40
+ /** Most negative price per kWh observed inside the window */
41
+ minPricePerKwh: number;
42
+ /** Arithmetic mean price per kWh across the window's delivery periods */
43
+ averagePricePerKwh: number;
44
+ }
45
+ /**
46
+ * The EPEX SPOT day-ahead price series that applies to this device.
47
+ */
48
+ export interface EnyoEpexSpotPrices {
49
+ /** Currency of every price in {@link entries} */
50
+ currency: EnyoCurrencyEnum;
51
+ /**
52
+ * Length of one delivery period. EPEX SPOT day-ahead moved to 15-minute
53
+ * periods in 2025, but older data is still hourly — read this instead of
54
+ * assuming a resolution.
55
+ */
56
+ resolution: ForecastResolutionEnum;
57
+ /**
58
+ * When the host last fetched this series from the exchange, in ISO format.
59
+ * Useful to detect stale data when the device was offline.
60
+ */
61
+ retrievedAtIso: string;
62
+ /** Price entries sorted ascending by `timestampIso`, without gaps */
63
+ entries: EnyoEpexSpotPriceEntry[];
64
+ }
65
+ /**
66
+ * Restricts a price query to a time range.
67
+ *
68
+ * Both fields are optional: with an empty filter the host returns the full
69
+ * currently known window — typically today plus tomorrow, once the day-ahead
70
+ * auction has cleared.
71
+ */
72
+ export interface EnyoEpexSpotPriceFilter {
73
+ /** Start of the requested range in ISO format (inclusive). Defaults to the start of the current delivery period. */
74
+ fromIso?: string;
75
+ /** End of the requested range in ISO format (exclusive). Defaults to the end of the known series. */
76
+ untilIso?: string;
77
+ }
@@ -27,4 +27,19 @@ export interface EnyoInverterApplianceMetadata {
27
27
  yearBuilt?: number;
28
28
  /** Groups of PV modules attached to this inverter, each with its own orientation */
29
29
  moduleGroups?: EnyoInverterModuleGroup[];
30
+ /**
31
+ * Whether grid feed-in of this inverter should be blocked while the
32
+ * electricity price is negative.
33
+ *
34
+ * This is a user/installer configuration flag, not a live state: it
35
+ * expresses the intent that during negative market prices (see
36
+ * {@link EnergyAppEpexSpotPrice}) the inverter's export to the grid should
37
+ * be curtailed to 0 W, because feeding in costs money instead of earning
38
+ * it. Whoever controls the inverter — typically the energy manager sending
39
+ * `SetInverterFeedInLimitV1` — is responsible for honoring the flag and for
40
+ * lifting the curtailment once prices turn positive again.
41
+ *
42
+ * `undefined` means "not configured" and should be treated as `false`.
43
+ */
44
+ blockFeedInOnNegativePrices?: boolean;
30
45
  }
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '0.0.185';
12
+ exports.SDK_VERSION = '0.0.186';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.185";
8
+ export declare const SDK_VERSION = "0.0.186";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -1,4 +1,4 @@
1
- export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation' | 'Savings' | 'FirmwareRegistry';
1
+ export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation' | 'Savings' | 'EpexSpotPrices' | 'FirmwareRegistry';
2
2
  export declare enum EnergyAppPermissionTypeEnum {
3
3
  RestrictedInternetAccess = "RestrictedInternetAccess",
4
4
  NetworkDeviceDiscovery = "NetworkDeviceDiscovery",
@@ -48,5 +48,6 @@ export declare enum EnergyAppPermissionTypeEnum {
48
48
  ProvidedFiles = "ProvidedFiles",
49
49
  Automation = "Automation",
50
50
  Savings = "Savings",
51
+ EpexSpotPrices = "EpexSpotPrices",
51
52
  FirmwareRegistry = "FirmwareRegistry"
52
53
  }
@@ -48,5 +48,6 @@ export var EnergyAppPermissionTypeEnum;
48
48
  EnergyAppPermissionTypeEnum["ProvidedFiles"] = "ProvidedFiles";
49
49
  EnergyAppPermissionTypeEnum["Automation"] = "Automation";
50
50
  EnergyAppPermissionTypeEnum["Savings"] = "Savings";
51
+ EnergyAppPermissionTypeEnum["EpexSpotPrices"] = "EpexSpotPrices";
51
52
  EnergyAppPermissionTypeEnum["FirmwareRegistry"] = "FirmwareRegistry";
52
53
  })(EnergyAppPermissionTypeEnum || (EnergyAppPermissionTypeEnum = {}));
@@ -42,6 +42,7 @@ import { EnergyAppFirmwareRegistry } from "./packages/energy-app-firmware-regist
42
42
  import { EnergyAppAutomation } from "./packages/energy-app-automation.js";
43
43
  import { EnergyAppSavings } from "./packages/energy-app-savings.js";
44
44
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.js";
45
+ import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.js";
45
46
  import { UseFetchOptions } from "./types/enyo-fetch.js";
46
47
  /**
47
48
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -323,6 +324,20 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
323
324
  * @returns The Device Test API instance
324
325
  */
325
326
  useDeviceTest(): EnergyAppDeviceTest;
327
+ /**
328
+ * Gets the EPEX SPOT Price API for reading the cleared day-ahead
329
+ * wholesale electricity prices that apply to this device.
330
+ *
331
+ * These are raw market prices — excluding grid fees, levies, taxes and
332
+ * supplier margin, and negative when supply outruns demand — intended for
333
+ * energy-manager decisions such as when to charge, when to run a flexible
334
+ * load, and when to curtail PV feed-in. For customer-billed pricing use
335
+ * {@link useElectricityPrices} instead.
336
+ * @returns The EPEX SPOT Price API instance
337
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
338
+ * permission is not granted.
339
+ */
340
+ useEpexSpotPrices(): EnergyAppEpexSpotPrice;
326
341
  /**
327
342
  * Gets the current SDK version.
328
343
  * @returns The semantic version string of the SDK
@@ -394,6 +394,22 @@ export class EnergyApp {
394
394
  useDeviceTest() {
395
395
  return this.energyAppSdk.useDeviceTest();
396
396
  }
397
+ /**
398
+ * Gets the EPEX SPOT Price API for reading the cleared day-ahead
399
+ * wholesale electricity prices that apply to this device.
400
+ *
401
+ * These are raw market prices — excluding grid fees, levies, taxes and
402
+ * supplier margin, and negative when supply outruns demand — intended for
403
+ * energy-manager decisions such as when to charge, when to run a flexible
404
+ * load, and when to curtail PV feed-in. For customer-billed pricing use
405
+ * {@link useElectricityPrices} instead.
406
+ * @returns The EPEX SPOT Price API instance
407
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
408
+ * permission is not granted.
409
+ */
410
+ useEpexSpotPrices() {
411
+ return this.energyAppSdk.useEpexSpotPrices();
412
+ }
397
413
  /**
398
414
  * Gets the current SDK version.
399
415
  * @returns The semantic version string of the SDK
@@ -41,6 +41,7 @@ import { EnergyAppFirmwareRegistry } from "./packages/energy-app-firmware-regist
41
41
  import { EnergyAppAutomation } from "./packages/energy-app-automation.js";
42
42
  import { EnergyAppSavings } from "./packages/energy-app-savings.js";
43
43
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.js";
44
+ import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.js";
44
45
  import { UseFetchOptions } from "./types/enyo-fetch.js";
45
46
  export declare enum EnergyAppStateEnum {
46
47
  Launching = "launching",
@@ -154,4 +155,6 @@ export interface EnyoEnergyAppSdk {
154
155
  useSavings: () => EnergyAppSavings;
155
156
  /** Get the Device Test API for answering the host's requests to test detected network devices and report whether appliances were found or created */
156
157
  useDeviceTest: () => EnergyAppDeviceTest;
158
+ /** Get the EPEX SPOT Price API for reading the cleared day-ahead wholesale electricity prices that apply to this device */
159
+ useEpexSpotPrices: () => EnergyAppEpexSpotPrice;
157
160
  }
package/dist/index.d.ts CHANGED
@@ -112,6 +112,8 @@ export * from './packages/energy-app-device-test.js';
112
112
  export * from './implementations/device-test/device-test-validators.js';
113
113
  export * from './types/enyo-savings.js';
114
114
  export * from './packages/energy-app-savings.js';
115
+ export * from './types/enyo-epex-spot-price.js';
116
+ export * from './packages/energy-app-epex-spot-price.js';
115
117
  export * from './types/enyo-automation.js';
116
118
  export * from './packages/energy-app-automation.js';
117
119
  export * from './implementations/automation/automation-validators.js';
package/dist/index.js CHANGED
@@ -112,6 +112,8 @@ export * from './packages/energy-app-device-test.js';
112
112
  export * from './implementations/device-test/device-test-validators.js';
113
113
  export * from './types/enyo-savings.js';
114
114
  export * from './packages/energy-app-savings.js';
115
+ export * from './types/enyo-epex-spot-price.js';
116
+ export * from './packages/energy-app-epex-spot-price.js';
115
117
  export * from './types/enyo-automation.js';
116
118
  export * from './packages/energy-app-automation.js';
117
119
  export * from './implementations/automation/automation-validators.js';
@@ -0,0 +1,123 @@
1
+ import { EnyoEpexNegativePriceWindow, EnyoEpexSpotPriceEntry, EnyoEpexSpotPriceFilter, EnyoEpexSpotPrices } from "../types/enyo-epex-spot-price.js";
2
+ /**
3
+ * Interface for reading EPEX SPOT day-ahead electricity prices.
4
+ *
5
+ * The host fetches the cleared prices that apply to this device and caches
6
+ * them locally, so every app on the device shares one source of truth instead
7
+ * of each one calling a price API itself. An energy manager can use the series
8
+ * to decide when to charge a battery, when to run a flexible load, and — via
9
+ * {@link EnyoInverterApplianceMetadata.blockFeedInOnNegativePrices} — when to
10
+ * stop exporting PV to the grid.
11
+ *
12
+ * What this API returns is **market data, not customer pricing**: the raw
13
+ * exchange result, excluding grid fees, levies, taxes and supplier margin, and
14
+ * negative whenever supply outruns demand. For what the customer is actually
15
+ * billed, use {@link EnergyAppEnergyPrices}; for forward-looking price
16
+ * forecasts published by other apps, use
17
+ * {@link EnergyAppDynamicPriceForecast}.
18
+ *
19
+ * Prices for the following day become known once the auction clears (14:00
20
+ * CET/CEST) — before that, only the current day is available, so an app must
21
+ * always tolerate a series that ends earlier than it would like.
22
+ *
23
+ * Access to this API requires the `EpexSpotPrices` permission
24
+ * ({@link EnergyAppPermissionType}); {@link EnergyApp.useEpexSpotPrices} throws
25
+ * when it has not been granted.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const epex = energyApp.useEpexSpotPrices();
30
+ *
31
+ * const now = await epex.getCurrentSpotPrice();
32
+ * if (now && now.pricePerKwh < 0) {
33
+ * // exporting costs money right now
34
+ * }
35
+ *
36
+ * // Re-plan whenever tomorrow's auction result arrives.
37
+ * epex.onSpotPricesUpdated(prices => scheduler.replan(prices.entries));
38
+ * ```
39
+ */
40
+ export interface EnergyAppEpexSpotPrice {
41
+ /**
42
+ * Retrieves the cached EPEX SPOT price series for a time range.
43
+ *
44
+ * Returns `null` when the host holds no prices at all (for example on a
45
+ * device that has never been online). A series that is merely shorter than
46
+ * the requested range is returned as-is with the entries that are known —
47
+ * check the last entry's `endTimestampIso` before planning against it.
48
+ *
49
+ * @param filter - Optional time range. Defaults to the current delivery
50
+ * period through the end of the known series.
51
+ * @returns Promise resolving to the price series, or `null` if none is available.
52
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
53
+ * permission is not granted.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const prices = await epex.getSpotPrices({
58
+ * fromIso: '2026-08-12T00:00:00Z',
59
+ * untilIso: '2026-08-13T00:00:00Z'
60
+ * });
61
+ * const cheapest = prices?.entries
62
+ * .reduce((min, e) => e.pricePerKwh < min.pricePerKwh ? e : min);
63
+ * ```
64
+ */
65
+ getSpotPrices(filter?: EnyoEpexSpotPriceFilter): Promise<EnyoEpexSpotPrices | null>;
66
+ /**
67
+ * Retrieves the price of the delivery period that contains "now".
68
+ *
69
+ * Convenience wrapper around {@link getSpotPrices} for control loops that
70
+ * only need the price they are currently exposed to.
71
+ *
72
+ * @returns Promise resolving to the current entry, or `null` when the
73
+ * current period is not covered by the cached series.
74
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
75
+ * permission is not granted.
76
+ */
77
+ getCurrentSpotPrice(): Promise<EnyoEpexSpotPriceEntry | null>;
78
+ /**
79
+ * Retrieves the contiguous runs of delivery periods priced below zero
80
+ * within the requested range.
81
+ *
82
+ * This is the same information as {@link getSpotPrices}, pre-grouped for
83
+ * the decisions that care about it: curtailing PV feed-in, and pulling
84
+ * consumption into hours the market is paying for. Windows are returned in
85
+ * chronological order; an empty array means no negative prices are known
86
+ * for the range.
87
+ *
88
+ * @param filter - Optional time range; same defaults as {@link getSpotPrices}.
89
+ * @returns Promise resolving to the negative-price windows in the range.
90
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
91
+ * permission is not granted.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const windows = await epex.getNegativePriceWindows();
96
+ * for (const w of windows) {
97
+ * console.log(`negative from ${w.startIso} for ${w.durationMinutes} min`);
98
+ * }
99
+ * ```
100
+ */
101
+ getNegativePriceWindows(filter?: EnyoEpexSpotPriceFilter): Promise<EnyoEpexNegativePriceWindow[]>;
102
+ /**
103
+ * Registers a listener invoked whenever the host has refreshed the cached
104
+ * series — most importantly when the day-ahead auction for the next day
105
+ * clears, which is the moment a scheduler can plan a full 24 hours ahead.
106
+ *
107
+ * The listener receives the complete refreshed series, not just the
108
+ * changed entries.
109
+ *
110
+ * @param listener - Callback invoked with the newly cached price series.
111
+ * @returns A unique listener id for {@link offSpotPricesUpdated}.
112
+ * @throws {EnergyAppPermissionNotGrantedError} If the `EpexSpotPrices`
113
+ * permission is not granted.
114
+ */
115
+ onSpotPricesUpdated(listener: (prices: EnyoEpexSpotPrices) => void | Promise<void>): string;
116
+ /**
117
+ * Removes a previously registered price-update listener. Unknown listener
118
+ * ids are ignored.
119
+ *
120
+ * @param listenerId - The id returned from {@link onSpotPricesUpdated}.
121
+ */
122
+ offSpotPricesUpdated(listenerId: string): void;
123
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -153,6 +153,21 @@ export declare enum EnyoInverterStateEnum {
153
153
  Fault = "fault",
154
154
  Standby = "standby"
155
155
  }
156
+ /**
157
+ * Operating state of a heating rod as reported on the data bus via
158
+ * {@link EnyoDataBusHeatingRodValuesV1}.
159
+ *
160
+ * This is the *observed* state of the element, not the configured mode: the
161
+ * appliance metadata's {@link EnyoHeatingRodApplianceModeEnum} describes how the
162
+ * heating rod is set up, while this enum says whether it is drawing power right
163
+ * now.
164
+ */
165
+ export declare enum EnyoHeatingRodStateEnum {
166
+ /** The heating element is not energized and consumes (close to) no power */
167
+ Idle = "idle",
168
+ /** The heating element is energized and heating */
169
+ Running = "running"
170
+ }
156
171
  export declare enum EnyoStringStateEnum {
157
172
  Off = "off",
158
173
  Sleeping = "sleeping",
@@ -235,6 +250,7 @@ export declare enum EnyoDataBusMessageEnum {
235
250
  ApplianceFlexibilityAnnouncementV1 = "ApplianceFlexibilityAnnouncementV1",
236
251
  ApplianceStateUpdateV1 = "ApplianceStateUpdateV1",
237
252
  HeatpumpValuesUpdateV1 = "HeatpumpValuesUpdateV1",
253
+ HeatingRodValuesUpdateV1 = "HeatingRodValuesUpdateV1",
238
254
  ChargingStartedV1 = "ChargingStartedV1",
239
255
  ChargingMeterValuesUpdateV1 = "ChargingMeterValuesUpdateV1",
240
256
  ChargingStoppedV1 = "ChargingStoppedV1",
@@ -418,6 +434,34 @@ export interface EnyoDataBusHeatpumpValuesV1 extends EnyoDataBusMessage {
418
434
  gridOperatorLimit?: EnyoGridOperatorLimit;
419
435
  };
420
436
  }
437
+ /**
438
+ * Live values of a heating rod (immersion / electric resistive heating
439
+ * element), published by the integration that owns the appliance.
440
+ *
441
+ * Send this whenever the power draw or the operating state changes, so an
442
+ * energy manager can account for the load and decide how much power to
443
+ * announce back via {@link EnyoDataBusSetHeatingRodAvailablePowerV2}.
444
+ */
445
+ export interface EnyoDataBusHeatingRodValuesV1 extends EnyoDataBusMessage {
446
+ type: 'message';
447
+ message: EnyoDataBusMessageEnum.HeatingRodValuesUpdateV1;
448
+ /** ID of the appliance that delivered these values */
449
+ applianceId: string;
450
+ data: {
451
+ /**
452
+ * Current electrical power consumption of the heating rod in Watt.
453
+ * Always positive (a heating rod only consumes). Omit when the
454
+ * integration cannot measure it — do not send `0` as a stand-in for
455
+ * "unknown", since a consumer cannot tell that apart from "off".
456
+ */
457
+ powerW?: number;
458
+ /**
459
+ * Current operating state of the heating element. Omit when the
460
+ * integration cannot determine it.
461
+ */
462
+ state?: EnyoHeatingRodStateEnum;
463
+ };
464
+ }
421
465
  export interface EnyoDataBusBatteryValuesUpdateV1 extends EnyoDataBusMessage {
422
466
  type: 'message';
423
467
  message: EnyoDataBusMessageEnum.BatteryValuesUpdateV1;
@@ -97,6 +97,22 @@ export var EnyoInverterStateEnum;
97
97
  EnyoInverterStateEnum["Fault"] = "fault";
98
98
  EnyoInverterStateEnum["Standby"] = "standby";
99
99
  })(EnyoInverterStateEnum || (EnyoInverterStateEnum = {}));
100
+ /**
101
+ * Operating state of a heating rod as reported on the data bus via
102
+ * {@link EnyoDataBusHeatingRodValuesV1}.
103
+ *
104
+ * This is the *observed* state of the element, not the configured mode: the
105
+ * appliance metadata's {@link EnyoHeatingRodApplianceModeEnum} describes how the
106
+ * heating rod is set up, while this enum says whether it is drawing power right
107
+ * now.
108
+ */
109
+ export var EnyoHeatingRodStateEnum;
110
+ (function (EnyoHeatingRodStateEnum) {
111
+ /** The heating element is not energized and consumes (close to) no power */
112
+ EnyoHeatingRodStateEnum["Idle"] = "idle";
113
+ /** The heating element is energized and heating */
114
+ EnyoHeatingRodStateEnum["Running"] = "running";
115
+ })(EnyoHeatingRodStateEnum || (EnyoHeatingRodStateEnum = {}));
100
116
  export var EnyoStringStateEnum;
101
117
  (function (EnyoStringStateEnum) {
102
118
  EnyoStringStateEnum["Off"] = "off";
@@ -177,6 +193,7 @@ export var EnyoDataBusMessageEnum;
177
193
  EnyoDataBusMessageEnum["ApplianceFlexibilityAnnouncementV1"] = "ApplianceFlexibilityAnnouncementV1";
178
194
  EnyoDataBusMessageEnum["ApplianceStateUpdateV1"] = "ApplianceStateUpdateV1";
179
195
  EnyoDataBusMessageEnum["HeatpumpValuesUpdateV1"] = "HeatpumpValuesUpdateV1";
196
+ EnyoDataBusMessageEnum["HeatingRodValuesUpdateV1"] = "HeatingRodValuesUpdateV1";
180
197
  EnyoDataBusMessageEnum["ChargingStartedV1"] = "ChargingStartedV1";
181
198
  EnyoDataBusMessageEnum["ChargingMeterValuesUpdateV1"] = "ChargingMeterValuesUpdateV1";
182
199
  EnyoDataBusMessageEnum["ChargingStoppedV1"] = "ChargingStoppedV1";
@@ -0,0 +1,77 @@
1
+ import { EnyoCurrencyEnum } from "./enyo-currency.js";
2
+ import { ForecastResolutionEnum } from "./enyo-forecasting.js";
3
+ /**
4
+ * A single cleared market price for one delivery period.
5
+ *
6
+ * Prices are the raw exchange result: they exclude grid fees, levies, taxes
7
+ * and any supplier margin, and they **can be negative** when generation
8
+ * exceeds demand.
9
+ */
10
+ export interface EnyoEpexSpotPriceEntry {
11
+ /** Start of the delivery period in ISO format (inclusive) */
12
+ timestampIso: string;
13
+ /** End of the delivery period in ISO format (exclusive) */
14
+ endTimestampIso: string;
15
+ /**
16
+ * Cleared price per megawatt hour, in the series' currency. This is the
17
+ * unit the exchange publishes (e.g. `-14.2` for EUR -14.20/MWh).
18
+ */
19
+ pricePerMwh: number;
20
+ /**
21
+ * The same price expressed per kilowatt hour (`pricePerMwh / 1000`), for
22
+ * consistency with the rest of the SDK, which prices energy per kWh.
23
+ */
24
+ pricePerKwh: number;
25
+ }
26
+ /**
27
+ * A contiguous run of delivery periods whose spot price is below zero.
28
+ *
29
+ * Convenience shape for the common decision "should PV feed-in be curtailed /
30
+ * should a flexible load be pulled forward?" — see
31
+ * {@link EnyoInverterApplianceMetadata.blockFeedInOnNegativePrices}.
32
+ */
33
+ export interface EnyoEpexNegativePriceWindow {
34
+ /** Start of the window in ISO format (inclusive) */
35
+ startIso: string;
36
+ /** End of the window in ISO format (exclusive) */
37
+ endIso: string;
38
+ /** Length of the window in minutes */
39
+ durationMinutes: number;
40
+ /** Most negative price per kWh observed inside the window */
41
+ minPricePerKwh: number;
42
+ /** Arithmetic mean price per kWh across the window's delivery periods */
43
+ averagePricePerKwh: number;
44
+ }
45
+ /**
46
+ * The EPEX SPOT day-ahead price series that applies to this device.
47
+ */
48
+ export interface EnyoEpexSpotPrices {
49
+ /** Currency of every price in {@link entries} */
50
+ currency: EnyoCurrencyEnum;
51
+ /**
52
+ * Length of one delivery period. EPEX SPOT day-ahead moved to 15-minute
53
+ * periods in 2025, but older data is still hourly — read this instead of
54
+ * assuming a resolution.
55
+ */
56
+ resolution: ForecastResolutionEnum;
57
+ /**
58
+ * When the host last fetched this series from the exchange, in ISO format.
59
+ * Useful to detect stale data when the device was offline.
60
+ */
61
+ retrievedAtIso: string;
62
+ /** Price entries sorted ascending by `timestampIso`, without gaps */
63
+ entries: EnyoEpexSpotPriceEntry[];
64
+ }
65
+ /**
66
+ * Restricts a price query to a time range.
67
+ *
68
+ * Both fields are optional: with an empty filter the host returns the full
69
+ * currently known window — typically today plus tomorrow, once the day-ahead
70
+ * auction has cleared.
71
+ */
72
+ export interface EnyoEpexSpotPriceFilter {
73
+ /** Start of the requested range in ISO format (inclusive). Defaults to the start of the current delivery period. */
74
+ fromIso?: string;
75
+ /** End of the requested range in ISO format (exclusive). Defaults to the end of the known series. */
76
+ untilIso?: string;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -27,4 +27,19 @@ export interface EnyoInverterApplianceMetadata {
27
27
  yearBuilt?: number;
28
28
  /** Groups of PV modules attached to this inverter, each with its own orientation */
29
29
  moduleGroups?: EnyoInverterModuleGroup[];
30
+ /**
31
+ * Whether grid feed-in of this inverter should be blocked while the
32
+ * electricity price is negative.
33
+ *
34
+ * This is a user/installer configuration flag, not a live state: it
35
+ * expresses the intent that during negative market prices (see
36
+ * {@link EnergyAppEpexSpotPrice}) the inverter's export to the grid should
37
+ * be curtailed to 0 W, because feeding in costs money instead of earning
38
+ * it. Whoever controls the inverter — typically the energy manager sending
39
+ * `SetInverterFeedInLimitV1` — is responsible for honoring the flag and for
40
+ * lifting the curtailment once prices turn positive again.
41
+ *
42
+ * `undefined` means "not configured" and should be treated as `false`.
43
+ */
44
+ blockFeedInOnNegativePrices?: boolean;
30
45
  }
package/dist/version.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.185";
8
+ export declare const SDK_VERSION = "0.0.186";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export const SDK_VERSION = '0.0.185';
8
+ export const SDK_VERSION = '0.0.186';
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enyo-energy/energy-app-sdk",
3
- "version": "0.0.185",
3
+ "version": "0.0.186",
4
4
  "description": "enyo Energy App SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",