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:
parent
8a4fe5add1
commit
302b444269
5 changed files with 145 additions and 1 deletions
|
@ -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."""
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Reference in a new issue