juham-automation 0.1.2__tar.gz → 0.1.4__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.
Files changed (49) hide show
  1. {juham_automation-0.1.2/juham_automation.egg-info → juham_automation-0.1.4}/PKG-INFO +11 -7
  2. {juham_automation-0.1.2 → juham_automation-0.1.4}/README.rst +6 -2
  3. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/automation/energycostcalculator.py +106 -63
  4. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/automation/heatingoptimizer.py +120 -63
  5. {juham_automation-0.1.2 → juham_automation-0.1.4/juham_automation.egg-info}/PKG-INFO +11 -7
  6. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation.egg-info/requires.txt +1 -1
  7. {juham_automation-0.1.2 → juham_automation-0.1.4}/pyproject.toml +7 -9
  8. juham_automation-0.1.4/tests/automation/test_energycostcalculator.py +272 -0
  9. juham_automation-0.1.4/tests/automation/test_heatingoptimizer.py +333 -0
  10. juham_automation-0.1.2/tests/automation/test_energycostcalculator.py +0 -118
  11. juham_automation-0.1.2/tests/automation/test_heatingoptimizer.py +0 -134
  12. {juham_automation-0.1.2 → juham_automation-0.1.4}/LICENSE.rst +0 -0
  13. {juham_automation-0.1.2 → juham_automation-0.1.4}/MANIFEST.in +0 -0
  14. {juham_automation-0.1.2 → juham_automation-0.1.4}/examples/myapp.py +0 -0
  15. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/__init__.py +0 -0
  16. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/automation/__init__.py +0 -0
  17. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/automation/energybalancer.py +0 -0
  18. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/automation/powermeter_simulator.py +0 -0
  19. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/automation/spothintafi.py +0 -0
  20. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/automation/watercirculator.py +0 -0
  21. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/japp.py +0 -0
  22. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/py.typed +0 -0
  23. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/__init__.py +0 -0
  24. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/electricityprice_ts.py +0 -0
  25. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/energybalancer_ts.py +0 -0
  26. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/energycostcalculator_ts.py +0 -0
  27. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/forecast_ts.py +0 -0
  28. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/log_ts.py +0 -0
  29. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/power_ts.py +0 -0
  30. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/powermeter_ts.py +0 -0
  31. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation/ts/powerplan_ts.py +0 -0
  32. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation.egg-info/SOURCES.txt +0 -0
  33. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation.egg-info/dependency_links.txt +0 -0
  34. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation.egg-info/entry_points.txt +0 -0
  35. {juham_automation-0.1.2 → juham_automation-0.1.4}/juham_automation.egg-info/top_level.txt +0 -0
  36. {juham_automation-0.1.2 → juham_automation-0.1.4}/setup.cfg +0 -0
  37. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/__init__.py +0 -0
  38. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/automation/__init__.py +0 -0
  39. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/automation/test_energybalancer.py +0 -0
  40. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/automation/test_juham.py +0 -0
  41. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/automation/test_spothintafi.py +0 -0
  42. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/test_japp.py +0 -0
  43. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/ts/__init__.py +0 -0
  44. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/ts/test_energycostcalculator_ts.py +0 -0
  45. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/ts/test_forecast_ts.py +0 -0
  46. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/ts/test_log_ts.py +0 -0
  47. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/ts/test_power_ts.py +0 -0
  48. {juham_automation-0.1.2 → juham_automation-0.1.4}/tests/ts/test_powermeter_ts.py +0 -0
  49. {juham_automation-0.1.2 → juham_automation-0.1.4}/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.1.2
3
+ Version: 0.1.4
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"
@@ -56,9 +56,11 @@ Welcome to Juham™ - Juha's Ultimate Home Automation Masterpiece
56
56
  Project Description
57
57
  -------------------
58
58
 
59
- This package extends the ``juham_core`` package, providing home automation building blocks that address most common needs. It consists of two main sub-modules:
59
+ This package extends the ``juham_core`` package, providing home automation building blocks that address most common needs.
60
+ It consists of two main sub-modules:
60
61
 
