@enyo-energy/energy-app-sdk 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +8 -4
  2. package/dist/cjs/implementations/automation/automation-validators.cjs +143 -10
  3. package/dist/cjs/implementations/automation/automation-validators.d.cts +15 -3
  4. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +7 -2
  5. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +9 -2
  6. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +24 -0
  7. package/dist/cjs/packages/energy-app-automation.d.cts +44 -0
  8. package/dist/cjs/types/enyo-authentication.cjs +27 -1
  9. package/dist/cjs/types/enyo-authentication.d.cts +86 -7
  10. package/dist/cjs/types/enyo-automation.cjs +40 -0
  11. package/dist/cjs/types/enyo-automation.d.cts +252 -7
  12. package/dist/cjs/types/enyo-energy-manager.cjs +73 -0
  13. package/dist/cjs/types/enyo-energy-manager.d.cts +70 -1
  14. package/dist/cjs/types/enyo-heatpump-appliance.cjs +35 -1
  15. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +147 -0
  16. package/dist/cjs/types/enyo-onboarding-v2.d.cts +19 -0
  17. package/dist/cjs/version.cjs +1 -1
  18. package/dist/cjs/version.d.cts +1 -1
  19. package/dist/implementations/automation/automation-validators.d.ts +15 -3
  20. package/dist/implementations/automation/automation-validators.js +142 -9
  21. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +9 -2
  22. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +7 -2
  23. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +24 -0
  24. package/dist/packages/energy-app-automation.d.ts +44 -0
  25. package/dist/types/enyo-authentication.d.ts +86 -7
  26. package/dist/types/enyo-authentication.js +26 -0
  27. package/dist/types/enyo-automation.d.ts +252 -7
  28. package/dist/types/enyo-automation.js +40 -0
  29. package/dist/types/enyo-energy-manager.d.ts +70 -1
  30. package/dist/types/enyo-energy-manager.js +73 -0
  31. package/dist/types/enyo-heatpump-appliance.d.ts +147 -0
  32. package/dist/types/enyo-heatpump-appliance.js +34 -0
  33. package/dist/types/enyo-onboarding-v2.d.ts +19 -0
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. 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` | `PvSurplusThreshold` → `{ thresholdW }` (activate above, deactivate below) |
2966
- | **Actionsmart plug** | `EnyoAutomationActionTypeEnum.SmartPlugSwitch` | `{ applianceId, minDurationMinutes }` — `minDurationMinutes` is `5…360` in steps of `5` |
2965
+ | **Trigger — PV surplus above** | `EnyoAutomationTriggerTypeEnum.PvSurplusThreshold` | `{ thresholdW }` (activate above, deactivate below) |
2966
+ | **TriggerPV 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` (for PV surplus: `{ triggerType, surplusW, thresholdW }`).
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
@@ -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
- /** Minimum smart-plug on-duration, in minutes. */
24
- exports.AUTOMATION_MIN_DURATION_MINUTES = 5;
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
- /** Step size the smart-plug on-duration must be a multiple of, in minutes. */
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
- if (typeof minDurationMinutes !== 'number' ||
207
- !Number.isInteger(minDurationMinutes) ||
208
- minDurationMinutes < exports.AUTOMATION_MIN_DURATION_MINUTES ||
209
- minDurationMinutes > exports.AUTOMATION_MAX_DURATION_MINUTES ||
210
- minDurationMinutes % exports.AUTOMATION_DURATION_STEP_MINUTES !== 0) {
211
- throw new AutomationValidationError(`${label}.minDurationMinutes must be an integer between ${exports.AUTOMATION_MIN_DURATION_MINUTES} and ${exports.AUTOMATION_MAX_DURATION_MINUTES} in steps of ${exports.AUTOMATION_DURATION_STEP_MINUTES}.`);
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
- /** Minimum smart-plug on-duration, in minutes. */
11
- export declare const AUTOMATION_MIN_DURATION_MINUTES = 5;
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
- /** Step size the smart-plug on-duration must be a multiple of, in minutes. */
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 `requiresWebAuthentication` to force the login into a web browser
366
- * when the provider rejects a custom-scheme redirect such as `enyoapp://`.
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 `requiresWebAuthentication` to force the login into a web browser
309
- * when the provider rejects a custom-scheme redirect such as `enyoapp://`.
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 {
@@ -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";
@@ -34,11 +34,78 @@ export interface EnyoUsernamePasswordAuthentication {
34
34
  /** Optional additional fields to display in the authentication form */
35
35
  additionalFields?: EnyoAuthenticationAdditionalField[];
36
36
  }
37
+ /**
38
+ * How the host encodes the pending request into the enyo callback URL it hands
39
+ * out as {@link EnyoOauthAuthenticationStart.enyoRedirectUrl}.
40
+ *
41
+ * OAuth providers differ in what they accept as a registered `redirect_uri`:
42
+ * some match the URL exactly and reject anything carrying a query string,
43
+ * others match only the path prefix. Declaring the shape the provider's OAuth
44
+ * app was registered with avoids a generic "invalid redirect_uri" that only
45
+ * surfaces after the user has already typed a password.
46
+ */
47
+ export declare enum EnyoOauthRedirectUrlPatternEnum {
48
+ /**
49
+ * The request id is carried as a query parameter, e.g.
50
+ * `https://api.enyo-energy.de/oauth-callback?from=<requestId>`. The host's
51
+ * default.
52
+ */
53
+ QueryParam = "query-param",
54
+ /**
55
+ * The request id is carried as a trailing path segment, e.g.
56
+ * `https://api.enyo-energy.de/oauth-callback/<requestId>`. Use this for
57
+ * providers that reject a registered `redirect_uri` containing a query
58
+ * string.
59
+ */
60
+ PathSegment = "path-segment"
61
+ }
62
+ /**
63
+ * Constraints a package puts on the enyo callback URL the host generates for an
64
+ * OAuth flow. Every field is optional — an omitted field leaves the choice to
65
+ * the host.
66
+ *
67
+ * These are properties of the *provider*, not preferences: set them because the
68
+ * vendor's OAuth app rejects the alternative, not because one shape looks
69
+ * tidier. The filter the host applied travels back with the request as
70
+ * {@link EnyoOauthAuthenticationStart.redirectUrlFilter}, so an app can confirm
71
+ * what it got instead of parsing the URL.
72
+ */
73
+ export interface EnyoOauthRedirectUrlFilter {
74
+ /**
75
+ * Require an `https` callback URL rather than a custom app scheme such as
76
+ * `enyoapp://`, which forces the login to run in a web browser instead of an
77
+ * in-app / native flow.
78
+ *
79
+ * Set it when the provider will not accept a custom-scheme redirect. Many
80
+ * OAuth providers reject anything that is not `https`, so a redirect of
81
+ * `enyoapp://…` fails at the authorization server before the installer has
82
+ * typed a password, with nothing on screen that points at the cause. The
83
+ * native flow is the better experience where it works — it keeps the
84
+ * installer inside the app — so only turn this on when the provider forces
85
+ * it.
86
+ */
87
+ webOnly?: boolean;
88
+ /**
89
+ * Which {@link EnyoOauthRedirectUrlPatternEnum} the callback URL must
90
+ * follow. Omit to let the host choose (today: `QueryParam`).
91
+ */
92
+ pattern?: EnyoOauthRedirectUrlPatternEnum;
93
+ }
37
94
  export interface EnyoOauthAuthentication {
38
95
  description?: EnyoPackageConfigurationTranslatedValue[];
39
96
  /** If the client id and client secret need to be provided by the user*/
40
97
  clientIdName?: EnyoPackageConfigurationTranslatedValue[];
41
98
  clientSecretName?: EnyoPackageConfigurationTranslatedValue[];
99
+ /**
100
+ * Constraints on the enyo callback URL the host generates for this flow —
101
+ * `https`-only and/or a specific URL pattern. The host applies them when
102
+ * building {@link EnyoOauthAuthenticationStart.enyoRedirectUrl}, before the
103
+ * request ever reaches the package's `listenForOauthStart` listener.
104
+ *
105
+ * Omit it to let the host pick, which is right unless the provider's OAuth
106
+ * app was registered with a redirect URI the default does not match.
107
+ */
108
+ redirectUrlFilter?: EnyoOauthRedirectUrlFilter;
42
109
  }
43
110
  /**
44
111
  * OAuth authentication start configuration
@@ -60,19 +127,31 @@ export interface EnyoOauthAuthenticationStart {
60
127
  * such as `enyoapp://`.
61
128
  *
62
129
  * Set by the host when the flow that started the login declared the
63
- * constraint today
64
- * {@link EnyoOnboardingV2AuthBlock.requiresWebAuthentication} on an
65
- * onboarding v2 auth block. It is reported here so a package building the
66
- * provider's authorize URL can act on it explicitly: pick the matching
67
- * registered OAuth client, or fail fast with a clear message instead of
68
- * letting the authorization server answer "invalid redirect_uri" after the
69
- * user has already typed a password.
130
+ * constraint. It is reported here so a package building the provider's
131
+ * authorize URL can act on it explicitly: pick the matching registered
132
+ * OAuth client, or fail fast with a clear message instead of letting the
133
+ * authorization server answer "invalid redirect_uri" after the user has
134
+ * already typed a password.
70
135
  *
71
136
  * Absent or `false` means the host chose the redirect itself and it may
72
137
  * carry a custom scheme. Prefer reading this flag over sniffing
73
138
  * {@link enyoRedirectUrl}'s scheme.
139
+ *
140
+ * @deprecated Read {@link redirectUrlFilter}`.webOnly` instead, which
141
+ * carries the same information alongside the URL-pattern constraint. The
142
+ * host keeps both in sync: whenever this flag is `true`,
143
+ * `redirectUrlFilter.webOnly` is `true` as well.
74
144
  */
75
145
  requiresWebAuthentication?: boolean;
146
+ /**
147
+ * The {@link EnyoOauthRedirectUrlFilter} the host applied when building
148
+ * {@link enyoRedirectUrl} — the resolved constraints, not the requested
149
+ * ones, so a package can confirm which callback shape it got rather than
150
+ * parsing the URL.
151
+ *
152
+ * Absent when the flow declared no constraints and the host chose freely.
153
+ */
154
+ redirectUrlFilter?: EnyoOauthRedirectUrlFilter;
76
155
  }
77
156
  export interface EnyoOauthAuthenticationRedirectUrlResponse {
78
157
  redirectUrl: string;
@@ -19,6 +19,38 @@ var EnyoAutomationTriggerTypeEnum;
19
19
  * falls below.
20
20
  */
21
21
  EnyoAutomationTriggerTypeEnum["PvSurplusThreshold"] = "pv-surplus-threshold";
22
+ /**
23
+ * Fires while the current PV surplus is *below* a user-configured threshold
24
+ * (see {@link EnyoAutomationPvSurplusBelowThresholdTrigger}). This is the
25
+ * inverse of {@link PvSurplusThreshold} and expresses the "turn the target
26
+ * off once the PV surplus reaches X Watt" use case: the automation is
27
+ * active (target on) while the surplus stays below the threshold and is
28
+ * deactivated (target off) as soon as the surplus reaches or exceeds it.
29
+ */
30
+ EnyoAutomationTriggerTypeEnum["PvSurplusBelowThreshold"] = "pv-surplus-below-threshold";
31
+ /**
32
+ * Fires while the current electricity price per kWh is below a
33
+ * user-configured limit (see {@link EnyoAutomationBelowPriceLimitTrigger}).
34
+ * The automation is activated when the price drops below the limit and
35
+ * deactivated when it reaches or exceeds it again.
36
+ */
37
+ EnyoAutomationTriggerTypeEnum["BelowPriceLimit"] = "below-price-limit";
38
+ /**
39
+ * Fires during the cheapest share of the day — e.g. the cheapest 25 % of
40
+ * the day's price intervals (see
41
+ * {@link EnyoAutomationCheapestShareOfDayTrigger}). The provider ranks the
42
+ * day's known prices and marks the cheapest intervals as active, so the
43
+ * automation follows the price ranking rather than an absolute price.
44
+ */
45
+ EnyoAutomationTriggerTypeEnum["CheapestShareOfDay"] = "cheapest-share-of-day";
46
+ /**
47
+ * Fires during user-defined time windows — the "Zeitplan" trigger (see
48
+ * {@link EnyoAutomationScheduleTrigger}). A schedule holds one or more
49
+ * windows, each with a start and end time of day and an optional set of
50
+ * weekdays it applies to, so "weekdays 06:00-08:00 and 18:00-22:00, and all
51
+ * day on Sunday" is a single trigger.
52
+ */
53
+ EnyoAutomationTriggerTypeEnum["Schedule"] = "schedule";
22
54
  })(EnyoAutomationTriggerTypeEnum || (exports.EnyoAutomationTriggerTypeEnum = EnyoAutomationTriggerTypeEnum = {}));
23
55
  /**
24
56
  * The kinds of action an automation can perform when its trigger is active.
@@ -70,6 +102,14 @@ var EnyoAutomationMqttPlaceholderEnum;
70
102
  EnyoAutomationMqttPlaceholderEnum["State"] = "{{state}}";
71
103
  /** Replaced with the current PV surplus in Watts at the moment of publishing. */
72
104
  EnyoAutomationMqttPlaceholderEnum["SurplusW"] = "{{surplusW}}";
105
+ /**
106
+ * Replaced with the current electricity price per kWh at the moment of
107
+ * publishing. Only meaningful for price-based triggers
108
+ * ({@link EnyoAutomationTriggerTypeEnum.BelowPriceLimit} and
109
+ * {@link EnyoAutomationTriggerTypeEnum.CheapestShareOfDay}); resolves to
110
+ * `null` when no price is known.
111
+ */
112
+ EnyoAutomationMqttPlaceholderEnum["PricePerKwh"] = "{{pricePerKwh}}";
73
113
  /** Replaced with the ISO 8601 timestamp of the event. */
74
114
  EnyoAutomationMqttPlaceholderEnum["TimestampIso"] = "{{timestampIso}}";
75
115
  /** Replaced with the id of the automation that fired. */