mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Add service response data for listing calendar events (#94759)
* Add service response data for listing calendar events Add the capability of response data for for the entity component. * Rename input arguments and add service description * Improve list events to be more user friendly Allow the end date to be determined based on a relative time duration. Make the start time optional and set to "now". Add additional test coverage. Update demo calendar to actually perform date range checks. * Wrap docstrings properly. * Increase test coverage * Update to use new API calls * Readability improvements * Wrap docstrings * Require at least one of end or duration * Check for multiple entity matches earlier in the request * Update documentation strings
This commit is contained in:
parent
65454c945d
commit
b9b5fe6be8
@ -8,7 +8,7 @@ from http import HTTPStatus
|
||||
from itertools import groupby
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, cast, final
|
||||
from typing import Any, Final, cast, final
|
||||
|
||||
from aiohttp import web
|
||||
from dateutil.rrule import rrulestr
|
||||
@ -19,7 +19,12 @@ from homeassistant.components.websocket_api import ERR_NOT_FOUND, ERR_NOT_SUPPOR
|
||||
from homeassistant.components.websocket_api.connection import ActiveConnection
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
@ -32,10 +37,12 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.json import JsonValueType
|
||||
|
||||
from .const import (
|
||||
CONF_EVENT,
|
||||
EVENT_DESCRIPTION,
|
||||
EVENT_DURATION,
|
||||
EVENT_END,
|
||||
EVENT_END_DATE,
|
||||
EVENT_END_DATETIME,
|
||||
@ -250,6 +257,21 @@ CALENDAR_EVENT_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_LIST_EVENTS: Final = "list_events"
|
||||
SERVICE_LIST_EVENTS_SCHEMA: Final = vol.All(
|
||||
cv.has_at_least_one_key(EVENT_END_DATETIME, EVENT_DURATION),
|
||||
cv.has_at_most_one_key(EVENT_END_DATETIME, EVENT_DURATION),
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Optional(EVENT_START_DATETIME): datetime.datetime,
|
||||
vol.Optional(EVENT_END_DATETIME): datetime.datetime,
|
||||
vol.Optional(EVENT_DURATION): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Track states and offer events for calendars."""
|
||||
@ -274,7 +296,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_create_event,
|
||||
required_features=[CalendarEntityFeature.CREATE_EVENT],
|
||||
)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_LIST_EVENTS,
|
||||
SERVICE_LIST_EVENTS_SCHEMA,
|
||||
async_list_events_service,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
@ -743,3 +770,21 @@ async def async_create_event(entity: CalendarEntity, call: ServiceCall) -> None:
|
||||
EVENT_END: end,
|
||||
}
|
||||
await entity.async_create_event(**params)
|
||||
|
||||
|
||||
async def async_list_events_service(
|
||||
calendar: CalendarEntity, service_call: ServiceCall
|
||||
) -> ServiceResponse:
|
||||
"""List events on a calendar during a time drange."""
|
||||
start = service_call.data.get(EVENT_START_DATETIME, dt_util.now())
|
||||
if EVENT_DURATION in service_call.data:
|
||||
end = start + service_call.data[EVENT_DURATION]
|
||||
else:
|
||||
end = service_call.data[EVENT_END_DATETIME]
|
||||
calendar_event_list = await calendar.async_get_events(calendar.hass, start, end)
|
||||
events: list[JsonValueType] = [
|
||||
dataclasses.asdict(event) for event in calendar_event_list
|
||||
]
|
||||
return {
|
||||
"events": events,
|
||||
}
|
||||
|
@ -40,3 +40,4 @@ EVENT_TIME_FIELDS = {
|
||||
EVENT_IN,
|
||||
}
|
||||
EVENT_TYPES = "event_types"
|
||||
EVENT_DURATION = "duration"
|
||||
|
@ -52,3 +52,27 @@ create_event:
|
||||
example: "Conference Room - F123, Bldg. 002"
|
||||
selector:
|
||||
text:
|
||||
list_events:
|
||||
name: List event
|
||||
description: List events on a calendar within a time range.
|
||||
target:
|
||||
entity:
|
||||
domain: calendar
|
||||
fields:
|
||||
start_date_time:
|
||||
name: Start time
|
||||
description: Return active events after this time (exclusive). When not set, defaults to now.
|
||||
example: "2022-03-22 20:00:00"
|
||||
selector:
|
||||
datetime:
|
||||
end_date_time:
|
||||
name: End time
|
||||
description: Return active events before this time (exclusive). Cannot be used with 'duration'.
|
||||
example: "2022-03-22 22:00:00"
|
||||
selector:
|
||||
datetime:
|
||||
duration:
|
||||
name: Duration
|
||||
description: Return active events from start_date_time until the specified duration.
|
||||
selector:
|
||||
duration:
|
||||
|
@ -27,10 +27,10 @@ def setup_platform(
|
||||
|
||||
def calendar_data_future() -> CalendarEvent:
|
||||
"""Representation of a Demo Calendar for a future event."""
|
||||
one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
|
||||
half_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
|
||||
return CalendarEvent(
|
||||
start=one_hour_from_now,
|
||||
end=one_hour_from_now + datetime.timedelta(minutes=60),
|
||||
start=half_hour_from_now,
|
||||
end=half_hour_from_now + datetime.timedelta(minutes=60),
|
||||
summary="Future Event",
|
||||
description="Future Description",
|
||||
location="Future Location",
|
||||
@ -67,4 +67,9 @@ class DemoCalendar(CalendarEntity):
|
||||
end_date: datetime.datetime,
|
||||
) -> list[CalendarEvent]:
|
||||
"""Return calendar events within a datetime range."""
|
||||
assert start_date < end_date
|
||||
if self._event.start_datetime_local >= end_date:
|
||||
return []
|
||||
if self._event.end_datetime_local < start_date:
|
||||
return []
|
||||
return [self._event]
|
||||
|
@ -12,7 +12,7 @@ import logging
|
||||
import math
|
||||
import sys
|
||||
from timeit import default_timer as timer
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, final
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, TypeVar, final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -49,6 +49,9 @@ from .typing import UNDEFINED, StateType, UndefinedType
|
||||
if TYPE_CHECKING:
|
||||
from .entity_platform import EntityPlatform
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SLOW_UPDATE_WARNING = 10
|
||||
DATA_ENTITY_SOURCE = "entity_info"
|
||||
@ -1130,13 +1133,13 @@ class Entity(ABC):
|
||||
"""Return the representation."""
|
||||
return f"<entity {self.entity_id}={self._stringify_state(self.available)}>"
|
||||
|
||||
async def async_request_call(self, coro: Coroutine[Any, Any, Any]) -> None:
|
||||
async def async_request_call(self, coro: Coroutine[Any, Any, _T]) -> _T:
|
||||
"""Process request batched."""
|
||||
if self.parallel_updates:
|
||||
await self.parallel_updates.acquire()
|
||||
|
||||
try:
|
||||
await coro
|
||||
return await coro
|
||||
finally:
|
||||
if self.parallel_updates:
|
||||
self.parallel_updates.release()
|
||||
|
@ -19,7 +19,14 @@ from homeassistant.const import (
|
||||
CONF_SCAN_INTERVAL,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import (
|
||||
Event,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
@ -217,18 +224,21 @@ class EntityComponent(Generic[_EntityT]):
|
||||
schema: dict[str | vol.Marker, Any] | vol.Schema,
|
||||
func: str | Callable[..., Any],
|
||||
required_features: list[int] | None = None,
|
||||
supports_response: SupportsResponse = SupportsResponse.NONE,
|
||||
) -> None:
|
||||
"""Register an entity service."""
|
||||
if isinstance(schema, dict):
|
||||
schema = cv.make_entity_service_schema(schema)
|
||||
|
||||
async def handle_service(call: ServiceCall) -> None:
|
||||
async def handle_service(call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle the service."""
|
||||
await service.entity_service_call(
|
||||
return await service.entity_service_call(
|
||||
self.hass, self._platforms.values(), func, call, required_features
|
||||
)
|
||||
|
||||
self.hass.services.async_register(self.domain, name, handle_service, schema)
|
||||
self.hass.services.async_register(
|
||||
self.domain, name, handle_service, schema, supports_response
|
||||
)
|
||||
|
||||
async def async_setup_platform(
|
||||
self,
|
||||
|
@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from collections.abc import Awaitable, Callable, Coroutine, Iterable
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from functools import cache, partial, wraps
|
||||
@ -26,7 +26,13 @@ from homeassistant.const import (
|
||||
ENTITY_MATCH_ALL,
|
||||
ENTITY_MATCH_NONE,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import (
|
||||
Context,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
TemplateError,
|
||||
@ -672,10 +678,10 @@ def async_set_service_schema(
|
||||
async def entity_service_call( # noqa: C901
|
||||
hass: HomeAssistant,
|
||||
platforms: Iterable[EntityPlatform],
|
||||
func: str | Callable[..., Any],
|
||||
func: str | Callable[..., Coroutine[Any, Any, ServiceResponse]],
|
||||
call: ServiceCall,
|
||||
required_features: Iterable[int] | None = None,
|
||||
) -> None:
|
||||
) -> ServiceResponse | None:
|
||||
"""Handle an entity service call.
|
||||
|
||||
Calls all platforms simultaneously.
|
||||
@ -791,7 +797,16 @@ async def entity_service_call( # noqa: C901
|
||||
entities.append(entity)
|
||||
|
||||
if not entities:
|
||||
return
|
||||
if call.return_response:
|
||||
raise HomeAssistantError(
|
||||
"Service call requested response data but did not match any entities"
|
||||
)
|
||||
return None
|
||||
|
||||
if call.return_response and len(entities) != 1:
|
||||
raise HomeAssistantError(
|
||||
"Service call requested response data but matched more than one entity"
|
||||
)
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
[
|
||||
@ -804,8 +819,10 @@ async def entity_service_call( # noqa: C901
|
||||
]
|
||||
)
|
||||
assert not pending
|
||||
for future in done:
|
||||
future.result() # pop exception if have
|
||||
|
||||
response_data: ServiceResponse | None
|
||||
for task in done:
|
||||
response_data = task.result() # pop exception if have
|
||||
|
||||
tasks: list[asyncio.Task[None]] = []
|
||||
|
||||
@ -824,28 +841,32 @@ async def entity_service_call( # noqa: C901
|
||||
for future in done:
|
||||
future.result() # pop exception if have
|
||||
|
||||
return response_data if call.return_response else None
|
||||
|
||||
|
||||
async def _handle_entity_call(
|
||||
hass: HomeAssistant,
|
||||
entity: Entity,
|
||||
func: str | Callable[..., Any],
|
||||
func: str | Callable[..., Coroutine[Any, Any, ServiceResponse]],
|
||||
data: dict | ServiceCall,
|
||||
context: Context,
|
||||
) -> None:
|
||||
) -> ServiceResponse:
|
||||
"""Handle calling service method."""
|
||||
entity.async_set_context(context)
|
||||
|
||||
task: asyncio.Future[ServiceResponse] | None
|
||||
if isinstance(func, str):
|
||||
result = hass.async_run_job(
|
||||
task = hass.async_run_job(
|
||||
partial(getattr(entity, func), **data) # type: ignore[arg-type]
|
||||
)
|
||||
else:
|
||||
result = hass.async_run_job(func, entity, data)
|
||||
task = hass.async_run_job(func, entity, data)
|
||||
|
||||
# Guard because callback functions do not return a task when passed to
|
||||
# async_run_job.
|
||||
if result is not None:
|
||||
await result
|
||||
result: ServiceResponse | None = None
|
||||
if task is not None:
|
||||
result = await task
|
||||
|
||||
if asyncio.iscoroutine(result):
|
||||
_LOGGER.error(
|
||||
@ -856,7 +877,9 @@ async def _handle_entity_call(
|
||||
func,
|
||||
entity.entity_id,
|
||||
)
|
||||
await result
|
||||
result = await result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@bind_hass
|
||||
|
@ -10,7 +10,7 @@ import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components.calendar import DOMAIN
|
||||
from homeassistant.components.calendar import DOMAIN, SERVICE_LIST_EVENTS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.util.dt as dt_util
|
||||
@ -384,3 +384,122 @@ async def test_create_event_service_invalid_params(
|
||||
target={"entity_id": "calendar.calendar_1"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_list_events_service(hass: HomeAssistant) -> None:
|
||||
"""Test listing events from the service call using exlplicit start and end time."""
|
||||
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
start = dt_util.now()
|
||||
end = start + timedelta(days=1)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_LIST_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.calendar_1",
|
||||
"start_date_time": start,
|
||||
"end_date_time": end,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response
|
||||
assert "events" in response
|
||||
events = response["events"]
|
||||
assert len(events) == 1
|
||||
assert events[0]["summary"] == "Future Event"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity", "duration", "expected_events"),
|
||||
[
|
||||
# Calendar 1 has an hour long event starting in 30 minutes. No events in the
|
||||
# next 15 minutes, but it shows up an hour from now.
|
||||
("calendar.calendar_1", "00:15:00", []),
|
||||
("calendar.calendar_1", "01:00:00", ["Future Event"]),
|
||||
# Calendar 2 has a active event right now
|
||||
("calendar.calendar_2", "00:15:00", ["Current Event"]),
|
||||
],
|
||||
)
|
||||
async def test_list_events_service_duration(
|
||||
hass: HomeAssistant,
|
||||
entity: str,
|
||||
duration: str,
|
||||
expected_events: list[str],
|
||||
) -> None:
|
||||
"""Test listing events using a time duration."""
|
||||
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_LIST_EVENTS,
|
||||
{
|
||||
"entity_id": entity,
|
||||
"duration": duration,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response
|
||||
assert "events" in response
|
||||
events = response["events"]
|
||||
assert [event["summary"] for event in events] == expected_events
|
||||
|
||||
|
||||
async def test_list_events_positive_duration(hass: HomeAssistant) -> None:
|
||||
"""Test listing events requires a positive duration."""
|
||||
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(vol.Invalid, match="should be positive"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_LIST_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.calendar_1",
|
||||
"duration": "-01:00:00",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None:
|
||||
"""Test listing events specifying fields that are exclusive."""
|
||||
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
end = dt_util.now() + timedelta(days=1)
|
||||
|
||||
with pytest.raises(vol.Invalid, match="at most one of"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_LIST_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.calendar_1",
|
||||
"end_date_time": end,
|
||||
"duration": "01:00:00",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_list_events_missing_fields(hass: HomeAssistant) -> None:
|
||||
"""Test listing events missing some required fields."""
|
||||
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(vol.Invalid, match="at least one of"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_LIST_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.calendar_1",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
@ -14,8 +14,14 @@ from homeassistant.const import (
|
||||
ENTITY_MATCH_NONE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity_component import EntityComponent, async_update_entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@ -474,7 +480,7 @@ async def test_extract_all_use_match_all(
|
||||
|
||||
|
||||
async def test_register_entity_service(hass: HomeAssistant) -> None:
|
||||
"""Test not expanding a group."""
|
||||
"""Test registering an enttiy service and calling it."""
|
||||
entity = MockEntity(entity_id=f"{DOMAIN}.entity")
|
||||
calls = []
|
||||
|
||||
@ -524,6 +530,70 @@ async def test_register_entity_service(hass: HomeAssistant) -> None:
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
async def test_register_entity_service_response_data(hass: HomeAssistant) -> None:
|
||||
"""Test an enttiy service that does not support response data."""
|
||||
entity = MockEntity(entity_id=f"{DOMAIN}.entity")
|
||||
|
||||
async def generate_response(
|
||||
target: MockEntity, call: ServiceCall
|
||||
) -> ServiceResponse:
|
||||
assert call.return_response
|
||||
return {"response-key": "response-value"}
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
await component.async_setup({})
|
||||
await component.async_add_entities([entity])
|
||||
|
||||
component.async_register_entity_service(
|
||||
"hello",
|
||||
{"some": str},
|
||||
generate_response,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
response_data = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"hello",
|
||||
service_data={"entity_id": entity.entity_id, "some": "data"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response_data == {"response-key": "response-value"}
|
||||
|
||||
|
||||
async def test_register_entity_service_response_data_multiple_matches(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test asking for service response data but matching many entities."""
|
||||
entity1 = MockEntity(entity_id=f"{DOMAIN}.entity1")
|
||||
entity2 = MockEntity(entity_id=f"{DOMAIN}.entity2")
|
||||
|
||||
async def generate_response(
|
||||
target: MockEntity, call: ServiceCall
|
||||
) -> ServiceResponse:
|
||||
raise ValueError("Should not be invoked")
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
await component.async_setup({})
|
||||
await component.async_add_entities([entity1, entity2])
|
||||
|
||||
component.async_register_entity_service(
|
||||
"hello",
|
||||
{},
|
||||
generate_response,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="matched more than one entity"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"hello",
|
||||
target={"entity_id": [entity1.entity_id, entity2.entity_id]},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_platforms_shutdown_on_stop(hass: HomeAssistant) -> None:
|
||||
"""Test that we shutdown platforms on stop."""
|
||||
platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None])
|
||||
|
Loading…
x
Reference in New Issue
Block a user