juham-automation 0.1.1__py3-none-any.whl → 0.1.3__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.
@@ -6,6 +6,7 @@ from juham_core import Juham
6
6
  from juham_core.timeutils import (
7
7
  elapsed_seconds_in_day,
8
8
  elapsed_seconds_in_hour,
9
+ elapsed_seconds_in_interval,
9
10
  quantize,
10
11
  timestamp,
11
12
  )
@@ -28,17 +29,20 @@ class EnergyCostCalculator(Juham):
28
29
  _kwh_to_joule_coeff: float = 1000.0 * 3600
29
30
  _joule_to_kwh_coeff: float = 1.0 / _kwh_to_joule_coeff
30
31
 
31
- energy_balancing_interval: float = 3600
32
+ energy_balancing_interval: int = 900 # in seconds (15 minutes)
32
33
 
33
34
  def __init__(self, name: str = "ecc") -> None:
34
35
  super().__init__(name)
35
36
  self.current_ts: float = 0
37
+ self.total_balance_interval : float = 0
36
38
  self.total_balance_hour: float = 0
37
39
  self.total_balance_day: float = 0
40
+ self.net_energy_balance_cost_interval: float = 0
38
41
  self.net_energy_balance_cost_hour: float = 0
39
42
  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())
43
+ self.net_energy_balance_start_interval : float = elapsed_seconds_in_interval(timestamp(), self.energy_balancing_interval)
44
+ self.net_energy_balance_start_hour : float = elapsed_seconds_in_hour(timestamp())
45
+ self.net_energy_balance_start_day : float = elapsed_seconds_in_day(timestamp())
42
46
  self.spots: list[dict[str, float]] = []
43
47
  self.init_topics()
44
48
 
@@ -88,76 +92,68 @@ class EnergyCostCalculator(Juham):
88
92
  """
89
93
  return price * self._joule_to_kwh_coeff
90
94
 
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
 
96
+
97
+ def get_price_at(self, ts: float) -> float:
98
+ """Return the spot price applicable at the given timestamp.
95
99
  Args:
96
- ts_prev (float): previous time
97
- ts_now (float): current time
100
+ ts (float): current time
98
101
  Returns:
99
102
  Electricity prices for the given interval
100
103
  """
101
- prev_price = None
102
- current_price = None
103
104
 
104
105
  for i in range(0, len(self.spots) - 1):
105
106
  r0 = self.spots[i]
106
107
  r1 = self.spots[i + 1]
107
108
  ts0 = r0["Timestamp"]
108
109
  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
110
+ if ts >= ts0 and ts < ts1:
111
+ return r0["PriceWithTax"]
112
+
113
+ # If timestamp is exactly equal to the last spot timestamp or beyond
114
+ if ts >= self.spots[-1]["Timestamp"]:
115
+ return self.spots[-1]["PriceWithTax"]
116
+
117
+ self.error(f"PANIC: Timestamp {ts} out of bounds for spot price lookup")
118
+ return 0.0
119
+
120
+
117
121
 
118
122
  def calculate_net_energy_cost(
119
123
  self, ts_prev: float, ts_now: float, energy: float
120
124
  ) -> float:
121
- """Given time interval as start and stop Calculate the cost over the
122
- given time period. Positive values indicate revenue, negative cost.
125
+ """
126
+ Calculate the cost (or revenue) of energy consumed/produced over the given time interval.
127
+ Positive values indicate revenue, negative values indicate cost.
123
128
 
124
129
  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
130
+ ts_prev (float): Start timestamp of the interval
131
+ ts_now (float): End timestamp of the interval
132
+ energy (float): Energy consumed during the interval (in watts or Joules)
133
+
128
134
  Returns:
129
- Cost or revenue
135
+ float: Total cost/revenue for the interval
130
136
  """
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
137
+ cost = 0.0
138
+ current = ts_prev
139
+ interval = self.energy_balancing_interval
140
+
141
+ while current < ts_now:
142
+ next_ts = min(ts_now, current + interval)
143
+ # Get spot price at start and end of interval
144
+ price_start = self.map_kwh_prices_to_joules(self.get_price_at(current))
145
+ price_end = self.map_kwh_prices_to_joules(self.get_price_at(next_ts))
146
+
147
+ # Trapezoidal integration: average price over interval
148
+ avg_price = (price_start + price_end) / 2.0
149
+ dt = next_ts - current
150
+ cost += energy * avg_price * dt
151
+
152
+ current = next_ts
153
+
159
154
  return cost
