juham-automation 0.0.30__py3-none-any.whl → 0.0.31__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.
@@ -6,6 +6,27 @@ from juham_core import Juham, timestamp
6
6
  from masterpiece import MqttMsg
7
7
 
8
8
 
9
+ class Consumer:
10
+ """Class representing a consumer.
11
+
12
+ This class is used to represent a consumer in the energy balancer.
13
+ It contains the name of the consumer and its power consumption.
14
+
15
+ """
16
+
17
+ def __init__(self, name: str, power) -> None:
18
+ """Initialize the consumer
19
+
20
+ Args:
21
+ name (str): name of the consumer
22
+ power (float): power of the consumer in watts
23
+ """
24
+ self.name = name
25
+ self.power: float = power
26
+ self.start: float = 0.0
27
+ self.stop: float = 0.0
28
+
29
+
9
30
  class EnergyBalancer(Juham):
10
31
  """The energy balancer monitors the balance between produced and consumed energy
11
32
  within the balancing interval to determine if there is enough energy available for
@@ -19,6 +40,8 @@ class EnergyBalancer(Juham):
19
40
 
20
41
  The energy balancer is used in conjunction with a power meter that reads
21
42
  the total power consumption of the house. The energy balancer uses the power meter
43
+
44
+
22
45
  """
23
46
 
24
47
  #: Description of the attribute
@@ -35,15 +58,15 @@ class EnergyBalancer(Juham):
35
58
  super().__init__(name)
36
59
 
37
60
  self.topic_in_consumers = self.make_topic_name("energybalance/consumers")
38
- self.topic_out_energybalance = self.make_topic_name("energybalance/status")
61
+ self.topic_out_status = self.make_topic_name("energybalance/status")
62
+ self.topic_out_diagnostics = self.make_topic_name("energybalance/diagnostics")
39
63
  self.topic_in_power = self.make_topic_name("net_energy_balance")
40
64
 
41
65
  self.net_energy_balance: float = 0.0 # Energy balance in joules (watt-seconds)
42
66
  self.current_interval_ts: float = -1
43
67
  self.needed_energy: float = 0.0 # Energy needed in joules (watt-seconds)
44
68
  self.net_energy_balancing_mode: bool = False
45
- self.consumers: dict[str, float] = {}
46
- self.active_consumers: dict[str, dict[float, float]] = {}
69
+ self.consumers: dict[str, Consumer] = {}
47
70
 
48
71
  @override
49
72
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
@@ -79,7 +102,7 @@ class EnergyBalancer(Juham):
79
102
  m (dict[str, Any]): power consumer message
80
103
  ts (float): current time
81
104
  """
82
- self.consumers[m["Unit"]] = m["Power"]
105
+ self.consumers[m["Unit"]] = Consumer(m["Unit"], m["Power"])
83
106
  self.info(f"Consumer {m['Unit']} added, power: {m['Power']}")
84
107
 
85
108
  def update_energy_balance(self, power: float, ts: float) -> None:
@@ -98,6 +121,7 @@ class EnergyBalancer(Juham):
98
121
  """
99
122
 
100
123
  # regardless of the mode, if we hit the end of the interval, reset the balance
124
+
101
125
  interval_ts: float = ts % self.energy_balancing_interval
102
126
  if self.current_interval_ts < 0 or interval_ts <= self.current_interval_ts:
103
127
  # time runs backwards, must be a new interval
@@ -118,7 +142,7 @@ class EnergyBalancer(Juham):
118
142
  # if we have enough energy to power the radiator for the rest of the time slot
119
143
  if self.net_energy_balance >= self.needed_energy:
120
144
  self.net_energy_balancing_mode = True
121
- self.initialize_active_consumers(ts)
145
+ self.initialize_consumer_timelines(ts)
122
146
  self.publish_energybalance(ts)
123
147
 
124
148
  def calculate_needed_energy(self, interval_ts: float) -> float:
