@enyo-energy/energy-app-sdk 0.0.198 → 1.0.0

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 (32) hide show
  1. package/README.md +5 -3
  2. package/dist/cjs/energy-app-permission.type.cjs +6 -0
  3. package/dist/cjs/energy-app-permission.type.d.cts +11 -1
  4. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-utils.cjs +102 -0
  5. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-utils.d.cts +78 -0
  6. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.cjs +48 -28
  7. package/dist/cjs/implementations/energy-manager-settings/energy-manager-settings-validators.d.cts +9 -6
  8. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +36 -0
  9. package/dist/cjs/index.cjs +1 -0
  10. package/dist/cjs/index.d.cts +1 -0
  11. package/dist/cjs/types/enyo-energy-manager-settings.cjs +76 -11
  12. package/dist/cjs/types/enyo-energy-manager-settings.d.cts +102 -42
  13. package/dist/cjs/types/enyo-onboarding-v2.cjs +21 -0
  14. package/dist/cjs/types/enyo-onboarding-v2.d.cts +41 -1
  15. package/dist/cjs/version.cjs +1 -1
  16. package/dist/cjs/version.d.cts +1 -1
  17. package/dist/energy-app-permission.type.d.ts +11 -1
  18. package/dist/energy-app-permission.type.js +6 -0
  19. package/dist/implementations/energy-manager-settings/energy-manager-settings-utils.d.ts +78 -0
  20. package/dist/implementations/energy-manager-settings/energy-manager-settings-utils.js +98 -0
  21. package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.d.ts +9 -6
  22. package/dist/implementations/energy-manager-settings/energy-manager-settings-validators.js +49 -29
  23. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +37 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/types/enyo-energy-manager-settings.d.ts +102 -42
  27. package/dist/types/enyo-energy-manager-settings.js +75 -10
  28. package/dist/types/enyo-onboarding-v2.d.ts +41 -1
  29. package/dist/types/enyo-onboarding-v2.js +21 -0
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -1136,8 +1136,9 @@ A closed, SDK-defined set of user-facing controls the active energy manager hono
1136
1136
  | `BatteryControl` | boolean |
1137
1137
  | `BatteryChargingMode` | `paced` \| `immediate` |
1138
1138
  | `BatteryChargeFromGrid` | boolean |
1139
- | `BatteryDischargeWhileChargingWh` | number \| `null` (Wh allowance; `null`/unset = disabled) |
1140
- | `BlockBatteryDischargeWhileEvCharging` | boolean |
1139
+ | `BatteryEvDischargeMode` | `fixed-wh` \| `soc-limit` \| `intelligent` \| `block-discharge` \| `unmanaged` |
1140
+ | `BatteryEvDischargeFixedWh` | number, **Wh** (only under `fixed-wh`) |
1141
+ | `BatteryEvDischargeSocLimitPercent` | number, **%** floor (only under `soc-limit`) |
1141
1142
  | `HeatpumpControl` | boolean |
1142
1143
  | `HeatingRodControl` | boolean |
1143
1144
  | `HeatingRodMode` | `pv-surplus-only` \| `boost` |
@@ -1183,7 +1184,8 @@ em.unsubscribeEnergyManagerSettings(id);
1183
1184
  Three things to get right:
1184
1185
 
1185
1186
  - **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
+ - **How the battery may feed a charging EV is one mode plus its parameter.** `BatteryEvDischargeMode` picks the strategy; `BatteryEvDischargeFixedWh` and `BatteryEvDischargeSocLimitPercent` are read only under their own mode. `block-discharge` (actively hold the battery back) and `unmanaged` (do not intervene either way) are **not** synonyms.
1188
+ - **Eight settings are gated by another** (`heatingRodMode` needs `heatingRodControl`, `priceLimitCtPerKwh` only applies under `price-limit`, …). The tree is exported as `ENERGY_MANAGER_SETTING_DEPENDENCIES`, with two helpers over it: `getEnergyManagerSettingDependency(setting)` returns the direct gate and the value it must hold (or `null` for an ungated root), and `isEnergyManagerSettingActive(setting, values)` walks the whole chain — the gates are now two deep, so a Wh budget is live only when the mode is `fixed-wh` *and* battery control is on.
1187
1189
  - **`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
1190
 
1189
1191
  #### `useElectricityTariff(): EnergyAppElectricityTariff`
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EnergyAppPermissionTypeEnum = void 0;
4
+ /**
5
+ * All permissions an Energy App package can request.
6
+ *
7
+ * Source of truth for the permission catalogue — {@link EnergyAppPermissionType}
8
+ * is derived from it.
9
+ */
4
10
  var EnergyAppPermissionTypeEnum;
5
11
  (function (EnergyAppPermissionTypeEnum) {
6
12
  EnergyAppPermissionTypeEnum["RestrictedInternetAccess"] = "RestrictedInternetAccess";
@@ -1,4 +1,9 @@
1
- export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation' | 'Savings' | 'EpexSpotPrices' | 'FirmwareRegistry';
1
+ /**
2
+ * All permissions an Energy App package can request.
3
+ *
4
+ * Source of truth for the permission catalogue — {@link EnergyAppPermissionType}
5
+ * is derived from it.
6
+ */
2
7
  export declare enum EnergyAppPermissionTypeEnum {
3
8
  RestrictedInternetAccess = "RestrictedInternetAccess",
4
9
  NetworkDeviceDiscovery = "NetworkDeviceDiscovery",
@@ -52,3 +57,8 @@ export declare enum EnergyAppPermissionTypeEnum {
52
57
  EpexSpotPrices = "EpexSpotPrices",
53
58
  FirmwareRegistry = "FirmwareRegistry"
54
59
  }
60
+ /**
61
+ * String union of every permission name, derived from
62
+ * {@link EnergyAppPermissionTypeEnum} so both spellings stay interchangeable.
63
+ */
64
+ export type EnergyAppPermissionType = `${EnergyAppPermissionTypeEnum}`;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Lookups over the energy-manager settings gate tree.
4
+ *
5
+ * {@link ENERGY_MANAGER_SETTING_DEPENDENCIES} is the data; these are the two
6
+ * questions worth asking of it. Both exist so a cockpit, an energy manager and
7
+ * the validator answer them the same way instead of each restating the tree.
8
+ *
9
+ * The tree is not flat. `BatteryEvDischargeFixedWh` is gated on
10
+ * `BatteryEvDischargeMode`, which is itself gated on `BatteryControl` — so
11
+ * "which setting does this depend on" and "may this setting take effect right
12
+ * now" are genuinely different questions, and the second one has to walk.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.getEnergyManagerSettingDependency = getEnergyManagerSettingDependency;
16
+ exports.isEnergyManagerSettingActive = isEnergyManagerSettingActive;
17
+ const enyo_energy_manager_settings_js_1 = require("../../types/enyo-energy-manager-settings.cjs");
18
+ /**
19
+ * The setting a given setting depends on, and the value that gate must hold.
20
+ *
21
+ * Returns `null` for an ungated setting — `BatteryControl`, `HeatpumpControl`,
22
+ * `HeatingRodControl` and `ChargerControl` are the roots of the tree and depend
23
+ * on nothing.
24
+ *
25
+ * The gate's required *value* comes back with it, because the gate alone is not
26
+ * actionable: knowing `PriceLimitCtPerKwh` depends on `DefaultChargeMode` does
27
+ * not tell you it needs that mode to be `price-limit` specifically.
28
+ *
29
+ * Only the **direct** gate is returned, one level up. Use
30
+ * {@link isEnergyManagerSettingActive} to resolve a whole chain.
31
+ *
32
+ * @param setting - The setting to look up.
33
+ * @returns Its {@link EnergyManagerSettingDependency}, or `null` when ungated.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * getEnergyManagerSettingDependency(EnergyManagerSettingEnum.HeatingRodMode);
38
+ * // → {requires: EnergyManagerSettingEnum.HeatingRodControl, equals: true}
39
+ *
40
+ * getEnergyManagerSettingDependency(EnergyManagerSettingEnum.BatteryEvDischargeFixedWh);
41
+ * // → {requires: EnergyManagerSettingEnum.BatteryEvDischargeMode, equals: 'fixed-wh'}
42
+ *
43
+ * getEnergyManagerSettingDependency(EnergyManagerSettingEnum.BatteryControl);
44
+ * // → null
45
+ * ```
46
+ */
47
+ function getEnergyManagerSettingDependency(setting) {
48
+ return enyo_energy_manager_settings_js_1.ENERGY_MANAGER_SETTING_DEPENDENCIES[setting] ?? null;
49
+ }
50
+ /**
51
+ * Whether a setting can take effect given the values currently stored — the
52
+ * question a cockpit asks before rendering a control, and an energy manager asks
53
+ * before acting on one.
54
+ *
55
+ * Walks the whole chain, not just the direct gate: a fixed-Wh budget is live
56
+ * only when the mode is `fixed-wh` **and** battery control is on. Checking one
57
+ * level would report a budget as active under a mode whose own gate is closed.
58
+ *
59
+ * An ungated setting is always active. A gate that is unset (`undefined`) counts
60
+ * as not holding — consistent with the rest of this model, where absent is
61
+ * neither `true` nor `false` but "no choice was made".
62
+ *
63
+ * Says nothing about whether the setting is *supported*; check
64
+ * {@link EnergyManagerSettingsState.supported} for that. A setting can be
65
+ * perfectly active and still not implemented by the installed energy manager.
66
+ *
67
+ * @param setting - The setting to test.
68
+ * @param values - The currently stored values.
69
+ * @returns True when every gate above this setting holds.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const values = {
74
+ * batteryControl: true,
75
+ * batteryEvDischargeMode: EnergyManagerBatteryEvDischargeModeEnum.SocLimit,
76
+ * };
77
+ *
78
+ * isEnergyManagerSettingActive(EnergyManagerSettingEnum.BatteryEvDischargeSocLimitPercent, values);
79
+ * // → true
80
+ * isEnergyManagerSettingActive(EnergyManagerSettingEnum.BatteryEvDischargeFixedWh, values);
81
+ * // → false — the mode is soc-limit, so a Wh budget could never be spent
82
+ * ```
83
+ */
84
+ function isEnergyManagerSettingActive(setting, values) {
85
+ // The tree is small and authored by hand, but a cycle would hang the walk —
86
+ // bound it by the number of settings rather than trusting the data.
87
+ const seen = new Set();
88
+ let current = setting;
89
+ while (current) {
90
+ if (seen.has(current))
91
+ return false;
92
+ seen.add(current);
93
+ const dependency = getEnergyManagerSettingDependency(current);
94
+ if (!dependency)
95
+ return true;
96
+ const [key] = enyo_energy_manager_settings_js_1.ENERGY_MANAGER_SETTING_VALUE_KEYS[dependency.requires];
97
+ if (!key || values[key] !== dependency.equals)
98
+ return false;
99
+ current = dependency.requires;
100
+ }
101
+ return true;
102
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Lookups over the energy-manager settings gate tree.
3
+ *
4
+ * {@link ENERGY_MANAGER_SETTING_DEPENDENCIES} is the data; these are the two
5
+ * questions worth asking of it. Both exist so a cockpit, an energy manager and
6
+ * the validator answer them the same way instead of each restating the tree.
7
+ *
8
+ * The tree is not flat. `BatteryEvDischargeFixedWh` is gated on
9
+ * `BatteryEvDischargeMode`, which is itself gated on `BatteryControl` — so
10
+ * "which setting does this depend on" and "may this setting take effect right
11
+ * now" are genuinely different questions, and the second one has to walk.
12
+ */
13
+ import type { EnergyManagerSettingDependency, EnergyManagerSettingEnum, EnergyManagerSettingValues } from '../../types/enyo-energy-manager-settings.cjs';
14
+ /**
15
+ * The setting a given setting depends on, and the value that gate must hold.
16
+ *
17
+ * Returns `null` for an ungated setting — `BatteryControl`, `HeatpumpControl`,
18
+ * `HeatingRodControl` and `ChargerControl` are the roots of the tree and depend
19
+ * on nothing.
20
+ *
21
+ * The gate's required *value* comes back with it, because the gate alone is not
22
+ * actionable: knowing `PriceLimitCtPerKwh` depends on `DefaultChargeMode` does
23
+ * not tell you it needs that mode to be `price-limit` specifically.
24
+ *
25
+ * Only the **direct** gate is returned, one level up. Use
26
+ * {@link isEnergyManagerSettingActive} to resolve a whole chain.
27
+ *
28
+ * @param setting - The setting to look up.
29
+ * @returns Its {@link EnergyManagerSettingDependency}, or `null` when ungated.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * getEnergyManagerSettingDependency(EnergyManagerSettingEnum.HeatingRodMode);
34
+ * // → {requires: EnergyManagerSettingEnum.HeatingRodControl, equals: true}
35
+ *
36
+ * getEnergyManagerSettingDependency(EnergyManagerSettingEnum.BatteryEvDischargeFixedWh);
37
+ * // → {requires: EnergyManagerSettingEnum.BatteryEvDischargeMode, equals: 'fixed-wh'}
38
+ *
39
+ * getEnergyManagerSettingDependency(EnergyManagerSettingEnum.BatteryControl);
40
+ * // → null
41
+ * ```
42
+ */
43
+ export declare function getEnergyManagerSettingDependency(setting: EnergyManagerSettingEnum): EnergyManagerSettingDependency | null;
44
+ /**
45
+ * Whether a setting can take effect given the values currently stored — the
46
+ * question a cockpit asks before rendering a control, and an energy manager asks
47
+ * before acting on one.
48
+ *
49
+ * Walks the whole chain, not just the direct gate: a fixed-Wh budget is live
50
+ * only when the mode is `fixed-wh` **and** battery control is on. Checking one
51
+ * level would report a budget as active under a mode whose own gate is closed.
52
+ *
53
+ * An ungated setting is always active. A gate that is unset (`undefined`) counts
54
+ * as not holding — consistent with the rest of this model, where absent is
55
+ * neither `true` nor `false` but "no choice was made".
56
+ *
57
+ * Says nothing about whether the setting is *supported*; check
58
+ * {@link EnergyManagerSettingsState.supported} for that. A setting can be
59
+ * perfectly active and still not implemented by the installed energy manager.
60
+ *
61
+ * @param setting - The setting to test.
62
+ * @param values - The currently stored values.
63
+ * @returns True when every gate above this setting holds.
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const values = {
68
+ * batteryControl: true,
69
+ * batteryEvDischargeMode: EnergyManagerBatteryEvDischargeModeEnum.SocLimit,
70
+ * };
71
+ *
72
+ * isEnergyManagerSettingActive(EnergyManagerSettingEnum.BatteryEvDischargeSocLimitPercent, values);
73
+ * // → true
74
+ * isEnergyManagerSettingActive(EnergyManagerSettingEnum.BatteryEvDischargeFixedWh, values);
75
+ * // → false — the mode is soc-limit, so a Wh budget could never be spent
76
+ * ```
77
+ */
78
+ export declare function isEnergyManagerSettingActive(setting: EnergyManagerSettingEnum, values: EnergyManagerSettingValues): boolean;
@@ -59,15 +59,16 @@ const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
59
59
  const PRICE_LIMIT_MIN_CT = -100;
60
60
  const PRICE_LIMIT_MAX_CT = 200;
61
61
  /**
62
- * Below this, a {@link EnergyManagerSettingValues.batteryDischargeWhileChargingWh}
62
+ * Below this, a {@link EnergyManagerSettingValues.batteryEvDischargeFixedWh}
63
63
  * budget is almost certainly kWh entered into a Wh field.
64
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.
65
+ * `10` would be ten watt-hours — a rounding error against any house battery,
66
+ * where `10` kWh was plainly meant. `0` is handled separately: it is legal, and
67
+ * warned about for duplicating mode `block-discharge` rather than for being a unit slip.
69
68
  */
70
69
  const DISCHARGE_BUDGET_MIN_PLAUSIBLE_WH = 100;
70
+ /** Every {@link EnergyManagerBatteryEvDischargeModeEnum} value. */
71
+ const EV_DISCHARGE_MODES = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerBatteryEvDischargeModeEnum));
71
72
  /** Value enums checked per field, for the enum-valued settings. */
