seperate rich logging system

This commit is contained in:
John Lancaster
2024-03-10 12:31:50 -05:00
parent 1fa0df360c
commit 4e2557e714
4 changed files with 83 additions and 45 deletions

View File

@@ -2,7 +2,7 @@ import json
from dataclasses import dataclass from dataclasses import dataclass
from appdaemon.plugins.mqtt.mqttapi import Mqtt from appdaemon.plugins.mqtt.mqttapi import Mqtt
from console import init_logging from console import setup_component_logging
from room_control import RoomController from room_control import RoomController
@@ -13,11 +13,9 @@ class Button(Mqtt):
rich: bool = False rich: bool = False
async def initialize(self): async def initialize(self):
if level := self.args.get('rich', False): setup_component_logging(self)
self.rich = True
init_logging(self, level)
self.app: RoomController = await self.get_app(self.args['app']) 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.button = self.args['button']
self.setup_buttons(self.button) self.setup_buttons(self.button)

View File

@@ -8,9 +8,15 @@ from rich.highlighter import NullHighlighter
from rich.logging import RichHandler from rich.logging import RichHandler
from rich.theme import Theme from rich.theme import Theme
console = Console( console = Console(
width=150, 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<text>', result) return self.md_regex.sub(r'\g<text>', result)
def create_handler() -> RichHandler: class RoomControllerFormatter(logging.Formatter):
handler = RichHandler( 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, console=console,
highlighter=NullHighlighter(), highlighter=NullHighlighter(),
markup=True, markup=True,
@@ -31,10 +63,24 @@ def create_handler() -> RichHandler:
omit_repeated_times=False, omit_repeated_times=False,
log_time_format='%Y-%m-%d %I:%M:%S %p', 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 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): def init_logging(self: ADAPI, level):
for h in logging.getLogger('AppDaemon').handlers: for h in logging.getLogger('AppDaemon').handlers:
og_formatter = h.formatter 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): if not any(isinstance(h, RichHandler) for h in self.logger.handlers):
self.logger.propagate = False self.logger.propagate = False
self.logger.setLevel(level) 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'Added rich handler for [bold green]{self.logger.name}[/]')
# self.log(f'Formatter for RichHandler: {handler.formatter}') # self.log(f'Formatter for RichHandler: {handler.formatter}')

View File

