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,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./energy-history-service"), exports);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The part of an energy manager's settings the energy history service needs, read structurally to stay off a
3
+ * concrete manager.
4
+ *
5
+ * All of these describe the **plant**: how much its battery holds, and how good its own recorded data has to
6
+ * be before a statement is built from it. They are read off the manager rather than handed in, because a
7
+ * caller could only fill them by reading this same global place - an option filled that way is a pass-through,
8
+ * and it would let two callers disagree about what counts as a covered day of one and the same plant.
9
+ *
10
+ * Not part of the published surface: it describes how the service reads a manager, not something a consumer of
11
+ * the library implements.
12
+ */
13
+ export interface iPlantEnergyDials {
14
+ /** The capacity of the battery in watt hours */
15
+ batteryCapacityWattage?: number;
16
+ /** Share of the expected consumption readings a historical day has to carry to be counted, 0 to 1 */
17
+ historyMinimumDayCoverage?: number;
18
+ /** The quantile of the historical consumption windows the model free bound is calculated with, 0 to 1 */
19
+ historyConsumptionQuantile?: number;
20
+ /** How many usable consumption window sums are needed before that quantile means anything */
21
+ historyMinimumConsumptionDays?: number;
22
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,12 +1,58 @@
1
1
  import { HTTPSOptions } from './HTTPSOptions';
2
2
  export declare class HTTPSService {
3
+ /**
4
+ * The status code the callback is handed for a request that produced no answer: no answer of the endpoint,
5
+ * an exhausted retry chain, a failing socket or a failing response.
6
+ *
7
+ * Negative on purpose. It is outside the range HTTP status codes occupy, so no comparison a caller writes
8
+ * against a real code - `=== 200`, a check for the successful range, a switch over the classes - can match
9
+ * it by accident; a failure that a caller reads as success is worse than one it never hears about. Zero was
10
+ * not an option: the successful path already yields it for an answer that ended without a status code, so
11
+ * the two cases would be indistinguishable.
12
+ */
13
+ static readonly failureStatusCode: number;
14
+ /**
15
+ * The body the callback is handed alongside `failureStatusCode`.
16
+ *
17
+ * Not the empty string, which a caller looking only at the body would take for an answer that happened to
18
+ * carry nothing. It names neither host nor path, because a caller is free to log what it receives and the
19
+ * path of a request carries its query.
20
+ */
21
+ static readonly failureResponse: string;
22
+ /**
23
+ * How long one attempt may go without any traffic on its socket before it is given up on and handed to the
24
+ * retry chain.
25
+ *
26
+ * A limit is needed at all because silence is not an error: an endpoint that accepts the connection and
27
+ * then says nothing produces neither `error` nor `response`, so without one the attempt - and every caller
28
+ * waiting on its callback - stays open until the process ends.
29
+ *
30
+ * Thirty seconds, because the whole chain has to stay inside the cadence of the callers that repeat: the
31
+ * weather service asks every ten minutes, and the default of five retries makes six attempts, so a fully
32
+ * silent endpoint costs about three minutes. That leaves a request no way to still be running when the next
33
+ * one starts, while staying far above the seconds a loaded endpoint may legitimately need to answer.
34
+ */
35
+ static readonly requestTimeoutMs: number;
36
+ /**
37
+ * Sends one HTTPS request and reports its outcome to `responseCallback` **exactly once**, on every way the
38
+ * request can end: an answer, an exhausted retry chain, a failing socket or a failing response. A caller
39
+ * may therefore wrap this in a promise and rely on it settling.
40
+ * @param options - Host, path, headers, method and port of the request
41
+ * @param postData - The body to send; nothing is written when it is empty
42
+ * @param retryOnError - How many further attempts a failing socket may cost before the request is given up
43
+ * @param responseCallback - Receives body and status code; a failure arrives as `failureResponse` and
44
+ * `failureStatusCode`
45
+ */
3
46
  static request(options: HTTPSOptions, postData?: string, retryOnError?: number, responseCallback?: (data: string, statuscode: number) => void): void;
4
47
  /**
5
- * Downloads a file from a given url to the given location.
48
+ * Downloads a file from a given url to the given location, and **settles either way**: on every way the
49
+ * download can end - the file written, the endpoint refusing, the request failing, the answer breaking off
50
+ * or the disk failing - the promise is resolved or rejected exactly once.
6
51
  * If the location doesn't exist, it will be created quietly.
7
52
  * @param url - URL to download file from
8
53
  * @param filePath - Path to save file to
9
- * @returns A promise that resolves to true if the file was downloaded successfully, false otherwise
54
+ * @returns A promise that resolves to true once the file is written and to false when the transfer failed;
55
+ * it is rejected when the endpoint refused the download or when the file could not be written
10
56
  */
11
57
  static downloadFile(url: string, filePath: string): Promise<boolean>;
12
58
  private static defaultCallback;
@@ -44,31 +44,79 @@ const logging_1 = require("../logging");
44
44
  const enums_1 = require("../enums");
45
45
  const utils_1 = require("../utils");
46
46
  class HTTPSService {
47
+ /**
48
+ * Sends one HTTPS request and reports its outcome to `responseCallback` **exactly once**, on every way the
49
+ * request can end: an answer, an exhausted retry chain, a failing socket or a failing response. A caller
50
+ * may therefore wrap this in a promise and rely on it settling.
51
+ * @param options - Host, path, headers, method and port of the request
52
+ * @param postData - The body to send; nothing is written when it is empty
53
+ * @param retryOnError - How many further attempts a failing socket may cost before the request is given up
54
+ * @param responseCallback - Receives body and status code; a failure arrives as `failureResponse` and
55
+ * `failureStatusCode`
56
+ */
47
57
  static request(options, postData = '', retryOnError = 5, responseCallback = HTTPSService.defaultCallback) {
48
58
  const responseData = [];
49
- const req = https_1.default.request(options, (res) => {
59
+ // True as soon as this attempt has done with the callback: either it has delivered the outcome, or it has
60
+ // handed the attempt to a retry that delivers for it. Both ways out of a request can fire for the same
61
+ // one - a socket error may still arrive after the response has ended, and an `end` may follow an error on
62
+ // the response - and every one of them is guarded by this flag. Two calls would be a new fault class in
63
+ // place of the old one, and an unguarded late error would open a second retry chain that calls the
64
+ // callback again from its own end.
65
+ let settled = false;
66
+ const settle = (data, statuscode) => {
67
+ if (settled) {
68
+ return;
69
+ }
70
+ settled = true;
71
+ responseCallback(data, statuscode);
72
+ };
73
+ // A copy rather than the options themselves: the limit belongs to this service, not to the request the
74
+ // caller described, and a caller may hand the same options to a second request.
75
+ const req = https_1.default.request({ ...options, timeout: HTTPSService.requestTimeoutMs }, (res) => {
50
76
  res.on('data', (data) => {
51
77
  responseData.push(data.toString());
52
78
  });
53
79
  res.on('end', () => {
54
80
  var _a;
55
- responseCallback(responseData.join(''), (_a = res.statusCode) !== null && _a !== void 0 ? _a : 0);
81
+ settle(responseData.join(''), (_a = res.statusCode) !== null && _a !== void 0 ? _a : 0);
56
82
  });
57
83
  res.on('error', (e) => {
58
84
  logging_1.ServerLogService.writeLog(enums_1.LogLevel.Error, `HTTPS Error: ${e}`);
85
+ // No retry from here: part of the answer may already have been handed out, and repeating a request
86
+ // whose body was sent is not free. The response ends here, so the caller hears about it here.
87
+ settle(HTTPSService.failureResponse, HTTPSService.failureStatusCode);
59
88
  });
60
89
  });
90
+ req.on('timeout', () => {
91
+ if (settled) {
92
+ // The socket of a request whose outcome is already delivered may idle as long as it likes; tearing
93
+ // that one down would be about a request nobody is waiting for any more.
94
+ return;
95
+ }
96
+ // `timeout` only reports the idle socket, it does not end anything - node leaves that to this listener.
97
+ // Destroying with an error routes the silence into the handler below, so it costs a retry and ends in
98
+ // the callback exactly like an endpoint that was never reachable. The message names neither host nor
99
+ // path: that handler logs what it is given, and the path of a request carries its query.
100
+ req.destroy(new Error(`HTTPS request timed out after ${HTTPSService.requestTimeoutMs} ms`));
101
+ });
61
102
  req.on('error', (e) => {
62
103
  logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `HTTPS Error: ${e}`);
104
+ if (settled) {
105
+ return;
106
+ }
63
107
  if (retryOnError > 0) {
64
108
  logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `HTTPS request failed --> ${retryOnError} retries left`);
109
+ // The retry inherits the callback, so this attempt is done with it from here on.
110
+ settled = true;
65
111
  utils_1.Utils.guardedTimeout(() => {
66
112
  HTTPSService.request(options, postData, retryOnError - 1, responseCallback);
67
113
  }, 100);
114
+ return;
68
115
  }
69
- else {
70
- logging_1.ServerLogService.writeLog(enums_1.LogLevel.Error, 'HTTPS request failed after retries');
71
- }
116
+ // Neither the request nor the error is quoted here: the path of a request carries its query, and this
117
+ // repository is public.
118
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Error, 'HTTPS request failed after retries');
119
+ settle(HTTPSService.failureResponse, HTTPSService.failureStatusCode);
72
120
  });
73
121
  if (postData !== '') {
74
122
  req.write(postData);
@@ -76,11 +124,14 @@ class HTTPSService {
76
124
  req.end();
77
125
  }
78
126
  /**
79
- * Downloads a file from a given url to the given location.
127
+ * Downloads a file from a given url to the given location, and **settles either way**: on every way the
128
+ * download can end - the file written, the endpoint refusing, the request failing, the answer breaking off
129
+ * or the disk failing - the promise is resolved or rejected exactly once.
80
130
  * If the location doesn't exist, it will be created quietly.
81
131
  * @param url - URL to download file from
82
132
  * @param filePath - Path to save file to
83
- * @returns A promise that resolves to true if the file was downloaded successfully, false otherwise
133
+ * @returns A promise that resolves to true once the file is written and to false when the transfer failed;
134
+ * it is rejected when the endpoint refused the download or when the file could not be written
84
135
  */
85
136
  static async downloadFile(url, filePath) {
86
137
  return new Promise((resolve, reject) => {
@@ -91,10 +142,34 @@ class HTTPSService {
91
142
  }
92
143
  const file = fs.createWriteStream(filePath);
93
144
  let fileInfo = null;
145
+ // True as soon as the download is over, whichever way it went. Several of the ways below fire for the
146
+ // same download - a socket error still turns up once the file has been written - and without this the
147
+ // clean up of a failed one would take a finished file with it.
148
+ let settled = false;
149
+ const succeed = () => {
150
+ if (settled) {
151
+ return;
152
+ }
153
+ settled = true;
154
+ resolve(true);
155
+ };
156
+ /**
157
+ * Ends a failed download: closes the write stream and removes what was written of the file before the
158
+ * caller hears about it. A torso that stays behind is taken for a finished download the next time.
159
+ * @param reason - What to reject with, or nothing to report the failure as `false`
160
+ */
161
+ const discard = (reason) => {
162
+ if (settled) {
163
+ return;
164
+ }
165
+ settled = true;
166
+ file.destroy();
167
+ fs.unlink(filePath, () => (reason !== undefined ? reject(reason) : resolve(false)));
168
+ };
94
169
  const request = https_1.default.get(url, (response) => {
95
170
  var _a, _b;
96
171
  if (response.statusCode !== 200) {
97
- reject(new Error(`Failed to get '${url}' (${response.statusCode})`));
172
+ discard(new Error(`Failed to get '${url}' (${response.statusCode})`));
98
173
  return;
99
174
  }
100
175
  fileInfo = {
@@ -102,13 +177,31 @@ class HTTPSService {
102
177
  size: parseInt((_b = response.headers['content-length']) !== null && _b !== void 0 ? _b : '0', 10),
103
178
  };
104
179
  logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `Downloaded File\tType: "${fileInfo.mime}"\tSize:${fileInfo.size}`);
180
+ // An answer that breaks off once it is being piped ends the download without the request failing and
181
+ // without the file ever finishing, so it is heard here or nowhere. Same class of failure as a failing
182
+ // socket, so it is reported the same way.
183
+ response.on('error', (err) => {
184
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `Error Downloading File: ${err}`);
185
+ discard();
186
+ });
105
187
  response.pipe(file);
106
188
  });
107
189
  // The destination stream is ended by the time it's called
108
- file.on('finish', () => resolve(true));
190
+ file.on('finish', () => succeed());
191
+ // A failing disk - no space, no permission - fails the write stream and nothing else: request and
192
+ // answer are both fine. Without this the event goes unhandled and the caller waits for a download that
193
+ // is already over.
194
+ file.on('error', (err) => {
195
+ var _a;
196
+ // Only the code of the failure travels on: the message of a real one quotes the file it was writing,
197
+ // and the caller is free to log what it is handed.
198
+ const code = (_a = err.code) !== null && _a !== void 0 ? _a : err.name;
199
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `Error writing downloaded file: ${code}`);
200
+ discard(new Error(`Writing the downloaded file failed (${code})`));
201
+ });
109
202
  request.on('error', (err) => {
110
203
  logging_1.ServerLogService.writeLog(enums_1.LogLevel.DeepTrace, `Error Downloading File: ${err}`);
111
- fs.unlink(filePath, () => resolve(false));
204
+ discard();
112
205
  });
113
206
  });
114
207
  }
@@ -117,3 +210,36 @@ class HTTPSService {
117
210
  }
118
211
  }
119
212
  exports.HTTPSService = HTTPSService;
213
+ /**
214
+ * The status code the callback is handed for a request that produced no answer: no answer of the endpoint,
215
+ * an exhausted retry chain, a failing socket or a failing response.
216
+ *
217
+ * Negative on purpose. It is outside the range HTTP status codes occupy, so no comparison a caller writes
218
+ * against a real code - `=== 200`, a check for the successful range, a switch over the classes - can match
219
+ * it by accident; a failure that a caller reads as success is worse than one it never hears about. Zero was
220
+ * not an option: the successful path already yields it for an answer that ended without a status code, so
221
+ * the two cases would be indistinguishable.
222
+ */
223
+ HTTPSService.failureStatusCode = -1;
224
+ /**
225
+ * The body the callback is handed alongside `failureStatusCode`.
226
+ *
227
+ * Not the empty string, which a caller looking only at the body would take for an answer that happened to
228
+ * carry nothing. It names neither host nor path, because a caller is free to log what it receives and the
229
+ * path of a request carries its query.
230
+ */
231
+ HTTPSService.failureResponse = 'HTTPS request failed';
232
+ /**
233
+ * How long one attempt may go without any traffic on its socket before it is given up on and handed to the
234
+ * retry chain.
235
+ *
236
+ * A limit is needed at all because silence is not an error: an endpoint that accepts the connection and
237
+ * then says nothing produces neither `error` nor `response`, so without one the attempt - and every caller
238
+ * waiting on its callback - stays open until the process ends.
239
+ *
240
+ * Thirty seconds, because the whole chain has to stay inside the cadence of the callers that repeat: the
241
+ * weather service asks every ten minutes, and the default of five retries makes six attempts, so a fully
242
+ * silent endpoint costs about three minutes. That leaves a request no way to still be running when the next
243
+ * one starts, while staying far above the seconds a loaded endpoint may legitimately need to answer.
244
+ */
245
+ HTTPSService.requestTimeoutMs = 30000;
@@ -7,6 +7,7 @@ export * from './time-callback-service';
7
7
  export * from './network';
8
8
  export * from './ac';
9
9
  export * from './calendar';
10
+ export * from './energy';
10
11
  export * from './blockAutomaticHandler';
11
12
  export * from './file-info';
12
13
  export * from './https-service';
@@ -23,6 +23,7 @@ __exportStar(require("./time-callback-service"), exports);
23
23
  __exportStar(require("./network"), exports);
24
24
  __exportStar(require("./ac"), exports);
25
25
  __exportStar(require("./calendar"), exports);
26
+ __exportStar(require("./energy"), exports);
26
27
  __exportStar(require("./blockAutomaticHandler"), exports);
27
28
  __exportStar(require("./file-info"), exports);
28
29
  __exportStar(require("./https-service"), exports);
@@ -22,6 +22,18 @@ export declare class TimeCallbackService {
22
22
  static recalcSunTimes(calculationDate?: Date): void;
23
23
  static removeCallback(pCallback: ITimeCallback): void;
24
24
  static updateSunRise(pDay?: Date, lat?: number, long?: number): void;
25
+ /**
26
+ * The sunrise of a given day, as this service defines it.
27
+ *
28
+ * The counterpart of {@link getSunsetForDate}, and it exists so that a caller which needs both ends of one
29
+ * day gets them from one definition of the sun: two libraries put sunrise and sunset minutes apart, and a
30
+ * window bounded by one at each end is bounded by two different mornings.
31
+ * @param pDay - The day to calculate for; defaults to today.
32
+ * @param lat - The latitude to calculate for; defaults to the configured one.
33
+ * @param long - The longitude to calculate for; defaults to the configured one.
34
+ * @returns The moment of sunrise on that day.
35
+ */
36
+ static getSunriseForDate(pDay?: Date, lat?: number, long?: number): Date;
25
37
  static updateSunSet(pDay?: Date, lat?: number, long?: number): void;
26
38
  static getSunsetForDate(pDay?: Date, lat?: number, long?: number): Date;
27
39
  static hoursTilSunset(): number;
@@ -143,7 +143,21 @@ Next Sunset: ${TimeCallbackService._nextSunSet.toLocaleString('de-DE')}`);
143
143
  }
144
144
  }
145
145
  static updateSunRise(pDay = new Date(), lat, long) {
146
- TimeCallbackService._nextSunRise = (0, sunrise_sunset_js_1.getSunrise)(lat !== null && lat !== void 0 ? lat : settings_service_1.SettingsService.latitude, long !== null && long !== void 0 ? long : settings_service_1.SettingsService.longitude, pDay);
146
+ TimeCallbackService._nextSunRise = this.getSunriseForDate(pDay, lat !== null && lat !== void 0 ? lat : settings_service_1.SettingsService.latitude, long !== null && long !== void 0 ? long : settings_service_1.SettingsService.longitude);
147
+ }
148
+ /**
149
+ * The sunrise of a given day, as this service defines it.
150
+ *
151
+ * The counterpart of {@link getSunsetForDate}, and it exists so that a caller which needs both ends of one
152
+ * day gets them from one definition of the sun: two libraries put sunrise and sunset minutes apart, and a
153
+ * window bounded by one at each end is bounded by two different mornings.
154
+ * @param pDay - The day to calculate for; defaults to today.
155
+ * @param lat - The latitude to calculate for; defaults to the configured one.
156
+ * @param long - The longitude to calculate for; defaults to the configured one.
157
+ * @returns The moment of sunrise on that day.
158
+ */
159
+ static getSunriseForDate(pDay = new Date(), lat, long) {
160
+ return (0, sunrise_sunset_js_1.getSunrise)(lat !== null && lat !== void 0 ? lat : settings_service_1.SettingsService.latitude, long !== null && long !== void 0 ? long : settings_service_1.SettingsService.longitude, pDay);
147
161
  }
148
162
  static updateSunSet(pDay = new Date(), lat, long) {
149
163
  TimeCallbackService._nextSunSet = this.getSunsetForDate(pDay, lat !== null && lat !== void 0 ? lat : settings_service_1.SettingsService.latitude, long !== null && long !== void 0 ? long : settings_service_1.SettingsService.longitude);
@@ -152,7 +166,7 @@ Next Sunset: ${TimeCallbackService._nextSunSet.toLocaleString('de-DE')}`);
152
166
  return (0, sunrise_sunset_js_1.getSunset)(lat !== null && lat !== void 0 ? lat : settings_service_1.SettingsService.latitude, long !== null && long !== void 0 ? long : settings_service_1.SettingsService.longitude, pDay);
153
167
  }
154
168
  static hoursTilSunset() {
155
- return (this.nextSunSet.getTime() - utils_1.Utils.nowMS()) / 1000 / 60;
169
+ return (this.nextSunSet.getTime() - utils_1.Utils.nowMS()) / (1000 * 60 * 60);
156
170
  }
157
171
  }
158
172
  exports.TimeCallbackService = TimeCallbackService;
@@ -1,2 +1,3 @@
1
1
  export * from './weather-service';
2
+ export * from './weather-history-backfill';
2
3
  export { RainNextMinutesInfo } from './rain-next-minutes-info';
@@ -16,5 +16,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.RainNextMinutesInfo = void 0;
18
18
  __exportStar(require("./weather-service"), exports);
19
+ __exportStar(require("./weather-history-backfill"), exports);
19
20
  var rain_next_minutes_info_1 = require("./rain-next-minutes-info");
20
21
  Object.defineProperty(exports, "RainNextMinutesInfo", { enumerable: true, get: function () { return rain_next_minutes_info_1.RainNextMinutesInfo; } });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The part of the One Call 3.0 day summary answer the backfill uses.
3
+ *
4
+ * The leaves are `unknown` on purpose. This shape describes an answer from beyond the trust boundary, and
5
+ * declaring them `number` would be a promise nothing on this side can keep - `JSON.parse` returns whatever
6
+ * arrived, and a cast does not check it. `unknown` forces every leaf through a plausibility check before it is
7
+ * used, which is the only place the values are actually established as numbers.
8
+ *
9
+ * Not part of the published surface: it describes a foreign answer, not something anyone implements.
10
+ */
11
+ export type OpenWeatherDaySummary = {
12
+ /** Cloud cover of the day, as the endpoint reports it */
13
+ cloud_cover?: {
14
+ afternoon?: unknown;
15
+ };
16
+ /** Air temperatures of the day, as the endpoint reports them */
17
+ temperature?: {
18
+ min?: unknown;
19
+ max?: unknown;
20
+ };
21
+ /** Precipitation of the day, as the endpoint reports it. Absent on an endpoint that does not send it */
22
+ precipitation?: {
23
+ total?: unknown;
24
+ };
25
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import { iWeatherDaySummary } from '../../interfaces/iWeatherDaySummary';
2
+ /**
3
+ * Fetches the daily weather aggregate of one past day, or `undefined` when that day cannot be obtained.
4
+ * Handed in so the backfill can be exercised without talking to the weather service.
5
+ */
6
+ export type WeatherDaySummaryFetcher = (date: Date) => Promise<iWeatherDaySummary | undefined>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,94 @@
1
+ import { iPersist, iWeatherSettings } from '../../interfaces';
2
+ import { WeatherDaySummaryFetcher } from './weather-day-summary-fetcher';
3
+ /**
4
+ * Fills the gaps in the stored daily weather aggregates from the One Call 3.0 day summary endpoint.
5
+ *
6
+ * Its own service instead of a method on the weather service: that one serves the live forecast, while this
7
+ * one walks a window of past days and must stay bounded and throttled. The full recorded history is far larger
8
+ * than a day's request quota, so the fitting window is the hard limit and the throttle keeps an accidentally
9
+ * widened window from draining the quota in one burst.
10
+ */
11
+ export declare class WeatherHistoryBackfill {
12
+ /** Milliseconds between two calls to the weather service. */
13
+ static readonly defaultThrottleMs: number;
14
+ /**
15
+ * The band a daily air temperature in degrees Celsius can occupy. Wide on purpose - it is there to reject
16
+ * what cannot be a temperature at all, not to second guess the weather service on a hot afternoon. The
17
+ * bounds sit outside the coldest and the hottest reading ever recorded on the planet.
18
+ */
19
+ private static readonly minTemperature;
20
+ private static readonly maxTemperature;
21
+ /**
22
+ * The most millimetres of precipitation a single day can carry. Wide on purpose, like the temperature band:
23
+ * it rejects what cannot be a daily rainfall at all, it does not second guess the weather service on a
24
+ * thunderstorm. The bound sits above the highest daily total ever recorded on the planet.
25
+ */
26
+ private static readonly maxPrecipitation;
27
+ private static readonly host;
28
+ /**
29
+ * The past days of the window that were already fetched on the running day, and the running day that was
30
+ * recorded on.
31
+ *
32
+ * The attempt is remembered, not its outcome: a past day is archive, so a second fetch on the same calendar
33
+ * day cannot yield anything the first one did not, whatever became of the first. That also covers the fetch
34
+ * that succeeded while the write did not land - `persistWeatherDaySummary` reports nothing back, so a
35
+ * failing write is invisible from here and would otherwise cost the full window once per run, and the
36
+ * backfill runs far more often than once a day. The running day is remembered alongside, so a passing outage
37
+ * heals on the next calendar day instead of staying suppressed until a restart.
38
+ */
39
+ private static attemptedPastDays;
40
+ private static attemptedPastDaysRecordedOn;
41
+ /**
42
+ * Fetches the daily weather aggregates that are missing within the fitting window and stores them, plus the
43
+ * running day's aggregate on every run. Past days that are already stored are not fetched again, so a
44
+ * service restart does not cost quota; the running day always is, because it is a forecast that moves. A past
45
+ * day that was already fetched on the running day is not fetched again until the next calendar day.
46
+ * @param persist - The persistence to read the present days from and to write the fetched ones to
47
+ * @param referenceDate - The running day; the window ends on it, inclusive
48
+ * @param historyWindowDays - Length of the history window in days; calls are bounded by it plus one
49
+ * @param fetcher - The fetcher to use, defaults to the configured One Call 3.0 day summary endpoint
50
+ * @param throttleMs - Milliseconds to wait between two calls
51
+ * @returns - The number of days that were fetched and handed to the persistence. Not the number that
52
+ * arrived: `persistWeatherDaySummary` reports nothing back, so whether a row landed cannot be seen from
53
+ * here. Do not read this as a count of stored rows - look in the table for that.
54
+ */
55
+ static run(persist: iPersist | undefined, referenceDate: Date, historyWindowDays: number, fetcher?: WeatherDaySummaryFetcher, throttleMs?: number): Promise<number>;
56
+ /**
57
+ * Forgets which past days were already fetched, so the next run offers the whole window again. Called by
58
+ * `run` itself on a change of the running day; separate so a test can start from a known state.
59
+ */
60
+ static resetAttemptedPastDays(): void;
61
+ /**
62
+ * Builds the fetcher against the One Call 3.0 day summary endpoint.
63
+ * @param settings - The weather settings to use, defaults to the configured ones
64
+ * @returns - The fetcher, or `undefined` when there is no location or no key to work with
65
+ */
66
+ static createOpenWeatherFetcher(settings?: iWeatherSettings | undefined): WeatherDaySummaryFetcher | undefined;
67
+ /**
68
+ * The days of the fitting window, oldest first, **including the running day**. The running day is in there
69
+ * on purpose: the gate reads today's cloud cover and maximum temperature out of the same table and through
70
+ * the same reader as the history, so that both sides of the fit are the same quantity from the same field.
71
+ * That is one extra call per run - the alternative was the gate deriving today's figures from a different
72
+ * product, which measurably is not the same number.
73
+ * @param referenceDate - The running day
74
+ * @param historyWindowDays - Length of the history window in days
75
+ * @returns - `historyWindowDays + 1` day starts, the last of which is the running day
76
+ */
77
+ private static windowDays;
78
+ private static fetchDaySummary;
79
+ private static parseDaySummary;
80
+ /**
81
+ * Establishes one field of the answer as a reading: a real number within the band its quantity can occupy.
82
+ *
83
+ * `typeof` rather than `Number(...)`: a value that has to be converted first is not a reading of the
84
+ * quantity, and converting would turn `null` into 0 and `""` into 0 - both of which pass every band. The
85
+ * band itself catches what is numeric but cannot be the quantity, `NaN` and the infinities included, since
86
+ * neither compares inside it.
87
+ * @param value - The field as it arrived
88
+ * @param min - Lowest value the quantity can take
89
+ * @param max - Highest value the quantity can take
90
+ * @returns - The reading, or undefined when the field is not one
91
+ */
92
+ private static plausibleReading;
93
+ private static dayKey;
94
+ }