juham-core 0.0.0__tar.gz
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_core-0.0.0/LICENSE.rst +25 -0
- juham_core-0.0.0/MANIFEST.in +12 -0
- juham_core-0.0.0/PKG-INFO +98 -0
- juham_core-0.0.0/README.rst +42 -0
- juham_core-0.0.0/juham_core/__init__.py +19 -0
- juham_core-0.0.0/juham_core/juham.py +497 -0
- juham_core-0.0.0/juham_core/rcloud.py +95 -0
- juham_core-0.0.0/juham_core/rthread.py +113 -0
- juham_core-0.0.0/juham_core/timeutils.py +171 -0
- juham_core-0.0.0/juham_core.egg-info/PKG-INFO +98 -0
- juham_core-0.0.0/juham_core.egg-info/SOURCES.txt +16 -0
- juham_core-0.0.0/juham_core.egg-info/dependency_links.txt +1 -0
- juham_core-0.0.0/juham_core.egg-info/requires.txt +10 -0
- juham_core-0.0.0/juham_core.egg-info/top_level.txt +1 -0
- juham_core-0.0.0/pyproject.toml +69 -0
- juham_core-0.0.0/setup.cfg +4 -0
- juham_core-0.0.0/tests/__init__.py +0 -0
- juham_core-0.0.0/tests/test_juham.py +103 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024, Juha Meskanen
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
7
|
+
a copy of this software and associated documentation files (the
|
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
12
|
+
the following conditions:
|
|
13
|
+
|
|
14
|
+
**The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.**
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
20
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
21
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
22
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
23
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
24
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. **
|
|
25
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
include docs/source/README.rst
|
|
2
|
+
include docs/source/CHANGELOG.rst
|
|
3
|
+
include docs/source/LICENSE.rst
|
|
4
|
+
include docs/source/CONTRIBUTING.rst
|
|
5
|
+
|
|
6
|
+
recursive-include examples *
|
|
7
|
+
recursive-include tests *
|
|
8
|
+
recursive-include docs/build/html *
|
|
9
|
+
|
|
10
|
+
# files to be excluded
|
|
11
|
+
global-exclude *~ \#*
|
|
12
|
+
config/*
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: juham-core
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Juha's Ultimate Home Automation Masterpiece
|
|
5
|
+
Author-email: J Meskanen <juham.api@gmail.com>
|
|
6
|
+
Maintainer-email: "J. Meskanen" <juham.api@gmail.com>
|
|
7
|
+
License: LICENSE
|
|
8
|
+
=======
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2024, Juha Meskanen
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
13
|
+
a copy of this software and associated documentation files (the
|
|
14
|
+
"Software"), to deal in the Software without restriction, including
|
|
15
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
16
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
17
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
18
|
+
the following conditions:
|
|
19
|
+
|
|
20
|
+
**The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.**
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
25
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
26
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
27
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
28
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
29
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
30
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. **
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Project-URL: Homepage, https://meskanen.com
|
|
34
|
+
Project-URL: Bug Reports, https://meskanen.com
|
|
35
|
+
Project-URL: Funding, https://meskanen.com
|
|
36
|
+
Project-URL: Say Thanks!, http://meskanen.com
|
|
37
|
+
Project-URL: Source, https://meskanen.com
|
|
38
|
+
Keywords: home,automation,juham
|
|
39
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
40
|
+
Classifier: Intended Audience :: Developers
|
|
41
|
+
Classifier: Topic :: Software Development
|
|
42
|
+
Classifier: License :: Public Domain
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
44
|
+
Requires-Python: >=3.8
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE.rst
|
|
47
|
+
Requires-Dist: masterpiece>=0.1.15
|
|
48
|
+
Requires-Dist: masterpiece_influx>=0.0.2
|
|
49
|
+
Requires-Dist: masterpiece_pahomqtt>=0.0.3
|
|
50
|
+
Requires-Dist: requests>=2.31
|
|
51
|
+
Requires-Dist: pytz>=2024.1
|
|
52
|
+
Requires-Dist: importlib-metadata
|
|
53
|
+
Provides-Extra: dev
|
|
54
|
+
Requires-Dist: check-manifest; extra == "dev"
|
|
55
|
+
Requires-Dist: types-pyz; extra == "dev"
|
|
56
|
+
|
|
57
|
+
Welcome to Juham™ - Juha's Ultimate Home Automation Masterpiece
|
|
58
|
+
===============================================================
|
|
59
|
+
|
|
60
|
+
Description
|
|
61
|
+
-----------
|
|
62
|
+
|
|
63
|
+
The ``juham-core`` package introduces the ``Juham`` class, the core functionality for Juha's
|
|
64
|
+
ultimate home automation solution.
|
|
65
|
+
|
|
66
|
+
``Juham`` extend the functionality of ``Masterpiece`` object by adding capabilities for MQTT
|
|
67
|
+
communication and integration with time series databases. The base class provides only a minimal
|
|
68
|
+
set of interfaces. Actual features, such as MQTT and time series database implementations — along with home
|
|
69
|
+
automation-specific functionalities — are provided through separate plugin modules built on ``Juham`` class.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Project Status
|
|
73
|
+
--------------
|
|
74
|
+
|
|
75
|
+
**Current State**: **Pre-Alpha (Status 2)**
|
|
76
|
+
|
|
77
|
+
In its current form, Juham™ may still resemble more of a home automation experiment (or even a "mess") than
|
|
78
|
+
a "masterpiece," but I'm working hard to reach that goal!
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
Goals
|
|
82
|
+
-----
|
|
83
|
+
|
|
84
|
+
The aim of Juham™ is to have fun by learning Python and GitLab ecosystems, by developing a home automation
|
|
85
|
+
framework capable of controlling all the devices in my home, with the potential to be adapted for other homes as well.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
Special Thanks
|
|
90
|
+
--------------
|
|
91
|
+
|
|
92
|
+
This project would not have been possible without the generous support of two exceptional
|
|
93
|
+
individuals: my friend, **Teppo K.**, and my son, **Mahi**.
|
|
94
|
+
|
|
95
|
+
- Teppo provided the initial spark for this project by donating a Raspberry Pi, a temperature sensor, and an inspiring demonstration of his own home automation system.
|
|
96
|
+
- My son Mahi has been instrumental in translating my ideas into Python code, offering invaluable support and encouragement throughout the development process.
|
|
97
|
+
|
|
98
|
+
I am deeply grateful to both of you — thank you!
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Welcome to Juham™ - Juha's Ultimate Home Automation Masterpiece
|
|
2
|
+
===============================================================
|
|
3
|
+
|
|
4
|
+
Description
|
|
5
|
+
-----------
|
|
6
|
+
|
|
7
|
+
The ``juham-core`` package introduces the ``Juham`` class, the core functionality for Juha's
|
|
8
|
+
ultimate home automation solution.
|
|
9
|
+
|
|
10
|
+
``Juham`` extend the functionality of ``Masterpiece`` object by adding capabilities for MQTT
|
|
11
|
+
communication and integration with time series databases. The base class provides only a minimal
|
|
12
|
+
set of interfaces. Actual features, such as MQTT and time series database implementations — along with home
|
|
13
|
+
automation-specific functionalities — are provided through separate plugin modules built on ``Juham`` class.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Project Status
|
|
17
|
+
--------------
|
|
18
|
+
|
|
19
|
+
**Current State**: **Pre-Alpha (Status 2)**
|
|
20
|
+
|
|
21
|
+
In its current form, Juham™ may still resemble more of a home automation experiment (or even a "mess") than
|
|
22
|
+
a "masterpiece," but I'm working hard to reach that goal!
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Goals
|
|
26
|
+
-----
|
|
27
|
+
|
|
28
|
+
The aim of Juham™ is to have fun by learning Python and GitLab ecosystems, by developing a home automation
|
|
29
|
+
framework capable of controlling all the devices in my home, with the potential to be adapted for other homes as well.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Special Thanks
|
|
34
|
+
--------------
|
|
35
|
+
|
|
36
|
+
This project would not have been possible without the generous support of two exceptional
|
|
37
|
+
individuals: my friend, **Teppo K.**, and my son, **Mahi**.
|
|
38
|
+
|
|
39
|
+
- Teppo provided the initial spark for this project by donating a Raspberry Pi, a temperature sensor, and an inspiring demonstration of his own home automation system.
|
|
40
|
+
- My son Mahi has been instrumental in translating my ideas into Python code, offering invaluable support and encouragement throughout the development process.
|
|
41
|
+
|
|
42
|
+
I am deeply grateful to both of you — thank you!
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
Base classes for Juham - Juha's Ultimate Home Automation framework
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .juham import Juham
|
|
10
|
+
from .rcloud import RCloud, RCloudThread
|
|
11
|
+
from .rthread import RThread, MasterPieceThread
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Juham",
|
|
15
|
+
"RThread",
|
|
16
|
+
"RCloud",
|
|
17
|
+
"RCloudThread",
|
|
18
|
+
"MasterPieceThread",
|
|
19
|
+
]
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import traceback
|
|
3
|
+
from typing import Any, Dict, Optional, cast, Union
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
from masterpiece.mqtt import Mqtt, MqttMsg
|
|
7
|
+
from masterpiece import MasterPiece, URL
|
|
8
|
+
from masterpiece.timeseries import TimeSeries, Measurement
|
|
9
|
+
from .timeutils import timestamp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Juham(MasterPiece):
|
|
13
|
+
"""Base class for automation objects with MQTT networking and time series data storage.
|
|
14
|
+
|
|
15
|
+
To configure the class to use a specific MQTT and database set
|
|
16
|
+
the `database_class_id` and `mqtt_class_id` class attributes to desired
|
|
17
|
+
MQTT and database implementations. When instantiated the object will instantiate
|
|
18
|
+
the given MQTT and database objects with it.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
database_class_id: str = ""
|
|
22
|
+
mqtt_class_id: str = ""
|
|
23
|
+
write_attempts: int = 3
|
|
24
|
+
mqtt_root_topic: str = ""
|
|
25
|
+
mqtt_host: str = "localhost"
|
|
26
|
+
mqtt_port: int = 1883
|
|
27
|
+
|
|
28
|
+
def __init__(self, name: str = "") -> None:
|
|
29
|
+
"""Constructs new automation object with the given name, configured
|
|
30
|
+
time series recorder and MQTT network features.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
name (str): name of the object
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(name)
|
|
36
|
+
self.database_client: Optional[Union[TimeSeries, None]] = None
|
|
37
|
+
self.mqtt_client: Optional[Union[Mqtt, None]] = None
|
|
38
|
+
self.mqtt_topic_base: str = ""
|
|
39
|
+
self.mqtt_topic_control: str = ""
|
|
40
|
+
self.mqtt_topic_log: str = ""
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
44
|
+
data: Dict[str, Any] = super().to_dict()
|
|
45
|
+
data["_base"] = {}
|
|
46
|
+
attributes = ["mqtt_host", "mqtt_port", "mqtt_root_topic", "write_attempts"]
|
|
47
|
+
for attr in attributes:
|
|
48
|
+
if getattr(self, attr) != getattr(type(self), attr):
|
|
49
|
+
data["_base"][attr] = getattr(self, attr)
|
|
50
|
+
if self.database_client is not None:
|
|
51
|
+
data["_database"] = {"db_client": self.database_client.to_dict()}
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
def from_dict(self, data: Dict[str, Any]) -> None:
|
|
56
|
+
super().from_dict(data)
|
|
57
|
+
for key, value in data["_base"].items():
|
|
58
|
+
if key == "db_client":
|
|
59
|
+
self.database_client = cast(
|
|
60
|
+
Optional[TimeSeries], MasterPiece.instantiate(value["_class"])
|
|
61
|
+
)
|
|
62
|
+
if self.database_client is not None:
|
|
63
|
+
self.database_client.from_dict(value)
|
|
64
|
+
else:
|
|
65
|
+
setattr(self, key, value)
|
|
66
|
+
|
|
67
|
+
def initialize(self) -> None:
|
|
68
|
+
"""Initialize time series database and mqtt networking for use. This method must be called
|
|
69
|
+
after the object name has been set .
|
|
70
|
+
"""
|
|
71
|
+
self.init_database(self.name)
|
|
72
|
+
self.init_mqtt(self.name)
|
|
73
|
+
|
|
74
|
+
def measurement(self, name: str) -> Measurement:
|
|
75
|
+
"""Instantiates measurement object.
|
|
76
|
+
Args:
|
|
77
|
+
measurement (str): name of the object
|
|
78
|
+
Returns
|
|
79
|
+
(Measurement) measurement object
|
|
80
|
+
"""
|
|
81
|
+
timeseries: TimeSeries = cast(TimeSeries, self.database_client)
|
|
82
|
+
return timeseries.measurement(name)
|
|
83
|
+
|
|
84
|
+
def init_database(self, name: str) -> None:
|
|
85
|
+
"""Instantiates the configured time series database object.
|
|
86
|
+
|
|
87
|
+
Issues a warning if the :attr:`~database_class_id` has not
|
|
88
|
+
been configured, in which case the object will not have the time series
|
|
89
|
+
recording feature.
|
|
90
|
+
|
|
91
|
+
This method is called internally and typically there is no need to call it
|
|
92
|
+
from the application code.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
Juham.database_class_id != None
|
|
97
|
+
and MasterPiece.find_class(Juham.database_class_id) != None
|
|
98
|
+
):
|
|
99
|
+
self.database_client = cast(
|
|
100
|
+
Optional[TimeSeries], MasterPiece.instantiate(Juham.database_class_id)
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
self.warning("Suspicious configuration: no database_class_id set")
|
|
104
|
+
|
|
105
|
+
def init_topic_base(self) -> None:
|
|
106
|
+
url: URL = self.make_url()
|
|
107
|
+
self.mqtt_root_topic = self.root().make_url().get()[1:]
|
|
108
|
+
self.mqtt_topic_base = url.get()[1:]
|
|
109
|
+
self.mqtt_topic_control = self.mqtt_root_topic + "/control"
|
|
110
|
+
self.mqtt_topic_log = self.mqtt_root_topic + "/log"
|
|
111
|
+
|
|
112
|
+
def make_topic_name(self, topic: str) -> str:
|
|
113
|
+
"""Make topic name for the object. The topic name
|
|
114
|
+
consists of the base name plus the given 'topic'.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
topic (str): topic name
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
str: mqtt topic name
|
|
121
|
+
"""
|
|
122
|
+
return f"{self.mqtt_root_topic}/{topic}"
|
|
123
|
+
|
|
124
|
+
def init_mqtt(self, name: str) -> None:
|
|
125
|
+
"""Instantiates the configured MQTT object for networking. Calls `init_topic()`
|
|
126
|
+
to construct topic base name for the object, and instantiates the mqtt
|
|
127
|
+
client.
|
|
128
|
+
|
|
129
|
+
This method is called internally and typically there is no need to call it
|
|
130
|
+
from the application code.
|
|
131
|
+
|
|
132
|
+
Issues a warning if the :attr:`mqtt_class_id` has not
|
|
133
|
+
been configured, even though objects without a capability to communicate
|
|
134
|
+
are rather crippled.
|
|
135
|
+
"""
|
|
136
|
+
self.init_topic_base()
|
|
137
|
+
if Juham.mqtt_class_id == "":
|
|
138
|
+
self.warning(
|
|
139
|
+
f"Suscpicious configuration: no mqtt_class_id set for {self.name}:{self.get_class_id()}"
|
|
140
|
+
)
|
|
141
|
+
elif not Juham.find_class(Juham.mqtt_class_id):
|
|
142
|
+
self.error(
|
|
143
|
+
f"Couldn't create mqtt broker {Juham.mqtt_class_id}, class not imported"
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
self.mqtt_client = cast(
|
|
147
|
+
Optional[Mqtt], MasterPiece.instantiate(Juham.mqtt_class_id, name)
|
|
148
|
+
)
|
|
149
|
+
if self.mqtt_client is not None:
|
|
150
|
+
self.mqtt_client.on_message = self.on_message
|
|
151
|
+
self.mqtt_client.on_connect = self.on_connect
|
|
152
|
+
self.mqtt_client.on_disconnect = self.on_disconnect
|
|
153
|
+
if (
|
|
154
|
+
self.mqtt_client.connect_to_server(self.mqtt_host, self.mqtt_port)
|
|
155
|
+
!= 0
|
|
156
|
+
):
|
|
157
|
+
self.error(
|
|
158
|
+
f"Couldn't connect to the mqtt broker at {self.mqtt_client.host}"
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
self.debug(
|
|
162
|
+
f"{self.name} with mqtt broker {self.mqtt_client.name} connected to {self.mqtt_client.host}"
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
self.error(f"Couldn't create mqtt broker {Juham.mqtt_class_id}")
|
|
166
|
+
|
|
167
|
+
def subscribe(self, topic: str) -> None:
|
|
168
|
+
"""Subscribe to the given MQTT topic.
|
|
169
|
+
|
|
170
|
+
This method sets up the subscription to the specified MQTT topic and registers
|
|
171
|
+
the :meth:`on_message` method as the callback for incoming messages.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
topic (str): The MQTT topic to subscribe to.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
::
|
|
178
|
+
|
|
179
|
+
# configure
|
|
180
|
+
obj.subscribe('foo/bar')
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
if self.mqtt_client:
|
|
184
|
+
self.mqtt_client.connected_flag = True
|
|
185
|
+
self.mqtt_client.subscribe(topic)
|
|
186
|
+
self.info(f"{self.name} subscribed to { topic}")
|
|
187
|
+
|
|
188
|
+
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
|
|
189
|
+
"""MQTT message notification on arrived message.
|
|
190
|
+
|
|
191
|
+
Called whenever a new message is posted on one of the
|
|
192
|
+
topics the object has subscribed to via subscribe() method.
|
|
193
|
+
This method is the heart of automation: here, derived subclasses should
|
|
194
|
+
automate whatever they were designed to automate. For example, they could switch a
|
|
195
|
+
relay when a boiler temperature sensor signals that the temperature is too low for
|
|
196
|
+
a comforting shower for say one's lovely wife.
|
|
197
|
+
|
|
198
|
+
For more information on this method consult MQTT documentation available
|
|
199
|
+
in many public sources.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
client (obj): MQTT client
|
|
203
|
+
userdata (Any): application specific data
|
|
204
|
+
msg (object): The MQTT message
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
if msg.topic == self.mqtt_topic_control:
|
|
208
|
+
m = json.loads(msg.payload)
|
|
209
|
+
if m["command"] == "shutdown" and self.mqtt_client:
|
|
210
|
+
self.mqtt_client.disconnect()
|
|
211
|
+
self.mqtt_client.loop_stop()
|
|
212
|
+
|
|
213
|
+
def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
|
|
214
|
+
"""Notification on connect.
|
|
215
|
+
|
|
216
|
+
This method is called whenever the MQTT broker is connected.
|
|
217
|
+
For more information on this method consult MQTT documentation available
|
|
218
|
+
in many public sources.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
client (obj): MQTT client
|
|
222
|
+
userdata (Any): application specific data
|
|
223
|
+
flags (int): Consult MQTT
|
|
224
|
+
rc (int): See MQTT docs
|
|
225
|
+
"""
|
|
226
|
+
if self.mqtt_client:
|
|
227
|
+
self.mqtt_client.subscribe(self.mqtt_topic_control)
|
|
228
|
+
self.debug(self.name + " connected to the mqtt broker ")
|
|
229
|
+
|
|
230
|
+
def on_disconnect(self, client: object, userdata: Any, rc: int = 0) -> None:
|
|
231
|
+
"""Notification on disconnect.
|
|
232
|
+
|
|
233
|
+
This method is called whenever the MQTT broker is disconnected.
|
|
234
|
+
For more information on this method consult MQTT documentation available
|
|
235
|
+
in many public sources.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
client (obj): MQTT client
|
|
239
|
+
userdata (Any): application specific data
|
|
240
|
+
rc (int): See MQTT docs
|
|
241
|
+
"""
|
|
242
|
+
self.info(f"{self.name} disconnected from the mqtt broker, {rc} ")
|
|
243
|
+
|
|
244
|
+
def write(self, point: Measurement) -> None:
|
|
245
|
+
"""Writes the given measurement to the database. In case of an error,
|
|
246
|
+
it tries again until the maximum number of attempts is reached. If it
|
|
247
|
+
is still unsuccessful, it gives up and passes the first encountered
|
|
248
|
+
exception to the caller.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
point: a measurement describing a time stamp and related attributes for one measurement.
|
|
252
|
+
"""
|
|
253
|
+
if not self.database_client:
|
|
254
|
+
raise ValueError("Database client is not initialized.")
|
|
255
|
+
|
|
256
|
+
first_exception: Optional[BaseException] = None
|
|
257
|
+
for i in range(self.write_attempts):
|
|
258
|
+
try:
|
|
259
|
+
self.database_client.write(point)
|
|
260
|
+
return
|
|
261
|
+
except Exception as e:
|
|
262
|
+
if first_exception is None:
|
|
263
|
+
first_exception = e
|
|
264
|
+
self.warning(f"Writing ts failed, attempt {i+1}: {repr(e)}")
|
|
265
|
+
|
|
266
|
+
self.log_message(
|
|
267
|
+
"Error",
|
|
268
|
+
f"Writing failed after {self.write_attempts} attempts, giving up",
|
|
269
|
+
"".join(
|
|
270
|
+
traceback.format_exception_only(type(first_exception), first_exception)
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def write_point(
|
|
275
|
+
self, name: str, tags: dict[str, Any], fields: dict[str, Any], ts: str
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Writes the given measurement to the database. In case of an error,
|
|
278
|
+
it tries again until the maximum number of attempts is reached. If it
|
|
279
|
+
is still unsuccessful, it gives up and passes the first encountered
|
|
280
|
+
exception to the caller.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
point: a measurement describing a time stamp and related attributes for one measurement.
|
|
284
|
+
"""
|
|
285
|
+
if not self.database_client:
|
|
286
|
+
raise ValueError("Database client is not initialized.")
|
|
287
|
+
|
|
288
|
+
first_exception: Optional[BaseException] = None
|
|
289
|
+
for i in range(self.write_attempts):
|
|
290
|
+
try:
|
|
291
|
+
self.database_client.write_dict(name, tags, fields, ts)
|
|
292
|
+
return
|
|
293
|
+
except Exception as e:
|
|
294
|
+
if first_exception is None:
|
|
295
|
+
first_exception = e
|
|
296
|
+
self.warning(f"Writing ts failed, attempt {i+1}: {repr(e)}")
|
|
297
|
+
|
|
298
|
+
self.log_message(
|
|
299
|
+
"Error",
|
|
300
|
+
f"Writing failed after {self.write_attempts} attempts, giving up",
|
|
301
|
+
"".join(
|
|
302
|
+
traceback.format_exception_only(type(first_exception), first_exception)
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def read_last_value(
|
|
307
|
+
self,
|
|
308
|
+
measurement: str,
|
|
309
|
+
tags: Optional[dict[str, Any]] = None,
|
|
310
|
+
fields: Optional[list[str]] = None,
|
|
311
|
+
) -> dict[str, Any]:
|
|
312
|
+
"""Writes the given measurement to the database. In case of an error,
|
|
313
|
+
it tries again until the maximum number of attempts is reached. If it
|
|
314
|
+
is still unsuccessful, it gives up and passes the first encountered
|
|
315
|
+
exception to the caller.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
point: a measurement describing a time stamp and related attributes for one measurement.
|
|
319
|
+
"""
|
|
320
|
+
if not self.database_client:
|
|
321
|
+
raise ValueError("Database client is not initialized.")
|
|
322
|
+
|
|
323
|
+
first_exception: Optional[BaseException] = None
|
|
324
|
+
for i in range(self.write_attempts):
|
|
325
|
+
try:
|
|
326
|
+
return self.database_client.read_last_value(measurement, tags, fields)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
if first_exception is None:
|
|
329
|
+
first_exception = e
|
|
330
|
+
self.warning(f"Reading ts failed, attempt {i+1}: {repr(e)}")
|
|
331
|
+
|
|
332
|
+
self.log_message(
|
|
333
|
+
"Error",
|
|
334
|
+
f"Reading failed after {self.write_attempts} attempts, giving up",
|
|
335
|
+
"".join(
|
|
336
|
+
traceback.format_exception_only(type(first_exception), first_exception)
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
return {}
|
|
340
|
+
|
|
341
|
+
def read(self, point: Measurement) -> None:
|
|
342
|
+
"""Reads the given measurement from the database.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
point: point with initialized time stamp.
|
|
346
|
+
|
|
347
|
+
... note: NOT IMPLEMENTED YET
|
|
348
|
+
"""
|
|
349
|
+
# if self.database_client:
|
|
350
|
+
# self.database_client.read(point)
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
@override
|
|
354
|
+
def debug(self, msg: str, details: str = "") -> None:
|
|
355
|
+
"""Logs the given debug message to the database after logging it using
|
|
356
|
+
the BaseClass's info() method.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
msg (str): The information message to be logged.
|
|
360
|
+
details (str): Additional detailed information for the message to be logged
|
|
361
|
+
"""
|
|
362
|
+
super().debug(msg, details)
|
|
363
|
+
self.log_message("Debug", msg, details="")
|
|
364
|
+
|
|
365
|
+
@override
|
|
366
|
+
def info(self, msg: str, details: str = "") -> None:
|
|
367
|
+
"""Logs the given information message to the database after logging it
|
|
368
|
+
using the BaseClass's info() method.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
msg : The information message to be logged.
|
|
372
|
+
details : Additional detailed information for the message to be logged
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
::
|
|
376
|
+
|
|
377
|
+
obj = new Base('test')
|
|
378
|
+
obj.info('Message arrived', str(msg))
|
|
379
|
+
"""
|
|
380
|
+
super().info(msg, details)
|
|
381
|
+
self.log_message("Info", msg, details="")
|
|
382
|
+
|
|
383
|
+
@override
|
|
384
|
+
def warning(self, msg: str, details: str = "") -> None:
|
|
385
|
+
"""Logs the given warning message to the database after logging it
|
|
386
|
+
using the BaseClass's info() method.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
msg (str): The information message to be logged.
|
|
390
|
+
details (str): Additional detailed information for the message to be logged
|
|
391
|
+
"""
|
|
392
|
+
super().warning(msg, details)
|
|
393
|
+
self.log_message("Warn", msg, details)
|
|
394
|
+
|
|
395
|
+
@override
|
|
396
|
+
def error(self, msg: str, details: str = "") -> None:
|
|
397
|
+
"""Logs the given error message to the database after logging it using
|
|
398
|
+
the BaseClass's info() method.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
msg (str): The information message to be logged.
|
|
402
|
+
details (str): Additional detailed information for the message to be logged
|
|
403
|
+
"""
|
|
404
|
+
super().error(msg, details)
|
|
405
|
+
self.log_message("Error", msg, details)
|
|
406
|
+
|
|
407
|
+
def log_message(self, type: str, msg: str, details: str = "") -> None:
|
|
408
|
+
"""Publish the given log message to the MQTT 'log' topic.
|
|
409
|
+
|
|
410
|
+
This method constructs a log message with a timestamp, class type, source name,
|
|
411
|
+
message, and optional details. It then publishes this message to the 'log' topic
|
|
412
|
+
using the MQTT protocol.
|
|
413
|
+
|
|
414
|
+
Parameters:
|
|
415
|
+
type : str
|
|
416
|
+
The classification or type of the log message (e.g., 'Error', 'Info').
|
|
417
|
+
msg : str
|
|
418
|
+
The main log message to be published.
|
|
419
|
+
details : str, optional
|
|
420
|
+
Additional details about the log message (default is an empty string).
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
None
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
Exception
|
|
427
|
+
If there is an issue with the MQTT client while publishing the message.
|
|
428
|
+
|
|
429
|
+
Example:
|
|
430
|
+
::
|
|
431
|
+
|
|
432
|
+
# publish info message to the Juham's 'log' topic
|
|
433
|
+
self.log_message("Info", f"Some cool message {some_stuff}", str(dict))
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
lmsg: dict[str, Any] = {
|
|
438
|
+
"Timestamp": timestamp(),
|
|
439
|
+
"Class": type,
|
|
440
|
+
"Source": self.name,
|
|
441
|
+
"Msg": msg,
|
|
442
|
+
"Details": str(details),
|
|
443
|
+
}
|
|
444
|
+
self.publish(self.mqtt_topic_log, json.dumps(lmsg), 1)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
if self._log is not None:
|
|
447
|
+
self._log.error(f"Publishing log event failed {str(e)}")
|
|
448
|
+
|
|
449
|
+
def publish(self, topic: str, msg: str, qos: int = 1, retain: bool = True) -> None:
|
|
450
|
+
"""Publish the given message to the given MQTT topic.
|
|
451
|
+
For more information consult MQTT.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
topic (str): topic
|
|
455
|
+
msg (str): message to be published
|
|
456
|
+
qos (int, optional): quality of service. Defaults to 1.
|
|
457
|
+
retain (bool, optional): retain. Defaults to True.
|
|
458
|
+
"""
|
|
459
|
+
if self.mqtt_client:
|
|
460
|
+
self.mqtt_client.publish(topic, msg, qos, retain)
|
|
461
|
+
|
|
462
|
+
def shutdown(self) -> None:
|
|
463
|
+
"""Shut down all services, free resources, stop threads, disconnect
|
|
464
|
+
from mqtt, in general, prepare for shutdown."""
|
|
465
|
+
if self.mqtt_client:
|
|
466
|
+
self.mqtt_client.disconnect()
|
|
467
|
+
self.mqtt_client.loop_stop()
|
|
468
|
+
|
|
469
|
+
@override
|
|
470
|
+
def run(self) -> None:
|
|
471
|
+
"""Start a new thread to runs the network loop in the background.
|
|
472
|
+
|
|
473
|
+
Allows the main program to continue executing while the MQTT
|
|
474
|
+
client handles incoming and outgoing messages in the background.
|
|
475
|
+
"""
|
|
476
|
+
self.initialize()
|
|
477
|
+
if self.mqtt_client:
|
|
478
|
+
self.mqtt_client.loop_start()
|
|
479
|
+
super().run()
|
|
480
|
+
|
|
481
|
+
@override
|
|
482
|
+
def run_forever(self) -> None:
|
|
483
|
+
"""Starts the network loop and blocks the main thread, continuously
|
|
484
|
+
running the loop to process MQTT messages.
|
|
485
|
+
|
|
486
|
+
The loop will run indefinitely unless the connection is lost or
|
|
487
|
+
the program is terminated.
|
|
488
|
+
"""
|
|
489
|
+
self.initialize()
|
|
490
|
+
if self.mqtt_client:
|
|
491
|
+
self.info(f"{self.name} has mqtt client, calling forever...")
|
|
492
|
+
self.mqtt_client.loop_forever()
|
|
493
|
+
self.info(f"{self.name} mqtt client run_forever returned")
|
|
494
|
+
else:
|
|
495
|
+
self.error(
|
|
496
|
+
f"{self.name} does NOT have mqtt client, cannot run_forever, giving up"
|
|
497
|
+
)
|