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.
- juham_automation/automation/energybalancer.py +78 -55
- juham_automation/automation/heatingoptimizer.py +1 -3
- juham_automation/ts/energybalancer_ts.py +35 -9
- {juham_automation-0.0.30.dist-info → juham_automation-0.0.31.dist-info}/METADATA +1 -1
- {juham_automation-0.0.30.dist-info → juham_automation-0.0.31.dist-info}/RECORD +9 -9
- {juham_automation-0.0.30.dist-info → juham_automation-0.0.31.dist-info}/WHEEL +0 -0
- {juham_automation-0.0.30.dist-info → juham_automation-0.0.31.dist-info}/entry_points.txt +0 -0
- {juham_automation-0.0.30.dist-info → juham_automation-0.0.31.dist-info}/licenses/LICENSE.rst +0 -0
- {juham_automation-0.0.30.dist-info → juham_automation-0.0.31.dist-info}/top_level.txt +0 -0
@@ -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.
|
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,
|
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.
|
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
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
178
|
-
"""
|
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
|
-
|
183
|
-
ts (float): current time
|
197
|
+
ts (float): current time.
|
184
198
|
|
185
199
|
Returns:
|
186
|
-
|
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
|
-
#
|
193
|
-
|
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
|
-
#
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
"""
|
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.
|
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
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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("
|
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"]))
|
@@ -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=
|
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=
|
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=
|
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.
|
21
|
-
juham_automation-0.0.
|
22
|
-
juham_automation-0.0.
|
23
|
-
juham_automation-0.0.
|
24
|
-
juham_automation-0.0.
|
25
|
-
juham_automation-0.0.
|
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,,
|
File without changes
|
File without changes
|
{juham_automation-0.0.30.dist-info → juham_automation-0.0.31.dist-info}/licenses/LICENSE.rst
RENAMED
File without changes
|
File without changes
|