From 4afede9e0899db4e64b438fe6e08140db8abb1eb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 13 Jul 2021 00:27:48 +0200 Subject: [PATCH] Add schedule selector for Netatmo (#52909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/components/netatmo/const.py | 3 +- homeassistant/components/netatmo/select.py | 163 +++++++++++++++++++++ tests/components/netatmo/test_select.py | 65 ++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/netatmo/select.py create mode 100644 tests/components/netatmo/test_select.py diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index e974d43134e..8b2fb8701da 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -2,6 +2,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN API = "api" @@ -10,7 +11,7 @@ DOMAIN = "netatmo" MANUFACTURER = "Netatmo" DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" -PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN] +PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN] MODEL_NAPLUG = "Relay" MODEL_NATHERM1 = "Smart Thermostat" diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py new file mode 100644 index 00000000000..726ae919099 --- /dev/null +++ b/homeassistant/components/netatmo/select.py @@ -0,0 +1,163 @@ +"""Support for the Netatmo climate schedule selector.""" +from __future__ import annotations + +import logging +from typing import cast + +import pyatmo + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .climate import get_all_home_ids +from .const import ( + DATA_HANDLER, + DATA_SCHEDULES, + DOMAIN, + EVENT_TYPE_SCHEDULE, + MANUFACTURER, + SIGNAL_NAME, +) +from .data_handler import HOMEDATA_DATA_CLASS_NAME, NetatmoDataHandler +from .netatmo_entity_base import NetatmoBase + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Netatmo energy platform schedule selector.""" + data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] + + await data_handler.register_data_class( + HOMEDATA_DATA_CLASS_NAME, HOMEDATA_DATA_CLASS_NAME, None + ) + home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) + + if not home_data or home_data.raw_data == {}: + raise PlatformNotReady + + if HOMEDATA_DATA_CLASS_NAME not in data_handler.data: + raise PlatformNotReady + + entities = [ + NetatmoScheduleSelect( + data_handler, + home_id, + list(hass.data[DOMAIN][DATA_SCHEDULES][home_id].values()), + ) + for home_id in get_all_home_ids(home_data) + if home_id in hass.data[DOMAIN][DATA_SCHEDULES] + ] + + _LOGGER.debug("Adding climate schedule select entities %s", entities) + async_add_entities(entities, True) + + +class NetatmoScheduleSelect(NetatmoBase, SelectEntity): + """Representation a Netatmo thermostat schedule selector.""" + + def __init__( + self, data_handler: NetatmoDataHandler, home_id: str, options: list + ) -> None: + """Initialize the select entity.""" + SelectEntity.__init__(self) + super().__init__(data_handler) + + self._home_id = home_id + + self._data_classes.extend( + [ + { + "name": HOMEDATA_DATA_CLASS_NAME, + SIGNAL_NAME: HOMEDATA_DATA_CLASS_NAME, + }, + ] + ) + + self._device_name = self._data.homes[home_id]["name"] + self._attr_name = f"{MANUFACTURER} {self._device_name}" + + self._model: str = "NATherm1" + + self._attr_unique_id = f"{self._home_id}-schedule-select" + + self._attr_current_option = ( + self._data._get_selected_schedule( # pylint: disable=protected-access + home_id=self._home_id + ).get("name") + ) + self._attr_options = options + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + + for event_type in (EVENT_TYPE_SCHEDULE,): + self._listeners.append( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{event_type}", + self.handle_event, + ) + ) + + async def handle_event(self, event: dict) -> None: + """Handle webhook events.""" + data = event["data"] + + if self._home_id != data["home_id"]: + return + + if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: + self._attr_current_option = self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._home_id + ].get(data["schedule_id"]) + self.async_write_ha_state() + + @property + def _data(self) -> pyatmo.AsyncHomeData: + """Return data for this entity.""" + return cast( + pyatmo.AsyncHomeData, + self.data_handler.data[self._data_classes[0]["name"]], + ) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): + if name != option: + continue + _LOGGER.debug( + "Setting %s schedule to %s (%s)", + self._home_id, + option, + sid, + ) + await self._data.async_switch_home_schedule( + home_id=self._home_id, schedule_id=sid + ) + break + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_current_option = ( + self._data._get_selected_schedule( # pylint: disable=protected-access + home_id=self._home_id + ).get("name") + ) + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = { + schedule_id: schedule_data.get("name") + for schedule_id, schedule_data in ( + self._data.schedules[self._home_id].items() + ) + } + self._attr_options = list( + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].values() + ) diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py new file mode 100644 index 00000000000..838b2e2d290 --- /dev/null +++ b/tests/components/netatmo/test_select.py @@ -0,0 +1,65 @@ +"""The tests for the Netatmo climate platform.""" +from unittest.mock import patch + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID, SERVICE_SELECT_OPTION + +from .common import selected_platforms, simulate_webhook + + +async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): + """Test service for selecting Netatmo schedule with thermostats.""" + with selected_platforms(["climate", "select"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + select_entity_livingroom = "select.netatmo_myhome" + + assert hass.states.get(select_entity_livingroom).state == "Default" + assert hass.states.get(select_entity_livingroom).attributes[ATTR_OPTIONS] == [ + "Default", + "Winter", + ] + + # Fake backend response changing schedule + response = { + "event_type": "schedule", + "schedule_id": "b1b54a2f45795764f59d50d8", + "previous_schedule_id": "59d32176d183948b05ab4dce", + "push_type": "home_event_changed", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(select_entity_livingroom).state == "Winter" + + # Test setting a different schedule + with patch( + "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + ) as mock_switch_home_schedule: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: select_entity_livingroom, + ATTR_OPTION: "Default", + }, + blocking=True, + ) + await hass.async_block_till_done() + mock_switch_home_schedule.assert_called_once_with( + home_id="91763b24c43d3e344f424e8b", schedule_id="591b54a2764ff4d50d8b5795" + ) + + # Fake backend response changing schedule + response = { + "event_type": "schedule", + "schedule_id": "591b54a2764ff4d50d8b5795", + "previous_schedule_id": "b1b54a2f45795764f59d50d8", + "push_type": "home_event_changed", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(select_entity_livingroom).state == "Default"