WIP, not functional

This commit is contained in:
John Lancaster
2024-05-07 07:55:21 -05:00
parent 7b67d062a0
commit a7b27cf916
4 changed files with 68 additions and 25 deletions

14
config/.rich_logging.yaml Normal file
View File

@@ -0,0 +1,14 @@
version: 1
disable_existing_loggers: false
formatters:
rich:
style: "{"
format: "[room]{appname}[/] {message}"
datefmt: '%H:%M:%S.%f'
handlers:
rich:
formatter: rich
'()': 'rich.logging.RichHandler'
markup: True
show_path: false
omit_repeated_times: false

View File

@@ -1,9 +1,13 @@
import json
import logging import logging
import logging.config import logging.config
import re import re
from abc import ABC from abc import ABC
from dataclasses import asdict from dataclasses import asdict
from importlib.resources import files
import yaml
from appdaemon.adapi import ADAPI
from rich.console import Console from rich.console import Console
from rich.highlighter import RegexHighlighter from rich.highlighter import RegexHighlighter
from rich.theme import Theme from rich.theme import Theme
@@ -42,6 +46,14 @@ class RCHighlighter(RegexHighlighter):
] ]
def load_rich_config():
with files('room_control.config').joinpath('rich_logging.yaml').open('r') as f:
RICH_CFG = yaml.safe_load(f)
RICH_CFG['handlers']['rich']['console'] = console
RICH_CFG['handlers']['rich']['highlighter'] = RCHighlighter()
return RICH_CFG
RICH_HANDLER_CFG = { RICH_HANDLER_CFG = {
'()': 'rich.logging.RichHandler', '()': 'rich.logging.RichHandler',
'markup': True, 'markup': True,
@@ -61,19 +73,19 @@ class ContextSettingFilter(logging.Filter, ABC):
return record return record
class RoomFilter(logging.Filter):
"""Used to filter out messages that have a component field because they will have already been printed by their respective logger."""
def filter(self, record: logging.LogRecord) -> bool:
return getattr(record, 'component', None) is None
# @dataclass # @dataclass
# class RoomControllerFilter(ContextSettingFilter): # class RoomControllerFilter(ContextSettingFilter):
# room: str # room: str
# component: Optional[str] = None # component: Optional[str] = None
class RoomFilter(logging.Filter):
"""Used to filter out messages that have a component field because they will have already been printed by their respective logger."""
def filter(self, record: logging.LogRecord) -> bool:
return not hasattr(record, 'component')
# class RoomControllerFormatter(logging.Formatter): # class RoomControllerFormatter(logging.Formatter):
# def format(self, record: logging.LogRecord): # def format(self, record: logging.LogRecord):
# return super().format(record) # return super().format(record)
@@ -87,6 +99,11 @@ class UnMarkupFilter(logging.Filter):
return record return record
class JSONFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
return json.dumps(record.__dict__)
def room_logging_config(name: str): def room_logging_config(name: str):
return { return {
'version': 1, 'version': 1,
@@ -106,6 +123,7 @@ def room_logging_config(name: str):
'format': '{asctime}.{msecs:03.0f} {levelname:8} {name}: {message}', 'format': '{asctime}.{msecs:03.0f} {levelname:8} {name}: {message}',
'datefmt': '%Y-%m-%d %H:%M:%S', 'datefmt': '%Y-%m-%d %H:%M:%S',
}, },
'json': {'()': 'room_control.console.JSONFormatter'},
}, },
'handlers': { 'handlers': {
'rich_room': { 'rich_room': {
@@ -119,7 +137,14 @@ def room_logging_config(name: str):
'class': 'logging.handlers.RotatingFileHandler', 'class': 'logging.handlers.RotatingFileHandler',
# 'class': 'logging.FileHandler', # 'class': 'logging.FileHandler',
'filename': f'/logs/{name}.log', 'filename': f'/logs/{name}.log',
'mode': 'w', 'maxBytes': 1000000,
'backupCount': 3,
},
'json': {
'filters': ['unmarkup'],
'formatter': 'json',
'filename': f'/logs/{name}.jsonl',
'class': 'logging.handlers.RotatingFileHandler',
'maxBytes': 1000000, 'maxBytes': 1000000,
'backupCount': 3, 'backupCount': 3,
}, },
@@ -128,7 +153,11 @@ def room_logging_config(name: str):
f'AppDaemon.{name}': { f'AppDaemon.{name}': {
'level': 'INFO', 'level': 'INFO',
'propagate': False, 'propagate': False,
'handlers': ['rich_room', 'file'], 'handlers': [
'rich_room',
'file',
'json',
],
}, },
}, },
} }
@@ -137,6 +166,9 @@ def room_logging_config(name: str):
def component_logging_config(parent_room: str, component: str): def component_logging_config(parent_room: str, component: str):
logger_name = f'AppDaemon.{parent_room}.{component}' logger_name = f'AppDaemon.{parent_room}.{component}'
cfg = load_rich_config()
LOG_CFG = { LOG_CFG = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Literal, Optional
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 pydantic import BaseModel, TypeAdapter from pydantic import BaseModel, TypeAdapter, ValidationError
from .console import setup_component_logging from .console import setup_component_logging
@@ -83,16 +83,18 @@ class Motion(Hass):
) )
self.listen_state(**base_kwargs, new='off', callback=self.callback_light_off) self.listen_state(**base_kwargs, new='off', callback=self.callback_light_off)
if callbacks := self.callbacks(): for handle, cb in self.callbacks():
for handle, entry in callbacks.items(): self.log(f'Handle [yellow]{handle[:4]}[/]: {cb.function}', level='DEBUG')
self.log(
f'Handle [yellow]{handle[:4]}[/]: {entry.function}', level='DEBUG'
)
def callbacks(self): def callbacks(self):
"""Returns a dictionary of validated CallbackEntry objects that are associated with this app""" """Returns a dictionary of validated CallbackEntry objects that are associated with this app"""
data = TypeAdapter(Callbacks).validate_python(self.get_callback_entries()) self_callbacks = self.get_callback_entries().get(self.name, {})
return data.get(self.name, {}) for handle, cb_dict in self_callbacks.items():
try:
yield handle, CallbackEntry.model_validate(cb_dict)
except ValidationError as e:
self.logger.error('Error parsing callback dictionary')
self.logger.error(e)
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"""

View File

@@ -1,4 +1,5 @@
import datetime import datetime
import json
import logging import logging
import logging.config import logging.config
from copy import deepcopy from copy import deepcopy
@@ -8,7 +9,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 .console import console, room_logging_config from .console import room_logging_config
from .model import ControllerStateConfig, RoomControllerConfig from .model import ControllerStateConfig, RoomControllerConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -95,10 +96,6 @@ 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.logger.isEnabledFor(logging.DEBUG):
# # table = self._room_config.rich_table(self.name)
# 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)
# schedule the transitions # schedule the transitions
@@ -209,9 +206,7 @@ class RoomController(Hass, Mqtt):
elif isinstance(scene_kwargs, dict): elif isinstance(scene_kwargs, dict):
self.call_service('scene/apply', **scene_kwargs) self.call_service('scene/apply', **scene_kwargs)
if self.logger.isEnabledFor(logging.INFO): self.log(f'Applied scene:\n{json.dumps(scene_kwargs, indent=2)}', level='DEBUG')
self.log('Applied scene:')
console.print(scene_kwargs['entities'])
elif scene_kwargs is None: elif scene_kwargs is None:
self.log('No scene, ignoring...') self.log('No scene, ignoring...')