more rich work

This commit is contained in:
John Lancaster
2024-03-10 15:44:42 -05:00
parent b8e5a65347
commit e90ad5a071
3 changed files with 78 additions and 46 deletions

View File

@@ -4,26 +4,42 @@ import re
from appdaemon.adapi import ADAPI from appdaemon.adapi import ADAPI
from appdaemon.logging import AppNameFormatter from appdaemon.logging import AppNameFormatter
from rich.console import Console from rich.console import Console
from rich.highlighter import NullHighlighter, RegexHighlighter from rich.highlighter import RegexHighlighter
from rich.logging import RichHandler from rich.logging import RichHandler
from rich.theme import Theme from rich.theme import Theme
class RCHighlighter(RegexHighlighter):
highlights = [
r'(?P<light>(light|switch)\.\w+)',
r'(?P<time>\d+:\d+:\d+)',
r'(?P<z2m>zigbee2mqtt/)',
r'(?P<sensor>binary_sensor\.\w+)',
# r"'state': '(?P<on>on)|(?P<off>off)'"
r'(?P<true>True)|(?P<false>False)',
]
console = Console( console = Console(
width=150, width=150,
theme=Theme({ theme=Theme(
'log.time': 'none', {
'logging.level.info': 'none', 'log.time': 'none',
'logging.level.info': 'none',
'room': 'italic bright_cyan', 'room': 'italic bright_cyan',
'component': 'dark_violet', 'component': 'dark_violet',
'friendly_name': 'yellow',
'entity_id': 'light_slate_blue', 'light': 'light_slate_blue',
'time': 'yellow', 'sensor': 'green',
'time': 'yellow',
'z2m': 'bright_black', 'z2m': 'bright_black',
'topic': 'chartreuse2', 'topic': 'chartreuse2',
}), 'true': 'green',
'false': 'red',
}
),
log_time_format='%Y-%m-%d %I:%M:%S %p', log_time_format='%Y-%m-%d %I:%M:%S %p',
highlighter=RCHighlighter(),
) )
@@ -47,11 +63,11 @@ class RoomControllerFormatter(logging.Formatter):
datefmt = '%Y-%m-%d %I:%M:%S %p' datefmt = '%Y-%m-%d %I:%M:%S %p'
style = '{' style = '{'
validate=True validate = True
super().__init__(fmt, datefmt, style, validate) super().__init__(fmt, datefmt, style, validate)
# console.print(f'Format: [bold yellow]{fmt}[/]') # console.print(f'Format: [bold yellow]{fmt}[/]')
def format(self, record: logging.LogRecord): def format(self, record: logging.LogRecord):
parts = record.name.split('.') parts = record.name.split('.')
record.room = parts[1] record.room = parts[1]
@@ -61,14 +77,6 @@ class RoomControllerFormatter(logging.Formatter):
return super().format(record) return super().format(record)
class RCHighlighter(RegexHighlighter):
highlights = [
r"(?P<entity_id>(light|switch)\.\w+)",
r'(?P<time>\d+:\d+:\d+)',
r'(?P<z2m>zigbee2mqtt/)'
]
def new_handler() -> RichHandler: def new_handler() -> RichHandler:
return RichHandler( return RichHandler(
console=console, console=console,
@@ -93,16 +101,14 @@ def setup_component_logging(self):
self.logger = logger.getChild(typ) self.logger = logger.getChild(typ)
if len(self.logger.handlers) == 0: if len(self.logger.handlers) == 0:
self.logger.setLevel(logging.INFO) self.logger.setLevel(logging.INFO)
self.logger.addHandler(setup_handler(room=self.args["app"], component=typ)) self.logger.addHandler(setup_handler(room=self.args['app'], component=typ))
self.logger.propagate = False 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
h.setFormatter( h.setFormatter(UnMarkupFormatter(fmt=og_formatter._fmt, datefmt=og_formatter.datefmt, style='{'))
UnMarkupFormatter(fmt=og_formatter._fmt, datefmt=og_formatter.datefmt, style='{')
)
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

View File

@@ -30,6 +30,9 @@ class Motion(Hass):
self.app: RoomController = self.get_app(self.args['app']) self.app: RoomController = self.get_app(self.args['app'])
self.log(f'Connected to AD app [room]{self.app.name}[/]') self.log(f'Connected to AD app [room]{self.app.name}[/]')
assert self.entity_exists(self.args['sensor'])
assert self.entity_exists(self.args['ref_entity'])
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
@@ -48,7 +51,9 @@ class Motion(Hass):
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 [friendly_name]{self.sensor.friendly_name}[/]')
if self.sensor_state:
self.log(f'{self.sensor.friendly_name} is already on', level='WARNING')
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"""
@@ -61,18 +66,21 @@ class Motion(Hass):
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 [friendly_name]{self.sensor.friendly_name}[/] for {duration}')
if not self.sensor_state:
self.log(f'{self.sensor.friendly_name} is currently off', level='WARNING')
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'Detected {entity} turning on', level='DEBUG')
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'Detected {entity} turning off', level='DEBUG')
self.listen_motion_on() self.listen_motion_on()
def get_app_callbacks(self, name: str = None): def get_app_callbacks(self, name: str = None):
@@ -100,4 +108,4 @@ class Motion(Hass):
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')
self.cancel_listen_state(handle) self.cancel_listen_state(handle)
self.log(f'cancelled callback for sensor {entity} turning {new}') self.log(f'cancelled callback for sensor {entity} turning {new}', level='DEBUG')

View File

@@ -10,8 +10,9 @@ 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 setup_handler from console import console, setup_handler
from rich.table import Table from rich.console import Console, ConsoleOptions, RenderResult, Group
from rich.table import Table, Column
def str_to_timedelta(input_str: str) -> datetime.timedelta: def str_to_timedelta(input_str: str) -> datetime.timedelta:
@@ -63,6 +64,12 @@ class RoomState:
def from_json(cls, json_input): def from_json(cls, json_input):
return cls(**json_input) return cls(**json_input)
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
table = Table('Entity ID', 'State')
for name, state in self.scene.items():
table.add_row(name, str(state))
yield table
@dataclass @dataclass
class RoomConfig: class RoomConfig:
@@ -90,6 +97,17 @@ class RoomConfig:
cfg: Dict = yaml.load(f, Loader=yaml.SafeLoader)[app_name] cfg: Dict = yaml.load(f, Loader=yaml.SafeLoader)[app_name]
return cls.from_app_config(cfg) return cls.from_app_config(cfg)
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
table = Table(
Column('Time', width=15),
Column('Scene'),
highlight=True, padding=1, collapse_padding=True,
)
for state in self.states:
lines = [f'{name:20}{state["state"]} Brightness: {state["brightness"]:<4} Temp: {state["color_temp"]}' for name, state in state.scene.items()]
table.add_row(state.time.strftime('%I:%M:%S %p'), '\n'.join(lines))
yield table
def rich_table(self, app_name: str) -> Table: def rich_table(self, app_name: str) -> Table:
table = Table(title=app_name, expand=True, highlight=True, padding=1, collapse_padding=True) table = Table(title=app_name, expand=True, highlight=True, padding=1, collapse_padding=True)
table.add_column('Time') table.add_column('Time')
@@ -154,7 +172,7 @@ class RoomController(Hass, Mqtt):
def initialize(self): def initialize(self):
self.logger = logger.getChild(self.name) self.logger = logger.getChild(self.name)
if not self.logger.hasHandlers(): if not self.logger.hasHandlers():
self.logger.setLevel(logging.INFO) self.logger.setLevel(self.args.get('rich', logging.INFO))
self.logger.addHandler(setup_handler(room=self.name)) self.logger.addHandler(setup_handler(room=self.name))
# console.log(f'[yellow]Added RichHandler to {self.logger.name}[/]') # console.log(f'[yellow]Added RichHandler to {self.logger.name}[/]')
@@ -209,9 +227,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.logger.isEnabledFor(logging.DEBUG):
# table = self._room_config.rich_table(self.name) # table = self._room_config.rich_table(self.name)
# console.log(table, highlight=False) console.print(self._room_config)
self.states = sorted(self.states, key=lambda s: s.time, reverse=True) self.states = sorted(self.states, key=lambda s: s.time, reverse=True)
@@ -245,12 +263,10 @@ class RoomController(Hass, Mqtt):
def current_scene(self, now: datetime.time = None) -> Dict[str, Dict[str, str | int]]: def current_scene(self, now: datetime.time = None) -> Dict[str, Dict[str, str | int]]:
state = self.current_state(now) state = self.current_state(now)
# print(f'{type(state).__name__}') assert type(state).__name__ == 'RoomState' # needed this way instead of isinstance(...) for the reloading to work
# assert isinstance(state, RoomState), f'Invalid state: {type(state).__name__}' if self.logger.isEnabledFor(logging.DEBUG):
assert type(state).__name__ == 'RoomState' # needed for the reloading to work self.log('Current scene:')
# self.log(f'Current scene: {state}') console.print(state)
self.log('Current scene:')
self.log(state)
return state.scene return state.scene
def app_entity_states(self) -> Dict[str, str]: def app_entity_states(self) -> Dict[str, str]:
@@ -329,7 +345,9 @@ class RoomController(Hass, Mqtt):
scene[entity]['state'] = 'on' scene[entity]['state'] = 'on'
self.call_service('scene/apply', entities=scene, transition=0) self.call_service('scene/apply', entities=scene, transition=0)
self.log(f'Applied scene: {scene}') if self.logger.isEnabledFor(logging.INFO):
self.log('Applied scene:')
console.print(scene)
elif scene is None: elif scene is None:
self.log('No scene, ignoring...') self.log('No scene, ignoring...')