@@ -141,10 +165,10 @@ class EnergyBalancer(Juham):
141
165
  self.energy_balancing_interval - interval_ts
142
166
  ) / num_consumers
143
167
  for consumer in self.consumers.values():
144
- required_power += consumer * remaining_ts_consumer
168
+ required_power += consumer.power * remaining_ts_consumer
145
169
  return required_power
146
170
 
147
- def initialize_active_consumers(self, ts: float) -> None:
171
+ def initialize_consumer_timelines(self, ts: float) -> None:
148
172
  """Initialize the list of active consumers with their start and stop times.
149
173
 
150
174
  Args:
@@ -161,46 +185,59 @@ class EnergyBalancer(Juham):
161
185
  self.energy_balancing_interval - interval_ts
162
186
  ) / num_consumers
163
187
 
164
- # Reset the active consumers dictionary
165
- self.active_consumers.clear()
166
-
167
- for consumer_name, consumer_data in self.consumers.items():
168
- start: float = interval_ts
169
- stop: float = start + secs_per_consumer
170
-
171
- # Add the consumer to the active consumers dictionary with its start and stop times
172
- self.active_consumers[consumer_name] = {start: stop}
173
-
174
- # Update interval_ts to the stop time for the next consumer
175
- interval_ts = stop
188
+ for consumer in self.consumers.values():
189
+ consumer.start = interval_ts
190
+ consumer.stop = interval_ts + secs_per_consumer
191
+ interval_ts += secs_per_consumer
176
192
 
177
- def consider_net_energy_balance(self, unit: str, ts: float) -> bool:
178
- """Check if there is enough energy available for the consumer to heat
179
- the water in the remaining time within the balancing interval.
193
+ def publish_energybalance(self, ts: float) -> None:
194
+ """Publish diagnostics and status.
180
195
 
181
196
  Args:
182
- unit (str): name of the consumer
183
- ts (float): current time
197
+ ts (float): current time.
184
198
 
185
199
  Returns:
186
- bool: true if the given consumer is active
200
+ None
187
201
  """
188
- # Check if the consumer exists in the active_consumers dictionary
189
- if unit not in self.active_consumers:
190
- return False # The consumer is not found, so return False
191
202
 
192
- # Get the start and stop time dictionary for the consumer
193
- consumer_times = self.active_consumers[unit]
203
+ # publish diagnostics
204
+ m: dict[str, Any] = {
205
+ "EnergyBalancer": self.name,
206
+ "CurrentBalance": self.net_energy_balance,
207
+ "NeededBalance": self.needed_energy,
208
+ "Timestamp": ts,
209
+ }
210
+ self.publish(self.topic_out_diagnostics, json.dumps(m))
194
211
 
195
- # map the current time to the balancing interval time slot
196
- interval_ts: float = ts % self.energy_balancing_interval
197
212
 
198
- # Check if current time (ts) is within the active range
199
- for start_ts, stop_ts in consumer_times.items():
200
- if start_ts <= interval_ts < stop_ts:
201
- return True # If the current time is within the range, the consumer is active
213
+ # publish consumer statuses to control consumers
214
+ num_consumers: int = len(self.consumers)
215
+ if num_consumers == 0:
216
+ return # If there are no consumers, we simply do nothing
217
+ interval_ts = ts % self.energy_balancing_interval
218
+ for consumer in self.consumers.values():
219
+ m: dict[str, Any] = {
220
+ "EnergyBalancer": self.name,
221
+ "Unit": consumer.name,
222
+ "Power": consumer.power,
223
+ "Mode": consumer.start <= interval_ts < consumer.stop,
224
+ "Timestamp": ts,
225
+ }
226
+ self.publish(self.topic_out_status, json.dumps(m))
227
+
228
+ def detect_consumer_status(self, name: str, ts: float) -> bool:
229
+ """Detect consumer status
230
+
231
+ Args:
232
+ name (str): name of the consumer
233
+ ts (float): current time.
202
234
 
203
- return False # If no matching time range was found, return False
235
+ Returns:
236
+ True if the consumer is active, False otherwise
237
+ """
238
+ consumer: Consumer = self.consumers[name]
239
+ interval_ts = ts % self.energy_balancing_interval
240
+ return consumer.start <= interval_ts < consumer.stop
204
241
 
205
242
  def reset_net_energy_balance(self, interval_ts: float) -> None:
206
243
  """Reset the net energy balance at the end of the interval."""
@@ -208,7 +245,10 @@ class EnergyBalancer(Juham):
208
245
  self.current_interval_ts = interval_ts
209
246
  self.needed_energy = self.calculate_needed_energy(interval_ts)
210
247
  self.net_energy_balancing_mode = False
211
- self.active_consumers.clear() # Clear the active consumers at the end of the interval
248
+ for consumer in self.consumers.values():
249
+ consumer.start = 0.0
250
+ consumer.stop = 0.0
251
+
212
252
  self.info("Energy balance reset, interval ended")
213
253
 
214
254
  def activate_balancing_mode(self, ts: float) -> None:
@@ -224,20 +264,3 @@ class EnergyBalancer(Juham):
224
264
  self.info("Balance used, or the end of the interval reached, disable")
225
265
  self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
226
266
 
227
- def publish_energybalance(self, ts: float) -> None:
228
- """Publish energy balance information.
229
-
230
- Args:
231
- ts (float): current time
232
- Returns:
233
- dict: diagnostics information
234
- """
235
- m: dict[str, Any] = {
236
- "Unit": self.name,
237
- "Mode": self.net_energy_balancing_mode,
238
- "Rc": self.net_energy_balancing_mode,
239
- "CurrentBalance": self.net_energy_balance,
240
- "NeededBalance": self.needed_energy,
241
- "Timestamp": ts,
242
- }
243
- self.publish(self.topic_out_energybalance, json.dumps(m))
@@ -321,9 +321,7 @@ class HeatingOptimizer(Juham):
321
321
  """
322
322
  if m["Unit"] == self.name:
323
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
- )
324
+
327
325
 
328
326
  def consider_heating(self, ts: float) -> int:
329
327
  """Consider whether the target boiler needs heating. Check first if the solar
@@ -9,7 +9,7 @@ from juham_core.timeutils import epoc2utc
9
9
 
10
10
 
11
11
  class EnergyBalancerTs(JuhamTs):
12
- """Heating optimizer diagnosis.
12
+ """Record energy balance data to time series database.
13
13
 
14
14
  This class listens the "energybalance" MQTT topic and records the
15
15
  messages to time series database.
@@ -19,27 +19,53 @@ class EnergyBalancerTs(JuhamTs):
19
19
  """Construct record object with the given name."""
20
20
 
21
21
  super().__init__(name)
22
- self.topic_name = self.make_topic_name("energybalance")
22
+ self.topic_in_status = self.make_topic_name("energybalance/status")
23
+ self.topic_in_diagnostics = self.make_topic_name("energybalance/diagnostics")
23
24
 
24
25
  @override
25
26
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
26
27
  super().on_connect(client, userdata, flags, rc)
27
- self.subscribe(self.topic_name)
28
- self.debug(f"Subscribed to {self.topic_name}")
28
+ if rc == 0:
29
+ self.subscribe(self.topic_in_status)
30
+ self.subscribe(self.topic_in_diagnostics)
29
31
 
30
32
  @override
