Skip to content

Commit 9ddcaff

Browse files
committed
Merge branch 'dev' of https://github.com/AppDaemon/appdaemon into dev
2 parents 0dc6899 + 05ea24f commit 9ddcaff

16 files changed

+458
-191
lines changed

appdaemon/adapi.py

Lines changed: 92 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,37 +1802,37 @@ def deregister_service(self, service: str, namespace: str | None = None) -> bool
18021802
return self.AD.services.deregister_service(namespace, *service.split("/"), __name=self.name)
18031803

18041804
def list_services(self, namespace: str = "global") -> list[dict[str, str]]:
1805-
"""List all services available within AD
1806-
1807-
Using this function, an App can request all available services within AD
1805+
"""List all services available within AppDaemon
18081806
18091807
Args:
1810-
namespace(str, optional): If a `namespace` is provided, AppDaemon will request
1811-
the services within the given namespace. On the other hand, if no namespace is given,
1812-
AppDaemon will use the last specified namespace or the default namespace.
1813-
To get all services across AD, pass `global`. See the section on `namespaces <APPGUIDE.html#namespaces>`__
1814-
for a detailed description. In most cases, it is safe to ignore this parameter.
1808+
namespace(str, optional): If a ``namespace`` is provided, this function will return services only in that
1809+
namespace. Otherwise, the default value for ``namespace`` is ``global``, which will return services
1810+
across all namespaces. See the section on `namespaces <APPGUIDE.html#namespaces>`__ for more
1811+
information.
18151812
18161813
Returns:
18171814
List of dictionary with keys ``namespace``, ``domain``, and ``service``.
18181815
18191816
Examples:
1820-
>>> self.list_services(namespace="global")
1817+
>>> services = self.list_services()
1818+
1819+
>>> services = self.list_services("default")
1820+
1821+
>>> services = self.list_services("mqtt")
18211822
18221823
"""
18231824

18241825
self.logger.debug("list_services: %s", namespace)
1825-
return self.AD.services.list_services(namespace) # retrieve services
1826+
return self.AD.services.list_services(namespace)
18261827

1827-
@overload
1828+
@overload # This overload provides the type hints for the Hass-specific version of this method
18281829
async def call_service(
18291830
self,
18301831
service: str,
18311832
namespace: str | None = None,
1832-
timeout: int | float | None = None,
1833-
return_result: bool = True,
1833+
timeout: str | int | float | None = None,
18341834
callback: Callable | None = None,
1835-
hass_timeout: float = 10,
1835+
hass_timeout: str | int | float | None = None,
18361836
suppress_log_messages: bool = False,
18371837
**data,
18381838
) -> Any: ...
@@ -1842,82 +1842,90 @@ async def call_service(
18421842
self,
18431843
service: str,
18441844
namespace: str | None = None,
1845-
timeout: int | float | None = None, # Used by utils.sync_decorator
1846-
**data: Any | None,
1845+
timeout: str | int | float | None = None, # Used by utils.sync_decorator
1846+
callback: Callable[[Any], Any] | None = None,
1847+
**data: dict[str, Any] | None,
18471848
) -> Any:
18481849
"""Calls a Service within AppDaemon.
18491850
1850-
This function can call any service and provide any required parameters.
1851-
By default, there are standard services that can be called within AD. Other
1852-
services that can be called, are dependent on the plugin used, or those registered
1853-
by individual apps using the `register_service` api.
1854-
In a future release, all available services can be found using AD's Admin UI.
1855-
For `listed services`, the part before the first period is the ``domain``,
1856-
and the part after is the `service name`. For instance, `light/turn_on`
1857-
has a domain of `light` and a service name of `turn_on`.
1858-
1859-
The default behaviour of the call service api is not to wait for any result, typically
1860-
known as "fire and forget". If it is required to get the results of the call, keywords
1861-
"return_result" or "callback" can be added.
1862-
1863-
Args:
1864-
service (str): The service name.
1865-
namespace(str, optional): If a `namespace` is provided, AppDaemon will change
1866-
the state of the given entity in the given namespace. On the other hand,
1867-
if no namespace is given, AppDaemon will use the last specified namespace
1868-
or the default namespace. See the section on `namespaces <APPGUIDE.html#namespaces>`__
1869-
for a detailed description. In most cases, it is safe to ignore this parameter.
1870-
return_result(str, option): If `return_result` is provided and set to `True` AD will attempt
1871-
to wait for the result, and return it after execution. In the case of Home Assistant calls that do not
1872-
return values this may seem pointless, but it does force the call to be synchronous with respect to Home Assistant
1873-
whcih can in turn highlight slow performing services if they timeout or trigger thread warnings.
1874-
callback: The non-async callback to be executed when complete.
1875-
hass_timeout (Home Assistant Specific): time in seconds to wait for Home Assistant's
1876-
response for this specific service call. If not specified defaults to the value of
1877-
the ``q_timeout`` parameter in the HASS plugin configuration, which itself defaults
1878-
to 30 seconds. See `Some Notes on Service Calls <APPGUIDE.html#some-notes-on-service-calls>`__
1879-
suppress_log_messages (Home Assistant Specific, False): if set to ``True`` Appdaemon will suppress
1880-
logging of warnings for service calls to Home Assistant, specifically timeouts and
1881-
non OK statuses. Use this flag and set it to ``True`` to supress these log messages
1882-
if you are performing your own error checking as described
1883-
`here <APPGUIDE.html#some-notes-on-service-calls>`__
1884-
**data: Each service has different parameter requirements. This argument
1885-
allows you to specify a comma-separated list of keyword value pairs, e.g.,
1886-
`entity_id = light.office_1`. These parameters will be different for
1887-
every service and can be discovered using the developer tools. Most all
1888-
service calls require an ``entity_id``.
1889-
1890-
Returns:
1891-
Result of the `call_service` function if any, see `service call notes <APPGUIDE.html#some-notes-on-service-calls>`__ for more details.
1851+
Services represent specific actions, and are generally registered by plugins or provided by AppDaemon itself.
1852+
The app calls the service only by referencing the service with a string in the format ``<domain>/<service>``, so
1853+
there is no direct coupling between apps and services. This allows any app to call any service, even ones from
1854+
other plugins.
1855+
1856+
Services often require additional parameters, such as ``entity_id``, which AppDaemon will pass to the service
1857+
call as appropriate, if used when calling this function. This allows arbitrary data to be passed to the service
1858+
calls.
1859+
1860+
Apps can also register their own services using their ``self.regsiter_service`` method.
1861+
1862+
Args:
1863+
service (str): The service name in the format `<domain>/<service>`. For example, `light/turn_on`.
1864+
namespace (str, optional): It's safe to ignore this parameter in most cases because the default namespace
1865+
will be used. However, if a `namespace` is provided, the service call will be made in that namespace. If
1866+
there's a plugin associated with that namespace, it will do the service call. If no namespace is given,
1867+
AppDaemon will use the app's namespace, which can be set using the ``self.set_namespace`` method. See
1868+
the section on `namespaces <APPGUIDE.html#namespaces>`__ for more information.
1869+
timeout (str | int | float, optional): The internal AppDaemon timeout for the service call. If no value is
1870+
specified, the default timeout is 60s. The default value can be changed using the
1871+
``appdaemon.internal_function_timeout`` config setting.
1872+
callback (callable): The non-async callback to be executed when complete. It should accept a single
1873+
argument, which will be the result of the service call. This is the recommended method for calling
1874+
services which might take a long time to complete. This effectively bypasses the ``timeout`` argument
1875+
because it only applies to this function, which will return immediately instead of waiting for the
1876+
result if a `callback` is specified.
1877+
hass_timeout (str | int | float, optional): Only applicable to the Hass plugin. Sets the amount of time to
1878+
wait for a response from Home Assistant. If no value is specified, the default timeout is 10s. The
1879+
default value can be changed using the ``ws_timeout`` setting the in the Hass plugin configuration in
1880+
``appdaemon.yaml``. Even if no data is returned from the service call, Home Assistant will still send an
1881+
acknowledgement back to AppDaemon, which this timeout applies to. Note that this is separate from the
1882+
``timeout``. If ``timeout`` is shorter than this one, it will trigger before this one does.
1883+
suppress_log_messages (bool, optional): Only applicable to the Hass plugin. If this is set to ``True``,
1884+
Appdaemon will suppress logging of warnings for service calls to Home Assistant, specifically timeouts
1885+
and non OK statuses. Use this flag and set it to ``True`` to supress these log messages if you are
1886+
performing your own error checking as described `here <APPGUIDE.html#some-notes-on-service-calls>`__
1887+
service_data (dict, optional): Used as an additional dictionary to pass arguments into the ``service_data``
1888+
field of the JSON that goes to Home Assistant. This is useful if you have a dictionary that you want to
1889+
pass in that has a key like ``target`` which is otherwise used for the ``target`` argument.
1890+
**data: Any other keyword arguments get passed to the service call as ``service_data``. Each service takes
1891+
different parameters, so this will vary from service to service. For example, most services require
1892+
``entity_id``. The parameters for each service can be found in the actions tab of developer tools in
1893+
the Home Assistant web interface.
1894+
1895+
Returns:
1896+
Result of the `call_service` function if any, see
1897+
`service call notes <APPGUIDE.html#some-notes-on-service-calls>`__ for more details.
18921898
18931899
Examples:
18941900
HASS
1901+
^^^^
18951902
1896-
>>> self.call_service("light/turn_on", entity_id = "light.office_lamp", color_name = "red")
1897-
>>> self.call_service("notify/notify", title = "Hello", message = "Hello World")
1898-
>>> self.call_service(
1903+
>>> self.call_service("light/turn_on", entity_id="light.office_lamp", color_name="red")
1904+
>>> self.call_service("notify/notify", title="Hello", message="Hello World")
1905+
>>> events = self.call_service(
18991906
"calendar/get_events",
19001907
entity_id="calendar.home",
19011908
start_date_time="2024-08-25 00:00:00",
19021909
end_date_time="2024-08-27 00:00:00",
1903-
return_result=True,
1904-
hass_timeout=10
1905-
)
1910+
)["result]["response"]["calendar.home"]["events"]
19061911
19071912
MQTT
1913+
^^^^
19081914
1909-
>>> call_service("mqtt/subscribe", topic="homeassistant/living_room/light", qos=2)
1910-
>>> call_service("mqtt/publish", topic="homeassistant/living_room/light", payload="on")
1915+
>>> self.call_service("mqtt/subscribe", topic="homeassistant/living_room/light", qos=2)
1916+
>>> self.call_service("mqtt/publish", topic="homeassistant/living_room/light", payload="on")
19111917
19121918
Utility
1919+
^^^^^^^
1920+
1921+
It's important that the ``namespace`` arg is set to ``admin`` for these services, as they do not exist
1922+
within the default namespace, and apps cannot exist in the ``admin`` namespace. If the namespace is not
1923+
specified, calling the method will raise an exception.
19131924
1914-
>>> call_service("app/restart", app="notify_app", namespace="appdaemon")
1915-
>>> call_service("app/stop", app="lights_app", namespace="appdaemon")
1916-
>>> call_service("app/reload", namespace="appdaemon")
1925+
>>> self.call_service("app/restart", app="notify_app", namespace="admin")
1926+
>>> self.call_service("app/stop", app="lights_app", namespace="admin")
1927+
>>> self.call_service("app/reload", namespace="admin")
19171928
1918-
For Utility, it is important that the `namespace` arg is set to ``appdaemon``
1919-
as no app can work within that `namespace`. If namespace is not specified,
1920-
calling this function will raise an error.
19211929
"""
19221930
self.logger.debug("call_service: %s, %s", service, data)
19231931
self._check_service(service)
@@ -1932,7 +1940,18 @@ async def call_service(
19321940
for e in eid:
19331941
self._check_entity(namespace, e)
19341942

1935-
return await self.AD.services.call_service(namespace, *service.split("/", 2), name=self.name, data=data)
1943+
domain, service_name = service.split("/", 2)
1944+
coro = self.AD.services.call_service(
1945+
namespace=namespace,
1946+
domain=domain,
1947+
service=service_name,
1948+
data=data
1949+
)
1950+
if callback is None:
1951+
return await coro
1952+
else:
1953+
task = self.AD.loop.create_task(coro)
1954+
task.add_done_callback(lambda f: callback(f.result()))
19361955

19371956
# Sequences
19381957

appdaemon/app_management.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,10 +488,10 @@ async def create_app_object(self, app_name: str) -> None:
488488
class_name
489489
)
490490

491-
if utils.count_positional_arguments(app_class.__init__) != 3:
492-
raise ade.BadClassSignature(class_name)
493-
494491
new_obj = app_class(self.AD, cfg)
492+
assert isinstance(getattr(new_obj, "AD", None), type(self.AD)), 'App objects need to have a reference to the AppDaemon object'
493+
assert isinstance(getattr(new_obj, "config_model", None), AppConfig), 'App objects need to have a reference to their config model'
494+
495495
self.objects[app_name] = ManagedObject(
496496
type="app",
497497
object=new_obj,

appdaemon/appdaemon.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,6 @@ def exclude_dirs(self):
228228
def import_paths(self):
229229
return self.config.import_paths
230230

231-
@property
232-
def internal_function_timeout(self):
233-
return self.config.internal_function_timeout
234-
235231
@property
236232
def invalid_config_warnings(self):
237233
return self.config.invalid_config_warnings

appdaemon/entity.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,12 @@ async def call_service(self, service: str, namespace: str | None = None, **kwarg
358358
namespace = namespace or self.namespace
359359
kwargs["entity_id"] = self.entity_id
360360
self.logger.debug("call_service: %s/%s, %s", self.domain, service, kwargs)
361-
return await self.AD.services.call_service(namespace, self.domain, service, self.name, kwargs)
361+
return await self.AD.services.call_service(
362+
namespace=namespace,
363+
domain=self.domain,
364+
service=service,
365+
data=kwargs
366+
) # fmt: skip
362367

363368
async def wait_state(
364369
self,

appdaemon/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def exception_handler(appdaemon: "AppDaemon", loop: asyncio.AbstractEventLoop, c
4444
"""Handler to attach to the main event loop as a backstop for any async exception"""
4545
user_exception_block(
4646
logging.getLogger('Error'),
47-
context['exception'],
47+
context.get('exception'),
4848
appdaemon.app_dir,
4949
header='Unhandled exception in event loop'
5050
)

appdaemon/http.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,12 @@ async def call_service(self, request):
691691

692692
self.logger.debug("call_service() args = %s", args)
693693

694-
res = await self.AD.services.call_service(namespace, domain, service, args)
694+
res = await self.AD.services.call_service(
695+
namespace=namespace,
696+
domain=domain,
697+
service=service,
698+
data=args
699+
) # fmt: skip
695700
return web.json_response({"response": res}, status=200, dumps=utils.convert_json)
696701

697702
except Exception:

appdaemon/models/config/appdaemon.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from pytz.tzinfo import BaseTzInfo
1010
from typing_extensions import deprecated
1111

12+
from appdaemon import utils
1213
from appdaemon.models.config.http import CoercedPath
1314

1415
from ...models.config.plugin import HASSConfig, MQTTConfig
1516
from ...version import __version__
1617
from .misc import FilterConfig, NamespaceConfig
1718

18-
1919
def plugin_discriminator(plugin):
2020
if isinstance(plugin, dict):
2121
return plugin["type"].lower()
@@ -67,7 +67,10 @@ class AppDaemonConfig(BaseModel, extra="allow"):
6767
admin_delay: int = 1
6868
plugin_performance_update: int = 10
6969
"""How often in seconds to update the admin entities with the plugin performance data"""
70-
max_utility_skew: timedelta = Field(default_factory=lambda: timedelta(seconds=2), before_validator=lambda v: timedelta(seconds=v))
70+
max_utility_skew: Annotated[
71+
timedelta,
72+
BeforeValidator(utils.convert_timedelta)
73+
] = Field(default_factory=lambda: timedelta(seconds=2))
7174
check_app_updates_profile: bool = False
7275
production_mode: bool = False
7376
invalid_config_warnings: bool = True
@@ -76,7 +79,12 @@ class AppDaemonConfig(BaseModel, extra="allow"):
7679
qsize_warning_threshold: int = 50
7780
qsize_warning_step: int = 60
7881
qsize_warning_iterations: int = 10
79-
internal_function_timeout: int = 60
82+
internal_function_timeout: Annotated[
83+
timedelta,
84+
BeforeValidator(utils.convert_timedelta)
85+
] = Field(default_factory=lambda: timedelta(seconds=60))
86+
"""Timeout for internal function calls. This determines how long apps can wait in their thread for an async function
87+
to complete in the main thread."""
8088
use_dictionary_unpacking: Annotated[bool, deprecated("This option is no longer necessary")] = False
8189
uvloop: bool = False
8290
use_stream: bool = False

appdaemon/models/config/plugin.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
from .common import CoercedPath
1212

13+
from appdaemon import utils
14+
1315

1416
class PluginConfig(BaseModel, extra="allow"):
1517
type: str
@@ -93,7 +95,12 @@ class HASSConfig(PluginConfig):
9395
cert_verify: bool | None = None
9496
commtype: str = "WS"
9597
q_timeout: int = 30
96-
return_result: bool | None = None
98+
ws_timeout: Annotated[
99+
timedelta,
100+
BeforeValidator(utils.convert_timedelta)
101+
] = Field(default_factory=lambda: timedelta(seconds=10))
102+
"""Default timeout for waiting for responses from the websocket connection"""
103+
# return_result: bool | None = None
97104
suppress_log_messages: bool = False
98105
retry_secs: int = 5
99106
services_sleep_time: int = 60

0 commit comments

Comments
 (0)