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.
Files changed (160) hide show
  1. package/lib/action/index.d.ts +1 -0
  2. package/lib/action/index.js +3 -1
  3. package/lib/action/soilSensorChangeAction.d.ts +16 -0
  4. package/lib/action/soilSensorChangeAction.js +15 -0
  5. package/lib/devices/dachs/dachs.d.ts +79 -2
  6. package/lib/devices/dachs/dachs.js +214 -18
  7. package/lib/devices/device-cluster.js +7 -0
  8. package/lib/devices/devices.d.ts +36 -1
  9. package/lib/devices/devices.js +67 -0
  10. package/lib/devices/jsObject/jsObjectEnergyManager.d.ts +16 -1
  11. package/lib/devices/jsObject/jsObjectEnergyManager.js +23 -0
  12. package/lib/devices/sharedFunctions/index.d.ts +1 -0
  13. package/lib/devices/sharedFunctions/index.js +1 -0
  14. package/lib/devices/sharedFunctions/soilSensor.d.ts +23 -0
  15. package/lib/devices/sharedFunctions/soilSensor.js +58 -0
  16. package/lib/devices/victron/victron-device.d.ts +83 -1
  17. package/lib/devices/victron/victron-device.js +151 -2
  18. package/lib/devices/victron/victron-morning-release.d.ts +16 -0
  19. package/lib/devices/victron/victron-morning-release.js +2 -0
  20. package/lib/devices/zigbee/BaseDevices/index.d.ts +1 -0
  21. package/lib/devices/zigbee/BaseDevices/index.js +1 -0
  22. package/lib/devices/zigbee/BaseDevices/zigbeeSoilSensor.d.ts +33 -0
  23. package/lib/devices/zigbee/BaseDevices/zigbeeSoilSensor.js +62 -0
  24. package/lib/devices/zigbee/index.d.ts +1 -0
  25. package/lib/devices/zigbee/index.js +1 -0
  26. package/lib/devices/zigbee/zigbeeCooloSoilSensor.d.ts +44 -0
  27. package/lib/devices/zigbee/zigbeeCooloSoilSensor.js +81 -0
  28. package/lib/enums/DeviceCapability.d.ts +2 -0
  29. package/lib/enums/DeviceCapability.js +2 -0
  30. package/lib/enums/commandType.d.ts +1 -0
  31. package/lib/enums/commandType.js +1 -0
  32. package/lib/enums/device-cluster-type.d.ts +2 -1
  33. package/lib/enums/device-cluster-type.js +1 -0
  34. package/lib/enums/deviceType.d.ts +1 -0
  35. package/lib/enums/deviceType.js +1 -0
  36. package/lib/interfaces/baseDevices/iAirQualityCollector.d.ts +2 -1
  37. package/lib/interfaces/baseDevices/iAirQualityReadings.d.ts +50 -0
  38. package/lib/interfaces/baseDevices/iAirQualityReadings.js +2 -0
  39. package/lib/interfaces/baseDevices/iAirQualitySensor.d.ts +1 -54
  40. package/lib/interfaces/baseDevices/iAirQualitySensor.js +0 -5
  41. package/lib/interfaces/baseDevices/iEnergyManager.d.ts +43 -0
  42. package/lib/interfaces/baseDevices/iHumidityCollector.d.ts +0 -1
  43. package/lib/interfaces/baseDevices/iHumidityCollector.js +0 -2
  44. package/lib/interfaces/baseDevices/iSoilCollector.d.ts +23 -0
  45. package/lib/interfaces/baseDevices/iSoilCollector.js +2 -0
  46. package/lib/interfaces/baseDevices/iSoilSensor.d.ts +32 -0
  47. package/lib/interfaces/baseDevices/iSoilSensor.js +2 -0
  48. package/lib/interfaces/baseDevices/iTemperatureCollector.d.ts +0 -1
  49. package/lib/interfaces/baseDevices/iTemperatureCollector.js +0 -2
  50. package/lib/interfaces/baseDevices/index.d.ts +7 -0
  51. package/lib/interfaces/baseDevices/index.js +7 -0
  52. package/lib/interfaces/baseDevices/undefinedAirQualityValue.d.ts +4 -0
  53. package/lib/interfaces/baseDevices/undefinedAirQualityValue.js +7 -0
  54. package/lib/interfaces/baseDevices/undefinedHumidityValue.d.ts +4 -0
  55. package/lib/interfaces/baseDevices/undefinedHumidityValue.js +7 -0
  56. package/lib/interfaces/baseDevices/undefinedSoilMoistureValue.d.ts +6 -0
  57. package/lib/interfaces/baseDevices/undefinedSoilMoistureValue.js +9 -0
  58. package/lib/interfaces/baseDevices/undefinedTempValue.d.ts +4 -0
  59. package/lib/interfaces/baseDevices/undefinedTempValue.js +7 -0
  60. package/lib/interfaces/dachs/KeyListEntity.d.ts +0 -5
  61. package/lib/interfaces/dachs/KeyListEntityResponse.d.ts +6 -0
  62. package/lib/interfaces/dachs/KeyListEntityResponse.js +2 -0
  63. package/lib/interfaces/dachs/index.d.ts +1 -0
  64. package/lib/interfaces/dachs/index.js +1 -0
  65. package/lib/interfaces/deviceSettings/iDachsDeviceSettings.d.ts +15 -0
  66. package/lib/interfaces/iActuatorStateSample.d.ts +13 -0
  67. package/lib/interfaces/iActuatorStateSample.js +2 -0
  68. package/lib/interfaces/iBatteryLevelSample.d.ts +13 -0
  69. package/lib/interfaces/iBatteryLevelSample.js +2 -0
  70. package/lib/interfaces/iConsumptionWindowSample.d.ts +26 -0
  71. package/lib/interfaces/iConsumptionWindowSample.js +2 -0
  72. package/lib/interfaces/iDachsHistoryGateResult.d.ts +29 -0
  73. package/lib/interfaces/iDachsHistoryGateResult.js +2 -0
  74. package/lib/interfaces/iEnergyHistoryBasis.d.ts +26 -0
  75. package/lib/interfaces/iEnergyHistoryBasis.js +2 -0
  76. package/lib/interfaces/iEnergyHistoryEstimate.d.ts +21 -0
  77. package/lib/interfaces/iEnergyHistoryEstimate.js +2 -0
  78. package/lib/interfaces/iEnergyHistoryFeatures.d.ts +21 -0
  79. package/lib/interfaces/iEnergyHistoryFeatures.js +2 -0
  80. package/lib/interfaces/iEnergyHistoryModel.d.ts +21 -0
  81. package/lib/interfaces/iEnergyHistoryModel.js +2 -0
  82. package/lib/interfaces/iEnergyHistoryOptions.d.ts +29 -0
  83. package/lib/interfaces/iEnergyHistoryOptions.js +2 -0
  84. package/lib/interfaces/iEnergyHistoryOutlook.d.ts +41 -0
  85. package/lib/interfaces/iEnergyHistoryOutlook.js +2 -0
  86. package/lib/interfaces/iEnergyHistorySample.d.ts +19 -0
  87. package/lib/interfaces/iEnergyHistorySample.js +2 -0
  88. package/lib/interfaces/iFossilGeneratorRun.d.ts +22 -0
  89. package/lib/interfaces/iFossilGeneratorRun.js +2 -0
  90. package/lib/interfaces/iFossilGeneratorSource.d.ts +21 -0
  91. package/lib/interfaces/iFossilGeneratorSource.js +2 -0
  92. package/lib/interfaces/iMorningReserveVerdict.d.ts +38 -0
  93. package/lib/interfaces/iMorningReserveVerdict.js +2 -0
  94. package/lib/interfaces/iPersist.d.ts +69 -1
  95. package/lib/interfaces/iProjectedSocBand.d.ts +7 -0
  96. package/lib/interfaces/iProjectedSocBand.js +2 -0
  97. package/lib/interfaces/iSoilMoistureSample.d.ts +13 -0
  98. package/lib/interfaces/iSoilMoistureSample.js +2 -0
  99. package/lib/interfaces/iWeatherDaySummary.d.ts +33 -0
  100. package/lib/interfaces/iWeatherDaySummary.js +2 -0
  101. package/lib/interfaces/index.d.ts +17 -0
  102. package/lib/interfaces/index.js +17 -0
  103. package/lib/services/dbo/actuator-state-row.d.ts +14 -0
  104. package/lib/services/dbo/actuator-state-row.js +2 -0
  105. package/lib/services/dbo/battery-level-row.d.ts +16 -0
  106. package/lib/services/dbo/battery-level-row.js +2 -0
  107. package/lib/services/dbo/energy-consumption-row.d.ts +16 -0
  108. package/lib/services/dbo/energy-consumption-row.js +2 -0
  109. package/lib/services/dbo/postgreSqlPersist.d.ts +52 -1
  110. package/lib/services/dbo/postgreSqlPersist.js +392 -27
  111. package/lib/services/dbo/soil-moisture-row.d.ts +14 -0
  112. package/lib/services/dbo/soil-moisture-row.js +2 -0
  113. package/lib/services/dbo/weather-day-summary-row.d.ts +20 -0
  114. package/lib/services/dbo/weather-day-summary-row.js +2 -0
  115. package/lib/services/energy/energy-history-service.d.ts +168 -0
  116. package/lib/services/energy/energy-history-service.js +499 -0
  117. package/lib/services/energy/index.d.ts +1 -0
  118. package/lib/services/energy/index.js +17 -0
  119. package/lib/services/energy/plant-energy-dials.d.ts +22 -0
  120. package/lib/services/energy/plant-energy-dials.js +2 -0
  121. package/lib/services/https-service.d.ts +48 -2
  122. package/lib/services/https-service.js +136 -10
  123. package/lib/services/index.d.ts +1 -0
  124. package/lib/services/index.js +1 -0
  125. package/lib/services/time-callback-service.d.ts +12 -0
  126. package/lib/services/time-callback-service.js +16 -2
  127. package/lib/services/weather/index.d.ts +1 -0
  128. package/lib/services/weather/index.js +1 -0
  129. package/lib/services/weather/open-weather-day-summary.d.ts +25 -0
  130. package/lib/services/weather/open-weather-day-summary.js +2 -0
  131. package/lib/services/weather/weather-day-summary-fetcher.d.ts +6 -0
  132. package/lib/services/weather/weather-day-summary-fetcher.js +2 -0
  133. package/lib/services/weather/weather-history-backfill.d.ts +94 -0
  134. package/lib/services/weather/weather-history-backfill.js +247 -0
  135. package/lib/services/weather/weather-service.js +29 -4
  136. package/lib/settings-service-instance.js +5 -2
  137. package/lib/settingsObjects/deviceSettings/dachsSettings.d.ts +6 -0
  138. package/lib/settingsObjects/deviceSettings/dachsSettings.js +14 -1
  139. package/lib/settingsObjects/deviceSettings/victronDeviceSettings.d.ts +116 -0
  140. package/lib/settingsObjects/deviceSettings/victronDeviceSettings.js +150 -5
  141. package/lib/tsconfig.tsbuildinfo +1 -1
  142. package/lib/utils/dayMs.d.ts +7 -0
  143. package/lib/utils/dayMs.js +10 -0
  144. package/lib/utils/energy-history-dials.d.ts +19 -0
  145. package/lib/utils/energy-history-dials.js +2 -0
  146. package/lib/utils/energy-history-utils.d.ts +143 -0
  147. package/lib/utils/energy-history-utils.js +470 -0
  148. package/lib/utils/energy-manager-utils.d.ts +123 -1
  149. package/lib/utils/energy-manager-utils.js +226 -0
  150. package/lib/utils/index.d.ts +4 -0
  151. package/lib/utils/index.js +4 -0
  152. package/lib/utils/model-shadow.d.ts +61 -0
  153. package/lib/utils/model-shadow.js +83 -0
  154. package/lib/utils/morning-reserve-dials.d.ts +21 -0
  155. package/lib/utils/morning-reserve-dials.js +2 -0
  156. package/lib/utils/morning-verdict.d.ts +5 -0
  157. package/lib/utils/morning-verdict.js +2 -0
  158. package/lib/utils/utils.d.ts +0 -1
  159. package/lib/utils/utils.js +5 -5
  160. package/package.json +3 -2
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WeatherHistoryBackfill = void 0;
4
+ const logging_1 = require("../../logging");
5
+ const enums_1 = require("../../enums");
6
+ const HTTPSOptions_1 = require("../HTTPSOptions");
7
+ const https_service_1 = require("../https-service");
8
+ const settings_service_1 = require("../../settings-service");
9
+ const utils_1 = require("../../utils");
10
+ /**
11
+ * Fills the gaps in the stored daily weather aggregates from the One Call 3.0 day summary endpoint.
12
+ *
13
+ * Its own service instead of a method on the weather service: that one serves the live forecast, while this
14
+ * one walks a window of past days and must stay bounded and throttled. The full recorded history is far larger
15
+ * than a day's request quota, so the fitting window is the hard limit and the throttle keeps an accidentally
16
+ * widened window from draining the quota in one burst.
17
+ */
18
+ class WeatherHistoryBackfill {
19
+ /**
20
+ * Fetches the daily weather aggregates that are missing within the fitting window and stores them, plus the
21
+ * running day's aggregate on every run. Past days that are already stored are not fetched again, so a
22
+ * service restart does not cost quota; the running day always is, because it is a forecast that moves. A past
23
+ * day that was already fetched on the running day is not fetched again until the next calendar day.
24
+ * @param persist - The persistence to read the present days from and to write the fetched ones to
25
+ * @param referenceDate - The running day; the window ends on it, inclusive
26
+ * @param historyWindowDays - Length of the history window in days; calls are bounded by it plus one
27
+ * @param fetcher - The fetcher to use, defaults to the configured One Call 3.0 day summary endpoint
28
+ * @param throttleMs - Milliseconds to wait between two calls
29
+ * @returns - The number of days that were fetched and handed to the persistence. Not the number that
30
+ * arrived: `persistWeatherDaySummary` reports nothing back, so whether a row landed cannot be seen from
31
+ * here. Do not read this as a count of stored rows - look in the table for that.
32
+ */
33
+ static async run(persist, referenceDate, historyWindowDays, fetcher, throttleMs = WeatherHistoryBackfill.defaultThrottleMs) {
34
+ if (persist === undefined) {
35
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Debug, 'WeatherHistoryBackfill: no persistence configured --> nothing to do');
36
+ return 0;
37
+ }
38
+ if (historyWindowDays < 1) {
39
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Debug, `WeatherHistoryBackfill: window of ${historyWindowDays} days --> nothing to do`);
40
+ return 0;
41
+ }
42
+ const activeFetcher = fetcher !== null && fetcher !== void 0 ? fetcher : WeatherHistoryBackfill.createOpenWeatherFetcher();
43
+ if (activeFetcher === undefined) {
44
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Warn, 'WeatherHistoryBackfill: no usable weather configuration --> skipped');
45
+ return 0;
46
+ }
47
+ const days = WeatherHistoryBackfill.windowDays(referenceDate, historyWindowDays);
48
+ const present = new Set((await persist.getWeatherDaySummaries(days[0], days[days.length - 1])).map((summary) => WeatherHistoryBackfill.dayKey(summary.date)));
49
+ // The last day of the window is the running one. Its aggregate is a forecast and moves through the day,
50
+ // so it is fetched on every run and the upsert overwrites it; a past day is archive and does not change,
51
+ // so a stored one is never paid for twice.
52
+ const runningDay = WeatherHistoryBackfill.dayKey(days[days.length - 1]);
53
+ if (WeatherHistoryBackfill.attemptedPastDaysRecordedOn !== runningDay) {
54
+ WeatherHistoryBackfill.resetAttemptedPastDays();
55
+ WeatherHistoryBackfill.attemptedPastDaysRecordedOn = runningDay;
56
+ }
57
+ let fetched = 0;
58
+ // Handed to the persistence, not confirmed as arrived - persistWeatherDaySummary reports nothing back.
59
+ let handedOver = 0;
60
+ let heldBack = 0;
61
+ for (const day of days) {
62
+ const key = WeatherHistoryBackfill.dayKey(day);
63
+ if (key !== runningDay && present.has(key)) {
64
+ continue;
65
+ }
66
+ if (key !== runningDay && WeatherHistoryBackfill.attemptedPastDays.has(key)) {
67
+ heldBack++;
68
+ continue;
69
+ }
70
+ if (fetched > 0 && throttleMs > 0) {
71
+ await utils_1.Utils.delay(throttleMs);
72
+ }
73
+ fetched++;
74
+ if (key !== runningDay) {
75
+ // Noted before the outcome is known, on purpose - see {@link attemptedPastDays}.
76
+ WeatherHistoryBackfill.attemptedPastDays.add(key);
77
+ }
78
+ const summary = await activeFetcher(day);
79
+ if (summary === undefined) {
80
+ continue;
81
+ }
82
+ persist.persistWeatherDaySummary(summary);
83
+ handedOver++;
84
+ }
85
+ logging_1.ServerLogService.writeLog(handedOver > 0 ? enums_1.LogLevel.Info : enums_1.LogLevel.Debug, `WeatherHistoryBackfill: handed ${handedOver} of ${fetched} fetched day(s) to the persistence within the last ` +
86
+ `${historyWindowDays} days plus today, ${heldBack} day(s) held back after an earlier fetch today`);
87
+ return handedOver;
88
+ }
89
+ /**
90
+ * Forgets which past days were already fetched, so the next run offers the whole window again. Called by
91
+ * `run` itself on a change of the running day; separate so a test can start from a known state.
92
+ */
93
+ static resetAttemptedPastDays() {
94
+ WeatherHistoryBackfill.attemptedPastDays.clear();
95
+ WeatherHistoryBackfill.attemptedPastDaysRecordedOn = undefined;
96
+ }
97
+ /**
98
+ * Builds the fetcher against the One Call 3.0 day summary endpoint.
99
+ * @param settings - The weather settings to use, defaults to the configured ones
100
+ * @returns - The fetcher, or `undefined` when there is no location or no key to work with
101
+ */
102
+ static createOpenWeatherFetcher(settings) {
103
+ var _a;
104
+ if (settings === void 0) { settings = (_a = settings_service_1.SettingsService.settings) === null || _a === void 0 ? void 0 : _a.weather; }
105
+ if (settings === undefined || !settings.appid || !settings.lattitude || !settings.longitude) {
106
+ return undefined;
107
+ }
108
+ const location = settings;
109
+ return (date) => WeatherHistoryBackfill.fetchDaySummary(location, date);
110
+ }
111
+ /**
112
+ * The days of the fitting window, oldest first, **including the running day**. The running day is in there
113
+ * on purpose: the gate reads today's cloud cover and maximum temperature out of the same table and through
114
+ * the same reader as the history, so that both sides of the fit are the same quantity from the same field.
115
+ * That is one extra call per run - the alternative was the gate deriving today's figures from a different
116
+ * product, which measurably is not the same number.
117
+ * @param referenceDate - The running day
118
+ * @param historyWindowDays - Length of the history window in days
119
+ * @returns - `historyWindowDays + 1` day starts, the last of which is the running day
120
+ */
121
+ static windowDays(referenceDate, historyWindowDays) {
122
+ const midnight = new Date(referenceDate);
123
+ midnight.setHours(0, 0, 0, 0);
124
+ const days = [];
125
+ for (let offset = historyWindowDays; offset >= 0; offset--) {
126
+ const day = new Date(midnight);
127
+ // Calendar arithmetic instead of subtracting milliseconds, so a daylight saving change does not shift
128
+ // every older day of the window by an hour.
129
+ day.setDate(day.getDate() - offset);
130
+ days.push(day);
131
+ }
132
+ return days;
133
+ }
134
+ static async fetchDaySummary(settings, date) {
135
+ const day = WeatherHistoryBackfill.dayKey(date);
136
+ return new Promise((resolve) => {
137
+ // HTTPSOptions logs the path it is constructed with, so it is constructed with a redacted one and the
138
+ // real query - which carries location and key - is set afterwards. The repository is public.
139
+ // Do NOT collapse these two into one constructor call: that puts the key into a log line. The guard is
140
+ // "keeps key and location out of the path HTTPSOptions is constructed with" in
141
+ // test/services/dachs-history-persistence.test.ts.
142
+ const options = new HTTPSOptions_1.HTTPSOptions(WeatherHistoryBackfill.host, `/data/3.0/onecall/day_summary (${day})`, {}, 'GET', 443);
143
+ options.path =
144
+ `/data/3.0/onecall/day_summary?lat=${settings.lattitude}&lon=${settings.longitude}` +
145
+ `&date=${day}&appid=${settings.appid}&units=metric`;
146
+ // This promise settles because the callback always comes: `HTTPSService.request` reports every way a
147
+ // request can end - an answer, an exhausted retry chain, a failing socket, an endpoint that accepts the
148
+ // connection and then stays silent - to its callback exactly once, and bounds each attempt by its own
149
+ // time limit. A failure arrives as `HTTPSService.failureStatusCode`, which is not 200 and is discarded
150
+ // below like any other unusable answer. Nothing here has to watch the clock as well; a second deadline
151
+ // would only be able to fire before the retry does and throw away the answer it was about to bring.
152
+ https_service_1.HTTPSService.request(options, '', 1, (response, statusCode) => {
153
+ resolve(WeatherHistoryBackfill.parseDaySummary(response, statusCode, date, day));
154
+ });
155
+ });
156
+ }
157
+ static parseDaySummary(response, statusCode, date, day) {
158
+ var _a, _b, _c, _d;
159
+ if (statusCode !== 200) {
160
+ // Neither the answer nor the request is logged: both carry the key and the location.
161
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Warn, `WeatherHistoryBackfill: day summary for ${day} answered ${statusCode}`);
162
+ return undefined;
163
+ }
164
+ const parsed = utils_1.Utils.guardedFunction(() => JSON.parse(response), undefined, `WeatherHistoryBackfill: unreadable day summary for ${day}`);
165
+ // Each leaf is established as a number in a plausible range here rather than trusted from the shape
166
+ // above. This is the boundary: what passes is written into the table the start decision is read from,
167
+ // and nothing between here and that decision looks at the values again.
168
+ const cloudCover = WeatherHistoryBackfill.plausibleReading((_a = parsed === null || parsed === void 0 ? void 0 : parsed.cloud_cover) === null || _a === void 0 ? void 0 : _a.afternoon, 0, 100);
169
+ const tempMin = WeatherHistoryBackfill.plausibleReading((_b = parsed === null || parsed === void 0 ? void 0 : parsed.temperature) === null || _b === void 0 ? void 0 : _b.min, WeatherHistoryBackfill.minTemperature, WeatherHistoryBackfill.maxTemperature);
170
+ const tempMax = WeatherHistoryBackfill.plausibleReading((_c = parsed === null || parsed === void 0 ? void 0 : parsed.temperature) === null || _c === void 0 ? void 0 : _c.max, WeatherHistoryBackfill.minTemperature, WeatherHistoryBackfill.maxTemperature);
171
+ if (cloudCover === undefined || tempMin === undefined || tempMax === undefined) {
172
+ // No substitute values: a made up cloud cover would be fitted as if it had been measured. A value
173
+ // outside its band is discarded exactly like a missing one, for the same reason.
174
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Warn, `WeatherHistoryBackfill: incomplete day summary for ${day}`);
175
+ return undefined;
176
+ }
177
+ // Read AFTER the completeness check and deliberately outside it. The three fields above decide whether the
178
+ // day counts; precipitation must never be able to discard one. An endpoint that does not send the field,
179
+ // or a dry day it chooses to omit, would otherwise tear a hole into the history the start decision reads -
180
+ // a defect that would only show up as a gradually thinning window, weeks later. Absent stays absent all
181
+ // the way into the column; it is not filled with a 0, which would be an ordinary reading rather than a
182
+ // visibly missing one.
183
+ const precipitation = WeatherHistoryBackfill.plausibleReading((_d = parsed === null || parsed === void 0 ? void 0 : parsed.precipitation) === null || _d === void 0 ? void 0 : _d.total, 0, WeatherHistoryBackfill.maxPrecipitation);
184
+ const dayStart = new Date(date);
185
+ dayStart.setHours(0, 0, 0, 0);
186
+ return {
187
+ date: dayStart,
188
+ cloudCover: cloudCover,
189
+ tempMin: tempMin,
190
+ tempMax: tempMax,
191
+ precipitation: precipitation,
192
+ };
193
+ }
194
+ /**
195
+ * Establishes one field of the answer as a reading: a real number within the band its quantity can occupy.
196
+ *
197
+ * `typeof` rather than `Number(...)`: a value that has to be converted first is not a reading of the
198
+ * quantity, and converting would turn `null` into 0 and `""` into 0 - both of which pass every band. The
199
+ * band itself catches what is numeric but cannot be the quantity, `NaN` and the infinities included, since
200
+ * neither compares inside it.
201
+ * @param value - The field as it arrived
202
+ * @param min - Lowest value the quantity can take
203
+ * @param max - Highest value the quantity can take
204
+ * @returns - The reading, or undefined when the field is not one
205
+ */
206
+ static plausibleReading(value, min, max) {
207
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
208
+ return undefined;
209
+ }
210
+ return value;
211
+ }
212
+ static dayKey(date) {
213
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
214
+ const day = `${date.getDate()}`.padStart(2, '0');
215
+ return `${date.getFullYear()}-${month}-${day}`;
216
+ }
217
+ }
218
+ exports.WeatherHistoryBackfill = WeatherHistoryBackfill;
219
+ /** Milliseconds between two calls to the weather service. */
220
+ WeatherHistoryBackfill.defaultThrottleMs = 1500;
221
+ /**
222
+ * The band a daily air temperature in degrees Celsius can occupy. Wide on purpose - it is there to reject
223
+ * what cannot be a temperature at all, not to second guess the weather service on a hot afternoon. The
224
+ * bounds sit outside the coldest and the hottest reading ever recorded on the planet.
225
+ */
226
+ WeatherHistoryBackfill.minTemperature = -95;
227
+ WeatherHistoryBackfill.maxTemperature = 60;
228
+ /**
229
+ * The most millimetres of precipitation a single day can carry. Wide on purpose, like the temperature band:
230
+ * it rejects what cannot be a daily rainfall at all, it does not second guess the weather service on a
231
+ * thunderstorm. The bound sits above the highest daily total ever recorded on the planet.
232
+ */
233
+ WeatherHistoryBackfill.maxPrecipitation = 2000;
234
+ WeatherHistoryBackfill.host = 'api.openweathermap.org';
235
+ /**
236
+ * The past days of the window that were already fetched on the running day, and the running day that was
237
+ * recorded on.
238
+ *
239
+ * The attempt is remembered, not its outcome: a past day is archive, so a second fetch on the same calendar
240
+ * day cannot yield anything the first one did not, whatever became of the first. That also covers the fetch
241
+ * that succeeded while the write did not land - `persistWeatherDaySummary` reports nothing back, so a
242
+ * failing write is invisible from here and would otherwise cost the full window once per run, and the
243
+ * backfill runs far more often than once a day. The running day is remembered alongside, so a passing outage
244
+ * heals on the next calendar day instead of staying suppressed until a restart.
245
+ */
246
+ WeatherHistoryBackfill.attemptedPastDays = new Set();
247
+ WeatherHistoryBackfill.attemptedPastDaysRecordedOn = undefined;
@@ -314,18 +314,43 @@ class WeatherService {
314
314
  return;
315
315
  }
