@enyo-energy/energy-app-sdk 1.44.0 → 1.46.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 (44) hide show
  1. package/README.md +196 -1
  2. package/dist/cjs/energy-app-package-definition.d.cts +46 -1
  3. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.cjs +79 -1
  4. package/dist/cjs/implementations/heatpump/heatpump-metadata-validators.d.cts +4 -2
  5. package/dist/cjs/implementations/subscriptions/define-subscription-plan.cjs +48 -0
  6. package/dist/cjs/implementations/subscriptions/define-subscription-plan.d.cts +40 -0
  7. package/dist/cjs/implementations/subscriptions/subscription-offer-validators.cjs +301 -0
  8. package/dist/cjs/implementations/subscriptions/subscription-offer-validators.d.cts +54 -0
  9. package/dist/cjs/implementations/subscriptions/subscription-plan-validators.cjs +8 -0
  10. package/dist/cjs/implementations/subscriptions/subscription-plan-validators.d.cts +8 -3
  11. package/dist/cjs/index.cjs +2 -0
  12. package/dist/cjs/index.d.cts +2 -0
  13. package/dist/cjs/types/enyo-authentication.d.cts +11 -0
  14. package/dist/cjs/types/enyo-charger-appliance.cjs +13 -0
  15. package/dist/cjs/types/enyo-charger-appliance.d.cts +29 -2
  16. package/dist/cjs/types/enyo-heatpump-appliance.cjs +33 -1
  17. package/dist/cjs/types/enyo-heatpump-appliance.d.cts +86 -0
  18. package/dist/cjs/types/enyo-settings.d.cts +59 -1
  19. package/dist/cjs/types/enyo-subscription-offer.cjs +113 -0
  20. package/dist/cjs/types/enyo-subscription-offer.d.cts +358 -0
  21. package/dist/cjs/version.cjs +1 -1
  22. package/dist/cjs/version.d.cts +1 -1
  23. package/dist/energy-app-package-definition.d.ts +46 -1
  24. package/dist/implementations/heatpump/heatpump-metadata-validators.d.ts +4 -2
  25. package/dist/implementations/heatpump/heatpump-metadata-validators.js +80 -2
  26. package/dist/implementations/subscriptions/define-subscription-plan.d.ts +40 -0
  27. package/dist/implementations/subscriptions/define-subscription-plan.js +45 -0
  28. package/dist/implementations/subscriptions/subscription-offer-validators.d.ts +54 -0
  29. package/dist/implementations/subscriptions/subscription-offer-validators.js +298 -0
  30. package/dist/implementations/subscriptions/subscription-plan-validators.d.ts +8 -3
  31. package/dist/implementations/subscriptions/subscription-plan-validators.js +8 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +2 -0
  34. package/dist/types/enyo-authentication.d.ts +11 -0
  35. package/dist/types/enyo-charger-appliance.d.ts +29 -2
  36. package/dist/types/enyo-charger-appliance.js +13 -0
  37. package/dist/types/enyo-heatpump-appliance.d.ts +86 -0
  38. package/dist/types/enyo-heatpump-appliance.js +32 -0
  39. package/dist/types/enyo-settings.d.ts +59 -1
  40. package/dist/types/enyo-subscription-offer.d.ts +358 -0
  41. package/dist/types/enyo-subscription-offer.js +110 -0
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -1255,6 +1255,68 @@ settings.listenForSettingsChanges((settingName, newValue) => {
1255
1255
  const allSettings = await settings.getSettingsConfig();
1256
1256
  ```
1257
1257
 
1258
+ ##### Action settings (buttons)
1259
+
1260
+ A setting of type `'action'` renders as a button for a one-off command (restart a device, start a self-test, …). Every tap writes the current time as an ISO 8601 string to the setting, so each tap arrives as a change event — react to the event, not to the stored value.
1261
+
1262
+ Use an action only for something that *happens*. Anything that has a state the user turns on or off (e.g. "charging cable locked") is a `checkbox`, not an action.
1263
+
1264
+ ```typescript
1265
+ await settings.addSettingConfig({
1266
+ name: `restart-${applianceId}`,
1267
+ applianceId,
1268
+ type: 'action',
1269
+ required: false,
1270
+ fieldName: [{ language: 'en', value: 'Restart wallbox' }, { language: 'de', value: 'Wallbox neu starten' }],
1271
+ actionOptions: {
1272
+ buttonCaption: [{ language: 'en', value: 'Restart now' }, { language: 'de', value: 'Jetzt neu starten' }],
1273
+ // optional: ask before triggering
1274
+ confirmationText: [{ language: 'en', value: 'Restart the wallbox?' }, { language: 'de', value: 'Wallbox neu starten?' }],
1275
+ },
1276
+ });
1277
+
1278
+ settings.listenForSettingsChanges((event) => {
1279
+ if (event.settingName === `restart-${applianceId}`) {
1280
+ void restartWallbox();
1281
+ }
1282
+ });
1283
+ ```
1284
+
1285
+ ##### Expert settings and onboarding
1286
+
1287
+ These two optional flags work on a single setting and on a setting group:
1288
+
1289
+ - `expertSetting: true` hides the setting from the default view and shows it only in the expert / advanced section,
1290
+ e.g. for installers.
1291
+ - `showInOnboarding: true` shows the setting during onboarding, so the user can set it while setting up the app.
1292
+
1293
+ A setting without its own value inherits the flag from its group. A value set on the setting overrides the group's.
1294
+ Both default to `false`.
1295
+
1296
+ ```typescript
1297
+ await settings.addSettingGroup({
1298
+ groupName: 'modbus',
1299
+ caption: [{ language: 'en', value: 'Modbus' }, { language: 'de', value: 'Modbus' }],
1300
+ expertSetting: true, // all settings of the group are expert settings ...
1301
+ settings: [
1302
+ {
1303
+ name: 'modbus-host',
1304
+ type: 'text',
1305
+ required: true,
1306
+ fieldName: [{ language: 'en', value: 'IP address' }, { language: 'de', value: 'IP-Adresse' }],
1307
+ expertSetting: false, // ... except this one, which overrides the group
1308
+ showInOnboarding: true,
1309
+ },
1310
+ {
1311
+ name: 'modbus-unit-id',
1312
+ type: 'integer',
1313
+ required: false,
1314
+ fieldName: [{ language: 'en', value: 'Unit ID' }, { language: 'de', value: 'Unit-ID' }],
1315
+ },
1316
+ ],
1317
+ });
1318
+ ```
1319
+
1258
1320
  #### `useConfigurationManager(): EnergyAppConfigurationManager`
1259
1321
 
1260
1322
  Register **internal**, non user-facing configurations for your package and react to value changes. Unlike `useSettings()`, configurations registered here are NOT rendered in the Energy App UI — they are intended for values the package itself reads and writes at runtime (e.g. internal feature toggles, tuning parameters, calibration values) and need to be persisted across restarts.
@@ -3180,7 +3242,9 @@ const packageDef = defineEnergyAppPackage({
3180
3242
  | `pricingModel` | `per-package` (default): one price for the package on the device. `per-unit`: the price is charged per unit — see [Per-unit plans](#per-unit-plans). |
3181
3243
  | `prices` | One net price per currency. `amountWithoutTax` is always the **total for one interval** — per month, per year, or the single one-time amount; per unit for `per-unit` plans — in the currency (not cent), at most two decimals; enyo adds VAT. |
3182
3244
  | `trialDays` | Optional free trial in days. Not allowed for `one-time` plans. |
3183
- | `storeEntry` | Title, short description and optional benefits; `de` and `en` are required. |
3245
+ | `storeEntry` | Title, short description and optional benefits; `de` and `en` are required. Prefer `includedFeatures` over `benefits`. |
3246
+ | `includedFeatures` | Optional feature keys from `subscriptionFeatures`, or `{ featureKey, value }` with a translated value — see [Offer screen](#offer-screen). |
3247
+ | `highlight` | Optional badge: `recommended`, `best-value` or `most-popular`. At most one plan per package. |
3184
3248
  | `internalComment` | Optional note, never shown to users. |
3185
3249
 
3186
3250
  What a plan unlocks is decided by the app: check `isPlanActive(subscriptions, planKey)` at runtime (see
@@ -3209,6 +3273,97 @@ In tests, `InMemorySubscriptions` takes the mode as its third constructor argume
3209
3273
  throws when an `optional` package reports `SubscriptionRequired`. The same check is available as
3210
3274
  `assertEnergyAppStateMatchesSubscriptionMode(mode, state)`.
3211
3275
 
3276
+ ### Offer screen
3277
+
3278
+ Promote the plans on the screen where enyo sells them: declare a **feature catalog** once, say which features the
3279
+ **free tier** and each plan include, and build the screen from **blocks**. All three are optional. Without an offer
3280
+ screen, enyo shows only the plan cards.
3281
+
3282
+ ```typescript
3283
+ import {
3284
+ defineSubscriptionFeature,
3285
+ defineSubscriptionOfferScreen,
3286
+ EnyoSubscriptionOfferHintKindEnum,
3287
+ EnyoSubscriptionOfferIconEnum,
3288
+ EnyoSubscriptionPlanHighlightEnum
3289
+ } from '@enyo-energy/energy-app-sdk';
3290
+
3291
+ defineEnergyAppPackage({
3292
+ // ...
3293
+ subscriptionMode: EnyoSubscriptionModeEnum.Optional,
3294
+ subscriptionFeatures: [
3295
+ defineSubscriptionFeature({
3296
+ featureKey: 'pv-charging',
3297
+ icon: EnyoSubscriptionOfferIconEnum.Sun,
3298
+ storeEntry: [
3299
+ { language: 'en', title: 'PV surplus charging' },
3300
+ { language: 'de', title: 'PV-Überschussladen' }
3301
+ ]
3302
+ }),
3303
+ defineSubscriptionFeature({
3304
+ featureKey: 'price-optimisation',
3305
+ icon: EnyoSubscriptionOfferIconEnum.PiggyBank,
3306
+ storeEntry: [
3307
+ { language: 'en', title: 'Dynamic price optimisation', description: 'Charges when electricity is cheapest.' },
3308
+ { language: 'de', title: 'Optimierung nach dynamischen Preisen', description: 'Lädt, wenn Strom am günstigsten ist.' }
3309
+ ]
3310
+ }),
3311
+ defineSubscriptionFeature({
3312
+ featureKey: 'vehicles',
3313
+ icon: EnyoSubscriptionOfferIconEnum.Car,
3314
+ storeEntry: [{ language: 'en', title: 'Connected vehicles' }, { language: 'de', title: 'Verbundene Fahrzeuge' }]
3315
+ })
3316
+ ],
3317
+ subscriptionFreeTier: {
3318
+ storeEntry: [{ language: 'en', title: 'Basic' }, { language: 'de', title: 'Basis' }],
3319
+ includedFeatures: ['pv-charging']
3320
+ },
3321
+ subscriptionPlans: [
3322
+ defineSubscriptionPlan({
3323
+ planKey: 'premium',
3324
+ highlight: EnyoSubscriptionPlanHighlightEnum.Recommended,
3325
+ includedFeatures: [
3326
+ 'pv-charging',
3327
+ 'price-optimisation',
3328
+ { featureKey: 'vehicles', value: [{ language: 'en', value: 'up to 5' }, { language: 'de', value: 'bis zu 5' }] }
3329
+ ],
3330
+ // paymentInterval, prices, storeEntry ...
3331
+ })
3332
+ ],
3333
+ subscriptionOfferScreen: defineSubscriptionOfferScreen({
3334
+ blocks: [
3335
+ { type: 'hero', icon: 'piggy-bank', storeEntry: [
3336
+ { language: 'en', title: 'Charge for less', text: 'Smart charging with dynamic prices.' },
3337
+ { language: 'de', title: 'Günstiger laden', text: 'Intelligentes Laden mit dynamischen Preisen.' }
3338
+ ] },
3339
+ { type: 'comparison' }, // Basic vs. Premium, one row per feature
3340
+ { type: 'hint', kind: EnyoSubscriptionOfferHintKindEnum.PricingReason, icon: 'handshake', storeEntry: [
3341
+ { language: 'en', title: 'Why does this cost money?', text: 'The car vendors charge us for every connected vehicle.' },
3342
+ { language: 'de', title: 'Warum kostet das Geld?', text: 'Die Fahrzeughersteller berechnen uns jedes verbundene Fahrzeug.' }
3343
+ ] },
3344
+ { type: 'plans' }
3345
+ ]
3346
+ })
3347
+ });
3348
+ ```
3349
+
3350
+ | Block | Shows | Notes |
3351
+ |---|---|---|
3352
+ | `hero` | A large headline with optional text and icon | `title` is required. |
3353
+ | `features` | Features with icon and description | `featureKeys` picks and orders them; omit it to list all. |
3354
+ | `comparison` | A table: one row per feature, one column per tier | The free column is shown when a free tier is declared (`includeFreeTier` overrides this). `planKeys` and `featureKeys` pick the columns and rows. A cell shows a check mark, the feature's `value`, or nothing. Needs at least two columns. |
3355
+ | `hint` | A highlighted note | `kind`: `pricing-reason` (why it costs money), `requirement`, `info`, `legal`. `text` is required. |
3356
+ | `plans` | The plan cards and checkout | At most one. Without it, the plans follow the last block. |
3357
+
3358
+ - **Icons** come from a fixed set, `EnyoSubscriptionOfferIconEnum` (`piggy-bank`, `sun`, `car`, `wallbox`, `heat-pump`,
3359
+ `handshake`, …), so every app's offer screen looks the same.
3360
+ - **A plan's `includedFeatures` should list everything the customer gets with it**, including the free tier's
3361
+ features. The comparison table takes each column straight from the tier's list.
3362
+ - **`subscriptionFreeTier` is only allowed with `subscriptionMode: 'optional'`**.
3363
+ - Every text needs `de` and `en`. `validatePackageSubscriptionPlans()` checks all of this as well (it calls
3364
+ `validatePackageSubscriptionOffer()`). It also warns about features that no tier includes and comparison rows
3365
+ that stay empty.
3366
+
3212
3367
  ### Per-unit plans
3213
3368
 
3214
3369
  For a `per-unit` plan the price is charged once per unit — 3 units on a yearly plan for 10 € cost 30 € per year
@@ -3873,6 +4028,26 @@ await applianceManager.updateHeatpumpDomesticHotWater(applianceId, 0, { timeProg
3873
4028
 
3874
4029
  Validate before publishing with `validateHeatpumpHeatingCurve`, `validateHeatpumpTimeProgram` or `validateHeatpumpAdvancedMetadata` (or their `assertValid*` counterparts). Each returns `{ ok, errors, warnings }`.
3875
4030
 
4031
+ **SG Ready relays.** You can declare 0–n SG Ready inputs in `metadata.heatpump.sgReadyRelays`. Each one has a
4032
+ `function` that says what it acts on: `heating` (a heating circuit), `dhw-heatpump` (DHW heated by the compressor)
4033
+ or `dhw-heating-rod` (DHW heated by the heating rod). `zoneIndex` names the heating circuit or DHW zone; omit it for
4034
+ the first one. If only some of the four states (`blocked`, `normal`, `recommended`, `forced`) are wired, list them
4035
+ in `supportedStates`. `activeState` reports the current state. This works independently of `connectionType`.
4036
+
4037
+ ```typescript
4038
+ sgReadyRelays: [
4039
+ { index: 0, function: EnyoHeatpumpSgReadyFunctionEnum.Heating, zoneIndex: 0, name: 'K1/K2' },
4040
+ { index: 1, function: EnyoHeatpumpSgReadyFunctionEnum.DhwHeatpump, name: 'K3/K4' },
4041
+ { index: 2, function: EnyoHeatpumpSgReadyFunctionEnum.DhwHeatingRod,
4042
+ supportedStates: [EnyoHeatpumpSgReadyStateEnum.Normal, EnyoHeatpumpSgReadyStateEnum.Forced] },
4043
+ ]
4044
+ ```
4045
+
4046
+ `validateHeatpumpAdvancedMetadata` checks the relays too. It rejects duplicate indexes, unknown functions or
4047
+ states, and an `activeState` that is not in `supportedStates`. It warns about unknown zones, two relays with the
4048
+ same function on one zone, a relay without `normal`, and a `dhw-heating-rod` relay when `additionalDevices` has
4049
+ no `HeatingRod`.
4050
+
3876
4051
  ### WallboxIntegrationEnergyApp
3877
4052
 
3878
4053
  Drives an EV wallbox / charger. Has the richest command surface of all integrations.
@@ -4240,6 +4415,26 @@ Two charger facts belong on the appliance rather than on a forecast, and `EnyoCh
4240
4415
 
4241
4416
  Both are for marking on the same power scale a consumer already draws from `maxChargingPowerKw`, instead of hard-coding thresholds only the EMS knows.
4242
4417
 
4418
+ **DC chargers.** A DC charger (CCS, CHAdeMO) declares the `DcCharging` feature in `availableFeatures`. The EMS
4419
+ controls it by power in watts, not by current per phase. These fields apply:
4420
+ - `minChargingPowerKw`: the DC minimum, often several kW.
4421
+ - `maxChargingPowerKw`: the maximum power.
4422
+ - `powerStepW`: the granularity of power setpoints, e.g. `500` for 0.5 kW steps. Omit it when the charger accepts
4423
+ any watt value.
4424
+
4425
+ The current- and phase-based fields (`minChargingCurrentAmpere`, `ampereStep`, `availablePhases`, phase switching)
4426
+ do not apply to DC chargers.
4427
+
4428
+ ```typescript
4429
+ charger: {
4430
+ availableFeatures: [EnyoChargerApplianceAvailableFeaturesEnum.DcCharging, EnyoChargerApplianceAvailableFeaturesEnum.PowerLimitation],
4431
+ minChargingPowerKw: 5,
4432
+ maxChargingPowerKw: 22,
4433
+ powerStepW: 500,
4434
+ // status, authorizationMode, ...
4435
+ }
4436
+ ```
4437
+
4243
4438
  ### BatteryCommandForecast
4244
4439
 
4245
4440
  A discriminated union on `mode`:
@@ -4,6 +4,7 @@ import type { EnyoFileTranslation } from "./types/enyo-file.cjs";
4
4
  import type { EnyoOnboardingV2Guide } from "./types/enyo-onboarding-v2.cjs";
5
5
  import type { EnyoOnboardingTranslatedContent } from "./types/enyo-onboarding.cjs";
6
6
  import type { EnyoSubscriptionMode, EnyoSubscriptionPaymentInterval, EnyoSubscriptionPrice, EnyoSubscriptionPricingModel } from "./types/enyo-subscription.cjs";
7
+ import type { EnyoSubscriptionFeature, EnyoSubscriptionFreeTier, EnyoSubscriptionIncludedFeature, EnyoSubscriptionOfferScreen, EnyoSubscriptionPlanHighlight } from "./types/enyo-subscription-offer.cjs";
7
8
  export type EnergyAppPackageLanguage = 'de' | 'en';
8
9
  export declare enum EnergyAppPackageCategory {
9
10
  Inverter = "inverter",
@@ -294,7 +295,13 @@ export interface EnergyAppPackageSubscriptionPlanStoreEntry {
294
295
  title: string;
295
296
  /** One sentence describing what the plan unlocks */
296
297
  shortDescription: string;
297
- /** Optional list of benefits, shown as bullet points on the offer screen */
298
+ /**
299
+ * Optional list of benefits, shown as bullet points on the plan card.
300
+ *
301
+ * Prefer {@link EnergyAppPackageSubscriptionPlan.includedFeatures}: features
302
+ * carry icons and descriptions and feed the comparison table. The host
303
+ * shows `benefits` only for plans without `includedFeatures`.
304
+ */
298
305
  benefits?: string[];
299
306
  }
300
307
  /**
@@ -358,6 +365,21 @@ export interface EnergyAppPackageSubscriptionPlan {
358
365
  trialDays?: number;
359
366
  /** Localized store information. Entries for `de` and `en` are required. */
360
367
  storeEntry: EnergyAppPackageSubscriptionPlanStoreEntry[];
368
+ /**
369
+ * The features this plan includes, as keys of
370
+ * {@link EnergyAppPackageDefinition.subscriptionFeatures} — or with a
371
+ * translated value such as "up to 5 vehicles" — in display order. Shown on
372
+ * the plan card and in the comparison table.
373
+ *
374
+ * List every feature the customer gets with the plan, including those of
375
+ * the free tier, so the comparison table reads correctly.
376
+ */
377
+ includedFeatures?: EnyoSubscriptionIncludedFeature[];
378
+ /**
379
+ * Optional badge that makes this plan stand out on the offer screen, e.g.
380
+ * `recommended`. At most one plan of a package may be highlighted.
381
+ */
382
+ highlight?: EnyoSubscriptionPlanHighlight;
361
383
  /** Optional internal note explaining this plan; never shown to users. */
362
384
  internalComment?: string;
363
385
  }
@@ -1064,6 +1086,29 @@ export interface EnergyAppPackageDefinition {
1064
1086
  * Omit for packages that are free.
1065
1087
  */
1066
1088
  subscriptionPlans?: EnergyAppPackageSubscriptionPlan[];
1089
+ /**
1090
+ * The package's feature catalog: every feature that the plans and the free
1091
+ * tier refer to, declared once with its icon and translations. enyo builds
1092
+ * the feature lists and the comparison table of the offer screen from it.
1093
+ *
1094
+ * Only used together with {@link subscriptionPlans}. Validate it with
1095
+ * `validatePackageSubscriptionPlans()`.
1096
+ */
1097
+ subscriptionFeatures?: EnyoSubscriptionFeature[];
1098
+ /**
1099
+ * What the app offers without a plan, shown as the free column of the
1100
+ * comparison table. Only allowed with `subscriptionMode: 'optional'`.
1101
+ */
1102
+ subscriptionFreeTier?: EnyoSubscriptionFreeTier;
1103
+ /**
1104
+ * The blocks of the screen on which enyo offers the plans — a headline,
1105
+ * feature lists, a comparison table, hints such as why the app costs
1106
+ * money, and the plan cards — in display order.
1107
+ *
1108
+ * Omit to show only the plan cards. Validate it with
1109
+ * `validatePackageSubscriptionPlans()`.
1110
+ */
1111
+ subscriptionOfferScreen?: EnyoSubscriptionOfferScreen;
1067
1112
  /**
1068
1113
  * Whether the app needs one of its plans to work at all.
1069
1114
  *
@@ -3,7 +3,8 @@
3
3
  * Client-side validation for the advanced heatpump metadata an integration
4
4
  * publishes: heating curves ({@link EnyoHeatpumpApplianceHeatingCurve}), time
5
5
  * programs ({@link EnyoHeatpumpTimeProgram}), the room-temperature wiring of
6
- * heating circuits and the program-switch modes of circuits and DHW zones.
6
+ * heating circuits, the program-switch modes of circuits and DHW zones, and
7
+ * the SG Ready relays ({@link EnyoHeatpumpApplianceSgReadyRelay}).
7
8
  *
8
9
  * None of these shapes can be checked at the type level — a curve with one
9
10
  * point, an unsorted curve, `'25:00'` as a switching time or a period on day
@@ -47,6 +48,8 @@ const TIME_OF_DAY = /^([01]\d|2[0-3]):([0-5]\d)$/;
47
48
  const LEVELS = new Set(Object.values(enyo_heatpump_appliance_js_1.EnyoHeatpumpTimeProgramLevelEnum));
48
49
  const PROGRAM_MODES = new Set(Object.values(enyo_heatpump_appliance_js_1.EnyoHeatpumpProgramModeEnum));
49
50
  const OVERRIDE_PROGRAM_MODES = new Set([enyo_heatpump_appliance_js_1.EnyoHeatpumpProgramModeEnum.Comfort, enyo_heatpump_appliance_js_1.EnyoHeatpumpProgramModeEnum.Eco]);
51
+ const SG_READY_FUNCTIONS = new Set(Object.values(enyo_heatpump_appliance_js_1.EnyoHeatpumpSgReadyFunctionEnum));
52
+ const SG_READY_STATES = new Set(Object.values(enyo_heatpump_appliance_js_1.EnyoHeatpumpSgReadyStateEnum));
50
53
  /**
51
54
  * Validates the program-switch fields of one heating circuit or DHW zone
52
55
  * (`supportedProgramModes`, `programModeTemperaturesC`, `activeProgramMode`)
@@ -121,6 +124,79 @@ function checkHeatingCurveOffset(circuit, prefix, errors) {
121
124
  }
122
125
  });
123
126
  }
127
+ /**
128
+ * Validates the SG Ready relays of a heatpump and appends the problems to
129
+ * `errors` / `warnings`.
130
+ *
131
+ * Errors: duplicate relay `index`, an unknown `function`, an unknown or empty
132
+ * `supportedStates`, an unknown `activeState` or one outside
133
+ * `supportedStates`.
134
+ *
135
+ * Warnings: a `zoneIndex` naming no declared heating circuit / DHW zone, two
136
+ * relays with the same function on the same zone, `supportedStates` without
137
+ * `Normal` or with duplicates, a `dhw-heating-rod` relay while the
138
+ * installation does not list a heating rod in `additionalDevices`.
139
+ *
140
+ * @param relays - The declared SG Ready relays.
141
+ * @param metadata - The heatpump metadata, for zones and additional devices.
142
+ * @param errors - Receives blocking problems.
143
+ * @param warnings - Receives advisory problems.
144
+ */
145
+ function checkSgReadyRelays(relays, metadata, errors, warnings) {
146
+ const indexes = relays.map(relay => relay.index);
147
+ const duplicates = indexes.filter((index, i) => indexes.indexOf(index) !== i);
148
+ if (duplicates.length > 0) {
149
+ errors.push(`sgReadyRelays contains duplicate index ${[...new Set(duplicates)].join(', ')}`);
150
+ }
151
+ const targets = new Set();
152
+ for (const relay of relays) {
153
+ const prefix = `sgReadyRelays[index=${relay.index}]`;
154
+ if (!SG_READY_FUNCTIONS.has(relay.function)) {
155
+ errors.push(`${prefix}: function "${relay.function}" is not a valid value`);
156
+ continue;
157
+ }
158
+ const isHeating = relay.function === enyo_heatpump_appliance_js_1.EnyoHeatpumpSgReadyFunctionEnum.Heating;
159
+ const zones = isHeating ? metadata.heatingCircuits : metadata.domesticHotWater;
160
+ const zoneField = isHeating ? 'heatingCircuits' : 'domesticHotWater';
161
+ const zoneIndex = relay.zoneIndex ?? zones?.[0]?.index ?? 0;
162
+ if (relay.zoneIndex !== undefined && zones?.length && !zones.some(zone => zone.index === relay.zoneIndex)) {
163
+ warnings.push(`${prefix}: zoneIndex ${relay.zoneIndex} is not an index of ${zoneField}`);
164
+ }
165
+ const target = `${relay.function}@${zoneIndex}`;
166
+ if (targets.has(target)) {
167
+ warnings.push(`${prefix}: another relay already has function ${relay.function} on zone ${zoneIndex}`);
168
+ }
169
+ targets.add(target);
170
+ if (relay.function === enyo_heatpump_appliance_js_1.EnyoHeatpumpSgReadyFunctionEnum.DhwHeatingRod
171
+ && !(metadata.additionalDevices ?? []).includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpApplianceAdditionalDeviceEnum.HeatingRod)) {
172
+ warnings.push(`${prefix}: function dhw-heating-rod is declared but additionalDevices does not list a HeatingRod`);
173
+ }
174
+ const supported = relay.supportedStates;
175
+ if (supported !== undefined) {
176
+ if (supported.length === 0) {
177
+ errors.push(`${prefix}: supportedStates must not be empty — omit it when all four states are wired`);
178
+ }
179
+ for (const state of supported) {
180
+ if (!SG_READY_STATES.has(state))
181
+ errors.push(`${prefix}: supportedStates contains unknown state "${state}"`);
182
+ }
183
+ if (new Set(supported).size !== supported.length) {
184
+ warnings.push(`${prefix}: supportedStates contains duplicate states`);
185
+ }
186
+ if (supported.length > 0 && !supported.includes(enyo_heatpump_appliance_js_1.EnyoHeatpumpSgReadyStateEnum.Normal)) {
187
+ warnings.push(`${prefix}: supportedStates does not include normal — the relay could never release the heatpump`);
188
+ }
189
+ }
190
+ if (relay.activeState !== undefined) {
191
+ if (!SG_READY_STATES.has(relay.activeState)) {
192
+ errors.push(`${prefix}: activeState "${relay.activeState}" is not a valid value`);
193
+ }
194
+ else if (supported !== undefined && !supported.includes(relay.activeState)) {
195
+ errors.push(`${prefix}: activeState ${relay.activeState} is not one of supportedStates`);
196
+ }
197
+ }
198
+ }
199
+ }
124
200
  /**
125
201
  * Parses an `HH:mm` time of day into minutes since midnight.
126
202
  *
@@ -374,6 +450,7 @@ function assertValidHeatpumpTimeProgram(program) {
374
450
  * - Heating curve offset: errors from {@link checkHeatingCurveOffset}.
375
451
  * Warning: `HeatingCurveOffset` is declared but no circuit has a
376
452
  * `heatingCurveOffsetRangeK` — no offset other than `0` could be accepted.
453
+ * - SG Ready relays: errors and warnings from {@link checkSgReadyRelays}.
377
454
  *
378
455
  * @param metadata - The heatpump metadata to validate.
379
456
  * @returns The validation result.
@@ -476,5 +553,6 @@ function validateHeatpumpAdvancedMetadata(metadata) {
476
553
  merge(`domesticHotWater[index=${zone.index}].timeProgram`, validateHeatpumpTimeProgram(zone.timeProgram));
477
554
  }
478
555
  }
556
+ checkSgReadyRelays(metadata.sgReadyRelays ?? [], metadata, errors, warnings);
479
557
  return { ok: errors.length === 0, errors, warnings };
480
558
  }
@@ -2,7 +2,8 @@
2
2
  * Client-side validation for the advanced heatpump metadata an integration
3
3
  * publishes: heating curves ({@link EnyoHeatpumpApplianceHeatingCurve}), time
4
4
  * programs ({@link EnyoHeatpumpTimeProgram}), the room-temperature wiring of
5
- * heating circuits and the program-switch modes of circuits and DHW zones.
5
+ * heating circuits, the program-switch modes of circuits and DHW zones, and
6
+ * the SG Ready relays ({@link EnyoHeatpumpApplianceSgReadyRelay}).
6
7
  *
7
8
  * None of these shapes can be checked at the type level — a curve with one
8
9
  * point, an unsorted curve, `'25:00'` as a switching time or a period on day
@@ -116,8 +117,9 @@ export declare function assertValidHeatpumpTimeProgram(program: EnyoHeatpumpTime
116
117
  * - Heating curve offset: errors from {@link checkHeatingCurveOffset}.
117
118
  * Warning: `HeatingCurveOffset` is declared but no circuit has a
118
119
  * `heatingCurveOffsetRangeK` — no offset other than `0` could be accepted.
120
+ * - SG Ready relays: errors and warnings from {@link checkSgReadyRelays}.
119
121
  *
120
122
  * @param metadata - The heatpump metadata to validate.
121
123
  * @returns The validation result.
122
124
  */
123
- export declare function validateHeatpumpAdvancedMetadata(metadata: Pick<EnyoHeatpumpApplianceMetadata, 'availableFeatures' | 'heatingCircuits' | 'domesticHotWater' | 'building'>): HeatpumpMetadataValidationResult;
125
+ export declare function validateHeatpumpAdvancedMetadata(metadata: Pick<EnyoHeatpumpApplianceMetadata, 'availableFeatures' | 'heatingCircuits' | 'domesticHotWater' | 'building'> & Partial<Pick<EnyoHeatpumpApplianceMetadata, 'sgReadyRelays' | 'additionalDevices'>>): HeatpumpMetadataValidationResult;
@@ -20,6 +20,9 @@ exports.isSubscriptionActive = isSubscriptionActive;
20
20
  exports.isPlanActive = isPlanActive;
21
21
  exports.isSameSubscriptionUnit = isSameSubscriptionUnit;
22
22
  exports.isUnitActive = isUnitActive;
23
+ exports.defineSubscriptionFeature = defineSubscriptionFeature;
24
+ exports.defineSubscriptionOfferScreen = defineSubscriptionOfferScreen;
25
+ exports.getIncludedFeatureKey = getIncludedFeatureKey;
23
26
  const enyo_subscription_js_1 = require("../../types/enyo-subscription.cjs");
24
27
  /**
25
28
  * Subscription statuses in which the customer may use what a plan unlocks.
@@ -122,3 +125,48 @@ function isUnitActive(subscriptions, planKey, unit) {
122
125
  && isSubscriptionActive(subscription)
123
126
  && (subscription.units ?? []).some(paid => isSameSubscriptionUnit(paid, unit)));
124
127
  }
128
+ /**
129
+ * Identity helper that type-checks a feature of the package's feature catalog
130
+ * ({@link EnergyAppPackageDefinition.subscriptionFeatures}) where it is
131
+ * written.
132
+ *
133
+ * @param feature - The feature declaration.
134
+ * @returns The same feature, unchanged.
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * defineSubscriptionFeature({
139
+ * featureKey: 'price-optimisation',
140
+ * icon: EnyoSubscriptionOfferIconEnum.PiggyBank,
141
+ * storeEntry: [
142
+ * {language: 'en', title: 'Dynamic price optimisation'},
143
+ * {language: 'de', title: 'Optimierung nach dynamischen Preisen'}
144
+ * ]
145
+ * })
146
+ * ```
147
+ */
148
+ function defineSubscriptionFeature(feature) {
149
+ return feature;
150
+ }
151
+ /**
152
+ * Identity helper that type-checks the package's offer screen
153
+ * ({@link EnergyAppPackageDefinition.subscriptionOfferScreen}) where it is
154
+ * written.
155
+ *
156
+ * @param screen - The offer screen declaration.
157
+ * @returns The same offer screen, unchanged.
158
+ */
159
+ function defineSubscriptionOfferScreen(screen) {
160
+ return screen;
161
+ }
162
+ /**
163
+ * Returns the feature key of an included feature, whether it was declared as
164
+ * a plain key or with a value.
165
+ *
166
+ * @param feature - An entry of {@link EnergyAppPackageSubscriptionPlan.includedFeatures}
167
+ * or {@link EnyoSubscriptionFreeTier.includedFeatures}.
168
+ * @returns Its {@link EnyoSubscriptionFeature.featureKey}.
169
+ */
170
+ function getIncludedFeatureKey(feature) {
171
+ return typeof feature === 'string' ? feature : feature.featureKey;
172
+ }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import type { EnergyAppPackageSubscriptionPlan } from '../../energy-app-package-definition.cjs';
16
16
  import { EnyoSubscription, EnyoSubscriptionStatus, EnyoSubscriptionUnit } from '../../types/enyo-subscription.cjs';
17
+ import type { EnyoSubscriptionFeature, EnyoSubscriptionIncludedFeature, EnyoSubscriptionOfferScreen } from '../../types/enyo-subscription-offer.cjs';
17
18
  /**
18
19
  * Subscription statuses in which the customer may use what a plan unlocks.
19
20
  *
@@ -98,3 +99,42 @@ export declare function isSameSubscriptionUnit(a: Pick<EnyoSubscriptionUnit, 'ty
98
99
  * ```
99
100
  */
100
101
  export declare function isUnitActive(subscriptions: EnyoSubscription[], planKey: string, unit: Pick<EnyoSubscriptionUnit, 'type' | 'referenceId'>): boolean;
102
+ /**
103
+ * Identity helper that type-checks a feature of the package's feature catalog
104
+ * ({@link EnergyAppPackageDefinition.subscriptionFeatures}) where it is
105
+ * written.
106
+ *
107
+ * @param feature - The feature declaration.
108
+ * @returns The same feature, unchanged.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * defineSubscriptionFeature({
113
+ * featureKey: 'price-optimisation',
114
+ * icon: EnyoSubscriptionOfferIconEnum.PiggyBank,
115
+ * storeEntry: [
116
+ * {language: 'en', title: 'Dynamic price optimisation'},
117
+ * {language: 'de', title: 'Optimierung nach dynamischen Preisen'}
118
+ * ]
119
+ * })
120
+ * ```
121
+ */
122
+ export declare function defineSubscriptionFeature(feature: EnyoSubscriptionFeature): EnyoSubscriptionFeature;
123
+ /**
124
+ * Identity helper that type-checks the package's offer screen
125
+ * ({@link EnergyAppPackageDefinition.subscriptionOfferScreen}) where it is
126
+ * written.
127
+ *
128
+ * @param screen - The offer screen declaration.
129
+ * @returns The same offer screen, unchanged.
130
+ */
131
+ export declare function defineSubscriptionOfferScreen(screen: EnyoSubscriptionOfferScreen): EnyoSubscriptionOfferScreen;
132
+ /**
133
+ * Returns the feature key of an included feature, whether it was declared as
134
+ * a plain key or with a value.
135
+ *
136
+ * @param feature - An entry of {@link EnergyAppPackageSubscriptionPlan.includedFeatures}
137
+ * or {@link EnyoSubscriptionFreeTier.includedFeatures}.
138
+ * @returns Its {@link EnyoSubscriptionFeature.featureKey}.
139
+ */
140
+ export declare function getIncludedFeatureKey(feature: EnyoSubscriptionIncludedFeature): string;