juham-watermeter 0.0.4__py3-none-any.whl → 0.0.6__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_watermeter/__init__.py +2 -2
- juham_watermeter/leakdetector.py +170 -170
- juham_watermeter/watermeter_imgdiff.py +327 -328
- juham_watermeter/watermeter_ocr.py +273 -273
- juham_watermeter/watermeter_ts.py +100 -100
- juham_watermeter/webcamera.py +172 -174
- {juham_watermeter-0.0.4.dist-info → juham_watermeter-0.0.6.dist-info}/LICENSE.rst +22 -22
- juham_watermeter-0.0.6.dist-info/METADATA +131 -0
- juham_watermeter-0.0.6.dist-info/RECORD +13 -0
- {juham_watermeter-0.0.4.dist-info → juham_watermeter-0.0.6.dist-info}/WHEEL +1 -1
- juham_watermeter-0.0.4.dist-info/METADATA +0 -27
- juham_watermeter-0.0.4.dist-info/RECORD +0 -13
- {juham_watermeter-0.0.4.dist-info → juham_watermeter-0.0.6.dist-info}/entry_points.txt +0 -0
- {juham_watermeter-0.0.4.dist-info → juham_watermeter-0.0.6.dist-info}/top_level.txt +0 -0
juham_watermeter/__init__.py
CHANGED
@@ -7,14 +7,14 @@ Web-camera based watermeter classes with leak detector
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
from .webcamera import WebCamera
|
10
|
-
from .watermeter_ocr import WaterMeterOCR
|
10
|
+
#from .watermeter_ocr import WaterMeterOCR
|
11
11
|
from .watermeter_imgdiff import WaterMeterImgDiff
|
12
12
|
from .watermeter_ts import WaterMeterTs
|
13
13
|
from .leakdetector import LeakDetector
|
14
14
|
|
15
15
|
__all__ = [
|
16
16
|
"WebCamera",
|
17
|
-
"WaterMeterOCR",
|
17
|
+
# "WaterMeterOCR",
|
18
18
|
"WaterMeterImgDiff",
|
19
19
|
"WaterMeterTs",
|
20
20
|
"LeakDetector",
|
juham_watermeter/leakdetector.py
CHANGED
@@ -1,170 +1,170 @@
|
|
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
|
-
|
8
|
-
import json
|
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 = {
|
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
|
-
# Optionally trigger external alarm here (e.g., email notification)
|
129
|
-
self.warning(
|
130
|
-
f"LEAK SUSPECT",
|
131
|
-
"Flow {flow_rate} lpm, last detected motion {self.amotion_last_detected_tss}",
|
132
|
-
)
|
133
|
-
|
134
|
-
self.zero_usage_periods_count = 0
|
135
|
-
# self.debug(f"LeakDetector: flow {flow_rate} liters per minute detected")
|
136
|
-
else:
|
137
|
-
self.zero_usage_periods_count += 1
|
138
|
-
if self.zero_usage_periods_count > self.conseq_zero_periods:
|
139
|
-
self.leak_detected = False
|
140
|
-
self.motion_last_detected_ts = current_ts
|
141
|
-
# self.debug(
|
142
|
-
# f"Leak status reset after {self.zero_usage_periods_count} subsequent reports with no flow"
|
143
|
-
# )
|
144
|
-
self.publish_leak_status(current_ts, self.leak_detected)
|
145
|
-
|
146
|
-
def process_motion_data(self, data: dict[str, float]) -> None:
|
147
|
-
"""
|
148
|
-
Update the last detected motion timestamp.
|
149
|
-
|
150
|
-
Args:
|
151
|
-
data (dict): Motion sensor data containing timestamp.
|
152
|
-
"""
|
153
|
-
if "motion" in data and data["motion"]:
|
154
|
-
# self.debug(f"Leak detection period reset, motion detected")
|
155
|
-
self.motion_last_detected_ts = data["ts"]
|
156
|
-
|
157
|
-
@override
|
158
|
-
def to_dict(self) -> dict[str, Any]:
|
159
|
-
data = super().to_dict()
|
160
|
-
attributes = {attr: getattr(self, attr) for attr in self._LEAKDETECTOR_ATTRS}
|
161
|
-
data[self._LEAKDETECTOR] = attributes
|
162
|
-
return data
|
163
|
-
|
164
|
-
@override
|
165
|
-
def from_dict(self, data: dict[str, Any]) -> None:
|
166
|
-
super().from_dict(data)
|
167
|
-
if self._LEAKDETECTOR in data:
|
168
|
-
attributes = data[self._LEAKDETECTOR]
|
169
|
-
for attr in self._LEAKDETECTOR_ATTRS:
|
170
|
-
setattr(self, attr, attributes.get(attr, None))
|
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
|
+
|
8
|
+
import json
|
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 = {
|
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
|
+
# Optionally trigger external alarm here (e.g., email notification)
|
129
|
+
self.warning(
|
130
|
+
f"LEAK SUSPECT",
|
131
|
+
"Flow {flow_rate} lpm, last detected motion {self.amotion_last_detected_tss}",
|
132
|
+
)
|
133
|
+
|
134
|
+
self.zero_usage_periods_count = 0
|
135
|
+
# self.debug(f"LeakDetector: flow {flow_rate} liters per minute detected")
|
136
|
+
else:
|
137
|
+
self.zero_usage_periods_count += 1
|
138
|
+
if self.zero_usage_periods_count > self.conseq_zero_periods:
|
139
|
+
self.leak_detected = False
|
140
|
+
self.motion_last_detected_ts = current_ts
|
141
|
+
# self.debug(
|
142
|
+
# f"Leak status reset after {self.zero_usage_periods_count} subsequent reports with no flow"
|
143
|
+
# )
|
144
|
+
self.publish_leak_status(current_ts, self.leak_detected)
|
145
|
+
|
146
|
+
def process_motion_data(self, data: dict[str, float]) -> None:
|
147
|
+
"""
|
148
|
+
Update the last detected motion timestamp.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
data (dict): Motion sensor data containing timestamp.
|
152
|
+
"""
|
153
|
+
if "motion" in data and data["motion"]:
|
154
|
+
# self.debug(f"Leak detection period reset, motion detected")
|
155
|
+
self.motion_last_detected_ts = data["ts"]
|
156
|
+
|
157
|
+
@override
|
158
|
+
def to_dict(self) -> dict[str, Any]:
|
159
|
+
data = super().to_dict()
|
160
|
+
attributes = {attr: getattr(self, attr) for attr in self._LEAKDETECTOR_ATTRS}
|
161
|
+
data[self._LEAKDETECTOR] = attributes
|
162
|
+
return data
|
163
|
+
|
164
|
+
@override
|
165
|
+
def from_dict(self, data: dict[str, Any]) -> None:
|
166
|
+
super().from_dict(data)
|
167
|
+
if self._LEAKDETECTOR in data:
|
168
|
+
attributes = data[self._LEAKDETECTOR]
|
169
|
+
for attr in self._LEAKDETECTOR_ATTRS:
|
170
|
+
setattr(self, attr, attributes.get(attr, None))
|