juham-automation 0.0.12__py3-none-any.whl → 0.2.8__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.
@@ -7,10 +7,11 @@ Juham - Juha's Ultimate Home Automation Masterpiece
7
7
  """
8
8
 
9
9
  from .automation import EnergyCostCalculator
10
- from .automation import PowerMeterSimulator
11
10
  from .automation import SpotHintaFi
12
11
  from .automation import WaterCirculator
13
- from .automation import HotWaterOptimizer
12
+ from .automation import HeatingOptimizer
13
+ from .automation import EnergyBalancer
14
+ from .automation import LeakDetector
14
15
  from .ts import EnergyCostCalculatorTs
15
16
  from .ts import ForecastTs
16
17
  from .ts import LogTs
@@ -18,6 +19,7 @@ from .ts import PowerTs
18
19
  from .ts import PowerPlanTs
19
20
  from .ts import PowerMeterTs
20
21
  from .ts import ElectricityPriceTs
22
+ from .ts import EnergyBalancerTs
21
23
  from .japp import JApp
22
24
 
23
25
 
@@ -25,7 +27,9 @@ __all__ = [
25
27
  "EnergyCostCalculator",
26
28
  "EnergyCostCalculatorTs",
27
29
  "ForecastTs",
28
- "HotWaterOptimizer",
30
+ "HeatingOptimizer",
31
+ "EnergyBalancer",
32
+ "LeakDetector",
29
33
  "LogTs",
30
34
  "PowerTs",
31
35
  "PowerPlanTs",
@@ -33,6 +37,6 @@ __all__ = [
33
37
  "SpotHintaFi",
34
38
  "WaterCirculator",
35
39
  "JApp",
36
- "PowerMeterSimulator",
37
40
  "ElectricityPriceTs",
41
+ "EnergyBalancerTs",
38
42
  ]
@@ -9,13 +9,15 @@ Juham - Juha's Ultimate Home Automation classes
9
9
  from .energycostcalculator import EnergyCostCalculator
10
10
  from .spothintafi import SpotHintaFi
11
11
  from .watercirculator import WaterCirculator
12
- from .hotwateroptimizer import HotWaterOptimizer
13
- from .powermeter_simulator import PowerMeterSimulator
12
+ from .heatingoptimizer import HeatingOptimizer
13
+ from .energybalancer import EnergyBalancer
14
+ from .leakdetector import LeakDetector
14
15
 
15
16
  __all__ = [
16
17
  "EnergyCostCalculator",
17
- "HotWaterOptimizer",
18
+ "HeatingOptimizer",
18
19
  "SpotHintaFi",
19
20
  "WaterCirculator",
20
- "PowerMeterSimulator",
21
+ "EnergyBalancer",
22
+ "LeakDetector",
21
23
  ]
@@ -0,0 +1,281 @@
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 Consumer:
10
+ """Class representing a consumer.
11
+
12
+ This class is used to represent a consumer in the energy balancer.
13
+ It contains the name of the consumer and its power consumption.
14
+
15
+ """
16
+
17
+ def __init__(self, name: str, power: float, weight: float) -> None:
18
+ """Initialize the consumer
19
+
20
+ Args:
21
+ name (str): name of the consumer
22
+ power (float): power of the consumer in watts
23
+ """
24
+ self.name = name
25
+ self.power: float = power
26
+ self.start: float = 0.0
27
+ self.stop: float = 0.0
28
+ self.weight: float = weight
29
+
30
+
31
+ class EnergyBalancer(Juham):
32
+ """The energy balancer monitors the balance between produced and consumed energy
33
+ within the balancing interval to determine if there is enough energy available for
34
+ a given energy-consuming device, such as heating radiators, to operate within the
35
+ remaining time of the interval.
36
+
37
+ Any number of energy-consuming devices can be connected to the energy balancer, with
38
+ without any restrictions on the power consumption. The energy balancer will monitor the
39
+ power consumption of the devices and determine if there is enough energy available for
40
+ the devices to operate within the remaining time of the balancing interval.
41
+
42
+ The energy balancer is used in conjunction with a power meter that reads
43
+ the total power consumption of the house. The energy balancer uses the power meter
44
+
45
+
46
+ """
47
+
48
+ #: Description of the attribute
49
+ energy_balancing_interval: int = 900
50
+ """The time interval in seconds for energy balancing."""
51
+
52
+ def __init__(self, name: str = "energybalancer") -> None:
53
+ """Initialize the energy balancer.
54
+
55
+ Args:
56
+ name (str): name of the heating radiator
57
+ power (float): power of the consumer in watts
58
+ """
59
+ super().__init__(name)
60
+
61
+ self.topic_in_consumers = self.make_topic_name("energybalance/consumers")
62
+ self.topic_out_status = self.make_topic_name("energybalance/status")
63
+ self.topic_out_diagnostics = self.make_topic_name("energybalance/diagnostics")
64
+ self.topic_in_power = self.make_topic_name("net_energy_balance")
65
+
66
+ self.net_energy_balance: float = 0.0 # Energy balance in joules (watt-seconds)
67
+ self.current_interval_ts: float = -1
68
+ self.needed_energy: float = 0.0 # Energy needed in joules (watt-seconds)
69
+ self.net_energy_balancing_mode: bool = False
70
+ self.consumers: dict[str, Consumer] = {}
71
+
72
+ @override
73
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
74
+ super().on_connect(client, userdata, flags, rc)
75
+ if rc == 0:
76
+ self.subscribe(self.topic_in_power)
77
+ self.subscribe(self.topic_in_consumers)
78
+
79
+ @override
80
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
81
+ ts: float = timestamp()
82
+
83
+ if msg.topic == self.topic_in_power:
84
+ self.on_power(json.loads(msg.payload.decode()), ts)
85
+ elif msg.topic == self.topic_in_consumers:
86
+ self.on_consumer(json.loads(msg.payload.decode()))
87
+ else:
88
+ super().on_message(client, userdata, msg)
89
+
90
+ def on_power(self, m: dict[str, Any], ts: float) -> None:
91
+ """Handle the power consumption. Read the current power balance and accumulate
92
+ to the net energy balance to reflect the energy produced (or consumed) within the
93
+ current time slot.
94
+ Args:
95
+ m (dict[str, Any]): power consumption message
96
+ ts (float): current time
97
+ """
98
+ self.update_energy_balance(m["power"], ts)
99
+
100
+ def on_consumer(self, m: dict[str, Any]) -> None:
101
+ """Add consumer, e.g. heating radiator to be controlled.
102
+ Args:
103
+ m (dict[str, Any]): power consumer message
104
+ ts (float): current time
105
+ """
106
+ self.consumers[m["Unit"]] = Consumer(m["Unit"], m["Power"], m["Weight"])
107
+ self.info(
108
+ f"Consumer {m['Unit']} added, power: {m['Power']}, weight: {m['Weight']}"
109
+ )
110
+
111
+ def update_energy_balance(self, power: float, ts: float) -> None:
112
+ """Update the current net net energy balance. The change in the balance is calculate the
113
+ energy balance, which the time elapsed since the last update, multiplied by the
114
+ power. Positive energy balance means we have produced energy that can be consumed
115
+ at the end of the interval. The target is to use all the energy produced during the
116
+ balancing interval. This method is called by the powermeter reading the
117
+ total power consumption of the house.
118
+
119
+ Args:
120
+ power (float): power reading from the powermeter. Positive value means
121
+ energy produced, negative value means energy consumed. The value of 0 means
122
+ the house is not consuming or producing energy.
123
+ ts (float): current time in utc seconds
124
+ """
125
+
126
+ # regardless of the mode, if we hit the end of the interval, reset the balance
127
+
128
+ interval_ts: float = ts % self.energy_balancing_interval
129
+ if self.current_interval_ts < 0 or interval_ts <= self.current_interval_ts:
130
+ # time runs backwards, must be a new interval
131
+ self.reset_net_energy_balance(interval_ts)
132
+ else:
133
+ # update the energy balance with the elapsed time and the power
134
+ elapsed_ts = interval_ts - self.current_interval_ts
135
+ balance: float = elapsed_ts * power # joules i.e. watt-seconds
136
+ self.net_energy_balance = self.net_energy_balance + balance
137
+ self.current_interval_ts = interval_ts
138
+ self.needed_energy = self.calculate_needed_energy(interval_ts)
139
+ if self.net_energy_balancing_mode:
140
+ if self.net_energy_balance <= 0:
141
+ # if we have used all the energy, disable the balancing mode
142
+ self.reset_net_energy_balance(0.0)
143
+ else:
144
+ # consider enabling the balancing mode
145
+ # if we have enough energy to power the radiator for the rest of the time slot
146
+ if self.net_energy_balance >= self.needed_energy:
147
+ self.net_energy_balancing_mode = True
148
+ self.initialize_consumer_timelines(ts)
149
+ self.publish_energybalance(ts)
150
+
151
+ def calculate_needed_energy(self, interval_ts: float) -> float:
152
+ """Calculate the energy needed to power the consumer for the rest of the time slot.
153
+ Assumes consumers run one at a time in serialized manner and are evenly distributed over the time slot
154
+ to minimze the power peak.
155
+
156
+ Args:
157
+ interval_ts (float): current elapsed seconds within the balancing interval.
158
+
159
+ Returns:
160
+ float: energy needed in joules
161
+ """
162
+
163
+ required_power: float = 0.0
164
+ num_consumers: int = len(self.consumers)
165
+ if num_consumers == 0:
166
+ return 0.0
167
+ remaining_ts_consumer: float = (
168
+ self.energy_balancing_interval - interval_ts
169
+ ) / num_consumers
170
+ total_weight: float = 0.0
171
+ for consumer in self.consumers.values():
172
+ total_weight += consumer.weight
173
+ equal_weight: float = total_weight / num_consumers
174
+ for consumer in self.consumers.values():
175
+ required_power += (
176
+ (consumer.weight / equal_weight)
177
+ * consumer.power
178
+ * remaining_ts_consumer
179
+ )
180
+ return required_power
181
+
182
+ def initialize_consumer_timelines(self, ts: float) -> None:
183
+ """Initialize the list of active consumers with their start and stop times.
184
+
185
+ Args:
186
+ ts (float): current time.
187
+
188
+ Returns:
189
+ None
190
+ """
191
+ num_consumers: int = len(self.consumers)
192
+ if num_consumers == 0:
193
+ return # If there are no consumers, we simply do nothing
194
+ interval_ts: float = ts % self.energy_balancing_interval
195
+ secs_per_consumer: float = (
196
+ self.energy_balancing_interval - interval_ts
197
+ ) / num_consumers
198
+
199
+ total_weight: float = 0.0
200
+ for consumer in self.consumers.values():
201
+ total_weight += consumer.weight
202
+ equal_weight: float = total_weight / num_consumers
203
+
204
+ for consumer in self.consumers.values():
205
+ consumer.start = interval_ts
206
+ consumer.stop = (
207
+ interval_ts + secs_per_consumer * consumer.weight / equal_weight
208
+ )
209
+ interval_ts += secs_per_consumer
210
+
211
+ def publish_energybalance(self, ts: float) -> None:
212
+ """Publish diagnostics and status.
213
+
214
+ Args:
215
+ ts (float): current time.
216
+
217
+ Returns:
218
+ None
219
+ """
220
+
221
+ # publish diagnostics
222
+ m: dict[str, Any] = {
223
+ "EnergyBalancer": self.name,
224
+ "CurrentBalance": self.net_energy_balance,
225
+ "NeededBalance": self.needed_energy,
226
+ "Timestamp": ts,
227
+ }
228
+ self.publish(self.topic_out_diagnostics, json.dumps(m))
229
+
230
+ # publish consumer statuses to control consumers
231
+ num_consumers: int = len(self.consumers)
232
+ if num_consumers == 0:
233
+ return # If there are no consumers, we simply do nothing
234
+ interval_ts = ts % self.energy_balancing_interval
235
+ for consumer in self.consumers.values():
236
+ m = {
237
+ "EnergyBalancer": self.name,
238
+ "Unit": consumer.name,
239
+ "Power": consumer.power,
240
+ "Mode": consumer.start <= interval_ts < consumer.stop,
241
+ "Weight": consumer.weight,
242
+ "Timestamp": ts,
243
+ }
244
+ self.publish(self.topic_out_status, json.dumps(m))
245
+
246
+ def detect_consumer_status(self, name: str, ts: float) -> bool:
247
+ """Detect consumer status
248
+
249
+ Args:
250
+ name (str): name of the consumer
251
+ ts (float): current time.
252
+
253
+ Returns:
254
+ True if the consumer is active, False otherwise
255
+ """
256
+ consumer: Consumer = self.consumers[name]
257
+ interval_ts = ts % self.energy_balancing_interval
258
+ return consumer.start <= interval_ts < consumer.stop
259
+
260
+ def reset_net_energy_balance(self, interval_ts: float) -> None:
261
+ """Reset the net energy balance at the end of the interval."""
262
+ self.net_energy_balance = 0.0
263
+ self.current_interval_ts = interval_ts
264
+ self.needed_energy = self.calculate_needed_energy(interval_ts)
265
+ self.net_energy_balancing_mode = False
266
+ for consumer in self.consumers.values():
267
+ consumer.start = 0.0
268
+ consumer.stop = 0.0
269
+
270
+ def activate_balancing_mode(self, ts: float) -> None:
271
+ """Activate balancing mode when enough energy is available."""
272
+ self.net_energy_balancing_mode = True
273
+ self.info(
274
+ f"{int(self.net_energy_balance/3600)} Wh is enough to supply the radiator, enable"
275
+ )
276
+
277
+ def deactivate_balancing_mode(self) -> None:
278
+ """Deactivate balancing mode when energy is depleted or interval ends."""
279
+ self.net_energy_balancing_mode = False
280
+ self.info("Balance used, or the end of the interval reached, disable")
281
+ self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
@@ -6,19 +6,21 @@ 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
  )
12
13
 
13
14
 
14
15
  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:
16
+ """The EnergyCostCalculator class calculates the net energy balance and cost between produced
17
+ (or consumed) energy for Time-Based Settlement (TBS). It performs the following functions:
17
18
 
