diff --git a/moonraker/app.py b/moonraker/app.py index 5a5cf7f04..a38eb631c 100644 --- a/moonraker/app.py +++ b/moonraker/app.py @@ -28,7 +28,8 @@ APIDefinition, APITransport, TransportType, - RequestType + RequestType, + KlippyState ) from .utils import json_wrapper as jsonw from .websockets import ( @@ -1133,11 +1134,10 @@ async def get(self) -> None: "The [authorization] section in moonraker.conf must be " "configured to enable CORS." ) - kstate = self.server.get_klippy_state() - if kstate != "disconnected": - kinfo = self.server.get_klippy_info() - kmsg = kinfo.get("state_message", kstate) - summary.append(f"Klipper reports {kmsg.lower()}") + kconn: Klippy = self.server.lookup_component("klippy_connection") + kstate = kconn.state + if kstate != KlippyState.DISCONNECTED: + summary.append(f"Klipper reports {kstate.message.lower()}") else: summary.append( "Moonraker is not currently connected to Klipper. Make sure " diff --git a/moonraker/common.py b/moonraker/common.py index b35ebba23..050e5ee75 100644 --- a/moonraker/common.py +++ b/moonraker/common.py @@ -123,6 +123,37 @@ def aborted(self) -> bool: def is_printing(self) -> bool: return self.value in [2, 4] +class KlippyState(ExtendedEnum): + DISCONNECTED = 1 + STARTUP = 2 + READY = 3 + ERROR = 4 + SHUTDOWN = 5 + + @classmethod + def from_string(cls, enum_name: str, msg: str = ""): + str_name = enum_name.upper() + for name, member in cls.__members__.items(): + if name == str_name: + instance = cls(member.value) + if msg: + instance.set_message(msg) + return instance + raise ValueError(f"No enum member named {enum_name}") + + + def set_message(self, msg: str) -> None: + self._state_message: str = msg + + @property + def message(self) -> str: + if hasattr(self, "_state_message"): + return self._state_message + return "" + + def startup_complete(self) -> bool: + return self.value > 2 + class Subscribable: def send_status( self, status: Dict[str, Any], eventtime: float diff --git a/moonraker/components/job_state.py b/moonraker/components/job_state.py index 650da2b4e..eec17cd83 100644 --- a/moonraker/components/job_state.py +++ b/moonraker/components/job_state.py @@ -15,7 +15,7 @@ Dict, List, ) -from ..common import JobEvent +from ..common import JobEvent, KlippyState if TYPE_CHECKING: from ..confighelper import ConfigHelper from .klippy_apis import KlippyAPI @@ -27,8 +27,8 @@ def __init__(self, config: ConfigHelper) -> None: self.server.register_event_handler( "server:klippy_started", self._handle_started) - async def _handle_started(self, state: str) -> None: - if state != "ready": + async def _handle_started(self, state: KlippyState) -> None: + if state != KlippyState.READY: return kapis: KlippyAPI = self.server.lookup_component('klippy_apis') sub: Dict[str, Optional[List[str]]] = {"print_stats": None} diff --git a/moonraker/components/mqtt.py b/moonraker/components/mqtt.py index 539a61aca..2d3b852e2 100644 --- a/moonraker/components/mqtt.py +++ b/moonraker/components/mqtt.py @@ -18,7 +18,8 @@ Subscribable, WebRequest, APITransport, - JsonRPC + JsonRPC, + KlippyState ) from ..utils import json_wrapper as jsonw @@ -372,7 +373,7 @@ async def component_init(self) -> None: self._do_reconnect(first=True) ) - async def _handle_klippy_started(self, state: str) -> None: + async def _handle_klippy_started(self, state: KlippyState) -> None: if self.status_objs: args = {'objects': self.status_objs} try: diff --git a/moonraker/components/octoprint_compat.py b/moonraker/components/octoprint_compat.py index e1d9a1145..8d77dd1c8 100644 --- a/moonraker/components/octoprint_compat.py +++ b/moonraker/components/octoprint_compat.py @@ -6,7 +6,7 @@ from __future__ import annotations import logging -from ..common import RequestType, TransportType +from ..common import RequestType, TransportType, KlippyState # Annotation imports from typing import ( @@ -16,6 +16,7 @@ List, ) if TYPE_CHECKING: + from ..klippy_connection import KlippyConnection from ..confighelper import ConfigHelper from ..common import WebRequest from .klippy_apis import KlippyAPI as APIComp @@ -153,10 +154,11 @@ def _handle_status_update(self, status: Dict[str, Any]) -> None: data.update(status[heater_name]) def printer_state(self) -> str: - klippy_state = self.server.get_klippy_state() - if klippy_state in ["disconnected", "startup"]: + kconn: KlippyConnection = self.server.lookup_component("klippy_connection") + klippy_state = kconn.state + if not klippy_state.startup_complete(): return 'Offline' - elif klippy_state != 'ready': + elif klippy_state != KlippyState.READY: return 'Error' return { 'standby': 'Operational', @@ -202,11 +204,11 @@ async def _get_server(self, """ Server status """ - klippy_state = self.server.get_klippy_state() + kconn: KlippyConnection = self.server.lookup_component("klippy_connection") + klippy_state = kconn.state return { 'server': OCTO_VERSION, - 'safemode': ( - None if klippy_state == 'ready' else 'settings') + 'safemode': None if klippy_state == KlippyState.READY else 'settings' } async def _post_login_user(self, diff --git a/moonraker/components/power.py b/moonraker/components/power.py index f354dc84b..355688b48 100644 --- a/moonraker/components/power.py +++ b/moonraker/components/power.py @@ -12,7 +12,7 @@ import time from urllib.parse import quote, urlencode from ..utils import json_wrapper as jsonw -from ..common import RequestType +from ..common import RequestType, KlippyState # Annotation imports from typing import ( @@ -262,11 +262,11 @@ def __init__(self, config: ConfigHelper) -> None: 'initial_state', None ) - def _schedule_firmware_restart(self, state: str = "") -> None: + def _schedule_firmware_restart(self, state: KlippyState) -> None: if not self.need_scheduled_restart: return self.need_scheduled_restart = False - if state == "ready": + if state == KlippyState.READY: logging.info( f"Power Device {self.name}: Klipper reports 'ready', " "aborting FIRMWARE_RESTART" @@ -304,8 +304,9 @@ async def process_power_changed(self) -> None: await self.process_bound_services() if self.state == "on" and self.klipper_restart: self.need_scheduled_restart = True - klippy_state = self.server.get_klippy_state() - if klippy_state in ["disconnected", "startup"]: + kconn: KlippyConnection = self.server.lookup_component("klippy_connection") + klippy_state = kconn.state + if not klippy_state.startup_complete(): # If klippy is currently disconnected or hasn't proceeded past # the startup state, schedule the restart in the # "klippy_started" event callback. @@ -338,7 +339,8 @@ def process_klippy_shutdown(self) -> None: self.off_when_shutdown_delay, self._power_off_on_shutdown) def _power_off_on_shutdown(self) -> None: - if self.server.get_klippy_state() != "shutdown": + kconn: KlippyConnection = self.server.lookup_component("klippy_connection") + if kconn.state != KlippyState.SHUTDOWN: return logging.info( f"Powering off device '{self.name}' due to klippy shutdown") diff --git a/moonraker/components/simplyprint.py b/moonraker/components/simplyprint.py index 4da37ed79..0259d414d 100644 --- a/moonraker/components/simplyprint.py +++ b/moonraker/components/simplyprint.py @@ -17,7 +17,7 @@ import tempfile from queue import SimpleQueue from ..loghelper import LocalQueueHandler -from ..common import Subscribable, WebRequest, JobEvent +from ..common import Subscribable, WebRequest, JobEvent, KlippyState from ..utils import json_wrapper as jsonw from typing import ( @@ -640,12 +640,12 @@ def _on_websocket_removed(self, ws: BaseRemoteConnection) -> None: self.cache.firmware_info.update(ui_data) self.send_sp("machine_data", ui_data) - def _on_klippy_startup(self, state: str) -> None: - if state != "ready": + def _on_klippy_startup(self, state: KlippyState) -> None: + if state != KlippyState.READY: self._update_state("error") kconn: KlippyConnection kconn = self.server.lookup_component("klippy_connection") - self.send_sp("printer_error", {"error": kconn.state_message}) + self.send_sp("printer_error", {"error": kconn.state.message}) self.send_sp("connection", {"new": "connected"}) self._send_firmware_data() @@ -653,7 +653,7 @@ def _on_klippy_shutdown(self) -> None: self._update_state("error") kconn: KlippyConnection kconn = self.server.lookup_component("klippy_connection") - self.send_sp("printer_error", {"error": kconn.state_message}) + self.send_sp("printer_error", {"error": kconn.state.message}) def _on_klippy_disconnected(self) -> None: self._update_state("offline") @@ -927,10 +927,11 @@ def _update_temps(self, eventtime: float) -> None: self.send_sp("temps", temp_data) def _update_state_from_klippy(self) -> None: - kstate = self.server.get_klippy_state() - if kstate == "ready": + kconn: KlippyConnection = self.server.lookup_component("klippy_connection") + klippy_state = kconn.state + if klippy_state == KlippyState.READY: sp_state = "operational" - elif kstate in ["error", "shutdown"]: + elif klippy_state in [KlippyState.ERROR, KlippyState.SHUTDOWN]: sp_state = "error" else: sp_state = "offline" @@ -1613,7 +1614,8 @@ async def start_print(self) -> None: self.simplyprint.send_sp("file_progress", data) async def _check_can_print(self) -> bool: - if self.server.get_klippy_state() != "ready": + kconn: KlippyConnection = self.server.lookup_component("klippy_connection") + if kconn.state != KlippyState.READY: return False kapi: KlippyAPI = self.server.lookup_component("klippy_apis") try: diff --git a/moonraker/klippy_connection.py b/moonraker/klippy_connection.py index b7400e69b..7dcfa9265 100644 --- a/moonraker/klippy_connection.py +++ b/moonraker/klippy_connection.py @@ -14,6 +14,7 @@ import pathlib from .utils import ServerError, get_unix_peer_credentials from .utils import json_wrapper as jsonw +from .common import KlippyState # Annotation imports from typing import ( @@ -78,8 +79,8 @@ def __init__(self, server: Server) -> None: self._peer_cred: Dict[str, int] = {} self._service_info: Dict[str, Any] = {} self.init_attempts: int = 0 - self._state: str = "disconnected" - self._state_message: str = "Klippy Disconnected" + self._state: KlippyState = KlippyState.DISCONNECTED + self._state.set_message("Klippy Disconnected") self.subscriptions: Dict[Subscribable, Subscription] = {} self.subscription_cache: Dict[str, Dict[str, Any]] = {} # Setup remote methods accessable to Klippy. Note that all @@ -106,14 +107,14 @@ def klippy_apis(self) -> KlippyAPI: return self.server.lookup_component("klippy_apis") @property - def state(self) -> str: + def state(self) -> KlippyState: if self.is_connected() and not self._klippy_started: - return "startup" + return KlippyState.STARTUP return self._state @property def state_message(self) -> str: - return self._state_message + return self._state.message @property def klippy_info(self) -> Dict[str, Any]: @@ -241,7 +242,7 @@ def _on_agent_method_received(**kwargs) -> None: connection.call_method(method_name, kwargs) self.remote_methods[method_name] = _on_agent_method_received self.klippy_reg_methods.append(method_name) - if self._methods_registered and self._state != "disconnected": + if self._methods_registered and self._state != KlippyState.DISCONNECTED: coro = self.klippy_apis.register_method(method_name) return self.event_loop.create_task(coro) return None @@ -331,7 +332,7 @@ async def _init_klippy_connection(self) -> bool: self._methods_registered = False self._missing_reqs.clear() self.init_attempts = 0 - self._state = "startup" + self._state = KlippyState.STARTUP while self.server.is_running(): await asyncio.sleep(INIT_TIME) await self._check_ready() @@ -391,8 +392,10 @@ async def _check_ready(self) -> None: msg = f"Klipper Version: {version}" self.server.add_log_rollover_item("klipper_version", msg) self._klippy_info = dict(result) + state_message: str = self._state.message if "state_message" in self._klippy_info: - self._state_message = self._klippy_info["state_message"] + state_message = self._klippy_info["state_message"] + self._state.set_message(state_message) if "state" not in result: return if send_id: @@ -400,19 +403,20 @@ async def _check_ready(self) -> None: await self.server.send_event("server:klippy_identified") # Request initial endpoints to register info, emergency stop APIs await self._request_endpoints() - self._state = result["state"] - if self._state != "startup": + self._state = KlippyState.from_string(result["state"], state_message) + if self._state != KlippyState.STARTUP: await self._request_initial_subscriptions() # Register remaining endpoints available await self._request_endpoints() startup_state = self._state - await self.server.send_event( - "server:klippy_started", startup_state - ) + await self.server.send_event("server:klippy_started", startup_state) self._klippy_started = True - if self._state != "ready": - logging.info("\n" + self._state_message) - if self._state == "shutdown" and startup_state != "shutdown": + if self._state != KlippyState.READY: + logging.info("\n" + self._state.message) + if ( + self._state == KlippyState.SHUTDOWN and + startup_state != KlippyState.SHUTDOWN + ): # Klippy shutdown during startup event self.server.send_event("server:klippy_shutdown") else: @@ -425,10 +429,10 @@ async def _check_ready(self) -> None: logging.exception( f"Unable to register method '{method}'") self._methods_registered = True - if self._state == "ready": + if self._state == KlippyState.READY: logging.info("Klippy ready") await self.server.send_event("server:klippy_ready") - if self._state == "shutdown": + if self._state == KlippyState.SHUTDOWN: # Klippy shutdown during ready event self.server.send_event("server:klippy_shutdown") else: @@ -520,21 +524,23 @@ def _process_status_update( self.subscription_cache.setdefault(field, {}).update(item) if 'webhooks' in status: wh: Dict[str, str] = status['webhooks'] + state_message: str = self._state.message if "state_message" in wh: - self._state_message = wh["state_message"] + state_message = wh["state_message"] + self._state.set_message(state_message) # XXX - process other states (startup, ready, error, etc)? if "state" in wh: - state = wh["state"] + new_state = KlippyState.from_string(wh["state"], state_message) if ( - state == "shutdown" and + new_state == KlippyState.SHUTDOWN and not self._klippy_initializing and - self._state != "shutdown" + self._state != KlippyState.SHUTDOWN ): # If the shutdown state is received during initialization # defer the event, the init routine will handle it. logging.info("Klippy has shutdown") self.server.send_event("server:klippy_shutdown") - self._state = state + self._state = new_state for conn, sub in self.subscriptions.items(): conn_status: Dict[str, Any] = {} for name, fields in sub.items(): @@ -657,7 +663,7 @@ def is_connected(self) -> bool: return self.writer is not None and not self.closing def is_ready(self) -> bool: - return self._state == "ready" + return self._state == KlippyState.READY def is_printing(self) -> bool: if not self.is_ready(): @@ -705,8 +711,8 @@ async def _on_connection_closed(self) -> None: self._klippy_initializing = False self._klippy_started = False self._methods_registered = False - self._state = "disconnected" - self._state_message = "Klippy Disconnected" + self._state = KlippyState.DISCONNECTED + self._state.set_message("Klippy Disconnected") for request in self.pending_requests.values(): request.set_exception(ServerError("Klippy Disconnected", 503)) self.pending_requests = {} diff --git a/moonraker/server.py b/moonraker/server.py index f6c48009d..a41613161 100755 --- a/moonraker/server.py +++ b/moonraker/server.py @@ -368,9 +368,6 @@ def get_host_info(self) -> Dict[str, Any]: def get_klippy_info(self) -> Dict[str, Any]: return self.klippy_connection.klippy_info - def get_klippy_state(self) -> str: - return self.klippy_connection.state - def _handle_term_signal(self) -> None: logging.info("Exiting with signal SIGTERM") self.event_loop.register_callback(self._stop_server, "terminate") @@ -447,7 +444,7 @@ async def _handle_info_request(self, web_request: WebRequest) -> Dict[str, Any]: ] return { 'klippy_connected': self.klippy_connection.is_connected(), - 'klippy_state': self.klippy_connection.state, + 'klippy_state': str(self.klippy_connection.state), 'components': list(self.components.keys()), 'failed_components': self.failed_components, 'registered_directories': reg_dirs,