From 4e2557e7148c1a433855d0bfd1b170e7fe0c24b7 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sun, 10 Mar 2024 12:31:50 -0500 Subject: [PATCH] seperate rich logging system --- button.py | 8 +++---- console.py | 56 ++++++++++++++++++++++++++++++++++++++++++++----- motion.py | 36 +++++++++++++------------------ room_control.py | 28 ++++++++++++------------- 4 files changed, 83 insertions(+), 45 deletions(-) diff --git a/button.py b/button.py index e904a94..b59020d 100644 --- a/button.py +++ b/button.py @@ -2,7 +2,7 @@ import json from dataclasses import dataclass from appdaemon.plugins.mqtt.mqttapi import Mqtt -from console import init_logging +from console import setup_component_logging from room_control import RoomController @@ -13,11 +13,9 @@ class Button(Mqtt): rich: bool = False async def initialize(self): - if level := self.args.get('rich', False): - self.rich = True - init_logging(self, level) - + setup_component_logging(self) self.app: RoomController = await self.get_app(self.args['app']) + self.log(f'Connected to AD app [room]{self.app.name}[/]') self.button = self.args['button'] self.setup_buttons(self.button) diff --git a/console.py b/console.py index 4f0fec4..8db61c2 100644 --- a/console.py +++ b/console.py @@ -8,9 +8,15 @@ from rich.highlighter import NullHighlighter from rich.logging import RichHandler from rich.theme import Theme + console = Console( width=150, - theme=Theme({'appname': 'italic bright_cyan'}), + theme=Theme({ + # 'appname': 'italic bright_cyan', + 'room': 'italic bright_cyan', + 'component': 'violet' + }), + log_time_format='%Y-%m-%d %I:%M:%S %p', ) @@ -22,8 +28,34 @@ class UnMarkupFormatter(AppNameFormatter): return self.md_regex.sub(r'\g', result) -def create_handler() -> RichHandler: - handler = RichHandler( +class RoomControllerFormatter(logging.Formatter): + def __init__(self, room: str, component: str = None): + self.log_fields = {'room': room} + + fmt = '[room]{room:>12}[/]' + if component is not None: + fmt += ' [component]{component:<9}[/]' + self.log_fields['component'] = component + fmt += ' {message}' + + datefmt = '%Y-%m-%d %I:%M:%S %p' + style = '{' + validate=True + + super().__init__(fmt, datefmt, style, validate) + # console.print(f'Format: [bold yellow]{fmt}[/]') + + def format(self, record: logging.LogRecord): + parts = record.name.split('.') + record.room = parts[1] + if len(parts) == 3: + record.component = parts[2] + + return super().format(record) + + +def new_handler() -> RichHandler: + return RichHandler( console=console, highlighter=NullHighlighter(), markup=True, @@ -31,10 +63,24 @@ def create_handler() -> RichHandler: omit_repeated_times=False, log_time_format='%Y-%m-%d %I:%M:%S %p', ) - handler.setFormatter(AppNameFormatter(fmt='[appname]{appname}[/] {message}', style='{')) + + +def setup_handler(**kwargs) -> RichHandler: + handler = new_handler() + handler.setFormatter(RoomControllerFormatter(**kwargs)) return handler +def setup_component_logging(self): + typ = type(self).__name__ + logger = logging.getLogger(f'room_control.{self.args["app"]}') + self.logger = logger.getChild(typ) + if len(self.logger.handlers) == 0: + self.logger.setLevel(logging.INFO) + self.logger.addHandler(setup_handler(room=self.args["app"], component=typ)) + self.logger.propagate = False + + def init_logging(self: ADAPI, level): for h in logging.getLogger('AppDaemon').handlers: og_formatter = h.formatter @@ -45,7 +91,7 @@ def init_logging(self: ADAPI, level): if not any(isinstance(h, RichHandler) for h in self.logger.handlers): self.logger.propagate = False self.logger.setLevel(level) - self.logger.addHandler(create_handler()) + self.logger.addHandler(setup_handler()) self.log(f'Added rich handler for [bold green]{self.logger.name}[/]') # self.log(f'Formatter for RichHandler: {handler.formatter}') diff --git a/motion.py b/motion.py index e8b4f95..8745a81 100644 --- a/motion.py +++ b/motion.py @@ -3,9 +3,9 @@ from datetime import timedelta from appdaemon.entity import Entity from appdaemon.plugins.hass.hassapi import Hass -from room_control import RoomController +from console import setup_component_logging -from appdaemon import utils +from room_control import RoomController class Motion(Hass): @@ -26,33 +26,32 @@ class Motion(Hass): return self.ref_entity.get_state() == 'on' def initialize(self): + setup_component_logging(self) self.app: RoomController = self.get_app(self.args['app']) - self.log(f'Connected to app {self.app.name}') + self.log(f'Connected to AD app [room]{self.app.name}[/]') base_kwargs = dict( entity_id=self.ref_entity.entity_id, - immediate=True, # avoids needing to sync the state + immediate=True, # avoids needing to sync the state ) # don't need to await these because they'll already get turned into a task by the utils.sync_wrapper decorator self.listen_state(**base_kwargs, attribute='brightness', callback=self.callback_light_on) self.listen_state(**base_kwargs, new='off', callback=self.callback_light_off) def listen_motion_on(self): - """Sets up the motion on callback to activate the room - """ + """Sets up the motion on callback to activate the room""" self.cancel_motion_callback() self.listen_state( callback=self.app.activate_all_off, entity_id=self.sensor.entity_id, new='on', oneshot=True, - cause='motion on' + cause='motion on', ) self.log(f'Waiting for motion on {self.sensor.friendly_name}') def listen_motion_off(self, duration: timedelta): - """Sets up the motion off callback to deactivate the room - """ + """Sets up the motion off callback to deactivate the room""" self.cancel_motion_callback() self.listen_state( callback=self.app.deactivate, @@ -60,27 +59,24 @@ class Motion(Hass): new='off', duration=duration.total_seconds(), oneshot=True, - cause='motion off' + cause='motion off', ) self.log(f'Waiting for motion to stop on {self.sensor.friendly_name} for {duration}') def callback_light_on(self, entity=None, attribute=None, old=None, new=None, kwargs=None): - """Called when the light turns on - """ + """Called when the light turns on""" if new is not None: self.log(f'{entity} turned on') duration = self.app.off_duration() self.listen_motion_off(duration) def callback_light_off(self, entity=None, attribute=None, old=None, new=None, kwargs=None): - """Called when the light turns off - """ + """Called when the light turns off""" self.log(f'{entity} turned off') self.listen_motion_on() def get_app_callbacks(self, name: str = None): - """Gets all the callbacks associated with the app - """ + """Gets all the callbacks associated with the app""" name = name or self.name callbacks = { handle: info @@ -89,19 +85,17 @@ class Motion(Hass): if app_name == name } return callbacks - + def get_sensor_callbacks(self): return { - handle: info - for handle, info in self.get_app_callbacks().items() - if info['entity'] == self.sensor.entity_id + handle: info for handle, info in self.get_app_callbacks().items() if info['entity'] == self.sensor.entity_id } def cancel_motion_callback(self): callbacks = self.get_sensor_callbacks() # self.log(f'Found {len(callbacks)} callbacks for {self.sensor.entity_id}') for handle, info in callbacks.items(): - entity = info["entity"] + entity = info['entity'] kwargs = info['kwargs'] if (m := re.match('new=(?P.*?)\s', kwargs)) is not None: new = m.group('new') diff --git a/room_control.py b/room_control.py index e299f7d..8ef2f8e 100755 --- a/room_control.py +++ b/room_control.py @@ -1,4 +1,5 @@ import datetime +import logging from copy import deepcopy from dataclasses import dataclass, field from pathlib import Path @@ -9,7 +10,7 @@ from appdaemon.entity import Entity from appdaemon.plugins.hass.hassapi import Hass from appdaemon.plugins.mqtt.mqttapi import Mqtt from astral import SunDirection -from console import console, init_logging, deinit_logging +from console import setup_handler from rich.table import Table @@ -21,6 +22,9 @@ def str_to_timedelta(input_str: str) -> datetime.timedelta: return datetime.timedelta() +logger = logging.getLogger(__name__) + + @dataclass class RoomState: scene: Dict[str, Dict[str, str | int]] @@ -128,7 +132,6 @@ class RoomConfig: return state.off_duration -@dataclass(init=False) class RoomController(Hass, Mqtt): """Class for linking room's lights with a motion sensor. @@ -139,8 +142,6 @@ class RoomController(Hass, Mqtt): - When the light comes on, check if it's attributes match what they should, given the time. """ - rich_logging: bool = False - @property def states(self) -> List[RoomState]: return self._room_config.states @@ -151,21 +152,20 @@ class RoomController(Hass, Mqtt): self._room_config.states = new def initialize(self): - if (level := self.args.get('rich', False)): - init_logging(self, level) - self.rich_logging = True - - self.log(f'Initializing {self}') + self.logger = logger.getChild(self.name) + if not self.logger.hasHandlers(): + self.logger.setLevel(logging.INFO) + self.logger.addHandler(setup_handler(room=self.name)) + # console.log(f'[yellow]Added RichHandler to {self.logger.name}[/]') self.app_entities = self.gather_app_entities() # self.log(f'entities: {self.app_entities}') self.refresh_state_times() self.run_daily(callback=self.refresh_state_times, start='00:00:00') + self.log(f'Initialized [bold green]{type(self).__name__}[/]') def terminate(self): self.log('[bold red]Terminating[/]', level='DEBUG') - deinit_logging(self) - self.log('Success', level='DEBUG') def gather_app_entities(self) -> List[str]: """Returns a list of all the entities involved in any of the states""" @@ -209,9 +209,9 @@ class RoomController(Hass, Mqtt): assert isinstance(state.time, datetime.time), f'Invalid time: {state.time}' - if self.rich_logging: - table = self._room_config.rich_table(self.name) - console.log(table, highlight=False) + # if self.rich_logging: + # table = self._room_config.rich_table(self.name) + # console.log(table, highlight=False) self.states = sorted(self.states, key=lambda s: s.time, reverse=True)