hoffmation-base 3.7.3 → 4.0.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 (131) hide show
  1. package/lib/devices/dachs/dachs.d.ts +79 -2
  2. package/lib/devices/dachs/dachs.js +214 -18
  3. package/lib/devices/devices.d.ts +36 -1
  4. package/lib/devices/devices.js +64 -0
  5. package/lib/devices/jsObject/jsObjectEnergyManager.d.ts +16 -1
  6. package/lib/devices/jsObject/jsObjectEnergyManager.js +23 -0
  7. package/lib/devices/victron/victron-device.d.ts +83 -1
  8. package/lib/devices/victron/victron-device.js +151 -2
  9. package/lib/devices/victron/victron-morning-release.d.ts +16 -0
  10. package/lib/devices/victron/victron-morning-release.js +2 -0
  11. package/lib/enums/DeviceCapability.d.ts +1 -0
  12. package/lib/enums/DeviceCapability.js +1 -0
  13. package/lib/interfaces/baseDevices/iAirQualityCollector.d.ts +2 -1
  14. package/lib/interfaces/baseDevices/iAirQualityReadings.d.ts +50 -0
  15. package/lib/interfaces/baseDevices/iAirQualityReadings.js +2 -0
  16. package/lib/interfaces/baseDevices/iAirQualitySensor.d.ts +1 -54
  17. package/lib/interfaces/baseDevices/iAirQualitySensor.js +0 -5
  18. package/lib/interfaces/baseDevices/iEnergyManager.d.ts +43 -0
  19. package/lib/interfaces/baseDevices/iHumidityCollector.d.ts +0 -1
  20. package/lib/interfaces/baseDevices/iHumidityCollector.js +0 -2
  21. package/lib/interfaces/baseDevices/iTemperatureCollector.d.ts +0 -1
  22. package/lib/interfaces/baseDevices/iTemperatureCollector.js +0 -2
  23. package/lib/interfaces/baseDevices/index.d.ts +4 -0
  24. package/lib/interfaces/baseDevices/index.js +4 -0
  25. package/lib/interfaces/baseDevices/undefinedAirQualityValue.d.ts +4 -0
  26. package/lib/interfaces/baseDevices/undefinedAirQualityValue.js +7 -0
  27. package/lib/interfaces/baseDevices/undefinedHumidityValue.d.ts +4 -0
  28. package/lib/interfaces/baseDevices/undefinedHumidityValue.js +7 -0
  29. package/lib/interfaces/baseDevices/undefinedTempValue.d.ts +4 -0
  30. package/lib/interfaces/baseDevices/undefinedTempValue.js +7 -0
  31. package/lib/interfaces/dachs/KeyListEntity.d.ts +0 -5
  32. package/lib/interfaces/dachs/KeyListEntityResponse.d.ts +6 -0
  33. package/lib/interfaces/dachs/KeyListEntityResponse.js +2 -0
  34. package/lib/interfaces/dachs/index.d.ts +1 -0
  35. package/lib/interfaces/dachs/index.js +1 -0
  36. package/lib/interfaces/deviceSettings/iDachsDeviceSettings.d.ts +15 -0
  37. package/lib/interfaces/iActuatorStateSample.d.ts +13 -0
  38. package/lib/interfaces/iActuatorStateSample.js +2 -0
  39. package/lib/interfaces/iBatteryLevelSample.d.ts +13 -0
  40. package/lib/interfaces/iBatteryLevelSample.js +2 -0
  41. package/lib/interfaces/iConsumptionWindowSample.d.ts +26 -0
  42. package/lib/interfaces/iConsumptionWindowSample.js +2 -0
  43. package/lib/interfaces/iDachsHistoryGateResult.d.ts +29 -0
  44. package/lib/interfaces/iDachsHistoryGateResult.js +2 -0
  45. package/lib/interfaces/iEnergyHistoryBasis.d.ts +26 -0
  46. package/lib/interfaces/iEnergyHistoryBasis.js +2 -0
  47. package/lib/interfaces/iEnergyHistoryEstimate.d.ts +21 -0
  48. package/lib/interfaces/iEnergyHistoryEstimate.js +2 -0
  49. package/lib/interfaces/iEnergyHistoryFeatures.d.ts +21 -0
  50. package/lib/interfaces/iEnergyHistoryFeatures.js +2 -0
  51. package/lib/interfaces/iEnergyHistoryModel.d.ts +21 -0
  52. package/lib/interfaces/iEnergyHistoryModel.js +2 -0
  53. package/lib/interfaces/iEnergyHistoryOptions.d.ts +29 -0
  54. package/lib/interfaces/iEnergyHistoryOptions.js +2 -0
  55. package/lib/interfaces/iEnergyHistoryOutlook.d.ts +41 -0
  56. package/lib/interfaces/iEnergyHistoryOutlook.js +2 -0
  57. package/lib/interfaces/iEnergyHistorySample.d.ts +19 -0
  58. package/lib/interfaces/iEnergyHistorySample.js +2 -0
  59. package/lib/interfaces/iFossilGeneratorRun.d.ts +22 -0
  60. package/lib/interfaces/iFossilGeneratorRun.js +2 -0
  61. package/lib/interfaces/iFossilGeneratorSource.d.ts +21 -0
  62. package/lib/interfaces/iFossilGeneratorSource.js +2 -0
  63. package/lib/interfaces/iMorningReserveVerdict.d.ts +38 -0
  64. package/lib/interfaces/iMorningReserveVerdict.js +2 -0
  65. package/lib/interfaces/iPersist.d.ts +51 -0
  66. package/lib/interfaces/iProjectedSocBand.d.ts +7 -0
  67. package/lib/interfaces/iProjectedSocBand.js +2 -0
  68. package/lib/interfaces/iWeatherDaySummary.d.ts +21 -0
  69. package/lib/interfaces/iWeatherDaySummary.js +2 -0
  70. package/lib/interfaces/index.d.ts +16 -0
  71. package/lib/interfaces/index.js +16 -0
  72. package/lib/interfaces/settings/iShutterSettings.d.ts +22 -1
  73. package/lib/services/dbo/actuator-state-row.d.ts +14 -0
  74. package/lib/services/dbo/actuator-state-row.js +2 -0
  75. package/lib/services/dbo/battery-level-row.d.ts +16 -0
  76. package/lib/services/dbo/battery-level-row.js +2 -0
  77. package/lib/services/dbo/energy-consumption-row.d.ts +16 -0
  78. package/lib/services/dbo/energy-consumption-row.js +2 -0
  79. package/lib/services/dbo/postgreSqlPersist.d.ts +48 -1
  80. package/lib/services/dbo/postgreSqlPersist.js +298 -28
  81. package/lib/services/dbo/weather-day-summary-row.d.ts +18 -0
  82. package/lib/services/dbo/weather-day-summary-row.js +2 -0
  83. package/lib/services/energy/energy-history-service.d.ts +168 -0
  84. package/lib/services/energy/energy-history-service.js +499 -0
  85. package/lib/services/energy/index.d.ts +1 -0
  86. package/lib/services/energy/index.js +17 -0
  87. package/lib/services/energy/plant-energy-dials.d.ts +22 -0
  88. package/lib/services/energy/plant-energy-dials.js +2 -0
  89. package/lib/services/https-service.d.ts +48 -2
  90. package/lib/services/https-service.js +136 -10
  91. package/lib/services/index.d.ts +1 -0
  92. package/lib/services/index.js +1 -0
  93. package/lib/services/time-callback-service.d.ts +12 -0
  94. package/lib/services/time-callback-service.js +16 -2
  95. package/lib/services/weather/index.d.ts +1 -0
  96. package/lib/services/weather/index.js +1 -0
  97. package/lib/services/weather/open-weather-day-summary.d.ts +21 -0
  98. package/lib/services/weather/open-weather-day-summary.js +2 -0
  99. package/lib/services/weather/weather-day-summary-fetcher.d.ts +6 -0
  100. package/lib/services/weather/weather-day-summary-fetcher.js +2 -0
  101. package/lib/services/weather/weather-history-backfill.d.ts +88 -0
  102. package/lib/services/weather/weather-history-backfill.js +228 -0
  103. package/lib/services/weather/weather-service.d.ts +31 -0
  104. package/lib/services/weather/weather-service.js +117 -20
  105. package/lib/settings-service-instance.js +5 -2
  106. package/lib/settingsObjects/deviceSettings/dachsSettings.d.ts +6 -0
  107. package/lib/settingsObjects/deviceSettings/dachsSettings.js +14 -1
  108. package/lib/settingsObjects/deviceSettings/shutterSettings.d.ts +6 -0
  109. package/lib/settingsObjects/deviceSettings/shutterSettings.js +12 -2
  110. package/lib/settingsObjects/deviceSettings/victronDeviceSettings.d.ts +116 -0
  111. package/lib/settingsObjects/deviceSettings/victronDeviceSettings.js +150 -5
  112. package/lib/tsconfig.tsbuildinfo +1 -1
  113. package/lib/utils/dayMs.d.ts +7 -0
  114. package/lib/utils/dayMs.js +10 -0
  115. package/lib/utils/energy-history-dials.d.ts +19 -0
  116. package/lib/utils/energy-history-dials.js +2 -0
  117. package/lib/utils/energy-history-utils.d.ts +143 -0
  118. package/lib/utils/energy-history-utils.js +470 -0
  119. package/lib/utils/energy-manager-utils.d.ts +123 -1
  120. package/lib/utils/energy-manager-utils.js +226 -0
  121. package/lib/utils/index.d.ts +4 -0
  122. package/lib/utils/index.js +4 -0
  123. package/lib/utils/model-shadow.d.ts +61 -0
  124. package/lib/utils/model-shadow.js +83 -0
  125. package/lib/utils/morning-reserve-dials.d.ts +21 -0
  126. package/lib/utils/morning-reserve-dials.js +2 -0
  127. package/lib/utils/morning-verdict.d.ts +5 -0
  128. package/lib/utils/morning-verdict.js +2 -0
  129. package/lib/utils/utils.d.ts +7 -1
  130. package/lib/utils/utils.js +15 -5
  131. package/package.json +3 -2
