hoffmation-base 3.8.0 → 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 (127) 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/services/dbo/actuator-state-row.d.ts +14 -0
  73. package/lib/services/dbo/actuator-state-row.js +2 -0
  74. package/lib/services/dbo/battery-level-row.d.ts +16 -0
  75. package/lib/services/dbo/battery-level-row.js +2 -0
  76. package/lib/services/dbo/energy-consumption-row.d.ts +16 -0
  77. package/lib/services/dbo/energy-consumption-row.js +2 -0
  78. package/lib/services/dbo/postgreSqlPersist.d.ts +48 -1
  79. package/lib/services/dbo/postgreSqlPersist.js +298 -28
  80. package/lib/services/dbo/weather-day-summary-row.d.ts +18 -0
  81. package/lib/services/dbo/weather-day-summary-row.js +2 -0
  82. package/lib/services/energy/energy-history-service.d.ts +168 -0
  83. package/lib/services/energy/energy-history-service.js +499 -0
  84. package/lib/services/energy/index.d.ts +1 -0
  85. package/lib/services/energy/index.js +17 -0
  86. package/lib/services/energy/plant-energy-dials.d.ts +22 -0
  87. package/lib/services/energy/plant-energy-dials.js +2 -0
  88. package/lib/services/https-service.d.ts +48 -2
  89. package/lib/services/https-service.js +136 -10
  90. package/lib/services/index.d.ts +1 -0
  91. package/lib/services/index.js +1 -0
  92. package/lib/services/time-callback-service.d.ts +12 -0
  93. package/lib/services/time-callback-service.js +16 -2
  94. package/lib/services/weather/index.d.ts +1 -0
  95. package/lib/services/weather/index.js +1 -0
  96. package/lib/services/weather/open-weather-day-summary.d.ts +21 -0
  97. package/lib/services/weather/open-weather-day-summary.js +2 -0
  98. package/lib/services/weather/weather-day-summary-fetcher.d.ts +6 -0
  99. package/lib/services/weather/weather-day-summary-fetcher.js +2 -0
  100. package/lib/services/weather/weather-history-backfill.d.ts +88 -0
  101. package/lib/services/weather/weather-history-backfill.js +228 -0
  102. package/lib/services/weather/weather-service.js +29 -4
  103. package/lib/settings-service-instance.js +5 -2
  104. package/lib/settingsObjects/deviceSettings/dachsSettings.d.ts +6 -0
  105. package/lib/settingsObjects/deviceSettings/dachsSettings.js +14 -1
  106. package/lib/settingsObjects/deviceSettings/victronDeviceSettings.d.ts +116 -0
  107. package/lib/settingsObjects/deviceSettings/victronDeviceSettings.js +150 -5
  108. package/lib/tsconfig.tsbuildinfo +1 -1
  109. package/lib/utils/dayMs.d.ts +7 -0
  110. package/lib/utils/dayMs.js +10 -0
  111. package/lib/utils/energy-history-dials.d.ts +19 -0
  112. package/lib/utils/energy-history-dials.js +2 -0
  113. package/lib/utils/energy-history-utils.d.ts +143 -0
  114. package/lib/utils/energy-history-utils.js +470 -0
  115. package/lib/utils/energy-manager-utils.d.ts +123 -1
  116. package/lib/utils/energy-manager-utils.js +226 -0
  117. package/lib/utils/index.d.ts +4 -0
  118. package/lib/utils/index.js +4 -0
  119. package/lib/utils/model-shadow.d.ts +61 -0
  120. package/lib/utils/model-shadow.js +83 -0
  121. package/lib/utils/morning-reserve-dials.d.ts +21 -0
  122. package/lib/utils/morning-reserve-dials.js +2 -0
  123. package/lib/utils/morning-verdict.d.ts +5 -0
  124. package/lib/utils/morning-verdict.js +2 -0
  125. package/lib/utils/utils.d.ts +0 -1
  126. package/lib/utils/utils.js +5 -5
  127. package/package.json +3 -2
@@ -1,9 +1,14 @@
1
- import { iBatteryDevice, iEnergyManager, iExcessEnergyConsumer, iJsonOmitKeys } from '../../interfaces';
1
+ import { iBatteryDevice, iEnergyHistoryOutlook, iEnergyManager, iExcessEnergyConsumer, iJsonOmitKeys, iMorningReserveVerdict } from '../../interfaces';
2
2
  import { VictronDeviceSettings } from '../../settingsObjects';
3
3
  import { Battery } from '../sharedFunctions';
4
4
  import { VictronDeviceData, VictronMqttConnectionOptions, VictronMqttConsumer } from 'victron-mqtt-consumer';
