Skip to content

Commit 12129e9

Browse files
allenporterballoobMartinHjelmare
authored
Update service call return values and error handling (#94657)
* Update return signature of service calls * Add timeout error handling in websocket api for service calls * Update recorder tests to remove assertion on service call * Remove timeout behavior and update callers that depend on it today * Fix tests * Add missing else * await coro directly * Fix more tests * Update the intent task to use wait instead of timeout * Remove script service call limits and limit constants * Update tests that depend on service call limits * Use wait instead of wait_for and add test * Update homeassistant/helpers/intent.py Co-authored-by: Martin Hjelmare <[email protected]> --------- Co-authored-by: Paulus Schoutsen <[email protected]> Co-authored-by: Martin Hjelmare <[email protected]>
1 parent 950b25b commit 12129e9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+388
-434
lines changed

homeassistant/components/intent/__init__.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,22 @@ async def async_call_service(self, intent_obj: intent.Intent, state: State) -> N
7070
if state.domain == COVER_DOMAIN:
7171
# on = open
7272
# off = close
73-
await hass.services.async_call(
74-
COVER_DOMAIN,
75-
SERVICE_OPEN_COVER
76-
if self.service == SERVICE_TURN_ON
77-
else SERVICE_CLOSE_COVER,
78-
{ATTR_ENTITY_ID: state.entity_id},
79-
context=intent_obj.context,
80-
blocking=True,
81-
limit=self.service_timeout,
73+
await self._run_then_background(
74+
hass.async_create_task(
75+
hass.services.async_call(
76+
COVER_DOMAIN,
77+
SERVICE_OPEN_COVER
78+
if self.service == SERVICE_TURN_ON
79+
else SERVICE_CLOSE_COVER,
80+
{ATTR_ENTITY_ID: state.entity_id},
81+
context=intent_obj.context,
82+
blocking=True,
83+
)
84+
)
8285
)
86+
return
8387

84-
elif not hass.services.has_service(state.domain, self.service):
88+
if not hass.services.has_service(state.domain, self.service):
8589
raise intent.IntentHandleError(
8690
f"Service {self.service} does not support entity {state.entity_id}"
8791
)

homeassistant/core.py

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,6 @@
130130
# How long to wait to log tasks that are blocking
131131
BLOCK_LOG_TIMEOUT = 60
132132

133-
# How long we wait for the result of a service call
134-
SERVICE_CALL_LIMIT = 10 # seconds
135-
136133

137134
class ConfigSource(StrEnum):
138135
"""Source of core configuration."""
@@ -1807,17 +1804,14 @@ def call(
18071804
service_data: dict[str, Any] | None = None,
18081805
blocking: bool = False,
18091806
context: Context | None = None,
1810-
limit: float | None = SERVICE_CALL_LIMIT,
18111807
target: dict[str, Any] | None = None,
18121808
) -> bool | None:
18131809
"""Call a service.
18141810
18151811
See description of async_call for details.
18161812
"""
18171813
return asyncio.run_coroutine_threadsafe(
1818-
self.async_call(
1819-
domain, service, service_data, blocking, context, limit, target
1820-
),
1814+
self.async_call(domain, service, service_data, blocking, context, target),
18211815
self._hass.loop,
18221816
).result()
18231817

@@ -1828,16 +1822,11 @@ async def async_call(
18281822
service_data: dict[str, Any] | None = None,
18291823
blocking: bool = False,
18301824
context: Context | None = None,
1831-
limit: float | None = SERVICE_CALL_LIMIT,
18321825
target: dict[str, Any] | None = None,
1833-
) -> bool | None:
1826+
) -> None:
18341827
"""Call a service.
18351828
18361829
Specify blocking=True to wait until service is executed.
1837-
Waits a maximum of limit, which may be None for no timeout.
1838-
1839-
If blocking = True, will return boolean if service executed
1840-
successfully within limit.
18411830
18421831
This method will fire an event to indicate the service has been called.
18431832
@@ -1888,33 +1877,9 @@ async def async_call(
18881877
coro = self._execute_service(handler, service_call)
18891878
if not blocking:
18901879
self._run_service_in_background(coro, service_call)
1891-
return None
1880+
return
18921881

1893-
task = self._hass.async_create_task(coro)
1894-
try:
1895-
await asyncio.wait({task}, timeout=limit)
1896-
except asyncio.CancelledError:
1897-
# Task calling us was cancelled, so cancel service call task, and wait for
1898-
# it to be cancelled, within reason, before leaving.
1899-
_LOGGER.debug("Service call was cancelled: %s", service_call)
1900-
task.cancel()
1901-
await asyncio.wait({task}, timeout=SERVICE_CALL_LIMIT)
1902-
raise
1903-
1904-
if task.cancelled():
1905-
# Service call task was cancelled some other way, such as during shutdown.
1906-
_LOGGER.debug("Service was cancelled: %s", service_call)
1907-
raise asyncio.CancelledError
1908-
if task.done():
1909-
# Propagate any exceptions that might have happened during service call.
1910-
task.result()
1911-
# Service call completed successfully!
1912-
return True
1913-
# Service call task did not complete before timeout expired.
1914-
# Let it keep running in background.
1915-
self._run_service_in_background(task, service_call)
1916-
_LOGGER.debug("Service did not complete before timeout: %s", service_call)
1917-
return False
1882+
await coro
19181883

19191884
def _run_service_in_background(
19201885
self,

homeassistant/helpers/intent.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -493,15 +493,36 @@ async def async_handle_states(
493493
async def async_call_service(self, intent_obj: Intent, state: State) -> None:
494494
"""Call service on entity."""
495495
hass = intent_obj.hass
496-
await hass.services.async_call(
497-
self.domain,
498-
self.service,
499-
{ATTR_ENTITY_ID: state.entity_id},
500-
context=intent_obj.context,
501-
blocking=True,
502-
limit=self.service_timeout,
496+
await self._run_then_background(
497+
hass.async_create_task(
498+
hass.services.async_call(
499+
self.domain,
500+
self.service,
501+
{ATTR_ENTITY_ID: state.entity_id},
502+
context=intent_obj.context,
503+
blocking=True,
504+
),
505+
f"intent_call_service_{self.domain}_{self.service}",
506+
)
503507
)
504508

509+
async def _run_then_background(self, task: asyncio.Task) -> None:
510+
"""Run task with timeout to (hopefully) catch validation errors.
511+
512+
After the timeout the task will continue to run in the background.
513+
"""
514+
try:
515+
await asyncio.wait({task}, timeout=self.service_timeout)
516+
except asyncio.TimeoutError:
517+
pass
518+
except asyncio.CancelledError:
519+
# Task calling us was cancelled, so cancel service call task, and wait for
520+
# it to be cancelled, within reason, before leaving.
521+
_LOGGER.debug("Service call was cancelled: %s", task.get_name())
522+
task.cancel()
523+
await asyncio.wait({task}, timeout=5)
524+
raise
525+
505526

506527
class IntentCategory(Enum):
507528
"""Category of an intent."""

homeassistant/helpers/script.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
SERVICE_TURN_ON,
6565
)
6666
from homeassistant.core import (
67-
SERVICE_CALL_LIMIT,
6867
Context,
6968
Event,
7069
HassJob,
@@ -664,28 +663,16 @@ async def _async_call_service_step(self):
664663
and params[CONF_SERVICE] == "trigger"
665664
or params[CONF_DOMAIN] in ("python_script", "script")
666665
)
667-
# If this might start a script then disable the call timeout.
668-
# Otherwise use the normal service call limit.
669-
if running_script:
670-
limit = None
671-
else:
672-
limit = SERVICE_CALL_LIMIT
673-
674-
trace_set_result(params=params, running_script=running_script, limit=limit)
675-
service_task = self._hass.async_create_task(
676-
self._hass.services.async_call(
677-
**params,
678-
blocking=True,
679-
context=self._context,
680-
limit=limit,
666+
trace_set_result(params=params, running_script=running_script)
667+
await self._async_run_long_action(
668+
self._hass.async_create_task(
669+
self._hass.services.async_call(
670+
**params,
671+
blocking=True,
672+
context=self._context,
673+
)
681674
)
682675
)
683-
if limit is not None:
684-
# There is a call limit, so just wait for it to finish.
685-
await service_task
686-
return
687-
688-
await self._async_run_long_action(service_task)
689676

690677
async def _async_device_step(self):
691678
"""Perform the device automation specified in the action."""

tests/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,7 @@ def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]:
13531353
def capture_events(event: Event) -> None:
13541354
events.append(event)
13551355

1356-
hass.bus.async_listen(event_name, capture_events)
1356+
hass.bus.async_listen(event_name, capture_events, run_immediately=True)
13571357

13581358
return events
13591359

tests/components/abode/test_light.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ async def test_switch_off(hass: HomeAssistant) -> None:
6363
await setup_platform(hass, LIGHT_DOMAIN)
6464

6565
with patch("jaraco.abode.devices.light.Light.switch_off") as mock_switch_off:
66-
assert await hass.services.async_call(
66+
await hass.services.async_call(
6767
LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True
6868
)
6969
await hass.async_block_till_done()

tests/components/abode/test_switch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ async def test_switch_on(hass: HomeAssistant) -> None:
4949
await setup_platform(hass, SWITCH_DOMAIN)
5050

5151
with patch("jaraco.abode.devices.switch.Switch.switch_on") as mock_switch_on:
52-
assert await hass.services.async_call(
52+
await hass.services.async_call(
5353
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True
5454
)
5555
await hass.async_block_till_done()
@@ -62,7 +62,7 @@ async def test_switch_off(hass: HomeAssistant) -> None:
6262
await setup_platform(hass, SWITCH_DOMAIN)
6363

6464
with patch("jaraco.abode.devices.switch.Switch.switch_off") as mock_switch_off:
65-
assert await hass.services.async_call(
65+
await hass.services.async_call(
6666
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True
6767
)
6868
await hass.async_block_till_done()

0 commit comments

Comments
 (0)