Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for Radio Thermostat wifi-enabled home thermostats."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 import radiotherm
8 
10  FAN_AUTO,
11  FAN_OFF,
12  FAN_ON,
13  PRESET_AWAY,
14  PRESET_HOME,
15  ClimateEntity,
16  ClimateEntityFeature,
17  HVACAction,
18  HVACMode,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature
22 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.helpers.entity_platform import AddEntitiesCallback
24 
25 from . import DOMAIN
26 from .coordinator import RadioThermUpdateCoordinator
27 from .entity import RadioThermostatEntity
28 
29 ATTR_FAN_ACTION = "fan_action"
30 
31 PRESET_HOLIDAY = "holiday"
32 
33 PRESET_ALTERNATE = "alternate"
34 
35 STATE_CIRCULATE = "circulate"
36 
37 PRESET_MODES = [PRESET_HOME, PRESET_ALTERNATE, PRESET_AWAY, PRESET_HOLIDAY]
38 
39 OPERATION_LIST = [HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF]
40 CT30_FAN_OPERATION_LIST = [FAN_ON, FAN_AUTO]
41 CT80_FAN_OPERATION_LIST = [FAN_ON, STATE_CIRCULATE, FAN_AUTO]
42 
43 # Mappings from radiotherm json data codes to and from Home Assistant state
44 # flags. CODE is the thermostat integer code and these map to and
45 # from Home Assistant state flags.
46 
47 # Programmed temperature mode of the thermostat.
48 CODE_TO_TEMP_MODE = {
49  0: HVACMode.OFF,
50  1: HVACMode.HEAT,
51  2: HVACMode.COOL,
52  3: HVACMode.AUTO,
53 }
54 TEMP_MODE_TO_CODE = {v: k for k, v in CODE_TO_TEMP_MODE.items()}
55 
56 # Programmed fan mode (circulate is supported by CT80 models)
57 CODE_TO_FAN_MODE = {0: FAN_AUTO, 1: STATE_CIRCULATE, 2: FAN_ON}
58 
59 FAN_MODE_TO_CODE = {v: k for k, v in CODE_TO_FAN_MODE.items()}
60 
61 # Active thermostat state (is it heating or cooling?). In the future
62 # this should probably made into heat and cool binary sensors.
63 CODE_TO_TEMP_STATE = {0: HVACAction.IDLE, 1: HVACAction.HEATING, 2: HVACAction.COOLING}
64 
65 # Active fan state. This is if the fan is actually on or not. In the
66 # future this should probably made into a binary sensor for the fan.
67 CODE_TO_FAN_STATE = {0: FAN_OFF, 1: FAN_ON}
68 
69 PRESET_MODE_TO_CODE = {
70  PRESET_HOME: 0,
71  PRESET_ALTERNATE: 1,
72  PRESET_AWAY: 2,
73  PRESET_HOLIDAY: 3,
74 }
75 
76 CODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_CODE.items()}
77 
78 
79 PARALLEL_UPDATES = 1
80 
81 CONF_HOLD_TEMP = "hold_temp"
82 
83 
84 def round_temp(temperature):
85  """Round a temperature to the resolution of the thermostat.
86 
87  RadioThermostats can handle 0.5 degree temps so the input
88  temperature is rounded to that value and returned.
89  """
90  return round(temperature * 2.0) / 2.0
91 
92 
94  hass: HomeAssistant,
95  entry: ConfigEntry,
96  async_add_entities: AddEntitiesCallback,
97 ) -> None:
98  """Set up climate for a radiotherm device."""
99  coordinator: RadioThermUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
100  async_add_entities([RadioThermostat(coordinator)])
101 
102 
104  """Representation of a Radio Thermostat."""
105 
106  _attr_hvac_modes = OPERATION_LIST
107  _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
108  _attr_precision = PRECISION_HALVES
109  _attr_name = None
110  _enable_turn_on_off_backwards_compatibility = False
111 
112  def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None:
113  """Initialize the thermostat."""
114  super().__init__(coordinator)
115  self._attr_unique_id_attr_unique_id = self.init_datainit_datainit_data.mac
116  self._attr_fan_modes_attr_fan_modes = CT30_FAN_OPERATION_LIST
117  self._attr_supported_features_attr_supported_features = (
118  ClimateEntityFeature.TARGET_TEMPERATURE
119  | ClimateEntityFeature.FAN_MODE
120  | ClimateEntityFeature.TURN_OFF
121  | ClimateEntityFeature.TURN_ON
122  )
123  if not isinstance(self.devicedevice, radiotherm.thermostat.CT80):
124  return
125  self._attr_fan_modes_attr_fan_modes = CT80_FAN_OPERATION_LIST
126  self._attr_supported_features_attr_supported_features |= ClimateEntityFeature.PRESET_MODE
127  self._attr_preset_modes_attr_preset_modes = PRESET_MODES
128 
129  async def async_set_fan_mode(self, fan_mode: str) -> None:
130  """Turn fan on/off."""
131  if (code := FAN_MODE_TO_CODE.get(fan_mode)) is None:
132  raise ValueError(f"{fan_mode} is not a valid fan mode")
133  await self.hasshasshass.async_add_executor_job(self._set_fan_mode_set_fan_mode, code)
134  self._attr_fan_mode_attr_fan_mode = fan_mode
135  self.async_write_ha_stateasync_write_ha_state()
136  await self.coordinator.async_request_refresh()
137 
138  def _set_fan_mode(self, code: int) -> None:
139  """Turn fan on/off."""
140  self.devicedevice.fmode = code
141 
142  @callback
143  def _process_data(self) -> None:
144  """Update and validate the data from the thermostat."""
145  data = self.datadatadata.tstat
146  if isinstance(self.devicedevice, radiotherm.thermostat.CT80):
147  self._attr_current_humidity_attr_current_humidity = self.datadatadata.humidity
148  self._attr_preset_mode_attr_preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]]
149  # Map thermostat values into various STATE_ flags.
150  self._attr_current_temperature_attr_current_temperature = data["temp"]
151  self._attr_fan_mode_attr_fan_mode = CODE_TO_FAN_MODE[data["fmode"]]
152  self._attr_extra_state_attributes_attr_extra_state_attributes = {
153  ATTR_FAN_ACTION: CODE_TO_FAN_STATE[data["fstate"]]
154  }
155  self._attr_hvac_mode_attr_hvac_mode = CODE_TO_TEMP_MODE[data["tmode"]]
156  if self.hvac_modehvac_modehvac_modehvac_mode == HVACMode.OFF:
157  self._attr_hvac_action_attr_hvac_action = None
158  else:
159  self._attr_hvac_action_attr_hvac_action = CODE_TO_TEMP_STATE[data["tstate"]]
160  if self.hvac_modehvac_modehvac_modehvac_mode == HVACMode.COOL:
161  self._attr_target_temperature_attr_target_temperature = data["t_cool"]
162  elif self.hvac_modehvac_modehvac_modehvac_mode == HVACMode.HEAT:
163  self._attr_target_temperature_attr_target_temperature = data["t_heat"]
164  elif self.hvac_modehvac_modehvac_modehvac_mode == HVACMode.AUTO:
165  # This doesn't really work - tstate is only set if the HVAC is
166  # active. If it's idle, we don't know what to do with the target
167  # temperature.
168  if self.hvac_actionhvac_actionhvac_action == HVACAction.COOLING:
169  self._attr_target_temperature_attr_target_temperature = data["t_cool"]
170  elif self.hvac_actionhvac_actionhvac_action == HVACAction.HEATING:
171  self._attr_target_temperature_attr_target_temperature = data["t_heat"]
172 
173  async def async_set_temperature(self, **kwargs: Any) -> None:
174  """Set new target temperature."""
175  if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
176  return
177  await self.hasshasshass.async_add_executor_job(self._set_temperature_set_temperature, temperature)
178  self._attr_target_temperature_attr_target_temperature = temperature
179  self.async_write_ha_stateasync_write_ha_state()
180  await self.coordinator.async_request_refresh()
181 
182  def _set_temperature(self, temperature: int) -> None:
183  """Set new target temperature."""
184  temperature = round_temp(temperature)
185  if self.hvac_modehvac_modehvac_modehvac_mode == HVACMode.COOL:
186  self.devicedevice.t_cool = temperature
187  elif self.hvac_modehvac_modehvac_modehvac_mode == HVACMode.HEAT:
188  self.devicedevice.t_heat = temperature
189  elif self.hvac_modehvac_modehvac_modehvac_mode == HVACMode.AUTO:
190  if self.hvac_actionhvac_actionhvac_action == HVACAction.COOLING:
191  self.devicedevice.t_cool = temperature
192  elif self.hvac_actionhvac_actionhvac_action == HVACAction.HEATING:
193  self.devicedevice.t_heat = temperature
194 
195  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
196  """Set operation mode (auto, cool, heat, off)."""
197  await self.hasshasshass.async_add_executor_job(self._set_hvac_mode_set_hvac_mode, hvac_mode)
198  self._attr_hvac_mode_attr_hvac_mode = hvac_mode
199  self.async_write_ha_stateasync_write_ha_state()
200  await self.coordinator.async_request_refresh()
201 
202  def _set_hvac_mode(self, hvac_mode: HVACMode) -> None:
203  """Set operation mode (auto, cool, heat, off)."""
204  if hvac_mode in (HVACMode.OFF, HVACMode.AUTO):
205  self.devicedevice.tmode = TEMP_MODE_TO_CODE[hvac_mode]
206  # Setting t_cool or t_heat automatically changes tmode.
207  elif hvac_mode == HVACMode.COOL:
208  self.devicedevice.t_cool = self.target_temperaturetarget_temperature
209  elif hvac_mode == HVACMode.HEAT:
210  self.devicedevice.t_heat = self.target_temperaturetarget_temperature
211 
212  async def async_set_preset_mode(self, preset_mode: str) -> None:
213  """Set Preset mode (Home, Alternate, Away, Holiday)."""
214  if preset_mode not in PRESET_MODES:
215  raise ValueError(f"{preset_mode} is not a valid preset_mode")
216  await self.hasshasshass.async_add_executor_job(self._set_preset_mode_set_preset_mode, preset_mode)
217  self._attr_preset_mode_attr_preset_mode = preset_mode
218  self.async_write_ha_stateasync_write_ha_state()
219  await self.coordinator.async_request_refresh()
220 
221  def _set_preset_mode(self, preset_mode: str) -> None:
222  """Set Preset mode (Home, Alternate, Away, Holiday)."""
223  assert isinstance(self.devicedevice, radiotherm.thermostat.CT80)
224  self.devicedevice.program_mode = PRESET_MODE_TO_CODE[preset_mode]
None __init__(self, RadioThermUpdateCoordinator coordinator)
Definition: climate.py:112
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:195
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:97