@enyo-energy/energy-app-sdk 1.29.0 → 1.30.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.
package/README.md CHANGED
@@ -3522,33 +3522,58 @@ Both are for marking on the same power scale a consumer already draws from `maxC
3522
3522
 
3523
3523
  ### BatteryCommandForecast
3524
3524
 
3525
- Relative `{seconds, mode, powerW}` schedule where `mode` is one of `'charge'`, `'discharge'`, or `'auto'`. `auto` returns control to the appliance and must always carry `powerW = 0`.
3525
+ A discriminated union on `mode`:
3526
+
3527
+ - `mode: 'auto'` — hand the battery back to its own logic over the horizon; no schedule. May carry an optional `reason` (`EnyoDataBusCommandReason`) saying **why** nothing is prescribed — e.g. `PriceSpreadTooSmall` when trading would not pay. Set it only when every Auto slot of the horizon shares the same reason; when they differ, leave it out rather than guess. Without it, an empty Auto forecast tends to read as "no forecast".
3528
+ - `mode: 'schedule'` — follow `relativeSchedule`, a list of `{seconds, direction, powerW}` setpoints, where `direction` is `charge`, `discharge` or `idle`.
3526
3529
 
3527
3530
  The type is named `BatteryCommandForecast` to make the distinction with the existing [`BatteryForecast`](#batteryforecast) class (which forecasts state-of-charge from history) explicit.
3528
3531
 
3529
3532
  ```typescript
3530
3533
  import {
3534
+ ApplianceForecastResolutionEnum,
3531
3535
  BatteryCommandForecast,
3536
+ BatteryCommandForecastDirectionEnum,
3532
3537
  BatteryCommandForecastModeEnum,
3538
+ EnyoCurrencyEnum,
3539
+ EnyoDataBusCommandReasonCategoryEnum,
3540
+ EnyoDataBusCommandReasonTypeEnum,
3533
3541
  } from '@enyo-energy/energy-app-sdk';
3534
3542
 
3535
- const forecast: BatteryCommandForecast = {
3543
+ // A planned cadence:
3544
+ const scheduled: BatteryCommandForecast = {
3545
+ resolution: ApplianceForecastResolutionEnum.FifteenMinutes,
3546
+ mode: BatteryCommandForecastModeEnum.Schedule,
3536
3547
  relativeSchedule: [
3537
- { seconds: 0, mode: BatteryCommandForecastModeEnum.Charge, powerW: 3000 },
3538
- { seconds: 1800, mode: BatteryCommandForecastModeEnum.Discharge, powerW: 2500 },
3539
- { seconds: 3600, mode: BatteryCommandForecastModeEnum.Auto, powerW: 0 },
3548
+ { seconds: 0, direction: BatteryCommandForecastDirectionEnum.Charge, powerW: 3000 },
3549
+ { seconds: 1800, direction: BatteryCommandForecastDirectionEnum.Discharge, powerW: 2500 },
3550
+ { seconds: 3600, direction: BatteryCommandForecastDirectionEnum.Idle, powerW: 0 },
3540
3551
  ],
3541
3552
  estimatedSavings: { costSavings: 0.18, currency: 'EUR' },
3542
3553
  };
3554
+ await forecasts.publishBatteryForecast('battery-1', scheduled);
3543
3555
 
3544
- await forecasts.publishBatteryForecast('battery-1', forecast);
3556
+ // Nothing planned, and why:
3557
+ const auto: BatteryCommandForecast = {
3558
+ resolution: ApplianceForecastResolutionEnum.FifteenMinutes,
3559
+ mode: BatteryCommandForecastModeEnum.Auto,
3560
+ reason: {
3561
+ type: EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall,
3562
+ category: EnyoDataBusCommandReasonCategoryEnum.EnergyPrice,
3563
+ electricityPricePerKwh: 0.352,
3564
+ currency: EnyoCurrencyEnum.EUR,
3565
+ context: { arbitrage: { buyPricePerKwh: 0.352, breakEvenPricePerKwh: 0.527, sellPricePerKwh: 0.429, roundTripEfficiency: 0.757 } },
3566
+ },
3567
+ };
3568
+ await forecasts.publishBatteryForecast('battery-1', auto);
3545
3569
  ```
3546
3570
 
3547
- Per-entry invariants:
3571
+ Invariants:
3548
3572
 
3549
- - `seconds`: finite, non-negative; first entry `= 0`; subsequent entries strictly increasing.
3550
- - `mode`: one of `charge` / `discharge` / `auto`.
3551
- - `powerW`: finite, non-negative. **MUST be `0` when `mode === 'auto'`.**
3573
+ - `mode: 'auto'` must not carry a `relativeSchedule`. A `reason` must have a valid `type`; its `context.arbitrage` prices must be EUR/kWh within a plausible band (a ct value such as `35.2` is rejected), `roundTripEfficiency` must lie in (0, 1], and each window's start must lie before its end.
3574
+ - `seconds`: finite, non-negative; first entry `= 0`; strictly increasing; a multiple of the `resolution` step.
3575
+ - `direction`: one of `charge` / `discharge` / `idle`. `idle` hands the slot to the battery's own logic — to block charging or discharging, use that direction with `powerW = 0` instead.
3576
+ - `powerW`: finite, non-negative. **MUST be `0` when `direction === 'idle'`.**
3552
3577
 
3553
3578
  ### HeatpumpForecast
3554
3579
 
@@ -4190,6 +4215,14 @@ New reason types came with this surface, so a skipped row can say *why* instead
4190
4215
 
4191
4216
  **Battery first on PV surplus.** When a surplus consumer (EV on PV, heating rod, heat pump boost, surplus automation) is held back or reduced because the battery is charged first up to the owner's `batteryPvPriorityTargetSocPercent`, the energy manager states `BatteryPvPriority` (category `PvSurplus`) on that consumer: `context.batteryPvPriority.targetSocPercent` for the target, `socPercent` for the pack's current SoC and `powerW` for the surplus the battery is taking — e.g. "The battery charges to 40 % first (now 28 %)."
4192
4217
 
4218
+ **Battery trading: why or why not.** The `arbitrage` context (`EnyoDataBusCommandReasonArbitrageContext`) carries the trough-against-peak arithmetic behind a battery buy: `buyPricePerKwh` / `buyStartIso` / `buyEndIso`, `sellPricePerKwh` / `sellStartIso` / `sellEndIso`, `breakEvenPricePerKwh` (losses, wear and margin included) and `roundTripEfficiency` (a fraction, `0.757`). Prices are **EUR/kWh like every machine-readable price in the SDK** — `0.336`, not `33.6`. Three uses:
4219
+
4220
+ - `PriceSpreadTooSmall` (category `EnergyPrice`) — the battery stays on Auto because buying in the trough and using the energy at the peak would not pay back: "Charging at 35.2 ct/kWh only pays off from 52.7 ct/kWh, but the peak averages 42.9 ct/kWh." Set `electricityPricePerKwh` to the buy price and all the figures; on a flat horizon send it without `arbitrage`. The trough is not "too expensive" — the gap is too small — so this is not `ElectricityPriceAboveThreshold`. Where PV makes Auto mean self-consumption, `SelfConsumptionOptimization` stays the right type.
4221
+ - `ElectricityPriceBelowThreshold` on a grid charge — the buy and the peak it is for: "Charging at 33.6 ct for the 17:00 peak at 43.7 ct."
4222
+ - `ScheduledOptimization` on a price hold (`Discharge 0`) — `sellStartIso` / `sellPricePerKwh`: "Stored energy kept for 17:00 (43.7 ct)." Not `ElectricityPriceBelowThreshold`, which would say power is cheap for a command that stops the battery.
4223
+
4224
+ **Installation limits.** Power capped by a limit the owner or installer set — not the grid operator — carries `InstallationLimit` (category `Contention`) with `powerW` set to the ceiling and `context.installationLimit` = `{source, limitW}`, where `source` is `EnyoInstallationLimitSourceEnum.DesiredGridLimit` (`desiredKwLimit`) or `ChargerLimit` (`chargerLimitW`). `limitW` is in **W** even for the desired limit, which is configured in kW. `GridOperatorPowerLimitationActive` stays reserved for limits the grid operator imposed.
4225
+
4193
4226
  **Grid-optimised charging.** A cost-optimised session placed in, or waiting for, a low-load hour because of `gridOptimizedEvCharging` carries `GridLoadOptimized` (category `Schedule`) with `context.placement` (chosen window and deadline) and `context.gridLoad` (`epexPricePerKwh`, `currency`). The EPEX price is a grid-load proxy, not what the customer pays, so the text must not promise a saving — e.g. "Charging from 02:00, when the grid is less busy — ready by 07:00."
4194
4227
 
4195
4228
  **Battery vs. heating rod.** When the owner set `heatingRodBatteryDischargeSocLimitPercent`, two reason types (category `BatteryState`) say whether the battery feeds the rod: `BatterySupportsHeatingRod` while it may (with `powerW` for its contribution), `BatteryReservedFromHeatingRod` once it reaches the floor. Both carry `context.batteryToHeatingRod.socLimitPercent` and `socPercent` — e.g. "Heating rod no longer runs from the battery — it is at your 80 % limit."
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApplianceCommandForecastValidationError = void 0;
4
4
  exports.validateChargerForecast = validateChargerForecast;
5
5
  exports.validateBatteryCommandForecast = validateBatteryCommandForecast;
6
+ exports.validateBatteryForecastReason = validateBatteryForecastReason;
6
7
  exports.validateHeatpumpForecast = validateHeatpumpForecast;
7
8
  exports.validateChargerSchedule = validateChargerSchedule;
8
9
  exports.validateBatterySchedule = validateBatterySchedule;
@@ -69,7 +70,9 @@ function validateChargerForecast(forecast) {
69
70
  * {@link BatteryCommandForecastModeEnum}: when `mode = 'auto'` no
70
71
  * schedule is expected; when `mode = 'schedule'` the embedded
71
72
  * {@link BatteryCommandForecastScheduled.relativeSchedule} is validated
72
- * by {@link validateBatterySchedule}.
73
+ * by {@link validateBatterySchedule}. An Auto forecast's optional
74
+ * {@link BatteryCommandForecastAuto.reason} is checked by
75
+ * {@link validateBatteryForecastReason}.
73
76
  */
74
77
  function validateBatteryCommandForecast(forecast) {
75
78
  if (!forecast || typeof forecast !== 'object') {
@@ -84,10 +87,75 @@ function validateBatteryCommandForecast(forecast) {
84
87
  if (forecast.relativeSchedule !== undefined) {
85
88
  throw new ApplianceCommandForecastValidationError("BatteryCommandForecast with mode='auto' must not carry a relativeSchedule.");
86
89
  }
90
+ if (forecast.reason !== undefined) {
91
+ validateBatteryForecastReason(forecast.reason, 'BatteryCommandForecast.reason');
92
+ }
87
93
  return;
88
94
  }
89
95
  validateBatterySchedule(forecast.relativeSchedule, forecast.resolution);
90
96
  }
97
+ /**
98
+ * Lowest and highest plausible price per kWh in a reason's `arbitrage`
99
+ * context, in EUR/kWh. Wide enough for negative and spiking dynamic prices,
100
+ * narrow enough to catch a ct value (`33.6`) in a EUR field.
101
+ */
102
+ const ARBITRAGE_PRICE_MIN_PER_KWH = -1;
103
+ const ARBITRAGE_PRICE_MAX_PER_KWH = 5;
104
+ /**
105
+ * Validates the reason attached to an Auto {@link BatteryCommandForecast}.
106
+ * Throws on the first violation.
107
+ *
108
+ * Checks that the reason is an object whose `type` is an
109
+ * {@link EnyoDataBusCommandReasonTypeEnum} member and, when it carries
110
+ * `context.arbitrage`, that:
111
+ * - every price is a finite EUR/kWh figure within a plausible band (catches
112
+ * ct in a EUR field),
113
+ * - `roundTripEfficiency` lies in (0, 1] (catches `75.7` for `0.757`),
114
+ * - each window's start lies before its end, where both are given.
115
+ *
116
+ * @param reason - The reason to validate.
117
+ * @param fieldName - Field path used in error messages.
118
+ * @throws {ApplianceCommandForecastValidationError} On the first violation.
119
+ */
120
+ function validateBatteryForecastReason(reason, fieldName) {
121
+ if (!reason || typeof reason !== 'object') {
122
+ throw new ApplianceCommandForecastValidationError(`${fieldName} must be an object.`);
123
+ }
124
+ validateEnumField(reason.type, Object.values(enyo_data_bus_value_js_1.EnyoDataBusCommandReasonTypeEnum), `${fieldName}.type`, true);
125
+ const arbitrage = reason.context?.arbitrage;
126
+ if (arbitrage === undefined)
127
+ return;
128
+ for (const key of ['buyPricePerKwh', 'sellPricePerKwh', 'breakEvenPricePerKwh']) {
129
+ const price = arbitrage[key];
130
+ if (price === undefined)
131
+ continue;
132
+ if (!Number.isFinite(price)) {
133
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${key} must be a finite number; got ${price}.`);
134
+ }
135
+ if (price < ARBITRAGE_PRICE_MIN_PER_KWH || price > ARBITRAGE_PRICE_MAX_PER_KWH) {
136
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${key} is ${price}, outside ` +
137
+ `${ARBITRAGE_PRICE_MIN_PER_KWH}…${ARBITRAGE_PRICE_MAX_PER_KWH} — prices are per kWh in ` +
138
+ 'the reason\'s currency (EUR/kWh), not ct/kWh.');
139
+ }
140
+ }
141
+ const efficiency = arbitrage.roundTripEfficiency;
142
+ if (efficiency !== undefined && (!Number.isFinite(efficiency) || efficiency <= 0 || efficiency > 1)) {
143
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.roundTripEfficiency is ${efficiency}; it is a fraction in (0, 1], ` +
144
+ 'e.g. 0.757 rather than 75.7.');
145
+ }
146
+ for (const [start, end] of [['buyStartIso', 'buyEndIso'], ['sellStartIso', 'sellEndIso']]) {
147
+ const startIso = arbitrage[start];
148
+ const endIso = arbitrage[end];
149
+ for (const [key, iso] of [[start, startIso], [end, endIso]]) {
150
+ if (iso !== undefined && Number.isNaN(Date.parse(iso))) {
151
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${key} is not an ISO 8601 timestamp: ${iso}.`);
152
+ }
153
+ }
154
+ if (startIso !== undefined && endIso !== undefined && Date.parse(startIso) >= Date.parse(endIso)) {
155
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${start} must lie before ${end}.`);
156
+ }
157
+ }
158
+ }
91
159
  /**
92
160
  * Validates a {@link HeatpumpForecast}. Throws on the first violation —
93
161
  * the error message names the offending field / index.
@@ -1,4 +1,5 @@
1
1
  import { AirConditioningForecast, AirConditioningForecastScheduleEntry, ApplianceForecastResolutionEnum, BatteryCommandForecast, BatteryCommandForecastScheduleEntry, ChargerForecast, ChargerForecastScheduleEntry, HeatingRodForecast, HeatingRodForecastScheduleEntry, HeatpumpForecast, HeatpumpForecastScheduleEntry, SmartPlugForecast, SmartPlugForecastScheduleEntry } from '../../types/enyo-appliance-command-forecast.cjs';
2
+ import { type EnyoDataBusCommandReason } from '../../types/enyo-data-bus-value.cjs';
2
3
  /**
3
4
  * Thrown when a forecast payload passed to one of the validators (or to
4
5
  * {@link EnergyAppApplianceEnergyManagerForecast.publishChargerForecast}
@@ -23,9 +24,28 @@ export declare function validateChargerForecast(forecast: ChargerForecast): void
23
24
  * {@link BatteryCommandForecastModeEnum}: when `mode = 'auto'` no
24
25
  * schedule is expected; when `mode = 'schedule'` the embedded
25
26
  * {@link BatteryCommandForecastScheduled.relativeSchedule} is validated
26
- * by {@link validateBatterySchedule}.
27
+ * by {@link validateBatterySchedule}. An Auto forecast's optional
28
+ * {@link BatteryCommandForecastAuto.reason} is checked by
29
+ * {@link validateBatteryForecastReason}.
27
30
  */
