Source code for juham_core.juham_thread
"""
The `rthread` module provides foundational classes for creating multi-threaded automation objects.
Classes:
AutomationObject: A generic base class for automation objects.
IWorkerThread: A base class for threads that can be spawned by automation objects.
These classes are highly flexible and designed to handle various tasks asynchronously,
making them suitable for a wide range of applications.
Justification for subclassing from `Thread`: sharing the common memory space.
.. todo:: Decouple the functionality from the thread so that it
can be run by any means, e.g., by process or asyncio.
"""
import json
import time
from typing import Any, Optional, cast
from typing_extensions import override
from masterpiece import MasterPieceThread
from masterpiece.mqtt import MqttMsg
from .juham_ts import JuhamTs
[docs]
class JuhamThread(JuhamTs):
"""Base class of automation classes that need to run automation tasks using asynchronously running thread.
Spawns the thread upon creation.
Subscribes to 'event' topic to listen log events from the thread, and dispatches
them to corresponding logging methods e.g. `self.info()`.
"""
_systemstatus_topic = "status"
def __init__(self, name: str) -> None:
"""Construct automation object. By default no thread is created nor started.
Args:
name (str): name of the automation object.
"""
super().__init__(name)
self.worker: Optional[MasterPieceThread]
self.event_topic = self.make_topic_name("event")
[docs]
def disconnect(self) -> None:
"""Request the asynchronous acquisition thread to stop after it has finished its current job.
This method does not wait for the thread to stop. See `shutdown()`.
"""
if self.worker != None:
worker: MasterPieceThread = cast(MasterPieceThread, self.worker)
worker.stay = False
[docs]
@override
def shutdown(self) -> None:
if self.worker is not None:
self.worker.stop() # request to thread to exit its processing loop
self.worker.join() # wait for the thread to complete
super().shutdown()
[docs]
@override
def on_message(self, client: object, userdata: Any, msg: MqttMsg) -> None:
start_time = time.time()
if msg.topic == self.event_topic:
em = json.loads(msg.payload.decode())
self.on_event(em)
else:
self.error(f"Unknown message to {self.name}: {msg.topic}")
end_time: float = time.time()
elapsed_time = end_time - start_time
self.update_metrics(elapsed_time)
[docs]
@override
def update_metrics(self, elapsed: float) -> None:
super().update_metrics(elapsed)
if self._elapsed > 2.0:
sysinfo: dict[str, dict] = {
"threads": {self.name: self.acquire_time_spent()}
}
self.publish(
self._systemstatus_topic, json.dumps(sysinfo), qos=0, retain=False
)
[docs]
def on_event(self, em: dict[str, Any]) -> None:
"""Notification event callback e.g info or warning.
Args:
em (dictionary): dictionary describing the event
"""
if em["type"] == "Info":
self.info(em["msg"], em["details"])
elif em["type"] == "Debug":
self.debug(em["msg"], em["details"])
elif em["type"] == "Warning":
self.warning(em["msg"], em["details"])
elif em["type"] == "Error":
self.error(em["msg"], em["details"])
else:
self.error("PANIC: unknown event type " + em["type"], str(em))
[docs]
@override
def run(self) -> None:
"""Initialize and start the asynchronous acquisition thread."""
super().run()
if self.worker is not None:
self.worker.mqtt_client = self.mqtt_client
self.worker.name = "thread_" + self.name
self.worker.event_topic = self.event_topic
self.worker.start()
self.info(f"Starting up {self.name} - {self.worker.__class__} ")
else:
self.warning(f"No thread, cannot run {self.name}")