1 """Support for Calendar event device sensors."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Iterable
8 from http
import HTTPStatus
9 from itertools
import groupby
12 from typing
import Any, Final, cast, final
14 from aiohttp
import web
15 from dateutil.rrule
import rrulestr
16 import voluptuous
as vol
58 EVENT_RECURRENCE_RANGE,
68 CalendarEntityFeature,
73 _LOGGER = logging.getLogger(__name__)
75 ENTITY_ID_FORMAT = DOMAIN +
".{}"
76 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
77 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
78 SCAN_INTERVAL = datetime.timedelta(seconds=60)
81 VALID_FREQS = {
"DAILY",
"WEEKLY",
"MONTHLY",
"YEARLY"}
84 MIN_NEW_EVENT_DURATION = datetime.timedelta(seconds=1)
88 MIN_EVENT_DURATION = datetime.timedelta(seconds=0)
91 def _has_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
92 """Assert that all datetime values have a timezone."""
94 def validate(obj: dict[str, Any]) -> dict[str, Any]:
95 """Validate that all datetime values have a timezone."""
99 and isinstance(value, datetime.datetime)
100 and value.tzinfo
is None
102 raise vol.Invalid(
"Expected all values to have a timezone")
109 """Verify that all datetime values have a consistent timezone."""
111 def validate(obj: dict[str, Any]) -> dict[str, Any]:
112 """Test that all keys that are datetime values have the same timezone."""
115 if not (value := obj.get(key))
or not isinstance(value, datetime.datetime):
117 tzinfos.append(value.tzinfo)
118 uniq_values = groupby(tzinfos)
119 if len(
list(uniq_values)) > 1:
120 raise vol.Invalid(
"Expected all values to have the same timezone")
127 """Convert all datetime values to the local timezone."""
129 def validate(obj: dict[str, Any]) -> dict[str, Any]:
130 """Convert all keys that are datetime values to local timezone."""
132 if (value := obj.get(k))
and isinstance(value, datetime.datetime):
133 obj[k] = dt_util.as_local(value)
140 start_key: str, end_key: str, min_duration: datetime.timedelta
141 ) -> Callable[[dict[str, Any]], dict[str, Any]]:
142 """Verify that the time span between start and end has a minimum duration."""
144 def validate(obj: dict[str, Any]) -> dict[str, Any]:
145 if (start := obj.get(start_key))
and (end := obj.get(end_key)):
146 duration = end - start
147 if duration < min_duration:
149 f
"Expected minimum event duration of {min_duration} ({start}, {end})"
157 """Verify that all values are of the same type."""
159 def validate(obj: dict[str, Any]) -> dict[str, Any]:
160 """Test that all keys in the dict have values of the same type."""
161 uniq_values = groupby(type(obj[k])
for k
in keys)
162 if len(
list(uniq_values)) > 1:
163 raise vol.Invalid(f
"Expected all values to be the same type: {keys}")
170 """Validate a recurrence rule string."""
172 raise vol.Invalid(
"rrule value is None")
174 if not isinstance(value, str):
175 raise vol.Invalid(
"rrule value expected a string")
179 except ValueError
as err:
180 raise vol.Invalid(f
"Invalid rrule '{value}': {err}")
from err
183 rule_parts =
dict(s.split(
"=", 1)
for s
in value.split(
";"))
184 if not (freq := rule_parts.get(
"FREQ")):
185 raise vol.Invalid(
"rrule did not contain FREQ")
187 if freq
not in VALID_FREQS:
188 raise vol.Invalid(f
"Invalid frequency for rule: {value}")
194 """Convert any empty string values to None."""
198 CREATE_EVENT_SERVICE =
"create_event"
199 CREATE_EVENT_SCHEMA = vol.All(
200 cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
201 cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
202 cv.make_entity_service_schema(
204 vol.Required(EVENT_SUMMARY): cv.string,
205 vol.Optional(EVENT_DESCRIPTION, default=
""): cv.string,
206 vol.Optional(EVENT_LOCATION): cv.string,
208 EVENT_START_DATE,
"dates",
"Start and end dates must both be specified"
211 EVENT_END_DATE,
"dates",
"Start and end dates must both be specified"
214 EVENT_START_DATETIME,
216 "Start and end datetimes must both be specified",
221 "Start and end datetimes must both be specified",
223 vol.Optional(EVENT_IN): vol.Schema(
225 vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES): cv.positive_int,
226 vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES): cv.positive_int,
234 _has_min_duration(EVENT_START_DATETIME, EVENT_END_DATETIME, MIN_NEW_EVENT_DURATION),
237 WEBSOCKET_EVENT_SCHEMA = vol.Schema(
240 vol.Required(EVENT_START): vol.Any(cv.date, cv.datetime),
241 vol.Required(EVENT_END): vol.Any(cv.date, cv.datetime),
242 vol.Required(EVENT_SUMMARY): cv.string,
243 vol.Optional(EVENT_DESCRIPTION): cv.string,
244 vol.Optional(EVENT_LOCATION): cv.string,
245 vol.Optional(EVENT_RRULE): _validate_rrule,
255 CALENDAR_EVENT_SCHEMA = vol.Schema(
258 vol.Required(
"start"): vol.Any(cv.date, cv.datetime),
259 vol.Required(
"end"): vol.Any(cv.date, cv.datetime),
260 vol.Required(EVENT_SUMMARY): cv.string,
261 vol.Optional(EVENT_RRULE): _validate_rrule,
268 extra=vol.ALLOW_EXTRA,
271 SERVICE_GET_EVENTS: Final =
"get_events"
272 SERVICE_GET_EVENTS_SCHEMA: Final = vol.All(
273 cv.has_at_least_one_key(EVENT_END_DATETIME, EVENT_DURATION),
274 cv.has_at_most_one_key(EVENT_END_DATETIME, EVENT_DURATION),
275 cv.make_entity_service_schema(
277 vol.Optional(EVENT_START_DATETIME): cv.datetime,
278 vol.Optional(EVENT_END_DATETIME): cv.datetime,
279 vol.Optional(EVENT_DURATION): vol.All(
280 cv.time_period, cv.positive_timedelta
287 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
288 """Track states and offer events for calendars."""
289 component = hass.data[DATA_COMPONENT] = EntityComponent[CalendarEntity](
290 _LOGGER, DOMAIN, hass, SCAN_INTERVAL
296 frontend.async_register_built_in_panel(
297 hass,
"calendar",
"calendar",
"hass:calendar"
300 websocket_api.async_register_command(hass, handle_calendar_event_create)
301 websocket_api.async_register_command(hass, handle_calendar_event_delete)
302 websocket_api.async_register_command(hass, handle_calendar_event_update)
304 component.async_register_entity_service(
305 CREATE_EVENT_SERVICE,
308 required_features=[CalendarEntityFeature.CREATE_EVENT],
310 component.async_register_entity_service(
312 SERVICE_GET_EVENTS_SCHEMA,
313 async_get_events_service,
314 supports_response=SupportsResponse.ONLY,
316 await component.async_setup(config)
321 """Set up a config entry."""
326 """Unload a config entry."""
330 def get_date(date: dict[str, Any]) -> datetime.datetime:
331 """Get the dateTime from date or dateTime as a local."""
333 parsed_date = dt_util.parse_date(date[
"date"])
335 return dt_util.start_of_local_day(
336 datetime.datetime.combine(parsed_date, datetime.time.min)
338 parsed_datetime = dt_util.parse_datetime(date[
"dateTime"])
339 assert parsed_datetime
340 return dt_util.as_local(parsed_datetime)
343 @dataclasses.dataclass
345 """An event on a calendar."""
347 start: datetime.date | datetime.datetime
348 end: datetime.date | datetime.datetime
350 description: str |
None =
None
351 location: str |
None =
None
353 uid: str |
None =
None
354 recurrence_id: str |
None =
None
355 rrule: str |
None =
None
359 """Return event start time as a local datetime."""
364 """Return event end time as a local datetime."""
369 """Return true if the event is an all day event."""
370 return not isinstance(self.
startstart, datetime.datetime)
373 """Return a dict representation of the event."""
375 **dataclasses.asdict(self, dict_factory=_event_dict_factory),
376 "all_day": self.
all_dayall_day,
380 """Perform validation on the CalendarEvent."""
382 def skip_none(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
383 return {k: v
for k, v
in obj
if v
is not None}
387 except vol.Invalid
as err:
389 f
"Failed to validate CalendarEvent: {err}"
396 not isinstance(self.
startstart, datetime.datetime)
397 and not isinstance(self.
endend, datetime.datetime)
400 self.
endend = self.
startstart + datetime.timedelta(days=1)
404 """Convert CalendarEvent dataclass items to dictionary of attributes."""
405 result: dict[str, str] = {}
406 for name, value
in obj:
407 if isinstance(value, (datetime.datetime, datetime.date)):
408 result[name] = value.isoformat()
409 elif value
is not None:
410 result[name] =
str(value)
415 """Convert CalendarEvent dataclass items to the API format."""
416 result: dict[str, Any] = {}
417 for name, value
in obj:
418 if isinstance(value, datetime.datetime):
419 result[name] = {
"dateTime": dt_util.as_local(value).isoformat()}
420 elif isinstance(value, datetime.date):
421 result[name] = {
"date": value.isoformat()}
428 obj: Iterable[tuple[str, Any]],
429 ) -> dict[str, JsonValueType]:
430 """Convert CalendarEvent dataclass items to dictionary of attributes."""
434 if name
in LIST_EVENT_FIELDS
and value
is not None
439 dt_or_d: datetime.datetime | datetime.date,
440 ) -> datetime.datetime:
441 """Convert a calendar event date/datetime to a datetime if needed."""
442 if isinstance(dt_or_d, datetime.datetime):
443 return dt_util.as_local(dt_or_d)
444 return dt_util.start_of_local_day(dt_or_d)
447 def _get_api_date(dt_or_d: datetime.datetime | datetime.date) -> dict[str, str]:
448 """Convert a calendar event date/datetime to a datetime if needed."""
449 if isinstance(dt_or_d, datetime.datetime):
450 return {
"dateTime": dt_util.as_local(dt_or_d).isoformat()}
451 return {
"date": dt_or_d.isoformat()}
454 def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.timedelta]:
455 """Extract the offset from the event summary.
457 Return a tuple with the updated event summary and offset time.
461 reg = f
"{offset_prefix}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)"
462 search = re.search(reg, summary)
463 if search
and search.group(1):
464 time = search.group(1)
466 if time[0]
in (
"+",
"-"):
467 time = f
"{time[0]}0:{time[1:]}"
471 offset_time = cv.time_period_str(time)
472 summary = (summary[: search.start()] + summary[search.end() :]).strip()
473 return (summary, offset_time)
474 return (summary, datetime.timedelta())
478 start: datetime.datetime, offset_time: datetime.timedelta
480 """Have we reached the offset time specified in the event title."""
481 if offset_time == datetime.timedelta():
483 return start + offset_time <= dt_util.now(start.tzinfo)
487 """A class that describes calendar entities."""
491 """Base class for calendar event entities."""
493 entity_description: CalendarEntityDescription
495 _entity_component_unrecorded_attributes = frozenset({
"description"})
497 _alarm_unsubs: list[CALLBACK_TYPE] |
None =
None
500 def event(self) -> CalendarEvent | None:
501 """Return the next upcoming event."""
502 raise NotImplementedError
507 """Return the entity state attributes."""
508 if (event := self.
eventevent)
is None:
512 "message": event.summary,
513 "all_day": event.all_day,
514 "start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT),
515 "end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT),
516 "location": event.location
if event.location
else "",
517 "description": event.description
if event.description
else "",
523 """Return the state of the calendar event."""
524 if (event := self.
eventevent)
is None:
529 if event.start_datetime_local <= now < event.end_datetime_local:
536 """Write the state to the state machine.
538 This sets up listeners to handle state transitions for start or end of
539 the current or upcoming event.
552 event = self.
eventevent
553 if event
is None or now >= event.end_datetime_local:
554 _LOGGER.debug(
"No alarms needed for %s (event=%s)", self.
entity_identity_id, event)
558 def update(_: datetime.datetime) ->
None:
559 """Update state and reschedule next alarms."""
560 _LOGGER.debug(
"Running %s update", self.
entity_identity_id)
563 if now < event.start_datetime_local:
568 event.start_datetime_local,
575 "Scheduled %d updates for %s (%s, %s)",
578 event.start_datetime_local,
579 event.end_datetime_local,
583 """Run when entity will be removed from hass.
585 To be extended by integrations.
594 start_date: datetime.datetime,
595 end_date: datetime.datetime,
596 ) -> list[CalendarEvent]:
597 """Return calendar events within a datetime range."""
598 raise NotImplementedError
601 """Add a new event to calendar."""
602 raise NotImplementedError
607 recurrence_id: str |
None =
None,
608 recurrence_range: str |
None =
None,
610 """Delete an event on the calendar."""
611 raise NotImplementedError
616 event: dict[str, Any],
617 recurrence_id: str |
None =
None,
618 recurrence_range: str |
None =
None,
620 """Delete an event on the calendar."""
621 raise NotImplementedError
625 """View to retrieve calendar content."""
627 url =
"/api/calendars/{entity_id}"
628 name =
"api:calendars:calendar"
630 def __init__(self, component: EntityComponent[CalendarEntity]) ->
None:
631 """Initialize calendar view."""
634 async
def get(self, request: web.Request, entity_id: str) -> web.Response:
635 """Return calendar events."""
637 entity, CalendarEntity
639 return web.Response(status=HTTPStatus.BAD_REQUEST)
641 start = request.query.get(
"start")
642 end = request.query.get(
"end")
643 if start
is None or end
is None:
644 return web.Response(status=HTTPStatus.BAD_REQUEST)
646 start_date = dt_util.parse_datetime(start)
647 end_date = dt_util.parse_datetime(end)
648 except (ValueError, AttributeError):
649 return web.Response(status=HTTPStatus.BAD_REQUEST)
650 if start_date
is None or end_date
is None:
651 return web.Response(status=HTTPStatus.BAD_REQUEST)
652 if start_date > end_date:
653 return web.Response(status=HTTPStatus.BAD_REQUEST)
656 calendar_event_list = await entity.async_get_events(
657 request.app[http.KEY_HASS],
658 dt_util.as_local(start_date),
659 dt_util.as_local(end_date),
661 except HomeAssistantError
as err:
662 _LOGGER.debug(
"Error reading events: %s", err)
663 return self.json_message(
664 f
"Error reading events: {err}", HTTPStatus.INTERNAL_SERVER_ERROR
669 dataclasses.asdict(event, dict_factory=_api_event_dict_factory)
670 for event
in calendar_event_list
676 """View to retrieve calendar list."""
678 url =
"/api/calendars"
679 name =
"api:calendars"
681 def __init__(self, component: EntityComponent[CalendarEntity]) ->
None:
682 """Initialize calendar view."""
685 async
def get(self, request: web.Request) -> web.Response:
686 """Retrieve calendar list."""
687 hass = request.app[http.KEY_HASS]
688 calendar_list: list[dict[str, str]] = []
690 for entity
in self.
componentcomponent.entities:
691 state = hass.states.get(entity.entity_id)
693 calendar_list.append({
"name": state.name,
"entity_id": entity.entity_id})
695 return self.json(sorted(calendar_list, key=
lambda x: cast(str, x[
"name"])))
698 @websocket_api.websocket_command(
{
vol.Required("type"):
"calendar/event/create",
699 vol.Required(
"entity_id"): cv.entity_id,
700 CONF_EVENT: WEBSOCKET_EVENT_SCHEMA,
703 @websocket_api.async_response
705 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
707 """Handle creation of a calendar event."""
708 if not (entity := hass.data[DATA_COMPONENT].
get_entity(msg[
"entity_id"])):
709 connection.send_error(msg[
"id"], ERR_NOT_FOUND,
"Entity not found")
713 not entity.supported_features
714 or not entity.supported_features & CalendarEntityFeature.CREATE_EVENT
716 connection.send_message(
717 websocket_api.error_message(
718 msg[
"id"], ERR_NOT_SUPPORTED,
"Calendar does not support event creation"
724 await entity.async_create_event(**msg[CONF_EVENT])
725 except HomeAssistantError
as ex:
726 connection.send_error(msg[
"id"],
"failed",
str(ex))
728 connection.send_result(msg[
"id"])
731 @websocket_api.websocket_command(
{
vol.Required("type"):
"calendar/event/delete",
732 vol.Required(
"entity_id"): cv.entity_id,
733 vol.Required(EVENT_UID): cv.string,
734 vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
735 vol.All(cv.string, _empty_as_none),
None
737 vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
740 @websocket_api.async_response
742 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
744 """Handle delete of a calendar event."""
746 if not (entity := hass.data[DATA_COMPONENT].
get_entity(msg[
"entity_id"])):
747 connection.send_error(msg[
"id"], ERR_NOT_FOUND,
"Entity not found")
751 not entity.supported_features
752 or not entity.supported_features & CalendarEntityFeature.DELETE_EVENT
754 connection.send_message(
755 websocket_api.error_message(
756 msg[
"id"], ERR_NOT_SUPPORTED,
"Calendar does not support event deletion"
762 await entity.async_delete_event(
764 recurrence_id=msg.get(EVENT_RECURRENCE_ID),
765 recurrence_range=msg.get(EVENT_RECURRENCE_RANGE),
767 except (HomeAssistantError, ValueError)
as ex:
768 _LOGGER.error(
"Error handling Calendar Event call: %s", ex)
769 connection.send_error(msg[
"id"],
"failed",
str(ex))
771 connection.send_result(msg[
"id"])
774 @websocket_api.websocket_command(
{
vol.Required("type"):
"calendar/event/update",
775 vol.Required(
"entity_id"): cv.entity_id,
776 vol.Required(EVENT_UID): cv.string,
777 vol.Optional(EVENT_RECURRENCE_ID): vol.Any(
778 vol.All(cv.string, _empty_as_none),
None
780 vol.Optional(EVENT_RECURRENCE_RANGE): cv.string,
781 vol.Required(CONF_EVENT): WEBSOCKET_EVENT_SCHEMA,
784 @websocket_api.async_response
786 hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
788 """Handle creation of a calendar event."""
789 if not (entity := hass.data[DATA_COMPONENT].
get_entity(msg[
"entity_id"])):
790 connection.send_error(msg[
"id"], ERR_NOT_FOUND,
"Entity not found")
794 not entity.supported_features
795 or not entity.supported_features & CalendarEntityFeature.UPDATE_EVENT
797 connection.send_message(
798 websocket_api.error_message(
799 msg[
"id"], ERR_NOT_SUPPORTED,
"Calendar does not support event update"
805 await entity.async_update_event(
808 recurrence_id=msg.get(EVENT_RECURRENCE_ID),
809 recurrence_range=msg.get(EVENT_RECURRENCE_RANGE),
811 except (HomeAssistantError, ValueError)
as ex:
812 _LOGGER.error(
"Error handling Calendar Event call: %s", ex)
813 connection.send_error(msg[
"id"],
"failed",
str(ex))
815 connection.send_result(msg[
"id"])
819 values: dict[str, Any],
820 ) -> tuple[datetime.datetime | datetime.date, datetime.datetime | datetime.date]:
821 """Parse a create event service call and convert the args ofr a create event entity call.
823 This converts the input service arguments into a `start` and `end` date or date time. This
824 exists because service calls use `start_date` and `start_date_time` whereas the
825 normal entity methods can take either a `datetime` or `date` as a single `start` argument.
826 It also handles the other service call variations like "in days" as well.
829 if event_in := values.get(EVENT_IN):
830 days = event_in.get(EVENT_IN_DAYS, 7 * event_in.get(EVENT_IN_WEEKS, 0))
831 today = datetime.date.today()
833 today + datetime.timedelta(days=days),
834 today + datetime.timedelta(days=days + 1),
837 if EVENT_START_DATE
in values
and EVENT_END_DATE
in values:
838 return (values[EVENT_START_DATE], values[EVENT_END_DATE])
840 if EVENT_START_DATETIME
in values
and EVENT_END_DATETIME
in values:
841 return (values[EVENT_START_DATETIME], values[EVENT_END_DATETIME])
843 raise ValueError(
"Missing required fields to set start or end date/datetime")
847 """Add a new event to calendar."""
851 **{k: v
for k, v
in call.data.items()
if k
not in EVENT_TIME_FIELDS},
855 await entity.async_create_event(**params)
859 calendar: CalendarEntity, service_call: ServiceCall
860 ) -> ServiceResponse:
861 """List events on a calendar during a time range."""
862 start = service_call.data.get(EVENT_START_DATETIME, dt_util.now())
863 if EVENT_DURATION
in service_call.data:
864 end = start + service_call.data[EVENT_DURATION]
866 end = service_call.data[EVENT_END_DATETIME]
867 calendar_event_list = await calendar.async_get_events(
868 calendar.hass, dt_util.as_local(start), dt_util.as_local(end)
872 dataclasses.asdict(event, dict_factory=_list_events_dict_factory)
873 for event
in calendar_event_list
876
None async_create_event(self, **Any kwargs)
None async_update_event(self, str uid, dict[str, Any] event, str|None recurrence_id=None, str|None recurrence_range=None)
None async_delete_event(self, str uid, str|None recurrence_id=None, str|None recurrence_range=None)
None async_write_ha_state(self)
dict[str, Any]|None state_attributes(self)
None async_will_remove_from_hass(self)
CalendarEvent|None event(self)
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime.datetime start_date, datetime.datetime end_date)
None __init__(self, EntityComponent[CalendarEntity] component)
web.Response get(self, web.Request request, str entity_id)
datetime.datetime start_datetime_local(self)
dict[str, Any] as_dict(self)
datetime.datetime end_datetime_local(self)
None __init__(self, EntityComponent[CalendarEntity] component)
web.Response get(self, web.Request request)
None async_write_ha_state(self)
CalendarEntity get_entity(HomeAssistant hass, str entity_id)
None handle_calendar_event_update(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Callable[[dict[str, Any]], dict[str, Any]] _as_local_timezone(*Any keys)
ServiceResponse async_get_events_service(CalendarEntity calendar, ServiceCall service_call)
Callable[[dict[str, Any]], dict[str, Any]] _has_min_duration(str start_key, str end_key, datetime.timedelta min_duration)
None handle_calendar_event_delete(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Callable[[dict[str, Any]], dict[str, Any]] _has_same_type(*Any keys)
Callable[[dict[str, Any]], dict[str, Any]] _has_consistent_timezone(*Any keys)
str|None _empty_as_none(str|None value)
tuple[str, datetime.timedelta] extract_offset(str summary, str offset_prefix)
datetime.datetime get_date(dict[str, Any] date)
bool is_offset_reached(datetime.datetime start, datetime.timedelta offset_time)
dict[str, JsonValueType] _list_events_dict_factory(Iterable[tuple[str, Any]] obj)
dict[str, Any] _api_event_dict_factory(Iterable[tuple[str, Any]] obj)
str _validate_rrule(Any value)
datetime.datetime _get_datetime_local(datetime.datetime|datetime.date dt_or_d)
tuple[datetime.datetime|datetime.date, datetime.datetime|datetime.date] _validate_timespan(dict[str, Any] values)
bool async_setup(HomeAssistant hass, ConfigType config)
Callable[[dict[str, Any]], dict[str, Any]] _has_timezone(*Any keys)
None async_create_event(CalendarEntity entity, ServiceCall call)
None handle_calendar_event_create(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
dict[str, str] _event_dict_factory(Iterable[tuple[str, Any]] obj)
dict[str, str] _get_api_date(datetime.datetime|datetime.date dt_or_d)
IssData update(pyiss.ISS iss)
dict[str, Any] validate(SchemaCommonFlowHandler handler, dict[str, Any] user_input)
CALLBACK_TYPE async_track_point_in_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)