160
155
 
156
+
161
157
  def on_powerconsumption(self, ts_now: float, m: dict[Any, Any]) -> None:
162
158
  """Calculate net energy cost and update the hourly consumption attribute
163
159
  accordingly.
@@ -170,21 +166,29 @@ class EnergyCostCalculator(Juham):
170
166
  if not self.spots:
171
167
  self.info("Waiting for electricity prices...")
172
168
  elif self.current_ts == 0:
169
+ self.net_energy_balance_cost_interval = 0.0
173
170
  self.net_energy_balance_cost_hour = 0.0
174
171
  self.net_energy_balance_cost_day = 0.0
175
172
  self.current_ts = ts_now
176
173
  self.net_energy_balance_start_hour = quantize(
174
+ 3600, ts_now
175
+ )
176
+ self.net_energy_balance_start_interval = quantize(
177
177
  self.energy_balancing_interval, ts_now
178
178
  )
179
179
  else:
180
180
  # calculate cost of energy consumed/produced
181
181
  dp: float = self.calculate_net_energy_cost(self.current_ts, ts_now, power)
182
+ self.net_energy_balance_cost_interval = self.net_energy_balance_cost_interval + dp
182
183
  self.net_energy_balance_cost_hour = self.net_energy_balance_cost_hour + dp
183
184
  self.net_energy_balance_cost_day = self.net_energy_balance_cost_day + dp
184
185
 
185
186
  # calculate and publish energy balance
186
187
  dt = ts_now - self.current_ts # time elapsed since previous call
187
188
  balance = dt * power # energy consumed/produced in this slot in Joules
189
+ self.total_balance_interval = (
190
+ self.total_balance_interval + balance * self._joule_to_kwh_coeff
191
+ )
188
192
  self.total_balance_hour = (
189
193
  self.total_balance_hour + balance * self._joule_to_kwh_coeff
190
194
  )
@@ -195,16 +199,32 @@ class EnergyCostCalculator(Juham):
195
199
  self.publish_energy_cost(
196
200
  ts_now,
197
201
  self.name,
202
+ self.net_energy_balance_cost_interval,
198
203
  self.net_energy_balance_cost_hour,
199
204
  self.net_energy_balance_cost_day,
200
205
  )
201
206
 
202
207
  # Check if the current energy balancing interval has ended
203
208
  # 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
- ):
209
+ if ts_now - self.net_energy_balance_start_hour > self.energy_balancing_interval:
210
+ # publish average energy cost per hour
211
+ if abs(self.total_balance_interval) > 0:
212
+ msg = {
213
+ "name": self.name,
214
+ "average_interval": self.net_energy_balance_cost_interval
215
+ / self.total_balance_interval,
216
+ "ts": ts_now,
217
+ }
218
+ self.publish(self.topic_out_energy_cost, json.dumps(msg), 0, False)
219
+
220
+ # reset for the next hour
221
+ self.total_balance_interval = 0
222
+ self.net_energy_balance_cost_interval = 0.0
223
+ self.net_energy_balance_start_interval = ts_now
224
+
225
+ # Check if the current energy balancing interval has ended
226
+ # If so, reset the net_energy_balance attribute for the next interval
227
+ if ts_now - self.net_energy_balance_start_hour > 3600:
208
228
  # publish average energy cost per hour
209
229
  if abs(self.total_balance_hour) > 0:
210
230
  msg = {
@@ -253,9 +273,9 @@ class EnergyCostCalculator(Juham):
253
273
  self.publish(self.topic_out_net_energy_balance, json.dumps(msg), 1, True)
254
274
 
255
275
  def publish_energy_cost(
256
- self, ts_now: float, site: str, cost_hour: float, cost_day: float
276
+ self, ts_now: float, site: str, cost_interval : float, cost_hour: float, cost_day: float
257
277
  ) -> None:
258
- """Publish daily and hourly energy cost/revenue
278
+ """Publish daily, hourly and per interval energy cost/revenue
259
279
 
260
280
  Args:
261
281
  ts_now (float): timestamp
@@ -263,5 +283,5 @@ class EnergyCostCalculator(Juham):
263
283
  cost_hour (float): cost or revenue per hour.
264
284
  cost_day (float) : cost or revenue per day
265
285
  """
