@enyo-energy/energy-app-sdk 0.0.146 → 0.0.147

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 (38) hide show
  1. package/README.md +184 -0
  2. package/dist/cjs/energy-app.cjs +9 -0
  3. package/dist/cjs/energy-app.d.cts +8 -0
  4. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  5. package/dist/cjs/implementations/appliance-command-forecast/appliance-command-forecast-validators.cjs +228 -0
  6. package/dist/cjs/implementations/appliance-command-forecast/appliance-command-forecast-validators.d.cts +67 -0
  7. package/dist/cjs/implementations/energy-manager-diagnostics/energy-manager-diagnostics-validators.cjs +169 -0
  8. package/dist/cjs/implementations/energy-manager-diagnostics/energy-manager-diagnostics-validators.d.cts +26 -0
  9. package/dist/cjs/index.cjs +4 -0
  10. package/dist/cjs/index.d.cts +4 -0
  11. package/dist/cjs/packages/energy-app-appliance-energy-manager-forecast.cjs +2 -0
  12. package/dist/cjs/packages/energy-app-appliance-energy-manager-forecast.d.cts +88 -0
  13. package/dist/cjs/packages/energy-app-diagnostics.d.cts +55 -13
  14. package/dist/cjs/types/enyo-appliance-command-forecast.cjs +89 -0
  15. package/dist/cjs/types/enyo-appliance-command-forecast.d.cts +307 -0
  16. package/dist/cjs/types/enyo-diagnostics.cjs +86 -1
  17. package/dist/cjs/types/enyo-diagnostics.d.cts +203 -1
  18. package/dist/cjs/version.cjs +1 -1
  19. package/dist/cjs/version.d.cts +1 -1
  20. package/dist/energy-app.d.ts +8 -0
  21. package/dist/energy-app.js +9 -0
  22. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  23. package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.d.ts +67 -0
  24. package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.js +217 -0
  25. package/dist/implementations/energy-manager-diagnostics/energy-manager-diagnostics-validators.d.ts +26 -0
  26. package/dist/implementations/energy-manager-diagnostics/energy-manager-diagnostics-validators.js +164 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +4 -0
  29. package/dist/packages/energy-app-appliance-energy-manager-forecast.d.ts +88 -0
  30. package/dist/packages/energy-app-appliance-energy-manager-forecast.js +1 -0
  31. package/dist/packages/energy-app-diagnostics.d.ts +55 -13
  32. package/dist/types/enyo-appliance-command-forecast.d.ts +307 -0
  33. package/dist/types/enyo-appliance-command-forecast.js +86 -0
  34. package/dist/types/enyo-diagnostics.d.ts +203 -1
  35. package/dist/types/enyo-diagnostics.js +85 -0
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -49,6 +49,12 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
49
49
  - [EvChargingForecast](#evchargingforecast)
50
50
  - [HeatpumpConsumptionForecast](#heatpumpconsumptionforecast)
51
51
  - [HeatpumpDhwTemperatureForecast](#heatpumpdhwtemperatureforecast)
52
+ - [Appliance Energy-Manager Forecast](#appliance-energy-manager-forecast)
53
+ - [`useApplianceEnergyManagerForecast()`](#useapplianceenergymanagerforecast-energyappapplianceenergymanagerforecast)
54
+ - [ChargerForecast](#chargerforecast)
55
+ - [BatteryCommandForecast](#batterycommandforecast)
56
+ - [HeatpumpForecast](#heatpumpforecast)
57
+ - [Validators](#validators)
52
58
  - [Examples](#examples)
53
59
  - [Basic Energy App](#basic-energy-app)
54
60
  - [Device Integration](#device-integration)
@@ -130,6 +136,7 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
130
136
  | Forecast EV charging demand | [`EvChargingForecast`](#evchargingforecast) |
131
137
  | Forecast heatpump electrical consumption | [`HeatpumpConsumptionForecast`](#heatpumpconsumptionforecast) |
132
138
  | Forecast heatpump DHW tank temperature | [`HeatpumpDhwTemperatureForecast`](#heatpumpdhwtemperatureforecast) |
139
+ | Announce a charger / battery / heatpump command plan you **intend to apply** | [`useApplianceEnergyManagerForecast()`](#useapplianceenergymanagerforecast-energyappapplianceenergymanagerforecast) |
133
140
  | Talk to an EEBUS / SHIP / SPINE device | [`useEebus()`](#useeebus-energyappeebus) |
134
141
  | Speak MQTT (SDK broker or external) | [`useMqtt()`](#usemqtt-energyappmqtt) |
135
142
  | Scan or talk to Bluetooth LE peripherals | [`useBluetooth()`](#usebluetooth-energyappbluetooth) |
@@ -1898,6 +1905,183 @@ energyApp.onShutdown(async () => {
1898
1905
 
1899
1906
  > **Tip:** if your app needs more than one forecaster, prefer [`EnergyManagerEnergyApp`](#energymanagerenergyapp) — it manages construction, caching, and disposal for you.
1900
1907
 
1908
+ ## Appliance Energy-Manager Forecast
1909
+
1910
+ The [Forecasting](#forecasting) module above predicts what an appliance will **do** based on history. The Appliance Energy-Manager Forecast package goes the other way: it lets an energy-manager app declare what it **intends to command** each appliance to do over the upcoming horizon, plus the temperature trajectories its commands are expected to produce. Three appliance families are supported today — chargers, batteries, and heatpumps — and the heatpump payload can carry any combination of DHW boost, room pre-heating, buffer-tank boost, and a relative power-announcement schedule in one call.
1911
+
1912
+ How the runtime fans these forecasts out to subscribers (data bus, RPC, …) is an internal implementation detail of the SDK runtime — apps just call `publish*` and the SDK takes care of the rest.
1913
+
1914
+ **Required permission:** `EnergyManager`.
1915
+
1916
+ ### `useApplianceEnergyManagerForecast(): EnergyAppApplianceEnergyManagerForecast`
1917
+
1918
+ ```typescript
1919
+ const forecasts = energyApp.useApplianceEnergyManagerForecast();
1920
+ ```
1921
+
1922
+ | Method | Purpose |
1923
+ |---|---|
1924
+ | `publishChargerForecast(applianceId, forecast: ChargerForecast)` | Publish the planned phase / power schedule for a charger. |
1925
+ | `publishBatteryForecast(applianceId, forecast: BatteryCommandForecast)` | Publish the planned charge / discharge / auto cadence for a battery. |
1926
+ | `publishHeatpumpForecast(applianceId, forecast: HeatpumpForecast)` | Publish any combination of DHW boost / room pre-heating / buffer-tank boost / power-announcement schedule for a heatpump. |
1927
+
1928
+ Every call validates the payload first and rejects with `ApplianceCommandForecastValidationError` if any invariant is broken — `publish*` never goes through the runtime with malformed data.
1929
+
1930
+ Every forecast also accepts shared optional metadata via [`ApplianceForecastMetadata`](#validators):
1931
+
1932
+ - `generatedAtIso?: string` — ISO 8601 generation timestamp. Stamped by the runtime when omitted.
1933
+ - `reason?: string` — free-form note (e.g. `"follow PV peak"`, `"§14a DR event"`).
1934
+ - `estimatedSavings?: ApplianceForecastEstimatedSavings` — see below.
1935
+
1936
+ ```typescript
1937
+ interface ApplianceForecastEstimatedSavings {
1938
+ costSavings: number; // positive = savings, negative = extra cost (in `currency`)
1939
+ currency: string; // ISO 4217 code
1940
+ co2SavingsGrams?: number;
1941
+ selfConsumptionGainWh?: number;
1942
+ note?: string; // e.g. "vs. flat-tariff baseline"
1943
+ }
1944
+ ```
1945
+
1946
+ ### ChargerForecast
1947
+
1948
+ Relative phase / power schedule that mirrors an OCPP TxProfile but adds explicit `numberOfPhases` (1 / 2 / 3).
1949
+
1950
+ ```typescript
1951
+ import { ChargerForecast } from '@enyo-energy/energy-app-sdk';
1952
+
1953
+ const forecast: ChargerForecast = {
1954
+ relativeSchedule: [
1955
+ // Right now: 11 kW across three phases
1956
+ { seconds: 0, powerW: 11_000, numberOfPhases: 3 },
1957
+ // In 30 minutes: derate to 3.7 kW on one phase
1958
+ { seconds: 1800, powerW: 3_700, numberOfPhases: 1 },
1959
+ // In one hour: pause
1960
+ { seconds: 3600, powerW: 0 },
1961
+ ],
1962
+ estimatedSavings: { costSavings: 0.42, currency: 'EUR', co2SavingsGrams: 120 },
1963
+ reason: 'follow PV peak',
1964
+ };
1965
+
1966
+ await forecasts.publishChargerForecast('charger-1', forecast);
1967
+ ```
1968
+
1969
+ Per-entry invariants:
1970
+
1971
+ - `seconds`: finite, non-negative; first entry `= 0`; subsequent entries strictly increasing.
1972
+ - `powerW`: finite, non-negative (`0` means "pause").
1973
+ - `numberOfPhases`: optional; if set, must be `1`, `2`, or `3`.
1974
+
1975
+ ### BatteryCommandForecast
1976
+
1977
+ Relative `{seconds, mode, powerW}` schedule where `mode` is one of `'charge'`, `'discharge'`, or `'auto'`. `auto` returns control to the appliance and must always carry `powerW = 0`.
1978
+
1979
+ The type is named `BatteryCommandForecast` to make the distinction with the existing [`BatteryForecast`](#batteryforecast) class (which forecasts state-of-charge from history) explicit.
1980
+
1981
+ ```typescript
1982
+ import {
1983
+ BatteryCommandForecast,
1984
+ BatteryCommandForecastModeEnum,
1985
+ } from '@enyo-energy/energy-app-sdk';
1986
+
1987
+ const forecast: BatteryCommandForecast = {
1988
+ relativeSchedule: [
1989
+ { seconds: 0, mode: BatteryCommandForecastModeEnum.Charge, powerW: 3000 },
1990
+ { seconds: 1800, mode: BatteryCommandForecastModeEnum.Discharge, powerW: 2500 },
1991
+ { seconds: 3600, mode: BatteryCommandForecastModeEnum.Auto, powerW: 0 },
1992
+ ],
1993
+ estimatedSavings: { costSavings: 0.18, currency: 'EUR' },
1994
+ };
1995
+
1996
+ await forecasts.publishBatteryForecast('battery-1', forecast);
1997
+ ```
1998
+
1999
+ Per-entry invariants:
2000
+
2001
+ - `seconds`: finite, non-negative; first entry `= 0`; subsequent entries strictly increasing.
2002
+ - `mode`: one of `charge` / `discharge` / `auto`.
2003
+ - `powerW`: finite, non-negative. **MUST be `0` when `mode === 'auto'`.**
2004
+
2005
+ ### HeatpumpForecast
2006
+
2007
+ The heatpump payload can carry any combination of the four supported command families in one call — at least one must be present and non-empty. Each command family also accepts its own forecasted temperature trajectory so subscribers can reason about the plan and its expected outcome together.
2008
+
2009
+ ```typescript
2010
+ import { HeatpumpForecast } from '@enyo-energy/energy-app-sdk';
2011
+
2012
+ const forecast: HeatpumpForecast = {
2013
+ // ----- DHW boost -----
2014
+ dhwBoosts: [
2015
+ { startIso: '2026-06-10T13:00:00.000Z', endIso: '2026-06-10T15:00:00.000Z', targetTemperatureC: 60 },
2016
+ ],
2017
+ dhwTemperatureForecast: [
2018
+ { timestampIso: '2026-06-10T12:00:00.000Z', temperatureC: 48 },
2019
+ { timestampIso: '2026-06-10T13:00:00.000Z', temperatureC: 52 },
2020
+ { timestampIso: '2026-06-10T15:00:00.000Z', temperatureC: 60 },
2021
+ ],
2022
+
2023
+ // ----- Room pre-heating (per heating circuit) -----
2024
+ roomPreHeatings: [
2025
+ { startIso: '2026-06-10T05:00:00.000Z', endIso: '2026-06-10T07:00:00.000Z', targetTemperatureC: 22, circuitIndex: 0 },
2026
+ ],
2027
+ roomTemperatureForecast: [
2028
+ { timestampIso: '2026-06-10T05:00:00.000Z', temperatureC: 19 },
2029
+ { timestampIso: '2026-06-10T07:00:00.000Z', temperatureC: 22 },
2030
+ ],
2031
+
2032
+ // ----- Buffer-tank boost -----
2033
+ bufferTankBoosts: [
2034
+ { startIso: '2026-06-10T13:00:00.000Z', endIso: '2026-06-10T14:00:00.000Z', targetTemperatureC: 55 },
2035
+ ],
2036
+ bufferTankTemperatureForecast: [
2037
+ { timestampIso: '2026-06-10T13:00:00.000Z', temperatureC: 45 },
2038
+ { timestampIso: '2026-06-10T14:00:00.000Z', temperatureC: 55 },
2039
+ ],
2040
+
2041
+ // ----- Power-announcement schedule (relative) -----
2042
+ powerAnnouncementSchedule: [
2043
+ { seconds: 0, powerW: 1500 },
2044
+ { seconds: 1800, powerW: 3000 },
2045
+ { seconds: 3600, powerW: 0 },
2046
+ ],
2047
+
2048
+ estimatedSavings: { costSavings: 1.05, currency: 'EUR', co2SavingsGrams: 320 },
2049
+ reason: 'soak PV during 13–15h window',
2050
+ };
2051
+
2052
+ await forecasts.publishHeatpumpForecast('heatpump-1', forecast);
2053
+ ```
2054
+
2055
+ Per-family invariants:
2056
+
2057
+ - **`dhwBoosts` / `bufferTankBoosts`** — each window must satisfy `startIso < endIso`, sorted ascending and non-overlapping, `targetTemperatureC ∈ [0, 100]`.
2058
+ - **`roomPreHeatings`** — same shape as the boost windows but `targetTemperatureC ∈ [0, 40]`. Non-overlap is enforced **per `circuitIndex`** so different heating circuits can pre-heat in parallel.
2059
+ - **`powerAnnouncementSchedule`** — relative schedule (seconds-since-effective), first entry at `seconds = 0`, strictly increasing thereafter; per-entry `powerW` finite and non-negative.
2060
+ - **Temperature trajectories** — strictly increasing `timestampIso`; `temperatureC ∈ [−50, 150]`.
2061
+
2062
+ ### Validators
2063
+
2064
+ The validators that `publish*` runs internally are exported as standalone pure functions so apps can validate forecasts while building them — for instance, to surface user-facing errors in a planning UI before holding the forecast in state.
2065
+
2066
+ ```typescript
2067
+ import {
2068
+ validateChargerForecast,
2069
+ validateBatteryCommandForecast,
2070
+ validateHeatpumpForecast,
2071
+ ApplianceCommandForecastValidationError,
2072
+ } from '@enyo-energy/energy-app-sdk';
2073
+
2074
+ try {
2075
+ validateHeatpumpForecast(forecast);
2076
+ } catch (error) {
2077
+ if (error instanceof ApplianceCommandForecastValidationError) {
2078
+ // surface error.message — it names the offending field / index
2079
+ }
2080
+ }
2081
+ ```
2082
+
2083
+ Granular helpers are exported alongside the top-level validators: `validateChargerSchedule`, `validateBatterySchedule`, `validateDhwBoostWindows`, `validateRoomPreHeatingWindows`, `validateBufferTankBoostWindows`, `validatePowerAnnouncementSchedule`, `validateTemperatureForecast`.
2084
+
1901
2085
  ## Examples
1902
2086
 
1903
2087
  ### Basic Energy App
@@ -298,6 +298,15 @@ class EnergyApp {
298
298
  useConfigurationManager() {
299
299
  return this.energyAppSdk.useConfigurationManager();
300
300
  }
301
+ /**
302
+ * Gets the Appliance Energy-Manager Forecast API for publishing
303
+ * forecasted command plans per appliance (charger, battery,
304
+ * heatpump). The publisher must hold the `EnergyManager` permission.
305
+ * @returns The Appliance Energy-Manager Forecast API instance
306
+ */
307
+ useApplianceEnergyManagerForecast() {
308
+ return this.energyAppSdk.useApplianceEnergyManagerForecast();
309
+ }
301
310
  /**
302
311
  * Gets the current SDK version.
303
312
  * @returns The semantic version string of the SDK
@@ -35,6 +35,7 @@ import { EnergyAppWifi } from "./packages/energy-app-wifi.cjs";
35
35
  import { EnergyAppUdp } from "./packages/energy-app-udp.cjs";
36
36
  import { EnergyAppGridConnectionPoint } from "./packages/energy-app-grid-connection-point.cjs";
37
37
  import { EnergyAppConfigurationManager } from "./packages/energy-app-configuration-manager.cjs";
38
+ import { EnergyAppApplianceEnergyManagerForecast } from "./packages/energy-app-appliance-energy-manager-forecast.cjs";
38
39
  /**
39
40
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
40
41
  * to the runtime-provided `energyAppSdkInstance` global.
@@ -232,6 +233,13 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
232
233
  * @returns The Configuration Manager API instance
233
234
  */
234
235
  useConfigurationManager(): EnergyAppConfigurationManager;
236
+ /**
237
+ * Gets the Appliance Energy-Manager Forecast API for publishing
238
+ * forecasted command plans per appliance (charger, battery,
239
+ * heatpump). The publisher must hold the `EnergyManager` permission.
240
+ * @returns The Appliance Energy-Manager Forecast API instance
241
+ */
242
+ useApplianceEnergyManagerForecast(): EnergyAppApplianceEnergyManagerForecast;
235
243
  /**
236
244
  * Gets the current SDK version.
237
245
  * @returns The semantic version string of the SDK
@@ -34,6 +34,7 @@ import { EnergyAppWifi } from "./packages/energy-app-wifi.cjs";
34
34
  import { EnergyAppUdp } from "./packages/energy-app-udp.cjs";
35
35
  import { EnergyAppGridConnectionPoint } from "./packages/energy-app-grid-connection-point.cjs";
36
36
  import { EnergyAppConfigurationManager } from "./packages/energy-app-configuration-manager.cjs";
37
+ import { EnergyAppApplianceEnergyManagerForecast } from "./packages/energy-app-appliance-energy-manager-forecast.cjs";
37
38
  export declare enum EnergyAppStateEnum {
38
39
  Launching = "launching",
39
40
  Running = "running",
@@ -130,4 +131,6 @@ export interface EnyoEnergyAppSdk {
130
131
  useGridConnectionPoint: () => EnergyAppGridConnectionPoint;
131
132
  /** Get the Configuration Manager API for registering internal (non user-facing) package configurations with change notifications */
132
133
  useConfigurationManager: () => EnergyAppConfigurationManager;
134
+ /** Get the Appliance Energy-Manager Forecast API for publishing forecasted command plans per appliance (charger, battery, heatpump) */
135
+ useApplianceEnergyManagerForecast: () => EnergyAppApplianceEnergyManagerForecast;
133
136
  }
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApplianceCommandForecastValidationError = void 0;
4
+ exports.validateChargerForecast = validateChargerForecast;
5
+ exports.validateBatteryCommandForecast = validateBatteryCommandForecast;
6
+ exports.validateHeatpumpForecast = validateHeatpumpForecast;
7
+ exports.validateChargerSchedule = validateChargerSchedule;
8
+ exports.validateBatterySchedule = validateBatterySchedule;
9
+ exports.validateHeatpumpSchedule = validateHeatpumpSchedule;
10
+ exports.validateHeatpumpScheduleEntry = validateHeatpumpScheduleEntry;
11
+ const enyo_appliance_command_forecast_js_1 = require("../../types/enyo-appliance-command-forecast.cjs");
12
+ /**
13
+ * Thrown when a forecast payload passed to one of the validators (or to
14
+ * {@link EnergyAppApplianceEnergyManagerForecast.publishChargerForecast}
15
+ * and friends) violates the invariants declared on its data interface.
16
+ *
17
+ * The message names the offending field / index so callers can surface it
18
+ * directly to the user.
19
+ */
20
+ class ApplianceCommandForecastValidationError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = 'ApplianceCommandForecastValidationError';
24
+ }
25
+ }
26
+ exports.ApplianceCommandForecastValidationError = ApplianceCommandForecastValidationError;
27
+ const TEMPERATURE_TRAJECTORY_MIN_C = -50;
28
+ const TEMPERATURE_TRAJECTORY_MAX_C = 150;
29
+ const RESOLUTION_SECONDS = {
30
+ [enyo_appliance_command_forecast_js_1.ApplianceForecastResolutionEnum.OneMinute]: 60,
31
+ [enyo_appliance_command_forecast_js_1.ApplianceForecastResolutionEnum.FifteenMinutes]: 900,
32
+ };
33
+ /**
34
+ * Validates a {@link ChargerForecast}. Throws on the first violation —
35
+ * the error message names the offending field / index.
36
+ */
37
+ function validateChargerForecast(forecast) {
38
+ if (!forecast || typeof forecast !== 'object') {
39
+ throw new ApplianceCommandForecastValidationError('ChargerForecast must be an object.');
40
+ }
41
+ validateMetadata(forecast);
42
+ validateChargerSchedule(forecast.relativeSchedule, forecast.resolution);
43
+ }
44
+ /**
45
+ * Validates a {@link BatteryCommandForecast}. Throws on the first
46
+ * violation — the error message names the offending field / index.
47
+ *
48
+ * The forecast is a discriminated union on
49
+ * {@link BatteryCommandForecastModeEnum}: when `mode = 'auto'` no
50
+ * schedule is expected; when `mode = 'schedule'` the embedded
51
+ * {@link BatteryCommandForecastScheduled.relativeSchedule} is validated
52
+ * by {@link validateBatterySchedule}.
53
+ */
54
+ function validateBatteryCommandForecast(forecast) {
55
+ if (!forecast || typeof forecast !== 'object') {
56
+ throw new ApplianceCommandForecastValidationError('BatteryCommandForecast must be an object.');
57
+ }
58
+ validateMetadata(forecast);
59
+ const allowedModes = new Set(Object.values(enyo_appliance_command_forecast_js_1.BatteryCommandForecastModeEnum));
60
+ if (!allowedModes.has(forecast.mode)) {
61
+ throw new ApplianceCommandForecastValidationError(`BatteryCommandForecast.mode is invalid: ${forecast.mode}.`);
62
+ }
63
+ if (forecast.mode === enyo_appliance_command_forecast_js_1.BatteryCommandForecastModeEnum.Auto) {
64
+ if (forecast.relativeSchedule !== undefined) {
65
+ throw new ApplianceCommandForecastValidationError("BatteryCommandForecast with mode='auto' must not carry a relativeSchedule.");
66
+ }
67
+ return;
68
+ }
69
+ validateBatterySchedule(forecast.relativeSchedule, forecast.resolution);
70
+ }
71
+ /**
72
+ * Validates a {@link HeatpumpForecast}. Throws on the first violation —
73
+ * the error message names the offending field / index.
74
+ *
75
+ * The forecast carries a single unified relative schedule; every entry
76
+ * is validated by {@link validateHeatpumpScheduleEntry}.
77
+ */
78
+ function validateHeatpumpForecast(forecast) {
79
+ if (!forecast || typeof forecast !== 'object') {
80
+ throw new ApplianceCommandForecastValidationError('HeatpumpForecast must be an object.');
81
+ }
82
+ validateMetadata(forecast);
83
+ validateHeatpumpSchedule(forecast.relativeSchedule, forecast.resolution);
84
+ }
85
+ /**
86
+ * Validates a charger relative schedule (the inner schedule used by
87
+ * {@link ChargerForecast.relativeSchedule}). The `resolution` argument
88
+ * is the value declared on
89
+ * {@link ApplianceForecastMetadata.resolution}; consecutive entries'
90
+ * `seconds` must be spaced by exactly that many seconds.
91
+ */
92
+ function validateChargerSchedule(entries, resolution) {
93
+ const stepSeconds = resolveResolutionSeconds(resolution);
94
+ validateNonEmptySchedule(entries, 'relativeSchedule');
95
+ for (let i = 0; i < entries.length; i++) {
96
+ const entry = entries[i];
97
+ validateSecondsField(entry.seconds, `relativeSchedule[${i}].seconds`);
98
+ validatePowerW(entry.powerW, `relativeSchedule[${i}].powerW`);
99
+ if (entry.numberOfPhases !== undefined && ![1, 2, 3].includes(entry.numberOfPhases)) {
100
+ throw new ApplianceCommandForecastValidationError(`relativeSchedule[${i}].numberOfPhases must be 1, 2, or 3; got ${entry.numberOfPhases}.`);
101
+ }
102
+ }
103
+ validateFirstEntryStartsAtZero(entries[0].seconds, 'relativeSchedule');
104
+ validateSecondsMatchResolution(entries.map((e) => e.seconds), stepSeconds, 'relativeSchedule');
105
+ }
106
+ /**
107
+ * Validates a battery relative schedule (the inner schedule used by
108
+ * {@link BatteryCommandForecastScheduled.relativeSchedule}). The
109
+ * `resolution` argument is the value declared on
110
+ * {@link ApplianceForecastMetadata.resolution}; consecutive entries'
111
+ * `seconds` must be spaced by exactly that many seconds.
112
+ */
113
+ function validateBatterySchedule(entries, resolution) {
114
+ const stepSeconds = resolveResolutionSeconds(resolution);
115
+ validateNonEmptySchedule(entries, 'relativeSchedule');
116
+ const allowedDirections = new Set(Object.values(enyo_appliance_command_forecast_js_1.BatteryCommandForecastDirectionEnum));
117
+ for (let i = 0; i < entries.length; i++) {
118
+ const entry = entries[i];
119
+ validateSecondsField(entry.seconds, `relativeSchedule[${i}].seconds`);
120
+ validatePowerW(entry.powerW, `relativeSchedule[${i}].powerW`);
121
+ if (!allowedDirections.has(entry.direction)) {
122
+ throw new ApplianceCommandForecastValidationError(`relativeSchedule[${i}].direction is invalid: ${entry.direction}.`);
123
+ }
124
+ }
125
+ validateFirstEntryStartsAtZero(entries[0].seconds, 'relativeSchedule');
126
+ validateSecondsMatchResolution(entries.map((e) => e.seconds), stepSeconds, 'relativeSchedule');
127
+ }
128
+ /**
129
+ * Validates a heatpump unified relative schedule (the schedule used by
130
+ * {@link HeatpumpForecast.relativeSchedule}). Enforces that the
131
+ * schedule is non-empty, starts at `seconds = 0`, has entries spaced
132
+ * by exactly `resolution`, and that every per-entry value falls in the
133
+ * plausible range documented on
134
+ * {@link HeatpumpForecastScheduleEntry}.
135
+ */
136
+ function validateHeatpumpSchedule(entries, resolution) {
137
+ const stepSeconds = resolveResolutionSeconds(resolution);
138
+ validateNonEmptySchedule(entries, 'relativeSchedule');
139
+ for (let i = 0; i < entries.length; i++) {
140
+ validateHeatpumpScheduleEntry(entries[i], `relativeSchedule[${i}]`);
141
+ }
142
+ validateFirstEntryStartsAtZero(entries[0].seconds, 'relativeSchedule');
143
+ validateSecondsMatchResolution(entries.map((e) => e.seconds), stepSeconds, 'relativeSchedule');
144
+ }
145
+ /**
146
+ * Validates a single {@link HeatpumpForecastScheduleEntry}. Used by
147
+ * {@link validateHeatpumpSchedule} and exposed for callers that build
148
+ * entries incrementally.
149
+ */
150
+ function validateHeatpumpScheduleEntry(entry, fieldName) {
151
+ validateSecondsField(entry.seconds, `${fieldName}.seconds`);
152
+ if (entry.powerW !== undefined) {
153
+ validatePowerW(entry.powerW, `${fieldName}.powerW`);
154
+ }
155
+ validateTemperatureField(entry.dhwTemperatureC, `${fieldName}.dhwTemperatureC`);
156
+ validateTemperatureField(entry.roomTemperatureC, `${fieldName}.roomTemperatureC`);
157
+ validateTemperatureField(entry.bufferTankTemperatureC, `${fieldName}.bufferTankTemperatureC`);
158
+ validateBooleanField(entry.dhwBoostActive, `${fieldName}.dhwBoostActive`);
159
+ validateBooleanField(entry.roomPreHeatingActive, `${fieldName}.roomPreHeatingActive`);
160
+ validateBooleanField(entry.bufferTankBoostActive, `${fieldName}.bufferTankBoostActive`);
161
+ }
162
+ function validateMetadata(forecast) {
163
+ if (!(forecast.resolution in RESOLUTION_SECONDS)) {
164
+ throw new ApplianceCommandForecastValidationError(`resolution is invalid: ${forecast.resolution}. Allowed values: ${Object.values(enyo_appliance_command_forecast_js_1.ApplianceForecastResolutionEnum).join(', ')}.`);
165
+ }
166
+ if (forecast.estimatedSavings !== undefined) {
167
+ const savings = forecast.estimatedSavings;
168
+ if (typeof savings.currency !== 'string' || savings.currency.length === 0) {
169
+ throw new ApplianceCommandForecastValidationError('estimatedSavings.currency must be a non-empty string.');
170
+ }
171
+ if (!Number.isFinite(savings.costSavings)) {
172
+ throw new ApplianceCommandForecastValidationError(`estimatedSavings.costSavings must be a finite number; got ${savings.costSavings}.`);
173
+ }
174
+ }
175
+ }
176
+ function resolveResolutionSeconds(resolution) {
177
+ const step = RESOLUTION_SECONDS[resolution];
178
+ if (step === undefined) {
179
+ throw new ApplianceCommandForecastValidationError(`resolution is invalid: ${resolution}. Allowed values: ${Object.values(enyo_appliance_command_forecast_js_1.ApplianceForecastResolutionEnum).join(', ')}.`);
180
+ }
181
+ return step;
182
+ }
183
+ function validateNonEmptySchedule(entries, fieldName) {
184
+ if (!Array.isArray(entries) || entries.length === 0) {
185
+ throw new ApplianceCommandForecastValidationError(`${fieldName} must contain at least one entry.`);
186
+ }
187
+ }
188
+ function validateSecondsField(seconds, fieldName) {
189
+ if (!Number.isFinite(seconds) || seconds < 0) {
190
+ throw new ApplianceCommandForecastValidationError(`${fieldName}=${seconds} must be a finite non-negative number.`);
191
+ }
192
+ }
193
+ function validatePowerW(powerW, fieldName) {
194
+ if (!Number.isFinite(powerW) || powerW < 0) {
195
+ throw new ApplianceCommandForecastValidationError(`${fieldName}=${powerW} must be a finite non-negative number.`);
196
+ }
197
+ }
198
+ function validateTemperatureField(value, fieldName) {
199
+ if (value === undefined) {
200
+ return;
201
+ }
202
+ if (!Number.isFinite(value) ||
203
+ value < TEMPERATURE_TRAJECTORY_MIN_C ||
204
+ value > TEMPERATURE_TRAJECTORY_MAX_C) {
205
+ throw new ApplianceCommandForecastValidationError(`${fieldName}=${value} is outside the plausible range [${TEMPERATURE_TRAJECTORY_MIN_C}, ${TEMPERATURE_TRAJECTORY_MAX_C}].`);
206
+ }
207
+ }
208
+ function validateBooleanField(value, fieldName) {
209
+ if (value === undefined) {
210
+ return;
211
+ }
212
+ if (typeof value !== 'boolean') {
213
+ throw new ApplianceCommandForecastValidationError(`${fieldName} must be a boolean when provided; got ${typeof value}.`);
214
+ }
215
+ }
216
+ function validateFirstEntryStartsAtZero(firstSeconds, fieldName) {
217
+ if (firstSeconds !== 0) {
218
+ throw new ApplianceCommandForecastValidationError(`${fieldName}[0].seconds must be 0 (got ${firstSeconds}); the receiving appliance needs an authoritative "right now" setpoint.`);
219
+ }
220
+ }
221
+ function validateSecondsMatchResolution(secondsList, stepSeconds, fieldName) {
222
+ for (let i = 1; i < secondsList.length; i++) {
223
+ const delta = secondsList[i] - secondsList[i - 1];
224
+ if (delta !== stepSeconds) {
225
+ throw new ApplianceCommandForecastValidationError(`${fieldName}[${i}].seconds (${secondsList[i]}) must be exactly ${stepSeconds}s after the previous entry (${secondsList[i - 1]}); got delta=${delta}s. The forecast's resolution determines the required step.`);
226
+ }
227
+ }
228
+ }
@@ -0,0 +1,67 @@
1
+ import { ApplianceForecastResolutionEnum, BatteryCommandForecast, BatteryCommandForecastScheduleEntry, ChargerForecast, ChargerForecastScheduleEntry, HeatpumpForecast, HeatpumpForecastScheduleEntry } from '../../types/enyo-appliance-command-forecast.cjs';
2
+ /**
3
+ * Thrown when a forecast payload passed to one of the validators (or to
4
+ * {@link EnergyAppApplianceEnergyManagerForecast.publishChargerForecast}
5
+ * and friends) violates the invariants declared on its data interface.
6
+ *
7
+ * The message names the offending field / index so callers can surface it
8
+ * directly to the user.
9
+ */
10
+ export declare class ApplianceCommandForecastValidationError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ /**
14
+ * Validates a {@link ChargerForecast}. Throws on the first violation —
15
+ * the error message names the offending field / index.
16
+ */
17
+ export declare function validateChargerForecast(forecast: ChargerForecast): void;
18
+ /**
19
+ * Validates a {@link BatteryCommandForecast}. Throws on the first
20
+ * violation — the error message names the offending field / index.
21
+ *
22
+ * The forecast is a discriminated union on
23
+ * {@link BatteryCommandForecastModeEnum}: when `mode = 'auto'` no
24
+ * schedule is expected; when `mode = 'schedule'` the embedded
25
+ * {@link BatteryCommandForecastScheduled.relativeSchedule} is validated
26
+ * by {@link validateBatterySchedule}.
27
+ */
28
+ export declare function validateBatteryCommandForecast(forecast: BatteryCommandForecast): void;
29
+ /**
30
+ * Validates a {@link HeatpumpForecast}. Throws on the first violation —
31
+ * the error message names the offending field / index.
32
+ *
33
+ * The forecast carries a single unified relative schedule; every entry
34
+ * is validated by {@link validateHeatpumpScheduleEntry}.
35
+ */
36
+ export declare function validateHeatpumpForecast(forecast: HeatpumpForecast): void;
37
+ /**
38
+ * Validates a charger relative schedule (the inner schedule used by
39
+ * {@link ChargerForecast.relativeSchedule}). The `resolution` argument
40
+ * is the value declared on
41
+ * {@link ApplianceForecastMetadata.resolution}; consecutive entries'
42
+ * `seconds` must be spaced by exactly that many seconds.
43
+ */
44
+ export declare function validateChargerSchedule(entries: ChargerForecastScheduleEntry[], resolution: ApplianceForecastResolutionEnum): void;
45
+ /**
46
+ * Validates a battery relative schedule (the inner schedule used by
47
+ * {@link BatteryCommandForecastScheduled.relativeSchedule}). The
48
+ * `resolution` argument is the value declared on
49
+ * {@link ApplianceForecastMetadata.resolution}; consecutive entries'
50
+ * `seconds` must be spaced by exactly that many seconds.
51
+ */
52
+ export declare function validateBatterySchedule(entries: BatteryCommandForecastScheduleEntry[], resolution: ApplianceForecastResolutionEnum): void;
53
+ /**
54
+ * Validates a heatpump unified relative schedule (the schedule used by
55
+ * {@link HeatpumpForecast.relativeSchedule}). Enforces that the
56
+ * schedule is non-empty, starts at `seconds = 0`, has entries spaced
57
+ * by exactly `resolution`, and that every per-entry value falls in the
58
+ * plausible range documented on
59
+ * {@link HeatpumpForecastScheduleEntry}.
60
+ */
61
+ export declare function validateHeatpumpSchedule(entries: HeatpumpForecastScheduleEntry[], resolution: ApplianceForecastResolutionEnum): void;
62
+ /**
63
+ * Validates a single {@link HeatpumpForecastScheduleEntry}. Used by
64
+ * {@link validateHeatpumpSchedule} and exposed for callers that build
65
+ * entries incrementally.
66
+ */
67
+ export declare function validateHeatpumpScheduleEntry(entry: HeatpumpForecastScheduleEntry, fieldName: string): void;