diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index a14cadf45c4..5a05818d3f2 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -39,6 +39,8 @@ from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, ATTR_SELECTED_SCHEDULE, + ATTR_TARGET_TEMPERATURE, + ATTR_TIME_PERIOD, CONF_URL_ENERGY, DATA_SCHEDULES, DOMAIN, @@ -47,8 +49,11 @@ from .const import ( EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, NETATMO_CREATE_CLIMATE, + SERVICE_CLEAR_TEMPERATURE_SETTING, SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, SERVICE_SET_SCHEDULE, + SERVICE_SET_TEMPERATURE_WITH_END_DATETIME, + SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD, ) from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom from .netatmo_entity_base import NetatmoBase @@ -143,6 +148,34 @@ async def async_setup_entry( }, "_async_service_set_preset_mode_with_end_datetime", ) + platform.async_register_entity_service( + SERVICE_SET_TEMPERATURE_WITH_END_DATETIME, + { + vol.Required(ATTR_TARGET_TEMPERATURE): vol.All( + vol.Coerce(float), vol.Range(min=7, max=30) + ), + vol.Required(ATTR_END_DATETIME): cv.datetime, + }, + "_async_service_set_temperature_with_end_datetime", + ) + platform.async_register_entity_service( + SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD, + { + vol.Required(ATTR_TARGET_TEMPERATURE): vol.All( + vol.Coerce(float), vol.Range(min=7, max=30) + ), + vol.Required(ATTR_TIME_PERIOD): vol.All( + cv.time_period, + cv.positive_timedelta, + ), + }, + "_async_service_set_temperature_with_time_period", + ) + platform.async_register_entity_service( + SERVICE_CLEAR_TEMPERATURE_SETTING, + {}, + "_async_service_clear_temperature_setting", + ) class NetatmoThermostat(NetatmoBase, ClimateEntity): @@ -441,12 +474,48 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): mode=PRESET_MAP_NETATMO[preset_mode], end_time=end_timestamp ) _LOGGER.debug( - "Setting %s preset to %s with optional end datetime to %s", + "Setting %s preset to %s with end datetime %s", self._room.home.entity_id, preset_mode, end_timestamp, ) + async def _async_service_set_temperature_with_end_datetime( + self, **kwargs: Any + ) -> None: + target_temperature = kwargs[ATTR_TARGET_TEMPERATURE] + end_datetime = kwargs[ATTR_END_DATETIME] + end_timestamp = int(dt_util.as_timestamp(end_datetime)) + + _LOGGER.debug( + "Setting %s to target temperature %s with end datetime %s", + self._room.entity_id, + target_temperature, + end_timestamp, + ) + await self._room.async_therm_manual(target_temperature, end_timestamp) + + async def _async_service_set_temperature_with_time_period( + self, **kwargs: Any + ) -> None: + target_temperature = kwargs[ATTR_TARGET_TEMPERATURE] + time_period = kwargs[ATTR_TIME_PERIOD] + + _LOGGER.debug( + "Setting %s to target temperature %s with time period %s", + self._room.entity_id, + target_temperature, + time_period, + ) + + now_timestamp = dt_util.as_timestamp(dt_util.utcnow()) + end_timestamp = int(now_timestamp + time_period.seconds) + await self._room.async_therm_manual(target_temperature, end_timestamp) + + async def _async_service_clear_temperature_setting(self, **kwargs: Any) -> None: + _LOGGER.debug("Clearing %s temperature setting", self._room.entity_id) + await self._room.async_therm_home() + @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 8a281d4d4a2..3fe456dd657 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -89,12 +89,17 @@ ATTR_PSEUDO = "pseudo" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" ATTR_SELECTED_SCHEDULE = "selected_schedule" +ATTR_TARGET_TEMPERATURE = "target_temperature" +ATTR_TIME_PERIOD = "time_period" +SERVICE_CLEAR_TEMPERATURE_SETTING = "clear_temperature_setting" 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" +SERVICE_SET_TEMPERATURE_WITH_END_DATETIME = "set_temperature_with_end_datetime" +SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD = "set_temperature_with_time_period" # 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 228f84f175d..cab0528199d 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -46,6 +46,56 @@ set_preset_mode_with_end_datetime: selector: datetime: +set_temperature_with_end_datetime: + target: + entity: + integration: netatmo + domain: climate + fields: + target_temperature: + required: true + example: "19.5" + selector: + number: + min: 7 + max: 30 + step: 0.5 + end_datetime: + required: true + example: '"2019-04-20 05:04:20"' + selector: + datetime: + +set_temperature_with_time_period: + target: + entity: + integration: netatmo + domain: climate + fields: + target_temperature: + required: true + example: "19.5" + selector: + number: + min: 7 + max: 30 + step: 0.5 + time_period: + required: true + default: + hours: 3 + minutes: 0 + seconds: 0 + days: 0 + selector: + duration: + +clear_temperature_setting: + target: + entity: + integration: netatmo + domain: climate + set_persons_home: target: entity: diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index bdb51808852..e504b27b599 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -121,7 +121,7 @@ "description": "Unregisters the webhook from the Netatmo backend." }, "set_preset_mode_with_end_datetime": { - "name": "Set preset mode with end datetime", + "name": "Set preset mode with end date & time", "description": "Sets the preset mode for a Netatmo climate device. The preset mode must match a preset mode configured at Netatmo.", "fields": { "preset_mode": { @@ -129,10 +129,42 @@ "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." + "name": "End date & time", + "description": "Date & time the preset will be active until." } } + }, + "set_temperature_with_end_datetime": { + "name": "Set temperature with end date & time", + "description": "Sets the target temperature for a Netatmo climate device with an end date & time.", + "fields": { + "target_temperature": { + "name": "Target temperature", + "description": "The target temperature for the device." + }, + "end_datetime": { + "name": "[%key:component::netatmo::services::set_preset_mode_with_end_datetime::fields::end_datetime::name%]", + "description": "Date & time the target temperature will be active until." + } + } + }, + "set_temperature_with_time_period": { + "name": "Set temperature with time period", + "description": "Sets the target temperature for a Netatmo climate device with time period.", + "fields": { + "target_temperature": { + "name": "[%key:component::netatmo::services::set_temperature_with_end_datetime::fields::target_temperature::name%]", + "description": "[%key:component::netatmo::services::set_temperature_with_end_datetime::fields::target_temperature::description%]" + }, + "time_period": { + "name": "Time period", + "description": "The time period which the temperature setting will be active for." + } + } + }, + "clear_temperature_setting": { + "name": "Clear temperature setting", + "description": "Clears any temperature setting for a Netatmo climate device reverting it to the current preset or schedule." } } } diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 99000403a38..848aad331bd 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -22,8 +22,14 @@ from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_ from homeassistant.components.netatmo.const import ( ATTR_END_DATETIME, ATTR_SCHEDULE_NAME, + ATTR_TARGET_TEMPERATURE, + ATTR_TIME_PERIOD, + DOMAIN as NETATMO_DOMAIN, + SERVICE_CLEAR_TEMPERATURE_SETTING, SERVICE_SET_PRESET_MODE_WITH_END_DATETIME, SERVICE_SET_SCHEDULE, + SERVICE_SET_TEMPERATURE_WITH_END_DATETIME, + SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant @@ -359,6 +365,203 @@ async def test_service_preset_modes_thermostat( assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30 +async def test_service_set_temperature_with_end_datetime( + hass: HomeAssistant, config_entry, netatmo_auth +) -> None: + """Test service setting temperature with an end datetime.""" + 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" + + assert hass.states.get(climate_entity_livingroom).state == "auto" + + # Test service setting the temperature without an end datetime + await hass.services.async_call( + NETATMO_DOMAIN, + SERVICE_SET_TEMPERATURE_WITH_END_DATETIME, + { + ATTR_ENTITY_ID: climate_entity_livingroom, + ATTR_TARGET_TEMPERATURE: 25, + ATTR_END_DATETIME: "2023-11-17 12:23:00", + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test webhook room mode change to "manual" + response = { + "room_id": "2746182631", + "home": { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "country": "DE", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "therm_setpoint_mode": "manual", + "therm_setpoint_temperature": 25, + "therm_setpoint_end_time": 1612749189, + } + ], + "modules": [ + {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"} + ], + }, + "mode": "manual", + "event_type": "set_point", + "push_type": "display_change", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "heat" + assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 25 + + +async def test_service_set_temperature_with_time_period( + hass: HomeAssistant, config_entry, netatmo_auth +) -> None: + """Test service setting temperature with an end datetime.""" + 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" + + assert hass.states.get(climate_entity_livingroom).state == "auto" + + # Test service setting the temperature without an end datetime + await hass.services.async_call( + NETATMO_DOMAIN, + SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD, + { + ATTR_ENTITY_ID: climate_entity_livingroom, + ATTR_TARGET_TEMPERATURE: 25, + ATTR_TIME_PERIOD: "02:24:00", + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test webhook room mode change to "manual" + response = { + "room_id": "2746182631", + "home": { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "country": "DE", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "therm_setpoint_mode": "manual", + "therm_setpoint_temperature": 25, + "therm_setpoint_end_time": 1612749189, + } + ], + "modules": [ + {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"} + ], + }, + "mode": "manual", + "event_type": "set_point", + "push_type": "display_change", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "heat" + assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 25 + + +async def test_service_clear_temperature_setting( + hass: HomeAssistant, config_entry, netatmo_auth +) -> None: + """Test service clearing temperature setting.""" + 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" + + assert hass.states.get(climate_entity_livingroom).state == "auto" + + # Simulate a room thermostat change to manual boost + response = { + "room_id": "2746182631", + "home": { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "country": "DE", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "therm_setpoint_mode": "manual", + "therm_setpoint_temperature": 25, + "therm_setpoint_end_time": 1612749189, + } + ], + "modules": [ + {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"} + ], + }, + "mode": "manual", + "event_type": "set_point", + "push_type": "display_change", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "heat" + assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 25 + + # Test service setting the temperature without an end datetime + await hass.services.async_call( + NETATMO_DOMAIN, + SERVICE_CLEAR_TEMPERATURE_SETTING, + {ATTR_ENTITY_ID: climate_entity_livingroom}, + blocking=True, + ) + await hass.async_block_till_done() + + # Test webhook room mode change to "home" + response = { + "room_id": "2746182631", + "home": { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "country": "DE", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "therm_setpoint_mode": "home", + } + ], + "modules": [ + {"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"} + ], + }, + "mode": "home", + "event_type": "cancel_set_point", + "push_type": "display_change", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "auto" + + async def test_webhook_event_handling_no_data( hass: HomeAssistant, config_entry, netatmo_auth ) -> None: