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

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 (36) hide show
  1. package/README.md +469 -11
  2. package/dist/cjs/implementations/appliances/appliance-manager.cjs +400 -235
  3. package/dist/cjs/implementations/appliances/appliance-manager.d.cts +228 -70
  4. package/dist/cjs/implementations/appliances/in-memory-appliance-manager.cjs +15 -14
  5. package/dist/cjs/implementations/appliances/in-memory-appliance-manager.d.cts +3 -10
  6. package/dist/cjs/implementations/network-devices/network-access-guard.cjs +69 -35
  7. package/dist/cjs/implementations/network-devices/network-access-guard.d.cts +31 -6
  8. package/dist/cjs/implementations/network-devices/network-device-manager.cjs +208 -36
  9. package/dist/cjs/implementations/network-devices/network-device-manager.d.cts +143 -23
  10. package/dist/cjs/implementations/ocpp/ocpp16.cjs +1 -0
  11. package/dist/cjs/implementations/ocpp/ocpp16.d.cts +1 -0
  12. package/dist/cjs/integrations/wallbox-integration-energy-app.d.cts +4 -2
  13. package/dist/cjs/types/enyo-appliance.d.cts +63 -0
  14. package/dist/cjs/types/enyo-charger-appliance.cjs +2 -0
  15. package/dist/cjs/types/enyo-charger-appliance.d.cts +3 -1
  16. package/dist/cjs/types/enyo-data-bus-value.d.cts +8 -2
  17. package/dist/cjs/version.cjs +1 -1
  18. package/dist/cjs/version.d.cts +1 -1
  19. package/dist/implementations/appliances/appliance-manager.d.ts +228 -70
  20. package/dist/implementations/appliances/appliance-manager.js +396 -234
  21. package/dist/implementations/appliances/in-memory-appliance-manager.d.ts +3 -10
  22. package/dist/implementations/appliances/in-memory-appliance-manager.js +15 -14
  23. package/dist/implementations/network-devices/network-access-guard.d.ts +31 -6
  24. package/dist/implementations/network-devices/network-access-guard.js +69 -35
  25. package/dist/implementations/network-devices/network-device-manager.d.ts +143 -23
  26. package/dist/implementations/network-devices/network-device-manager.js +206 -35
  27. package/dist/implementations/ocpp/ocpp16.d.ts +1 -0
  28. package/dist/implementations/ocpp/ocpp16.js +1 -0
  29. package/dist/integrations/wallbox-integration-energy-app.d.ts +4 -2
  30. package/dist/types/enyo-appliance.d.ts +63 -0
  31. package/dist/types/enyo-charger-appliance.d.ts +3 -1
  32. package/dist/types/enyo-charger-appliance.js +2 -0
  33. package/dist/types/enyo-data-bus-value.d.ts +8 -2
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -22,11 +22,17 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
22
22
  - [Energy Resources](#energy-resources)
23
23
  - [User Features](#user-features)
24
24
  - [App Intelligence](#app-intelligence)
25
+ - [Networking & Protocols](#networking--protocols)
26
+ - [Location & Site](#location--site)
27
+ - [Energy Domain APIs](#energy-domain-apis)
28
+ - [Operational Utilities](#operational-utilities)
25
29
  - [Advanced Modbus Integration](#advanced-modbus-integration)
30
+ - [Appliance Management](#appliance-management)
26
31
  - [Network Devices & Access Recovery](#network-devices--access-recovery)
27
32
  - [NetworkAccessGuard](#networkaccessguard)
28
33
  - [NetworkDeviceManager](#networkdevicemanager)
29
34
  - [Startup pattern](#startup-pattern)
35
+ - [Retry Framework](#retry-framework)
30
36
  - [Device Integrations](#device-integrations)
31
37
  - [IntegrationEnergyApp (Base Class)](#integrationenergyapp-base-class)
32
38
  - [HeatpumpIntegrationEnergyApp](#heatpumpintegrationenergyapp)
@@ -124,6 +130,25 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
124
130
  | Forecast EV charging demand | [`EvChargingForecast`](#evchargingforecast) |
125
131
  | Forecast heatpump electrical consumption | [`HeatpumpConsumptionForecast`](#heatpumpconsumptionforecast) |
126
132
  | Forecast heatpump DHW tank temperature | [`HeatpumpDhwTemperatureForecast`](#heatpumpdhwtemperatureforecast) |
133
+ | Talk to an EEBUS / SHIP / SPINE device | [`useEebus()`](#useeebus-energyappeebus) |
134
+ | Speak MQTT (SDK broker or external) | [`useMqtt()`](#usemqtt-energyappmqtt) |
135
+ | Scan or talk to Bluetooth LE peripherals | [`useBluetooth()`](#usebluetooth-energyappbluetooth) |
136
+ | Send/receive UDP datagrams | [`useUdp()`](#useudp-energyappudp) |
137
+ | Read serial Modbus RTU | [`useModbusRtu()`](#usemodbusrtu-energyappmodbusrtu) |
138
+ | List known WiFi SSIDs in range | [`useWifi()`](#usewifi-energyappwifi) |
139
+ | Query historical timeseries (PV, battery, meter, …) | [`useTimeseries()`](#usetimeseries-energyapptimeseries) |
140
+ | Read site location (zip or coordinates) | [`useLocation()`](#uselocation-energyapplocation) |
141
+ | Read grid connection point (fuse, phases, max power) | [`useGridConnectionPoint()`](#usegridconnectionpoint-energyappgridconnectionpoint) |
142
+ | Retrieve secrets from the developer org secret store | [`useSecretManager()`](#usesecretmanager-energyappsecretmanager) |
143
+ | Submit energy-manager diagnostics | [`useDiagnostics()`](#usediagnostics-energyappdiagnostics) |
144
+ | Register a weather / PV / dynamic-price forecast provider | [`useWeatherForecasting()`](#useweatherforecasting-energyappweatherforecasting) / [`usePvForecasting()`](#usepvforecasting-energyapppvforecasting) / [`useDynamicPriceForecast()`](#usedynamicpriceforecast-energyappdynamicpriceforecast) |
145
+ | Manage electricity tariffs (default tariff, price per kWh) | [`useElectricityTariff()`](#useelectricitytariff-energyappelectricitytariff) |
146
+ | Register a PV system (kWp, DC strings, orientation) | [`usePvSystem()`](#usepvsystem-energyapppvsystem) |
147
+ | Discover capabilities of the active energy manager | [`useEnergyManager()`](#useenergymanager-energyappenergymanager) |
148
+ | Drive a multi-step onboarding flow | [`useOnboarding()`](#useonboarding-energyapponboarding) |
149
+ | Allocate process-local sequential IDs | [`useSequenceGenerator()`](#usesequencegenerator-energyappsequencegenerator) |
150
+ | Manage retries with circuit-breaker semantics | [`RetryManager`](#retry-framework) |
151
+ | Keep an `applianceId` cache in sync with the SDK | [`ApplianceManager`](#appliance-management) |
127
152
 
128
153
  > **Rule of thumb:** if your app *receives* commands and drives hardware, you want an **Integration**. If your app *produces* predictions, you want a **Forecast** (and likely an `EnergyManagerEnergyApp` to wire several together).
129
154
 
@@ -229,9 +254,14 @@ const packageDef = defineEnergyAppPackage({
229
254
  - `Meter`: Energy metering applications
230
255
  - `EnergyManagement`: Overall energy optimization
231
256
  - `HeatPump`: Heat pump control systems
257
+ - `AirConditioning`: Air-conditioning units
232
258
  - `BatteryStorage`: Battery management
233
259
  - `ClimateControl`: HVAC and climate systems
234
- - `ElectricityTariff`: Dynamic pricing integration
260
+ - `DynamicElectricityTariff`: Dynamic / spot-price tariff providers
261
+ - `StaticElectricityTariff`: Fixed-price tariff providers
262
+ - `TemperatureSensor`: Standalone temperature sensors
263
+ - `SmartPlug`: Smart-plug appliances
264
+ - `Other`: Anything not covered above
235
265
 
236
266
  ### Permissions System
237
267
 
@@ -260,6 +290,42 @@ Energy Apps use a granular permissions system to control access to system resour
260
290
  - **`Vehicle`**: Access vehicle information
261
291
  - **`Charge`**: Manage charging sessions
262
292
 
293
+ #### Command Permissions
294
+
295
+ - **`InverterControlCommands`**: Send inverter control commands (e.g. feed-in limit)
296
+ - **`BatteryControlCommands`**: Send battery / storage control commands
297
+ - **`ChargerControlCommands`**: Send wallbox / charger control commands
298
+
299
+ #### Networking & Protocol Permissions
300
+
301
+ - **`ModbusRtu`**: Communicate over Modbus RTU (serial)
302
+ - **`EebusDeviceManagement`**: Pair / discover / connect EEBUS devices
303
+ - **`EebusDataAccess`**: Read EEBUS use-case data
304
+ - **`EebusControl`**: Send EEBUS control commands (write features)
305
+ - **`Mqtt`**: Connect to the internal SDK MQTT broker or external brokers
306
+ - **`Bluetooth`**: Scan and talk to BLE peripherals
307
+ - **`Wifi`**: List known WiFi SSIDs
308
+ - **`Udp`**: Bind UDP sockets and exchange datagrams
309
+ - **`ChildProcess`**: Spawn child processes from the runtime
310
+
311
+ #### Data & Domain Permissions
312
+
313
+ - **`Timeseries`**: Query historical timeseries data
314
+ - **`EnergyPrices`**: Read current and forecast electricity prices
315
+ - **`ElectricityTariff`**: Manage electricity tariffs
316
+ - **`EnergyManager`**: Run as the active energy manager
317
+ - **`EnergyManagerInfo`**: Read information about the active energy manager
318
+ - **`WeatherForecastRegister`** / **`WeatherForecastUse`**: Publish / consume weather forecasts
319
+ - **`PvForecastRegister`** / **`PvForecastUse`**: Publish / consume PV forecasts
320
+ - **`DynamicPriceForecastRegister`** / **`DynamicPriceForecastUse`**: Publish / consume dynamic-price forecasts
321
+ - **`PvSystemRegister`** / **`PvSystemUse`**: Register / read PV system configuration
322
+
323
+ #### Site & Identity Permissions
324
+
325
+ - **`LocationZipCode`**: Read the site's zip-code-level location
326
+ - **`LocationCoordinates`**: Read the site's full coordinates
327
+ - **`SecretManager`**: Read developer-org secrets
328
+
263
329
  #### Internet Access
264
330
 
265
331
  - **`RestrictedInternetAccess`**: Access specific internet domains only
@@ -268,20 +334,30 @@ Energy Apps use a granular permissions system to control access to system resour
268
334
 
269
335
  ### Lifecycle Management
270
336
 
271
- #### `register(callback: (packageName: string, version: number) => void)`
337
+ #### `register(callback: (packageName: string, version: number, channel: EnyoPackageChannel, deviceId: string) => void | Promise<void>)`
272
338
 
273
- Register a callback that executes when your Energy App starts:
339
+ Register a callback that executes when your Energy App starts. The callback receives the package name, version, release channel (`stable` / `beta` / …), and the device ID the package is running on. It may be `async`.
274
340
 
275
341
  ```typescript
276
- energyApp.register((packageName, version) => {
277
- console.log(`${packageName} v${version} is now running`);
342
+ energyApp.register(async (packageName, version, channel, deviceId) => {
343
+ console.log(`${packageName} v${version} on ${channel} (device ${deviceId}) is now running`);
278
344
  // Initialize your app here
279
345
  });
280
346
  ```
281
347
 
282
- #### `onShutdown(callback: () => Promise<void>)`
348
+ #### `onNetworkStatusChanged(listener: (online: boolean) => void | Promise<void>): string`
349
+
350
+ Subscribe to system-online transitions. Returns a listener ID. Pairs well with [`isSystemOnline()`](#issystemonline-boolean) for first-state, then deltas:
351
+
352
+ ```typescript
353
+ const listenerId = energyApp.onNetworkStatusChanged((online) => {
354
+ console.log(online ? 'System back online' : 'System went offline');
355
+ });
356
+ ```
357
+
358
+ #### `onShutdown(callback: () => void | Promise<void>)`
283
359
 
284
- Register cleanup logic for graceful shutdown:
360
+ Register cleanup logic for graceful shutdown. The callback may be sync or async; it runs on Node `beforeExit` **and** `exit`.
285
361
 
286
362
  ```typescript
287
363
  energyApp.onShutdown(async () => {
@@ -480,7 +556,7 @@ const intervalId = interval.createInterval('30s', (clockId) => {
480
556
  interval.stopInterval(intervalId);
481
557
  ```
482
558
 
483
- **Available intervals**: `'10s'`, `'30s'`, `'1m'`, `'5m'`, `'1hr'`
559
+ **Available intervals**: `'1s'`, `'5s'`, `'10s'`, `'30s'`, `'1m'`, `'5m'`, `'1hr'` (defined by the `IntervalDuration` type — any other string is rejected).
484
560
 
485
561
  ### Energy Resources
486
562
 
@@ -766,6 +842,301 @@ await learningPhase.completeLearningPhase(heatpumpPhaseId);
766
842
  await learningPhase.removeLearningPhase(phaseId);
767
843
  ```
768
844
 
845
+ ### Networking & Protocols
846
+
847
+ #### `useEebus(): EnergyAppEebus`
848
+
849
+ Talk to EEBUS / SHIP / SPINE devices. The returned facade exposes four sub-interfaces:
850
+
851
+ - `devices` — SHIP-level lifecycle: discovery, pairing, connection.
852
+ - `identity` — Node Identification (NID), observable identity, supported use-case discovery.
853
+ - `useCases` — typed use-case clients (LPC, LPP, MGCP, MPC, OHPCF).
854
+ - `spine` — low-level SPINE escape hatch for features not yet wrapped.
855
+
856
+ ```typescript
857
+ const eebus = energyApp.useEebus();
858
+
859
+ const discovered = await eebus.devices.getDiscoveredDevices();
860
+ const device = await eebus.devices.pairDevice(discovered[0].ski);
861
+
862
+ const identity = await eebus.identity.get(device.ski);
863
+ const useCases = await eebus.identity.getSupportedUseCases(device.ski);
864
+ ```
865
+
866
+ Requires the `EebusDeviceManagement` permission for the calls above. `EebusDataAccess` / `EebusControl` gate reads and writes on use-case features.
867
+
868
+ #### `useMqtt(): EnergyAppMqtt`
869
+
870
+ Connect to the internal SDK MQTT broker or an external broker, publish, subscribe, and observe connection status.
871
+
872
+ ```typescript
873
+ const mqtt = energyApp.useMqtt();
874
+ const client = await mqtt.connectToSdkBroker();
875
+
876
+ await client.subscribe('sensors/+/temperature');
877
+ client.onTopic('sensors/+/temperature', (payload) => {
878
+ console.log('Sensor reading:', payload.toString());
879
+ });
880
+
881
+ await client.publish('control/pump', 'on', /* qos */ 1, /* retain */ false);
882
+ ```
883
+
884
+ For external brokers use `connectToExternalBroker(brokerUrl, options)`. Requires the `Mqtt` permission.
885
+
886
+ #### `useBluetooth(): EnergyAppBluetooth`
887
+
888
+ Scan for BLE peripherals and perform GATT read / write / notify against them.
889
+
890
+ ```typescript
891
+ const ble = energyApp.useBluetooth();
892
+
893
+ const devices = await ble.scan({ durationMs: 5000 });
894
+
895
+ await ble.withDevice(devices[0].address, async (session) => {
896
+ const value = await session.read('1800', '2a00');
897
+ await session.write('180a', '2a29', new TextEncoder().encode('hi'));
898
+ });
899
+ ```
900
+
901
+ Notifications can be consumed three ways from the session: `notifications(svc, ch).onValue(cb)` (push), `.next(timeoutMs)` (pull-once), or `.values()` (async iterator). Requires the `Bluetooth` permission.
902
+
903
+ #### `useUdp(): EnergyAppUdp`
904
+
905
+ Bind UDP sockets and exchange datagrams. Lazily instantiates a single server instance and reuses it on subsequent calls; the permission gate runs on every accessor call so revocations surface consistently.
906
+
907
+ ```typescript
908
+ const udp = energyApp.useUdp();
909
+ const socket = await udp.bind(5000);
910
+
911
+ socket.onMessage((data, rinfo) => {
912
+ console.log(`Received ${data.length}B from ${rinfo.address}:${rinfo.port}`);
913
+ });
914
+
915
+ await socket.send(new TextEncoder().encode('hello'), 5001, '192.168.1.50');
916
+ ```
917
+
918
+ Throws `EnergyAppPermissionNotGrantedError` if the `Udp` permission isn't granted.
919
+
920
+ #### `useModbusRtu(): EnergyAppModbusRtu`
921
+
922
+ Modbus RTU over serial. Open a port with baud rate / parity / data bits / stop bits, then read/write registers by slave ID.
923
+
924
+ ```typescript
925
+ const rtu = energyApp.useModbusRtu();
926
+ const client = await rtu.connect('/dev/ttyUSB0', { baudRate: 9600, parity: 'none' });
927
+
928
+ const registers = await client.readRegisters(/* slaveId */ 1, /* startReg */ 0, /* count */ 10);
929
+ await client.writeRegisters(1, 100, [42, 43]);
930
+ ```
931
+
932
+ Requires the `ModbusRtu` permission.
933
+
934
+ #### `useWifi(): EnergyAppWifi`
935
+
936
+ List the SSIDs the device is configured to join that are currently in range.
937
+
938
+ ```typescript
939
+ const wifi = energyApp.useWifi();
940
+ const ssids = await wifi.getKnownSsids();
941
+ for (const { ssid } of ssids) console.log(ssid);
942
+ ```
943
+
944
+ Requires the `Wifi` permission.
945
+
946
+ ### Location & Site
947
+
948
+ #### `useLocation(): EnergyAppLocation`
949
+
950
+ Two-tier location API. Zip-code resolution and full coordinates are gated by separate permissions so apps can opt into the minimum precision they need.
951
+
952
+ ```typescript
953
+ const location = energyApp.useLocation();
954
+
955
+ const zip = await location.getZipCodeLocation(); // requires LocationZipCode
956
+ const full = await location.getLocation(); // requires LocationCoordinates
957
+ if (full) console.log(`lat=${full.latitude} lon=${full.longitude}`);
958
+ ```
959
+
960
+ #### `useGridConnectionPoint(): EnergyAppGridConnectionPoint`
961
+
962
+ Read the site's grid connection details — main fuse rating, number of phases, and the maximum allowed grid power. Use this to size dispatch envelopes and avoid violating the contractual cap.
963
+
964
+ ```typescript
965
+ const gcp = energyApp.useGridConnectionPoint();
966
+ const point = await gcp.getGridConnectionPoint();
967
+ if (point) {
968
+ console.log(`Fuse ${point.fuseAmpere}A across ${point.numberOfPhases} phases`);
969
+ }
970
+ ```
971
+
972
+ ### Energy Domain APIs
973
+
974
+ #### `useEnergyManager(): EnergyAppEnergyManager`
975
+
976
+ Read information about the currently active energy manager (vendor, version, supported features). Useful for apps that want to behave differently depending on which manager owns dispatch.
977
+
978
+ ```typescript
979
+ const em = energyApp.useEnergyManager();
980
+ const info = await em.getEnergyManagerInfo();
981
+ if (info) console.log(`Active manager: ${info.name} v${info.version}`);
982
+ ```
983
+
984
+ Requires the `EnergyManagerInfo` permission.
985
+
986
+ #### `useElectricityTariff(): EnergyAppElectricityTariff`
987
+
988
+ Register, retrieve, and manage electricity tariffs. One tariff can be marked as the system default.
989
+
990
+ ```typescript
991
+ const tariffs = energyApp.useElectricityTariff();
992
+
993
+ await tariffs.registerTariff({ id: 't1', name: 'Spot 2026', pricePerKwh: 0.21 });
994
+ await tariffs.makeDefaultTariff('t1');
995
+
996
+ const defaultTariff = await tariffs.getDefaultTariff();
997
+ const all = await tariffs.getAllTariffs();
998
+ ```
999
+
1000
+ Requires the `ElectricityTariff` permission.
1001
+
1002
+ #### `useWeatherForecasting(): EnergyAppWeatherForecasting`
1003
+
1004
+ Register a weather-forecast provider (e.g. wraps an external API) and / or consume forecasts by zip code or coordinates.
1005
+
1006
+ ```typescript
1007
+ const weather = energyApp.useWeatherForecasting();
1008
+
1009
+ await weather.registerForecast({ forecastId: 'wx-prod', name: 'OpenWeather' });
1010
+ const byZip = await weather.getWeatherForecastByZipCode('wx-prod');
1011
+ const byCoords = await weather.getWeatherForecastByCoordinates('wx-prod', 48.13, 11.57);
1012
+ ```
1013
+
1014
+ Publishers need `WeatherForecastRegister`; consumers need `WeatherForecastUse`.
1015
+
1016
+ #### `usePvForecasting(): EnergyAppPvForecasting`
1017
+
1018
+ Same shape as weather forecasting, but for PV production.
1019
+
1020
+ ```typescript
1021
+ const pvForecast = energyApp.usePvForecasting();
1022
+ await pvForecast.registerForecast({ forecastId: 'pv-prod', name: 'Solargis' });
1023
+ const forecast = await pvForecast.getPvForecast('pv-prod');
1024
+ ```
1025
+
1026
+ Publishers need `PvForecastRegister`; consumers need `PvForecastUse`.
1027
+
1028
+ #### `useDynamicPriceForecast(): EnergyAppDynamicPriceForecast`
1029
+
1030
+ Publish and consume forward-looking electricity-price forecasts (e.g. day-ahead spot). The data is forecast only — never settled prices.
1031
+
1032
+ ```typescript
1033
+ const dpf = energyApp.useDynamicPriceForecast();
1034
+
1035
+ await dpf.registerForecast({ forecastId: 'epex-da', name: 'EPEX Day-Ahead', vendor: 'EPEX' });
1036
+ await dpf.publishForecast('epex-da', {
1037
+ currency: 'EUR',
1038
+ resolution: '1h',
1039
+ entries: [{ timestampIso: '2026-05-23T10:00:00Z', consumptionPricePerKwh: 0.21 }]
1040
+ });
1041
+
1042
+ const latest = await dpf.getLatestForecast();
1043
+ dpf.onForecastPublished((forecast) => console.log('new forecast', forecast.forecastId));
1044
+ ```
1045
+
1046
+ Publishers need `DynamicPriceForecastRegister`; consumers need `DynamicPriceForecastUse`.
1047
+
1048
+ #### `usePvSystem(): EnergyAppPvSystem`
1049
+
1050
+ Register a PV system's structural configuration (kWp, DC string orientations, associated appliances) so other apps can reason about expected production.
1051
+
1052
+ ```typescript
1053
+ const pv = energyApp.usePvSystem();
1054
+ await pv.registerPvSystem({
1055
+ id: 'pv-1',
1056
+ kWp: 9.6,
1057
+ dcStrings: [
1058
+ { azimuth: 180, tilt: 30 },
1059
+ { azimuth: 90, tilt: 30 }
1060
+ ]
1061
+ });
1062
+ const systems = await pv.getPvSystems();
1063
+ ```
1064
+
1065
+ Publishers need `PvSystemRegister`; consumers need `PvSystemUse`.
1066
+
1067
+ #### `useTimeseries(): EnergyAppTimeseries`
1068
+
1069
+ Query historical 15-minute aggregated data across the energy domain (PV production, battery SoC / power, meter values, grid power, home consumption, heatpump electrical / thermal, air-conditioning, temperature sensors). Some endpoints also support 1-minute resolution.
1070
+
1071
+ ```typescript
1072
+ const ts = energyApp.useTimeseries();
1073
+ const last24h = await ts.query({
1074
+ dataType: 'pvProduction',
1075
+ resolution: '15m',
1076
+ startTime: Date.now() - 24 * 60 * 60 * 1000,
1077
+ endTime: Date.now()
1078
+ });
1079
+ ```
1080
+
1081
+ Requires the `Timeseries` permission.
1082
+
1083
+ #### `useDiagnostics(): EnergyAppDiagnostics`
1084
+
1085
+ Energy-manager packages can submit their current state, forecast, and control plan to internal diagnostics for offline analysis. Fire-and-forget.
1086
+
1087
+ ```typescript
1088
+ const diag = energyApp.useDiagnostics();
1089
+ diag.energyManagerDiagnostics(
1090
+ { batterySoc: 47, gridPowerW: 1200 },
1091
+ { pvNext24h: [...] },
1092
+ { actions: [{ applianceId: 'battery-1', mode: 'charge', powerW: 3000 }] }
1093
+ );
1094
+ ```
1095
+
1096
+ ### Operational Utilities
1097
+
1098
+ #### `useOnboarding(): EnergyAppOnboarding`
1099
+
1100
+ Drive a multi-step onboarding guide — start / advance / back / skip / cancel, persist responses, and observe step transitions.
1101
+
1102
+ ```typescript
1103
+ const guide = energyApp.useOnboarding();
1104
+
1105
+ await guide.startGuide('pv-setup', EnyoOnboardingGuideCategory.PvSystem);
1106
+ const step = await guide.nextStep('pv-setup');
1107
+ await guide.respondToStep('pv-setup', { answer: 'yes' });
1108
+
1109
+ const listenerId = guide.onStepListener('pv-setup', (event) => {
1110
+ console.log('step changed:', event.stepId);
1111
+ });
1112
+ ```
1113
+
1114
+ #### `useSecretManager(): EnergyAppSecretManager`
1115
+
1116
+ Encrypted retrieval / storage of developer-org secrets (API keys, vendor tokens, OAuth client secrets). Strongly typed accessors keep the call site safe.
1117
+
1118
+ ```typescript
1119
+ const secrets = energyApp.useSecretManager();
1120
+
1121
+ await secrets.saveSecret('weather-api', { token: 'xyz' });
1122
+ const cred = await secrets.getSecret<{ token: string }>('weather-api');
1123
+
1124
+ const names = await secrets.listAvailableSecrets();
1125
+ await secrets.removeSecret('weather-api');
1126
+ ```
1127
+
1128
+ Requires the `SecretManager` permission.
1129
+
1130
+ #### `useSequenceGenerator(): EnergyAppSequenceGenerator`
1131
+
1132
+ Process-local monotonic counter, keyed by an arbitrary name. Use it for stable request / message IDs without coordinating across instances.
1133
+
1134
+ ```typescript
1135
+ const seq = energyApp.useSequenceGenerator();
1136
+ const reqId = await seq.next('mqtt-publish'); // 1, 2, 3, …
1137
+ const txId = await seq.next('ocpp-tx'); // independent counter
1138
+ ```
1139
+
769
1140
  ## Advanced Modbus Integration
770
1141
 
771
1142
  The SDK includes a powerful, vendor-agnostic Modbus implementation for energy management systems. This allows you to connect to any Modbus-enabled device through configuration without code changes.
@@ -954,6 +1325,60 @@ The Modbus implementation follows a clean, modular architecture:
954
1325
 
955
1326
  This modular design ensures maintainability, testability, and extensibility for future enhancements.
956
1327
 
1328
+ ## Appliance Management
1329
+
1330
+ `ApplianceManager` is the recommended way to keep the SDK's appliance list and your in-process state in sync. It wraps `useAppliances()` with caching, identifier-based lookup strategies, bulk operations, and helpers that the device-integrations and `NetworkDeviceManager` consume internally.
1331
+
1332
+ ```typescript
1333
+ import { ApplianceManager } from '@enyo-energy/energy-app-sdk';
1334
+
1335
+ const applianceManager = await ApplianceManager.initialize(energyApp, {
1336
+ // Optional config: identifier strategy, cache options, etc.
1337
+ });
1338
+
1339
+ // Create or update by identifier (idempotent — uses the configured strategy).
1340
+ const applianceId = await applianceManager.createOrUpdateAppliance({
1341
+ identifier: 'sn-1234567890',
1342
+ name: [{ language: 'en', name: 'Inverter A' }],
1343
+ type: EnyoApplianceTypeEnum.Inverter,
1344
+ /* ... */
1345
+ });
1346
+
1347
+ // Lookups
1348
+ const inverters = await applianceManager.getAppliancesByType(EnyoApplianceTypeEnum.Inverter);
1349
+ const byId = await applianceManager.findApplianceById(applianceId);
1350
+ const matches = await applianceManager.findByIdentifier('sn-1234567890');
1351
+
1352
+ // State transitions
1353
+ await applianceManager.updateApplianceState(
1354
+ applianceId,
1355
+ EnyoApplianceConnectionType.Modbus,
1356
+ EnyoApplianceStateEnum.Connected
1357
+ );
1358
+ ```
1359
+
1360
+ **Key methods**
1361
+
1362
+ | Method | Purpose |
1363
+ |---|---|
1364
+ | `static initialize(app, config?)` | Build a manager, prime the cache, install SDK listeners. |
1365
+ | `createOrUpdateAppliance(config)` | Upsert by the configured `IdentifierStrategy`. Returns the appliance ID. Throws `MissingIdentifierError` if the strategy returns no identifier, or `DuplicateIdentifierError` if the identifier maps to more than one appliance. |
1366
+ | `updateAppliance(id, patch)` | Patch an existing appliance. |
1367
+ | `removeAppliance(id)` / `removeAppliancesByIdentifier(id)` | Delete one / many. |
1368
+ | `findApplianceById(id)` | SDK lookup by appliance ID. Returns `null` on not-found; **propagates** SDK errors. |
1369
+ | `findByIdentifier(extractedId)` | Cache-first lookup keyed by the configured identifier strategy. Falls through to one SDK list call on cache miss. |
1370
+ | `findFirstByStrategies(value, strategies)` | Probes each strategy in order against the SDK list; returns the first match plus which strategy hit. |
1371
+ | `findAppliancesByNetworkDeviceId(deviceId)` | Synchronous, cache-backed reverse lookup from a NetworkDevice to its appliances. |
1372
+ | `getAppliancesByType(type)` / `getAllAppliancesByType(type)` | Filtered listing (own / all). |
1373
+ | `updateApplianceState(id, connection, state)` | State transitions (`Connected` / `Offline` / `Error` / …). |
1374
+ | `setAppliancesStateByIdentifier(id, state)` | Bulk state transition for every appliance sharing an identifier; preserves each appliance's existing `connectionType`. |
1375
+ | `bulkUpdate(updates)` | Atomic batch of state changes. |
1376
+ | `setIdentifierStrategy(strategy, rebuildCache)` / `getIdentifierStrategy()` | Swap the identifier-resolution strategy at runtime. `rebuildCache` is required: pass `false` to keep the cached appliances and just recompute the in-memory identifier index against the new strategy, or `true` to force a full refresh from the SDK. |
1377
+ | `refreshCache()` / `clearCache()` | Manual cache control. |
1378
+ | `dispose()` | Release SDK listeners. |
1379
+
1380
+ Identifier strategies are exported from the package — typical choices match on serial number, hostname, or a composite of `manufacturer + model + sn`.
1381
+
957
1382
  ## Network Devices & Access Recovery
958
1383
 
959
1384
  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:
@@ -1070,7 +1495,9 @@ What the manager handles for you:
1070
1495
 
1071
1496
  - **Access-denied recovery** — `withAccessGuard` / `ensureAccess` delegate to the bundled `NetworkAccessGuard`.
1072
1497
  - **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).
1498
+ - **Listener dedup** — both the manager's SDK access-change listener and the guard's own restored callback feed into the same internal `dispatchAccessRestored`. The manager records which devices have already been dispatched for the current `'granted'` transition and short-circuits a second dispatch, so the dedup is order-independent and does not rely on SDK listener FIFO semantics. The mark is cleared on the next non-granted transition or device removal.
1499
+ - **AccessDenied vs AccessRevoked** — both signal "the package can no longer read this device", but the source differs. `onApplianceAccessDenied` fires when `withAccessGuard` catches a runtime read error (the SDK's "Network access denied" message). `onApplianceAccessRevoked` fires when the SDK explicitly reports a status transition to `'denied'` or `'pending'` (typically a user-driven UI action). Wire both if you want a single "lost access" signal — they will not double-fire for one underlying event.
1500
+ - **One manager per `EnergyApp`** — `NetworkDeviceManager.initialize` enforces a single active manager per `EnergyApp` instance. Calling it a second time without first calling `dispose()` throws `NetworkDeviceManagerAlreadyInitializedError`. After disposal a fresh manager can be created.
1074
1501
  - **Device removal** — on `listenForNetworkDeviceRemoved`, the manager fires `onApplianceNetworkDeviceRemoved` per affected appliance and clears its cache.
1075
1502
  - **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
1503
 
@@ -1113,6 +1540,37 @@ async function connectDevice(networkDeviceId: string) {
1113
1540
 
1114
1541
  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
1542
 
1543
+ ## Retry Framework
1544
+
1545
+ `RetryManager` centralises retry / backoff / circuit-breaker logic so polling loops don't have to reinvent it. Register one entry per logical operation, give it a `RetryPolicy`, and run attempts through `execute(id, fn)` — the manager handles attempt counting, exponential backoff, transition into `Open` after repeated failures, and recovery on the next success.
1546
+
1547
+ ```typescript
1548
+ import { RetryManager, exponentialBackoff } from '@enyo-energy/energy-app-sdk';
1549
+
1550
+ const retries = new RetryManager();
1551
+
1552
+ retries.register('modbus-inverter-1', {
1553
+ backoff: exponentialBackoff({ initialMs: 1_000, maxMs: 60_000, factor: 2 }),
1554
+ maxAttempts: Infinity, // keep retrying forever
1555
+ openAfterConsecutiveFailures: 5, // trip the breaker after 5 fails
1556
+ });
1557
+
1558
+ const value = await retries.execute('modbus-inverter-1', () =>
1559
+ modbusClient.readHoldingRegisters(40000, 4)
1560
+ );
1561
+
1562
+ // React to circuit-breaker transitions (Idle → Retrying → Open → Closed).
1563
+ const unsubscribe = retries.onStateChange((snapshot) => {
1564
+ console.log(`[${snapshot.id}] ${snapshot.state} (attempt ${snapshot.attempts})`);
1565
+ });
1566
+
1567
+ retries.statuses(); // current snapshots of every registered op
1568
+ retries.reset('modbus-inverter-1');
1569
+ retries.unregister('modbus-inverter-1');
1570
+ ```
1571
+
1572
+ Backoff helpers (`exponentialBackoff`, `fixedBackoff`, `linearBackoff` — see `src/implementations/retry/backoff.ts`) and the dedicated error types (`RetryAbortedError`, `RetryOpenError`) live alongside the manager so you can distinguish "we gave up" from "the caller cancelled".
1573
+
1116
1574
  ## Device Integrations
1117
1575
 
1118
1576
  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.
@@ -1426,7 +1884,7 @@ const battery = new BatteryForecast(energyApp, 'battery-1', {
1426
1884
 
1427
1885
  await Promise.all([pv.initialize(), battery.initialize()]);
1428
1886
 
1429
- energyApp.useInterval().createInterval('15m', () => {
1887
+ energyApp.useInterval().createInterval('5m', () => {
1430
1888
  const pvNext24h = pv.getForecast();
1431
1889
  const batteryNext24h = battery.getForecast();
1432
1890
  runDispatch(pvNext24h, batteryNext24h);
@@ -1963,7 +2421,7 @@ For more information about the CLI, visit [@enyo-energy/cli on npm](https://www.
1963
2421
 
1964
2422
  ---
1965
2423
 
1966
- **Package Version:** 0.0.34
2424
+ **Package Version:** see `package.json` (`version` field) — currently `0.0.134`
1967
2425
  **SDK Version:** Auto-injected during build
1968
2426
  **License:** ISC
1969
2427
  **Repository:** [github.com/enyo-energy/energy-app-sdk](https://github.com/enyo-energy/energy-app-sdk)