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:
Eric Callahan 2023-11-03 06:35:13 -04:00
parent 7deb9fac4c
commit 6e8b720d17
No known key found for this signature in database
GPG Key ID: 5A1EB336DFB4C71B
7 changed files with 110 additions and 158 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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],

View File

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