@enyo-energy/energy-app-sdk
v0.0.167
Published
enyo Energy App SDK
Keywords
Readme
enyo Energy App SDK
The official TypeScript SDK for building Energy Apps on the enyo platform. Create powerful energy management applications that integrate with inverters, batteries, charging stations, and smart home devices.
Table of Contents
- Installation
- Quick Start
- Choosing the Right API
- Core Concepts
- API Reference
- Advanced Modbus Integration
- Appliance Management
- Network Devices & Access Recovery
- Retry Framework
- Device Integrations
- Forecasting
- Appliance Energy-Manager Forecast
- Examples
- Troubleshooting
- External Libraries
- CLI Tool
- Releasing Your App
Installation
Install the SDK using npm:
npm install @enyo-energy/energy-app-sdkQuick Start
Create a basic Energy App that responds to system events:
import { EnergyApp, defineEnergyAppPackage, EnergyAppPackageCategory, EnergyAppStateEnum } from '@enyo-energy/energy-app-sdk';
// Initialize the Energy App
const energyApp = new EnergyApp();
energyApp.register((packageName: string, version: number) => {
console.log(`Energy App ${packageName} v${version} is starting...`);
console.log(`System is ${energyApp.isSystemOnline() ? 'online' : 'offline'}`);
// Set app state to running
energyApp.updateEnergyAppState(EnergyAppStateEnum.Running);
// Your app logic starts here
startApp();
});
async function startApp() {
// Use SDK APIs
const storage = energyApp.useStorage();
const dataBus = energyApp.useDataBus();
// Store app configuration
await storage.save('config', { initialized: true, timestamp: Date.now() });
// Listen for data bus messages
dataBus.listenForMessages(['InverterValuesUpdateV1'], (message) => {
console.log('Received inverter data:', message);
});
}Choosing the Right API
The SDK exposes several layered building blocks. Pick the one that matches the kind of app you are building before diving into the API reference:
- Core SDK (
EnergyApp) — the always-present facade for system lifecycle, storage, data bus, settings, notifications, and HTTP. Every Energy App uses it. - Modbus helpers (
EnergyAppModbusInverter/Battery/Meter) — vendor-agnostic, configuration-driven Modbus access for raw register polling. - Device Integrations (
*IntegrationEnergyApp) — inbound abstractions for apps that drive a real device (heatpump, wallbox, inverter, storage, air conditioning). They subscribe to the right data-bus commands, dispatch them to your handlers, auto-acknowledge, and expose typedpublish*helpers for status updates. - Forecasting (
*Forecast,EnergyManagerEnergyApp) — outbound abstractions for apps that predict future PV production, consumption, battery state, EV charging load, heatpump load, or DHW tank temperature using historical timeseries plus live data-bus updates.
Decision Matrix
| If you want to… | Use |
|---|---|
| React to system lifecycle, store data, send notifications | EnergyApp |
| Talk to a Modbus device through configuration only | EnergyAppModbusInverter / Battery / Meter |
| Build a device integration for a heatpump | HeatpumpIntegrationEnergyApp |
| Build a device integration for an EV wallbox | WallboxIntegrationEnergyApp |
| Build a device integration for a battery / storage system | StorageIntegrationEnergyApp |
| Build a device integration for a PV inverter | InverterIntegrationEnergyApp |
| Build a device integration for an air-conditioning unit | AirConditioningIntegrationEnergyApp |
| Build an energy manager that orchestrates many forecasters | EnergyManagerEnergyApp |
| Forecast PV production for a single inverter | PvProductionForecast |
| Forecast battery state-of-charge | BatteryForecast |
| Forecast total household consumption | HomeConsumptionForecast |
| Forecast EV charging demand | EvChargingForecast |
| Forecast heatpump electrical consumption | HeatpumpConsumptionForecast |
| Forecast heatpump DHW tank temperature | HeatpumpDhwTemperatureForecast |
| Announce a charger / battery / heatpump command plan you intend to apply | useApplianceEnergyManagerForecast() |
| Talk to an EEBUS / SHIP / SPINE device | useEebus() |
| Speak MQTT (SDK broker or external) | useMqtt() |
| Scan or talk to Bluetooth LE peripherals | useBluetooth() |
| Send/receive UDP datagrams | useUdp() |
| Read serial Modbus RTU | useModbusRtu() |
| List known WiFi SSIDs in range | useWifi() |
| Query historical timeseries (PV, battery, meter, …) | useTimeseries() |
| Read site location (zip or coordinates) | useLocation() |
| Read grid connection point (fuse, phases, max power) | useGridConnectionPoint() |
| Retrieve secrets from the developer org secret store | useSecretManager() |
| Submit energy-manager diagnostics | useDiagnostics() |
| Register a weather / PV / dynamic-price forecast provider | useWeatherForecasting() / usePvForecasting() / useDynamicPriceForecast() |
| Manage electricity tariffs (default tariff, price per kWh) | useElectricityTariff() |
| Register a PV system (kWp, DC strings, orientation) | usePvSystem() |
| Discover capabilities of the active energy manager | useEnergyManager() |
| Drive a multi-step onboarding flow | useOnboarding() |
| Allocate process-local sequential IDs | useSequenceGenerator() |
| Manage retries with circuit-breaker semantics | RetryManager |
| Keep an applianceId cache in sync with the SDK | ApplianceManager |
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
EnergyManagerEnergyAppto wire several together).
Core Concepts
Energy App Lifecycle
Energy Apps follow a specific lifecycle managed by the enyo system:
- Initialization: Your app registers with the system
- Running: App performs its main functionality
- State Management: App reports its current state
- Shutdown: Graceful cleanup when system stops
const energyApp = new EnergyApp();
// Register startup callback
energyApp.register((packageName, version) => {
console.log(`${packageName} v${version} started`);
energyApp.updateEnergyAppState(EnergyAppStateEnum.Running);
});
// Register shutdown callback
energyApp.onShutdown(async () => {
console.log('Cleaning up resources...');
// Perform cleanup tasks
});App States
launching: Initial state when app is starting uprunning: App is functioning normallyconfiguration-required: App needs user configurationinternet-connection-required: App needs internet connectivity
Package Definition
Every Energy App must be defined using defineEnergyAppPackage():
import {
defineEnergyAppPackage,
EnergyAppPackageCategory,
EnergyAppPermissionTypeEnum
} from '@enyo-energy/energy-app-sdk';
const packageDef = defineEnergyAppPackage({
version: '1',
packageName: 'solar-optimizer',
// Optional: Internal documentation for developers (not shown to users)
internalDescription: 'This app optimizes solar energy production using weather forecasts and AI predictions.',
logo: './assets/logo.png',
categories: [
EnergyAppPackageCategory.Inverter,
EnergyAppPackageCategory.EnergyManagement
],
storeEntry: [
{
language: 'en',
title: 'Solar Optimizer',
shortDescription: 'Optimize your solar energy production',
description: 'Advanced solar energy optimization with AI-driven predictions and real-time adjustments.'
},
{
language: 'de',
title: 'Solar Optimierer',
shortDescription: 'Optimieren Sie Ihre Solarenergieproduktion',
description: 'Erweiterte Solarenergie-Optimierung mit KI-gesteuerten Vorhersagen und Echtzeitanpassungen.'
}
],
// Permissions can be objects with internal comments (recommended for documentation)
permissions: [
{ permission: EnergyAppPermissionTypeEnum.Modbus, internalComment: 'Required to read inverter registers via Modbus TCP' },
{ permission: EnergyAppPermissionTypeEnum.SendDataBusValues, internalComment: 'Used to publish inverter power values to the data bus' },
{ permission: EnergyAppPermissionTypeEnum.SubscribeDataBus, internalComment: 'Listens for battery state updates' },
{ permission: EnergyAppPermissionTypeEnum.Storage, internalComment: 'Stores configuration and historical optimization data' }
],
// Note: Simple permission types are also supported for backwards compatibility:
// permissions: [EnergyAppPermissionTypeEnum.Modbus, EnergyAppPermissionTypeEnum.Storage]
options: {
restrictedInternetAccess: {
origins: ['api.weather.com', 'solar-forecasting.com']
},
deviceDetection: {
modbus: [{
unitIds: [1],
registerAddress: 40001,
registerSize: 2,
type: 'string',
matchingValues: ['SolarMax', 'SMA']
}]
}
}
});Package Categories
Inverter: Solar inverter managementWallbox: EV charging station integrationMeter: Energy metering applicationsEnergyManagement: Overall energy optimizationHeatPump: Heat pump control systemsAirConditioning: Air-conditioning unitsBatteryStorage: Battery managementClimateControl: HVAC and climate systemsDynamicElectricityTariff: Dynamic / spot-price tariff providersStaticElectricityTariff: Fixed-price tariff providersTemperatureSensor: Standalone temperature sensorsSmartPlug: Smart-plug appliancesOther: Anything not covered above
Permissions System
Energy Apps use a granular permissions system to control access to system resources:
Core Permissions
Storage: Access persistent key-value storageNetworkDeviceDiscovery: Discover devices on the local networkNetworkDeviceSearch: Search for specific network devicesNetworkDeviceAccess: Access discovered network devicesModbus: Communicate via Modbus protocol
Data Bus Permissions
SendDataBusValues: Send sensor data and measurementsSubscribeDataBus: Listen to data from other devicesSendDataBusCommands: Send control commands
Device Permissions
Appliance: Manage appliances created by your packageAllAppliances: Access all appliances in the systemOcppServer: Run OCPP server for EV chargingChargingCard: Manage EV charging cardsVehicle: Access vehicle informationCharge: Manage charging sessions
Command Permissions
InverterControlCommands: Send inverter control commands (e.g. feed-in limit)BatteryControlCommands: Send battery / storage control commandsChargerControlCommands: Send wallbox / charger control commands
Networking & Protocol Permissions
ModbusRtu: Communicate over Modbus RTU (serial)EebusDeviceManagement: Pair / discover / connect EEBUS devicesEebusDataAccess: Read EEBUS use-case dataEebusControl: Send EEBUS control commands (write features)Mqtt: Connect to the internal SDK MQTT broker or external brokersBluetooth: Scan and talk to BLE peripheralsWifi: List known WiFi SSIDsUdp: Bind UDP sockets and exchange datagramsChildProcess: Spawn child processes from the runtime
Data & Domain Permissions
Timeseries: Query historical timeseries dataEnergyPrices: Read current and forecast electricity pricesElectricityTariff: Manage electricity tariffsEnergyManager: Run as the active energy managerEnergyManagerInfo: Read information about the active energy managerWeatherForecastRegister/WeatherForecastUse: Publish / consume weather forecastsPvForecastRegister/PvForecastUse: Publish / consume PV forecastsDynamicPriceForecastRegister/DynamicPriceForecastUse: Publish / consume dynamic-price forecastsPvSystemRegister/PvSystemUse: Register / read PV system configuration
Site & Identity Permissions
LocationZipCode: Read the site's zip-code-level locationLocationCoordinates: Read the site's full coordinatesSecretManager: Read developer-org secrets
Internet Access
RestrictedInternetAccess: Access specific internet domains only
API Reference
Lifecycle Management
register(callback: (packageName: string, version: number, channel: EnyoPackageChannel, deviceId: string) => void | Promise<void>)
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.
energyApp.register(async (packageName, version, channel, deviceId) => {
console.log(`${packageName} v${version} on ${channel} (device ${deviceId}) is now running`);
// Initialize your app here
});onNetworkStatusChanged(listener: (online: boolean) => void | Promise<void>): string
Subscribe to system-online transitions. Returns a listener ID. Pairs well with isSystemOnline() for first-state, then deltas:
const listenerId = energyApp.onNetworkStatusChanged((online) => {
console.log(online ? 'System back online' : 'System went offline');
});onShutdown(callback: () => void | Promise<void>)
Register cleanup logic for graceful shutdown. The callback may be sync or async; it runs on Node beforeExit and exit.
energyApp.onShutdown(async () => {
// Close connections
await modbusClient.disconnect();
// Save final state
await storage.save('lastShutdown', Date.now());
});updateEnergyAppState(state: EnergyAppStateEnum)
Update your app's current state:
import { EnergyAppStateEnum } from '@enyo-energy/energy-app-sdk';
// App needs configuration
energyApp.updateEnergyAppState(EnergyAppStateEnum.ConfigurationRequired);
// App is ready and running
energyApp.updateEnergyAppState(EnergyAppStateEnum.Running);System APIs
isSystemOnline(): boolean
Check system connectivity:
if (energyApp.isSystemOnline()) {
// Fetch remote data
syncWithCloud();
} else {
// Use cached data
loadOfflineData();
}useFetch(): typeof fetch
Get HTTP client with system configuration:
const fetch = energyApp.useFetch();
const response = await fetch('https://api.weather.com/forecast', {
method: 'GET',
headers: { 'Authorization': 'Bearer token' }
});getSdkVersion(): string
Get the SDK version:
console.log(`Using SDK version: ${energyApp.getSdkVersion()}`);Device Communication
useNetworkDevices(): EnergyAppNetworkDevice
Discover and access network devices:
const networkDevices = energyApp.useNetworkDevices();
// Discover devices
const devices = await networkDevices.discover();
// Search for specific device types
const inverters = await networkDevices.search({
deviceType: 'inverter',
manufacturer: 'SMA'
});
// Get device details
const deviceInfo = await networkDevices.getDeviceInfo(device.id);useModbus(): EnergyAppModbus
Access Modbus communication:
const modbus = energyApp.useModbus();
// Connect to device
const client = await modbus.connect({
host: '192.168.1.100',
port: 502,
unitId: 1
});
// Read holding registers
const registers = await client.readHoldingRegisters(1001, 10);
// Write single register
await client.writeSingleRegister(2001, 500);useOcpp(): EnergyAppOcpp
Handle OCPP charging station communication:
const ocpp = energyApp.useOcpp();
// Start OCPP server
const server = ocpp.createServer({
port: 8080,
onChargePointConnect: (chargePointId) => {
console.log(`Charge point ${chargePointId} connected`);
}
});
// Send remote commands
await server.sendRemoteStartTransaction(chargePointId, {
connectorId: 1,
idTag: 'user123'
});Data Management
useStorage(): EnergyAppStorage
Persistent key-value storage:
const storage = energyApp.useStorage();
// Save configuration
await storage.save('config', {
inverterHost: '192.168.1.100',
pollInterval: 30000
});
// Load configuration
const config = await storage.load<ConfigType>('config');
// List all keys
const keys = await storage.listKeys();
// Remove data
await storage.remove('oldData');useDataBus(): EnergyAppDataBus
Send and receive system-wide data:
const dataBus = energyApp.useDataBus();
// Send inverter data
dataBus.sendMessage([{
messageType: 'InverterValuesUpdateV1',
applianceId: 'inverter-1',
timestamp: Date.now(),
values: {
powerW: 3500,
energyWh: 25000,
voltageV: 230
}
}]);
// Listen for battery updates
const listenerId = dataBus.listenForMessages(
['BatteryValuesUpdateV1'],
(message) => {
console.log('Battery SoC:', message.values.stateOfCharge);
}
);
// Stop listening
dataBus.unsubscribe(listenerId);useInterval(): EnergyAppInterval
Manage recurring tasks:
const interval = energyApp.useInterval();
// Create recurring data collection
const intervalId = interval.createInterval('30s', (clockId) => {
collectSensorData();
});
// Stop interval
interval.stopInterval(intervalId);Available intervals: '1s', '5s', '10s', '30s', '1m', '5m', '1hr' (defined by the IntervalDuration type — any other string is rejected).
Energy Resources
useAppliances(): EnergyAppAppliance
Manage energy appliances:
const appliances = energyApp.useAppliances();
// Register new appliance
const applianceId = await appliances.save({
name: [{ language: 'en', name: 'Solar Inverter' }],
type: 'inverter',
manufacturer: 'SMA',
model: 'SB5000',
networkDevice: deviceInfo
}, undefined);
// List your appliances
const myAppliances = await appliances.list();
// Get appliance details
const appliance = await appliances.getById(applianceId);
// Remove appliance
await appliances.removeById(applianceId);useVehicle(): EnergyAppVehicle
Access electric vehicle information:
const vehicles = energyApp.useVehicle();
// Get all vehicles
const vehicleList = await vehicles.getVehicles();
// Get vehicle details
const vehicle = await vehicles.getVehicleById(vehicleId);
// Update vehicle state
await vehicles.updateVehicleState(vehicleId, {
batteryLevel: 80,
isPluggedIn: true,
estimatedRange: 320
});useCharge(): EnergyAppCharge
Manage charging sessions:
const charging = energyApp.useCharge();
// Start charging session
const sessionId = await charging.startCharge({
vehicleId: 'vehicle-123',
connectorId: 1,
maxPowerKw: 22
});
// Get active sessions
const sessions = await charging.getActiveSessions();
// Stop charging
await charging.stopCharge(sessionId);useChargingCard(): EnergyAppChargingCard
Handle charging authentication:
const chargingCards = energyApp.useChargingCard();
// Validate charging card
const isValid = await chargingCards.validateCard('RFID-12345');
// Get card information
const cardInfo = await chargingCards.getCardInfo('RFID-12345');User Features
useAuthentication(): EnergyAppAuthentication
Handle user authentication:
const auth = energyApp.useAuthentication();
// Get current user
const user = await auth.getCurrentUser();
// Check permissions
const hasPermission = await auth.hasPermission('admin');
// Authenticate action
const token = await auth.createAuthToken('user-action');useSettings(): EnergyAppSettings
Manage app settings and configuration:
const settings = energyApp.useSettings();
// Add setting configuration
await settings.addSettingConfig({
name: 'pollInterval',
displayName: [{ language: 'en', name: 'Poll Interval (seconds)' }],
type: 'number',
defaultValue: '30',
validation: {
min: 10,
max: 300
}
});
// Update setting value
await settings.updateSetting('pollInterval', '60');
// Listen for setting changes
settings.listenForSettingsChanges((settingName, newValue) => {
console.log(`Setting ${settingName} changed to ${newValue}`);
});
// Get all settings
const allSettings = await settings.getSettingsConfig();useConfigurationManager(): EnergyAppConfigurationManager
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.
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).
const configManager = energyApp.useConfigurationManager();
// Register the full set of internal configurations in a single call
await configManager.registerConfigurations([
{
key: 'pollIntervalMs',
type: 'number',
defaultValue: 5000,
numberOptions: { minValue: 1000, maxValue: 60000, step: 1000 }
},
{
key: 'logLevel',
type: 'select',
defaultValue: 'info',
selectOptions: [
{ value: 'debug' },
{ value: 'info' },
{ value: 'warn' },
{ value: 'error' }
]
}
]);
// Read the current (or default) value for a configuration
const pollInterval = await configManager.getConfiguration('pollIntervalMs');
// React to value changes
configManager.onConfigurationChanged(event => {
console.log(
`Configuration ${event.key} changed from ${event.previousValue} to ${event.newValue}`
);
});
// Remove configurations (e.g. on cleanup or after a migration)
await configManager.unregisterConfigurations(['logLevel']);useElectricityPrices(): EnergyAppElectricityPrices
Access electricity pricing information:
const prices = energyApp.useElectricityPrices();
// Get current electricity price
const currentPrice = await prices.getCurrentPrice();
// Get price forecast
const forecast = await prices.getPriceForecast({
hoursAhead: 24
});
// Listen for price changes
prices.onPriceChange((newPrice) => {
console.log(`New electricity price: ${newPrice.pricePerKwh} €/kWh`);
});useNotification(): EnergyAppNotification
Send notifications to users:
const notifications = energyApp.useNotification();
// Send info notification
await notifications.sendNotification({
type: 'info',
title: 'Energy Optimization',
message: 'Your system is running optimally',
timestamp: Date.now()
});
// Send warning
await notifications.sendNotification({
type: 'warning',
title: 'High Energy Consumption',
message: 'Consider reducing energy usage during peak hours'
});
// Send critical alert
await notifications.sendNotification({
type: 'error',
title: 'System Fault',
message: 'Inverter communication lost - please check connection'
});App Intelligence
useLearningPhase(): EnergyAppLearningPhase
Track and manage learning phases — periods where an app or a specific appliance is calibrating, gathering data, or optimizing its behavior:
const learningPhase = energyApp.useLearningPhase();
// Register a package-wide learning phase
const phaseId = await learningPhase.registerLearningPhase({
name: 'consumption-pattern-analysis',
reason: [
{ language: 'en', value: 'Analyzing energy consumption patterns' },
{ language: 'de', value: 'Analyse der Energieverbrauchsmuster' }
],
description: [
{ language: 'en', value: 'The system is learning your household energy consumption patterns to optimize scheduling.' },
{ language: 'de', value: 'Das System lernt Ihre Energieverbrauchsmuster, um die Planung zu optimieren.' }
]
});
// Register a learning phase for a specific appliance
const heatpumpPhaseId = await learningPhase.registerLearningPhase({
name: 'heating-curve-optimization',
applianceId: 'heatpump-001',
reason: [
{ language: 'en', value: 'Optimizing heating curve' },
{ language: 'de', value: 'Optimierung der Heizkurve' }
]
});
// Check if a learning phase is still active
const isLearning = await learningPhase.isInLearningPhase(heatpumpPhaseId);
console.log(`Heatpump is ${isLearning ? 'still learning' : 'done learning'}`);
// Check if a learning phase is completed
const isDone = await learningPhase.isLearningPhaseCompleted(heatpumpPhaseId);
// Get all learning phases with their status and duration
const allPhases = await learningPhase.getLearningPhases();
for (const phase of allPhases) {
console.log(`${phase.name}: ${phase.durationInHours}h, active: ${!phase.endDate}`);
}
// Get learning phases for a specific appliance
const heatpumpPhases = await learningPhase.getLearningPhasesByApplianceId('heatpump-001');
// Complete a learning phase
await learningPhase.completeLearningPhase(heatpumpPhaseId);
// Remove a learning phase
await learningPhase.removeLearningPhase(phaseId);Networking & Protocols
useEebus(): EnergyAppEebus
Talk to EEBUS / SHIP / SPINE devices. The returned facade exposes four sub-interfaces:
devices— SHIP-level lifecycle: discovery, pairing, connection.identity— Node Identification (NID), observable identity, supported use-case discovery.useCases— typed use-case clients (LPC, LPP, MGCP, MPC, OHPCF).spine— low-level SPINE escape hatch for features not yet wrapped.
const eebus = energyApp.useEebus();
const discovered = await eebus.devices.getDiscoveredDevices();
const device = await eebus.devices.pairDevice(discovered[0].ski);
const identity = await eebus.identity.get(device.ski);
const useCases = await eebus.identity.getSupportedUseCases(device.ski);Requires the EebusDeviceManagement permission for the calls above. EebusDataAccess / EebusControl gate reads and writes on use-case features.
useMqtt(): EnergyAppMqtt
Connect to the internal SDK MQTT broker or an external broker, publish, subscribe, and observe connection status.
const mqtt = energyApp.useMqtt();
const client = await mqtt.connectToSdkBroker();
await client.subscribe('sensors/+/temperature');
client.onTopic('sensors/+/temperature', (payload) => {
console.log('Sensor reading:', payload.toString());
});
await client.publish('control/pump', 'on', /* qos */ 1, /* retain */ false);For external brokers use connectToExternalBroker(brokerUrl, options). Requires the Mqtt permission.
useBluetooth(): EnergyAppBluetooth
Scan for BLE peripherals and perform GATT read / write / notify against them.
const ble = energyApp.useBluetooth();
const devices = await ble.scan({ durationMs: 5000 });
await ble.withDevice(devices[0].address, async (session) => {
const value = await session.read('1800', '2a00');
await session.write('180a', '2a29', new TextEncoder().encode('hi'));
});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.
useUdp(): EnergyAppUdp
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.
const udp = energyApp.useUdp();
const socket = await udp.bind(5000);
socket.onMessage((data, rinfo) => {
console.log(`Received ${data.length}B from ${rinfo.address}:${rinfo.port}`);
});
await socket.send(new TextEncoder().encode('hello'), 5001, '192.168.1.50');Throws EnergyAppPermissionNotGrantedError if the Udp permission isn't granted.
useModbusRtu(): EnergyAppModbusRtu
Modbus RTU over serial. Open a port with baud rate / parity / data bits / stop bits, then read/write registers by slave ID.
const rtu = energyApp.useModbusRtu();
const client = await rtu.connect('/dev/ttyUSB0', { baudRate: 9600, parity: 'none' });
const registers = await client.readRegisters(/* slaveId */ 1, /* startReg */ 0, /* count */ 10);
await client.writeRegisters(1, 100, [42, 43]);Requires the ModbusRtu permission.
useWifi(): EnergyAppWifi
List the SSIDs the device is configured to join that are currently in range.
const wifi = energyApp.useWifi();
const ssids = await wifi.getKnownSsids();
for (const { ssid } of ssids) console.log(ssid);Requires the Wifi permission.
Location & Site
useLocation(): EnergyAppLocation
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.
const location = energyApp.useLocation();
const zip = await location.getZipCodeLocation(); // requires LocationZipCode
const full = await location.getLocation(); // requires LocationCoordinates
if (full) console.log(`lat=${full.latitude} lon=${full.longitude}`);useGridConnectionPoint(): EnergyAppGridConnectionPoint
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.
const gcp = energyApp.useGridConnectionPoint();
const point = await gcp.getGridConnectionPoint();
if (point) {
console.log(`Fuse ${point.fuseAmpere}A across ${point.numberOfPhases} phases`);
}Energy Domain APIs
useEnergyManager(): EnergyAppEnergyManager
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.
const em = energyApp.useEnergyManager();
const info = await em.getEnergyManagerInfo();
if (info) console.log(`Active manager: ${info.name} v${info.version}`);Requires the EnergyManagerInfo permission.
useElectricityTariff(): EnergyAppElectricityTariff
Register, retrieve, and manage electricity tariffs. One tariff can be marked as the system default.
const tariffs = energyApp.useElectricityTariff();
await tariffs.registerTariff({ id: 't1', name: 'Spot 2026', pricePerKwh: 0.21 });
await tariffs.makeDefaultTariff('t1');
const defaultTariff = await tariffs.getDefaultTariff();
const all = await tariffs.getAllTariffs();Requires the ElectricityTariff permission.
useWeatherForecasting(): EnergyAppWeatherForecasting
Register a weather-forecast provider (e.g. wraps an external API) and / or consume forecasts by zip code or coordinates.
const weather = energyApp.useWeatherForecasting();
await weather.registerForecast({ forecastId: 'wx-prod', name: 'OpenWeather' });
const byZip = await weather.getWeatherForecastByZipCode('wx-prod');
const byCoords = await weather.getWeatherForecastByCoordinates('wx-prod', 48.13, 11.57);Publishers need WeatherForecastRegister; consumers need WeatherForecastUse.
usePvForecasting(): EnergyAppPvForecasting
Same shape as weather forecasting, but for PV production.
const pvForecast = energyApp.usePvForecasting();
await pvForecast.registerForecast({ forecastId: 'pv-prod', name: 'Solargis' });
const forecast = await pvForecast.getPvForecast('pv-prod');Publishers need PvForecastRegister; consumers need PvForecastUse.
useDynamicPriceForecast(): EnergyAppDynamicPriceForecast
Publish and consume forward-looking electricity-price forecasts (e.g. day-ahead spot). The data is forecast only — never settled prices.
const dpf = energyApp.useDynamicPriceForecast();
await dpf.registerForecast({ forecastId: 'epex-da', name: 'EPEX Day-Ahead', vendor: 'EPEX' });
await dpf.publishForecast('epex-da', {
currency: 'EUR',
resolution: '1h',
entries: [{ timestampIso: '2026-05-23T10:00:00Z', consumptionPricePerKwh: 0.21 }]
});
const latest = await dpf.getLatestForecast();
dpf.onForecastPublished((forecast) => console.log('new forecast', forecast.forecastId));Publishers need DynamicPriceForecastRegister; consumers need DynamicPriceForecastUse.
usePvSystem(): EnergyAppPvSystem
Register a PV system's structural configuration (kWp, DC string orientations, associated appliances) so other apps can reason about expected production.
const pv = energyApp.usePvSystem();
await pv.registerPvSystem({
id: 'pv-1',
kWp: 9.6,
dcStrings: [
{ azimuth: 180, tilt: 30 },
{ azimuth: 90, tilt: 30 }
]
});
const systems = await pv.getPvSystems();Publishers need PvSystemRegister; consumers need PvSystemUse.
useTimeseries(): EnergyAppTimeseries
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.
const ts = energyApp.useTimeseries();
const last24h = await ts.query({
dataType: 'pvProduction',
resolution: '15m',
startTime: Date.now() - 24 * 60 * 60 * 1000,
endTime: Date.now()
});Requires the Timeseries permission.
useDiagnostics(): EnergyAppDiagnostics
Energy-manager packages can submit their current state, forecast, and control plan to internal diagnostics for offline analysis. Fire-and-forget.
const diag = energyApp.useDiagnostics();
diag.energyManagerDiagnostics(
{ batterySoc: 47, gridPowerW: 1200 },
{ pvNext24h: [...] },
{ actions: [{ applianceId: 'battery-1', mode: 'charge', powerW: 3000 }] }
);Operational Utilities
useOnboarding(): EnergyAppOnboarding
Drive a multi-step onboarding guide — start / advance / back / skip / cancel, persist responses, and observe step transitions.
const guide = energyApp.useOnboarding();
await guide.startGuide('pv-setup', EnyoOnboardingGuideCategory.PvSystem);
const step = await guide.nextStep('pv-setup');
await guide.respondToStep('pv-setup', { answer: 'yes' });
const listenerId = guide.onStepListener('pv-setup', (event) => {
console.log('step changed:', event.stepId);
});useSecretManager(): EnergyAppSecretManager
Encrypted retrieval / storage of developer-org secrets (API keys, vendor tokens, OAuth client secrets). Strongly typed accessors keep the call site safe.
const secrets = energyApp.useSecretManager();
await secrets.saveSecret('weather-api', { token: 'xyz' });
const cred = await secrets.getSecret<{ token: string }>('weather-api');
const names = await secrets.listAvailableSecrets();
await secrets.removeSecret('weather-api');Requires the SecretManager permission.
useSequenceGenerator(): EnergyAppSequenceGenerator
Process-local monotonic counter, keyed by an arbitrary name. Use it for stable request / message IDs without coordinating across instances.
const seq = energyApp.useSequenceGenerator();
const reqId = await seq.next('mqtt-publish'); // 1, 2, 3, …
const txId = await seq.next('ocpp-tx'); // independent counterAdvanced Modbus Integration
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.
✨ Features
- Vendor Agnostic - Works with SMA, Fronius or any Modbus device via configuration
- Type Safe - Full TypeScript support with proper DataBus message types
- Configurable - Register addresses, data types, scaling all configurable
- Fault Tolerant - Built-in connection health monitoring and retry logic
- Modular - Clean separation between inverters, batteries, and meters
🚀 Quick Start
Basic Modbus Setup
import { EnergyApp } from '@enyo-energy/energy-app-sdk';
import {
EnergyAppModbusInverter,
EnergyAppModbusBattery,
EnergyAppModbusMeter
} from '@enyo-energy/energy-app-sdk';
const energyApp = new EnergyApp();
const dependencies = { client: energyApp, randomUUID: () => crypto.randomUUID() };
// Network device configuration
const networkDevice = {
id: 'device-1',
hostname: '192.168.1.100',
// ... other network device properties
};
// Create configurable inverter
const modbusInverter = new EnergyAppModbusInverter(dependencies, {
name: [{ language: 'en', name: 'Solar Inverter' }],
registers: {
serialNumber: {
address: 400001,
dataType: 'uint16'
},
power: {
address: 400010,
dataType: 'int32',
required: true
},
voltageL1: {
address: 400020,
dataType: 'uint32',
scale: 2
},
totalEnergy: {
address: 400030,
dataType: 'uint32'
}
},
options: {
unitId: 1,
timeout: 5000
}
}, networkDevice);
// Create battery with inverter dependency
const modbusBattery = new EnergyAppModbusBattery(dependencies, {
inverter: modbusInverter,
name: [{ language: 'en', name: 'Battery Storage' }],
registers: {
soc: {
address: 500001,
dataType: 'uint16',
required: true
},
current: {
address: 500005,
dataType: 'int16',
scale: 1
},
voltage: {
address: 500010,
dataType: 'uint16',
scale: 1
}
},
options: {}
});
// Create standalone meter
const modbusMeter = new EnergyAppModbusMeter(dependencies, {
name: [{ language: 'en', name: 'Grid Meter' }],
registers: {
gridPower: {
address: 600001,
dataType: 'int32',
required: true
},
gridFeedInEnergy: {
address: 600010,
dataType: 'uint32'
},
gridConsumptionEnergy: {
address: 600020,
dataType: 'uint32'
}
},
options: {
unitId: 2
}
}, networkDevice);
// Connect and start data collection
await modbusInverter.connect();
await modbusBattery.connect();
await modbusMeter.connect();
// Fetch data (returns proper DataBus messages)
const inverterData = await modbusInverter.updateData();
const batteryData = await modbusBattery.updateData();
const meterData = await modbusMeter.updateData();📊 Supported Data Types
uint16- 16-bit unsigned integer (1 register)int16- 16-bit signed integer (1 register)uint32- 32-bit unsigned integer (2 registers)int32- 32-bit signed integer (2 registers)float32- 32-bit float (2 registers)
⚙️ Register Configuration
interface EnergyAppModbusRegisterConfig {
address: number; // Modbus register address
dataType: EnergyAppModbusDataType; // Data type
scale?: number; // Scaling factor (FIX2 = scale 2)
quantity?: number; // Number of registers (auto-calculated)
required?: boolean; // Whether register is required
}🔄 DataBus Integration
The Modbus implementation seamlessly integrates with the enyo DataBus using typed messages:
EnyoDataBusInverterValuesV1- Inverter data messagesEnyoDataBusBatteryValuesUpdateV1- Battery data messagesEnyoDataBusMeterValuesUpdateV1- Meter data messages
All messages include proper timestamps and message types for seamless integration with the enyo platform.
🛠️ Error Handling
The implementation provides comprehensive error handling with specific error types:
- EnergyAppModbusConfigurationError - Invalid configuration parameters
- EnergyAppModbusConnectionError - Connection and communication failures
- EnergyAppModbusReadError - Register read failures with context
try {
await modbusInverter.connect();
const data = await modbusInverter.updateData();
} catch (error) {
if (error instanceof EnergyAppModbusConnectionError) {
console.error('Connection failed:', error.message);
// Handle connection issues
} else if (error instanceof EnergyAppModbusConfigurationError) {
console.error('Configuration error:', error.message);
// Fix configuration issues
}
}🏗️ Architecture
The Modbus implementation follows a clean, modular architecture:
- EnergyAppModbusInverter - Configurable inverter implementation
- EnergyAppModbusBattery - Battery with inverter dependency support
- EnergyAppModbusMeter - Standalone meter implementation
- EnergyAppModbusRegisterMapper - Configuration-driven register access
- EnergyAppModbusDataTypeConverter - Vendor-agnostic data handling
- EnergyAppModbusFaultTolerantReader - Fault-tolerant communication layer
- EnergyAppModbusConnectionHealth - Connection health monitoring
This modular design ensures maintainability, testability, and extensibility for future enhancements.
Appliance Management
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.
import { ApplianceManager } from '@enyo-energy/energy-app-sdk';
const applianceManager = await ApplianceManager.initialize(energyApp, {
// Optional config: identifier strategy, cache options, etc.
});
// Create or update by identifier (idempotent — uses the configured strategy).
const applianceId = await applianceManager.createOrUpdateAppliance({
identifier: 'sn-1234567890',
name: [{ language: 'en', name: 'Inverter A' }],
type: EnyoApplianceTypeEnum.Inverter,
/* ... */
});
// Lookups
const inverters = await applianceManager.getAppliancesByType(EnyoApplianceTypeEnum.Inverter);
const byId = await applianceManager.findApplianceById(applianceId);
const matches = await applianceManager.findByIdentifier('sn-1234567890');
// State transitions
await applianceManager.updateApplianceState(
applianceId,
EnyoApplianceConnectionType.Modbus,
EnyoApplianceStateEnum.Connected
);Key methods
| Method | Purpose |
|---|---|
| static initialize(app, config?) | Build a manager, prime the cache, install SDK listeners. |
| 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. |
| updateAppliance(id, patch) | Patch an existing appliance. |
| removeAppliance(id) / removeAppliancesByIdentifier(id) | Delete one / many. |
| findApplianceById(id) | SDK lookup by appliance ID. Returns null on not-found; propagates SDK errors. |
| findByIdentifier(extractedId) | Cache-first lookup keyed by the configured identifier strategy. Falls through to one SDK list call on cache miss. |
| findFirstByStrategies(value, strategies) | Probes each strategy in order against the SDK list; returns the first match plus which strategy hit. |
| findAppliancesByNetworkDeviceId(deviceId) | Synchronous, cache-backed reverse lookup from a NetworkDevice to its appliances. |
| getAppliancesByType(type) / getAllAppliancesByType(type) | Filtered listing (own / all). |
| updateApplianceState(id, connection, state) | State transitions (Connected / Offline / Error / …). |
| setAppliancesStateByIdentifier(id, state) | Bulk state transition for every appliance sharing an identifier; preserves each appliance's existing connectionType. |
| bulkUpdate(updates) | Atomic batch of state changes. |
| 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. |
| refreshCache() / clearCache() | Manual cache control. |
| dispose() | Release SDK listeners. |
Identifier strategies are exported from the package — typical choices match on serial number, hostname, or a composite of manufacturer + model + sn.
Network Devices & Access Recovery
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:
- Network-access-denied errors —
EnyoNetworkDevice.accessStatusis 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. - 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.
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.
NetworkAccessGuard
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.
import { NetworkAccessGuard } from '@enyo-energy/energy-app-sdk';
const accessGuard = new NetworkAccessGuard(energyApp, {
ports: [502],
onAccessRestored: async (networkDeviceId) => {
await myModbusPool.reconnect(networkDeviceId);
},
});
// Precondition before a Modbus connect:
if (!(await accessGuard.ensureAccess(networkDevice.id))) {
console.warn(`Modbus port access not granted for ${networkDevice.hostname}`);
return;
}
// Wrap any Modbus read so an access-denied error triggers recovery:
const registers = await accessGuard.withAccessGuard(networkDevice.id, () =>
modbusClient.readHoldingRegisters(40000, 4),
);Recovery lifecycle:
- A read fails inside
withAccessGuard. The guard detects the access-denied error viaNetworkAccessGuard.isAccessDeniedError(error), re-throws it to the caller (so the current poll cycle fails fast), and kicks off recovery in the background. - 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), theonAccessRestoredhandler fires immediately. - Otherwise the device stays in a pending set and the
listenForDeviceAccessChangeregistration fires the handler when the SDK reports the device flipped to'granted'.
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.
The guard exposes:
| Method | Purpose |
| --- | --- |
| static isAccessDeniedError(error) | Recognise the SDK's access-denied error string |
| ensureAccess(deviceId) | Idempotent port-allow-list request before a connect |
| withAccessGuard(deviceId, action) | Wrap any async TCP call — recovers on access-denied |
| recoverAccess(deviceId) | Explicit recovery trigger after catching an access-denied error |
| onAccessRestored(handler) / onAccessDenied(handler) | Register additional handlers at runtime; returns a disposer |
| isRecovering(deviceId) | Introspect whether a recovery is in flight |
| dispose() | Tear down the SDK listener |
NetworkDeviceManager
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.
import {
ApplianceManager,
EnergyApp,
NetworkDeviceManager,
} from '@enyo-energy/energy-app-sdk';
const energyApp = new EnergyApp();
const applianceManager = await ApplianceManager.initialize(energyApp);
const networkManager = await NetworkDeviceManager.initialize(
energyApp,
applianceManager,
{
ports: [502],
autoToggleApplianceState: true,
onApplianceAccessRestored: async ({ appliance, networkDeviceId }) => {
// Re-establish a Modbus session and restart the polling loop for this appliance.
await myModbusPool.reconnect(networkDeviceId);
},
onApplianceAccessRevoked: async ({ appliance, networkDeviceId }) => {
// User revoked access in the UI — tear down the connection.
await myModbusPool.disconnect(networkDeviceId);
},
onApplianceAccessDenied: async ({ appliance, networkDeviceId }) => {
// An access-denied error was just observed at runtime — mark the
// appliance offline. `autoToggleApplianceState: true` already does
// this; the handler is here for any custom side-effects.
},
onApplianceNetworkDeviceRemoved: async ({ appliance, networkDeviceId }) => {
await myModbusPool.disconnect(networkDeviceId);
},
onNetworkDeviceDetected: async (devices) => {
// New device found — classify + connect.
for (const device of devices) {
await classifyAndConnect(device);
}
},
onNetworkDeviceAccessChanged: async (deviceId, status) => {
// Optional: raw access-status passthrough, fires even for devices
// the package has no appliances on yet. Useful for first-time
// onboarding where a 'granted' transition needs to drive a discovery
// pass before any appliance exists.
},
},
);
// Every Modbus read inside the poll loop:
await networkManager.withAccessGuard(networkDeviceId, () =>
modbusClient.readHoldingRegisters(40000, 4),
);What the manager handles for you:
- Access-denied recovery —
withAccessGuard/ensureAccessdelegate to the bundledNetworkAccessGuard. - User-driven transitions — on
listenForDeviceAccessChange, the manager dispatchesonApplianceAccessRestoredon'granted'andonApplianceAccessRevokedon'denied'/'pending', resolving each transition into the per-appliance events your reconnect/disconnect code needs. - 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. - AccessDenied vs AccessRevoked — both signal "the package can no longer read this device", but the source differs.
onApplianceAccessDeniedfires whenwithAccessGuardcatches a runtime read error (the SDK's "Network access denied" message).onApplianceAccessRevokedfires 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. - One manager per
EnergyApp—NetworkDeviceManager.initializeenforces a single active manager perEnergyAppinstance. Calling it a second time without first callingdispose()throwsNetworkDeviceManagerAlreadyInitializedError. After disposal a fresh manager can be created. - Device removal — on
listenForNetworkDeviceRemoved, the manager firesonApplianceNetworkDeviceRemovedper affected appliance and clears its cache. - Optional auto-state toggle — with
autoToggleApplianceState: true, the manager flips affected appliances toEnyoApplianceStateEnum.Offlineon denial / revocation / removal, and back toEnyoApplianceStateEnum.Connectedon restoration, viaapplianceManager.updateApplianceState(...).
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.
Startup pattern
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:
client.register(async () => {
const applianceManager = await ApplianceManager.initialize(client);
const networkManager = await NetworkDeviceManager.initialize(
client,
applianceManager,
{
ports: [502],
onApplianceAccessRestored: ({ networkDeviceId }) => connectDevice(networkDeviceId),
onApplianceAccessRevoked: ({ networkDeviceId }) => disconnectDevice(networkDeviceId),
onNetworkDeviceDetected: async (devices) => {
for (const device of devices) await connectDevice(device.id);
},
},
);
// Warm-restart: reconnect to devices that already have access.
const granted = await client.useNetworkDevices().getDevices({ accessStatus: 'granted' });
for (const device of granted) {
await connectDevice(device.id);
}
client.updateEnergyAppState(EnergyAppStateEnum.Running);
});
async function connectDevice(networkDeviceId: string) {
if (!(await networkManager.ensureAccess(networkDeviceId))) return;
// ...classify, open modbus client, register appliances...
}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.
Retry Framework
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.
import { RetryManager, exponentialBackoff } from '@enyo-energy/energy-app-sdk';
const retries = new RetryManager();
retries.register('modbus-inverter-1', {
backoff: exponentialBackoff({ initialMs: 1_000, maxMs: 60_000, factor: 2 }),
maxAttempts: Infinity, // keep retrying forever
openAfterConsecutiveFailures: 5, // trip the breaker after 5 fails
});
const value = await retries.execute('modbus-inverter-1', () =>
modbusClient.readHoldingRegisters(40000, 4)
);
// React to circuit-breaker transitions (Idle → Retrying → Open → Closed).
const unsubscribe = retries.onStateChange((snapshot) => {
console.log(`[${snapshot.id}] ${snapshot.state} (attempt ${snapshot.attempts})`);
});
retries.statuses(); // current snapshots of every registered op
retries.reset('modbus-inverter-1');
retries.unregister('modbus-inverter-1');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".
Device Integrations
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.
✨ What the integration framework does for you
- Subscribes to the relevant
*CommandV1data-bus messages for the appliance type. - Dispatches each command to the async handler you register.
- Auto-acknowledges every command via a
CommandAcknowledgeV1response containing yourAccepted/Rejected/NotSupportedanswer. - Handles broadcast
GridOperatorPowerLimitationV1(§14a EnWG) and routes it once per managed appliance. - Manages lifecycle — auto-starts on construction and auto-stops on shutdown by default.
- Exposes typed
publish*helpers so your handler implementations can broadcast status updates back to the system without hand-building messages.
🚀 Quick Start
import {
HeatpumpIntegrationEnergyApp,
EnyoSourceEnum,
EnyoCommandAcknowledgeAnswerEnum,
ApplianceManager,
} from '@enyo-energy/energy-app-sdk';
class MyHeatpumpApp extends HeatpumpIntegrationEnergyApp {
constructor(applianceManager: ApplianceManager) {
super({ source: EnyoSourceEnum.Device, applianceManager });
}
protected async handleHeatpumpOverheating(message) {
await driveOverheating(message.data);
return { answer: EnyoCommandAcknowledgeAnswerEnum.Accepted };
}
protected async handleHeatpumpAvailablePowerAnnouncement(message) {
await scaleHeatpumpToEnvelope(message.data);
return { answer: EnyoCommandAcknowledgeAnswerEnum.Accepted };
}
protected async handleGridOperatorPowerLimitation(message, applianceId) {
await applyGridLimit(applianceId, message.data);
return { applianceId, answer: EnyoCommandAcknowledgeAnswerEnum.Accepted };
}
}IntegrationEnergyApp (Base Class)
IntegrationEnergyApp is the abstract base every device integration extends. It owns the data-bus subscription/acknowledgment loop and the broadcast routing so subclasses only declare what commands they care about and how to fulfil them.
Constructor options (IntegrationEnergyAppOptions)
| Field | Type | Default | Purpose |
|---|---|---|---|
| source | EnyoSourceEnum | required | Source identifier for outbound messages (typically Device). |
| applianceManager | ApplianceManager | optional | Lookup all appliances of the integration's managedApplianceType. |
| applianceIds | string[] | optional | Explicit list of appliance IDs to manage. Overrides the manager-based lookup. |
| autoStart | boolean | true | Subscribe to the data bus immediately after construction. |
| autoStopOnShutdown | boolean | true | Register an SDK shutdown hook to release listeners. |
Lifecycle
start(): void— idempotent; registers all command handlers via the subclass'sregisterHandlers().stop(): void— releases listeners and disposes the outbound command handler.
For subclass authors
protected abstract registerHandlers(): void— callregisterCommandHandler(messageType, handler)for each command you want to receive.protected abstract handleGridOperatorPowerLimitation(message, applianceId)— handle the §14a EnWG broadcast per managed appliance.protected abstract get managedApplianceType(): EnyoApplianceTypeEnum— used to resolve appliance IDs when no explicit list is given.
HeatpumpIntegrationEnergyApp
Drives a heatpump. Manages building / DHW overheating commands and grid-power-availability announcements.
- Subscribed commands:
HeatpumpOverheatingV1,HeatpumpAvailablePowerAnnouncementV1,GridOperatorPowerLimitationV1(broadcast) - Implement:
handleHeatpumpOverheating,handleHeatpumpAvailablePowerAnnouncement,handleGridOperatorPowerLimitation - Publish helpers:
publishHeatpumpValuesUpdate(applianceId, values)— operation mode, electrical and thermal power, energies.publishHeatpumpTemperatures(applianceId, temperatures)— outdoor, flow, return, DHW tanks, heating circuits, buffer tank.
WallboxIntegrationEnergyApp
Drives an EV wallbox / charger. Has the richest command surface of all integrations.
- Subscribed commands:
StartChargeV1,StopChargeV1,PauseChargingV1,ResumeChargingV1,ChangeChargingPowerV1,SetChargingScheduleV1,ResetChargerV1,RebootChargerV1,RequestChargerLogsV1,ClearChargingProfilesV1, `Grid
