@enyo-energy/energy-app-sdk 0.0.183 → 0.0.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -346,6 +346,7 @@ Energy Apps use a granular permissions system to control access to system resour
346
346
  - **`PvForecastRegister`** / **`PvForecastUse`**: Publish / consume PV forecasts
347
347
  - **`DynamicPriceForecastRegister`** / **`DynamicPriceForecastUse`**: Publish / consume dynamic-price forecasts
348
348
  - **`PvSystemRegister`** / **`PvSystemUse`**: Register / read PV system configuration
349
+ - **`Savings`**: Publish and read back day-scoped savings reports
349
350
 
350
351
  #### Site & Identity Permissions
351
352
 
@@ -1120,6 +1121,66 @@ diag.energyManagerDiagnostics(
1120
1121
  );
1121
1122
  ```
1122
1123
 
1124
+ #### `useSavings(): EnergyAppSavings`
1125
+
1126
+ Publish what the energy management saved the customer on a finished day, and read back which days were already reported.
1127
+
1128
+ The app settles a day by replaying its **measured** environment through a simulation of the same house running **uncontrolled**, and pricing both worlds against the tariff that actually applied. Publishing is an upsert keyed by `dayIso` + `method`, so a day may be recomputed after a backfill or a bugfix. The platform stores days and owns every aggregation above them (month, year, lifetime) — and excludes `Low` confidence days from those rollups.
1129
+
1130
+ ```typescript
1131
+ const savings = energyApp.useSavings();
1132
+
1133
+ // On boot: which days still need settling?
1134
+ const { missingDayIsos } = await savings.getDailySavings({
1135
+ startDayIso: '2026-07-01',
1136
+ endDayIso: '2026-07-31'
1137
+ });
1138
+
1139
+ for (const dayIso of missingDayIsos) {
1140
+ await savings.publishDailySavings({
1141
+ schemaVersion: 1,
1142
+ dayIso,
1143
+ timeZone: 'Europe/Berlin',
1144
+ dayStartUtcMs: startOfLocalDayUtcMs(dayIso),
1145
+ dayEndUtcMs: endOfLocalDayUtcMs(dayIso), // 23 or 25 h on DST days
1146
+ method: EnyoSavingsMethodEnum.Settled,
1147
+ computedAtIso: new Date().toISOString(),
1148
+ calculatorVersion: '3.2.0',
1149
+ confidence: EnyoSavingsConfidenceEnum.High,
1150
+ confidenceIssues: [],
1151
+ costs: {
1152
+ currency: EnyoCurrencyEnum.EUR,
1153
+ optimizedCost: 1.42,
1154
+ baselineCost: 3.07,
1155
+ savings: 1.65,
1156
+ savingsFromSelfConsumption: 1.12,
1157
+ savingsFromArbitrage: 0.53
1158
+ },
1159
+ energy: { /* both worlds, Wh */ },
1160
+ metrics: { /* both worlds */ },
1161
+ attribution: [
1162
+ { applianceType: EnergyAppApplianceTypeEnum.Charger, savings: 0.91, shiftedEnergyWh: 12400 }
1163
+ ],
1164
+ coverage: [
1165
+ { series: 'pv', source: EnyoSavingsDataSourceEnum.Measured, expectedBuckets: 96, presentBuckets: 96 }
1166
+ ],
1167
+ assumptions: [
1168
+ { key: 'battery.dischargeEfficiency', value: 0.95 }
1169
+ ]
1170
+ });
1171
+ }
1172
+ ```
1173
+
1174
+ Notes worth respecting when producing a report:
1175
+
1176
+ - **Publish both worlds, never only the delta.** A lone savings number is unauditable and cannot be re-aggregated.
1177
+ - **Assumptions travel with the report.** The counterfactual rests on guesses; without them, changing a default silently rewrites history.
1178
+ - **`dayIso` is a local calendar date.** The IANA zone and the exact UTC bounds go alongside — anything assuming 96 buckets is wrong twice a year.
1179
+ - **Per-slot detail (`slots`) is opt-in.** Settlement is stateless, so the app can regenerate it on demand; only publish it for days under investigation.
1180
+ - **Units follow the platform:** energy in Wh, power in W, prices per kWh, currency as `EnyoCurrencyEnum`.
1181
+
1182
+ Requires the `Savings` permission.
1183
+
1123
1184
  ### Operational Utilities
1124
1185
 
1125
1186
  #### `useOnboarding(): EnergyAppOnboarding`
@@ -50,4 +50,5 @@ var EnergyAppPermissionTypeEnum;
50
50
  EnergyAppPermissionTypeEnum["Udp"] = "Udp";
51
51
  EnergyAppPermissionTypeEnum["ProvidedFiles"] = "ProvidedFiles";
52
52
  EnergyAppPermissionTypeEnum["Automation"] = "Automation";
53
+ EnergyAppPermissionTypeEnum["Savings"] = "Savings";
53
54
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -1,4 +1,4 @@
1
- export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation';
1
+ export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation' | 'Savings';
2
2
  export declare enum EnergyAppPermissionTypeEnum {
3
3
  RestrictedInternetAccess = "RestrictedInternetAccess",
4
4
  NetworkDeviceDiscovery = "NetworkDeviceDiscovery",
@@ -46,5 +46,6 @@ export declare enum EnergyAppPermissionTypeEnum {
46
46
  ChildProcess = "ChildProcess",
47
47
  Udp = "Udp",
48
48
  ProvidedFiles = "ProvidedFiles",
49
- Automation = "Automation"
49
+ Automation = "Automation",
50
+ Savings = "Savings"
50
51
  }
@@ -352,6 +352,19 @@ class EnergyApp {
352
352
  useAutomations() {
353
353
  return this.energyAppSdk.useAutomations();
354
354
  }
355
+ /**
356
+ * Gets the Savings API for publishing day-scoped savings reports and reading
357
+ * them back. An energy manager app settles a finished day by replaying its
358
+ * measured environment against an uncontrolled simulation and publishes both
359
+ * worlds here; the platform stores the days and owns every aggregation above
360
+ * them (month, year, lifetime).
361
+ * @returns The Savings API instance
362
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Savings` permission is
363
+ * not granted.
364
+ */
365
+ useSavings() {
366
+ return this.energyAppSdk.useSavings();
367
+ }
355
368
  /**
356
369
  * Gets the current SDK version.
357
370
  * @returns The semantic version string of the SDK
@@ -39,6 +39,7 @@ import { EnergyAppApplianceEnergyManagerForecast } from "./packages/energy-app-a
39
39
  import { EnergyAppBattery } from "./packages/energy-app-battery.cjs";
40
40
  import { EnergyAppFile } from "./packages/energy-app-file.cjs";
41
41
  import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
42
+ import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
42
43
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
43
44
  /**
44
45
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -281,6 +282,17 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
281
282
  * not granted.
282
283
  */