28
31
  export declare function validateBatteryCommandForecast(forecast: BatteryCommandForecast): void;
32
+ /**
33
+ * Validates the reason attached to an Auto {@link BatteryCommandForecast}.
34
+ * Throws on the first violation.
35
+ *
36
+ * Checks that the reason is an object whose `type` is an
37
+ * {@link EnyoDataBusCommandReasonTypeEnum} member and, when it carries
38
+ * `context.arbitrage`, that:
39
+ * - every price is a finite EUR/kWh figure within a plausible band (catches
40
+ * ct in a EUR field),
41
+ * - `roundTripEfficiency` lies in (0, 1] (catches `75.7` for `0.757`),
42
+ * - each window's start lies before its end, where both are given.
43
+ *
44
+ * @param reason - The reason to validate.
45
+ * @param fieldName - Field path used in error messages.
46
+ * @throws {ApplianceCommandForecastValidationError} On the first violation.
47
+ */
48
+ export declare function validateBatteryForecastReason(reason: EnyoDataBusCommandReason, fieldName: string): void;
29
49
  /**
30
50
  * Validates a {@link HeatpumpForecast}. Throws on the first violation —
31
51
  * the error message names the offending field / index.
@@ -47,6 +47,7 @@
47
47
  import { EnyoAirConditioningApplianceModeEnum, EnyoAirConditioningOptimizationModeEnum } from './enyo-air-conditioning-appliance.cjs';
48
48
  import { EnyoAutomationTriggerTypeEnum } from './enyo-automation.cjs';
49
49
  import { EnyoChargeModeEnum } from './enyo-data-bus-value.cjs';
50
+ import type { EnyoDataBusCommandReason } from './enyo-data-bus-value.cjs';
50
51
  import { EnyoSmartPlugApplianceStateEnum } from './enyo-smart-plug-appliance.cjs';
51
52
  /**
52
53
  * Optional metadata describing the estimated savings of a forecasted
@@ -262,6 +263,17 @@ export interface BatteryCommandForecastScheduleEntry {
262
263
  export interface BatteryCommandForecastAuto extends ApplianceForecastMetadata {
263
264
  /** Discriminator — set to {@link BatteryCommandForecastModeEnum.Auto}. */
