juham-automation 0.0.13__py3-none-any.whl → 0.0.14__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.
@@ -1,159 +1,159 @@
1
- from typing import Any
2
- from typing_extensions import override
3
- import json
4
-
5
- from masterpiece.mqtt import MqttMsg
6
- from juham_core import Juham
7
- from juham_core.timeutils import timestamp
8
-
9
-
10
- class WaterCirculator(Juham):
11
- """Hot Water Circulation Automation
12
-
13
- This system monitors motion sensor data to detect home occupancy.
14
-
15
- - **When motion is detected**: The water circulator pump is activated, ensuring hot water is
16
- instantly available when the tap is turned on.
17
- - **When no motion is detected for a specified period (in seconds)**: The pump automatically
18
- switches off to conserve energy.
19
-
20
- Future improvement idea
21
- ------------------------
22
-
23
- In cold countries, such as Finland, energy conservation during the winter season may not be a priority.
24
- In this case, an additional temperature sensor measuring the outside temperature could be used to determine whether
25
- the circulator should be switched off at all. The circulating water could potentially act as an additional heating radiator.
26
-
27
- Points to consider
28
- ------------------
29
-
30
- - Switching the pump on and off may affect its lifetime.
31
- - Keeping the pump running with hot water could impact the lifespan of the pipes, potentially causing
32
- corrosion due to constant hot water flow.
33
-
34
- """
35
-
36
- uptime = 60 * 60 # one hour
37
- min_temperature = 37
38
-
39
- def __init__(self, name: str, temperature_sensor: str) -> None:
40
- super().__init__(name)
41
-
42
- # input topics
43
- self.motion_topic = self.make_topic_name("motion") # motion detection
44
- self.temperature_topic = self.make_topic_name(temperature_sensor)
45
-
46
- # relay to be controlled
47
- self.topic_power = self.make_topic_name("power")
48
-
49
- # for the pump controlling logic
50
- self.current_motion: bool = False
51
- self.relay_started_ts: float = 0
52
- self.water_temperature: float = 0
53
- self.water_temperature_updated: float = 0
54
- self.initialized = False
55
-
56
- @override
57
- def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
58
- super().on_connect(client, userdata, flags, rc)
59
- if rc == 0:
60
- self.subscribe(self.motion_topic)
61
- self.subscribe(self.temperature_topic)
62
- # reset the relay to make sure the initial state matches the state of us
63
- self.publish_relay_state(0)
64
-
65
- @override
66
- def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
67
- if msg.topic == self.temperature_topic:
68
- m = json.loads(msg.payload.decode())
69
- self.on_temperature_sensor(m, timestamp())
70
- elif msg.topic == self.motion_topic:
71
- m = json.loads(msg.payload.decode())
72
- self.on_motion_sensor(m, timestamp())
73
- else:
74
- super().on_message(client, userdata, msg)
75
-
76
- def on_temperature_sensor(self, m: dict[str, Any], ts_utc_now: float) -> None:
77
- """Handle message from the hot water pipe temperature sensor.
78
- Records the temperature and updates the water_temperature_updated attribute.
79
-
80
- Args:
81
- m (dict): temperature reading from the hot water blump sensor
82
- ts_utc_now (float): _current utc time
83
- """
84
-
85
- self.water_temperature = m["temperature"]
86
- self.water_temperature_updated = ts_utc_now
87
- # self.info(
88
- # f"Temperature of circulating water updated to {self.water_temperature} C"
89
- # )
90
-
91
- def on_motion_sensor(self, m: dict[str, dict[str, Any]], ts_utc_now: float) -> None:
92
- """Control the water cirulator bump.
93
-
94
- Given message from the motion sensor consider switching the
95
- circulator bump on.
96
-
97
- Args:
98
- msg (dict): directionary holding motion sensor data
99
- ts_utc_now (float): current time stamp
100
- """
101
- sensor = m["sensor"]
102
- vibration: bool = bool(m["vibration"])
103
- motion: bool = bool(m["motion"])
104
-
105
- if motion or vibration:
106
- # self.debug(f"Life form detected in {sensor}")
107
- # honey I'm home
108
- if not self.current_motion:
109
- if self.water_temperature > self.min_temperature:
110
- self.publish_relay_state(0)
111
- # self.debug(
112
- # f"Circulator: motion detected but water warm already {self.water_temperature} > {self.min_temperature} C"
113
- # )
114
- else:
115
- self.current_motion = True
116
- self.relay_started_ts = ts_utc_now
117
- self.publish_relay_state(1)
118
- self.initialized = True
119
- self.info(
120
- f"Circulator pump started, will run for {int(self.uptime / 60)} minutes "
121
- )
122
- else:
123
- self.publish_relay_state(1)
124
- self.relay_started_ts = ts_utc_now
125
- # self.debug(
126
- # f"Circulator pump has been running for {int(ts_utc_now - self.relay_started_ts)/60} minutes",
127
- # " ",
128
- # )
129
- else:
130
- if self.current_motion or not self.initialized:
131
- elapsed: float = ts_utc_now - self.relay_started_ts
132
- if elapsed > self.uptime:
133
- self.publish_relay_state(0)
134
- self.info(
135
- f"Circulator pump stopped, no motion in {int(elapsed/60)} minutes detected",
136
- "",
137
- )
138
- self.current_motion = False
139
- self.initialized = True
140
- else:
141
- self.publish_relay_state(1)
142
- # self.debug(
143
- # f"Circulator bump stop countdown {int(self.uptime - (ts_utc_now - self.relay_started_ts ))/60} min"
144
- # )
145
- else:
146
- self.publish_relay_state(0)
147
- # self.debug(
148
- # f"Circulator bump off already, temperature {self.water_temperature} C",
149
- # "",
150
- # )
151
-
152
- def publish_relay_state(self, state: int) -> None:
153
- """Publish power status.
154
-
155
- Args:
156
- state (int): 1 for on, 0 for off, as defined by Juham 'power' topic
157
- """
158
- heat = {"Unit": self.name, "Timestamp": timestamp(), "State": state}
159
- self.publish(self.topic_power, json.dumps(heat), 1, False)
1
+ from typing import Any
2
+ from typing_extensions import override
3
+ import json
4
+
5
+ from masterpiece.mqtt import MqttMsg
6
+ from juham_core import Juham
7
+ from juham_core.timeutils import timestamp
8
+
9
+
10
+ class WaterCirculator(Juham):
11
+ """Hot Water Circulation Automation
12
+
13
+ This system monitors motion sensor data to detect home occupancy.
14
+
15
+ - **When motion is detected**: The water circulator pump is activated, ensuring hot water is
16
+ instantly available when the tap is turned on.
17
+ - **When no motion is detected for a specified period (in seconds)**: The pump automatically
18
+ switches off to conserve energy.
19
+
20
+ Future improvement idea
21
+ ------------------------
22
+
23
+ In cold countries, such as Finland, energy conservation during the winter season may not be a priority.
24
+ In this case, an additional temperature sensor measuring the outside temperature could be used to determine whether
25
+ the circulator should be switched off at all. The circulating water could potentially act as an additional heating radiator.
26
+
27
+ Points to consider
28
+ ------------------
29
+
30
+ - Switching the pump on and off may affect its lifetime.
31
+ - Keeping the pump running with hot water could impact the lifespan of the pipes, potentially causing
32
+ corrosion due to constant hot water flow.
33
+
34
+ """
35
+
36
+ uptime = 60 * 60 # one hour
37
+ min_temperature = 37
38
+
39
+ def __init__(self, name: str, temperature_sensor: str) -> None:
40
+ super().__init__(name)
41
+
42
+ # input topics
43
+ self.motion_topic = self.make_topic_name("motion") # motion detection
44
+ self.temperature_topic = self.make_topic_name(temperature_sensor)
45
+
46
+ # relay to be controlled
47
+ self.topic_power = self.make_topic_name("power")
48
+
49
+ # for the pump controlling logic
50
+ self.current_motion: bool = False
51
+ self.relay_started_ts: float = 0
52
+ self.water_temperature: float = 0
53
+ self.water_temperature_updated: float = 0
54
+ self.initialized = False
55
+
56
+ @override
57
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
58
+ super().on_connect(client, userdata, flags, rc)
59
+ if rc == 0:
60
+ self.subscribe(self.motion_topic)
61
+ self.subscribe(self.temperature_topic)
62
+ # reset the relay to make sure the initial state matches the state of us
63
+ self.publish_relay_state(0)
64
+
65
+ @override
66
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
67
+ if msg.topic == self.temperature_topic:
68
+ m = json.loads(msg.payload.decode())
69
+ self.on_temperature_sensor(m, timestamp())
70
+ elif msg.topic == self.motion_topic:
71
+ m = json.loads(msg.payload.decode())
72
+ self.on_motion_sensor(m, timestamp())
73
+ else:
74
+ super().on_message(client, userdata, msg)
75
+
76
+ def on_temperature_sensor(self, m: dict[str, Any], ts_utc_now: float) -> None:
77
+ """Handle message from the hot water pipe temperature sensor.
78
+ Records the temperature and updates the water_temperature_updated attribute.
79
+
80
+ Args:
81
+ m (dict): temperature reading from the hot water blump sensor
82
+ ts_utc_now (float): _current utc time
83
+ """
84
+
85
+ self.water_temperature = m["temperature"]
86
+ self.water_temperature_updated = ts_utc_now
87
+ # self.info(
88
+ # f"Temperature of circulating water updated to {self.water_temperature} C"
89
+ # )
90
+
91
+ def on_motion_sensor(self, m: dict[str, dict[str, Any]], ts_utc_now: float) -> None:
92
+ """Control the water cirulator bump.
93
+
94
+ Given message from the motion sensor consider switching the
95
+ circulator bump on.
96
+
97
+ Args:
98
+ msg (dict): directionary holding motion sensor data
99
+ ts_utc_now (float): current time stamp
100
+ """
101
+ sensor = m["sensor"]
102
+ vibration: bool = bool(m["vibration"])
103
+ motion: bool = bool(m["motion"])
104
+
105
+ if motion or vibration:
106
+ # self.debug(f"Life form detected in {sensor}")
107
+ # honey I'm home
108
+ if not self.current_motion:
109
+ if self.water_temperature > self.min_temperature:
110
+ self.publish_relay_state(0)
111
+ # self.debug(
112
+ # f"Circulator: motion detected but water warm already {self.water_temperature} > {self.min_temperature} C"
113
+ # )
114
+ else:
115
+ self.current_motion = True
116
+ self.relay_started_ts = ts_utc_now
117
+ self.publish_relay_state(1)
118
+ self.initialized = True
119
+ self.info(
120
+ f"Circulator pump started, will run for {int(self.uptime / 60)} minutes "
121
+ )
122
+ else:
123
+ self.publish_relay_state(1)
124
+ self.relay_started_ts = ts_utc_now
125
+ # self.debug(
126
+ # f"Circulator pump has been running for {int(ts_utc_now - self.relay_started_ts)/60} minutes",
127
+ # " ",
128
+ # )
129
+ else:
130
+ if self.current_motion or not self.initialized:
131
+ elapsed: float = ts_utc_now - self.relay_started_ts
132
+ if elapsed > self.uptime:
133
+ self.publish_relay_state(0)
134
+ self.info(
135
+ f"Circulator pump stopped, no motion in {int(elapsed/60)} minutes detected",
136
+ "",
137
+ )
138
+ self.current_motion = False
139
+ self.initialized = True
140
+ else:
141
+ self.publish_relay_state(1)
142
+ # self.debug(
143
+ # f"Circulator bump stop countdown {int(self.uptime - (ts_utc_now - self.relay_started_ts ))/60} min"
144
+ # )
145
+ else:
146
+ self.publish_relay_state(0)
147
+ # self.debug(
148
+ # f"Circulator bump off already, temperature {self.water_temperature} C",
149
+ # "",
150
+ # )
151
+
152
+ def publish_relay_state(self, state: int) -> None:
153
+ """Publish power status.
154
+
155
+ Args:
156
+ state (int): 1 for on, 0 for off, as defined by Juham 'power' topic
157
+ """
158
+ heat = {"Unit": self.name, "Timestamp": timestamp(), "State": state}
159
+ self.publish(self.topic_power, json.dumps(heat), 1, False)
juham_automation/japp.py CHANGED
@@ -1,49 +1,49 @@
1
- from masterpiece import Application
2
- from juham_core import Juham
3
-
4
- from .ts import ForecastTs
5
- from .ts import PowerTs
6
- from .ts import PowerPlanTs
7
- from .ts import PowerMeterTs
8
- from .ts import LogTs
9
- from .ts import EnergyCostCalculatorTs
10
- from .ts import ElectricityPriceTs
11
- from .automation import SpotHintaFi
12
- from .automation import EnergyCostCalculator
13
-
14
-
15
- class JApp(Application):
16
- """Juham home automation application base class. Registers new plugin
17
- group 'juham' on which general purpose Juham plugins can be written on.
18
- """
19
-
20
- def __init__(self, name: str) -> None:
21
- """Creates home automation application with the given name.
22
- If --enable_plugins is False create hard coded configuration
23
- by calling instantiate_classes() method.
24
-
25
- Args:
26
- name (str): name for the application
27
- """
28
- super().__init__(name, Juham(name))
29
-
30
- def instantiate_classes(self) -> None:
31
- """Instantiate automation classes .
32
-
33
- Returns:
34
- None
35
- """
36
- self.add(ForecastTs())
37
- self.add(PowerTs())
38
- self.add(PowerPlanTs())
39
- self.add(PowerMeterTs())
40
- self.add(LogTs())
41
- self.add(SpotHintaFi())
42
- self.add(EnergyCostCalculator())
43
- self.add(EnergyCostCalculatorTs())
44
- self.add(ElectricityPriceTs())
45
-
46
- @classmethod
47
- def register(cls) -> None:
48
- """Register plugin group `juham`."""
49
- Application.register_plugin_group("juham")
1
+ from masterpiece import Application
2
+ from juham_core import Juham
3
+
4
+ from .ts import ForecastTs
5
+ from .ts import PowerTs
6
+ from .ts import PowerPlanTs
7
+ from .ts import PowerMeterTs
8
+ from .ts import LogTs
9
+ from .ts import EnergyCostCalculatorTs
10
+ from .ts import ElectricityPriceTs
11
+ from .automation import SpotHintaFi
12
+ from .automation import EnergyCostCalculator
13
+
14
+
15
+ class JApp(Application):
16
+ """Juham home automation application base class. Registers new plugin
17
+ group 'juham' on which general purpose Juham plugins can be written on.
18
+ """
19
+
20
+ def __init__(self, name: str) -> None:
21
+ """Creates home automation application with the given name.
22
+ If --enable_plugins is False create hard coded configuration
23
+ by calling instantiate_classes() method.
24
+
25
+ Args:
26
+ name (str): name for the application
27
+ """
28
+ super().__init__(name, Juham(name))
29
+
30
+ def instantiate_classes(self) -> None:
31
+ """Instantiate automation classes .
32
+
33
+ Returns:
34
+ None
35
+ """
36
+ self.add(ForecastTs())
37
+ self.add(PowerTs())
38
+ self.add(PowerPlanTs())
39
+ self.add(PowerMeterTs())
40
+ self.add(LogTs())
41
+ self.add(SpotHintaFi())
42
+ self.add(EnergyCostCalculator())
43
+ self.add(EnergyCostCalculatorTs())
44
+ self.add(ElectricityPriceTs())
45
+
46
+ @classmethod
47
+ def register(cls) -> None:
48
+ """Register plugin group `juham`."""
49
+ Application.register_plugin_group("juham")
@@ -1,25 +1,25 @@
1
- """
2
- Description
3
- ===========
4
-
5
- Juham - Juha's Ultimate Home Automation Timeseries
6
-
7
- """
8
-
9
- from .energycostcalculator_ts import EnergyCostCalculatorTs
10
- from .log_ts import LogTs
11
- from .power_ts import PowerTs
12
- from .powerplan_ts import PowerPlanTs
13
- from .powermeter_ts import PowerMeterTs
14
- from .electricityprice_ts import ElectricityPriceTs
15
- from .forecast_ts import ForecastTs
16
-
17
- __all__ = [
18
- "EnergyCostCalculatorTs",
19
- "ForecastTs",
20
- "LogTs",
21
- "PowerTs",
22
- "PowerPlanTs",
23
- "PowerMeterTs",
24
- "ElectricityPriceTs",
25
- ]
1
+ """
2
+ Description
3
+ ===========
4
+
5
+ Juham - Juha's Ultimate Home Automation Timeseries
6
+
7
+ """
8
+
9
+ from .energycostcalculator_ts import EnergyCostCalculatorTs
10
+ from .log_ts import LogTs
11
+ from .power_ts import PowerTs
12
+ from .powerplan_ts import PowerPlanTs
13
+ from .powermeter_ts import PowerMeterTs
14
+ from .electricityprice_ts import ElectricityPriceTs
15
+ from .forecast_ts import ForecastTs
16
+
17
+ __all__ = [
18
+ "EnergyCostCalculatorTs",
19
+ "ForecastTs",
20
+ "LogTs",
21
+ "PowerTs",
22
+ "PowerPlanTs",
23
+ "PowerMeterTs",
24
+ "ElectricityPriceTs",
25
+ ]
@@ -1,51 +1,51 @@
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.timeutils import epoc2utc
9
- from juham_core import JuhamTs
10
-
11
-
12
- class ElectricityPriceTs(JuhamTs):
13
- """Spot electricity price for reading hourly electricity prices from"""
14
-
15
- def __init__(self, name: str = "electricityprice_ts") -> None:
16
- super().__init__(name)
17
-
18
- self.spot_topic = self.make_topic_name("spot")
19
-
20
- @override
21
- def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
22
- super().on_connect(client, userdata, flags, rc)
23
- if rc == 0:
24
- self.subscribe(self.spot_topic)
25
-
26
- @override
27
- def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
28
- if msg.topic == self.spot_topic:
29
- em = json.loads(msg.payload.decode())
30
- self.on_spot(em)
31
- else:
32
- super().on_message(client, userdata, msg)
33
-
34
- def on_spot(self, m: dict[Any, Any]) -> None:
35
- """Write hourly spot electricity prices to time series database.
36
-
37
- Args:
38
- m (dict): holding hourlys spot electricity prices
39
- """
40
- grid_cost : float
41
- for h in m:
42
- if "GridCost" in h:
43
- grid_cost = h["GridCost"]
44
- point = (
45
- self.measurement("spot")
46
- .tag("hour", h["Timestamp"])
47
- .field("value", h["PriceWithTax"])
48
- .field("grid", grid_cost)
49
- .time(epoc2utc(h["Timestamp"]))
50
- )
51
- self.write(point)
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.timeutils import epoc2utc
9
+ from juham_core import JuhamTs
10
+
11
+
12
+ class ElectricityPriceTs(JuhamTs):
13
+ """Spot electricity price for reading hourly electricity prices from"""
14
+
15
+ def __init__(self, name: str = "electricityprice_ts") -> None:
16
+ super().__init__(name)
17
+
18
+ self.spot_topic = self.make_topic_name("spot")
19
+
20
+ @override
21
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
22
+ super().on_connect(client, userdata, flags, rc)
23
+ if rc == 0:
24
+ self.subscribe(self.spot_topic)
25
+
26
+ @override
27
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
28
+ if msg.topic == self.spot_topic:
29
+ em = json.loads(msg.payload.decode())
30
+ self.on_spot(em)
31
+ else:
32
+ super().on_message(client, userdata, msg)
33
+
34
+ def on_spot(self, m: dict[Any, Any]) -> None:
35
+ """Write hourly spot electricity prices to time series database.
36
+
37
+ Args:
38
+ m (dict): holding hourlys spot electricity prices
39
+ """
40
+ grid_cost : float
41
+ for h in m:
42
+ if "GridCost" in h:
43
+ grid_cost = h["GridCost"]
44
+ point = (
45
+ self.measurement("spot")
46
+ .tag("hour", h["Timestamp"])
47
+ .field("value", h["PriceWithTax"])
48
+ .field("grid", grid_cost)
49
+ .time(epoc2utc(h["Timestamp"]))
50
+ )
51
+ self.write(point)
@@ -1,43 +1,43 @@
1
- from typing import Any
2
- from typing_extensions import override
3
- import json
4
-
5
- from masterpiece.mqtt import MqttMsg
6
-
7
- from juham_core import JuhamTs
8
- from juham_core.timeutils import (
9
- epoc2utc,
10
- timestampstr,
11
- )
12
-
13
-
14
- class EnergyCostCalculatorTs(JuhamTs):
15
- """The EnergyCostCalculator recorder."""
16
-
17
- def __init__(self, name: str = "ecc_ts") -> None:
18
- super().__init__(name)
19
- self.topic_net_energy_balance = self.make_topic_name("net_energy_cost")
20
-
21
- @override
22
- def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
23
- super().on_connect(client, userdata, flags, rc)
24
- if rc == 0:
25
- self.subscribe(self.topic_net_energy_balance)
26
-
27
- @override
28
- def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
29
- super().on_message(client, userdata, msg)
30
- if msg.topic == self.topic_net_energy_balance:
31
- m = json.loads(msg.payload.decode())
32
- self.record_powerconsumption(m)
33
-
34
- def record_powerconsumption(self, m: dict[str, Any]) -> None:
35
- """Record powerconsumption
36
-
37
- Args:
38
- m (dict[str, Any]): to be recorded
39
- """
40
-
41
- self.write_point(
42
- "energycost", {"site": m["name"]}, m, timestampstr(m["ts"])
43
- )
1
+ from typing import Any
2
+ from typing_extensions import override
3
+ import json
4
+
5
+ from masterpiece.mqtt import MqttMsg
6
+
7
+ from juham_core import JuhamTs
8
+ from juham_core.timeutils import (
9
+ epoc2utc,
10
+ timestampstr,
11
+ )
12
+
13
+
14
+ class EnergyCostCalculatorTs(JuhamTs):
15
+ """The EnergyCostCalculator recorder."""
16
+
17
+ def __init__(self, name: str = "ecc_ts") -> None:
18
+ super().__init__(name)
19
+ self.topic_net_energy_balance = self.make_topic_name("net_energy_cost")
20
+
21
+ @override
22
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
23
+ super().on_connect(client, userdata, flags, rc)
24
+ if rc == 0:
25
+ self.subscribe(self.topic_net_energy_balance)
26
+
27
+ @override
28
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
29
+ super().on_message(client, userdata, msg)
30
+ if msg.topic == self.topic_net_energy_balance:
31
+ m = json.loads(msg.payload.decode())
32
+ self.record_powerconsumption(m)
33
+
34
+ def record_powerconsumption(self, m: dict[str, Any]) -> None:
35
+ """Record powerconsumption
36
+
37
+ Args:
38
+ m (dict[str, Any]): to be recorded
39
+ """
40
+
41
+ self.write_point(
42
+ "energycost", {"site": m["name"]}, m, timestampstr(m["ts"])
43
+ )