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