job_state: combine events into a single handler
Emit a single event where the first argument contains a "JobEvent" enumeration that describes the particular event. This reduces the number of callbacks registered by JobState consumers and allows them react to multiple state changes in the same callback. The individual events remain for compatibility, however they are deprecated. Current modules should be updated to use the "job_state:state_changed" event and new modules must use this event. Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
7deb9fac4c
commit
6e8b720d17
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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],
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user