72
73
  const BATTERY_CHARGING_MODES = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerBatteryChargingModeEnum));
73
74
  const HEATING_ROD_MODES = new Set(Object.values(enyo_energy_manager_settings_js_1.EnergyManagerHeatingRodModeEnum));
@@ -115,12 +116,15 @@ function gateValue(values, setting) {
115
116
  * zone the runtime resolves (**error**).
116
117
  * - `priceLimitCtPerKwh` is a finite number (**error**), and lies in a plausible
117
118
  * 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
- * - `blockBatteryDischargeWhileEvCharging` does not sit alongside a positive
122
- * `batteryDischargeWhileChargingWh` budget (**warning** — the block overrides
123
- * the budget, so it could never be spent).
119
+ * - `batteryEvDischargeMode` holds an enum member (**error**).
120
+ * - `batteryEvDischargeFixedWh` is a finite non-negative number (**error**), and
121
+ * is neither `0` nor a negligible positive figure (**warning** — the first
122
+ * duplicates mode `block-discharge`, the second catches a kWh value in a Wh field).
123
+ * - `batteryEvDischargeSocLimitPercent` is a finite number in 0…100 (**error**),
124
+ * and not `100` (**warning** — a floor nothing can discharge past).
125
+ * - A parameter belonging to a mode other than the selected one is flagged by
126
+ * the gate check below, since {@link ENERGY_MANAGER_SETTING_DEPENDENCIES}
127
+ * gates each on its own mode.
124
128
  * - A value stored for an unsupported setting (**warning** — the energy manager
125
129
  * will ignore it).
126
130
  * - A value stored while its gate does not hold (**warning** — it can never take
@@ -162,8 +166,7 @@ function validateEnergyManagerSettingsState(state) {
162
166
  }
163
167
  const values = state.values ?? {};
164
168
  // ── value formats ──────────────────────────────────────────────────────
165
- for (const key of ['batteryControl', 'batteryChargeFromGrid',
166
- 'blockBatteryDischargeWhileEvCharging', 'heatpumpControl',
169
+ for (const key of ['batteryControl', 'batteryChargeFromGrid', 'heatpumpControl',
167
170
  'heatingRodControl', 'chargerControl']) {
168
171
  if (values[key] !== undefined && typeof values[key] !== 'boolean') {
169
172
  errors.push(`\`${key}\` must be a boolean when set.`);
@@ -191,27 +194,44 @@ function validateEnergyManagerSettingsState(state) {
191
194
  'cents per kWh, not EUR per kWh.');
192
195
  }
193
196
  }
194
- if (values.batteryDischargeWhileChargingWh !== undefined
195
- && values.batteryDischargeWhileChargingWh !== null) {
196
- const budget = values.batteryDischargeWhileChargingWh;
197
+ if (values.batteryEvDischargeMode !== undefined
198
+ && !EV_DISCHARGE_MODES.has(values.batteryEvDischargeMode)) {
199
+ errors.push(`\`batteryEvDischargeMode\` "${values.batteryEvDischargeMode}" is not an ` +
200
+ 'EnergyManagerBatteryEvDischargeModeEnum member.');
201
+ }
202
+ if (values.batteryEvDischargeFixedWh !== undefined) {
203
+ const budget = values.batteryEvDischargeFixedWh;
197
204
  if (typeof budget !== 'number' || !Number.isFinite(budget)) {
198
- errors.push('`batteryDischargeWhileChargingWh` must be a finite number or `null` when set.');
205
+ errors.push('`batteryEvDischargeFixedWh` must be a finite number when set.');
199
206
  }
200
207
  else if (budget < 0) {
201
- errors.push(`\`batteryDischargeWhileChargingWh\` is ${budget}; a discharge budget cannot be ` +
202
- 'negative. Use `null` to disable the transfer entirely.');
208
+ errors.push(`\`batteryEvDischargeFixedWh\` is ${budget}; a budget cannot be negative. ` +
209
+ `Use mode "${enyo_energy_manager_settings_js_1.EnergyManagerBatteryEvDischargeModeEnum.BlockDischarge}" to forbid the transfer.`);
210
+ }
211
+ else if (budget === 0) {
212
+ warnings.push('`batteryEvDischargeFixedWh` is 0, which says the same as mode ' +
213
+ `"${enyo_energy_manager_settings_js_1.EnergyManagerBatteryEvDischargeModeEnum.BlockDischarge}" — prefer the mode, which says it ` +
214
+ 'where a reader will look.');
203
215
  }
204
- else if (budget > 0 && budget < DISCHARGE_BUDGET_MIN_PLAUSIBLE_WH) {
205
- warnings.push(`\`batteryDischargeWhileChargingWh\` is ${budget} Wh, which is negligible against ` +
206
- 'any house battery — this field is in watt-hours, not kilowatt-hours.');
216
+ else if (budget < DISCHARGE_BUDGET_MIN_PLAUSIBLE_WH) {
217
+ warnings.push(`\`batteryEvDischargeFixedWh\` is ${budget} Wh, which is negligible against any ` +
218
+ 'house battery — this field is in watt-hours, not kilowatt-hours.');
207
219
  }
208
220
  }
209
- if (values.blockBatteryDischargeWhileEvCharging === true
210
- && typeof values.batteryDischargeWhileChargingWh === 'number'
211
- && values.batteryDischargeWhileChargingWh > 0) {
212
- warnings.push('`blockBatteryDischargeWhileEvCharging` is true, which closes the tap entirely, so the ' +
213
- `\`batteryDischargeWhileChargingWh\` budget of ${values.batteryDischargeWhileChargingWh} Wh ` +
214
- 'can never be spent. Clear one of the two.');
221
+ if (values.batteryEvDischargeSocLimitPercent !== undefined) {
222
+ const soc = values.batteryEvDischargeSocLimitPercent;
223
+ if (typeof soc !== 'number' || !Number.isFinite(soc)) {
224
+ errors.push('`batteryEvDischargeSocLimitPercent` must be a finite number when set.');
225
+ }
226
+ else if (soc < 0 || soc > 100) {
227
+ errors.push(`\`batteryEvDischargeSocLimitPercent\` is ${soc}; it is a percentage and must lie ` +
228
+ 'between 0 and 100.');
229
+ }
230
+ else if (soc === 100) {
231
+ warnings.push('`batteryEvDischargeSocLimitPercent` is 100, a floor the battery is already at, so ' +
232
+ `nothing can ever be discharged — mode "${enyo_energy_manager_settings_js_1.EnergyManagerBatteryEvDischargeModeEnum.BlockDischarge}" ` +
233
+ 'states that intent directly.');
234
+ }
215
235
  }
216
236
  if (values.costOptimizedTargetTime !== undefined) {
217
237
  if (typeof values.costOptimizedTargetTime !== 'string'
@@ -56,12 +56,15 @@ export interface EnergyManagerSettingsValidationResult {
56
56
  * zone the runtime resolves (**error**).
57
57
  * - `priceLimitCtPerKwh` is a finite number (**error**), and lies in a plausible
58
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
- * - `blockBatteryDischargeWhileEvCharging` does not sit alongside a positive
63
- * `batteryDischargeWhileChargingWh` budget (**warning** — the block overrides
64
- * the budget, so it could never be spent).
59
+ * - `batteryEvDischargeMode` holds an enum member (**error**).
60
+ * - `batteryEvDischargeFixedWh` is a finite non-negative number (**error**), and
61
+ * is neither `0` nor a negligible positive figure (**warning** — the first
62
+ * duplicates mode `block-discharge`, the second catches a kWh value in a Wh field).
63
+ * - `batteryEvDischargeSocLimitPercent` is a finite number in 0…100 (**error**),
64
+ * and not `100` (**warning** — a floor nothing can discharge past).
65
+ * - A parameter belonging to a mode other than the selected one is flagged by
66
+ * the gate check below, since {@link ENERGY_MANAGER_SETTING_DEPENDENCIES}
67
+ * gates each on its own mode.
65
68
  * - A value stored for an unsupported setting (**warning** — the energy manager
66
69
  * will ignore it).
67
70
  * - A value stored while its gate does not hold (**warning** — it can never take
@@ -147,6 +147,7 @@ function validateOnboardingGuideV2(guide, context) {
147
147
  `"${enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo}" when enyo takes the setup over.`);
148
148
  }
149
149
  validateNetworkScanFlag(guide, warnings);
150
+ validateApplianceBinding(guide, errors, warnings);
150
151
  return { ok: errors.length === 0, errors, warnings };
151
152
  }
152
153
  /**
@@ -645,6 +646,41 @@ function validateAdditionalSetupBlocks(step, at, errors, warnings) {
645
646
  }
646
647
  }
647
648
  }
649
+ /**
650
+ * Checks {@link EnyoOnboardingV2Guide.applianceId} against the guide's
651
+ * {@link EnyoOnboardingV2Guide.startVariant}.
652
+ *
653
+ * The binding is required on {@link EnyoOnboardingV2StartVariant.Maintenance}
654
+ * and meaningless everywhere else. A maintenance run services an appliance that
655
+ * already exists, so a guide that does not name one cannot be bound to anything
656
+ * — that is a blocking error, not advice. The reverse is only a warning: an
657
+ * installation guide carrying an appliance id is harmless, the host ignores it,
658
+ * but it is a sign the wrong variant was chosen.
659
+ *
660
+ * @param guide - The guide being validated.
661
+ * @param errors - Collector for blocking problems.
662
+ * @param warnings - Collector for advisory problems.
663
+ */
664
+ function validateApplianceBinding(guide, errors, warnings) {
665
+ const isMaintenance = guide.startVariant === enyo_onboarding_v2_js_1.EnyoOnboardingV2StartVariant.Maintenance;
666
+ const applianceId = typeof guide.applianceId === 'string' ? guide.applianceId.trim() : undefined;
667
+ if (isMaintenance) {
668
+ if (!applianceId) {
669
+ errors.push('`applianceId` is required on a `' +
670
+ `${enyo_onboarding_v2_js_1.EnyoOnboardingV2StartVariant.Maintenance}` +
671
+ '` guide — a maintenance run services an appliance that already exists, so the ' +
672
+ 'guide must name it.');
673
+ }
674
+ return;
675
+ }
676
+ if (applianceId) {
677
+ warnings.push('`applianceId` is set on a `' +
678
+ `${guide.startVariant}` +
679
+ '` guide and is ignored — only a `' +
680
+ `${enyo_onboarding_v2_js_1.EnyoOnboardingV2StartVariant.Maintenance}` +
681
+ '` guide binds an existing appliance.');
682
+ }
683
+ }
648
684
  /**
649
685
  * Checks {@link EnyoOnboardingV2Guide.requiresNetworkScan} against what the guide
650
686
  * actually does.
@@ -42,6 +42,7 @@ __exportStar(require("./types/enyo-energy-manager.cjs"), exports);
42
42
  __exportStar(require("./packages/energy-app-energy-manager.cjs"), exports);
43
43
  __exportStar(require("./types/enyo-energy-manager-settings.cjs"), exports);
44
44
  __exportStar(require("./implementations/energy-manager-settings/energy-manager-settings-validators.cjs"), exports);
45
+ __exportStar(require("./implementations/energy-manager-settings/energy-manager-settings-utils.cjs"), exports);
45
46
  __exportStar(require("./types/enyo-electricity-tariff.cjs"), exports);
46
47
  __exportStar(require("./packages/energy-app-electricity-tariff.cjs"), exports);
47
48
  __exportStar(require("./types/enyo-pv-forecast.cjs"), exports);
@@ -26,6 +26,7 @@ export * from './types/enyo-energy-manager.cjs';
26
26
  export * from './packages/energy-app-energy-manager.cjs';
27
27
  export * from './types/enyo-energy-manager-settings.cjs';
28
28
  export * from './implementations/energy-manager-settings/energy-manager-settings-validators.cjs';
29
+ export * from './implementations/energy-manager-settings/energy-manager-settings-utils.cjs';
29
30
  export * from './types/enyo-electricity-tariff.cjs';
30
31
  export * from './packages/energy-app-electricity-tariff.cjs';
31
32
  export * from './types/enyo-pv-forecast.cjs';
@@ -27,7 +27,7 @@
27
27
  * (`../implementations/energy-manager-settings/energy-manager-settings-validators.ts`).
28
28
  */
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.ENERGY_MANAGER_SETTING_VALUE_KEYS = exports.ENERGY_MANAGER_SETTING_DEPENDENCIES = exports.EnergyManagerHeatingRodModeEnum = exports.EnergyManagerBatteryChargingModeEnum = exports.EnergyManagerSettingEnum = void 0;
30
+ exports.ENERGY_MANAGER_SETTING_VALUE_KEYS = exports.ENERGY_MANAGER_SETTING_DEPENDENCIES = exports.EnergyManagerHeatingRodModeEnum = exports.EnergyManagerBatteryEvDischargeModeEnum = exports.EnergyManagerBatteryChargingModeEnum = exports.EnergyManagerSettingEnum = void 0;
31
31
  /**
32
32
  * The general settings an energy manager can honour.
33
33
  *
@@ -44,10 +44,12 @@ var EnergyManagerSettingEnum;
44
44
  EnergyManagerSettingEnum["BatteryChargingMode"] = "battery-charging-mode";
45
45
  /** Whether the battery may be charged from the grid, not only from PV surplus. */
46
46
  EnergyManagerSettingEnum["BatteryChargeFromGrid"] = "battery-charge-from-grid";
47
- /** How much energy the battery may discharge into a charging vehicle, in Wh. Unset/`null` disables it. */
48
- EnergyManagerSettingEnum["BatteryDischargeWhileChargingWh"] = "battery-discharge-while-charging-wh";
49
- /** Hard block on battery discharge into a charging vehicle. Overrides any Wh allowance. */
50
- EnergyManagerSettingEnum["BlockBatteryDischargeWhileEvCharging"] = "block-battery-discharge-while-ev-charging";
47
+ /** How the battery may feed a charging vehicle — see {@link EnergyManagerBatteryEvDischargeModeEnum}. */
48
+ EnergyManagerSettingEnum["BatteryEvDischargeMode"] = "battery-ev-discharge-mode";
49
+ /** The per-session budget, in Wh. Only under {@link EnergyManagerBatteryEvDischargeModeEnum.FixedWh}. */
50
+ EnergyManagerSettingEnum["BatteryEvDischargeFixedWh"] = "battery-ev-discharge-fixed-wh";
51
+ /** The SoC floor, in %. Only under {@link EnergyManagerBatteryEvDischargeModeEnum.SocLimit}. */
52
+ EnergyManagerSettingEnum["BatteryEvDischargeSocLimitPercent"] = "battery-ev-discharge-soc-limit-percent";
51
53
  // ── Heat pump ──────────────────────────────────────────────────────────
52
54
  /** Whether the energy manager may steer the heat pump. */
53
55
  EnergyManagerSettingEnum["HeatpumpControl"] = "heatpump-control";
@@ -86,6 +88,64 @@ var EnergyManagerBatteryChargingModeEnum;
86
88
  */
87
89
  EnergyManagerBatteryChargingModeEnum["Immediate"] = "immediate";
88
90
  })(EnergyManagerBatteryChargingModeEnum || (exports.EnergyManagerBatteryChargingModeEnum = EnergyManagerBatteryChargingModeEnum = {}));
91
+ /**
92
+ * How the house battery may feed a vehicle that is charging.
93
+ *
94
+ * Replaces the earlier pair of a boolean block and a nullable Wh budget. That
95
+ * shape could express "off" two ways and had no room for the answers installers
96
+ * actually want — drain to a floor, or let enyo work it out — so the strategy is
97
+ * now named once here and its parameter lives in a separate setting.
98
+ *
99
+ * Read {@link None} and {@link Unmanaged} carefully: they are not synonyms, and
100
+ * confusing them is the difference between the energy manager holding the
101
+ * battery back and it simply not looking.
102
+ */
103
+ var EnergyManagerBatteryEvDischargeModeEnum;
104
+ (function (EnergyManagerBatteryEvDischargeModeEnum) {
105
+ /**
106
+ * Allow a fixed budget per session, given by
107
+ * {@link EnergyManagerSettingValues.batteryEvDischargeFixedWh}.
108
+ *
109
+ * The plain answer to "how much of my house battery may go into the car" —
110
+ * an amount, spent and then stopped.
111
+ */
112
+ EnergyManagerBatteryEvDischargeModeEnum["FixedWh"] = "fixed-wh";
113
+ /**
114
+ * Discharge into the vehicle until the battery reaches the floor given by
115
+ * {@link EnergyManagerSettingValues.batteryEvDischargeSocLimitPercent}.
116
+ *
117
+ * The same intent as {@link FixedWh} expressed in the unit owners actually
118
+ * reason in — "keep half the battery for the house" survives a change of
119
+ * battery, where a watt-hour figure does not.
120
+ */
121
+ EnergyManagerBatteryEvDischargeModeEnum["SocLimit"] = "soc-limit";
122
+ /**
123
+ * Let enyo decide, session by session.
124
+ *
125
+ * The energy manager weighs price, forecast, house load and departure time
126
+ * instead of following a fixed rule. Nothing further to configure — the
127
+ * other two settings carry no meaning here.
128
+ */
129
+ EnergyManagerBatteryEvDischargeModeEnum["Intelligent"] = "intelligent";
130
+ /**
131
+ * Never discharge the battery into a vehicle. The energy manager actively
132
+ * holds it back.
133
+ *
134
+ * This is an instruction, not an absence of one — contrast {@link Unmanaged},
135
+ * which is the absence. Named for what it does rather than for the amount it
136
+ * permits, so that the two cannot be read as the same answer.
137
+ */
138
+ EnergyManagerBatteryEvDischargeModeEnum["BlockDischarge"] = "block-discharge";
139
+ /**
140
+ * The energy manager does not manage this flow at all.
141
+ *
142
+ * It neither permits nor prevents: whatever the hardware does on its own
143
+ * stands, and the battery may well end up feeding the car as a side effect.
144
+ * Choose this when something else owns the decision, never as a way to
145
+ * express "no discharge" — that is {@link BlockDischarge}.
146
+ */
147
+ EnergyManagerBatteryEvDischargeModeEnum["Unmanaged"] = "unmanaged";
148
+ })(EnergyManagerBatteryEvDischargeModeEnum || (exports.EnergyManagerBatteryEvDischargeModeEnum = EnergyManagerBatteryEvDischargeModeEnum = {}));
89
149
  /**
90
150
  * What the heating rod is allowed to do.
91
151
  *
@@ -131,13 +191,17 @@ exports.ENERGY_MANAGER_SETTING_DEPENDENCIES = {
131
191
  requires: EnergyManagerSettingEnum.BatteryControl,
132
192
  equals: true,
133
193
  },
134
- [EnergyManagerSettingEnum.BatteryDischargeWhileChargingWh]: {
194
+ [EnergyManagerSettingEnum.BatteryEvDischargeMode]: {
135
195
  requires: EnergyManagerSettingEnum.BatteryControl,
136
196
  equals: true,
137
197
  },
138
- [EnergyManagerSettingEnum.BlockBatteryDischargeWhileEvCharging]: {
139
- requires: EnergyManagerSettingEnum.BatteryControl,
140
- equals: true,
198
+ [EnergyManagerSettingEnum.BatteryEvDischargeFixedWh]: {
199
+ requires: EnergyManagerSettingEnum.BatteryEvDischargeMode,
200
+ equals: EnergyManagerBatteryEvDischargeModeEnum.FixedWh,
201
+ },
202
+ [EnergyManagerSettingEnum.BatteryEvDischargeSocLimitPercent]: {
203
+ requires: EnergyManagerSettingEnum.BatteryEvDischargeMode,
204
+ equals: EnergyManagerBatteryEvDischargeModeEnum.SocLimit,
141
205
  },
142
206
  [EnergyManagerSettingEnum.HeatingRodMode]: {
143
207
  requires: EnergyManagerSettingEnum.HeatingRodControl,
@@ -167,8 +231,9 @@ exports.ENERGY_MANAGER_SETTING_VALUE_KEYS = {
167
231
  [EnergyManagerSettingEnum.BatteryControl]: ['batteryControl'],
168
232
  [EnergyManagerSettingEnum.BatteryChargingMode]: ['batteryChargingMode'],
169
233
  [EnergyManagerSettingEnum.BatteryChargeFromGrid]: ['batteryChargeFromGrid'],
170
- [EnergyManagerSettingEnum.BatteryDischargeWhileChargingWh]: ['batteryDischargeWhileChargingWh'],
171
- [EnergyManagerSettingEnum.BlockBatteryDischargeWhileEvCharging]: ['blockBatteryDischargeWhileEvCharging'],
234
+ [EnergyManagerSettingEnum.BatteryEvDischargeMode]: ['batteryEvDischargeMode'],
235
+ [EnergyManagerSettingEnum.BatteryEvDischargeFixedWh]: ['batteryEvDischargeFixedWh'],
236
+ [EnergyManagerSettingEnum.BatteryEvDischargeSocLimitPercent]: ['batteryEvDischargeSocLimitPercent'],
172
237
  [EnergyManagerSettingEnum.HeatpumpControl]: ['heatpumpControl'],
173
238
  [EnergyManagerSettingEnum.HeatingRodControl]: ['heatingRodControl'],
174
239
  [EnergyManagerSettingEnum.HeatingRodMode]: ['heatingRodMode'],