juham-automation 0.0.27__py3-none-any.whl → 0.0.29__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.
@@ -1,9 +1,8 @@
1
1
  import json
2
- from typing import Any, Dict
2
+ from typing import Any
3
3
  from typing_extensions import override
4
4
 
5
5
  from juham_core import Juham, timestamp
6
- from juham_core.timeutils import timestampstr, quantize
7
6
  from masterpiece import MqttMsg
8
7
 
9
8
 
@@ -13,15 +12,18 @@ class EnergyBalancer(Juham):
13
12
  a given energy-consuming device, such as heating radiators, to operate within the
14
13
  remaining time of the interval.
15
14
 
16
- Any number of energy-consuming devices can be connected to the energy balancer.
17
- The energy balancer is typically used in conjunction with a power meter that reads
15
+ Any number of energy-consuming devices can be connected to the energy balancer, with
16
+ without any restrictions on the power consumption. The energy balancer will monitor the
17
+ power consumption of the devices and determine if there is enough energy available for
18
+ the devices to operate within the remaining time of the balancing interval.
19
+
20
+ The energy balancer is used in conjunction with a power meter that reads
18
21
  the total power consumption of the house. The energy balancer uses the power meter
19
22
  """
20
23
 
21
24
  #: Description of the attribute
22
25
  energy_balancing_interval: int = 3600
23
- radiator_power: float = 3000
24
- timezone: str = "Europe/Helsinki"
26
+ """The time interval in seconds for energy balancing."""
25
27
 
26
28
  def __init__(self, name: str = "energybalancer") -> None:
27
29
  """Initialize the energy balancer.
@@ -32,19 +34,22 @@ class EnergyBalancer(Juham):
32
34
  """
33
35
  super().__init__(name)
34
36
 
35
- self.topic_in_powerconsumption = self.make_topic_name("powerconsumption")
37
+ self.topic_in_consumers = self.make_topic_name("energybalance_consumers")
36
38
  self.topic_in_net_energy_balance = self.make_topic_name("net_energy_balance")
37
39
  self.topic_out_energybalance = self.make_topic_name("energybalance")
38
40
  self.net_energy_balance: float = 0.0 # Energy balance in joules (watt-seconds)
39
- self.net_energy_balance_ts: float = -1
40
- self.needed_energy: float = self.energy_balancing_interval * self.radiator_power
41
+ self.current_interval_ts: float = -1
42
+ self.needed_energy: float = 0.0 # Energy needed in joules (watt-seconds)
41
43
  self.net_energy_balancing_mode: bool = False
44
+ self.consumers: dict[str, float] = {}
45
+ self.active_consumers: dict[str, dict[float, float]] = {}
42
46
 
43
47
  @override
44
48
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
45
49
  super().on_connect(client, userdata, flags, rc)
46
50
  if rc == 0:
47
51
  self.subscribe(self.topic_in_net_energy_balance)
52
+ self.subscribe(self.topic_in_consumers)
48
53
 
49
54
  @override
50
55
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
@@ -52,6 +57,8 @@ class EnergyBalancer(Juham):
52
57
 
53
58
  if msg.topic == self.topic_in_net_energy_balance:
54
59
  self.on_power(json.loads(msg.payload.decode()), ts)
60
+ elif msg.topic == self.topic_in_consumers:
61
+ self.on_consumer(json.loads(msg.payload.decode()))
55
62
  else:
56
63
  super().on_message(client, userdata, msg)
57
64
 
@@ -65,13 +72,22 @@ class EnergyBalancer(Juham):
65
72
  """
66
73
  self.update_energy_balance(m["power"], ts)
67
74
 
75
+ def on_consumer(self, m: dict[str, Any]) -> None:
76
+ """Add consumer, e.g. heating radiator to be controlled.
77
+ Args:
78
+ m (dict[str, Any]): power consumer message
79
+ ts (float): current time
80
+ """
81
+ self.consumers[m["Unit"]] = m["Power"]
82
+ self.info(f"Consumer {m['Unit']} added, power: {m['Power']}")
83
+
68
84
  def update_energy_balance(self, power: float, ts: float) -> None:
69
85
  """Update the current net net energy balance. The change in the balance is calculate the
70
86
  energy balance, which the time elapsed since the last update, multiplied by the
71
87
  power. Positive energy balance means we have produced energy that can be consumed
72
88
  at the end of the interval. The target is to use all the energy produced during the
73
- balancing interval. This method is typically called by the powermeter reading the
74
- total power consumption of the house
89
+ balancing interval. This method is called by the powermeter reading the
90
+ total power consumption of the house.
75
91
 
76
92
  Args:
77
93
  power (float): power reading from the powermeter. Positive value means
@@ -81,50 +97,115 @@ class EnergyBalancer(Juham):
81
97
  """
82
98
 
83
99
  # regardless of the mode, if we hit the end of the interval, reset the balance
84
- quantized_ts: float = ts % self.energy_balancing_interval
85
- if self.net_energy_balance_ts < 0:
86
- self.net_energy_balance_ts = quantized_ts
87
- self.needed_energy = self.energy_balancing_interval - quantized_ts
88
- elif quantized_ts <= self.net_energy_balance_ts:
89
- self.reset_net_energy_balance()
100
+ interval_ts: float = ts % self.energy_balancing_interval
101
+ if self.current_interval_ts < 0 or interval_ts <= self.current_interval_ts:
102
+ # time runs backwards, must be a new interval
103
+ self.reset_net_energy_balance(interval_ts)
90
104
  else:
91
105
  # update the energy balance with the elapsed time and the power
92
- elapsed_ts = quantized_ts - self.net_energy_balance_ts
106
+ elapsed_ts = interval_ts - self.current_interval_ts
93
107
  balance: float = elapsed_ts * power # joules i.e. watt-seconds
94
108
  self.net_energy_balance = self.net_energy_balance + balance
95
- self.net_energy_balance_ts = quantized_ts
96
- self.needed_energy = (
97
- self.energy_balancing_interval - quantized_ts
98
- ) * self.radiator_power
99
-
109
+ self.current_interval_ts = interval_ts
110
+ self.needed_energy = self.calculate_needed_energy(interval_ts)
100
111
  if self.net_energy_balancing_mode:
101
112
  if self.net_energy_balance <= 0:
102
113
  # if we have used all the energy, disable the balancing mode
103
- self.reset_net_energy_balance()
114
+ self.reset_net_energy_balance(0.0)
104
115
  else:
116
+ # consider enabling the balancing mode
117
+ # if we have enough energy to power the radiator for the rest of the time slot
105
118
  if self.net_energy_balance >= self.needed_energy:
106
119
  self.net_energy_balancing_mode = True
120
+ self.initialize_active_consumers(ts)
107
121
  self.publish_energybalance(ts)
108
122
 