316
316
  const host = 'api.openweathermap.org';
317
- const path = `/data/3.0/onecall?lat=${WeatherService.latitude}&lon=${WeatherService.longitude}&appid=${WeatherService.appID}&units=metric&lang=de`;
317
+ // HTTPSOptions logs the path it is constructed with, so it is constructed with a redacted one and the
318
+ // real query - which carries key and location - is set afterwards. The repository is public and an
319
+ // operator runs at Debug, which is the level that log line goes out on.
320
+ // Do NOT collapse these two into one constructor call: that puts the key into a log line. The guard is
321
+ // "carries key and location in the request and in no log line" in test/services/weather-service.test.ts.
322
+ const options = new HTTPSOptions_1.HTTPSOptions(host, '/data/3.0/onecall (forecast)', {}, 'GET', 443);
323
+ options.path =
324
+ `/data/3.0/onecall?lat=${WeatherService.latitude}&lon=${WeatherService.longitude}` +
325
+ `&appid=${WeatherService.appID}&units=metric&lang=de`;
318
326
  logging_1.ServerLogService.writeLog(enums_1.LogLevel.Debug, 'Send WeatherAPi Request for data update.');
319
- https_service_1.HTTPSService.request(new HTTPSOptions_1.HTTPSOptions(host, path, {}, 'GET', 443), '', 5, (response) => {
327
+ https_service_1.HTTPSService.request(options, '', 5, (response, statusCode) => {
328
+ if (statusCode < 200 || statusCode >= 300) {
329
+ // Everything outside the successful range ends the update here, and the last forecast stays in place:
330
+ // an old forecast is a worse answer than a fresh one, an error page is none at all.
331
+ // This covers both ways a request can miss its forecast. `HTTPSService.failureStatusCode` means there
332
+ // was no answer to begin with, and any other code means the endpoint answered something that is not
333
+ // one - a refused key answers 401, an exhausted quota 429. The body cannot be what decides that:
334
+ // those answers are readable JSON as well, so parsing them succeeds and yields a WeatherResponse
335
+ // whose every field is undefined.
336
+ // Only the code is named: the request carries the key and the answer mirrors the coordinate back.
337
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Warn, `WeatherAPi request answered ${statusCode} --> no forecast update`);
338
+ return;
339
+ }
320
340
  logging_1.ServerLogService.writeLog(enums_1.LogLevel.Debug, 'WeatherAPi Response erhalten');
321
- logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `WeatherAPi Response: ${response}`);
341
+ // The length rather than the body: the answer mirrors the coordinate of the request back, so a raw
342
+ // dump writes the location of the plant into the log even though it carries no key itself.
343
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `WeatherAPi Response of ${response.length} characters received`);
322
344
  utils_1.Utils.guardedFunction(() => {
323
345
  const responseObj = JSON.parse(response);
324
346
  WeatherService.processHourlyWeather(responseObj);
325
347
  for (const dataUpdateCbsKey in this._dataUpdateCbs) {
326
348
  this._dataUpdateCbs[dataUpdateCbsKey]();
327
349
  }
328
- }, this, `Response from Weather API call at https://${host}/${path}: ${response}`);
350
+ }, this,
351
+ // Neither the request nor the answer is named here: the first carries the key, the second mirrors
352
+ // the location back. The status of the call is already reported by the two lines above.
353
+ 'WeatherService: the answer of the weather API could not be processed');
329
354
  });
