juham-automation 0.0.29__tar.gz → 0.0.31__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 (48) hide show
  1. {juham_automation-0.0.29/juham_automation.egg-info → juham_automation-0.0.31}/PKG-INFO +1 -1
  2. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/automation/energybalancer.py +86 -59
  3. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/automation/heatingoptimizer.py +3 -5
  4. juham_automation-0.0.31/juham_automation/ts/energybalancer_ts.py +73 -0
  5. {juham_automation-0.0.29 → juham_automation-0.0.31/juham_automation.egg-info}/PKG-INFO +1 -1
  6. {juham_automation-0.0.29 → juham_automation-0.0.31}/pyproject.toml +1 -1
  7. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/automation/test_energybalancer.py +2 -2
  8. juham_automation-0.0.29/juham_automation/ts/energybalancer_ts.py +0 -47
  9. {juham_automation-0.0.29 → juham_automation-0.0.31}/LICENSE.rst +0 -0
  10. {juham_automation-0.0.29 → juham_automation-0.0.31}/MANIFEST.in +0 -0
  11. {juham_automation-0.0.29 → juham_automation-0.0.31}/README.rst +0 -0
  12. {juham_automation-0.0.29 → juham_automation-0.0.31}/examples/myapp.py +0 -0
  13. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/__init__.py +0 -0
  14. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/automation/__init__.py +0 -0
  15. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/automation/energycostcalculator.py +0 -0
  16. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/automation/powermeter_simulator.py +0 -0
  17. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/automation/spothintafi.py +0 -0
  18. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/automation/watercirculator.py +0 -0
  19. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/japp.py +0 -0
  20. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/py.typed +0 -0
  21. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/__init__.py +0 -0
  22. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/electricityprice_ts.py +0 -0
  23. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/energycostcalculator_ts.py +0 -0
  24. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/forecast_ts.py +0 -0
  25. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/log_ts.py +0 -0
  26. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/power_ts.py +0 -0
  27. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/powermeter_ts.py +0 -0
  28. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation/ts/powerplan_ts.py +0 -0
  29. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation.egg-info/SOURCES.txt +0 -0
  30. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation.egg-info/dependency_links.txt +0 -0
  31. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation.egg-info/entry_points.txt +0 -0
  32. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation.egg-info/requires.txt +0 -0
  33. {juham_automation-0.0.29 → juham_automation-0.0.31}/juham_automation.egg-info/top_level.txt +0 -0
  34. {juham_automation-0.0.29 → juham_automation-0.0.31}/setup.cfg +0 -0
  35. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/__init__.py +0 -0
  36. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/automation/__init__.py +0 -0
  37. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/automation/test_energycostcalculator.py +0 -0
  38. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/automation/test_heatingoptimizer.py +0 -0
  39. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/automation/test_juham.py +0 -0
  40. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/automation/test_spothintafi.py +0 -0
  41. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/test_japp.py +0 -0
  42. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/ts/__init__.py +0 -0
  43. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/ts/test_energycostcalculator_ts.py +0 -0
  44. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/ts/test_forecast_ts.py +0 -0
  45. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/ts/test_log_ts.py +0 -0
  46. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/ts/test_power_ts.py +0 -0
  47. {juham_automation-0.0.29 → juham_automation-0.0.31}/tests/ts/test_powermeter_ts.py +0 -0
  48. {juham_automation-0.0.29 → juham_automation-0.0.31}/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.0.29
3
+ Version: 0.0.31
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>
@@ -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) -> 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
@@ -34,28 +57,29 @@ class EnergyBalancer(Juham):
34
57
  """
35
58
  super().__init__(name)
36
59
 
37
- self.topic_in_consumers = self.make_topic_name("energybalance_consumers")
38
- self.topic_in_net_energy_balance = self.make_topic_name("net_energy_balance")
39
- self.topic_out_energybalance = self.make_topic_name("energybalance")
60
+ self.topic_in_consumers = self.make_topic_name("energybalance/consumers")
61
+ self.topic_out_status = self.make_topic_name("energybalance/status")
62
+ self.topic_out_diagnostics = self.make_topic_name("energybalance/diagnostics")
63
+ self.topic_in_power = self.make_topic_name("net_energy_balance")
64
+
40
65
  self.net_energy_balance: float = 0.0 # Energy balance in joules (watt-seconds)
41
66
  self.current_interval_ts: float = -1
42
67
  self.needed_energy: float = 0.0 # Energy needed in joules (watt-seconds)
43
68
  self.net_energy_balancing_mode: bool = False
44
- self.consumers: dict[str, float] = {}
45
- self.active_consumers: dict[str, dict[float, float]] = {}
69
+ self.consumers: dict[str, Consumer] = {}
46
70
 
47
71
  @override
48
72
  def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
49
73
  super().on_connect(client, userdata, flags, rc)
50
74
  if rc == 0:
51
- self.subscribe(self.topic_in_net_energy_balance)
75
+ self.subscribe(self.topic_in_power)
52
76
  self.subscribe(self.topic_in_consumers)
53
77
 
54
78
  @override
55
79
  def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
56
80
  ts: float = timestamp()
57
81
 
58
- if msg.topic == self.topic_in_net_energy_balance:
82
+ if msg.topic == self.topic_in_power:
59
83
  self.on_power(json.loads(msg.payload.decode()), ts)
60
84
  elif msg.topic == self.topic_in_consumers:
61
85
  self.on_consumer(json.loads(msg.payload.decode()))
@@ -78,7 +102,7 @@ class EnergyBalancer(Juham):
78
102
  m (dict[str, Any]): power consumer message
79
103
  ts (float): current time
80
104
  """
81
- self.consumers[m["Unit"]] = m["Power"]
105
+ self.consumers[m["Unit"]] = Consumer(m["Unit"], m["Power"])
82
106
  self.info(f"Consumer {m['Unit']} added, power: {m['Power']}")
83
107
 
84
108
  def update_energy_balance(self, power: float, ts: float) -> None:
@@ -97,6 +121,7 @@ class EnergyBalancer(Juham):
97
121
  """
98
122
 
99
123
  # regardless of the mode, if we hit the end of the interval, reset the balance
124
+
100
125
  interval_ts: float = ts % self.energy_balancing_interval
101
126
  if self.current_interval_ts < 0 or interval_ts <= self.current_interval_ts:
102
127
  # time runs backwards, must be a new interval
@@ -117,7 +142,7 @@ class EnergyBalancer(Juham):
117
142
  # if we have enough energy to power the radiator for the rest of the time slot
118
143
  if self.net_energy_balance >= self.needed_energy:
119
144
  self.net_energy_balancing_mode = True
120
- self.initialize_active_consumers(ts)
145
+ self.initialize_consumer_timelines(ts)
121
146
  self.publish_energybalance(ts)
122
147
 
123
148
  def calculate_needed_energy(self, interval_ts: float) -> float:
@@ -131,16 +156,19 @@ class EnergyBalancer(Juham):
131
156
  Returns:
132
157
  float: energy needed in joules
133
158
  """
159
+
134
160
  required_power: float = 0.0
135
161
  num_consumers: int = len(self.consumers)
162
+ if num_consumers == 0:
163
+ return 0.0
136
164
  remaining_ts_consumer: float = (
137
165
  self.energy_balancing_interval - interval_ts
138
166
  ) / num_consumers
139
167
  for consumer in self.consumers.values():
140
- required_power += consumer * remaining_ts_consumer
168
+ required_power += consumer.power * remaining_ts_consumer
141
169
  return required_power
142
170
 
143
- def initialize_active_consumers(self, ts: float) -> None:
171
+ def initialize_consumer_timelines(self, ts: float) -> None:
144
172
  """Initialize the list of active consumers with their start and stop times.
145
173
 
146
174
  Args:
@@ -157,46 +185,59 @@ class EnergyBalancer(Juham):
157
185
  self.energy_balancing_interval - interval_ts
158
186
  ) / num_consumers
159
187
 
160
- # Reset the active consumers dictionary
161
- self.active_consumers.clear()
162
-
163
- for consumer_name, consumer_data in self.consumers.items():
164
- start: float = interval_ts
165
- stop: float = start + secs_per_consumer
166
-
167
- # Add the consumer to the active consumers dictionary with its start and stop times
168
- self.active_consumers[consumer_name] = {start: stop}
169
-
170
- # Update interval_ts to the stop time for the next consumer
171
- 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
172
192
 
173
- def consider_net_energy_balance(self, unit: str, ts: float) -> bool:
174
- """Check if there is enough energy available for the consumer to heat
175
- the water in the remaining time within the balancing interval.
193
+ def publish_energybalance(self, ts: float) -> None:
194
+ """Publish diagnostics and status.
176
195
 
177
196
  Args:
178
- unit (str): name of the consumer
179
- ts (float): current time
197
+ ts (float): current time.
180
198
 
181
199
  Returns:
182
- bool: true if the given consumer is active
200
+ None
183
201
  """
184
- # Check if the consumer exists in the active_consumers dictionary
185
- if unit not in self.active_consumers:
186
- return False # The consumer is not found, so return False
187
202
 
188
- # Get the start and stop time dictionary for the consumer
189
- 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))
211
+
190
212
 
191
- # map the current time to the balancing interval time slot
192
- interval_ts: float = ts % self.energy_balancing_interval
213
+ # publish consumer statuses to control consumers
214
+ num_consumers: int = len(self.consumers)
215
+ if num_consumers == 0:
216
+ return # If there are no consumers, we simply do nothing
217
+ interval_ts = ts % self.energy_balancing_interval
218
+ for consumer in self.consumers.values():
219
+ m: dict[str, Any] = {
220
+ "EnergyBalancer": self.name,
221
+ "Unit": consumer.name,
222
+ "Power": consumer.power,
223
+ "Mode": consumer.start <= interval_ts < consumer.stop,
224
+ "Timestamp": ts,
225
+ }
226
+ self.publish(self.topic_out_status, json.dumps(m))
227
+
228
+ def detect_consumer_status(self, name: str, ts: float) -> bool:
229
+ """Detect consumer status
193
230
 
194
- # Check if current time (ts) is within the active range
195
- for start_ts, stop_ts in consumer_times.items():
196
- if start_ts <= interval_ts < stop_ts:
197
- return True # If the current time is within the range, the consumer is active
231
+ Args:
232
+ name (str): name of the consumer
233
+ ts (float): current time.
198
234
 
199
- return False # If no matching time range was found, return False
235
+ Returns:
236
+ True if the consumer is active, False otherwise
237
+ """
238
+ consumer: Consumer = self.consumers[name]
239
+ interval_ts = ts % self.energy_balancing_interval
240
+ return consumer.start <= interval_ts < consumer.stop
200
241
 
201
242
  def reset_net_energy_balance(self, interval_ts: float) -> None:
202
243
  """Reset the net energy balance at the end of the interval."""
@@ -204,7 +245,10 @@ class EnergyBalancer(Juham):
204
245
  self.current_interval_ts = interval_ts
205
246
  self.needed_energy = self.calculate_needed_energy(interval_ts)
206
247
  self.net_energy_balancing_mode = False
207
- self.active_consumers.clear() # Clear the active consumers at the end of the interval
248
+ for consumer in self.consumers.values():
249
+ consumer.start = 0.0
250
+ consumer.stop = 0.0
251
+
208
252
  self.info("Energy balance reset, interval ended")
209
253
 
210
254
  def activate_balancing_mode(self, ts: float) -> None:
@@ -220,20 +264,3 @@ class EnergyBalancer(Juham):
220
264
  self.info("Balance used, or the end of the interval reached, disable")
221
265
  self.net_energy_balance = 0.0 # Reset the energy balance at the interval's end
222
266
 
223
- def publish_energybalance(self, ts: float) -> None:
224
- """Publish energy balance information.
225
-
226
- Args:
227
- ts (float): current time
228
- Returns:
229
- dict: diagnostics information
230
- """
231
- m: dict[str, Any] = {
232
- "Unit": self.name,
233
- "Mode": self.net_energy_balancing_mode,
234
- "Rc": self.net_energy_balancing_mode,
235
- "CurrentBalance": self.net_energy_balance,
236
- "NeededBalance": self.needed_energy,
237
- "Timestamp": ts,
238
- }
239
- self.publish(self.topic_out_energybalance, json.dumps(m))
@@ -88,8 +88,8 @@ class HeatingOptimizer(Juham):
88
88
  self.topic_forecast = self.make_topic_name("forecast")
89
89
  self.topic_temperature = self.make_topic_name(temperature_sensor)
90
90
  self.topic_powerplan = self.make_topic_name("powerplan")
91
- self.topic_in_energybalance = self.make_topic_name("energybalance")
92
- self.topic_out_energybalance = self.make_topic_name("energybalance_consumers")
91
+ self.topic_in_energybalance = self.make_topic_name("energybalance/status")
92
+ self.topic_out_energybalance = self.make_topic_name("energybalance/consumers")
93
93
  self.topic_out_power = self.make_topic_name("power")
94
94
 
95
95
  self.current_temperature = 100
@@ -321,9 +321,7 @@ class HeatingOptimizer(Juham):
321
321
  """
322
322
  if m["Unit"] == self.name:
323
323
  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
- )
324
+
327
325
 
328
326
  def consider_heating(self, ts: float) -> int:
329
327
  """Consider whether the target boiler needs heating. Check first if the solar
@@ -0,0 +1,73 @@
1
+ import json
2
+ from typing import Any
3
+ from typing_extensions import override
4
+
5
+ from masterpiece.mqtt import MqttMsg
6
+
7
+ from juham_core import JuhamTs
8
+ from juham_core.timeutils import epoc2utc
9
+
10
+
11
+ class EnergyBalancerTs(JuhamTs):
12
+ """Record energy balance data to time series database.
13
+
14
+ This class listens the "energybalance" MQTT topic and records the
15
+ messages to time series database.
16
+ """
17
+
18
+ def __init__(self, name: str = "energybalancer_ts") -> None:
19
+ """Construct record object with the given name."""
20
+
21
+ super().__init__(name)
22
+ self.topic_in_status = self.make_topic_name("energybalance/status")
23
+ self.topic_in_diagnostics = self.make_topic_name("energybalance/diagnostics")
24
+
25
+ @override
26
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
27
+ super().on_connect(client, userdata, flags, rc)
28
+ if rc == 0:
29
+ self.subscribe(self.topic_in_status)
30
+ self.subscribe(self.topic_in_diagnostics)
31
+
32
+ @override
33
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
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)
40
+
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.
45
+ """
46
+ if not "Power" in m or not "Timestamp" in m:
47
+ self.error(f"INVALID STATUS msg {m}")
48
+ return
49
+ point = (
50
+ self.measurement("energybalance")
51
+ .tag("Unit", m["Unit"])
52
+ .field("Mode", m["Mode"])
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"])
69
+ .field("CurrentBalance", m["CurrentBalance"])
70
+ .field("NeededBalance", m["NeededBalance"])
71
+ .time(epoc2utc(m["Timestamp"]))
72
+ )
73
+ self.write(point)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: juham-automation
3
- Version: 0.0.29
3
+ Version: 0.0.31
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>
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "juham-automation"
8
- version = "0.0.29"
8
+ version = "0.0.31"
9
9
  description = "Juha's Ultimate Home Automation Masterpiece"
10
10
  readme = {file = "README.rst", content-type = "text/markdown"}
11
11
  requires-python = ">=3.8"
@@ -82,8 +82,8 @@ class TestEnergyBalancing(unittest.TestCase):
82
82
  self.assertEqual(energy, self.optimizer.net_energy_balance)
83
83
 
84
84
  # Call the method to check if balancing mode should be activated
85
- main_on: bool = self.optimizer.consider_net_energy_balance("main", ts)
86
- sun_on: bool = self.optimizer.consider_net_energy_balance("sun", ts)
85
+ main_on: bool = self.optimizer.detect_consumer_status("main", ts)
86
+ sun_on: bool = self.optimizer.detect_consumer_status("sun", ts)
87
87
 
88
88
  if energy >= self.optimizer.needed_energy:
89
89
  self.assertTrue(main_on or sun_on, "One of the consumers must be ON")
@@ -1,47 +0,0 @@
1
- import json
2
- from typing import Any
3
- from typing_extensions import override
4
-
5
- from masterpiece.mqtt import MqttMsg
6
-
7
- from juham_core import JuhamTs
8
- from juham_core.timeutils import epoc2utc
9
-
10
-
11
- class EnergyBalancerTs(JuhamTs):
12
- """Heating optimizer diagnosis.
13
-
14
- This class listens the "energybalance" MQTT topic and records the
15
- messages to time series database.
16
- """
17
-
18
- def __init__(self, name: str = "energybalancer_ts") -> None:
19
- """Construct record object with the given name."""
20
-
21
- super().__init__(name)
22
- self.topic_name = self.make_topic_name("energybalance")
23
-
24
- @override
25
- def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
26
- super().on_connect(client, userdata, flags, rc)
27
- self.subscribe(self.topic_name)
28
- self.debug(f"Subscribed to {self.topic_name}")
29
-
30
- @override
31
- def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
32
- """Standard mqtt message notification method.
33
-
34
- This method is called upon new arrived message.
35
- """
36
-
37
- m = json.loads(msg.payload.decode())
38
- point = (
39
- self.measurement("energybalance")
40
- .tag("Unit", m["Unit"])
41
- .field("Mode", m["Mode"])
42
- .field("Rc", m["Rc"])
43
- .field("CurrentBalance", m["CurrentBalance"])
44
- .field("NeededBalance", m["NeededBalance"])
45
- .time(epoc2utc(m["Timestamp"]))
46
- )
47
- self.write(point)