From 1d35b91a14d1c68ad666e859aa77294512b1e8ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 20 Mar 2022 20:37:01 +0100 Subject: [PATCH] Add calendar platform to Twente Milieu (#68190) * Add calendar platform to Twente Milieu * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Sorting... Co-authored-by: Martin Hjelmare --- .../components/twentemilieu/__init__.py | 2 +- .../components/twentemilieu/calendar.py | 101 ++++++++++++++++++ .../components/twentemilieu/const.py | 10 ++ .../components/twentemilieu/entity.py | 36 +++++++ .../components/twentemilieu/sensor.py | 31 ++---- .../components/twentemilieu/test_calendar.py | 83 ++++++++++++++ 6 files changed, 241 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/twentemilieu/calendar.py create mode 100644 homeassistant/components/twentemilieu/entity.py create mode 100644 tests/components/twentemilieu/test_calendar.py diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index cee6ffbf38e..bbe306392ba 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=3600) SERVICE_UPDATE = "update" SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string}) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.CALENDAR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py new file mode 100644 index 00000000000..0d2768e5eb2 --- /dev/null +++ b/homeassistant/components/twentemilieu/calendar.py @@ -0,0 +1,101 @@ +"""Support for Twente Milieu Calendar.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +import homeassistant.util.dt as dt_util + +from .const import DOMAIN, WASTE_TYPE_TO_DESCRIPTION +from .entity import TwenteMilieuEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Twente Milieu calendar based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.data[CONF_ID]] + async_add_entities([TwenteMilieuCalendar(coordinator, entry)]) + + +class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice): + """Defines a Twente Milieu calendar.""" + + _attr_name = "Twente Milieu" + _attr_icon = "mdi:delete-empty" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the Twente Milieu entity.""" + super().__init__(coordinator, entry) + self._attr_unique_id = str(entry.data[CONF_ID]) + self._event: dict[str, Any] | None = None + + @property + def event(self) -> dict[str, Any] | None: + """Return the next upcoming event.""" + return self._event + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[dict[str, Any]]: + """Return calendar events within a datetime range.""" + events: list[dict[str, Any]] = [] + for waste_type, waste_dates in self.coordinator.data.items(): + events.extend( + { + "all_day": True, + "start": {"date": waste_date.isoformat()}, + "end": {"date": waste_date.isoformat()}, + "summary": WASTE_TYPE_TO_DESCRIPTION[waste_type], + } + for waste_date in waste_dates + if start_date.date() <= waste_date <= end_date.date() + ) + + return events + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + next_waste_pickup_type = None + next_waste_pickup_date = None + for waste_type, waste_dates in self.coordinator.data.items(): + if ( + waste_dates + and ( + next_waste_pickup_date is None + or waste_dates[0] # type: ignore[unreachable] + < next_waste_pickup_date + ) + and waste_dates[0] >= dt_util.now().date() + ): + next_waste_pickup_date = waste_dates[0] + next_waste_pickup_type = waste_type + + self._event = None + if next_waste_pickup_date is not None and next_waste_pickup_type is not None: + self._event = { + "all_day": True, + "start": {"date": next_waste_pickup_date.isoformat()}, + "end": {"date": next_waste_pickup_date.isoformat()}, + "summary": WASTE_TYPE_TO_DESCRIPTION[next_waste_pickup_type], + } + + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index 95ab903cc17..c9f2f935772 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging from typing import Final +from twentemilieu import WasteType + DOMAIN: Final = "twentemilieu" LOGGER = logging.getLogger(__package__) @@ -11,3 +13,11 @@ SCAN_INTERVAL = timedelta(hours=1) CONF_POST_CODE = "post_code" CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" + +WASTE_TYPE_TO_DESCRIPTION = { + WasteType.NON_RECYCLABLE: "Non-recyclable Waste Pickup", + WasteType.ORGANIC: "Organic Waste Pickup", + WasteType.PACKAGES: "Packages Waste Pickup", + WasteType.PAPER: "Paper Waste Pickup", + WasteType.TREE: "Christmas Tree Pickup", +} diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py new file mode 100644 index 00000000000..d075b1cbf6b --- /dev/null +++ b/homeassistant/components/twentemilieu/entity.py @@ -0,0 +1,36 @@ +"""Base entity for the Twente Milieu integration.""" +from __future__ import annotations + +from datetime import date + +from twentemilieu import WasteType + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +class TwenteMilieuEntity(CoordinatorEntity[dict[WasteType, list[date]]], Entity): + """Defines a Twente Milieu entity.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the Twente Milieu entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + configuration_url="https://www.twentemilieu.nl", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, + manufacturer="Twente Milieu", + name="Twente Milieu", + ) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index a56523d53d0..ab69aba9abf 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -14,15 +14,11 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, WASTE_TYPE_TO_DESCRIPTION +from .entity import TwenteMilieuEntity @dataclass @@ -43,35 +39,35 @@ SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( TwenteMilieuSensorDescription( key="tree", waste_type=WasteType.TREE, - name="Christmas Tree Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.TREE], icon="mdi:pine-tree", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Non-recyclable", waste_type=WasteType.NON_RECYCLABLE, - name="Non-recyclable Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.NON_RECYCLABLE], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Organic", waste_type=WasteType.ORGANIC, - name="Organic Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.ORGANIC], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Paper", waste_type=WasteType.PAPER, - name="Paper Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PAPER], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Plastic", waste_type=WasteType.PACKAGES, - name="Packages Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PACKAGES], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), @@ -90,7 +86,7 @@ async def async_setup_entry( ) -class TwenteMilieuSensor(CoordinatorEntity[dict[WasteType, list[date]]], SensorEntity): +class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity): """Defines a Twente Milieu sensor.""" entity_description: TwenteMilieuSensorDescription @@ -102,16 +98,9 @@ class TwenteMilieuSensor(CoordinatorEntity[dict[WasteType, list[date]]], SensorE entry: ConfigEntry, ) -> None: """Initialize the Twente Milieu entity.""" - super().__init__(coordinator=coordinator) + super().__init__(coordinator, entry) self.entity_description = description self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}" - self._attr_device_info = DeviceInfo( - configuration_url="https://www.twentemilieu.nl", - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, - manufacturer="Twente Milieu", - name="Twente Milieu", - ) @property def native_value(self) -> date | None: diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py new file mode 100644 index 00000000000..27e3ff8ebf3 --- /dev/null +++ b/tests/components/twentemilieu/test_calendar.py @@ -0,0 +1,83 @@ +"""Tests for the Twente Milieu calendar.""" +from http import HTTPStatus + +import pytest + +from homeassistant.components.twentemilieu.const import DOMAIN +from homeassistant.const import ATTR_ICON, STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.freeze_time("2022-01-05 00:00:00+00:00") +async def test_waste_pickup_calendar( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Twente Milieu waste pickup calendar.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("calendar.twente_milieu") + entry = entity_registry.async_get("calendar.twente_milieu") + assert entry + assert state + assert entry.unique_id == "12345" + assert state.attributes[ATTR_ICON] == "mdi:delete-empty" + assert state.attributes["all_day"] is True + assert state.attributes["message"] == "Christmas Tree Pickup" + assert not state.attributes["location"] + assert not state.attributes["description"] + assert state.state == STATE_OFF + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "12345")} + assert device_entry.manufacturer == "Twente Milieu" + assert device_entry.name == "Twente Milieu" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert device_entry.configuration_url == "https://www.twentemilieu.nl" + assert not device_entry.model + assert not device_entry.sw_version + + +async def test_api_calendar( + hass: HomeAssistant, + init_integration: MockConfigEntry, + hass_client, +) -> None: + """Test the API returns the calendar.""" + client = await hass_client() + response = await client.get("/api/calendars") + assert response.status == HTTPStatus.OK + data = await response.json() + assert data == [ + { + "entity_id": "calendar.twente_milieu", + "name": "Twente Milieu", + } + ] + + +async def test_api_events( + hass: HomeAssistant, + init_integration: MockConfigEntry, + hass_client, +) -> None: + """Test the Twente Milieu calendar view.""" + client = await hass_client() + response = await client.get( + "/api/calendars/calendar.twente_milieu?start=2022-01-05&end=2022-01-06" + ) + assert response.status == HTTPStatus.OK + events = await response.json() + assert len(events) == 1 + assert events[0] == { + "all_day": True, + "start": {"date": "2022-01-06"}, + "end": {"date": "2022-01-06"}, + "summary": "Christmas Tree Pickup", + }