283
284
  useAutomations(): EnergyAppAutomation;
285
+ /**
286
+ * Gets the Savings API for publishing day-scoped savings reports and reading
287
+ * them back. An energy manager app settles a finished day by replaying its
288
+ * measured environment against an uncontrolled simulation and publishes both
289
+ * worlds here; the platform stores the days and owns every aggregation above
290
+ * them (month, year, lifetime).
291
+ * @returns The Savings API instance
292
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Savings` permission is
293
+ * not granted.
294
+ */
295
+ useSavings(): EnergyAppSavings;
284
296
  /**
285
297
  * Gets the current SDK version.
286
298
  * @returns The semantic version string of the SDK
@@ -38,6 +38,7 @@ import { EnergyAppApplianceEnergyManagerForecast } from "./packages/energy-app-a
38
38
  import { EnergyAppBattery } from "./packages/energy-app-battery.cjs";
39
39
  import { EnergyAppFile } from "./packages/energy-app-file.cjs";
40
40
  import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
41
+ import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
41
42
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
42
43
  export declare enum EnergyAppStateEnum {
43
44
  Launching = "launching",
@@ -145,4 +146,6 @@ export interface EnyoEnergyAppSdk {
145
146
  useFiles: () => EnergyAppFile;
146
147
  /** Get the Automation API for reading user-configured automations and (with EnergyManager permission) registering triggers, reporting trigger state, and publishing automation forecasts */
147
148
  useAutomations: () => EnergyAppAutomation;
149
+ /** Get the Savings API for publishing day-scoped savings reports and reading back which days were already reported */
150
+ useSavings: () => EnergyAppSavings;
148
151
  }
@@ -119,6 +119,8 @@ __exportStar(require("./packages/energy-app-battery.cjs"), exports);
119
119
  __exportStar(require("./types/enyo-fetch.cjs"), exports);
120
120
  __exportStar(require("./types/enyo-file.cjs"), exports);
121
121
  __exportStar(require("./packages/energy-app-file.cjs"), exports);
122
+ __exportStar(require("./types/enyo-savings.cjs"), exports);
123
+ __exportStar(require("./packages/energy-app-savings.cjs"), exports);
122
124
  __exportStar(require("./types/enyo-automation.cjs"), exports);
123
125
  __exportStar(require("./packages/energy-app-automation.cjs"), exports);
124
126
  __exportStar(require("./implementations/automation/automation-validators.cjs"), exports);
@@ -103,6 +103,8 @@ export * from './packages/energy-app-battery.cjs';
103
103
  export * from './types/enyo-fetch.cjs';
104
104
  export * from './types/enyo-file.cjs';
105
105
  export * from './packages/energy-app-file.cjs';
106
+ export * from './types/enyo-savings.cjs';
107
+ export * from './packages/energy-app-savings.cjs';
106
108
  export * from './types/enyo-automation.cjs';
107
109
  export * from './packages/energy-app-automation.cjs';
108
110
  export * from './implementations/automation/automation-validators.cjs';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,126 @@
1
+ import { EnyoDailySavingsReport, EnyoDailySavingsRequest, EnyoDailySavingsResponse } from "../types/enyo-savings.cjs";
2
+ /**
3
+ * Interface for publishing and reading back day-scoped savings reports.
4
+ *
5
+ * An energy manager app replays a finished day's measured environment through a
6
+ * simulation of the same house running uncontrolled, prices both worlds against
7
+ * the tariff that actually applied, and publishes the difference here. The
8
+ * platform stores the day-level records and owns every aggregation above them
9
+ * (month, year, lifetime) — a device is the wrong place to keep a running annual
10
+ * figure, because it can be replaced, reset, or offline for a week.
11
+ *
12
+ * Publishing is an upsert keyed by {@link EnyoDailySavingsReport.dayIso} plus
13
+ * {@link EnyoDailySavingsReport.method}, in the same spirit as
14
+ * {@link EnergyAppFile.registerFile}. This is deliberate: settlement is stateless
15
+ * and recomputable, so a day may legitimately be recomputed after a backfill, a
16
+ * bugfix, or a change in assumptions.
17
+ *
18
+ * Access to this API requires the `Savings` permission
19
+ * ({@link EnergyAppPermissionType}); {@link EnergyApp.useSavings} throws when it
20
+ * has not been granted.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const savings = energyApp.useSavings();
25
+ *
26
+ * // On boot: find the days that still need settling.
27
+ * const { missingDayIsos } = await savings.getDailySavings({
28
+ * startDayIso: '2026-07-01',
29
+ * endDayIso: '2026-07-31'
30
+ * });
31
+ * for (const dayIso of missingDayIsos) {
32
+ * await savings.publishDailySavings(await settleDay(dayIso));
33
+ * }
34
+ * ```
35
+ */
36
+ export interface EnergyAppSavings {
37
+ /**
38
+ * Publishes (upserts) the savings report for one day.
39
+ *
40
+ * The record is keyed by `report.dayIso` + `report.method`: re-publishing the
41
+ * same day with the same method replaces the stored record entirely, rather
42
+ * than appending a second one. Publishing a `Settled` report therefore does
43
+ * not remove an earlier `Projected` report for the same day — the two are
44
+ * distinct records and consumers should prefer the settled one.
45
+ *
46
+ * The report should carry both worlds, not only the delta: a lone savings
47
+ * number is unauditable and cannot be re-aggregated.
48
+ * {@link EnyoDailySavingsReport.confidence} must reflect the state of the
49
+ * inputs, since the platform excludes low-confidence days from rollups.
50
+ *
51
+ * @param report - The complete, self-contained report for one local calendar
52
+ * day, including costs, energy terms, coverage and the assumptions the
53
+ * counterfactual rested on.
54
+ * @returns Promise that resolves once the report has been stored by the host.
55
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Savings` permission is
56
+ * not granted.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * await savings.publishDailySavings({
61
+ * schemaVersion: 1,
62
+ * dayIso: '2026-07-14',
63
+ * timeZone: 'Europe/Berlin',
64
+ * dayStartUtcMs: Date.parse('2026-07-14T00:00:00+02:00'),
65
+ * dayEndUtcMs: Date.parse('2026-07-15T00:00:00+02:00'),
66
+ * method: EnyoSavingsMethodEnum.Settled,
67
+ * computedAtIso: '2026-07-15T02:14:03+02:00',
68
+ * calculatorVersion: '3.2.0',
69
+ * confidence: EnyoSavingsConfidenceEnum.High,
70
+ * confidenceIssues: [],
71
+ * costs: {
72
+ * currency: EnyoCurrencyEnum.EUR,
73
+ * optimizedCost: 1.42,
74
+ * baselineCost: 3.07,
75
+ * savings: 1.65,
76
+ * savingsFromSelfConsumption: 1.12,
77
+ * savingsFromArbitrage: 0.53
78
+ * },
79
+ * energy: { ... },
80
+ * metrics: { ... },
81
+ * attribution: [
82
+ * { applianceType: EnergyAppApplianceTypeEnum.Charger, savings: 0.91, shiftedEnergyWh: 12400 }
83
+ * ],
84
+ * coverage: [
85
+ * { series: 'pv', source: EnyoSavingsDataSourceEnum.Measured, expectedBuckets: 96, presentBuckets: 96 }
86
+ * ],
87
+ * assumptions: [
88
+ * { key: 'battery.dischargeEfficiency', value: 0.95 }
89
+ * ]
90
+ * });
91
+ * ```
92
+ */
93
+ publishDailySavings(report: EnyoDailySavingsReport): Promise<void>;
94
+ /**
95
+ * Reads back previously published reports for a range of local days.
96
+ *
97
+ * This is the counterpart that makes the API usable without duplicated state:
98
+ * the app asks the host which days it has already reported instead of
99
+ * maintaining its own shadow index. The response's
100
+ * {@link EnyoDailySavingsResponse.missingDayIsos} is exactly the backfill
101
+ * work list.
102
+ *
103
+ * Per-slot detail is only returned when it was published AND
104
+ * {@link EnyoDailySavingsRequest.includeSlots} is set.
105
+ *
106
+ * @param request - Inclusive local day range, with optional method filter and
107
+ * opt-in per-slot detail.
108
+ * @returns Promise resolving to the stored reports plus the days in the range
109
+ * that carry none.
110
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Savings` permission is
111
+ * not granted.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const response = await savings.getDailySavings({
116
+ * startDayIso: '2026-07-01',
117
+ * endDayIso: '2026-07-07',
118
+ * method: EnyoSavingsMethodEnum.Settled
119
+ * });
120
+ * const weekTotal = response.reports
121
+ * .filter(r => r.confidence === EnyoSavingsConfidenceEnum.High)
122
+ * .reduce((sum, r) => sum + r.costs.savings, 0);
123
+ * ```
124
+ */
125
+ getDailySavings(request: EnyoDailySavingsRequest): Promise<EnyoDailySavingsResponse>;
126
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnyoSavingsDataSourceEnum = exports.EnyoSavingsIssueCodeEnum = exports.EnyoSavingsConfidenceEnum = exports.EnyoSavingsMethodEnum = void 0;
4
+ /**
5
+ * How the figures in an {@link EnyoDailySavingsReport} were produced.
6
+ */
7
+ var EnyoSavingsMethodEnum;
8
+ (function (EnyoSavingsMethodEnum) {
9
+ /**
10
+ * Measured day replayed against an uncontrolled simulation, after the fact.
11
+ *
12
+ * The environment (PV generation, base load, EV sessions, prices, ...) is
13
+ * taken from what was actually measured; only the control decisions are
14
+ * simulated away. This is the authoritative method and the only one that
15
+ * should feed customer-facing totals.
16
+ */
17
+ EnyoSavingsMethodEnum["Settled"] = "Settled";
18
+ /**
19
+ * Forward-looking estimate for a day that has not finished yet.
20
+ *
21
+ * Both worlds rest on forecasts, so the figure will change until the day is
22
+ * settled. Consumers should treat a `Projected` report as provisional and
23
+ * expect it to be replaced by a `Settled` one.
24
+ */
25
+ EnyoSavingsMethodEnum["Projected"] = "Projected";
26
+ })(EnyoSavingsMethodEnum || (exports.EnyoSavingsMethodEnum = EnyoSavingsMethodEnum = {}));
27
+ /**
28
+ * How much trust a consumer may place in a published report.
29
+ *
30
+ * Rollups (month, year, lifetime) MUST exclude {@link EnyoSavingsConfidenceEnum.Low}
31
+ * days. Keeping that rule on the platform side — rather than in every consumer —
32
+ * is the reason confidence is a first-class field rather than something derived
33
+ * from {@link EnyoDailySavingsReport.coverage}.
34
+ */
35
+ var EnyoSavingsConfidenceEnum;
36
+ (function (EnyoSavingsConfidenceEnum) {
37
+ /** All inputs were complete and the two worlds balanced; safe to aggregate. */
38
+ EnyoSavingsConfidenceEnum["High"] = "High";
39
+ /** At least one {@link EnyoSavingsIssue} applies; exclude from rollups. */
40
+ EnyoSavingsConfidenceEnum["Low"] = "Low";
41
+ })(EnyoSavingsConfidenceEnum || (exports.EnyoSavingsConfidenceEnum = EnyoSavingsConfidenceEnum = {}));
42
+ /**
43
+ * Machine-readable reason why a report's confidence is not
44
+ * {@link EnyoSavingsConfidenceEnum.High}.
45
+ */
46
+ var EnyoSavingsIssueCodeEnum;
47
+ (function (EnyoSavingsIssueCodeEnum) {
48
+ /** An input series did not cover the whole day. */
49
+ EnyoSavingsIssueCodeEnum["IncompleteData"] = "IncompleteData";
50
+ /** The two worlds did not move the same energy — the comparison is not valid. */
51
+ EnyoSavingsIssueCodeEnum["EnergyImbalance"] = "EnergyImbalance";
52
+ /** No tariff for (part of) the day; a default price was substituted. */
53
+ EnyoSavingsIssueCodeEnum["TariffUnavailable"] = "TariffUnavailable";
54
+ /** PV was curtailed by control, so measured generation understates the roof. */
55
+ EnyoSavingsIssueCodeEnum["GenerationCurtailed"] = "GenerationCurtailed";
56
+ /** The day was only partially observed (e.g. commissioning day). */
57
+ EnyoSavingsIssueCodeEnum["PartialDay"] = "PartialDay";
58
+ })(EnyoSavingsIssueCodeEnum || (exports.EnyoSavingsIssueCodeEnum = EnyoSavingsIssueCodeEnum = {}));
59
+ /**
60
+ * Origin of one input series used by the calculation, as reported in
61
+ * {@link EnyoSavingsSeriesCoverage}.
62
+ */
63
+ var EnyoSavingsDataSourceEnum;
64
+ (function (EnyoSavingsDataSourceEnum) {
65
+ /** Real measured values from the device / platform timeseries. */
66
+ EnyoSavingsDataSourceEnum["Measured"] = "Measured";
67
+ /** Substituted values (defaults, interpolation, a standard load profile). */
68
+ EnyoSavingsDataSourceEnum["Fallback"] = "Fallback";
69
+ /** Nothing available — the series contributed no data at all. */
70
+ EnyoSavingsDataSourceEnum["Missing"] = "Missing";
71
+ })(EnyoSavingsDataSourceEnum || (exports.EnyoSavingsDataSourceEnum = EnyoSavingsDataSourceEnum = {}));