diff --git a/config/.rich_logging.yaml b/config/.rich_logging.yaml new file mode 100644 index 0000000..a6c0a63 --- /dev/null +++ b/config/.rich_logging.yaml @@ -0,0 +1,14 @@ +version: 1 +disable_existing_loggers: false +formatters: + rich: + style: "{" + format: "[room]{appname}[/] {message}" + datefmt: '%H:%M:%S.%f' +handlers: + rich: + formatter: rich + '()': 'rich.logging.RichHandler' + markup: True + show_path: false + omit_repeated_times: false diff --git a/src/room_control/console.py b/src/room_control/console.py index 9464e87..cfe6668 100644 --- a/src/room_control/console.py +++ b/src/room_control/console.py @@ -1,9 +1,13 @@ +import json import logging import logging.config import re from abc import ABC from dataclasses import asdict +from importlib.resources import files +import yaml +from appdaemon.adapi import ADAPI from rich.console import Console from rich.highlighter import RegexHighlighter from rich.theme import Theme @@ -42,6 +46,14 @@ class RCHighlighter(RegexHighlighter): ] +def load_rich_config(): + with files('room_control.config').joinpath('rich_logging.yaml').open('r') as f: + RICH_CFG = yaml.safe_load(f) + RICH_CFG['handlers']['rich']['console'] = console + RICH_CFG['handlers']['rich']['highlighter'] = RCHighlighter() + return RICH_CFG + + RICH_HANDLER_CFG = { '()': 'rich.logging.RichHandler', 'markup': True, @@ -61,19 +73,19 @@ class ContextSettingFilter(logging.Filter, ABC): return record -class RoomFilter(logging.Filter): - """Used to filter out messages that have a component field because they will have already been printed by their respective logger.""" - - def filter(self, record: logging.LogRecord) -> bool: - return getattr(record, 'component', None) is None - - # @dataclass # class RoomControllerFilter(ContextSettingFilter): # room: str # component: Optional[str] = None +class RoomFilter(logging.Filter): + """Used to filter out messages that have a component field because they will have already been printed by their respective logger.""" + + def filter(self, record: logging.LogRecord) -> bool: + return not hasattr(record, 'component') + + # class RoomControllerFormatter(logging.Formatter): # def format(self, record: logging.LogRecord): # return super().format(record) @@ -87,6 +99,11 @@ class UnMarkupFilter(logging.Filter): return record +class JSONFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + return json.dumps(record.__dict__) + + def room_logging_config(name: str): return { 'version': 1, @@ -106,6 +123,7 @@ def room_logging_config(name: str): 'format': '{asctime}.{msecs:03.0f} {levelname:8} {name}: {message}', 'datefmt': '%Y-%m-%d %H:%M:%S', }, + 'json': {'()': 'room_control.console.JSONFormatter'}, }, 'handlers': { 'rich_room': { @@ -119,7 +137,14 @@ def room_logging_config(name: str): 'class': 'logging.handlers.RotatingFileHandler', # 'class': 'logging.FileHandler', 'filename': f'/logs/{name}.log', - 'mode': 'w', + 'maxBytes': 1000000, + 'backupCount': 3, + }, + 'json': { + 'filters': ['unmarkup'], + 'formatter': 'json', + 'filename': f'/logs/{name}.jsonl', + 'class': 'logging.handlers.RotatingFileHandler', 'maxBytes': 1000000, 'backupCount': 3, }, @@ -128,7 +153,11 @@ def room_logging_config(name: str): f'AppDaemon.{name}': { 'level': 'INFO', 'propagate': False, - 'handlers': ['rich_room', 'file'], + 'handlers': [ + 'rich_room', + 'file', + 'json', + ], }, }, } @@ -137,6 +166,9 @@ def room_logging_config(name: str): def component_logging_config(parent_room: str, component: str): logger_name = f'AppDaemon.{parent_room}.{component}' + cfg = load_rich_config() + + LOG_CFG = { 'version': 1, 'disable_existing_loggers': False, diff --git a/src/room_control/motion.py b/src/room_control/motion.py index 69aebb5..ba8611a 100644 --- a/src/room_control/motion.py +++ b/src/room_control/motion.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Literal, Optional from appdaemon.entity import Entity from appdaemon.plugins.hass.hassapi import Hass -from pydantic import BaseModel, TypeAdapter +from pydantic import BaseModel, TypeAdapter, ValidationError from .console import setup_component_logging @@ -83,16 +83,18 @@ class Motion(Hass): ) self.listen_state(**base_kwargs, new='off', callback=self.callback_light_off) - if callbacks := self.callbacks(): - for handle, entry in callbacks.items(): - self.log( - f'Handle [yellow]{handle[:4]}[/]: {entry.function}', level='DEBUG' - ) + for handle, cb in self.callbacks(): + self.log(f'Handle [yellow]{handle[:4]}[/]: {cb.function}', level='DEBUG') def callbacks(self): """Returns a dictionary of validated CallbackEntry objects that are associated with this app""" - data = TypeAdapter(Callbacks).validate_python(self.get_callback_entries()) - return data.get(self.name, {}) + self_callbacks = self.get_callback_entries().get(self.name, {}) + for handle, cb_dict in self_callbacks.items(): + try: + yield handle, CallbackEntry.model_validate(cb_dict) + except ValidationError as e: + self.logger.error('Error parsing callback dictionary') + self.logger.error(e) def listen_motion_on(self): """Sets up the motion on callback to activate the room""" diff --git a/src/room_control/room_control.py b/src/room_control/room_control.py index 3123138..f860f70 100755 --- a/src/room_control/room_control.py +++ b/src/room_control/room_control.py @@ -1,4 +1,5 @@ import datetime +import json import logging import logging.config from copy import deepcopy @@ -8,7 +9,7 @@ from appdaemon.entity import Entity from appdaemon.plugins.hass.hassapi import Hass from appdaemon.plugins.mqtt.mqttapi import Mqtt -from .console import console, room_logging_config +from .console import room_logging_config from .model import ControllerStateConfig, RoomControllerConfig logger = logging.getLogger(__name__) @@ -95,10 +96,6 @@ class RoomController(Hass, Mqtt): assert isinstance(state.time, datetime.time), f'Invalid time: {state.time}' - # if self.logger.isEnabledFor(logging.DEBUG): - # # table = self._room_config.rich_table(self.name) - # console.print(self._room_config) - self.states = sorted(self.states, key=lambda s: s.time, reverse=True) # schedule the transitions @@ -209,9 +206,7 @@ class RoomController(Hass, Mqtt): elif isinstance(scene_kwargs, dict): self.call_service('scene/apply', **scene_kwargs) - if self.logger.isEnabledFor(logging.INFO): - self.log('Applied scene:') - console.print(scene_kwargs['entities']) + self.log(f'Applied scene:\n{json.dumps(scene_kwargs, indent=2)}', level='DEBUG') elif scene_kwargs is None: self.log('No scene, ignoring...')