@enyo-energy/energy-app-sdk 0.0.195 → 0.0.197

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 (68) hide show
  1. package/README.md +121 -1
  2. package/dist/cjs/energy-app.cjs +14 -0
  3. package/dist/cjs/energy-app.d.cts +13 -0
  4. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  5. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.cjs +263 -0
  6. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.d.cts +91 -0
  7. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +76 -1
  8. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +66 -2
  9. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-dynamic-validators.cjs +163 -0
  10. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-dynamic-validators.d.cts +90 -0
  11. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.cjs +167 -0
  12. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.d.cts +86 -0
  13. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +150 -0
  14. package/dist/cjs/index.cjs +8 -0
  15. package/dist/cjs/index.d.cts +8 -0
  16. package/dist/cjs/packages/energy-app-energy-manager.d.cts +136 -0
  17. package/dist/cjs/packages/energy-app-onboarding-v2.cjs +2 -0
  18. package/dist/cjs/packages/energy-app-onboarding-v2.d.cts +291 -0
  19. package/dist/cjs/packages/energy-app-onboarding.d.cts +13 -6
  20. package/dist/cjs/types/enyo-authentication.d.cts +19 -0
  21. package/dist/cjs/types/enyo-energy-manager-settings.cjs +182 -0
  22. package/dist/cjs/types/enyo-energy-manager-settings.d.cts +305 -0
  23. package/dist/cjs/types/enyo-heating-rod-appliance.d.cts +50 -0
  24. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +40 -0
  25. package/dist/cjs/types/enyo-onboarding-v2-additional-setup.cjs +38 -0
  26. package/dist/cjs/types/enyo-onboarding-v2-additional-setup.d.cts +123 -0
  27. package/dist/cjs/types/enyo-onboarding-v2-dynamic.cjs +28 -0
  28. package/dist/cjs/types/enyo-onboarding-v2-dynamic.d.cts +102 -0
  29. package/dist/cjs/types/enyo-onboarding-v2-provider.cjs +51 -0
  30. package/dist/cjs/types/enyo-onboarding-v2-provider.d.cts +105 -0
  31. package/dist/cjs/types/enyo-onboarding-v2.cjs +26 -1
  32. package/dist/cjs/types/enyo-onboarding-v2.d.cts +212 -4
  33. package/dist/cjs/version.cjs +1 -1
  34. package/dist/cjs/version.d.cts +1 -1
  35. package/dist/energy-app.d.ts +13 -0
  36. package/dist/energy-app.js +14 -0
  37. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  38. package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.d.ts +91 -0
  39. package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.js +257 -0
  40. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +66 -2
  41. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +75 -1
  42. package/dist/implementations/onboarding-v2/onboarding-v2-dynamic-validators.d.ts +90 -0
  43. package/dist/implementations/onboarding-v2/onboarding-v2-dynamic-validators.js +157 -0
  44. package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.d.ts +86 -0
  45. package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.js +161 -0
  46. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +151 -1
  47. package/dist/index.d.ts +8 -0
  48. package/dist/index.js +8 -0
  49. package/dist/packages/energy-app-energy-manager.d.ts +136 -0
  50. package/dist/packages/energy-app-onboarding-v2.d.ts +291 -0
  51. package/dist/packages/energy-app-onboarding-v2.js +1 -0
  52. package/dist/packages/energy-app-onboarding.d.ts +13 -6
  53. package/dist/types/enyo-authentication.d.ts +19 -0
  54. package/dist/types/enyo-energy-manager-settings.d.ts +305 -0
  55. package/dist/types/enyo-energy-manager-settings.js +179 -0
  56. package/dist/types/enyo-heating-rod-appliance.d.ts +50 -0
  57. package/dist/types/enyo-heatpump-appliance.d.ts +40 -0
  58. package/dist/types/enyo-onboarding-v2-additional-setup.d.ts +123 -0
  59. package/dist/types/enyo-onboarding-v2-additional-setup.js +35 -0
  60. package/dist/types/enyo-onboarding-v2-dynamic.d.ts +102 -0
  61. package/dist/types/enyo-onboarding-v2-dynamic.js +27 -0
  62. package/dist/types/enyo-onboarding-v2-provider.d.ts +105 -0
  63. package/dist/types/enyo-onboarding-v2-provider.js +48 -0
  64. package/dist/types/enyo-onboarding-v2.d.ts +212 -4
  65. package/dist/types/enyo-onboarding-v2.js +25 -0
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -172,7 +172,8 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
172
172
  | Manage electricity tariffs (default tariff, price per kWh) | [`useElectricityTariff()`](#useelectricitytariff-energyappelectricitytariff) |
173
173
  | Register a PV system (kWp, DC strings, orientation) | [`usePvSystem()`](#usepvsystem-energyapppvsystem) |
174
174
  | Discover capabilities of the active energy manager | [`useEnergyManager()`](#useenergymanager-energyappenergymanager) |
175
- | Drive a multi-step onboarding flow | [`useOnboarding()`](#useonboarding-energyapponboarding) |
175
+ | Serve your v2 onboarding guides when the host asks for them | [`useOnboardingV2()`](#useonboardingv2-energyapponboardingv2) |
176
+ | Drive a multi-step onboarding flow (v1, deprecated) | [`useOnboarding()`](#useonboarding-energyapponboarding) |
176
177
  | Allocate process-local sequential IDs | [`useSequenceGenerator()`](#usesequencegenerator-energyappsequencegenerator) |
177
178
  | Manage retries with circuit-breaker semantics | [`RetryManager`](#retry-framework) |
178
179
  | Keep an `applianceId` cache in sync with the SDK | [`ApplianceManager`](#appliance-management) |
@@ -1126,6 +1127,65 @@ if (info) console.log(`Active manager: ${info.name} v${info.version}`);
1126
1127
 
1127
1128
  Requires the `EnergyManagerInfo` permission.
1128
1129
 
1130
+ ##### General settings
1131
+
1132
+ A closed, SDK-defined set of user-facing controls the active energy manager honours — the contract between the enyo cockpit and whichever manager is installed. Not to be confused with `useSettings()`, which is per-package, string-valued, and rendered on your own config screen.
1133
+
1134
+ | Setting | Type |
1135
+ |---|---|
1136
+ | `BatteryControl` | boolean |
1137
+ | `BatteryChargingMode` | `paced` \| `immediate` |
1138
+ | `BatteryChargeFromGrid` | boolean |
1139
+ | `BatteryDischargeWhileChargingWh` | number \| `null` (Wh allowance; `null`/unset = disabled) |
1140
+ | `BlockBatteryDischargeWhileEvDischarging` | boolean |
1141
+ | `HeatpumpControl` | boolean |
1142
+ | `HeatingRodControl` | boolean |
1143
+ | `HeatingRodMode` | `pv-surplus-only` \| `boost` |
1144
+ | `ChargerControl` | boolean |
1145
+ | `DefaultChargeMode` | `EnyoChargeModeEnum` |
1146
+ | `PriceLimitCtPerKwh` | number, **ct/kWh** (`7` = 7 ct/kWh) |
1147
+ | `CostOptimizedTarget` | wall-clock `"07:30"` + IANA timezone |
1148
+
1149
+ An energy manager declares what it honours, next to `registerFeatures()`:
1150
+
1151
+ ```typescript
1152
+ em.registerSupportedSettings([
1153
+ EnergyManagerSettingEnum.BatteryControl,
1154
+ EnergyManagerSettingEnum.BatteryChargingMode,
1155
+ EnergyManagerSettingEnum.ChargerControl,
1156
+ EnergyManagerSettingEnum.DefaultChargeMode,
1157
+ ]);
1158
+
1159
+ // seed defaults without ever overriding the user
1160
+ await em.setEnergyManagerSettings(
1161
+ { batteryControl: true, batteryChargingMode: EnergyManagerBatteryChargingModeEnum.Paced },
1162
+ { onlyIfUnset: true },
1163
+ );
1164
+ ```
1165
+
1166
+ Anyone can read and react:
1167
+
1168
+ ```typescript
1169
+ const state = await em.getEnergyManagerSettings(); // null when no EM is configured
1170
+ if (state?.supported.includes(EnergyManagerSettingEnum.ChargerControl)
1171
+ && state.values.chargerControl === false) {
1172
+ skipChargerOptimization();
1173
+ }
1174
+
1175
+ const id = em.listenForEnergyManagerSettingsChange(async ({ changed, state }) => {
1176
+ if (changed.includes(EnergyManagerSettingEnum.DefaultChargeMode)) {
1177
+ await applyChargeMode(state.values.defaultChargeMode);
1178
+ }
1179
+ });
1180
+ em.unsubscribeEnergyManagerSettings(id);
1181
+ ```
1182
+
1183
+ Three things to get right:
1184
+
1185
+ - **Absent is not `false`.** `undefined` means unsupported or never chosen; `false` means the user switched it off. Check `supported` first, then the value — collapsing the two steers hardware someone deliberately disabled.
1186
+ - **Six settings are gated by another** (`heatingRodMode` needs `heatingRodControl`, `priceLimitCtPerKwh` only applies under `price-limit`, …). The tree is exported as `ENERGY_MANAGER_SETTING_DEPENDENCIES` so the UI and the validator share one source of truth.
1187
+ - **`priceLimitCtPerKwh` is in cents**, unlike the SDK's machine-readable price fields (`electricityPricePerKwh`, EUR/kWh). Divide by 100 when comparing. `validateEnergyManagerSettingsState()` warns on values outside a plausible ct band, which catches the mix-up.
1188
+
1129
1189
  #### `useElectricityTariff(): EnergyAppElectricityTariff`
1130
1190
 
1131
1191
  Register, retrieve, and manage electricity tariffs. One tariff can be marked as the system default.
@@ -1334,8 +1394,68 @@ Requires the `Savings` permission.
1334
1394
 
1335
1395
  ### Operational Utilities
1336
1396
 
1397
+ #### `useOnboardingV2(): EnergyAppOnboardingV2`
1398
+
1399
+ Serve the onboarding guides your app ships. Guides are **pulled, not published**: you register one handler, and the host calls it with "give me your v2 onboarding guides". You answer with the **complete set** or with **nothing** — there is no save, update or delete, and every answer replaces the host's picture of what your app offers.
1400
+
1401
+ ```typescript
1402
+ await energyApp.useOnboardingV2().registerOnboardingGuidesHandler(async (request) => {
1403
+ const result = { requestId: request.requestId, guides: buildGuides() };
1404
+
1405
+ const { ok, errors } = validateOnboardingV2GuidesResult(result, {
1406
+ files: packageDefinition.files,
1407
+ });
1408
+ if (!ok) {
1409
+ console.error('onboarding guides invalid', errors);
1410
+ return null; // keep whatever the host already has
1411
+ }
1412
+
1413
+ return result;
1414
+ });
1415
+ ```
1416
+
1417
+ `null` and `[]` are **not** the same answer: `[]` says "I genuinely have no guides" and drops the host's cached ones, `null` says "I cannot answer right now" and leaves them alone. Use `null` for transient failures. The host stops waiting after `request.timeoutMs`, so build the guides in memory rather than fetching them.
1418
+
1419
+ Guides declare **slots**, not values: `onboardingV2Block.dynamic(id, kind)` for an OCPP URL or a device IP. Register a second handler to fill them, and return `null` for anything you cannot resolve — a dynamic block is passive content, so an unresolved value never strands a run.
1420
+
1421
+ ```typescript
1422
+ await energyApp.useOnboardingV2().registerDynamicValueHandler(async (request) => {
1423
+ if (request.kind !== EnyoOnboardingV2DynamicKind.OcppUrl) return null;
1424
+
1425
+ const { cloud, local } = await energyApp.useOcpp().getAvailableConnectionDetails();
1426
+ const endpoint = cloud ?? local;
1427
+ if (!endpoint) return null; // unavailable beats wrong
1428
+
1429
+ return { requestId: request.requestId, kind: request.kind, value: endpoint.url };
1430
+ });
1431
+ ```
1432
+
1433
+ Guides can also carry `additional-setup` blocks — the one interactive block **your app** judges. It collects fields (including masked `Password`/`Token` fields, which v2 previously had no way to gather at all), calls a third handler, and branches on the outcome you return:
1434
+
1435
+ ```typescript
1436
+ await energyApp.useOnboardingV2().registerAdditionalSetupHandler(async (request) => {
1437
+ if (request.setupKey !== 'vendor-cloud-token') return { requestId: request.requestId, outcome: 'failed' };
1438
+ const token = request.values.find((v) => v.name === 'api-token')?.value;
1439
+ if (!token || !(await vendorCloud.verify(token))) {
1440
+ return { requestId: request.requestId, outcome: 'invalid' };
1441
+ }
1442
+ await energyApp.useSecretManager().saveSecret('vendor-cloud', { token });
1443
+ return { requestId: request.requestId, outcome: 'connected' };
1444
+ });
1445
+ ```
1446
+
1447
+ Every such block must declare a `failed` outcome — it absorbs a rejection, a timeout, a missing handler, and an outcome value matching no branch (guide and handler are linked only by strings). Secrets are never logged, never prefilled, and not kept in run state. See [ONBOARDING.md](./ONBOARDING.md#app-defined-setups-additional-setup).
1448
+
1449
+ Your answer wins over the host's own resolution, so a wrong value is worse than none — the installer pastes it into a charger and it fails minutes later as an `ocpp-connect` timeout. `validateOnboardingV2DynamicResult()` catches the copy-target mistakes (whitespace, a non-absolute URL, a plaintext scheme).
1450
+
1451
+ Each guide must carry the `vendorId`, `modelIds` and `startVariant` it applies to — that is how the host selects one for a run, and there is no publish step left to bind them. See [ONBOARDING.md](./ONBOARDING.md#serving-guides-the-host-pulls-the-app-never-publishes) for the full v2 model.
1452
+
1453
+ Not permission-gated.
1454
+
1337
1455
  #### `useOnboarding(): EnergyAppOnboarding`
1338
1456
 
1457
+ > **Deprecated** — the v1 model. New guides use the v2 graph model and are served through [`useOnboardingV2()`](#useonboardingv2-energyapponboardingv2).
1458
+
1339
1459
  Drive a multi-step onboarding guide — start / advance / back / skip / cancel, persist responses, and observe step transitions.
1340
1460
 
1341
1461
  ```typescript
@@ -117,6 +117,20 @@ class EnergyApp {
117
117
  useOnboarding() {
118
118
  return this.energyAppSdk.useOnboarding();
119
119
  }
120
+ /**
121
+ * Gets the Onboarding v2 API for serving this app's onboarding guides.
122
+ *
123
+ * Guides are pulled rather than published: the app registers one handler and
124
+ * the host calls it with "give me your v2 onboarding guides", receiving the
125
+ * complete current set or nothing. There is no save, update or delete —
126
+ * every answer replaces the host's picture of what this app offers.
127
+ *
128
+ * Available to every app — this API is not permission-gated.
129
+ * @returns The Onboarding v2 API instance
130
+ */
131
+ useOnboardingV2() {
132
+ return this.energyAppSdk.useOnboardingV2();
133
+ }
120
134
  /**
121
135
  * Gets the Secret Manager API for retrieving secrets from the developer organization.
122
136
  * Provides methods to fetch secrets that have been configured in the developer org's secret store.
@@ -16,6 +16,7 @@ import { EnergyAppNotification } from "./packages/energy-app-notification.cjs";
16
16
  import { EnergyAppSecretManager } from "./packages/energy-app-secret-manager.cjs";
17
17
  import { EnergyAppLocation } from "./packages/energy-app-location.cjs";
18
18
  import { EnergyAppOnboarding } from "./packages/energy-app-onboarding.cjs";
19
+ import { EnergyAppOnboardingV2 } from "./packages/energy-app-onboarding-v2.cjs";
19
20
  import { EnergyAppTimeseries } from "./packages/energy-app-timeseries.cjs";
20
21
  import { EnyoPackageChannel } from "./enyo-package-channel.cjs";
21
22
  import { EnergyAppEnergyManager } from "./packages/energy-app-energy-manager.cjs";
@@ -99,6 +100,18 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
99
100
  useElectricityPrices(): EnergyAppEnergyPrices;
100
101
  useNotification(): EnergyAppNotification;
101
102
  useOnboarding(): EnergyAppOnboarding;
103
+ /**
104
+ * Gets the Onboarding v2 API for serving this app's onboarding guides.
105
+ *
106
+ * Guides are pulled rather than published: the app registers one handler and
107
+ * the host calls it with "give me your v2 onboarding guides", receiving the
108
+ * complete current set or nothing. There is no save, update or delete —
109
+ * every answer replaces the host's picture of what this app offers.
110
+ *
111
+ * Available to every app — this API is not permission-gated.
112
+ * @returns The Onboarding v2 API instance
113
+ */
114
+ useOnboardingV2(): EnergyAppOnboardingV2;
102
115
  /**
103
116
  * Gets the Secret Manager API for retrieving secrets from the developer organization.
104
117
  * Provides methods to fetch secrets that have been configured in the developer org's secret store.
@@ -15,6 +15,7 @@ import { EnergyAppNotification } from "./packages/energy-app-notification.cjs";
15
15
  import { EnergyAppSecretManager } from "./packages/energy-app-secret-manager.cjs";
16
16
  import { EnergyAppLocation } from "./packages/energy-app-location.cjs";
17
17
  import { EnergyAppOnboarding } from "./packages/energy-app-onboarding.cjs";
18
+ import { EnergyAppOnboardingV2 } from "./packages/energy-app-onboarding-v2.cjs";
18
19
  import { EnergyAppTimeseries } from "./packages/energy-app-timeseries.cjs";
19
20
  import { EnyoPackageChannel } from "./enyo-package-channel.cjs";
20
21
  import { EnergyAppEnergyManager } from "./packages/energy-app-energy-manager.cjs";
@@ -106,6 +107,8 @@ export interface EnyoEnergyAppSdk {
106
107
  useLocation: () => EnergyAppLocation;
107
108
  /** Get the Onboarding API */
108
109
  useOnboarding: () => EnergyAppOnboarding;
110
+ /** Get the Onboarding v2 API for registering the handler the host calls to collect this app's onboarding guides */
111
+ useOnboardingV2: () => EnergyAppOnboardingV2;
109
112
  /** Get the Timeseries API for querying historical energy data */
110
113
  useTimeseries: () => EnergyAppTimeseries;
111
114
  /** Get the Energy Manager API for retrieving energy manager info and capabilities */
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ /**
3
+ * Client-side validation for {@link EnergyManagerSettingsState} — the general
4
+ * settings an energy manager declares and the values stored against them.
5
+ *
6
+ * Two classes of problem live here, and only the second is obvious:
7
+ *
8
+ * 1. **Malformed values.** A `HH:mm` that is not a time, a timezone that is not
9
+ * an IANA zone, a price that is not a finite number. These fail at planning
10
+ * time, in the dark, on the night the charge was supposed to happen.
11
+ * 2. **Values that can never take effect.** A heating-rod mode stored while rod
12
+ * control is off, a price limit while the default charge mode is
13
+ * cost-optimized. Nothing rejects these — they simply do nothing, which reads
14
+ * to a user as "the app ignored me".
15
+ *
16
+ * The second class is why {@link ENERGY_MANAGER_SETTING_DEPENDENCIES} exists as
17
+ * data: the gate tree is checked here rather than restated by every surface.
18
+ *
19
+ * `errors` mean the state is malformed; `warnings` are advisory. Use
20
+ * {@link validateEnergyManagerSettingsState} for the non-throwing result, or
21
+ * {@link assertValidEnergyManagerSettingsState} to throw.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.EnergyManagerSettingsValidationError = void 0;
25
+ exports.validateEnergyManagerSettingsState = validateEnergyManagerSettingsState;
26
+ exports.assertValidEnergyManagerSettingsState = assertValidEnergyManagerSettingsState;
27
+ const enyo_energy_manager_settings_js_1 = require("../../types/enyo-energy-manager-settings.cjs");
28
+ const enyo_data_bus_value_js_1 = require("../../types/enyo-data-bus-value.cjs");
29
+ /**
30
+ * Thrown by {@link assertValidEnergyManagerSettingsState} when a state fails
31
+ * validation. The message lists every blocking error so callers can surface them
32
+ * directly.
33
+ */
34
+ class EnergyManagerSettingsValidationError extends Error {
35
+ /** The individual blocking errors that caused the failure. */
36
+ errors;
37
+ /**
38
+ * @param errors - The blocking validation errors.
39
+ */
40
+ constructor(errors) {
41
+ super(`Invalid energy manager settings:\n- ${errors.join('\n- ')}`);
42
+ this.name = 'EnergyManagerSettingsValidationError';
43
+ this.errors = errors;
44
+ }
45
+ }
46
+ exports.EnergyManagerSettingsValidationError = EnergyManagerSettingsValidationError;
47
+ /** Every {@link EnergyManagerSettingEnum} value. */
48
+ const SETTINGS = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerSettingEnum));
49
+ /** 24-hour `HH:mm`, the format {@link EnergyManagerSettingValues.costOptimizedTargetTime} uses. */
50
+ const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
51
+ /**
52
+ * Plausible band for a price limit in ct/kWh.
53
+ *
54
+ * Wide on purpose, and negative at the bottom: wholesale prices go negative, and
55
+ * "charge only when I am paid to" is a real preference. The band exists to catch
56
+ * a EUR/kWh value entered into a ct/kWh field (`0.07` where `7` was meant) and
57
+ * the reverse (`700`), not to express an opinion about tariffs.
58
+ */
59
+ const PRICE_LIMIT_MIN_CT = -100;
60
+ const PRICE_LIMIT_MAX_CT = 200;
61
+ /**
62
+ * Below this, a {@link EnergyManagerSettingValues.batteryDischargeWhileChargingWh}
63
+ * budget is almost certainly kWh entered into a Wh field.
64
+ *
65
+ * `10` would be ten watt-hours — a rounding error against any house battery, and
66
+ * indistinguishable from "no discharge at all" in practice, where `10` kWh was
67
+ * plainly meant. `0` is exempt: it is a legitimate, if roundabout, way to say
68
+ * disabled.
69
+ */
70
+ const DISCHARGE_BUDGET_MIN_PLAUSIBLE_WH = 100;
71
+ /** Value enums checked per field, for the enum-valued settings. */
72
+ const BATTERY_CHARGING_MODES = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerBatteryChargingModeEnum));
73
+ const HEATING_ROD_MODES = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerHeatingRodModeEnum));
74
+ const CHARGE_MODES = new Set(Object.values(enyo_data_bus_value_js_1.EnyoChargeModeEnum));
75
+ /**
76
+ * True when `timezone` is an IANA zone this runtime recognises.
77
+ *
78
+ * Uses `Intl` rather than a pattern: `"Europe/Berlin"` and `"Europe/Berln"` are
79
+ * indistinguishable by shape, and only the first one resolves a wall-clock time.
80
+ *
81
+ * @param timezone - The candidate IANA timezone identifier.
82
+ * @returns True when the runtime accepts it.
83
+ */
84
+ function isValidTimezone(timezone) {
85
+ try {
86
+ new Intl.DateTimeFormat('en-US', { timeZone: timezone });
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Reads the effective value a dependency is gated on.
95
+ *
96
+ * @param values - The stored values.
97
+ * @param setting - The gating setting.
98
+ * @returns The gate's current value, or `undefined` when it is unset.
99
+ */
100
+ function gateValue(values, setting) {
101
+ const [key] = enyo_energy_manager_settings_js_1.ENERGY_MANAGER_SETTING_VALUE_KEYS[setting];
102
+ return key ? values[key] : undefined;
103
+ }
104
+ /**
105
+ * Validates an energy manager settings state: the declared support list, the
106
+ * value formats, and the gate tree between them.
107
+ *
108
+ * Checks applied:
109
+ *
110
+ * - `supported` holds only {@link EnergyManagerSettingEnum} members, without
111
+ * duplicates (**error**).
112
+ * - Each stored value has the right type, and each enum-valued setting holds an
113
+ * enum member (**error**).
114
+ * - `costOptimizedTargetTime` is `HH:mm`, and `costOptimizedTimezone` is an IANA
115
+ * zone the runtime resolves (**error**).
116
+ * - `priceLimitCtPerKwh` is a finite number (**error**), and lies in a plausible
117
+ * ct/kWh band (**warning** — catches a EUR/kWh value in a ct/kWh field).
118
+ * - `batteryDischargeWhileChargingWh` is a finite non-negative number or `null`
119
+ * (**error**), and is not a negligible positive figure (**warning** — catches
120
+ * a kWh value in a Wh field).
121
+ * - A value stored for an unsupported setting (**warning** — the energy manager
122
+ * will ignore it).
123
+ * - A value stored while its gate does not hold (**warning** — it can never take
124
+ * effect; see {@link ENERGY_MANAGER_SETTING_DEPENDENCIES}).
125
+ * - A supported gate whose dependants are unsupported (**warning** — a
126
+ * half-built control surface).
127
+ * - `costOptimizedTargetTime` set without a timezone (**warning** — the
128
+ * wall-clock value is ambiguous across a DST boundary).
129
+ *
130
+ * @param state - The settings state to validate.
131
+ * @returns The {@link EnergyManagerSettingsValidationResult}.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const {ok, errors, warnings} = validateEnergyManagerSettingsState(state);
136
+ * warnings.forEach((w) => console.warn('energy manager settings:', w));
137
+ * if (!ok) console.error('energy manager settings are malformed', errors);
138
+ * ```
139
+ */
140
+ function validateEnergyManagerSettingsState(state) {
141
+ const errors = [];
142
+ const warnings = [];
143
+ if (!state)
144
+ return { ok: false, errors: ['state is required.'], warnings };
145
+ if (!Array.isArray(state.supported)) {
146
+ return { ok: false, errors: ['`supported` must be an array.'], warnings };
147
+ }
148
+ const supported = new Set();
149
+ for (const setting of state.supported) {
150
+ if (!SETTINGS.has(setting)) {
151
+ errors.push(`\`supported\` contains "${setting}", which is not an EnergyManagerSettingEnum member.`);
152
+ }
153
+ else if (supported.has(setting)) {
154
+ errors.push(`\`supported\` lists "${setting}" more than once.`);
155
+ }
156
+ else {
157
+ supported.add(setting);
158
+ }
159
+ }
160
+ const values = state.values ?? {};
161
+ // ── value formats ──────────────────────────────────────────────────────
162
+ for (const key of ['batteryControl', 'batteryChargeFromGrid',
163
+ 'blockBatteryDischargeWhileEvDischarging', 'heatpumpControl',
164
+ 'heatingRodControl', 'chargerControl']) {
165
+ if (values[key] !== undefined && typeof values[key] !== 'boolean') {
166
+ errors.push(`\`${key}\` must be a boolean when set.`);
167
+ }
168
+ }
169
+ if (values.batteryChargingMode !== undefined
170
+ && !BATTERY_CHARGING_MODES.has(values.batteryChargingMode)) {
171
+ errors.push(`\`batteryChargingMode\` "${values.batteryChargingMode}" is not an EnergyManagerBatteryChargingModeEnum member.`);
172
+ }
173
+ if (values.heatingRodMode !== undefined && !HEATING_ROD_MODES.has(values.heatingRodMode)) {
174
+ errors.push(`\`heatingRodMode\` "${values.heatingRodMode}" is not an EnergyManagerHeatingRodModeEnum member.`);
175
+ }
176
+ if (values.defaultChargeMode !== undefined && !CHARGE_MODES.has(values.defaultChargeMode)) {
177
+ errors.push(`\`defaultChargeMode\` "${values.defaultChargeMode}" is not an EnyoChargeModeEnum member.`);
178
+ }
179
+ if (values.priceLimitCtPerKwh !== undefined) {
180
+ if (typeof values.priceLimitCtPerKwh !== 'number'
181
+ || !Number.isFinite(values.priceLimitCtPerKwh)) {
182
+ errors.push('`priceLimitCtPerKwh` must be a finite number when set.');
183
+ }
184
+ else if (values.priceLimitCtPerKwh < PRICE_LIMIT_MIN_CT
185
+ || values.priceLimitCtPerKwh > PRICE_LIMIT_MAX_CT) {
186
+ warnings.push(`\`priceLimitCtPerKwh\` is ${values.priceLimitCtPerKwh}, outside the plausible ` +
187
+ `${PRICE_LIMIT_MIN_CT}…${PRICE_LIMIT_MAX_CT} ct/kWh band — this field is in ` +
188
+ 'cents per kWh, not EUR per kWh.');
189
+ }
190
+ }
191
+ if (values.batteryDischargeWhileChargingWh !== undefined
192
+ && values.batteryDischargeWhileChargingWh !== null) {
193
+ const budget = values.batteryDischargeWhileChargingWh;
194
+ if (typeof budget !== 'number' || !Number.isFinite(budget)) {
195
+ errors.push('`batteryDischargeWhileChargingWh` must be a finite number or `null` when set.');
196
+ }
197
+ else if (budget < 0) {
198
+ errors.push(`\`batteryDischargeWhileChargingWh\` is ${budget}; a discharge budget cannot be ` +
199
+ 'negative. Use `null` to disable the transfer entirely.');
200
+ }
201
+ else if (budget > 0 && budget < DISCHARGE_BUDGET_MIN_PLAUSIBLE_WH) {
202
+ warnings.push(`\`batteryDischargeWhileChargingWh\` is ${budget} Wh, which is negligible against ` +
203
+ 'any house battery — this field is in watt-hours, not kilowatt-hours.');
204
+ }
205
+ }
206
+ if (values.costOptimizedTargetTime !== undefined) {
207
+ if (typeof values.costOptimizedTargetTime !== 'string'
208
+ || !TIME_RE.test(values.costOptimizedTargetTime)) {
209
+ errors.push(`\`costOptimizedTargetTime\` "${values.costOptimizedTargetTime}" is not a 24-hour HH:mm time.`);
210
+ }
211
+ if (values.costOptimizedTimezone === undefined) {
212
+ warnings.push('`costOptimizedTargetTime` is set without `costOptimizedTimezone` — the wall-clock ' +
213
+ 'value cannot be resolved unambiguously across a DST boundary.');
214
+ }
215
+ }
216
+ if (values.costOptimizedTimezone !== undefined) {
217
+ if (typeof values.costOptimizedTimezone !== 'string'
218
+ || !isValidTimezone(values.costOptimizedTimezone)) {
219
+ errors.push(`\`costOptimizedTimezone\` "${values.costOptimizedTimezone}" is not an IANA timezone.`);
220
+ }
221
+ }
222
+ // ── stored but unsupported ─────────────────────────────────────────────
223
+ for (const setting of Object.values(enyo_energy_manager_settings_js_1.EnergyManagerSettingEnum)) {
224
+ const keys = enyo_energy_manager_settings_js_1.ENERGY_MANAGER_SETTING_VALUE_KEYS[setting];
225
+ const held = keys.filter((key) => values[key] !== undefined);
226
+ if (held.length && !supported.has(setting)) {
227
+ warnings.push(`"${setting}" holds a value (${held.join(', ')}) but is not in \`supported\` — ` +
228
+ 'the energy manager will ignore it.');
229
+ }
230
+ }
231
+ // ── gate tree ──────────────────────────────────────────────────────────
232
+ for (const [setting, dependency] of Object.entries(enyo_energy_manager_settings_js_1.ENERGY_MANAGER_SETTING_DEPENDENCIES)) {
233
+ if (!dependency)
234
+ continue;
235
+ const key = setting;
236
+ const held = enyo_energy_manager_settings_js_1.ENERGY_MANAGER_SETTING_VALUE_KEYS[key].some((k) => values[k] !== undefined);
237
+ if (held && gateValue(values, dependency.requires) !== dependency.equals) {
238
+ warnings.push(`"${key}" holds a value, but it only takes effect while "${dependency.requires}" ` +
239
+ `is ${JSON.stringify(dependency.equals)} — it currently cannot.`);
240
+ }
241
+ if (supported.has(dependency.requires) && !supported.has(key)
242
+ && gateValue(values, dependency.requires) === dependency.equals) {
243
+ warnings.push(`"${dependency.requires}" is supported and active, but its dependent setting ` +
244
+ `"${key}" is not — the user is offered a gate with nothing behind it.`);
245
+ }
246
+ }
247
+ return { ok: errors.length === 0, errors, warnings };
248
+ }
249
+ /**
250
+ * Like {@link validateEnergyManagerSettingsState}, but throws
251
+ * {@link EnergyManagerSettingsValidationError} when there are blocking errors.
252
+ * Warnings never throw; the validated state is returned on success for chaining.
253
+ *
254
+ * @param state - The settings state to validate.
255
+ * @returns The same state when it has no blocking errors.
256
+ * @throws {EnergyManagerSettingsValidationError} When validation produces any error.
257
+ */
258
+ function assertValidEnergyManagerSettingsState(state) {
259
+ const { ok, errors } = validateEnergyManagerSettingsState(state);
260
+ if (!ok)
261
+ throw new EnergyManagerSettingsValidationError(errors);
262
+ return state;
263
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Client-side validation for {@link EnergyManagerSettingsState} — the general
3
+ * settings an energy manager declares and the values stored against them.
4
+ *
5
+ * Two classes of problem live here, and only the second is obvious:
6
+ *
7
+ * 1. **Malformed values.** A `HH:mm` that is not a time, a timezone that is not
8
+ * an IANA zone, a price that is not a finite number. These fail at planning
9
+ * time, in the dark, on the night the charge was supposed to happen.
10
+ * 2. **Values that can never take effect.** A heating-rod mode stored while rod
11
+ * control is off, a price limit while the default charge mode is
12
+ * cost-optimized. Nothing rejects these — they simply do nothing, which reads
13
+ * to a user as "the app ignored me".
14
+ *
15
+ * The second class is why {@link ENERGY_MANAGER_SETTING_DEPENDENCIES} exists as
16
+ * data: the gate tree is checked here rather than restated by every surface.
17
+ *
18
+ * `errors` mean the state is malformed; `warnings` are advisory. Use
19
+ * {@link validateEnergyManagerSettingsState} for the non-throwing result, or
20
+ * {@link assertValidEnergyManagerSettingsState} to throw.
21
+ */
22
+ import type { EnergyManagerSettingsState } from '../../types/enyo-energy-manager-settings.cjs';
23
+ /**
24
+ * Thrown by {@link assertValidEnergyManagerSettingsState} when a state fails
25
+ * validation. The message lists every blocking error so callers can surface them
26
+ * directly.
27
+ */
28
+ export declare class EnergyManagerSettingsValidationError extends Error {
29
+ /** The individual blocking errors that caused the failure. */
30
+ readonly errors: string[];
31
+ /**
32
+ * @param errors - The blocking validation errors.
33
+ */
34
+ constructor(errors: string[]);
35
+ }
36
+ /** The outcome of validating an {@link EnergyManagerSettingsState}. */
37
+ export interface EnergyManagerSettingsValidationResult {
38
+ /** True when there are no blocking `errors` (warnings are still allowed). */
39
+ ok: boolean;
40
+ /** Blocking problems — the state is malformed. */
41
+ errors: string[];
42
+ /** Advisory problems — allowed, but usually worth fixing. */
43
+ warnings: string[];
44
+ }
45
+ /**
46
+ * Validates an energy manager settings state: the declared support list, the
47
+ * value formats, and the gate tree between them.
48
+ *
49
+ * Checks applied:
50
+ *
51
+ * - `supported` holds only {@link EnergyManagerSettingEnum} members, without
52
+ * duplicates (**error**).
53
+ * - Each stored value has the right type, and each enum-valued setting holds an
54
+ * enum member (**error**).
55
+ * - `costOptimizedTargetTime` is `HH:mm`, and `costOptimizedTimezone` is an IANA
56
+ * zone the runtime resolves (**error**).
57
+ * - `priceLimitCtPerKwh` is a finite number (**error**), and lies in a plausible
58
+ * ct/kWh band (**warning** — catches a EUR/kWh value in a ct/kWh field).
59
+ * - `batteryDischargeWhileChargingWh` is a finite non-negative number or `null`
60
+ * (**error**), and is not a negligible positive figure (**warning** — catches
61
+ * a kWh value in a Wh field).
62
+ * - A value stored for an unsupported setting (**warning** — the energy manager
63
+ * will ignore it).
64
+ * - A value stored while its gate does not hold (**warning** — it can never take
65
+ * effect; see {@link ENERGY_MANAGER_SETTING_DEPENDENCIES}).
66
+ * - A supported gate whose dependants are unsupported (**warning** — a
67
+ * half-built control surface).
68
+ * - `costOptimizedTargetTime` set without a timezone (**warning** — the
69
+ * wall-clock value is ambiguous across a DST boundary).
70
+ *
71
+ * @param state - The settings state to validate.
72
+ * @returns The {@link EnergyManagerSettingsValidationResult}.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const {ok, errors, warnings} = validateEnergyManagerSettingsState(state);
77
+ * warnings.forEach((w) => console.warn('energy manager settings:', w));
78
+ * if (!ok) console.error('energy manager settings are malformed', errors);
79
+ * ```
80
+ */
81
+ export declare function validateEnergyManagerSettingsState(state: EnergyManagerSettingsState): EnergyManagerSettingsValidationResult;
82
+ /**
83
+ * Like {@link validateEnergyManagerSettingsState}, but throws
84
+ * {@link EnergyManagerSettingsValidationError} when there are blocking errors.
85
+ * Warnings never throw; the validated state is returned on success for chaining.
86
+ *
87
+ * @param state - The settings state to validate.
88
+ * @returns The same state when it has no blocking errors.
89
+ * @throws {EnergyManagerSettingsValidationError} When validation produces any error.
90
+ */
91
+ export declare function assertValidEnergyManagerSettingsState(state: EnergyManagerSettingsState): EnergyManagerSettingsState;