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,10 +1,10 @@
1
- import { iActuator, iBaseDevice, iDachsDeviceSettings, iDachsSettings } from '../../interfaces';
1
+ import { iActuator, iBaseDevice, iDachsDeviceSettings, iDachsSettings, iFossilGeneratorSource } from '../../interfaces';
2
2
  import { DeviceCapability } from '../../enums';
3
3
  import { DachsTemperatureSensor } from './dachsTemperatureSensor';
4
4
  import { ActuatorSetStateCommand, ActuatorToggleCommand, ActuatorWriteStateToDeviceCommand, RestoreTargetAutomaticValueCommand } from '../../command';
5
5
  import { BlockAutomaticHandler } from '../../services';
6
6
  import { RoomBaseDevice } from '../RoomBaseDevice';
7
- export declare class Dachs extends RoomBaseDevice implements iBaseDevice, iActuator {
7
+ export declare class Dachs extends RoomBaseDevice implements iBaseDevice, iActuator, iFossilGeneratorSource {
8
8
  /** @inheritDoc */
9
9
  settings: iDachsDeviceSettings;
10
10
  /** @inheritDoc */
@@ -47,10 +47,41 @@ export declare class Dachs extends RoomBaseDevice implements iBaseDevice, iActua
47
47
  private _tempHeatStorage;
48
48
  private fetchedData;
49
49
  private _blockStarted;
50
+ /** The decision of the last history gate evaluation; undefined out of season or without a charge level. */
51
+ private _historyGateResult;
52
+ private _historyGateEvaluations;
53
+ private _historyGateStatements;
54
+ private _lastGateDecision;
50
55
  /** @inheritDoc */
51
56
  get customName(): string;
52
57
  /** @inheritDoc */
53
58
  get actuatorOn(): boolean;
59
+ /**
60
+ * The unit persists its own state changes under {@link id}, so this is the id they are recorded under.
61
+ * @inheritDoc
62
+ */
63
+ get actuatorId(): string;
64
+ /**
65
+ * A live view of the setting rather than a copy: it is editable at runtime, and the correction of a
66
+ * historical day describes the unit as it is configured now.
67
+ * @inheritDoc
68
+ */
69
+ get ratedElectricalWattage(): number;
70
+ /**
71
+ * A live view of the setting, for the same reason as {@link ratedElectricalWattage}.
72
+ * @inheritDoc
73
+ */
74
+ get conversionFactor(): number;
75
+ /**
76
+ * The upper warm water temperature the Dachs is currently allowed to heat to.
77
+ *
78
+ * Only a stated warm season lowers the ceiling. The heating settings are optional and fall back to
79
+ * {@link HeatingMode.None}, so "not winter" covers the installation that states nothing at all - and
80
+ * reading that absence as summer takes the ceiling away from it the whole year round, without a switch
81
+ * anywhere to notice it by.
82
+ * @returns The applicable maximum warm water temperature in °C.
83
+ */
84
+ private get currentWarmWaterMaxTemp();
54
85
  constructor(options: iDachsSettings);
55
86
  /** @inheritDoc */
56
87
  get id(): string;
@@ -72,11 +103,57 @@ export declare class Dachs extends RoomBaseDevice implements iBaseDevice, iActua
72
103
  */
73
104
  private onBatteryLevelChange;
74
105
  private checkAllDesiredStates;
106
+ /**
107
+ * The state of charge of the plant's battery, as far as it is known.
108
+ * @returns The level in percent, or {@link NO_STATE_OF_CHARGE} while there is no energy manager or it
109
+ * carries no level - neither of which is a battery that is empty.
110
+ */
111
+ private static get currentStateOfCharge();
75
112
  private onHeatStorageTempChange;
76
113
  private onWarmWaterTempChange;
77
114
  private checkWwPumpDesiredState;
78
115
  private checkHeatingRod;
79
116
  private shouldDachsBeStarted;
117
+ /**
118
+ * What this unit makes of what the plant said about the coming morning.
119
+ *
120
+ * Its own question comes first and is the only one it answers itself: while the warm water sits below the
121
+ * minimum the unit is needed for heat, so energy does not decide and the plant's verdict is recorded without
122
+ * being acted on. Everything below that is translation - the plant judged whether the morning holds, this
123
+ * unit only says what it does about it.
124
+ *
125
+ * **A verdict the plant reached without its fitted model may move something, a modelled one may not.** The
126
+ * measured verdicts are arithmetic on the plant's own recorded consumption and carry no unmeasured
127
+ * assumption; the modelled one rests on a fit whose window length has never been checked against recorded
128
+ * data. Stage 4 therefore names which side of the reserve the band fell on and moves nothing - behaviourally
129
+ * the same as stage 5 and distinguishable from it only by the reason line. Whether the model beats the
130
+ * trivial rule is measured once, by the energy manager, over the plant's single shadow record.
131
+ *
132
+ * The stage numbers are this unit's log vocabulary rather than the plant's ladder: stage 1 is the warm water
133
+ * question nobody else has, stages 2 and 3 are the measured verdicts it acts on, stage 4 the modelled one it
134
+ * only reports, stage 5 no verdict at all.
135
+ * @returns The decision, or undefined while the heating mode is winter or the plant says nothing about the
136
+ * coming morning.
137
+ */
138
+ private evaluateHistoryGate;
139
+ /**
140
+ * Assembles a gate result and the reason line the operator reads it by.
141
+ * @param stage - The stage of this unit's own vocabulary that decided.
142
+ * @param suppress - Whether the electricity driven start is suppressed.
143
+ * @param request - Whether a start is asked for.
144
+ * @param detail - What the plant said, or what this unit answered instead.
145
+ * @param verdict - The plant's verdict, for the numbers the line is read by.
146
+ * @param lowerEdgeSoc - The projected morning charge level at the lower edge.
147
+ * @param upperEdgeSoc - The projected morning charge level at the upper edge.
148
+ * @returns The decision.
149
+ */
150
+ private static gateResult;
151
+ /**
152
+ * Writes the gate decision to the log - on info whenever the decision changed, on debug otherwise, as the
153
+ * evaluation runs on every battery and temperature update.
154
+ * @param result - The decision to log, if there is one.
155
+ */
156
+ private logHistoryGate;
80
157
  private checkAlternativeActuator;
81
158
  private onDachsRunningStateChange;
82
159
  }
@@ -19,6 +19,14 @@ const command_1 = require("../../command");
19
19
  const services_1 = require("../../services");
20
20
  const settings_service_1 = require("../../settings-service");
21
21
  const RoomBaseDevice_1 = require("../RoomBaseDevice");
22
+ /**
23
+ * What an energy manager reports while its battery states no charge level at all - see the battery level of
24
+ * `victron-device.ts`, which answers with this instead of a level.
25
+ *
26
+ * A marker, not a low battery: read as a number it lands below every threshold of this unit, so whoever
27
+ * decides on it has to tell the two apart first.
28
+ */
29
+ const NO_STATE_OF_CHARGE = -1;
22
30
  class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
23
31
  /** @inheritDoc */
24
32
  get customName() {
@@ -28,6 +36,43 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
28
36
  get actuatorOn() {
29
37
  return this._dachsOn;
30
38
  }
39
+ /**
40
+ * The unit persists its own state changes under {@link id}, so this is the id they are recorded under.
41
+ * @inheritDoc
42
+ */
43
+ get actuatorId() {
44
+ return this.id;
45
+ }
46
+ /**
47
+ * A live view of the setting rather than a copy: it is editable at runtime, and the correction of a
48
+ * historical day describes the unit as it is configured now.
49
+ * @inheritDoc
50
+ */
51
+ get ratedElectricalWattage() {
52
+ return this.settings.dachsRatedElectricalWattage;
53
+ }
54
+ /**
55
+ * A live view of the setting, for the same reason as {@link ratedElectricalWattage}.
56
+ * @inheritDoc
57
+ */
58
+ get conversionFactor() {
59
+ return this.settings.dachsConversionFactor;
60
+ }
61
+ /**
62
+ * The upper warm water temperature the Dachs is currently allowed to heat to.
63
+ *
64
+ * Only a stated warm season lowers the ceiling. The heating settings are optional and fall back to
65
+ * {@link HeatingMode.None}, so "not winter" covers the installation that states nothing at all - and
66
+ * reading that absence as summer takes the ceiling away from it the whole year round, without a switch
67
+ * anywhere to notice it by.
68
+ * @returns The applicable maximum warm water temperature in °C.
69
+ */
70
+ get currentWarmWaterMaxTemp() {
71
+ const mode = settings_service_1.SettingsService.heatMode;
72
+ return mode === enums_1.HeatingMode.Summer || mode === enums_1.HeatingMode.TransitionalSeason
73
+ ? this.settings.summerWarmWaterDesiredMaxTemp
74
+ : this.settings.warmWaterDesiredMaxTemp;
75
+ }
31
76
  constructor(options) {
32
77
  var _a, _b;
33
78
  const info = new DeviceInfo_1.DeviceInfo();
@@ -52,6 +97,8 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
52
97
  * The timestamp of the last time the Block was enforced.
53
98
  */
54
99
  this._blockStarted = 0;
100
+ this._historyGateEvaluations = 0;
101
+ this._historyGateStatements = 0;
55
102
  this.jsonOmitTopLevelKeys.push(...['warmWaterPump', 'heatingRod']);
56
103
  this.jsonOmitKeys.push(...[
57
104
  'client',
@@ -65,6 +112,9 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
65
112
  devices_1.Devices.alLDevices[allDevicesKey] = this;
66
113
  this.deviceCapabilities.push(enums_1.DeviceCapability.actuator);
67
114
  this.deviceCapabilities.push(enums_1.DeviceCapability.blockAutomatic);
115
+ // How the unit announces itself as one of the plant's fuel burning generators: whoever corrects a
116
+ // historical charge level for fuel burnt reads the device list, not a list handed to it by a caller.
117
+ this.deviceCapabilities.push(enums_1.DeviceCapability.fossilGenerator);
68
118
  if (options.influxDb) {
69
119
  this._influxClient = new lib_1.DachsInfluxClient(options.influxDb);
70
120
  }
@@ -149,7 +199,7 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
149
199
  this.settings.disableDachsOwnWW ||
150
200
  ((_a = this.warmWaterDachsAlternativeActuator) === null || _a === void 0 ? void 0 : _a.actuatorOn) ||
151
201
  this.heatStorageTempSensor.temperatureSensor.temperature < this.warmWaterSensor.temperatureSensor.temperature ||
152
- this.warmWaterSensor.temperatureSensor.temperature > this.settings.warmWaterDesiredMaxTemp) {
202
+ this.warmWaterSensor.temperatureSensor.temperature > this.currentWarmWaterMaxTemp) {
153
203
  return;
154
204
  }
155
205
  const startPumpCommand = new command_1.ActuatorSetStateCommand(c, true, 'Dachs is starting/on');
@@ -200,6 +250,10 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
200
250
  if (this.blockAutomationHandler.automaticBlockActive) {
201
251
  return;
202
252
  }
253
+ // Read, not refreshed. The plant's history belongs to the energy manager, which keeps it up to date on a
254
+ // cadence of its own; asking here costs no query, so the decision may be taken as often as it likes.
255
+ this._historyGateResult = this.evaluateHistoryGate();
256
+ this.logHistoryGate(this._historyGateResult);
203
257
  const shouldDachsBeStarted = this.shouldDachsBeStarted(action, batteryLevel);
204
258
  this.checkHeatingRod(action, batteryLevel);
205
259
  this.checkAlternativeActuator(shouldDachsBeStarted, action);
@@ -211,13 +265,20 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
211
265
  setStateCommand.overrideCommandSource = enums_1.CommandSource.Automatic;
212
266
  this.setActuator(setStateCommand);
213
267
  }
214
- onHeatStorageTempChange(action) {
268
+ /**
269
+ * The state of charge of the plant's battery, as far as it is known.
270
+ * @returns The level in percent, or {@link NO_STATE_OF_CHARGE} while there is no energy manager or it
271
+ * carries no level - neither of which is a battery that is empty.
272
+ */
273
+ static get currentStateOfCharge() {
215
274
  var _a, _b;
216
- this.checkAllDesiredStates(action, (_b = (_a = devices_1.Devices.energymanager) === null || _a === void 0 ? void 0 : _a.batteryLevel) !== null && _b !== void 0 ? _b : 0);
275
+ return (_b = (_a = devices_1.Devices.energymanager) === null || _a === void 0 ? void 0 : _a.batteryLevel) !== null && _b !== void 0 ? _b : NO_STATE_OF_CHARGE;
276
+ }
277
+ onHeatStorageTempChange(action) {
278
+ this.checkAllDesiredStates(action, Dachs.currentStateOfCharge);
217
279
  }
218
280
  onWarmWaterTempChange(action) {
219
- var _a, _b;
220
- this.checkAllDesiredStates(action, (_b = (_a = devices_1.Devices.energymanager) === null || _a === void 0 ? void 0 : _a.batteryLevel) !== null && _b !== void 0 ? _b : 0);
281
+ this.checkAllDesiredStates(action, Dachs.currentStateOfCharge);
221
282
  }
222
283
  checkWwPumpDesiredState(action) {
223
284
  var _a, _b;
@@ -245,9 +306,9 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
245
306
  desiredWwPumpState = false;
246
307
  reason = `Temperature of warm water pump ${wwTemp}°C is higher than temperature of heat storage ${heatStorageTemp}°C`;
247
308
  }
248
- else if (wwTemp > this.settings.warmWaterDesiredMaxTemp) {
309
+ else if (wwTemp > this.currentWarmWaterMaxTemp) {
249
310
  desiredWwPumpState = false;
250
- reason = `Temperature of warm water pump ${wwTemp}°C is higher than the desired max value`;
311
+ reason = `Temperature of warm water pump ${wwTemp}°C is higher than the desired max value of ${this.currentWarmWaterMaxTemp}°C`;
251
312
  }
252
313
  else if (this._dachsOn) {
253
314
  desiredWwPumpState = true;
@@ -298,6 +359,12 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
298
359
  shouldDachsBeStarted(action, batteryLevel) {
299
360
  var _a, _b, _c;
300
361
  const dayType = services_1.TimeCallbackService.dayType(new models_1.SunTimeOffsets());
362
+ /**
363
+ * Set where the empty battery earns the release of the start block. Both the release and the block that
364
+ * may take its place are written further down, behind the heat driven branches - a decision taken before
365
+ * them could either swallow a heat driven start or leave the block standing while one happens.
366
+ */
367
+ let emptyBatteryEarnsRelease = false;
301
368
  if (this.blockDachsStart !== undefined) {
302
369
  if (this.settings.disableDachsTemporarily) {
303
370
  const blockAction = new command_1.ActuatorSetStateCommand(action, true, `Dachs is disabled temporarily`, null);
@@ -322,10 +389,12 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
322
389
  return false;
323
390
  }
324
391
  else if (batteryLevel < this.settings.batteryLevelAllowStartThreshold) {
325
- const liftAction = new command_1.ActuatorSetStateCommand(action, false, `Battery reached ${batteryLevel}%, Dachs is now allowed to run if needed`, null);
326
- this.blockDachsStart.setActuator(liftAction);
392
+ // The empty battery earns a release here, but nothing is written yet: whether the release happens
393
+ // or a block takes its place is decided behind the heat driven branches, so that neither outcome
394
+ // can reach around a heat driven start.
395
+ emptyBatteryEarnsRelease = true;
327
396
  }
328
- else if (utils_1.Utils.nowMS() - this._blockStarted > 180 * 60 * 60 &&
397
+ else if (utils_1.Utils.nowMS() - this._blockStarted > 3 * 60 * 60 * 1000 &&
329
398
  (((_a = settings_service_1.SettingsService.settings.heaterSettings) === null || _a === void 0 ? void 0 : _a.mode) === enums_1.HeatingMode.Winter ||
330
399
  this.warmWaterDachsAlternativeActuator === undefined)) {
331
400
  const liftAction = new command_1.ActuatorSetStateCommand(action, false, `Battery is at ${batteryLevel}%, but Dachs wasn't allowed to run for 3 hours, Dachs is now allowed to run if needed`, null);
@@ -342,17 +411,49 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
342
411
  return false;
343
412
  }
344
413
  }
345
- if (this._dachsOn) {
414
+ const alreadyRunning = this._dachsOn;
415
+ // The heat driven decision is taken but not returned yet, so that the block actuator below is written
416
+ // with it already known.
417
+ let heatDrivenStart = undefined;
418
+ if (!alreadyRunning) {
419
+ if (((_c = settings_service_1.SettingsService.settings.heaterSettings) === null || _c === void 0 ? void 0 : _c.mode) === enums_1.HeatingMode.Winter &&
420
+ this.heatStorageTempSensor.temperatureSensor.temperature < this.settings.winterMinimumHeatStorageTemp) {
421
+ // It is winter and heat storage is kinda cold --> Start
422
+ heatDrivenStart = true;
423
+ }
424
+ else if (this.heatStorageTempSensor.temperatureSensor.temperature > this.settings.heatStorageMaxStartTemp) {
425
+ // Heat Storage is already quite full, don't start
426
+ heatDrivenStart = false;
427
+ }
428
+ }
429
+ // Everything above is heat driven and stays untouched; from here on the decision is electricity driven,
430
+ // which is the only branch the history gate speaks about. The gate is read - and the block actuator
431
+ // written - only at this point, so neither of its two directions can reach around a heat driven decision.
432
+ const gate = this._historyGateResult;
433
+ const gateSuppresses = heatDrivenStart === undefined && (gate === null || gate === void 0 ? void 0 : gate.suppress) === true;
434
+ if (emptyBatteryEarnsRelease && this.blockDachsStart !== undefined) {
435
+ if (gateSuppresses) {
436
+ const gateBlockAction = new command_1.ActuatorSetStateCommand(action, true, `History gate suppresses the start - ${gate === null || gate === void 0 ? void 0 : gate.reason}`, null);
437
+ gateBlockAction.overrideCommandSource = enums_1.CommandSource.Force;
438
+ this.blockDachsStart.setActuator(gateBlockAction);
439
+ this._blockStarted = utils_1.Utils.nowMS();
440
+ }
441
+ else {
442
+ const liftAction = new command_1.ActuatorSetStateCommand(action, false, `Battery reached ${batteryLevel}%, Dachs is now allowed to run if needed`, null);
443
+ this.blockDachsStart.setActuator(liftAction);
444
+ }
445
+ }
446
+ if (alreadyRunning) {
346
447
  // We are already running
347
448
  return false;
348
449
  }
349
- if (((_c = settings_service_1.SettingsService.settings.heaterSettings) === null || _c === void 0 ? void 0 : _c.mode) === enums_1.HeatingMode.Winter &&
350
- this.heatStorageTempSensor.temperatureSensor.temperature < this.settings.winterMinimumHeatStorageTemp) {
351
- // It is winter and heat storage is kinda cold --> Start
450
+ if (heatDrivenStart !== undefined) {
451
+ return heatDrivenStart;
452
+ }
453
+ if ((gate === null || gate === void 0 ? void 0 : gate.request) === true) {
352
454
  return true;
353
455
  }
354
- else if (this.heatStorageTempSensor.temperatureSensor.temperature > this.settings.heatStorageMaxStartTemp) {
355
- // Heat Storage is already quite full, don't start
456
+ if (gateSuppresses) {
356
457
  return false;
357
458
  }
358
459
  if ((dayType === enums_1.TimeOfDay.Daylight || dayType === enums_1.TimeOfDay.BeforeSunrise) &&
@@ -366,6 +467,102 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
366
467
  }
367
468
  return !this.settings.blockAutomaticSettings;
368
469
  }
470
+ /**
471
+ * What this unit makes of what the plant said about the coming morning.
472
+ *
473
+ * Its own question comes first and is the only one it answers itself: while the warm water sits below the
474
+ * minimum the unit is needed for heat, so energy does not decide and the plant's verdict is recorded without
475
+ * being acted on. Everything below that is translation - the plant judged whether the morning holds, this
476
+ * unit only says what it does about it.
477
+ *
478
+ * **A verdict the plant reached without its fitted model may move something, a modelled one may not.** The
479
+ * measured verdicts are arithmetic on the plant's own recorded consumption and carry no unmeasured
480
+ * assumption; the modelled one rests on a fit whose window length has never been checked against recorded
481
+ * data. Stage 4 therefore names which side of the reserve the band fell on and moves nothing - behaviourally
482
+ * the same as stage 5 and distinguishable from it only by the reason line. Whether the model beats the
483
+ * trivial rule is measured once, by the energy manager, over the plant's single shadow record.
484
+ *
485
+ * The stage numbers are this unit's log vocabulary rather than the plant's ladder: stage 1 is the warm water
486
+ * question nobody else has, stages 2 and 3 are the measured verdicts it acts on, stage 4 the modelled one it
487
+ * only reports, stage 5 no verdict at all.
488
+ * @returns The decision, or undefined while the heating mode is winter or the plant says nothing about the
489
+ * coming morning.
490
+ */
491
+ evaluateHistoryGate() {
492
+ var _a, _b;
493
+ if (((_a = settings_service_1.SettingsService.settings.heaterSettings) === null || _a === void 0 ? void 0 : _a.mode) === enums_1.HeatingMode.Winter) {
494
+ // During winter operation the heat demand decides when the unit runs, so the gate stays out of it.
495
+ return undefined;
496
+ }
497
+ // An absent verdict covers the installation without an energy manager, the one whose battery reports no
498
+ // charge level and the one whose manager states no reserve to judge against. The unit does not tell them
499
+ // apart: in none of them is there a judgement to act on, and the stock ladder below then decides as it did
500
+ // before the gate existed. Not logged here either - the plant already says why it stays silent, and this
501
+ // runs on every battery and temperature update.
502
+ const verdict = (_b = devices_1.Devices.energymanager) === null || _b === void 0 ? void 0 : _b.morningReserveVerdict;
503
+ if (verdict === undefined) {
504
+ return undefined;
505
+ }
506
+ this._historyGateEvaluations++;
507
+ if (verdict.modelFitted) {
508
+ this._historyGateStatements++;
509
+ }
510
+ const warmWater = this.warmWaterSensor.temperatureSensor.temperature;
511
+ if (warmWater < this.settings.warmWaterDesiredMinTemp) {
512
+ // The band of a verdict that was not acted on would read as though it had been; the line collapses onto
513
+ // the charge level instead, which is what "energy did not decide this" looks like in the log.
514
+ return Dachs.gateResult(1, false, false, `warm water ${warmWater}°C is below the minimum ${this.settings.warmWaterDesiredMinTemp}°C`, verdict, verdict.currentSoc, verdict.currentSoc);
515
+ }
516
+ if (verdict.holds === undefined) {
517
+ return Dachs.gateResult(5, false, false, verdict.reason, verdict, verdict.band.lower, verdict.band.upper);
518
+ }
519
+ if (!verdict.measured) {
520
+ return Dachs.gateResult(4, false, false, `${verdict.reason}, which this unit does not act on`, verdict, verdict.band.lower, verdict.band.upper);
521
+ }
522
+ return verdict.holds
523
+ ? Dachs.gateResult(2, true, false, verdict.reason, verdict, verdict.band.lower, verdict.band.upper)
524
+ : Dachs.gateResult(3, false, true, verdict.reason, verdict, verdict.band.lower, verdict.band.upper);
525
+ }
526
+ /**
527
+ * Assembles a gate result and the reason line the operator reads it by.
528
+ * @param stage - The stage of this unit's own vocabulary that decided.
529
+ * @param suppress - Whether the electricity driven start is suppressed.
530
+ * @param request - Whether a start is asked for.
531
+ * @param detail - What the plant said, or what this unit answered instead.
532
+ * @param verdict - The plant's verdict, for the numbers the line is read by.
533
+ * @param lowerEdgeSoc - The projected morning charge level at the lower edge.
534
+ * @param upperEdgeSoc - The projected morning charge level at the upper edge.
535
+ * @returns The decision.
536
+ */
537
+ static gateResult(stage, suppress, request, detail, verdict, lowerEdgeSoc, upperEdgeSoc) {
538
+ return {
539
+ suppress,
540
+ request,
541
+ reason: `Stage ${stage}: ${detail} (soc ${utils_1.Utils.round(verdict.currentSoc, 2)}%, band ` +
542
+ `${utils_1.Utils.round(lowerEdgeSoc, 2)}%..${utils_1.Utils.round(upperEdgeSoc, 2)}%, reserve ${verdict.reserve}%, ` +
543
+ `${verdict.sampleDays} days)`,
544
+ currentSoc: verdict.currentSoc,
545
+ lowerEdgeSoc,
546
+ upperEdgeSoc,
547
+ reserve: verdict.reserve,
548
+ sampleDays: verdict.sampleDays,
549
+ };
550
+ }
551
+ /**
552
+ * Writes the gate decision to the log - on info whenever the decision changed, on debug otherwise, as the
553
+ * evaluation runs on every battery and temperature update.
554
+ * @param result - The decision to log, if there is one.
555
+ */
556
+ logHistoryGate(result) {
557
+ if (result === undefined) {
558
+ return;
559
+ }
560
+ const decision = `${result.suppress}/${result.request}`;
561
+ const changed = decision !== this._lastGateDecision;
562
+ this._lastGateDecision = decision;
563
+ this.log(changed ? enums_1.LogLevel.Info : enums_1.LogLevel.Debug, `History gate: ${result.reason}; a statement was possible in ${this._historyGateStatements} of ` +
564
+ `${this._historyGateEvaluations} evaluations`);
565
+ }
369
566
  checkAlternativeActuator(shouldDachsBeStarted, action) {
370
567
  var _a, _b;
371
568
  if (!this.warmWaterDachsAlternativeActuator) {
@@ -397,8 +594,7 @@ class Dachs extends RoomBaseDevice_1.RoomBaseDevice {
397
594
  this.warmWaterDachsAlternativeActuator.setActuator(command);
398
595
  }
399
596
  onDachsRunningStateChange(runStateChange) {
400
- var _a, _b;
401
- this.checkAllDesiredStates(runStateChange, (_b = (_a = devices_1.Devices.energymanager) === null || _a === void 0 ? void 0 : _a.batteryLevel) !== null && _b !== void 0 ? _b : 0);
597
+ this.checkAllDesiredStates(runStateChange, Dachs.currentStateOfCharge);
402
598
  }
403
599
  }
404
600
  exports.Dachs = Dachs;
@@ -1,4 +1,4 @@
1
- import { iBaseDevice, iConfig, iDeviceConfig, iEnergyManager, iRoomImportEnforcer, iTemperatureCollector } from '../interfaces';
1
+ import { iBaseDevice, iConfig, iDeviceConfig, iEnergyManager, iFossilGeneratorSource, iRoomImportEnforcer, iTemperatureCollector } from '../interfaces';
2
2
  import { Dachs } from './dachs';
3
3
  import { UnifiAccess, UnifiProtect } from './unifi';
4
4
  export declare class Devices {
@@ -67,9 +67,44 @@ export declare class Devices {
67
67
  * A reference to the Unifi Access device
68
68
  */
69
69
  static unifiAccess?: UnifiAccess;
70
+ /**
71
+ * The devices already reported as announcing a fuel burning generator without stating the fields of one,
72
+ * so the line is written on change instead of on every read of {@link fossilGenerators}.
73
+ */
74
+ private static readonly _incompleteGeneratorsReported;
70
75
  constructor(pDeviceData: {
71
76
  [id: string]: iDeviceConfig;
72
77
  }, pRoomImportEnforcer?: iRoomImportEnforcer, config?: iConfig);
78
+ /**
79
+ * The fuel burning generators of the plant - every device that announced itself as one by carrying
80
+ * {@link DeviceCapability.fossilGenerator}.
81
+ *
82
+ * Which generators a plant has is a property of the plant, not of whoever asks about it, so it is answered
83
+ * here rather than handed to a service by one of its callers. Reading the device list instead of keeping a
84
+ * list of registrations is what makes it independent of construction order: devices are built in the order
85
+ * the installation's configuration lists them, and a generator built before the reader would have to hope
86
+ * the reader existed already.
87
+ *
88
+ * Read anew on every use for the same reason - a generator added later is simply found the next time, and
89
+ * the two numbers of each entry are getters onto settings that are editable at runtime.
90
+ *
91
+ * The capability announces the intent, the three fields are what the correction actually works with, and
92
+ * both are checked. Cast into the role unchecked, a device that carries the capability without the fields
93
+ * hands over `undefined`, the correction turns into `NaN` and its entry is dropped without a word - the
94
+ * day is then under corrected, the photovoltaic looks better than it was, and the gate suppresses a start
95
+ * the house needed. That is the direction without a way back, so such a device is left out and named.
96
+ * @returns One entry per usable generator; empty while the plant has none.
97
+ */
98
+ static get fossilGenerators(): iFossilGeneratorSource[];
99
+ /**
100
+ * Names a device that announces itself as a fuel burning generator without stating what one has to state.
101
+ *
102
+ * Said once per device rather than once per read: this list is read on every history load, and a line per
103
+ * read would bury the change. The note is dropped again as soon as the device answers completely, so a
104
+ * field that only arrives later is not reported forever.
105
+ * @param key - The key the device is listed under.
106
+ */
107
+ private static reportIncompleteGenerator;
73
108
  static midnightReset(): void;
74
109
  static resetDetectionsToday(): void;
75
110
  static getBatteryInfo(): string;
@@ -61,6 +61,65 @@ class Devices {
61
61
  shelly_1.ShellyDevice.checkMissing();
62
62
  tuya_1.TuyaDevice.checkMissing();
63
63
  }
64
+ /**
65
+ * The fuel burning generators of the plant - every device that announced itself as one by carrying
66
+ * {@link DeviceCapability.fossilGenerator}.
67
+ *
68
+ * Which generators a plant has is a property of the plant, not of whoever asks about it, so it is answered
69
+ * here rather than handed to a service by one of its callers. Reading the device list instead of keeping a
70
+ * list of registrations is what makes it independent of construction order: devices are built in the order
71
+ * the installation's configuration lists them, and a generator built before the reader would have to hope
72
+ * the reader existed already.
73
+ *
74
+ * Read anew on every use for the same reason - a generator added later is simply found the next time, and
75
+ * the two numbers of each entry are getters onto settings that are editable at runtime.
76
+ *
77
+ * The capability announces the intent, the three fields are what the correction actually works with, and
78
+ * both are checked. Cast into the role unchecked, a device that carries the capability without the fields
79
+ * hands over `undefined`, the correction turns into `NaN` and its entry is dropped without a word - the
80
+ * day is then under corrected, the photovoltaic looks better than it was, and the gate suppresses a start
81
+ * the house needed. That is the direction without a way back, so such a device is left out and named.
82
+ * @returns One entry per usable generator; empty while the plant has none.
83
+ */
84
+ static get fossilGenerators() {
85
+ const generators = [];
86
+ for (const key in Devices.alLDevices) {
87
+ const device = Devices.alLDevices[key];
88
+ if (!device.deviceCapabilities.includes(enums_1.DeviceCapability.fossilGenerator)) {
89
+ continue;
90
+ }
91
+ const candidate = device;
92
+ // Finiteness rather than a plausible range: a negative or zero rating over corrects, which only makes
93
+ // the photovoltaic look worse, while an absent one poisons the whole sum of the day.
94
+ if (typeof candidate.actuatorId !== 'string' ||
95
+ candidate.actuatorId === '' ||
96
+ !Number.isFinite(candidate.ratedElectricalWattage) ||
97
+ !Number.isFinite(candidate.conversionFactor)) {
98
+ Devices.reportIncompleteGenerator(key);
99
+ continue;
100
+ }
101
+ Devices._incompleteGeneratorsReported.delete(key);
102
+ generators.push(candidate);
103
+ }
104
+ return generators;
105
+ }
106
+ /**
107
+ * Names a device that announces itself as a fuel burning generator without stating what one has to state.
108
+ *
109
+ * Said once per device rather than once per read: this list is read on every history load, and a line per
110
+ * read would bury the change. The note is dropped again as soon as the device answers completely, so a
111
+ * field that only arrives later is not reported forever.
112
+ * @param key - The key the device is listed under.
113
+ */
114
+ static reportIncompleteGenerator(key) {
115
+ if (Devices._incompleteGeneratorsReported.has(key)) {
116
+ return;
117
+ }
118
+ Devices._incompleteGeneratorsReported.add(key);
119
+ logging_1.ServerLogService.writeLog(enums_1.LogLevel.Error, `Device '${key}' announces itself as a fuel burning generator but states no usable actuator id, rated ` +
120
+ 'wattage and conversion factor, so nothing is subtracted for it and the photovoltaic looks better ' +
121
+ 'than it was');
122
+ }
64
123
  static midnightReset() {
65
124
  // Nothing yet
66
125
  }
@@ -375,3 +434,8 @@ Devices.dachs = undefined;
375
434
  * @default undefined (no warm water temperature sensor)
376
435
  */
377
436
  Devices.temperatureWarmWater = undefined;
437
+ /**
438
+ * The devices already reported as announcing a fuel burning generator without stating the fields of one,
439
+ * so the line is written on change instead of on every read of {@link fossilGenerators}.
440
+ */
441
+ Devices._incompleteGeneratorsReported = new Set();
@@ -1,5 +1,5 @@
1
1
  import { IoBrokerBaseDevice } from '../IoBrokerBaseDevice';
2
- import { iDisposable, iEnergyManager, iExcessEnergyConsumer } from '../../interfaces';
2
+ import { iDisposable, iEnergyHistoryOutlook, iEnergyManager, iExcessEnergyConsumer, iMorningReserveVerdict } from '../../interfaces';
3
3
  import { IoBrokerDeviceInfo } from '../IoBrokerDeviceInfo';
4
4
  import { PhaseState } from '../models';
5
5
  export declare class JsObjectEnergyManager extends IoBrokerBaseDevice implements iEnergyManager, iDisposable {
@@ -24,6 +24,21 @@ export declare class JsObjectEnergyManager extends IoBrokerBaseDevice implements
24
24
  get excessEnergy(): number;
25
25
  /** @inheritDoc */
26
26
  get acBlocked(): boolean;
27
+ /**
28
+ * The same shared implementation the other energy manager delegates to - not a stub, and not a copy. This
29
+ * manager measures three phases and carries neither a battery nor the dials that say how a history is read,
30
+ * so the shared code answers nothing for it and reads nothing for it either. That is the honest answer and
31
+ * the one an installation on this manager already got; it is reached by running the real path rather than
32
+ * by declaring it here.
33
+ * @inheritDoc
34
+ */
35
+ get morningOutlook(): iEnergyHistoryOutlook | undefined;
36
+ /**
37
+ * Delegated for the same reason {@link morningOutlook} is - and it answers nothing here for the same reason
38
+ * too: this manager states neither the reserve nor the sun threshold a verdict would be measured against.
39
+ * @inheritDoc
40
+ */
41
+ get morningReserveVerdict(): iMorningReserveVerdict | undefined;
27
42
  private _excessEnergyConsumerConsumption;
28
43
  /**
29
44
  * Example:
@@ -62,6 +62,25 @@ class JsObjectEnergyManager extends IoBrokerBaseDevice_1.IoBrokerBaseDevice {
62
62
  get acBlocked() {
63
63
  return this.excessEnergy < 200;
64
64
  }
65
+ /**
66
+ * The same shared implementation the other energy manager delegates to - not a stub, and not a copy. This
67
+ * manager measures three phases and carries neither a battery nor the dials that say how a history is read,
68
+ * so the shared code answers nothing for it and reads nothing for it either. That is the honest answer and
69
+ * the one an installation on this manager already got; it is reached by running the real path rather than
70
+ * by declaring it here.
71
+ * @inheritDoc
72
+ */
73
+ get morningOutlook() {
74
+ return utils_1.EnergyManagerUtils.morningOutlook(this);
75
+ }
76
+ /**
77
+ * Delegated for the same reason {@link morningOutlook} is - and it answers nothing here for the same reason
78
+ * too: this manager states neither the reserve nor the sun threshold a verdict would be measured against.
79
+ * @inheritDoc
80
+ */
81
+ get morningReserveVerdict() {
82
+ return utils_1.EnergyManagerUtils.morningReserveVerdict(this, this.morningOutlook);
83
+ }
65
84
  /**
66
85
  * Example:
67
86
  * ________________________________________
@@ -137,6 +156,10 @@ class JsObjectEnergyManager extends IoBrokerBaseDevice_1.IoBrokerBaseDevice {
137
156
  }
138
157
  }
139
158
  calculateExcessEnergy() {
159
+ // The same call the other manager's loop makes, so whether anything is read is decided in one place
160
+ // rather than by which manager happens to ask. This one states no dials, so nothing is read and no
161
+ // request quota is spent.
162
+ utils_1.EnergyManagerUtils.refreshEnergyHistory(this);
140
163
  const phaseProduction = this._currentProduction / 3.0;
141
164
  this._phaseAState = new models_2.PhaseState(this._powerValuePhaseA, phaseProduction);
142
165
  this._phaseBState = new models_2.PhaseState(this._powerValuePhaseB, phaseProduction);