Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Platform for eQ-3 climate entities."""
2 
3 import logging
4 from typing import Any
5 
6 from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode
7 from eq3btsmart.exceptions import Eq3Exception
8 
10  ATTR_HVAC_MODE,
11  PRESET_NONE,
12  ClimateEntity,
13  ClimateEntityFeature,
14  HVACAction,
15  HVACMode,
16 )
17 from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.exceptions import ServiceValidationError
20 from homeassistant.helpers import device_registry as dr
21 from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
22 from homeassistant.helpers.entity_platform import AddEntitiesCallback
23 
24 from . import Eq3ConfigEntry
25 from .const import (
26  EQ_TO_HA_HVAC,
27  HA_TO_EQ_HVAC,
28  CurrentTemperatureSelector,
29  Preset,
30  TargetTemperatureSelector,
31 )
32 from .entity import Eq3Entity
33 
34 _LOGGER = logging.getLogger(__name__)
35 
36 
38  hass: HomeAssistant,
39  entry: Eq3ConfigEntry,
40  async_add_entities: AddEntitiesCallback,
41 ) -> None:
42  """Handle config entry setup."""
43 
45  [Eq3Climate(entry)],
46  )
47 
48 
50  """Climate entity to represent a eQ-3 thermostat."""
51 
52  _attr_name = None
53  _attr_supported_features = (
54  ClimateEntityFeature.TARGET_TEMPERATURE
55  | ClimateEntityFeature.PRESET_MODE
56  | ClimateEntityFeature.TURN_OFF
57  | ClimateEntityFeature.TURN_ON
58  )
59  _attr_temperature_unit = UnitOfTemperature.CELSIUS
60  _attr_min_temp = EQ3BT_OFF_TEMP
61  _attr_max_temp = EQ3BT_MAX_TEMP
62  _attr_precision = PRECISION_HALVES
63  _attr_hvac_modes = list(HA_TO_EQ_HVAC.keys())
64  _attr_preset_modes = list(Preset)
65  _attr_should_poll = False
66  _attr_available = False
67  _attr_hvac_mode: HVACMode | None = None
68  _attr_hvac_action: HVACAction | None = None
69  _attr_preset_mode: str | None = None
70  _target_temperature: float | None = None
71 
72  @callback
73  def _async_on_updated(self) -> None:
74  """Handle updated data from the thermostat."""
75 
76  if self._thermostat_thermostat.status is not None:
77  self._async_on_status_updated_async_on_status_updated()
78 
79  if self._thermostat_thermostat.device_data is not None:
80  self._async_on_device_updated_async_on_device_updated()
81 
82  super()._async_on_updated()
83 
84  @callback
85  def _async_on_status_updated(self) -> None:
86  """Handle updated status from the thermostat."""
87 
88  if self._thermostat_thermostat.status is None:
89  return
90 
91  self._target_temperature_target_temperature = self._thermostat_thermostat.status.target_temperature.value
92  self._attr_hvac_mode_attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat_thermostat.status.operation_mode]
93  self._attr_current_temperature_attr_current_temperature = self._get_current_temperature_get_current_temperature()
94  self._attr_target_temperature_attr_target_temperature = self._get_target_temperature_get_target_temperature()
95  self._attr_preset_mode_attr_preset_mode = self._get_current_preset_mode_get_current_preset_mode()
96  self._attr_hvac_action_attr_hvac_action = self._get_current_hvac_action_get_current_hvac_action()
97 
98  @callback
99  def _async_on_device_updated(self) -> None:
100  """Handle updated device data from the thermostat."""
101 
102  if self._thermostat_thermostat.device_data is None:
103  return
104 
105  device_registry = dr.async_get(self.hasshass)
106  if device := device_registry.async_get_device(
107  connections={(CONNECTION_BLUETOOTH, self._eq3_config_eq3_config.mac_address)},
108  ):
109  device_registry.async_update_device(
110  device.id,
111  sw_version=str(self._thermostat_thermostat.device_data.firmware_version),
112  serial_number=self._thermostat_thermostat.device_data.device_serial.value,
113  )
114 
115  def _get_current_temperature(self) -> float | None:
116  """Return the current temperature."""
117 
118  match self._eq3_config_eq3_config.current_temp_selector:
119  case CurrentTemperatureSelector.NOTHING:
120  return None
121  case CurrentTemperatureSelector.VALVE:
122  if self._thermostat_thermostat.status is None:
123  return None
124 
125  return float(self._thermostat_thermostat.status.valve_temperature)
126  case CurrentTemperatureSelector.UI:
127  return self._target_temperature_target_temperature
128  case CurrentTemperatureSelector.DEVICE:
129  if self._thermostat_thermostat.status is None:
130  return None
131 
132  return float(self._thermostat_thermostat.status.target_temperature.value)
133  case CurrentTemperatureSelector.ENTITY:
134  state = self.hasshass.states.get(self._eq3_config_eq3_config.external_temp_sensor)
135  if state is not None:
136  try:
137  return float(state.state)
138  except ValueError:
139  pass
140 
141  return None
142 
143  def _get_target_temperature(self) -> float | None:
144  """Return the target temperature."""
145 
146  match self._eq3_config_eq3_config.target_temp_selector:
147  case TargetTemperatureSelector.TARGET:
148  return self._target_temperature_target_temperature
149  case TargetTemperatureSelector.LAST_REPORTED:
150  if self._thermostat_thermostat.status is None:
151  return None
152 
153  return float(self._thermostat_thermostat.status.target_temperature.value)
154 
155  def _get_current_preset_mode(self) -> str:
156  """Return the current preset mode."""
157 
158  if (status := self._thermostat_thermostat.status) is None:
159  return PRESET_NONE
160  if status.is_window_open:
161  return Preset.WINDOW_OPEN
162  if status.is_boost:
163  return Preset.BOOST
164  if status.is_low_battery:
165  return Preset.LOW_BATTERY
166  if status.is_away:
167  return Preset.AWAY
168  if status.operation_mode is OperationMode.ON:
169  return Preset.OPEN
170  if status.presets is None:
171  return PRESET_NONE
172  if status.target_temperature == status.presets.eco_temperature:
173  return Preset.ECO
174  if status.target_temperature == status.presets.comfort_temperature:
175  return Preset.COMFORT
176 
177  return PRESET_NONE
178 
179  def _get_current_hvac_action(self) -> HVACAction:
180  """Return the current hvac action."""
181 
182  if (
183  self._thermostat_thermostat.status is None
184  or self._thermostat_thermostat.status.operation_mode is OperationMode.OFF
185  ):
186  return HVACAction.OFF
187  if self._thermostat_thermostat.status.valve == 0:
188  return HVACAction.IDLE
189  return HVACAction.HEATING
190 
191  async def async_set_temperature(self, **kwargs: Any) -> None:
192  """Set new target temperature."""
193 
194  if ATTR_HVAC_MODE in kwargs:
195  mode: HVACMode | None
196  if (mode := kwargs.get(ATTR_HVAC_MODE)) is None:
197  return
198 
199  if mode is not HVACMode.OFF:
200  await self.async_set_hvac_modeasync_set_hvac_modeasync_set_hvac_mode(mode)
201  else:
203  f"[{self._eq3_config.mac_address}] Can't change HVAC mode to off while changing temperature",
204  )
205 
206  temperature: float | None
207  if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
208  return
209 
210  previous_temperature = self._target_temperature_target_temperature
211  self._target_temperature_target_temperature = temperature
212 
213  self.async_write_ha_stateasync_write_ha_state()
214 
215  try:
216  await self._thermostat_thermostat.async_set_temperature(temperature)
217  except Eq3Exception:
218  _LOGGER.error(
219  "[%s] Failed setting temperature", self._eq3_config_eq3_config.mac_address
220  )
221  self._target_temperature_target_temperature = previous_temperature
222  self.async_write_ha_stateasync_write_ha_state()
223  except ValueError as ex:
224  raise ServiceValidationError("Invalid temperature") from ex
225 
226  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
227  """Set new target hvac mode."""
228 
229  if hvac_mode is HVACMode.OFF:
230  await self.async_set_temperatureasync_set_temperatureasync_set_temperature(temperature=EQ3BT_OFF_TEMP)
231 
232  try:
233  await self._thermostat_thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode])
234  except Eq3Exception:
235  _LOGGER.error("[%s] Failed setting HVAC mode", self._eq3_config_eq3_config.mac_address)
236 
237  async def async_set_preset_mode(self, preset_mode: str) -> None:
238  """Set new preset mode."""
239 
240  match preset_mode:
241  case Preset.BOOST:
242  await self._thermostat_thermostat.async_set_boost(True)
243  case Preset.AWAY:
244  await self._thermostat_thermostat.async_set_away(True)
245  case Preset.ECO:
246  await self._thermostat_thermostat.async_set_preset(Eq3Preset.ECO)
247  case Preset.COMFORT:
248  await self._thermostat_thermostat.async_set_preset(Eq3Preset.COMFORT)
249  case Preset.OPEN:
250  await self._thermostat_thermostat.async_set_mode(OperationMode.ON)
None async_set_temperature(self, **Any kwargs)
Definition: __init__.py:775
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: __init__.py:813
None async_set_preset_mode(self, str preset_mode)
Definition: climate.py:237
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:226
None async_setup_entry(HomeAssistant hass, Eq3ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:41