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

View File

@@ -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<text>', 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}')

View File

@@ -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<new>.*?)\s', kwargs)) is not None:
new = m.group('new')

View File

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