Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions appdaemon/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
Exceptions used by appdaemon

Custom exceptions used by AppDaemon and helper functions to format them in the logs.
"""

import asyncio
Expand All @@ -20,6 +19,7 @@

from aiohttp.client_exceptions import ClientConnectorError, ConnectionTimeoutError
from pydantic import ValidationError
from pytz import UnknownTimeZoneError

if TYPE_CHECKING:
from .appdaemon import AppDaemon
Expand Down Expand Up @@ -79,19 +79,11 @@ def user_exception_block(logger: Logger, exception: Exception, app_dir: Path | N
case AssertionError():
logger.error(f"{indent}{exc.__class__.__name__}: {exc}")
continue
case UnknownTimeZoneError() if exc == chain[0]:
logger.error(f"{indent}{exc.__class__.__name__}: {exc}")
logger.error(f"{indent} The specified time zone is not recognized. Check your 'time_zone' setting.")
case ValidationError():
for error in exc.errors():
match error:
case {"type": "missing", "msg": msg, "loc": loc}:
app_name = loc[0]
field = loc[-1]
logger.error(f"{indent}App '{app_name}' has an assertion error in field '{field}': {msg}")
case {"type": "assertion_error", "msg": msg, "loc": loc}:
app_name = loc[0]
field = loc[-1]
logger.error(f"{indent}Assertion error in app '{app_name}' field '{field}': {msg}")
case _:
pass
validation_block(chain[0], exc, logger, indent)
case AppDaemonException():
assert app_dir is not None, "app_dir is required to format exception block"
for i, line in enumerate(str(exc).splitlines()):
Expand Down Expand Up @@ -140,6 +132,39 @@ def user_exception_block(logger: Logger, exception: Exception, app_dir: Path | N
logger.error("=" * width)


def validation_block(root: BaseException, exc: ValidationError, logger: Logger, indent: str = " ") -> None:
"""Generate a user-friendly block of text for a ValidationError."""
for error in exc.errors():
match error:
case {"msg": str(msg), "type": str(type_), "loc": loc}:
match root:
case BadAppConfigFile():
app_name, *_, field = loc
if type_ == "missing" and field in ("module", "class"):
logger.error(f"{indent}App config error in '{app_name}', missing field '{field}'")
# There's a bunch of other types of validation errors that could come up here, but
# most of them are just confusing to the user, so we skip them.
case _:
# This is an error with appdaemon config, not app config
field_name = '.'.join(map(str, loc))
input_ = error.get("input")
match type_:
case "missing":
logger.error(f"{indent}Missing required field: {field_name}")
case "extra_forbidden":
logger.error(f"{indent}Unknown field: {field_name}")
case "float_parsing" | "int_parsing":
logger.error(f"{indent}Invalid value for {field_name}: {input_}")
case "value_error":
logger.error(f"{indent}{msg}")
case "url_parsing" | "url_scheme":
logger.error(f"{indent}Invalid URL for {field_name}: {input_}")
case _:
logger.error(f"{indent}'{type_}' error from {loc}")
case _:
logger.error(f"{indent}{error}")


def unexpected_block(logger: Logger, exception: Exception):
logger.error("=" * 75)
logger.error(f"Unexpected error: {exception}")
Expand Down
30 changes: 9 additions & 21 deletions appdaemon/models/config/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from ssl import _SSLMethod
from typing import Annotated, Any, Literal

from pydantic import BaseModel, BeforeValidator, Field, SecretBytes, SecretStr, field_validator, model_validator
from pydantic import AnyHttpUrl, BaseModel, BeforeValidator, Field, SecretBytes, SecretStr, field_validator, model_validator
from typing_extensions import deprecated


Expand Down Expand Up @@ -86,8 +86,8 @@ class StartupConditions(BaseModel):


class HASSConfig(PluginConfig, extra="forbid"):
ha_url: str = "http://supervisor/core"
token: SecretStr
ha_url: AnyHttpUrl = Field(default="http://supervisor/core", validate_default=True) # pyright: ignore[reportAssignmentType]
token: SecretStr = Field(default_factory=lambda: SecretStr(os.environ.get("SUPERVISOR_TOKEN"))) # pyright: ignore[reportArgumentType]
ha_key: Annotated[SecretStr, deprecated("'ha_key' is deprecated. Please use long lived tokens instead")] | None = None
appdaemon_startup_conditions: StartupConditions | None = None
"""Startup conditions that apply only when AppDaemon first starts."""
Expand All @@ -108,33 +108,21 @@ class HASSConfig(PluginConfig, extra="forbid"):
config_sleep_time: ParsedTimedelta = timedelta(seconds=60)
"""The sleep time in the background task that updates the config metadata every once in a while"""

@field_validator("ha_key", mode="after")
@classmethod
def validate_ha_key(cls, v: Any):
if v is None:
return os.environ.get("SUPERVISOR_TOKEN")
else:
return v

@field_validator("ha_url", mode="after")
@classmethod
def validate_ha_url(cls, v: str):
return v.rstrip("/")

@model_validator(mode="after")
def custom_validator(self):
assert "token" in self.model_fields_set or "ha_key" in self.model_fields_set, (
"Either 'token' or 'ha_key' must be set for the Home Assistant plugin"
)
if self.token.get_secret_value() is None:
raise ValueError(
"Home Assistant token must be set either via 'token' field or 'SUPERVISOR_TOKEN' env variable"
)
return self

@property
def websocket_url(self) -> str:
return f"{self.ha_url}/api/websocket"
return f"{self.ha_url!s}api/websocket"

@property
def states_api(self) -> str:
return f"{self.ha_url}/api/states"
return f"{self.ha_url!s}api/states"

def get_entity_api(self, entity_id: str) -> str:
return f"{self.states_api}/{entity_id}"
Expand Down
2 changes: 1 addition & 1 deletion appdaemon/plugins/hass/hassplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ async def http_method(
appropriate.
"""
kwargs = utils.clean_http_kwargs(kwargs)
url = utils.make_endpoint(self.config.ha_url, endpoint)
url = utils.make_endpoint(f"{self.config.ha_url!s}", endpoint)

try:
self.update_perf(
Expand Down
Loading