npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@enyo-energy/energy-app-sdk

v0.0.167

Published

enyo Energy App SDK

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.

npm version TypeScript

Table of Contents

Installation

Install the SDK using npm:

npm install @enyo-energy/energy-app-sdk

Quick 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 typed publish* 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 EnergyManagerEnergyApp to wire several together).

Core Concepts

Energy App Lifecycle

Energy Apps follow a specific lifecycle managed by the enyo system:

  1. Initialization: Your app registers with the system
  2. Running: App performs its main functionality
  3. State Management: App reports its current state
  4. 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 up
  • running: App is functioning normally
  • configuration-required: App needs user configuration
  • internet-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 management
  • Wallbox: EV charging station integration
  • Meter: Energy metering applications
  • EnergyManagement: Overall energy optimization
  • HeatPump: Heat pump control systems
  • AirConditioning: Air-conditioning units
  • BatteryStorage: Battery management
  • ClimateControl: HVAC and climate systems
  • DynamicElectricityTariff: Dynamic / spot-price tariff providers
  • StaticElectricityTariff: Fixed-price tariff providers
  • TemperatureSensor: Standalone temperature sensors
  • SmartPlug: Smart-plug appliances
  • Other: 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 storage
  • NetworkDeviceDiscovery: Discover devices on the local network
  • NetworkDeviceSearch: Search for specific network devices
  • NetworkDeviceAccess: Access discovered network devices
  • Modbus: Communicate via Modbus protocol

Data Bus Permissions

  • SendDataBusValues: Send sensor data and measurements
  • SubscribeDataBus: Listen to data from other devices
  • SendDataBusCommands: Send control commands

Device Permissions

  • Appliance: Manage appliances created by your package
  • AllAppliances: Access all appliances in the system
  • OcppServer: Run OCPP server for EV charging
  • ChargingCard: Manage EV charging cards
  • Vehicle: Access vehicle information
  • Charge: Manage charging sessions

Command Permissions

  • InverterControlCommands: Send inverter control commands (e.g. feed-in limit)
  • BatteryControlCommands: Send battery / storage control commands
  • ChargerControlCommands: Send wallbox / charger control commands

Networking & Protocol Permissions

  • ModbusRtu: Communicate over Modbus RTU (serial)
  • EebusDeviceManagement: Pair / discover / connect EEBUS devices
  • EebusDataAccess: Read EEBUS use-case data
  • EebusControl: Send EEBUS control commands (write features)
  • Mqtt: Connect to the internal SDK MQTT broker or external brokers
  • Bluetooth: Scan and talk to BLE peripherals
  • Wifi: List known WiFi SSIDs
  • Udp: Bind UDP sockets and exchange datagrams
  • ChildProcess: Spawn child processes from the runtime

Data & Domain Permissions

  • Timeseries: Query historical timeseries data
  • EnergyPrices: Read current and forecast electricity prices
  • ElectricityTariff: Manage electricity tariffs
  • EnergyManager: Run as the active energy manager
  • EnergyManagerInfo: Read information about the active energy manager
  • WeatherForecastRegister / WeatherForecastUse: Publish / consume weather forecasts
  • PvForecastRegister / PvForecastUse: Publish / consume PV forecasts
  • DynamicPriceForecastRegister / DynamicPriceForecastUse: Publish / consume dynamic-price forecasts
  • PvSystemRegister / PvSystemUse: Register / read PV system configuration

Site & Identity Permissions

  • LocationZipCode: Read the site's zip-code-level location
  • LocationCoordinates: Read the site's full coordinates
  • SecretManager: 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 counter

Advanced 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 messages
  • EnyoDataBusBatteryValuesUpdateV1 - Battery data messages
  • EnyoDataBusMeterValuesUpdateV1 - 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:

  1. Network-access-denied errorsEnyoNetworkDevice.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.
  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.

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:

  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.
  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.
  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'.

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 recoverywithAccessGuard / ensureAccess delegate to the bundled NetworkAccessGuard.
  • 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.
  • 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. 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.
  • One manager per EnergyAppNetworkDeviceManager.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.
  • Device removal — on listenForNetworkDeviceRemoved, the manager fires onApplianceNetworkDeviceRemoved per affected appliance and clears its cache.
  • 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(...).

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 *CommandV1 data-bus messages for the appliance type.
  • Dispatches each command to the async handler you register.
  • Auto-acknowledges every command via a CommandAcknowledgeV1 response containing your Accepted / Rejected / NotSupported answer.
  • 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's registerHandlers().
  • stop(): void — releases listeners and disposes the outbound command handler.

For subclass authors

  • protected abstract registerHandlers(): void — call registerCommandHandler(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