mirror of https://github.com/home-assistant/core
Make radiotherm hold mode a switch (#73104)
This commit is contained in:
parent
1331c75ec2
commit
329595bf73
|
@ -963,9 +963,12 @@ omit =
|
||||||
homeassistant/components/radio_browser/__init__.py
|
homeassistant/components/radio_browser/__init__.py
|
||||||
homeassistant/components/radio_browser/media_source.py
|
homeassistant/components/radio_browser/media_source.py
|
||||||
homeassistant/components/radiotherm/__init__.py
|
homeassistant/components/radiotherm/__init__.py
|
||||||
|
homeassistant/components/radiotherm/entity.py
|
||||||
homeassistant/components/radiotherm/climate.py
|
homeassistant/components/radiotherm/climate.py
|
||||||
homeassistant/components/radiotherm/coordinator.py
|
homeassistant/components/radiotherm/coordinator.py
|
||||||
homeassistant/components/radiotherm/data.py
|
homeassistant/components/radiotherm/data.py
|
||||||
|
homeassistant/components/radiotherm/switch.py
|
||||||
|
homeassistant/components/radiotherm/util.py
|
||||||
homeassistant/components/rainbird/*
|
homeassistant/components/rainbird/*
|
||||||
homeassistant/components/raincloud/*
|
homeassistant/components/raincloud/*
|
||||||
homeassistant/components/rainmachine/__init__.py
|
homeassistant/components/rainmachine/__init__.py
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
"""The radiotherm component."""
|
"""The radiotherm component."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Coroutine
|
||||||
from socket import timeout
|
from socket import timeout
|
||||||
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
from radiotherm.validate import RadiothermTstatError
|
from radiotherm.validate import RadiothermTstatError
|
||||||
|
|
||||||
|
@ -10,30 +12,46 @@ from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import CONF_HOLD_TEMP, DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import RadioThermUpdateCoordinator
|
from .coordinator import RadioThermUpdateCoordinator
|
||||||
from .data import async_get_init_data
|
from .data import async_get_init_data
|
||||||
|
from .util import async_set_time
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.CLIMATE]
|
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SWITCH]
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_call_or_raise_not_ready(
|
||||||
|
coro: Coroutine[Any, Any, _T], host: str
|
||||||
|
) -> _T:
|
||||||
|
"""Call a coro or raise ConfigEntryNotReady."""
|
||||||
|
try:
|
||||||
|
return await coro
|
||||||
|
except RadiothermTstatError as ex:
|
||||||
|
msg = f"{host} was busy (invalid value returned): {ex}"
|
||||||
|
raise ConfigEntryNotReady(msg) from ex
|
||||||
|
except timeout as ex:
|
||||||
|
msg = f"{host} timed out waiting for a response: {ex}"
|
||||||
|
raise ConfigEntryNotReady(msg) from ex
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Radio Thermostat from a config entry."""
|
"""Set up Radio Thermostat from a config entry."""
|
||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
try:
|
init_coro = async_get_init_data(hass, host)
|
||||||
init_data = await async_get_init_data(hass, host)
|
init_data = await _async_call_or_raise_not_ready(init_coro, host)
|
||||||
except RadiothermTstatError as ex:
|
coordinator = RadioThermUpdateCoordinator(hass, init_data)
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
f"{host} was busy (invalid value returned): {ex}"
|
|
||||||
) from ex
|
|
||||||
except timeout as ex:
|
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
f"{host} timed out waiting for a response: {ex}"
|
|
||||||
) from ex
|
|
||||||
|
|
||||||
hold_temp = entry.options[CONF_HOLD_TEMP]
|
|
||||||
coordinator = RadioThermUpdateCoordinator(hass, init_data, hold_temp)
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
# Only set the time if the thermostat is
|
||||||
|
# not in hold mode since setting the time
|
||||||
|
# clears the hold for some strange design
|
||||||
|
# choice
|
||||||
|
if not coordinator.data.tstat["hold"]:
|
||||||
|
time_coro = async_set_time(hass, init_data.tstat)
|
||||||
|
await _async_call_or_raise_not_ready(time_coro, host)
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Support for Radio Thermostat wifi-enabled home thermostats."""
|
"""Support for Radio Thermostat wifi-enabled home thermostats."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -27,19 +26,13 @@ from homeassistant.const import (
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers import device_registry as dr
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN
|
||||||
from .const import CONF_HOLD_TEMP
|
|
||||||
from .coordinator import RadioThermUpdateCoordinator
|
from .coordinator import RadioThermUpdateCoordinator
|
||||||
from .data import RadioThermUpdate
|
from .entity import RadioThermostatEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -92,10 +85,11 @@ PRESET_MODE_TO_CODE = {
|
||||||
|
|
||||||
CODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_CODE.items()}
|
CODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_CODE.items()}
|
||||||
|
|
||||||
CODE_TO_HOLD_STATE = {0: False, 1: True}
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
CONF_HOLD_TEMP = "hold_temp"
|
||||||
|
|
||||||
|
|
||||||
def round_temp(temperature):
|
def round_temp(temperature):
|
||||||
"""Round a temperature to the resolution of the thermostat.
|
"""Round a temperature to the resolution of the thermostat.
|
||||||
|
@ -150,18 +144,17 @@ async def async_setup_platform(
|
||||||
_LOGGER.error("No Radiotherm Thermostats detected")
|
_LOGGER.error("No Radiotherm Thermostats detected")
|
||||||
return
|
return
|
||||||
|
|
||||||
hold_temp: bool = config[CONF_HOLD_TEMP]
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.flow.async_init(
|
hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": SOURCE_IMPORT},
|
context={"source": SOURCE_IMPORT},
|
||||||
data={CONF_HOST: host, CONF_HOLD_TEMP: hold_temp},
|
data={CONF_HOST: host},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEntity):
|
class RadioThermostat(RadioThermostatEntity, ClimateEntity):
|
||||||
"""Representation of a Radio Thermostat."""
|
"""Representation of a Radio Thermostat."""
|
||||||
|
|
||||||
_attr_hvac_modes = OPERATION_LIST
|
_attr_hvac_modes = OPERATION_LIST
|
||||||
|
@ -171,49 +164,22 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt
|
||||||
def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None:
|
def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None:
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.device = coordinator.init_data.tstat
|
self._attr_name = self.init_data.name
|
||||||
self._attr_name = coordinator.init_data.name
|
self._attr_unique_id = self.init_data.mac
|
||||||
self._hold_temp = coordinator.hold_temp
|
self._attr_fan_modes = CT30_FAN_OPERATION_LIST
|
||||||
self._hold_set = False
|
|
||||||
self._attr_unique_id = coordinator.init_data.mac
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
name=coordinator.init_data.name,
|
|
||||||
model=coordinator.init_data.model,
|
|
||||||
manufacturer="Radio Thermostats",
|
|
||||||
sw_version=coordinator.init_data.fw_version,
|
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.init_data.mac)},
|
|
||||||
)
|
|
||||||
self._is_model_ct80 = isinstance(self.device, radiotherm.thermostat.CT80)
|
|
||||||
self._attr_supported_features = (
|
self._attr_supported_features = (
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
||||||
)
|
)
|
||||||
self._process_data()
|
if not isinstance(self.device, radiotherm.thermostat.CT80):
|
||||||
if not self._is_model_ct80:
|
|
||||||
self._attr_fan_modes = CT30_FAN_OPERATION_LIST
|
|
||||||
return
|
return
|
||||||
self._attr_fan_modes = CT80_FAN_OPERATION_LIST
|
self._attr_fan_modes = CT80_FAN_OPERATION_LIST
|
||||||
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
||||||
self._attr_preset_modes = PRESET_MODES
|
self._attr_preset_modes = PRESET_MODES
|
||||||
|
|
||||||
@property
|
|
||||||
def data(self) -> RadioThermUpdate:
|
|
||||||
"""Returnt the last update."""
|
|
||||||
return self.coordinator.data
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Register callbacks."""
|
|
||||||
# Set the time on the device. This shouldn't be in the
|
|
||||||
# constructor because it's a network call. We can't put it in
|
|
||||||
# update() because calling it will clear any temporary mode or
|
|
||||||
# temperature in the thermostat. So add it as a future job
|
|
||||||
# for the event loop to run.
|
|
||||||
self.hass.async_add_job(self.set_time)
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
|
|
||||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
"""Turn fan on/off."""
|
"""Turn fan on/off."""
|
||||||
if (code := FAN_MODE_TO_CODE.get(fan_mode)) is None:
|
if (code := FAN_MODE_TO_CODE.get(fan_mode)) is None:
|
||||||
raise HomeAssistantError(f"{fan_mode} is not a valid fan mode")
|
raise ValueError(f"{fan_mode} is not a valid fan mode")
|
||||||
await self.hass.async_add_executor_job(self._set_fan_mode, code)
|
await self.hass.async_add_executor_job(self._set_fan_mode, code)
|
||||||
self._attr_fan_mode = fan_mode
|
self._attr_fan_mode = fan_mode
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -223,16 +189,11 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt
|
||||||
"""Turn fan on/off."""
|
"""Turn fan on/off."""
|
||||||
self.device.fmode = code
|
self.device.fmode = code
|
||||||
|
|
||||||
@callback
|
|
||||||
def _handle_coordinator_update(self) -> None:
|
|
||||||
self._process_data()
|
|
||||||
return super()._handle_coordinator_update()
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _process_data(self) -> None:
|
def _process_data(self) -> None:
|
||||||
"""Update and validate the data from the thermostat."""
|
"""Update and validate the data from the thermostat."""
|
||||||
data = self.data.tstat
|
data = self.data.tstat
|
||||||
if self._is_model_ct80:
|
if isinstance(self.device, radiotherm.thermostat.CT80):
|
||||||
self._attr_current_humidity = self.data.humidity
|
self._attr_current_humidity = self.data.humidity
|
||||||
self._attr_preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]]
|
self._attr_preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]]
|
||||||
# Map thermostat values into various STATE_ flags.
|
# Map thermostat values into various STATE_ flags.
|
||||||
|
@ -242,7 +203,6 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt
|
||||||
ATTR_FAN_ACTION: CODE_TO_FAN_STATE[data["fstate"]]
|
ATTR_FAN_ACTION: CODE_TO_FAN_STATE[data["fstate"]]
|
||||||
}
|
}
|
||||||
self._attr_hvac_mode = CODE_TO_TEMP_MODE[data["tmode"]]
|
self._attr_hvac_mode = CODE_TO_TEMP_MODE[data["tmode"]]
|
||||||
self._hold_set = CODE_TO_HOLD_STATE[data["hold"]]
|
|
||||||
if self.hvac_mode == HVACMode.OFF:
|
if self.hvac_mode == HVACMode.OFF:
|
||||||
self._attr_hvac_action = None
|
self._attr_hvac_action = None
|
||||||
else:
|
else:
|
||||||
|
@ -264,15 +224,12 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||||
return
|
return
|
||||||
hold_changed = kwargs.get("hold_changed", False)
|
await self.hass.async_add_executor_job(self._set_temperature, temperature)
|
||||||
await self.hass.async_add_executor_job(
|
|
||||||
partial(self._set_temperature, temperature, hold_changed)
|
|
||||||
)
|
|
||||||
self._attr_target_temperature = temperature
|
self._attr_target_temperature = temperature
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
def _set_temperature(self, temperature: int, hold_changed: bool) -> None:
|
def _set_temperature(self, temperature: int) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
temperature = round_temp(temperature)
|
temperature = round_temp(temperature)
|
||||||
if self.hvac_mode == HVACMode.COOL:
|
if self.hvac_mode == HVACMode.COOL:
|
||||||
|
@ -285,26 +242,6 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt
|
||||||
elif self.hvac_action == HVACAction.HEATING:
|
elif self.hvac_action == HVACAction.HEATING:
|
||||||
self.device.t_heat = temperature
|
self.device.t_heat = temperature
|
||||||
|
|
||||||
# Only change the hold if requested or if hold mode was turned
|
|
||||||
# on and we haven't set it yet.
|
|
||||||
if hold_changed or not self._hold_set:
|
|
||||||
if self._hold_temp:
|
|
||||||
self.device.hold = 1
|
|
||||||
self._hold_set = True
|
|
||||||
else:
|
|
||||||
self.device.hold = 0
|
|
||||||
|
|
||||||
def set_time(self) -> None:
|
|
||||||
"""Set device time."""
|
|
||||||
# Calling this clears any local temperature override and
|
|
||||||
# reverts to the scheduled temperature.
|
|
||||||
now = dt_util.now()
|
|
||||||
self.device.time = {
|
|
||||||
"day": now.weekday(),
|
|
||||||
"hour": now.hour,
|
|
||||||
"minute": now.minute,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set operation mode (auto, cool, heat, off)."""
|
"""Set operation mode (auto, cool, heat, off)."""
|
||||||
await self.hass.async_add_executor_job(self._set_hvac_mode, hvac_mode)
|
await self.hass.async_add_executor_job(self._set_hvac_mode, hvac_mode)
|
||||||
|
@ -325,7 +262,7 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set Preset mode (Home, Alternate, Away, Holiday)."""
|
"""Set Preset mode (Home, Alternate, Away, Holiday)."""
|
||||||
if preset_mode not in PRESET_MODES:
|
if preset_mode not in PRESET_MODES:
|
||||||
raise HomeAssistantError("{preset_mode} is not a valid preset_mode")
|
raise ValueError(f"{preset_mode} is not a valid preset_mode")
|
||||||
await self.hass.async_add_executor_job(self._set_preset_mode, preset_mode)
|
await self.hass.async_add_executor_job(self._set_preset_mode, preset_mode)
|
||||||
self._attr_preset_mode = preset_mode
|
self._attr_preset_mode = preset_mode
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -333,4 +270,5 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt
|
||||||
|
|
||||||
def _set_preset_mode(self, preset_mode: str) -> None:
|
def _set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set Preset mode (Home, Alternate, Away, Holiday)."""
|
"""Set Preset mode (Home, Alternate, Away, Holiday)."""
|
||||||
|
assert isinstance(self.device, radiotherm.thermostat.CT80)
|
||||||
self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode]
|
self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode]
|
||||||
|
|
|
@ -11,11 +11,11 @@ import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import dhcp
|
from homeassistant.components import dhcp
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from .const import CONF_HOLD_TEMP, DOMAIN
|
from .const import DOMAIN
|
||||||
from .data import RadioThermInitData, async_get_init_data
|
from .data import RadioThermInitData, async_get_init_data
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -68,7 +68,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=init_data.name,
|
title=init_data.name,
|
||||||
data={CONF_HOST: ip_address},
|
data={CONF_HOST: ip_address},
|
||||||
options={CONF_HOLD_TEMP: False},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._set_confirm_only()
|
self._set_confirm_only()
|
||||||
|
@ -100,7 +99,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=init_data.name,
|
title=init_data.name,
|
||||||
data={CONF_HOST: import_info[CONF_HOST]},
|
data={CONF_HOST: import_info[CONF_HOST]},
|
||||||
options={CONF_HOLD_TEMP: import_info[CONF_HOLD_TEMP]},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
|
@ -125,7 +123,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=init_data.name,
|
title=init_data.name,
|
||||||
data=user_input,
|
data=user_input,
|
||||||
options={CONF_HOLD_TEMP: False},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
@ -133,34 +130,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@callback
|
|
||||||
def async_get_options_flow(config_entry):
|
|
||||||
"""Get the options flow for this handler."""
|
|
||||||
return OptionsFlowHandler(config_entry)
|
|
||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
|
||||||
"""Handle a option flow for radiotherm."""
|
|
||||||
|
|
||||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
|
||||||
"""Initialize options flow."""
|
|
||||||
self.config_entry = config_entry
|
|
||||||
|
|
||||||
async def async_step_init(self, user_input=None):
|
|
||||||
"""Handle options flow."""
|
|
||||||
if user_input is not None:
|
|
||||||
return self.async_create_entry(title="", data=user_input)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="init",
|
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(
|
|
||||||
CONF_HOLD_TEMP,
|
|
||||||
default=self.config_entry.options[CONF_HOLD_TEMP],
|
|
||||||
): bool
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
|
@ -2,6 +2,4 @@
|
||||||
|
|
||||||
DOMAIN = "radiotherm"
|
DOMAIN = "radiotherm"
|
||||||
|
|
||||||
CONF_HOLD_TEMP = "hold_temp"
|
|
||||||
|
|
||||||
TIMEOUT = 25
|
TIMEOUT = 25
|
||||||
|
|
|
@ -20,12 +20,9 @@ UPDATE_INTERVAL = timedelta(seconds=15)
|
||||||
class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]):
|
class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]):
|
||||||
"""DataUpdateCoordinator to gather data for radio thermostats."""
|
"""DataUpdateCoordinator to gather data for radio thermostats."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistant, init_data: RadioThermInitData) -> None:
|
||||||
self, hass: HomeAssistant, init_data: RadioThermInitData, hold_temp: bool
|
|
||||||
) -> None:
|
|
||||||
"""Initialize DataUpdateCoordinator."""
|
"""Initialize DataUpdateCoordinator."""
|
||||||
self.init_data = init_data
|
self.init_data = init_data
|
||||||
self.hold_temp = hold_temp
|
|
||||||
self._description = f"{init_data.name} ({init_data.host})"
|
self._description = f"{init_data.name} ({init_data.host})"
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
|
@ -39,10 +36,8 @@ class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]):
|
||||||
try:
|
try:
|
||||||
return await async_get_data(self.hass, self.init_data.tstat)
|
return await async_get_data(self.hass, self.init_data.tstat)
|
||||||
except RadiothermTstatError as ex:
|
except RadiothermTstatError as ex:
|
||||||
raise UpdateFailed(
|
msg = f"{self._description} was busy (invalid value returned): {ex}"
|
||||||
f"{self._description} was busy (invalid value returned): {ex}"
|
raise UpdateFailed(msg) from ex
|
||||||
) from ex
|
|
||||||
except timeout as ex:
|
except timeout as ex:
|
||||||
raise UpdateFailed(
|
msg = f"{self._description}) timed out waiting for a response: {ex}"
|
||||||
f"{self._description}) timed out waiting for a response: {ex}"
|
raise UpdateFailed(msg) from ex
|
||||||
) from ex
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""The radiotherm integration base entity."""
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .coordinator import RadioThermUpdateCoordinator
|
||||||
|
from .data import RadioThermUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class RadioThermostatEntity(CoordinatorEntity[RadioThermUpdateCoordinator]):
|
||||||
|
"""Base class for radiotherm entities."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.init_data = coordinator.init_data
|
||||||
|
self.device = coordinator.init_data.tstat
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
name=self.init_data.name,
|
||||||
|
model=self.init_data.model,
|
||||||
|
manufacturer="Radio Thermostats",
|
||||||
|
sw_version=self.init_data.fw_version,
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, self.init_data.mac)},
|
||||||
|
)
|
||||||
|
self._process_data()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> RadioThermUpdate:
|
||||||
|
"""Returnt the last update."""
|
||||||
|
return self.coordinator.data
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@abstractmethod
|
||||||
|
def _process_data(self) -> None:
|
||||||
|
"""Update and validate the data from the thermostat."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
self._process_data()
|
||||||
|
return super()._handle_coordinator_update()
|
|
@ -0,0 +1,65 @@
|
||||||
|
"""Support for radiotherm switches."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import RadioThermUpdateCoordinator
|
||||||
|
from .entity import RadioThermostatEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up switches for a radiotherm device."""
|
||||||
|
coordinator: RadioThermUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
async_add_entities([RadioThermHoldSwitch(coordinator)])
|
||||||
|
|
||||||
|
|
||||||
|
class RadioThermHoldSwitch(RadioThermostatEntity, SwitchEntity):
|
||||||
|
"""Provides radiotherm hold switch support."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None:
|
||||||
|
"""Initialize the hold mode switch."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_name = f"{coordinator.init_data.name} Hold"
|
||||||
|
self._attr_unique_id = f"{coordinator.init_data.mac}_hold"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the icon for the switch."""
|
||||||
|
return "mdi:timer-off" if self.is_on else "mdi:timer"
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _process_data(self) -> None:
|
||||||
|
"""Update and validate the data from the thermostat."""
|
||||||
|
data = self.data.tstat
|
||||||
|
self._attr_is_on = bool(data["hold"])
|
||||||
|
|
||||||
|
def _set_hold(self, hold: bool) -> None:
|
||||||
|
"""Set hold mode."""
|
||||||
|
self.device.hold = int(hold)
|
||||||
|
|
||||||
|
async def _async_set_hold(self, hold: bool) -> None:
|
||||||
|
"""Set hold mode."""
|
||||||
|
await self.hass.async_add_executor_job(self._set_hold, hold)
|
||||||
|
self._attr_is_on = hold
|
||||||
|
self.async_write_ha_state()
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Enable permanent hold."""
|
||||||
|
await self._async_set_hold(True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Disable permanent hold."""
|
||||||
|
await self._async_set_hold(False)
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""Utils for radiotherm."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from radiotherm.thermostat import CommonThermostat
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
|
async def async_set_time(hass: HomeAssistant, device: CommonThermostat) -> None:
|
||||||
|
"""Sync time to the thermostat."""
|
||||||
|
await hass.async_add_executor_job(_set_time, device)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_time(device: CommonThermostat) -> None:
|
||||||
|
"""Set device time."""
|
||||||
|
# Calling this clears any local temperature override and
|
||||||
|
# reverts to the scheduled temperature.
|
||||||
|
now = dt_util.now()
|
||||||
|
device.time = {
|
||||||
|
"day": now.weekday(),
|
||||||
|
"hour": now.hour,
|
||||||
|
"minute": now.minute,
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ from radiotherm.validate import RadiothermTstatError
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow
|
from homeassistant import config_entries, data_entry_flow
|
||||||
from homeassistant.components import dhcp
|
from homeassistant.components import dhcp
|
||||||
from homeassistant.components.radiotherm.const import CONF_HOLD_TEMP, DOMAIN
|
from homeassistant.components.radiotherm.const import DOMAIN
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
@ -110,17 +110,12 @@ async def test_import(hass):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
data={CONF_HOST: "1.2.3.4", CONF_HOLD_TEMP: True},
|
data={CONF_HOST: "1.2.3.4"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result["title"] == "My Name"
|
assert result["title"] == "My Name"
|
||||||
assert result["data"] == {
|
assert result["data"] == {CONF_HOST: "1.2.3.4"}
|
||||||
CONF_HOST: "1.2.3.4",
|
|
||||||
}
|
|
||||||
assert result["options"] == {
|
|
||||||
CONF_HOLD_TEMP: True,
|
|
||||||
}
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,7 +128,7 @@ async def test_import_cannot_connect(hass):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
data={CONF_HOST: "1.2.3.4", CONF_HOLD_TEMP: True},
|
data={CONF_HOST: "1.2.3.4"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
@ -268,35 +263,3 @@ async def test_user_unique_id_already_exists(hass):
|
||||||
|
|
||||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
assert result2["reason"] == "already_configured"
|
assert result2["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow(hass):
|
|
||||||
"""Test config flow options."""
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain=DOMAIN,
|
|
||||||
data={CONF_HOST: "1.2.3.4"},
|
|
||||||
unique_id="aa:bb:cc:dd:ee:ff",
|
|
||||||
options={CONF_HOLD_TEMP: False},
|
|
||||||
)
|
|
||||||
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.radiotherm.data.radiotherm.get_thermostat",
|
|
||||||
return_value=_mock_radiotherm(),
|
|
||||||
):
|
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
|
||||||
assert result["step_id"] == "init"
|
|
||||||
|
|
||||||
result = await hass.config_entries.options.async_configure(
|
|
||||||
result["flow_id"], user_input={CONF_HOLD_TEMP: True}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
|
||||||
assert entry.options == {CONF_HOLD_TEMP: True}
|
|
||||||
|
|
Loading…
Reference in New Issue