juham-automation 0.0.17__tar.gz → 0.0.19__tar.gz

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 (64) hide show
  1. {juham_automation-0.0.17/juham_automation.egg-info → juham_automation-0.0.19}/PKG-INFO +1 -1
  2. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/automation/hotwateroptimizer.py +70 -31
  3. {juham_automation-0.0.17 → juham_automation-0.0.19/juham_automation.egg-info}/PKG-INFO +1 -1
  4. {juham_automation-0.0.17 → juham_automation-0.0.19}/pyproject.toml +1 -1
  5. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/__pycache__/test_hotwateroptimizer.cpython-312.pyc +0 -0
  6. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/test_hotwateroptimizer.py +44 -0
  7. {juham_automation-0.0.17 → juham_automation-0.0.19}/LICENSE.rst +0 -0
  8. {juham_automation-0.0.17 → juham_automation-0.0.19}/MANIFEST.in +0 -0
  9. {juham_automation-0.0.17 → juham_automation-0.0.19}/README.rst +0 -0
  10. {juham_automation-0.0.17 → juham_automation-0.0.19}/docs/source/CHANGELOG.rst +0 -0
  11. {juham_automation-0.0.17 → juham_automation-0.0.19}/docs/source/CONTRIBUTING.rst +0 -0
  12. {juham_automation-0.0.17 → juham_automation-0.0.19}/docs/source/LICENSE.rst +0 -0
  13. {juham_automation-0.0.17 → juham_automation-0.0.19}/docs/source/README.rst +0 -0
  14. {juham_automation-0.0.17 → juham_automation-0.0.19}/examples/myapp.log +0 -0
  15. {juham_automation-0.0.17 → juham_automation-0.0.19}/examples/myapp.py +0 -0
  16. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/__init__.py +0 -0
  17. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/automation/__init__.py +0 -0
  18. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/automation/energycostcalculator.py +0 -0
  19. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/automation/powermeter_simulator.py +0 -0
  20. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/automation/spothintafi.py +0 -0
  21. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/automation/watercirculator.py +0 -0
  22. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/japp.py +0 -0
  23. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/py.typed +0 -0
  24. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/__init__.py +0 -0
  25. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/electricityprice_ts.py +0 -0
  26. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/energycostcalculator_ts.py +0 -0
  27. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/forecast_ts.py +0 -0
  28. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/log_ts.py +0 -0
  29. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/power_ts.py +0 -0
  30. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/powermeter_ts.py +0 -0
  31. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation/ts/powerplan_ts.py +0 -0
  32. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation.egg-info/SOURCES.txt +0 -0
  33. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation.egg-info/dependency_links.txt +0 -0
  34. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation.egg-info/entry_points.txt +0 -0
  35. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation.egg-info/requires.txt +0 -0
  36. {juham_automation-0.0.17 → juham_automation-0.0.19}/juham_automation.egg-info/top_level.txt +0 -0
  37. {juham_automation-0.0.17 → juham_automation-0.0.19}/setup.cfg +0 -0
  38. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/__init__.py +0 -0
  39. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  40. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/__pycache__/test_japp.cpython-312.pyc +0 -0
  41. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/__init__.py +0 -0
  42. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  43. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/__pycache__/test_energycostcalculator.cpython-312.pyc +0 -0
  44. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/__pycache__/test_juham.cpython-312.pyc +0 -0
  45. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/__pycache__/test_powermeter_simulator.cpython-312.pyc +0 -0
  46. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/__pycache__/test_spothintafi.cpython-312.pyc +0 -0
  47. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/test_energycostcalculator.py +0 -0
  48. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/test_juham.py +0 -0
  49. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/automation/test_spothintafi.py +0 -0
  50. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/test_japp.py +0 -0
  51. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__init__.py +0 -0
  52. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__pycache__/__init__.cpython-312.pyc +0 -0
  53. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__pycache__/test_energycostcalculator_ts.cpython-312.pyc +0 -0
  54. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__pycache__/test_forecast_ts.cpython-312.pyc +0 -0
  55. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__pycache__/test_log_ts.cpython-312.pyc +0 -0
  56. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__pycache__/test_power_ts.cpython-312.pyc +0 -0
  57. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__pycache__/test_powermeter_ts.cpython-312.pyc +0 -0
  58. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/__pycache__/test_powerplan_ts.cpython-312.pyc +0 -0
  59. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/test_energycostcalculator_ts.py +0 -0
  60. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/test_forecast_ts.py +0 -0
  61. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/test_log_ts.py +0 -0
  62. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/test_power_ts.py +0 -0
  63. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/test_powermeter_ts.py +0 -0
  64. {juham_automation-0.0.17 → juham_automation-0.0.19}/tests/ts/test_powerplan_ts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.17
3
+ Version: 0.0.19
4
4
  Summary: Juha's Ultimate Home Automation Masterpiece
5
5
  Author-email: J Meskanen <juham.api@gmail.com>
6
6
  Maintainer-email: "J. Meskanen" <juham.api@gmail.com>
