juham-automation 0.0.26__py3-none-any.whl → 0.0.28__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 +113 -32
- juham_automation/automation/heatingoptimizer.py +18 -2
- {juham_automation-0.0.26.dist-info → juham_automation-0.0.28.dist-info}/METADATA +2 -2
- {juham_automation-0.0.26.dist-info → juham_automation-0.0.28.dist-info}/RECORD +8 -8
- {juham_automation-0.0.26.dist-info → juham_automation-0.0.28.dist-info}/WHEEL +1 -1
- {juham_automation-0.0.26.dist-info → juham_automation-0.0.28.dist-info}/entry_points.txt +0 -0
- {juham_automation-0.0.26.dist-info → juham_automation-0.0.28.dist-info}/licenses/LICENSE.rst +0 -0
- {juham_automation-0.0.26.dist-info → juham_automation-0.0.28.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,8 @@
|
|
1
1
|
import json
|
2
|
-
from typing import Any
|
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
|
-
|
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
|
-
|
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.
|
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.
|
40
|
-
self.needed_energy: float =
|
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
|
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
|
-
|
85
|
-
if self.
|
86
|
-
|
87
|
-
self.
|
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 =
|
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.
|
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
|
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
|
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
|
182
|
+
bool: true if the given consumer is active
|
119
183
|
"""
|
120
|
-
|
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.
|
126
|
-
self.
|
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."""
|
@@ -90,7 +90,7 @@ class HeatingOptimizer(Juham):
|
|
90
90
|
self.topic_powerplan = self.make_topic_name("powerplan")
|
91
91
|
self.topic_in_powerconsumption = self.make_topic_name("powerconsumption")
|
92
92
|
self.topic_in_net_energy_balance = self.make_topic_name("net_energy_balance")
|
93
|
-
self.
|
93
|
+
self.topic_out_energybalance = self.make_topic_name("energybalance_consumers")
|
94
94
|
self.topic_out_power = self.make_topic_name("power") # controls the relay
|
95
95
|
|
96
96
|
self.current_temperature = 100
|
@@ -113,6 +113,18 @@ class HeatingOptimizer(Juham):
|
|
113
113
|
self.subscribe(self.topic_temperature)
|
114
114
|
self.subscribe(self.topic_in_powerconsumption)
|
115
115
|
self.subscribe(self.topic_in_net_energy_balance)
|
116
|
+
self.register_as_consumer()
|
117
|
+
|
118
|
+
def register_as_consumer(self) -> None:
|
119
|
+
"""Register this device as a consumer to the energy balancer. The energy balancer will then add this device
|
120
|
+
to its list of consumers and will tell the device when to heat."""
|
121
|
+
|
122
|
+
consumer: dict[str, Any] = {
|
123
|
+
"Unit": self.name,
|
124
|
+
"Power": self.radiator_power,
|
125
|
+
}
|
126
|
+
self.publish(self.topic_out_energybalance, json.dumps(consumer), 1, False)
|
127
|
+
self.info(f"Registered {self.name} as consumer with {self.radiator_power}W", "")
|
116
128
|
|
117
129
|
def sort_by_rank(
|
118
130
|
self, hours: list[dict[str, Any]], ts_utc_now: float
|
@@ -309,7 +321,11 @@ class HeatingOptimizer(Juham):
|
|
309
321
|
Returns:
|
310
322
|
bool: true if production exceeds the consumption
|
311
323
|
"""
|
312
|
-
|
324
|
+
if m["Unit"] == self.name:
|
325
|
+
self.net_energy_balance_mode = m["Mode"]
|
326
|
+
self.info(
|
327
|
+
"Net energy balance mode for {self.name} set to {m['Mode']}", str(m)
|
328
|
+
)
|
313
329
|
|
314
330
|
def consider_heating(self, ts: float) -> int:
|
315
331
|
"""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.
|
3
|
+
Version: 0.0.28
|
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>
|
@@ -44,7 +44,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
44
44
|
Requires-Python: >=3.8
|
45
45
|
Description-Content-Type: text/markdown
|
46
46
|
License-File: LICENSE.rst
|
47
|
-
Requires-Dist: juham_core>=0.1.
|
47
|
+
Requires-Dist: juham_core>=0.1.5
|
48
48
|
Provides-Extra: dev
|
49
49
|
Requires-Dist: check-manifest; extra == "dev"
|
50
50
|
Requires-Dist: types-pyz; extra == "dev"
|
@@ -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=
|
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=
|
7
|
+
juham_automation/automation/heatingoptimizer.py,sha256=f8T7KOa4Chgw2jfyxOkpvxTBfPZxc6lMQZYV4xopWyw,21664
|
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.
|
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.28.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
|
21
|
+
juham_automation-0.0.28.dist-info/METADATA,sha256=sPrwas2n-HhRjXduTKr5TQwI4F8R-GoMj5gHDEyqd2o,6837
|
22
|
+
juham_automation-0.0.28.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
23
|
+
juham_automation-0.0.28.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
|
24
|
+
juham_automation-0.0.28.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
|
25
|
+
juham_automation-0.0.28.dist-info/RECORD,,
|
File without changes
|
{juham_automation-0.0.26.dist-info → juham_automation-0.0.28.dist-info}/licenses/LICENSE.rst
RENAMED
File without changes
|
File without changes
|