Skip to content

Commit 1fb90d1

Browse files
authored
Merge pull request #2348 from AppDaemon/event-timeout
Event timeout
2 parents 88bfab3 + 81e62df commit 1fb90d1

File tree

9 files changed

+354
-125
lines changed

9 files changed

+354
-125
lines changed

appdaemon/adapi.py

Lines changed: 253 additions & 62 deletions
Large diffs are not rendered by default.

appdaemon/appdaemon.py

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
from concurrent.futures import ThreadPoolExecutor
55
from pathlib import Path
66
from threading import RLock
7-
from typing import TYPE_CHECKING, Optional
8-
7+
from typing import TYPE_CHECKING, Any
98

109
from appdaemon.admin_loop import AdminLoop
1110
from appdaemon.app_management import AppManagement
@@ -30,7 +29,8 @@
3029

3130

3231
class AppDaemon(metaclass=Singleton):
33-
"""Top-level container for the subsystem objects. This gets passed to the subsystem objects and stored in them as the ``self.AD`` attribute.
32+
"""Top-level container for the subsystem objects. This gets passed to the subsystem objects and stored in them as
33+
the ``self.AD`` attribute.
3434
3535
Asyncio:
3636
@@ -81,36 +81,22 @@ class AppDaemon(metaclass=Singleton):
8181
"""
8282

8383
# subsystems
84-
app_management: AppManagement
85-
callbacks: Callbacks = None
86-
events: Events = None
87-
futures: Futures
84+
app_management: "AppManagement"
85+
callbacks: "Callbacks"
86+
events: "Events"
87+
futures: "Futures"
8888
logging: "Logging"
89-
plugins: PluginManagement
90-
scheduler: Scheduler
91-
services: Services
92-
sequences: Sequences
93-
state: State
94-
threading: Threading
95-
thread_async: ThreadAsync = None
96-
utility: Utility
97-
98-
# settings
99-
app_dir: Path
100-
"""Defined in the main YAML config under ``appdaemon.app_dir``. Defaults to ``./apps``
101-
"""
102-
config_dir: Path
103-
"""Path to the AppDaemon configuration files. Defaults to the first folder that has ``./apps``
104-
105-
- ``~/.homeassistant``
106-
- ``/etc/appdaemon``
107-
"""
108-
apps: bool
109-
"""Flag for whether ``disable_apps`` was set in the AppDaemon config
110-
"""
111-
112-
admin_loop: AdminLoop | None = None
113-
http: Optional["HTTP"] = None
89+
plugins: "PluginManagement"
90+
scheduler: "Scheduler"
91+
services: "Services"
92+
sequences: "Sequences"
93+
state: "State"
94+
threading: "Threading"
95+
thread_async: "ThreadAsync | None" = None
96+
utility: "Utility"
97+
98+
admin_loop: "AdminLoop | None" = None
99+
http: "HTTP | None" = None
114100
global_lock: RLock = RLock()
115101

116102
# shut down flag
@@ -122,13 +108,13 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
122108
self.config = ad_config_model
123109
self.booted = "booting"
124110
self.logger = logging.get_logger()
125-
self.logging.register_ad(self) # needs to go last to reference the config object
111+
self.logging.register_ad(self) # needs to go last to reference the config object
126112

127-
self.global_vars = {}
113+
self.global_vars: Any = {}
128114
self.main_thread_id = threading.current_thread().ident
129115

130116
if not self.apps:
131-
self.logging.log("INFO", "Apps are disabled")
117+
self.logger.info("Apps are disabled")
132118

133119
# Initialize subsystems
134120
self.callbacks = Callbacks(self)
@@ -143,7 +129,8 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
143129
assert self.config_dir is not None, "Config_dir not set. This is a development problem"
144130
assert self.config_dir.exists(), f"{self.config_dir} does not exist"
145131
assert os.access(
146-
self.config_dir, os.R_OK | os.X_OK
132+
self.config_dir,
133+
os.R_OK | os.X_OK,
147134
), f"{self.config_dir} does not have the right permissions"
148135

149136
# this will always be None because it never gets set in ad_kwargs in __main__.py
@@ -152,7 +139,8 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
152139
if not self.app_dir.exists():
153140
self.app_dir.mkdir()
154141
assert os.access(
155-
self.app_dir, os.R_OK | os.W_OK | os.X_OK
142+
self.app_dir,
143+
os.R_OK | os.W_OK | os.X_OK,
156144
), f"{self.app_dir} does not have the right permissions"
157145

158146
self.logger.info(f"Using {self.app_dir} as app_dir")
@@ -179,19 +167,25 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
179167
# Property definitions
180168
#
181169
@property
182-
def admin_delay(self):
170+
def admin_delay(self) -> int:
183171
return self.config.admin_delay
184172

185173
@property
186-
def api_port(self):
174+
def api_port(self) -> int | None:
187175
return self.config.api_port
188176

189177
@property
190-
def app_dir(self):
178+
def app_dir(self) -> Path:
179+
"""Defined in the main YAML config under ``appdaemon.app_dir``. Defaults to ``./apps``"""
191180
return self.config.app_dir
192181

182+
@app_dir.setter
183+
def app_dir(self, path: os.PathLike) -> None:
184+
self.config.app_dir = Path(path)
185+
193186
@property
194187
def apps(self):
188+
"""Flag for whether ``disable_apps`` was set in the AppDaemon config"""
195189
return not self.config.disable_apps
196190

197191
@property
@@ -204,8 +198,17 @@ def check_app_updates_profile(self):
204198

205199
@property
206200
def config_dir(self):
201+
"""Path to the AppDaemon configuration files. Defaults to the first folder that has ``./apps``
202+
203+
- ``~/.homeassistant``
204+
- ``/etc/appdaemon``
205+
"""
207206
return self.config.config_dir
208207

208+
@config_dir.setter
209+
def config_dir(self, path: os.PathLike) -> None:
210+
self.config.config_dir = Path(path)
211+
209212
@property
210213
def config_file(self):
211214
return self.config.config_file
@@ -366,7 +369,7 @@ def terminate(self):
366369
def register_http(self, http: "HTTP"):
367370
"""Sets the ``self.http`` attribute with a :class:`~.http.HTTP` object and starts the admin loop."""
368371

369-
self.http: "HTTP" = http
372+
self.http = http
370373
# Create admin loop
371374

372375
if http.old_admin is not None or http.admin is not None:

appdaemon/events.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
from collections.abc import Iterable
21
import datetime
32
import json
43
import traceback
54
import uuid
5+
from collections.abc import Callable, Iterable
66
from copy import deepcopy
77
from logging import Logger
88
from typing import TYPE_CHECKING, Any, Protocol
9-
from collections.abc import Callable
109

1110
import appdaemon.utils as utils
1211

12+
from .plugin_management import PluginBase
13+
1314
if TYPE_CHECKING:
1415
from appdaemon.appdaemon import AppDaemon
1516

@@ -190,13 +191,14 @@ async def fire_event(self, namespace: str, event: str, **kwargs):
190191

191192
self.logger.debug("fire_plugin_event() %s %s %s", namespace, event, kwargs)
192193
plugin = self.AD.plugins.get_plugin_object(namespace)
193-
194-
if hasattr(plugin, "fire_plugin_event"):
195-
# We assume that the event will come back to us via the plugin
196-
return await plugin.fire_plugin_event(event, namespace, **kwargs)
197-
else:
198-
# Just fire the event locally
199-
await self.AD.events.process_event(namespace, {"event_type": event, "data": kwargs})
194+
match plugin:
195+
case PluginBase() as plugin:
196+
if hasattr(plugin, "fire_plugin_event"):
197+
# We assume that the event will come back to us via the plugin
198+
return await plugin.fire_plugin_event(event, namespace, **kwargs)
199+
else:
200+
# Just fire the event locally
201+
await self.AD.events.process_event(namespace, {"event_type": event, "data": kwargs})
200202

201203
async def process_event(self, namespace: str, data: dict[str, Any]):
202204
"""Processes an event that has been received either locally or from a plugin.

