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,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
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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', () =>
|
|
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
|
-
|
|
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;
|
package/lib/services/index.d.ts
CHANGED
package/lib/services/index.js
CHANGED
|
@@ -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 = (
|
|
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
|
|
169
|
+
return (this.nextSunSet.getTime() - utils_1.Utils.nowMS()) / (1000 * 60 * 60);
|
|
156
170
|
}
|
|
157
171
|
}
|
|
158
172
|
exports.TimeCallbackService = TimeCallbackService;
|
|
@@ -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,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,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
|
+
}
|