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.
@@ -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",
@@ -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))