Skip to content

Commit c2f34d1

Browse files
authored
Merge branch 'dev' into ha-ssl
2 parents 48cab9e + 6331c4f commit c2f34d1

File tree

14 files changed

+259
-72
lines changed

14 files changed

+259
-72
lines changed

.github/workflows/python-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: actions/checkout@v5
1616
- name: Install uv and set the python version
1717
id: setup-uv
18-
uses: astral-sh/setup-uv@v6
18+
uses: astral-sh/setup-uv@v7
1919
with:
2020
version: "0.8.15" # It is considered best practice to pin to a specific uv version
2121
# https://github.com/astral-sh/setup-uv?tab=readme-ov-file#enable-caching
@@ -50,7 +50,7 @@ jobs:
5050
- uses: actions/checkout@v5
5151
- name: Install uv and set the python version
5252
id: setup-uv
53-
uses: astral-sh/setup-uv@v6
53+
uses: astral-sh/setup-uv@v7
5454
with:
5555
# https://docs.astral.sh/uv/guides/integration/github/#multiple-python-versions
5656
python-version: ${{ matrix.python-version }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nosetests.xml
4646
coverage.xml
4747
*,cover
4848
.hypothesis/
49+
tests/conf/namespaces
4950

5051
# Translations
5152
*.mo

appdaemon/adapi.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -765,9 +765,9 @@ def _check_entity(self, namespace: str, entity_id: str | None) -> None:
765765
"""Ensures that the entity exists in the given namespace"""
766766
if entity_id is not None and "." in entity_id and not self.AD.state.entity_exists(namespace, entity_id):
767767
if namespace == "default":
768-
self.logger.warning(f"Entity {entity_id} not found in the default namespace")
768+
self.logger.warning("Entity %s not found in the default namespace", entity_id)
769769
else:
770-
self.logger.warning(f"Entity {entity_id} not found in namespace {namespace}")
770+
self.logger.warning("Entity %s not found in namespace %s", entity_id, namespace)
771771

772772
@staticmethod
773773
def get_ad_version() -> str:
@@ -817,7 +817,7 @@ async def add_entity(
817817
>>> self.add_entity('mqtt.living_room_temperature', namespace='mqtt')
818818
819819
"""
820-
namespace = namespace or self.namespace
820+
namespace = namespace if namespace is not None else self.namespace
821821
return await self.AD.state.add_entity(namespace, entity_id, state, attributes)
822822

823823
@utils.sync_decorator
@@ -850,7 +850,7 @@ async def entity_exists(self, entity_id: str, namespace: str | None = None) -> b
850850
>>> if self.entity_exists("mqtt.security_settings", namespace = "mqtt"):
851851
>>> #do something
852852
"""
853-
namespace = namespace or self.namespace
853+
namespace = namespace if namespace is not None else self.namespace
854854
return self.AD.state.entity_exists(namespace, entity_id)
855855

856856
@utils.sync_decorator
@@ -877,7 +877,7 @@ async def split_entity(self, entity_id: str, namespace: str | None = None) -> li
877877
>>> #do something specific to scenes
878878
879879
"""
880-
namespace = namespace or self.namespace
880+
namespace = namespace if namespace is not None else self.namespace
881881
self._check_entity(namespace, entity_id)
882882
return entity_id.split(".")
883883

@@ -907,7 +907,7 @@ async def remove_entity(self, entity_id: str, namespace: str | None = None) -> N
907907
>>> self.remove_entity('mqtt.living_room_temperature', namespace = 'mqtt')
908908
909909
"""
910-
namespace = namespace or self.namespace
910+
namespace = namespace if namespace is not None else self.namespace
911911
await self.AD.state.remove_entity(namespace, entity_id)
912912

913913
@staticmethod
@@ -952,7 +952,7 @@ async def get_plugin_config(self, namespace: str | None = None) -> Any:
952952
My current position is 50.8333(Lat), 4.3333(Long)
953953
954954
"""
955-
namespace = namespace or self.namespace
955+
namespace = namespace if namespace is not None else self.namespace
956956
return self.AD.plugins.get_plugin_meta(namespace)
957957

958958
@utils.sync_decorator
@@ -976,7 +976,7 @@ async def friendly_name(self, entity_id: str, namespace: str | None = None) -> s
976976
device_tracker.andrew (Andrew Tracker) is on.
977977
978978
"""
979-
namespace = namespace or self.namespace
979+
namespace = namespace if namespace is not None else self.namespace
980980
self._check_entity(namespace, entity_id)
981981
return await self.get_state(
982982
entity_id=entity_id,
@@ -1584,7 +1584,7 @@ async def listen_state(
15841584
"""
15851585
kwargs = dict(new=new, old=old, duration=duration, attribute=attribute, **kwargs)
15861586
kwargs = {k: v for k, v in kwargs.items() if v is not None}
1587-
namespace = namespace or self.namespace
1587+
namespace = namespace if namespace is not None else self.namespace
15881588

15891589
# pre-fill some arguments here
15901590
add_callback = functools.partial(
@@ -1724,9 +1724,10 @@ async def get_state(
17241724
if kwargs:
17251725
self.logger.warning(f"Extra kwargs passed to get_state, will be ignored: {kwargs}")
17261726

1727+
namespace = namespace if namespace is not None else self.namespace
17271728
return await self.AD.state.get_state(
17281729
name=self.name,
1729-
namespace=namespace or self.namespace,
1730+
namespace=namespace,
17301731
entity_id=entity_id,
17311732
attribute=attribute,
17321733
default=default,
@@ -1783,7 +1784,7 @@ async def set_state(
17831784
>>> self.set_state("light.office_1", state="off", namespace="hass")
17841785
17851786
"""
1786-
namespace = namespace or self.namespace
1787+
namespace = namespace if namespace is not None else self.namespace
17871788
if check_existence:
17881789
self._check_entity(namespace, entity_id)
17891790
return await self.AD.state.set_state(
@@ -1846,7 +1847,7 @@ def register_service(self, service: str, cb: Callable, namespace: str | None = N
18461847
self._check_service(service)
18471848
self.logger.debug("register_service: %s, %s", service, kwargs)
18481849

1849-
namespace = namespace or self.namespace
1850+
namespace = namespace if namespace is not None else self.namespace
18501851
try:
18511852
domain, service = service.split("/", 2)
18521853
except ValueError as e:
@@ -1886,7 +1887,7 @@ def deregister_service(self, service: str, namespace: str | None = None) -> bool
18861887
>>> self.deregister_service("myservices/service1")
18871888
18881889
"""
1889-
namespace = namespace or self.namespace
1890+
namespace = namespace if namespace is not None else self.namespace
18901891
self.logger.debug("deregister_service: %s, %s", service, namespace)
18911892
self._check_service(service)
18921893
return self.AD.services.deregister_service(namespace, *service.split("/"), name=self.name)
@@ -1996,7 +1997,7 @@ async def call_service(
19961997
"""
19971998
self.logger.debug("call_service: %s, %s", service, data)
19981999
self._check_service(service)
1999-
namespace = namespace or self.namespace
2000+
namespace = namespace if namespace is not None else self.namespace
20002001

20012002
# Check the entity_id if it exists
20022003
if eid := data.get("entity_id"):
@@ -2054,7 +2055,7 @@ async def run_sequence(self, sequence: str | list[dict[str, dict[str, str]]], na
20542055
])
20552056
20562057
"""
2057-
namespace = namespace or self.namespace
2058+
namespace = namespace if namespace is not None else self.namespace
20582059
self.logger.debug("Calling run_sequence() for %s from %s", sequence, self.name)
20592060

20602061
try:
@@ -2197,11 +2198,12 @@ async def listen_event(
21972198
"""
21982199
self.logger.debug(f"Calling listen_event() for {self.name} for {event}: {kwargs}")
21992200

2201+
namespace = namespace if namespace is not None else self.namespace
22002202
# pre-fill some arguments here
22012203
add_callback = functools.partial(
22022204
self.AD.events.add_event_callback,
22032205
name=self.name,
2204-
namespace=namespace or self.namespace,
2206+
namespace=namespace,
22052207
cb=callback,
22062208
timeout=timeout,
22072209
oneshot=oneshot,
@@ -2311,7 +2313,7 @@ async def fire_event(
23112313
# Convert to float if it's not None
23122314
timeout = utils.parse_timedelta(timeout).total_seconds() if timeout is not None else timeout
23132315
kwargs["timeout"] = timeout
2314-
namespace = namespace or self.namespace
2316+
namespace = namespace if namespace is not None else self.namespace
23152317
await self.AD.events.fire_event(namespace, event, **kwargs)
23162318

23172319
#
@@ -3743,7 +3745,7 @@ async def sleep(delay: float, result: T = None) -> T:
37433745
#
37443746

37453747
def get_entity(self, entity: str, namespace: str | None = None, check_existence: bool = True) -> Entity:
3746-
namespace = namespace or self.namespace
3748+
namespace = namespace if namespace is not None else self.namespace
37473749
if check_existence:
37483750
self._check_entity(namespace, entity)
37493751
return Entity(self, namespace, entity)

appdaemon/appdaemon.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def __init__(
119119
self.booted = "booting"
120120
self.logger = logging.get_logger()
121121
self.logging.register_ad(self) # needs to go last to reference the config object
122+
self._shutdown_logger = self.logging.get_child("_shutdown")
122123
self.stop_event = asyncio.Event()
123124

124125
self.global_vars: Any = {}
@@ -390,22 +391,22 @@ async def stop(self) -> None:
390391
- :meth:`Scheduler <appdaemon.scheduler.Scheduler.stop>`
391392
- :meth:`State <appdaemon.state.State.stop>`
392393
"""
393-
self.logger.info("Stopping AppDaemon")
394+
self._shutdown_logger.info("Stopping AppDaemon")
394395
self.stopping = True
395396

396397
# Subsystems are able to create tasks during their stop methods
397398
if self.apps_enabled:
398399
try:
399400
await asyncio.wait_for(self.app_management.stop(), timeout=3)
400401
except asyncio.TimeoutError:
401-
self.logger.warning("AppManagement stop timed out, continuing shutdown")
402+
self._shutdown_logger.warning("AppManagement stop timed out, continuing shutdown")
402403
if self.thread_async is not None:
403404
self.thread_async.stop()
404405
if self.plugins is not None:
405406
try:
406407
await asyncio.wait_for(self.plugins.stop(), timeout=1)
407408
except asyncio.TimeoutError:
408-
self.logger.warning("Timed out stopping plugins, continuing shutdown")
409+
self._shutdown_logger.warning("Timed out stopping plugins, continuing shutdown")
409410
self.sched.stop()
410411
self.state.stop()
411412
self.threading.stop()
@@ -420,7 +421,20 @@ async def stop(self) -> None:
420421
all_coro = asyncio.wait(running_tasks, return_when=asyncio.ALL_COMPLETED, timeout=3)
421422
gather_task = asyncio.create_task(all_coro, name="appdaemon_stop_tasks")
422423
gather_task.add_done_callback(lambda _: self.logger.debug("All tasks finished"))
423-
self.logger.debug("Waiting for tasks to finish...")
424+
self._shutdown_logger.debug("Waiting for tasks %s to finish...", len(running_tasks))
425+
426+
# These is left here for future debugging purposes
427+
# await asyncio.sleep(2.0)
428+
# still_running = [
429+
# task
430+
# for task in asyncio.all_tasks()
431+
# if task is not current_task and task is not gather_task and not task.done()
432+
# ]
433+
# self._shutdown_logger.debug("%s tasks still running after 2 seconds", len(still_running))
434+
# if still_running:
435+
# for task in still_running:
436+
# self._shutdown_logger.debug("Still running: %s", task.get_name())
437+
424438
await gather_task
425439

426440
#

appdaemon/models/config/plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class StartupConditions(BaseModel):
8585
event: EventStartupCondition | None = None
8686

8787

88-
class HASSConfig(PluginConfig):
88+
class HASSConfig(PluginConfig, extra="forbid"):
8989
ha_url: str = "http://supervisor/core"
9090
token: SecretStr
9191
ha_key: Annotated[SecretStr, deprecated("'ha_key' is deprecated. Please use long lived tokens instead")] | None = None
@@ -101,6 +101,7 @@ class HASSConfig(PluginConfig):
101101
commtype: Annotated[str, deprecated("'commtype' is deprecated")] | None = None
102102
ws_timeout: ParsedTimedelta = timedelta(seconds=10)
103103
"""Default timeout for waiting for responses from the websocket connection"""
104+
ws_max_msg_size: int = 4 * 1024 * 1024
104105
suppress_log_messages: bool = False
105106
services_sleep_time: ParsedTimedelta = timedelta(seconds=60)
106107
"""The sleep time in the background task that updates the internal list of available services every once in a while"""

appdaemon/plugins/hass/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@ def __str__(self):
4141
if self.namespace != "default":
4242
res += f" with namespace '{self.namespace}'"
4343
return res
44+
45+
@dataclass
46+
class HassConnectionError(ade.AppDaemonException):
47+
msg: str
48+
49+
def __str__(self) -> str:
50+
return self.msg

0 commit comments

Comments
 (0)