juham-automation 0.0.2__py3-none-any.whl → 0.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- juham_automation/__init__.py +42 -37
- juham_automation/automation/__init__.py +23 -0
- juham_automation/automation/energybalancer.py +281 -0
- juham_automation/automation/energycostcalculator.py +310 -0
- juham_automation/automation/heatingoptimizer.py +971 -0
- juham_automation/automation/leakdetector.py +162 -0
- juham_automation/automation/spothintafi.py +140 -0
- juham_automation/automation/watercirculator.py +140 -0
- juham_automation/japp.py +53 -55
- juham_automation/ts/__init__.py +27 -0
- juham_automation/ts/electricityprice_ts.py +51 -0
- juham_automation/ts/energybalancer_ts.py +73 -0
- juham_automation/ts/energycostcalculator_ts.py +45 -0
- juham_automation/ts/forecast_ts.py +97 -0
- juham_automation/ts/log_ts.py +60 -0
- juham_automation/ts/power_ts.py +52 -0
- juham_automation/ts/powermeter_ts.py +68 -0
- juham_automation/ts/powerplan_ts.py +64 -0
- juham_automation-0.2.8.dist-info/METADATA +199 -0
- juham_automation-0.2.8.dist-info/RECORD +25 -0
- {juham_automation-0.0.2.dist-info → juham_automation-0.2.8.dist-info}/WHEEL +1 -1
- {juham_automation-0.0.2.dist-info → juham_automation-0.2.8.dist-info}/entry_points.txt +4 -2
- {juham_automation-0.0.2.dist-info → juham_automation-0.2.8.dist-info/licenses}/LICENSE.rst +25 -25
- juham_automation-0.0.2.dist-info/METADATA +0 -103
- juham_automation-0.0.2.dist-info/RECORD +0 -9
- {juham_automation-0.0.2.dist-info → juham_automation-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Leak Detector based on Motion Detection and Water Meter Monitoring.
|
|
3
|
+
|
|
4
|
+
This class listens to water meter and motion sensor topics to detect potential water leaks.
|
|
5
|
+
If a leak is detected, the system triggers a leak alarm.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
from typing_extensions import override
|
|
11
|
+
from masterpiece.mqtt import MqttMsg
|
|
12
|
+
from juham_core.timeutils import timestamp
|
|
13
|
+
from juham_core import Juham
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LeakDetector(Juham):
|
|
17
|
+
"""
|
|
18
|
+
Water Leak Detector Class
|
|
19
|
+
|
|
20
|
+
Listens to water meter and motion sensor topics to identify potential water leaks.
|
|
21
|
+
If water consumption is detected without corresponding motion, or if water usage
|
|
22
|
+
remains constant for prolonged periods, a leak alarm is triggered.
|
|
23
|
+
|
|
24
|
+
Detection considers the time since the last motion detection and compares it to
|
|
25
|
+
the configured leak detection period, which is the maximum runtime of water-consuming
|
|
26
|
+
appliances.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_LEAKDETECTOR: str = "leakdetector"
|
|
30
|
+
_LEAKDETECTOR_ATTRS: list[str] = [
|
|
31
|
+
"watermeter_topic",
|
|
32
|
+
"motion_topic",
|
|
33
|
+
"motion_last_detected_ts",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
watermeter_topic: str = "watermeter"
|
|
37
|
+
motion_topic: str = "motion"
|
|
38
|
+
leak_detection_period: float = (
|
|
39
|
+
3 * 3600.0
|
|
40
|
+
) # Maximum runtime for appliances, in seconds
|
|
41
|
+
location: str = "home"
|
|
42
|
+
conseq_zero_periods: int = (
|
|
43
|
+
60 # this many subsequent zero flow reports imply no leak
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: str = "leakdetector") -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize the leak detector.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name (str, optional): Name of the detector instance. Defaults to "leakdetector".
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(name)
|
|
54
|
+
self.motion_last_detected_ts: float = timestamp()
|
|
55
|
+
self.watermeter_full_topic: str = self.make_topic_name(self.watermeter_topic)
|
|
56
|
+
self.motion_full_topic: str = self.make_topic_name(self.motion_topic)
|
|
57
|
+
self.leak_detected: bool = False
|
|
58
|
+
self.zero_usage_periods_count: int = 0
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Handle MQTT connection. Subscribe to water meter and motion topics.
|
|
64
|
+
"""
|
|
65
|
+
super().on_connect(client, userdata, flags, rc)
|
|
66
|
+
if rc == 0:
|
|
67
|
+
self.subscribe(self.watermeter_full_topic)
|
|
68
|
+
self.subscribe(self.motion_full_topic)
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Process incoming MQTT messages for water meter and motion topics.
|
|
74
|
+
"""
|
|
75
|
+
payload = json.loads(msg.payload.decode())
|
|
76
|
+
if msg.topic == self.watermeter_full_topic:
|
|
77
|
+
self.process_water_meter_data(payload)
|
|
78
|
+
elif msg.topic == self.motion_full_topic:
|
|
79
|
+
self.process_motion_data(payload)
|
|
80
|
+
else:
|
|
81
|
+
super().on_message(client, userdata, msg)
|
|
82
|
+
|
|
83
|
+
def detect_activity(self, current_ts: float) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Check if activity (motion) has been detected within the leak detection period.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
current_ts (float): Current timestamp.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if activity detected within the period, False otherwise.
|
|
92
|
+
"""
|
|
93
|
+
elapsed_time = current_ts - self.motion_last_detected_ts
|
|
94
|
+
return elapsed_time < self.leak_detection_period
|
|
95
|
+
|
|
96
|
+
def publish_leak_status(self, current_ts: float, leak_suspected: bool) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Publish the leak detection status.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
current_ts (float): Current timestamp.
|
|
102
|
+
leak_suspected (bool): Whether a leak is suspected.
|
|
103
|
+
"""
|
|
104
|
+
status : dict[str, Any] = {
|
|
105
|
+
"location": self.location,
|
|
106
|
+
"sensor": self.name,
|
|
107
|
+
"leak_suspected": leak_suspected,
|
|
108
|
+
"ts": current_ts,
|
|
109
|
+
}
|
|
110
|
+
payload = json.dumps(status)
|
|
111
|
+
self.publish(self.watermeter_full_topic, payload, qos=1, retain=False)
|
|
112
|
+
|
|
113
|
+
def process_water_meter_data(self, data: dict[str, float]) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Handle water meter data and apply leak detection logic.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
data (dict): Water meter data containing flow rate and timestamp.
|
|
119
|
+
"""
|
|
120
|
+
if "active_lpm" in data and "ts" in data:
|
|
121
|
+
flow_rate = data["active_lpm"]
|
|
122
|
+
current_ts = data["ts"]
|
|
123
|
+
|
|
124
|
+
if flow_rate > 0.0:
|
|
125
|
+
if not self.detect_activity(current_ts):
|
|
126
|
+
if not self.leak_detected:
|
|
127
|
+
self.leak_detected = True
|
|
128
|
+
readable :str = datetime.fromtimestamp(self.motion_last_detected_ts).strftime("%Y-%m-%d %H:%M:%S")
|
|
129
|
+
self.warning(f"LEAK SUSPECT", f"Flow {flow_rate} lpm, last detected motion {readable}")
|
|
130
|
+
|
|
131
|
+
self.zero_usage_periods_count = 0
|
|
132
|
+
else:
|
|
133
|
+
self.zero_usage_periods_count += 1
|
|
134
|
+
if self.zero_usage_periods_count > self.conseq_zero_periods:
|
|
135
|
+
self.leak_detected = False
|
|
136
|
+
self.motion_last_detected_ts = current_ts
|
|
137
|
+
self.publish_leak_status(current_ts, self.leak_detected)
|
|
138
|
+
|
|
139
|
+
def process_motion_data(self, data: dict[str, float]) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Update the last detected motion timestamp.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
data (dict): Motion sensor data containing timestamp.
|
|
145
|
+
"""
|
|
146
|
+
if "motion" in data and data["motion"]:
|
|
147
|
+
self.motion_last_detected_ts = data["ts"]
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def to_dict(self) -> dict[str, Any]:
|
|
151
|
+
data = super().to_dict()
|
|
152
|
+
attributes = {attr: getattr(self, attr) for attr in self._LEAKDETECTOR_ATTRS}
|
|
153
|
+
data[self._LEAKDETECTOR] = attributes
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
def from_dict(self, data: dict[str, Any]) -> None:
|
|
158
|
+
super().from_dict(data)
|
|
159
|
+
if self._LEAKDETECTOR in data:
|
|
160
|
+
attributes = data[self._LEAKDETECTOR]
|
|
161
|
+
for attr in self._LEAKDETECTOR_ATTRS:
|
|
162
|
+
setattr(self, attr, attributes.get(attr, None))
|
|
@@ -0,0 +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)
|
|
@@ -0,0 +1,140 @@
|
|
|
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 = 1800 # half an 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
|
+
|
|
88
|
+
def on_motion_sensor(self, m: dict[str, dict[str, Any]], ts_utc_now: float) -> None:
|
|
89
|
+
"""Control the water cirulator bump.
|
|
90
|
+
|
|
91
|
+
Given message from the motion sensor consider switching the
|
|
92
|
+
circulator bump on.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
msg (dict): directionary holding motion sensor data
|
|
96
|
+
ts_utc_now (float): current time stamp
|
|
97
|
+
"""
|
|
98
|
+
sensor = m["sensor"]
|
|
99
|
+
vibration: bool = bool(m["vibration"])
|
|
100
|
+
motion: bool = bool(m["motion"])
|
|
101
|
+
|
|
102
|
+
if motion or vibration:
|
|
103
|
+
if not self.current_motion:
|
|
104
|
+
if self.water_temperature > self.min_temperature:
|
|
105
|
+
self.publish_relay_state(0)
|
|
106
|
+
else:
|
|
107
|
+
self.current_motion = True
|
|
108
|
+
self.relay_started_ts = ts_utc_now
|
|
109
|
+
self.publish_relay_state(1)
|
|
110
|
+
self.initialized = True
|
|
111
|
+
self.info(
|
|
112
|
+
f"Circulator pump started, will run for {int(self.uptime / 60)} minutes "
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
self.publish_relay_state(1)
|
|
116
|
+
self.relay_started_ts = ts_utc_now
|
|
117
|
+
else:
|
|
118
|
+
if self.current_motion or not self.initialized:
|
|
119
|
+
elapsed: float = ts_utc_now - self.relay_started_ts
|
|
120
|
+
if elapsed > self.uptime:
|
|
121
|
+
self.publish_relay_state(0)
|
|
122
|
+
self.info(
|
|
123
|
+
f"Circulator pump stopped, no motion in {int(elapsed/60)} minutes detected",
|
|
124
|
+
"",
|
|
125
|
+
)
|
|
126
|
+
self.current_motion = False
|
|
127
|
+
self.initialized = True
|
|
128
|
+
else:
|
|
129
|
+
self.publish_relay_state(1)
|
|
130
|
+
else:
|
|
131
|
+
self.publish_relay_state(0)
|
|
132
|
+
|
|
133
|
+
def publish_relay_state(self, state: int) -> None:
|
|
134
|
+
"""Publish power status.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
state (int): 1 for on, 0 for off, as defined by Juham 'power' topic
|
|
138
|
+
"""
|
|
139
|
+
heat = {"Unit": self.name, "Timestamp": timestamp(), "State": state}
|
|
140
|
+
self.publish(self.topic_power, json.dumps(heat), 1, False)
|
juham_automation/japp.py
CHANGED
|
@@ -1,55 +1,53 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from .ts
|
|
6
|
-
from .ts
|
|
7
|
-
from .ts
|
|
8
|
-
from .ts
|
|
9
|
-
from .ts
|
|
10
|
-
from .ts
|
|
11
|
-
from .ts
|
|
12
|
-
from .
|
|
13
|
-
from .automation
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.add(
|
|
39
|
-
self.add(
|
|
40
|
-
self.add(
|
|
41
|
-
self.add(
|
|
42
|
-
self.add(
|
|
43
|
-
self.add(
|
|
44
|
-
self.add(
|
|
45
|
-
self.add(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
self.add(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"""Register plugin group `juham`."""
|
|
55
|
-
Application.register_plugin_group("juham")
|
|
1
|
+
from juham_automation.automation.energybalancer import EnergyBalancer
|
|
2
|
+
from masterpiece import Application
|
|
3
|
+
from juham_core import Juham
|
|
4
|
+
|
|
5
|
+
from .ts import ForecastTs
|
|
6
|
+
from .ts import PowerTs
|
|
7
|
+
from .ts import PowerPlanTs
|
|
8
|
+
from .ts import PowerMeterTs
|
|
9
|
+
from .ts import EnergyBalancerTs
|
|
10
|
+
from .ts import LogTs
|
|
11
|
+
from .ts import EnergyCostCalculatorTs
|
|
12
|
+
from .ts import ElectricityPriceTs
|
|
13
|
+
from .automation import SpotHintaFi
|
|
14
|
+
from .automation import EnergyCostCalculator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JApp(Application):
|
|
18
|
+
"""Juham home automation application base class. Registers new plugin
|
|
19
|
+
group 'juham' on which general purpose Juham plugins can be written on.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str) -> None:
|
|
23
|
+
"""Creates home automation application with the given name.
|
|
24
|
+
If --enable_plugins is False create hard coded configuration
|
|
25
|
+
by calling instantiate_classes() method.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name (str): name for the application
|
|
29
|
+
"""
|
|
30
|
+
super().__init__(name, Juham(name))
|
|
31
|
+
|
|
32
|
+
def instantiate_classes(self) -> None:
|
|
33
|
+
"""Instantiate automation classes .
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
None
|
|
37
|
+
"""
|
|
38
|
+
self.add(ForecastTs())
|
|
39
|
+
self.add(PowerTs())
|
|
40
|
+
self.add(PowerPlanTs())
|
|
41
|
+
self.add(PowerMeterTs())
|
|
42
|
+
self.add(LogTs())
|
|
43
|
+
self.add(SpotHintaFi())
|
|
44
|
+
self.add(EnergyCostCalculator())
|
|
45
|
+
self.add(EnergyCostCalculatorTs())
|
|
46
|
+
self.add(ElectricityPriceTs())
|
|
47
|
+
self.add(EnergyBalancer())
|
|
48
|
+
self.add(EnergyBalancerTs())
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def register(cls) -> None:
|
|
52
|
+
"""Register plugin group `juham`."""
|
|
53
|
+
Application.register_plugin_group("juham")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
Time series recorders for Juha's Ultimate Home Automation classes.
|
|
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
|
+
from .energybalancer_ts import EnergyBalancerTs
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"EnergyCostCalculatorTs",
|
|
20
|
+
"ForecastTs",
|
|
21
|
+
"LogTs",
|
|
22
|
+
"PowerTs",
|
|
23
|
+
"PowerPlanTs",
|
|
24
|
+
"PowerMeterTs",
|
|
25
|
+
"ElectricityPriceTs",
|
|
26
|
+
"EnergyBalancerTs",
|
|
27
|
+
]
|
|
@@ -0,0 +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 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)
|