264
265
  mode: BatteryCommandForecastModeEnum.Auto;
266
+ /**
267
+ * Why no schedule is prescribed over the horizon — e.g.
268
+ * {@link EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall} when
269
+ * trading would not pay.
270
+ *
271
+ * Set it only when every Auto slot of the horizon shares the same reason;
272
+ * when they differ, leave it out rather than pick one. Optional, so a
273
+ * consumer that does not read it is unaffected — but without it an empty
274
+ * Auto forecast tends to read as "no forecast".
275
+ */
276
+ reason?: EnyoDataBusCommandReason;
265
277
  }
266
278
  /**
267
279
  * Forecast variant that declares an explicit relative schedule the
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EnyoPowerSourceEnum = exports.EnyoHeatpumpHeatSourceEnum = exports.EnyoHeatpumpControlPurposeEnum = exports.EnyoChargingProfileTypeEnum = exports.EnyoCommandAcknowledgeAnswerEnum = exports.EnyoStorageControlDirectionEnum = exports.EnyoStorageControlModeEnum = exports.EnyoStorageScheduleDirectionEnum = exports.EnyoStorageScheduleModeEnum = exports.EnyoChargingLimitRequestResultEnum = exports.EnyoDataBusMessageEnum = exports.EnyoChargeInitiatorEnum = exports.EnyoPriceLimitModeEnum = exports.EnyoChargeModeEnum = exports.EnyoChargingStopReason = exports.EnyoChargingMeterValueContext = exports.EnyoStringStateEnum = exports.EnyoHeatingRodStateEnum = exports.EnyoInverterStateEnum = exports.EnyoBatteryStateEnum = exports.EnyoGridOperatorLimitTypeEnum = exports.EnyoSwitchingProtectionHoldEnum = exports.EnyoBatteryToEvHoldTriggerEnum = exports.EnyoDataBusCommandReasonCategoryEnum = exports.EnyoDataBusCommandReasonTypeEnum = void 0;
3
+ exports.EnyoPowerSourceEnum = exports.EnyoHeatpumpHeatSourceEnum = exports.EnyoHeatpumpControlPurposeEnum = exports.EnyoChargingProfileTypeEnum = exports.EnyoCommandAcknowledgeAnswerEnum = exports.EnyoStorageControlDirectionEnum = exports.EnyoStorageControlModeEnum = exports.EnyoStorageScheduleDirectionEnum = exports.EnyoStorageScheduleModeEnum = exports.EnyoChargingLimitRequestResultEnum = exports.EnyoDataBusMessageEnum = exports.EnyoChargeInitiatorEnum = exports.EnyoPriceLimitModeEnum = exports.EnyoChargeModeEnum = exports.EnyoChargingStopReason = exports.EnyoChargingMeterValueContext = exports.EnyoStringStateEnum = exports.EnyoHeatingRodStateEnum = exports.EnyoInverterStateEnum = exports.EnyoBatteryStateEnum = exports.EnyoGridOperatorLimitTypeEnum = exports.EnyoSwitchingProtectionHoldEnum = exports.EnyoBatteryToEvHoldTriggerEnum = exports.EnyoInstallationLimitSourceEnum = exports.EnyoDataBusCommandReasonCategoryEnum = exports.EnyoDataBusCommandReasonTypeEnum = void 0;
4
4
  /**
5
5
  * Enum representing the reason type for why a data bus command was issued.
6
6
  * Used to attach context to commands for logging, debugging, and UI display.
@@ -14,6 +14,12 @@ var EnyoDataBusCommandReasonTypeEnum;
14
14
  * cost per kWh of delivered heat rather than by price alone, add
15
15
  * {@link EnyoDataBusCommandReasonContext.efficiency} so the arithmetic is
16
16
  * visible — a warm hour at a higher price can still be the cheaper heat.
17
+ *
18
+ * When a battery buys from the grid to cover a later peak, add
19
+ * {@link EnyoDataBusCommandReasonContext.arbitrage} with the buy price and
20
+ * the peak it is for (`sellStartIso`, `sellPricePerKwh`), so the text can
21
+ * say what the buy is for: "Charging at 33.6 ct for the 17:00 peak at
22
+ * 43.7 ct."
17
23
  */
18
24
  EnyoDataBusCommandReasonTypeEnum["ElectricityPriceBelowThreshold"] = "electricity-price-below-threshold";
19
25
  /** Command issued because the electricity price is above a configured threshold */
@@ -55,7 +61,13 @@ var EnyoDataBusCommandReasonTypeEnum;
55
61
  EnyoDataBusCommandReasonTypeEnum["BatteryCapacityAvailable"] = "battery-capacity-available";
56
62
  /** Command issued because battery capacity is unavailable */
57
63
  EnyoDataBusCommandReasonTypeEnum["BatteryCapacityUnavailable"] = "battery-capacity-unavailable";
58
- /** Command issued because a grid operator power limitation is active */
64
+ /**
65
+ * Command issued because a grid operator power limitation is active.
66
+ *
67
+ * Only for a limit the grid operator imposed. A cap set by the owner or
68
+ * installer — the desired grid limit, the charger limit — is
69
+ * {@link InstallationLimit}.
70
+ */
59
71
  EnyoDataBusCommandReasonTypeEnum["GridOperatorPowerLimitationActive"] = "grid-operator-power-limitation-active";
60
72
  /** Command issued because a grid operator power limitation is inactive */
61
73
  EnyoDataBusCommandReasonTypeEnum["GridOperatorPowerLimitationInactive"] = "grid-operator-power-limitation-inactive";
@@ -79,7 +91,14 @@ var EnyoDataBusCommandReasonTypeEnum;
79
91
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}) with
80
92
  * {@link EnyoDataBusCommandReason.context} — `forecast` for what was seen
81
93
  * coming, `efficiency` for why this slot won, `thermalStorage` for what
82
- * ends the block, `placement` for when it runs.
94
+ * ends the block, `placement` for when it runs, `arbitrage` for the price
95
+ * spread a battery block was placed for.
96
+ *
97
+ * Also the type for a battery held back (`Discharge 0`) so that stored
98
+ * energy is kept for a later, dearer peak: set `arbitrage.sellStartIso`
99
+ * and `arbitrage.sellPricePerKwh` so the text reads "Stored energy kept for
100
+ * 17:00 (43.7 ct)." Not {@link ElectricityPriceBelowThreshold} — that
101
+ * would say power is cheap for a command that stops the battery.
83
102
  */
84
103
  EnyoDataBusCommandReasonTypeEnum["ScheduledOptimization"] = "scheduled-optimization";
85
104
  /** Command issued because the user explicitly requested it */
@@ -109,7 +128,15 @@ var EnyoDataBusCommandReasonTypeEnum;
109
128
  * limit."
110
129
  */
111
130
  EnyoDataBusCommandReasonTypeEnum["HomeConsumptionHigh"] = "home-consumption-high";
112
- /** Command issued to optimize self-consumption */
131
+ /**
132
+ * Command issued to optimize self-consumption — e.g. a battery handed back
133
+ * to its own logic so it stores PV surplus and covers the house load.
134
+ *
135
+ * Only true where there is something to self-consume. A battery on a site
136
+ * without PV that is left on Auto because trading does not pay is
137
+ * {@link PriceSpreadTooSmall}; do not use this type as the default for
138
+ * every Auto hand-back.
139
+ */
113
140
  EnyoDataBusCommandReasonTypeEnum["SelfConsumptionOptimization"] = "self-consumption-optimization";
114
141
  /**
115
142
  * Nothing left to deliver — the run's energy (or temperature, or state of
@@ -318,6 +345,37 @@ var EnyoDataBusCommandReasonTypeEnum;
318
345
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}.
319
346
  */
320
347
  EnyoDataBusCommandReasonTypeEnum["GridLoadOptimized"] = "grid-load-optimized";
348
+ /**
349
+ * The battery is left to its own logic because buying in the cheap hours
350
+ * and using the energy in the dear ones would not pay back after storage
351
+ * losses and wear. The trough is not "expensive" — the gap between trough
352
+ * and peak is too small.
353
+ *
354
+ * Set {@link EnyoDataBusCommandReason.electricityPricePerKwh} to the buy
355
+ * price and {@link EnyoDataBusCommandReasonContext.arbitrage} to the
356
+ * figures behind the verdict. On a flat horizon — prices barely move, so
357
+ * there is no cycle to weigh at all — send the type without `arbitrage`;
358
+ * the text then says so without figures.
359
+ *
360
+ * Told apart from {@link ElectricityPriceAboveThreshold}, which claims the
361
+ * price is over a level, and from {@link SelfConsumptionOptimization},
362
+ * which stays the right type where PV makes Auto mean self-consumption.
363
+ * Not {@link ScheduledOptimization}: nothing is placed, this is a refusal
364
+ * to trade. Category {@link EnyoDataBusCommandReasonCategoryEnum.EnergyPrice}.
365
+ */
366
+ EnyoDataBusCommandReasonTypeEnum["PriceSpreadTooSmall"] = "price-spread-too-small";
367
+ /**
368
+ * Power is capped by a limit the owner or installer set for the
369
+ * installation — not by the grid operator.
370
+ *
371
+ * Set {@link EnyoDataBusCommandReason.powerW} to the ceiling and
372
+ * {@link EnyoDataBusCommandReasonContext.installationLimit} to say which
373
+ * limit it is, so the text can tell "your desired grid limit of 8 kW" from
374
+ * "the charger limit of 11 kW set by your installer". Told apart from
375
+ * {@link GridOperatorPowerLimitationActive}, which would be untrue here.
376
+ * Category {@link EnyoDataBusCommandReasonCategoryEnum.Contention}.
377
+ */
378
+ EnyoDataBusCommandReasonTypeEnum["InstallationLimit"] = "installation-limit";
321
379
  })(EnyoDataBusCommandReasonTypeEnum || (exports.EnyoDataBusCommandReasonTypeEnum = EnyoDataBusCommandReasonTypeEnum = {}));