330
355
  }
331
356
  static recalcAzimuth() {
@@ -32,7 +32,9 @@ class SettingsServiceInstance {
32
32
  return lat;
33
33
  }
34
34
  }
35
- return 51.529556852253826;
35
+ // A coarse fallback so the sun times resolve at all when no location is configured. Deliberately not a
36
+ // site: one decimal is roughly city scale, and this default is public.
37
+ return 51.0;
36
38
  }
37
39
  get longitude() {
38
40
  var _a, _b;
@@ -42,7 +44,8 @@ class SettingsServiceInstance {
42
44
  return longitude;
43
45
  }
44
46
  }
45
- return 7.097266042276687;
47
+ // Coarse for the same reason as the latitude above.
48
+ return 7.0;
46
49
  }
47
50
  initialize(config) {
48
51
  this.settings = config;
@@ -31,6 +31,12 @@ export declare class DachsDeviceSettings extends ActuatorSettings implements iDa
31
31
  winterMinimumHeatStorageTemp: number;
32
32
  /** @inheritDoc */
33
33
  winterMinimumPreNightHeatStorageTemp: number;
34
+ /** @inheritDoc */
35
+ summerWarmWaterDesiredMaxTemp: number;
36
+ /** @inheritDoc */
37
+ dachsRatedElectricalWattage: number;
38
+ /** @inheritDoc */
39
+ dachsConversionFactor: number;
34
40
  fromPartialObject(data: Partial<DachsDeviceSettings>): void;
35
41
  toJSON(): Partial<DachsDeviceSettings>;
36
42
  }
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DachsDeviceSettings = void 0;
4
4
  const utils_1 = require("../../utils");
