Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for TP-Link thermostats."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, cast
7 
8 from kasa import Device, DeviceType
9 from kasa.smart.modules.temperaturecontrol import ThermostatState
10 
12  ATTR_TEMPERATURE,
13  ClimateEntity,
14  ClimateEntityFeature,
15  HVACAction,
16  HVACMode,
17 )
18 from homeassistant.const import PRECISION_TENTHS
19 from homeassistant.core import HomeAssistant, callback
20 from homeassistant.exceptions import ServiceValidationError
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 
23 from . import TPLinkConfigEntry
24 from .const import UNIT_MAPPING
25 from .coordinator import TPLinkDataUpdateCoordinator
26 from .entity import CoordinatedTPLinkEntity, async_refresh_after
27 
28 # Upstream state to HVACAction
29 STATE_TO_ACTION = {
30  ThermostatState.Idle: HVACAction.IDLE,
31  ThermostatState.Heating: HVACAction.HEATING,
32  ThermostatState.Off: HVACAction.OFF,
33 }
34 
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 
40  hass: HomeAssistant,
41  config_entry: TPLinkConfigEntry,
42  async_add_entities: AddEntitiesCallback,
43 ) -> None:
44  """Set up climate entities."""
45  data = config_entry.runtime_data
46  parent_coordinator = data.parent_coordinator
47  device = parent_coordinator.device
48 
49  # As there are no standalone thermostats, we just iterate over the children.
51  TPLinkClimateEntity(child, parent_coordinator, parent=device)
52  for child in device.children
53  if child.device_type is DeviceType.Thermostat
54  )
55 
56 
58  """Representation of a TPLink thermostat."""
59 
60  _attr_name = None
61  _attr_supported_features = (
62  ClimateEntityFeature.TARGET_TEMPERATURE
63  | ClimateEntityFeature.TURN_OFF
64  | ClimateEntityFeature.TURN_ON
65  )
66  _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
67  _attr_precision = PRECISION_TENTHS
68 
69  # This disables the warning for async_turn_{on,off}, can be removed later.
70  _enable_turn_on_off_backwards_compatibility = False
71 
72  def __init__(
73  self,
74  device: Device,
75  coordinator: TPLinkDataUpdateCoordinator,
76  *,
77  parent: Device,
78  ) -> None:
79  """Initialize the climate entity."""
80  self._state_feature_state_feature = device.features["state"]
81  self._mode_feature_mode_feature = device.features["thermostat_mode"]
82  self._temp_feature_temp_feature = device.features["temperature"]
83  self._target_feature_target_feature = device.features["target_temperature"]
84 
85  self._attr_min_temp_attr_min_temp = self._target_feature_target_feature.minimum_value
86  self._attr_max_temp_attr_max_temp = self._target_feature_target_feature.maximum_value
87  self._attr_temperature_unit_attr_temperature_unit = UNIT_MAPPING[cast(str, self._temp_feature_temp_feature.unit)]
88 
89  super().__init__(device, coordinator, parent=parent)
90 
91  @async_refresh_after
92  async def async_set_temperature(self, **kwargs: Any) -> None:
93  """Set target temperature."""
94  await self._target_feature_target_feature.set_value(int(kwargs[ATTR_TEMPERATURE]))
95 
96  @async_refresh_after
97  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
98  """Set hvac mode (heat/off)."""
99  if hvac_mode is HVACMode.HEAT:
100  await self._state_feature_state_feature.set_value(True)
101  elif hvac_mode is HVACMode.OFF:
102  await self._state_feature_state_feature.set_value(False)
103  else:
104  raise ServiceValidationError(f"Tried to set unsupported mode: {hvac_mode}")
105 
106  @async_refresh_after
107  async def async_turn_on(self) -> None:
108  """Turn heating on."""
109  await self._state_feature_state_feature.set_value(True)
110 
111  @async_refresh_after
112  async def async_turn_off(self) -> None:
113  """Turn heating off."""
114  await self._state_feature_state_feature.set_value(False)
115 
116  @callback
117  def _async_update_attrs(self) -> None:
118  """Update the entity's attributes."""
119  self._attr_current_temperature_attr_current_temperature = cast(float | None, self._temp_feature_temp_feature.value)
120  self._attr_target_temperature_attr_target_temperature = cast(float | None, self._target_feature_target_feature.value)
121 
122  self._attr_hvac_mode_attr_hvac_mode = (
123  HVACMode.HEAT if self._state_feature_state_feature.value else HVACMode.OFF
124  )
125 
126  if (
127  self._mode_feature_mode_feature.value not in STATE_TO_ACTION
128  and self._attr_hvac_action_attr_hvac_action is not HVACAction.OFF
129  ):
130  _LOGGER.warning(
131  "Unknown thermostat state, defaulting to OFF: %s",
132  self._mode_feature_mode_feature.value,
133  )
134  self._attr_hvac_action_attr_hvac_action = HVACAction.OFF
135  return
136 
137  self._attr_hvac_action_attr_hvac_action = STATE_TO_ACTION[
138  cast(ThermostatState, self._mode_feature_mode_feature.value)
139  ]
140 
141  def _get_unique_id(self) -> str:
142  """Return unique id."""
143  return f"{self._device.device_id}_climate"