322
380
  /**
323
381
  * Coarse, machine-readable grouping of why a data bus command was issued.
@@ -360,6 +418,23 @@ var EnyoDataBusCommandReasonCategoryEnum;
360
418
  /** Any reason not covered by the categories above. */
361
419
  EnyoDataBusCommandReasonCategoryEnum["Other"] = "other";
362
420
  })(EnyoDataBusCommandReasonCategoryEnum || (exports.EnyoDataBusCommandReasonCategoryEnum = EnyoDataBusCommandReasonCategoryEnum = {}));
421
+ /**
422
+ * Which owner- or installer-set limit capped the power behind an
423
+ * {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
424
+ */
425
+ var EnyoInstallationLimitSourceEnum;
426
+ (function (EnyoInstallationLimitSourceEnum) {
427
+ /**
428
+ * The soft grid limit the user or installer would like import and export
429
+ * to stay within — {@link EnyoGridConnectionPoint.desiredKwLimit}.
430
+ */
431
+ EnyoInstallationLimitSourceEnum["DesiredGridLimit"] = "desired-grid-limit";
432
+ /**
433
+ * The installer's cap on the total power all wallboxes may draw —
434
+ * {@link EnyoGridConnectionPoint.chargerLimitW}.
435
+ */
436
+ EnyoInstallationLimitSourceEnum["ChargerLimit"] = "charger-limit";
437
+ })(EnyoInstallationLimitSourceEnum || (exports.EnyoInstallationLimitSourceEnum = EnyoInstallationLimitSourceEnum = {}));
363
438
  /**
364
439
  * Which part of the owner's battery-to-EV setting
365
440
  * (`batteryEvDischargeMode`) caused a
@@ -30,6 +30,12 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
30
30
  * cost per kWh of delivered heat rather than by price alone, add
31
31
  * {@link EnyoDataBusCommandReasonContext.efficiency} so the arithmetic is
32
32
  * visible — a warm hour at a higher price can still be the cheaper heat.
33
+ *
34
+ * When a battery buys from the grid to cover a later peak, add
35
+ * {@link EnyoDataBusCommandReasonContext.arbitrage} with the buy price and
36
+ * the peak it is for (`sellStartIso`, `sellPricePerKwh`), so the text can
37
+ * say what the buy is for: "Charging at 33.6 ct for the 17:00 peak at
38
+ * 43.7 ct."
33
39
  */
34
40
  ElectricityPriceBelowThreshold = "electricity-price-below-threshold",
35
41
  /** Command issued because the electricity price is above a configured threshold */
@@ -71,7 +77,13 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
71
77
  BatteryCapacityAvailable = "battery-capacity-available",
72
78
  /** Command issued because battery capacity is unavailable */
73
79
  BatteryCapacityUnavailable = "battery-capacity-unavailable",
74
- /** Command issued because a grid operator power limitation is active */
80
+ /**
81
+ * Command issued because a grid operator power limitation is active.
82
+ *
83
+ * Only for a limit the grid operator imposed. A cap set by the owner or
84
+ * installer — the desired grid limit, the charger limit — is
85
+ * {@link InstallationLimit}.
86
+ */
75
87
  GridOperatorPowerLimitationActive = "grid-operator-power-limitation-active",
76
88
  /** Command issued because a grid operator power limitation is inactive */
77
89
  GridOperatorPowerLimitationInactive = "grid-operator-power-limitation-inactive",
@@ -95,7 +107,14 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
95
107
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}) with
96
108
  * {@link EnyoDataBusCommandReason.context} — `forecast` for what was seen
97
109
  * coming, `efficiency` for why this slot won, `thermalStorage` for what
98
- * ends the block, `placement` for when it runs.
110
+ * ends the block, `placement` for when it runs, `arbitrage` for the price
111
+ * spread a battery block was placed for.
112
+ *
113
+ * Also the type for a battery held back (`Discharge 0`) so that stored
114
+ * energy is kept for a later, dearer peak: set `arbitrage.sellStartIso`
115
+ * and `arbitrage.sellPricePerKwh` so the text reads "Stored energy kept for
116
+ * 17:00 (43.7 ct)." Not {@link ElectricityPriceBelowThreshold} — that
117
+ * would say power is cheap for a command that stops the battery.
99
118
  */
100
119
  ScheduledOptimization = "scheduled-optimization",
101
120
  /** Command issued because the user explicitly requested it */
@@ -125,7 +144,15 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
125
144
  * limit."
126
145
  */
127
146
  HomeConsumptionHigh = "home-consumption-high",
128
- /** Command issued to optimize self-consumption */
147
+ /**
148
+ * Command issued to optimize self-consumption — e.g. a battery handed back
149
+ * to its own logic so it stores PV surplus and covers the house load.
150
+ *
151
+ * Only true where there is something to self-consume. A battery on a site
152
+ * without PV that is left on Auto because trading does not pay is
153
+ * {@link PriceSpreadTooSmall}; do not use this type as the default for
154
+ * every Auto hand-back.
155
+ */
129
156
  SelfConsumptionOptimization = "self-consumption-optimization",
130
157
  /**
131
158
  * Nothing left to deliver — the run's energy (or temperature, or state of
@@ -333,7 +360,38 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
333
360
  * less busy — ready by 07:00." Category
334
361
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}.
335
362
  */
336
- GridLoadOptimized = "grid-load-optimized"
363
+ GridLoadOptimized = "grid-load-optimized",
364
+ /**
365
+ * The battery is left to its own logic because buying in the cheap hours
366
+ * and using the energy in the dear ones would not pay back after storage
367
+ * losses and wear. The trough is not "expensive" — the gap between trough
368
+ * and peak is too small.
369
+ *
370
+ * Set {@link EnyoDataBusCommandReason.electricityPricePerKwh} to the buy
371
+ * price and {@link EnyoDataBusCommandReasonContext.arbitrage} to the
372
+ * figures behind the verdict. On a flat horizon — prices barely move, so
373
+ * there is no cycle to weigh at all — send the type without `arbitrage`;
374
+ * the text then says so without figures.
375
+ *
376
+ * Told apart from {@link ElectricityPriceAboveThreshold}, which claims the
377
+ * price is over a level, and from {@link SelfConsumptionOptimization},
378
+ * which stays the right type where PV makes Auto mean self-consumption.
379
+ * Not {@link ScheduledOptimization}: nothing is placed, this is a refusal
380
+ * to trade. Category {@link EnyoDataBusCommandReasonCategoryEnum.EnergyPrice}.
381
+ */
382
+ PriceSpreadTooSmall = "price-spread-too-small",
383
+ /**
384
+ * Power is capped by a limit the owner or installer set for the
385
+ * installation — not by the grid operator.
386
+ *
387
+ * Set {@link EnyoDataBusCommandReason.powerW} to the ceiling and
388
+ * {@link EnyoDataBusCommandReasonContext.installationLimit} to say which
389
+ * limit it is, so the text can tell "your desired grid limit of 8 kW" from
390
+ * "the charger limit of 11 kW set by your installer". Told apart from
391
+ * {@link GridOperatorPowerLimitationActive}, which would be untrue here.
392
+ * Category {@link EnyoDataBusCommandReasonCategoryEnum.Contention}.
393
+ */
394
+ InstallationLimit = "installation-limit"
337
395
  }
338
396
  /**
339
397
  * Coarse, machine-readable grouping of why a data bus command was issued.
@@ -651,6 +709,124 @@ export interface EnyoDataBusCommandReasonContext {
651
709
  * {@link EnyoDataBusCommandReasonTypeEnum.GridLoadOptimized}.
652
710
  */
653
711
  gridLoad?: EnyoDataBusCommandReasonGridLoadContext;
