juham-systemstatus 0.1.2__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.
@@ -0,0 +1,13 @@
1
+ """
2
+ Description
3
+ ===========
4
+
5
+ System info
6
+
7
+ """
8
+
9
+ from .systemstatus import SystemStatus
10
+
11
+ __all__ = [
12
+ "SystemStatus",
13
+ ]
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,244 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ import json
3
+ import psutil
4
+ import threading
5
+ from typing import Any, Optional, cast
6
+ from typing_extensions import override
7
+ from masterpiece.mqtt import MqttMsg, Mqtt
8
+ from masterpiece import MasterPieceThread
9
+ from juham.core import RThread
10
+ from juham.core.time import timestamp, epoc2utc
11
+
12
+
13
+ class SystemStatusThread(MasterPieceThread):
14
+ """Asynchronous thread for acquiring system info. Currently fetches
15
+ memory usage, disk usage, for each partition and CPU utilization, all in percentages.
16
+ """
17
+
18
+ # class attributes
19
+ _systemstatus_topic: str = ""
20
+ _interval: float = 60 # seconds
21
+ _location = "unknown"
22
+
23
+ def __init__(self, client: Optional[Mqtt] = None):
24
+ """Construct with the given mqtt client. Acquires system metrics
25
+ e.g. CPU load and space left on the device and publishes the data to
26
+ systemstatus_topic.
27
+
28
+ Args:
29
+ client (object, optional): MQTT client. Defaults to None.
30
+ """
31
+ super().__init__(client)
32
+ self.mqtt_client: Optional[Mqtt] = client
33
+
34
+ def init(self, topic: str, interval: float, location: str) -> None:
35
+ """Initialize the data acquisition thread
36
+
37
+ Args:
38
+ topic (str): mqtt topic to publish the acquired system info
39
+ interval (float): update interval in seconds
40
+ location (str): geographic location
41
+ """
42
+ self._systemstatus_topic = topic
43
+ self._interval = interval
44
+ self._location = location
45
+
46
+ def get_thread_counts(self) -> dict[str, int]:
47
+ """Fetch the number of total, active and idle threads in the current process.
48
+
49
+ Returns:
50
+ Thread info (dict[str, int])
51
+ """
52
+ all_threads = threading.enumerate()
53
+ total_threads = len(all_threads)
54
+ active_threads = sum(1 for thread in all_threads if thread.is_alive())
55
+
56
+ return {
57
+ "total_threads": total_threads,
58
+ "active_threads": active_threads,
59
+ "idle_threads": total_threads - active_threads,
60
+ }
61
+
62
+ def get_system_info(self) -> dict[str, dict]:
63
+ """Fetch system info e.g. CPU loads, thread count, disk and ram usage.
64
+
65
+ Returns:
66
+ Thread info (dict[str, dict])
67
+ """
68
+ cpus = psutil.cpu_percent(interval=1, percpu=True) # List of CPU loads per core
69
+
70
+ cpu_loads: dict[str, float] = {}
71
+ i: int = 0
72
+ for cpu in cpus:
73
+ cpu_loads[f"cpu{i}"] = cpu
74
+ i = i + i
75
+
76
+ # Memory info
77
+ memory = psutil.virtual_memory() # Virtual memory details
78
+ available_memory = memory.available # Available memory in bytes
79
+ total_memory = memory.total # Total memory in bytes
80
+
81
+ # Disk space info per partition
82
+ partitions = psutil.disk_partitions()
83
+ disk_info = {}
84
+ for partition in partitions:
85
+ try:
86
+ usage = psutil.disk_usage(partition.mountpoint)
87
+ disk_info[partition.device] = {
88
+ "mountpoint": partition.mountpoint,
89
+ "total": usage.total,
90
+ "free": usage.free,
91
+ "used": usage.used,
92
+ "percent": usage.percent,
93
+ }
94
+ except PermissionError:
95
+ # Skip partitions that we don't have permission to access
96
+ continue
97
+
98
+ return {
99
+ "cpu_loads": cpu_loads,
100
+ "memory": {
101
+ "avail_memory": available_memory,
102
+ "total_memory": total_memory,
103
+ "memory_usage": available_memory / total_memory * 100.0,
104
+ },
105
+ "disk_info": disk_info,
106
+ }
107
+
108
+ @override
109
+ def update_interval(self) -> float:
110
+ return self._interval
111
+
112
+ @override
113
+ def update(self) -> bool:
114
+ sysinfo: dict[str, dict] = self.get_system_info()
115
+ sysinfo.update({"threads": self.get_thread_counts()})
116
+ msg = json.dumps(sysinfo)
117
+ self.publish(self._systemstatus_topic, msg, qos=1, retain=False)
118
+ # self.debug(f"System status published to {self._systemstatus_topic}")
119
+ return True
120
+
121
+
122
+ class SystemStatus(RThread):
123
+ """Constructs a data acquisition thread for reading system status
124
+ info, e.g. available disk space and publishes the data to the systemstatus topic.
125
+
126
+ """
127
+
128
+ _SYSTEMSTATUS: str = "systemstatus"
129
+
130
+ workerThreadId: str = SystemStatusThread.get_class_id()
131
+ update_interval: float = 60
132
+ topic = "system"
133
+ location = "home"
134
+
135
+ def __init__(self, name="systemstatus") -> None:
136
+ """Constructs system status automation object for acquiring and publishing
137
+ system info e.g. available memory and CPU loads.
138
+
139
+ Args:
140
+ name (str, optional): name of the object.
141
+ """
142
+ super().__init__(name)
143
+ self.worker: Optional[SystemStatusThread] = None
144
+ self.systemstatus_topic: str = self.make_topic_name(self.topic)
145
+ self.debug(f"System status with name {name} created")
146
+
147
+ @override
148
+ def on_connect(self, client: object, userdata: Any, flags: int, rc: int) -> None:
149
+ super().on_connect(client, userdata, flags, rc)
150
+ if rc == 0:
151
+ self.subscribe(self.systemstatus_topic)
152
+
153
+ @override
154
+ def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
155
+ if msg.topic == self.systemstatus_topic:
156
+ em = json.loads(msg.payload.decode())
157
+ self.record(timestamp(), em)
158
+ else:
159
+ super().on_message(client, userdata, msg)
160
+
161
+ def record(self, ts: float, info: dict[str, Any]) -> None:
162
+ """Writes system info to the time series database
163
+
164
+ Args:
165
+ ts (float): utc time
166
+ em (dict): energy meter message
167
+ """
168
+
169
+ cpu_loads: dict[str, float] = info["cpu_loads"]
170
+ disk_info: dict[str, float] = info["disk_info"]
171
+ memory: dict[str, int] = info["memory"]
172
+ threads: dict[str, int] = info["threads"]
173
+
174
+ try:
175
+ self.write_point(
176
+ "systemstatus",
177
+ {"location": self.location, "category": "threads"},
178
+ threads,
179
+ epoc2utc(ts),
180
+ )
181
+ except Exception as e:
182
+ self.error(f"Writing memory to influx failed {str(e)}")
183
+
184
+ try:
185
+ self.write_point(
186
+ "systemstatus",
187
+ {"location": self.location, "category": "memory"},
188
+ memory,
189
+ epoc2utc(ts),
190
+ )
191
+ except Exception as e:
192
+ self.error(f"Writing memory to influx failed {str(e)}")
193
+
194
+ try:
195
+ self.write_point(
196
+ "systemstatus",
197
+ {"location": self.location, "category": "cpu"},
198
+ cpu_loads,
199
+ epoc2utc(ts),
200
+ )
201
+ except Exception as e:
202
+ self.error(f"Writing cpu_loads to influx failed {str(e)}")
203
+
204
+ try:
205
+ value: Any
206
+ for attr, value in disk_info.items():
207
+ self.write_point(
208
+ "systemstatus",
209
+ {"location": self.location, "category": "disk"},
210
+ {"mountpoint": value["mountpoint"], "percent": value["percent"]},
211
+ epoc2utc(ts),
212
+ )
213
+ except Exception as e:
214
+ self.error(f"Writing disk_info to influx failed {str(e)}")
215
+
216
+ @override
217
+ def run(self) -> None:
218
+ # create, initialize and start the asynchronous thread for acquiring forecast
219
+
220
+ self.worker = cast(
221
+ SystemStatusThread, self.instantiate(SystemStatus.workerThreadId)
222
+ )
223
+ self.worker.init(
224
+ self.systemstatus_topic,
225
+ self.update_interval,
226
+ self.location,
227
+ )
228
+ super().run()
229
+
230
+ @override
231
+ def to_dict(self) -> dict:
232
+ data = super().to_dict()
233
+ data[self._SYSTEMSTATUS] = {
234
+ "topic": self.systemstatus_topic,
235
+ "interval": self.update_interval,
236
+ }
237
+ return data
238
+
239
+ @override
240
+ def from_dict(self, data) -> None:
241
+ super().from_dict(data)
242
+ if self._SYSTEMSTATUS in data:
243
+ for key, value in data[self._SYSTEMSTATUS].items():
244
+ setattr(self, key, value)
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.1
2
+ Name: juham_systemstatus
3
+ Version: 0.1.2
4
+ Summary: A Weather forecast plugin extending `Juham` applications
5
+ Author-email: J Meskanen <juham.api@gmail.com>
6
+ Maintainer-email: "J. Meskanen" <juham.api@gmail.com>
7
+ License: MIT License
8
+ ===========
9
+
10
+ Copyright (c) 2024, Juha Meskanen
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+
29
+
30
+ Project-URL: Homepage, https://meskanen.com
31
+ Project-URL: Bug Reports, https://meskanen.com
32
+ Project-URL: Funding, https://meskanen.com
33
+ Project-URL: Say Thanks!, http://meskanen.com
34
+ Project-URL: Source, https://meskanen.com
35
+ Keywords: object-oriented,plugin,framework
36
+ Classifier: Development Status :: 2 - Pre-Alpha
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: Topic :: Software Development
39
+ Classifier: License :: OSI Approved :: MIT License
40
+ Classifier: Programming Language :: Python :: 3.8
41
+ Requires-Python: >=3.8
42
+ Description-Content-Type: text/markdown
43
+ Requires-Dist: juham>=0.1.5
44
+ Requires-Dist: pytz>=2024.1
45
+ Requires-Dist: psutil>=5.9.0
46
+ Requires-Dist: importlib-metadata
47
+ Provides-Extra: dev
48
+ Requires-Dist: check-manifest; extra == "dev"
@@ -0,0 +1,8 @@
1
+ juham_systemstatus/__init__.py,sha256=QNlqbx5_OuhcdnTI1y2gbhwWm9Q1TPHA0KwuRZFl3fo,135
2
+ juham_systemstatus/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
3
+ juham_systemstatus/systemstatus.py,sha256=og50rzdekR5WrNahnUb4uZD7RweUBY2C6iDnVfIeD1w,8509
4
+ juham_systemstatus-0.1.2.dist-info/METADATA,sha256=qwA6UuKcxi_Ts1CGBBQ_p_5ZUlRPSJEWiQUss2fl-4U,2286
5
+ juham_systemstatus-0.1.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
6
+ juham_systemstatus-0.1.2.dist-info/entry_points.txt,sha256=8ibdfYNmRXQ2VYHmCkMpx3gI39Vd-hVUMF2Jx2goQbU,70
7
+ juham_systemstatus-0.1.2.dist-info/top_level.txt,sha256=s5k8_U-Q95qDGgjtC6l1tniPEILNDQ3U5qxVGXoAhLc,19
8
+ juham_systemstatus-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [juham.plugins]
2
+ systemstatus_plugin = juham_systemstatus:SystemStatus
@@ -0,0 +1 @@
1
+ juham_systemstatus