diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9f34df9b39c..f4715015844 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -8,6 +8,7 @@ from pyatmo.modules import NATherm1 import voluptuous as vol from homeassistant.components.climate import ( + ATTR_PRESET_MODE, DEFAULT_MIN_TEMP, PRESET_AWAY, PRESET_BOOST, @@ -30,8 +31,10 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util from .const import ( + ATTR_END_DATETIME, ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, ATTR_SELECTED_SCHEDULE, @@ -43,6 +46,7 @@ from .const import ( EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, NETATMO_CREATE_CLIMATE, + SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, SERVICE_SET_SCHEDULE, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom @@ -59,6 +63,8 @@ SUPPORT_FLAGS = ( ) SUPPORT_PRESET = [PRESET_AWAY, PRESET_BOOST, PRESET_FROST_GUARD, PRESET_SCHEDULE] +THERM_MODES = (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY) + STATE_NETATMO_SCHEDULE = "schedule" STATE_NETATMO_HG = "hg" STATE_NETATMO_MAX = "max" @@ -124,6 +130,14 @@ async def async_setup_entry( {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, "_async_service_set_schedule", ) + platform.async_register_entity_service( + SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, + { + vol.Required(ATTR_PRESET_MODE): vol.In(THERM_MODES), + vol.Required(ATTR_END_DATETIME): cv.datetime, + }, + "_async_service_set_preset_mode_with_end_datetime", + ) class NetatmoThermostat(NetatmoBase, ClimateEntity): @@ -314,7 +328,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): await self._room.async_therm_set(STATE_NETATMO_HOME) elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) - elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY): + elif preset_mode in THERM_MODES: await self._room.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -410,6 +424,23 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): schedule_id, ) + async def _async_service_set_preset_mode_with_end_datetime( + self, **kwargs: Any + ) -> None: + preset_mode = kwargs[ATTR_PRESET_MODE] + end_datetime = kwargs[ATTR_END_DATETIME] + end_timestamp = int(dt_util.as_timestamp(end_datetime)) + + await self._room.home.async_set_thermmode( + mode=PRESET_MAP_NETATMO[preset_mode], end_time=end_timestamp + ) + _LOGGER.debug( + "Setting %s preset to %s with optional end datetime to %s", + self._room.home.entity_id, + preset_mode, + end_timestamp, + ) + @property def device_info(self) -> DeviceInfo: """Return the device info for the thermostat.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 3e489fe8ea5..9e7ac33c8b6 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -69,6 +69,7 @@ DEFAULT_PERSON = "unknown" DEFAULT_WEBHOOKS = False ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" +ATTR_END_DATETIME = "end_datetime" ATTR_EVENT_TYPE = "event_type" ATTR_FACE_URL = "face_url" ATTR_HEATING_POWER_REQUEST = "heating_power_request" @@ -86,6 +87,7 @@ SERVICE_SET_CAMERA_LIGHT = "set_camera_light" SERVICE_SET_PERSON_AWAY = "set_person_away" SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_SCHEDULE = "set_schedule" +SERVICE_SET_PRESET_MODE_WITH_END_DATETIME = "set_preset_mode_with_end_datetime" # Climate events EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 726d6867d2d..228f84f175d 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -26,6 +26,26 @@ set_schedule: selector: text: +set_preset_mode_with_end_datetime: + target: + entity: + integration: netatmo + domain: climate + fields: + preset_mode: + required: true + example: "away" + selector: + select: + options: + - "away" + - "Frost Guard" + end_datetime: + required: true + example: '"2019-04-20 05:04:20"' + selector: + datetime: + set_persons_home: target: entity: diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index e9125f33016..593320827fd 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -115,6 +115,20 @@ "unregister_webhook": { "name": "Unregister webhook", "description": "Unregisters the webhook from the Netatmo backend." + }, + "set_preset_mode_with_end_datetime": { + "name": "Set preset mode with end datetime", + "description": "Sets the preset mode for a Netatmo climate device. The preset mode must match a preset mode configured at Netatmo.", + "fields": { + "preset_mode": { + "name": "Preset mode", + "description": "Climate preset mode such as Schedule, Away or Frost Guard." + }, + "end_datetime": { + "name": "End datetime", + "description": "Datetime for until when the preset will be active." + } + } } } } diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 6e4ae0e67cb..99000403a38 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -1,7 +1,9 @@ """The tests for the Netatmo climate platform.""" +from datetime import timedelta from unittest.mock import patch import pytest +from voluptuous.error import MultipleInvalid from homeassistant.components.climate import ( ATTR_HVAC_MODE, @@ -18,11 +20,14 @@ from homeassistant.components.climate import ( ) from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE from homeassistant.components.netatmo.const import ( + ATTR_END_DATETIME, ATTR_SCHEDULE_NAME, + SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, SERVICE_SET_SCHEDULE, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from .common import selected_platforms, simulate_webhook @@ -458,6 +463,78 @@ async def test_service_schedule_thermostats( assert "summer is not a valid schedule" in caplog.text +async def test_service_preset_mode_with_end_time_thermostats( + hass: HomeAssistant, config_entry, caplog: pytest.LogCaptureFixture, netatmo_auth +) -> None: + """Test service for set preset mode with end datetime for Netatmo thermostats.""" + with selected_platforms(["climate"]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + climate_entity_livingroom = "climate.livingroom" + + # Test setting a valid preset mode (that allow an end datetime in Netatmo == THERM_MODES) and a valid end datetime + await hass.services.async_call( + "netatmo", + SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, + { + ATTR_ENTITY_ID: climate_entity_livingroom, + ATTR_PRESET_MODE: PRESET_AWAY, + ATTR_END_DATETIME: (dt_util.now() + timedelta(days=10)).strftime( + "%Y-%m-%d %H:%M:%S" + ), + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake webhook thermostat mode change to "Away" + response = { + "event_type": "therm_mode", + "home": {"id": "91763b24c43d3e344f424e8b", "therm_mode": "away"}, + "mode": "away", + "previous_mode": "schedule", + "push_type": "home_event_changed", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" + ) + + # Test setting an invalid preset mode (not in THERM_MODES) and a valid end datetime + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + "netatmo", + SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, + { + ATTR_ENTITY_ID: climate_entity_livingroom, + ATTR_PRESET_MODE: PRESET_BOOST, + ATTR_END_DATETIME: (dt_util.now() + timedelta(days=10)).strftime( + "%Y-%m-%d %H:%M:%S" + ), + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test setting a valid preset mode (that allow an end datetime in Netatmo == THERM_MODES) without an end datetime + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + "netatmo", + SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, + { + ATTR_ENTITY_ID: climate_entity_livingroom, + ATTR_PRESET_MODE: PRESET_AWAY, + }, + blocking=True, + ) + await hass.async_block_till_done() + + async def test_service_preset_mode_already_boost_valves( hass: HomeAssistant, config_entry, netatmo_auth ) -> None: