@enyo-energy/energy-app-sdk 0.0.180 → 0.0.182
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 +37 -1
- package/dist/cjs/implementations/forecasts/air-conditioning-consumption-forecast.cjs +221 -0
- package/dist/cjs/implementations/forecasts/air-conditioning-consumption-forecast.d.cts +94 -0
- package/dist/cjs/implementations/forecasts/air-conditioning-room-temperature-forecast.cjs +234 -0
- package/dist/cjs/implementations/forecasts/air-conditioning-room-temperature-forecast.d.cts +97 -0
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +184 -0
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +131 -0
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +181 -0
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.d.cts +50 -0
- package/dist/cjs/index.cjs +11 -0
- package/dist/cjs/index.d.cts +11 -0
- package/dist/cjs/integrations/energy-manager-energy-app.cjs +53 -0
- package/dist/cjs/integrations/energy-manager-energy-app.d.cts +21 -0
- package/dist/cjs/integrations/heatpump-integration-energy-app.cjs +1 -0
- package/dist/cjs/integrations/heatpump-integration-energy-app.d.cts +18 -1
- package/dist/cjs/integrations/storage-integration-energy-app.cjs +3 -0
- package/dist/cjs/integrations/storage-integration-energy-app.d.cts +19 -1
- package/dist/cjs/integrations/wallbox-integration-energy-app.cjs +1 -0
- package/dist/cjs/integrations/wallbox-integration-energy-app.d.cts +16 -1
- package/dist/cjs/packages/energy-app-diagnostics.d.cts +84 -3
- package/dist/cjs/packages/energy-app-onboarding.d.cts +15 -0
- package/dist/cjs/packages/energy-app-timeseries.d.cts +11 -3
- package/dist/cjs/types/enyo-air-conditioning-appliance.d.cts +8 -0
- package/dist/cjs/types/enyo-appliance.d.cts +1 -0
- package/dist/cjs/types/enyo-battery-appliance.d.cts +17 -0
- package/dist/cjs/types/enyo-charger-appliance.cjs +21 -1
- package/dist/cjs/types/enyo-charger-appliance.d.cts +62 -0
- package/dist/cjs/types/enyo-data-bus-value.cjs +79 -1
- package/dist/cjs/types/enyo-data-bus-value.d.cts +293 -1
- package/dist/cjs/types/enyo-flexibility-announcement.cjs +93 -0
- package/dist/cjs/types/enyo-flexibility-announcement.d.cts +284 -0
- package/dist/cjs/types/enyo-heating-rod-appliance.d.cts +8 -0
- package/dist/cjs/types/enyo-heatpump-appliance.cjs +24 -1
- package/dist/cjs/types/enyo-heatpump-appliance.d.cts +43 -0
- package/dist/cjs/types/enyo-inverter-appliance.d.cts +18 -0
- package/dist/cjs/types/enyo-onboarding-v2.cjs +134 -0
- package/dist/cjs/types/enyo-onboarding-v2.d.cts +305 -0
- package/dist/cjs/types/enyo-onboarding.cjs +7 -0
- package/dist/cjs/types/enyo-onboarding.d.cts +20 -0
- package/dist/cjs/types/enyo-timeseries.d.cts +45 -3
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/forecasts/air-conditioning-consumption-forecast.d.ts +94 -0
- package/dist/implementations/forecasts/air-conditioning-consumption-forecast.js +217 -0
- package/dist/implementations/forecasts/air-conditioning-room-temperature-forecast.d.ts +97 -0
- package/dist/implementations/forecasts/air-conditioning-room-temperature-forecast.js +230 -0
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +131 -0
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +177 -0
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.d.ts +50 -0
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +175 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/integrations/energy-manager-energy-app.d.ts +21 -0
- package/dist/integrations/energy-manager-energy-app.js +53 -0
- package/dist/integrations/heatpump-integration-energy-app.d.ts +18 -1
- package/dist/integrations/heatpump-integration-energy-app.js +1 -0
- package/dist/integrations/storage-integration-energy-app.d.ts +19 -1
- package/dist/integrations/storage-integration-energy-app.js +3 -0
- package/dist/integrations/wallbox-integration-energy-app.d.ts +16 -1
- package/dist/integrations/wallbox-integration-energy-app.js +1 -0
- package/dist/packages/energy-app-diagnostics.d.ts +84 -3
- package/dist/packages/energy-app-onboarding.d.ts +15 -0
- package/dist/packages/energy-app-timeseries.d.ts +11 -3
- package/dist/types/enyo-air-conditioning-appliance.d.ts +8 -0
- package/dist/types/enyo-appliance.d.ts +1 -0
- package/dist/types/enyo-battery-appliance.d.ts +17 -0
- package/dist/types/enyo-charger-appliance.d.ts +62 -0
- package/dist/types/enyo-charger-appliance.js +20 -0
- package/dist/types/enyo-data-bus-value.d.ts +293 -1
- package/dist/types/enyo-data-bus-value.js +78 -0
- package/dist/types/enyo-flexibility-announcement.d.ts +284 -0
- package/dist/types/enyo-flexibility-announcement.js +90 -0
- package/dist/types/enyo-heating-rod-appliance.d.ts +8 -0
- package/dist/types/enyo-heatpump-appliance.d.ts +43 -0
- package/dist/types/enyo-heatpump-appliance.js +23 -0
- package/dist/types/enyo-inverter-appliance.d.ts +18 -0
- package/dist/types/enyo-onboarding-v2.d.ts +305 -0
- package/dist/types/enyo-onboarding-v2.js +131 -0
- package/dist/types/enyo-onboarding.d.ts +20 -0
- package/dist/types/enyo-onboarding.js +7 -0
- package/dist/types/enyo-timeseries.d.ts +45 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
|
|
|
49
49
|
- [EvChargingForecast](#evchargingforecast)
|
|
50
50
|
- [HeatpumpConsumptionForecast](#heatpumpconsumptionforecast)
|
|
51
51
|
- [HeatpumpDhwTemperatureForecast](#heatpumpdhwtemperatureforecast)
|
|
52
|
+
- [AirConditioningConsumptionForecast](#airconditioningconsumptionforecast)
|
|
53
|
+
- [AirConditioningRoomTemperatureForecast](#airconditioningroomtemperatureforecast)
|
|
52
54
|
- [Appliance Energy-Manager Forecast](#appliance-energy-manager-forecast)
|
|
53
55
|
- [`useApplianceEnergyManagerForecast()`](#useapplianceenergymanagerforecast-energyappapplianceenergymanagerforecast)
|
|
54
56
|
- [ChargerForecast](#chargerforecast)
|
|
@@ -144,6 +146,8 @@ The SDK exposes several layered building blocks. Pick the one that matches the k
|
|
|
144
146
|
| Forecast EV charging demand | [`EvChargingForecast`](#evchargingforecast) |
|
|
145
147
|
| Forecast heatpump electrical consumption | [`HeatpumpConsumptionForecast`](#heatpumpconsumptionforecast) |
|
|
146
148
|
| Forecast heatpump DHW tank temperature | [`HeatpumpDhwTemperatureForecast`](#heatpumpdhwtemperatureforecast) |
|
|
149
|
+
| Forecast air conditioning electrical consumption | [`AirConditioningConsumptionForecast`](#airconditioningconsumptionforecast) |
|
|
150
|
+
| Forecast air conditioning room temperature | [`AirConditioningRoomTemperatureForecast`](#airconditioningroomtemperatureforecast) |
|
|
147
151
|
| Announce a charger / battery / heatpump command plan you **intend to apply** | [`useApplianceEnergyManagerForecast()`](#useapplianceenergymanagerforecast-energyappapplianceenergymanagerforecast) |
|
|
148
152
|
| Talk to an EEBUS / SHIP / SPINE device | [`useEebus()`](#useeebus-energyappeebus) |
|
|
149
153
|
| Speak MQTT (SDK broker or external) | [`useMqtt()`](#usemqtt-energyappmqtt) |
|
|
@@ -1744,6 +1748,8 @@ new EnergyManagerEnergyApp({
|
|
|
1744
1748
|
- `getEvChargingForecast(applianceId, config?)`
|
|
1745
1749
|
- `getHeatpumpConsumptionForecast(applianceId, config?)`
|
|
1746
1750
|
- `getHeatpumpDhwTemperatureForecast(applianceId, config?)`
|
|
1751
|
+
- `getAirConditioningConsumptionForecast(applianceId, config?)`
|
|
1752
|
+
- `getAirConditioningRoomTemperatureForecast(applianceId, config?)`
|
|
1747
1753
|
|
|
1748
1754
|
**Lifecycle**
|
|
1749
1755
|
|
|
@@ -1761,7 +1767,7 @@ const batteryForecast = battery.getForecast();
|
|
|
1761
1767
|
|
|
1762
1768
|
## Forecasting
|
|
1763
1769
|
|
|
1764
|
-
The forecasting module provides 24-hour predictions across the energy domains the SDK already understands (PV, battery, home consumption, EV charging, heatpump consumption, DHW temperature). Every forecaster follows the same lifecycle and shares the same configuration shape, so once you've used one you've used them all.
|
|
1770
|
+
The forecasting module provides 24-hour predictions across the energy domains the SDK already understands (PV, battery, home consumption, EV charging, heatpump consumption, DHW temperature, air conditioning consumption, air conditioning room temperature). Every forecaster follows the same lifecycle and shares the same configuration shape, so once you've used one you've used them all.
|
|
1765
1771
|
|
|
1766
1772
|
### ✨ Common pattern
|
|
1767
1773
|
|
|
@@ -1880,6 +1886,36 @@ new HeatpumpDhwTemperatureForecast(app, applianceId, {
|
|
|
1880
1886
|
- **Notable config:** `dhwTankIndex` selects a specific tank (zero-based); omit to average across all tanks.
|
|
1881
1887
|
- **Live source:** heatpump temperature timeseries / live updates.
|
|
1882
1888
|
|
|
1889
|
+
### AirConditioningConsumptionForecast
|
|
1890
|
+
|
|
1891
|
+
Forecasts the electrical consumption of an air conditioning appliance (cooling + heating combined).
|
|
1892
|
+
|
|
1893
|
+
```typescript
|
|
1894
|
+
new AirConditioningConsumptionForecast(app, applianceId, { source: EnyoSourceEnum.Device, config? });
|
|
1895
|
+
```
|
|
1896
|
+
|
|
1897
|
+
- **Output per slot:** `{ powerW: number; powerWh: number }`
|
|
1898
|
+
- **History default:** 14 days — AC load is weather-driven and bursty, so a wider window than the heatpump's 7 days gives a more stable per-slot profile.
|
|
1899
|
+
- **Live source:** `AirConditioningValuesUpdateV1`.
|
|
1900
|
+
- **Note:** `powerW` is optional on that message; updates without a power reading are ignored rather than counted as `0 W`, so an appliance that only reports its operation mode does not drag the forecast towards zero.
|
|
1901
|
+
- **Note:** like the heatpump forecaster, it does not adjust for forecasted weather; layer a correction on top if you need that.
|
|
1902
|
+
|
|
1903
|
+
### AirConditioningRoomTemperatureForecast
|
|
1904
|
+
|
|
1905
|
+
Forecasts the room temperature served by an air conditioning appliance — the input you need to plan pre-cooling on forecasted PV surplus.
|
|
1906
|
+
|
|
1907
|
+
```typescript
|
|
1908
|
+
new AirConditioningRoomTemperatureForecast(app, applianceId, {
|
|
1909
|
+
source: EnyoSourceEnum.Device,
|
|
1910
|
+
config?: { roomIndex?: number, ...ForecastConfig }
|
|
1911
|
+
});
|
|
1912
|
+
```
|
|
1913
|
+
|
|
1914
|
+
- **Output per slot:** `{ temperatureC: number }` (rounded to 0.1 °C).
|
|
1915
|
+
- **History default:** 7 days.
|
|
1916
|
+
- **Notable config:** `roomIndex` selects a specific room (zero-based); omit to average across all rooms. When set, it is echoed back on the published message's `data.roomIndex`.
|
|
1917
|
+
- **Live source:** `AirConditioningTemperaturesUpdateV1`.
|
|
1918
|
+
|
|
1883
1919
|
### 🚀 Common usage example
|
|
1884
1920
|
|
|
1885
1921
|
```typescript
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AirConditioningConsumptionForecast = void 0;
|
|
4
|
+
const enyo_data_bus_value_js_1 = require("../../types/enyo-data-bus-value.cjs");
|
|
5
|
+
const forecast_types_js_1 = require("./forecast-types.cjs");
|
|
6
|
+
const forecast_utils_js_1 = require("./forecast-utils.cjs");
|
|
7
|
+
/**
|
|
8
|
+
* Builds a 24-hour air conditioning electrical consumption forecast for a
|
|
9
|
+
* single air conditioning appliance (cooling and heating combined). Historical
|
|
10
|
+
* buckets are sourced from the dedicated
|
|
11
|
+
* `useTimeseries().getAirConditioningPowerTimeseries()` endpoint, and live
|
|
12
|
+
* `AirConditioningValuesUpdateV1` data-bus messages are merged in as new
|
|
13
|
+
* readings arrive.
|
|
14
|
+
*
|
|
15
|
+
* Air conditioning load follows the household's daily occupancy rhythm, so the
|
|
16
|
+
* algorithm uses a same-weekday recency-weighted average over the configured
|
|
17
|
+
* history window, with an all-weekday fallback. The default history window is
|
|
18
|
+
* 14 days (longer than the heatpump's 7): AC operation is weather-driven and
|
|
19
|
+
* bursty, so a wider window yields a more stable per-slot profile.
|
|
20
|
+
*
|
|
21
|
+
* Note: `powerW` is optional on `AirConditioningValuesUpdateV1`. Messages
|
|
22
|
+
* without a power reading are ignored rather than counted as `0 W`, so that an
|
|
23
|
+
* appliance that only reports its operation mode does not drag the forecast
|
|
24
|
+
* towards zero.
|
|
25
|
+
*
|
|
26
|
+
* Like the heatpump forecaster, this implementation does not adjust for
|
|
27
|
+
* forecasted outdoor temperature; layer a correction on top of the returned
|
|
28
|
+
* `powerW` values using the weather forecast for the same horizon if you need
|
|
29
|
+
* that.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const forecast = new AirConditioningConsumptionForecast(app, 'ac-1', {
|
|
34
|
+
* source: EnyoSourceEnum.Device,
|
|
35
|
+
* });
|
|
36
|
+
* await forecast.initialize();
|
|
37
|
+
* const result = forecast.getForecast();
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
class AirConditioningConsumptionForecast {
|
|
41
|
+
app;
|
|
42
|
+
applianceId;
|
|
43
|
+
/**
|
|
44
|
+
* Default history window. AC load is weather-driven and bursty, so a longer
|
|
45
|
+
* window than the heatpump's is used to stabilise the per-slot profile.
|
|
46
|
+
*/
|
|
47
|
+
static DEFAULT_HISTORY_DAYS = 14;
|
|
48
|
+
config;
|
|
49
|
+
history = [];
|
|
50
|
+
listenerId;
|
|
51
|
+
currentSlotMs;
|
|
52
|
+
currentSlotPowerSum = 0;
|
|
53
|
+
currentSlotSamples = 0;
|
|
54
|
+
initialized = false;
|
|
55
|
+
source;
|
|
56
|
+
/**
|
|
57
|
+
* @param app - The energy-app SDK instance.
|
|
58
|
+
* @param applianceId - Air conditioning appliance ID.
|
|
59
|
+
* @param options.source - Source identifier used for outgoing forecast messages.
|
|
60
|
+
* @param options.config - Optional config overrides; `historyDays` defaults to 14.
|
|
61
|
+
*/
|
|
62
|
+
constructor(app, applianceId, options) {
|
|
63
|
+
this.app = app;
|
|
64
|
+
this.applianceId = applianceId;
|
|
65
|
+
this.config = (0, forecast_types_js_1.resolveForecastConfig)({ historyDays: AirConditioningConsumptionForecast.DEFAULT_HISTORY_DAYS }, options.config);
|
|
66
|
+
this.source = options.source;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Loads historical air conditioning power buckets via
|
|
70
|
+
* `useTimeseries().getAirConditioningPowerTimeseries()` and starts listening
|
|
71
|
+
* to live `AirConditioningValuesUpdateV1` events for ongoing updates.
|
|
72
|
+
* Idempotent.
|
|
73
|
+
*/
|
|
74
|
+
async initialize() {
|
|
75
|
+
if (this.initialized)
|
|
76
|
+
return;
|
|
77
|
+
this.initialized = true;
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const startMs = now - this.config.historyDays * 24 * 60 * 60 * 1000;
|
|
80
|
+
const response = await this.app.useTimeseries().getAirConditioningPowerTimeseries({
|
|
81
|
+
startDateIso: new Date(startMs).toISOString(),
|
|
82
|
+
endDateIso: new Date(now).toISOString(),
|
|
83
|
+
applianceIds: [this.applianceId],
|
|
84
|
+
resolution: this.config.resolution,
|
|
85
|
+
});
|
|
86
|
+
for (const entry of response.entries) {
|
|
87
|
+
this.history.push({
|
|
88
|
+
timestampIso: entry.timestampIso,
|
|
89
|
+
values: {
|
|
90
|
+
powerW: entry.airConditioningPowerW,
|
|
91
|
+
powerWh: entry.airConditioningPowerWh,
|
|
92
|
+
samples: 1,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
this.listenerId = this.app.useDataBus().listenForMessages([enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.AirConditioningValuesUpdateV1], (msg) => this.onAirConditioningValues(msg));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns the current forecast as an
|
|
100
|
+
* `EnyoDataBusAirConditioningConsumptionForecastV1`-shaped payload over the
|
|
101
|
+
* configured horizon.
|
|
102
|
+
*/
|
|
103
|
+
getForecast() {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
this.flushCurrentSlot(now);
|
|
106
|
+
const parsed = (0, forecast_utils_js_1.parseHistory)(this.history);
|
|
107
|
+
const todayWeekday = new Date(now).getUTCDay();
|
|
108
|
+
const sameWeekdayMap = (0, forecast_utils_js_1.buildSlotMap)(parsed, (b) => b.weekday === todayWeekday);
|
|
109
|
+
const fallbackMap = (0, forecast_utils_js_1.buildSlotMap)(parsed);
|
|
110
|
+
const isoIndex = (0, forecast_utils_js_1.buildIsoIndex)(parsed, (v) => v.powerW);
|
|
111
|
+
const buckets = (0, forecast_utils_js_1.buildForecastSlots)(now, this.config.horizonHours, this.config.resolution).map((slotMs) => {
|
|
112
|
+
const key = formatSlotKey(slotMs);
|
|
113
|
+
const slotBuckets = sameWeekdayMap.get(key) ?? fallbackMap.get(key);
|
|
114
|
+
const avg = (0, forecast_utils_js_1.weightedAverageBySlot)(slotBuckets, now, this.config.historyDays, {
|
|
115
|
+
powerW: (v) => v.powerW,
|
|
116
|
+
});
|
|
117
|
+
const powerW = Math.max(0, Math.round(avg.powerW));
|
|
118
|
+
return {
|
|
119
|
+
timestampIso: new Date(slotMs).toISOString(),
|
|
120
|
+
payload: { powerW, powerWh: (0, forecast_utils_js_1.wToWhPer15Min)(powerW) },
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
if (this.config.alignToRecentActuals) {
|
|
124
|
+
(0, forecast_utils_js_1.alignForecastToRecentActuals)(buckets, (iso) => isoIndex.get(iso), 'powerW', ['powerW', 'powerWh']);
|
|
125
|
+
for (const b of buckets) {
|
|
126
|
+
b.payload.powerW = Math.max(0, Math.round(b.payload.powerW));
|
|
127
|
+
b.payload.powerWh = (0, forecast_utils_js_1.wToWhPer15Min)(b.payload.powerW);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const entries = buckets.map((b) => ({
|
|
131
|
+
timestampIso: b.timestampIso,
|
|
132
|
+
powerW: b.payload.powerW,
|
|
133
|
+
powerWh: b.payload.powerWh,
|
|
134
|
+
}));
|
|
135
|
+
const result = {
|
|
136
|
+
generatedAtIso: new Date(now).toISOString(),
|
|
137
|
+
data: { resolution: this.config.resolution, entries },
|
|
138
|
+
};
|
|
139
|
+
if (this.config.publishToBus) {
|
|
140
|
+
this.publish(result);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Computes and explicitly publishes the current forecast on the data bus.
|
|
146
|
+
*/
|
|
147
|
+
publishForecast() {
|
|
148
|
+
const result = this.getForecast();
|
|
149
|
+
if (!this.config.publishToBus) {
|
|
150
|
+
this.publish(result);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Detaches data-bus listeners and clears in-memory history.
|
|
155
|
+
*/
|
|
156
|
+
dispose() {
|
|
157
|
+
if (this.listenerId) {
|
|
158
|
+
this.app.useDataBus().unsubscribe(this.listenerId);
|
|
159
|
+
this.listenerId = undefined;
|
|
160
|
+
}
|
|
161
|
+
this.history.length = 0;
|
|
162
|
+
this.initialized = false;
|
|
163
|
+
}
|
|
164
|
+
onAirConditioningValues(message) {
|
|
165
|
+
if (message.applianceId !== this.applianceId)
|
|
166
|
+
return;
|
|
167
|
+
const power = message.data.values.powerW;
|
|
168
|
+
if (typeof power !== 'number' || Number.isNaN(power))
|
|
169
|
+
return;
|
|
170
|
+
const ts = new Date(message.timestampIso).getTime();
|
|
171
|
+
const slotMs = (0, forecast_utils_js_1.roundDownTo15Minutes)(ts);
|
|
172
|
+
if (this.currentSlotMs === undefined || slotMs !== this.currentSlotMs) {
|
|
173
|
+
this.flushCurrentSlot(ts);
|
|
174
|
+
this.currentSlotMs = slotMs;
|
|
175
|
+
this.currentSlotPowerSum = 0;
|
|
176
|
+
this.currentSlotSamples = 0;
|
|
177
|
+
}
|
|
178
|
+
this.currentSlotPowerSum += power;
|
|
179
|
+
this.currentSlotSamples += 1;
|
|
180
|
+
}
|
|
181
|
+
flushCurrentSlot(nowMs) {
|
|
182
|
+
if (this.currentSlotMs === undefined || this.currentSlotSamples === 0)
|
|
183
|
+
return;
|
|
184
|
+
const avg = this.currentSlotPowerSum / this.currentSlotSamples;
|
|
185
|
+
(0, forecast_utils_js_1.upsertLiveBucket)(this.history, this.currentSlotMs, { powerW: avg, powerWh: (0, forecast_utils_js_1.wToWhPer15Min)(avg), samples: this.currentSlotSamples }, (existing, incoming) => {
|
|
186
|
+
const total = existing.samples + incoming.samples;
|
|
187
|
+
const mergedPowerW = (existing.powerW * existing.samples + incoming.powerW * incoming.samples) / total;
|
|
188
|
+
return {
|
|
189
|
+
powerW: mergedPowerW,
|
|
190
|
+
powerWh: (0, forecast_utils_js_1.wToWhPer15Min)(mergedPowerW),
|
|
191
|
+
samples: total,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
if (this.currentSlotMs < (0, forecast_utils_js_1.roundDownTo15Minutes)(nowMs)) {
|
|
195
|
+
this.currentSlotMs = undefined;
|
|
196
|
+
this.currentSlotPowerSum = 0;
|
|
197
|
+
this.currentSlotSamples = 0;
|
|
198
|
+
}
|
|
199
|
+
(0, forecast_utils_js_1.trimHistory)(this.history, nowMs, this.config.historyDays * 24 * 60 * 60 * 1000 + forecast_utils_js_1.FIFTEEN_MIN_MS);
|
|
200
|
+
}
|
|
201
|
+
publish(result) {
|
|
202
|
+
const message = {
|
|
203
|
+
id: globalThis.crypto.randomUUID(),
|
|
204
|
+
type: 'message',
|
|
205
|
+
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.AirConditioningConsumptionForecastV1,
|
|
206
|
+
source: this.source,
|
|
207
|
+
applianceId: this.applianceId,
|
|
208
|
+
timestampIso: result.generatedAtIso,
|
|
209
|
+
resolution: 'dynamic',
|
|
210
|
+
data: result.data,
|
|
211
|
+
};
|
|
212
|
+
this.app.useDataBus().sendMessage([message]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.AirConditioningConsumptionForecast = AirConditioningConsumptionForecast;
|
|
216
|
+
function formatSlotKey(slotMs) {
|
|
217
|
+
const d = new Date(slotMs);
|
|
218
|
+
return `${d.getUTCHours().toString().padStart(2, '0')}:${d.getUTCMinutes()
|
|
219
|
+
.toString()
|
|
220
|
+
.padStart(2, '0')}`;
|
|
221
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { EnergyApp } from '../../energy-app.cjs';
|
|
2
|
+
import { EnyoDataBusAirConditioningConsumptionForecastV1 } from '../../types/enyo-data-bus-value.cjs';
|
|
3
|
+
import { EnyoSourceEnum } from '../../types/enyo-source.enum.cjs';
|
|
4
|
+
import { BaseForecast, Forecaster, ForecastConfig } from './forecast-types.cjs';
|
|
5
|
+
/**
|
|
6
|
+
* Forecast result returned by
|
|
7
|
+
* {@link AirConditioningConsumptionForecast.getForecast}, shaped to match
|
|
8
|
+
* `EnyoDataBusAirConditioningConsumptionForecastV1.data`.
|
|
9
|
+
*/
|
|
10
|
+
export type AirConditioningConsumptionForecastResult = BaseForecast<EnyoDataBusAirConditioningConsumptionForecastV1['data']>;
|
|
11
|
+
/**
|
|
12
|
+
* Builds a 24-hour air conditioning electrical consumption forecast for a
|
|
13
|
+
* single air conditioning appliance (cooling and heating combined). Historical
|
|
14
|
+
* buckets are sourced from the dedicated
|
|
15
|
+
* `useTimeseries().getAirConditioningPowerTimeseries()` endpoint, and live
|
|
16
|
+
* `AirConditioningValuesUpdateV1` data-bus messages are merged in as new
|
|
17
|
+
* readings arrive.
|
|
18
|
+
*
|
|
19
|
+
* Air conditioning load follows the household's daily occupancy rhythm, so the
|
|
20
|
+
* algorithm uses a same-weekday recency-weighted average over the configured
|
|
21
|
+
* history window, with an all-weekday fallback. The default history window is
|
|
22
|
+
* 14 days (longer than the heatpump's 7): AC operation is weather-driven and
|
|
23
|
+
* bursty, so a wider window yields a more stable per-slot profile.
|
|
24
|
+
*
|
|
25
|
+
* Note: `powerW` is optional on `AirConditioningValuesUpdateV1`. Messages
|
|
26
|
+
* without a power reading are ignored rather than counted as `0 W`, so that an
|
|
27
|
+
* appliance that only reports its operation mode does not drag the forecast
|
|
28
|
+
* towards zero.
|
|
29
|
+
*
|
|
30
|
+
* Like the heatpump forecaster, this implementation does not adjust for
|
|
31
|
+
* forecasted outdoor temperature; layer a correction on top of the returned
|
|
32
|
+
* `powerW` values using the weather forecast for the same horizon if you need
|
|
33
|
+
* that.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const forecast = new AirConditioningConsumptionForecast(app, 'ac-1', {
|
|
38
|
+
* source: EnyoSourceEnum.Device,
|
|
39
|
+
* });
|
|
40
|
+
* await forecast.initialize();
|
|
41
|
+
* const result = forecast.getForecast();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare class AirConditioningConsumptionForecast implements Forecaster {
|
|
45
|
+
private readonly app;
|
|
46
|
+
readonly applianceId: string;
|
|
47
|
+
/**
|
|
48
|
+
* Default history window. AC load is weather-driven and bursty, so a longer
|
|
49
|
+
* window than the heatpump's is used to stabilise the per-slot profile.
|
|
50
|
+
*/
|
|
51
|
+
static readonly DEFAULT_HISTORY_DAYS = 14;
|
|
52
|
+
private readonly config;
|
|
53
|
+
private readonly history;
|
|
54
|
+
private listenerId;
|
|
55
|
+
private currentSlotMs;
|
|
56
|
+
private currentSlotPowerSum;
|
|
57
|
+
private currentSlotSamples;
|
|
58
|
+
private initialized;
|
|
59
|
+
private readonly source;
|
|
60
|
+
/**
|
|
61
|
+
* @param app - The energy-app SDK instance.
|
|
62
|
+
* @param applianceId - Air conditioning appliance ID.
|
|
63
|
+
* @param options.source - Source identifier used for outgoing forecast messages.
|
|
64
|
+
* @param options.config - Optional config overrides; `historyDays` defaults to 14.
|
|
65
|
+
*/
|
|
66
|
+
constructor(app: EnergyApp, applianceId: string, options: {
|
|
67
|
+
source: EnyoSourceEnum;
|
|
68
|
+
config?: ForecastConfig;
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Loads historical air conditioning power buckets via
|
|
72
|
+
* `useTimeseries().getAirConditioningPowerTimeseries()` and starts listening
|
|
73
|
+
* to live `AirConditioningValuesUpdateV1` events for ongoing updates.
|
|
74
|
+
* Idempotent.
|
|
75
|
+
*/
|
|
76
|
+
initialize(): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Returns the current forecast as an
|
|
79
|
+
* `EnyoDataBusAirConditioningConsumptionForecastV1`-shaped payload over the
|
|
80
|
+
* configured horizon.
|
|
81
|
+
*/
|
|
82
|
+
getForecast(): AirConditioningConsumptionForecastResult;
|
|
83
|
+
/**
|
|
84
|
+
* Computes and explicitly publishes the current forecast on the data bus.
|
|
85
|
+
*/
|
|
86
|
+
publishForecast(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Detaches data-bus listeners and clears in-memory history.
|
|
89
|
+
*/
|
|
90
|
+
dispose(): void;
|
|
91
|
+
private onAirConditioningValues;
|
|
92
|
+
private flushCurrentSlot;
|
|
93
|
+
private publish;
|
|
94
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AirConditioningRoomTemperatureForecast = void 0;
|
|
4
|
+
const enyo_data_bus_value_js_1 = require("../../types/enyo-data-bus-value.cjs");
|
|
5
|
+
const forecast_types_js_1 = require("./forecast-types.cjs");
|
|
6
|
+
const forecast_utils_js_1 = require("./forecast-utils.cjs");
|
|
7
|
+
/**
|
|
8
|
+
* Builds a 24-hour room temperature forecast for a single air conditioning
|
|
9
|
+
* appliance from historical air conditioning temperature timeseries data, with
|
|
10
|
+
* live updates merged in from `AirConditioningTemperaturesUpdateV1` data-bus
|
|
11
|
+
* events.
|
|
12
|
+
*
|
|
13
|
+
* Room temperature follows the household's daily occupancy and setpoint
|
|
14
|
+
* rhythm, which is strongly weekday-cyclic, so the algorithm uses a
|
|
15
|
+
* same-weekday recency-weighted average over the configured history window
|
|
16
|
+
* (default 7 d), with an all-weekday fallback. A single room can be selected
|
|
17
|
+
* via {@link AirConditioningRoomTemperatureForecastConfig.roomIndex};
|
|
18
|
+
* otherwise all reported rooms are averaged per timestamp.
|
|
19
|
+
*
|
|
20
|
+
* The forecast is the primary input for planning AC flexibility — for example
|
|
21
|
+
* pre-cooling on forecasted PV surplus — since it says where the room
|
|
22
|
+
* temperature is heading if nothing changes.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const forecast = new AirConditioningRoomTemperatureForecast(app, 'ac-1', {
|
|
27
|
+
* source: EnyoSourceEnum.Device,
|
|
28
|
+
* config: { roomIndex: 0 },
|
|
29
|
+
* });
|
|
30
|
+
* await forecast.initialize();
|
|
31
|
+
* const result = forecast.getForecast();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
class AirConditioningRoomTemperatureForecast {
|
|
35
|
+
app;
|
|
36
|
+
applianceId;
|
|
37
|
+
/** Default history window. Room temperature cycles are weekday-cyclic. */
|
|
38
|
+
static DEFAULT_HISTORY_DAYS = 7;
|
|
39
|
+
config;
|
|
40
|
+
history = [];
|
|
41
|
+
roomIndex;
|
|
42
|
+
listenerId;
|
|
43
|
+
currentSlotMs;
|
|
44
|
+
currentSlotTempSum = 0;
|
|
45
|
+
currentSlotSamples = 0;
|
|
46
|
+
initialized = false;
|
|
47
|
+
source;
|
|
48
|
+
/**
|
|
49
|
+
* @param app - The energy-app SDK instance.
|
|
50
|
+
* @param applianceId - Air conditioning appliance ID.
|
|
51
|
+
* @param options.source - Source identifier used for outgoing forecast messages.
|
|
52
|
+
* @param options.config - Optional config overrides; `historyDays` defaults to 7.
|
|
53
|
+
*/
|
|
54
|
+
constructor(app, applianceId, options) {
|
|
55
|
+
this.app = app;
|
|
56
|
+
this.applianceId = applianceId;
|
|
57
|
+
this.config = (0, forecast_types_js_1.resolveForecastConfig)({ historyDays: AirConditioningRoomTemperatureForecast.DEFAULT_HISTORY_DAYS }, options.config);
|
|
58
|
+
this.source = options.source;
|
|
59
|
+
this.roomIndex = options.config?.roomIndex;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pulls the air conditioning temperature timeseries history and starts
|
|
63
|
+
* listening to live `AirConditioningTemperaturesUpdateV1` events.
|
|
64
|
+
* Idempotent.
|
|
65
|
+
*/
|
|
66
|
+
async initialize() {
|
|
67
|
+
if (this.initialized)
|
|
68
|
+
return;
|
|
69
|
+
this.initialized = true;
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const startMs = now - this.config.historyDays * 24 * 60 * 60 * 1000;
|
|
72
|
+
const response = await this.app.useTimeseries().getAirConditioningTemperatureTimeseries({
|
|
73
|
+
startDateIso: new Date(startMs).toISOString(),
|
|
74
|
+
endDateIso: new Date(now).toISOString(),
|
|
75
|
+
applianceIds: [this.applianceId],
|
|
76
|
+
resolution: this.config.resolution,
|
|
77
|
+
});
|
|
78
|
+
for (const entry of response.entries) {
|
|
79
|
+
const temperature = this.extractHistoricalTemperature(entry.rooms);
|
|
80
|
+
if (temperature === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
this.history.push({
|
|
83
|
+
timestampIso: entry.timestampIso,
|
|
84
|
+
values: { temperatureC: temperature, samples: 1 },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
this.listenerId = this.app.useDataBus().listenForMessages([enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.AirConditioningTemperaturesUpdateV1], (msg) => this.onAirConditioningTemperatures(msg));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns the current forecast as an
|
|
91
|
+
* `EnyoDataBusAirConditioningRoomTemperatureForecastV1`-shaped payload over
|
|
92
|
+
* the configured horizon.
|
|
93
|
+
*/
|
|
94
|
+
getForecast() {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
this.flushCurrentSlot(now);
|
|
97
|
+
const parsed = (0, forecast_utils_js_1.parseHistory)(this.history);
|
|
98
|
+
const todayWeekday = new Date(now).getUTCDay();
|
|
99
|
+
const sameWeekdayMap = (0, forecast_utils_js_1.buildSlotMap)(parsed, (b) => b.weekday === todayWeekday);
|
|
100
|
+
const fallbackMap = (0, forecast_utils_js_1.buildSlotMap)(parsed);
|
|
101
|
+
const isoIndex = (0, forecast_utils_js_1.buildIsoIndex)(parsed, (v) => v.temperatureC);
|
|
102
|
+
const buckets = (0, forecast_utils_js_1.buildForecastSlots)(now, this.config.horizonHours, this.config.resolution).map((slotMs) => {
|
|
103
|
+
const key = formatSlotKey(slotMs);
|
|
104
|
+
const slotBuckets = sameWeekdayMap.get(key) ?? fallbackMap.get(key);
|
|
105
|
+
const avg = (0, forecast_utils_js_1.weightedAverageBySlot)(slotBuckets, now, this.config.historyDays, {
|
|
106
|
+
temperatureC: (v) => v.temperatureC,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
timestampIso: new Date(slotMs).toISOString(),
|
|
110
|
+
payload: { temperatureC: avg.temperatureC },
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
if (this.config.alignToRecentActuals) {
|
|
114
|
+
(0, forecast_utils_js_1.alignForecastToRecentActuals)(buckets, (iso) => isoIndex.get(iso), 'temperatureC', ['temperatureC']);
|
|
115
|
+
}
|
|
116
|
+
const entries = buckets.map((b) => ({
|
|
117
|
+
timestampIso: b.timestampIso,
|
|
118
|
+
temperatureC: round1(b.payload.temperatureC),
|
|
119
|
+
}));
|
|
120
|
+
const result = {
|
|
121
|
+
generatedAtIso: new Date(now).toISOString(),
|
|
122
|
+
data: {
|
|
123
|
+
resolution: this.config.resolution,
|
|
124
|
+
...(this.roomIndex !== undefined ? { roomIndex: this.roomIndex } : {}),
|
|
125
|
+
entries,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
if (this.config.publishToBus) {
|
|
129
|
+
this.publish(result);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Computes and explicitly publishes the current forecast on the data bus.
|
|
135
|
+
*/
|
|
136
|
+
publishForecast() {
|
|
137
|
+
const result = this.getForecast();
|
|
138
|
+
if (!this.config.publishToBus) {
|
|
139
|
+
this.publish(result);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detaches data-bus listeners and clears in-memory history.
|
|
144
|
+
*/
|
|
145
|
+
dispose() {
|
|
146
|
+
if (this.listenerId) {
|
|
147
|
+
this.app.useDataBus().unsubscribe(this.listenerId);
|
|
148
|
+
this.listenerId = undefined;
|
|
149
|
+
}
|
|
150
|
+
this.history.length = 0;
|
|
151
|
+
this.initialized = false;
|
|
152
|
+
}
|
|
153
|
+
onAirConditioningTemperatures(message) {
|
|
154
|
+
if (message.applianceId !== this.applianceId)
|
|
155
|
+
return;
|
|
156
|
+
const rooms = message.data.rooms;
|
|
157
|
+
if (!rooms || rooms.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
const temperature = this.extractLiveTemperature(rooms);
|
|
160
|
+
if (temperature === undefined)
|
|
161
|
+
return;
|
|
162
|
+
const ts = new Date(message.timestampIso).getTime();
|
|
163
|
+
const slotMs = (0, forecast_utils_js_1.roundDownTo15Minutes)(ts);
|
|
164
|
+
if (this.currentSlotMs === undefined || slotMs !== this.currentSlotMs) {
|
|
165
|
+
this.flushCurrentSlot(ts);
|
|
166
|
+
this.currentSlotMs = slotMs;
|
|
167
|
+
this.currentSlotTempSum = 0;
|
|
168
|
+
this.currentSlotSamples = 0;
|
|
169
|
+
}
|
|
170
|
+
this.currentSlotTempSum += temperature;
|
|
171
|
+
this.currentSlotSamples += 1;
|
|
172
|
+
}
|
|
173
|
+
flushCurrentSlot(nowMs) {
|
|
174
|
+
if (this.currentSlotMs === undefined || this.currentSlotSamples === 0)
|
|
175
|
+
return;
|
|
176
|
+
const avg = this.currentSlotTempSum / this.currentSlotSamples;
|
|
177
|
+
(0, forecast_utils_js_1.upsertLiveBucket)(this.history, this.currentSlotMs, { temperatureC: avg, samples: this.currentSlotSamples }, (existing, incoming) => {
|
|
178
|
+
const total = existing.samples + incoming.samples;
|
|
179
|
+
return {
|
|
180
|
+
temperatureC: (existing.temperatureC * existing.samples + incoming.temperatureC * incoming.samples) / total,
|
|
181
|
+
samples: total,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
if (this.currentSlotMs < (0, forecast_utils_js_1.roundDownTo15Minutes)(nowMs)) {
|
|
185
|
+
this.currentSlotMs = undefined;
|
|
186
|
+
this.currentSlotTempSum = 0;
|
|
187
|
+
this.currentSlotSamples = 0;
|
|
188
|
+
}
|
|
189
|
+
(0, forecast_utils_js_1.trimHistory)(this.history, nowMs, this.config.historyDays * 24 * 60 * 60 * 1000 + forecast_utils_js_1.FIFTEEN_MIN_MS);
|
|
190
|
+
}
|
|
191
|
+
extractHistoricalTemperature(rooms) {
|
|
192
|
+
if (!rooms || rooms.length === 0)
|
|
193
|
+
return undefined;
|
|
194
|
+
const matching = this.roomIndex !== undefined
|
|
195
|
+
? rooms.filter((r) => r.index === this.roomIndex)
|
|
196
|
+
: rooms;
|
|
197
|
+
if (matching.length === 0)
|
|
198
|
+
return undefined;
|
|
199
|
+
const sum = matching.reduce((acc, r) => acc + r.averageTemperatureC, 0);
|
|
200
|
+
return sum / matching.length;
|
|
201
|
+
}
|
|
202
|
+
extractLiveTemperature(rooms) {
|
|
203
|
+
const matching = this.roomIndex !== undefined
|
|
204
|
+
? rooms.filter((r) => r.index === this.roomIndex)
|
|
205
|
+
: rooms;
|
|
206
|
+
if (matching.length === 0)
|
|
207
|
+
return undefined;
|
|
208
|
+
const sum = matching.reduce((acc, r) => acc + r.temperatureC, 0);
|
|
209
|
+
return sum / matching.length;
|
|
210
|
+
}
|
|
211
|
+
publish(result) {
|
|
212
|
+
const message = {
|
|
213
|
+
id: globalThis.crypto.randomUUID(),
|
|
214
|
+
type: 'message',
|
|
215
|
+
message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.AirConditioningRoomTemperatureForecastV1,
|
|
216
|
+
source: this.source,
|
|
217
|
+
applianceId: this.applianceId,
|
|
218
|
+
timestampIso: result.generatedAtIso,
|
|
219
|
+
resolution: 'dynamic',
|
|
220
|
+
data: result.data,
|
|
221
|
+
};
|
|
222
|
+
this.app.useDataBus().sendMessage([message]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
exports.AirConditioningRoomTemperatureForecast = AirConditioningRoomTemperatureForecast;
|
|
226
|
+
function round1(value) {
|
|
227
|
+
return Math.round(value * 10) / 10;
|
|
228
|
+
}
|
|
229
|
+
function formatSlotKey(slotMs) {
|
|
230
|
+
const d = new Date(slotMs);
|
|
231
|
+
return `${d.getUTCHours().toString().padStart(2, '0')}:${d.getUTCMinutes()
|
|
232
|
+
.toString()
|
|
233
|
+
.padStart(2, '0')}`;
|
|
234
|
+
}
|