@enyo-energy/energy-app-sdk 1.13.0 → 1.15.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 +8 -4
- package/dist/cjs/implementations/appliances/appliance-manager.cjs +2 -1
- package/dist/cjs/implementations/appliances/appliance-manager.d.cts +12 -0
- package/dist/cjs/implementations/appliances/in-memory-appliance-manager.cjs +3 -1
- package/dist/cjs/implementations/automation/automation-validators.cjs +143 -10
- package/dist/cjs/implementations/automation/automation-validators.d.cts +15 -3
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +7 -2
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +9 -2
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +24 -0
- package/dist/cjs/packages/energy-app-automation.d.cts +44 -0
- package/dist/cjs/types/enyo-appliance.d.cts +8 -0
- package/dist/cjs/types/enyo-authentication.cjs +27 -1
- package/dist/cjs/types/enyo-authentication.d.cts +86 -7
- package/dist/cjs/types/enyo-automation.cjs +40 -0
- package/dist/cjs/types/enyo-automation.d.cts +252 -7
- package/dist/cjs/types/enyo-energy-manager.cjs +73 -0
- package/dist/cjs/types/enyo-energy-manager.d.cts +70 -1
- package/dist/cjs/types/enyo-heatpump-appliance.cjs +35 -1
- package/dist/cjs/types/enyo-heatpump-appliance.d.cts +147 -0
- package/dist/cjs/types/enyo-onboarding-v2.d.cts +19 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/appliances/appliance-manager.d.ts +12 -0
- package/dist/implementations/appliances/appliance-manager.js +2 -1
- package/dist/implementations/appliances/in-memory-appliance-manager.js +3 -1
- package/dist/implementations/automation/automation-validators.d.ts +15 -3
- package/dist/implementations/automation/automation-validators.js +142 -9
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +9 -2
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +7 -2
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +24 -0
- package/dist/packages/energy-app-automation.d.ts +44 -0
- package/dist/types/enyo-appliance.d.ts +8 -0
- package/dist/types/enyo-authentication.d.ts +86 -7
- package/dist/types/enyo-authentication.js +26 -0
- package/dist/types/enyo-automation.d.ts +252 -7
- package/dist/types/enyo-automation.js +40 -0
- package/dist/types/enyo-energy-manager.d.ts +70 -1
- package/dist/types/enyo-energy-manager.js +73 -0
- package/dist/types/enyo-heatpump-appliance.d.ts +147 -0
- package/dist/types/enyo-heatpump-appliance.js +34 -0
- package/dist/types/enyo-onboarding-v2.d.ts +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2962,17 +2962,21 @@ An `EnyoAutomation` is `{ id, name, enabled, trigger, actions[] }`. The pieces:
|
|
|
2962
2962
|
|
|
2963
2963
|
| Concept | Type | Values / fields |
|
|
2964
2964
|
| --- | --- | --- |
|
|
2965
|
-
| **Trigger** | `EnyoAutomationTriggerTypeEnum` | `
|
|
2966
|
-
| **
|
|
2965
|
+
| **Trigger — PV surplus above** | `EnyoAutomationTriggerTypeEnum.PvSurplusThreshold` | `{ thresholdW }` (activate above, deactivate below) |
|
|
2966
|
+
| **Trigger — PV surplus below** | `EnyoAutomationTriggerTypeEnum.PvSurplusBelowThreshold` | `{ thresholdW }` — the inverse: active while surplus stays below, deactivates (turns the target **off**) as soon as `surplusW >= thresholdW` |
|
|
2967
|
+
| **Trigger — below price limit** | `EnyoAutomationTriggerTypeEnum.BelowPriceLimit` | `{ limitPerKwh, currency? }` — active while the current **full gross** price per kWh (incl. taxes, grid fees and all other components) is strictly below the limit; negative limits are allowed |
|
|
2968
|
+
| **Trigger — cheapest share of day** | `EnyoAutomationTriggerTypeEnum.CheapestShareOfDay` | `{ sharePercent }` — active during the cheapest `sharePercent` % of the day's price intervals (e.g. `25` for the cheapest 25 %); integer `1…100` |
|
|
2969
|
+
| **Trigger — schedule ("Zeitplan")** | `EnyoAutomationTriggerTypeEnum.Schedule` | `{ windows[], timezone? }` — each window is `{ startTimeOfDay, endTimeOfDay, daysOfWeek? }` in local `HH:mm` (`0` = Sunday … `6` = Saturday, omit for every day); windows may wrap past midnight and may overlap — the trigger is active while **any** window covers the current time |
|
|
2970
|
+
| **Action — smart plug** | `EnyoAutomationActionTypeEnum.SmartPlugSwitch` | `{ applianceId, minDurationMinutes }` — `minDurationMinutes` is `1…360`: whole minutes up to `5` (so a **1-minute** runtime is supported), multiples of `5` above that |
|
|
2967
2971
|
| **Action — MQTT** | `EnyoAutomationActionTypeEnum.Mqtt` | `{ topic, payloadTemplate, updateChargingPvSurplus, publishOptions? }` |
|
|
2968
2972
|
| **Scheduling** | `EnyoAutomationSchedulingModeEnum` | `Mandatory` (run exactly while active) or `Flexible` (Energy Manager may choose whether/when within the active window) |
|
|
2969
2973
|
| **Target kind** | `EnyoAutomationTargetKindEnum` | `Load` (consumes power — counts in the energy balance) or `Signal` (control signal only) |
|
|
2970
2974
|
|
|
2971
|
-
The **MQTT** `payloadTemplate` is JSON that may embed the placeholders in `EnyoAutomationMqttPlaceholderEnum` — `{{state}}` (`on`/`off`), `{{surplusW}}`, `{{timestampIso}}`, `{{automationId}}` — which the platform substitutes before publishing.
|
|
2975
|
+
The **MQTT** `payloadTemplate` is JSON that may embed the placeholders in `EnyoAutomationMqttPlaceholderEnum` — `{{state}}` (`on`/`off`), `{{surplusW}}`, `{{pricePerKwh}}`, `{{timestampIso}}`, `{{automationId}}` — which the platform substitutes before publishing.
|
|
2972
2976
|
|
|
2973
2977
|
Two things travel over the **data bus** vs. the **API**:
|
|
2974
2978
|
|
|
2975
|
-
- **Trigger state** is the data-bus message `AutomationTriggerV1` (`EnyoDataBusAutomationTriggerV1`): `{ automationId, data: { active, trigger } }`, where `trigger` is the per-type `EnyoAutomationTriggerData` (
|
|
2979
|
+
- **Trigger state** is the data-bus message `AutomationTriggerV1` (`EnyoDataBusAutomationTriggerV1`): `{ automationId, data: { active, trigger } }`, where `trigger` is the per-type `EnyoAutomationTriggerData` (PV surplus: `{ triggerType, surplusW, thresholdW }`; below-price-limit: `{ triggerType, pricePerKwh, limitPerKwh, currency }`; cheapest-share-of-day: `{ triggerType, pricePerKwh, sharePercent, thresholdPricePerKwh, currency }`; schedule: `{ triggerType, windowIndex?, windowStartIso?, windowEndIso? }`).
|
|
2976
2980
|
- The **forecast** is a method — `publishAutomationForecast()` — not a data-bus message.
|
|
2977
2981
|
|
|
2978
2982
|
### ⚡ Guide: Energy Manager apps
|
|
@@ -211,7 +211,7 @@ class ApplianceManager {
|
|
|
211
211
|
airConditioning: appliance.airConditioning,
|
|
212
212
|
heatingRod: appliance.heatingRod,
|
|
213
213
|
smartPlug: appliance.smartPlug,
|
|
214
|
-
// Conditionally spread the
|
|
214
|
+
// Conditionally spread the optional top-level fields that are NOT
|
|
215
215
|
// covered by MERGEABLE_METADATA_KEYS. If they were always materialized
|
|
216
216
|
// as explicit keys, an omitted (undefined) value would clobber the
|
|
217
217
|
// stored value during mergeApplianceData's `{...existing, ...update}`
|
|
@@ -219,6 +219,7 @@ class ApplianceManager {
|
|
|
219
219
|
// while dropping omitted fields so the existing value is preserved.
|
|
220
220
|
...(appliance.cloudPackageId !== undefined && { cloudPackageId: appliance.cloudPackageId }),
|
|
221
221
|
...(appliance.availableFeatures !== undefined && { availableFeatures: appliance.availableFeatures }),
|
|
222
|
+
...(appliance.compatibilityModes !== undefined && { compatibilityModes: appliance.compatibilityModes }),
|
|
222
223
|
};
|
|
223
224
|
let applianceData = newApplianceData;
|
|
224
225
|
if (existingApplianceId) {
|
|
@@ -65,6 +65,13 @@ export interface ApplianceConfig {
|
|
|
65
65
|
heatingRod?: EnyoHeatingRodApplianceMetadata;
|
|
66
66
|
smartPlug?: EnyoSmartPlugApplianceMetadata;
|
|
67
67
|
availableFeatures?: EnyoApplianceAvailableFeaturesEnum[];
|
|
68
|
+
/**
|
|
69
|
+
* Optional vendor- or integration-specific compatibility modes. Forwarded to
|
|
70
|
+
* {@link EnyoAppliance.compatibilityModes} when the appliance is created or
|
|
71
|
+
* updated. Omit to keep the stored value on an update; pass an explicit `[]`
|
|
72
|
+
* to clear it.
|
|
73
|
+
*/
|
|
74
|
+
compatibilityModes?: string[];
|
|
68
75
|
/**
|
|
69
76
|
* Optional identifier of the cloud-deployed energy app package that manages
|
|
70
77
|
* this appliance. Forwarded to {@link EnyoAppliance.cloudPackageId} when the
|
|
@@ -443,6 +450,11 @@ export interface PartialEnyoAppliance {
|
|
|
443
450
|
smartPlug?: Partial<EnyoSmartPlugApplianceMetadata>;
|
|
444
451
|
/** Optional custom name for the appliance, defined by the user */
|
|
445
452
|
customName?: string;
|
|
453
|
+
/**
|
|
454
|
+
* Optional list of vendor- or integration-specific compatibility modes that
|
|
455
|
+
* are active for this appliance. Mirrors {@link EnyoAppliance.compatibilityModes}.
|
|
456
|
+
*/
|
|
457
|
+
compatibilityModes?: string[];
|
|
446
458
|
/**
|
|
447
459
|
* Optional identifier of the cloud-deployed energy app package that manages
|
|
448
460
|
* this appliance. Mirrors {@link EnyoAppliance.cloudPackageId}.
|
|
@@ -54,7 +54,8 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
54
54
|
};
|
|
55
55
|
// Build the incoming appliance data covering every ApplianceConfig field.
|
|
56
56
|
// The two optional top-level fields that are NOT shallow-merged by
|
|
57
|
-
// mergeApplianceData (`cloudPackageId`, `availableFeatures
|
|
57
|
+
// mergeApplianceData (`cloudPackageId`, `availableFeatures`,
|
|
58
|
+
// `compatibilityModes`) are only
|
|
58
59
|
// materialized when provided so an omitted value cannot clobber the stored
|
|
59
60
|
// one during an update; pass an explicit value (e.g. `[]`) to clear them.
|
|
60
61
|
const newApplianceData = {
|
|
@@ -73,6 +74,7 @@ class InMemoryApplianceManager extends appliance_manager_js_1.ApplianceManager {
|
|
|
73
74
|
heatingRod: appliance.heatingRod,
|
|
74
75
|
...(appliance.cloudPackageId !== undefined && { cloudPackageId: appliance.cloudPackageId }),
|
|
75
76
|
...(appliance.availableFeatures !== undefined && { availableFeatures: appliance.availableFeatures }),
|
|
77
|
+
...(appliance.compatibilityModes !== undefined && { compatibilityModes: appliance.compatibilityModes }),
|
|
76
78
|
};
|
|
77
79
|
// On update, merge onto the stored appliance so type-specific metadata and
|
|
78
80
|
// top-level fields that were not supplied in this call are preserved.
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AUTOMATION_DURATION_STEP_MINUTES = exports.AUTOMATION_MAX_DURATION_MINUTES = exports.AUTOMATION_MIN_DURATION_MINUTES = exports.AutomationValidationError = void 0;
|
|
3
|
+
exports.AUTOMATION_MAX_CHEAPEST_SHARE_PERCENT = exports.AUTOMATION_MIN_CHEAPEST_SHARE_PERCENT = exports.AUTOMATION_DURATION_STEP_MINUTES = exports.AUTOMATION_MAX_DURATION_MINUTES = exports.AUTOMATION_MIN_DURATION_MINUTES = exports.AutomationValidationError = void 0;
|
|
4
4
|
exports.validateAutomation = validateAutomation;
|
|
5
5
|
exports.validateTrigger = validateTrigger;
|
|
6
6
|
exports.validateAction = validateAction;
|
|
7
7
|
exports.validateAutomationForecast = validateAutomationForecast;
|
|
8
8
|
exports.validateAutomationTriggerData = validateAutomationTriggerData;
|
|
9
9
|
const enyo_automation_js_1 = require("../../types/enyo-automation.cjs");
|
|
10
|
+
const enyo_currency_js_1 = require("../../types/enyo-currency.cjs");
|
|
10
11
|
const enyo_mqtt_js_1 = require("../../types/enyo-mqtt.cjs");
|
|
12
|
+
const price_schedule_resolver_js_1 = require("../pricing/price-schedule-resolver.cjs");
|
|
11
13
|
/**
|
|
12
14
|
* Thrown when an automation, action or automation forecast violates one of the
|
|
13
15
|
* invariants enforced by this module. The message names the offending field /
|
|
@@ -20,12 +22,24 @@ class AutomationValidationError extends Error {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
exports.AutomationValidationError = AutomationValidationError;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Minimum smart-plug on-duration, in minutes. Short runtimes below
|
|
27
|
+
* {@link AUTOMATION_DURATION_STEP_MINUTES} are allowed at a one-minute
|
|
28
|
+
* granularity, so a one-minute runtime is valid.
|
|
29
|
+
*/
|
|
30
|
+
exports.AUTOMATION_MIN_DURATION_MINUTES = 1;
|
|
25
31
|
/** Maximum smart-plug on-duration, in minutes (6 hours). */
|
|
26
32
|
exports.AUTOMATION_MAX_DURATION_MINUTES = 360;
|
|
27
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Step size the smart-plug on-duration must be a multiple of, in minutes, once
|
|
35
|
+
* it exceeds {@link AUTOMATION_DURATION_STEP_MINUTES}. Durations at or below
|
|
36
|
+
* this value are free to use any whole minute (1, 2, 3, 4, 5).
|
|
37
|
+
*/
|
|
28
38
|
exports.AUTOMATION_DURATION_STEP_MINUTES = 5;
|
|
39
|
+
/** Minimum share of the day, in percent, a cheapest-share trigger may select. */
|
|
40
|
+
exports.AUTOMATION_MIN_CHEAPEST_SHARE_PERCENT = 1;
|
|
41
|
+
/** Maximum share of the day, in percent, a cheapest-share trigger may select. */
|
|
42
|
+
exports.AUTOMATION_MAX_CHEAPEST_SHARE_PERCENT = 100;
|
|
29
43
|
/** Duration in seconds of each {@link EnyoForecastResolution} value. */
|
|
30
44
|
const FORECAST_RESOLUTION_SECONDS = {
|
|
31
45
|
'10s': 10,
|
|
@@ -87,6 +101,31 @@ function validateTrigger(trigger) {
|
|
|
87
101
|
throw new AutomationValidationError('PvSurplusThreshold trigger.thresholdW must be a finite number >= 0.');
|
|
88
102
|
}
|
|
89
103
|
break;
|
|
104
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.PvSurplusBelowThreshold:
|
|
105
|
+
if (typeof trigger.thresholdW !== 'number' || !Number.isFinite(trigger.thresholdW) || trigger.thresholdW < 0) {
|
|
106
|
+
throw new AutomationValidationError('PvSurplusBelowThreshold trigger.thresholdW must be a finite number >= 0.');
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.BelowPriceLimit:
|
|
110
|
+
// Negative limits are valid: dynamic prices can turn negative.
|
|
111
|
+
if (typeof trigger.limitPerKwh !== 'number' || !Number.isFinite(trigger.limitPerKwh)) {
|
|
112
|
+
throw new AutomationValidationError('BelowPriceLimit trigger.limitPerKwh must be a finite number.');
|
|
113
|
+
}
|
|
114
|
+
if (trigger.currency !== undefined) {
|
|
115
|
+
requireEnumMember(trigger.currency, enyo_currency_js_1.EnyoCurrencyEnum, 'BelowPriceLimit trigger.currency');
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.CheapestShareOfDay:
|
|
119
|
+
if (typeof trigger.sharePercent !== 'number' ||
|
|
120
|
+
!Number.isInteger(trigger.sharePercent) ||
|
|
121
|
+
trigger.sharePercent < exports.AUTOMATION_MIN_CHEAPEST_SHARE_PERCENT ||
|
|
122
|
+
trigger.sharePercent > exports.AUTOMATION_MAX_CHEAPEST_SHARE_PERCENT) {
|
|
123
|
+
throw new AutomationValidationError(`CheapestShareOfDay trigger.sharePercent must be an integer between ${exports.AUTOMATION_MIN_CHEAPEST_SHARE_PERCENT} and ${exports.AUTOMATION_MAX_CHEAPEST_SHARE_PERCENT}.`);
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.Schedule:
|
|
127
|
+
validateScheduleTrigger(trigger);
|
|
128
|
+
break;
|
|
90
129
|
default:
|
|
91
130
|
throw new AutomationValidationError(`Automation.trigger.type is invalid: ${trigger.type}.`);
|
|
92
131
|
}
|
|
@@ -173,13 +212,90 @@ function validateAutomationTriggerData(trigger) {
|
|
|
173
212
|
}
|
|
174
213
|
switch (trigger.triggerType) {
|
|
175
214
|
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.PvSurplusThreshold:
|
|
215
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.PvSurplusBelowThreshold:
|
|
176
216
|
requireFiniteNonNegative(trigger.surplusW, 'AutomationTriggerData.surplusW');
|
|
177
217
|
requireFiniteNonNegative(trigger.thresholdW, 'AutomationTriggerData.thresholdW');
|
|
178
218
|
break;
|
|
219
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.BelowPriceLimit:
|
|
220
|
+
requireFinite(trigger.pricePerKwh, 'AutomationTriggerData.pricePerKwh');
|
|
221
|
+
requireFinite(trigger.limitPerKwh, 'AutomationTriggerData.limitPerKwh');
|
|
222
|
+
requireEnumMember(trigger.currency, enyo_currency_js_1.EnyoCurrencyEnum, 'AutomationTriggerData.currency');
|
|
223
|
+
break;
|
|
224
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.CheapestShareOfDay:
|
|
225
|
+
requireFinite(trigger.pricePerKwh, 'AutomationTriggerData.pricePerKwh');
|
|
226
|
+
requireFinite(trigger.thresholdPricePerKwh, 'AutomationTriggerData.thresholdPricePerKwh');
|
|
227
|
+
if (typeof trigger.sharePercent !== 'number' ||
|
|
228
|
+
!Number.isInteger(trigger.sharePercent) ||
|
|
229
|
+
trigger.sharePercent < exports.AUTOMATION_MIN_CHEAPEST_SHARE_PERCENT ||
|
|
230
|
+
trigger.sharePercent > exports.AUTOMATION_MAX_CHEAPEST_SHARE_PERCENT) {
|
|
231
|
+
throw new AutomationValidationError(`AutomationTriggerData.sharePercent must be an integer between ${exports.AUTOMATION_MIN_CHEAPEST_SHARE_PERCENT} and ${exports.AUTOMATION_MAX_CHEAPEST_SHARE_PERCENT}.`);
|
|
232
|
+
}
|
|
233
|
+
requireEnumMember(trigger.currency, enyo_currency_js_1.EnyoCurrencyEnum, 'AutomationTriggerData.currency');
|
|
234
|
+
break;
|
|
235
|
+
case enyo_automation_js_1.EnyoAutomationTriggerTypeEnum.Schedule:
|
|
236
|
+
if (trigger.windowIndex !== undefined && (!Number.isInteger(trigger.windowIndex) || trigger.windowIndex < 0)) {
|
|
237
|
+
throw new AutomationValidationError('AutomationTriggerData.windowIndex must be an integer >= 0.');
|
|
238
|
+
}
|
|
239
|
+
requireOptionalIsoTimestamp(trigger.windowStartIso, 'AutomationTriggerData.windowStartIso');
|
|
240
|
+
requireOptionalIsoTimestamp(trigger.windowEndIso, 'AutomationTriggerData.windowEndIso');
|
|
241
|
+
break;
|
|
179
242
|
default:
|
|
180
243
|
throw new AutomationValidationError(`AutomationTriggerData.triggerType is invalid: ${trigger.triggerType}.`);
|
|
181
244
|
}
|
|
182
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Validates the windows of a {@link EnyoAutomationTriggerTypeEnum.Schedule}
|
|
248
|
+
* ("Zeitplan") trigger: at least one window, well-formed `HH:mm` times that do
|
|
249
|
+
* not start and end at the same minute, weekday numbers in range, and a valid
|
|
250
|
+
* IANA timezone when given. Windows are a union and may overlap, so no overlap
|
|
251
|
+
* check is performed.
|
|
252
|
+
*/
|
|
253
|
+
function validateScheduleTrigger(trigger) {
|
|
254
|
+
if (!Array.isArray(trigger.windows) || trigger.windows.length === 0) {
|
|
255
|
+
throw new AutomationValidationError('Schedule trigger.windows must contain at least one window.');
|
|
256
|
+
}
|
|
257
|
+
trigger.windows.forEach((window, index) => {
|
|
258
|
+
const label = `Schedule trigger.windows[${index}]`;
|
|
259
|
+
if (!window || typeof window !== 'object') {
|
|
260
|
+
throw new AutomationValidationError(`${label} must be an object.`);
|
|
261
|
+
}
|
|
262
|
+
const start = (0, price_schedule_resolver_js_1.parseTimeOfDay)(window.startTimeOfDay);
|
|
263
|
+
if (start === null) {
|
|
264
|
+
throw new AutomationValidationError(`${label}.startTimeOfDay must be a 24-hour 'HH:mm' time, got '${window.startTimeOfDay}'.`);
|
|
265
|
+
}
|
|
266
|
+
const end = (0, price_schedule_resolver_js_1.parseTimeOfDay)(window.endTimeOfDay);
|
|
267
|
+
if (end === null) {
|
|
268
|
+
throw new AutomationValidationError(`${label}.endTimeOfDay must be a 24-hour 'HH:mm' time, got '${window.endTimeOfDay}'.`);
|
|
269
|
+
}
|
|
270
|
+
if (start === end) {
|
|
271
|
+
throw new AutomationValidationError(`${label} must not start and end at the same time of day.`);
|
|
272
|
+
}
|
|
273
|
+
if (window.daysOfWeek !== undefined) {
|
|
274
|
+
if (!Array.isArray(window.daysOfWeek) || window.daysOfWeek.length === 0) {
|
|
275
|
+
throw new AutomationValidationError(`${label}.daysOfWeek must not be empty — omit it for 'every day'.`);
|
|
276
|
+
}
|
|
277
|
+
const seenDays = new Set();
|
|
278
|
+
for (const day of window.daysOfWeek) {
|
|
279
|
+
if (!Number.isInteger(day) || day < 0 || day > 6) {
|
|
280
|
+
throw new AutomationValidationError(`${label}.daysOfWeek must contain integers 0 (Sunday) to 6 (Saturday), got ${day}.`);
|
|
281
|
+
}
|
|
282
|
+
if (seenDays.has(day)) {
|
|
283
|
+
throw new AutomationValidationError(`${label}.daysOfWeek contains the duplicate day ${day}.`);
|
|
284
|
+
}
|
|
285
|
+
seenDays.add(day);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
if (trigger.timezone !== undefined) {
|
|
290
|
+
requireNonEmptyString(trigger.timezone, 'Schedule trigger.timezone');
|
|
291
|
+
try {
|
|
292
|
+
new Intl.DateTimeFormat('en-US', { timeZone: trigger.timezone });
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
throw new AutomationValidationError(`Schedule trigger.timezone must be a valid IANA time zone identifier, got '${trigger.timezone}'.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
183
299
|
function validateMqttAction(action, label) {
|
|
184
300
|
requireNonEmptyString(action.topic, `${label}.topic`);
|
|
185
301
|
if (typeof action.updateChargingPvSurplus !== 'boolean') {
|
|
@@ -203,12 +319,16 @@ function validateSmartPlugSwitchAction(action, label, knownSmartPlugApplianceIds
|
|
|
203
319
|
throw new AutomationValidationError(`${label}.applianceId does not reference a smart-plug appliance that supports the smart-plug-switch action: ${action.applianceId}.`);
|
|
204
320
|
}
|
|
205
321
|
const { minDurationMinutes } = action;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
minDurationMinutes
|
|
210
|
-
minDurationMinutes
|
|
211
|
-
|
|
322
|
+
// Short runtimes (1-5 minutes) are allowed at whole-minute granularity so a
|
|
323
|
+
// one-minute pulse can be configured; anything longer keeps the 5-minute step.
|
|
324
|
+
const isWholeMinutesInRange = typeof minDurationMinutes === 'number' &&
|
|
325
|
+
Number.isInteger(minDurationMinutes) &&
|
|
326
|
+
minDurationMinutes >= exports.AUTOMATION_MIN_DURATION_MINUTES &&
|
|
327
|
+
minDurationMinutes <= exports.AUTOMATION_MAX_DURATION_MINUTES;
|
|
328
|
+
const matchesStep = minDurationMinutes <= exports.AUTOMATION_DURATION_STEP_MINUTES ||
|
|
329
|
+
minDurationMinutes % exports.AUTOMATION_DURATION_STEP_MINUTES === 0;
|
|
330
|
+
if (!isWholeMinutesInRange || !matchesStep) {
|
|
331
|
+
throw new AutomationValidationError(`${label}.minDurationMinutes must be an integer between ${exports.AUTOMATION_MIN_DURATION_MINUTES} and ${exports.AUTOMATION_MAX_DURATION_MINUTES}; values above ${exports.AUTOMATION_DURATION_STEP_MINUTES} must be a multiple of ${exports.AUTOMATION_DURATION_STEP_MINUTES}.`);
|
|
212
332
|
}
|
|
213
333
|
}
|
|
214
334
|
/**
|
|
@@ -233,6 +353,19 @@ function validatePayloadTemplate(template, label) {
|
|
|
233
353
|
throw new AutomationValidationError(`${label} is not valid JSON once placeholders are substituted.`);
|
|
234
354
|
}
|
|
235
355
|
}
|
|
356
|
+
function requireOptionalIsoTimestamp(value, label) {
|
|
357
|
+
if (value === undefined) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) {
|
|
361
|
+
throw new AutomationValidationError(`${label} must be a valid ISO 8601 timestamp: ${String(value)}.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function requireFinite(value, label) {
|
|
365
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
366
|
+
throw new AutomationValidationError(`${label} must be a finite number.`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
236
369
|
function requireFiniteNonNegative(value, label) {
|
|
237
370
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
238
371
|
throw new AutomationValidationError(`${label} must be a finite number >= 0.`);
|
|
@@ -7,12 +7,24 @@ import { EnyoAutomation, EnyoAutomationAction, EnyoAutomationForecast, EnyoAutom
|
|
|
7
7
|
export declare class AutomationValidationError extends Error {
|
|
8
8
|
constructor(message: string);
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Minimum smart-plug on-duration, in minutes. Short runtimes below
|
|
12
|
+
* {@link AUTOMATION_DURATION_STEP_MINUTES} are allowed at a one-minute
|
|
13
|
+
* granularity, so a one-minute runtime is valid.
|
|
14
|
+
*/
|
|
15
|
+
export declare const AUTOMATION_MIN_DURATION_MINUTES = 1;
|
|
12
16
|
/** Maximum smart-plug on-duration, in minutes (6 hours). */
|
|
13
17
|
export declare const AUTOMATION_MAX_DURATION_MINUTES = 360;
|
|
14
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Step size the smart-plug on-duration must be a multiple of, in minutes, once
|
|
20
|
+
* it exceeds {@link AUTOMATION_DURATION_STEP_MINUTES}. Durations at or below
|
|
21
|
+
* this value are free to use any whole minute (1, 2, 3, 4, 5).
|
|
22
|
+
*/
|
|
15
23
|
export declare const AUTOMATION_DURATION_STEP_MINUTES = 5;
|
|
24
|
+
/** Minimum share of the day, in percent, a cheapest-share trigger may select. */
|
|
25
|
+
export declare const AUTOMATION_MIN_CHEAPEST_SHARE_PERCENT = 1;
|
|
26
|
+
/** Maximum share of the day, in percent, a cheapest-share trigger may select. */
|
|
27
|
+
export declare const AUTOMATION_MAX_CHEAPEST_SHARE_PERCENT = 100;
|
|
16
28
|
/**
|
|
17
29
|
* Validates a complete {@link EnyoAutomation}. Throws on the first violation.
|
|
18
30
|
*
|
|
@@ -362,8 +362,12 @@ exports.onboardingV2Block = {
|
|
|
362
362
|
* @param label - Translated sign-in button text (de/en).
|
|
363
363
|
* @param outcome - The single success handle (`{id, label}`).
|
|
364
364
|
* @param opts - Optional translated `help` naming the account that is needed,
|
|
365
|
-
* and `
|
|
366
|
-
* when the provider rejects a custom-scheme redirect
|
|
365
|
+
* and `redirectUrlFilter` to constrain the enyo callback URL the host
|
|
366
|
+
* generates — `webOnly` when the provider rejects a custom-scheme redirect
|
|
367
|
+
* such as `enyoapp://`, `pattern` when its OAuth app was registered with a
|
|
368
|
+
* redirect URI that carries the request id as a path segment rather than a
|
|
369
|
+
* query parameter. `requiresWebAuthentication` is the deprecated spelling
|
|
370
|
+
* of `redirectUrlFilter.webOnly`.
|
|
367
371
|
*/
|
|
368
372
|
auth: (id, label, outcome, opts) => ({
|
|
369
373
|
id,
|
|
@@ -372,6 +376,7 @@ exports.onboardingV2Block = {
|
|
|
372
376
|
outcome,
|
|
373
377
|
help: opts?.help,
|
|
374
378
|
requiresWebAuthentication: opts?.requiresWebAuthentication,
|
|
379
|
+
redirectUrlFilter: opts?.redirectUrlFilter,
|
|
375
380
|
}),
|
|
376
381
|
/**
|
|
377
382
|
* A link block: a fixed URL the installer opens or copies.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* before publishing.
|
|
11
11
|
*/
|
|
12
12
|
import type { EnyoOnboardingTranslatedContent } from '../../types/enyo-onboarding.cjs';
|
|
13
|
+
import type { EnyoOauthRedirectUrlFilter } from '../../types/enyo-authentication.cjs';
|
|
13
14
|
import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2Credential, EnyoOnboardingV2SelectOption, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection, EnyoOnboardingV2PauseReason } from '../../types/enyo-onboarding-v2.cjs';
|
|
14
15
|
import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2AuthOutcome, EnyoOnboardingV2SetupField, EnyoOnboardingV2SetupOutcome, EnyoOnboardingV2SetupSkipHandle, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DeviceSelectBlock, EnyoOnboardingV2EebusDeviceSelectBlock, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2InputOutcome, EnyoOnboardingV2InputValueType, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.cjs';
|
|
15
16
|
/**
|
|
@@ -305,12 +306,18 @@ export declare const onboardingV2Block: {
|
|
|
305
306
|
* @param label - Translated sign-in button text (de/en).
|
|
306
307
|
* @param outcome - The single success handle (`{id, label}`).
|
|
307
308
|
* @param opts - Optional translated `help` naming the account that is needed,
|
|
308
|
-
* and `
|
|
309
|
-
* when the provider rejects a custom-scheme redirect
|
|
309
|
+
* and `redirectUrlFilter` to constrain the enyo callback URL the host
|
|
310
|
+
* generates — `webOnly` when the provider rejects a custom-scheme redirect
|
|
311
|
+
* such as `enyoapp://`, `pattern` when its OAuth app was registered with a
|
|
312
|
+
* redirect URI that carries the request id as a path segment rather than a
|
|
313
|
+
* query parameter. `requiresWebAuthentication` is the deprecated spelling
|
|
314
|
+
* of `redirectUrlFilter.webOnly`.
|
|
310
315
|
*/
|
|
311
316
|
auth: (id: string, label: EnyoOnboardingTranslatedContent[], outcome: EnyoOnboardingV2AuthOutcome, opts?: {
|
|
312
317
|
help?: EnyoOnboardingTranslatedContent[];
|
|
318
|
+
/** @deprecated Use `redirectUrlFilter.webOnly`. */
|
|
313
319
|
requiresWebAuthentication?: boolean;
|
|
320
|
+
redirectUrlFilter?: EnyoOauthRedirectUrlFilter;
|
|
314
321
|
}) => EnyoOnboardingV2Block;
|
|
315
322
|
/**
|
|
316
323
|
* A link block: a fixed URL the installer opens or copies.
|
|
@@ -20,6 +20,7 @@ exports.validateOnboardingGuideV2 = validateOnboardingGuideV2;
|
|
|
20
20
|
exports.assertValidOnboardingGuideV2 = assertValidOnboardingGuideV2;
|
|
21
21
|
const enyo_onboarding_v2_js_1 = require("../../types/enyo-onboarding-v2.cjs");
|
|
22
22
|
const enyo_device_test_js_1 = require("../../types/enyo-device-test.cjs");
|
|
23
|
+
const enyo_authentication_js_1 = require("../../types/enyo-authentication.cjs");
|
|
23
24
|
const enyo_onboarding_v2_additional_setup_js_1 = require("../../types/enyo-onboarding-v2-additional-setup.cjs");
|
|
24
25
|
const public_file_validators_js_1 = require("../files/public-file-validators.cjs");
|
|
25
26
|
const define_public_file_js_1 = require("../files/define-public-file.cjs");
|
|
@@ -581,6 +582,8 @@ function validateEebusPairOutcomes(block, at, errors, warnings) {
|
|
|
581
582
|
'a successful pairing would have nowhere to go.');
|
|
582
583
|
}
|
|
583
584
|
}
|
|
585
|
+
/** The redirect-URL patterns an auth block's `redirectUrlFilter` may name. */
|
|
586
|
+
const REDIRECT_URL_PATTERNS = new Set(Object.values(enyo_authentication_js_1.EnyoOauthRedirectUrlPatternEnum));
|
|
584
587
|
/**
|
|
585
588
|
* Every block type that owns a decision — a picker shares a step with none of
|
|
586
589
|
* them.
|
|
@@ -685,6 +688,27 @@ function validateAuthBlocks(step, at, errors, warnings) {
|
|
|
685
688
|
if (!block.outcome?.label?.length) {
|
|
686
689
|
warnings.push(`${at}: auth block "${block.id}" outcome has no label.`);
|
|
687
690
|
}
|
|
691
|
+
const filter = block.redirectUrlFilter;
|
|
692
|
+
if (filter !== undefined) {
|
|
693
|
+
if (typeof filter !== 'object') {
|
|
694
|
+
errors.push(`${at}: auth block "${block.id}" redirectUrlFilter must be an object.`);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
if (filter.webOnly !== undefined && typeof filter.webOnly !== 'boolean') {
|
|
698
|
+
errors.push(`${at}: auth block "${block.id}" redirectUrlFilter.webOnly must be a boolean.`);
|
|
699
|
+
}
|
|
700
|
+
if (filter.pattern !== undefined &&
|
|
701
|
+
!REDIRECT_URL_PATTERNS.has(filter.pattern)) {
|
|
702
|
+
errors.push(`${at}: auth block "${block.id}" redirectUrlFilter.pattern is invalid: ${filter.pattern}. ` +
|
|
703
|
+
`Allowed values: ${[...REDIRECT_URL_PATTERNS].join(', ')}.`);
|
|
704
|
+
}
|
|
705
|
+
if (block.requiresWebAuthentication === true &&
|
|
706
|
+
filter.webOnly === false) {
|
|
707
|
+
warnings.push(`${at}: auth block "${block.id}" sets requiresWebAuthentication but redirectUrlFilter.webOnly is false — ` +
|
|
708
|
+
'the filter wins; drop the deprecated flag.');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
688
712
|
}
|
|
689
713
|
if (authBlocks.length > 1) {
|
|
690
714
|
errors.push(`${at}: more than one auth block; a step can hold at most one login.`);
|
|
@@ -17,6 +17,14 @@ import { EnyoAutomation, EnyoAutomationForecast, EnyoAutomationTriggerTypeEnum }
|
|
|
17
17
|
* (via `useDataBus()`) whenever the configured threshold is crossed. Trigger
|
|
18
18
|
* state is a data-bus message, not a method on this package.
|
|
19
19
|
*
|
|
20
|
+
* Supported triggers (see {@link EnyoAutomationTriggerTypeEnum}):
|
|
21
|
+
* `PvSurplusThreshold` (surplus above X W), `PvSurplusBelowThreshold` (turn the
|
|
22
|
+
* target off once the surplus reaches X W), `BelowPriceLimit` (price per kWh
|
|
23
|
+
* below a limit), `CheapestShareOfDay` (the cheapest N % of the day, e.g. 25 %)
|
|
24
|
+
* and `Schedule` (the "Zeitplan" trigger — one or more start/end time windows,
|
|
25
|
+
* each optionally restricted to certain weekdays). A provider registers each
|
|
26
|
+
* type it can evaluate independently.
|
|
27
|
+
*
|
|
20
28
|
* @example
|
|
21
29
|
* ```typescript
|
|
22
30
|
* // A "pool pump on solar" automation the user created:
|
|
@@ -58,6 +66,42 @@ import { EnyoAutomation, EnyoAutomationForecast, EnyoAutomationTriggerTypeEnum }
|
|
|
58
66
|
* },
|
|
59
67
|
* },
|
|
60
68
|
* }]);
|
|
69
|
+
*
|
|
70
|
+
* // A price-driven automation: run the pump during the cheapest 25 % of the day
|
|
71
|
+
* // for at least one minute per activation.
|
|
72
|
+
* const cheapHours: EnyoAutomation = {
|
|
73
|
+
* id: 'pump-cheap-hours',
|
|
74
|
+
* name: 'Pool pump in cheap hours',
|
|
75
|
+
* enabled: true,
|
|
76
|
+
* trigger: {type: EnyoAutomationTriggerTypeEnum.CheapestShareOfDay, sharePercent: 25},
|
|
77
|
+
* actions: [
|
|
78
|
+
* {
|
|
79
|
+
* id: 'switch-pump',
|
|
80
|
+
* type: EnyoAutomationActionTypeEnum.SmartPlugSwitch,
|
|
81
|
+
* schedulingMode: EnyoAutomationSchedulingModeEnum.Flexible,
|
|
82
|
+
* targetKind: EnyoAutomationTargetKindEnum.Load,
|
|
83
|
+
* applianceId: 'shelly-pool-ch0',
|
|
84
|
+
* minDurationMinutes: 1,
|
|
85
|
+
* },
|
|
86
|
+
* ],
|
|
87
|
+
* };
|
|
88
|
+
*
|
|
89
|
+
* // A "Zeitplan": weekday mornings and evenings, plus all day on Sunday.
|
|
90
|
+
* const schedule: EnyoAutomation['trigger'] = {
|
|
91
|
+
* type: EnyoAutomationTriggerTypeEnum.Schedule,
|
|
92
|
+
* windows: [
|
|
93
|
+
* {startTimeOfDay: '06:00', endTimeOfDay: '08:00', daysOfWeek: [1, 2, 3, 4, 5]},
|
|
94
|
+
* {startTimeOfDay: '18:00', endTimeOfDay: '22:00', daysOfWeek: [1, 2, 3, 4, 5]},
|
|
95
|
+
* {startTimeOfDay: '00:00', endTimeOfDay: '23:59', daysOfWeek: [0]},
|
|
96
|
+
* ],
|
|
97
|
+
* timezone: 'Europe/Berlin',
|
|
98
|
+
* };
|
|
99
|
+
*
|
|
100
|
+
* // "Turn the heating rod off as soon as the PV surplus reaches 3 kW":
|
|
101
|
+
* const offOnSurplus: EnyoAutomation['trigger'] = {
|
|
102
|
+
* type: EnyoAutomationTriggerTypeEnum.PvSurplusBelowThreshold,
|
|
103
|
+
* thresholdW: 3000,
|
|
104
|
+
* };
|
|
61
105
|
* ```
|
|
62
106
|
*/
|
|
63
107
|
export interface EnergyAppAutomation {
|
|
@@ -277,6 +277,14 @@ export interface EnyoAppliance {
|
|
|
277
277
|
smartPlug?: EnyoSmartPlugApplianceMetadata;
|
|
278
278
|
/** Optional custom name for the appliance, defined by the user */
|
|
279
279
|
customName?: string;
|
|
280
|
+
/**
|
|
281
|
+
* Optional list of vendor- or integration-specific compatibility modes that
|
|
282
|
+
* are active for this appliance. Each entry is a free-form, non-localized
|
|
283
|
+
* identifier describing a behavioural deviation the appliance requires
|
|
284
|
+
* (e.g. a firmware quirk workaround or a legacy protocol dialect).
|
|
285
|
+
* Consumers that do not know a given mode should ignore it.
|
|
286
|
+
*/
|
|
287
|
+
compatibilityModes?: string[];
|
|
280
288
|
/**
|
|
281
289
|
* Optional identifier of the cloud-deployed energy app package that manages
|
|
282
290
|
* this appliance. Set when the appliance is provisioned and operated by a
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EnyoAuthenticationStateEnum = void 0;
|
|
3
|
+
exports.EnyoAuthenticationStateEnum = exports.EnyoOauthRedirectUrlPatternEnum = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* How the host encodes the pending request into the enyo callback URL it hands
|
|
6
|
+
* out as {@link EnyoOauthAuthenticationStart.enyoRedirectUrl}.
|
|
7
|
+
*
|
|
8
|
+
* OAuth providers differ in what they accept as a registered `redirect_uri`:
|
|
9
|
+
* some match the URL exactly and reject anything carrying a query string,
|
|
10
|
+
* others match only the path prefix. Declaring the shape the provider's OAuth
|
|
11
|
+
* app was registered with avoids a generic "invalid redirect_uri" that only
|
|
12
|
+
* surfaces after the user has already typed a password.
|
|
13
|
+
*/
|
|
14
|
+
var EnyoOauthRedirectUrlPatternEnum;
|
|
15
|
+
(function (EnyoOauthRedirectUrlPatternEnum) {
|
|
16
|
+
/**
|
|
17
|
+
* The request id is carried as a query parameter, e.g.
|
|
18
|
+
* `https://api.enyo-energy.de/oauth-callback?from=<requestId>`. The host's
|
|
19
|
+
* default.
|
|
20
|
+
*/
|
|
21
|
+
EnyoOauthRedirectUrlPatternEnum["QueryParam"] = "query-param";
|
|
22
|
+
/**
|
|
23
|
+
* The request id is carried as a trailing path segment, e.g.
|
|
24
|
+
* `https://api.enyo-energy.de/oauth-callback/<requestId>`. Use this for
|
|
25
|
+
* providers that reject a registered `redirect_uri` containing a query
|
|
26
|
+
* string.
|
|
27
|
+
*/
|
|
28
|
+
EnyoOauthRedirectUrlPatternEnum["PathSegment"] = "path-segment";
|
|
29
|
+
})(EnyoOauthRedirectUrlPatternEnum || (exports.EnyoOauthRedirectUrlPatternEnum = EnyoOauthRedirectUrlPatternEnum = {}));
|
|
4
30
|
var EnyoAuthenticationStateEnum;
|
|
5
31
|
(function (EnyoAuthenticationStateEnum) {
|
|
6
32
|
EnyoAuthenticationStateEnum["Authenticated"] = "Authenticated";
|