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
|
from itertools import groupby
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any, cast, final
|
from typing import Any, Final, cast, final
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from dateutil.rrule import rrulestr
|
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.components.websocket_api.connection import ActiveConnection
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
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
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
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.template import DATE_STR_FORMAT
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
from homeassistant.util.json import JsonValueType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
EVENT_DESCRIPTION,
|
EVENT_DESCRIPTION,
|
||||||
|
EVENT_DURATION,
|
||||||
EVENT_END,
|
EVENT_END,
|
||||||
EVENT_END_DATE,
|
EVENT_END_DATE,
|
||||||
EVENT_END_DATETIME,
|
EVENT_END_DATETIME,
|
||||||
@ -250,6 +257,21 @@ CALENDAR_EVENT_SCHEMA = vol.Schema(
|
|||||||
extra=vol.ALLOW_EXTRA,
|
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:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Track states and offer events for calendars."""
|
"""Track states and offer events for calendars."""
|
||||||
@ -274,7 +296,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
async_create_event,
|
async_create_event,
|
||||||
required_features=[CalendarEntityFeature.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)
|
await component.async_setup(config)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -743,3 +770,21 @@ async def async_create_event(entity: CalendarEntity, call: ServiceCall) -> None:
|
|||||||
EVENT_END: end,
|
EVENT_END: end,
|
||||||
}
|
}
|
||||||
await entity.async_create_event(**params)
|
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_IN,
|
||||||
}
|
}
|
||||||
EVENT_TYPES = "event_types"
|
EVENT_TYPES = "event_types"
|
||||||
|
EVENT_DURATION = "duration"
|
||||||
|
@ -52,3 +52,27 @@ create_event:
|
|||||||
example: "Conference Room - F123, Bldg. 002"
|
example: "Conference Room - F123, Bldg. 002"
|
||||||
selector:
|
selector:
|
||||||
text:
|
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:
|
def calendar_data_future() -> CalendarEvent:
|
||||||
"""Representation of a Demo Calendar for a future event."""
|
"""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(
|
return CalendarEvent(
|
||||||
start=one_hour_from_now,
|
start=half_hour_from_now,
|
||||||
end=one_hour_from_now + datetime.timedelta(minutes=60),
|
end=half_hour_from_now + datetime.timedelta(minutes=60),
|
||||||
summary="Future Event",
|
summary="Future Event",
|
||||||
description="Future Description",
|
description="Future Description",
|
||||||
location="Future Location",
|
location="Future Location",
|
||||||
@ -67,4 +67,9 @@ class DemoCalendar(CalendarEntity):
|
|||||||
end_date: datetime.datetime,
|
end_date: datetime.datetime,
|
||||||
) -> list[CalendarEvent]:
|
) -> list[CalendarEvent]:
|
||||||
"""Return calendar events within a datetime range."""
|
"""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]
|
return [self._event]
|
||||||
|
@ -12,7 +12,7 @@ import logging
|
|||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
from timeit import default_timer as timer
|
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
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -49,6 +49,9 @@ from .typing import UNDEFINED, StateType, UndefinedType
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .entity_platform import EntityPlatform
|
from .entity_platform import EntityPlatform
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
SLOW_UPDATE_WARNING = 10
|
SLOW_UPDATE_WARNING = 10
|
||||||
DATA_ENTITY_SOURCE = "entity_info"
|
DATA_ENTITY_SOURCE = "entity_info"
|
||||||
@ -1130,13 +1133,13 @@ class Entity(ABC):
|
|||||||
"""Return the representation."""
|
"""Return the representation."""
|
||||||
return f"<entity {self.entity_id}={self._stringify_state(self.available)}>"
|
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."""
|
"""Process request batched."""
|
||||||
if self.parallel_updates:
|
if self.parallel_updates:
|
||||||
await self.parallel_updates.acquire()
|
await self.parallel_updates.acquire()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await coro
|
return await coro
|
||||||
finally:
|
finally:
|
||||||
if self.parallel_updates:
|
if self.parallel_updates:
|
||||||
self.parallel_updates.release()
|
self.parallel_updates.release()
|
||||||
|
@ -19,7 +19,14 @@ from homeassistant.const import (
|
|||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
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.exceptions import HomeAssistantError
|
||||||
from homeassistant.loader import async_get_integration, bind_hass
|
from homeassistant.loader import async_get_integration, bind_hass
|
||||||
from homeassistant.setup import async_prepare_setup_platform
|
from homeassistant.setup import async_prepare_setup_platform
|
||||||
@ -217,18 +224,21 @@ class EntityComponent(Generic[_EntityT]):
|
|||||||
schema: dict[str | vol.Marker, Any] | vol.Schema,
|
schema: dict[str | vol.Marker, Any] | vol.Schema,
|
||||||
func: str | Callable[..., Any],
|
func: str | Callable[..., Any],
|
||||||
required_features: list[int] | None = None,
|
required_features: list[int] | None = None,
|
||||||
|
supports_response: SupportsResponse = SupportsResponse.NONE,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register an entity service."""
|
"""Register an entity service."""
|
||||||
if isinstance(schema, dict):
|
if isinstance(schema, dict):
|
||||||
schema = cv.make_entity_service_schema(schema)
|
schema = cv.make_entity_service_schema(schema)
|
||||||
|
|
||||||
async def handle_service(call: ServiceCall) -> None:
|
async def handle_service(call: ServiceCall) -> ServiceResponse:
|
||||||
"""Handle the service."""
|
"""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, 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(
|
async def async_setup_platform(
|
||||||
self,
|
self,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable, Iterable
|
from collections.abc import Awaitable, Callable, Coroutine, Iterable
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import cache, partial, wraps
|
from functools import cache, partial, wraps
|
||||||
@ -26,7 +26,13 @@ from homeassistant.const import (
|
|||||||
ENTITY_MATCH_ALL,
|
ENTITY_MATCH_ALL,
|
||||||
ENTITY_MATCH_NONE,
|
ENTITY_MATCH_NONE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, HomeAssistant, ServiceCall, callback
|
from homeassistant.core import (
|
||||||
|
Context,
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
ServiceResponse,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
TemplateError,
|
TemplateError,
|
||||||
@ -672,10 +678,10 @@ def async_set_service_schema(
|
|||||||
async def entity_service_call( # noqa: C901
|
async def entity_service_call( # noqa: C901
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
platforms: Iterable[EntityPlatform],
|
platforms: Iterable[EntityPlatform],
|
||||||
func: str | Callable[..., Any],
|
func: str | Callable[..., Coroutine[Any, Any, ServiceResponse]],
|
||||||
call: ServiceCall,
|
call: ServiceCall,
|
||||||
required_features: Iterable[int] | None = None,
|
required_features: Iterable[int] | None = None,
|
||||||
) -> None:
|
) -> ServiceResponse | None:
|
||||||
"""Handle an entity service call.
|
"""Handle an entity service call.
|
||||||
|
|
||||||
Calls all platforms simultaneously.
|
Calls all platforms simultaneously.
|
||||||
@ -791,7 +797,16 @@ async def entity_service_call( # noqa: C901
|
|||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
if not entities:
|
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(
|
done, pending = await asyncio.wait(
|
||||||
[
|
[
|
||||||
@ -804,8 +819,10 @@ async def entity_service_call( # noqa: C901
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert not pending
|
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]] = []
|
tasks: list[asyncio.Task[None]] = []
|
||||||
|
|
||||||
@ -824,28 +841,32 @@ async def entity_service_call( # noqa: C901
|
|||||||
for future in done:
|
for future in done:
|
||||||
future.result() # pop exception if have
|
future.result() # pop exception if have
|
||||||
|
|
||||||
|
return response_data if call.return_response else None
|
||||||
|
|
||||||
|
|
||||||
async def _handle_entity_call(
|
async def _handle_entity_call(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
func: str | Callable[..., Any],
|
func: str | Callable[..., Coroutine[Any, Any, ServiceResponse]],
|
||||||
data: dict | ServiceCall,
|
data: dict | ServiceCall,
|
||||||
context: Context,
|
context: Context,
|
||||||
) -> None:
|
) -> ServiceResponse:
|
||||||
"""Handle calling service method."""
|
"""Handle calling service method."""
|
||||||
entity.async_set_context(context)
|
entity.async_set_context(context)
|
||||||
|
|
||||||
|
task: asyncio.Future[ServiceResponse] | None
|
||||||
if isinstance(func, str):
|
if isinstance(func, str):
|
||||||
result = hass.async_run_job(
|
task = hass.async_run_job(
|
||||||
partial(getattr(entity, func), **data) # type: ignore[arg-type]
|
partial(getattr(entity, func), **data) # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
else:
|
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
|
# Guard because callback functions do not return a task when passed to
|
||||||
# async_run_job.
|
# async_run_job.
|
||||||
if result is not None:
|
result: ServiceResponse | None = None
|
||||||
await result
|
if task is not None:
|
||||||
|
result = await task
|
||||||
|
|
||||||
if asyncio.iscoroutine(result):
|
if asyncio.iscoroutine(result):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@ -856,7 +877,9 @@ async def _handle_entity_call(
|
|||||||
func,
|
func,
|
||||||
entity.entity_id,
|
entity.entity_id,
|
||||||
)
|
)
|
||||||
await result
|
result = await result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
|
@ -10,7 +10,7 @@ import pytest
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
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.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.util.dt as dt_util
|
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"},
|
target={"entity_id": "calendar.calendar_1"},
|
||||||
blocking=True,
|
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,
|
ENTITY_MATCH_NONE,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import (
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
ServiceResponse,
|
||||||
|
SupportsResponse,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity_component import EntityComponent, async_update_entity
|
from homeassistant.helpers.entity_component import EntityComponent, async_update_entity
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
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:
|
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")
|
entity = MockEntity(entity_id=f"{DOMAIN}.entity")
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
@ -524,6 +530,70 @@ async def test_register_entity_service(hass: HomeAssistant) -> None:
|
|||||||
assert len(calls) == 2
|
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:
|
async def test_platforms_shutdown_on_stop(hass: HomeAssistant) -> None:
|
||||||
"""Test that we shutdown platforms on stop."""
|
"""Test that we shutdown platforms on stop."""
|
||||||
platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None])
|
platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user