@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.
- package/README.md +121 -1
- package/dist/cjs/energy-app.cjs +14 -0
- package/dist/cjs/energy-app.d.cts +13 -0
- package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
- package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.cjs +263 -0
- package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.d.cts +91 -0
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +76 -1
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +66 -2
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-dynamic-validators.cjs +163 -0
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-dynamic-validators.d.cts +90 -0
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.cjs +167 -0
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.d.cts +86 -0
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +150 -0
- package/dist/cjs/index.cjs +8 -0
- package/dist/cjs/index.d.cts +8 -0
- package/dist/cjs/packages/energy-app-energy-manager.d.cts +136 -0
- package/dist/cjs/packages/energy-app-onboarding-v2.cjs +2 -0
- package/dist/cjs/packages/energy-app-onboarding-v2.d.cts +291 -0
- package/dist/cjs/packages/energy-app-onboarding.d.cts +13 -6
- package/dist/cjs/types/enyo-authentication.d.cts +19 -0
- package/dist/cjs/types/enyo-energy-manager-settings.cjs +182 -0
- package/dist/cjs/types/enyo-energy-manager-settings.d.cts +305 -0
- package/dist/cjs/types/enyo-heating-rod-appliance.d.cts +50 -0
- package/dist/cjs/types/enyo-heatpump-appliance.d.cts +40 -0
- package/dist/cjs/types/enyo-onboarding-v2-additional-setup.cjs +38 -0
- package/dist/cjs/types/enyo-onboarding-v2-additional-setup.d.cts +123 -0
- package/dist/cjs/types/enyo-onboarding-v2-dynamic.cjs +28 -0
- package/dist/cjs/types/enyo-onboarding-v2-dynamic.d.cts +102 -0
- package/dist/cjs/types/enyo-onboarding-v2-provider.cjs +51 -0
- package/dist/cjs/types/enyo-onboarding-v2-provider.d.cts +105 -0
- package/dist/cjs/types/enyo-onboarding-v2.cjs +26 -1
- package/dist/cjs/types/enyo-onboarding-v2.d.cts +212 -4
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app.d.ts +13 -0
- package/dist/energy-app.js +14 -0
- package/dist/enyo-energy-app-sdk.d.ts +3 -0
- package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.d.ts +91 -0
- package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.js +257 -0
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +66 -2
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +75 -1
- package/dist/implementations/onboarding-v2/onboarding-v2-dynamic-validators.d.ts +90 -0
- package/dist/implementations/onboarding-v2/onboarding-v2-dynamic-validators.js +157 -0
- package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.d.ts +86 -0
- package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.js +161 -0
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +151 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/packages/energy-app-energy-manager.d.ts +136 -0
- package/dist/packages/energy-app-onboarding-v2.d.ts +291 -0
- package/dist/packages/energy-app-onboarding-v2.js +1 -0
- package/dist/packages/energy-app-onboarding.d.ts +13 -6
- package/dist/types/enyo-authentication.d.ts +19 -0
- package/dist/types/enyo-energy-manager-settings.d.ts +305 -0
- package/dist/types/enyo-energy-manager-settings.js +179 -0
- package/dist/types/enyo-heating-rod-appliance.d.ts +50 -0
- package/dist/types/enyo-heatpump-appliance.d.ts +40 -0
- package/dist/types/enyo-onboarding-v2-additional-setup.d.ts +123 -0
- package/dist/types/enyo-onboarding-v2-additional-setup.js +35 -0
- package/dist/types/enyo-onboarding-v2-dynamic.d.ts +102 -0
- package/dist/types/enyo-onboarding-v2-dynamic.js +27 -0
- package/dist/types/enyo-onboarding-v2-provider.d.ts +105 -0
- package/dist/types/enyo-onboarding-v2-provider.js +48 -0
- package/dist/types/enyo-onboarding-v2.d.ts +212 -4
- package/dist/types/enyo-onboarding-v2.js +25 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
-
|
|
|
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
|
package/dist/cjs/energy-app.cjs
CHANGED
|
@@ -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 */
|
package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.cjs
ADDED
|
@@ -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
|
+
}
|
package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.d.cts
ADDED
|
@@ -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;
|