Skip to content

Commit 748d842

Browse files
committed
improved home assistant connection errors
1 parent d255a5e commit 748d842

File tree

18 files changed

+121
-145
lines changed

18 files changed

+121
-145
lines changed

appdaemon/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
from . import models as cfg
12
from .adapi import ADAPI
23
from .appdaemon import AppDaemon
34
from .plugins.hass.hassapi import Hass
45
from .plugins.mqtt.mqttapi import Mqtt
5-
from . import models as cfg
66

77
__all__ = ["ADAPI", "AppDaemon", "Hass", "Mqtt", "cfg"]

appdaemon/__main__.py

Lines changed: 48 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,13 @@
1717
import os
1818
import signal
1919
import sys
20-
from collections.abc import Callable, Generator
20+
from collections.abc import Generator
2121
from contextlib import ExitStack, contextmanager
2222
from logging import Logger
2323
from pathlib import Path
2424
from time import perf_counter
2525

26-
2726
import appdaemon.appdaemon as ad
28-
from .dependency_manager import DependencyManager
2927
import appdaemon.utils as utils
3028
from appdaemon import exceptions as ade
3129
from appdaemon.app_management import UpdateMode
@@ -34,6 +32,7 @@
3432
from appdaemon.http import HTTP
3533
from appdaemon.logging import Logging
3634

35+
from .dependency_manager import DependencyManager
3736
from .models.config.yaml import MainConfig
3837

3938
logger = logging.getLogger(__name__)
@@ -52,41 +51,36 @@
5251

