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 import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_PRESET_MODE,
DEFAULT_MIN_TEMP, DEFAULT_MIN_TEMP,
PRESET_AWAY, PRESET_AWAY,
PRESET_BOOST, 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.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
ATTR_END_DATETIME,
ATTR_HEATING_POWER_REQUEST, ATTR_HEATING_POWER_REQUEST,
ATTR_SCHEDULE_NAME, ATTR_SCHEDULE_NAME,
ATTR_SELECTED_SCHEDULE, ATTR_SELECTED_SCHEDULE,
@ -43,6 +46,7 @@ from .const import (
EVENT_TYPE_SET_POINT, EVENT_TYPE_SET_POINT,
EVENT_TYPE_THERM_MODE, EVENT_TYPE_THERM_MODE,
NETATMO_CREATE_CLIMATE, NETATMO_CREATE_CLIMATE,
SERVICE_SET_PRESET_MODE_WITH_END_DATETIME,
SERVICE_SET_SCHEDULE, SERVICE_SET_SCHEDULE,
) )
from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom 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] 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_SCHEDULE = "schedule"
STATE_NETATMO_HG = "hg" STATE_NETATMO_HG = "hg"
STATE_NETATMO_MAX = "max" STATE_NETATMO_MAX = "max"
@ -124,6 +130,14 @@ async def async_setup_entry(
{vol.Required(ATTR_SCHEDULE_NAME): cv.string}, {vol.Required(ATTR_SCHEDULE_NAME): cv.string},
"_async_service_set_schedule", "_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): class NetatmoThermostat(NetatmoBase, ClimateEntity):
@ -314,7 +328,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
await self._room.async_therm_set(STATE_NETATMO_HOME) await self._room.async_therm_set(STATE_NETATMO_HOME)
elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX):
await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) 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]) await self._room.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode])
else: else:
_LOGGER.error("Preset mode '%s' not available", preset_mode) _LOGGER.error("Preset mode '%s' not available", preset_mode)
@ -410,6 +424,23 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
schedule_id, 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 @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device info for the thermostat.""" """Return the device info for the thermostat."""

View file

@ -69,6 +69,7 @@ DEFAULT_PERSON = "unknown"
DEFAULT_WEBHOOKS = False DEFAULT_WEBHOOKS = False
ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" ATTR_CAMERA_LIGHT_MODE = "camera_light_mode"
ATTR_END_DATETIME = "end_datetime"
ATTR_EVENT_TYPE = "event_type" ATTR_EVENT_TYPE = "event_type"
ATTR_FACE_URL = "face_url" ATTR_FACE_URL = "face_url"
ATTR_HEATING_POWER_REQUEST = "heating_power_request" 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_PERSON_AWAY = "set_person_away"
SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_PERSONS_HOME = "set_persons_home"
SERVICE_SET_SCHEDULE = "set_schedule" SERVICE_SET_SCHEDULE = "set_schedule"
SERVICE_SET_PRESET_MODE_WITH_END_DATETIME = "set_preset_mode_with_end_datetime"
# Climate events # Climate events
EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point"

View file

@ -26,6 +26,26 @@ set_schedule:
selector: selector:
text: 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: set_persons_home:
target: target:
entity: entity:

View file

@ -115,6 +115,20 @@
"unregister_webhook": { "unregister_webhook": {
"name": "Unregister webhook", "name": "Unregister webhook",
"description": "Unregisters the webhook from the Netatmo backend." "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.""" """The tests for the Netatmo climate platform."""
from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from voluptuous.error import MultipleInvalid
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_HVAC_MODE, 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.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE
from homeassistant.components.netatmo.const import ( from homeassistant.components.netatmo.const import (
ATTR_END_DATETIME,
ATTR_SCHEDULE_NAME, ATTR_SCHEDULE_NAME,
SERVICE_SET_PRESET_MODE_WITH_END_DATETIME,
SERVICE_SET_SCHEDULE, SERVICE_SET_SCHEDULE,
) )
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from .common import selected_platforms, simulate_webhook 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 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( async def test_service_preset_mode_already_boost_valves(
hass: HomeAssistant, config_entry, netatmo_auth hass: HomeAssistant, config_entry, netatmo_auth
) -> None: ) -> None: