Skip to content

Commit b2f9dd6

Browse files
authored
Merge pull request #2258 from AppDaemon/call_service-fix
final type stuff
2 parents 3238273 + cf5d64c commit b2f9dd6

File tree

4 files changed

+125
-116
lines changed

4 files changed

+125
-116
lines changed

appdaemon/plugins/hass/exceptions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11

2+
from dataclasses import dataclass, field
3+
4+
from appdaemon import exceptions as ade
5+
6+
27
class HAAuthenticationError(Exception):
38
pass
49

@@ -9,3 +14,17 @@ class HAEventsSubError(Exception):
914

1015
class HAFailedAuthentication(Exception):
1116
pass
17+
18+
19+
@dataclass
20+
class ScriptNotFound(ade.AppDaemonException):
21+
script_name: str
22+
namespace: str
23+
plugin_name: str
24+
domain: str = field(init=False, default="script")
25+
26+
def __str__(self):
27+
res = f"'{self.script_name}' not found in plugin '{self.plugin_name}'"
28+
if self.namespace != "default":
29+
res += f" with namespace '{self.namespace}'"
30+
return res

appdaemon/plugins/hass/hassapi.py

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from ast import literal_eval
33
from collections.abc import Iterable
44
from copy import deepcopy
5-
from dataclasses import dataclass, field
65
from datetime import datetime, timedelta
76
from pathlib import Path
87
from typing import TYPE_CHECKING, Any, Callable, Literal, Type, overload
@@ -15,9 +14,10 @@
1514
from appdaemon.models.notification.android import AndroidData
1615
from appdaemon.models.notification.base import NotificationData
1716
from appdaemon.models.notification.iOS import iOSData
17+
from appdaemon.plugins.hass.exceptions import ScriptNotFound
1818
from appdaemon.plugins.hass.hassplugin import HassPlugin
1919
from appdaemon.plugins.hass.notifications import AndroidNotification
20-
20+
from appdaemon.services import ServiceCallback
2121

2222
# Check if the module is being imported using the legacy method
2323
if __name__ == Path(__file__).name:
@@ -36,20 +36,6 @@
3636
from ...models.config.app import AppConfig
3737

3838

39-
@dataclass
40-
class ScriptNotFound(ade.AppDaemonException):
41-
script_name: str
42-
namespace: str
43-
plugin_name: str
44-
domain: str = field(init=False, default="script")
45-
46-
def __str__(self):
47-
res = f"'{self.script_name}' not found in plugin '{self.plugin_name}'"
48-
if self.namespace != "default":
49-
res += f" with namespace '{self.namespace}'"
50-
return res
51-
52-
5339
class Hass(ADBase, ADAPI):
5440
"""HASS API class for the users to inherit from.
5541
@@ -438,6 +424,105 @@ def constrain_input_select(self, value: str | Iterable[str]) -> bool:
438424
# Helper functions for services
439425
#
440426

427+
@overload
428+
@utils.sync_decorator
429+
async def call_service(
430+
self,
431+
service: str,
432+
namespace: str | None = None,
433+
timeout: str | int | float | None = None,
434+
callback: ServiceCallback | None = None,
435+
hass_timeout: str | int | float | None = None,
436+
suppress_log_messages: bool = False,
437+
**data,
438+
) -> Any: ...
439+
440+
@utils.sync_decorator
441+
async def call_service(self, *args, **kwargs) -> Any:
442+
"""Calls a Service within AppDaemon.
443+
444+
Services represent specific actions, and are generally registered by plugins or provided by AppDaemon itself.
445+
The app calls the service only by referencing the service with a string in the format ``<domain>/<service>``, so
446+
there is no direct coupling between apps and services. This allows any app to call any service, even ones from
447+
other plugins.
448+
449+
Services often require additional parameters, such as ``entity_id``, which AppDaemon will pass to the service
450+
call as appropriate, if used when calling this function. This allows arbitrary data to be passed to the service
451+
calls.
452+
453+
Apps can also register their own services using their ``self.regsiter_service`` method.
454+
455+
Args:
456+
service (str): The service name in the format `<domain>/<service>`. For example, `light/turn_on`.
457+
namespace (str, optional): It's safe to ignore this parameter in most cases because the default namespace
458+
will be used. However, if a `namespace` is provided, the service call will be made in that namespace. If
459+
there's a plugin associated with that namespace, it will do the service call. If no namespace is given,
460+
AppDaemon will use the app's namespace, which can be set using the ``self.set_namespace`` method. See
461+
the section on `namespaces <APPGUIDE.html#namespaces>`__ for more information.
462+
timeout (str | int | float, optional): The internal AppDaemon timeout for the service call. If no value is
463+
specified, the default timeout is 60s. The default value can be changed using the
464+
``appdaemon.internal_function_timeout`` config setting.
465+
callback (callable): The non-async callback to be executed when complete. It should accept a single
466+
argument, which will be the result of the service call. This is the recommended method for calling
467+
services which might take a long time to complete. This effectively bypasses the ``timeout`` argument
468+
because it only applies to this function, which will return immediately instead of waiting for the
469+
result if a `callback` is specified.
470+
hass_timeout (str | int | float, optional): Only applicable to the Hass plugin. Sets the amount of time to
471+
wait for a response from Home Assistant. If no value is specified, the default timeout is 10s. The
472+
default value can be changed using the ``ws_timeout`` setting the in the Hass plugin configuration in
473+
``appdaemon.yaml``. Even if no data is returned from the service call, Home Assistant will still send an
474+
acknowledgement back to AppDaemon, which this timeout applies to. Note that this is separate from the
475+
``timeout``. If ``timeout`` is shorter than this one, it will trigger before this one does.
476+
suppress_log_messages (bool, optional): Only applicable to the Hass plugin. If this is set to ``True``,
477+
Appdaemon will suppress logging of warnings for service calls to Home Assistant, specifically timeouts
478+
and non OK statuses. Use this flag and set it to ``True`` to supress these log messages if you are
479+
performing your own error checking as described `here <APPGUIDE.html#some-notes-on-service-calls>`__
480+
service_data (dict, optional): Used as an additional dictionary to pass arguments into the ``service_data``
481+
field of the JSON that goes to Home Assistant. This is useful if you have a dictionary that you want to
482+
pass in that has a key like ``target`` which is otherwise used for the ``target`` argument.
483+
**data: Any other keyword arguments get passed to the service call as ``service_data``. Each service takes
484+
different parameters, so this will vary from service to service. For example, most services require
485+
``entity_id``. The parameters for each service can be found in the actions tab of developer tools in
486+
the Home Assistant web interface.
487+
488+
Returns:
489+
Result of the `call_service` function if any, see
490+
`service call notes <APPGUIDE.html#some-notes-on-service-calls>`__ for more details.
491+
492+
Examples:
493+
HASS
494+
^^^^
495+
496+
>>> self.call_service("light/turn_on", entity_id="light.office_lamp", color_name="red")
497+
>>> self.call_service("notify/notify", title="Hello", message="Hello World")
498+
>>> events = self.call_service(
499+
"calendar/get_events",
500+
entity_id="calendar.home",
501+
start_date_time="2024-08-25 00:00:00",
502+
end_date_time="2024-08-27 00:00:00",
503+
)["result"]["response"]["calendar.home"]["events"]
504+
505+
MQTT
506+
^^^^
507+
508+
>>> self.call_service("mqtt/subscribe", topic="homeassistant/living_room/light", qos=2)
509+
>>> self.call_service("mqtt/publish", topic="homeassistant/living_room/light", payload="on")
510+
511+
Utility
512+
^^^^^^^
513+
514+
It's important that the ``namespace`` arg is set to ``admin`` for these services, as they do not exist
515+
within the default namespace, and apps cannot exist in the ``admin`` namespace. If the namespace is not
516+
specified, calling the method will raise an exception.
517+
518+
>>> self.call_service("app/restart", app="notify_app", namespace="admin")
519+
>>> self.call_service("app/stop", app="lights_app", namespace="admin")
520+
>>> self.call_service("app/reload", namespace="admin")
521+
522+
"""
523+
# We just wrap the ADAPI.call_service method here to add some additional arguments and docstrings
524+
return await super().call_service(*args, **kwargs)
525+
441526
def get_service_info(self, service: str) -> dict | None:
442527
"""Get some information about what kind of data the service expects to receive, which is helpful for debugging.
443528

appdaemon/plugins/hass/hassapi.pyi

Lines changed: 0 additions & 99 deletions
This file was deleted.

appdaemon/services.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import threading
44
from collections import defaultdict
55
from logging import Logger
6-
from typing import TYPE_CHECKING, Any, Callable
6+
from typing import TYPE_CHECKING, Any, Callable, Protocol
77

88
from appdaemon import utils
99
from appdaemon.exceptions import DomainException, NamespaceException, ServiceException
@@ -12,6 +12,10 @@
1212
from appdaemon.appdaemon import AppDaemon
1313

1414

15+
class ServiceCallback(Protocol):
16+
def __call__(self, result: Any) -> None: ...
17+
18+
1519
class Services:
1620
"""Subsystem container for handling services
1721

0 commit comments

Comments
 (0)