@@ -1,6 +1,128 @@
1
- import { iExcessEnergyConsumer } from '../interfaces';
1
+ import { iEnergyHistoryOutlook, iEnergyManager, iExcessEnergyConsumer, iMorningReserveVerdict } from '../interfaces';
2
2
  import { EnergyConsumerStateChange } from './energy-consumer-state-change';
3
3
  export declare class EnergyManagerUtils {
4
+ /**
5
+ * The plant's single reading of its recorded history, and the single record of what the model rung would
6
+ * have decided.
7
+ *
8
+ * Static, the way {@link Devices.energymanager} is a single place: there is one energy manager, so there is
9
+ * one history and one shadow. Two of either would mean the paid backfill twice a day and, after a week, two
10
+ * half samples of a measurement that only means anything whole.
11
+ *
12
+ * Held here rather than on a manager so that **every** manager can carry
13
+ * {@link iEnergyManager.morningOutlook} by delegating instead of copying - see
14
+ * {@link EnergyManagerUtils.turnOnAdditionalConsumer}, which is shared for the same reason.
15
+ */
16
+ private static _energyHistory;
17
+ private static _modelShadow;
18
+ /**
19
+ * Which manager the two above were built for. A different manager is a different plant reading, and
20
+ * carrying the old cache over would answer about the previous manager's battery.
21
+ */
22
+ private static _historyOwner;
23
+ /**
24
+ * Brings the plant's recorded history up to date, as far as anything has gone stale.
25
+ *
26
+ * Meant to be called from the manager's own loop, however fast that runs: each of the reads behind it
27
+ * throttles itself, so calling it every five seconds costs the same number of queries as calling it once an
28
+ * hour. A manager that states no dials reads nothing at all and pays no request quota.
29
+ * @param manager - The plant's energy manager.
30
+ */
31
+ static refreshEnergyHistory(manager: iEnergyManager): void;
32
+ /**
33
+ * What the plant's recorded history says about the coming morning low - the shared implementation of
34
+ * {@link iEnergyManager.morningOutlook}, so no manager has to write one of its own.
35
+ *
36
+ * **The one place the marker for "no charge level" is told from a charge level.** Run through the
37
+ * projection the marker moves the whole band, and on a clear morning that band still clears the reserve, so
38
+ * a consumer would be answered at exactly the charge level at which it must not be. A manager without a
39
+ * battery at all lands in the same branch, and rightly: neither offers a starting point for a projection.
40
+ *
41
+ * Pure arithmetic over what {@link refreshEnergyHistory} has already read, so a consumer may ask on every
42
+ * one of its own decisions without costing a query.
43
+ * @param manager - The plant's energy manager.
44
+ * @returns The outlook, or undefined while this manager can say nothing about the coming morning.
45
+ */
46
+ static morningOutlook(manager: iEnergyManager): iEnergyHistoryOutlook | undefined;
47
+ /**
48
+ * Whether the plant expects the coming morning to hold its reserve - the shared implementation of
49
+ * {@link iEnergyManager.morningReserveVerdict}.
50
+ *
51
+ * Four rungs, in the order they have to be checked in: the model free bound holds the reserve, the bound
52
+ * misses it and no sun is left to change that, the fitted band falls entirely on one side of the reserve,
53
+ * and no statement. A bound below the reserve while sun is still left is deliberately **not** a "misses":
54
+ * the yield of the remaining day can still carry the morning, and only the model can say whether it will.
55
+ *
56
+ * The first two rungs are arithmetic on the plant's own measured consumption and carry no unmeasured
57
+ * assumption, which is what {@link iMorningReserveVerdict.measured} marks them by. The third rests on a fit
58
+ * whose window length has never been checked against recorded data - it is reported so an operator can read
59
+ * what the model would have said, and consumers that move something leave it alone.
60
+ * @param manager - The plant's energy manager, whose settings state the two thresholds.
61
+ * @param outlook - What the plant said, or undefined while it can say nothing. Handed in rather than read
62
+ * again, so a caller that already read it judges the same moment it read.
63
+ * @returns The verdict, or undefined while there is no outlook or the manager states no thresholds to judge
64
+ * by - a verdict against a guessed reserve would read exactly like one against a stated reserve.
65
+ */
66
+ static morningReserveVerdict(manager: iEnergyManager, outlook: iEnergyHistoryOutlook | undefined): iMorningReserveVerdict | undefined;
67
+ /**
68
+ * Assembles one verdict out of the rung that reached it and the numbers it was reached on.
69
+ * @param outlook - What the plant said.
70
+ * @param reserve - The reserve in percent the rung measured against.
71
+ * @param holds - Whether the coming morning holds, misses, or cannot be judged.
72
+ * @param measured - Whether the rung needed no fitted model.
73
+ * @param band - The projected morning charge level at both edges, collapsed where no band was needed.
74
+ * @param reason - What made this rung answer.
75
+ * @returns The verdict.
76
+ */
77
+ private static verdict;
78
+ /**
79
+ * How the model free bound reads in a reason line - either its value or why there is none.
80
+ *
81
+ * The three ways it can be absent look alike from the outside and must not: an operator who reads "no
82
+ * consumption history" while the history is there looks in the wrong place.
83
+ * @param outlook - What the plant said.
84
+ * @returns The text to put into the reason.
85
+ */
86
+ private static boundText;
87
+ /**
88
+ * Why there is no band. Which of the three causes is named decides where the operator looks, and from the
89
+ * outside they look alike.
90
+ * @param outlook - What the plant said.
91
+ * @returns The text to put into the reason.
92
+ */
93
+ private static missingModelText;
94
+ /**
95
+ * Records what the rung that rests on the fitted model would have decided, without letting it move
96
+ * anything.
97
+ *
98
+ * The rung rests on a fit whose window length has never been measured against recorded data, and on
99
+ * synthetic data the delivered length scores worse than the trivial rule the line quotes. Reading a week of
100
+ * these lines is what turns that open question into a measurement - see {@link ModelShadow}.
101
+ *
102
+ * Measured against the plant's single reserve, so a week of lines is one sample: two tallies against two
103
+ * thresholds cannot be added.
104
+ * @param manager - The plant's energy manager.
105
+ * @param outlook - What the plant said, or undefined while it can say nothing.
106
+ * @param alreadyDecided - Whether a rung that needs no model already answered this situation. The model
107
+ * rung sits below such a rung, so a verdict taken where the other one decides is not a verdict this rung
108
+ * ever reaches in operation.
109
+ */
110
+ static observeModelShadow(manager: iEnergyManager, outlook: iEnergyHistoryOutlook | undefined, alreadyDecided: boolean): void;
111
+ /**
112
+ * The plant's history, built on first use and rebuilt when the manager it belongs to changes.
113
+ * @param manager - The plant's energy manager.
114
+ * @returns The service, or undefined while this manager states no dials to read a history with.
115
+ */
116
+ private static energyHistory;
117
+ /**
118
+ * A live view of the dials the history is read and fitted with.
119
+ *
120
+ * Getters rather than a snapshot: all three are editable at runtime, and a service holding the values of the
121
+ * moment of construction would answer on a window nobody configured any more.
122
+ * @param manager - The plant's energy manager.
123
+ * @returns The options, or undefined while the manager states none of them.
124
+ */
125
+ private static energyHistoryOptions;
4
126
  static turnOnAdditionalConsumer(excessEnergyConsumer: iExcessEnergyConsumer[], lastDeviceChange: EnergyConsumerStateChange | undefined): void | undefined | EnergyConsumerStateChange;
5
127
  static turnOffAdditionalConsumer(excessEnergyConsumer: iExcessEnergyConsumer[], lastDeviceChange: EnergyConsumerStateChange | undefined): void | undefined | EnergyConsumerStateChange;
6
128
  }