266
- msg = {"name": site, "cost_hour": cost_hour, "cost_day": cost_day, "ts": ts_now}
286
+ msg = {"name": site, "cost_interval": cost_interval, "cost_hour": cost_hour, "cost_day": cost_day, "ts": ts_now}
267
287
  self.publish(self.topic_out_energy_cost, json.dumps(msg), 1, True)
@@ -33,7 +33,7 @@ class HeatingOptimizer(Juham):
33
33
  the hour is free. The UOI threshold determines the slots that are allowed to be consumed.
34
34
  """
35
35
 
36
- energy_balancing_interval: float = 3600
36
+ energy_balancing_interval: float = 900
37
37
  """Energy balancing interval, as regulated by the industry/converment. In seconds"""
38
38
 
39
39
  radiator_power: float = 6000 # W
@@ -61,18 +61,18 @@ class HeatingOptimizer(Juham):
61
61
  """Weight determining how large a share of the time slot a consumer receives compared to others ."""
62
62
 
63
63
  temperature_limits: dict[int, tuple[float, float]] = {
64
- 1: (68.0, 70.0), # January
65
- 2: (68.0, 70.0), # February
66
- 3: (65.0, 70.0), # March
67
- 4: (60.0, 70.0), # April
68
- 5: (50.0, 70.0), # May
69
- 6: (40.0, 60.0), # June
70
- 7: (40.0, 60.0), # July
71
- 8: (60.0, 65.0), # August
72
- 9: (60.0, 65.0), # September
73
- 10: (65.0, 66.0), # October
74
- 11: (66.0, 68.0), # November
75
- 12: (67.0, 70.0), # December
64
+ 1: (60.0, 65.0), # January
65
+ 2: (55.0, 65.0), # February
66
+ 3: (50.0, 64.0), # March
67
+ 4: (20.0, 50.0), # April
68
+ 5: (10.0, 40.0), # May
69
+ 6: (10.0, 38.0), # June
70
+ 7: (10.0, 38.0), # July
71
+ 8: (35.0, 40.0), # August
72
+ 9: (40.0, 50.0), # September
73
+ 10: (45.0, 55.0), # October
74
+ 11: (50.0, 58.0), # November
75
+ 12: (55.0, 62.0), # December
76
76
  }
77
77
  """Temperature limits for each month. The minimum temperature is maintained regardless of the cost.
78
78
  The limits are defined as a dictionary where the keys are month numbers (1-12)
@@ -128,9 +128,8 @@ class HeatingOptimizer(Juham):
128
128
  self.topic_out_energybalance = self.make_topic_name("energybalance/consumers")
129
129
  self.topic_out_power = self.make_topic_name("power")
130
130
 
131
- self.current_temperature = 100
132
- self.current_heating_plan = 0
133
- self.current_relay_state = -1
131
+ self.current_temperature : float = 100.0
132
+ self.current_relay_state : int = -1
134
133
  self.heating_plan: list[dict[str, int]] = []
135
134
  self.power_plan: list[dict[str, Any]] = []
136
135
  self.ranked_spot_prices: list[dict[Any, Any]] = []
@@ -195,7 +194,7 @@ class HeatingOptimizer(Juham):
195
194
  ranked_hours: list[dict[str, Any]] = []
196
195
  for h in sh:
197
196
  utc_ts = h["Timestamp"]
198
- if utc_ts > ts_utc_now:
197
+ if utc_ts >= ts_utc_now:
199
198
  ranked_hours.append(h)
200
199
 
201
200
  return ranked_hours