appdaemon/plugin_management.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import importlib
55
import sys
66
import traceback
7-
from collections.abc import Generator, Iterable
7+
from collections.abc import Generator, Iterable, Mapping
88
from logging import Logger
99
from pathlib import Path
1010
from typing import TYPE_CHECKING, Any, Dict, Type
@@ -138,7 +138,13 @@ async def remove_entity(self, namespace: str, entity: str) -> None:
138138
# pass
139139

140140
# @abc.abstractmethod
141-
async def fire_plugin_event(self):
141+
async def fire_plugin_event(
142+
self,
143+
event: str,
144+
namespace: str,
145+
timeout: str | int | float | datetime.timedelta | None = None,
146+
**kwargs: Any,
147+
) -> dict[str, Any] | None: # fmt: skip
142148
raise NotImplementedError
143149

144150
@utils.warning_decorator(error_text="Unexpected error during notify_plugin_started()")
@@ -233,7 +239,7 @@ class PluginManagement:
233239
"""Flag for if PluginManagement should be shutting down
234240
"""
235241

236-
def __init__(self, ad: "AppDaemon", config: dict[str, PluginConfig]):
242+
def __init__(self, ad: "AppDaemon", config: Mapping[str, PluginConfig]):
237243
self.AD = ad
238244
self.config = config
239245
self.stopping = False