712
+ /**
713
+ * The trough-against-peak arithmetic behind a battery buy that was or was
714
+ * not made. Set on {@link EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall},
715
+ * and alongside {@link EnyoDataBusCommandReasonTypeEnum.ElectricityPriceBelowThreshold}
716
+ * (a buy for a peak) or {@link EnyoDataBusCommandReasonTypeEnum.ScheduledOptimization}
717
+ * (energy kept for a peak).
718
+ */
719
+ arbitrage?: EnyoDataBusCommandReasonArbitrageContext;
720
+ /**
721
+ * Which owner- or installer-set limit capped the power. Set on
722
+ * {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
723
+ */
724
+ installationLimit?: EnyoDataBusCommandReasonInstallationLimitContext;
725
+ }
726
+ /**
727
+ * The trough-against-peak arithmetic behind a battery buy that was or was not
728
+ * made.
729
+ *
730
+ * Prices are per kWh of electricity in
731
+ * {@link EnyoDataBusCommandReason.currency} — **EUR/kWh, not ct**, like
732
+ * {@link EnyoDataBusCommandReason.electricityPricePerKwh}: `0.336`, not
733
+ * `33.6`. Every field is optional so each use sets only what it states:
734
+ *
735
+ * - {@link EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall} — all of them.
736
+ * - {@link EnyoDataBusCommandReasonTypeEnum.ElectricityPriceBelowThreshold} on
737
+ * a grid charge — the buy, and the peak it is for.
738
+ * - {@link EnyoDataBusCommandReasonTypeEnum.ScheduledOptimization} on a hold —
739
+ * the peak the energy is kept for.
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * // No cycle charge: buying at 35.2 ct needs 52.7 ct to pay back, the peak averages 42.9 ct.
744
+ * const refused: EnyoDataBusCommandReason = {
745
+ * type: EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall,
746
+ * category: EnyoDataBusCommandReasonCategoryEnum.EnergyPrice,
747
+ * electricityPricePerKwh: 0.352,
748
+ * currency: EnyoCurrencyEnum.EUR,
749
+ * context: {arbitrage: {
750
+ * buyPricePerKwh: 0.352,
751
+ * buyStartIso: '2026-10-13T11:00:00Z',
752
+ * buyEndIso: '2026-10-13T13:00:00Z',
753
+ * sellPricePerKwh: 0.429,
754
+ * sellStartIso: '2026-10-13T16:00:00Z',
755
+ * sellEndIso: '2026-10-13T19:00:00Z',
756
+ * breakEvenPricePerKwh: 0.527,
757
+ * roundTripEfficiency: 0.757,
758
+ * }},
759
+ * };
760
+ *
761
+ * // Stored energy kept for the 17:00 peak:
762
+ * const hold: EnyoDataBusCommandReason = {
763
+ * type: EnyoDataBusCommandReasonTypeEnum.ScheduledOptimization,
764
+ * category: EnyoDataBusCommandReasonCategoryEnum.Schedule,
765
+ * powerW: 0,
766
+ * currency: EnyoCurrencyEnum.EUR,
767
+ * context: {arbitrage: {sellStartIso: '2026-10-13T17:00:00Z', sellPricePerKwh: 0.437}},
768
+ * };
769
+ * ```
770
+ */
771
+ export interface EnyoDataBusCommandReasonArbitrageContext {
772
+ /** Average price, EUR/kWh, over the cheapest window the energy would be bought in. */
773
+ buyPricePerKwh?: number;
774
+ /** ISO 8601 start of the buy window. */
775
+ buyStartIso?: string;
776
+ /** ISO 8601 end of the buy window. */
777
+ buyEndIso?: string;
778
+ /** Average price, EUR/kWh, over the peak window the energy would be used in. */
779
+ sellPricePerKwh?: number;
780
+ /** ISO 8601 start of the peak window. */
781
+ sellStartIso?: string;
782
+ /** ISO 8601 end of the peak window. */
783
+ sellEndIso?: string;
784
+ /**
785
+ * Price, EUR/kWh, the peak must reach for the buy to pay back — losses,
786
+ * wear and margin included.
787
+ */
788
+ breakEvenPricePerKwh?: number;
789
+ /** Round-trip efficiency the verdict assumed, as a fraction (0–1): `0.757`, not `75.7`. */
790
+ roundTripEfficiency?: number;
791
+ }
792
+ /**
793
+ * Which owner- or installer-set limit capped the power behind an
794
+ * {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
795
+ */
796
+ export declare enum EnyoInstallationLimitSourceEnum {
797
+ /**
798
+ * The soft grid limit the user or installer would like import and export
799
+ * to stay within — {@link EnyoGridConnectionPoint.desiredKwLimit}.
800
+ */
801
+ DesiredGridLimit = "desired-grid-limit",
802
+ /**
803
+ * The installer's cap on the total power all wallboxes may draw —
804
+ * {@link EnyoGridConnectionPoint.chargerLimitW}.
805
+ */
806
+ ChargerLimit = "charger-limit"
807
+ }
808
+ /**
809
+ * The limit behind an {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * const reason: EnyoDataBusCommandReason = {
814
+ * type: EnyoDataBusCommandReasonTypeEnum.InstallationLimit,
815
+ * category: EnyoDataBusCommandReasonCategoryEnum.Contention,
816
+ * powerW: 11000,
817
+ * context: {installationLimit: {source: EnyoInstallationLimitSourceEnum.ChargerLimit, limitW: 11000}},
818
+ * };
819
+ * ```
820
+ */
821
+ export interface EnyoDataBusCommandReasonInstallationLimitContext {
822
+ /** Which limit applied. */
823
+ source: EnyoInstallationLimitSourceEnum;
824
+ /**
825
+ * The limit, in **watts** — also for
826
+ * {@link EnyoInstallationLimitSourceEnum.DesiredGridLimit}, whose
827
+ * configured value `desiredKwLimit` is in kW: convert before setting it.
828
+ */
829
+ limitW: number;
654
830
  }
655
831
  /**
656
832
  * The market signal behind a
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '1.29.0';
12
+ exports.SDK_VERSION = '1.30.0';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "1.29.0";
8
+ export declare const SDK_VERSION = "1.30.0";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -1,4 +1,5 @@
1
1
  import { AirConditioningForecast, AirConditioningForecastScheduleEntry, ApplianceForecastResolutionEnum, BatteryCommandForecast, BatteryCommandForecastScheduleEntry, ChargerForecast, ChargerForecastScheduleEntry, HeatingRodForecast, HeatingRodForecastScheduleEntry, HeatpumpForecast, HeatpumpForecastScheduleEntry, SmartPlugForecast, SmartPlugForecastScheduleEntry } from '../../types/enyo-appliance-command-forecast.js';
2
+ import { type EnyoDataBusCommandReason } from '../../types/enyo-data-bus-value.js';
2
3
  /**
3
4
  * Thrown when a forecast payload passed to one of the validators (or to
4
5
  * {@link EnergyAppApplianceEnergyManagerForecast.publishChargerForecast}
@@ -23,9 +24,28 @@ export declare function validateChargerForecast(forecast: ChargerForecast): void
23
24
  * {@link BatteryCommandForecastModeEnum}: when `mode = 'auto'` no
24
25
  * schedule is expected; when `mode = 'schedule'` the embedded
25
26
  * {@link BatteryCommandForecastScheduled.relativeSchedule} is validated
26
- * by {@link validateBatterySchedule}.
27
+ * by {@link validateBatterySchedule}. An Auto forecast's optional
28
+ * {@link BatteryCommandForecastAuto.reason} is checked by
29
+ * {@link validateBatteryForecastReason}.
27
30
  */
28
31
  export declare function validateBatteryCommandForecast(forecast: BatteryCommandForecast): void;
32
+ /**
33
+ * Validates the reason attached to an Auto {@link BatteryCommandForecast}.
34
+ * Throws on the first violation.
35
+ *
36
+ * Checks that the reason is an object whose `type` is an
37
+ * {@link EnyoDataBusCommandReasonTypeEnum} member and, when it carries
38
+ * `context.arbitrage`, that:
39
+ * - every price is a finite EUR/kWh figure within a plausible band (catches
40
+ * ct in a EUR field),
41
+ * - `roundTripEfficiency` lies in (0, 1] (catches `75.7` for `0.757`),
42
+ * - each window's start lies before its end, where both are given.
43
+ *
44
+ * @param reason - The reason to validate.
45
+ * @param fieldName - Field path used in error messages.
46
+ * @throws {ApplianceCommandForecastValidationError} On the first violation.
47
+ */
48
+ export declare function validateBatteryForecastReason(reason: EnyoDataBusCommandReason, fieldName: string): void;
29
49
  /**
30
50
  * Validates a {@link HeatpumpForecast}. Throws on the first violation —
31
51
  * the error message names the offending field / index.
@@ -1,7 +1,7 @@
1
1
  import { ApplianceForecastResolutionEnum, BatteryCommandForecastDirectionEnum, BatteryCommandForecastModeEnum, } from '../../types/enyo-appliance-command-forecast.js';
2
2
  import { EnyoAirConditioningApplianceModeEnum, EnyoAirConditioningOptimizationModeEnum, } from '../../types/enyo-air-conditioning-appliance.js';
3
3
  import { EnyoAutomationTriggerTypeEnum } from '../../types/enyo-automation.js';
4
- import { EnyoChargeModeEnum } from '../../types/enyo-data-bus-value.js';
4
+ import { EnyoChargeModeEnum, EnyoDataBusCommandReasonTypeEnum, } from '../../types/enyo-data-bus-value.js';
5
5
  import { EnyoSmartPlugApplianceStateEnum } from '../../types/enyo-smart-plug-appliance.js';
6
6
  /**
7
7
  * Thrown when a forecast payload passed to one of the validators (or to
@@ -49,7 +49,9 @@ export function validateChargerForecast(forecast) {
49
49
  * {@link BatteryCommandForecastModeEnum}: when `mode = 'auto'` no
50
50
  * schedule is expected; when `mode = 'schedule'` the embedded
51
51
  * {@link BatteryCommandForecastScheduled.relativeSchedule} is validated
52
- * by {@link validateBatterySchedule}.
52
+ * by {@link validateBatterySchedule}. An Auto forecast's optional
53
+ * {@link BatteryCommandForecastAuto.reason} is checked by
54
+ * {@link validateBatteryForecastReason}.
53
55
  */
