juham-automation 0.0.29__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
@@ -34,28 +57,29 @@ class EnergyBalancer(Juham):
34
57
  """
35
58
  super().__init__(name)
36
59
 
37
- self.topic_in_consumers = self.make_topic_name("energybalance_consumers")
38
- self.topic_in_net_energy_balance = self.make_topic_name("net_energy_balance")
39
- self.topic_out_energybalance = self.make_topic_name("energybalance")
60
+ self.topic_in_consumers = self.make_topic_name("energybalance/consumers")
61
+ self.topic_out_status = self.make_topic_name("energybalance/status")
62
+ self.topic_out_diagnostics = self.make_topic_name("energybalance/diagnostics")
63
+ self.topic_in_power = self.make_topic_name("net_energy_balance")
64
+
40
65
  self.net_energy_balance: float = 0.0 # Energy balance in joules (watt-seconds)
41
66
  self.current_interval_ts: float = -1
42
67
  self.needed_energy: float = 0.0 # Energy needed in joules (watt-seconds)
43
68
  self.net_energy_balancing_mode: bool = False
44
- self.consumers: dict[str, float] = {}
45
- self.active_consumers: dict[str, dict[float, float]] = {}
69
+ self.consumers: dict[str, Consumer] = {}
46
70
 
47
71
  @override
48
72
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
49
73
  super().on_connect(client, userdata, flags, rc)
50
74
  if rc == 0:
51
- self.subscribe(self.topic_in_net_energy_balance)
75
+ self.subscribe(self.topic_in_power)
52
76
  self.subscribe(self.topic_in_consumers)
53
77
 
54
78
  @override
55
79
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
56
80
  ts: float = timestamp()
57
81
 
58
- if msg.topic == self.topic_in_net_energy_balance:
82
+ if msg.topic == self.topic_in_power:
59
83
  self.on_power(json.loads(msg.payload.decode()), ts)
60
84
  elif msg.topic == self.topic_in_consumers:
61
85
  self.on_consumer(json.loads(msg.payload.decode()))
@@ -78,7 +102,7 @@ class EnergyBalancer(Juham):
78
102
  m (dict[str, Any]): power consumer message
79
103
  ts (float): current time
80
104
  """
81
- self.consumers[m["Unit"]] = m["Power"]
105
+ self.consumers[m["Unit"]] = Consumer(m["Unit"], m["Power"])
82
106
  self.info(f"Consumer {m['Unit']} added, power: {m['Power']}")
83
107
 
84
108
  def update_energy_balance(self, power: float, ts: float) -> None:
@@ -97,6 +121,7 @@ class EnergyBalancer(Juham):
97
121
  """
98
122
 
99
123
  # regardless of the mode, if we hit the end of the interval, reset the balance
124
+
100
125
  interval_ts: float = ts % self.energy_balancing_interval
101
126
  if self.current_interval_ts < 0 or interval_ts <= self.current_interval_ts:
102
127
  # time runs backwards, must be a new interval
@@ -117,7 +142,7 @@ class EnergyBalancer(Juham):
117
142
  # if we have enough energy to power the radiator for the rest of the time slot
118
143
  if self.net_energy_balance >= self.needed_energy:
119
144
  self.net_energy_balancing_mode = True
120
- self.initialize_active_consumers(ts)
145
+ self.initialize_consumer_timelines(ts)
121
146
  self.publish_energybalance(ts)
122
147
 
123
148
  def calculate_needed_energy(self, interval_ts: float) -> float:
@@ -131,16 +156,19 @@ class EnergyBalancer(Juham):
131
156
  Returns:
132
157
  float: energy needed in joules
133
158
  """
159
+
134
160
  required_power: float = 0.0
135
161
  num_consumers: int = len(self.consumers)
162
+ if num_consumers == 0:
163
+ return 0.0
136
164
  remaining_ts_consumer: float = (
137
165
  self.energy_balancing_interval - interval_ts
138
166
  ) / num_consumers
139
167
  for consumer in self.consumers.values():
140
- required_power += consumer * remaining_ts_consumer
168
+ required_power += consumer.power * remaining_ts_consumer
141
169
  return required_power
142
170
 
143
- def initialize_active_consumers(self, ts: float) -> None:
171
+ def initialize_consumer_timelines(self, ts: float) -> None:
144
172
  """Initialize the list of active consumers with their start and stop times.
145
173
 
146
174
  Args:
@@ -157,46 +185,59 @@ class EnergyBalancer(Juham):
157
185
  self.energy_balancing_interval - interval_ts
158
186
  ) / num_consumers
159
187
 
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
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
172
192
 
173
- def consider_net_energy_balance(self, unit: str, ts: float) -> bool:
174
- """Check if there is enough energy available for the consumer to heat
175
- the water in the remaining time within the balancing interval.
193
+ def publish_energybalance(self, ts: float) -> None:
194
+ """Publish diagnostics and status.
176
195
 
177
196
  Args:
178
- unit (str): name of the consumer
179
- ts (float): current time
197
+ ts (float): current time.
180
198
 
181
199
  Returns:
182
- bool: true if the given consumer is active
200
+ None
183
201
  """
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
202
 
188
- # Get the start and stop time dictionary for the consumer
189
- 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))
211
+
190
212
 
191
- # map the current time to the balancing interval time slot
192
- interval_ts: float = ts % self.energy_balancing_interval
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
193
230
 
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
231
+ Args:
232
+ name (str): name of the consumer
233
+ ts (float): current time.
198
234
 
199
- 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
200
241
 
201
242
  def reset_net_energy_balance(self, interval_ts: float) -> None:
202
243
  """Reset the net energy balance at the end of the interval."""
@@ -204,7 +245,10 @@ class EnergyBalancer(Juham):
204
245
  self.current_interval_ts = interval_ts
205
246
  self.needed_energy = self.calculate_needed_energy(interval_ts)
206
247
  self.net_energy_balancing_mode = False
207
- 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
+
208
252
  self.info("Energy balance reset, interval ended")
209
253
 
210
254
  def activate_balancing_mode(self, ts: float) -> None:
@@ -220,20 +264,3 @@ class EnergyBalancer(Juham):
220
264
  self.info("Balance used, or the end of the interval reached, disable")
221
265
  self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
222
266
 
223
- def publish_energybalance(self, ts: float) -> None:
224
- """Publish energy balance information.
225
-
226
- Args:
227
- ts (float): current time
228
- Returns:
229
- dict: diagnostics information
230
- """
231
- m: dict[str, Any] = {
232
- "Unit": self.name,
233
- "Mode": self.net_energy_balancing_mode,
234
- "Rc": self.net_energy_balancing_mode,
235
- "CurrentBalance": self.net_energy_balance,
236
- "NeededBalance": self.needed_energy,
237
- "Timestamp": ts,
238
- }
239
- self.publish(self.topic_out_energybalance, json.dumps(m))
@@ -88,8 +88,8 @@ 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_energybalance = self.make_topic_name("energybalance")
92
- self.topic_out_energybalance = self.make_topic_name("energybalance_consumers")
91
+ self.topic_in_energybalance = self.make_topic_name("energybalance/status")
92
+ self.topic_out_energybalance = self.make_topic_name("energybalance/consumers")
93
93
  self.topic_out_power = self.make_topic_name("power")
94
94
 
95
95
  self.current_temperature = 100
@@ -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.29
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=V_2t6hkRWf0--o9njfa8G-AtHPYcPVNY2rDrO5WRqsc,10698
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=UYvffqQDk96oEvNBmu7R0W3Qq6kMmfW92Xzejlr7Le8,21481
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.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,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.1)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5