history: add support for auxiliary fields

Allow other components to register custom fields tracked and reported
in job history.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2023-11-06 18:06:14 -05:00
parent b6896c7c0a
commit 1dfbffb422
1 changed files with 95 additions and 44 deletions

View File

@ -6,7 +6,12 @@ from __future__ import annotations
import time import time
import logging import logging
from asyncio import Lock from asyncio import Lock
from ..common import JobEvent, RequestType from ..common import (
JobEvent,
RequestType,
HistoryFieldData,
FieldTracker
)
# Annotation imports # Annotation imports
from typing import ( from typing import (
@ -15,18 +20,30 @@ from typing import (
Union, Union,
Optional, Optional,
Dict, Dict,
List, List
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from ..confighelper import ConfigHelper from ..confighelper import ConfigHelper
from ..common import WebRequest from ..common import WebRequest
from .database import MoonrakerDatabase as DBComp from .database import MoonrakerDatabase as DBComp
from .job_state import JobState from .job_state import JobState
from .file_manager.file_manager import FileManager from .file_manager.file_manager import FileManager
Totals = Dict[str, Union[float, int]]
AuxTotals = List[Dict[str, Any]]
HIST_NAMESPACE = "history" HIST_NAMESPACE = "history"
HIST_VERSION = 1 HIST_VERSION = 1
MAX_JOBS = 10000 MAX_JOBS = 10000
BASE_TOTALS = {
"total_jobs": 0,
"total_time": 0.,
"total_print_time": 0.,
"total_filament_used": 0.,
"longest_job": 0.,
"longest_print": 0.
}
class History: class History:
def __init__(self, config: ConfigHelper) -> None: def __init__(self, config: ConfigHelper) -> None:
@ -34,17 +51,13 @@ class History:
self.file_manager: FileManager = self.server.lookup_component( self.file_manager: FileManager = self.server.lookup_component(
'file_manager') 'file_manager')
self.request_lock = Lock() self.request_lock = Lock()
FieldTracker.class_init(self)
self.auxiliary_fields: List[HistoryFieldData] = []
database: DBComp = self.server.lookup_component("database") database: DBComp = self.server.lookup_component("database")
self.job_totals: Dict[str, float] = database.get_item( hist_info: Dict[str, Any]
"moonraker", "history.job_totals", hist_info = database.get_item("moonraker", "history", {}).result()
{ self.job_totals: Totals = hist_info.get("job_totals", dict(BASE_TOTALS))
'total_jobs': 0, self.aux_totals: AuxTotals = hist_info.get("aux_totals", [])
'total_time': 0.,
'total_print_time': 0.,
'total_filament_used': 0.,
'longest_job': 0.,
'longest_print': 0.
}).result()
self.server.register_event_handler( self.server.register_event_handler(
"server:klippy_disconnect", self._handle_disconnect) "server:klippy_disconnect", self._handle_disconnect)
@ -75,6 +88,7 @@ class History:
self.current_job: Optional[PrinterJob] = None self.current_job: Optional[PrinterJob] = None
self.current_job_id: Optional[str] = None self.current_job_id: Optional[str] = None
self.job_paused: bool = False
self.next_job_id: int = 0 self.next_job_id: int = 0
self.cached_job_ids = self.history_ns.keys().result() self.cached_job_ids = self.history_ns.keys().result()
if self.cached_job_ids: if self.cached_job_ids:
@ -189,30 +203,30 @@ class History:
return {"count": count, "jobs": jobs} return {"count": count, "jobs": jobs}
async def _handle_job_totals(self, async def _handle_job_totals(
web_request: WebRequest self, web_request: WebRequest
) -> Dict[str, Dict[str, float]]: ) -> Dict[str, Union[Totals, AuxTotals]]:
return {'job_totals': self.job_totals} return {
"job_totals": self.job_totals,
async def _handle_job_total_reset(self, "auxiliary_totals": self.aux_totals
web_request: WebRequest,
) -> Dict[str, Dict[str, float]]:
if self.current_job is not None:
raise self.server.error(
"Job in progress, cannot reset totals")
last_totals = dict(self.job_totals)
self.job_totals = {
'total_jobs': 0,
'total_time': 0.,
'total_print_time': 0.,
'total_filament_used': 0.,
'longest_job': 0.,
'longest_print': 0.
} }
async def _handle_job_total_reset(
self, web_request: WebRequest
) -> Dict[str, Union[Totals, AuxTotals]]:
if self.current_job is not None:
raise self.server.error("Job in progress, cannot reset totals")
last_totals = self.job_totals
self.job_totals = dict(BASE_TOTALS)
last_aux_totals = self.aux_totals
self._update_aux_totals(reset=True)
database: DBComp = self.server.lookup_component("database") database: DBComp = self.server.lookup_component("database")
await database.insert_item( await database.insert_item("moonraker", "history.job_totals", self.job_totals)
"moonraker", "history.job_totals", self.job_totals) await database.insert_item("moonraker", "history.aux_totals", self.aux_totals)
return {'last_totals': last_totals} return {
"last_totals": last_totals,
"last_auxiliary_totals": last_aux_totals
}
def _on_job_state_changed( def _on_job_state_changed(
self, self,
@ -220,6 +234,7 @@ class History:
prev_stats: Dict[str, Any], prev_stats: Dict[str, Any],
new_stats: Dict[str, Any] new_stats: Dict[str, Any]
) -> None: ) -> None:
self.job_paused = job_event == JobEvent.PAUSED
if job_event == JobEvent.STARTED: if job_event == JobEvent.STARTED:
if self.current_job is not None: if self.current_job is not None:
# Finish with the previous state # Finish with the previous state
@ -251,6 +266,9 @@ class History:
self.current_job = job self.current_job = job
self.current_job_id = job_id self.current_job_id = job_id
self.grab_job_metadata() self.grab_job_metadata()
for field in self.auxiliary_fields:
field.tracker.reset()
self.current_job.set_aux_data(self.auxiliary_fields)
self.history_ns[job_id] = job.get_stats() self.history_ns[job_id] = job.get_stats()
self.cached_job_ids.append(job_id) self.cached_job_ids.append(job_id)
self.next_job_id += 1 self.next_job_id += 1
@ -281,6 +299,7 @@ class History:
self.current_job.finish(status, pstats) self.current_job.finish(status, pstats)
# Regrab metadata incase metadata wasn't parsed yet due to file upload # Regrab metadata incase metadata wasn't parsed yet due to file upload
self.grab_job_metadata() self.grab_job_metadata()
self.current_job.set_aux_data(self.auxiliary_fields)
self.save_current_job() self.save_current_job()
self._update_job_totals() self._update_job_totals()
logging.debug( logging.debug(
@ -332,17 +351,30 @@ class History:
if self.current_job is None: if self.current_job is None:
return return
job = self.current_job job = self.current_job
self.job_totals['total_jobs'] += 1 self._accumulate_total("total_jobs", 1)
self.job_totals['total_time'] += job.get('total_duration') self._accumulate_total("total_time", job.total_duration)
self.job_totals['total_print_time'] += job.get('print_duration') self._accumulate_total("total_print_time", job.print_duration)
self.job_totals['total_filament_used'] += job.get('filament_used') self._accumulate_total("total_filament_used", job.filament_used)
self.job_totals['longest_job'] = max( self._maximize_total("longest_job", job.total_duration)
self.job_totals['longest_job'], job.get('total_duration')) self._maximize_total("longest_print", job.print_duration)
self.job_totals['longest_print'] = max( self._update_aux_totals()
self.job_totals['longest_print'], job.get('print_duration'))
database: DBComp = self.server.lookup_component("database") database: DBComp = self.server.lookup_component("database")
database.insert_item( database.insert_item("moonraker", "history.job_totals", self.job_totals)
"moonraker", "history.job_totals", self.job_totals) database.insert_item("moonraker", "history.aux_totals", self.aux_totals)
def _accumulate_total(self, field: str, val: Union[int, float]) -> None:
self.job_totals[field] += val
def _maximize_total(self, field: str, val: Union[int, float]) -> None:
self.job_totals[field] = max(self.job_totals[field], val)
def _update_aux_totals(self, reset: bool = False) -> None:
last_totals = self.aux_totals
self.aux_totals = [
field.get_totals(last_totals, reset)
for field in self.auxiliary_fields
if field.has_totals()
]
def send_history_event(self, evt_action: str) -> None: def send_history_event(self, evt_action: str) -> None:
if self.current_job is None or self.current_job_id is None: if self.current_job is None or self.current_job_id is None:
@ -363,6 +395,20 @@ class History:
) )
return job return job
def register_auxiliary_field(self, new_field: HistoryFieldData) -> None:
for field in self.auxiliary_fields:
if field == new_field:
raise self.server.error(
f"Field {field.name} already registered by "
f"provider {field.provider}."
)
self.auxiliary_fields.append(new_field)
def tracking_enabled(self, check_paused: bool) -> bool:
if self.current_job is None:
return False
return not self.job_paused if check_paused else True
def on_exit(self) -> None: def on_exit(self) -> None:
jstate: JobState = self.server.lookup_component("job_state") jstate: JobState = self.server.lookup_component("job_state")
last_ps = jstate.get_last_stats() last_ps = jstate.get_last_stats()
@ -378,6 +424,7 @@ class PrinterJob:
self.status: str = "in_progress" self.status: str = "in_progress"
self.start_time = time.time() self.start_time = time.time()
self.total_duration: float = 0. self.total_duration: float = 0.
self.auxiliary_data: List[Dict[str, Any]] = []
self.update_from_ps(data) self.update_from_ps(data)
def finish(self, def finish(self,
@ -401,10 +448,14 @@ class PrinterJob:
return return
setattr(self, name, val) setattr(self, name, val)
def set_aux_data(self, fields: List[HistoryFieldData]) -> None:
self.auxiliary_data = [field.as_dict() for field in fields]
def update_from_ps(self, data: Dict[str, Any]) -> None: def update_from_ps(self, data: Dict[str, Any]) -> None:
for i in data: for i in data:
if hasattr(self, i): if hasattr(self, i):
setattr(self, i, data[i]) setattr(self, i, data[i])
def load_component(config: ConfigHelper) -> History: def load_component(config: ConfigHelper) -> History:
return History(config) return History(config)