54
56
  export function validateBatteryCommandForecast(forecast) {
55
57
  if (!forecast || typeof forecast !== 'object') {
@@ -64,10 +66,75 @@ export function validateBatteryCommandForecast(forecast) {
64
66
  if (forecast.relativeSchedule !== undefined) {
65
67
  throw new ApplianceCommandForecastValidationError("BatteryCommandForecast with mode='auto' must not carry a relativeSchedule.");
66
68
  }
69
+ if (forecast.reason !== undefined) {
70
+ validateBatteryForecastReason(forecast.reason, 'BatteryCommandForecast.reason');
71
+ }
67
72
  return;
68
73
  }
69
74
  validateBatterySchedule(forecast.relativeSchedule, forecast.resolution);
70
75
  }
76
+ /**
77
+ * Lowest and highest plausible price per kWh in a reason's `arbitrage`
78
+ * context, in EUR/kWh. Wide enough for negative and spiking dynamic prices,
79
+ * narrow enough to catch a ct value (`33.6`) in a EUR field.
80
+ */
81
+ const ARBITRAGE_PRICE_MIN_PER_KWH = -1;
82
+ const ARBITRAGE_PRICE_MAX_PER_KWH = 5;
83
+ /**
84
+ * Validates the reason attached to an Auto {@link BatteryCommandForecast}.
85
+ * Throws on the first violation.
86
+ *
87
+ * Checks that the reason is an object whose `type` is an
88
+ * {@link EnyoDataBusCommandReasonTypeEnum} member and, when it carries
89
+ * `context.arbitrage`, that:
90
+ * - every price is a finite EUR/kWh figure within a plausible band (catches
91
+ * ct in a EUR field),
92
+ * - `roundTripEfficiency` lies in (0, 1] (catches `75.7` for `0.757`),
93
+ * - each window's start lies before its end, where both are given.
94
+ *
95
+ * @param reason - The reason to validate.
96
+ * @param fieldName - Field path used in error messages.
97
+ * @throws {ApplianceCommandForecastValidationError} On the first violation.
98
+ */
99
+ export function validateBatteryForecastReason(reason, fieldName) {
100
+ if (!reason || typeof reason !== 'object') {
101
+ throw new ApplianceCommandForecastValidationError(`${fieldName} must be an object.`);
102
+ }
103
+ validateEnumField(reason.type, Object.values(EnyoDataBusCommandReasonTypeEnum), `${fieldName}.type`, true);
104
+ const arbitrage = reason.context?.arbitrage;
105
+ if (arbitrage === undefined)
106
+ return;
107
+ for (const key of ['buyPricePerKwh', 'sellPricePerKwh', 'breakEvenPricePerKwh']) {
108
+ const price = arbitrage[key];
109
+ if (price === undefined)
110
+ continue;
111
+ if (!Number.isFinite(price)) {
112
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${key} must be a finite number; got ${price}.`);
113
+ }
114
+ if (price < ARBITRAGE_PRICE_MIN_PER_KWH || price > ARBITRAGE_PRICE_MAX_PER_KWH) {
115
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${key} is ${price}, outside ` +
116
+ `${ARBITRAGE_PRICE_MIN_PER_KWH}…${ARBITRAGE_PRICE_MAX_PER_KWH} — prices are per kWh in ` +
117
+ 'the reason\'s currency (EUR/kWh), not ct/kWh.');
118
+ }
119
+ }
120
+ const efficiency = arbitrage.roundTripEfficiency;
121
+ if (efficiency !== undefined && (!Number.isFinite(efficiency) || efficiency <= 0 || efficiency > 1)) {
122
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.roundTripEfficiency is ${efficiency}; it is a fraction in (0, 1], ` +
123
+ 'e.g. 0.757 rather than 75.7.');
124
+ }
125
+ for (const [start, end] of [['buyStartIso', 'buyEndIso'], ['sellStartIso', 'sellEndIso']]) {
126
+ const startIso = arbitrage[start];
127
+ const endIso = arbitrage[end];
128
+ for (const [key, iso] of [[start, startIso], [end, endIso]]) {
129
+ if (iso !== undefined && Number.isNaN(Date.parse(iso))) {
130
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${key} is not an ISO 8601 timestamp: ${iso}.`);
131
+ }
132
+ }
133
+ if (startIso !== undefined && endIso !== undefined && Date.parse(startIso) >= Date.parse(endIso)) {
134
+ throw new ApplianceCommandForecastValidationError(`${fieldName}.context.arbitrage.${start} must lie before ${end}.`);
135
+ }
136
+ }
137
+ }
71
138
  /**
72
139
  * Validates a {@link HeatpumpForecast}. Throws on the first violation —
73
140
  * the error message names the offending field / index.
@@ -47,6 +47,7 @@
47
47
  import { EnyoAirConditioningApplianceModeEnum, EnyoAirConditioningOptimizationModeEnum } from './enyo-air-conditioning-appliance.js';
48
48
  import { EnyoAutomationTriggerTypeEnum } from './enyo-automation.js';
49
49
  import { EnyoChargeModeEnum } from './enyo-data-bus-value.js';
50
+ import type { EnyoDataBusCommandReason } from './enyo-data-bus-value.js';
50
51
  import { EnyoSmartPlugApplianceStateEnum } from './enyo-smart-plug-appliance.js';
51
52
  /**
52
53
  * Optional metadata describing the estimated savings of a forecasted
@@ -262,6 +263,17 @@ export interface BatteryCommandForecastScheduleEntry {
262
263
  export interface BatteryCommandForecastAuto extends ApplianceForecastMetadata {
263
264
  /** Discriminator — set to {@link BatteryCommandForecastModeEnum.Auto}. */
264
265
  mode: BatteryCommandForecastModeEnum.Auto;
266
+ /**
267
+ * Why no schedule is prescribed over the horizon — e.g.
268
+ * {@link EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall} when
269
+ * trading would not pay.
270
+ *
271
+ * Set it only when every Auto slot of the horizon shares the same reason;
272
+ * when they differ, leave it out rather than pick one. Optional, so a
273
+ * consumer that does not read it is unaffected — but without it an empty
274
+ * Auto forecast tends to read as "no forecast".
275
+ */
276
+ reason?: EnyoDataBusCommandReason;
265
277
  }
266
278
  /**
267
279
  * Forecast variant that declares an explicit relative schedule the
@@ -30,6 +30,12 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
30
30
  * cost per kWh of delivered heat rather than by price alone, add
31
31
  * {@link EnyoDataBusCommandReasonContext.efficiency} so the arithmetic is
32
32
  * visible — a warm hour at a higher price can still be the cheaper heat.
33
+ *
34
+ * When a battery buys from the grid to cover a later peak, add
35
+ * {@link EnyoDataBusCommandReasonContext.arbitrage} with the buy price and
36
+ * the peak it is for (`sellStartIso`, `sellPricePerKwh`), so the text can
37
+ * say what the buy is for: "Charging at 33.6 ct for the 17:00 peak at
38
+ * 43.7 ct."
33
39
  */
34
40
  ElectricityPriceBelowThreshold = "electricity-price-below-threshold",
35
41
  /** Command issued because the electricity price is above a configured threshold */
@@ -71,7 +77,13 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
71
77
  BatteryCapacityAvailable = "battery-capacity-available",
72
78
  /** Command issued because battery capacity is unavailable */
73
79
  BatteryCapacityUnavailable = "battery-capacity-unavailable",
74
- /** Command issued because a grid operator power limitation is active */
80
+ /**
81
+ * Command issued because a grid operator power limitation is active.
82
+ *
83
+ * Only for a limit the grid operator imposed. A cap set by the owner or
84
+ * installer — the desired grid limit, the charger limit — is
85
+ * {@link InstallationLimit}.
86
+ */
75
87
  GridOperatorPowerLimitationActive = "grid-operator-power-limitation-active",
76
88
  /** Command issued because a grid operator power limitation is inactive */
77
89
  GridOperatorPowerLimitationInactive = "grid-operator-power-limitation-inactive",
@@ -95,7 +107,14 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
95
107
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}) with
96
108
  * {@link EnyoDataBusCommandReason.context} — `forecast` for what was seen
97
109
  * coming, `efficiency` for why this slot won, `thermalStorage` for what
98
- * ends the block, `placement` for when it runs.
110
+ * ends the block, `placement` for when it runs, `arbitrage` for the price
111
+ * spread a battery block was placed for.
112
+ *
113
+ * Also the type for a battery held back (`Discharge 0`) so that stored
114
+ * energy is kept for a later, dearer peak: set `arbitrage.sellStartIso`
115
+ * and `arbitrage.sellPricePerKwh` so the text reads "Stored energy kept for
116
+ * 17:00 (43.7 ct)." Not {@link ElectricityPriceBelowThreshold} — that
117
+ * would say power is cheap for a command that stops the battery.
99
118
  */