18
19
  * Subscribes to 'spot' and 'power' MQTT topics.
19
20
  * 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.
21
+ * Calculates the cost of energy consumed/produced based on the spot prices.
22
+ * Publishes the calculated values to the MQTT net energy balance and cost topics.
23
+
22
24
 
23
25
  This information helps other home automation components optimize energy usage and
24
26
  minimize electricity bills.
@@ -27,17 +29,20 @@ class EnergyCostCalculator(Juham):
27
29
  _kwh_to_joule_coeff: float = 1000.0 * 3600
28
30
  _joule_to_kwh_coeff: float = 1.0 / _kwh_to_joule_coeff
29
31
 
30
- energy_balancing_interval: float = 3600
32
+ energy_balancing_interval: int = 900 # in seconds (15 minutes)
31
33
 
32
34
  def __init__(self, name: str = "ecc") -> None:
33
35
  super().__init__(name)
34
36
  self.current_ts: float = 0
37
+ self.total_balance_interval : float = 0
35
38
  self.total_balance_hour: float = 0
36
39
  self.total_balance_day: float = 0
40
+ self.net_energy_balance_cost_interval: float = 0
37
41
  self.net_energy_balance_cost_hour: float = 0
38
42
  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())
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())
41
46
  self.spots: list[dict[str, float]] = []
42
47
  self.init_topics()
43
48
 
@@ -64,13 +69,13 @@ class EnergyCostCalculator(Juham):
64
69
  elif msg.topic == self.topic_in_powerconsumption:
65
70
  self.on_powerconsumption(ts_now, m)
66
71
  else:
67
- self.error(f"Unknown event {msg.topic}")
72
+ super().on_message(client, userdata, msg)
68
73
 
69
74
  def on_spot(self, spot: dict[Any, Any]) -> None:
70
- """Stores the received per hour electricity prices to spots list.
75
+ """Stores the received per slot electricity prices to spots list.
71
76
 
72
77
  Args:
73
- spot (list): list of hourly spot prices
78
+ spot (list): list of spot prices
74
79
  """
75
80
 
76
81
  for s in spot:
@@ -87,76 +92,91 @@ class EnergyCostCalculator(Juham):
87
92
  """
88
93
  return price * self._joule_to_kwh_coeff
89
94
 
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.
95
+
96
+ def get_price_at(self, ts: float) -> float:
97
+ """Return the spot price applicable at the given timestamp.
93
98
 
94
99
  Args:
95
- ts_prev (float): previous time
96
- ts_now (float): current time
100
+ ts (float): current time (epoch seconds)
101
+
97
102
  Returns:
98
- 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.
99
106
  """
100
- prev_price = None
101
- 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", "")
102
119
 
103
120
  for i in range(0, len(self.spots) - 1):
104
121
  r0 = self.spots[i]
105
122
  r1 = self.spots[i + 1]
106
123
  ts0 = r0["Timestamp"]
107
124
  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
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
+
116
144
 
117
145
  def calculate_net_energy_cost(
118
146
  self, ts_prev: float, ts_now: float, energy: float
119
147
  ) -> float:
120
- """Given time interval as start and stop Calculate the cost over the
121
- 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.
122
151
 
123
152
  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
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
+
127
157
  Returns:
128
- Cost or revenue
158
+ float: Total cost/revenue for the interval
129
159
  """
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
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
+
158
177
  return cost
159
178
 
179
+
160
180
  def on_powerconsumption(self, ts_now: float, m: dict[Any, Any]) -> None:
161
181
  """Calculate net energy cost and update the hourly consumption attribute
162
182
  accordingly.
@@ -169,21 +189,29 @@ class EnergyCostCalculator(Juham):
169
189
  if not self.spots:
170
190
  self.info("Waiting for electricity prices...")
171
191
  elif self.current_ts == 0:
192
+ self.net_energy_balance_cost_interval = 0.0
172
193
  self.net_energy_balance_cost_hour = 0.0
173
194
  self.net_energy_balance_cost_day = 0.0
174
195
  self.current_ts = ts_now
175
- self.net_energy_balance_start_hour = quantize(
196
+ self.net_energy_balance_start_interval = quantize(
176
197
  self.energy_balancing_interval, ts_now
177
198
  )
199
+ self.net_energy_balance_start_hour = quantize(
200
+ 3600, ts_now
201
+ )
178
202
  else:
179
203
  # calculate cost of energy consumed/produced
180
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
181
206
  self.net_energy_balance_cost_hour = self.net_energy_balance_cost_hour + dp
182
207
  self.net_energy_balance_cost_day = self.net_energy_balance_cost_day + dp
183
208
 
184
209
  # calculate and publish energy balance
185
210
  dt = ts_now - self.current_ts # time elapsed since previous call
186
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
+ )
187
215
  self.total_balance_hour = (
188
216
  self.total_balance_hour + balance * self._joule_to_kwh_coeff
189
217
  )
@@ -194,16 +222,32 @@ class EnergyCostCalculator(Juham):
194
222
  self.publish_energy_cost(
195
223
  ts_now,
196
224
  self.name,
225
+ self.net_energy_balance_cost_interval,
197
226
  self.net_energy_balance_cost_hour,
198
227
  self.net_energy_balance_cost_day,
199
228
  )
200
229
 
201
230
  # Check if the current energy balancing interval has ended
202
231
  # 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
- ):
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:
207
251
  # publish average energy cost per hour
208
252
  if abs(self.total_balance_hour) > 0:
209
253
  msg = {
@@ -252,9 +296,9 @@ class EnergyCostCalculator(Juham):
252
296
  self.publish(self.topic_out_net_energy_balance, json.dumps(msg), 1, True)
253
297
 
254
298
  def publish_energy_cost(
255
- 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
256
300
  ) -> None:
257
- """Publish daily and hourly energy cost/revenue
301
+ """Publish daily, hourly and per interval energy cost/revenue
258
302
 
259
303
  Args:
260
304
  ts_now (float): timestamp
@@ -262,5 +306,5 @@ class EnergyCostCalculator(Juham):
262
306
  cost_hour (float): cost or revenue per hour.
263
307
  cost_day (float) : cost or revenue per day
264
308
  """
265
- 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}
266
310
  self.publish(self.topic_out_energy_cost, json.dumps(msg), 1, True)