juham_core.juham_thread — juham-shelly documentation

# 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

    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")

        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

        @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()

        @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)

        @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
                )

        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))

        @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}")