31
33
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
32
- """Standard mqtt message notification method.
34
+ if msg.topic == self.topic_in_status:
35
+ self.on_status(json.loads(msg.payload.decode()))
36
+ elif msg.topic == self.topic_in_diagnostics:
37
+ self.on_diagnostics(json.loads(msg.payload.decode()))
38
+ else:
39
+ super().on_message(client, userdata, msg)
33
40
 
34
- This method is called upon new arrived message.
41
+ def on_status(self, m: dict[str, Any]) -> None:
42
+ """Handle energybalance message.
43
+ Args:
44
+ m (dict[str, Any]): Message from energybalance topic.
35
45
  """
36
-
37
- m = json.loads(msg.payload.decode())
46
+ if not "Power" in m or not "Timestamp" in m:
47
+ self.error(f"INVALID STATUS msg {m}")
48
+ return
38
49
  point = (
39
50
  self.measurement("energybalance")
40
51
  .tag("Unit", m["Unit"])
41
52
  .field("Mode", m["Mode"])
42
- .field("Rc", m["Rc"])
53
+ .field("Power", float(m["Power"]))
54
+ .time(epoc2utc(m["Timestamp"]))
55
+ )
56
+ self.write(point)
57
+
58
+ def on_diagnostics(self, m: dict[str, Any]) -> None:
59
+ """Handle energybalance diagnostics.
60
+ Args:
61
+ m (dict[str, Any]): Message from energybalance topic.
62
+ """
63
+ if not "Timestamp" in m:
64
+ self.error(f"INVALID DIAGNOSTICS msg {m}")
65
+ return
66
+ point = (
67
+ self.measurement("energybalance")
68
+ .tag("EnergyBalancer", m["EnergyBalancer"])
43
69
  .field("CurrentBalance", m["CurrentBalance"])
44
70
  .field("NeededBalance", m["NeededBalance"])
45
71
  .time(epoc2utc(m["Timestamp"]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.30
3
+ Version: 0.0.31
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,24 +2,24 @@ 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=HcBTCI4eS429Up2-lx5zkAZQmZGn2ZCVTwGCavsjGg4,10722
5
+ juham_automation/automation/energybalancer.py,sha256=_08KikTDO3zHsRCV0oI7xyRpMyk594Q1C0acalRQoQs,10855
6
6
  juham_automation/automation/energycostcalculator.py,sha256=v30wxRpuY2gGBSMJifrFRTjsRU9t-iCiq33Vds7s3O8,10877
7
- juham_automation/automation/heatingoptimizer.py,sha256=V56gZEsjJOf4veZCZYT1JUv8Tv8lmZ-65zLEb6T7q8s,21488
7
+ juham_automation/automation/heatingoptimizer.py,sha256=LMZ4Cr1CTsk1HI9D0P27l1bPFDepylhDh87dTd204r0,21367
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
11
11
  juham_automation/ts/__init__.py,sha256=kTEzVkDi6ednH4-fxKxrY6enlTuTXmSw09pPAQX3CMc,612
12
12
  juham_automation/ts/electricityprice_ts.py,sha256=BYs120V4teVjSqPc8PpPDjOTc5dOrVM9Maqse7E8cvk,1684
13
- juham_automation/ts/energybalancer_ts.py,sha256=KpMYnyzAKghPEMHAVHrh_aplorn9TeSvZr-DVpfir3c,1476
13
+ juham_automation/ts/energybalancer_ts.py,sha256=XHl56G5fBjJOCSIJtdjzGpSTMAQN1xsnXpC4Ipl7ynw,2585
14
14
  juham_automation/ts/energycostcalculator_ts.py,sha256=MbeYEGlziVgq4zI40Tk71zxeDPeKafEG3s0LqDRiz0g,1277
15
15
  juham_automation/ts/forecast_ts.py,sha256=Gk46hIlS8ijxs-zyy8fBvXrhI7J-8e5Gt2QEe6gFB6s,3158
16
16
  juham_automation/ts/log_ts.py,sha256=XsNaazuPmRUZLUqxU0DZae_frtT6kAFcXJTc598CtOA,1750
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.30.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
- juham_automation-0.0.30.dist-info/METADATA,sha256=Be-9u_ReioR9XS4Z4U8jtaQBxrSbdGfoJPQHx9OkaXg,6837
22
- juham_automation-0.0.30.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
23
- juham_automation-0.0.30.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
- juham_automation-0.0.30.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
- juham_automation-0.0.30.dist-info/RECORD,,
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,,