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.
@@ -1,100 +1,100 @@
1
- """Water meter time series recorder
2
-
3
- """
4
-
5
- import json
6
-
7
- from typing import Any
8
- from typing_extensions import override
9
- from masterpiece.mqtt import MqttMsg
10
-
11
- from juham_core.timeutils import epoc2utc
12
- from juham_core import JuhamTs
13
-
14
-
15
- class WaterMeterTs(JuhamTs):
16
- """Watermeter timeseries recorder. Listens the watermeter MQTT topic
17
- and writes the measurements to timeseries database.
18
-
19
- """
20
-
21
- _WATERMETER: str = "watermeter_ts"
22
- _WATERMETER_ATTRS: list[str] = [
23
- "topic",
24
- ]
25
-
26
- topic = "watermeter" # topic to listen
27
-
28
- def __init__(self, name="watermeter_ts") -> None:
29
- """Watermeter recorder, for listening watermeter MQTT topic and saving
30
- measurements to timeseries database.
31
-
32
- Args:
33
- name (str, optional): name of the object.
34
- """
35
- super().__init__(name)
36
- self.watermeter_topic: str = self.make_topic_name(self.topic)
37
-
38
- @override
39
- def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
40
- super().on_connect(client, userdata, flags, rc)
41
- if rc == 0:
42
- self.subscribe(self.watermeter_topic)
43
-
44
- @override
45
- def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
46
- if msg.topic == self.watermeter_topic:
47
- em = json.loads(msg.payload.decode())
48
- self.record(em)
49
- else:
50
- super().on_message(client, userdata, msg)
51
-
52
- def record(self, info: dict[str, float]) -> None:
53
- """Writes system info to the time series database
54
-
55
- Args:
56
- ts (float): utc time
57
- em (dict): water meter message
58
- """
59
-
60
- try:
61
- if "leak_suspected" in info:
62
- self.write_point(
63
- "watermeter",
64
- {"sensor": info["sensor"], "location": info["location"]},
65
- {
66
- "leak_suspected": info["leak_suspected"],
67
- "ts": info["ts"],
68
- },
69
- epoc2utc(info["ts"]),
70
- )
71
- elif "active_lpm" in info:
72
- self.write_point(
73
- "watermeter",
74
- {"sensor": info["sensor"], "location": info["location"]},
75
- {
76
- "total_liter": info["total_liter"],
77
- "active_lpm": info["active_lpm"],
78
- "ts": info["ts"],
79
- },
80
- epoc2utc(info["ts"]),
81
- )
82
- except Exception as e:
83
- self.error(f"Writing memory to influx failed {str(e)}")
84
-
85
- @override
86
- def to_dict(self) -> dict[str, Any]:
87
- data = super().to_dict() # Call super class method
88
- watermeter_data = {}
89
- for attr in self._WATERMETER_ATTRS:
90
- watermeter_data[attr] = getattr(self, attr)
91
- data[self._WATERMETER] = watermeter_data
92
- return data
93
-
94
- @override
95
- def from_dict(self, data: dict[str, Any]) -> None:
96
- super().from_dict(data) # Call super class method
97
- if self._WATERMETER in data:
98
- watermeter_data = data[self._WATERMETER]
99
- for attr in self._WATERMETER_ATTRS:
100
- setattr(self, attr, watermeter_data.get(attr, None))
1
+ """Water meter time series recorder
2
+
3
+ """
4
+
5
+ import json
6
+
7
+ from typing import Any
8
+ from typing_extensions import override
9
+ from masterpiece.mqtt import MqttMsg
10
+
11
+ from juham_core.timeutils import epoc2utc
12
+ from juham_core import JuhamTs
13
+
14
+
15
+ class WaterMeterTs(JuhamTs):
16
+ """Watermeter timeseries recorder. Listens the watermeter MQTT topic
17
+ and writes the measurements to timeseries database.
18
+
19
+ """
20
+
21
+ _WATERMETER: str = "watermeter_ts"
22
+ _WATERMETER_ATTRS: list[str] = [
23
+ "topic",
24
+ ]
25
+
26
+ topic = "watermeter" # topic to listen
27
+
28
+ def __init__(self, name="watermeter_ts") -> None:
29
+ """Watermeter recorder, for listening watermeter MQTT topic and saving
30
+ measurements to timeseries database.
31
+
32
+ Args:
33
+ name (str, optional): name of the object.
34
+ """
35
+ super().__init__(name)
36
+ self.watermeter_topic: str = self.make_topic_name(self.topic)
37
+
38
+ @override
39
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
40
+ super().on_connect(client, userdata, flags, rc)
41
+ if rc == 0:
42
+ self.subscribe(self.watermeter_topic)
43
+
44
+ @override
45
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
46
+ if msg.topic == self.watermeter_topic:
47
+ em = json.loads(msg.payload.decode())
48
+ self.record(em)
49
+ else:
50
+ super().on_message(client, userdata, msg)
51
+
52
+ def record(self, info: dict[str, float]) -> None:
53
+ """Writes system info to the time series database
54
+
55
+ Args:
56
+ ts (float): utc time
57
+ em (dict): water meter message
58
+ """
59
+
60
+ try:
61
+ if "leak_suspected" in info:
62
+ self.write_point(
63
+ "watermeter",
64
+ {"sensor": info["sensor"], "location": info["location"]},
65
+ {
66
+ "leak_suspected": info["leak_suspected"],
67
+ "ts": info["ts"],
68
+ },
69
+ epoc2utc(info["ts"]),
70
+ )
71
+ elif "active_lpm" in info:
72
+ self.write_point(
73
+ "watermeter",
74
+ {"sensor": info["sensor"], "location": info["location"]},
75
+ {
76
+ "total_liter": info["total_liter"],
77
+ "active_lpm": info["active_lpm"],
78
+ "ts": info["ts"],
79
+ },
80
+ epoc2utc(info["ts"]),
81
+ )
82
+ except Exception as e:
83
+ self.error(f"Writing memory to influx failed {str(e)}")
84
+
85
+ @override
86
+ def to_dict(self) -> dict[str, Any]:
87
+ data = super().to_dict() # Call super class method
88
+ watermeter_data = {}
89
+ for attr in self._WATERMETER_ATTRS:
90
+ watermeter_data[attr] = getattr(self, attr)
91
+ data[self._WATERMETER] = watermeter_data
92
+ return data
93
+
94
+ @override
95
+ def from_dict(self, data: dict[str, Any]) -> None:
96
+ super().from_dict(data) # Call super class method
97
+ if self._WATERMETER in data:
98
+ watermeter_data = data[self._WATERMETER]
99
+ for attr in self._WATERMETER_ATTRS:
100
+ setattr(self, attr, watermeter_data.get(attr, None))
@@ -1,174 +1,172 @@
1
- """Web camera with basic image processing features.
2
-
3
- """
4
-
5
- import cv2
6
- import numpy as np
7
- from typing import Any, Optional
8
- from typing_extensions import override
9
- from masterpiece.mqtt import Mqtt
10
- from masterpiece import MasterPieceThread
11
- from juham_core import JuhamThread
12
- from juham_core.timeutils import timestamp
13
-
14
-
15
- class WebCameraThread(MasterPieceThread):
16
- """Asynchronous thread for capturing and processing images of web camera."""
17
-
18
- # class attributes
19
- _interval: float = 60 # seconds
20
- _location = "unknown"
21
- _camera: int = 0 # e.g. 0 built-in, 1 external camera
22
- _expected_image_size: int = 640 * 480
23
-
24
- def __init__(self, client: Optional[Mqtt] = None):
25
- """Construct with the given mqtt client. Initializes minimal 1x1 image
26
- with timestamp 0.0, which are updated to actual image size and timestamp
27
- with each captured image, for sub classes to process.
28
-
29
- Args:
30
- client (object, optional): MQTT client. Defaults to None.
31
- """
32
- super().__init__(client)
33
- self.mqtt_client: Optional[Mqtt] = client
34
- self.image: np.ndarray = np.zeros((1, 1, 3), dtype=np.uint8)
35
- self.image_timestamp: float = 0.0
36
-
37
- def init(
38
- self,
39
- interval: float,
40
- location: str,
41
- camera: int,
42
- ) -> None:
43
- """Initialize the data acquisition thread
44
-
45
- Args:
46
- interval (float): update interval in seconds
47
- location (str): geographic location
48
- camera(int) : ordinal specifying the camera to be used (0, 1)
49
-
50
- """
51
- self._interval = interval
52
- self._location = location
53
- self._camera = camera
54
-
55
- def capture_image(self) -> np.ndarray:
56
- """
57
- Captures an image from the webcam and returns the image as a numpy array.
58
-
59
- Returns:
60
- np.ndarray: The captured image in the form of a NumPy array.
61
- """
62
- # Initialize the webcam (0 for the built-in camera or 1 for the USB webcam)
63
- cap = cv2.VideoCapture(self._camera)
64
-
65
- # Check if the webcam is opened correctly
66
- if not cap.isOpened():
67
- print("CANNOT ACCESS THE CAMERA")
68
- self.error(f"Could not access the camera {self._camera}.")
69
- return np.zeros(
70
- (1, 1, 3), dtype=np.uint8
71
- ) # Return an empty array if the camera isn't accessible
72
-
73
- # Capture a frame
74
- try:
75
- ret, frame = cap.read()
76
-
77
- if not ret:
78
- print("WTF, Camera failed again")
79
- self.error("Could not capture image.")
80
- frame = np.zeros(
81
- (1, 1, 3), dtype=np.uint8
82
- ) # Return 1x1 array if capture failed
83
- finally:
84
- cap.release()
85
- print("Camera released")
86
- return frame # Return the captured image
87
-
88
- def process_image(self, image: np.ndarray) -> np.ndarray:
89
- """
90
- Processes the captured image by converting it to grayscale and applying thresholding.
91
-
92
- Args:
93
- image (np.ndarray): The input image to process.
94
-
95
- Returns:
96
- np.ndarray: The processed image after grayscale conversion and thresholding.
97
- """
98
- if image.size < self._expected_image_size: # Check if the image is empty
99
- self.error("Received an empty image for processing.")
100
- return image
101
-
102
- # Convert the image to grayscale
103
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
104
- return gray
105
-
106
- def enhance_contrast(self, image: np.ndarray) -> np.ndarray:
107
- """
108
- Enhances the contrast of a grayscale image while suppressing noise.
109
-
110
- Args:
111
- image (np.ndarray): The input grayscale image.
112
-
113
- Returns:
114
- np.ndarray: The contrast-enhanced image with reduced noise amplification.
115
- """
116
- if image.ndim != 2: # Ensure the image is grayscale
117
- self.error("Contrast enhancement requires a grayscale image.")
118
- return image
119
-
120
- # Apply a slight Gaussian blur to reduce noise before enhancing contrast
121
- blurred = cv2.GaussianBlur(image, (3, 3), 0)
122
-
123
- # Apply CLAHE with conservative parameters to avoid over-enhancement
124
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
125
- contrast_enhanced = clahe.apply(blurred)
126
-
127
- return contrast_enhanced
128
-
129
- @override
130
- def update_interval(self) -> float:
131
- return self._interval
132
-
133
- @override
134
- def update(self) -> bool:
135
- self.image = self.capture_image()
136
- self.image_timestamp = timestamp()
137
- return self.image.size == self._expected_image_size
138
-
139
-
140
- class WebCamera(JuhamThread):
141
- """Base class for web camera."""
142
-
143
- _WEBCAMERA: str = "webcamera"
144
- _WEBCAMERA_ATTRS: list[str] = ["location", "camera"]
145
-
146
- _workerThreadId: str = WebCameraThread.get_class_id()
147
- update_interval: float = 60
148
- location = "home"
149
- camera: int = 0
150
-
151
- def __init__(self, name="webcam") -> None:
152
- """Constructs system status automation object for acquiring and publishing
153
- system info e.g. available memory and CPU loads.
154
-
155
- Args:
156
- name (str, optional): name of the object.
157
- """
158
- super().__init__(name)
159
- self.worker: Optional[WebCameraThread] = None
160
-
161
- def to_dict(self) -> dict[str, Any]:
162
- data = super().to_dict() # Call parent class method
163
- webcam_data = {}
164
- for attr in self._WEBCAMERA_ATTRS:
165
- webcam_data[attr] = getattr(self, attr)
166
- data[self._WEBCAMERA] = webcam_data
167
- return data
168
-
169
- def from_dict(self, data: dict[str, Any]) -> None:
170
- super().from_dict(data) # Call parent class method
171
- if self._WEBCAMERA in data:
172
- webcam_data = data[self._WEBCAMERA]
173
- for attr in self._WEBCAMERA_ATTRS:
174
- setattr(self, attr, webcam_data.get(attr, None))
1
+ """Web camera with basic image processing features.
2
+
3
+ """
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from typing import Any, Optional
8
+ from typing_extensions import override
9
+ from masterpiece.mqtt import Mqtt
10
+ from masterpiece import MasterPieceThread
11
+ from juham_core import JuhamThread
12
+ from juham_core.timeutils import timestamp
13
+
14
+
15
+ class WebCameraThread(MasterPieceThread):
16
+ """Asynchronous thread for capturing and processing images of web camera."""
17
+
18
+ # class attributes
19
+ _interval: float = 60 # seconds
20
+ _location = "unknown"
21
+ _camera: int = 0 # e.g. 0 built-in, 1 external camera
22
+ _expected_image_size: int = 640 * 480
23
+
24
+ def __init__(self, client: Optional[Mqtt] = None):
25
+ """Construct with the given mqtt client. Initializes minimal 1x1 image
26
+ with timestamp 0.0, which are updated to actual image size and timestamp
27
+ with each captured image, for sub classes to process.
28
+
29
+ Args:
30
+ client (object, optional): MQTT client. Defaults to None.
31
+ """
32
+ super().__init__(client)
33
+ self.mqtt_client: Optional[Mqtt] = client
34
+ self.image: np.ndarray = np.zeros((1, 1, 3), dtype=np.uint8)
35
+ self.image_timestamp: float = 0.0
36
+
37
+ def init(
38
+ self,
39
+ interval: float,
40
+ location: str,
41
+ camera: int,
42
+ ) -> None:
43
+ """Initialize the data acquisition thread
44
+
45
+ Args:
46
+ interval (float): update interval in seconds
47
+ location (str): geographic location
48
+ camera(int) : ordinal specifying the camera to be used (0, 1)
49
+
50
+ """
51
+ self._interval = interval
52
+ self._location = location
53
+ self._camera = camera
54
+
55
+ def capture_image(self) -> np.ndarray:
56
+ """
57
+ Captures an image from the webcam and returns the image as a numpy array.
58
+
59
+ Returns:
60
+ np.ndarray: The captured image in the form of a NumPy array.
61
+ """
62
+ # Initialize the webcam (0 for the built-in camera or 1 for the USB webcam)
63
+ cap = cv2.VideoCapture(self._camera)
64
+
65
+ # Check if the webcam is opened correctly
66
+ if not cap.isOpened():
67
+ print("CANNOT ACCESS THE CAMERA")
68
+ self.error(f"Could not access the camera {self._camera}.")
69
+ return np.zeros(
70
+ (1, 1, 3), dtype=np.uint8
71
+ ) # Return an empty array if the camera isn't accessible
72
+
73
+ # Capture a frame
74
+ try:
75
+ ret, frame = cap.read()
76
+ if not ret:
77
+ print("WTF, Camera failed")
78
+ self.error("Could not capture image.")
79
+ frame = np.zeros(
80
+ (1, 1, 3), dtype=np.uint8
81
+ ) # Return 1x1 array if capture failed
82
+ finally:
83
+ cap.release()
84
+ return frame # Return the captured image
85
+
86
+ def process_image(self, image: np.ndarray) -> np.ndarray:
87
+ """
88
+ Processes the captured image by converting it to grayscale and applying thresholding.
89
+
90
+ Args:
91
+ image (np.ndarray): The input image to process.
92
+
93
+ Returns:
94
+ np.ndarray: The processed image after grayscale conversion and thresholding.
95
+ """
96
+ if image.size < self._expected_image_size: # Check if the image is empty
97
+ self.error("Received an empty image for processing.")
98
+ return image
99
+
100
+ # Convert the image to grayscale
101
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
102
+ return gray
103
+
104
+ def enhance_contrast(self, image: np.ndarray) -> np.ndarray:
105
+ """
106
+ Enhances the contrast of a grayscale image while suppressing noise.
107
+
108
+ Args:
109
+ image (np.ndarray): The input grayscale image.
110
+
111
+ Returns:
112
+ np.ndarray: The contrast-enhanced image with reduced noise amplification.
113
+ """
114
+ if image.ndim != 2: # Ensure the image is grayscale
115
+ self.error("Contrast enhancement requires a grayscale image.")
116
+ return image
117
+
118
+ # Apply a slight Gaussian blur to reduce noise before enhancing contrast
119
+ blurred = cv2.GaussianBlur(image, (3, 3), 0)
120
+
121
+ # Apply CLAHE with conservative parameters to avoid over-enhancement
122
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
123
+ contrast_enhanced = clahe.apply(blurred)
124
+
125
+ return contrast_enhanced
126
+
127
+ @override
128
+ def update_interval(self) -> float:
129
+ return self._interval
130
+
131
+ @override
132
+ def update(self) -> bool:
133
+ self.image = self.capture_image()
134
+ self.image_timestamp = timestamp()
135
+ return self.image.size == self._expected_image_size
136
+
137
+
138
+ class WebCamera(JuhamThread):
139
+ """Base class for web camera."""
140
+
141
+ _WEBCAMERA: str = "webcamera"
142
+ _WEBCAMERA_ATTRS: list[str] = ["location", "camera"]
143
+
144
+ _workerThreadId: str = WebCameraThread.get_class_id()
145
+ update_interval: float = 60
146
+ location = "home"
147
+ camera: int = 0
148
+
149
+ def __init__(self, name="webcam") -> None:
150
+ """Constructs system status automation object for acquiring and publishing
151
+ system info e.g. available memory and CPU loads.
152
+
153
+ Args:
154
+ name (str, optional): name of the object.
155
+ """
156
+ super().__init__(name)
157
+ self.worker: Optional[WebCameraThread] = None
158
+
159
+ def to_dict(self) -> dict[str, Any]:
160
+ data = super().to_dict() # Call parent class method
161
+ webcam_data = {}
162
+ for attr in self._WEBCAMERA_ATTRS:
163
+ webcam_data[attr] = getattr(self, attr)
164
+ data[self._WEBCAMERA] = webcam_data
165
+ return data
166
+
167
+ def from_dict(self, data: dict[str, Any]) -> None:
168
+ super().from_dict(data) # Call parent class method
169
+ if self._WEBCAMERA in data:
170
+ webcam_data = data[self._WEBCAMERA]
171
+ for attr in self._WEBCAMERA_ATTRS:
172
+ setattr(self, attr, webcam_data.get(attr, None))
@@ -1,22 +1,22 @@
1
- MIT License
2
- ===========
3
-
4
- Copyright (c) 2024, Juha Meskanen
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
-
1
+ MIT License
2
+ ===========
3
+
4
+ Copyright (c) 2024, Juha Meskanen
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+