5
5
  const actuatorSettings_1 = require("./actuatorSettings");
6
+ // Shared with the energy manager's settings rather than copied: the two would otherwise drift apart on what
7
+ // counts as an unusable share, and an installation would be answered differently depending on which device
8
+ // happened to read the value.
9
+ const victronDeviceSettings_1 = require("./victronDeviceSettings");
6
10
  class DachsDeviceSettings extends actuatorSettings_1.ActuatorSettings {
7
11
  constructor() {
8
12
  super(...arguments);
@@ -36,9 +40,15 @@ class DachsDeviceSettings extends actuatorSettings_1.ActuatorSettings {
36
40
  this.winterMinimumHeatStorageTemp = 55;
37
41
  /** @inheritDoc */
38
42
  this.winterMinimumPreNightHeatStorageTemp = 65;
43
+ /** @inheritDoc */
44
+ this.summerWarmWaterDesiredMaxTemp = 58;
45
+ /** @inheritDoc */
46
+ this.dachsRatedElectricalWattage = 5500;
47
+ /** @inheritDoc */
48
+ this.dachsConversionFactor = 0.8;
39
49
  }
40
50
  fromPartialObject(data) {
41
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
51
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
42
52
  this.disableHeatingRod = (_a = data.disableHeatingRod) !== null && _a !== void 0 ? _a : this.disableHeatingRod;
43
53
  this.disableDachsOwnWW = (_b = data.disableDachsOwnWW) !== null && _b !== void 0 ? _b : this.disableDachsOwnWW;
44
54
  this.disableDachsTemporarily = (_c = data.disableDachsTemporarily) !== null && _c !== void 0 ? _c : this.disableDachsTemporarily;
@@ -58,6 +68,9 @@ class DachsDeviceSettings extends actuatorSettings_1.ActuatorSettings {
58
68
  this.winterMinimumHeatStorageTemp = (_p = data.winterMinimumHeatStorageTemp) !== null && _p !== void 0 ? _p : this.winterMinimumHeatStorageTemp;
59
69
  this.winterMinimumPreNightHeatStorageTemp =
60
70
  (_q = data.winterMinimumPreNightHeatStorageTemp) !== null && _q !== void 0 ? _q : this.winterMinimumPreNightHeatStorageTemp;
71
+ this.summerWarmWaterDesiredMaxTemp = (_r = data.summerWarmWaterDesiredMaxTemp) !== null && _r !== void 0 ? _r : this.summerWarmWaterDesiredMaxTemp;
72
+ this.dachsRatedElectricalWattage = (_s = data.dachsRatedElectricalWattage) !== null && _s !== void 0 ? _s : this.dachsRatedElectricalWattage;
73
+ this.dachsConversionFactor = (0, victronDeviceSettings_1.acceptedShare)('dachsConversionFactor', data.dachsConversionFactor, this.dachsConversionFactor);
61
74
  super.fromPartialObject(data);
62
75
  }
63
76
  toJSON() {
@@ -1,4 +1,35 @@
1
1
  import { DeviceSettings } from './deviceSettings';
2
+ /**
3
+ * Takes over a setting that has to lie between 0 and 1, or keeps the value in force and reports the refusal.
4
+ *
5
+ * Kept rather than clamped, and that is the whole decision: 90 written for "90 percent" clamps to 1 and turns
6
+ * a dial nobody touched to its extreme - a coverage demanding every single reading of a day, a quantile at
7
+ * the maximum of the sample, a conversion factor that loses nothing at all. Each of those is a plausible
8
+ * looking number, and a plausible looking number is exactly the one an operator never goes looking for. The
9
+ * value in force is the documented default, the refused input is named in the log, and no unusable input
10
+ * quietly becomes a different plant.
11
+ *
12
+ * Deliberately loud rather than thrown: a settings file is read at startup, and one mistyped share must not
13
+ * cost the whole installation its automation.
14
+ *
15
+ * Lives here rather than on the settings base class only because of how this change was cut; it is shared by
16
+ * every settings class that carries a share and belongs next to them.
17
+ * @param name - The setting, so the log line names what was refused
18
+ * @param value - What was handed in, or undefined when the caller set nothing
19
+ * @param current - The value in force, kept whenever the input is unusable
20
+ * @returns - The value to use from now on
21
+ */
22
+ export declare function acceptedShare(name: string, value: number | undefined, current: number): number;
23
+ /**
24
+ * Reports a history window shorter than the number of days the fit demands before it produces anything.
25
+ *
26
+ * Reported rather than corrected: which of the two numbers the operator meant cannot be told from here, and
27
+ * the combination is not dangerous - the fit simply never yields a band and the decision falls back to the
28
+ * rung that needs no history. Only nobody would ever see why it never got past that rung.
29
+ * @param windowDays - The length of the sliding window in days
30
+ * @param minimumDays - How many usable days the fit demands
31
+ */
32
+ export declare function reportImpossibleWindow(windowDays: number, minimumDays: number): void;
2
33
  export declare class VictronDeviceSettings extends DeviceSettings {
3
34
  /**
4
35
  * The default time interval in minutes for battery-change reporting regardless of battery level.
@@ -47,6 +78,91 @@ export declare class VictronDeviceSettings extends DeviceSettings {
47
78
  * @default 10000
48
79
  */
49
80
  batteryCapacityWattage: number;
81
+ /**
82
+ * The share of the expected consumption readings a recorded day has to carry before its window sum is
83
+ * counted at all, between 0 and 1.
84
+ *
85
+ * A dial of the plant rather than of whoever reads its history: an incompletely covered night adds up to
86
+ * too little, therefore looks like a frugal night, pulls the quantile down and makes the resulting bound
87
+ * too optimistic - and two readers who disagree about what counts as a covered day of the same plant would
88
+ * be answering about two different plants. Deliberately strict, because a bound that is too optimistic
89
+ * suppresses, and a wrong suppression has no way back: the morning is simply below the reserve by then.
90
+ * @default 0.9
91
+ */
92
+ historyMinimumDayCoverage: number;
93
+ /**
94
+ * The quantile of the recorded consumption windows a model free bound is calculated with, between 0 and 1.
95
+ * An upper quantile rather than the median, because the bound has to hold on a heavy night and not on half
96
+ * of them.
97
+ * @default 0.9
98
+ */
99
+ historyConsumptionQuantile: number;
100
+ /**
101
+ * How many usable consumption window sums are needed before that quantile means anything at all.
102
+ *
103
+ * An upper quantile of one night is that night, and of two nights it sits all but on their maximum - so a
104
+ * single quiet night would yield a high bound and a suppression with nothing behind it. Deliberately its
105
+ * own number rather than a shared minimum with the day count a model fit needs: that one protects the
106
+ * stability of a fit, this one protects the meaning of a quantile.
107
+ * @default 10
108
+ */
109
+ historyMinimumConsumptionDays: number;
110
+ /**
111
+ * The state of charge in percent the coming morning's low is expected to stay above.
112
+ *
113
+ * A property of the battery and therefore stated once, here: "how low may the morning get" is the same
114
+ * question whichever device asks it, and two numbers for one battery would let a plant judge one and the
115
+ * same morning twice. Where consumers legitimately differ is how sure they want to be before they act, and
116
+ * that is the split between the verdicts that need no model and the ones that do - see
117
+ * {@link iMorningReserveVerdict.measured}, not a second reserve.
118
+ *
119
+ * It is also the threshold the model shadow measures against, and a shadow whose sample is split across two
120
+ * thresholds measures nothing - see {@link ModelShadow}.
121
+ * @default 20
122
+ */
123
+ minimumMorningSocReserve: number;
124
+ /**
125
+ * Below this many remaining sun hours no further photovoltaic yield is expected to change the coming
126
+ * morning.
127
+ *
128
+ * Plant wide next to the reserve, because it describes this installation's photovoltaic rather than whoever
129
+ * asks: the last half hour before sunset yields the same nothing for every consumer.
130
+ * @default 0.5
131
+ */
132
+ noSunThresholdHours: number;
133
+ /**
134
+ * The length of the sliding window in days the history is read and fitted from. Older days describe a
135
+ * differently grown installation and are deliberately left out.
136
+ *
137
+ * Stated once for the whole plant, here, because it says how much of the recorded data is telling enough to
138
+ * answer with - a property of that data and not of whoever asks. Every consumer of the history asks the
139
+ * same question, and a second window meant a second read and a second paid backfill of the same days.
140
+ *
141
+ * The default is a starting point, not a finding: it has not been established against recorded data. In the
142
+ * back test the estimate gets monotonically worse the longer the window is, and a window of sixty days
143
+ * already scores worse than the trivial threshold rule. Treat this value as unproven until a run against
144
+ * the recorded history has produced a number.
145
+ * @default 90
146
+ */
147
+ historyWindowDays: number;
148
+ /**
149
+ * How many usable historical days are needed before the weights are fitted at all. Below this number no
150
+ * band is produced at all and every consumer falls back to what needs no history.
151
+ *
152
+ * Plant wide for the same reason {@link historyWindowDays} is: how much evidence makes a fit meaningful is a
153
+ * property of the recorded data.
154
+ * @default 15
155
+ */
156
+ historyMinimumDays: number;
157
+ /**
158
+ * How many standard deviations of the residuals each band edge lies away from the point estimate.
159
+ *
160
+ * Plant wide, because the band is part of the answer rather than of the judgement made on it. A consumer
161
+ * that one day wants edges of its own width reads {@link iEnergyHistoryOutlook.residualSigma} and forms
162
+ * them; it does not fit a second model.
163
+ * @default 1.0
164
+ */
165
+ historyBandSigma: number;
50
166
  /**
51
167
  * The normal base consumption of the house in wattage
52
168
  * @default 600