juham-automation 0.0.26__tar.gz → 0.0.28__tar.gz
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-0.0.26/juham_automation.egg-info → juham_automation-0.0.28}/PKG-INFO +2 -2
- juham_automation-0.0.28/juham_automation/automation/energybalancer.py +239 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/heatingoptimizer.py +18 -2
- {juham_automation-0.0.26 → juham_automation-0.0.28/juham_automation.egg-info}/PKG-INFO +2 -2
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation.egg-info/requires.txt +1 -1
- {juham_automation-0.0.26 → juham_automation-0.0.28}/pyproject.toml +2 -2
- juham_automation-0.0.28/tests/automation/test_energybalancer.py +107 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/automation/test_heatingoptimizer.py +5 -4
- juham_automation-0.0.26/juham_automation/automation/energybalancer.py +0 -158
- juham_automation-0.0.26/tests/automation/test_energybalancer.py +0 -183
- {juham_automation-0.0.26 → juham_automation-0.0.28}/LICENSE.rst +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/MANIFEST.in +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/README.rst +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/examples/myapp.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/__init__.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/__init__.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/energycostcalculator.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/powermeter_simulator.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/spothintafi.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/watercirculator.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/japp.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/py.typed +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/__init__.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/electricityprice_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/energybalancer_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/energycostcalculator_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/forecast_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/log_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/power_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/powermeter_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/powerplan_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation.egg-info/SOURCES.txt +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation.egg-info/dependency_links.txt +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation.egg-info/entry_points.txt +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation.egg-info/top_level.txt +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/setup.cfg +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/__init__.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/automation/__init__.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/automation/test_energycostcalculator.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/automation/test_juham.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/automation/test_spothintafi.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/test_japp.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/__init__.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/test_energycostcalculator_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/test_forecast_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/test_log_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/test_power_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/test_powermeter_ts.py +0 -0
- {juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/test_powerplan_ts.py +0 -0
@@ -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"
|
@@ -0,0 +1,239 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any
|
3
|
+
from typing_extensions import override
|
4
|
+
|
5
|
+
from juham_core import Juham, timestamp
|
6
|
+
from masterpiece import MqttMsg
|
7
|
+
|
8
|
+
|
9
|
+
class EnergyBalancer(Juham):
|
10
|
+
"""The energy balancer monitors the balance between produced and consumed energy
|
11
|
+
within the balancing interval to determine if there is enough energy available for
|
12
|
+
a given energy-consuming device, such as heating radiators, to operate within the
|
13
|
+
remaining time of the interval.
|
14
|
+
|
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
|
21
|
+
the total power consumption of the house. The energy balancer uses the power meter
|
22
|
+
"""
|
23
|
+
|
24
|
+
#: Description of the attribute
|
25
|
+
energy_balancing_interval: int = 3600
|
26
|
+
"""The time interval in seconds for energy balancing."""
|
27
|
+
|
28
|
+
def __init__(self, name: str = "energybalancer") -> None:
|
29
|
+
"""Initialize the energy balancer.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
name (str): name of the heating radiator
|
33
|
+
power (float): power of the consumer in watts
|
34
|
+
"""
|
35
|
+
super().__init__(name)
|
36
|
+
|
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")
|
40
|
+
self.net_energy_balance: float = 0.0 # Energy balance in joules (watt-seconds)
|
41
|
+
self.current_interval_ts: float = -1
|
42
|
+
self.needed_energy: float = 0.0 # Energy needed in joules (watt-seconds)
|
43
|
+
self.net_energy_balancing_mode: bool = False
|
44
|
+
self.consumers: dict[str, float] = {}
|
45
|
+
self.active_consumers: dict[str, dict[float, float]] = {}
|
46
|
+
|
47
|
+
@override
|
48
|
+
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
49
|
+
super().on_connect(client, userdata, flags, rc)
|
50
|
+
if rc == 0:
|
51
|
+
self.subscribe(self.topic_in_net_energy_balance)
|
52
|
+
self.subscribe(self.topic_in_consumers)
|
53
|
+
|
54
|
+
@override
|
55
|
+
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
56
|
+
ts: float = timestamp()
|
57
|
+
|
58
|
+
if msg.topic == self.topic_in_net_energy_balance:
|
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()))
|
62
|
+
else:
|
63
|
+
super().on_message(client, userdata, msg)
|
64
|
+
|
65
|
+
def on_power(self, m: dict[str, Any], ts: float) -> None:
|
66
|
+
"""Handle the power consumption. Read the current power balance and accumulate
|
67
|
+
to the net energy balance to reflect the energy produced (or consumed) within the
|
68
|
+
current time slot.
|
69
|
+
Args:
|
70
|
+
m (dict[str, Any]): power consumption message
|
71
|
+
ts (float): current time
|
72
|
+
"""
|
73
|
+
self.update_energy_balance(m["power"], ts)
|
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
|
+
|
84
|
+
def update_energy_balance(self, power: float, ts: float) -> None:
|
85
|
+
"""Update the current net net energy balance. The change in the balance is calculate the
|
86
|
+
energy balance, which the time elapsed since the last update, multiplied by the
|
87
|
+
power. Positive energy balance means we have produced energy that can be consumed
|
88
|
+
at the end of the interval. The target is to use all the energy produced during the
|
89
|
+
balancing interval. This method is called by the powermeter reading the
|
90
|
+
total power consumption of the house.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
power (float): power reading from the powermeter. Positive value means
|
94
|
+
energy produced, negative value means energy consumed. The value of 0 means
|
95
|
+
the house is not consuming or producing energy.
|
96
|
+
ts (float): current time in utc seconds
|
97
|
+
"""
|
98
|
+
|
99
|
+
# regardless of the mode, if we hit the end of the interval, reset the 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)
|
104
|
+
else:
|
105
|
+
# update the energy balance with the elapsed time and the power
|
106
|
+
elapsed_ts = interval_ts - self.current_interval_ts
|
107
|
+
balance: float = elapsed_ts * power # joules i.e. watt-seconds
|
108
|
+
self.net_energy_balance = self.net_energy_balance + balance
|
109
|
+
self.current_interval_ts = interval_ts
|
110
|
+
self.needed_energy = self.calculate_needed_energy(interval_ts)
|
111
|
+
if self.net_energy_balancing_mode:
|
112
|
+
if self.net_energy_balance <= 0:
|
113
|
+
# if we have used all the energy, disable the balancing mode
|
114
|
+
self.reset_net_energy_balance(0.0)
|
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
|
118
|
+
if self.net_energy_balance >= self.needed_energy:
|
119
|
+
self.net_energy_balancing_mode = True
|
120
|
+
self.initialize_active_consumers(ts)
|
121
|
+
self.publish_energybalance(ts)
|
122
|
+
|
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:
|
174
|
+
"""Check if there is enough energy available for the consumer to heat
|
175
|
+
the water in the remaining time within the balancing interval.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
unit (str): name of the consumer
|
179
|
+
ts (float): current time
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
bool: true if the given consumer is active
|
183
|
+
"""
|
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
|
200
|
+
|
201
|
+
def reset_net_energy_balance(self, interval_ts: float) -> None:
|
202
|
+
"""Reset the net energy balance at the end of the interval."""
|
203
|
+
self.net_energy_balance = 0.0
|
204
|
+
self.current_interval_ts = interval_ts
|
205
|
+
self.needed_energy = self.calculate_needed_energy(interval_ts)
|
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")
|
209
|
+
|
210
|
+
def activate_balancing_mode(self, ts: float) -> None:
|
211
|
+
"""Activate balancing mode when enough energy is available."""
|
212
|
+
self.net_energy_balancing_mode = True
|
213
|
+
self.info(
|
214
|
+
f"{int(self.net_energy_balance/3600)} Wh is enough to supply the radiator, enable"
|
215
|
+
)
|
216
|
+
|
217
|
+
def deactivate_balancing_mode(self) -> None:
|
218
|
+
"""Deactivate balancing mode when energy is depleted or interval ends."""
|
219
|
+
self.net_energy_balancing_mode = False
|
220
|
+
self.info("Balance used, or the end of the interval reached, disable")
|
221
|
+
self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
|
222
|
+
|
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))
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/heatingoptimizer.py
RENAMED
@@ -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"
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
5
5
|
|
6
6
|
[project]
|
7
7
|
name = "juham-automation"
|
8
|
-
version = "0.0.
|
8
|
+
version = "0.0.28"
|
9
9
|
description = "Juha's Ultimate Home Automation Masterpiece"
|
10
10
|
readme = {file = "README.rst", content-type = "text/markdown"}
|
11
11
|
requires-python = ">=3.8"
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
27
27
|
]
|
28
28
|
|
29
29
|
dependencies = [
|
30
|
-
"juham_core >= 0.1.
|
30
|
+
"juham_core >= 0.1.5",
|
31
31
|
]
|
32
32
|
|
33
33
|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import unittest
|
2
|
+
from typing import List, Any
|
3
|
+
from juham_automation.automation.energybalancer import EnergyBalancer
|
4
|
+
from juham_core.timeutils import (
|
5
|
+
quantize,
|
6
|
+
)
|
7
|
+
from masterpiece.mqtt import MqttMsg
|
8
|
+
|
9
|
+
|
10
|
+
class SimpleMqttMsg(MqttMsg):
|
11
|
+
def __init__(self, topic: str, payload: Any):
|
12
|
+
self._topic = topic
|
13
|
+
self._payload = payload
|
14
|
+
|
15
|
+
@property
|
16
|
+
def payload(self) -> Any:
|
17
|
+
return self._payload
|
18
|
+
|
19
|
+
@payload.setter
|
20
|
+
def payload(self, value: Any) -> None:
|
21
|
+
self._payload = value
|
22
|
+
|
23
|
+
@property
|
24
|
+
def topic(self) -> str:
|
25
|
+
return self._topic
|
26
|
+
|
27
|
+
@topic.setter
|
28
|
+
def topic(self, value: str) -> None:
|
29
|
+
self._topic = value
|
30
|
+
|
31
|
+
|
32
|
+
class TestEnergyBalancing(unittest.TestCase):
|
33
|
+
|
34
|
+
balancing_interval: int = EnergyBalancer.energy_balancing_interval
|
35
|
+
power: int = 3000 # Power of the radiator (in watts )
|
36
|
+
|
37
|
+
def setUp(self) -> None:
|
38
|
+
"""Create balancer with two consumers"""
|
39
|
+
self.optimizer = EnergyBalancer("test_optimizer")
|
40
|
+
self.optimizer.on_consumer({"Unit": "main", "Power": 2000})
|
41
|
+
self.optimizer.on_consumer({"Unit": "sun", "Power": 1000})
|
42
|
+
|
43
|
+
def test_initial_state(self) -> None:
|
44
|
+
self.assertEqual(
|
45
|
+
self.optimizer.net_energy_balance, 0, "Initial energy balance should be 0"
|
46
|
+
)
|
47
|
+
self.assertEqual(
|
48
|
+
self.balancing_interval, self.optimizer.energy_balancing_interval
|
49
|
+
)
|
50
|
+
self.assertFalse(self.optimizer.net_energy_balancing_mode)
|
51
|
+
|
52
|
+
self.assertEqual(self.optimizer.needed_energy, 0.0)
|
53
|
+
|
54
|
+
# time within the interval should be zero
|
55
|
+
self.assertEqual(-1, self.optimizer.current_interval_ts)
|
56
|
+
|
57
|
+
def test_set_power(self) -> None:
|
58
|
+
"""Test setting power consumption and check if the energy balance is updated."""
|
59
|
+
step: int = 60
|
60
|
+
for ts in range(0, self.balancing_interval, step):
|
61
|
+
self.optimizer.update_energy_balance(self.power, ts)
|
62
|
+
self.assertEqual(ts * self.power, self.optimizer.net_energy_balance)
|
63
|
+
self.assertEqual(ts, self.optimizer.current_interval_ts)
|
64
|
+
|
65
|
+
self.optimizer.update_energy_balance(self.power, ts + step)
|
66
|
+
self.assertEqual(0, self.optimizer.net_energy_balance)
|
67
|
+
|
68
|
+
def test_consider_net_energy_balance(self) -> None:
|
69
|
+
"""Test case to simulate passing time and check energy balancing behavior.
|
70
|
+
Pass power consumption self.power, which should switch the heating on just
|
71
|
+
in the middle of the interval."""
|
72
|
+
|
73
|
+
balancing_interval: int = self.balancing_interval
|
74
|
+
|
75
|
+
step: int = balancing_interval // 10
|
76
|
+
for ts in range(0, self.balancing_interval * 10, step):
|
77
|
+
interval_ts: float = ts % balancing_interval # quantized timestamp
|
78
|
+
energy: float = self.power * interval_ts # in watt-seconds
|
79
|
+
self.optimizer.update_energy_balance(self.power, ts)
|
80
|
+
|
81
|
+
# make sure the optimizer is in the right state
|
82
|
+
self.assertEqual(energy, self.optimizer.net_energy_balance)
|
83
|
+
|
84
|
+
# Call the method to check if balancing mode should be activated
|
85
|
+
main_on: bool = self.optimizer.consider_net_energy_balance("main", ts)
|
86
|
+
sun_on: bool = self.optimizer.consider_net_energy_balance("sun", ts)
|
87
|
+
|
88
|
+
if energy >= self.optimizer.needed_energy:
|
89
|
+
self.assertTrue(main_on or sun_on, "One of the consumers must be ON")
|
90
|
+
self.assertFalse(sun_on and main_on, "Only one at a time")
|
91
|
+
else:
|
92
|
+
self.assertFalse(main_on, "Not enough energy, main should be OFF")
|
93
|
+
self.assertFalse(sun_on, "Not enough energy, sun should be OFF")
|
94
|
+
|
95
|
+
def test_quantization(self) -> None:
|
96
|
+
"""Test that timestamps are quantized to the interval boundaries correctly."""
|
97
|
+
test_times: List[int] = [3601, 7200, 10800] # Slightly over boundaries
|
98
|
+
for ts in test_times:
|
99
|
+
quantized_ts = quantize(self.optimizer.energy_balancing_interval, ts)
|
100
|
+
expected_quantized_ts = (
|
101
|
+
ts // self.optimizer.energy_balancing_interval
|
102
|
+
) * self.optimizer.energy_balancing_interval
|
103
|
+
self.assertEqual(
|
104
|
+
quantized_ts,
|
105
|
+
expected_quantized_ts,
|
106
|
+
f"Timestamp {ts} should be quantized to {expected_quantized_ts}",
|
107
|
+
)
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/tests/automation/test_heatingoptimizer.py
RENAMED
@@ -107,7 +107,7 @@ class TestHeatingOptimizer(unittest.TestCase):
|
|
107
107
|
|
108
108
|
def test_consider_net_energy_balance(self) -> None:
|
109
109
|
"""Test case to simulate passing time and check energy balancing behavior."""
|
110
|
-
data: dict[str, Any] = {"Mode": False}
|
110
|
+
data: dict[str, Any] = {"Unit": "main", "Mode": False}
|
111
111
|
mock_msg = SimpleMqttMsg(
|
112
112
|
topic=self.optimizer.topic_in_net_energy_balance,
|
113
113
|
payload=json.dumps(data).encode("utf-8"),
|
@@ -122,11 +122,12 @@ class TestHeatingOptimizer(unittest.TestCase):
|
|
122
122
|
None,
|
123
123
|
SimpleMqttMsg(
|
124
124
|
topic=self.optimizer.topic_in_net_energy_balance,
|
125
|
-
payload=json.dumps({"Mode": True}).encode("utf-8"),
|
125
|
+
payload=json.dumps({"Unit": "sun", "Mode": True}).encode("utf-8"),
|
126
126
|
),
|
127
127
|
)
|
128
|
-
self.
|
129
|
-
self.optimizer.net_energy_balance_mode,
|
128
|
+
self.assertFalse(
|
129
|
+
self.optimizer.net_energy_balance_mode,
|
130
|
+
f"At time {0}, heating should be OFF",
|
130
131
|
)
|
131
132
|
|
132
133
|
|
@@ -1,158 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from typing import Any, Dict
|
3
|
-
from typing_extensions import override
|
4
|
-
|
5
|
-
from juham_core import Juham, timestamp
|
6
|
-
from juham_core.timeutils import timestampstr, quantize
|
7
|
-
from masterpiece import MqttMsg
|
8
|
-
|
9
|
-
|
10
|
-
class EnergyBalancer(Juham):
|
11
|
-
"""The energy balancer monitors the balance between produced and consumed energy
|
12
|
-
within the balancing interval to determine if there is enough energy available for
|
13
|
-
a given energy-consuming device, such as heating radiators, to operate within the
|
14
|
-
remaining time of the interval.
|
15
|
-
|
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
|
18
|
-
the total power consumption of the house. The energy balancer uses the power meter
|
19
|
-
"""
|
20
|
-
|
21
|
-
#: Description of the attribute
|
22
|
-
energy_balancing_interval: int = 3600
|
23
|
-
radiator_power: float = 3000
|
24
|
-
timezone: str = "Europe/Helsinki"
|
25
|
-
|
26
|
-
def __init__(self, name: str = "energybalancer") -> None:
|
27
|
-
"""Initialize the energy balancer.
|
28
|
-
|
29
|
-
Args:
|
30
|
-
name (str): name of the heating radiator
|
31
|
-
power (float): power of the consumer in watts
|
32
|
-
"""
|
33
|
-
super().__init__(name)
|
34
|
-
|
35
|
-
self.topic_in_powerconsumption = self.make_topic_name("powerconsumption")
|
36
|
-
self.topic_in_net_energy_balance = self.make_topic_name("net_energy_balance")
|
37
|
-
self.topic_out_energybalance = self.make_topic_name("energybalance")
|
38
|
-
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.net_energy_balancing_mode: bool = False
|
42
|
-
|
43
|
-
@override
|
44
|
-
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
45
|
-
super().on_connect(client, userdata, flags, rc)
|
46
|
-
if rc == 0:
|
47
|
-
self.subscribe(self.topic_in_net_energy_balance)
|
48
|
-
|
49
|
-
@override
|
50
|
-
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
51
|
-
ts: float = timestamp()
|
52
|
-
|
53
|
-
if msg.topic == self.topic_in_net_energy_balance:
|
54
|
-
self.on_power(json.loads(msg.payload.decode()), ts)
|
55
|
-
else:
|
56
|
-
super().on_message(client, userdata, msg)
|
57
|
-
|
58
|
-
def on_power(self, m: dict[str, Any], ts: float) -> None:
|
59
|
-
"""Handle the power consumption. Read the current power balance and accumulate
|
60
|
-
to the net energy balance to reflect the energy produced (or consumed) within the
|
61
|
-
current time slot.
|
62
|
-
Args:
|
63
|
-
m (dict[str, Any]): power consumption message
|
64
|
-
ts (float): current time
|
65
|
-
"""
|
66
|
-
self.update_energy_balance(m["power"], ts)
|
67
|
-
|
68
|
-
def update_energy_balance(self, power: float, ts: float) -> None:
|
69
|
-
"""Update the current net net energy balance. The change in the balance is calculate the
|
70
|
-
energy balance, which the time elapsed since the last update, multiplied by the
|
71
|
-
power. Positive energy balance means we have produced energy that can be consumed
|
72
|
-
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
|
75
|
-
|
76
|
-
Args:
|
77
|
-
power (float): power reading from the powermeter. Positive value means
|
78
|
-
energy produced, negative value means energy consumed. The value of 0 means
|
79
|
-
the house is not consuming or producing energy.
|
80
|
-
ts (float): current time in utc seconds
|
81
|
-
"""
|
82
|
-
|
83
|
-
# 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()
|
90
|
-
else:
|
91
|
-
# update the energy balance with the elapsed time and the power
|
92
|
-
elapsed_ts = quantized_ts - self.net_energy_balance_ts
|
93
|
-
balance: float = elapsed_ts * power # joules i.e. watt-seconds
|
94
|
-
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
|
-
|
100
|
-
if self.net_energy_balancing_mode:
|
101
|
-
if self.net_energy_balance <= 0:
|
102
|
-
# if we have used all the energy, disable the balancing mode
|
103
|
-
self.reset_net_energy_balance()
|
104
|
-
else:
|
105
|
-
if self.net_energy_balance >= self.needed_energy:
|
106
|
-
self.net_energy_balancing_mode = True
|
107
|
-
self.publish_energybalance(ts)
|
108
|
-
|
109
|
-
def consider_net_energy_balance(self, ts: float) -> bool:
|
110
|
-
"""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.
|
113
|
-
|
114
|
-
Args:
|
115
|
-
ts (float): current time
|
116
|
-
|
117
|
-
Returns:
|
118
|
-
bool: true if production exceeds the consumption
|
119
|
-
"""
|
120
|
-
return self.net_energy_balancing_mode
|
121
|
-
|
122
|
-
def reset_net_energy_balance(self) -> None:
|
123
|
-
"""Reset the net energy balance at the end of the interval."""
|
124
|
-
self.net_energy_balance = 0.0
|
125
|
-
self.needed_energy = self.energy_balancing_interval * self.radiator_power
|
126
|
-
self.net_energy_balance_ts = 0
|
127
|
-
self.net_energy_balancing_mode = False
|
128
|
-
|
129
|
-
def activate_balancing_mode(self, ts: float) -> None:
|
130
|
-
"""Activate balancing mode when enough energy is available."""
|
131
|
-
self.net_energy_balancing_mode = True
|
132
|
-
self.info(
|
133
|
-
f"{int(self.net_energy_balance/3600)} Wh is enough to supply the radiator, enable"
|
134
|
-
)
|
135
|
-
|
136
|
-
def deactivate_balancing_mode(self) -> None:
|
137
|
-
"""Deactivate balancing mode when energy is depleted or interval ends."""
|
138
|
-
self.net_energy_balancing_mode = False
|
139
|
-
self.info("Balance used, or the end of the interval reached, disable")
|
140
|
-
self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
|
141
|
-
|
142
|
-
def publish_energybalance(self, ts: float) -> None:
|
143
|
-
"""Publish energy balance information.
|
144
|
-
|
145
|
-
Args:
|
146
|
-
ts (float): current time
|
147
|
-
Returns:
|
148
|
-
dict: diagnostics information
|
149
|
-
"""
|
150
|
-
m: dict[str, Any] = {
|
151
|
-
"Unit": self.name,
|
152
|
-
"Mode": self.net_energy_balancing_mode,
|
153
|
-
"Rc": self.net_energy_balancing_mode,
|
154
|
-
"CurrentBalance": self.net_energy_balance,
|
155
|
-
"NeededBalance": self.needed_energy,
|
156
|
-
"Timestamp": ts,
|
157
|
-
}
|
158
|
-
self.publish(self.topic_out_energybalance, json.dumps(m))
|
@@ -1,183 +0,0 @@
|
|
1
|
-
import unittest
|
2
|
-
from typing import List, Any
|
3
|
-
from juham_automation.automation.energybalancer import EnergyBalancer
|
4
|
-
from juham_core.timeutils import (
|
5
|
-
quantize,
|
6
|
-
timestamp,
|
7
|
-
timestamp_hour,
|
8
|
-
timestampstr,
|
9
|
-
is_hour_within_schedule,
|
10
|
-
timestamp_hour_local,
|
11
|
-
)
|
12
|
-
from masterpiece.mqtt import MqttMsg
|
13
|
-
from juham_automation.automation.heatingoptimizer import HeatingOptimizer
|
14
|
-
|
15
|
-
|
16
|
-
class SimpleMqttMsg(MqttMsg):
|
17
|
-
def __init__(self, topic: str, payload: Any):
|
18
|
-
self._topic = topic
|
19
|
-
self._payload = payload
|
20
|
-
|
21
|
-
@property
|
22
|
-
def payload(self) -> Any:
|
23
|
-
return self._payload
|
24
|
-
|
25
|
-
@payload.setter
|
26
|
-
def payload(self, value: Any) -> None:
|
27
|
-
self._payload = value
|
28
|
-
|
29
|
-
@property
|
30
|
-
def topic(self) -> str:
|
31
|
-
return self._topic
|
32
|
-
|
33
|
-
@topic.setter
|
34
|
-
def topic(self, value: str) -> None:
|
35
|
-
self._topic = value
|
36
|
-
|
37
|
-
|
38
|
-
class TestEnergyBalancing(unittest.TestCase):
|
39
|
-
|
40
|
-
balancing_interval: int = EnergyBalancer.energy_balancing_interval
|
41
|
-
power: int = 3000 # Power of the radiator (in watts )
|
42
|
-
|
43
|
-
def setUp(self) -> None:
|
44
|
-
self.optimizer = EnergyBalancer("test_optimizer")
|
45
|
-
self.optimizer.radiator_power = 3000 # Set the radiator power
|
46
|
-
|
47
|
-
def test_initial_state(self) -> None:
|
48
|
-
self.assertEqual(
|
49
|
-
self.optimizer.net_energy_balance, 0, "Initial energy balance should be 0"
|
50
|
-
)
|
51
|
-
self.assertEqual(
|
52
|
-
self.balancing_interval, self.optimizer.energy_balancing_interval
|
53
|
-
)
|
54
|
-
self.assertFalse(self.optimizer.net_energy_balancing_mode)
|
55
|
-
|
56
|
-
self.assertEqual(
|
57
|
-
self.optimizer.needed_energy, self.balancing_interval * self.power
|
58
|
-
)
|
59
|
-
|
60
|
-
# time within the interval should be zero
|
61
|
-
self.assertEqual(-1, self.optimizer.net_energy_balance_ts)
|
62
|
-
|
63
|
-
def test_set_power(self) -> None:
|
64
|
-
"""Test setting power consumption and check if the energy balance is updated."""
|
65
|
-
step: int = 60
|
66
|
-
for ts in range(0, self.balancing_interval, step):
|
67
|
-
self.optimizer.update_energy_balance(self.power, ts)
|
68
|
-
self.assertEqual(ts * self.power, self.optimizer.net_energy_balance)
|
69
|
-
self.assertEqual(ts, self.optimizer.net_energy_balance_ts)
|
70
|
-
|
71
|
-
self.optimizer.update_energy_balance(self.power, ts + step)
|
72
|
-
self.assertEqual(0, self.optimizer.net_energy_balance)
|
73
|
-
|
74
|
-
def test_consider_net_energy_balance(self) -> None:
|
75
|
-
"""Test case to simulate passing time and check energy balancing behavior.
|
76
|
-
Pass power consumption self.power, which should switch the heating on just
|
77
|
-
in the middle of the interval."""
|
78
|
-
|
79
|
-
balancing_interval: int = self.balancing_interval
|
80
|
-
energy: float = 0
|
81
|
-
step: int = balancing_interval // 10
|
82
|
-
for ts in range(0, self.balancing_interval, step):
|
83
|
-
energy = self.power * ts # in watt-seconds
|
84
|
-
self.optimizer.update_energy_balance(self.power, ts)
|
85
|
-
|
86
|
-
# make sure the optimizer is in the right state
|
87
|
-
self.assertEqual(energy, self.optimizer.net_energy_balance)
|
88
|
-
|
89
|
-
# Call the method to check if balancing mode should be activated
|
90
|
-
heating_on: bool = self.optimizer.consider_net_energy_balance(ts)
|
91
|
-
|
92
|
-
# Calculate the remaining energy needed to power the radiator for the rest of the time slot
|
93
|
-
remaining_time: float = self.optimizer.energy_balancing_interval - ts
|
94
|
-
required_energy: float = self.optimizer.radiator_power * remaining_time
|
95
|
-
|
96
|
-
# Check if heating was enabled or not based on energy balance
|
97
|
-
if energy >= required_energy and remaining_time > 0:
|
98
|
-
self.assertTrue(heating_on, f"At time {ts}, heating should be ON")
|
99
|
-
else:
|
100
|
-
self.assertFalse(heating_on, f"At time {ts}, heating should be OFF")
|
101
|
-
|
102
|
-
# Ensure heating state is correct
|
103
|
-
if energy >= required_energy and remaining_time > 0:
|
104
|
-
self.assertTrue(self.optimizer.net_energy_balancing_mode)
|
105
|
-
else:
|
106
|
-
self.assertFalse(self.optimizer.net_energy_balancing_mode)
|
107
|
-
|
108
|
-
# Step 2: Testing energy balancing reset at the end of the interval
|
109
|
-
# self.optimizer.net_energy_balance = 0 # Reset energy before boundary test
|
110
|
-
self.optimizer.update_energy_balance(self.power, self.balancing_interval)
|
111
|
-
heating_on = self.optimizer.consider_net_energy_balance(ts)
|
112
|
-
self.assertFalse(heating_on, "Heating should be OFF after interval reset")
|
113
|
-
self.assertEqual(
|
114
|
-
self.optimizer.net_energy_balance,
|
115
|
-
0,
|
116
|
-
"Energy balance should be reset to 0 at the end of the interval",
|
117
|
-
)
|
118
|
-
self.assertFalse(
|
119
|
-
self.optimizer.net_energy_balancing_mode,
|
120
|
-
"Balancing mode should be OFF after interval reset",
|
121
|
-
)
|
122
|
-
|
123
|
-
# Step 3: Testing activation when energy is sufficient
|
124
|
-
self.optimizer.update_energy_balance(
|
125
|
-
self.power * 1800, self.balancing_interval // 10
|
126
|
-
)
|
127
|
-
|
128
|
-
ts = 3600 + 60 # Start of a new interval
|
129
|
-
heating_on = self.optimizer.consider_net_energy_balance(ts)
|
130
|
-
self.assertTrue(
|
131
|
-
heating_on, "Heating should be ON when enough energy is available"
|
132
|
-
)
|
133
|
-
self.assertTrue(
|
134
|
-
self.optimizer.net_energy_balancing_mode,
|
135
|
-
"Balancing mode should be ON when enough energy is available",
|
136
|
-
)
|
137
|
-
|
138
|
-
def test_consider_net_energy_balance_start_middle(self) -> None:
|
139
|
-
"""Start from the middle of the interval."""
|
140
|
-
|
141
|
-
balancing_interval: int = self.balancing_interval
|
142
|
-
energy: float = 0
|
143
|
-
step: int = balancing_interval // 10
|
144
|
-
for ts in range(self.balancing_interval // 2, self.balancing_interval, step):
|
145
|
-
|
146
|
-
self.optimizer.update_energy_balance(self.power, ts)
|
147
|
-
|
148
|
-
# make sure the optimizer is in the right state
|
149
|
-
self.assertEqual(energy, self.optimizer.net_energy_balance)
|
150
|
-
|
151
|
-
# Call the method to check if balancing mode should be activated
|
152
|
-
heating_on: bool = self.optimizer.consider_net_energy_balance(ts)
|
153
|
-
|
154
|
-
# Calculate the remaining energy needed to power the radiator for the rest of the time slot
|
155
|
-
remaining_time: float = self.optimizer.energy_balancing_interval - ts
|
156
|
-
required_energy: float = self.optimizer.radiator_power * remaining_time
|
157
|
-
|
158
|
-
# Check if heating was enabled or not based on energy balance
|
159
|
-
if energy >= required_energy and remaining_time > 0:
|
160
|
-
self.assertTrue(heating_on, f"At time {ts}, heating should be ON")
|
161
|
-
else:
|
162
|
-
self.assertFalse(heating_on, f"At time {ts}, heating should be OFF")
|
163
|
-
|
164
|
-
# Ensure heating state is correct
|
165
|
-
if energy >= required_energy and remaining_time > 0:
|
166
|
-
self.assertTrue(self.optimizer.net_energy_balancing_mode)
|
167
|
-
else:
|
168
|
-
self.assertFalse(self.optimizer.net_energy_balancing_mode)
|
169
|
-
energy = energy + self.power * step # in watt-seconds
|
170
|
-
|
171
|
-
def test_quantization(self) -> None:
|
172
|
-
"""Test that timestamps are quantized to the interval boundaries correctly."""
|
173
|
-
test_times: List[int] = [3601, 7200, 10800] # Slightly over boundaries
|
174
|
-
for ts in test_times:
|
175
|
-
quantized_ts = quantize(self.optimizer.energy_balancing_interval, ts)
|
176
|
-
expected_quantized_ts = (
|
177
|
-
ts // self.optimizer.energy_balancing_interval
|
178
|
-
) * self.optimizer.energy_balancing_interval
|
179
|
-
self.assertEqual(
|
180
|
-
quantized_ts,
|
181
|
-
expected_quantized_ts,
|
182
|
-
f"Timestamp {ts} should be quantized to {expected_quantized_ts}",
|
183
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/spothintafi.py
RENAMED
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/automation/watercirculator.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/electricityprice_ts.py
RENAMED
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/energybalancer_ts.py
RENAMED
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation/ts/energycostcalculator_ts.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation.egg-info/dependency_links.txt
RENAMED
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/juham_automation.egg-info/entry_points.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/tests/automation/test_energycostcalculator.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{juham_automation-0.0.26 → juham_automation-0.0.28}/tests/ts/test_energycostcalculator_ts.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|