juham-automation 0.0.16__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.16/juham_automation.egg-info → juham_automation-0.0.19}/PKG-INFO +1 -1
  2. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/automation/hotwateroptimizer.py +84 -37
  3. {juham_automation-0.0.16 → juham_automation-0.0.19/juham_automation.egg-info}/PKG-INFO +1 -1
  4. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation.egg-info/SOURCES.txt +1 -0
  5. {juham_automation-0.0.16 → juham_automation-0.0.19}/pyproject.toml +1 -1
  6. juham_automation-0.0.19/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  7. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/__pycache__/test_hotwateroptimizer.cpython-312.pyc +0 -0
  8. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/test_hotwateroptimizer.py +44 -0
  9. {juham_automation-0.0.16 → juham_automation-0.0.19}/LICENSE.rst +0 -0
  10. {juham_automation-0.0.16 → juham_automation-0.0.19}/MANIFEST.in +0 -0
  11. {juham_automation-0.0.16 → juham_automation-0.0.19}/README.rst +0 -0
  12. {juham_automation-0.0.16 → juham_automation-0.0.19}/docs/source/CHANGELOG.rst +0 -0
  13. {juham_automation-0.0.16 → juham_automation-0.0.19}/docs/source/CONTRIBUTING.rst +0 -0
  14. {juham_automation-0.0.16 → juham_automation-0.0.19}/docs/source/LICENSE.rst +0 -0
  15. {juham_automation-0.0.16 → juham_automation-0.0.19}/docs/source/README.rst +0 -0
  16. {juham_automation-0.0.16 → juham_automation-0.0.19}/examples/myapp.log +0 -0
  17. {juham_automation-0.0.16 → juham_automation-0.0.19}/examples/myapp.py +0 -0
  18. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/__init__.py +0 -0
  19. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/automation/__init__.py +0 -0
  20. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/automation/energycostcalculator.py +0 -0
  21. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/automation/powermeter_simulator.py +0 -0
  22. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/automation/spothintafi.py +0 -0
  23. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/automation/watercirculator.py +0 -0
  24. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/japp.py +0 -0
  25. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/py.typed +0 -0
  26. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/__init__.py +0 -0
  27. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/electricityprice_ts.py +0 -0
  28. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/energycostcalculator_ts.py +0 -0
  29. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/forecast_ts.py +0 -0
  30. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/log_ts.py +0 -0
  31. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/power_ts.py +0 -0
  32. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/powermeter_ts.py +0 -0
  33. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation/ts/powerplan_ts.py +0 -0
  34. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation.egg-info/dependency_links.txt +0 -0
  35. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation.egg-info/entry_points.txt +0 -0
  36. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation.egg-info/requires.txt +0 -0
  37. {juham_automation-0.0.16 → juham_automation-0.0.19}/juham_automation.egg-info/top_level.txt +0 -0
  38. {juham_automation-0.0.16 → juham_automation-0.0.19}/setup.cfg +0 -0
  39. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/__init__.py +0 -0
  40. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/__pycache__/test_japp.cpython-312.pyc +0 -0
  41. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/__init__.py +0 -0
  42. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  43. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/__pycache__/test_energycostcalculator.cpython-312.pyc +0 -0
  44. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/__pycache__/test_juham.cpython-312.pyc +0 -0
  45. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/__pycache__/test_powermeter_simulator.cpython-312.pyc +0 -0
  46. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/__pycache__/test_spothintafi.cpython-312.pyc +0 -0
  47. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/test_energycostcalculator.py +0 -0
  48. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/test_juham.py +0 -0
  49. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/automation/test_spothintafi.py +0 -0
  50. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/test_japp.py +0 -0
  51. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__init__.py +0 -0
  52. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__pycache__/__init__.cpython-312.pyc +0 -0
  53. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__pycache__/test_energycostcalculator_ts.cpython-312.pyc +0 -0
  54. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__pycache__/test_forecast_ts.cpython-312.pyc +0 -0
  55. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__pycache__/test_log_ts.cpython-312.pyc +0 -0
  56. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__pycache__/test_power_ts.cpython-312.pyc +0 -0
  57. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__pycache__/test_powermeter_ts.cpython-312.pyc +0 -0
  58. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/__pycache__/test_powerplan_ts.cpython-312.pyc +0 -0
  59. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/test_energycostcalculator_ts.py +0 -0
  60. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/test_forecast_ts.py +0 -0
  61. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/test_log_ts.py +0 -0
  62. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/test_power_ts.py +0 -0
  63. {juham_automation-0.0.16 → juham_automation-0.0.19}/tests/ts/test_powermeter_ts.py +0 -0
  64. {juham_automation-0.0.16 → 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.16
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>
@@ -16,13 +16,17 @@ from juham_core.timeutils import (
16
16
 
17
17
  class HotWaterOptimizer(Juham):
18
18
  """Automation class for optimized control of temperature driven home energy consumers e.g hot
19
- water radiators. Reads spot prices, electricity forecast and temperatures to minimize electricity bill.
20
- Additional control over heating is provided by the following attributes
19
+ water radiators. Reads spot prices, electricity forecast, power meter and temperatures to minimize electricity bill.
21
20
 
22
- Computes UOI - optimization utilization index for each hour, based on the spot price and the solar power forecast.
23
- Value of 0 means the hour is expensive, value of 1 means the hour is free. The UOI threshold determines the slots
24
- that are allowed to be consumed.
21
+ Represents a heating system that knows the power rating of its radiator (e.g., 3kW).
22
+ The system subscribes to the 'power' topic to track the current power balance. If the solar panels
23
+ generate more energy than is being consumed, the optimizer activates a relay to ensure that all excess energy
24
+ produced within that hour is used for heating. The goal is to achieve a net zero energy balance for each hour,
25
+ ensuring that any surplus energy from the solar panels is fully utilized.
25
26
 
27
+ Computes also UOI - optimization utilization index for each hour, based on the spot price and the solar power forecast.
28
+ For negative energy balance this determines when energy is consumed. Value of 0 means the hour is expensive, value of 1 means
29
+ the hour is free. The UOI threshold determines the slots that are allowed to be consumed.
26
30
 
27
31
  """
28
32
 
@@ -172,41 +176,64 @@ class HotWaterOptimizer(Juham):
172
176
  self.debug(f"Forecast sorted for the next {str(len(ranked_hours))} hours")
173
177
  return ranked_hours
174
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
+
175
221
  @override
176
222
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
177
223
  m = None
178
224
  ts: float = timestamp()
179
225
  ts_utc_quantized: float = quantize(3600, ts - 3600)
180
226
  if msg.topic == self.topic_spot:
181
- self.ranked_spot_prices = self.sort_by_rank(
182
- json.loads(msg.payload.decode()), ts_utc_quantized
183
- )
184
- self.debug(
185
- f"Spot prices received and ranked for {len(self.ranked_spot_prices)} hours"
186
- )
187
- self.power_plan = [] # reset power plan, it depends on spot prices
227
+ self.on_spot(json.loads(msg.payload.decode()), ts_utc_quantized)
188
228
  return
189
229
  elif msg.topic == self.topic_forecast:
190
- forecast = json.loads(msg.payload.decode())
191
- # reject messages that don't have solarenergy forecast
192
-
193
- for f in forecast:
194
- if not "solarenergy" in f:
195
- return
196
-
197
- self.ranked_solarpower = self.sort_by_power(forecast, ts_utc_quantized)
198
- self.debug(
199
- f"Solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
200
- )
201
- self.power_plan = [] # reset power plan, it depends on forecast
230
+ self.on_forecast(json.loads(msg.payload.decode()), ts_utc_quantized)
202
231
  return
203
232
  elif msg.topic == self.topic_temperature:
204
233
  m = json.loads(msg.payload.decode())
205
234
  self.current_temperature = m["temperature"]
206
235
  elif msg.topic == self.topic_in_net_energy_balance:
207
- m = json.loads(msg.payload.decode())
208
- self.net_energy_balance = m["energy"]
209
- self.net_energy_power = m["power"]
236
+ self.on_power(json.loads(msg.payload.decode()), ts)
210
237
  elif msg.topic == self.topic_in_powerconsumption:
211
238
  m = json.loads(msg.payload.decode())
212
239
  self.current_power = m["real_total"]
@@ -216,7 +243,10 @@ class HotWaterOptimizer(Juham):
216
243
  self.on_powerplan(ts)
217
244
 
218
245
  def on_powerplan(self, ts_utc_now: float) -> None:
219
- """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.
220
250
 
221
251
  Args:
222
252
  ts_utc_now (float): utc time
@@ -299,28 +329,43 @@ class HotWaterOptimizer(Juham):
299
329
  bool: true if production exceeds the consumption
300
330
  """
301
331
 
332
+ # if net energy balance is negative, then we are not in balancing mode
333
+ if self.net_energy_balance < 0:
334
+ return False
335
+
302
336
  # elapsed and remaining time within the current balancing slot
303
337
  elapsed_ts = ts - quantize(self.energy_balancing_interval, ts)
304
338
  remaining_ts = self.energy_balancing_interval - elapsed_ts
305
339
 
306
340
  # don't bother to switch the relay on for small intervals, to avoid
307
341
  # wearing contactors out
308
- if remaining_ts < self.operation_threshold:
342
+ if (
343
+ not self.net_energy_balancing_mode
344
+ and remaining_ts < self.operation_threshold
345
+ ):
309
346
  print(
310
347
  f"Skipping balance, remaining time {remaining_ts}s < {self.operation_threshold}s"
311
348
  )
312
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
313
358
 
314
- # check if the balance is sufficient for heating the next half of the energy balancing interval
315
- # if yes then switch heating on for the next half an hour
316
- 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
317
362
  elapsed_interval = ts - self.net_energy_balance_ts
318
363
  print(
319
- f"Needed energy {needed_energy}Wh, current balance {self.net_energy_balance}Wh"
364
+ f"Needed energy {int(needed_energy)/3600}kWh, current balance {int(self.net_energy_balance/3600)}kWh"
320
365
  )
321
366
 
322
367
  if (
323
- self.net_energy_balance > needed_energy
368
+ self.net_energy_balance >= needed_energy
324
369
  ) and not self.net_energy_balancing_rc:
325
370
  self.net_energy_balance_ts = ts
326
371
  self.net_energy_balancing_rc = True # heat
@@ -330,7 +375,7 @@ class HotWaterOptimizer(Juham):
330
375
  # check if we have reach the end of the interval, or consumed all the energy
331
376
  # of the current slot. If so switch the energy balancer mode off
332
377
  if (
333
- elapsed_interval > self.energy_balancing_interval / 2.0
378
+ elapsed_interval > self.energy_balancing_interval
334
379
  or self.net_energy_balance < 0
335
380
  ):
336
381
  self.net_energy_balancing_rc = False # heating off
@@ -338,7 +383,9 @@ class HotWaterOptimizer(Juham):
338
383
  return self.net_energy_balancing_rc
339
384
 
340
385
  def consider_heating(self, ts: float) -> int:
341
- """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.
342
389
 
343
390
  Args:
344
391
  ts (float): current UTC time
@@ -349,7 +396,7 @@ class HotWaterOptimizer(Juham):
349
396
 
350
397
  # check if we have energy to consume, if so return 1
351
398
  if self.consider_net_energy_balance(ts):
352
- self.warning("Net energy balance positive")
399
+ self.info("Net energy balance positive")
353
400
  return 1
354
401
  elif self.net_energy_balancing_mode:
355
402
  balancing_slot_start_ts = quantize(self.energy_balancing_interval, ts)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.16
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>
@@ -33,6 +33,7 @@ juham_automation/ts/powermeter_ts.py
33
33
  juham_automation/ts/powerplan_ts.py
34
34
  tests/__init__.py
35
35
  tests/test_japp.py
36
+ tests/__pycache__/__init__.cpython-312.pyc
36
37
  tests/__pycache__/test_japp.cpython-312.pyc
37
38
  tests/automation/__init__.py
38
39
  tests/automation/test_energycostcalculator.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "juham-automation"
8
- version = "0.0.16"
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()