@enyo-energy/energy-app-sdk 0.0.131 → 0.0.133

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 (46) hide show
  1. package/README.md +207 -0
  2. package/dist/cjs/energy-app-permission.type.cjs +2 -0
  3. package/dist/cjs/energy-app-permission.type.d.cts +3 -1
  4. package/dist/cjs/energy-app.cjs +20 -0
  5. package/dist/cjs/energy-app.d.cts +18 -0
  6. package/dist/cjs/enyo-energy-app-sdk.d.cts +6 -0
  7. package/dist/cjs/implementations/network-devices/network-access-guard.cjs +239 -0
  8. package/dist/cjs/implementations/network-devices/network-access-guard.d.cts +165 -0
  9. package/dist/cjs/implementations/network-devices/network-device-manager.cjs +416 -0
  10. package/dist/cjs/implementations/network-devices/network-device-manager.d.cts +266 -0
  11. package/dist/cjs/index.cjs +5 -0
  12. package/dist/cjs/index.d.cts +5 -0
  13. package/dist/cjs/packages/energy-app-configuration-manager.cjs +2 -0
  14. package/dist/cjs/packages/energy-app-configuration-manager.d.cts +56 -0
  15. package/dist/cjs/packages/energy-app-dynamic-price-forecast.cjs +2 -0
  16. package/dist/cjs/packages/energy-app-dynamic-price-forecast.d.cts +141 -0
  17. package/dist/cjs/types/enyo-battery-appliance.d.cts +7 -0
  18. package/dist/cjs/types/enyo-configuration-manager.cjs +2 -0
  19. package/dist/cjs/types/enyo-configuration-manager.d.cts +84 -0
  20. package/dist/cjs/types/enyo-forecasting.d.cts +83 -0
  21. package/dist/cjs/types/enyo-inverter-appliance.d.cts +2 -0
  22. package/dist/cjs/version.cjs +1 -1
  23. package/dist/cjs/version.d.cts +1 -1
  24. package/dist/energy-app-permission.type.d.ts +3 -1
  25. package/dist/energy-app-permission.type.js +2 -0
  26. package/dist/energy-app.d.ts +18 -0
  27. package/dist/energy-app.js +20 -0
  28. package/dist/enyo-energy-app-sdk.d.ts +6 -0
  29. package/dist/implementations/network-devices/network-access-guard.d.ts +165 -0
  30. package/dist/implementations/network-devices/network-access-guard.js +235 -0
  31. package/dist/implementations/network-devices/network-device-manager.d.ts +266 -0
  32. package/dist/implementations/network-devices/network-device-manager.js +412 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +5 -0
  35. package/dist/packages/energy-app-configuration-manager.d.ts +56 -0
  36. package/dist/packages/energy-app-configuration-manager.js +1 -0
  37. package/dist/packages/energy-app-dynamic-price-forecast.d.ts +141 -0
  38. package/dist/packages/energy-app-dynamic-price-forecast.js +1 -0
  39. package/dist/types/enyo-battery-appliance.d.ts +7 -0
  40. package/dist/types/enyo-configuration-manager.d.ts +84 -0
  41. package/dist/types/enyo-configuration-manager.js +1 -0
  42. package/dist/types/enyo-forecasting.d.ts +83 -0
  43. package/dist/types/enyo-inverter-appliance.d.ts +2 -0
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,10 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
23
23
  - [User Features](#user-features)
24
24
  - [App Intelligence](#app-intelligence)
25
25
  - [Advanced Modbus Integration](#advanced-modbus-integration)
26
+ - [Network Devices & Access Recovery](#network-devices--access-recovery)
27
+ - [NetworkAccessGuard](#networkaccessguard)
28
+ - [NetworkDeviceManager](#networkdevicemanager)
29
+ - [Startup pattern](#startup-pattern)
26
30
  - [Device Integrations](#device-integrations)
27
31
  - [IntegrationEnergyApp (Base Class)](#integrationenergyapp-base-class)
28
32
  - [HeatpumpIntegrationEnergyApp](#heatpumpintegrationenergyapp)
@@ -612,6 +616,50 @@ settings.listenForSettingsChanges((settingName, newValue) => {
612
616
  const allSettings = await settings.getSettingsConfig();
613
617
  ```
614
618
 
619
+ #### `useConfigurationManager(): EnergyAppConfigurationManager`
620
+
621
+ Register **internal**, non user-facing configurations for your package and react to value changes. Unlike `useSettings()`, configurations registered here are NOT rendered in the Energy App UI — they are intended for values the package itself reads and writes at runtime (e.g. internal feature toggles, tuning parameters, calibration values) and need to be persisted across restarts.
622
+
623
+ Each configuration is addressed by a unique `key` and is either of type `number` (with optional `minValue` / `maxValue` / `step` constraints) or `select` (with a fixed list of allowed `selectOptions`).
624
+
625
+ ```typescript
626
+ const configManager = energyApp.useConfigurationManager();
627
+
628
+ // Register the full set of internal configurations in a single call
629
+ await configManager.registerConfigurations([
630
+ {
631
+ key: 'pollIntervalMs',
632
+ type: 'number',
633
+ defaultValue: 5000,
634
+ numberOptions: { minValue: 1000, maxValue: 60000, step: 1000 }
635
+ },
636
+ {
637
+ key: 'logLevel',
638
+ type: 'select',
639
+ defaultValue: 'info',
640
+ selectOptions: [
641
+ { value: 'debug' },
642
+ { value: 'info' },
643
+ { value: 'warn' },
644
+ { value: 'error' }
645
+ ]
646
+ }
647
+ ]);
648
+
649
+ // Read the current (or default) value for a configuration
650
+ const pollInterval = await configManager.getConfiguration('pollIntervalMs');
651
+
652
+ // React to value changes
653
+ configManager.onConfigurationChanged(event => {
654
+ console.log(
655
+ `Configuration ${event.key} changed from ${event.previousValue} to ${event.newValue}`
656
+ );
657
+ });
658
+
659
+ // Remove configurations (e.g. on cleanup or after a migration)
660
+ await configManager.unregisterConfigurations(['logLevel']);
661
+ ```
662
+
615
663
  #### `useElectricityPrices(): EnergyAppElectricityPrices`
616
664
 
617
665
  Access electricity pricing information:
@@ -906,6 +954,165 @@ The Modbus implementation follows a clean, modular architecture:
906
954
 
907
955
  This modular design ensures maintainability, testability, and extensibility for future enhancements.
908
956
 
957
+ ## Network Devices & Access Recovery
958
+
959
+ Packages that talk to local hardware over TCP (Modbus, SunSpec, EEBUS over SHIP, REST) must deal with two failure modes the `useNetworkDevices()` API exposes only at a low level:
960
+
961
+ 1. **Network-access-denied errors** — `EnyoNetworkDevice.accessStatus` is device-wide (`granted | denied | pending`). It does **not** carry per-port grants. A device can report `'granted'` while your package never received (or has since lost) access to its Modbus port, and the first symptom is the runtime error `[NET] Network access denied: Host '...:502' is not in the allowed list.` from a poll cycle.
962
+ 2. **User-driven access transitions** — the user revokes or re-grants access via the UI; the SDK fires `listenForDeviceAccessChange`, and packages need to disconnect / reconnect accordingly.
963
+
964
+ The SDK ships two classes that encapsulate this lifecycle so packages don't reinvent it: a low-level **`NetworkAccessGuard`** for access-denied recovery, and a higher-level **`NetworkDeviceManager`** that wires the guard together with all the network-device listeners and the package's `ApplianceManager`.
965
+
966
+ ### NetworkAccessGuard
967
+
968
+ `NetworkAccessGuard` recovers from access-denied errors raised by the SDK's network layer. Construct one per package with the ports it needs and a restored-callback that reconnects whatever client was reading from the device.
969
+
970
+ ```typescript
971
+ import { NetworkAccessGuard } from '@enyo-energy/energy-app-sdk';
972
+
973
+ const accessGuard = new NetworkAccessGuard(energyApp, {
974
+ ports: [502],
975
+ onAccessRestored: async (networkDeviceId) => {
976
+ await myModbusPool.reconnect(networkDeviceId);
977
+ },
978
+ });
979
+
980
+ // Precondition before a Modbus connect:
981
+ if (!(await accessGuard.ensureAccess(networkDevice.id))) {
982
+ console.warn(`Modbus port access not granted for ${networkDevice.hostname}`);
983
+ return;
984
+ }
985
+
986
+ // Wrap any Modbus read so an access-denied error triggers recovery:
987
+ const registers = await accessGuard.withAccessGuard(networkDevice.id, () =>
988
+ modbusClient.readHoldingRegisters(40000, 4),
989
+ );
990
+ ```
991
+
992
+ Recovery lifecycle:
993
+
994
+ 1. A read fails inside `withAccessGuard`. The guard detects the access-denied error via `NetworkAccessGuard.isAccessDeniedError(error)`, re-throws it to the caller (so the current poll cycle fails fast), and kicks off recovery in the background.
995
+ 2. The guard calls `requestDeviceAccess(deviceId, ports)`. If the SDK answers `'granted'` synchronously (the port was just missing from the allow-list and no user prompt is needed), the `onAccessRestored` handler fires immediately.
996
+ 3. Otherwise the device stays in a pending set and the `listenForDeviceAccessChange` registration fires the handler when the SDK reports the device flipped to `'granted'`.
997
+
998
+ Re-entrancy: repeated `recoverAccess(...)` calls for the same device while a recovery is already in flight are coalesced — the handler runs exactly once per restoration.
999
+
1000
+ The guard exposes:
1001
+
1002
+ | Method | Purpose |
1003
+ | --- | --- |
1004
+ | `static isAccessDeniedError(error)` | Recognise the SDK's access-denied error string |
1005
+ | `ensureAccess(deviceId)` | Idempotent port-allow-list request before a connect |
1006
+ | `withAccessGuard(deviceId, action)` | Wrap any async TCP call — recovers on access-denied |
1007
+ | `recoverAccess(deviceId)` | Explicit recovery trigger after catching an access-denied error |
1008
+ | `onAccessRestored(handler)` / `onAccessDenied(handler)` | Register additional handlers at runtime; returns a disposer |
1009
+ | `isRecovering(deviceId)` | Introspect whether a recovery is in flight |
1010
+ | `dispose()` | Tear down the SDK listener |
1011
+
1012
+ ### NetworkDeviceManager
1013
+
1014
+ `NetworkDeviceManager` is the recommended entry point for any package that owns appliances backed by NetworkDevices. It bundles a `NetworkAccessGuard` with the three NetworkDevice-related SDK listeners (`listenForDeviceAccessChange`, `listenForDetectedDevice`, `listenForNetworkDeviceRemoved`) and resolves every event into **per-appliance callbacks** by joining against the package's `ApplianceManager`.
1015
+
1016
+ ```typescript
1017
+ import {
1018
+ ApplianceManager,
1019
+ EnergyApp,
1020
+ NetworkDeviceManager,
1021
+ } from '@enyo-energy/energy-app-sdk';
1022
+
1023
+ const energyApp = new EnergyApp();
1024
+ const applianceManager = await ApplianceManager.initialize(energyApp);
1025
+
1026
+ const networkManager = await NetworkDeviceManager.initialize(
1027
+ energyApp,
1028
+ applianceManager,
1029
+ {
1030
+ ports: [502],
1031
+ autoToggleApplianceState: true,
1032
+ onApplianceAccessRestored: async ({ appliance, networkDeviceId }) => {
1033
+ // Re-establish a Modbus session and restart the polling loop for this appliance.
1034
+ await myModbusPool.reconnect(networkDeviceId);
1035
+ },
1036
+ onApplianceAccessRevoked: async ({ appliance, networkDeviceId }) => {
1037
+ // User revoked access in the UI — tear down the connection.
1038
+ await myModbusPool.disconnect(networkDeviceId);
1039
+ },
1040
+ onApplianceAccessDenied: async ({ appliance, networkDeviceId }) => {
1041
+ // An access-denied error was just observed at runtime — mark the
1042
+ // appliance offline. `autoToggleApplianceState: true` already does
1043
+ // this; the handler is here for any custom side-effects.
1044
+ },
1045
+ onApplianceNetworkDeviceRemoved: async ({ appliance, networkDeviceId }) => {
1046
+ await myModbusPool.disconnect(networkDeviceId);
1047
+ },
1048
+ onNetworkDeviceDetected: async (devices) => {
1049
+ // New device found — classify + connect.
1050
+ for (const device of devices) {
1051
+ await classifyAndConnect(device);
1052
+ }
1053
+ },
1054
+ onNetworkDeviceAccessChanged: async (deviceId, status) => {
1055
+ // Optional: raw access-status passthrough, fires even for devices
1056
+ // the package has no appliances on yet. Useful for first-time
1057
+ // onboarding where a 'granted' transition needs to drive a discovery
1058
+ // pass before any appliance exists.
1059
+ },
1060
+ },
1061
+ );
1062
+
1063
+ // Every Modbus read inside the poll loop:
1064
+ await networkManager.withAccessGuard(networkDeviceId, () =>
1065
+ modbusClient.readHoldingRegisters(40000, 4),
1066
+ );
1067
+ ```
1068
+
1069
+ What the manager handles for you:
1070
+
1071
+ - **Access-denied recovery** — `withAccessGuard` / `ensureAccess` delegate to the bundled `NetworkAccessGuard`.
1072
+ - **User-driven transitions** — on `listenForDeviceAccessChange`, the manager dispatches `onApplianceAccessRestored` on `'granted'` and `onApplianceAccessRevoked` on `'denied'` / `'pending'`, resolving each transition into the per-appliance events your reconnect/disconnect code needs.
1073
+ - **Listener dedup** — the manager registers its access-change listener *before* the guard's, so a `'granted'` event during a recovery cycle dispatches only once (the manager observes `isRecovering(deviceId) === true` and skips, letting the guard's own restored callback win).
1074
+ - **Device removal** — on `listenForNetworkDeviceRemoved`, the manager fires `onApplianceNetworkDeviceRemoved` per affected appliance and clears its cache.
1075
+ - **Optional auto-state toggle** — with `autoToggleApplianceState: true`, the manager flips affected appliances to `EnyoApplianceStateEnum.Offline` on denial / revocation / removal, and back to `EnyoApplianceStateEnum.Connected` on restoration, via `applianceManager.updateApplianceState(...)`.
1076
+
1077
+ Every handler is also registerable at runtime via `manager.onApplianceAccessRestored(fn)` / `onApplianceAccessDenied(fn)` / `onApplianceAccessRevoked(fn)` / `onApplianceNetworkDeviceRemoved(fn)` / `onNetworkDeviceDetected(fn)` / `onNetworkDeviceAccessChanged(fn)`, each returning a disposer.
1078
+
1079
+ ### Startup pattern
1080
+
1081
+ The SDK's `listenForDeviceAccessChange` only fires on *transitions* — devices that are already `'granted'` from a previous session won't trigger it. Recommended startup flow for a package that supports both first-onboarding and warm restarts:
1082
+
1083
+ ```typescript
1084
+ client.register(async () => {
1085
+ const applianceManager = await ApplianceManager.initialize(client);
1086
+ const networkManager = await NetworkDeviceManager.initialize(
1087
+ client,
1088
+ applianceManager,
1089
+ {
1090
+ ports: [502],
1091
+ onApplianceAccessRestored: ({ networkDeviceId }) => connectDevice(networkDeviceId),
1092
+ onApplianceAccessRevoked: ({ networkDeviceId }) => disconnectDevice(networkDeviceId),
1093
+ onNetworkDeviceDetected: async (devices) => {
1094
+ for (const device of devices) await connectDevice(device.id);
1095
+ },
1096
+ },
1097
+ );
1098
+
1099
+ // Warm-restart: reconnect to devices that already have access.
1100
+ const granted = await client.useNetworkDevices().getDevices({ accessStatus: 'granted' });
1101
+ for (const device of granted) {
1102
+ await connectDevice(device.id);
1103
+ }
1104
+
1105
+ client.updateEnergyAppState(EnergyAppStateEnum.Running);
1106
+ });
1107
+
1108
+ async function connectDevice(networkDeviceId: string) {
1109
+ if (!(await networkManager.ensureAccess(networkDeviceId))) return;
1110
+ // ...classify, open modbus client, register appliances...
1111
+ }
1112
+ ```
1113
+
1114
+ This pattern matches the wiring used by real Sungrow / Fronius energy-app packages: one `NetworkDeviceManager` per package, `ensureAccess` before every connect, `withAccessGuard` around every poll, and a single `getDevices({ accessStatus: 'granted' })` pass at startup to cover the warm-restart case.
1115
+
909
1116
  ## Device Integrations
910
1117
 
911
1118
  Device Integrations are the high-level building blocks for apps that **drive a real device** — a heatpump, EV wallbox, PV inverter, battery storage system, or air-conditioning unit. Each integration class hides the data-bus plumbing for its appliance type so you only implement the business logic that physically controls the device.
@@ -31,6 +31,8 @@ var EnergyAppPermissionTypeEnum;
31
31
  EnergyAppPermissionTypeEnum["WeatherForecastUse"] = "WeatherForecastUse";
32
32
  EnergyAppPermissionTypeEnum["PvForecastRegister"] = "PvForecastRegister";
33
33
  EnergyAppPermissionTypeEnum["PvForecastUse"] = "PvForecastUse";
34
+ EnergyAppPermissionTypeEnum["DynamicPriceForecastRegister"] = "DynamicPriceForecastRegister";
35
+ EnergyAppPermissionTypeEnum["DynamicPriceForecastUse"] = "DynamicPriceForecastUse";
34
36
  EnergyAppPermissionTypeEnum["PvSystemRegister"] = "PvSystemRegister";
35
37
  EnergyAppPermissionTypeEnum["PvSystemUse"] = "PvSystemUse";
36
38
  EnergyAppPermissionTypeEnum["InverterControlCommands"] = "InverterControlCommands";
@@ -1,4 +1,4 @@
1
- export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp';
1
+ export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | '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' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp';
2
2
  export declare enum EnergyAppPermissionTypeEnum {
3
3
  RestrictedInternetAccess = "RestrictedInternetAccess",
4
4
  NetworkDeviceDiscovery = "NetworkDeviceDiscovery",
@@ -28,6 +28,8 @@ export declare enum EnergyAppPermissionTypeEnum {
28
28
  WeatherForecastUse = "WeatherForecastUse",
29
29
  PvForecastRegister = "PvForecastRegister",
30
30
  PvForecastUse = "PvForecastUse",
31
+ DynamicPriceForecastRegister = "DynamicPriceForecastRegister",
32
+ DynamicPriceForecastUse = "DynamicPriceForecastUse",
31
33
  PvSystemRegister = "PvSystemRegister",
32
34
  PvSystemUse = "PvSystemUse",
33
35
  InverterControlCommands = "InverterControlCommands",
@@ -167,6 +167,16 @@ class EnergyApp {
167
167
  usePvForecasting() {
168
168
  return this.energyAppSdk.usePvForecasting();
169
169
  }
170
+ /**
171
+ * Gets the Dynamic Price Forecast API for publishing and consuming
172
+ * forward-looking electricity price forecasts (e.g. day-ahead spot
173
+ * prices). The data is forecast only — see
174
+ * {@link EnergyAppDynamicPriceForecast} for the full contract.
175
+ * @returns The Dynamic Price Forecast API instance
176
+ */
177
+ useDynamicPriceForecast() {
178
+ return this.energyAppSdk.useDynamicPriceForecast();
179
+ }
170
180
  /**
171
181
  * Gets the PV System API for managing PV system registrations and configurations.
172
182
  * Provides methods to register, retrieve, update, and remove PV systems
@@ -278,6 +288,16 @@ class EnergyApp {
278
288
  useGridConnectionPoint() {
279
289
  return this.energyAppSdk.useGridConnectionPoint();
280
290
  }
291
+ /**
292
+ * Gets the Configuration Manager API for registering internal (non user-facing)
293
+ * package configurations. Configurations are typed as either `number` or
294
+ * `select`, addressed by a unique key, and emit change events when their
295
+ * persisted value is updated.
296
+ * @returns The Configuration Manager API instance
297
+ */
298
+ useConfigurationManager() {
299
+ return this.energyAppSdk.useConfigurationManager();
300
+ }
281
301
  /**
282
302
  * Gets the current SDK version.
283
303
  * @returns The semantic version string of the SDK
@@ -22,6 +22,7 @@ import { EnergyAppEnergyManager } from "./packages/energy-app-energy-manager.cjs
22
22
  import { EnergyAppElectricityTariff } from "./packages/energy-app-electricity-tariff.cjs";
23
23
  import { EnergyAppWeatherForecasting } from "./packages/energy-app-weather-forecasting.cjs";
24
24
  import { EnergyAppPvForecasting } from "./packages/energy-app-pv-forecasting.cjs";
25
+ import { EnergyAppDynamicPriceForecast } from "./packages/energy-app-dynamic-price-forecast.cjs";
25
26
  import { EnergyAppPvSystem } from "./packages/energy-app-pv-system.cjs";
26
27
  import { EnergyAppSequenceGenerator } from "./packages/energy-app-sequence-generator.cjs";
27
28
  import { EnergyAppModbusRtu } from "./packages/energy-app-modbus-rtu.cjs";
@@ -33,6 +34,7 @@ import { EnergyAppLearningPhase } from "./packages/energy-app-learning-phase.cjs
33
34
  import { EnergyAppWifi } from "./packages/energy-app-wifi.cjs";
34
35
  import { EnergyAppUdp } from "./packages/energy-app-udp.cjs";
35
36
  import { EnergyAppGridConnectionPoint } from "./packages/energy-app-grid-connection-point.cjs";
37
+ import { EnergyAppConfigurationManager } from "./packages/energy-app-configuration-manager.cjs";
36
38
  /**
37
39
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
38
40
  * to the runtime-provided `energyAppSdkInstance` global.
@@ -125,6 +127,14 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
125
127
  * @returns The PV Forecasting API instance
126
128
  */
127
129
  usePvForecasting(): EnergyAppPvForecasting;
130
+ /**
131
+ * Gets the Dynamic Price Forecast API for publishing and consuming
132
+ * forward-looking electricity price forecasts (e.g. day-ahead spot
133
+ * prices). The data is forecast only — see
134
+ * {@link EnergyAppDynamicPriceForecast} for the full contract.
135
+ * @returns The Dynamic Price Forecast API instance
136
+ */
137
+ useDynamicPriceForecast(): EnergyAppDynamicPriceForecast;
128
138
  /**
129
139
  * Gets the PV System API for managing PV system registrations and configurations.
130
140
  * Provides methods to register, retrieve, update, and remove PV systems
@@ -214,6 +224,14 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
214
224
  * @returns The Grid Connection Point API instance
215
225
  */
216
226
  useGridConnectionPoint(): EnergyAppGridConnectionPoint;
227
+ /**
228
+ * Gets the Configuration Manager API for registering internal (non user-facing)
229
+ * package configurations. Configurations are typed as either `number` or
230
+ * `select`, addressed by a unique key, and emit change events when their
231
+ * persisted value is updated.
232
+ * @returns The Configuration Manager API instance
233
+ */
234
+ useConfigurationManager(): EnergyAppConfigurationManager;
217
235
  /**
218
236
  * Gets the current SDK version.
219
237
  * @returns The semantic version string of the SDK
@@ -21,6 +21,7 @@ import { EnergyAppEnergyManager } from "./packages/energy-app-energy-manager.cjs
21
21
  import { EnergyAppElectricityTariff } from "./packages/energy-app-electricity-tariff.cjs";
22
22
  import { EnergyAppWeatherForecasting } from "./packages/energy-app-weather-forecasting.cjs";
23
23
  import { EnergyAppPvForecasting } from "./packages/energy-app-pv-forecasting.cjs";
24
+ import { EnergyAppDynamicPriceForecast } from "./packages/energy-app-dynamic-price-forecast.cjs";
24
25
  import { EnergyAppPvSystem } from "./packages/energy-app-pv-system.cjs";
25
26
  import { EnergyAppSequenceGenerator } from "./packages/energy-app-sequence-generator.cjs";
26
27
  import { EnergyAppModbusRtu } from "./packages/energy-app-modbus-rtu.cjs";
@@ -32,6 +33,7 @@ import { EnergyAppLearningPhase } from "./packages/energy-app-learning-phase.cjs
32
33
  import { EnergyAppWifi } from "./packages/energy-app-wifi.cjs";
33
34
  import { EnergyAppUdp } from "./packages/energy-app-udp.cjs";
34
35
  import { EnergyAppGridConnectionPoint } from "./packages/energy-app-grid-connection-point.cjs";
36
+ import { EnergyAppConfigurationManager } from "./packages/energy-app-configuration-manager.cjs";
35
37
  export declare enum EnergyAppStateEnum {
36
38
  Launching = "launching",
37
39
  Running = "running",
@@ -102,6 +104,8 @@ export interface EnyoEnergyAppSdk {
102
104
  useWeatherForecasting: () => EnergyAppWeatherForecasting;
103
105
  /** Get the PV Forecasting API for managing PV forecast providers and retrieving PV forecasts */
104
106
  usePvForecasting: () => EnergyAppPvForecasting;
107
+ /** Get the Dynamic Price Forecast API for publishing and consuming forward-looking electricity price forecasts */
108
+ useDynamicPriceForecast: () => EnergyAppDynamicPriceForecast;
105
109
  /** Get the PV System API for managing PV system registrations and configurations */
106
110
  usePvSystem: () => EnergyAppPvSystem;
107
111
  /** Get the Sequence Generator API for generating unique sequential numbers per named sequence */
@@ -124,4 +128,6 @@ export interface EnyoEnergyAppSdk {
124
128
  useUdp: () => EnergyAppUdp;
125
129
  /** Get the Grid Connection Point API for retrieving fuse rating, phase count, and power limit of the site's grid connection */
126
130
  useGridConnectionPoint: () => EnergyAppGridConnectionPoint;
131
+ /** Get the Configuration Manager API for registering internal (non user-facing) package configurations with change notifications */
132
+ useConfigurationManager: () => EnergyAppConfigurationManager;
127
133
  }
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetworkAccessGuard = void 0;
4
+ /**
5
+ * Recovers from "Network access denied" failures raised by the SDK's
6
+ * network layer when a Modbus (or other TCP) call hits a host whose
7
+ * port is not in the package's allowed list.
8
+ *
9
+ * Why this exists: `EnyoNetworkDevice.accessStatus` is device-wide
10
+ * (`granted | denied | pending`) — it does NOT carry per-port grants.
11
+ * A device can report `'granted'` while our package never received
12
+ * (or has since lost) access to its Modbus port, and the first
13
+ * symptom is a runtime `[NET] Network access denied: Host '...:502'
14
+ * is not in the allowed list. Allowed origins: [], Allowed network
15
+ * devices: []` error from a poll cycle. The guard provides one place
16
+ * to detect that error, re-request access, and dispatch a reconnect
17
+ * once access returns.
18
+ *
19
+ * Typical wiring:
20
+ * 1. Construct one guard per package with the ports it needs.
21
+ * 2. On every Modbus call site, either:
22
+ * - wrap the call in {@link withAccessGuard}, which recovers in
23
+ * the background on access-denied errors and re-throws, or
24
+ * - catch the error, check {@link isAccessDeniedError}, and call
25
+ * {@link recoverAccess} explicitly.
26
+ * 3. Register an `onAccessRestored` handler (via the config or
27
+ * {@link onAccessRestored}) that performs the reconnect.
28
+ *
29
+ * Re-entrancy: repeated {@link recoverAccess} calls for the same
30
+ * device while a recovery is already in flight are coalesced — the
31
+ * restored handlers run exactly once per restoration.
32
+ *
33
+ * Precondition use: {@link ensureAccess} exposes the same idempotent
34
+ * `requestDeviceAccess` call so callers can use a single API for both
35
+ * "before a connect, make sure we have access" and "we just lost
36
+ * access, recover it".
37
+ */
38
+ class NetworkAccessGuard {
39
+ energyApp;
40
+ /** Devices we've re-requested access for and are awaiting a 'granted' signal on. */
41
+ pending = new Set();
42
+ /** Listener IDs registered against the network-devices service. */
43
+ listenerIds = [];
44
+ restoredHandlers = new Set();
45
+ deniedHandlers = new Set();
46
+ ports;
47
+ enableLogging;
48
+ disposed = false;
49
+ /**
50
+ * Recognise the SDK's network-access-denied error so callers don't
51
+ * have to re-implement the substring check. Matches both variants
52
+ * observed in production logs.
53
+ * @param error The error to inspect (any value is accepted)
54
+ * @returns `true` if the error message looks like an access-denied error
55
+ */
56
+ static isAccessDeniedError(error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ return /Network access denied|not in the allowed list/i.test(message);
59
+ }
60
+ /**
61
+ * Constructs a guard and immediately subscribes to the SDK's
62
+ * device-access-change events.
63
+ * @param energyApp The {@link EnergyApp} instance used to call the network-devices API
64
+ * @param config Required ports plus optional initial handlers and logging flag
65
+ */
66
+ constructor(energyApp, config) {
67
+ this.energyApp = energyApp;
68
+ this.ports = config.ports;
69
+ this.enableLogging = config.enableLogging ?? true;
70
+ if (config.onAccessRestored) {
71
+ this.restoredHandlers.add(config.onAccessRestored);
72
+ }
73
+ if (config.onAccessDenied) {
74
+ this.deniedHandlers.add(config.onAccessDenied);
75
+ }
76
+ const listenerId = this.energyApp.useNetworkDevices().listenForDeviceAccessChange((deviceId, status) => this.handleAccessChange(deviceId, status));
77
+ this.listenerIds.push(listenerId);
78
+ }
79
+ /**
80
+ * Registers an additional handler to be invoked whenever access
81
+ * is restored for any NetworkDevice the guard is recovering.
82
+ * @param handler The handler to register
83
+ * @returns A disposer that removes the handler again
84
+ */
85
+ onAccessRestored(handler) {
86
+ this.restoredHandlers.add(handler);
87
+ return () => this.restoredHandlers.delete(handler);
88
+ }
89
+ /**
90
+ * Registers an additional handler to be invoked whenever a
91
+ * recovery cycle starts (i.e. an access-denied error was just
92
+ * observed). Useful for marking dependent appliances offline.
93
+ * @param handler The handler to register
94
+ * @returns A disposer that removes the handler again
95
+ */
96
+ onAccessDenied(handler) {
97
+ this.deniedHandlers.add(handler);
98
+ return () => this.deniedHandlers.delete(handler);
99
+ }
100
+ /**
101
+ * Idempotently request access to the configured ports for the
102
+ * given NetworkDevice. Safe to call as a precondition before any
103
+ * Modbus operation.
104
+ * @param networkDeviceId The NetworkDevice to request access on
105
+ * @returns `true` when the SDK reports access is granted, `false` otherwise
106
+ */
107
+ async ensureAccess(networkDeviceId) {
108
+ try {
109
+ const result = await this.energyApp
110
+ .useNetworkDevices()
111
+ .requestDeviceAccess(networkDeviceId, this.ports);
112
+ return result.status === 'granted';
113
+ }
114
+ catch (error) {
115
+ if (this.enableLogging) {
116
+ console.warn(`[NetworkAccessGuard] requestDeviceAccess for ${networkDeviceId} failed: ${String(error)}`);
117
+ }
118
+ return false;
119
+ }
120
+ }
121
+ /**
122
+ * Wraps an async action so that an access-denied error
123
+ * automatically triggers a background recovery. The original
124
+ * error is re-thrown so the caller can fail-fast on the failed
125
+ * read; the next read after access is restored will succeed.
126
+ *
127
+ * Use this on Modbus call sites for the simplest integration:
128
+ * ```ts
129
+ * await guard.withAccessGuard(networkDeviceId, () =>
130
+ * modbusClient.readHoldingRegisters(...));
131
+ * ```
132
+ * @param networkDeviceId The NetworkDevice the action targets
133
+ * @param action The async operation to perform
134
+ * @returns The resolved value of `action`
135
+ */
136
+ async withAccessGuard(networkDeviceId, action) {
137
+ try {
138
+ return await action();
139
+ }
140
+ catch (error) {
141
+ if (NetworkAccessGuard.isAccessDeniedError(error)) {
142
+ void this.recoverAccess(networkDeviceId);
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+ /**
148
+ * Drive recovery from an access-denied error: notify denied
149
+ * handlers, re-request access, and fire the restored handlers
150
+ * once access is granted. The restored handlers run at most once
151
+ * per recovery, regardless of whether the grant is observed
152
+ * synchronously (from the request response) or asynchronously
153
+ * (from the access-change listener).
154
+ * @param networkDeviceId The NetworkDevice whose access needs to be recovered
155
+ */
156
+ async recoverAccess(networkDeviceId) {
157
+ if (this.disposed)
158
+ return;
159
+ if (this.pending.has(networkDeviceId)) {
160
+ if (this.enableLogging) {
161
+ console.debug(`[NetworkAccessGuard] recovery already in flight for ${networkDeviceId}`);
162
+ }
163
+ return;
164
+ }
165
+ this.pending.add(networkDeviceId);
166
+ if (this.enableLogging) {
167
+ console.log(`[NetworkAccessGuard] re-requesting access for ${networkDeviceId} after access-denied`);
168
+ }
169
+ await this.fireDenied(networkDeviceId);
170
+ const granted = await this.ensureAccess(networkDeviceId);
171
+ if (!granted) {
172
+ // Leave the device in `pending` so the listener can fire the
173
+ // handlers when the user eventually accepts the request.
174
+ return;
175
+ }
176
+ // Synchronous grant — the listener won't observe a transition,
177
+ // so fire the handlers ourselves and clear the pending mark.
178
+ this.pending.delete(networkDeviceId);
179
+ await this.fireRestored(networkDeviceId);
180
+ }
181
+ /**
182
+ * Returns `true` if a recovery cycle is currently in flight for
183
+ * the given NetworkDevice. Mainly useful for tests and
184
+ * introspection.
185
+ */
186
+ isRecovering(networkDeviceId) {
187
+ return this.pending.has(networkDeviceId);
188
+ }
189
+ /**
190
+ * Tears down the SDK listener and clears all handlers. After
191
+ * calling this the guard should no longer be used.
192
+ */
193
+ dispose() {
194
+ if (this.disposed)
195
+ return;
196
+ this.disposed = true;
197
+ const service = this.energyApp.useNetworkDevices();
198
+ for (const id of this.listenerIds) {
199
+ service.removeListener(id);
200
+ }
201
+ this.listenerIds.length = 0;
202
+ this.restoredHandlers.clear();
203
+ this.deniedHandlers.clear();
204
+ this.pending.clear();
205
+ }
206
+ async handleAccessChange(networkDeviceId, status) {
207
+ if (status !== 'granted')
208
+ return;
209
+ if (!this.pending.has(networkDeviceId))
210
+ return;
211
+ this.pending.delete(networkDeviceId);
212
+ await this.fireRestored(networkDeviceId);
213
+ }
214
+ async fireRestored(networkDeviceId) {
215
+ for (const handler of this.restoredHandlers) {
216
+ try {
217
+ await handler(networkDeviceId);
218
+ }
219
+ catch (error) {
220
+ if (this.enableLogging) {
221
+ console.warn(`[NetworkAccessGuard] access-restored handler for ${networkDeviceId} failed: ${String(error)}`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ async fireDenied(networkDeviceId) {
227
+ for (const handler of this.deniedHandlers) {
228
+ try {
229
+ await handler(networkDeviceId);
230
+ }
231
+ catch (error) {
232
+ if (this.enableLogging) {
233
+ console.warn(`[NetworkAccessGuard] access-denied handler for ${networkDeviceId} failed: ${String(error)}`);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ exports.NetworkAccessGuard = NetworkAccessGuard;