@@ -176,41 +176,64 @@ class HotWaterOptimizer(Juham):
176
176
  self.debug(f"Forecast sorted for the next {str(len(ranked_hours))} hours")
177
177
  return ranked_hours
178
178
 
179
+ def on_spot(self, m: list[dict[str, Any]], ts_quantized: float) -> None:
180
+ """Handle the spot prices.
181
+
182
+ Args:
183
+ list[dict[str, Any]]: list of spot prices
184
+ ts_quantized (float): current time
185
+ """
186
+ self.ranked_spot_prices = self.sort_by_rank(m, ts_quantized)
187
+
188
+ def on_forecast(
189
+ self, forecast: list[dict[str, Any]], ts_utc_quantized: float
190
+ ) -> None:
191
+ """Handle the solar forecast.
192
+
193
+ Args:
194
+ m (list[dict[str, Any]]): list of forecast prices
195
+ ts_quantized (float): current time
196
+ """
197
+ # reject forecasts that don't have solarenergy key
198
+ for f in forecast:
199
+ if not "solarenergy" in f:
200
+ return
201
+
202
+ self.ranked_solarpower = self.sort_by_power(forecast, ts_utc_quantized)
203
+ self.debug(
204
+ f"Solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
205
+ )
206
+ self.power_plan = [] # reset power plan, it depends on forecast
207
+
208
+ def on_power(self, m: dict[str, Any], ts: float) -> None:
209
+ """Handle the power consumption. Read the current power balance and accumulate
210
+ to the net energy balance to reflect the energy produced (or consumed) within the
211
+ current time slot.
212
+ Args:
213
+ m (dict[str, Any]): power consumption message
214
+ ts (float): current time
215
+ """
216
+ self.net_energy_power = m["power"]
217
+ balance: float = (ts - self.net_energy_balance_ts) * self.net_energy_power
218
+ self.net_energy_balance = self.net_energy_balance + balance
219
+ self.net_energy_balance_ts = ts
220
+
179
221
  @override
180
222
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
181
223
  m = None
182
224
  ts: float = timestamp()
183
225
  ts_utc_quantized: float = quantize(3600, ts - 3600)
184
226
  if msg.topic == self.topic_spot:
185
- self.ranked_spot_prices = self.sort_by_rank(
186
- json.loads(msg.payload.decode()), ts_utc_quantized
187
- )
188
- self.debug(
189
- f"Spot prices received and ranked for {len(self.ranked_spot_prices)} hours"
190
- )
191
- self.power_plan = [] # reset power plan, it depends on spot prices
227
+ self.on_spot(json.loads(msg.payload.decode()), ts_utc_quantized)
192
228
  return
193
229
  elif msg.topic == self.topic_forecast:
194
- forecast = json.loads(msg.payload.decode())
195
- # reject messages that don't have solarenergy forecast
196
-
197
- for f in forecast:
198
- if not "solarenergy" in f:
199
- return
200
-
201
- self.ranked_solarpower = self.sort_by_power(forecast, ts_utc_quantized)
202
- self.debug(
203
- f"Solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
204
- )
205
- self.power_plan = [] # reset power plan, it depends on forecast
230
+ self.on_forecast(json.loads(msg.payload.decode()), ts_utc_quantized)
206
231
  return
207
232
  elif msg.topic == self.topic_temperature:
208
233
  m = json.loads(msg.payload.decode())
209
234
  self.current_temperature = m["temperature"]
210
235
  elif msg.topic == self.topic_in_net_energy_balance:
211
- m = json.loads(msg.payload.decode())
212
- self.net_energy_balance = m["energy"]
213
- self.net_energy_power = m["power"]
236
+ self.on_power(json.loads(msg.payload.decode()), ts)
214
237
  elif msg.topic == self.topic_in_powerconsumption:
215
238
  m = json.loads(msg.payload.decode())
216
239
  self.current_power = m["real_total"]
@@ -220,7 +243,10 @@ class HotWaterOptimizer(Juham):
220
243
  self.on_powerplan(ts)
221
244
 
222
245
  def on_powerplan(self, ts_utc_now: float) -> None:
223
- """Apply power plan.
246
+ """Apply the power plan. Check if the relay needs to be switched on or off.
247
+ The relay is switched on if the current temperature is below the maximum
248
+ temperature and the current time is within the heating plan. The relay is switched off
249
+ if the current temperature is above the maximum temperature or the current time is outside.
224
250
 
225
251
  Args:
226
252
  ts_utc_now (float): utc time
@@ -313,22 +339,33 @@ class HotWaterOptimizer(Juham):
313
339
 
314
340
  # don't bother to switch the relay on for small intervals, to avoid
315
341
  # wearing contactors out
316
- if remaining_ts < self.operation_threshold:
342
+ if (
343
+ not self.net_energy_balancing_mode
344
+ and remaining_ts < self.operation_threshold
345
+ ):
317
346
  print(
318
347
  f"Skipping balance, remaining time {remaining_ts}s < {self.operation_threshold}s"
319
348
  )
320
349
  return False
350
+ elif remaining_ts <= 0:
351
+ self.net_energy_balancing_rc = False # heating off
352
+ self.info(
353
+ f"End of the balancing interval reached, disabled with {self.net_energy_balance/3600}kWh left"
354
+ )
355
+ self.net_energy_balance = 0.0
356
+ self.net_energy_balance_ts = ts
357
+ return False
321
358
 
322
- # check if the balance is sufficient for heating the next half of the energy balancing interval
323
- # if yes then switch heating on for the next half an hour
324
- needed_energy = 0.5 * self.radiator_power * remaining_ts
359
+ # check if the balance is sufficient for heating the remainin interval
360
+ # if yes then switch heating on
361
+ needed_energy = self.radiator_power * remaining_ts
325
362
  elapsed_interval = ts - self.net_energy_balance_ts
326
363
  print(
327
- f"Needed energy {int(needed_energy)}Wh, current balance {int(self.net_energy_balance)}Wh"
364
+ f"Needed energy {int(needed_energy)/3600}kWh, current balance {int(self.net_energy_balance/3600)}kWh"
328
365
  )
329
366
 
330
367
  if (
331
- self.net_energy_balance > needed_energy
368
+ self.net_energy_balance >= needed_energy
332
369
  ) and not self.net_energy_balancing_rc:
333
370
  self.net_energy_balance_ts = ts
334
371
  self.net_energy_balancing_rc = True # heat
@@ -338,7 +375,7 @@ class HotWaterOptimizer(Juham):
338
375
  # check if we have reach the end of the interval, or consumed all the energy
339
376
  # of the current slot. If so switch the energy balancer mode off
340
377
  if (
341
- elapsed_interval > self.energy_balancing_interval / 2.0
378
+ elapsed_interval > self.energy_balancing_interval
342
379
  or self.net_energy_balance < 0
343
380
  ):
344
381
  self.net_energy_balancing_rc = False # heating off
@@ -346,7 +383,9 @@ class HotWaterOptimizer(Juham):
346
383
  return self.net_energy_balancing_rc
347
384
 
348
385
  def consider_heating(self, ts: float) -> int:
349
- """Consider whether the target boiler needs heating.
386
+ """Consider whether the target boiler needs heating. Check first if the solar
387
+ energy is enough to heat the water the remaining time in the current slot.
388
+ If not, follow the predefined heating plan computed earlier based on the cheapest spot prices.
350
389
 
351
390
  Args:
352
391
  ts (float): current UTC time
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.17
3
+ Version: 0.0.19
4
4
  Summary: Juha's Ultimate Home Automation Masterpiece
5
5
  Author-email: J Meskanen <juham.api@gmail.com>
6
6
  Maintainer-email: "J. Meskanen" <juham.api@gmail.com>
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "juham-automation"
8
- version = "0.0.17"
8
+ version = "0.0.19"
9
9
  description = "Juha's Ultimate Home Automation Masterpiece"
10
10
  readme = {file = "README.rst", content-type = "text/markdown"}
11
11
  requires-python = ">=3.8"
@@ -104,6 +104,50 @@ class TestHotWaterOptimizer(unittest.TestCase):
104
104
  self.optimizer.on_message(None, None, mock_msg)
105
105
  self.assertEqual(self.optimizer.current_temperature, 55)
106
106
 
107
+ def test_consider_net_energy_balance(self):
108
+ # Test: Simulate passing time and accumulate energy
109
+ time_steps = [
110
+ 0,
111
+ 600,
112
+ 1200,
113
+ 1800,
114
+ 2400,
115
+ 3000,
116
+ 3300,
117
+ 3500,
118
+ 3600,
119
+ ] # Time steps in seconds
120
+ accumulated_power = 0
121
+
122
+ # Loop through the time steps and accumulate power
123
+ for ts in time_steps:
124
+ accumulated_power += 500000
125
+ self.optimizer.net_energy_balance = accumulated_power
126
+
127
+ # Call the method to check if balancing mode should be activated
128
+ heating_on = self.optimizer.consider_net_energy_balance(ts)
129
+
130
+ # Calculate the remaining energy needed to power the radiator for the rest of the time slot
131
+ remaining_time = self.optimizer.energy_balancing_interval - ts
132
+ required_energy = self.optimizer.radiator_power * remaining_time
133
+
134
+ # Check if heating was enabled or not based on energy balance
135
+ if accumulated_power >= required_energy:
136
+ self.assertTrue(heating_on, f"At time {ts}, heating should be ON")
137
+ else:
138
+ self.assertFalse(heating_on, f"At time {ts}, heating should be OFF")
139
+
140
+ # Ensure heating state is correct
141
+ if accumulated_power >= required_energy:
142
+ self.assertTrue(self.optimizer.net_energy_balancing_rc)
143
+ else:
144
+ self.assertFalse(self.optimizer.net_energy_balancing_rc)
145
+
146
+ # Check if the correct state transitions happen during the process
147
+ self.assertTrue(
148
+ self.optimizer.net_energy_balancing_rc
149
+ ) # At the end of the test, it should be on
150
+
107
151
 
108
152
  if __name__ == "__main__":
109
153
  unittest.main()