61
62
  ``automation``:
63
+
62
64
  - **spothintafi**: Acquires electricity prices in Finland.
63
65
  - **watercirculator**: Automates a water circulator pump based on hot water temperature and motion detection.
64
66
  - **heatingoptimizer**: Controls hot water radiators based on temperature sensors and electricity price data.
@@ -66,6 +68,7 @@ This package extends the ``juham_core`` package, providing home automation build
66
68
  - **energybalancer**: Handles real-time energy balancing and net billing.
67
69
 
68
70
  ``ts``:
71
+
69
72
  - This folder contains time series recorders that listen for Juham™ topics and store the data in a time series database for later inspection.
70
73
 
71
74
  Project Status
@@ -73,7 +76,8 @@ Project Status
73
76
 
74
77
  **Current State**: **Alpha (Status 3)**
75
78
 
76
- All classes have been tested to some extent, and no known bugs have been reported. However, the code still requires work in terms of design and robustness. For example, electricity prices are currently hard-coded to use euros, but this should be configurable to support multiple currencies.
79
+ All classes have been tested to some extent, and no known bugs have been reported. However, the code still requires
80
+ work in terms of design and robustness.
77
81
 
78
82
 
79
83
  Project Links
@@ -4,9 +4,11 @@ Welcome to Juham™ - Juha's Ultimate Home Automation Masterpiece
4
4
  Project Description
5
5
  -------------------
6
6
 
7
- This package extends the ``juham_core`` package, providing home automation building blocks that address most common needs. It consists of two main sub-modules:
7
+ This package extends the ``juham_core`` package, providing home automation building blocks that address most common needs.
8
+ It consists of two main sub-modules:
8
9
 
9
10
  ``automation``:
11
+
10
12
  - **spothintafi**: Acquires electricity prices in Finland.
11
13
  - **watercirculator**: Automates a water circulator pump based on hot water temperature and motion detection.
12
14
  - **heatingoptimizer**: Controls hot water radiators based on temperature sensors and electricity price data.
@@ -14,6 +16,7 @@ This package extends the ``juham_core`` package, providing home automation build
14
16
  - **energybalancer**: Handles real-time energy balancing and net billing.
15
17
 
16
18
  ``ts``:
19
+
17
20
  - This folder contains time series recorders that listen for Juham™ topics and store the data in a time series database for later inspection.
18
21
 
19
22
  Project Status
@@ -21,7 +24,8 @@ Project Status
21
24
 
22
25
  **Current State**: **Alpha (Status 3)**
23
26
 
24
- All classes have been tested to some extent, and no known bugs have been reported. However, the code still requires work in terms of design and robustness. For example, electricity prices are currently hard-coded to use euros, but this should be configurable to support multiple currencies.
27
+ All classes have been tested to some extent, and no known bugs have been reported. However, the code still requires
28
+ work in terms of design and robustness.
25
29
 
26
30
 
27
31
  Project Links
@@ -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
 
@@ -68,10 +72,10 @@ class EnergyCostCalculator(Juham):
68
72
  self.error(f"Unknown event {msg.topic}")
69
73
 
70
74
  def on_spot(self, spot: dict[Any, Any]) -> None:
71
- """Stores the received per hour electricity prices to spots list.
75
+ """Stores the received per slot electricity prices to spots list.
72
76
 
73
77
  Args:
74
- spot (list): list of hourly spot prices
78
+ spot (list): list of spot prices
75
79
  """
76
80
 
77
81
  for s in spot:
@@ -88,76 +92,91 @@ 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.
95
+
96
+ def get_price_at(self, ts: float) -> float:
97
+ """Return the spot price applicable at the given timestamp.
94
98
 
95
99
  Args:
96
- ts_prev (float): previous time
97
- ts_now (float): current time
100
+ ts (float): current time (epoch seconds)
101
+
98
102
  Returns:
