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
@@ -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))
|
juham_watermeter/webcamera.py
CHANGED
@@ -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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
contrast_enhanced
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
self.image
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
+
|