hoffmation-base 3.8.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/action/index.d.ts +1 -0
- package/lib/action/index.js +3 -1
- package/lib/action/soilSensorChangeAction.d.ts +16 -0
- package/lib/action/soilSensorChangeAction.js +15 -0
- package/lib/devices/dachs/dachs.d.ts +79 -2
- package/lib/devices/dachs/dachs.js +214 -18
- package/lib/devices/device-cluster.js +7 -0
- package/lib/devices/devices.d.ts +36 -1
- package/lib/devices/devices.js +67 -0
- package/lib/devices/jsObject/jsObjectEnergyManager.d.ts +16 -1
- package/lib/devices/jsObject/jsObjectEnergyManager.js +23 -0
- package/lib/devices/sharedFunctions/index.d.ts +1 -0
- package/lib/devices/sharedFunctions/index.js +1 -0
- package/lib/devices/sharedFunctions/soilSensor.d.ts +23 -0
- package/lib/devices/sharedFunctions/soilSensor.js +58 -0
- package/lib/devices/victron/victron-device.d.ts +83 -1
- package/lib/devices/victron/victron-device.js +151 -2
- package/lib/devices/victron/victron-morning-release.d.ts +16 -0
- package/lib/devices/victron/victron-morning-release.js +2 -0
- package/lib/devices/zigbee/BaseDevices/index.d.ts +1 -0
- package/lib/devices/zigbee/BaseDevices/index.js +1 -0
- package/lib/devices/zigbee/BaseDevices/zigbeeSoilSensor.d.ts +33 -0
- package/lib/devices/zigbee/BaseDevices/zigbeeSoilSensor.js +62 -0
- package/lib/devices/zigbee/index.d.ts +1 -0
- package/lib/devices/zigbee/index.js +1 -0
- package/lib/devices/zigbee/zigbeeCooloSoilSensor.d.ts +44 -0
- package/lib/devices/zigbee/zigbeeCooloSoilSensor.js +81 -0
- package/lib/enums/DeviceCapability.d.ts +2 -0
- package/lib/enums/DeviceCapability.js +2 -0
- package/lib/enums/commandType.d.ts +1 -0
- package/lib/enums/commandType.js +1 -0
- package/lib/enums/device-cluster-type.d.ts +2 -1
- package/lib/enums/device-cluster-type.js +1 -0
- package/lib/enums/deviceType.d.ts +1 -0
- package/lib/enums/deviceType.js +1 -0
- package/lib/interfaces/baseDevices/iAirQualityCollector.d.ts +2 -1
- package/lib/interfaces/baseDevices/iAirQualityReadings.d.ts +50 -0
- package/lib/interfaces/baseDevices/iAirQualityReadings.js +2 -0
- package/lib/interfaces/baseDevices/iAirQualitySensor.d.ts +1 -54
- package/lib/interfaces/baseDevices/iAirQualitySensor.js +0 -5
- package/lib/interfaces/baseDevices/iEnergyManager.d.ts +43 -0
- package/lib/interfaces/baseDevices/iHumidityCollector.d.ts +0 -1
- package/lib/interfaces/baseDevices/iHumidityCollector.js +0 -2
- package/lib/interfaces/baseDevices/iSoilCollector.d.ts +23 -0
- package/lib/interfaces/baseDevices/iSoilCollector.js +2 -0
- package/lib/interfaces/baseDevices/iSoilSensor.d.ts +32 -0
- package/lib/interfaces/baseDevices/iSoilSensor.js +2 -0
- package/lib/interfaces/baseDevices/iTemperatureCollector.d.ts +0 -1
- package/lib/interfaces/baseDevices/iTemperatureCollector.js +0 -2
- package/lib/interfaces/baseDevices/index.d.ts +7 -0
- package/lib/interfaces/baseDevices/index.js +7 -0
- package/lib/interfaces/baseDevices/undefinedAirQualityValue.d.ts +4 -0
- package/lib/interfaces/baseDevices/undefinedAirQualityValue.js +7 -0
- package/lib/interfaces/baseDevices/undefinedHumidityValue.d.ts +4 -0
- package/lib/interfaces/baseDevices/undefinedHumidityValue.js +7 -0
- package/lib/interfaces/baseDevices/undefinedSoilMoistureValue.d.ts +6 -0
- package/lib/interfaces/baseDevices/undefinedSoilMoistureValue.js +9 -0
- package/lib/interfaces/baseDevices/undefinedTempValue.d.ts +4 -0
- package/lib/interfaces/baseDevices/undefinedTempValue.js +7 -0
- package/lib/interfaces/dachs/KeyListEntity.d.ts +0 -5
- package/lib/interfaces/dachs/KeyListEntityResponse.d.ts +6 -0
- package/lib/interfaces/dachs/KeyListEntityResponse.js +2 -0
- package/lib/interfaces/dachs/index.d.ts +1 -0
- package/lib/interfaces/dachs/index.js +1 -0
- package/lib/interfaces/deviceSettings/iDachsDeviceSettings.d.ts +15 -0
- package/lib/interfaces/iActuatorStateSample.d.ts +13 -0
- package/lib/interfaces/iActuatorStateSample.js +2 -0
- package/lib/interfaces/iBatteryLevelSample.d.ts +13 -0
- package/lib/interfaces/iBatteryLevelSample.js +2 -0
- package/lib/interfaces/iConsumptionWindowSample.d.ts +26 -0
- package/lib/interfaces/iConsumptionWindowSample.js +2 -0
- package/lib/interfaces/iDachsHistoryGateResult.d.ts +29 -0
- package/lib/interfaces/iDachsHistoryGateResult.js +2 -0
- package/lib/interfaces/iEnergyHistoryBasis.d.ts +26 -0
- package/lib/interfaces/iEnergyHistoryBasis.js +2 -0
- package/lib/interfaces/iEnergyHistoryEstimate.d.ts +21 -0
- package/lib/interfaces/iEnergyHistoryEstimate.js +2 -0
- package/lib/interfaces/iEnergyHistoryFeatures.d.ts +21 -0
- package/lib/interfaces/iEnergyHistoryFeatures.js +2 -0
- package/lib/interfaces/iEnergyHistoryModel.d.ts +21 -0
- package/lib/interfaces/iEnergyHistoryModel.js +2 -0
- package/lib/interfaces/iEnergyHistoryOptions.d.ts +29 -0
- package/lib/interfaces/iEnergyHistoryOptions.js +2 -0
- package/lib/interfaces/iEnergyHistoryOutlook.d.ts +41 -0
- package/lib/interfaces/iEnergyHistoryOutlook.js +2 -0
- package/lib/interfaces/iEnergyHistorySample.d.ts +19 -0
- package/lib/interfaces/iEnergyHistorySample.js +2 -0
- package/lib/interfaces/iFossilGeneratorRun.d.ts +22 -0
- package/lib/interfaces/iFossilGeneratorRun.js +2 -0
- package/lib/interfaces/iFossilGeneratorSource.d.ts +21 -0
- package/lib/interfaces/iFossilGeneratorSource.js +2 -0
- package/lib/interfaces/iMorningReserveVerdict.d.ts +38 -0
- package/lib/interfaces/iMorningReserveVerdict.js +2 -0
- package/lib/interfaces/iPersist.d.ts +69 -1
- package/lib/interfaces/iProjectedSocBand.d.ts +7 -0
- package/lib/interfaces/iProjectedSocBand.js +2 -0
- package/lib/interfaces/iSoilMoistureSample.d.ts +13 -0
- package/lib/interfaces/iSoilMoistureSample.js +2 -0
- package/lib/interfaces/iWeatherDaySummary.d.ts +33 -0
- package/lib/interfaces/iWeatherDaySummary.js +2 -0
- package/lib/interfaces/index.d.ts +17 -0
- package/lib/interfaces/index.js +17 -0
- package/lib/services/dbo/actuator-state-row.d.ts +14 -0
- package/lib/services/dbo/actuator-state-row.js +2 -0
- package/lib/services/dbo/battery-level-row.d.ts +16 -0
- package/lib/services/dbo/battery-level-row.js +2 -0
- package/lib/services/dbo/energy-consumption-row.d.ts +16 -0
- package/lib/services/dbo/energy-consumption-row.js +2 -0
- package/lib/services/dbo/postgreSqlPersist.d.ts +52 -1
- package/lib/services/dbo/postgreSqlPersist.js +392 -27
- package/lib/services/dbo/soil-moisture-row.d.ts +14 -0
- package/lib/services/dbo/soil-moisture-row.js +2 -0
- package/lib/services/dbo/weather-day-summary-row.d.ts +20 -0
- package/lib/services/dbo/weather-day-summary-row.js +2 -0
- package/lib/services/energy/energy-history-service.d.ts +168 -0
- package/lib/services/energy/energy-history-service.js +499 -0
- package/lib/services/energy/index.d.ts +1 -0
- package/lib/services/energy/index.js +17 -0
- package/lib/services/energy/plant-energy-dials.d.ts +22 -0
- package/lib/services/energy/plant-energy-dials.js +2 -0
- package/lib/services/https-service.d.ts +48 -2
- package/lib/services/https-service.js +136 -10
- package/lib/services/index.d.ts +1 -0
- package/lib/services/index.js +1 -0
- package/lib/services/time-callback-service.d.ts +12 -0
- package/lib/services/time-callback-service.js +16 -2
- package/lib/services/weather/index.d.ts +1 -0
- package/lib/services/weather/index.js +1 -0
- package/lib/services/weather/open-weather-day-summary.d.ts +25 -0
- package/lib/services/weather/open-weather-day-summary.js +2 -0
- package/lib/services/weather/weather-day-summary-fetcher.d.ts +6 -0
- package/lib/services/weather/weather-day-summary-fetcher.js +2 -0
- package/lib/services/weather/weather-history-backfill.d.ts +94 -0
- package/lib/services/weather/weather-history-backfill.js +247 -0
- package/lib/services/weather/weather-service.js +29 -4
- package/lib/settings-service-instance.js +5 -2
- package/lib/settingsObjects/deviceSettings/dachsSettings.d.ts +6 -0
- package/lib/settingsObjects/deviceSettings/dachsSettings.js +14 -1
- package/lib/settingsObjects/deviceSettings/victronDeviceSettings.d.ts +116 -0
- package/lib/settingsObjects/deviceSettings/victronDeviceSettings.js +150 -5
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/dayMs.d.ts +7 -0
- package/lib/utils/dayMs.js +10 -0
- package/lib/utils/energy-history-dials.d.ts +19 -0
- package/lib/utils/energy-history-dials.js +2 -0
- package/lib/utils/energy-history-utils.d.ts +143 -0
- package/lib/utils/energy-history-utils.js +470 -0
- package/lib/utils/energy-manager-utils.d.ts +123 -1
- package/lib/utils/energy-manager-utils.js +226 -0
- package/lib/utils/index.d.ts +4 -0
- package/lib/utils/index.js +4 -0
- package/lib/utils/model-shadow.d.ts +61 -0
- package/lib/utils/model-shadow.js +83 -0
- package/lib/utils/morning-reserve-dials.d.ts +21 -0
- package/lib/utils/morning-reserve-dials.js +2 -0
- package/lib/utils/morning-verdict.d.ts +5 -0
- package/lib/utils/morning-verdict.js +2 -0
- package/lib/utils/utils.d.ts +0 -1
- package/lib/utils/utils.js +5 -5
- package/package.json +3 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { iEnergyHistoryOptions, iEnergyHistoryOutlook } from '../../interfaces';
|
|
2
|
+
import { LogLevel } from '../../enums';
|
|
3
|
+
/**
|
|
4
|
+
* What the plant's own recorded history says about the night ahead: where the state of charge will bottom out
|
|
5
|
+
* towards the coming morning, and how certain that is.
|
|
6
|
+
*
|
|
7
|
+
* **Passive** - no timer of its own, so read, backfill and fit happen only inside a {@link refresh} call and
|
|
8
|
+
* an owner that does not ask spends no request quota. **One instance for the whole plant**, held by the energy
|
|
9
|
+
* manager: a second would pay the bounded weather backfill twice a day and split the model shadow into two
|
|
10
|
+
* half samples of a measurement that only means anything whole.
|
|
11
|
+
*/
|
|
12
|
+
export declare class EnergyHistoryService {
|
|
13
|
+
private readonly options;
|
|
14
|
+
private readonly log;
|
|
15
|
+
/** One entry per historical day: what the house consumed from the evaluation moment to the morning low. */
|
|
16
|
+
private _consumptionSamples;
|
|
17
|
+
/** Whether the last read answered with consumption readings at all, so an empty result can be explained. */
|
|
18
|
+
private _consumptionReadingsSeen;
|
|
19
|
+
/** The stored daily weather aggregate of the running day. */
|
|
20
|
+
private _weatherToday;
|
|
21
|
+
/** The consumption readings of the running day. */
|
|
22
|
+
private _consumptionToday;
|
|
23
|
+
private _consumptionTodayAttemptedAt;
|
|
24
|
+
private _consumptionTodayRunning;
|
|
25
|
+
private _historyModel;
|
|
26
|
+
/** When the window was last read - set on the attempt, so a failed read waits just as long as a good one. */
|
|
27
|
+
private _historyAttemptedAt;
|
|
28
|
+
private _historyLoadRunning;
|
|
29
|
+
private _weatherBackfillAttemptedAt;
|
|
30
|
+
private _weatherBackfillRunning;
|
|
31
|
+
/** The reason there currently is no data basis, kept so it is logged on change instead of every run. */
|
|
32
|
+
private _historyIssue;
|
|
33
|
+
/**
|
|
34
|
+
* The generators already reported as carrying no runtime, so the change is reported rather than the state -
|
|
35
|
+
* a unit that legitimately stands still for a season would otherwise produce a line an hour for months.
|
|
36
|
+
*/
|
|
37
|
+
private readonly _generatorsWithoutRuntime;
|
|
38
|
+
/**
|
|
39
|
+
* Builds the plant's history. Nothing is read until {@link refresh} is called.
|
|
40
|
+
* @param options - How the recorded history is read and fitted, read anew on every use; hand in a live view
|
|
41
|
+
* when they are editable at runtime. How good the data has to be is read off the manager, see
|
|
42
|
+
* {@link iPlantEnergyDials}.
|
|
43
|
+
* @param log - Where to write to; handed in so the lines appear under the asking device.
|
|
44
|
+
*/
|
|
45
|
+
constructor(options: iEnergyHistoryOptions, log: (level: LogLevel, message: string) => void);
|
|
46
|
+
/**
|
|
47
|
+
* What the plant can say about the coming morning low, from what was read so far. Pure arithmetic over the
|
|
48
|
+
* cached reads - it never reaches for the persistence itself, that is {@link refresh}.
|
|
49
|
+
* @param currentSoc - The state of charge in percent the projection starts from.
|
|
50
|
+
* @param moment - The moment the outlook is asked about.
|
|
51
|
+
* @returns The outlook, including which of its inputs were missing.
|
|
52
|
+
*/
|
|
53
|
+
outlook(currentSoc: number, moment: Date): iEnergyHistoryOutlook;
|
|
54
|
+
/**
|
|
55
|
+
* Reads everything that has gone stale: the daily weather aggregates, the consumption of the running day and
|
|
56
|
+
* the sliding history window. Each of the three throttles itself, so calling this on every decision is what
|
|
57
|
+
* it is built for.
|
|
58
|
+
*/
|
|
59
|
+
refresh(): void;
|
|
60
|
+
private historyReadThrottled;
|
|
61
|
+
/**
|
|
62
|
+
* How much the house consumed since midnight of the day of the given moment.
|
|
63
|
+
* @param moment - The moment to count up to.
|
|
64
|
+
* @returns The consumption in kWh, or undefined while no reading of the running day is present.
|
|
65
|
+
*/
|
|
66
|
+
private consumedTodayKwh;
|
|
67
|
+
/**
|
|
68
|
+
* The dials of the plant, read anew on each use: a manager whose settings are edited at runtime must not be
|
|
69
|
+
* answered on the values of the moment of construction.
|
|
70
|
+
* @returns The manager's settings through the fields this service reads, or undefined without a manager.
|
|
71
|
+
*/
|
|
72
|
+
private get plantDials();
|
|
73
|
+
/**
|
|
74
|
+
* The usable capacity of the battery.
|
|
75
|
+
* @returns The capacity in watt hours, or undefined while no energy manager reports a usable one - an
|
|
76
|
+
* energy manager that does not know its battery is a missing input, not a battery of size zero.
|
|
77
|
+
*/
|
|
78
|
+
private get batteryCapacityWattHours();
|
|
79
|
+
private get minimumDayCoverage();
|
|
80
|
+
private get consumptionQuantile();
|
|
81
|
+
private get minimumConsumptionDays();
|
|
82
|
+
private remainingSunHoursAt;
|
|
83
|
+
/**
|
|
84
|
+
* The end of the window the next morning's low is looked for in: the next sunrise after the given moment
|
|
85
|
+
* plus a buffer hour. Read through the same service the sunset of {@link remainingSunHoursAt} comes from -
|
|
86
|
+
* the two bound one and the same window, and two libraries put their sunrise and their sunset minutes apart.
|
|
87
|
+
* @param moment - The evaluation moment.
|
|
88
|
+
* @returns The end of the window in milliseconds.
|
|
89
|
+
*/
|
|
90
|
+
private morningLowWindowEnd;
|
|
91
|
+
/**
|
|
92
|
+
* Adds up the consumption readings that fall into the given window.
|
|
93
|
+
* @param samples - The readings to add up.
|
|
94
|
+
* @param fromMs - The start of the window.
|
|
95
|
+
* @param toMs - The end of the window.
|
|
96
|
+
* @returns The consumed energy in kWh, or undefined while the window holds no reading at all - an absent
|
|
97
|
+
* reading must not read as "nothing consumed".
|
|
98
|
+
*/
|
|
99
|
+
private static consumedWithin;
|
|
100
|
+
/**
|
|
101
|
+
* Fills the gaps in the stored daily weather aggregates. Without them two of the four quantities of a
|
|
102
|
+
* historical day are missing and the model side never gets a basis.
|
|
103
|
+
*
|
|
104
|
+
* The trap: this does **not** sequence a write before the read that follows it in {@link refresh} - the run
|
|
105
|
+
* is started and not awaited, so the history read always sees the row of an earlier run. The running day's
|
|
106
|
+
* aggregate is therefore up to one interval old and the model side stays silent for the first interval after
|
|
107
|
+
* a start, both of which are the safe direction: a missing aggregate means no statement.
|
|
108
|
+
* @param source - The persistence the fetched days are handed to; established by the caller.
|
|
109
|
+
*/
|
|
110
|
+
private refreshWeatherHistory;
|
|
111
|
+
/**
|
|
112
|
+
* Reads the consumption of the running day on its own, far more often than the window. Served from the
|
|
113
|
+
* hourly window cache the running day would show systematically less than the historical samples it is
|
|
114
|
+
* compared against, and the model would read that as a quiet day and lean towards suppressing; a single day
|
|
115
|
+
* is cheap enough to repeat at the decision's pace.
|
|
116
|
+
* @param source - The persistence to read from; established by the caller.
|
|
117
|
+
*/
|
|
118
|
+
private refreshTodayConsumption;
|
|
119
|
+
/**
|
|
120
|
+
* Forgets everything that was read so far, so a lost data basis can never carry an old answer along.
|
|
121
|
+
* @param reason - Why there is no data basis; logged once per change, not once per evaluation.
|
|
122
|
+
*/
|
|
123
|
+
private dropHistory;
|
|
124
|
+
/**
|
|
125
|
+
* The evaluation moments of the sliding window, oldest first: the same time of day as the reference on
|
|
126
|
+
* each of the preceding days.
|
|
127
|
+
* @param reference - The running evaluation moment.
|
|
128
|
+
* @param windowDays - Length of the window in days.
|
|
129
|
+
* @returns One moment per day of the window.
|
|
130
|
+
*/
|
|
131
|
+
private static historyMoments;
|
|
132
|
+
/**
|
|
133
|
+
* The recorded state changes of one generator, with a rejected read confined to that one generator: one
|
|
134
|
+
* unreachable generator must not cost the charge levels, the consumption windows and the weather of the
|
|
135
|
+
* whole window as well - the same judgement per entry {@link EnergyHistoryUtils.correctForFossilGeneration}
|
|
136
|
+
* makes.
|
|
137
|
+
* @param source - The persistence to read from.
|
|
138
|
+
* @param actuatorId - The generator whose state changes are asked for.
|
|
139
|
+
* @param start - Start of the history window.
|
|
140
|
+
* @param end - End of the history window.
|
|
141
|
+
* @param failedReads - Collects the generators whose read was rejected, so the absent runtime that follows
|
|
142
|
+
* is not reported a second time under its own heading.
|
|
143
|
+
* @returns The recorded state changes, or an empty list when the read was rejected.
|
|
144
|
+
*/
|
|
145
|
+
private readGeneratorHistory;
|
|
146
|
+
/**
|
|
147
|
+
* Names every generator of the plant the window holds no runtime for. Nothing is subtracted for such a
|
|
148
|
+
* generator, so the day keeps a share the photovoltaic never produced, the bound comes out too optimistic
|
|
149
|
+
* and the gate suppresses a start the house needed - the direction without a way back, and invisible to an
|
|
150
|
+
* operator. Zero runtime cannot be told from a generator that truly stood still, so the line names how many
|
|
151
|
+
* state changes were recorded at all instead of claiming it can.
|
|
152
|
+
* @param generators - The generators of the plant.
|
|
153
|
+
* @param statesByActuator - What each of them has recorded over the window.
|
|
154
|
+
* @param failedReads - The generators whose read was rejected; their absent runtime is already explained.
|
|
155
|
+
* @param start - Start of the history window.
|
|
156
|
+
* @param end - End of the history window.
|
|
157
|
+
*/
|
|
158
|
+
private reportGeneratorsWithoutRuntime;
|
|
159
|
+
/**
|
|
160
|
+
* Reads the sliding window from the persistence and turns it into one sample per historical day, each taken
|
|
161
|
+
* at the same time of day as the running evaluation, and fits the weights from them. The consumption windows
|
|
162
|
+
* are collected independently of the weather aggregates, because the model free bound has to carry from the
|
|
163
|
+
* first day with or without a backfilled weather history.
|
|
164
|
+
* @param source - The persistence to read from.
|
|
165
|
+
* @param capacityWattHours - The battery capacity the generator share is converted into state of charge with.
|
|
166
|
+
*/
|
|
167
|
+
private loadHistory;
|
|
168
|
+
}
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EnergyHistoryService = void 0;
|
|
4
|
+
const enums_1 = require("../../enums");
|
|
5
|
+
const utils_1 = require("../../utils");
|
|
6
|
+
const devices_1 = require("../../devices");
|
|
7
|
+
const dbo_1 = require("../dbo");
|
|
8
|
+
const time_callback_service_1 = require("../time-callback-service");
|
|
9
|
+
const weather_1 = require("../weather");
|
|
10
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
12
|
+
/** How long after sunrise the morning low is still counted. */
|
|
13
|
+
const MORNING_LOW_BUFFER_MS = HOUR_MS;
|
|
14
|
+
/** How long a history read is reused before the persistence is asked again - a failed one included. */
|
|
15
|
+
const HISTORY_RELOAD_INTERVAL_MS = HOUR_MS;
|
|
16
|
+
/** How long the consumption of the running day is reused - see {@link refreshTodayConsumption}. */
|
|
17
|
+
const CONSUMPTION_RELOAD_INTERVAL_MS = 5 * 60 * 1000;
|
|
18
|
+
/**
|
|
19
|
+
* How long between two backfill runs of the daily weather aggregates. Shorter than the one day the window
|
|
20
|
+
* moves by, because the running day's row is a forecast rewritten as the day goes on and is read as a
|
|
21
|
+
* feature; days already stored are skipped, so the shorter interval costs at most one call.
|
|
22
|
+
*/
|
|
23
|
+
const WEATHER_BACKFILL_INTERVAL_MS = HOUR_MS;
|
|
24
|
+
/**
|
|
25
|
+
* Length of one persisted consumption interval, so the expected number of readings per window follows from the
|
|
26
|
+
* window rather than from the data. Not a setting but a property of this code: both energy managers write
|
|
27
|
+
* their calculation on this interval, and changing their database logger interval changes this one too.
|
|
28
|
+
*/
|
|
29
|
+
const CONSUMPTION_READING_INTERVAL_MS = 15 * 60 * 1000;
|
|
30
|
+
/** The quality bar while no energy manager states one; unlike the battery capacity, all three have one. */
|
|
31
|
+
const DEFAULT_MINIMUM_DAY_COVERAGE = 0.9;
|
|
32
|
+
const DEFAULT_CONSUMPTION_QUANTILE = 0.9;
|
|
33
|
+
const DEFAULT_MINIMUM_CONSUMPTION_DAYS = 10;
|
|
34
|
+
/**
|
|
35
|
+
* What the plant's own recorded history says about the night ahead: where the state of charge will bottom out
|
|
36
|
+
* towards the coming morning, and how certain that is.
|
|
37
|
+
*
|
|
38
|
+
* **Passive** - no timer of its own, so read, backfill and fit happen only inside a {@link refresh} call and
|
|
39
|
+
* an owner that does not ask spends no request quota. **One instance for the whole plant**, held by the energy
|
|
40
|
+
* manager: a second would pay the bounded weather backfill twice a day and split the model shadow into two
|
|
41
|
+
* half samples of a measurement that only means anything whole.
|
|
42
|
+
*/
|
|
43
|
+
class EnergyHistoryService {
|
|
44
|
+
/**
|
|
45
|
+
* Builds the plant's history. Nothing is read until {@link refresh} is called.
|
|
46
|
+
* @param options - How the recorded history is read and fitted, read anew on every use; hand in a live view
|
|
47
|
+
* when they are editable at runtime. How good the data has to be is read off the manager, see
|
|
48
|
+
* {@link iPlantEnergyDials}.
|
|
49
|
+
* @param log - Where to write to; handed in so the lines appear under the asking device.
|
|
50
|
+
*/
|
|
51
|
+
constructor(options, log) {
|
|
52
|
+
/** Whether the last read answered with consumption readings at all, so an empty result can be explained. */
|
|
53
|
+
this._consumptionReadingsSeen = false;
|
|
54
|
+
this._consumptionTodayAttemptedAt = 0;
|
|
55
|
+
this._consumptionTodayRunning = false;
|
|
56
|
+
/** When the window was last read - set on the attempt, so a failed read waits just as long as a good one. */
|
|
57
|
+
this._historyAttemptedAt = 0;
|
|
58
|
+
this._historyLoadRunning = false;
|
|
59
|
+
this._weatherBackfillAttemptedAt = 0;
|
|
60
|
+
this._weatherBackfillRunning = false;
|
|
61
|
+
/**
|
|
62
|
+
* The generators already reported as carrying no runtime, so the change is reported rather than the state -
|
|
63
|
+
* a unit that legitimately stands still for a season would otherwise produce a line an hour for months.
|
|
64
|
+
*/
|
|
65
|
+
this._generatorsWithoutRuntime = new Set();
|
|
66
|
+
this.options = options;
|
|
67
|
+
this.log = log;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* What the plant can say about the coming morning low, from what was read so far. Pure arithmetic over the
|
|
71
|
+
* cached reads - it never reaches for the persistence itself, that is {@link refresh}.
|
|
72
|
+
* @param currentSoc - The state of charge in percent the projection starts from.
|
|
73
|
+
* @param moment - The moment the outlook is asked about.
|
|
74
|
+
* @returns The outlook, including which of its inputs were missing.
|
|
75
|
+
*/
|
|
76
|
+
outlook(currentSoc, moment) {
|
|
77
|
+
var _a, _b;
|
|
78
|
+
const remainingSunHours = this.remainingSunHoursAt(moment);
|
|
79
|
+
const model = this._historyModel;
|
|
80
|
+
const windowSums = (_a = this._consumptionSamples) !== null && _a !== void 0 ? _a : [];
|
|
81
|
+
// Without a capacity there is no conversion between kWh and charge points, so the bound is left out rather
|
|
82
|
+
// than calculated as zero - a zero bound reads as "the morning is empty".
|
|
83
|
+
const capacityWattHours = this.batteryCapacityWattHours;
|
|
84
|
+
const consumptionKwh = capacityWattHours === undefined
|
|
85
|
+
? undefined
|
|
86
|
+
: utils_1.EnergyHistoryUtils.consumptionQuantileKwh(windowSums, this.consumptionQuantile, this.minimumConsumptionDays);
|
|
87
|
+
const worstCaseLowSoc = consumptionKwh === undefined || capacityWattHours === undefined
|
|
88
|
+
? undefined
|
|
89
|
+
: utils_1.EnergyHistoryUtils.worstCaseLowSoc(currentSoc, consumptionKwh, capacityWattHours);
|
|
90
|
+
// Both weather quantities come out of the same field of the same table the historical samples are built
|
|
91
|
+
// from. Read anywhere else they would be a different quantity with the same unit, and a weight measured on
|
|
92
|
+
// one quantity and applied to another is a guessed weight.
|
|
93
|
+
const today = this._weatherToday;
|
|
94
|
+
const consumedSoFarKwh = this.consumedTodayKwh(moment);
|
|
95
|
+
const features = today === undefined || consumedSoFarKwh === undefined
|
|
96
|
+
? undefined
|
|
97
|
+
: {
|
|
98
|
+
remainingSunHours,
|
|
99
|
+
cloudCover: today.cloudCover,
|
|
100
|
+
consumedSoFarKwh,
|
|
101
|
+
maxTemperature: today.tempMax,
|
|
102
|
+
};
|
|
103
|
+
let band = undefined;
|
|
104
|
+
if (model !== undefined && features !== undefined) {
|
|
105
|
+
const estimate = utils_1.EnergyHistoryUtils.estimate(model, features, this.options.bandSigma);
|
|
106
|
+
band = { lower: currentSoc + estimate.lowerEdgeDelta, upper: currentSoc + estimate.upperEdgeDelta };
|
|
107
|
+
}
|
|
108
|
+
const basis = {
|
|
109
|
+
batteryCapacityKnown: capacityWattHours !== undefined,
|
|
110
|
+
consumptionWindows: windowSums.length,
|
|
111
|
+
requiredConsumptionWindows: this.minimumConsumptionDays,
|
|
112
|
+
consumptionReadingsSeen: this._consumptionReadingsSeen,
|
|
113
|
+
weatherTodayKnown: today !== undefined,
|
|
114
|
+
consumptionTodayKnown: consumedSoFarKwh !== undefined,
|
|
115
|
+
modelFitted: model !== undefined,
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
currentSoc,
|
|
119
|
+
remainingSunHours,
|
|
120
|
+
worstCaseLowSoc,
|
|
121
|
+
band,
|
|
122
|
+
residualSigma: model === null || model === void 0 ? void 0 : model.residualSigma,
|
|
123
|
+
sampleDays: (_b = model === null || model === void 0 ? void 0 : model.sampleDays) !== null && _b !== void 0 ? _b : 0,
|
|
124
|
+
basis,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Reads everything that has gone stale: the daily weather aggregates, the consumption of the running day and
|
|
129
|
+
* the sliding history window. Each of the three throttles itself, so calling this on every decision is what
|
|
130
|
+
* it is built for.
|
|
131
|
+
*/
|
|
132
|
+
refresh() {
|
|
133
|
+
// These two guards are the whole protection of the request quota; there is no switch in front of any of
|
|
134
|
+
// this. While either is missing the service answers nothing, so a day fetched meanwhile is paid for
|
|
135
|
+
// nothing. Capacity first: it is also what the generator share of a historical day is removed with, and a
|
|
136
|
+
// fit on never corrected days is a failure that looks like a model.
|
|
137
|
+
const capacityWattHours = this.batteryCapacityWattHours;
|
|
138
|
+
if (capacityWattHours === undefined) {
|
|
139
|
+
this.dropHistory('the energy manager reports no usable battery capacity');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Read *before* the three reads below: the backfill fetches days in order to store them, so without a
|
|
143
|
+
// persistence it spends quota for rows that have nowhere to land. The give-up runs on the history
|
|
144
|
+
// interval, because dropping and reporting per pass would be a storm of its own.
|
|
145
|
+
const source = dbo_1.Persistence.dbo;
|
|
146
|
+
if (source === undefined) {
|
|
147
|
+
if (!this.historyReadThrottled()) {
|
|
148
|
+
this._historyAttemptedAt = utils_1.Utils.nowMS();
|
|
149
|
+
this.dropHistory('there is no persistence layer to read the history from');
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.refreshWeatherHistory(source);
|
|
154
|
+
this.refreshTodayConsumption(source);
|
|
155
|
+
if (this._historyLoadRunning || this.historyReadThrottled()) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this._historyAttemptedAt = utils_1.Utils.nowMS();
|
|
159
|
+
this._historyLoadRunning = true;
|
|
160
|
+
this.loadHistory(source, capacityWattHours)
|
|
161
|
+
.catch((error) => {
|
|
162
|
+
this.dropHistory(`reading the history failed: ${error.message}`);
|
|
163
|
+
})
|
|
164
|
+
.finally(() => {
|
|
165
|
+
this._historyLoadRunning = false;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
historyReadThrottled() {
|
|
169
|
+
return this._historyAttemptedAt > 0 && utils_1.Utils.nowMS() - this._historyAttemptedAt < HISTORY_RELOAD_INTERVAL_MS;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* How much the house consumed since midnight of the day of the given moment.
|
|
173
|
+
* @param moment - The moment to count up to.
|
|
174
|
+
* @returns The consumption in kWh, or undefined while no reading of the running day is present.
|
|
175
|
+
*/
|
|
176
|
+
consumedTodayKwh(moment) {
|
|
177
|
+
var _a;
|
|
178
|
+
return EnergyHistoryService.consumedWithin((_a = this._consumptionToday) !== null && _a !== void 0 ? _a : [], new Date(moment).setHours(0, 0, 0, 0), moment.getTime());
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The dials of the plant, read anew on each use: a manager whose settings are edited at runtime must not be
|
|
182
|
+
* answered on the values of the moment of construction.
|
|
183
|
+
* @returns The manager's settings through the fields this service reads, or undefined without a manager.
|
|
184
|
+
*/
|
|
185
|
+
get plantDials() {
|
|
186
|
+
var _a;
|
|
187
|
+
return (_a = devices_1.Devices.energymanager) === null || _a === void 0 ? void 0 : _a.settings;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The usable capacity of the battery.
|
|
191
|
+
* @returns The capacity in watt hours, or undefined while no energy manager reports a usable one - an
|
|
192
|
+
* energy manager that does not know its battery is a missing input, not a battery of size zero.
|
|
193
|
+
*/
|
|
194
|
+
get batteryCapacityWattHours() {
|
|
195
|
+
var _a;
|
|
196
|
+
const capacity = (_a = this.plantDials) === null || _a === void 0 ? void 0 : _a.batteryCapacityWattage;
|
|
197
|
+
return capacity !== undefined && Number.isFinite(capacity) && capacity > 0 ? capacity : undefined;
|
|
198
|
+
}
|
|
199
|
+
// What each of the three means is stated once, on iPlantEnergyDials and on the manager's settings.
|
|
200
|
+
get minimumDayCoverage() {
|
|
201
|
+
var _a, _b;
|
|
202
|
+
return (_b = (_a = this.plantDials) === null || _a === void 0 ? void 0 : _a.historyMinimumDayCoverage) !== null && _b !== void 0 ? _b : DEFAULT_MINIMUM_DAY_COVERAGE;
|
|
203
|
+
}
|
|
204
|
+
get consumptionQuantile() {
|
|
205
|
+
var _a, _b;
|
|
206
|
+
return (_b = (_a = this.plantDials) === null || _a === void 0 ? void 0 : _a.historyConsumptionQuantile) !== null && _b !== void 0 ? _b : DEFAULT_CONSUMPTION_QUANTILE;
|
|
207
|
+
}
|
|
208
|
+
get minimumConsumptionDays() {
|
|
209
|
+
var _a, _b;
|
|
210
|
+
return (_b = (_a = this.plantDials) === null || _a === void 0 ? void 0 : _a.historyMinimumConsumptionDays) !== null && _b !== void 0 ? _b : DEFAULT_MINIMUM_CONSUMPTION_DAYS;
|
|
211
|
+
}
|
|
212
|
+
remainingSunHoursAt(moment) {
|
|
213
|
+
const sunset = time_callback_service_1.TimeCallbackService.getSunsetForDate(moment);
|
|
214
|
+
return Math.max(0, (sunset.getTime() - moment.getTime()) / HOUR_MS);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The end of the window the next morning's low is looked for in: the next sunrise after the given moment
|
|
218
|
+
* plus a buffer hour. Read through the same service the sunset of {@link remainingSunHoursAt} comes from -
|
|
219
|
+
* the two bound one and the same window, and two libraries put their sunrise and their sunset minutes apart.
|
|
220
|
+
* @param moment - The evaluation moment.
|
|
221
|
+
* @returns The end of the window in milliseconds.
|
|
222
|
+
*/
|
|
223
|
+
morningLowWindowEnd(moment) {
|
|
224
|
+
let sunrise = time_callback_service_1.TimeCallbackService.getSunriseForDate(moment);
|
|
225
|
+
if (sunrise.getTime() <= moment.getTime()) {
|
|
226
|
+
sunrise = time_callback_service_1.TimeCallbackService.getSunriseForDate(new Date(moment.getTime() + DAY_MS));
|
|
227
|
+
}
|
|
228
|
+
return sunrise.getTime() + MORNING_LOW_BUFFER_MS;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Adds up the consumption readings that fall into the given window.
|
|
232
|
+
* @param samples - The readings to add up.
|
|
233
|
+
* @param fromMs - The start of the window.
|
|
234
|
+
* @param toMs - The end of the window.
|
|
235
|
+
* @returns The consumed energy in kWh, or undefined while the window holds no reading at all - an absent
|
|
236
|
+
* reading must not read as "nothing consumed".
|
|
237
|
+
*/
|
|
238
|
+
static consumedWithin(samples, fromMs, toMs) {
|
|
239
|
+
let sum = 0;
|
|
240
|
+
let found = false;
|
|
241
|
+
for (const sample of samples) {
|
|
242
|
+
const time = sample.date.getTime();
|
|
243
|
+
if (time <= fromMs || time > toMs) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
sum += sample.consumedKwh;
|
|
247
|
+
found = true;
|
|
248
|
+
}
|
|
249
|
+
return found ? sum : undefined;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Fills the gaps in the stored daily weather aggregates. Without them two of the four quantities of a
|
|
253
|
+
* historical day are missing and the model side never gets a basis.
|
|
254
|
+
*
|
|
255
|
+
* The trap: this does **not** sequence a write before the read that follows it in {@link refresh} - the run
|
|
256
|
+
* is started and not awaited, so the history read always sees the row of an earlier run. The running day's
|
|
257
|
+
* aggregate is therefore up to one interval old and the model side stays silent for the first interval after
|
|
258
|
+
* a start, both of which are the safe direction: a missing aggregate means no statement.
|
|
259
|
+
* @param source - The persistence the fetched days are handed to; established by the caller.
|
|
260
|
+
*/
|
|
261
|
+
refreshWeatherHistory(source) {
|
|
262
|
+
if (this._weatherBackfillRunning) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (this._weatherBackfillAttemptedAt > 0 &&
|
|
266
|
+
utils_1.Utils.nowMS() - this._weatherBackfillAttemptedAt < WEATHER_BACKFILL_INTERVAL_MS) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this._weatherBackfillAttemptedAt = utils_1.Utils.nowMS();
|
|
270
|
+
this._weatherBackfillRunning = true;
|
|
271
|
+
weather_1.WeatherHistoryBackfill.run(source, new Date(), this.options.windowDays)
|
|
272
|
+
.then((handedOver) => {
|
|
273
|
+
if (handedOver > 0) {
|
|
274
|
+
// "handed to" rather than "backfilled": the backfill cannot see whether a row arrived, so a line
|
|
275
|
+
// claiming a stored count would read as confirmation of something nobody checked. Frozen wording -
|
|
276
|
+
// operators grep and alert on this exact text, "history gate" in a plant wide service included.
|
|
277
|
+
this.log(enums_1.LogLevel.Info, `Handed ${handedOver} daily weather aggregate(s) to the persistence for the history gate`);
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
.catch((error) => {
|
|
281
|
+
this.log(enums_1.LogLevel.Warn, `Backfilling the daily weather aggregates failed: ${error.message}`);
|
|
282
|
+
})
|
|
283
|
+
.finally(() => {
|
|
284
|
+
this._weatherBackfillRunning = false;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Reads the consumption of the running day on its own, far more often than the window. Served from the
|
|
289
|
+
* hourly window cache the running day would show systematically less than the historical samples it is
|
|
290
|
+
* compared against, and the model would read that as a quiet day and lean towards suppressing; a single day
|
|
291
|
+
* is cheap enough to repeat at the decision's pace.
|
|
292
|
+
* @param source - The persistence to read from; established by the caller.
|
|
293
|
+
*/
|
|
294
|
+
refreshTodayConsumption(source) {
|
|
295
|
+
if (this._consumptionTodayRunning) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (this._consumptionTodayAttemptedAt > 0 &&
|
|
299
|
+
utils_1.Utils.nowMS() - this._consumptionTodayAttemptedAt < CONSUMPTION_RELOAD_INTERVAL_MS) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this._consumptionTodayAttemptedAt = utils_1.Utils.nowMS();
|
|
303
|
+
this._consumptionTodayRunning = true;
|
|
304
|
+
const now = new Date();
|
|
305
|
+
source
|
|
306
|
+
.getEnergyConsumptionHistory(new Date(new Date(now).setHours(0, 0, 0, 0)), now)
|
|
307
|
+
.then((readings) => {
|
|
308
|
+
this._consumptionToday = readings;
|
|
309
|
+
})
|
|
310
|
+
.catch((error) => {
|
|
311
|
+
this._consumptionToday = undefined;
|
|
312
|
+
this.log(enums_1.LogLevel.Warn, `Reading the consumption of the running day failed: ${error.message}`);
|
|
313
|
+
})
|
|
314
|
+
.finally(() => {
|
|
315
|
+
this._consumptionTodayRunning = false;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Forgets everything that was read so far, so a lost data basis can never carry an old answer along.
|
|
320
|
+
* @param reason - Why there is no data basis; logged once per change, not once per evaluation.
|
|
321
|
+
*/
|
|
322
|
+
dropHistory(reason) {
|
|
323
|
+
this._consumptionSamples = undefined;
|
|
324
|
+
this._consumptionReadingsSeen = false;
|
|
325
|
+
this._weatherToday = undefined;
|
|
326
|
+
this._historyModel = undefined;
|
|
327
|
+
// Dropped here too: this runs in front of the read that would otherwise clear it, so nothing else does.
|
|
328
|
+
this._consumptionToday = undefined;
|
|
329
|
+
if (this._historyIssue !== reason) {
|
|
330
|
+
this._historyIssue = reason;
|
|
331
|
+
// Frozen wording, same reason as the backfill line above: operators grep for this exact text.
|
|
332
|
+
this.log(enums_1.LogLevel.Info, `History gate has no data basis: ${reason}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The evaluation moments of the sliding window, oldest first: the same time of day as the reference on
|
|
337
|
+
* each of the preceding days.
|
|
338
|
+
* @param reference - The running evaluation moment.
|
|
339
|
+
* @param windowDays - Length of the window in days.
|
|
340
|
+
* @returns One moment per day of the window.
|
|
341
|
+
*/
|
|
342
|
+
static historyMoments(reference, windowDays) {
|
|
343
|
+
const moments = [];
|
|
344
|
+
for (let offset = windowDays; offset >= 1; offset--) {
|
|
345
|
+
const moment = new Date(reference);
|
|
346
|
+
// Calendar arithmetic, not milliseconds: over a window of months there is always a daylight saving
|
|
347
|
+
// change, and a day subtracted as milliseconds lands an hour off the running time of day beyond it.
|
|
348
|
+
moment.setDate(moment.getDate() - offset);
|
|
349
|
+
moments.push(moment);
|
|
350
|
+
}
|
|
351
|
+
return moments;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The recorded state changes of one generator, with a rejected read confined to that one generator: one
|
|
355
|
+
* unreachable generator must not cost the charge levels, the consumption windows and the weather of the
|
|
356
|
+
* whole window as well - the same judgement per entry {@link EnergyHistoryUtils.correctForFossilGeneration}
|
|
357
|
+
* makes.
|
|
358
|
+
* @param source - The persistence to read from.
|
|
359
|
+
* @param actuatorId - The generator whose state changes are asked for.
|
|
360
|
+
* @param start - Start of the history window.
|
|
361
|
+
* @param end - End of the history window.
|
|
362
|
+
* @param failedReads - Collects the generators whose read was rejected, so the absent runtime that follows
|
|
363
|
+
* is not reported a second time under its own heading.
|
|
364
|
+
* @returns The recorded state changes, or an empty list when the read was rejected.
|
|
365
|
+
*/
|
|
366
|
+
readGeneratorHistory(source, actuatorId, start, end, failedReads) {
|
|
367
|
+
return source.getActuatorHistory(actuatorId, start, end).catch((error) => {
|
|
368
|
+
failedReads.add(actuatorId);
|
|
369
|
+
// Not throttled beyond the hourly read: a read that keeps failing is a fault, and a fault that stops
|
|
370
|
+
// being said stops being fixed.
|
|
371
|
+
this.log(enums_1.LogLevel.Warn, `Reading the recorded state changes of generator '${actuatorId}' failed, so nothing is subtracted for ` +
|
|
372
|
+
`it and the photovoltaic looks better than it was: ${error.message}`);
|
|
373
|
+
return [];
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Names every generator of the plant the window holds no runtime for. Nothing is subtracted for such a
|
|
378
|
+
* generator, so the day keeps a share the photovoltaic never produced, the bound comes out too optimistic
|
|
379
|
+
* and the gate suppresses a start the house needed - the direction without a way back, and invisible to an
|
|
380
|
+
* operator. Zero runtime cannot be told from a generator that truly stood still, so the line names how many
|
|
381
|
+
* state changes were recorded at all instead of claiming it can.
|
|
382
|
+
* @param generators - The generators of the plant.
|
|
383
|
+
* @param statesByActuator - What each of them has recorded over the window.
|
|
384
|
+
* @param failedReads - The generators whose read was rejected; their absent runtime is already explained.
|
|
385
|
+
* @param start - Start of the history window.
|
|
386
|
+
* @param end - End of the history window.
|
|
387
|
+
*/
|
|
388
|
+
reportGeneratorsWithoutRuntime(generators, statesByActuator, failedReads, start, end) {
|
|
389
|
+
var _a;
|
|
390
|
+
for (const generator of generators) {
|
|
391
|
+
if (failedReads.has(generator.actuatorId)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const samples = (_a = statesByActuator.get(generator.actuatorId)) !== null && _a !== void 0 ? _a : [];
|
|
395
|
+
if (utils_1.EnergyHistoryUtils.onMillisecondsWithin(samples, start.getTime(), end.getTime()) > 0) {
|
|
396
|
+
this._generatorsWithoutRuntime.delete(generator.actuatorId);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (this._generatorsWithoutRuntime.has(generator.actuatorId)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
this._generatorsWithoutRuntime.add(generator.actuatorId);
|
|
403
|
+
this.log(enums_1.LogLevel.Warn, `Generator '${generator.actuatorId}' has no runtime recorded over the ${this.options.windowDays} day ` +
|
|
404
|
+
`history window (${samples.length} recorded state change(s)), so nothing is subtracted for it and the ` +
|
|
405
|
+
'photovoltaic looks better than it was');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Reads the sliding window from the persistence and turns it into one sample per historical day, each taken
|
|
410
|
+
* at the same time of day as the running evaluation, and fits the weights from them. The consumption windows
|
|
411
|
+
* are collected independently of the weather aggregates, because the model free bound has to carry from the
|
|
412
|
+
* first day with or without a backfilled weather history.
|
|
413
|
+
* @param source - The persistence to read from.
|
|
414
|
+
* @param capacityWattHours - The battery capacity the generator share is converted into state of charge with.
|
|
415
|
+
*/
|
|
416
|
+
async loadHistory(source, capacityWattHours) {
|
|
417
|
+
var _a;
|
|
418
|
+
const now = new Date();
|
|
419
|
+
// Ids to query with here, ratings to correct with further down - read separately, because a generator
|
|
420
|
+
// constructed while this read is in flight makes the two lists legitimately differ, and because the rating
|
|
421
|
+
// is editable at runtime and the correction describes the generator as it is configured now.
|
|
422
|
+
const queriedIds = devices_1.Devices.fossilGenerators.map((generator) => generator.actuatorId);
|
|
423
|
+
const moments = EnergyHistoryService.historyMoments(now, this.options.windowDays);
|
|
424
|
+
const start = (_a = moments[0]) !== null && _a !== void 0 ? _a : now;
|
|
425
|
+
// The weather aggregates are dated at local midnight while the evaluation moments carry the running time
|
|
426
|
+
// of day, so an unwidened start would lie behind the oldest day's row and cost that day its feature row -
|
|
427
|
+
// one day per window, every window. Only this read is widened; the timestamped ones would gain a partial
|
|
428
|
+
// day in the sums they feed.
|
|
429
|
+
const weatherStart = new Date(new Date(start).setHours(0, 0, 0, 0));
|
|
430
|
+
const failedGeneratorReads = new Set();
|
|
431
|
+
const [levels, generatorStates, weather, consumption] = await Promise.all([
|
|
432
|
+
source.getBatteryLevelHistory(start, now),
|
|
433
|
+
Promise.all(queriedIds.map((actuatorId) => this.readGeneratorHistory(source, actuatorId, start, now, failedGeneratorReads))),
|
|
434
|
+
source.getWeatherDaySummaries(weatherStart, now),
|
|
435
|
+
source.getEnergyConsumptionHistory(start, now),
|
|
436
|
+
]);
|
|
437
|
+
// Keyed by actuator, not by position: a read takes long enough for the generator list to have been
|
|
438
|
+
// edited meanwhile, and matching by index would then hand one generator another one's run times.
|
|
439
|
+
const statesByActuator = new Map(queriedIds.map((actuatorId, index) => [actuatorId, generatorStates[index]]));
|
|
440
|
+
const generators = devices_1.Devices.fossilGenerators;
|
|
441
|
+
this.reportGeneratorsWithoutRuntime(generators, statesByActuator, failedGeneratorReads, start, now);
|
|
442
|
+
// One sum per historical day over the same time of day window the running evaluation faces. A day whose
|
|
443
|
+
// readings do not cover that window is dropped rather than patched: its sum would look like a frugal night
|
|
444
|
+
// and make the bound too optimistic - the suppressing direction. Shortly after sunrise the horizon reaches
|
|
445
|
+
// past a calendar day and the windows overlap, which is admissible: both really did contain the shared
|
|
446
|
+
// stretch, while shortening the window would understate a night and suppress more.
|
|
447
|
+
const consumptionWindows = utils_1.EnergyHistoryUtils.windowConsumptionSums(consumption, now.getTime(), this.morningLowWindowEnd(now), CONSUMPTION_READING_INTERVAL_MS, this.minimumDayCoverage);
|
|
448
|
+
const samples = [];
|
|
449
|
+
// Iterating over the window rather than over what was answered is what makes the window a window and not
|
|
450
|
+
// a request: a persistence that answers more generously than asked cannot widen the fit.
|
|
451
|
+
for (const moment of moments) {
|
|
452
|
+
const windowEnd = this.morningLowWindowEnd(moment);
|
|
453
|
+
const rawDelta = utils_1.EnergyHistoryUtils.deltaToNextMorningLow(levels, moment.getTime(), windowEnd);
|
|
454
|
+
if (rawDelta === undefined) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const day = weather.find((entry) => entry.date.toDateString() === moment.toDateString());
|
|
458
|
+
const consumedSoFarKwh = EnergyHistoryService.consumedWithin(consumption, new Date(moment).setHours(0, 0, 0, 0), moment.getTime());
|
|
459
|
+
if (day === undefined || consumedSoFarKwh === undefined) {
|
|
460
|
+
// Only the model side needs all four quantities; no substitute value is invented.
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
// Counted to the end of the window rather than to the low point itself, and a generator added after the
|
|
464
|
+
// read carries no runtime at all - both can only make the photovoltaic look worse, the harmless
|
|
465
|
+
// direction.
|
|
466
|
+
const runs = generators.map((generator) => {
|
|
467
|
+
var _a;
|
|
468
|
+
return ({
|
|
469
|
+
runMilliseconds: utils_1.EnergyHistoryUtils.onMillisecondsWithin((_a = statesByActuator.get(generator.actuatorId)) !== null && _a !== void 0 ? _a : [], moment.getTime(), windowEnd),
|
|
470
|
+
ratedElectricalWattage: generator.ratedElectricalWattage,
|
|
471
|
+
conversionFactor: generator.conversionFactor,
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
samples.push({
|
|
475
|
+
features: {
|
|
476
|
+
remainingSunHours: this.remainingSunHoursAt(moment),
|
|
477
|
+
cloudCover: day.cloudCover,
|
|
478
|
+
consumedSoFarKwh,
|
|
479
|
+
maxTemperature: day.tempMax,
|
|
480
|
+
},
|
|
481
|
+
observedDelta: utils_1.EnergyHistoryUtils.correctForFossilGeneration(rawDelta, runs, capacityWattHours),
|
|
482
|
+
date: moment,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (samples.length === 0 && consumptionWindows.length === 0) {
|
|
486
|
+
this.dropHistory('the history window holds no usable day');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this._consumptionSamples = consumptionWindows;
|
|
490
|
+
// Out of the same answer as the historical ones, so both sides of the model read one field of one table.
|
|
491
|
+
this._weatherToday = weather.find((entry) => entry.date.toDateString() === now.toDateString());
|
|
492
|
+
// Kept apart from the window sums: "no readings at all" and "readings that no window could be formed from"
|
|
493
|
+
// are different states and must read differently.
|
|
494
|
+
this._consumptionReadingsSeen = consumption.length > 0;
|
|
495
|
+
this._historyModel = utils_1.EnergyHistoryUtils.fit(samples, this.options.minimumModelDays);
|
|
496
|
+
this._historyIssue = undefined;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
exports.EnergyHistoryService = EnergyHistoryService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './energy-history-service';
|