seperate rich logging system
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
56
console.py
56
console.py
@@ -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}')
|
||||||
|
|
||||||
|
|||||||
36
motion.py
36
motion.py
@@ -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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user