appdaemon/plugins/hass/hassplugin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,13 @@ async def call_plugin_service(
768768
#
769769

770770
@hass_check
771-
async def fire_plugin_event(self, event, namespace, timeout: float | None = None, **kwargs) -> dict | None:
771+
async def fire_plugin_event(
772+
self,
773+
event: str,
774+
namespace: str,
775+
timeout: str | int | float | datetime.timedelta | None = None,
776+
**kwargs: Any,
777+
) -> dict[str, Any] | None: # fmt: skip
772778
# if we get a request for not our namespace something has gone very wrong
773779
assert namespace == self.namespace
774780

appdaemon/state.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from datetime import timedelta
21
import threading
32
import traceback
43
import uuid
54
from copy import copy, deepcopy
5+
from datetime import timedelta
66
from logging import Logger
77
from pathlib import Path
8-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Set, Union, overload
8+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Set, overload
99

1010
from . import exceptions as ade
1111
from . import utils
@@ -74,8 +74,8 @@ async def add_namespace(
7474
namespace: str,
7575
writeback: str,
7676
persist: bool,
77-
name: str = None
78-
) -> Union[bool, Path]:
77+
name: str | None = None,
78+
) -> Path | bool | None: # fmt: skip
7979
"""Used to Add Namespaces from Apps"""
8080

8181
if self.namespace_exists(namespace):
@@ -533,11 +533,12 @@ async def add_entity(
533533
self,
534534
namespace: str,
535535
entity: str,
536-
state: str | dict,
536+
state: str | dict[str, Any],
537537
attributes: Optional[dict] = None
538-
):
538+
) -> None:
539539
"""Adds an entity to the internal state registry and fires the ``__AD_ENTITY_ADDED`` event"""
540540
if self.entity_exists(namespace, entity):
541+
self.logger.warning("%s already exists, will not be adding it", entity)
541542
return
542543

543544
state = {

appdaemon/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def parse_timedelta(s: str | int | float | timedelta | None) -> timedelta:
321321
case int() | float():
322322
return timedelta(seconds=s)
323323
case str():
324-
parts = tuple(float(p.strip()) for p in re.split(r"[^\d]+", s))
324+
parts = tuple(float(p.strip()) for p in re.split(r"[^\d\.]+", s))
325325
match len(parts):
326326
case 1:
327327
return timedelta(seconds=parts[0])

appdaemon/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "4.5.8"
1+
__version__ = "4.5.9"
22
__version_comments__ = ""

docs/HISTORY.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
# Change Log
22

3-
## 4.5.8
3+
## 4.5.9
4+
5+
**Features**
6+
7+
None
8+
9+
**Fixes**
10+
11+
- Passing through `timeout` kwarg in `dash_navigate` and `fire_event`
12+
- Fixed a bug with `parse_timedelta` in cases like `"00:2.5"`
13+
- Minor type fixes
14+
15+
**Breaking Changes**
16+
17+
None
18+
19+
**Changes in Behavior**
20+
21+
None
22+
23+
## 4.5.8 (2025-06-03)
424

525
**Features**
626

0 commit comments

Comments
 (0)