109
- def consider_net_energy_balance(self, ts: float) -> bool:
123
+ def calculate_needed_energy(self, interval_ts: float) -> float:
124
+ """Calculate the energy needed to power the consumer for the rest of the time slot.
125
+ Assumes consumers run one at a time in serialized manner and are evenly distributed over the time slot
126
+ to minimze the power peak.
127
+
128
+ Args:
129
+ interval_ts (float): current elapsed seconds within the balancing interval.
130
+
131
+ Returns:
132
+ float: energy needed in joules
133
+ """
134
+ required_power: float = 0.0
135
+ num_consumers: int = len(self.consumers)
136
+ remaining_ts_consumer: float = (
137
+ self.energy_balancing_interval - interval_ts
138
+ ) / num_consumers
139
+ for consumer in self.consumers.values():
140
+ required_power += consumer * remaining_ts_consumer
141
+ return required_power
142
+
143
+ def initialize_active_consumers(self, ts: float) -> None:
144
+ """Initialize the list of active consumers with their start and stop times.
145
+
146
+ Args:
147
+ ts (float): current time.
148
+
149
+ Returns:
150
+ None
151
+ """
152
+ num_consumers: int = len(self.consumers)
153
+ if num_consumers == 0:
154
+ return # If there are no consumers, we simply do nothing
155
+ interval_ts: float = ts % self.energy_balancing_interval
156
+ secs_per_consumer: float = (
157
+ self.energy_balancing_interval - interval_ts
158
+ ) / num_consumers
159
+
160
+ # Reset the active consumers dictionary
161
+ self.active_consumers.clear()
162
+
163
+ for consumer_name, consumer_data in self.consumers.items():
164
+ start: float = interval_ts
165
+ stop: float = start + secs_per_consumer
166
+
167
+ # Add the consumer to the active consumers dictionary with its start and stop times
168
+ self.active_consumers[consumer_name] = {start: stop}
169
+
170
+ # Update interval_ts to the stop time for the next consumer
171
+ interval_ts = stop
172
+
173
+ def consider_net_energy_balance(self, unit: str, ts: float) -> bool:
110
174
  """Check if there is enough energy available for the consumer to heat
111
- the water in the remaining time within the balancing interval, and switch
112
- the balancing mode on if sufficient.
175
+ the water in the remaining time within the balancing interval.
113
176
 
114
177
  Args:
178
+ unit (str): name of the consumer
115
179
  ts (float): current time
116
180
 
117
181
  Returns:
118
- bool: true if production exceeds the consumption
182
+ bool: true if the given consumer is active
119
183
  """
120
- return self.net_energy_balancing_mode
184
+ # Check if the consumer exists in the active_consumers dictionary
185
+ if unit not in self.active_consumers:
186
+ return False # The consumer is not found, so return False
187
+
188
+ # Get the start and stop time dictionary for the consumer
189
+ consumer_times = self.active_consumers[unit]
190
+
191
+ # map the current time to the balancing interval time slot
192
+ interval_ts: float = ts % self.energy_balancing_interval
193
+
194
+ # Check if current time (ts) is within the active range
195
+ for start_ts, stop_ts in consumer_times.items():
196
+ if start_ts <= interval_ts < stop_ts:
197
+ return True # If the current time is within the range, the consumer is active
198
+
199
+ return False # If no matching time range was found, return False
121
200
 
122
- def reset_net_energy_balance(self) -> None:
201
+ def reset_net_energy_balance(self, interval_ts: float) -> None:
123
202
  """Reset the net energy balance at the end of the interval."""
124
203
  self.net_energy_balance = 0.0
125
- self.needed_energy = self.energy_balancing_interval * self.radiator_power
126
- self.net_energy_balance_ts = 0
204
+ self.current_interval_ts = interval_ts
205
+ self.needed_energy = self.calculate_needed_energy(interval_ts)
127
206
  self.net_energy_balancing_mode = False
207
+ self.active_consumers.clear() # Clear the active consumers at the end of the interval
208
+ self.info("Energy balance reset, interval ended")
128
209
 
129
210
  def activate_balancing_mode(self, ts: float) -> None:
130
211
  """Activate balancing mode when enough energy is available."""
@@ -88,10 +88,9 @@ class HeatingOptimizer(Juham):
88
88
  self.topic_forecast = self.make_topic_name("forecast")
89
89
  self.topic_temperature = self.make_topic_name(temperature_sensor)
90
90
  self.topic_powerplan = self.make_topic_name("powerplan")
91
- self.topic_in_powerconsumption = self.make_topic_name("powerconsumption")
92
- self.topic_in_net_energy_balance = self.make_topic_name("net_energy_balance")
93
91
  self.topic_in_energybalance = self.make_topic_name("energybalance")
94
- self.topic_out_power = self.make_topic_name("power") # controls the relay
92
+ self.topic_out_energybalance = self.make_topic_name("energybalance_consumers")
93
+ self.topic_out_power = self.make_topic_name("power")
95
94
 