5
5
  import { BaseDevice } from '../BaseDevice';
6
6
  export declare class VictronDevice extends BaseDevice implements iEnergyManager, iBatteryDevice, iJsonOmitKeys {
7
+ /**
8
+ * The shortest horizon the battery reservation is spread over, in hours. Not a setting: it is a bound on
9
+ * the arithmetic rather than a dial of the plant - see {@link VictronDevice.reserveHorizonHours}.
10
+ */
11
+ private static readonly minimumReserveHorizonHours;
7
12
  /** @inheritDoc */
8
13
  readonly settings: VictronDeviceSettings;
9
14
  /** @inheritDoc */
@@ -17,11 +22,28 @@ export declare class VictronDevice extends BaseDevice implements iEnergyManager,
17
22
  private _lastPersistenceCalculation;
18
23
  private _nextPersistEntry;
19
24
  private _excessEnergy;
25
+ /** The last reported release state, so the five second loop writes a line on change, not on every pass. */
26
+ private _lastReleaseLine;
20
27
  constructor(opts: VictronMqttConnectionOptions);
21
28
  /** @inheritDoc */
22
29
  get acBlocked(): boolean;
23
30
  /** @inheritDoc */
24
31
  get batteryLevel(): number;
32
+ /**
33
+ * Delegated rather than written here, exactly the way this device delegates its consumer switching to
34
+ * {@link EnergyManagerUtils.turnOnAdditionalConsumer}: the answer is about the **plant**, and every energy
35
+ * manager owes the same one - so it is written once and no manager is left with a copy or a stub. What this
36
+ * device keeps for itself is the policy: the rungs, the reservation and the ac block.
37
+ * @inheritDoc
38
+ */
39
+ get morningOutlook(): iEnergyHistoryOutlook | undefined;
40
+ /**
41
+ * Delegated for the same reason {@link morningOutlook} is: the judgement is about the plant's battery and
42
+ * every manager owes the same one. What this device keeps for itself is what it *does* with the verdict -
43
+ * the reservation and the ac block below.
44
+ * @inheritDoc
45
+ */
46
+ get morningReserveVerdict(): iMorningReserveVerdict | undefined;
25
47
  get victronConsumer(): VictronMqttConsumer;
26
48
  get data(): VictronDeviceData;
27
49
  get excessEnergy(): number;
@@ -33,6 +55,7 @@ export declare class VictronDevice extends BaseDevice implements iEnergyManager,
33
55
  addExcessConsumer(device: iExcessEnergyConsumer): void;
34
56
  /** @inheritDoc */
35
57
  dispose(): void;
58
+ /** @inheritDoc */
36
59
  getReport(): string;
37
60
  recalculatePowerSharing(): void;
38
61
  toJSON(): Partial<VictronDevice>;
@@ -42,6 +65,65 @@ export declare class VictronDevice extends BaseDevice implements iEnergyManager,
42
65
  */
43
66
  setGridSetPoint(setPoint: number): void;
44
67
  private calculateExcessEnergy;
68
+ /**
69
+ * Rung 1, the only rung of the recorded history that decides: if the state of charge carries past the
70
+ * coming morning low even when no further yield arrives, nothing has to be held back and no consumer has to
71
+ * be kept off.
72
+ *
73
+ * This manager's own policy and rightly here rather than in the shared utils: what a plant does with the
74
+ * outlook is the manager's judgement, and the reservation and ac block it feeds are this device's.
75
+ *
76
+ * Arithmetic over the plant's own measured consumption - no fit, no weights, no window length. That is what
77
+ * separates it from the rung below, which measures against the lower edge of the fitted band and therefore
78
+ * only records (see {@link EnergyManagerUtils.observeModelShadow}).
79
+ * @param outlook - What the plant said, or undefined while it cannot say anything - which includes the
80
+ * absent charge level, so the guard against it stays in the shared utils alone.
81
+ * @returns The release, or undefined while the measured consumption cannot carry it.
82
+ */
83
+ private morningRelease;
84
+ /**
85
+ * How much of the production is held back for the battery.
86
+ *
87
+ * With the release nothing is held back: the coming morning clears the reserve on measured consumption
88
+ * alone. Without it the calculation is the one that was here before - it aims at a full battery by sunset,
89
+ * which over-reserves, and that is what an installation without a usable recorded history keeps.
90
+ * @param released - Whether rung 1 carried the coming morning.
91
+ * @param hoursTilSunset - The horizon the shortfall is spread over.
92
+ * @returns The wattage to hold back.
93
+ */
94
+ private neededBatteryWattage;
95
+ /**
96
+ * The horizon the shortfall is actually spread over: the remaining sun hours, but never less than
97
+ * {@link VictronDevice.minimumReserveHorizonHours}.
98
+ *
99
+ * The raw horizon is a difference against the next sunset and runs to zero, while the part of day this
100
+ * calculation is entered under is derived separately from today's sunset plus its own offsets - so the two
101
+ * do not turn at the same instant. In the gap the divisor is first tiny and then negative: a shortfall of a
102
+ * few percent becomes a rate in the megawatt range, at exactly zero an `Infinity` that leaves the process
103
+ * through `toJSON`, and past the turn a *negative* reservation that is added to the excess and offers
104
+ * energy the plant does not have. Every five seconds, and since the horizon is counted in hours rather than
105
+ * minutes, sixty times as large as when this was harmless.
106
+ *
107
+ * The bound is a rate bound, not a schedule: below half an hour of daylight there is nothing left to charge
108
+ * in, so the quotient stops describing a charging rate and only its magnitude still moves. Half an hour
109
+ * caps the reservation at twice the battery capacity per hour - still above anything the plant can take in,
110
+ * therefore never loosening a reservation that was meaningful, while keeping the value finite and signed
111
+ * the way the rest of the calculation assumes.
112
+ * @param hoursTilSunset - The raw remaining sun hours, which may be zero or negative.
113
+ * @returns The horizon to divide by, at least the minimum.
114
+ */
115
+ private reserveHorizonHours;
116
+ /**
117
+ * Reports the release whenever it changes, and never otherwise: this runs every five seconds, and a line
118
+ * per pass would bury the change nobody must miss. The very first pass is remembered but not reported while
119
+ * there is no release, so an installation whose recorded history holds nothing stays silent instead of
120
+ * announcing its unchanged behaviour once per start.
121
+ *
122
+ * Named after the reserve rather than after the reservation, because the same release also answers the ac
123
+ * block - and it does so around the clock, while the reservation only applies in daylight.
124
+ * @param release - The release of this pass, or undefined while the measured consumption cannot carry one.
125
+ */
126
+ private logMorningRelease;
45
127
  private turnOnAdditionalConsumer;
46
128
  private turnOffAdditionalConsumer;
47
129
  private persist;
@@ -50,6 +50,16 @@ class VictronDevice extends BaseDevice_1.BaseDevice {
50
50
  if (!this.settings.hasBattery) {
51
51
  return false;
52
52
  }
53
+ if (this.morningRelease(this.morningOutlook) !== undefined) {
54
+ // Where the plant's measured consumption alone shows the coming morning clears the reserve, that
55
+ // statement decides and the clock does not. The ladder below is a stand-in for the same question, and
56
+ // its sunny morning branch cannot answer the case it was built for: it reads the day's mean cloud
57
+ // cover, which a day that is dull in the morning and clears up at noon never falls below.
58
+ //
59
+ // Only the release direction. Blocking on the model would mean acting on the fitted band, which is
60
+ // exactly what the shadow exists to defer.
61
+ return false;
62
+ }
53
63
  const hours = new Date().getHours();
54
64
  if (hours > 18) {
55
65
  return this.batteryLevel < this.settings.minimumNightTimeAcBatteryLevel;
@@ -81,6 +91,25 @@ class VictronDevice extends BaseDevice_1.BaseDevice {
81
91
  }
82
92
  return level;
83
93
  }
94
+ /**
95
+ * Delegated rather than written here, exactly the way this device delegates its consumer switching to
96
+ * {@link EnergyManagerUtils.turnOnAdditionalConsumer}: the answer is about the **plant**, and every energy
97
+ * manager owes the same one - so it is written once and no manager is left with a copy or a stub. What this
98
+ * device keeps for itself is the policy: the rungs, the reservation and the ac block.
99
+ * @inheritDoc
100
+ */
101
+ get morningOutlook() {
102
+ return utils_1.EnergyManagerUtils.morningOutlook(this);
103
+ }
104
+ /**
105
+ * Delegated for the same reason {@link morningOutlook} is: the judgement is about the plant's battery and
106
+ * every manager owes the same one. What this device keeps for itself is what it *does* with the verdict -
107
+ * the reservation and the ac block below.
108
+ * @inheritDoc
109
+ */
110
+ get morningReserveVerdict() {
111
+ return utils_1.EnergyManagerUtils.morningReserveVerdict(this, this.morningOutlook);
112
+ }
84
113
  get victronConsumer() {
85
114
  return this._victronConsumer;
86
115
  }
@@ -124,8 +153,16 @@ class VictronDevice extends BaseDevice_1.BaseDevice {
124
153
  this._iCalculationInterval = null;
125
154
  }
126
155
  }
156
+ /** @inheritDoc */
127
157
  getReport() {
128
- return '';
158
+ var _a;
159
+ const response = [];
160
+ response.push(`Production: ${(_a = this.data.pvInverter.power) !== null && _a !== void 0 ? _a : 0}W`);
161
+ response.push(`Drawing Wattage: ${this.drawingWattage}W`);
162
+ response.push(`Injecting Wattage: ${this.injectingWattage}W`);
163
+ response.push(`Self Consuming Wattage: ${this.selfConsumingWattage}W`);
164
+ response.push(`Battery Level: ${this.batteryLevel}%`);
165
+ return response.join('\n');
129
166
  }
130
167
  recalculatePowerSharing() {
131
168
  this.calculateExcessEnergy();
@@ -167,6 +204,19 @@ class VictronDevice extends BaseDevice_1.BaseDevice {
167
204
  this.log(enums_1.LogLevel.Debug, 'No data available from Victron device.');
168
205
  return;
169
206
  }
207
+ // The manager's loop is what drives the plant's one reading, for every consumer of it. Asked, not
208
+ // scheduled: what it costs is decided by the data situation rather than by a setting, and each of the
209
+ // reads behind this throttles itself, so asking on every pass of the five second loop costs the same
210
+ // number of queries as asking once an hour.
211
+ utils_1.EnergyManagerUtils.refreshEnergyHistory(this);
212
+ // Read once and handed on, rather than each of the three asking again: the projection is arithmetic over
213
+ // one read, and three passes over it would still have to agree with each other.
214
+ const outlook = this.morningOutlook;
215
+ const release = this.morningRelease(outlook);
216
+ this.logMorningRelease(release);
217
+ // Fed from here and from nowhere else. `acBlocked` is a getter anyone may read at any rate, and a tally
218
+ // that counts reads rather than evaluations measures the callers instead of the model.
219
+ utils_1.EnergyManagerUtils.observeModelShadow(this, outlook, release !== undefined);
170
220
  // Step 1: Calculate battery need
171
221
  const hoursTilSunset = services_1.TimeCallbackService.hoursTilSunset();
172
222
  let neededBatteryWattage = 0;
@@ -176,7 +226,7 @@ class VictronDevice extends BaseDevice_1.BaseDevice {
176
226
  this.log(enums_1.LogLevel.Debug, 'No battery data available from Victron device.');
177
227
  return;
178
228
  }
179
- neededBatteryWattage = ((1 - this.batteryLevel / 100.0) * this.settings.batteryCapacityWattage) / hoursTilSunset;
229
+ neededBatteryWattage = this.neededBatteryWattage(release !== undefined, hoursTilSunset);
180
230
  // Step 2: Calculate expected solar output
181
231
  const solarOutput = (_a = this.data.pvInverter.power) !== null && _a !== void 0 ? _a : 0;
182
232
  // Step 3: Calculate expected base consumption
@@ -205,6 +255,100 @@ class VictronDevice extends BaseDevice_1.BaseDevice {
205
255
  }
206
256
  this.calculatePersistenceValues();
207
257
  }
258
+ /**
259
+ * Rung 1, the only rung of the recorded history that decides: if the state of charge carries past the
260
+ * coming morning low even when no further yield arrives, nothing has to be held back and no consumer has to
261
+ * be kept off.
262
+ *
263
+ * This manager's own policy and rightly here rather than in the shared utils: what a plant does with the
264
+ * outlook is the manager's judgement, and the reservation and ac block it feeds are this device's.
265
+ *
266
+ * Arithmetic over the plant's own measured consumption - no fit, no weights, no window length. That is what
267
+ * separates it from the rung below, which measures against the lower edge of the fitted band and therefore
268
+ * only records (see {@link EnergyManagerUtils.observeModelShadow}).
269
+ * @param outlook - What the plant said, or undefined while it cannot say anything - which includes the
270
+ * absent charge level, so the guard against it stays in the shared utils alone.
271
+ * @returns The release, or undefined while the measured consumption cannot carry it.
272
+ */
273
+ morningRelease(outlook) {
274
+ if (outlook === undefined) {
275
+ // The fallback holds back more, not less.
276
+ return undefined;
277
+ }
278
+ const reserve = this.settings.minimumMorningSocReserve;
279
+ const worstCaseLowSoc = outlook.worstCaseLowSoc;
280
+ if (worstCaseLowSoc === undefined || worstCaseLowSoc < reserve) {
281
+ return undefined;
282
+ }
283
+ return {
284
+ reason: `worst case low ${utils_1.Utils.round(worstCaseLowSoc, 2)}% holds the reserve ${reserve}% ` +
285
+ 'without any further yield',
286
+ };
287
+ }
288
+ /**
289
+ * How much of the production is held back for the battery.
290
+ *
291
+ * With the release nothing is held back: the coming morning clears the reserve on measured consumption
292
+ * alone. Without it the calculation is the one that was here before - it aims at a full battery by sunset,
293
+ * which over-reserves, and that is what an installation without a usable recorded history keeps.
294
+ * @param released - Whether rung 1 carried the coming morning.
295
+ * @param hoursTilSunset - The horizon the shortfall is spread over.
296
+ * @returns The wattage to hold back.
297
+ */
298
+ neededBatteryWattage(released, hoursTilSunset) {
299
+ if (released) {
300
+ return 0;
301
+ }
302
+ const missingShare = 1 - this.batteryLevel / 100.0;
303
+ return (missingShare * this.settings.batteryCapacityWattage) / this.reserveHorizonHours(hoursTilSunset);
304
+ }
305
+ /**
306
+ * The horizon the shortfall is actually spread over: the remaining sun hours, but never less than
307
+ * {@link VictronDevice.minimumReserveHorizonHours}.
308
+ *
309
+ * The raw horizon is a difference against the next sunset and runs to zero, while the part of day this
310
+ * calculation is entered under is derived separately from today's sunset plus its own offsets - so the two
311
+ * do not turn at the same instant. In the gap the divisor is first tiny and then negative: a shortfall of a
312
+ * few percent becomes a rate in the megawatt range, at exactly zero an `Infinity` that leaves the process
313
+ * through `toJSON`, and past the turn a *negative* reservation that is added to the excess and offers
314
+ * energy the plant does not have. Every five seconds, and since the horizon is counted in hours rather than
315
+ * minutes, sixty times as large as when this was harmless.
316
+ *
317
+ * The bound is a rate bound, not a schedule: below half an hour of daylight there is nothing left to charge
318
+ * in, so the quotient stops describing a charging rate and only its magnitude still moves. Half an hour
319
+ * caps the reservation at twice the battery capacity per hour - still above anything the plant can take in,
320
+ * therefore never loosening a reservation that was meaningful, while keeping the value finite and signed
321
+ * the way the rest of the calculation assumes.
322
+ * @param hoursTilSunset - The raw remaining sun hours, which may be zero or negative.
323
+ * @returns The horizon to divide by, at least the minimum.
324
+ */
325
+ reserveHorizonHours(hoursTilSunset) {
326
+ return Math.max(hoursTilSunset, VictronDevice.minimumReserveHorizonHours);
327
+ }
328
+ /**
329
+ * Reports the release whenever it changes, and never otherwise: this runs every five seconds, and a line
330
+ * per pass would bury the change nobody must miss. The very first pass is remembered but not reported while
331
+ * there is no release, so an installation whose recorded history holds nothing stays silent instead of
332
+ * announcing its unchanged behaviour once per start.
333
+ *
334
+ * Named after the reserve rather than after the reservation, because the same release also answers the ac
335
+ * block - and it does so around the clock, while the reservation only applies in daylight.
336
+ * @param release - The release of this pass, or undefined while the measured consumption cannot carry one.
337
+ */
338
+ logMorningRelease(release) {
339
+ const line = release === undefined
340
+ ? 'no model free release, aiming at a full battery by sunset'
341
+ : `released: ${release.reason}`;
342
+ if (line === this._lastReleaseLine) {
343
+ return;
344
+ }
345
+ const first = this._lastReleaseLine === undefined;
346
+ this._lastReleaseLine = line;
347
+ if (first && release === undefined) {
348
+ return;
349
+ }
350
+ this.log(enums_1.LogLevel.Info, `Morning reserve decision: ${line}`);
351
+ }
208
352
  turnOnAdditionalConsumer() {
209
353
  const result = utils_1.EnergyManagerUtils.turnOnAdditionalConsumer(this._excessEnergyConsumer, this._lastDeviceChange);
210
354
  if (result == undefined) {
@@ -249,3 +393,8 @@ class VictronDevice extends BaseDevice_1.BaseDevice {
249
393
  }
250
394
  }
251
395
  exports.VictronDevice = VictronDevice;
396
+ /**
397
+ * The shortest horizon the battery reservation is spread over, in hours. Not a setting: it is a bound on
398
+ * the arithmetic rather than a dial of the plant - see {@link VictronDevice.reserveHorizonHours}.
399
+ */
400
+ VictronDevice.minimumReserveHorizonHours = 0.5;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The one statement the plant's recorded history is allowed to act on in the victron manager: that the coming
3
+ * morning low clears the reserve on measured consumption alone, with no further yield assumed and no model
4
+ * involved.
5
+ *
6
+ * There is no counterpart for "the morning falls short". Saying so needs the fitted model, and the model runs
7
+ * in the shadow; the absent value therefore means the previous calculation applies, which needs nothing from
8
+ * the history.
9
+ *
10
+ * Not part of the published surface: it is how this manager carries its own decision from one private method
11
+ * to the next, not something a consumer of the library reads.
12
+ */
13
+ export interface iVictronMorningRelease {
14
+ /** What an operator reads the release by. */
15
+ reason: string;
16
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -23,6 +23,7 @@ export declare enum DeviceCapability {
23
23
  garageDoorOpener = 21,
24
24
  magnetSensor = 22,
25
25
  airQualitySensor = 23,
26
+ fossilGenerator = 24,
26
27
  bluetoothDetector = 101,
27
28
  trackableDevice = 102,
28
29
  scene = 103,
@@ -27,6 +27,7 @@ var DeviceCapability;
27
27
  DeviceCapability[DeviceCapability["garageDoorOpener"] = 21] = "garageDoorOpener";
28
28
  DeviceCapability[DeviceCapability["magnetSensor"] = 22] = "magnetSensor";
29
29
  DeviceCapability[DeviceCapability["airQualitySensor"] = 23] = "airQualitySensor";
30
+ DeviceCapability[DeviceCapability["fossilGenerator"] = 24] = "fossilGenerator";
30
31
  DeviceCapability[DeviceCapability["bluetoothDetector"] = 101] = "bluetoothDetector";
31
32
  DeviceCapability[DeviceCapability["trackableDevice"] = 102] = "trackableDevice";
32
33
  DeviceCapability[DeviceCapability["scene"] = 103] = "scene";
@@ -1,6 +1,7 @@
1
1
  import { iRoomDevice } from './iRoomDevice';
2
2
  import { AirQualitySensorChangeAction } from '../../action';
3
- import { iAirQualityReadings, iAirQualitySensor } from './iAirQualitySensor';
3
+ import { iAirQualityReadings } from './iAirQualityReadings';
4
+ import { iAirQualitySensor } from './iAirQualitySensor';
4
5
  /**
5
6
  * This interface represents a device measuring air quality.
6
7
  *
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The set of air quality metrics a sensor can report.
3
+ *
4
+ * Devices differ widely in what they measure, so every member is always present but defaults to
5
+ * `UNDEFINED_AIR_QUALITY_VALUE` until the device reports it. This mirrors the sentinel approach of
6
+ * `UNDEFINED_TEMP_VALUE` and spares consumers a null check on every metric; use
7
+ * {@link iAirQualitySensor.reports} to tell "not measured" apart from a measured zero.
8
+ */
9
+ export interface iAirQualityReadings {
10
+ /**
11
+ * The air quality index as calculated by the device (lower is better)
12
+ */
13
+ aqi: number;
14
+ /**
15
+ * The carbon dioxide concentration in ppm
16
+ */
17
+ co2: number;
18
+ /**
19
+ * The nitrogen oxide index (1 equals background level)
20
+ */
21
+ nox: number;
22
+ /**
23
+ * Particulate matter up to 1.0 µm in µg/m³
24
+ */
25
+ pm1p0: number;
26
+ /**
27
+ * Particulate matter up to 2.5 µm in µg/m³
28
+ */
29
+ pm2p5: number;
30
+ /**
31
+ * Particulate matter up to 4.0 µm in µg/m³
32
+ */
33
+ pm4p0: number;
34
+ /**
35
+ * Particulate matter up to 10.0 µm in µg/m³
36
+ */
37
+ pm10p0: number;
38
+ /**
39
+ * The total volatile organic compounds in ppb
40
+ */
41
+ tvoc: number;
42
+ /**
43
+ * The vape detection reading of the device
44
+ */
45
+ vape: number;
46
+ /**
47
+ * The volatile organic compounds index (100 equals background level)
48
+ */
49
+ voc: number;
50
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,59 +1,6 @@
1
1
  import { iJsonOmitKeys } from '../iJsonOmitKeys';
2
2
  import { AirQualitySensorChangeAction } from '../../action';
3
- /**
4
- * The value reported for a metric the sensor does not measure or has not reported yet.
5
- */
6
- export declare const UNDEFINED_AIR_QUALITY_VALUE = -1;
7
- /**
8
- * The set of air quality metrics a sensor can report.
9
- *
10
- * Devices differ widely in what they measure, so every member is always present but defaults to
11
- * {@link UNDEFINED_AIR_QUALITY_VALUE} until the device reports it. This mirrors the sentinel approach of
12
- * {@link UNDEFINED_TEMP_VALUE} and spares consumers a null check on every metric; use
13
- * {@link iAirQualitySensor.reports} to tell "not measured" apart from a measured zero.
14
- */
15
- export interface iAirQualityReadings {
16
- /**
17
- * The air quality index as calculated by the device (lower is better)
18
- */
19
- aqi: number;
20
- /**
21
- * The carbon dioxide concentration in ppm
22
- */
23
- co2: number;
24
- /**
25
- * The nitrogen oxide index (1 equals background level)
26
- */
27
- nox: number;
28
- /**
29
- * Particulate matter up to 1.0 µm in µg/m³
30
- */
31
- pm1p0: number;
32
- /**
33
- * Particulate matter up to 2.5 µm in µg/m³
34
- */
35
- pm2p5: number;
36
- /**
37
- * Particulate matter up to 4.0 µm in µg/m³
38
- */
39
- pm4p0: number;
40
- /**
41
- * Particulate matter up to 10.0 µm in µg/m³
42
- */
43
- pm10p0: number;
44
- /**
45
- * The total volatile organic compounds in ppb
46
- */
47
- tvoc: number;
48
- /**
49
- * The vape detection reading of the device
50
- */
51
- vape: number;
52
- /**
53
- * The volatile organic compounds index (100 equals background level)
54
- */
55
- voc: number;
56
- }
3
+ import { iAirQualityReadings } from './iAirQualityReadings';
57
4
  /**
58
5
  * Common handling for a device measuring air quality.
59
6
  */
@@ -1,7 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UNDEFINED_AIR_QUALITY_VALUE = void 0;
4
- /**
5
- * The value reported for a metric the sensor does not measure or has not reported yet.
6
- */
7
- exports.UNDEFINED_AIR_QUALITY_VALUE = -1;
@@ -1,6 +1,8 @@
1
1
  import { iBaseDevice } from './iBaseDevice';
2
2
  import { iDisposable } from '../iDisposeable';
3
+ import { iEnergyHistoryOutlook } from '../iEnergyHistoryOutlook';
3
4
  import { iExcessEnergyConsumer } from './iExcessEnergyConsumer';
5
+ import { iMorningReserveVerdict } from '../iMorningReserveVerdict';
4
6
  /**
5
7
  * Interface for devices that can manage energy consumption and production.
6
8
  *
@@ -17,6 +19,47 @@ export interface iEnergyManager extends iBaseDevice, iDisposable {
17
19
  * Whether the Energy Manager actively blocks AC
18
20
  */
19
21
  readonly acBlocked: boolean;
22
+ /**
23
+ * What the plant's own recorded history says about the coming morning low, or `undefined` while this
24
+ * manager cannot say anything about it - no battery, no stated capacity, or no charge level to project
25
+ * from.
26
+ *
27
+ * **Read, not asked with a charge level.** The level of the plant's battery is the manager's own, and a
28
+ * consumer that handed one in could hand in a different number than the manager itself reads. It is also
29
+ * why the guard against the "no reading" marker of {@link iBatteryDevice.batteryLevel} lives here, once,
30
+ * rather than in every consumer.
31
+ *
32
+ * **Free to read.** This getter must stay pure arithmetic over what was already read, so that a consumer
33
+ * asking on every one of its own decisions costs no query and no request quota. The reading itself is driven
34
+ * separately, from the manager's own loop.
35
+ *
36
+ * **Delegate, do not implement.** `EnergyManagerUtils.morningOutlook(this)` answers it for any manager, and
37
+ * `EnergyManagerUtils.refreshEnergyHistory(this)` in the manager's loop drives the reading - the same pair
38
+ * both managers in this repository use. There is one energy manager per installation, so the shared
39
+ * implementation keeps one reading and one shadow record; a manager that builds its own would pay the
40
+ * bounded weather backfill twice and split that record in half.
41
+ *
42
+ * The answer states facts about the plant and no arithmetic on them. The judgement that goes with it is
43
+ * {@link morningReserveVerdict}; read that one unless you genuinely need the raw quantities.
44
+ */
45
+ readonly morningOutlook: iEnergyHistoryOutlook | undefined;
46
+ /**
47
+ * Whether the plant expects the coming morning to hold the reserve its battery must not fall below, or
48
+ * `undefined` while it can say neither.
49
+ *
50
+ * **The judgement belongs here, not to the asking consumer.** How low the morning may get is a property of
51
+ * the battery, and how little sun still counts as none a property of the plant's photovoltaic - a fuel
52
+ * burning generator and an air conditioner do not want different answers to that. Where they differ is how
53
+ * sure they want to be before they act, and that is {@link iMorningReserveVerdict.measured}: the verdicts
54
+ * that need no fitted model may move something, the modelled ones are reported and left alone.
55
+ *
56
+ * **Free to read**, for the same reason {@link morningOutlook} is - pure arithmetic over what was already
57
+ * read.
58
+ *
59
+ * **Delegate, do not implement.** `EnergyManagerUtils.morningReserveVerdict(this, this.morningOutlook)`
60
+ * answers it for any manager.
61
+ */
62
+ readonly morningReserveVerdict: iMorningReserveVerdict | undefined;
20
63
  /**
21
64
  * The total wattaage being injected into the grid at the moment of last calculation.
22
65
  */
@@ -1,7 +1,6 @@
1
1
  import { iRoomDevice } from './iRoomDevice';
2
2
  import { HumiditySensorChangeAction } from '../../action';
3
3
  import { iHumiditySensor } from './iHumiditySensor';
4
- export declare const UNDEFINED_HUMIDITY_VALUE = -1;
5
4
  /**
6
5
  * This interface represents a humidity sensor device.
7
6
  *
@@ -1,4 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UNDEFINED_HUMIDITY_VALUE = void 0;
4
- exports.UNDEFINED_HUMIDITY_VALUE = -1;
@@ -1,7 +1,6 @@
1
1
  import { iRoomDevice } from './iRoomDevice';
2
2
  import { TemperatureSensorChangeAction } from '../../action';
3
3
  import { iTemperatureSensor } from './iTemperatureSensor';
4
- export declare const UNDEFINED_TEMP_VALUE = -99;
5
4
  /**
6
5
  * This interface represents a temperature sensor device.
7
6
  *
@@ -1,4 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UNDEFINED_TEMP_VALUE = void 0;
4
- exports.UNDEFINED_TEMP_VALUE = -99;
@@ -13,7 +13,11 @@ export * from './iGarageDoorOpener';
13
13
  export * from './iHandle';
14
14
  export * from './iHeater';
15
15
  export * from './iAirQualityCollector';
16
+ export * from './iAirQualityReadings';
16
17
  export * from './iAirQualitySensor';
18
+ export * from './undefinedAirQualityValue';
19
+ export * from './undefinedHumidityValue';
20
+ export * from './undefinedTempValue';
17
21
  export * from './iHumidityCollector';
18
22
  export * from './iIlluminationSensor';
19
23
  export * from './iLamp';
@@ -28,7 +28,11 @@ __exportStar(require("./iGarageDoorOpener"), exports);
28
28
  __exportStar(require("./iHandle"), exports);
29
29
  __exportStar(require("./iHeater"), exports);
30
30
  __exportStar(require("./iAirQualityCollector"), exports);
31
+ __exportStar(require("./iAirQualityReadings"), exports);
31
32
  __exportStar(require("./iAirQualitySensor"), exports);
33
+ __exportStar(require("./undefinedAirQualityValue"), exports);
34
+ __exportStar(require("./undefinedHumidityValue"), exports);
35
+ __exportStar(require("./undefinedTempValue"), exports);
32
36
  __exportStar(require("./iHumidityCollector"), exports);
33
37
  __exportStar(require("./iIlluminationSensor"), exports);
34
38
  __exportStar(require("./iLamp"), exports);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * The value reported for a metric the sensor does not measure or has not reported yet.
3
+ */
4
+ export declare const UNDEFINED_AIR_QUALITY_VALUE = -1;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UNDEFINED_AIR_QUALITY_VALUE = void 0;
4
+ /**
5
+ * The value reported for a metric the sensor does not measure or has not reported yet.
6
+ */
7
+ exports.UNDEFINED_AIR_QUALITY_VALUE = -1;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * The value reported while a device has no humidity reading of its own yet.
3
+ */
4
+ export declare const UNDEFINED_HUMIDITY_VALUE = -1;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UNDEFINED_HUMIDITY_VALUE = void 0;
4
+ /**
5
+ * The value reported while a device has no humidity reading of its own yet.
6
+ */
7
+ exports.UNDEFINED_HUMIDITY_VALUE = -1;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * The value reported while a device has no temperature reading of its own yet.
3
+ */
4
+ export declare const UNDEFINED_TEMP_VALUE = -99;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UNDEFINED_TEMP_VALUE = void 0;
4
+ /**
5
+ * The value reported while a device has no temperature reading of its own yet.
6
+ */
7
+ exports.UNDEFINED_TEMP_VALUE = -99;