juham-automation 0.0.30__py3-none-any.whl → 0.0.33__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,27 @@ from juham_core import Juham, timestamp
6
6
  from masterpiece import MqttMsg
7
7
 
8
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) -> 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
+
29
+
9
30
  class EnergyBalancer(Juham):
10
31
  """The energy balancer monitors the balance between produced and consumed energy
11
32
  within the balancing interval to determine if there is enough energy available for
@@ -19,6 +40,8 @@ class EnergyBalancer(Juham):
19
40
 
20
41
  The energy balancer is used in conjunction with a power meter that reads
21
42
  the total power consumption of the house. The energy balancer uses the power meter
43
+
44
+
22
45
  """
23
46
 
24
47
  #: Description of the attribute
@@ -35,15 +58,15 @@ class EnergyBalancer(Juham):
35
58
  super().__init__(name)
36
59
 
37
60
  self.topic_in_consumers = self.make_topic_name("energybalance/consumers")
38
- self.topic_out_energybalance = self.make_topic_name("energybalance/status")
61
+ self.topic_out_status = self.make_topic_name("energybalance/status")
62
+ self.topic_out_diagnostics = self.make_topic_name("energybalance/diagnostics")
39
63
  self.topic_in_power = self.make_topic_name("net_energy_balance")
40
64
 
41
65
  self.net_energy_balance: float = 0.0 # Energy balance in joules (watt-seconds)
42
66
  self.current_interval_ts: float = -1
43
67
  self.needed_energy: float = 0.0 # Energy needed in joules (watt-seconds)
44
68
  self.net_energy_balancing_mode: bool = False
45
- self.consumers: dict[str, float] = {}
46
- self.active_consumers: dict[str, dict[float, float]] = {}
69
+ self.consumers: dict[str, Consumer] = {}
47
70
 
48
71
  @override
49
72
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
@@ -79,7 +102,7 @@ class EnergyBalancer(Juham):
79
102
  m (dict[str, Any]): power consumer message
80
103
  ts (float): current time
81
104
  """
82
- self.consumers[m["Unit"]] = m["Power"]
105
+ self.consumers[m["Unit"]] = Consumer(m["Unit"], m["Power"])
83
106
  self.info(f"Consumer {m['Unit']} added, power: {m['Power']}")
84
107
 
85
108
  def update_energy_balance(self, power: float, ts: float) -> None:
@@ -98,6 +121,7 @@ class EnergyBalancer(Juham):
98
121
  """
99
122
 
100
123
  # regardless of the mode, if we hit the end of the interval, reset the balance
124
+
101
125
  interval_ts: float = ts % self.energy_balancing_interval
102
126
  if self.current_interval_ts < 0 or interval_ts <= self.current_interval_ts:
103
127
  # time runs backwards, must be a new interval
@@ -118,7 +142,7 @@ class EnergyBalancer(Juham):
118
142
  # if we have enough energy to power the radiator for the rest of the time slot
119
143
  if self.net_energy_balance >= self.needed_energy:
120
144
  self.net_energy_balancing_mode = True
121
- self.initialize_active_consumers(ts)
145
+ self.initialize_consumer_timelines(ts)
122
146
  self.publish_energybalance(ts)
123
147
 
124
148
  def calculate_needed_energy(self, interval_ts: float) -> float:
@@ -141,10 +165,10 @@ class EnergyBalancer(Juham):
141
165
  self.energy_balancing_interval - interval_ts
142
166
  ) / num_consumers
143
167
  for consumer in self.consumers.values():
144
- required_power += consumer * remaining_ts_consumer
168
+ required_power += consumer.power * remaining_ts_consumer
145
169
  return required_power
146
170
 
147
- def initialize_active_consumers(self, ts: float) -> None:
171
+ def initialize_consumer_timelines(self, ts: float) -> None:
148
172
  """Initialize the list of active consumers with their start and stop times.
149
173
 
150
174
  Args:
@@ -161,46 +185,58 @@ class EnergyBalancer(Juham):
161
185
  self.energy_balancing_interval - interval_ts
162
186
  ) / num_consumers
163
187
 
164
- # Reset the active consumers dictionary
165
- self.active_consumers.clear()
166
-
167
- for consumer_name, consumer_data in self.consumers.items():
168
- start: float = interval_ts
169
- stop: float = start + secs_per_consumer
170
-
171
- # Add the consumer to the active consumers dictionary with its start and stop times
172
- self.active_consumers[consumer_name] = {start: stop}
173
-
174
- # Update interval_ts to the stop time for the next consumer
175
- interval_ts = stop
188
+ for consumer in self.consumers.values():
189
+ consumer.start = interval_ts
190
+ consumer.stop = interval_ts + secs_per_consumer
191
+ interval_ts += secs_per_consumer
176
192
 
177
- def consider_net_energy_balance(self, unit: str, ts: float) -> bool:
178
- """Check if there is enough energy available for the consumer to heat
179
- the water in the remaining time within the balancing interval.
193
+ def publish_energybalance(self, ts: float) -> None:
194
+ """Publish diagnostics and status.
180
195
 
181
196
  Args:
182
- unit (str): name of the consumer
183
- ts (float): current time
197
+ ts (float): current time.
184
198
 
185
199
  Returns:
186
- bool: true if the given consumer is active
200
+ None
187
201
  """
188
- # Check if the consumer exists in the active_consumers dictionary
189
- if unit not in self.active_consumers:
190
- return False # The consumer is not found, so return False
191
202
 
192
- # Get the start and stop time dictionary for the consumer
193
- consumer_times = self.active_consumers[unit]
203
+ # publish diagnostics
204
+ m: dict[str, Any] = {
205
+ "EnergyBalancer": self.name,
206
+ "CurrentBalance": self.net_energy_balance,
207
+ "NeededBalance": self.needed_energy,
208
+ "Timestamp": ts,
209
+ }
210
+ self.publish(self.topic_out_diagnostics, json.dumps(m))
194
211
 
195
- # map the current time to the balancing interval time slot
196
- interval_ts: float = ts % self.energy_balancing_interval
212
+ # publish consumer statuses to control consumers
213
+ num_consumers: int = len(self.consumers)
214
+ if num_consumers == 0:
215
+ return # If there are no consumers, we simply do nothing
216
+ interval_ts = ts % self.energy_balancing_interval
217
+ for consumer in self.consumers.values():
218
+ m = {
219
+ "EnergyBalancer": self.name,
220
+ "Unit": consumer.name,
221
+ "Power": consumer.power,
222
+ "Mode": consumer.start <= interval_ts < consumer.stop,
223
+ "Timestamp": ts,
224
+ }
225
+ self.publish(self.topic_out_status, json.dumps(m))
226
+
227
+ def detect_consumer_status(self, name: str, ts: float) -> bool:
228
+ """Detect consumer status
197
229
 
198
- # Check if current time (ts) is within the active range
199
- for start_ts, stop_ts in consumer_times.items():
200
- if start_ts <= interval_ts < stop_ts:
201
- return True # If the current time is within the range, the consumer is active
230
+ Args:
231
+ name (str): name of the consumer
232
+ ts (float): current time.
202
233
 
203
- return False # If no matching time range was found, return False
234
+ Returns:
235
+ True if the consumer is active, False otherwise
236
+ """
237
+ consumer: Consumer = self.consumers[name]
238
+ interval_ts = ts % self.energy_balancing_interval
239
+ return consumer.start <= interval_ts < consumer.stop
204
240
 
205
241
  def reset_net_energy_balance(self, interval_ts: float) -> None:
206
242
  """Reset the net energy balance at the end of the interval."""
@@ -208,7 +244,10 @@ class EnergyBalancer(Juham):
208
244
  self.current_interval_ts = interval_ts
209
245
  self.needed_energy = self.calculate_needed_energy(interval_ts)
210
246
  self.net_energy_balancing_mode = False
211
- self.active_consumers.clear() # Clear the active consumers at the end of the interval
247
+ for consumer in self.consumers.values():
248
+ consumer.start = 0.0
249
+ consumer.stop = 0.0
250
+
212
251
  self.info("Energy balance reset, interval ended")
213
252
 
214
253
  def activate_balancing_mode(self, ts: float) -> None:
@@ -223,21 +262,3 @@ class EnergyBalancer(Juham):
223
262
  self.net_energy_balancing_mode = False
224
263
  self.info("Balance used, or the end of the interval reached, disable")
225
264
  self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
226
-
227
- def publish_energybalance(self, ts: float) -> None:
228
- """Publish energy balance information.
229
-
230
- Args:
231
- ts (float): current time
232
- Returns:
233
- dict: diagnostics information
234
- """
235
- m: dict[str, Any] = {
236
- "Unit": self.name,
237
- "Mode": self.net_energy_balancing_mode,
238
- "Rc": self.net_energy_balancing_mode,
239
- "CurrentBalance": self.net_energy_balance,
240
- "NeededBalance": self.needed_energy,
241
- "Timestamp": ts,
242
- }
243
- self.publish(self.topic_out_energybalance, json.dumps(m))
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  import json
2
3
  from typing import Any
3
4
  from typing_extensions import override
@@ -16,31 +17,63 @@ from juham_core.timeutils import (
16
17
 
17
18
  class HeatingOptimizer(Juham):
18
19
  """Automation class for optimized control of temperature driven home energy consumers e.g hot
19
- water radiators. Reads spot prices, electricity forecast, power meter and temperatures to minimize electricity bill.
20
+ water radiators. Reads spot prices, solar electricity forecast, power meter and
21
+ temperature to minimize electricity bill.
20
22
 
21
23
  Represents a heating system that knows the power rating of its radiator (e.g., 3kW).
24
+ Any number of heating devices can be controlled, each with its own temperature, schedule and electricity price ratings
25
+
22
26
  The system subscribes to the 'power' topic to track the current power balance. If the solar panels
23
27
  generate more energy than is being consumed, the optimizer activates a relay to ensure that all excess energy
24
- produced within that hour is used for heating. The goal is to achieve a net zero energy balance for each hour,
28
+ produced within that balancing interval is used for heating. The goal is to achieve a net zero energy balance for each hour,
25
29
  ensuring that any surplus energy from the solar panels is fully utilized.
26
30
 
27
31
  Computes also UOI - optimization utilization index for each hour, based on the spot price and the solar power forecast.
28
32
  For negative energy balance this determines when energy is consumed. Value of 0 means the hour is expensive, value of 1 means
29
33
  the hour is free. The UOI threshold determines the slots that are allowed to be consumed.
30
-
31
34
  """
32
35
 
33
- maximum_boiler_temperature: float = 70
34
- minimum_boiler_temperature: float = 40
35
36
  energy_balancing_interval: float = 3600
36
- radiator_power: float = 6000 #
37
- operation_threshold: float = 5 * 60
37
+ """Energy balancing interval, as regulated by the industry/converment. In seconds"""
38
+
39
+ radiator_power: float = 6000 # W
40
+ """Radiator power in Watts. This is the maximum power that the radiator can consume."""
41
+
38
42
  heating_hours_per_day: float = 4
43
+ """ Number of hours per day the radiator is allowed to heat."""
44
+
39
45
  schedule_start_hour: float = 0
46
+ """Start hour of the heating schedule."""
47
+
40
48
  schedule_stop_hour: float = 0
49
+ """Stop hour of the heating schedule. Heating is allowed only between these two hours."""
50
+
41
51
  timezone: str = "Europe/Helsinki"
52
+ """ Timezone of the heating system. This is used to convert UTC timestamps to local time."""
53
+
42
54
  expected_average_price: float = 0.2
55
+ """Expected average price of electricity, beyond which the heating is avoided."""
56
+
43
57
  uoi_threshold: float = 0.8
58
+ """Utilization Optimization Index threshold. This is the minimum UOI value that is allowed for the heating to be activated."""
59
+
60
+ temperature_limits: dict[int, tuple[float, float]] = {
61
+ 1: (20.0, 60.0), # January
62
+ 2: (20.0, 60.0), # February
63
+ 3: (20.0, 60.0), # March
64
+ 4: (20.0, 50.0), # April
65
+ 5: (20.0, 40.0), # May
66
+ 6: (20.0, 22.0), # June
67
+ 7: (20.0, 22.0), # July
68
+ 8: (20.0, 22.0), # August
69
+ 9: (20.0, 50.0), # September
70
+ 10: (20.0, 60.0), # October
71
+ 11: (20.0, 60.0), # November
72
+ 12: (20.0, 60.0), # December
73
+ }
74
+ """Temperature limits for each month. The minimum temperature is maintained regardless of the cost.
75
+ The limits are defined as a dictionary where the keys are month numbers (1-12)
76
+ and the values are tuples of (min_temp, max_temp). The min_temp and max_temp values are in degrees Celsius."""
44
77
 
45
78
  def __init__(
46
79
  self,
@@ -84,9 +117,9 @@ class HeatingOptimizer(Juham):
84
117
  self.start_hour = start_hour
85
118
  self.spot_limit = spot_limit
86
119
 
87
- self.topic_spot = self.make_topic_name("spot")
88
- self.topic_forecast = self.make_topic_name("forecast")
89
- self.topic_temperature = self.make_topic_name(temperature_sensor)
120
+ self.topic_in_spot = self.make_topic_name("spot")
121
+ self.topic_in_forecast = self.make_topic_name("forecast")
122
+ self.topic_in_temperature = self.make_topic_name(temperature_sensor)
90
123
  self.topic_powerplan = self.make_topic_name("powerplan")
91
124
  self.topic_in_energybalance = self.make_topic_name("energybalance/status")
92
125
  self.topic_out_energybalance = self.make_topic_name("energybalance/consumers")
@@ -107,9 +140,9 @@ class HeatingOptimizer(Juham):
107
140
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
108
141
  super().on_connect(client, userdata, flags, rc)
109
142
  if rc == 0:
110
- self.subscribe(self.topic_spot)
111
- self.subscribe(self.topic_forecast)
112
- self.subscribe(self.topic_temperature)
143
+ self.subscribe(self.topic_in_spot)
144
+ self.subscribe(self.topic_in_forecast)
145
+ self.subscribe(self.topic_in_temperature)
113
146
  self.subscribe(self.topic_in_energybalance)
114
147
  self.register_as_consumer()
115
148
 
@@ -122,7 +155,22 @@ class HeatingOptimizer(Juham):
122
155
  "Power": self.radiator_power,
123
156
  }
124
157
  self.publish(self.topic_out_energybalance, json.dumps(consumer), 1, False)
125
- self.info(f"Registered {self.name} as consumer with {self.radiator_power}W", "")
158
+ self.info(
159
+ f"Registered {self.name} as consumer with {self.radiator_power}W power",
160
+ "",
161
+ )
162
+
163
+ # Function to get the temperature limits based on the current month
164
+ def get_temperature_limits_for_current_month(self) -> tuple[float, float]:
165
+ """Get the temperature limits for the current month.
166
+ The limits are defined in a dictionary where the keys are month numbers (1-12)
167
+ and the values are tuples of (min_temp, max_temp).
168
+ Returns: tuple: (min_temp, max_temp)
169
+ """
170
+ current_month: int = datetime.now().month
171
+ # Get the min and max temperatures for the current month
172
+ min_temp, max_temp = self.temperature_limits[current_month]
173
+ return min_temp, max_temp
126
174
 
127
175
  def sort_by_rank(
128
176
  self, hours: list[dict[str, Any]], ts_utc_now: float
@@ -170,7 +218,7 @@ class HeatingOptimizer(Juham):
170
218
  reverse=True,
171
219
  )
172
220
  self.debug(
173
- f"Sorted {len(sh)} days of forecast starting at {timestampstr(ts_utc)}"
221
+ f"{self.name} sorted {len(sh)} days of forecast starting at {timestampstr(ts_utc)}"
174
222
  )
175
223
  ranked_hours: list[dict[str, Any]] = []
176
224
 
@@ -178,7 +226,9 @@ class HeatingOptimizer(Juham):
178
226
  utc_ts: float = float(h["ts"])
179
227
  if utc_ts >= ts_utc:
180
228
  ranked_hours.append(h)
181
- self.debug(f"Forecast sorted for the next {str(len(ranked_hours))} hours")
229
+ self.debug(
230
+ f"{self.name} forecast sorted for the next {str(len(ranked_hours))} hours"
231
+ )
182
232
  return ranked_hours
183
233
 
184
234
  def on_spot(self, m: list[dict[str, Any]], ts_quantized: float) -> None:
@@ -206,7 +256,7 @@ class HeatingOptimizer(Juham):
206
256
 
207
257
  self.ranked_solarpower = self.sort_by_power(forecast, ts_utc_quantized)
208
258
  self.debug(
209
- f"Solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
259
+ f"{self.name} solar energy forecast received and ranked for {len(self.ranked_solarpower)} hours"
210
260
  )
211
261
  self.power_plan = [] # reset power plan, it depends on forecast
212
262
 
@@ -215,13 +265,13 @@ class HeatingOptimizer(Juham):
215
265
  m = None
216
266
  ts: float = timestamp()
217
267
  ts_utc_quantized: float = quantize(3600, ts - 3600)
218
- if msg.topic == self.topic_spot:
268
+ if msg.topic == self.topic_in_spot:
219
269
  self.on_spot(json.loads(msg.payload.decode()), ts_utc_quantized)
220
270
  return
221
- elif msg.topic == self.topic_forecast:
271
+ elif msg.topic == self.topic_in_forecast:
222
272
  self.on_forecast(json.loads(msg.payload.decode()), ts_utc_quantized)
223
273
  return
224
- elif msg.topic == self.topic_temperature:
274
+ elif msg.topic == self.topic_in_temperature:
225
275
  m = json.loads(msg.payload.decode())
226
276
  self.current_temperature = m["temperature"]
227
277
  elif msg.topic == self.topic_in_energybalance:
@@ -250,24 +300,24 @@ class HeatingOptimizer(Juham):
250
300
  self.relay_started_ts = ts_utc_now
251
301
 
252
302
  if not self.ranked_spot_prices:
253
- self.debug("Waiting spot prices...", "")
303
+ self.debug("{self.name} waiting spot prices...", "")
254
304
  return
255
305
 
256
306
  if not self.power_plan:
257
307
  self.power_plan = self.create_power_plan()
258
308
  self.heating_plan = []
259
309
  self.info(
260
- f"Power plan of length {len(self.power_plan)} created",
310
+ f"{self.name} power plan of length {len(self.power_plan)} created",
261
311
  str(self.power_plan),
262
312
  )
263
313
 
264
314
  if not self.power_plan:
265
- self.error("Failed to create a power plan", "")
315
+ self.error("{self.name} failed to create a power plan", "")
266
316
  return
267
317
 
268
318
  if len(self.power_plan) < 3:
269
319
  self.warning(
270
- f"Suspiciously short {len(self.power_plan)} power plan, wait more data ..",
320
+ f"{self.name} has suspiciously short {len(self.power_plan)} power plan, waiting for more data ..",
271
321
  "",
272
322
  )
273
323
  self.heating_plan = []
@@ -276,21 +326,25 @@ class HeatingOptimizer(Juham):
276
326
 
277
327
  if not self.ranked_solarpower or len(self.ranked_solarpower) < 4:
278
328
  self.warning(
279
- f"Short of forecast {len(self.ranked_solarpower)}, optimization compromised..",
329
+ f"{self.name} short of forecast {len(self.ranked_solarpower)}, optimization compromised..",
280
330
  "",
281
331
  )
282
332
 
283
333
  if not self.heating_plan:
284
334
  self.heating_plan = self.create_heating_plan()
285
335
  if not self.heating_plan:
286
- self.error("Failed to create heating plan")
336
+ self.error("{self.name} failed to create heating plan")
287
337
  return
288
338
  else:
289
339
  self.info(
290
- f"Heating plan of length {len(self.heating_plan)} created", ""
340
+ f"{self.name} heating plan of length {len(self.heating_plan)} created",
341
+ "",
291
342
  )
292
343
  if len(self.heating_plan) < 3:
293
- self.info(f"Short heating plan {len(self.heating_plan)}, no can do", "")
344
+ self.warning(
345
+ f"{self.name} has too short heating plan {len(self.heating_plan)}, no can do",
346
+ "",
347
+ )
294
348
  self.heating_plan = []
295
349
  self.power_plan = []
296
350
  return
@@ -304,7 +358,7 @@ class HeatingOptimizer(Juham):
304
358
  }
305
359
  self.publish(self.topic_out_power, json.dumps(heat), 1, False)
306
360
  self.info(
307
- f"Relay state {self.name} changed to {relay} at {timestampstr(ts_utc_now)}",
361
+ f"{self.name} relay changed to {relay} at {timestampstr(ts_utc_now)}",
308
362
  "",
309
363
  )
310
364
  self.current_relay_state = relay
@@ -321,9 +375,6 @@ class HeatingOptimizer(Juham):
321
375
  """
322
376
  if m["Unit"] == self.name:
323
377
  self.net_energy_balance_mode = m["Mode"]
324
- self.info(
325
- "Net energy balance mode for {self.name} set to {m['Mode']}", str(m)
326
- )
327
378
 
328
379
  def consider_heating(self, ts: float) -> int:
329
380
  """Consider whether the target boiler needs heating. Check first if the solar
@@ -339,28 +390,45 @@ class HeatingOptimizer(Juham):
339
390
 
340
391
  # check if we have excess energy to spent within the current slot
341
392
  if self.net_energy_balance_mode:
342
- self.info("Positive net energy balance, spend it for heating")
393
+ self.debug(
394
+ "{self.name} with positive net energy balance, spend it for heating"
395
+ )
343
396
  return 1
344
397
 
345
- # no free energy available, don't spend if the current temperature is already high enough
346
- if self.current_temperature > self.maximum_boiler_temperature:
347
- self.info(
348
- f"Current temperature {self.current_temperature}C already beyond max {self.maximum_boiler_temperature}C"
349
- )
350
- return 0
351
398
  hour = timestamp_hour(ts)
399
+ state: int = -1
352
400
 
353
401
  # check if we are within the heating plan and see what the plan says
354
402
  for pp in self.heating_plan:
355
403
  ppts: float = pp["Timestamp"]
356
404
  h: float = timestamp_hour(ppts)
357
405
  if h == hour:
358
- return pp["State"]
406
+ state = pp["State"]
407
+ break
408
+
409
+ if state == -1:
410
+ self.error(f"{self.name} cannot find heating plan for hour {hour}")
411
+ return 0
412
+
413
+ min_temp, max_temp = self.get_temperature_limits_for_current_month()
414
+ self.debug(
415
+ f"{self.name} month's temperature limits: Min = {min_temp}°C, Max = {max_temp}°C"
416
+ )
417
+
418
+ # don't heat if the current temperature is already high enough
419
+ if self.current_temperature > max_temp:
420
+ self.debug(
421
+ f"{self.name} plan {state}, temp {self.current_temperature}°C already beyond max {max_temp}°C"
422
+ )
423
+ return 0
424
+ # heat if the current temperature is below the required minimum
425
+ if self.current_temperature < min_temp:
426
+ self.debug(
427
+ f"{self.name} plan {state}, temp {self.current_temperature}°C below min {min_temp}°C"
428
+ )
429
+ return 1
359
430
 
360
- # if we are not within the heating plan, then we are not heating
361
- # this should not happen, but just in case
362
- self.error(f"Cannot find heating plan for hour {hour}")
363
- return 0
431
+ return state # 1 = heating, 0 = not heating
364
432
 
365
433
  # compute utilization optimization index
366
434
  def compute_uoi(
@@ -427,7 +495,7 @@ class HeatingOptimizer(Juham):
427
495
  ts_utc_quantized = quantize(3600, timestamp() - 3600)
428
496
  starts: str = timestampstr(ts_utc_quantized)
429
497
  self.info(
430
- f"Trying to create power plan starting at {starts} with {len(self.ranked_spot_prices)} hourly spot prices",
498
+ f"{self.name} created power plan starting at {starts} with {len(self.ranked_spot_prices)} hours of spot prices",
431
499
  "",
432
500
  )
433
501
 
@@ -440,10 +508,11 @@ class HeatingOptimizer(Juham):
440
508
  )
441
509
 
442
510
  if len(spots) == 0:
443
- self.debug(
511
+ self.info(
444
512
  f"No spot prices initialized yet, can't proceed",
445
513
  "",
446
514
  )
515
+ return []
447
516
  self.info(
448
517
  f"Have spot prices for the next {len(spots)} hours",
449
518
  "",
@@ -499,7 +568,7 @@ class HeatingOptimizer(Juham):
499
568
 
500
569
  shplan = sorted(hplan, key=lambda x: x["FOM"], reverse=True)
501
570
 
502
- self.debug(f"Powerplan starts {starts} up to {len(shplan)} hours")
571
+ self.debug(f"{self.name} powerplan starts {starts} up to {len(shplan)} hours")
503
572
  return shplan
504
573
 
505
574
  def enable_relay(
@@ -549,5 +618,5 @@ class HeatingOptimizer(Juham):
549
618
  heating_plan.append(heat)
550
619
  hour = hour + 1
551
620
 
552
- self.info(f"Heating plan of {len(heating_plan)} hours created", "")
621
+ self.info(f"{self.name} heating plan of {len(heating_plan)} hours created", "")
553
622
  return heating_plan
@@ -9,7 +9,7 @@ from juham_core.timeutils import epoc2utc
9
9
 
10
10
 
11
11
  class EnergyBalancerTs(JuhamTs):
12
- """Heating optimizer diagnosis.
12
+ """Record energy balance data to time series database.
13
13
 
14
14
  This class listens the "energybalance" MQTT topic and records the
15
15
  messages to time series database.
@@ -19,27 +19,53 @@ class EnergyBalancerTs(JuhamTs):
19
19
  """Construct record object with the given name."""
20
20
 
21
21
  super().__init__(name)
22
- self.topic_name = self.make_topic_name("energybalance")
22
+ self.topic_in_status = self.make_topic_name("energybalance/status")
23
+ self.topic_in_diagnostics = self.make_topic_name("energybalance/diagnostics")
23
24
 
24
25
  @override
25
26
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
26
27
  super().on_connect(client, userdata, flags, rc)
27
- self.subscribe(self.topic_name)
28
- self.debug(f"Subscribed to {self.topic_name}")
28
+ if rc == 0:
29
+ self.subscribe(self.topic_in_status)
30
+ self.subscribe(self.topic_in_diagnostics)
29
31
 
30
32
  @override
31
33
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
32
- """Standard mqtt message notification method.
34
+ if msg.topic == self.topic_in_status:
35
+ self.on_status(json.loads(msg.payload.decode()))
36
+ elif msg.topic == self.topic_in_diagnostics:
37
+ self.on_diagnostics(json.loads(msg.payload.decode()))
38
+ else:
39
+ super().on_message(client, userdata, msg)
33
40
 
34
- This method is called upon new arrived message.
41
+ def on_status(self, m: dict[str, Any]) -> None:
42
+ """Handle energybalance message.
43
+ Args:
44
+ m (dict[str, Any]): Message from energybalance topic.
35
45
  """
36
-
37
- m = json.loads(msg.payload.decode())
46
+ if not "Power" in m or not "Timestamp" in m:
47
+ self.error(f"INVALID STATUS msg {m}")
48
+ return
38
49
  point = (
39
50
  self.measurement("energybalance")
40
51
  .tag("Unit", m["Unit"])
41
52
  .field("Mode", m["Mode"])
42
- .field("Rc", m["Rc"])
53
+ .field("Power", float(m["Power"]))
54
+ .time(epoc2utc(m["Timestamp"]))
55
+ )
56
+ self.write(point)
57
+
58
+ def on_diagnostics(self, m: dict[str, Any]) -> None:
59
+ """Handle energybalance diagnostics.
60
+ Args:
61
+ m (dict[str, Any]): Message from energybalance topic.
62
+ """
63
+ if not "Timestamp" in m:
64
+ self.error(f"INVALID DIAGNOSTICS msg {m}")
65
+ return
66
+ point = (
67
+ self.measurement("energybalance")
68
+ .tag("EnergyBalancer", m["EnergyBalancer"])
43
69
  .field("CurrentBalance", m["CurrentBalance"])
44
70
  .field("NeededBalance", m["NeededBalance"])
45
71
  .time(epoc2utc(m["Timestamp"]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.30
3
+ Version: 0.0.33
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>
@@ -2,24 +2,24 @@ juham_automation/__init__.py,sha256=32BL36bhT7OaSw22H7st-7-3IXcFM2Pf5js80hNA8W0,
2
2
  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
- juham_automation/automation/energybalancer.py,sha256=HcBTCI4eS429Up2-lx5zkAZQmZGn2ZCVTwGCavsjGg4,10722
5
+ juham_automation/automation/energybalancer.py,sha256=2WnnajZdZZjHqUT9onCQCdkAIy_1nom8-y3u_pv2GM0,10844
6
6
  juham_automation/automation/energycostcalculator.py,sha256=v30wxRpuY2gGBSMJifrFRTjsRU9t-iCiq33Vds7s3O8,10877
7
- juham_automation/automation/heatingoptimizer.py,sha256=V56gZEsjJOf4veZCZYT1JUv8Tv8lmZ-65zLEb6T7q8s,21488
7
+ juham_automation/automation/heatingoptimizer.py,sha256=DU-VvEHEZAj3C6CkT6K-7f-LhkAXRBYdf6RoxKbgydc,24412
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
11
11
  juham_automation/ts/__init__.py,sha256=kTEzVkDi6ednH4-fxKxrY6enlTuTXmSw09pPAQX3CMc,612
12
12
  juham_automation/ts/electricityprice_ts.py,sha256=BYs120V4teVjSqPc8PpPDjOTc5dOrVM9Maqse7E8cvk,1684
13
- juham_automation/ts/energybalancer_ts.py,sha256=KpMYnyzAKghPEMHAVHrh_aplorn9TeSvZr-DVpfir3c,1476
13
+ juham_automation/ts/energybalancer_ts.py,sha256=XHl56G5fBjJOCSIJtdjzGpSTMAQN1xsnXpC4Ipl7ynw,2585
14
14
  juham_automation/ts/energycostcalculator_ts.py,sha256=MbeYEGlziVgq4zI40Tk71zxeDPeKafEG3s0LqDRiz0g,1277
15
15
  juham_automation/ts/forecast_ts.py,sha256=Gk46hIlS8ijxs-zyy8fBvXrhI7J-8e5Gt2QEe6gFB6s,3158
16
16
  juham_automation/ts/log_ts.py,sha256=XsNaazuPmRUZLUqxU0DZae_frtT6kAFcXJTc598CtOA,1750
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.0.30.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
- juham_automation-0.0.30.dist-info/METADATA,sha256=Be-9u_ReioR9XS4Z4U8jtaQBxrSbdGfoJPQHx9OkaXg,6837
22
- juham_automation-0.0.30.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
23
- juham_automation-0.0.30.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
- juham_automation-0.0.30.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
- juham_automation-0.0.30.dist-info/RECORD,,
20
+ juham_automation-0.0.33.dist-info/licenses/LICENSE.rst,sha256=QVHD5V5_HSys2PdPdig_xKggDj8cGX33ALKqRsYyjtI,1089
21
+ juham_automation-0.0.33.dist-info/METADATA,sha256=paKzowab6A0iDnu5qduBVXOWofockK1LhXp2KzcSnX4,6837
22
+ juham_automation-0.0.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ juham_automation-0.0.33.dist-info/entry_points.txt,sha256=h-KzuKjmGPd4_iX_oiGvxx4IEc97dVbGGlhdh5ctbpI,605
24
+ juham_automation-0.0.33.dist-info/top_level.txt,sha256=jfohvtocvX_gfT21AhJk7Iay5ZiQsS3HzrDjF7S4Qp0,17
25
+ juham_automation-0.0.33.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5