96
95
  self.current_temperature = 100
97
96
  self.current_heating_plan = 0
@@ -111,8 +110,19 @@ class HeatingOptimizer(Juham):
111
110
  self.subscribe(self.topic_spot)
112
111
  self.subscribe(self.topic_forecast)
113
112
  self.subscribe(self.topic_temperature)
114
- self.subscribe(self.topic_in_powerconsumption)
115
- self.subscribe(self.topic_in_net_energy_balance)
113
+ self.subscribe(self.topic_in_energybalance)
114
+ self.register_as_consumer()
115
+
116
+ def register_as_consumer(self) -> None:
117
+ """Register this device as a consumer to the energy balancer. The energy balancer will then add this device
118
+ to its list of consumers and will tell the device when to heat."""
119
+
120
+ consumer: dict[str, Any] = {
121
+ "Unit": self.name,
122
+ "Power": self.radiator_power,
123
+ }
124
+ 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", "")
116
126
 
117
127
  def sort_by_rank(
118
128
  self, hours: list[dict[str, Any]], ts_utc_now: float
@@ -214,7 +224,7 @@ class HeatingOptimizer(Juham):
214
224
  elif msg.topic == self.topic_temperature:
215
225
  m = json.loads(msg.payload.decode())
216
226
  self.current_temperature = m["temperature"]
217
- elif msg.topic == self.topic_in_net_energy_balance:
227
+ elif msg.topic == self.topic_in_energybalance:
218
228
  decoded_payload = msg.payload.decode()
219
229
  m = json.loads(decoded_payload)
220
230
  self.on_netenergy_balance(m)
@@ -309,7 +319,11 @@ class HeatingOptimizer(Juham):
309
319
  Returns:
310
320
  bool: true if production exceeds the consumption
311
321
  """
312
- self.net_energy_balance_mode = m["Mode"]
322
+ if m["Unit"] == self.name:
323
+ self.net_energy_balance_mode = m["Mode"]
324
+ self.info(
325
+ "Net energy balance mode for {self.name} set to {m['Mode']}", str(m)
326
+ )
313
327
 
314
328
  def consider_heating(self, ts: float) -> int:
315
329
  """Consider whether the target boiler needs heating. Check first if the solar
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.27
3
+ Version: 0.0.29
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=Y8l3ARnLn1HPIR3IwNPOH69ky_JAI8lFgkXYO8oTUXY,6998
5
+ juham_automation/automation/energybalancer.py,sha256=V_2t6hkRWf0--o9njfa8G-AtHPYcPVNY2rDrO5WRqsc,10698
6
6
  juham_automation/automation/energycostcalculator.py,sha256=v30wxRpuY2gGBSMJifrFRTjsRU9t-iCiq33Vds7s3O8,10877
7
- juham_automation/automation/heatingoptimizer.py,sha256=X9M2Z0jzneJCHtsh3lXmGqct0GwF_sm2Zwcv-26ZPdU,20923
7
+ juham_automation/automation/heatingoptimizer.py,sha256=UYvffqQDk96oEvNBmu7R0W3Qq6kMmfW92Xzejlr7Le8,21481
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.27.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
- juham_automation-0.0.27.dist-info/METADATA,sha256=uQ3JKEJNyZgW0dgOXU2VcDRiEuORpAAUMz9IZoCKMTE,6837
22
- juham_automation-0.0.27.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
23
- juham_automation-0.0.27.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
- juham_automation-0.0.27.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
- juham_automation-0.0.27.dist-info/RECORD,,
20
+ juham_automation-0.0.29.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
+ juham_automation-0.0.29.dist-info/METADATA,sha256=oowx8jK8iXbNZL2EDQJ7VfNZNEdO5Z0TXyn20jhBgg8,6837
22
+ juham_automation-0.0.29.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
23
+ juham_automation-0.0.29.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
+ juham_automation-0.0.29.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
+ juham_automation-0.0.29.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (78.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5