100
119
  ScheduledOptimization = "scheduled-optimization",
101
120
  /** Command issued because the user explicitly requested it */
@@ -125,7 +144,15 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
125
144
  * limit."
126
145
  */
127
146
  HomeConsumptionHigh = "home-consumption-high",
128
- /** Command issued to optimize self-consumption */
147
+ /**
148
+ * Command issued to optimize self-consumption — e.g. a battery handed back
149
+ * to its own logic so it stores PV surplus and covers the house load.
150
+ *
151
+ * Only true where there is something to self-consume. A battery on a site
152
+ * without PV that is left on Auto because trading does not pay is
153
+ * {@link PriceSpreadTooSmall}; do not use this type as the default for
154
+ * every Auto hand-back.
155
+ */
129
156
  SelfConsumptionOptimization = "self-consumption-optimization",
130
157
  /**
131
158
  * Nothing left to deliver — the run's energy (or temperature, or state of
@@ -333,7 +360,38 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
333
360
  * less busy — ready by 07:00." Category
334
361
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}.
335
362
  */
336
- GridLoadOptimized = "grid-load-optimized"
363
+ GridLoadOptimized = "grid-load-optimized",
364
+ /**
365
+ * The battery is left to its own logic because buying in the cheap hours
366
+ * and using the energy in the dear ones would not pay back after storage
367
+ * losses and wear. The trough is not "expensive" — the gap between trough
368
+ * and peak is too small.
369
+ *
370
+ * Set {@link EnyoDataBusCommandReason.electricityPricePerKwh} to the buy
371
+ * price and {@link EnyoDataBusCommandReasonContext.arbitrage} to the
372
+ * figures behind the verdict. On a flat horizon — prices barely move, so
373
+ * there is no cycle to weigh at all — send the type without `arbitrage`;
374
+ * the text then says so without figures.
375
+ *
376
+ * Told apart from {@link ElectricityPriceAboveThreshold}, which claims the
377
+ * price is over a level, and from {@link SelfConsumptionOptimization},
378
+ * which stays the right type where PV makes Auto mean self-consumption.
379
+ * Not {@link ScheduledOptimization}: nothing is placed, this is a refusal
380
+ * to trade. Category {@link EnyoDataBusCommandReasonCategoryEnum.EnergyPrice}.
381
+ */
382
+ PriceSpreadTooSmall = "price-spread-too-small",
383
+ /**
384
+ * Power is capped by a limit the owner or installer set for the
385
+ * installation — not by the grid operator.
386
+ *
387
+ * Set {@link EnyoDataBusCommandReason.powerW} to the ceiling and
388
+ * {@link EnyoDataBusCommandReasonContext.installationLimit} to say which
389
+ * limit it is, so the text can tell "your desired grid limit of 8 kW" from
390
+ * "the charger limit of 11 kW set by your installer". Told apart from
391
+ * {@link GridOperatorPowerLimitationActive}, which would be untrue here.
392
+ * Category {@link EnyoDataBusCommandReasonCategoryEnum.Contention}.
393
+ */
394
+ InstallationLimit = "installation-limit"
337
395
  }
338
396
  /**
339
397
  * Coarse, machine-readable grouping of why a data bus command was issued.
@@ -651,6 +709,124 @@ export interface EnyoDataBusCommandReasonContext {
651
709
  * {@link EnyoDataBusCommandReasonTypeEnum.GridLoadOptimized}.
652
710
  */
653
711
  gridLoad?: EnyoDataBusCommandReasonGridLoadContext;
712
+ /**
713
+ * The trough-against-peak arithmetic behind a battery buy that was or was
714
+ * not made. Set on {@link EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall},
715
+ * and alongside {@link EnyoDataBusCommandReasonTypeEnum.ElectricityPriceBelowThreshold}
716
+ * (a buy for a peak) or {@link EnyoDataBusCommandReasonTypeEnum.ScheduledOptimization}
717
+ * (energy kept for a peak).
718
+ */
719
+ arbitrage?: EnyoDataBusCommandReasonArbitrageContext;
720
+ /**
721
+ * Which owner- or installer-set limit capped the power. Set on
722
+ * {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
723
+ */
724
+ installationLimit?: EnyoDataBusCommandReasonInstallationLimitContext;
725
+ }
726
+ /**
727
+ * The trough-against-peak arithmetic behind a battery buy that was or was not
728
+ * made.
729
+ *
730
+ * Prices are per kWh of electricity in
731
+ * {@link EnyoDataBusCommandReason.currency} — **EUR/kWh, not ct**, like
732
+ * {@link EnyoDataBusCommandReason.electricityPricePerKwh}: `0.336`, not
733
+ * `33.6`. Every field is optional so each use sets only what it states:
734
+ *
735
+ * - {@link EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall} — all of them.
736
+ * - {@link EnyoDataBusCommandReasonTypeEnum.ElectricityPriceBelowThreshold} on
737
+ * a grid charge — the buy, and the peak it is for.
738
+ * - {@link EnyoDataBusCommandReasonTypeEnum.ScheduledOptimization} on a hold —
739
+ * the peak the energy is kept for.
740
+ *
741
+ * @example
742
+ * ```typescript
743
+ * // No cycle charge: buying at 35.2 ct needs 52.7 ct to pay back, the peak averages 42.9 ct.
744
+ * const refused: EnyoDataBusCommandReason = {
745
+ * type: EnyoDataBusCommandReasonTypeEnum.PriceSpreadTooSmall,
746
+ * category: EnyoDataBusCommandReasonCategoryEnum.EnergyPrice,
747
+ * electricityPricePerKwh: 0.352,
748
+ * currency: EnyoCurrencyEnum.EUR,
749
+ * context: {arbitrage: {
750
+ * buyPricePerKwh: 0.352,
751
+ * buyStartIso: '2026-10-13T11:00:00Z',
752
+ * buyEndIso: '2026-10-13T13:00:00Z',
753
+ * sellPricePerKwh: 0.429,
754
+ * sellStartIso: '2026-10-13T16:00:00Z',
755
+ * sellEndIso: '2026-10-13T19:00:00Z',
756
+ * breakEvenPricePerKwh: 0.527,
757
+ * roundTripEfficiency: 0.757,
758
+ * }},
759
+ * };
760
+ *
761
+ * // Stored energy kept for the 17:00 peak:
762
+ * const hold: EnyoDataBusCommandReason = {
763
+ * type: EnyoDataBusCommandReasonTypeEnum.ScheduledOptimization,
764
+ * category: EnyoDataBusCommandReasonCategoryEnum.Schedule,
765
+ * powerW: 0,
766
+ * currency: EnyoCurrencyEnum.EUR,
767
+ * context: {arbitrage: {sellStartIso: '2026-10-13T17:00:00Z', sellPricePerKwh: 0.437}},
768
+ * };
769
+ * ```
770
+ */
771
+ export interface EnyoDataBusCommandReasonArbitrageContext {
772
+ /** Average price, EUR/kWh, over the cheapest window the energy would be bought in. */
773
+ buyPricePerKwh?: number;
774
+ /** ISO 8601 start of the buy window. */
775
+ buyStartIso?: string;
776
+ /** ISO 8601 end of the buy window. */
777
+ buyEndIso?: string;
778
+ /** Average price, EUR/kWh, over the peak window the energy would be used in. */
779
+ sellPricePerKwh?: number;
780
+ /** ISO 8601 start of the peak window. */
781
+ sellStartIso?: string;
782
+ /** ISO 8601 end of the peak window. */
783
+ sellEndIso?: string;
784
+ /**
785
+ * Price, EUR/kWh, the peak must reach for the buy to pay back — losses,
786
+ * wear and margin included.
787
+ */
788
+ breakEvenPricePerKwh?: number;
789
+ /** Round-trip efficiency the verdict assumed, as a fraction (0–1): `0.757`, not `75.7`. */
790
+ roundTripEfficiency?: number;
791
+ }
792
+ /**
793
+ * Which owner- or installer-set limit capped the power behind an
794
+ * {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
795
+ */
796
+ export declare enum EnyoInstallationLimitSourceEnum {
797
+ /**
798
+ * The soft grid limit the user or installer would like import and export
799
+ * to stay within — {@link EnyoGridConnectionPoint.desiredKwLimit}.
800
+ */
801
+ DesiredGridLimit = "desired-grid-limit",
802
+ /**
803
+ * The installer's cap on the total power all wallboxes may draw —
804
+ * {@link EnyoGridConnectionPoint.chargerLimitW}.
805
+ */
806
+ ChargerLimit = "charger-limit"
807
+ }
808
+ /**
809
+ * The limit behind an {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
810
+ *
811
+ * @example
812
+ * ```typescript
813
+ * const reason: EnyoDataBusCommandReason = {
814
+ * type: EnyoDataBusCommandReasonTypeEnum.InstallationLimit,
815
+ * category: EnyoDataBusCommandReasonCategoryEnum.Contention,
816
+ * powerW: 11000,
817
+ * context: {installationLimit: {source: EnyoInstallationLimitSourceEnum.ChargerLimit, limitW: 11000}},
818
+ * };
819
+ * ```
820
+ */
821
+ export interface EnyoDataBusCommandReasonInstallationLimitContext {
822
+ /** Which limit applied. */
823
+ source: EnyoInstallationLimitSourceEnum;
824
+ /**
825
+ * The limit, in **watts** — also for
826
+ * {@link EnyoInstallationLimitSourceEnum.DesiredGridLimit}, whose
827
+ * configured value `desiredKwLimit` is in kW: convert before setting it.
828
+ */
829
+ limitW: number;
654
830
  }
