Add service set_preset_mode_with_end_datetime in Netatmo integration (#101674)

* Add Netatmo climate service set_preset_mode_with_end_datetime to allow you to specify an end datetime for a preset mode

* Make end date optional

* address review comments

* Update strings

* Update homeassistant/components/netatmo/const.py

* Update homeassistant/components/netatmo/strings.json

---------

Co-authored-by: Adrien JOLY <adrien.joly-veillith@live.fr>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
Tobias Sauerwein 2023-10-14 14:44:16 +02:00 committed by GitHub
parent 8a4fe5add1
commit 302b444269
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 1 deletions

View file

@ -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."""

View file

@ -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"

View file

@ -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:

View file

@ -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."
}
}
}
}
}

View file

@ -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: