juham-automation 0.0.17__py3-none-any.whl → 0.0.26__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.
Files changed (26) hide show
  1. juham_automation/__init__.py +42 -38
  2. juham_automation/automation/__init__.py +23 -21
  3. juham_automation/automation/energybalancer.py +158 -0
  4. juham_automation/automation/energycostcalculator.py +267 -266
  5. juham_automation/automation/{hotwateroptimizer.py → heatingoptimizer.py} +539 -581
  6. juham_automation/automation/powermeter_simulator.py +139 -139
  7. juham_automation/automation/spothintafi.py +140 -140
  8. juham_automation/automation/watercirculator.py +159 -159
  9. juham_automation/japp.py +53 -49
  10. juham_automation/ts/__init__.py +27 -25
  11. juham_automation/ts/electricityprice_ts.py +51 -51
  12. juham_automation/ts/energybalancer_ts.py +47 -0
  13. juham_automation/ts/energycostcalculator_ts.py +43 -43
  14. juham_automation/ts/forecast_ts.py +97 -97
  15. juham_automation/ts/log_ts.py +57 -57
  16. juham_automation/ts/power_ts.py +49 -49
  17. juham_automation/ts/powermeter_ts.py +67 -70
  18. juham_automation/ts/powerplan_ts.py +45 -45
  19. juham_automation-0.0.26.dist-info/METADATA +152 -0
  20. juham_automation-0.0.26.dist-info/RECORD +25 -0
  21. {juham_automation-0.0.17.dist-info → juham_automation-0.0.26.dist-info}/entry_points.txt +3 -1
  22. {juham_automation-0.0.17.dist-info → juham_automation-0.0.26.dist-info}/licenses/LICENSE.rst +25 -25
  23. juham_automation-0.0.17.dist-info/METADATA +0 -106
  24. juham_automation-0.0.17.dist-info/RECORD +0 -23
  25. {juham_automation-0.0.17.dist-info → juham_automation-0.0.26.dist-info}/WHEEL +0 -0
  26. {juham_automation-0.0.17.dist-info → juham_automation-0.0.26.dist-info}/top_level.txt +0 -0
@@ -1,139 +1,139 @@
1
- import json
2
- from typing import Any, Dict, cast
3
- from typing_extensions import override
4
-
5
- from masterpiece.mqtt import MqttMsg
6
- from juham_core import Juham
7
- from juham_core.timeutils import timestamp
8
- from juham_core import MasterPieceThread, JuhamThread
9
-
10
-
11
- class PowerMeterSimulatorThread(MasterPieceThread):
12
- """Thread simulating Energy Meter."""
13
-
14
- _power: float = 1000.0 # W
15
- _power_topic: str = "power"
16
- _interval: float = 10 # 10 seconds
17
-
18
- def __init__(self) -> None:
19
- """Construct a thread for publishing power data.
20
-
21
- Args:
22
- topic (str, optional): MQTT topic to post the sensor readings. Defaults to None.
23
- interval (float, optional): Interval specifying how often the sensor is read. Defaults to 60 seconds.
24
- """
25
- super().__init__(None)
26
- self.current_ts: float = timestamp()
27
-
28
- @classmethod
29
- def initialize(cls, power_topic: str, power: float, interval: float) -> None:
30
- """Initialize thread class attributes.
31
-
32
- Args:
33
- power_topic (str): topic to publish the energy meter readings
34
- power (float): power to be simulated, the default is 1kW
35
- interval (float): update interval, the default is 10s
36
- """
37
- cls._power = power
38
- cls._interval = interval
39
- cls._power_topic = power_topic
40
-
41
- @override
42
- def update_interval(self) -> float:
43
- return self._interval
44
-
45
- def publish_active_power(self, ts: float) -> None:
46
- """Publish the active power, also known as real power. This is that
47
- part of the power that can be converted to useful work.
48
-
49
- Args:
50
- ts (str): time stamp of the event
51
-
52
- """
53
- dt = ts - self.current_ts
54
- self.current_ts = ts
55
-
56
- msg = {
57
- "timestamp": ts,
58
- "real_a": self._power * dt,
59
- "real_b": self._power * dt,
60
- "real_c": self._power * dt,
61
- "real_total": 3 * self._power * dt,
62
- }
63
- self.publish(self._power_topic, json.dumps(msg), 1, True)
64
-
65
- @override
66
- def update(self) -> bool:
67
- super().update()
68
- self.publish_active_power(timestamp())
69
- return True
70
-
71
-
72
- class PowerMeterSimulator(JuhamThread):
73
- """Simulator energy meter sensor. Spawns a thread
74
- to simulate Shelly PM mqtt messages"""
75
-
76
- workerThreadId = PowerMeterSimulatorThread.get_class_id()
77
- update_interval: float = 10
78
- power: float = 1000.0
79
-
80
- _POWERMETERSIMULATOR: str = "_powermetersimulator"
81
-
82
- def __init__(
83
- self,
84
- name: str = "em",
85
- interval: float = 0,
86
- ) -> None:
87
- """Create energy meter simulator.
88
-
89
- Args:
90
- name (str, optional): Name of the object. Defaults to 'em'.
91
- topic (str, optional): MQTT topic to publish the energy meter reports. Defaults to None.
92
- interval (float, optional): interval between events, in seconds. Defaults to None.
93
- """
94
- super().__init__(name)
95
- self.update_ts: float = 0.0
96
- if interval > 0.0:
97
- self.update_interval = interval
98
- self.power_topic = self.make_topic_name("powerconsumption") # target topic
99
-
100
- @override
101
- def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
102
- if msg.topic == self.power_topic:
103
- em = json.loads(msg.payload.decode())
104
- self.on_sensor(em)
105
- else:
106
- super().on_message(client, userdata, msg)
107
-
108
- def on_sensor(self, em: dict[Any, Any]) -> None:
109
- """Handle data coming from the energy meter.
110
-
111
- Simply log the event to indicate the presense of simulated device.
112
- Args:
113
- em (dict): data from the sensor
114
- """
115
- self.debug(f"Simulated power meter sensor {em}")
116
-
117
- @override
118
- def run(self) -> None:
119
- PowerMeterSimulatorThread.initialize(
120
- self.power_topic, self.power, self.update_interval
121
- )
122
- self.worker = cast(
123
- PowerMeterSimulatorThread,
124
- Juham.instantiate(PowerMeterSimulatorThread.get_class_id()),
125
- )
126
- super().run()
127
-
128
- @override
129
- def to_dict(self) -> Dict[str, Any]:
130
- data: Dict[str, Any] = super().to_dict()
131
- data[self._POWERMETERSIMULATOR] = {"power_topic": self.power_topic}
132
- return data
133
-
134
- @override
135
- def from_dict(self, data: Dict[str, Any]) -> None:
136
- super().from_dict(data)
137
- if self._POWERMETERSIMULATOR in data:
138
- for key, value in data[self._POWERMETERSIMULATOR].items():
139
- setattr(self, key, value)
1
+ import json
2
+ from typing import Any, Dict, cast
3
+ from typing_extensions import override
4
+
5
+ from masterpiece.mqtt import MqttMsg
6
+ from juham_core import Juham
7
+ from juham_core.timeutils import timestamp
8
+ from juham_core import MasterPieceThread, JuhamThread
9
+
10
+
11
+ class PowerMeterSimulatorThread(MasterPieceThread):
12
+ """Thread simulating Energy Meter."""
13
+
14
+ _power: float = 1000.0 # W
15
+ _power_topic: str = "power"
16
+ _interval: float = 10 # 10 seconds
17
+
18
+ def __init__(self) -> None:
19
+ """Construct a thread for publishing power data.
20
+
21
+ Args:
22
+ topic (str, optional): MQTT topic to post the sensor readings. Defaults to None.
23
+ interval (float, optional): Interval specifying how often the sensor is read. Defaults to 60 seconds.
24
+ """
25
+ super().__init__(None)
26
+ self.current_ts: float = timestamp()
27
+
28
+ @classmethod
29
+ def initialize(cls, power_topic: str, power: float, interval: float) -> None:
30
+ """Initialize thread class attributes.
31
+
32
+ Args:
33
+ power_topic (str): topic to publish the energy meter readings
34
+ power (float): power to be simulated, the default is 1kW
35
+ interval (float): update interval, the default is 10s
36
+ """
37
+ cls._power = power
38
+ cls._interval = interval
39
+ cls._power_topic = power_topic
40
+
41
+ @override
42
+ def update_interval(self) -> float:
43
+ return self._interval
44
+
45
+ def publish_active_power(self, ts: float) -> None:
46
+ """Publish the active power, also known as real power. This is that
47
+ part of the power that can be converted to useful work.
48
+
49
+ Args:
50
+ ts (str): time stamp of the event
51
+
52
+ """
53
+ dt = ts - self.current_ts
54
+ self.current_ts = ts
55
+
56
+ msg = {
57
+ "timestamp": ts,
58
+ "real_a": self._power * dt,
59
+ "real_b": self._power * dt,
60
+ "real_c": self._power * dt,
61
+ "real_total": 3 * self._power * dt,
62
+ }
63
+ self.publish(self._power_topic, json.dumps(msg), 1, True)
64
+
65
+ @override
66
+ def update(self) -> bool:
67
+ super().update()
68
+ self.publish_active_power(timestamp())
69
+ return True
70
+
71
+
72
+ class PowerMeterSimulator(JuhamThread):
73
+ """Simulator energy meter sensor. Spawns a thread
74
+ to simulate Shelly PM mqtt messages"""
75
+
76
+ workerThreadId = PowerMeterSimulatorThread.get_class_id()
77
+ update_interval: float = 10
78
+ power: float = 1000.0
79
+
80
+ _POWERMETERSIMULATOR: str = "_powermetersimulator"
81
+
82
+ def __init__(
83
+ self,
84
+ name: str = "em",
85
+ interval: float = 0,
86
+ ) -> None:
87
+ """Create energy meter simulator.
88
+
89
+ Args:
90
+ name (str, optional): Name of the object. Defaults to 'em'.
91
+ topic (str, optional): MQTT topic to publish the energy meter reports. Defaults to None.
92
+ interval (float, optional): interval between events, in seconds. Defaults to None.
93
+ """
94
+ super().__init__(name)
95
+ self.update_ts: float = 0.0
96
+ if interval > 0.0:
97
+ self.update_interval = interval
98
+ self.power_topic = self.make_topic_name("powerconsumption") # target topic
99
+
100
+ @override
101
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
102
+ if msg.topic == self.power_topic:
103
+ em = json.loads(msg.payload.decode())
104
+ self.on_sensor(em)
105
+ else:
106
+ super().on_message(client, userdata, msg)
107
+
108
+ def on_sensor(self, em: dict[Any, Any]) -> None:
109
+ """Handle data coming from the energy meter.
110
+
111
+ Simply log the event to indicate the presense of simulated device.
112
+ Args:
113
+ em (dict): data from the sensor
114
+ """
115
+ self.debug(f"Simulated power meter sensor {em}")
116
+
117
+ @override
118
+ def run(self) -> None:
119
+ PowerMeterSimulatorThread.initialize(
120
+ self.power_topic, self.power, self.update_interval
121
+ )
122
+ self.worker = cast(
123
+ PowerMeterSimulatorThread,
124
+ Juham.instantiate(PowerMeterSimulatorThread.get_class_id()),
125
+ )
126
+ super().run()
127
+
128
+ @override
129
+ def to_dict(self) -> Dict[str, Any]:
130
+ data: Dict[str, Any] = super().to_dict()
131
+ data[self._POWERMETERSIMULATOR] = {"power_topic": self.power_topic}
132
+ return data
133
+
134
+ @override
135
+ def from_dict(self, data: Dict[str, Any]) -> None:
136
+ super().from_dict(data)
137
+ if self._POWERMETERSIMULATOR in data:
138
+ for key, value in data[self._POWERMETERSIMULATOR].items():
139
+ setattr(self, key, value)
@@ -1,140 +1,140 @@
1
- from datetime import datetime
2
- import time
3
- import json
4
- from typing import Any, Dict, Optional, cast
5
- from typing_extensions import override
6
-
7
- from masterpiece.mqtt import Mqtt, MqttMsg
8
- from juham_core import JuhamCloudThread, JuhamThread
9
-
10
-
11
- class SpotHintaFiThread(JuhamCloudThread):
12
- """Thread running SpotHinta.fi.
13
-
14
- Periodically fetches the spot electricity prices and publishes them
15
- to 'spot' topic.
16
- """
17
-
18
- _spot_topic: str = ""
19
- _url: str = ""
20
- _interval: float = 12 * 3600
21
- grid_cost_day: float = 0.0314
22
- grid_cost_night: float = 0.0132
23
- grid_cost_tax: float = 0.028272
24
-
25
- def __init__(self, client: Optional[Mqtt] = None) -> None:
26
- super().__init__(client)
27
- self._interval = 60
28
-
29
- def init(self, topic: str, url: str, interval: float) -> None:
30
- self._spot_topic = topic
31
- self._url = url
32
- self._interval = interval
33
-
34
- @override
35
- def make_weburl(self) -> str:
36
- return self._url
37
-
38
- @override
39
- def update_interval(self) -> float:
40
- return self._interval
41
-
42
- @override
43
- def process_data(self, rawdata: Any) -> None:
44
- """Publish electricity price message to Juham topic.
45
-
46
- Args:
47
- rawdata (dict): electricity prices
48
- """
49
-
50
- super().process_data(rawdata)
51
- data = rawdata.json()
52
-
53
- spot = []
54
- for e in data:
55
- dt = datetime.fromisoformat(e["DateTime"]) # Correct timezone handling
56
- ts = int(dt.timestamp()) # Ensure integer timestamps like in the test
57
-
58
- hour = dt.strftime("%H") # Correctly extract hour
59
-
60
- if 6 <= int(hour) < 22:
61
- grid_cost = self.grid_cost_day
62
- else:
63
- grid_cost = self.grid_cost_night
64
-
65
- total_price = round(e["PriceWithTax"] + grid_cost + self.grid_cost_tax, 6)
66
- grid_cost_total = round(grid_cost + self.grid_cost_tax, 6)
67
-
68
- h = {
69
- "Timestamp": ts,
70
- "hour": hour,
71
- "Rank": e["Rank"],
72
- "PriceWithTax": total_price,
73
- "GridCost": grid_cost_total,
74
- }
75
- spot.append(h)
76
-
77
- self.publish(self._spot_topic, json.dumps(spot), 1, True)
78
- # self.info(f"Spot electricity prices published for the next {len(spot)} days")
79
-
80
-
81
- class SpotHintaFi(JuhamThread):
82
- """Spot electricity price for reading hourly electricity prices from
83
- https://api.spot-hinta.fi site.
84
- """
85
-
86
- _SPOTHINTAFI: str = "_spothintafi"
87
- worker_thread_id = SpotHintaFiThread.get_class_id()
88
- url = "https://api.spot-hinta.fi/TodayAndDayForward"
89
- update_interval = 12 * 3600
90
-
91
- def __init__(self, name: str = "rspothintafi") -> None:
92
- super().__init__(name)
93
- self.active_liter_lpm = -1
94
- self.update_ts = None
95
- self.spot_topic = self.make_topic_name("spot")
96
-
97
- @override
98
- def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
99
- super().on_connect(client, userdata, flags, rc)
100
- if rc == 0:
101
- self.subscribe(self.spot_topic)
102
-
103
- @override
104
- def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
105
- if msg.topic == self.spot_topic:
106
- em = json.loads(msg.payload.decode())
107
- self.on_spot(em)
108
- else:
109
- super().on_message(client, userdata, msg)
110
-
111
- def on_spot(self, m: dict[Any, Any]) -> None:
112
- """Write hourly spot electricity prices to time series database.
113
-
114
- Args:
115
- m (dict): holding hourly spot electricity prices
116
- """
117
- pass
118
-
119
- @override
120
- def run(self) -> None:
121
- self.worker = cast(SpotHintaFiThread, self.instantiate(self.worker_thread_id))
122
- self.worker.init(self.spot_topic, self.url, self.update_interval)
123
- super().run()
124
-
125
- @override
126
- def to_dict(self) -> Dict[str, Any]:
127
- data: Dict[str, Any] = super().to_dict()
128
- data[self._SPOTHINTAFI] = {
129
- "topic": self.spot_topic,
130
- "url": self.url,
131
- "interval": self.update_interval,
132
- }
133
- return data
134
-
135
- @override
136
- def from_dict(self, data: Dict[str, Any]) -> None:
137
- super().from_dict(data)
138
- if self._SPOTHINTAFI in data:
139
- for key, value in data[self._SPOTHINTAFI].items():
140
- setattr(self, key, value)
1
+ from datetime import datetime
2
+ import time
3
+ import json
4
+ from typing import Any, Dict, Optional, cast
5
+ from typing_extensions import override
6
+
7
+ from masterpiece.mqtt import Mqtt, MqttMsg
8
+ from juham_core import JuhamCloudThread, JuhamThread
9
+
10
+
11
+ class SpotHintaFiThread(JuhamCloudThread):
12
+ """Thread running SpotHinta.fi.
13
+
14
+ Periodically fetches the spot electricity prices and publishes them
15
+ to 'spot' topic.
16
+ """
17
+
18
+ _spot_topic: str = ""
19
+ _url: str = ""
20
+ _interval: float = 12 * 3600
21
+ grid_cost_day: float = 0.0314
22
+ grid_cost_night: float = 0.0132
23
+ grid_cost_tax: float = 0.028272
24
+
25
+ def __init__(self, client: Optional[Mqtt] = None) -> None:
26
+ super().__init__(client)
27
+ self._interval = 60
28
+
29
+ def init(self, topic: str, url: str, interval: float) -> None:
30
+ self._spot_topic = topic
31
+ self._url = url
32
+ self._interval = interval
33
+
34
+ @override
35
+ def make_weburl(self) -> str:
36
+ return self._url
37
+
38
+ @override
39
+ def update_interval(self) -> float:
40
+ return self._interval
41
+
42
+ @override
43
+ def process_data(self, rawdata: Any) -> None:
44
+ """Publish electricity price message to Juham topic.
45
+
46
+ Args:
47
+ rawdata (dict): electricity prices
48
+ """
49
+
50
+ super().process_data(rawdata)
51
+ data = rawdata.json()
52
+
53
+ spot = []
54
+ for e in data:
55
+ dt = datetime.fromisoformat(e["DateTime"]) # Correct timezone handling
56
+ ts = int(dt.timestamp()) # Ensure integer timestamps like in the test
57
+
58
+ hour = dt.strftime("%H") # Correctly extract hour
59
+
60
+ if 6 <= int(hour) < 22:
61
+ grid_cost = self.grid_cost_day
62
+ else:
63
+ grid_cost = self.grid_cost_night
64
+
65
+ total_price = round(e["PriceWithTax"] + grid_cost + self.grid_cost_tax, 6)
66
+ grid_cost_total = round(grid_cost + self.grid_cost_tax, 6)
67
+
68
+ h = {
69
+ "Timestamp": ts,
70
+ "hour": hour,
71
+ "Rank": e["Rank"],
72
+ "PriceWithTax": total_price,
73
+ "GridCost": grid_cost_total,
74
+ }
75
+ spot.append(h)
76
+
77
+ self.publish(self._spot_topic, json.dumps(spot), 1, True)
78
+ # self.info(f"Spot electricity prices published for the next {len(spot)} days")
79
+
80
+
81
+ class SpotHintaFi(JuhamThread):
82
+ """Spot electricity price for reading hourly electricity prices from
83
+ https://api.spot-hinta.fi site.
84
+ """
85
+
86
+ _SPOTHINTAFI: str = "_spothintafi"
87
+ worker_thread_id = SpotHintaFiThread.get_class_id()
88
+ url = "https://api.spot-hinta.fi/TodayAndDayForward"
89
+ update_interval = 12 * 3600
90
+
91
+ def __init__(self, name: str = "rspothintafi") -> None:
92
+ super().__init__(name)
93
+ self.active_liter_lpm = -1
94
+ self.update_ts = None
95
+ self.spot_topic = self.make_topic_name("spot")
96
+
97
+ @override
98
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
99
+ super().on_connect(client, userdata, flags, rc)
100
+ if rc == 0:
101
+ self.subscribe(self.spot_topic)
102
+
103
+ @override
104
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
105
+ if msg.topic == self.spot_topic:
106
+ em = json.loads(msg.payload.decode())
107
+ self.on_spot(em)
108
+ else:
109
+ super().on_message(client, userdata, msg)
110
+
111
+ def on_spot(self, m: dict[Any, Any]) -> None:
112
+ """Write hourly spot electricity prices to time series database.
113
+
114
+ Args:
115
+ m (dict): holding hourly spot electricity prices
116
+ """
117
+ pass
118
+
119
+ @override
120
+ def run(self) -> None:
121
+ self.worker = cast(SpotHintaFiThread, self.instantiate(self.worker_thread_id))
122
+ self.worker.init(self.spot_topic, self.url, self.update_interval)
123
+ super().run()
124
+
125
+ @override
126
+ def to_dict(self) -> Dict[str, Any]:
127
+ data: Dict[str, Any] = super().to_dict()
128
+ data[self._SPOTHINTAFI] = {
129
+ "topic": self.spot_topic,
130
+ "url": self.url,
131
+ "interval": self.update_interval,
132
+ }
133
+ return data
134
+
135
+ @override
136
+ def from_dict(self, data: Dict[str, Any]) -> None:
137
+ super().from_dict(data)
138
+ if self._SPOTHINTAFI in data:
139
+ for key, value in data[self._SPOTHINTAFI].items():
140
+ setattr(self, key, value)