diff --git a/moonraker/common.py b/moonraker/common.py index 1f001de..b35ebba 100644 --- a/moonraker/common.py +++ b/moonraker/common.py @@ -9,7 +9,7 @@ import sys import ipaddress import logging import copy -from enum import Flag, auto +from enum import Enum, Flag, auto from .utils import ServerError, Sentinel from .utils import json_wrapper as jsonw @@ -90,6 +90,39 @@ class TransportType(ExtendedFlag): MQTT = auto() INTERNAL = auto() +class ExtendedEnum(Enum): + @classmethod + def from_string(cls, enum_name: str): + str_name = enum_name.upper() + for name, member in cls.__members__.items(): + if name == str_name: + return cls(member.value) + raise ValueError(f"No enum member named {enum_name}") + + def __str__(self) -> str: + return self._name_.lower() # type: ignore + +class JobEvent(ExtendedEnum): + STANDBY = 1 + STARTED = 2 + PAUSED = 3 + RESUMED = 4 + COMPLETE = 5 + ERROR = 6 + CANCELLED = 7 + + @property + def finished(self) -> bool: + return self.value >= 5 + + @property + def aborted(self) -> bool: + return self.value >= 6 + + @property + def is_printing(self) -> bool: + return self.value in [2, 4] + class Subscribable: def send_status( self, status: Dict[str, Any], eventtime: float diff --git a/moonraker/components/history.py b/moonraker/components/history.py index 6c0fbd1..abb4250 100644 --- a/moonraker/components/history.py +++ b/moonraker/components/history.py @@ -6,6 +6,7 @@ from __future__ import annotations import time import logging from asyncio import Lock +from ..common import JobEvent # Annotation imports from typing import ( @@ -49,15 +50,7 @@ class History: self.server.register_event_handler( "server:klippy_shutdown", self._handle_shutdown) self.server.register_event_handler( - "job_state:started", self._on_job_started) - self.server.register_event_handler( - "job_state:complete", self._on_job_complete) - self.server.register_event_handler( - "job_state:cancelled", self._on_job_cancelled) - self.server.register_event_handler( - "job_state:standby", self._on_job_standby) - self.server.register_event_handler( - "job_state:error", self._on_job_error) + "job_state:state_changed", self._on_job_state_changed) self.server.register_notification("history:history_changed") self.server.register_endpoint( @@ -192,40 +185,25 @@ class History: "moonraker", "history.job_totals", self.job_totals) return {'last_totals': last_totals} - def _on_job_started(self, - prev_stats: Dict[str, Any], - new_stats: Dict[str, Any] - ) -> None: - if self.current_job is not None: - # Finish with the previous state + def _on_job_state_changed( + self, + job_event: JobEvent, + prev_stats: Dict[str, Any], + new_stats: Dict[str, Any] + ) -> None: + if job_event == JobEvent.STARTED: + if self.current_job is not None: + # Finish with the previous state + self.finish_job("cancelled", prev_stats) + self.add_job(PrinterJob(new_stats)) + elif job_event == JobEvent.COMPLETE: + self.finish_job("completed", new_stats) + elif job_event == JobEvent.ERROR: + self.finish_job("error", new_stats) + elif job_event in (JobEvent.CANCELLED, JobEvent.STANDBY): + # Cancel on "standby" for backward compatibility with + # `CLEAR_PAUSE/SDCARD_RESET_FILE` workflow self.finish_job("cancelled", prev_stats) - self.add_job(PrinterJob(new_stats)) - - def _on_job_complete(self, - prev_stats: Dict[str, Any], - new_stats: Dict[str, Any] - ) -> None: - self.finish_job("completed", new_stats) - - def _on_job_cancelled(self, - prev_stats: Dict[str, Any], - new_stats: Dict[str, Any] - ) -> None: - self.finish_job("cancelled", new_stats) - - def _on_job_error(self, - prev_stats: Dict[str, Any], - new_stats: Dict[str, Any] - ) -> None: - self.finish_job("error", new_stats) - - def _on_job_standby(self, - prev_stats: Dict[str, Any], - new_stats: Dict[str, Any] - ) -> None: - # Backward compatibility with - # `CLEAR_PAUSE/SDCARD_RESET_FILE` workflow - self.finish_job("cancelled", prev_stats) def _handle_shutdown(self) -> None: jstate: JobState = self.server.lookup_component("job_state") diff --git a/moonraker/components/job_queue.py b/moonraker/components/job_queue.py index 2748956..2369f15 100644 --- a/moonraker/components/job_queue.py +++ b/moonraker/components/job_queue.py @@ -8,6 +8,7 @@ from __future__ import annotations import asyncio import time import logging +from ..common import JobEvent # Annotation imports from typing import ( @@ -46,11 +47,8 @@ class JobQueue: self.server.register_event_handler( "server:klippy_shutdown", self._handle_shutdown) self.server.register_event_handler( - "job_state:complete", self._on_job_complete) - self.server.register_event_handler( - "job_state:error", self._on_job_abort) - self.server.register_event_handler( - "job_state:cancelled", self._on_job_abort) + "job_state:state_changed", self._on_job_state_changed + ) self.server.register_notification("job_queue:job_queue_changed") self.server.register_remote_method("pause_job_queue", self.pause_queue) @@ -85,10 +83,13 @@ class JobQueue: if not self.queued_jobs and self.automatic: self._set_queue_state("ready") - async def _on_job_complete(self, - prev_stats: Dict[str, Any], - new_stats: Dict[str, Any] - ) -> None: + async def _on_job_state_changed(self, job_event: JobEvent, *args) -> None: + if job_event == JobEvent.COMPLETE: + await self._on_job_complete() + elif job_event.aborted: + await self._on_job_abort() + + async def _on_job_complete(self) -> None: if not self.automatic: return async with self.lock: @@ -99,10 +100,7 @@ class JobQueue: self.pop_queue_handle = event_loop.delay_callback( self.job_delay, self._pop_job) - async def _on_job_abort(self, - prev_stats: Dict[str, Any], - new_stats: Dict[str, Any] - ) -> None: + async def _on_job_abort(self) -> None: async with self.lock: if self.queued_jobs: self._set_queue_state("paused") diff --git a/moonraker/components/job_state.py b/moonraker/components/job_state.py index 2d5abf8..650da2b 100644 --- a/moonraker/components/job_state.py +++ b/moonraker/components/job_state.py @@ -15,6 +15,7 @@ from typing import ( Dict, List, ) +from ..common import JobEvent if TYPE_CHECKING: from ..confighelper import ConfigHelper from .klippy_apis import KlippyAPI @@ -65,8 +66,16 @@ class JobState: f"Job State Changed - Prev State: {old_state}, " f"New State: {new_state}" ) + # NOTE: Individual job_state events are DEPRECATED. New modules + # should register handlers for "job_state: status_changed" and + # match against the JobEvent object provided. + self.server.send_event(f"job_state:{new_state}", prev_ps, new_ps) self.server.send_event( - f"job_state:{new_state}", prev_ps, new_ps) + "job_state:state_changed", + JobEvent.from_string(new_state), + prev_ps, + new_ps + ) if "info" in ps: cur_layer: Optional[int] = ps["info"].get("current_layer") if cur_layer is not None: diff --git a/moonraker/components/notifier.py b/moonraker/components/notifier.py index 4cf4236..44d0f44 100644 --- a/moonraker/components/notifier.py +++ b/moonraker/components/notifier.py @@ -10,6 +10,7 @@ import apprise import logging import pathlib import re +from ..common import JobEvent # Annotation imports from typing import ( @@ -29,23 +30,20 @@ class Notifier: def __init__(self, config: ConfigHelper) -> None: self.server = config.get_server() self.notifiers: Dict[str, NotifierInstance] = {} - self.events: Dict[str, NotifierEvent] = {} + self.events: Dict[str, List[NotifierInstance]] = {} prefix_sections = config.get_prefix_sections("notifier") - - self.register_events(config) self.register_remote_actions() - for section in prefix_sections: cfg = config[section] try: notifier = NotifierInstance(cfg) - - for event in self.events: - if event in notifier.events or "*" in notifier.events: - self.events[event].register_notifier(notifier) - + for job_event in list(JobEvent): + if job_event == JobEvent.STANDBY: + continue + evt_name = str(job_event) + if "*" in notifier.events or evt_name in notifier.events: + self.events.setdefault(evt_name, []).append(notifier) logging.info(f"Registered notifier: '{notifier.get_name()}'") - except Exception as e: msg = f"Failed to load notifier[{cfg.get_name()}]\n{e}" self.server.add_warning(msg) @@ -53,6 +51,9 @@ class Notifier: self.notifiers[notifier.get_name()] = notifier self.register_endpoints(config) + self.server.register_event_handler( + "job_state:state_changed", self._on_job_state_changed + ) def register_remote_actions(self): self.server.register_remote_method("notify", self.notify_action) @@ -61,40 +62,17 @@ class Notifier: if name not in self.notifiers: raise self.server.error(f"Notifier '{name}' not found", 404) notifier = self.notifiers[name] - await notifier.notify("remote_action", [], message) - def register_events(self, config: ConfigHelper): - - self.events["started"] = NotifierEvent( - "started", - "job_state:started", - config) - - self.events["complete"] = NotifierEvent( - "complete", - "job_state:complete", - config) - - self.events["error"] = NotifierEvent( - "error", - "job_state:error", - config) - - self.events["cancelled"] = NotifierEvent( - "cancelled", - "job_state:cancelled", - config) - - self.events["paused"] = NotifierEvent( - "paused", - "job_state:paused", - config) - - self.events["resumed"] = NotifierEvent( - "resumed", - "job_state:resumed", - config) + async def _on_job_state_changed( + self, + job_event: JobEvent, + prev_stats: Dict[str, Any], + new_stats: Dict[str, Any] + ) -> None: + evt_name = str(job_event) + for notifier in self.events.get(evt_name, []): + await notifier.notify(evt_name, [prev_stats, new_stats]) def register_endpoints(self, config: ConfigHelper): self.server.register_endpoint( @@ -134,34 +112,6 @@ class Notifier: "stats": print_stats } - -class NotifierEvent: - def __init__(self, identifier: str, event_name: str, config: ConfigHelper): - self.identifier = identifier - self.event_name = event_name - self.server = config.get_server() - self.notifiers: Dict[str, NotifierInstance] = {} - self.config = config - - self.server.register_event_handler(self.event_name, self._handle) - - def register_notifier(self, notifier: NotifierInstance): - self.notifiers[notifier.get_name()] = notifier - - async def _handle(self, *args) -> None: - logging.info(f"'{self.identifier}' notifier event triggered'") - await self.invoke_notifiers(args) - - async def invoke_notifiers(self, args): - for notifier_name in self.notifiers: - try: - notifier = self.notifiers[notifier_name] - await notifier.notify(self.identifier, args) - except Exception as e: - logging.info(f"Failed to notify [{notifier_name}]\n{e}") - continue - - class NotifierInstance: def __init__(self, config: ConfigHelper) -> None: self.config = config diff --git a/moonraker/components/simplyprint.py b/moonraker/components/simplyprint.py index 41732a6..4da37ed 100644 --- a/moonraker/components/simplyprint.py +++ b/moonraker/components/simplyprint.py @@ -17,7 +17,7 @@ import logging.handlers import tempfile from queue import SimpleQueue from ..loghelper import LocalQueueHandler -from ..common import Subscribable, WebRequest +from ..common import Subscribable, WebRequest, JobEvent from ..utils import json_wrapper as jsonw from typing import ( @@ -28,6 +28,7 @@ from typing import ( List, Union, Any, + Callable, ) if TYPE_CHECKING: from ..app import InternalTransport @@ -157,19 +158,7 @@ class SimplyPrint(Subscribable): self.server.register_event_handler( "server:klippy_disconnect", self._on_klippy_disconnected) self.server.register_event_handler( - "job_state:started", self._on_print_start) - self.server.register_event_handler( - "job_state:paused", self._on_print_paused) - self.server.register_event_handler( - "job_state:resumed", self._on_print_resumed) - self.server.register_event_handler( - "job_state:standby", self._on_print_standby) - self.server.register_event_handler( - "job_state:complete", self._on_print_complete) - self.server.register_event_handler( - "job_state:error", self._on_print_error) - self.server.register_event_handler( - "job_state:cancelled", self._on_print_cancelled) + "job_state:state_changed", self._on_job_state_changed) self.server.register_event_handler( "klippy_apis:pause_requested", self._on_pause_requested) self.server.register_event_handler( @@ -542,7 +531,7 @@ class SimplyPrint(Subscribable): async def _on_klippy_ready(self) -> None: last_stats: Dict[str, Any] = self.job_state.get_last_stats() if last_stats["state"] == "printing": - self._on_print_start(last_stats, last_stats, False) + self._on_print_started(last_stats, last_stats, False) else: self._update_state("operational") query: Optional[Dict[str, Any]] @@ -674,7 +663,14 @@ class SimplyPrint(Subscribable): self.cache.reset_print_state() self.printer_status = {} - def _on_print_start( + def _on_job_state_changed(self, job_event: JobEvent, *args) -> None: + callback: Optional[Callable] = getattr(self, f"_on_print_{job_event}", None) + if callback is not None: + callback(*args) + else: + logging.info(f"No defined callback for Job Event: {job_event}") + + def _on_print_started( self, prev_stats: Dict[str, Any], new_stats: Dict[str, Any], diff --git a/moonraker/components/update_manager/common.py b/moonraker/components/update_manager/common.py index a555134..4ed9289 100644 --- a/moonraker/components/update_manager/common.py +++ b/moonraker/components/update_manager/common.py @@ -9,7 +9,7 @@ import os import sys import copy import pathlib -from enum import Enum +from ...common import ExtendedEnum from ...utils import source_info from typing import ( TYPE_CHECKING, @@ -46,25 +46,13 @@ BASE_CONFIG: Dict[str, Dict[str, str]] = { } } -class ExtEnum(Enum): - @classmethod - def from_string(cls, enum_name: str): - str_name = enum_name.upper() - for name, member in cls.__members__.items(): - if name == str_name: - return cls(member.value) - raise ValueError(f"No enum member named {enum_name}") - - def __str__(self) -> str: - return self._name_.lower() # type: ignore - -class AppType(ExtEnum): +class AppType(ExtendedEnum): NONE = 1 WEB = 2 GIT_REPO = 3 ZIP = 4 -class Channel(ExtEnum): +class Channel(ExtendedEnum): STABLE = 1 BETA = 2 DEV = 3