core/homeassistant/components/calendar/__init__.py

240 lines
7.8 KiB
Python
Raw Normal View History

"""Support for Google Calendar event device sensors."""
2021-03-17 22:43:55 +00:00
from __future__ import annotations
import datetime
from http import HTTPStatus
import logging
import re
from typing import Any, cast, final
from aiohttp import web
from homeassistant.components import frontend, http
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
2019-07-31 19:25:30 +00:00
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
time_period_str,
)
from homeassistant.helpers.entity import Entity
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
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
DOMAIN = "calendar"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = datetime.timedelta(seconds=60)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for calendars."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
2019-07-31 19:25:30 +00:00
)
2018-06-15 17:37:46 +00:00
hass.http.register_view(CalendarListView(component))
hass.http.register_view(CalendarEventView(component))
frontend.async_register_built_in_panel(
hass, "calendar", "calendar", "hass:calendar"
2020-05-09 21:00:28 +00:00
)
2018-06-15 17:37:46 +00:00
await component.async_setup(config)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
component: EntityComponent = hass.data[DOMAIN]
return await component.async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
component: EntityComponent = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
def get_date(date: dict[str, Any]) -> datetime.datetime:
"""Get the dateTime from date or dateTime as a local."""
2019-07-31 19:25:30 +00:00
if "date" in date:
parsed_date = dt.parse_date(date["date"])
assert parsed_date
2019-07-31 19:25:30 +00:00
return dt.start_of_local_day(
datetime.datetime.combine(parsed_date, datetime.time.min)
2019-07-31 19:25:30 +00:00
)
parsed_datetime = dt.parse_datetime(date["dateTime"])
assert parsed_datetime
return dt.as_local(parsed_datetime)
def normalize_event(event: dict[str, Any]) -> dict[str, Any]:
"""Normalize a calendar event."""
normalized_event: dict[str, Any] = {}
2019-07-31 19:25:30 +00:00
start = event.get("start")
end = event.get("end")
start = get_date(start) if start is not None else None
end = get_date(end) if end is not None else None
2019-07-31 19:25:30 +00:00
normalized_event["dt_start"] = start
normalized_event["dt_end"] = end
start = start.strftime(DATE_STR_FORMAT) if start is not None else None
end = end.strftime(DATE_STR_FORMAT) if end is not None else None
2019-07-31 19:25:30 +00:00
normalized_event["start"] = start
normalized_event["end"] = end
# cleanup the string so we don't have a bunch of double+ spaces
2019-07-31 19:25:30 +00:00
summary = event.get("summary", "")
normalized_event["message"] = re.sub(" +", "", summary).strip()
normalized_event["location"] = event.get("location", "")
normalized_event["description"] = event.get("description", "")
normalized_event["all_day"] = "date" in event["start"]
return normalized_event
def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.timedelta]:
"""Extract the offset from the event summary.
Return a tuple with the updated event summary and offset time.
"""
# check if we have an offset tag in the message
# time is HH:MM or MM
reg = f"{offset_prefix}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)"
search = re.search(reg, summary)
if search and search.group(1):
time = search.group(1)
2019-07-31 19:25:30 +00:00
if ":" not in time:
if time[0] == "+" or time[0] == "-":
time = f"{time[0]}0:{time[1:]}"
else:
time = f"0:{time}"
offset_time = time_period_str(time)
2019-07-31 19:25:30 +00:00
summary = (summary[: search.start()] + summary[search.end() :]).strip()
return (summary, offset_time)
return (summary, datetime.timedelta())
def is_offset_reached(
start: datetime.datetime, offset_time: datetime.timedelta
) -> bool:
"""Have we reached the offset time specified in the event title."""
if offset_time == datetime.timedelta():
return False
return start + offset_time <= dt.now(start.tzinfo)
class CalendarEventDevice(Entity):
"""Base class for calendar event entities."""
@property
def event(self) -> dict[str, Any] | None:
"""Return the next upcoming event."""
raise NotImplementedError()
@final
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return the entity state attributes."""
2021-10-17 17:56:00 +00:00
if (event := self.event) is None:
return None
event = normalize_event(event)
return {
2019-07-31 19:25:30 +00:00
"message": event["message"],
"all_day": event["all_day"],
"start_time": event["start"],
"end_time": event["end"],
"location": event["location"],
"description": event["description"],
}
@property
def state(self) -> str | None:
"""Return the state of the calendar event."""
2021-10-17 17:56:00 +00:00
if (event := self.event) is None:
return STATE_OFF
event = normalize_event(event)
2019-07-31 19:25:30 +00:00
start = event["dt_start"]
end = event["dt_end"]
if start is None or end is None:
return STATE_OFF
now = dt.now()
if start <= now < end:
return STATE_ON
return STATE_OFF
async def async_get_events(
self,
hass: HomeAssistant,
start_date: datetime.datetime,
end_date: datetime.datetime,
) -> list[dict[str, Any]]:
"""Return calendar events within a datetime range."""
raise NotImplementedError()
class CalendarEventView(http.HomeAssistantView):
"""View to retrieve calendar content."""
2019-07-31 19:25:30 +00:00
url = "/api/calendars/{entity_id}"
name = "api:calendars:calendar"
def __init__(self, component: EntityComponent) -> None:
"""Initialize calendar view."""
self.component = component
async def get(self, request: web.Request, entity_id: str) -> web.Response:
"""Return calendar events."""
2018-06-15 17:37:46 +00:00
entity = self.component.get_entity(entity_id)
2019-07-31 19:25:30 +00:00
start = request.query.get("start")
end = request.query.get("end")
if start is None or end is None or entity is None:
return web.Response(status=HTTPStatus.BAD_REQUEST)
assert isinstance(entity, CalendarEventDevice)
try:
start_date = dt.parse_datetime(start)
end_date = dt.parse_datetime(end)
except (ValueError, AttributeError):
return web.Response(status=HTTPStatus.BAD_REQUEST)
if start_date is None or end_date is None:
return web.Response(status=HTTPStatus.BAD_REQUEST)
2018-06-15 17:37:46 +00:00
event_list = await entity.async_get_events(
2019-07-31 19:25:30 +00:00
request.app["hass"], start_date, end_date
)
return self.json(event_list)
2018-06-15 17:37:46 +00:00
class CalendarListView(http.HomeAssistantView):
"""View to retrieve calendar list."""
2019-07-31 19:25:30 +00:00
url = "/api/calendars"
2018-06-15 17:37:46 +00:00
name = "api:calendars"
def __init__(self, component: EntityComponent) -> None:
2018-06-15 17:37:46 +00:00
"""Initialize calendar view."""
self.component = component
async def get(self, request: web.Request) -> web.Response:
2018-06-15 17:37:46 +00:00
"""Retrieve calendar list."""
2019-07-31 19:25:30 +00:00
hass = request.app["hass"]
2021-03-17 22:43:55 +00:00
calendar_list: list[dict[str, str]] = []
2018-06-15 17:37:46 +00:00
for entity in self.component.entities:
state = hass.states.get(entity.entity_id)
2019-07-31 19:25:30 +00:00
calendar_list.append({"name": state.name, "entity_id": entity.entity_id})
2018-06-15 17:37:46 +00:00
2020-10-13 00:17:30 +00:00
return self.json(sorted(calendar_list, key=lambda x: cast(str, x["name"])))