@@ -2,7 +2,233 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EnergyManagerUtils = void 0;
4
4
  const enums_1 = require("../enums");
5
+ const services_1 = require("../services");
6
+ const model_shadow_1 = require("./model-shadow");
7
+ const utils_1 = require("./utils");
8
+ /**
9
+ * The subject the plant's shadow line names itself by. Deliberately not a manager's name: the record belongs
10
+ * to the plant, and whichever manager it happens to run under, an operator reads one line about one sample.
11
+ */
12
+ const MODEL_SHADOW_SUBJECT = 'energy manager model rung';
5
13
  class EnergyManagerUtils {
14
+ /**
15
+ * Brings the plant's recorded history up to date, as far as anything has gone stale.
16
+ *
17
+ * Meant to be called from the manager's own loop, however fast that runs: each of the reads behind it
18
+ * throttles itself, so calling it every five seconds costs the same number of queries as calling it once an
19
+ * hour. A manager that states no dials reads nothing at all and pays no request quota.
20
+ * @param manager - The plant's energy manager.
21
+ */
22
+ static refreshEnergyHistory(manager) {
23
+ var _a;
24
+ (_a = EnergyManagerUtils.energyHistory(manager)) === null || _a === void 0 ? void 0 : _a.refresh();
25
+ }
26
+ /**
27
+ * What the plant's recorded history says about the coming morning low - the shared implementation of
28
+ * {@link iEnergyManager.morningOutlook}, so no manager has to write one of its own.
29
+ *
30
+ * **The one place the marker for "no charge level" is told from a charge level.** Run through the
31
+ * projection the marker moves the whole band, and on a clear morning that band still clears the reserve, so
32
+ * a consumer would be answered at exactly the charge level at which it must not be. A manager without a
33
+ * battery at all lands in the same branch, and rightly: neither offers a starting point for a projection.
34
+ *
35
+ * Pure arithmetic over what {@link refreshEnergyHistory} has already read, so a consumer may ask on every
36
+ * one of its own decisions without costing a query.
37
+ * @param manager - The plant's energy manager.
38
+ * @returns The outlook, or undefined while this manager can say nothing about the coming morning.
39
+ */
40
+ static morningOutlook(manager) {
41
+ var _a;
42
+ const currentSoc = manager.batteryLevel;
43
+ if (currentSoc === undefined || currentSoc < 0) {
44
+ return undefined;
45
+ }
46
+ return (_a = EnergyManagerUtils.energyHistory(manager)) === null || _a === void 0 ? void 0 : _a.outlook(currentSoc, new Date());
47
+ }
48
+ /**
49
+ * Whether the plant expects the coming morning to hold its reserve - the shared implementation of
50
+ * {@link iEnergyManager.morningReserveVerdict}.
51
+ *
52
+ * Four rungs, in the order they have to be checked in: the model free bound holds the reserve, the bound
53
+ * misses it and no sun is left to change that, the fitted band falls entirely on one side of the reserve,
54
+ * and no statement. A bound below the reserve while sun is still left is deliberately **not** a "misses":
55
+ * the yield of the remaining day can still carry the morning, and only the model can say whether it will.
56
+ *
57
+ * The first two rungs are arithmetic on the plant's own measured consumption and carry no unmeasured
58
+ * assumption, which is what {@link iMorningReserveVerdict.measured} marks them by. The third rests on a fit
59
+ * whose window length has never been checked against recorded data - it is reported so an operator can read
60
+ * what the model would have said, and consumers that move something leave it alone.
61
+ * @param manager - The plant's energy manager, whose settings state the two thresholds.
62
+ * @param outlook - What the plant said, or undefined while it can say nothing. Handed in rather than read
63
+ * again, so a caller that already read it judges the same moment it read.
64
+ * @returns The verdict, or undefined while there is no outlook or the manager states no thresholds to judge
65
+ * by - a verdict against a guessed reserve would read exactly like one against a stated reserve.
66
+ */
67
+ static morningReserveVerdict(manager, outlook) {
68
+ if (outlook === undefined) {
69
+ return undefined;
70
+ }
71
+ const dials = manager.settings;
72
+ const reserve = dials.minimumMorningSocReserve;
73
+ const noSunThresholdHours = dials.noSunThresholdHours;
74
+ if (reserve === undefined || noSunThresholdHours === undefined) {
75
+ return undefined;
76
+ }
77
+ const collapsed = { lower: outlook.currentSoc, upper: outlook.currentSoc };
78
+ const boundText = EnergyManagerUtils.boundText(outlook);
79
+ const worstCaseLowSoc = outlook.worstCaseLowSoc;
80
+ if (worstCaseLowSoc !== undefined) {
81
+ if (worstCaseLowSoc >= reserve) {
82
+ return EnergyManagerUtils.verdict(outlook, reserve, true, true, collapsed, `${boundText} holds the reserve without a model`);
83
+ }
84
+ if (outlook.remainingSunHours < noSunThresholdHours) {
85
+ return EnergyManagerUtils.verdict(outlook, reserve, false, true, collapsed, `${boundText} misses the reserve and only ${utils_1.Utils.round(outlook.remainingSunHours, 2)}h of sun are left`);
86
+ }
87
+ }
88
+ const band = outlook.band;
89
+ if (band === undefined) {
90
+ return EnergyManagerUtils.verdict(outlook, reserve, undefined, false, collapsed, `no statement; ${EnergyManagerUtils.missingModelText(outlook)}; ${boundText}, ` +
91
+ `${utils_1.Utils.round(outlook.remainingSunHours, 2)}h of sun left`);
92
+ }
93
+ if (band.lower >= reserve) {
94
+ return EnergyManagerUtils.verdict(outlook, reserve, true, false, band, 'the lower band edge holds the reserve');
95
+ }
96
+ if (band.upper < reserve) {
97
+ return EnergyManagerUtils.verdict(outlook, reserve, false, false, band, 'the upper band edge misses the reserve');
98
+ }
99
+ return EnergyManagerUtils.verdict(outlook, reserve, undefined, false, band, `the reserve lies inside the band; ${boundText}`);
100
+ }
101
+ /**
102
+ * Assembles one verdict out of the rung that reached it and the numbers it was reached on.
103
+ * @param outlook - What the plant said.
104
+ * @param reserve - The reserve in percent the rung measured against.
105
+ * @param holds - Whether the coming morning holds, misses, or cannot be judged.
106
+ * @param measured - Whether the rung needed no fitted model.
107
+ * @param band - The projected morning charge level at both edges, collapsed where no band was needed.
108
+ * @param reason - What made this rung answer.
109
+ * @returns The verdict.
110
+ */
111
+ static verdict(outlook, reserve, holds, measured, band, reason) {
112
+ return {
113
+ holds,
114
+ measured,
115
+ modelFitted: outlook.basis.modelFitted,
116
+ reason,
117
+ currentSoc: outlook.currentSoc,
118
+ band,
119
+ reserve,
120
+ sampleDays: outlook.sampleDays,
121
+ };
122
+ }
123
+ /**
124
+ * How the model free bound reads in a reason line - either its value or why there is none.
125
+ *
126
+ * The three ways it can be absent look alike from the outside and must not: an operator who reads "no
127
+ * consumption history" while the history is there looks in the wrong place.
128
+ * @param outlook - What the plant said.
129
+ * @returns The text to put into the reason.
130
+ */
131
+ static boundText(outlook) {
132
+ if (outlook.worstCaseLowSoc !== undefined) {
133
+ return `worst case low ${utils_1.Utils.round(outlook.worstCaseLowSoc, 2)}%`;
134
+ }
135
+ if (!outlook.basis.batteryCapacityKnown) {
136
+ return 'no battery capacity reported';
137
+ }
138
+ if (outlook.basis.consumptionWindows === 0) {
139
+ return outlook.basis.consumptionReadingsSeen ? 'no usable consumption window' : 'no consumption history';
140
+ }
141
+ return (`only ${outlook.basis.consumptionWindows} of ${outlook.basis.requiredConsumptionWindows} ` +
142
+ 'required consumption windows');
143
+ }
144
+ /**
145
+ * Why there is no band. Which of the three causes is named decides where the operator looks, and from the
146
+ * outside they look alike.
147
+ * @param outlook - What the plant said.
148
+ * @returns The text to put into the reason.
149
+ */
150
+ static missingModelText(outlook) {
151
+ if (!outlook.basis.weatherTodayKnown) {
152
+ return 'no weather aggregate for the running day';
153
+ }
154
+ return outlook.basis.consumptionTodayKnown ? 'no fitted model' : 'no consumption reading for the running day';
155
+ }
156
+ /**
157
+ * Records what the rung that rests on the fitted model would have decided, without letting it move
158
+ * anything.
159
+ *
160
+ * The rung rests on a fit whose window length has never been measured against recorded data, and on
161
+ * synthetic data the delivered length scores worse than the trivial rule the line quotes. Reading a week of
162
+ * these lines is what turns that open question into a measurement - see {@link ModelShadow}.
163
+ *
164
+ * Measured against the plant's single reserve, so a week of lines is one sample: two tallies against two
165
+ * thresholds cannot be added.
166
+ * @param manager - The plant's energy manager.
167
+ * @param outlook - What the plant said, or undefined while it can say nothing.
168
+ * @param alreadyDecided - Whether a rung that needs no model already answered this situation. The model
169
+ * rung sits below such a rung, so a verdict taken where the other one decides is not a verdict this rung
170
+ * ever reaches in operation.
171
+ */
172
+ static observeModelShadow(manager, outlook, alreadyDecided) {
173
+ if (alreadyDecided || (outlook === null || outlook === void 0 ? void 0 : outlook.band) === undefined) {
174
+ return;
175
+ }
176
+ const dials = manager.settings;
177
+ const reserve = dials.minimumMorningSocReserve;
178
+ const shadow = EnergyManagerUtils._modelShadow;
179
+ if (reserve === undefined || shadow === undefined) {
180
+ // No yardstick is no measurement. Nothing is invented in its place: a rate against a guessed threshold
181
+ // would look exactly like a rate against a stated one.
182
+ return;
183
+ }
184
+ shadow.observe(outlook.band.lower >= reserve ? 'holds' : 'misses', outlook.currentSoc, `lower band edge ${utils_1.Utils.round(outlook.band.lower, 2)}% against reserve ${reserve}% ` +
185
+ `(soc ${utils_1.Utils.round(outlook.currentSoc, 2)}%, ${outlook.sampleDays} days)`);
186
+ }
187
+ /**
188
+ * The plant's history, built on first use and rebuilt when the manager it belongs to changes.
189
+ * @param manager - The plant's energy manager.
190
+ * @returns The service, or undefined while this manager states no dials to read a history with.
191
+ */
192
+ static energyHistory(manager) {
193
+ if (EnergyManagerUtils._historyOwner === manager) {
194
+ return EnergyManagerUtils._energyHistory;
195
+ }
196
+ const options = EnergyManagerUtils.energyHistoryOptions(manager);
197
+ EnergyManagerUtils._historyOwner = manager;
198
+ EnergyManagerUtils._energyHistory =
199
+ options === undefined ? undefined : new services_1.EnergyHistoryService(options, manager.log.bind(manager));
200
+ EnergyManagerUtils._modelShadow =
201
+ options === undefined ? undefined : new model_shadow_1.ModelShadow(MODEL_SHADOW_SUBJECT, manager.log.bind(manager));
202
+ return EnergyManagerUtils._energyHistory;
203
+ }
204
+ /**
205
+ * A live view of the dials the history is read and fitted with.
206
+ *
207
+ * Getters rather than a snapshot: all three are editable at runtime, and a service holding the values of the
208
+ * moment of construction would answer on a window nobody configured any more.
209
+ * @param manager - The plant's energy manager.
210
+ * @returns The options, or undefined while the manager states none of them.
211
+ */
212
+ static energyHistoryOptions(manager) {
213
+ const dials = manager.settings;
214
+ if ((dials === null || dials === void 0 ? void 0 : dials.historyWindowDays) === undefined ||
215
+ dials.historyMinimumDays === undefined ||
216
+ dials.historyBandSigma === undefined) {
217
+ return undefined;
218
+ }
219
+ const stated = dials;
220
+ return {
221
+ get windowDays() {
222
+ return stated.historyWindowDays;
223
+ },
224
+ get minimumModelDays() {
225
+ return stated.historyMinimumDays;
226
+ },
227
+ get bandSigma() {
228
+ return stated.historyBandSigma;
229
+ },
230
+ };
231
+ }
6
232
  static turnOnAdditionalConsumer(excessEnergyConsumer, lastDeviceChange) {
7
233
  const potentialDevices = excessEnergyConsumer.filter((e) => {
8
234
  if (e.energySettings.priority === -1 || e.on || !e.isAvailableForExcessEnergy()) {
@@ -1,6 +1,10 @@
1
1
  export * from './catchEmResult';
2
2
  export * from './RGB';
3
+ export * from './dayMs';
3
4
  export * from './utils';
4
5
  export * from './ringStorage';
5
6
  export * from './energy-manager-utils';
7
+ export * from './energy-history-utils';
8
+ export * from './model-shadow';
9
+ export * from './morning-verdict';
6
10
  export * from './energy-consumer-state-change';
@@ -16,7 +16,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./catchEmResult"), exports);
18
18
  __exportStar(require("./RGB"), exports);
19
+ __exportStar(require("./dayMs"), exports);
19
20
  __exportStar(require("./utils"), exports);
20
21
  __exportStar(require("./ringStorage"), exports);
21
22
  __exportStar(require("./energy-manager-utils"), exports);
23
+ __exportStar(require("./energy-history-utils"), exports);
24
+ __exportStar(require("./model-shadow"), exports);
25
+ __exportStar(require("./morning-verdict"), exports);
22
26
  __exportStar(require("./energy-consumer-state-change"), exports);
@@ -0,0 +1,61 @@
1
+ import { LogLevel } from '../enums';
2
+ import { MorningVerdict } from './morning-verdict';
3
+ /**
4
+ * Runs a decision stage alongside the stage that actually decides, and records what it would have decided.
5
+ *
6
+ * **Why a stage runs here instead of being switched on.** The length of the fitting window has never been
7
+ * measured against recorded data, and on synthetic data the delivered length scores worse than
8
+ * {@link ModelShadow.trivialSuppressSoc} - a single comparison against the state of charge. A stage that may
9
+ * be worse than that comparison must not move an actuator, and one that is better cannot be recognised as
10
+ * better without the comparison being made. After a week of operation the log holds enough lines to read the
11
+ * agreement rate off this plant's real days, which is what turns the open question into a measurement.
12
+ *
13
+ * **One per plant.** The measurement is a count of agreements over a week, so a second shadow beside the first
14
+ * halves the evidence rather than doubling it - and against a second yardstick the two rates cannot even be
15
+ * added. The energy manager holds the plant's one shadow and measures against the plant's one reserve.
16
+ */
17
+ export declare class ModelShadow {
18
+ private readonly subject;
19
+ private readonly log;
20
+ /**
21
+ * The state of charge in percent at or above which the trivial rule calls the coming morning safe.
22
+ *
23
+ * Deliberately not a setting. It is the yardstick the model is measured against, and a yardstick an
24
+ * installation can turn measures nothing: two plants would report agreement rates that cannot be compared,
25
+ * and a rate could be improved by moving the yardstick rather than the model.
26
+ */
27
+ static readonly trivialSuppressSoc: number;
28
+ private _agreements;
29
+ private _divergences;
30
+ /**
31
+ * The last pairing of the two verdicts, which is what the line is throttled on - not the line itself. The
32
+ * line carries the running tally and would therefore differ on every single evaluation.
33
+ */
34
+ private _lastPairing;
35
+ /**
36
+ * Builds a shadow for one stage. It holds no timer and reaches for nothing - it is only written to.
37
+ * @param subject - What the line names the observing stage by, so the operator sees which stage was read.
38
+ * @param log - Where the line goes; the device's own logger, so the line carries the device.
39
+ */
40
+ constructor(subject: string, log: (level: LogLevel, message: string) => void);
41
+ /**
42
+ * How often the model stage and the trivial rule reached the same verdict so far.
43
+ * @returns The count since this process started.
44
+ */
45
+ get agreements(): number;
46
+ /**
47
+ * How often the model stage and the trivial rule parted so far.
48
+ * @returns The count since this process started.
49
+ */
50
+ get divergences(): number;
51
+ /**
52
+ * Records one verdict of the model stage against what the trivial rule would have said, and reports it
53
+ * whenever the pairing of the two changed - the loops this sits in run every few seconds, and a line per
54
+ * pass would bury the change nobody must miss. The running tally rides along on every line, so the newest
55
+ * line alone answers the whole week.
56
+ * @param verdict - What the model stage says about the coming morning.
57
+ * @param currentSoc - The state of charge in percent the trivial rule is evaluated on.
58
+ * @param detail - The numbers the model stage reached its verdict on, for the operator.
59
+ */
60
+ observe(verdict: MorningVerdict, currentSoc: number, detail: string): void;
61
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModelShadow = void 0;
4
+ const enums_1 = require("../enums");
5
+ const utils_1 = require("./utils");
6
+ /**
7
+ * Runs a decision stage alongside the stage that actually decides, and records what it would have decided.
8
+ *
9
+ * **Why a stage runs here instead of being switched on.** The length of the fitting window has never been
10
+ * measured against recorded data, and on synthetic data the delivered length scores worse than
11
+ * {@link ModelShadow.trivialSuppressSoc} - a single comparison against the state of charge. A stage that may
12
+ * be worse than that comparison must not move an actuator, and one that is better cannot be recognised as
13
+ * better without the comparison being made. After a week of operation the log holds enough lines to read the
14
+ * agreement rate off this plant's real days, which is what turns the open question into a measurement.
15
+ *
16
+ * **One per plant.** The measurement is a count of agreements over a week, so a second shadow beside the first
17
+ * halves the evidence rather than doubling it - and against a second yardstick the two rates cannot even be
18
+ * added. The energy manager holds the plant's one shadow and measures against the plant's one reserve.
19
+ */
20
+ class ModelShadow {
21
+ /**
22
+ * Builds a shadow for one stage. It holds no timer and reaches for nothing - it is only written to.
23
+ * @param subject - What the line names the observing stage by, so the operator sees which stage was read.
24
+ * @param log - Where the line goes; the device's own logger, so the line carries the device.
25
+ */
26
+ constructor(subject, log) {
27
+ this.subject = subject;
28
+ this.log = log;
29
+ this._agreements = 0;
30
+ this._divergences = 0;
31
+ }
32
+ /**
33
+ * How often the model stage and the trivial rule reached the same verdict so far.
34
+ * @returns The count since this process started.
35
+ */
36
+ get agreements() {
37
+ return this._agreements;
38
+ }
39
+ /**
40
+ * How often the model stage and the trivial rule parted so far.
41
+ * @returns The count since this process started.
42
+ */
43
+ get divergences() {
44
+ return this._divergences;
45
+ }
46
+ /**
47
+ * Records one verdict of the model stage against what the trivial rule would have said, and reports it
48
+ * whenever the pairing of the two changed - the loops this sits in run every few seconds, and a line per
49
+ * pass would bury the change nobody must miss. The running tally rides along on every line, so the newest
50
+ * line alone answers the whole week.
51
+ * @param verdict - What the model stage says about the coming morning.
52
+ * @param currentSoc - The state of charge in percent the trivial rule is evaluated on.
53
+ * @param detail - The numbers the model stage reached its verdict on, for the operator.
54
+ */
55
+ observe(verdict, currentSoc, detail) {
56
+ const trivial = currentSoc >= ModelShadow.trivialSuppressSoc ? 'holds' : 'misses';
57
+ const agreed = trivial === verdict;
58
+ if (agreed) {
59
+ this._agreements++;
60
+ }
61
+ else {
62
+ this._divergences++;
63
+ }
64
+ const pairing = `${verdict}/${trivial}`;
65
+ if (pairing === this._lastPairing) {
66
+ return;
67
+ }
68
+ this._lastPairing = pairing;
69
+ this.log(enums_1.LogLevel.Info, `Model shadow (${this.subject}): the model says the coming morning ${verdict}, the trivial rule at ` +
70
+ `${ModelShadow.trivialSuppressSoc}% says it ${trivial} --> they ${agreed ? 'agree' : 'part'}; ` +
71
+ `${this._agreements} agreed, ${this._divergences} parted so far; ` +
72
+ `soc ${utils_1.Utils.round(currentSoc, 2)}%, ${detail}`);
73
+ }
74
+ }
75
+ exports.ModelShadow = ModelShadow;
76
+ /**
77
+ * The state of charge in percent at or above which the trivial rule calls the coming morning safe.
78
+ *
79
+ * Deliberately not a setting. It is the yardstick the model is measured against, and a yardstick an
80
+ * installation can turn measures nothing: two plants would report agreement rates that cannot be compared,
81
+ * and a rate could be improved by moving the yardstick rather than the model.
82
+ */
83
+ ModelShadow.trivialSuppressSoc = 55;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The two thresholds the plant judges its coming morning by, read structurally off the energy manager's
3
+ * settings.
4
+ *
5
+ * Both describe the battery and its yield rather than whoever asks about them: "how low may the morning get"
6
+ * is a property of the battery, and "how little sun is no sun" a property of the plant's photovoltaic. Two
7
+ * consumers differ in how sure they want to be before they act, not in what the morning has to hold.
8
+ *
9
+ * Optional, because a manager that states neither cannot judge and none is invented for it - a verdict against
10
+ * a guessed reserve reads exactly like one against a stated reserve. Without them the plant says nothing and
11
+ * every consumer falls back to what needs no history.
12
+ *
13
+ * Not part of the published surface: it describes how the shared implementation reads a manager, not something
14
+ * a consumer of the library implements.
15
+ */
16
+ export interface iMorningReserveDials {
17
+ /** The charge level in percent the coming morning's low is expected to stay above */
18
+ minimumMorningSocReserve?: number;
19
+ /** Below this many hours of sun left, no further yield is expected to change the outcome */
20
+ noSunThresholdHours?: number;
21
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * What a decision stage predicts about the coming morning low: whether the state of charge will still clear
3
+ * the reserve by then.
4
+ */
5
+ export type MorningVerdict = 'holds' | 'misses';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,7 +1,6 @@
1
1
  import { CatchEmResult } from './catchEmResult';
2
2
  import { iTimePair } from '../interfaces';
3
3
  import { RGB } from './RGB';
4
- export declare const DAYMS: number;
5
4
  export declare class Utils {
6
5
  static get timeTilMidnight(): number;
7
6
  static catchEm<T>(promise: Promise<T>): Promise<CatchEmResult<T>>;
@@ -23,6 +22,13 @@ export declare class Utils {
23
22
  static nowTime(): iTimePair;
24
23
  static dateByTimeSpan(hours: number, minutes: number, now?: Date): Date;
25
24
  static positiveMod(number: number, mod: number): number;
25
+ /**
26
+ * Calculates the shortest angular distance between two degree values.
27
+ * @param degreeA - The first degree value
28
+ * @param degreeB - The second degree value
29
+ * @returns The distance in degrees, always between 0 and 180
30
+ */
31
+ static degreeDistance(degreeA: number, degreeB: number): number;
26
32
  static degreeInBetween(minDegree: number, maxDegree: number, degreeToCheck: number): boolean;
27
33
  private static deepOmit;
28
34
  static nextMatchingDate(hours?: number, minutes?: number, now?: Date): Date;
@@ -3,16 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Utils = exports.DAYMS = void 0;
6
+ exports.Utils = void 0;
7
7
  const lodash_1 = __importDefault(require("lodash"));
8
8
  const logging_1 = require("../logging");
9
9
  const enums_1 = require("../enums");
10
10
  const i18n_1 = require("../i18n");
11
11
  const settings_service_1 = require("../settings-service");
12
- exports.DAYMS = 24 * 60 * 60 * 1000;
12
+ const dayMs_1 = require("./dayMs");
13
13
  class Utils {
14
14
  static get timeTilMidnight() {
15
- return new Date(Utils.nowMS() + exports.DAYMS).setHours(0, 0, 0, 0) - Utils.nowMS();
15
+ return new Date(Utils.nowMS() + dayMs_1.DAYMS).setHours(0, 0, 0, 0) - Utils.nowMS();
16
16
  }
17
17
  static async catchEm(promise) {
18
18
  return promise
@@ -175,8 +175,8 @@ class Utils {
175
175
  static beetweenDays(date, startDay, endDay) {
176
176
  const yearStart = new Date(date.getTime());
177
177
  yearStart.setMonth(0, 1);
178
- const startDate = new Date(yearStart.getTime() + startDay * exports.DAYMS);
179
- const endDate = new Date(yearStart.getTime() + endDay * exports.DAYMS);
178
+ const startDate = new Date(yearStart.getTime() + startDay * dayMs_1.DAYMS);
179
+ const endDate = new Date(yearStart.getTime() + endDay * dayMs_1.DAYMS);
180
180
  return date <= endDate && date >= startDate;
181
181
  }
182
182
  static nowTime() {
@@ -189,6 +189,16 @@ class Utils {
189
189
  static positiveMod(number, mod) {
190
190
  return ((number % mod) + mod) % mod;
191
191
  }
192
+ /**
193
+ * Calculates the shortest angular distance between two degree values.
194
+ * @param degreeA - The first degree value
195
+ * @param degreeB - The second degree value
196
+ * @returns The distance in degrees, always between 0 and 180
197
+ */
198
+ static degreeDistance(degreeA, degreeB) {
199
+ const delta = this.positiveMod(degreeA - degreeB, 360);
200
+ return delta > 180 ? 360 - delta : delta;
201
+ }
192
202
  static degreeInBetween(minDegree, maxDegree, degreeToCheck) {
193
203
  const modMin = this.positiveMod(minDegree, 360);
194
204
  const modMax = this.positiveMod(maxDegree, 360);