@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 +43 -10
- package/dist/cjs/implementations/appliance-command-forecast/appliance-command-forecast-validators.cjs +69 -1
- package/dist/cjs/implementations/appliance-command-forecast/appliance-command-forecast-validators.d.cts +21 -1
- package/dist/cjs/types/enyo-appliance-command-forecast.d.cts +12 -0
- package/dist/cjs/types/enyo-data-bus-value.cjs +79 -4
- package/dist/cjs/types/enyo-data-bus-value.d.cts +180 -4
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.d.ts +21 -1
- package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.js +69 -2
- package/dist/types/enyo-appliance-command-forecast.d.ts +12 -0
- package/dist/types/enyo-data-bus-value.d.ts +180 -4
- package/dist/types/enyo-data-bus-value.js +78 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
3543
|
+
// A planned cadence:
|
|
3544
|
+
const scheduled: BatteryCommandForecast = {
|
|
3545
|
+
resolution: ApplianceForecastResolutionEnum.FifteenMinutes,
|
|
3546
|
+
mode: BatteryCommandForecastModeEnum.Schedule,
|
|
3536
3547
|
relativeSchedule: [
|
|
3537
|
-
{ seconds: 0,
|
|
3538
|
-
{ seconds: 1800,
|
|
3539
|
-
{ seconds: 3600,
|
|
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
|
-
|
|
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
|
-
|
|
3571
|
+
Invariants:
|
|
3548
3572
|
|
|
3549
|
-
- `
|
|
3550
|
-
- `
|
|
3551
|
-
- `
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
package/dist/cjs/version.cjs
CHANGED
|
@@ -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.
|
|
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
|
package/dist/cjs/version.d.cts
CHANGED
package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.d.ts
CHANGED
|
@@ -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.
|
package/dist/implementations/appliance-command-forecast/appliance-command-forecast-validators.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
package/dist/version.js
CHANGED