99
- Electricity prices for the given interval
103
+ float: PriceWithTax for the slot that contains ts. Returns the last
104
+ known price if ts is equal/after the last spot timestamp.
105
+ Returns 0.0 and logs an error if no matching slot is found.
100
106
  """
101
- prev_price = None
102
- current_price = None
107
+ if not self.spots:
108
+ self.error(f"PANIC: no spot prices available; lookup ts={ts}")
109
+ return 0.0
110
+
111
+ # ensure spots sorted by timestamp (defensive)
112
+ try:
113
+ # cheap check — assumes list of dicts with "Timestamp"
114
+ if any(self.spots[i]["Timestamp"] > self.spots[i + 1]["Timestamp"] for i in range(len(self.spots) - 1)):
115
+ self.spots.sort(key=lambda r: r["Timestamp"])
116
+ except Exception:
117
+ # if unexpected structure, still try safe path below and log
118
+ self.debug("get_price_at: spot list structure unexpected while checking sort order", "")
103
119
 
104
120
  for i in range(0, len(self.spots) - 1):
105
121
  r0 = self.spots[i]
106
122
  r1 = self.spots[i + 1]
107
123
  ts0 = r0["Timestamp"]
108
124
  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
125
+ if ts >= ts0 and ts < ts1:
126
+ return r0["PriceWithTax"]
127
+
128
+ # If timestamp is exactly equal to the last spot timestamp or beyond
129
+ last = self.spots[-1]
130
+ if ts >= last["Timestamp"]:
131
+ return last["PriceWithTax"]
132
+
133
+ # If we get here, ts is before the first spot timestamp
134
+ first = self.spots[0]
135
+ self.error(
136
+ f"PANIC: Timestamp {ts} out of bounds for spot price lookup; "
137
+ f"first=(ts={first['Timestamp']}, price={first.get('PriceWithTax')}), "
138
+ f"last=(ts={last['Timestamp']}, price={last.get('PriceWithTax')}), "
139
+ f"len(spots)={len(self.spots)}"
140
+ )
141
+ return 0.0
142
+
143
+
117
144
 
118
145
  def calculate_net_energy_cost(
119
146
  self, ts_prev: float, ts_now: float, energy: float
120
147
  ) -> float:
121
- """Given time interval as start and stop Calculate the cost over the
122
- given time period. Positive values indicate revenue, negative cost.
148
+ """
149
+ Calculate the cost (or revenue) of energy consumed/produced over the given time interval.
150
+ Positive values indicate revenue, negative values indicate cost.
123
151
 
124
152
  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
153
+ ts_prev (float): Start timestamp of the interval
154
+ ts_now (float): End timestamp of the interval
155
+ energy (float): Energy consumed during the interval (in watts or Joules)
156
+
128
157
  Returns:
129
- Cost or revenue
158
+ float: Total cost/revenue for the interval
130
159
  """
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
160
+ cost = 0.0
161
+ current = ts_prev
162
+ interval = self.energy_balancing_interval
163
+
164
+ while current < ts_now:
165
+ next_ts = min(ts_now, current + interval)
166
+ # Get spot price at start and end of interval
167
+ price_start = self.map_kwh_prices_to_joules(self.get_price_at(current))
168
+ price_end = self.map_kwh_prices_to_joules(self.get_price_at(next_ts))
169
+
170
+ # Trapezoidal integration: average price over interval
171
+ avg_price = (price_start + price_end) / 2.0
172
+ dt = next_ts - current
173
+ cost += energy * avg_price * dt
174
+
175
+ current = next_ts
176
+
159
177
  return cost
160
178
 
179
+
161
180
  def on_powerconsumption(self, ts_now: float, m: dict[Any, Any]) -> None:
162
181
  """Calculate net energy cost and update the hourly consumption attribute
163
182
  accordingly.
@@ -170,21 +189,29 @@ class EnergyCostCalculator(Juham):
170
189
  if not self.spots:
171
190
  self.info("Waiting for electricity prices...")
172
191
  elif self.current_ts == 0:
192
+ self.net_energy_balance_cost_interval = 0.0
173
193
  self.net_energy_balance_cost_hour = 0.0
174
194
  self.net_energy_balance_cost_day = 0.0
175
195
  self.current_ts = ts_now
176
- self.net_energy_balance_start_hour = quantize(
196
+ self.net_energy_balance_start_interval = quantize(
177
197
  self.energy_balancing_interval, ts_now
178
198
  )
199
+ self.net_energy_balance_start_hour = quantize(
200
+ 3600, ts_now
201
+ )
179
202
  else:
180
203
  # calculate cost of energy consumed/produced
181
204
  dp: float = self.calculate_net_energy_cost(self.current_ts, ts_now, power)
205
+ self.net_energy_balance_cost_interval = self.net_energy_balance_cost_interval + dp
182
206
  self.net_energy_balance_cost_hour = self.net_energy_balance_cost_hour + dp
183
207
  self.net_energy_balance_cost_day = self.net_energy_balance_cost_day + dp
184
208
 
185
209
  # calculate and publish energy balance
186
210
  dt = ts_now - self.current_ts # time elapsed since previous call
187
211
  balance = dt * power # energy consumed/produced in this slot in Joules
212
+ self.total_balance_interval = (
213
+ self.total_balance_interval + balance * self._joule_to_kwh_coeff
214
+ )
188
215
  self.total_balance_hour = (
189
216
  self.total_balance_hour + balance * self._joule_to_kwh_coeff
190
217
  )
@@ -195,16 +222,32 @@ class EnergyCostCalculator(Juham):
195
222
  self.publish_energy_cost(
196
223
  ts_now,
197
224
  self.name,
225
+ self.net_energy_balance_cost_interval,
198
226
  self.net_energy_balance_cost_hour,
199
227
  self.net_energy_balance_cost_day,
200
228
  )
201
229
 
202
230
  # Check if the current energy balancing interval has ended
203
231
  # 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
- ):
232
+ if ts_now - self.net_energy_balance_start_interval > self.energy_balancing_interval:
233
+ # publish average energy cost per hour
234
+ if abs(self.total_balance_interval) > 0:
235
+ msg = {
236
+ "name": self.name,
237
+ "average_interval": self.net_energy_balance_cost_interval
238
+ / self.total_balance_interval,
239
+ "ts": ts_now,
240
+ }
241
+ self.publish(self.topic_out_energy_cost, json.dumps(msg), 0, False)
242
+
243
+ # reset for the next hour
244
+ self.total_balance_interval = 0
245
+ self.net_energy_balance_cost_interval = 0.0
246
+ self.net_energy_balance_start_interval = ts_now
247
+
248
+ # Check if the current energy balancing interval has ended
249
+ # If so, reset the net_energy_balance attribute for the next interval
250
+ if ts_now - self.net_energy_balance_start_hour > 3600:
208
251
  # publish average energy cost per hour
209
252
  if abs(self.total_balance_hour) > 0:
210
253
  msg = {
@@ -253,9 +296,9 @@ class EnergyCostCalculator(Juham):
253
296
  self.publish(self.topic_out_net_energy_balance, json.dumps(msg), 1, True)
254
297
 
255
298
  def publish_energy_cost(
256
- self, ts_now: float, site: str, cost_hour: float, cost_day: float
299
+ self, ts_now: float, site: str, cost_interval : float, cost_hour: float, cost_day: float
257
300
  ) -> None:
258
- """Publish daily and hourly energy cost/revenue
301
+ """Publish daily, hourly and per interval energy cost/revenue
259
302
 
260
303
  Args:
261
304
  ts_now (float): timestamp
@@ -263,5 +306,5 @@ class EnergyCostCalculator(Juham):
263
306
  cost_hour (float): cost or revenue per hour.
264
307
  cost_day (float) : cost or revenue per day
265
308
  """
266
- msg = {"name": site, "cost_hour": cost_hour, "cost_day": cost_day, "ts": ts_now}
309
+ msg = {"name": site, "cost_interval": cost_interval, "cost_hour": cost_hour, "cost_day": cost_day, "ts": ts_now}
267
310
  self.publish(self.topic_out_energy_cost, json.dumps(msg), 1, True)