655
831
  /**
656
832
  * The market signal behind a
@@ -11,6 +11,12 @@ export var EnyoDataBusCommandReasonTypeEnum;
11
11
  * cost per kWh of delivered heat rather than by price alone, add
12
12
  * {@link EnyoDataBusCommandReasonContext.efficiency} so the arithmetic is
13
13
  * visible — a warm hour at a higher price can still be the cheaper heat.
14
+ *
15
+ * When a battery buys from the grid to cover a later peak, add
16
+ * {@link EnyoDataBusCommandReasonContext.arbitrage} with the buy price and
17
+ * the peak it is for (`sellStartIso`, `sellPricePerKwh`), so the text can
18
+ * say what the buy is for: "Charging at 33.6 ct for the 17:00 peak at
19
+ * 43.7 ct."
14
20
  */
15
21
  EnyoDataBusCommandReasonTypeEnum["ElectricityPriceBelowThreshold"] = "electricity-price-below-threshold";
16
22
  /** Command issued because the electricity price is above a configured threshold */
@@ -52,7 +58,13 @@ export var EnyoDataBusCommandReasonTypeEnum;
52
58
  EnyoDataBusCommandReasonTypeEnum["BatteryCapacityAvailable"] = "battery-capacity-available";
53
59
  /** Command issued because battery capacity is unavailable */
54
60
  EnyoDataBusCommandReasonTypeEnum["BatteryCapacityUnavailable"] = "battery-capacity-unavailable";
55
- /** Command issued because a grid operator power limitation is active */
61
+ /**
62
+ * Command issued because a grid operator power limitation is active.
63
+ *
64
+ * Only for a limit the grid operator imposed. A cap set by the owner or
65
+ * installer — the desired grid limit, the charger limit — is
66
+ * {@link InstallationLimit}.
67
+ */
56
68
  EnyoDataBusCommandReasonTypeEnum["GridOperatorPowerLimitationActive"] = "grid-operator-power-limitation-active";
57
69
  /** Command issued because a grid operator power limitation is inactive */
58
70
  EnyoDataBusCommandReasonTypeEnum["GridOperatorPowerLimitationInactive"] = "grid-operator-power-limitation-inactive";
@@ -76,7 +88,14 @@ export var EnyoDataBusCommandReasonTypeEnum;
76
88
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}) with
77
89
  * {@link EnyoDataBusCommandReason.context} — `forecast` for what was seen
78
90
  * coming, `efficiency` for why this slot won, `thermalStorage` for what
79
- * ends the block, `placement` for when it runs.
91
+ * ends the block, `placement` for when it runs, `arbitrage` for the price
92
+ * spread a battery block was placed for.
93
+ *
94
+ * Also the type for a battery held back (`Discharge 0`) so that stored
95
+ * energy is kept for a later, dearer peak: set `arbitrage.sellStartIso`
96
+ * and `arbitrage.sellPricePerKwh` so the text reads "Stored energy kept for
97
+ * 17:00 (43.7 ct)." Not {@link ElectricityPriceBelowThreshold} — that
98
+ * would say power is cheap for a command that stops the battery.
80
99
  */
81
100
  EnyoDataBusCommandReasonTypeEnum["ScheduledOptimization"] = "scheduled-optimization";
82
101
  /** Command issued because the user explicitly requested it */
@@ -106,7 +125,15 @@ export var EnyoDataBusCommandReasonTypeEnum;
106
125
  * limit."
107
126
  */
108
127
  EnyoDataBusCommandReasonTypeEnum["HomeConsumptionHigh"] = "home-consumption-high";
109
- /** Command issued to optimize self-consumption */
128
+ /**
129
+ * Command issued to optimize self-consumption — e.g. a battery handed back
130
+ * to its own logic so it stores PV surplus and covers the house load.
131
+ *
132
+ * Only true where there is something to self-consume. A battery on a site
133
+ * without PV that is left on Auto because trading does not pay is
134
+ * {@link PriceSpreadTooSmall}; do not use this type as the default for
135
+ * every Auto hand-back.
136
+ */
110
137
  EnyoDataBusCommandReasonTypeEnum["SelfConsumptionOptimization"] = "self-consumption-optimization";
111
138
  /**
112
139
  * Nothing left to deliver — the run's energy (or temperature, or state of
@@ -315,6 +342,37 @@ export var EnyoDataBusCommandReasonTypeEnum;
315
342
  * {@link EnyoDataBusCommandReasonCategoryEnum.Schedule}.
316
343
  */
317
344
  EnyoDataBusCommandReasonTypeEnum["GridLoadOptimized"] = "grid-load-optimized";
345
+ /**
346
+ * The battery is left to its own logic because buying in the cheap hours
347
+ * and using the energy in the dear ones would not pay back after storage
348
+ * losses and wear. The trough is not "expensive" — the gap between trough
349
+ * and peak is too small.
350
+ *
351
+ * Set {@link EnyoDataBusCommandReason.electricityPricePerKwh} to the buy
352
+ * price and {@link EnyoDataBusCommandReasonContext.arbitrage} to the
353
+ * figures behind the verdict. On a flat horizon — prices barely move, so
354
+ * there is no cycle to weigh at all — send the type without `arbitrage`;
355
+ * the text then says so without figures.
356
+ *
357
+ * Told apart from {@link ElectricityPriceAboveThreshold}, which claims the
358
+ * price is over a level, and from {@link SelfConsumptionOptimization},
359
+ * which stays the right type where PV makes Auto mean self-consumption.
360
+ * Not {@link ScheduledOptimization}: nothing is placed, this is a refusal
361
+ * to trade. Category {@link EnyoDataBusCommandReasonCategoryEnum.EnergyPrice}.
362
+ */
363
+ EnyoDataBusCommandReasonTypeEnum["PriceSpreadTooSmall"] = "price-spread-too-small";
364
+ /**
365
+ * Power is capped by a limit the owner or installer set for the
366
+ * installation — not by the grid operator.
367
+ *
368
+ * Set {@link EnyoDataBusCommandReason.powerW} to the ceiling and
369
+ * {@link EnyoDataBusCommandReasonContext.installationLimit} to say which
370
+ * limit it is, so the text can tell "your desired grid limit of 8 kW" from
371
+ * "the charger limit of 11 kW set by your installer". Told apart from
372
+ * {@link GridOperatorPowerLimitationActive}, which would be untrue here.
373
+ * Category {@link EnyoDataBusCommandReasonCategoryEnum.Contention}.
374
+ */
375
+ EnyoDataBusCommandReasonTypeEnum["InstallationLimit"] = "installation-limit";
318
376
  })(EnyoDataBusCommandReasonTypeEnum || (EnyoDataBusCommandReasonTypeEnum = {}));
319
377
  /**
320
378
  * Coarse, machine-readable grouping of why a data bus command was issued.
@@ -357,6 +415,23 @@ export var EnyoDataBusCommandReasonCategoryEnum;
357
415
  /** Any reason not covered by the categories above. */
358
416
  EnyoDataBusCommandReasonCategoryEnum["Other"] = "other";
359
417
  })(EnyoDataBusCommandReasonCategoryEnum || (EnyoDataBusCommandReasonCategoryEnum = {}));
418
+ /**
419
+ * Which owner- or installer-set limit capped the power behind an
420
+ * {@link EnyoDataBusCommandReasonTypeEnum.InstallationLimit}.
421
+ */
422
+ export var EnyoInstallationLimitSourceEnum;
423
+ (function (EnyoInstallationLimitSourceEnum) {
424
+ /**
425
+ * The soft grid limit the user or installer would like import and export
426
+ * to stay within — {@link EnyoGridConnectionPoint.desiredKwLimit}.
427
+ */
428
+ EnyoInstallationLimitSourceEnum["DesiredGridLimit"] = "desired-grid-limit";
429
+ /**
430
+ * The installer's cap on the total power all wallboxes may draw —
431
+ * {@link EnyoGridConnectionPoint.chargerLimitW}.
432
+ */
433
+ EnyoInstallationLimitSourceEnum["ChargerLimit"] = "charger-limit";
434
+ })(EnyoInstallationLimitSourceEnum || (EnyoInstallationLimitSourceEnum = {}));
360
435
  /**
361
436
  * Which part of the owner's battery-to-EV setting
362
437
  * (`batteryEvDischargeMode`) caused a
package/dist/version.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "1.29.0";
8
+ export declare const SDK_VERSION = "1.30.0";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export const SDK_VERSION = '1.29.0';
8
+ export const SDK_VERSION = '1.30.0';
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enyo-energy/energy-app-sdk",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "enyo Energy App SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",