juham-automation 0.0.31__py3-none-any.whl → 0.0.33__py3-none-any.whl

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.
@@ -14,7 +14,7 @@ class Consumer:
14
14
 
15
15
  """
16
16
 
17
- def __init__(self, name: str, power) -> None:
17
+ def __init__(self, name: str, power: float) -> None:
18
18
  """Initialize the consumer
19
19
 
20
20
  Args:
@@ -209,14 +209,13 @@ class EnergyBalancer(Juham):
209
209
  }
210
210
  self.publish(self.topic_out_diagnostics, json.dumps(m))
211
211
 
212
-
213
212
  # publish consumer statuses to control consumers
214
213
  num_consumers: int = len(self.consumers)
215
214
  if num_consumers == 0:
216
215
  return # If there are no consumers, we simply do nothing
217
216
  interval_ts = ts % self.energy_balancing_interval
218
217
  for consumer in self.consumers.values():
219
- m: dict[str, Any] = {
218
+ m = {
220
219
  "EnergyBalancer": self.name,
221
220
  "Unit": consumer.name,
222
221
  "Power": consumer.power,
@@ -263,4 +262,3 @@ class EnergyBalancer(Juham):
263
262
  self.net_energy_balancing_mode = False
264
263
  self.info("Balance used, or the end of the interval reached, disable")
265
264
  self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
266
-
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  import json
2
3
  from typing import Any
3
4
  from typing_extensions import override
@@ -16,31 +17,63 @@ from juham_core.timeutils import (
16
17
 
17
18
  class HeatingOptimizer(Juham):
18
19
  """Automation class for optimized control of temperature driven home energy consumers e.g hot
19
- water radiators. Reads spot prices, electricity forecast, power meter and temperatures to minimize electricity bill.
20
+ water radiators. Reads spot prices, solar electricity forecast, power meter and
21
+ temperature to minimize electricity bill.
20
22
 
21
23
  Represents a heating system that knows the power rating of its radiator (e.g., 3kW).
24
+ Any number of heating devices can be controlled, each with its own temperature, schedule and electricity price ratings
25
+
22
26
  The system subscribes to the 'power' topic to track the current power balance. If the solar panels
23
27
  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,
28
+ produced within that balancing interval is used for heating. The goal is to achieve a net zero energy balance for each hour,
25
29
  ensuring that any surplus energy from the solar panels is fully utilized.
26
30
 
27
31
  Computes also UOI - optimization utilization index for each hour, based on the spot price and the solar power forecast.
28
32
  For negative energy balance this determines when energy is consumed. Value of 0 means the hour is expensive, value of 1 means
29
33
  the hour is free. The UOI threshold determines the slots that are allowed to be consumed.
30
-
31
34
  """
32
35
 
33
- maximum_boiler_temperature: float = 70
34
- minimum_boiler_temperature: float = 40
35
36
  energy_balancing_interval: float = 3600
36
- radiator_power: float = 6000 #
37
- operation_threshold: float = 5 * 60
37
+ """Energy balancing interval, as regulated by the industry/converment. In seconds"""
38
+
39
+ radiator_power: float = 6000 # W
40
+ """Radiator power in Watts. This is the maximum power that the radiator can consume."""
41
+
38
42
  heating_hours_per_day: float = 4
43
+ """ Number of hours per day the radiator is allowed to heat."""
44
+
39
45
  schedule_start_hour: float = 0
46
+ """Start hour of the heating schedule."""
47
+
40
48
  schedule_stop_hour: float = 0
49
+ """Stop hour of the heating schedule. Heating is allowed only between these two hours."""
50
+
41
51
  timezone: str = "Europe/Helsinki"
52
+ """ Timezone of the heating system. This is used to convert UTC timestamps to local time."""
53
+
42
54
  expected_average_price: float = 0.2
55
+ """Expected average price of electricity, beyond which the heating is avoided."""
56
+
43
57
  uoi_threshold: float = 0.8
58
+ """Utilization Optimization Index threshold. This is the minimum UOI value that is allowed for the heating to be activated."""
59
+
60
+ temperature_limits: dict[int, tuple[float, float]] = {
61
+ 1: (20.0, 60.0), # January
62
+ 2: (20.0, 60.0), # February
63
+ 3: (20.0, 60.0), # March
64
+ 4: (20.0, 50.0), # April
65
+ 5: (20.0, 40.0), # May
66
+ 6: (20.0, 22.0), # June
67
+ 7: (20.0, 22.0), # July
68
+ 8: (20.0, 22.0), # August
69
+ 9: (20.0, 50.0), # September
70
+ 10: (20.0, 60.0), # October
71
+ 11: (20.0, 60.0), # November
72
+ 12: (20.0, 60.0), # December
73
+ }
74
+ """Temperature limits for each month. The minimum temperature is maintained regardless of the cost.
75
+ The limits are defined as a dictionary where the keys are month numbers (1-12)
76
+ and the values are tuples of (min_temp, max_temp). The min_temp and max_temp values are in degrees Celsius."""
44
77
 
45
78
  def __init__(
46
79
  self,
@@ -84,9 +117,9 @@ class HeatingOptimizer(Juham):
84
117
  self.start_hour = start_hour
85
118
  self.spot_limit = spot_limit
86
119
 
87
- self.topic_spot = self.make_topic_name("spot")
88
- self.topic_forecast = self.make_topic_name("forecast")
89
- self.topic_temperature = self.make_topic_name(temperature_sensor)
120
+ self.topic_in_spot = self.make_topic_name("spot")
121
+ self.topic_in_forecast = self.make_topic_name("forecast")
122
+ self.topic_in_temperature = self.make_topic_name(temperature_sensor)
90
123
  self.topic_powerplan = self.make_topic_name("powerplan")
91
124
  self.topic_in_energybalance = self.make_topic_name("energybalance/status")
92
125
  self.topic_out_energybalance = self.make_topic_name("energybalance/consumers")
@@ -107,9 +140,9 @@ class HeatingOptimizer(Juham):
107
140
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
108
141
  super().on_connect(client, userdata, flags, rc)
109
142
  if rc == 0:
110
- self.subscribe(self.topic_spot)
111
- self.subscribe(self.topic_forecast)
112
- self.subscribe(self.topic_temperature)
143
+ self.subscribe(self.topic_in_spot)
144
+ self.subscribe(self.topic_in_forecast)
145
+ self.subscribe(self.topic_in_temperature)
113
146
  self.subscribe(self.topic_in_energybalance)
114
147
  self.register_as_consumer()
115
148
 
@@ -122,7 +155,22 @@ class HeatingOptimizer(Juham):
122
155
  "Power": self.radiator_power,
123
156
  }
124
157
  self.publish(self.topic_out_energybalance, json.dumps(consumer), 1, False)
125
- self.info(f"Registered {self.name} as consumer with {self.radiator_power}W", "")
158
+ self.info(
159
+ f"Registered {self.name} as consumer with {self.radiator_power}W power",
160
+ "",
161
+ )
162
+
163
+ # Function to get the temperature limits based on the current month
164
+ def get_temperature_limits_for_current_month(self) -> tuple[float, float]:
165
+ """Get the temperature limits for the current month.
166
+ The limits are defined in a dictionary where the keys are month numbers (1-12)
167
+ and the values are tuples of (min_temp, max_temp).
168
+ Returns: tuple: (min_temp, max_temp)
169
+ """
170
+ current_month: int = datetime.now().month
171
+ # Get the min and max temperatures for the current month
172
+ min_temp, max_temp = self.temperature_limits[current_month]
173
+ return min_temp, max_temp
126
174
 
127
175
  def sort_by_rank(
128
176
  self, hours: list[dict[str, Any]], ts_utc_now: float
@@ -170,7 +218,7 @@ class HeatingOptimizer(Juham):
170
218
  reverse=True,
171
219
  )
172
220
  self.debug(
173
- f"Sorted {len(sh)} days of forecast starting at {timestampstr(ts_utc)}"
221
+ f"{self.name} sorted {len(sh)} days of forecast starting at {timestampstr(ts_utc)}"
174
222
  )
175
223
  ranked_hours: list[dict[str, Any]] = []
176
224
 
@@ -178,7 +226,9 @@ class HeatingOptimizer(Juham):
178
226
  utc_ts: float = float(h["ts"])
179
227
  if utc_ts >= ts_utc:
180
228
  ranked_hours.append(h)
181
- self.debug(f"Forecast sorted for the next {str(len(ranked_hours))} hours")
229
+ self.debug(
230
+ f"{self.name} forecast sorted for the next {str(len(ranked_hours))} hours"
231
+ )
182
232
  return ranked_hours
183
233
 
184
234
  def on_spot(self, m: list[dict[str, Any]], ts_quantized: float) -> None:
@@ -206,7 +256,7 @@ class HeatingOptimizer(Juham):
206
256
 
207
257
  self.ranked_solarpower = self.sort_by_power(forecast, ts_utc_quantized)
208
258
  self.debug(
209
- f"Solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
259
+ f"{self.name} solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
210
260
  )
211
261
  self.power_plan = [] # reset power plan, it depends on forecast
212
262
 
@@ -215,13 +265,13 @@ class HeatingOptimizer(Juham):
215
265
  m = None
216
266
  ts: float = timestamp()
217
267
  ts_utc_quantized: float = quantize(3600, ts - 3600)
218
- if msg.topic == self.topic_spot:
268
+ if msg.topic == self.topic_in_spot:
219
269
  self.on_spot(json.loads(msg.payload.decode()), ts_utc_quantized)
220
270
  return
221
- elif msg.topic == self.topic_forecast:
271
+ elif msg.topic == self.topic_in_forecast:
222
272
  self.on_forecast(json.loads(msg.payload.decode()), ts_utc_quantized)
223
273
  return
224
- elif msg.topic == self.topic_temperature:
274
+ elif msg.topic == self.topic_in_temperature:
225
275
  m = json.loads(msg.payload.decode())
226
276
  self.current_temperature = m["temperature"]
227
277
  elif msg.topic == self.topic_in_energybalance:
@@ -250,24 +300,24 @@ class HeatingOptimizer(Juham):
250
300
  self.relay_started_ts = ts_utc_now
251
301
 
252
302
  if not self.ranked_spot_prices:
253
- self.debug("Waiting spot prices...", "")
303
+ self.debug("{self.name} waiting spot prices...", "")
254
304
  return
255
305
 
256
306
  if not self.power_plan:
257
307
  self.power_plan = self.create_power_plan()
258
308
  self.heating_plan = []
259
309
  self.info(
260
- f"Power plan of length {len(self.power_plan)} created",
310
+ f"{self.name} power plan of length {len(self.power_plan)} created",
261
311
  str(self.power_plan),
262
312
  )
263
313
 
264
314
  if not self.power_plan:
265
- self.error("Failed to create a power plan", "")
315
+ self.error("{self.name} failed to create a power plan", "")
266
316
  return
267
317
 
268
318
  if len(self.power_plan) < 3:
269
319
  self.warning(
270
- f"Suspiciously short {len(self.power_plan)} power plan, wait more data ..",
320
+ f"{self.name} has suspiciously short {len(self.power_plan)} power plan, waiting for more data ..",
271
321
  "",
272
322
  )
273
323
  self.heating_plan = []
@@ -276,21 +326,25 @@ class HeatingOptimizer(Juham):
276
326
 
277
327
  if not self.ranked_solarpower or len(self.ranked_solarpower) < 4:
278
328
  self.warning(
279
- f"Short of forecast {len(self.ranked_solarpower)}, optimization compromised..",
329
+ f"{self.name} short of forecast {len(self.ranked_solarpower)}, optimization compromised..",
280
330
  "",
281
331
  )
282
332
 
283
333
  if not self.heating_plan:
284
334
  self.heating_plan = self.create_heating_plan()
285
335
  if not self.heating_plan:
286
- self.error("Failed to create heating plan")
336
+ self.error("{self.name} failed to create heating plan")
287
337
  return
288
338
  else:
289
339
  self.info(
290
- f"Heating plan of length {len(self.heating_plan)} created", ""
340
+ f"{self.name} heating plan of length {len(self.heating_plan)} created",
341
+ "",
291
342
  )
292
343
  if len(self.heating_plan) < 3:
293
- self.info(f"Short heating plan {len(self.heating_plan)}, no can do", "")
344
+ self.warning(
345
+ f"{self.name} has too short heating plan {len(self.heating_plan)}, no can do",
346
+ "",
347
+ )
294
348
  self.heating_plan = []
295
349
  self.power_plan = []
296
350
  return
@@ -304,7 +358,7 @@ class HeatingOptimizer(Juham):
304
358
  }
305
359
  self.publish(self.topic_out_power, json.dumps(heat), 1, False)
306
360
  self.info(
307
- f"Relay state {self.name} changed to {relay} at {timestampstr(ts_utc_now)}",
361
+ f"{self.name} relay changed to {relay} at {timestampstr(ts_utc_now)}",
308
362
  "",
309
363
  )
310
364
  self.current_relay_state = relay
@@ -322,7 +376,6 @@ class HeatingOptimizer(Juham):
322
376
  if m["Unit"] == self.name:
323
377
  self.net_energy_balance_mode = m["Mode"]
324
378
 
325
-
326
379
  def consider_heating(self, ts: float) -> int:
327
380
  """Consider whether the target boiler needs heating. Check first if the solar
328
381
  energy is enough to heat the water the remaining time in the current slot.
@@ -337,28 +390,45 @@ class HeatingOptimizer(Juham):
337
390
 
338
391
  # check if we have excess energy to spent within the current slot
339
392
  if self.net_energy_balance_mode:
340
- self.info("Positive net energy balance, spend it for heating")
393
+ self.debug(
394
+ "{self.name} with positive net energy balance, spend it for heating"
395
+ )
341
396
  return 1
342
397
 
343
- # no free energy available, don't spend if the current temperature is already high enough
344
- if self.current_temperature > self.maximum_boiler_temperature:
345
- self.info(
346
- f"Current temperature {self.current_temperature}C already beyond max {self.maximum_boiler_temperature}C"
347
- )
348
- return 0
349
398
  hour = timestamp_hour(ts)
399
+ state: int = -1
350
400
 
351
401
  # check if we are within the heating plan and see what the plan says
352
402
  for pp in self.heating_plan:
353
403
  ppts: float = pp["Timestamp"]
354
404
  h: float = timestamp_hour(ppts)
355
405
  if h == hour:
356
- return pp["State"]
406
+ state = pp["State"]
407
+ break
357
408
 
358
- # if we are not within the heating plan, then we are not heating
359
- # this should not happen, but just in case
360
- self.error(f"Cannot find heating plan for hour {hour}")
361
- return 0
409
+ if state == -1:
410
+ self.error(f"{self.name} cannot find heating plan for hour {hour}")
411
+ return 0
412
+
413
+ min_temp, max_temp = self.get_temperature_limits_for_current_month()
414
+ self.debug(
415
+ f"{self.name} month's temperature limits: Min = {min_temp}°C, Max = {max_temp}°C"
416
+ )
417
+
418
+ # don't heat if the current temperature is already high enough
419
+ if self.current_temperature > max_temp:
420
+ self.debug(
421
+ f"{self.name} plan {state}, temp {self.current_temperature}°C already beyond max {max_temp}°C"
422
+ )
423
+ return 0
424
+ # heat if the current temperature is below the required minimum
425
+ if self.current_temperature < min_temp:
426
+ self.debug(
427
+ f"{self.name} plan {state}, temp {self.current_temperature}°C below min {min_temp}°C"
428
+ )
429
+ return 1
430
+
431
+ return state # 1 = heating, 0 = not heating
362
432
 
363
433
  # compute utilization optimization index
364
434
  def compute_uoi(
@@ -425,7 +495,7 @@ class HeatingOptimizer(Juham):
425
495
  ts_utc_quantized = quantize(3600, timestamp() - 3600)
426
496
  starts: str = timestampstr(ts_utc_quantized)
427
497
  self.info(
428
- f"Trying to create power plan starting at {starts} with {len(self.ranked_spot_prices)} hourly spot prices",
498
+ f"{self.name} created power plan starting at {starts} with {len(self.ranked_spot_prices)} hours of spot prices",
429
499
  "",
430
500
  )
431
501
 
@@ -438,10 +508,11 @@ class HeatingOptimizer(Juham):
438
508
  )
439
509
 
440
510
  if len(spots) == 0:
441
- self.debug(
511
+ self.info(
442
512
  f"No spot prices initialized yet, can't proceed",
443
513
  "",
444
514
  )
515
+ return []
445
516
  self.info(
446
517
  f"Have spot prices for the next {len(spots)} hours",
447
518
  "",
@@ -497,7 +568,7 @@ class HeatingOptimizer(Juham):
497
568
 
498
569
  shplan = sorted(hplan, key=lambda x: x["FOM"], reverse=True)
499
570
 
500
- self.debug(f"Powerplan starts {starts} up to {len(shplan)} hours")
571
+ self.debug(f"{self.name} powerplan starts {starts} up to {len(shplan)} hours")
501
572
  return shplan
502
573
 
503
574
  def enable_relay(
@@ -547,5 +618,5 @@ class HeatingOptimizer(Juham):
547
618
  heating_plan.append(heat)
548
619
  hour = hour + 1
549
620
 
550
- self.info(f"Heating plan of {len(heating_plan)} hours created", "")
621
+ self.info(f"{self.name} heating plan of {len(heating_plan)} hours created", "")
551
622
  return heating_plan
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.31
3
+ Version: 0.0.33
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>
@@ -2,9 +2,9 @@ juham_automation/__init__.py,sha256=32BL36bhT7OaSw22H7st-7-3IXcFM2Pf5js80hNA8W0,
2
2
  juham_automation/japp.py,sha256=L2u1mfKvun2fiXhB3AEJD9zMDcdFZ3_doXZYJJzu9tg,1646
3
3
  juham_automation/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
4
  juham_automation/automation/__init__.py,sha256=uxkIrcRSp1cFikn-oBRtQ8XiT9cSf7xjm3CS1RN7lAQ,522
5
- juham_automation/automation/energybalancer.py,sha256=_08KikTDO3zHsRCV0oI7xyRpMyk594Q1C0acalRQoQs,10855
5
+ juham_automation/automation/energybalancer.py,sha256=2WnnajZdZZjHqUT9onCQCdkAIy_1nom8-y3u_pv2GM0,10844
6
6
  juham_automation/automation/energycostcalculator.py,sha256=v30wxRpuY2gGBSMJifrFRTjsRU9t-iCiq33Vds7s3O8,10877
7
- juham_automation/automation/heatingoptimizer.py,sha256=LMZ4Cr1CTsk1HI9D0P27l1bPFDepylhDh87dTd204r0,21367
7
+ juham_automation/automation/heatingoptimizer.py,sha256=DU-VvEHEZAj3C6CkT6K-7f-LhkAXRBYdf6RoxKbgydc,24412
8
8
  juham_automation/automation/powermeter_simulator.py,sha256=3WZcjByRTdqnC77l7LjP-TEjmZ8XBEO4hClYsrjxmBE,4549
9
9
  juham_automation/automation/spothintafi.py,sha256=cZbi7w2fVweHX_fh1r5MTjGdesX9wDQta2mfVjtiwvw,4331
10
10
  juham_automation/automation/watercirculator.py,sha256=a8meMNaONbHcIH3y0vP0UulJc1-gZiLZpw7H8kAOreY,6410
@@ -17,9 +17,9 @@ juham_automation/ts/log_ts.py,sha256=XsNaazuPmRUZLUqxU0DZae_frtT6kAFcXJTc598CtOA
17
17
  juham_automation/ts/power_ts.py,sha256=e7bSeZjitY4C_gLup9L0NjvU_WnQsl3ayDhVShj32KY,1399
18
18
  juham_automation/ts/powermeter_ts.py,sha256=gXzfK2S4SzrQ9GqM0tsLaV6z_vYmTkBatTcaivASSXs,2188
19
19
  juham_automation/ts/powerplan_ts.py,sha256=LZeE7TnzPCDaugggKlaV-K48lDwwnC1ZNum50JYAWaY,1482
20
- juham_automation-0.0.31.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
- juham_automation-0.0.31.dist-info/METADATA,sha256=6PmgwoKzNtP2ZHHr_L_fhCcavtvkCOfL4COQcCje2A8,6837
22
- juham_automation-0.0.31.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
23
- juham_automation-0.0.31.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
- juham_automation-0.0.31.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
- juham_automation-0.0.31.dist-info/RECORD,,
20
+ juham_automation-0.0.33.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
+ juham_automation-0.0.33.dist-info/METADATA,sha256=paKzowab6A0iDnu5qduBVXOWofockK1LhXp2KzcSnX4,6837
22
+ juham_automation-0.0.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ juham_automation-0.0.33.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
+ juham_automation-0.0.33.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
+ juham_automation-0.0.33.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5