@@ -3,9 +3,9 @@ from datetime import timedelta
from appdaemon.entity import Entity from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass 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): class Motion(Hass):
@@ -26,33 +26,32 @@ class Motion(Hass):
return self.ref_entity.get_state() == 'on' return self.ref_entity.get_state() == 'on'
def initialize(self): def initialize(self):
setup_component_logging(self)
self.app: RoomController = self.get_app(self.args['app']) 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( base_kwargs = dict(
entity_id=self.ref_entity.entity_id, 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 # 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, attribute='brightness', callback=self.callback_light_on)
self.listen_state(**base_kwargs, new='off', callback=self.callback_light_off) self.listen_state(**base_kwargs, new='off', callback=self.callback_light_off)
def listen_motion_on(self): 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.cancel_motion_callback()
self.listen_state( self.listen_state(
callback=self.app.activate_all_off, callback=self.app.activate_all_off,
entity_id=self.sensor.entity_id, entity_id=self.sensor.entity_id,
new='on', new='on',
oneshot=True, oneshot=True,
cause='motion on' cause='motion on',
) )
self.log(f'Waiting for motion on {self.sensor.friendly_name}') self.log(f'Waiting for motion on {self.sensor.friendly_name}')
def listen_motion_off(self, duration: timedelta): 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.cancel_motion_callback()
self.listen_state( self.listen_state(
callback=self.app.deactivate, callback=self.app.deactivate,
@@ -60,27 +59,24 @@ class Motion(Hass):
new='off', new='off',
duration=duration.total_seconds(), duration=duration.total_seconds(),
oneshot=True, oneshot=True,
cause='motion off' cause='motion off',
) )
self.log(f'Waiting for motion to stop on {self.sensor.friendly_name} for {duration}') 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): 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: if new is not None:
self.log(f'{entity} turned on') self.log(f'{entity} turned on')
duration = self.app.off_duration() duration = self.app.off_duration()
self.listen_motion_off(duration) self.listen_motion_off(duration)
def callback_light_off(self, entity=None, attribute=None, old=None, new=None, kwargs=None): 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.log(f'{entity} turned off')
self.listen_motion_on() self.listen_motion_on()
def get_app_callbacks(self, name: str = None): 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 name = name or self.name
callbacks = { callbacks = {
handle: info handle: info
@@ -89,19 +85,17 @@ class Motion(Hass):
if app_name == name if app_name == name
} }
return callbacks return callbacks
def get_sensor_callbacks(self): def get_sensor_callbacks(self):
return { return {
handle: info handle: info for handle, info in self.get_app_callbacks().items() if info['entity'] == self.sensor.entity_id
for handle, info in self.get_app_callbacks().items()
if info['entity'] == self.sensor.entity_id
} }
def cancel_motion_callback(self): def cancel_motion_callback(self):
callbacks = self.get_sensor_callbacks() callbacks = self.get_sensor_callbacks()
# self.log(f'Found {len(callbacks)} callbacks for {self.sensor.entity_id}') # self.log(f'Found {len(callbacks)} callbacks for {self.sensor.entity_id}')
for handle, info in callbacks.items(): for handle, info in callbacks.items():
entity = info["entity"] entity = info['entity']
kwargs = info['kwargs'] kwargs = info['kwargs']
if (m := re.match('new=(?P<new>.*?)\s', kwargs)) is not None: if (m := re.match('new=(?P<new>.*?)\s', kwargs)) is not None:
new = m.group('new') new = m.group('new')

View File

@@ -1,4 +1,5 @@
import datetime import datetime
import logging
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@@ -9,7 +10,7 @@ from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass from appdaemon.plugins.hass.hassapi import Hass
from appdaemon.plugins.mqtt.mqttapi import Mqtt from appdaemon.plugins.mqtt.mqttapi import Mqtt
from astral import SunDirection from astral import SunDirection
from console import console, init_logging, deinit_logging from console import setup_handler
from rich.table import Table from rich.table import Table
@@ -21,6 +22,9 @@ def str_to_timedelta(input_str: str) -> datetime.timedelta:
return datetime.timedelta() return datetime.timedelta()
logger = logging.getLogger(__name__)
@dataclass @dataclass
class RoomState: class RoomState:
scene: Dict[str, Dict[str, str | int]] scene: Dict[str, Dict[str, str | int]]
@@ -128,7 +132,6 @@ class RoomConfig:
return state.off_duration return state.off_duration
@dataclass(init=False)
class RoomController(Hass, Mqtt): class RoomController(Hass, Mqtt):
"""Class for linking room's lights with a motion sensor. """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. - When the light comes on, check if it's attributes match what they should, given the time.
""" """
rich_logging: bool = False
@property @property
def states(self) -> List[RoomState]: def states(self) -> List[RoomState]:
return self._room_config.states return self._room_config.states
@@ -151,21 +152,20 @@ class RoomController(Hass, Mqtt):
self._room_config.states = new self._room_config.states = new
def initialize(self): def initialize(self):
if (level := self.args.get('rich', False)): self.logger = logger.getChild(self.name)
init_logging(self, level) if not self.logger.hasHandlers():
self.rich_logging = True self.logger.setLevel(logging.INFO)
self.logger.addHandler(setup_handler(room=self.name))
self.log(f'Initializing {self}') # console.log(f'[yellow]Added RichHandler to {self.logger.name}[/]')
self.app_entities = self.gather_app_entities() self.app_entities = self.gather_app_entities()
# self.log(f'entities: {self.app_entities}') # self.log(f'entities: {self.app_entities}')
self.refresh_state_times() self.refresh_state_times()
self.run_daily(callback=self.refresh_state_times, start='00:00:00') self.run_daily(callback=self.refresh_state_times, start='00:00:00')
self.log(f'Initialized [bold green]{type(self).__name__}[/]')
def terminate(self): def terminate(self):
self.log('[bold red]Terminating[/]', level='DEBUG') self.log('[bold red]Terminating[/]', level='DEBUG')
deinit_logging(self)
self.log('Success', level='DEBUG')
def gather_app_entities(self) -> List[str]: def gather_app_entities(self) -> List[str]:
"""Returns a list of all the entities involved in any of the states""" """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}' assert isinstance(state.time, datetime.time), f'Invalid time: {state.time}'
if self.rich_logging: # if self.rich_logging:
table = self._room_config.rich_table(self.name) # table = self._room_config.rich_table(self.name)
console.log(table, highlight=False) # console.log(table, highlight=False)
self.states = sorted(self.states, key=lambda s: s.time, reverse=True) self.states = sorted(self.states, key=lambda s: s.time, reverse=True)