juham-automation 0.0.12__py3-none-any.whl → 0.0.14__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/__init__.py +38 -38
- juham_automation/automation/__init__.py +21 -21
- juham_automation/automation/energycostcalculator.py +266 -266
- juham_automation/automation/hotwateroptimizer.py +567 -567
- juham_automation/automation/powermeter_simulator.py +139 -139
- juham_automation/automation/spothintafi.py +140 -140
- juham_automation/automation/watercirculator.py +159 -159
- juham_automation/japp.py +49 -49
- juham_automation/ts/__init__.py +25 -25
- juham_automation/ts/electricityprice_ts.py +51 -51
- juham_automation/ts/energycostcalculator_ts.py +43 -43
- juham_automation/ts/forecast_ts.py +97 -97
- juham_automation/ts/log_ts.py +57 -57
- juham_automation/ts/power_ts.py +49 -49
- juham_automation/ts/powermeter_ts.py +70 -70
- juham_automation/ts/powerplan_ts.py +45 -45
- {juham_automation-0.0.12.dist-info → juham_automation-0.0.14.dist-info}/LICENSE.rst +25 -25
- {juham_automation-0.0.12.dist-info → juham_automation-0.0.14.dist-info}/METADATA +105 -109
- juham_automation-0.0.14.dist-info/RECORD +23 -0
- {juham_automation-0.0.12.dist-info → juham_automation-0.0.14.dist-info}/WHEEL +1 -1
- juham_automation-0.0.12.dist-info/RECORD +0 -23
- {juham_automation-0.0.12.dist-info → juham_automation-0.0.14.dist-info}/entry_points.txt +0 -0
- {juham_automation-0.0.12.dist-info → juham_automation-0.0.14.dist-info}/top_level.txt +0 -0
juham_automation/__init__.py
CHANGED
@@ -1,38 +1,38 @@
|
|
1
|
-
"""
|
2
|
-
Description
|
3
|
-
===========
|
4
|
-
|
5
|
-
Juham - Juha's Ultimate Home Automation Masterpiece
|
6
|
-
|
7
|
-
"""
|
8
|
-
|
9
|
-
from .automation import EnergyCostCalculator
|
10
|
-
from .automation import PowerMeterSimulator
|
11
|
-
from .automation import SpotHintaFi
|
12
|
-
from .automation import WaterCirculator
|
13
|
-
from .automation import HotWaterOptimizer
|
14
|
-
from .ts import EnergyCostCalculatorTs
|
15
|
-
from .ts import ForecastTs
|
16
|
-
from .ts import LogTs
|
17
|
-
from .ts import PowerTs
|
18
|
-
from .ts import PowerPlanTs
|
19
|
-
from .ts import PowerMeterTs
|
20
|
-
from .ts import ElectricityPriceTs
|
21
|
-
from .japp import JApp
|
22
|
-
|
23
|
-
|
24
|
-
__all__ = [
|
25
|
-
"EnergyCostCalculator",
|
26
|
-
"EnergyCostCalculatorTs",
|
27
|
-
"ForecastTs",
|
28
|
-
"HotWaterOptimizer",
|
29
|
-
"LogTs",
|
30
|
-
"PowerTs",
|
31
|
-
"PowerPlanTs",
|
32
|
-
"PowerMeterTs",
|
33
|
-
"SpotHintaFi",
|
34
|
-
"WaterCirculator",
|
35
|
-
"JApp",
|
36
|
-
"PowerMeterSimulator",
|
37
|
-
"ElectricityPriceTs",
|
38
|
-
]
|
1
|
+
"""
|
2
|
+
Description
|
3
|
+
===========
|
4
|
+
|
5
|
+
Juham - Juha's Ultimate Home Automation Masterpiece
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from .automation import EnergyCostCalculator
|
10
|
+
from .automation import PowerMeterSimulator
|
11
|
+
from .automation import SpotHintaFi
|
12
|
+
from .automation import WaterCirculator
|
13
|
+
from .automation import HotWaterOptimizer
|
14
|
+
from .ts import EnergyCostCalculatorTs
|
15
|
+
from .ts import ForecastTs
|
16
|
+
from .ts import LogTs
|
17
|
+
from .ts import PowerTs
|
18
|
+
from .ts import PowerPlanTs
|
19
|
+
from .ts import PowerMeterTs
|
20
|
+
from .ts import ElectricityPriceTs
|
21
|
+
from .japp import JApp
|
22
|
+
|
23
|
+
|
24
|
+
__all__ = [
|
25
|
+
"EnergyCostCalculator",
|
26
|
+
"EnergyCostCalculatorTs",
|
27
|
+
"ForecastTs",
|
28
|
+
"HotWaterOptimizer",
|
29
|
+
"LogTs",
|
30
|
+
"PowerTs",
|
31
|
+
"PowerPlanTs",
|
32
|
+
"PowerMeterTs",
|
33
|
+
"SpotHintaFi",
|
34
|
+
"WaterCirculator",
|
35
|
+
"JApp",
|
36
|
+
"PowerMeterSimulator",
|
37
|
+
"ElectricityPriceTs",
|
38
|
+
]
|
@@ -1,21 +1,21 @@
|
|
1
|
-
"""
|
2
|
-
Description
|
3
|
-
===========
|
4
|
-
|
5
|
-
Juham - Juha's Ultimate Home Automation classes
|
6
|
-
|
7
|
-
"""
|
8
|
-
|
9
|
-
from .energycostcalculator import EnergyCostCalculator
|
10
|
-
from .spothintafi import SpotHintaFi
|
11
|
-
from .watercirculator import WaterCirculator
|
12
|
-
from .hotwateroptimizer import HotWaterOptimizer
|
13
|
-
from .powermeter_simulator import PowerMeterSimulator
|
14
|
-
|
15
|
-
__all__ = [
|
16
|
-
"EnergyCostCalculator",
|
17
|
-
"HotWaterOptimizer",
|
18
|
-
"SpotHintaFi",
|
19
|
-
"WaterCirculator",
|
20
|
-
"PowerMeterSimulator",
|
21
|
-
]
|
1
|
+
"""
|
2
|
+
Description
|
3
|
+
===========
|
4
|
+
|
5
|
+
Juham - Juha's Ultimate Home Automation classes
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from .energycostcalculator import EnergyCostCalculator
|
10
|
+
from .spothintafi import SpotHintaFi
|
11
|
+
from .watercirculator import WaterCirculator
|
12
|
+
from .hotwateroptimizer import HotWaterOptimizer
|
13
|
+
from .powermeter_simulator import PowerMeterSimulator
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"EnergyCostCalculator",
|
17
|
+
"HotWaterOptimizer",
|
18
|
+
"SpotHintaFi",
|
19
|
+
"WaterCirculator",
|
20
|
+
"PowerMeterSimulator",
|
21
|
+
]
|
@@ -1,266 +1,266 @@
|
|
1
|
-
from typing import Any
|
2
|
-
from typing_extensions import override
|
3
|
-
import json
|
4
|
-
from masterpiece.mqtt import MqttMsg
|
5
|
-
from juham_core import Juham
|
6
|
-
from juham_core.timeutils import (
|
7
|
-
elapsed_seconds_in_day,
|
8
|
-
elapsed_seconds_in_hour,
|
9
|
-
quantize,
|
10
|
-
timestamp,
|
11
|
-
)
|
12
|
-
|
13
|
-
|
14
|
-
class EnergyCostCalculator(Juham):
|
15
|
-
"""The EnergyCostCalculator class calculates the net energy balance between produced
|
16
|
-
and consumed energy for Time-Based Settlement (TBS). It performs the following functions:
|
17
|
-
|
18
|
-
* Subscribes to 'spot' and 'power' MQTT topics.
|
19
|
-
* Calculates the net energy and the rate of change of the net energy per hour and per day (24h)
|
20
|
-
* Publishes the calculated values to the MQTT net energy balance topic.
|
21
|
-
* Stores the data in a time series database.
|
22
|
-
|
23
|
-
This information helps other home automation components optimize energy usage and
|
24
|
-
minimize electricity bills.
|
25
|
-
"""
|
26
|
-
|
27
|
-
_kwh_to_joule_coeff: float = 1000.0 * 3600
|
28
|
-
_joule_to_kwh_coeff: float = 1.0 / _kwh_to_joule_coeff
|
29
|
-
|
30
|
-
energy_balancing_interval: float = 3600
|
31
|
-
|
32
|
-
def __init__(self, name: str = "ecc") -> None:
|
33
|
-
super().__init__(name)
|
34
|
-
self.current_ts: float = 0
|
35
|
-
self.total_balance_hour: float = 0
|
36
|
-
self.total_balance_day: float = 0
|
37
|
-
self.net_energy_balance_cost_hour: float = 0
|
38
|
-
self.net_energy_balance_cost_day: float = 0
|
39
|
-
self.net_energy_balance_start_hour = elapsed_seconds_in_hour(timestamp())
|
40
|
-
self.net_energy_balance_start_day = elapsed_seconds_in_day(timestamp())
|
41
|
-
self.spots: list[dict[str, float]] = []
|
42
|
-
self.init_topics()
|
43
|
-
|
44
|
-
def init_topics(self) -> None:
|
45
|
-
self.topic_in_spot = self.make_topic_name("spot")
|
46
|
-
self.topic_in_powerconsumption = self.make_topic_name("powerconsumption")
|
47
|
-
self.topic_out_net_energy_balance = self.make_topic_name("net_energy_balance")
|
48
|
-
self.topic_out_energy_cost = self.make_topic_name("net_energy_cost")
|
49
|
-
|
50
|
-
@override
|
51
|
-
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
52
|
-
super().on_connect(client, userdata, flags, rc)
|
53
|
-
if rc == 0:
|
54
|
-
self.subscribe(self.topic_in_spot)
|
55
|
-
self.subscribe(self.topic_in_powerconsumption)
|
56
|
-
|
57
|
-
@override
|
58
|
-
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
59
|
-
ts_now = timestamp()
|
60
|
-
|
61
|
-
m = json.loads(msg.payload.decode())
|
62
|
-
if msg.topic == self.topic_in_spot:
|
63
|
-
self.on_spot(m)
|
64
|
-
elif msg.topic == self.topic_in_powerconsumption:
|
65
|
-
self.on_powerconsumption(ts_now, m)
|
66
|
-
else:
|
67
|
-
self.error(f"Unknown event {msg.topic}")
|
68
|
-
|
69
|
-
def on_spot(self, spot: dict[Any, Any]) -> None:
|
70
|
-
"""Stores the received per hour electricity prices to spots list.
|
71
|
-
|
72
|
-
Args:
|
73
|
-
spot (list): list of hourly spot prices
|
74
|
-
"""
|
75
|
-
|
76
|
-
for s in spot:
|
77
|
-
self.spots.append(
|
78
|
-
{"Timestamp": s["Timestamp"], "PriceWithTax": s["PriceWithTax"]}
|
79
|
-
)
|
80
|
-
|
81
|
-
def map_kwh_prices_to_joules(self, price: float) -> float:
|
82
|
-
"""Convert the given electricity price in kWh to Watt seconds (J)
|
83
|
-
Args:
|
84
|
-
price (float): electricity price given as kWh
|
85
|
-
Returns:
|
86
|
-
Electricity price per watt second (J)
|
87
|
-
"""
|
88
|
-
return price * self._joule_to_kwh_coeff
|
89
|
-
|
90
|
-
def get_prices(self, ts_prev: float, ts_now: float) -> tuple[float, float]:
|
91
|
-
"""Fetch the electricity prices for the given two subsequent time
|
92
|
-
stamps.
|
93
|
-
|
94
|
-
Args:
|
95
|
-
ts_prev (float): previous time
|
96
|
-
ts_now (float): current time
|
97
|
-
Returns:
|
98
|
-
Electricity prices for the given interval
|
99
|
-
"""
|
100
|
-
prev_price = None
|
101
|
-
current_price = None
|
102
|
-
|
103
|
-
for i in range(0, len(self.spots) - 1):
|
104
|
-
r0 = self.spots[i]
|
105
|
-
r1 = self.spots[i + 1]
|
106
|
-
ts0 = r0["Timestamp"]
|
107
|
-
ts1 = r1["Timestamp"]
|
108
|
-
if ts_prev >= ts0 and ts_prev <= ts1:
|
109
|
-
prev_price = r0["PriceWithTax"]
|
110
|
-
if ts_now >= ts0 and ts_now <= ts1:
|
111
|
-
current_price = r0["PriceWithTax"]
|
112
|
-
if prev_price is not None and current_price is not None:
|
113
|
-
return prev_price, current_price
|
114
|
-
self.error("PANIC: run out of spot prices")
|
115
|
-
return 0.0, 0.0
|
116
|
-
|
117
|
-
def calculate_net_energy_cost(
|
118
|
-
self, ts_prev: float, ts_now: float, energy: float
|
119
|
-
) -> float:
|
120
|
-
"""Given time interval as start and stop Calculate the cost over the
|
121
|
-
given time period. Positive values indicate revenue, negative cost.
|
122
|
-
|
123
|
-
Args:
|
124
|
-
ts_prev (timestamp): beginning time stamp of the interval
|
125
|
-
ts_now (timestamp): end of the interval
|
126
|
-
energy (float): energy consumed during the time interval
|
127
|
-
Returns:
|
128
|
-
Cost or revenue
|
129
|
-
"""
|
130
|
-
cost: float = 0
|
131
|
-
prev = ts_prev
|
132
|
-
while prev < ts_now:
|
133
|
-
elapsed_seconds: float = ts_now - prev
|
134
|
-
if elapsed_seconds > self.energy_balancing_interval:
|
135
|
-
elapsed_seconds = self.energy_balancing_interval
|
136
|
-
now = prev + elapsed_seconds
|
137
|
-
start_per_kwh, stop_per_kwh = self.get_prices(prev, now)
|
138
|
-
start_price = self.map_kwh_prices_to_joules(start_per_kwh)
|
139
|
-
stop_price = self.map_kwh_prices_to_joules(stop_per_kwh)
|
140
|
-
if abs(stop_price - start_price) < 1e-24:
|
141
|
-
cost = cost + energy * elapsed_seconds * start_price
|
142
|
-
else:
|
143
|
-
# interpolate cost over energy balancing interval boundary
|
144
|
-
elapsed = now - prev
|
145
|
-
if elapsed < 0.00001:
|
146
|
-
return 0.0
|
147
|
-
ts_0 = quantize(self.energy_balancing_interval, now)
|
148
|
-
t1 = (ts_0 - prev) / elapsed
|
149
|
-
t2 = (now - ts_0) / elapsed
|
150
|
-
cost = (
|
151
|
-
cost
|
152
|
-
+ energy
|
153
|
-
* ((1.0 - t1) * start_price + t2 * stop_price)
|
154
|
-
* elapsed_seconds
|
155
|
-
)
|
156
|
-
|
157
|
-
prev = prev + elapsed_seconds
|
158
|
-
return cost
|
159
|
-
|
160
|
-
def on_powerconsumption(self, ts_now: float, m: dict[Any, Any]) -> None:
|
161
|
-
"""Calculate net energy cost and update the hourly consumption attribute
|
162
|
-
accordingly.
|
163
|
-
|
164
|
-
Args:
|
165
|
-
ts_now (float): time stamp of the energy consumed
|
166
|
-
m (dict): Juham MQTT message holding energy reading
|
167
|
-
"""
|
168
|
-
power = m["real_total"]
|
169
|
-
if not self.spots:
|
170
|
-
self.info("Waiting for electricity prices...")
|
171
|
-
elif self.current_ts == 0:
|
172
|
-
self.net_energy_balance_cost_hour = 0.0
|
173
|
-
self.net_energy_balance_cost_day = 0.0
|
174
|
-
self.current_ts = ts_now
|
175
|
-
self.net_energy_balance_start_hour = quantize(
|
176
|
-
self.energy_balancing_interval, ts_now
|
177
|
-
)
|
178
|
-
else:
|
179
|
-
# calculate cost of energy consumed/produced
|
180
|
-
dp: float = self.calculate_net_energy_cost(self.current_ts, ts_now, power)
|
181
|
-
self.net_energy_balance_cost_hour = self.net_energy_balance_cost_hour + dp
|
182
|
-
self.net_energy_balance_cost_day = self.net_energy_balance_cost_day + dp
|
183
|
-
|
184
|
-
# calculate and publish energy balance
|
185
|
-
dt = ts_now - self.current_ts # time elapsed since previous call
|
186
|
-
balance = dt * power # energy consumed/produced in this slot in Joules
|
187
|
-
self.total_balance_hour = (
|
188
|
-
self.total_balance_hour + balance * self._joule_to_kwh_coeff
|
189
|
-
)
|
190
|
-
self.total_balance_day = (
|
191
|
-
self.total_balance_day + balance * self._joule_to_kwh_coeff
|
192
|
-
)
|
193
|
-
self.publish_net_energy_balance(ts_now, self.name, balance, power)
|
194
|
-
self.publish_energy_cost(
|
195
|
-
ts_now,
|
196
|
-
self.name,
|
197
|
-
self.net_energy_balance_cost_hour,
|
198
|
-
self.net_energy_balance_cost_day,
|
199
|
-
)
|
200
|
-
|
201
|
-
# Check if the current energy balancing interval has ended
|
202
|
-
# If so, reset the net_energy_balance attribute for the next interval
|
203
|
-
if (
|
204
|
-
ts_now - self.net_energy_balance_start_hour
|
205
|
-
> self.energy_balancing_interval
|
206
|
-
):
|
207
|
-
# publish average energy cost per hour
|
208
|
-
if abs(self.total_balance_hour) > 0:
|
209
|
-
msg = {
|
210
|
-
"name": self.name,
|
211
|
-
"average_hour": self.net_energy_balance_cost_hour
|
212
|
-
/ self.total_balance_hour,
|
213
|
-
"ts": ts_now,
|
214
|
-
}
|
215
|
-
self.publish(self.topic_out_energy_cost, json.dumps(msg), 0, False)
|
216
|
-
|
217
|
-
# reset for the next hour
|
218
|
-
self.total_balance_hour = 0
|
219
|
-
self.net_energy_balance_cost_hour = 0.0
|
220
|
-
self.net_energy_balance_start_hour = ts_now
|
221
|
-
|
222
|
-
if ts_now - self.net_energy_balance_start_day > 24 * 3600:
|
223
|
-
if abs(self.total_balance_day) > 0:
|
224
|
-
msg = {
|
225
|
-
"name": self.name,
|
226
|
-
"average_day": self.net_energy_balance_cost_day
|
227
|
-
/ self.total_balance_day,
|
228
|
-
"ts": ts_now,
|
229
|
-
}
|
230
|
-
self.publish(self.topic_out_energy_cost, json.dumps(msg), 0, False)
|
231
|
-
# reset for the next day
|
232
|
-
self.total_balance_day = 0
|
233
|
-
self.net_energy_balance_cost_day = 0.0
|
234
|
-
self.net_energy_balance_start_day = ts_now
|
235
|
-
|
236
|
-
self.current_ts = ts_now
|
237
|
-
|
238
|
-
def publish_net_energy_balance(
|
239
|
-
self, ts_now: float, site: str, energy: float, power: float
|
240
|
-
) -> None:
|
241
|
-
"""Publish the net energy balance for the current energy balancing interval, as well as
|
242
|
-
the real-time power at which energy is currently being produced or consumed (the
|
243
|
-
rate of change of net energy).
|
244
|
-
|
245
|
-
Args:
|
246
|
-
ts_now (float): timestamp
|
247
|
-
site (str): site
|
248
|
-
energy (float): cost or revenue.
|
249
|
-
power (float) : momentary power (rage of change of energy)
|
250
|
-
"""
|
251
|
-
msg = {"site": site, "power": power, "energy": energy, "ts": ts_now}
|
252
|
-
self.publish(self.topic_out_net_energy_balance, json.dumps(msg), 1, True)
|
253
|
-
|
254
|
-
def publish_energy_cost(
|
255
|
-
self, ts_now: float, site: str, cost_hour: float, cost_day: float
|
256
|
-
) -> None:
|
257
|
-
"""Publish daily and hourly energy cost/revenue
|
258
|
-
|
259
|
-
Args:
|
260
|
-
ts_now (float): timestamp
|
261
|
-
site (str): site
|
262
|
-
cost_hour (float): cost or revenue per hour.
|
263
|
-
cost_day (float) : cost or revenue per day
|
264
|
-
"""
|
265
|
-
msg = {"name": site, "cost_hour": cost_hour, "cost_day": cost_day, "ts": ts_now}
|
266
|
-
self.publish(self.topic_out_energy_cost, json.dumps(msg), 1, True)
|
1
|
+
from typing import Any
|
2
|
+
from typing_extensions import override
|
3
|
+
import json
|
4
|
+
from masterpiece.mqtt import MqttMsg
|
5
|
+
from juham_core import Juham
|
6
|
+
from juham_core.timeutils import (
|
7
|
+
elapsed_seconds_in_day,
|
8
|
+
elapsed_seconds_in_hour,
|
9
|
+
quantize,
|
10
|
+
timestamp,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class EnergyCostCalculator(Juham):
|
15
|
+
"""The EnergyCostCalculator class calculates the net energy balance between produced
|
16
|
+
and consumed energy for Time-Based Settlement (TBS). It performs the following functions:
|
17
|
+
|
18
|
+
* Subscribes to 'spot' and 'power' MQTT topics.
|
19
|
+
* Calculates the net energy and the rate of change of the net energy per hour and per day (24h)
|
20
|
+
* Publishes the calculated values to the MQTT net energy balance topic.
|
21
|
+
* Stores the data in a time series database.
|
22
|
+
|
23
|
+
This information helps other home automation components optimize energy usage and
|
24
|
+
minimize electricity bills.
|
25
|
+
"""
|
26
|
+
|
27
|
+
_kwh_to_joule_coeff: float = 1000.0 * 3600
|
28
|
+
_joule_to_kwh_coeff: float = 1.0 / _kwh_to_joule_coeff
|
29
|
+
|
30
|
+
energy_balancing_interval: float = 3600
|
31
|
+
|
32
|
+
def __init__(self, name: str = "ecc") -> None:
|
33
|
+
super().__init__(name)
|
34
|
+
self.current_ts: float = 0
|
35
|
+
self.total_balance_hour: float = 0
|
36
|
+
self.total_balance_day: float = 0
|
37
|
+
self.net_energy_balance_cost_hour: float = 0
|
38
|
+
self.net_energy_balance_cost_day: float = 0
|
39
|
+
self.net_energy_balance_start_hour = elapsed_seconds_in_hour(timestamp())
|
40
|
+
self.net_energy_balance_start_day = elapsed_seconds_in_day(timestamp())
|
41
|
+
self.spots: list[dict[str, float]] = []
|
42
|
+
self.init_topics()
|
43
|
+
|
44
|
+
def init_topics(self) -> None:
|
45
|
+
self.topic_in_spot = self.make_topic_name("spot")
|
46
|
+
self.topic_in_powerconsumption = self.make_topic_name("powerconsumption")
|
47
|
+
self.topic_out_net_energy_balance = self.make_topic_name("net_energy_balance")
|
48
|
+
self.topic_out_energy_cost = self.make_topic_name("net_energy_cost")
|
49
|
+
|
50
|
+
@override
|
51
|
+
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
52
|
+
super().on_connect(client, userdata, flags, rc)
|
53
|
+
if rc == 0:
|
54
|
+
self.subscribe(self.topic_in_spot)
|
55
|
+
self.subscribe(self.topic_in_powerconsumption)
|
56
|
+
|
57
|
+
@override
|
58
|
+
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
59
|
+
ts_now = timestamp()
|
60
|
+
|
61
|
+
m = json.loads(msg.payload.decode())
|
62
|
+
if msg.topic == self.topic_in_spot:
|
63
|
+
self.on_spot(m)
|
64
|
+
elif msg.topic == self.topic_in_powerconsumption:
|
65
|
+
self.on_powerconsumption(ts_now, m)
|
66
|
+
else:
|
67
|
+
self.error(f"Unknown event {msg.topic}")
|
68
|
+
|
69
|
+
def on_spot(self, spot: dict[Any, Any]) -> None:
|
70
|
+
"""Stores the received per hour electricity prices to spots list.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
spot (list): list of hourly spot prices
|
74
|
+
"""
|
75
|
+
|
76
|
+
for s in spot:
|
77
|
+
self.spots.append(
|
78
|
+
{"Timestamp": s["Timestamp"], "PriceWithTax": s["PriceWithTax"]}
|
79
|
+
)
|
80
|
+
|
81
|
+
def map_kwh_prices_to_joules(self, price: float) -> float:
|
82
|
+
"""Convert the given electricity price in kWh to Watt seconds (J)
|
83
|
+
Args:
|
84
|
+
price (float): electricity price given as kWh
|
85
|
+
Returns:
|
86
|
+
Electricity price per watt second (J)
|
87
|
+
"""
|
88
|
+
return price * self._joule_to_kwh_coeff
|
89
|
+
|
90
|
+
def get_prices(self, ts_prev: float, ts_now: float) -> tuple[float, float]:
|
91
|
+
"""Fetch the electricity prices for the given two subsequent time
|
92
|
+
stamps.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
ts_prev (float): previous time
|
96
|
+
ts_now (float): current time
|
97
|
+
Returns:
|
98
|
+
Electricity prices for the given interval
|
99
|
+
"""
|
100
|
+
prev_price = None
|
101
|
+
current_price = None
|
102
|
+
|
103
|
+
for i in range(0, len(self.spots) - 1):
|
104
|
+
r0 = self.spots[i]
|
105
|
+
r1 = self.spots[i + 1]
|
106
|
+
ts0 = r0["Timestamp"]
|
107
|
+
ts1 = r1["Timestamp"]
|
108
|
+
if ts_prev >= ts0 and ts_prev <= ts1:
|
109
|
+
prev_price = r0["PriceWithTax"]
|
110
|
+
if ts_now >= ts0 and ts_now <= ts1:
|
111
|
+
current_price = r0["PriceWithTax"]
|
112
|
+
if prev_price is not None and current_price is not None:
|
113
|
+
return prev_price, current_price
|
114
|
+
self.error("PANIC: run out of spot prices")
|
115
|
+
return 0.0, 0.0
|
116
|
+
|
117
|
+
def calculate_net_energy_cost(
|
118
|
+
self, ts_prev: float, ts_now: float, energy: float
|
119
|
+
) -> float:
|
120
|
+
"""Given time interval as start and stop Calculate the cost over the
|
121
|
+
given time period. Positive values indicate revenue, negative cost.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
ts_prev (timestamp): beginning time stamp of the interval
|
125
|
+
ts_now (timestamp): end of the interval
|
126
|
+
energy (float): energy consumed during the time interval
|
127
|
+
Returns:
|
128
|
+
Cost or revenue
|
129
|
+
"""
|
130
|
+
cost: float = 0
|
131
|
+
prev = ts_prev
|
132
|
+
while prev < ts_now:
|
133
|
+
elapsed_seconds: float = ts_now - prev
|
134
|
+
if elapsed_seconds > self.energy_balancing_interval:
|
135
|
+
elapsed_seconds = self.energy_balancing_interval
|
136
|
+
now = prev + elapsed_seconds
|
137
|
+
start_per_kwh, stop_per_kwh = self.get_prices(prev, now)
|
138
|
+
start_price = self.map_kwh_prices_to_joules(start_per_kwh)
|
139
|
+
stop_price = self.map_kwh_prices_to_joules(stop_per_kwh)
|
140
|
+
if abs(stop_price - start_price) < 1e-24:
|
141
|
+
cost = cost + energy * elapsed_seconds * start_price
|
142
|
+
else:
|
143
|
+
# interpolate cost over energy balancing interval boundary
|
144
|
+
elapsed = now - prev
|
145
|
+
if elapsed < 0.00001:
|
146
|
+
return 0.0
|
147
|
+
ts_0 = quantize(self.energy_balancing_interval, now)
|
148
|
+
t1 = (ts_0 - prev) / elapsed
|
149
|
+
t2 = (now - ts_0) / elapsed
|
150
|
+
cost = (
|
151
|
+
cost
|
152
|
+
+ energy
|
153
|
+
* ((1.0 - t1) * start_price + t2 * stop_price)
|
154
|
+
* elapsed_seconds
|
155
|
+
)
|
156
|
+
|
157
|
+
prev = prev + elapsed_seconds
|
158
|
+
return cost
|
159
|
+
|
160
|
+
def on_powerconsumption(self, ts_now: float, m: dict[Any, Any]) -> None:
|
161
|
+
"""Calculate net energy cost and update the hourly consumption attribute
|
162
|
+
accordingly.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
ts_now (float): time stamp of the energy consumed
|
166
|
+
m (dict): Juham MQTT message holding energy reading
|
167
|
+
"""
|
168
|
+
power = m["real_total"]
|
169
|
+
if not self.spots:
|
170
|
+
self.info("Waiting for electricity prices...")
|
171
|
+
elif self.current_ts == 0:
|
172
|
+
self.net_energy_balance_cost_hour = 0.0
|
173
|
+
self.net_energy_balance_cost_day = 0.0
|
174
|
+
self.current_ts = ts_now
|
175
|
+
self.net_energy_balance_start_hour = quantize(
|
176
|
+
self.energy_balancing_interval, ts_now
|
177
|
+
)
|
178
|
+
else:
|
179
|
+
# calculate cost of energy consumed/produced
|
180
|
+
dp: float = self.calculate_net_energy_cost(self.current_ts, ts_now, power)
|
181
|
+
self.net_energy_balance_cost_hour = self.net_energy_balance_cost_hour + dp
|
182
|
+
self.net_energy_balance_cost_day = self.net_energy_balance_cost_day + dp
|
183
|
+
|
184
|
+
# calculate and publish energy balance
|
185
|
+
dt = ts_now - self.current_ts # time elapsed since previous call
|
186
|
+
balance = dt * power # energy consumed/produced in this slot in Joules
|
187
|
+
self.total_balance_hour = (
|
188
|
+
self.total_balance_hour + balance * self._joule_to_kwh_coeff
|
189
|
+
)
|
190
|
+
self.total_balance_day = (
|
191
|
+
self.total_balance_day + balance * self._joule_to_kwh_coeff
|
192
|
+
)
|
193
|
+
self.publish_net_energy_balance(ts_now, self.name, balance, power)
|
194
|
+
self.publish_energy_cost(
|
195
|
+
ts_now,
|
196
|
+
self.name,
|
197
|
+
self.net_energy_balance_cost_hour,
|
198
|
+
self.net_energy_balance_cost_day,
|
199
|
+
)
|
200
|
+
|
201
|
+
# Check if the current energy balancing interval has ended
|
202
|
+
# If so, reset the net_energy_balance attribute for the next interval
|
203
|
+
if (
|
204
|
+
ts_now - self.net_energy_balance_start_hour
|
205
|
+
> self.energy_balancing_interval
|
206
|
+
):
|
207
|
+
# publish average energy cost per hour
|
208
|
+
if abs(self.total_balance_hour) > 0:
|
209
|
+
msg = {
|
210
|
+
"name": self.name,
|
211
|
+
"average_hour": self.net_energy_balance_cost_hour
|
212
|
+
/ self.total_balance_hour,
|
213
|
+
"ts": ts_now,
|
214
|
+
}
|
215
|
+
self.publish(self.topic_out_energy_cost, json.dumps(msg), 0, False)
|
216
|
+
|
217
|
+
# reset for the next hour
|
218
|
+
self.total_balance_hour = 0
|
219
|
+
self.net_energy_balance_cost_hour = 0.0
|
220
|
+
self.net_energy_balance_start_hour = ts_now
|
221
|
+
|
222
|
+
if ts_now - self.net_energy_balance_start_day > 24 * 3600:
|
223
|
+
if abs(self.total_balance_day) > 0:
|
224
|
+
msg = {
|
225
|
+
"name": self.name,
|
226
|
+
"average_day": self.net_energy_balance_cost_day
|
227
|
+
/ self.total_balance_day,
|
228
|
+
"ts": ts_now,
|
229
|
+
}
|
230
|
+
self.publish(self.topic_out_energy_cost, json.dumps(msg), 0, False)
|
231
|
+
# reset for the next day
|
232
|
+
self.total_balance_day = 0
|
233
|
+
self.net_energy_balance_cost_day = 0.0
|
234
|
+
self.net_energy_balance_start_day = ts_now
|
235
|
+
|
236
|
+
self.current_ts = ts_now
|
237
|
+
|
238
|
+
def publish_net_energy_balance(
|
239
|
+
self, ts_now: float, site: str, energy: float, power: float
|
240
|
+
) -> None:
|
241
|
+
"""Publish the net energy balance for the current energy balancing interval, as well as
|
242
|
+
the real-time power at which energy is currently being produced or consumed (the
|
243
|
+
rate of change of net energy).
|
244
|
+
|
245
|
+
Args:
|
246
|
+
ts_now (float): timestamp
|
247
|
+
site (str): site
|
248
|
+
energy (float): cost or revenue.
|
249
|
+
power (float) : momentary power (rage of change of energy)
|
250
|
+
"""
|
251
|
+
msg = {"site": site, "power": power, "energy": energy, "ts": ts_now}
|
252
|
+
self.publish(self.topic_out_net_energy_balance, json.dumps(msg), 1, True)
|
253
|
+
|
254
|
+
def publish_energy_cost(
|
255
|
+
self, ts_now: float, site: str, cost_hour: float, cost_day: float
|
256
|
+
) -> None:
|
257
|
+
"""Publish daily and hourly energy cost/revenue
|
258
|
+
|
259
|
+
Args:
|
260
|
+
ts_now (float): timestamp
|
261
|
+
site (str): site
|
262
|
+
cost_hour (float): cost or revenue per hour.
|
263
|
+
cost_day (float) : cost or revenue per day
|
264
|
+
"""
|
265
|
+
msg = {"name": site, "cost_hour": cost_hour, "cost_day": cost_day, "ts": ts_now}
|
266
|
+
self.publish(self.topic_out_energy_cost, json.dumps(msg), 1, True)
|