@@ -268,7 +267,7 @@ class HeatingOptimizer(Juham):
268
267
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
269
268
  m = None
270
269
  ts: float = timestamp()
271
- ts_utc_quantized: float = quantize(3600, ts - 3600)
270
+ ts_utc_quantized: float = quantize(self.energy_balancing_interval, ts - self.energy_balancing_interval)
272
271
  if msg.topic == self.topic_in_spot:
273
272
  self.on_spot(json.loads(msg.payload.decode()), ts_utc_quantized)
274
273
  return
@@ -496,7 +495,7 @@ class HeatingOptimizer(Juham):
496
495
  Returns:
497
496
  list: list of utilization entries
498
497
  """
499
- ts_utc_quantized = quantize(3600, timestamp() - 3600)
498
+ ts_utc_quantized = quantize(self.energy_balancing_interval, timestamp() - self.energy_balancing_interval)
500
499
  starts: str = timestampstr(ts_utc_quantized)
501
500
  self.info(
502
501
  f"{self.name} created power plan starting at {starts} with {len(self.ranked_spot_prices)} hours of spot prices",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.1.1
3
+ Version: 0.1.3
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>
@@ -30,11 +30,11 @@ License: LICENSE
30
30
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. **
31
31
 
32
32
 
33
- Project-URL: Homepage, https://gitlab.com/juham/juham/juham-automation.git
34
- Project-URL: Bug Reports, https://gitlab.com/juham/juham/juham-automation.git
33
+ Project-URL: Homepage, https://gitlab.com/juham/juham/juham-automation
34
+ Project-URL: Bug Reports, https://gitlab.com/juham/juham/juham-automationt
35
35
  Project-URL: Funding, https://meskanen.com
36
36
  Project-URL: Say Thanks!, http://meskanen.com
37
- Project-URL: Source, https://gitlab.com/juham/juham/juham-automation.git
37
+ Project-URL: Source, https://gitlab.com/juham/juham/juham-automation
38
38
  Keywords: home,automation,juham
39
39
  Classifier: Development Status :: 3 - Alpha
40
40
  Classifier: Intended Audience :: Developers
@@ -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.5
47
+ Requires-Dist: juham_core>=0.1.6
48
48
  Provides-Extra: dev
49
49
  Requires-Dist: check-manifest; extra == "dev"
50
50
  Requires-Dist: coverage>=7.0; extra == "dev"
@@ -3,8 +3,8 @@ 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
5
  juham_automation/automation/energybalancer.py,sha256=Mf9bK-Xo4zNalePL5EIGvlFObMkjWgeh76gvcU-ydIk,11532
6
- juham_automation/automation/energycostcalculator.py,sha256=v30wxRpuY2gGBSMJifrFRTjsRU9t-iCiq33Vds7s3O8,10877
7
- juham_automation/automation/heatingoptimizer.py,sha256=JV6PChMzGzMnqoN-Io0TZ_pTDNblTKVzHhzjCYB9dho,24601
6
+ juham_automation/automation/energycostcalculator.py,sha256=OOvKhRbQ99wtnmoy9R3kGdAPMSkoDLa5GnZswpu52u0,11853
7
+ juham_automation/automation/heatingoptimizer.py,sha256=M03r9sZLAPavjj0LlFXiVZqAKHN5ZUvcDVi2QR5yOrk,24684
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.1.1.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
- juham_automation-0.1.1.dist-info/METADATA,sha256=_dLM_-h0wa8wC4ck2gzKP1dy6J370SnK2LLc6MyYyfg,7133
22
- juham_automation-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- juham_automation-0.1.1.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
- juham_automation-0.1.1.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
- juham_automation-0.1.1.dist-info/RECORD,,
20
+ juham_automation-0.1.3.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
+ juham_automation-0.1.3.dist-info/METADATA,sha256=5Suhmsd8drHmj91gPngyVohtnA3sPdwsjsVIGYaQR7w,7122
22
+ juham_automation-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ juham_automation-0.1.3.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
+ juham_automation-0.1.3.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
+ juham_automation-0.1.3.dist-info/RECORD,,