5352
# This dict sets up the default logging before the config has even been read.
5453
PRE_LOGGING = {
55-
'version': 1,
56-
'disable_existing_loggers': False,
57-
'formatters': {
58-
'bare': {
59-
'format': "{levelname}: {message}",
60-
'style': '{',
54+
"version": 1,
55+
"disable_existing_loggers": False,
56+
"formatters": {
57+
"bare": {
58+
"format": "{levelname}: {message}",
59+
"style": "{",
60+
},
61+
"full": {
62+
"format": "{asctime}.{msecs:03.0f} {levelname} AppDaemon: {message}",
63+
"style": "{",
64+
"datefmt": "%Y-%m-%d %H:%M:%S",
6165
},
62-
'full': {
63-
'format': "{asctime}.{msecs:03.0f} {levelname} AppDaemon: {message}",
64-
'style': '{',
65-
'datefmt': '%Y-%m-%d %H:%M:%S'
66-
}
6766
},
68-
'handlers': {
69-
'stdout': {
70-
'class': 'logging.StreamHandler',
71-
'formatter': 'full',
72-
'stream': 'ext://sys.stdout'
67+
"handlers": {
68+
"stdout": {
69+
"class": "logging.StreamHandler",
70+
"formatter": "full",
71+
"stream": "ext://sys.stdout",
72+
},
73+
"stderr": {
74+
"class": "logging.StreamHandler",
75+
"formatter": "bare",
76+
"stream": "ext://sys.stderr",
7377
},
74-
'stderr': {
75-
'class': 'logging.StreamHandler',
76-
'formatter': 'bare',
77-
'stream': 'ext://sys.stderr'
78-
}
7978
},
80-
'root': {
81-
'level': 'INFO',
82-
'handlers': ['stdout'],
79+
"root": {
80+
"level": "INFO",
81+
"handlers": ["stdout"],
8382
},
84-
'loggers': {
85-
'bare': {
86-
'handlers': ['stderr'],
87-
'propagate': False
88-
}
89-
}
83+
"loggers": {"bare": {"handlers": ["stderr"], "propagate": False}},
9084
}
9185

9286

@@ -196,7 +190,7 @@ def resolve_config_file(args: argparse.Namespace) -> tuple[Path, Path]:
196190
return config_file, config_dir
197191

198192

199-
def parse_config(args: argparse.Namespace, stop_function: Callable) -> MainConfig:
193+
def parse_config(args: argparse.Namespace) -> MainConfig:
200194
"""Parse configuration file and return MainConfig model.
201195
202196
Args:
@@ -228,26 +222,24 @@ def parse_config(args: argparse.Namespace, stop_function: Callable) -> MainConfi
228222
assert isinstance(ad_kwargs, dict), "AppDaemon configuration must be a dictionary"
229223

230224
# Batch assign required parameters
231-
ad_kwargs.update({
232-
"config_dir": config_dir,
233-
"config_file": config_file,
234-
"write_toml": args.write_toml,
235-
"stop_function": stop_function,
236-
})
225+
ad_kwargs.update(
226+
{
227+
"config_dir": config_dir,
228+
"config_file": config_file,
229+
"write_toml": args.write_toml,
230+
}
231+
)
237232

238233
# Conditionally assign time-related parameters
239234
for attr in ("timewarp", "starttime", "endtime"):
240-
if (value := getattr(args, attr)):
235+
if value := getattr(args, attr):
241236
ad_kwargs[attr] = value
242237

243238
# Set log level with fallback
244239
ad_kwargs["loglevel"] = args.debug or ad_kwargs.get("loglevel", "INFO")
245240

246241
# Handle module debug efficiently
247-
module_debug_cli = (
248-
{arg[0]: arg[1] for arg in args.moduledebug}
249-
if args.moduledebug else {}
250-
)
242+
module_debug_cli = {arg[0]: arg[1] for arg in args.moduledebug} if args.moduledebug else {}
251243

252244
if isinstance(ad_kwargs.get("module_debug"), dict):
253245
ad_kwargs["module_debug"] |= module_debug_cli
@@ -290,16 +282,13 @@ class ADMain:
290282
"""Pydantic model of the top-level object for the appdaemon.yaml file."""
291283
args: argparse.Namespace
292284

293-
stop_time: float = 0.0
294-
"""Stores the value of perf_counter() when self.stop is first called."""
295-
296285
def __init__(self, args: argparse.Namespace) -> None:
297286
self.args = args
298287
self.http_object = None
299288
self._cleanup_stack = ExitStack()
300289

301290
try:
302-
self.model = parse_config(self.args, self.stop)
291+
self.model = parse_config(self.args)
303292
self.setup_logging()
304293
utils.deprecation_warnings(self.model.appdaemon, self.logger)
305294

@@ -319,7 +308,11 @@ def __init__(self, args: argparse.Namespace) -> None:
319308
def __enter__(self):
320309
try:
321310
self._cleanup_stack.enter_context(
322-
ade.exception_context(self.logger, self.model.appdaemon.app_dir, header="ADMain")
311+
ade.exception_context(
312+
self.logger,
313+
self.model.appdaemon.app_dir,
314+
header="ADMain",
315+
)
323316
)
324317

325318
if self.args.pidfile is not None and pid is not None:
@@ -362,7 +355,7 @@ def handle_sig(self, signum: int):
362355
self.AD.thread_async.call_async_no_wait(self.AD.app_management.check_app_updates, mode=UpdateMode.TERMINATE)
363356
case (signal.SIGINT | signal.SIGTERM) as sig:
364357
self.logger.info(f"Received signal: {signal.Signals(sig).name}")
365-
self.stop()
358+
self.AD.stop()
366359

367360
@contextmanager
368361
def loop_context(self) -> Generator[asyncio.AbstractEventLoop]:
@@ -409,7 +402,7 @@ def signal_handlers(self, loop: asyncio.AbstractEventLoop):
409402
def stop(self):
410403
"""Stop AppDaemon and stop the event loop afterwards."""
411404
self.stop_time = perf_counter()
412-
task = self.loop.create_task(self.AD.stop())
405+
task = self.loop.create_task(self.AD._stop())
413406
task.add_done_callback(lambda _: self.loop.stop())
414407

415408
def run(self) -> None:
@@ -460,13 +453,12 @@ def run_context(self, loop: asyncio.AbstractEventLoop):
460453
self.logger.warning("-" * 60, exc_info=True)
461454
self.logger.warning("-" * 60)
462455
finally:
463-
self.logger.debug('Exiting self.run_context')
456+
self.logger.debug("Exiting self.run_context")
464457
self.loop.set_exception_handler(None)
465458
self.logger.info("AppDaemon is stopped.")
466459

467460
def setup_logging(self) -> None:
468-
"""Set up logging configuration and timezone.
469-
"""
461+
"""Set up logging configuration and timezone."""
470462
log_cfg = self.model.logs.model_dump(mode="python", by_alias=True, exclude_unset=True)
471463
self.logging = Logging(log_cfg, self.args.debug)
472464
self.logger = self.logging.get_logger().getChild("_startup")
@@ -491,7 +483,7 @@ def startup_text(self):
491483
# self.logging.dump_log_config()
492484
yield
493485
finally:
494-
stop_duration = perf_counter() - self.stop_time
486+
stop_duration = perf_counter() - self.AD.stop_time
495487
self.logger.info("AppDaemon main() stopped gracefully in %s", utils.format_timedelta(stop_duration))
496488

497489

appdaemon/adapi.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
from pathlib import Path
1515
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
1616

17-
from appdaemon import dependency
17+
from appdaemon import dependency, utils
1818
from appdaemon import exceptions as ade
19-
from appdaemon import utils
2019
from appdaemon.appdaemon import AppDaemon
2120
from appdaemon.entity import Entity
2221
from appdaemon.events import EventCallback

appdaemon/app_management.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
from pydantic import ValidationError
2323

24-
2524
from appdaemon.dependency import DependencyResolutionFail, get_full_module_name
2625
from appdaemon.dependency_manager import DependencyManager
2726
from appdaemon.models.config import AllAppConfig, AppConfig, GlobalModule
@@ -33,9 +32,9 @@
3332
from .models.internal.app_management import LoadingActions, ManagedObject, UpdateActions, UpdateMode
3433

3534
if TYPE_CHECKING:
36-
from .appdaemon import AppDaemon
37-
from .adbase import ADBase
3835
from .adapi import ADAPI
36+
from .adbase import ADBase
37+
from .appdaemon import AppDaemon
3938
from .plugin_management import PluginBase
4039

4140
T = TypeVar("T")

appdaemon/appdaemon.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from contextlib import ExitStack
77
from pathlib import Path
88
from threading import RLock
9+
from time import perf_counter
910
from typing import TYPE_CHECKING, Any
1011

1112
from appdaemon.admin_loop import AdminLoop
@@ -23,7 +24,6 @@
2324
from appdaemon.threads import Threading
2425
from appdaemon.utility_loop import Utility
2526

26-
2727
if TYPE_CHECKING:
2828
from appdaemon.http import HTTP
2929
from appdaemon.logging import Logging
@@ -82,7 +82,6 @@ class AppDaemon:
8282
"""
8383
exit_stack: ExitStack
8484

85-
8685
# subsystems
8786
app_management: AppManagement
8887
callbacks: Callbacks
@@ -102,8 +101,10 @@ class AppDaemon:
102101
http: "HTTP | None" = None
103102
global_lock: RLock = RLock()
104103

105-
# shut down flag
106104
stop_event: asyncio.Event
105+
"""Flag to indicate that AppDaemon is stopping. Set by :meth:`~.appdaemon.AppDaemon.stop` and checked by subsystems."""
106+
stop_time: float = 0.0
107+
"""Stores the value of perf_counter() when self.stop is first called."""
107108

108109
def __init__(
109110
self,
@@ -121,7 +122,6 @@ def __init__(
121122
self.logging.register_ad(self) # needs to go last to reference the config object
122123
self.stop_event = asyncio.Event()
123124

124-
125125
self.global_vars: Any = {}
126126
self.main_thread_id = threading.current_thread().ident
127127

@@ -314,9 +314,7 @@ def real_time(self, value: bool) -> None:
314314
if value:
315315
self.timewarp = 1.0
316316
else:
317-
raise NotImplementedError(
318-
"Setting real_time to False is not supported. Set timewarp to a value other than 1.0 instead."
319-
)
317+
raise NotImplementedError("Setting real_time to False is not supported. Set timewarp to a value other than 1.0 instead.")
320318

321319
@property
322320
def starttime(self):
@@ -336,10 +334,6 @@ def stopping(self, value: bool) -> None:
336334
else:
337335
self.stop_event.clear()
338336

339-
@property
340-
def stop_function(self):
341-
return self.config.stop_function or self.stop
342-
343337
@property
344338
def thread_duration_warning_threshold(self):
345339
return self.config.thread_duration_warning_threshold
@@ -397,7 +391,17 @@ def start(self) -> None:
397391
if self.apps_enabled:
398392
self.app_management.start()
399393

400-
async def stop(self) -> None:
394+
def stop(self) -> None:
395+
"""Stop AppDaemon gracefully by initiating the shutdown sequence.
396+
397+
Creates an async task to handle the shutdown process and schedules the event loop to stop once shutdown is
398+
complete. The actual shutdown logic is handled by _stop().
399+
"""
400+
self.stop_time = perf_counter()
401+
task = self.loop.create_task(self._stop())
402+
task.add_done_callback(lambda _: self.loop.stop())
403+
404+
async def _stop(self) -> None:
401405
"""Stop AppDaemon by calling the stop method of the subsystems.
402406
403407
This does not stop the event loop, but waits for all the existings tasks to finish before returning, which has a 3s timeout.

appdaemon/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from pathlib import Path
1919
from typing import TYPE_CHECKING, Any, Type
2020

21-
from aiohttp.client_exceptions import ClientConnectorError
21+
from aiohttp.client_exceptions import ClientConnectorError, ConnectionTimeoutError
2222
from pydantic import ValidationError
2323

2424
if TYPE_CHECKING:
@@ -102,7 +102,7 @@ def user_exception_block(logger: Logger, exception: Exception, app_dir: Path, he
102102
if user_line := get_user_line(exc, app_dir):
103103
for line, filename, func_name in list(user_line)[::-1]:
104104
logger.error(f"{indent}{filename} line {line} in {func_name}")
105-
case ClientConnectorError():
105+
case ClientConnectorError() | ConnectionTimeoutError():
106106
logger.error(f"{indent}{exc.__class__.__name__}: {exc}")
107107
break
108108
case OSError() if str(exc).endswith("address already in use"):

appdaemon/futures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncio
22
from collections import defaultdict
3-
from typing import TYPE_CHECKING
43
from concurrent.futures import Future
4+
from typing import TYPE_CHECKING
55

66
if TYPE_CHECKING:
77
from appdaemon.appdaemon import AppDaemon

0 commit comments

Comments
 (0)