@enyo-energy/energy-app-sdk 0.0.170 → 0.0.172

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +215 -0
  2. package/dist/cjs/energy-app-permission.type.cjs +1 -0
  3. package/dist/cjs/energy-app-permission.type.d.cts +3 -2
  4. package/dist/cjs/energy-app.cjs +16 -0
  5. package/dist/cjs/energy-app.d.cts +13 -0
  6. package/dist/cjs/enyo-energy-app-sdk.d.cts +5 -0
  7. package/dist/cjs/implementations/automation/automation-validators.cjs +254 -0
  8. package/dist/cjs/implementations/automation/automation-validators.d.cts +63 -0
  9. package/dist/cjs/index.cjs +3 -0
  10. package/dist/cjs/index.d.cts +3 -0
  11. package/dist/cjs/packages/energy-app-automation.cjs +2 -0
  12. package/dist/cjs/packages/energy-app-automation.d.cts +133 -0
  13. package/dist/cjs/packages/energy-app-modbus-rtu.d.cts +15 -0
  14. package/dist/cjs/packages/energy-app-modbus.d.cts +14 -0
  15. package/dist/cjs/packages/energy-app-udp.d.cts +14 -0
  16. package/dist/cjs/types/enyo-air-conditioning-appliance.cjs +13 -1
  17. package/dist/cjs/types/enyo-air-conditioning-appliance.d.cts +14 -1
  18. package/dist/cjs/types/enyo-appliance.d.cts +10 -0
  19. package/dist/cjs/types/enyo-automation.cjs +77 -0
  20. package/dist/cjs/types/enyo-automation.d.cts +232 -0
  21. package/dist/cjs/types/enyo-data-bus-value.cjs +2 -0
  22. package/dist/cjs/types/enyo-data-bus-value.d.cts +42 -1
  23. package/dist/cjs/version.cjs +1 -1
  24. package/dist/cjs/version.d.cts +1 -1
  25. package/dist/energy-app-permission.type.d.ts +3 -2
  26. package/dist/energy-app-permission.type.js +1 -0
  27. package/dist/energy-app.d.ts +13 -0
  28. package/dist/energy-app.js +16 -0
  29. package/dist/enyo-energy-app-sdk.d.ts +5 -0
  30. package/dist/implementations/automation/automation-validators.d.ts +63 -0
  31. package/dist/implementations/automation/automation-validators.js +245 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.js +3 -0
  34. package/dist/packages/energy-app-automation.d.ts +133 -0
  35. package/dist/packages/energy-app-automation.js +1 -0
  36. package/dist/packages/energy-app-modbus-rtu.d.ts +15 -0
  37. package/dist/packages/energy-app-modbus.d.ts +14 -0
  38. package/dist/packages/energy-app-udp.d.ts +14 -0
  39. package/dist/types/enyo-air-conditioning-appliance.d.ts +14 -1
  40. package/dist/types/enyo-air-conditioning-appliance.js +12 -0
  41. package/dist/types/enyo-appliance.d.ts +10 -0
  42. package/dist/types/enyo-automation.d.ts +232 -0
  43. package/dist/types/enyo-automation.js +74 -0
  44. package/dist/types/enyo-data-bus-value.d.ts +42 -1
  45. package/dist/types/enyo-data-bus-value.js +2 -0
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -55,6 +55,14 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
55
55
  - [BatteryCommandForecast](#batterycommandforecast)
56
56
  - [HeatpumpForecast](#heatpumpforecast)
57
57
  - [Validators](#validators)
58
+ - [Automations](#automations)
59
+ - [The model](#-the-model)
60
+ - [Guide: Energy Manager apps](#-guide-energy-manager-apps)
61
+ - [Guide: regular energy apps (smart plugs like Shelly)](#-guide-regular-energy-apps-smart-plugs-like-shelly)
62
+ - [End-to-end: pool pump on solar](#-end-to-end-pool-pump-on-solar)
63
+ - [Mandatory vs Flexible (current limitation)](#-mandatory-vs-flexible-current-limitation)
64
+ - [Permissions](#-permissions)
65
+ - [Validators](#-validators)
58
66
  - [Examples](#examples)
59
67
  - [Basic Energy App](#basic-energy-app)
60
68
  - [Device Integration](#device-integration)
@@ -2082,6 +2090,213 @@ try {
2082
2090
 
2083
2091
  Granular helpers are exported alongside the top-level validators: `validateChargerSchedule`, `validateBatterySchedule`, `validateDhwBoostWindows`, `validateRoomPreHeatingWindows`, `validateBufferTankBoostWindows`, `validatePowerAnnouncementSchedule`, `validateTemperatureForecast`.
2084
2092
 
2093
+ ## Automations
2094
+
2095
+ Automations let the end-user wire up simple **"when a trigger is active, do one or more actions"** rules — for example *"when PV surplus is above 2000 W, switch my pool pump for at least 10 minutes."* They are composed by the user in the platform UI from building blocks that energy apps contribute:
2096
+
2097
+ - An **Energy Manager app** *registers the trigger types* it can evaluate and publishes the live trigger state (and, optionally, a forecast).
2098
+ - A **regular energy app** (e.g. a Shelly smart-plug integration) *declares which of its appliances can be an action target* and *executes the switching* when the trigger fires.
2099
+ - The **user** creates the concrete automation in the app; energy apps only **read and observe** automations through the SDK — there is no `create()` on the SDK (authoring lives in the platform).
2100
+
2101
+ Access the API via `useAutomations()`:
2102
+
2103
+ ```typescript
2104
+ const automations = sdk.useAutomations();
2105
+ ```
2106
+
2107
+ ### 🧩 The model
2108
+
2109
+ An `EnyoAutomation` is `{ id, name, enabled, trigger, actions[] }`. The pieces:
2110
+
2111
+ | Concept | Type | Values / fields |
2112
+ | --- | --- | --- |
2113
+ | **Trigger** | `EnyoAutomationTriggerTypeEnum` | `PvSurplusThreshold` → `{ thresholdW }` (activate above, deactivate below) |
2114
+ | **Action — smart plug** | `EnyoAutomationActionTypeEnum.SmartPlugSwitch` | `{ applianceId, minDurationMinutes }` — `minDurationMinutes` is `5…360` in steps of `5` |
2115
+ | **Action — MQTT** | `EnyoAutomationActionTypeEnum.Mqtt` | `{ topic, payloadTemplate, updateChargingPvSurplus, publishOptions? }` |
2116
+ | **Scheduling** | `EnyoAutomationSchedulingModeEnum` | `Mandatory` (run exactly while active) or `Flexible` (Energy Manager may choose whether/when within the active window) |
2117
+ | **Target kind** | `EnyoAutomationTargetKindEnum` | `Load` (consumes power — counts in the energy balance) or `Signal` (control signal only) |
2118
+
2119
+ The **MQTT** `payloadTemplate` is JSON that may embed the placeholders in `EnyoAutomationMqttPlaceholderEnum` — `{{state}}` (`on`/`off`), `{{surplusW}}`, `{{timestampIso}}`, `{{automationId}}` — which the platform substitutes before publishing.
2120
+
2121
+ Two things travel over the **data bus** vs. the **API**:
2122
+
2123
+ - **Trigger state** is the data-bus message `AutomationTriggerV1` (`EnyoDataBusAutomationTriggerV1`): `{ automationId, data: { active, trigger } }`, where `trigger` is the per-type `EnyoAutomationTriggerData` (for PV surplus: `{ triggerType, surplusW, thresholdW }`).
2124
+ - The **forecast** is a method — `publishAutomationForecast()` — not a data-bus message.
2125
+
2126
+ ### ⚡ Guide: Energy Manager apps
2127
+
2128
+ Requires the **`EnergyManager`** permission (to register triggers / publish forecasts) and **`SendDataBusValues`** (to emit the trigger message).
2129
+
2130
+ **1. Register the trigger type once, at startup.** Registration is by enum value only — all user-facing wording is handled by the UI.
2131
+
2132
+ ```typescript
2133
+ import {EnergyApp, EnyoAutomationTriggerTypeEnum} from '@enyo-energy/energy-app-sdk';
2134
+
2135
+ const sdk = new EnergyApp();
2136
+ const automations = sdk.useAutomations();
2137
+
2138
+ sdk.register(async () => {
2139
+ await automations.registerTrigger(EnyoAutomationTriggerTypeEnum.PvSurplusThreshold);
2140
+ });
2141
+ ```
2142
+
2143
+ **2. Evaluate the condition and publish trigger state.** Watch the aggregated PV surplus and, for every automation that uses your trigger, publish an `AutomationTriggerV1` message whenever it crosses the user-configured `thresholdW`.
2144
+
2145
+ Message-type identifiers are the `EnyoDataBusMessageEnum` values passed as strings, the same convention used everywhere else in the data-bus API.
2146
+
2147
+ ```typescript
2148
+ import {EnyoAutomationTriggerTypeEnum} from '@enyo-energy/energy-app-sdk';
2149
+
2150
+ const dataBus = sdk.useDataBus();
2151
+
2152
+ dataBus.listenForMessages(['AggregatedStateUpdateV1'], (message: any) => {
2153
+ // gridFeedInW is the surplus fed to the grid (or derive from -gridPowerW when negative)
2154
+ const surplusW = message.data?.gridFeedInW ?? 0;
2155
+
2156
+ for (const automation of currentAutomations) {
2157
+ if (automation.trigger.type !== EnyoAutomationTriggerTypeEnum.PvSurplusThreshold) continue;
2158
+ const thresholdW = automation.trigger.thresholdW;
2159
+ const active = surplusW > thresholdW;
2160
+
2161
+ dataBus.sendMessage([{
2162
+ type: 'message',
2163
+ message: 'AutomationTriggerV1',
2164
+ automationId: automation.id,
2165
+ data: {
2166
+ active,
2167
+ trigger: {
2168
+ triggerType: EnyoAutomationTriggerTypeEnum.PvSurplusThreshold,
2169
+ surplusW,
2170
+ thresholdW,
2171
+ },
2172
+ },
2173
+ }]);
2174
+ }
2175
+ });
2176
+ ```
2177
+
2178
+ > Debounce/hysteresis (e.g. only emit on a real transition, add a deactivate margin) is the Energy Manager's responsibility — send a message when the `active` state actually changes, not on every tick.
2179
+
2180
+ **3. (Optional) Publish a forecast.** Combine your PV-surplus forecast with each automation's threshold to predict the windows where the trigger will be active, so the system can plan ahead. The forecast marks occupied windows only (no watts).
2181
+
2182
+ ```typescript
2183
+ await automations.publishAutomationForecast({
2184
+ automationId: 'pool-pump',
2185
+ resolution: '15m',
2186
+ entries: [
2187
+ { timestampIso: '2026-07-04T10:00:00.000Z', active: true, mandatory: false, hasLoad: true },
2188
+ { timestampIso: '2026-07-04T10:15:00.000Z', active: true, mandatory: false, hasLoad: true },
2189
+ { timestampIso: '2026-07-04T10:30:00.000Z', active: false },
2190
+ ],
2191
+ });
2192
+ ```
2193
+
2194
+ ### 🔌 Guide: regular energy apps (smart plugs like Shelly)
2195
+
2196
+ Requires the **`Automation`** permission (to read/observe automations) plus whatever your device needs to switch (e.g. network device access, Modbus, or `RestrictedInternetAccess` for a Shelly HTTP call).
2197
+
2198
+ **1. Advertise which appliances can be an action target.** When you register (or update) an appliance, set `supportedAutomationActions`. The automation UI then offers this appliance only for the action types it lists. A three-channel Shelly registers three `SmartPlug` appliances, one per channel.
2199
+
2200
+ ```typescript
2201
+ import {EnyoApplianceTypeEnum, EnyoAutomationActionTypeEnum} from '@enyo-energy/energy-app-sdk';
2202
+
2203
+ await sdk.useAppliances().save(
2204
+ {
2205
+ name: [{language: 'en', name: 'Pool pump'}],
2206
+ type: EnyoApplianceTypeEnum.SmartPlug,
2207
+ networkDeviceIds: [shellyDeviceId],
2208
+ supportedAutomationActions: [EnyoAutomationActionTypeEnum.SmartPlugSwitch],
2209
+ },
2210
+ 'shelly-pool-ch0',
2211
+ );
2212
+ ```
2213
+
2214
+ **2. Learn which automations target your appliances.** Fetch on startup and keep in sync with the listeners. Each listener returns an id you can pass to `removeListener()`.
2215
+
2216
+ ```typescript
2217
+ const automations = sdk.useAutomations();
2218
+
2219
+ let mine = (await automations.list()).filter(hasSmartPlugActionForMyAppliances);
2220
+
2221
+ automations.listenForAutomationCreated((a) => { /* add if it targets my appliance */ });
2222
+ automations.listenForAutomationUpdated((a) => { /* replace */ });
2223
+ automations.listenForAutomationRemoved((automationId) => { /* drop + switch off */ });
2224
+ ```
2225
+
2226
+ **3. Switch on the trigger message.** Subscribe to `AutomationTriggerV1`, resolve the automation's `SmartPlugSwitch` action to one of your appliances, and drive the relay. Honor `minDurationMinutes` locally (keep it on for at least that long after switching on).
2227
+
2228
+ ```typescript
2229
+ import {EnyoAutomationActionTypeEnum} from '@enyo-energy/energy-app-sdk';
2230
+
2231
+ sdk.useDataBus().listenForMessages(['AutomationTriggerV1'], async (message: any) => {
2232
+ const {automationId, data} = message; // data: { active, trigger }
2233
+ const automation = mine.find((a) => a.id === automationId);
2234
+ if (!automation) return;
2235
+
2236
+ for (const action of automation.actions) {
2237
+ if (action.type !== EnyoAutomationActionTypeEnum.SmartPlugSwitch) continue;
2238
+ if (data.active) {
2239
+ await switchOn(action.applianceId);
2240
+ scheduleMinRuntimeGuard(action.applianceId, action.minDurationMinutes);
2241
+ } else if (minRuntimeElapsed(action.applianceId)) {
2242
+ await switchOff(action.applianceId);
2243
+ }
2244
+ }
2245
+ });
2246
+ ```
2247
+
2248
+ ### 🅿️ End-to-end: pool pump on solar
2249
+
2250
+ The user's automation object (authored in the app) for *"when PV surplus > 2000 W, run the pool pump for at least 10 minutes, Energy Manager may choose the timing":*
2251
+
2252
+ ```typescript
2253
+ const automation = {
2254
+ id: 'pool-pump',
2255
+ name: 'Pool pump on solar',
2256
+ enabled: true,
2257
+ trigger: {type: EnyoAutomationTriggerTypeEnum.PvSurplusThreshold, thresholdW: 2000},
2258
+ actions: [{
2259
+ id: 'switch-pump',
2260
+ type: EnyoAutomationActionTypeEnum.SmartPlugSwitch,
2261
+ applianceId: 'shelly-pool-ch0',
2262
+ minDurationMinutes: 10,
2263
+ schedulingMode: EnyoAutomationSchedulingModeEnum.Flexible,
2264
+ targetKind: EnyoAutomationTargetKindEnum.Load,
2265
+ }],
2266
+ };
2267
+ ```
2268
+
2269
+ The **Energy Manager** publishes `AutomationTriggerV1` as the surplus crosses 2000 W; the **Shelly app** receives it and switches `shelly-pool-ch0`, keeping it on for ≥ 10 minutes.
2270
+
2271
+ ### ⏳ Mandatory vs Flexible (current limitation)
2272
+
2273
+ - **`Mandatory`** works fully today: the device app reacts directly to the `AutomationTriggerV1` `active` flag (on when `true`, off when `false` once the minimum runtime has elapsed).
2274
+ - **`Flexible`** lets the Energy Manager decide *whether and when* to actually run the action inside the active window. That decision does **not yet** have a dedicated Energy-Manager→device dispatch message — `AutomationTriggerV1` only reports that the *condition* holds. Until a dispatch message (planned: `AutomationActionCommandV1`, carrying `automationId`, `actionId`, `command`, and a run duration) is added, a device app treats `Flexible` like `Mandatory` and self-enforces `minDurationMinutes`.
2275
+
2276
+ ### 🔐 Permissions
2277
+
2278
+ | Capability | Permission |
2279
+ | --- | --- |
2280
+ | `list` / `getById` / `listenFor*` automations | `Automation` |
2281
+ | `registerTrigger` / `deregisterTrigger` / `publishAutomationForecast` | `EnergyManager` |
2282
+ | Emit the `AutomationTriggerV1` data-bus message | `SendDataBusValues` |
2283
+
2284
+ ### ✅ Validators
2285
+
2286
+ `validateAutomation`, `validateAutomationTriggerData`, and `validateAutomationForecast` are exported as pure functions (throwing `AutomationValidationError`, whose message names the offending field) so you can validate before persisting or publishing.
2287
+
2288
+ ```typescript
2289
+ import {validateAutomation, AutomationValidationError} from '@enyo-energy/energy-app-sdk';
2290
+
2291
+ try {
2292
+ validateAutomation(automation, knownSmartPlugApplianceIds);
2293
+ } catch (error) {
2294
+ if (error instanceof AutomationValidationError) {
2295
+ // surface error.message
2296
+ }
2297
+ }
2298
+ ```
2299
+
2085
2300
  ## Examples
2086
2301
 
2087
2302
  ### Basic Energy App
@@ -49,4 +49,5 @@ var EnergyAppPermissionTypeEnum;
49
49
  EnergyAppPermissionTypeEnum["ChildProcess"] = "ChildProcess";
50
50
  EnergyAppPermissionTypeEnum["Udp"] = "Udp";
51
51
  EnergyAppPermissionTypeEnum["ProvidedFiles"] = "ProvidedFiles";
52
+ EnergyAppPermissionTypeEnum["Automation"] = "Automation";
52
53
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -1,4 +1,4 @@
1
- export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles';
1
+ export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation';
2
2
  export declare enum EnergyAppPermissionTypeEnum {
3
3
  RestrictedInternetAccess = "RestrictedInternetAccess",
4
4
  NetworkDeviceDiscovery = "NetworkDeviceDiscovery",
@@ -45,5 +45,6 @@ export declare enum EnergyAppPermissionTypeEnum {
45
45
  Wifi = "Wifi",
46
46
  ChildProcess = "ChildProcess",
47
47
  Udp = "Udp",
48
- ProvidedFiles = "ProvidedFiles"
48
+ ProvidedFiles = "ProvidedFiles",
49
+ Automation = "Automation"
49
50
  }
@@ -34,6 +34,9 @@ class EnergyApp {
34
34
  this.energyAppSdk = energyAppSdkInstance;
35
35
  }
36
36
  }
37
+ healthcheck() {
38
+ return new Date();
39
+ }
37
40
  isSystemOnline() {
38
41
  return this.energyAppSdk.isSystemOnline();
39
42
  }
@@ -336,6 +339,19 @@ class EnergyApp {
336
339
  useFiles() {
337
340
  return this.energyAppSdk.useFiles();
338
341
  }
342
+ /**
343
+ * Gets the Automation API for reading user-configured automations and
344
+ * listening for their creation, update and removal. Apps holding the
345
+ * `EnergyManager` permission can additionally register trigger types,
346
+ * report live trigger state, and publish automation forecasts.
347
+ * @returns The Automation API instance
348
+ * @throws {EnergyAppPermissionNotGrantedError} If the required permission
349
+ * (`Automation` for reads, `EnergyManager` for provider methods) is
350
+ * not granted.
351
+ */
352
+ useAutomations() {
353
+ return this.energyAppSdk.useAutomations();
354
+ }
339
355
  /**
340
356
  * Gets the current SDK version.
341
357
  * @returns The semantic version string of the SDK
@@ -38,6 +38,7 @@ import { EnergyAppConfigurationManager } from "./packages/energy-app-configurati
38
38
  import { EnergyAppApplianceEnergyManagerForecast } from "./packages/energy-app-appliance-energy-manager-forecast.cjs";
39
39
  import { EnergyAppBattery } from "./packages/energy-app-battery.cjs";
40
40
  import { EnergyAppFile } from "./packages/energy-app-file.cjs";
41
+ import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
41
42
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
42
43
  /**
43
44
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -60,6 +61,7 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
60
61
  private readonly energyAppSdk;
61
62
  private udpInstance;
62
63
  constructor();
64
+ healthcheck(): Date;
63
65
  isSystemOnline(): boolean;
64
66
  /**
65
67
  * Registers a listener that gets called when the network status changes.
@@ -268,6 +270,17 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
268
270
  * permission is not granted.
269
271
  */
270
272
  useFiles(): EnergyAppFile;
273
+ /**
274
+ * Gets the Automation API for reading user-configured automations and
275
+ * listening for their creation, update and removal. Apps holding the
276
+ * `EnergyManager` permission can additionally register trigger types,
277
+ * report live trigger state, and publish automation forecasts.
278
+ * @returns The Automation API instance
279
+ * @throws {EnergyAppPermissionNotGrantedError} If the required permission
280
+ * (`Automation` for reads, `EnergyManager` for provider methods) is
281
+ * not granted.
282
+ */
283
+ useAutomations(): EnergyAppAutomation;
271
284
  /**
272
285
  * Gets the current SDK version.
273
286
  * @returns The semantic version string of the SDK
@@ -37,6 +37,7 @@ import { EnergyAppConfigurationManager } from "./packages/energy-app-configurati
37
37
  import { EnergyAppApplianceEnergyManagerForecast } from "./packages/energy-app-appliance-energy-manager-forecast.cjs";
38
38
  import { EnergyAppBattery } from "./packages/energy-app-battery.cjs";
39
39
  import { EnergyAppFile } from "./packages/energy-app-file.cjs";
40
+ import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
40
41
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
41
42
  export declare enum EnergyAppStateEnum {
42
43
  Launching = "launching",
@@ -54,6 +55,8 @@ export declare enum EnergyAppStateEnum {
54
55
  export interface EnyoEnergyAppSdk {
55
56
  /** Register a callback that gets called when the package is initialized */
56
57
  register: (callback: (packageName: string, version: number, channel: EnyoPackageChannel, deviceId: string) => void | Promise<void>) => void;
58
+ /** health check - returns the current date to check if alive */
59
+ healthcheck: () => Date;
57
60
  /** Register a callback that gets called when the system is shutting down */
58
61
  onShutdown: (callback: () => void | Promise<void>) => void;
59
62
  /** Update the state of the Energy App. Default state set is launching*/
@@ -140,4 +143,6 @@ export interface EnyoEnergyAppSdk {
140
143
  useBatteries: () => EnergyAppBattery;
141
144
  /** Get the File API for providing user-facing files whose translated names/explanations are shown by the host and whose content is produced on demand when the user stores them */
142
145
  useFiles: () => EnergyAppFile;
146
+ /** Get the Automation API for reading user-configured automations and (with EnergyManager permission) registering triggers, reporting trigger state, and publishing automation forecasts */
147
+ useAutomations: () => EnergyAppAutomation;
143
148
  }
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUTOMATION_DURATION_STEP_MINUTES = exports.AUTOMATION_MAX_DURATION_MINUTES = exports.AUTOMATION_MIN_DURATION_MINUTES = exports.AutomationValidationError = void 0;
4
+ exports.validateAutomation = validateAutomation;
5
+ exports.validateTrigger = validateTrigger;
6
+ exports.validateAction = validateAction;
7
+ exports.validateAutomationForecast = validateAutomationForecast;
8
+ exports.validateAutomationTriggerData = validateAutomationTriggerData;
9
+ const enyo_automation_js_1 = require("../../types/enyo-automation.cjs");
10
+ const enyo_mqtt_js_1 = require("../../types/enyo-mqtt.cjs");
11
+ /**
12
+ * Thrown when an automation, action or automation forecast violates one of the
13
+ * invariants enforced by this module. The message names the offending field /
14
+ * index so callers can surface it directly to the user.
15
+ */
16
+ class AutomationValidationError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'AutomationValidationError';
20
+ }
21
+ }
22
+ exports.AutomationValidationError = AutomationValidationError;
23
+ /** Minimum smart-plug on-duration, in minutes. */
24
+ exports.AUTOMATION_MIN_DURATION_MINUTES = 5;
25
+ /** Maximum smart-plug on-duration, in minutes (6 hours). */
26
+ exports.AUTOMATION_MAX_DURATION_MINUTES = 360;
27
+ /** Step size the smart-plug on-duration must be a multiple of, in minutes. */
28
+ exports.AUTOMATION_DURATION_STEP_MINUTES = 5;
29
+ /** Duration in seconds of each {@link EnyoForecastResolution} value. */
30
+ const FORECAST_RESOLUTION_SECONDS = {
31
+ '10s': 10,
32
+ '1m': 60,
33
+ '5m': 300,
34
+ '15m': 900,
35
+ '1h': 3600,
36
+ };
37
+ /**
38
+ * Validates a complete {@link EnyoAutomation}. Throws on the first violation.
39
+ *
40
+ * When `knownSmartPlugApplianceIds` is provided, every smart-plug action's
41
+ * `applianceId` must be a member of it — use this to ensure the action targets
42
+ * an appliance that actually supports {@link EnyoAutomationActionTypeEnum.SmartPlugSwitch}.
43
+ *
44
+ * @param automation - The automation to validate.
45
+ * @param knownSmartPlugApplianceIds - Optional set/array of appliance ids that
46
+ * support the smart-plug-switch action; when given, smart-plug action targets
47
+ * are checked against it.
48
+ * @throws {AutomationValidationError} On the first invariant violation.
49
+ */
50
+ function validateAutomation(automation, knownSmartPlugApplianceIds) {
51
+ if (!automation || typeof automation !== 'object') {
52
+ throw new AutomationValidationError('Automation must be an object.');
53
+ }
54
+ requireNonEmptyString(automation.id, 'Automation.id');
55
+ requireNonEmptyString(automation.name, 'Automation.name');
56
+ if (typeof automation.enabled !== 'boolean') {
57
+ throw new AutomationValidationError('Automation.enabled must be a boolean.');
58
+ }
59
+ validateTrigger(automation.trigger);
60
+ if (!Array.isArray(automation.actions) || automation.actions.length === 0) {
61
+ throw new AutomationValidationError('Automation.actions must contain at least one action.');
62
+ }
63
+ const knownIds = knownSmartPlugApplianceIds ? new Set(knownSmartPlugApplianceIds) : undefined;
64
+ const seenActionIds = new Set();
65
+ automation.actions.forEach((action, index) => {
66
+ requireNonEmptyString(action?.id, `Automation.actions[${index}].id`);
67
+ if (seenActionIds.has(action.id)) {
68
+ throw new AutomationValidationError(`Automation.actions[${index}].id is not unique: ${action.id}.`);
69
+ }
70
+ seenActionIds.add(action.id);
71
+ validateAction(action, index, knownIds);
72
+ });
73
+ }
74
+ /**
75
+ * Validates the trigger of an automation. Throws on the first violation.
76
+ *
77
+ * @param trigger - The trigger configuration.
78
+ * @throws {AutomationValidationError} On the first invariant violation.
79
+ */
80
+ function validateTrigger(trigger) {
81
+ if (!trigger || typeof trigger !== 'object') {
82
+ throw new AutomationValidationError('Automation.trigger must be an object.');
83
+ }
84
+ switch (trigger.type) {
85
+ case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.PvSurplusThreshold:
86
+ if (typeof trigger.thresholdW !== 'number' || !Number.isFinite(trigger.thresholdW) || trigger.thresholdW < 0) {
87
+ throw new AutomationValidationError('PvSurplusThreshold trigger.thresholdW must be a finite number >= 0.');
88
+ }
89
+ break;
90
+ default:
91
+ throw new AutomationValidationError(`Automation.trigger.type is invalid: ${trigger.type}.`);
92
+ }
93
+ }
94
+ /**
95
+ * Validates a single automation action. Throws on the first violation.
96
+ *
97
+ * @param action - The action to validate.
98
+ * @param index - Index of the action within its automation (for error messages).
99
+ * @param knownSmartPlugApplianceIds - Optional set of appliance ids that support
100
+ * the smart-plug-switch action.
101
+ * @throws {AutomationValidationError} On the first invariant violation.
102
+ */
103
+ function validateAction(action, index = 0, knownSmartPlugApplianceIds) {
104
+ const label = `Automation.actions[${index}]`;
105
+ if (!action || typeof action !== 'object') {
106
+ throw new AutomationValidationError(`${label} must be an object.`);
107
+ }
108
+ requireEnumMember(action.schedulingMode, enyo_automation_js_1.EnyoAutomationSchedulingModeEnum, `${label}.schedulingMode`);
109
+ requireEnumMember(action.targetKind, enyo_automation_js_1.EnyoAutomationTargetKindEnum, `${label}.targetKind`);
110
+ switch (action.type) {
111
+ case enyo_automation_js_1.EnyoAutomationActionTypeEnum.Mqtt:
112
+ validateMqttAction(action, label);
113
+ break;
114
+ case enyo_automation_js_1.EnyoAutomationActionTypeEnum.SmartPlugSwitch:
115
+ validateSmartPlugSwitchAction(action, label, knownSmartPlugApplianceIds);
116
+ break;
117
+ default:
118
+ throw new AutomationValidationError(`${label}.type is invalid: ${action.type}.`);
119
+ }
120
+ }
121
+ /**
122
+ * Validates an {@link EnyoAutomationForecast}: resolution must be known and the
123
+ * entries must be chronologically ordered, non-overlapping, and spaced exactly
124
+ * one resolution step apart. Throws on the first violation.
125
+ *
126
+ * @param forecast - The forecast to validate.
127
+ * @throws {AutomationValidationError} On the first invariant violation.
128
+ */
129
+ function validateAutomationForecast(forecast) {
130
+ if (!forecast || typeof forecast !== 'object') {
131
+ throw new AutomationValidationError('AutomationForecast must be an object.');
132
+ }
133
+ requireNonEmptyString(forecast.automationId, 'AutomationForecast.automationId');
134
+ const stepSeconds = FORECAST_RESOLUTION_SECONDS[forecast.resolution];
135
+ if (stepSeconds === undefined) {
136
+ throw new AutomationValidationError(`AutomationForecast.resolution is invalid: ${forecast.resolution}.`);
137
+ }
138
+ if (!Array.isArray(forecast.entries)) {
139
+ throw new AutomationValidationError('AutomationForecast.entries must be an array.');
140
+ }
141
+ let previousMs;
142
+ forecast.entries.forEach((entry, index) => {
143
+ const entryLabel = `AutomationForecast.entries[${index}]`;
144
+ if (!entry || typeof entry !== 'object') {
145
+ throw new AutomationValidationError(`${entryLabel} must be an object.`);
146
+ }
147
+ if (typeof entry.active !== 'boolean') {
148
+ throw new AutomationValidationError(`${entryLabel}.active must be a boolean.`);
149
+ }
150
+ const currentMs = Date.parse(entry.timestampIso);
151
+ if (Number.isNaN(currentMs)) {
152
+ throw new AutomationValidationError(`${entryLabel}.timestampIso is not a valid ISO 8601 timestamp: ${entry.timestampIso}.`);
153
+ }
154
+ if (previousMs !== undefined) {
155
+ const deltaSeconds = (currentMs - previousMs) / 1000;
156
+ if (deltaSeconds !== stepSeconds) {
157
+ throw new AutomationValidationError(`${entryLabel} is spaced ${deltaSeconds}s from the previous entry; expected ${stepSeconds}s for resolution ${forecast.resolution}.`);
158
+ }
159
+ }
160
+ previousMs = currentMs;
161
+ });
162
+ }
163
+ /**
164
+ * Validates the trigger-type-specific metadata carried by the
165
+ * `AutomationTriggerV1` data-bus message. Throws on the first violation.
166
+ *
167
+ * @param trigger - The trigger metadata payload.
168
+ * @throws {AutomationValidationError} On the first invariant violation.
169
+ */
170
+ function validateAutomationTriggerData(trigger) {
171
+ if (!trigger || typeof trigger !== 'object') {
172
+ throw new AutomationValidationError('AutomationTriggerData must be an object.');
173
+ }
174
+ switch (trigger.triggerType) {
175
+ case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.PvSurplusThreshold:
176
+ requireFiniteNonNegative(trigger.surplusW, 'AutomationTriggerData.surplusW');
177
+ requireFiniteNonNegative(trigger.thresholdW, 'AutomationTriggerData.thresholdW');
178
+ break;
179
+ default:
180
+ throw new AutomationValidationError(`AutomationTriggerData.triggerType is invalid: ${trigger.triggerType}.`);
181
+ }
182
+ }
183
+ function validateMqttAction(action, label) {
184
+ requireNonEmptyString(action.topic, `${label}.topic`);
185
+ if (typeof action.updateChargingPvSurplus !== 'boolean') {
186
+ throw new AutomationValidationError(`${label}.updateChargingPvSurplus must be a boolean.`);
187
+ }
188
+ if (typeof action.payloadTemplate !== 'string' || action.payloadTemplate.length === 0) {
189
+ throw new AutomationValidationError(`${label}.payloadTemplate must be a non-empty string.`);
190
+ }
191
+ validatePayloadTemplate(action.payloadTemplate, `${label}.payloadTemplate`);
192
+ const { qos } = action.publishOptions ?? {};
193
+ if (qos !== undefined && qos !== enyo_mqtt_js_1.MqttQos.AtMostOnce && qos !== enyo_mqtt_js_1.MqttQos.AtLeastOnce && qos !== enyo_mqtt_js_1.MqttQos.ExactlyOnce) {
194
+ throw new AutomationValidationError(`${label}.publishOptions.qos must be one of 0, 1, 2.`);
195
+ }
196
+ if (action.publishOptions?.retain !== undefined && typeof action.publishOptions.retain !== 'boolean') {
197
+ throw new AutomationValidationError(`${label}.publishOptions.retain must be a boolean.`);
198
+ }
199
+ }
200
+ function validateSmartPlugSwitchAction(action, label, knownSmartPlugApplianceIds) {
201
+ requireNonEmptyString(action.applianceId, `${label}.applianceId`);
202
+ if (knownSmartPlugApplianceIds && !knownSmartPlugApplianceIds.has(action.applianceId)) {
203
+ throw new AutomationValidationError(`${label}.applianceId does not reference a smart-plug appliance that supports the smart-plug-switch action: ${action.applianceId}.`);
204
+ }
205
+ const { minDurationMinutes } = action;
206
+ if (typeof minDurationMinutes !== 'number' ||
207
+ !Number.isInteger(minDurationMinutes) ||
208
+ minDurationMinutes < exports.AUTOMATION_MIN_DURATION_MINUTES ||
209
+ minDurationMinutes > exports.AUTOMATION_MAX_DURATION_MINUTES ||
210
+ minDurationMinutes % exports.AUTOMATION_DURATION_STEP_MINUTES !== 0) {
211
+ throw new AutomationValidationError(`${label}.minDurationMinutes must be an integer between ${exports.AUTOMATION_MIN_DURATION_MINUTES} and ${exports.AUTOMATION_MAX_DURATION_MINUTES} in steps of ${exports.AUTOMATION_DURATION_STEP_MINUTES}.`);
212
+ }
213
+ }
214
+ /**
215
+ * Ensures a payload template only references known placeholders and is
216
+ * structurally valid JSON once its placeholders are neutralised.
217
+ */
218
+ function validatePayloadTemplate(template, label) {
219
+ const knownPlaceholders = new Set(Object.values(enyo_automation_js_1.EnyoAutomationMqttPlaceholderEnum));
220
+ const tokens = template.match(/{{[^}]*}}/g) ?? [];
221
+ for (const token of tokens) {
222
+ if (!knownPlaceholders.has(token)) {
223
+ throw new AutomationValidationError(`${label} references an unknown placeholder: ${token}. Allowed: ${Object.values(enyo_automation_js_1.EnyoAutomationMqttPlaceholderEnum).join(', ')}.`);
224
+ }
225
+ }
226
+ // Replace every placeholder with a neutral JSON scalar so the surrounding
227
+ // structure can be validated regardless of the placeholder's runtime value.
228
+ const neutralised = template.replace(/{{[^}]*}}/g, '0');
229
+ try {
230
+ JSON.parse(neutralised);
231
+ }
232
+ catch {
233
+ throw new AutomationValidationError(`${label} is not valid JSON once placeholders are substituted.`);
234
+ }
235
+ }
236
+ function requireFiniteNonNegative(value, label) {
237
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
238
+ throw new AutomationValidationError(`${label} must be a finite number >= 0.`);
239
+ }
240
+ }
241
+ function requireNonEmptyString(value, label) {
242
+ if (typeof value !== 'string' || value.length === 0) {
243
+ throw new AutomationValidationError(`${label} must be a non-empty string.`);
244
+ }
245
+ }
246
+ function requireEnumMember(value, enumObject, label) {
247
+ const allowed = new Set(Object.values(enumObject));
248
+ if (typeof value !== 'string' && typeof value !== 'number') {
249
+ throw new AutomationValidationError(`${label} is required.`);
250
+ }
251
+ if (!allowed.has(value)) {
252
+ throw new AutomationValidationError(`${label} is invalid: ${value}. Allowed values: ${Object.values(enumObject).join(', ')}.`);
253
+ }
254
+ }
@@ -0,0 +1,63 @@
1
+ import { EnyoAutomation, EnyoAutomationAction, EnyoAutomationForecast, EnyoAutomationTriggerData } from '../../types/enyo-automation.cjs';
2
+ /**
3
+ * Thrown when an automation, action or automation forecast violates one of the
4
+ * invariants enforced by this module. The message names the offending field /
5
+ * index so callers can surface it directly to the user.
6
+ */
7
+ export declare class AutomationValidationError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ /** Minimum smart-plug on-duration, in minutes. */
11
+ export declare const AUTOMATION_MIN_DURATION_MINUTES = 5;
12
+ /** Maximum smart-plug on-duration, in minutes (6 hours). */
13
+ export declare const AUTOMATION_MAX_DURATION_MINUTES = 360;
14
+ /** Step size the smart-plug on-duration must be a multiple of, in minutes. */
15
+ export declare const AUTOMATION_DURATION_STEP_MINUTES = 5;
16
+ /**
17
+ * Validates a complete {@link EnyoAutomation}. Throws on the first violation.
18
+ *
19
+ * When `knownSmartPlugApplianceIds` is provided, every smart-plug action's
20
+ * `applianceId` must be a member of it — use this to ensure the action targets
21
+ * an appliance that actually supports {@link EnyoAutomationActionTypeEnum.SmartPlugSwitch}.
22
+ *
23
+ * @param automation - The automation to validate.
24
+ * @param knownSmartPlugApplianceIds - Optional set/array of appliance ids that
25
+ * support the smart-plug-switch action; when given, smart-plug action targets
26
+ * are checked against it.
27
+ * @throws {AutomationValidationError} On the first invariant violation.
28
+ */
29
+ export declare function validateAutomation(automation: EnyoAutomation, knownSmartPlugApplianceIds?: Iterable<string>): void;
30
+ /**
31
+ * Validates the trigger of an automation. Throws on the first violation.
32
+ *
33
+ * @param trigger - The trigger configuration.
34
+ * @throws {AutomationValidationError} On the first invariant violation.
35
+ */
36
+ export declare function validateTrigger(trigger: EnyoAutomation['trigger']): void;
37
+ /**
38
+ * Validates a single automation action. Throws on the first violation.
39
+ *
40
+ * @param action - The action to validate.
41
+ * @param index - Index of the action within its automation (for error messages).
42
+ * @param knownSmartPlugApplianceIds - Optional set of appliance ids that support
43
+ * the smart-plug-switch action.
44
+ * @throws {AutomationValidationError} On the first invariant violation.
45
+ */
46
+ export declare function validateAction(action: EnyoAutomationAction, index?: number, knownSmartPlugApplianceIds?: Set<string>): void;
47
+ /**
48
+ * Validates an {@link EnyoAutomationForecast}: resolution must be known and the
49
+ * entries must be chronologically ordered, non-overlapping, and spaced exactly
50
+ * one resolution step apart. Throws on the first violation.
51
+ *
52
+ * @param forecast - The forecast to validate.
53
+ * @throws {AutomationValidationError} On the first invariant violation.
54
+ */
55
+ export declare function validateAutomationForecast(forecast: EnyoAutomationForecast): void;
56
+ /**
57
+ * Validates the trigger-type-specific metadata carried by the
58
+ * `AutomationTriggerV1` data-bus message. Throws on the first violation.
59
+ *
60
+ * @param trigger - The trigger metadata payload.
61
+ * @throws {AutomationValidationError} On the first invariant violation.
62
+ */
63
+ export declare function validateAutomationTriggerData(trigger: EnyoAutomationTriggerData): void;