climate.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. """
  2. Platform to control tuya climate devices.
  3. """
  4. import logging
  5. from homeassistant.components.climate import (
  6. ClimateEntity,
  7. ClimateEntityFeature,
  8. HVACAction,
  9. HVACMode,
  10. )
  11. from homeassistant.components.climate.const import (
  12. ATTR_AUX_HEAT,
  13. ATTR_CURRENT_HUMIDITY,
  14. ATTR_CURRENT_TEMPERATURE,
  15. ATTR_FAN_MODE,
  16. ATTR_HUMIDITY,
  17. ATTR_HVAC_ACTION,
  18. ATTR_HVAC_MODE,
  19. ATTR_PRESET_MODE,
  20. ATTR_SWING_MODE,
  21. ATTR_TARGET_TEMP_HIGH,
  22. ATTR_TARGET_TEMP_LOW,
  23. DEFAULT_MAX_HUMIDITY,
  24. DEFAULT_MAX_TEMP,
  25. DEFAULT_MIN_HUMIDITY,
  26. DEFAULT_MIN_TEMP,
  27. )
  28. from homeassistant.const import (
  29. ATTR_TEMPERATURE,
  30. UnitOfTemperature,
  31. )
  32. from ..device import TuyaLocalDevice
  33. from ..helpers.device_config import TuyaEntityConfig
  34. from ..helpers.mixin import TuyaLocalEntity, unit_from_ascii
  35. _LOGGER = logging.getLogger(__name__)
  36. def validate_temp_unit(unit):
  37. unit = unit_from_ascii(unit)
  38. try:
  39. return UnitOfTemperature(unit)
  40. except ValueError:
  41. return None
  42. class TuyaLocalClimate(TuyaLocalEntity, ClimateEntity):
  43. """Representation of a Tuya Climate entity."""
  44. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  45. """
  46. Initialise the climate device.
  47. Args:
  48. device (TuyaLocalDevice): The device API instance.
  49. config (TuyaEntityConfig): The entity config.
  50. """
  51. dps_map = self._init_begin(device, config)
  52. self._aux_heat_dps = dps_map.pop(ATTR_AUX_HEAT, None)
  53. self._current_temperature_dps = dps_map.pop(ATTR_CURRENT_TEMPERATURE, None)
  54. self._current_humidity_dps = dps_map.pop(ATTR_CURRENT_HUMIDITY, None)
  55. self._fan_mode_dps = dps_map.pop(ATTR_FAN_MODE, None)
  56. self._humidity_dps = dps_map.pop(ATTR_HUMIDITY, None)
  57. self._hvac_mode_dps = dps_map.pop(ATTR_HVAC_MODE, None)
  58. self._hvac_action_dps = dps_map.pop(ATTR_HVAC_ACTION, None)
  59. self._preset_mode_dps = dps_map.pop(ATTR_PRESET_MODE, None)
  60. self._swing_mode_dps = dps_map.pop(ATTR_SWING_MODE, None)
  61. self._temperature_dps = dps_map.pop(ATTR_TEMPERATURE, None)
  62. self._temp_high_dps = dps_map.pop(ATTR_TARGET_TEMP_HIGH, None)
  63. self._temp_low_dps = dps_map.pop(ATTR_TARGET_TEMP_LOW, None)
  64. self._unit_dps = dps_map.pop("temperature_unit", None)
  65. self._mintemp_dps = dps_map.pop("min_temperature", None)
  66. self._maxtemp_dps = dps_map.pop("max_temperature", None)
  67. self._init_end(dps_map)
  68. self._support_flags = 0
  69. if self._aux_heat_dps:
  70. self._support_flags |= ClimateEntityFeature.AUX_HEAT
  71. if self._fan_mode_dps:
  72. self._support_flags |= ClimateEntityFeature.FAN_MODE
  73. if self._humidity_dps:
  74. self._support_flags |= ClimateEntityFeature.TARGET_HUMIDITY
  75. if self._preset_mode_dps:
  76. self._support_flags |= ClimateEntityFeature.PRESET_MODE
  77. if self._swing_mode_dps:
  78. self._support_flags |= ClimateEntityFeature.SWING_MODE
  79. if self._temp_high_dps and self._temp_low_dps:
  80. self._support_flags |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
  81. elif self._temperature_dps is not None:
  82. self._support_flags |= ClimateEntityFeature.TARGET_TEMPERATURE
  83. @property
  84. def supported_features(self):
  85. """Return the features supported by this climate device."""
  86. return self._support_flags
  87. @property
  88. def temperature_unit(self):
  89. """Return the unit of measurement."""
  90. # If there is a separate DPS that returns the units, use that
  91. if self._unit_dps is not None:
  92. unit = validate_temp_unit(self._unit_dps.get_value(self._device))
  93. # Only return valid units
  94. if unit is not None:
  95. return unit
  96. # If there unit attribute configured in the temperature dps, use that
  97. if self._temperature_dps:
  98. unit = validate_temp_unit(self._temperature_dps.unit)
  99. if unit is not None:
  100. return unit
  101. # Return the default unit from the device
  102. return self._device.temperature_unit
  103. @property
  104. def target_temperature(self):
  105. """Return the currently set target temperature."""
  106. if self._temperature_dps is None:
  107. raise NotImplementedError()
  108. return self._temperature_dps.get_value(self._device)
  109. @property
  110. def target_temperature_high(self):
  111. """Return the currently set high target temperature."""
  112. if self._temp_high_dps is None:
  113. raise NotImplementedError()
  114. return self._temp_high_dps.get_value(self._device)
  115. @property
  116. def target_temperature_low(self):
  117. """Return the currently set low target temperature."""
  118. if self._temp_low_dps is None:
  119. raise NotImplementedError()
  120. return self._temp_low_dps.get_value(self._device)
  121. @property
  122. def target_temperature_step(self):
  123. """Return the supported step of target temperature."""
  124. dps = self._temperature_dps
  125. if dps is None:
  126. dps = self._temp_high_dps
  127. if dps is None:
  128. dps = self._temp_low_dps
  129. if dps is None:
  130. return 1
  131. return dps.step(self._device)
  132. @property
  133. def min_temp(self):
  134. """Return the minimum supported target temperature."""
  135. # if a separate min_temperature dps is specified, the device tells us.
  136. if self._mintemp_dps is not None:
  137. return self._mintemp_dps.get_value(self._device)
  138. if self._temperature_dps is None:
  139. if self._temp_low_dps is None:
  140. return None
  141. r = self._temp_low_dps.range(self._device)
  142. else:
  143. r = self._temperature_dps.range(self._device)
  144. return DEFAULT_MIN_TEMP if r is None else r["min"]
  145. @property
  146. def max_temp(self):
  147. """Return the maximum supported target temperature."""
  148. # if a separate max_temperature dps is specified, the device tells us.
  149. if self._maxtemp_dps is not None:
  150. return self._maxtemp_dps.get_value(self._device)
  151. if self._temperature_dps is None:
  152. if self._temp_high_dps is None:
  153. return None
  154. r = self._temp_high_dps.range(self._device)
  155. else:
  156. r = self._temperature_dps.range(self._device)
  157. return DEFAULT_MAX_TEMP if r is None else r["max"]
  158. async def async_set_temperature(self, **kwargs):
  159. """Set new target temperature."""
  160. if kwargs.get(ATTR_PRESET_MODE) is not None:
  161. await self.async_set_preset_mode(kwargs.get(ATTR_PRESET_MODE))
  162. if kwargs.get(ATTR_TEMPERATURE) is not None:
  163. await self.async_set_target_temperature(kwargs.get(ATTR_TEMPERATURE))
  164. high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
  165. low = kwargs.get(ATTR_TARGET_TEMP_LOW)
  166. if high is not None or low is not None:
  167. await self.async_set_target_temperature_range(low, high)
  168. async def async_set_target_temperature(self, target_temperature):
  169. if self._temperature_dps is None:
  170. raise NotImplementedError()
  171. await self._temperature_dps.async_set_value(self._device, target_temperature)
  172. async def async_set_target_temperature_range(self, low, high):
  173. """Set the target temperature range."""
  174. dps_map = {}
  175. if low is not None and self._temp_low_dps is not None:
  176. dps_map.update(self._temp_low_dps.get_values_to_set(self._device, low))
  177. if high is not None and self._temp_high_dps is not None:
  178. dps_map.update(self._temp_high_dps.get_values_to_set(self._device, high))
  179. if dps_map:
  180. await self._device.async_set_properties(dps_map)
  181. @property
  182. def current_temperature(self):
  183. """Return the current measured temperature."""
  184. if self._current_temperature_dps is None:
  185. return None
  186. return self._current_temperature_dps.get_value(self._device)
  187. @property
  188. def target_humidity(self):
  189. """Return the currently set target humidity."""
  190. if self._humidity_dps is None:
  191. raise NotImplementedError()
  192. return self._humidity_dps.get_value(self._device)
  193. @property
  194. def min_humidity(self):
  195. """Return the minimum supported target humidity."""
  196. if self._humidity_dps is None:
  197. return None
  198. r = self._humidity_dps.range(self._device)
  199. return DEFAULT_MIN_HUMIDITY if r is None else r["min"]
  200. @property
  201. def max_humidity(self):
  202. """Return the maximum supported target humidity."""
  203. if self._humidity_dps is None:
  204. return None
  205. r = self._humidity_dps.range(self._device)
  206. return DEFAULT_MAX_HUMIDITY if r is None else r["max"]
  207. async def async_set_humidity(self, humidity: int):
  208. if self._humidity_dps is None:
  209. raise NotImplementedError()
  210. await self._humidity_dps.async_set_value(self._device, humidity)
  211. @property
  212. def current_humidity(self):
  213. """Return the current measured humidity."""
  214. if self._current_humidity_dps is None:
  215. return None
  216. return self._current_humidity_dps.get_value(self._device)
  217. @property
  218. def hvac_action(self):
  219. """Return the current HVAC action."""
  220. if self._hvac_action_dps is None:
  221. return None
  222. action = self._hvac_action_dps.get_value(self._device)
  223. try:
  224. return HVACAction(action)
  225. except ValueError:
  226. _LOGGER.warning(f"_Unrecognised HVAC Action {action} ignored")
  227. return None
  228. @property
  229. def hvac_mode(self):
  230. """Return current HVAC mode."""
  231. if self._hvac_mode_dps is None:
  232. return HVACMode.AUTO
  233. hvac_mode = self._hvac_mode_dps.get_value(self._device)
  234. try:
  235. return HVACMode(hvac_mode)
  236. except ValueError:
  237. _LOGGER.warning(f"Unrecognised HVAC Mode of {hvac_mode} ignored")
  238. return None
  239. @property
  240. def hvac_modes(self):
  241. """Return available HVAC modes."""
  242. if self._hvac_mode_dps is None:
  243. return []
  244. else:
  245. return self._hvac_mode_dps.values(self._device)
  246. async def async_set_hvac_mode(self, hvac_mode):
  247. """Set new HVAC mode."""
  248. if self._hvac_mode_dps is None:
  249. raise NotImplementedError()
  250. await self._hvac_mode_dps.async_set_value(self._device, hvac_mode)
  251. async def async_turn_on(self):
  252. """Turn on the climate device."""
  253. # Bypass the usual dps mapping to switch the power dp directly
  254. # this way the hvac_mode will be kept when toggling off and on.
  255. if self._hvac_mode_dps and self._hvac_mode_dps.type is bool:
  256. await self._device.async_set_property(self._hvac_mode_dps.id, True)
  257. else:
  258. await super().async_turn_on()
  259. async def async_turn_off(self):
  260. """Turn off the climate device."""
  261. # Bypass the usual dps mapping to switch the power dp directly
  262. # this way the hvac_mode will be kept when toggling off and on.
  263. if self._hvac_mode_dps and self._hvac_mode_dps.type is bool:
  264. await self._device.async_set_property(self._hvac_mode_dps.id, False)
  265. else:
  266. await super().async_turn_off()
  267. @property
  268. def is_aux_heat(self):
  269. """Return state of aux heater"""
  270. if self._aux_heat_dps is None:
  271. return None
  272. else:
  273. return self._aux_heat_dps.get_value(self._device)
  274. async def async_turn_aux_heat_on(self):
  275. """Turn on aux heater."""
  276. if self._aux_heat_dps is None:
  277. raise NotImplementedError()
  278. await self._aux_heat_dps.async_set_value(self._device, True)
  279. async def async_turn_aux_heat_off(self):
  280. """Turn off aux heater."""
  281. if self._aux_heat_dps is None:
  282. raise NotImplementedError()
  283. await self._aux_heat_dps.async_set_value(self._device, False)
  284. @property
  285. def preset_mode(self):
  286. """Return the current preset mode."""
  287. if self._preset_mode_dps is None:
  288. raise NotImplementedError()
  289. return self._preset_mode_dps.get_value(self._device)
  290. @property
  291. def preset_modes(self):
  292. """Return the list of presets that this device supports."""
  293. if self._preset_mode_dps is None:
  294. return None
  295. return self._preset_mode_dps.values(self._device)
  296. async def async_set_preset_mode(self, preset_mode):
  297. """Set the preset mode."""
  298. if self._preset_mode_dps is None:
  299. raise NotImplementedError()
  300. await self._preset_mode_dps.async_set_value(self._device, preset_mode)
  301. @property
  302. def swing_mode(self):
  303. """Return the current swing mode."""
  304. if self._swing_mode_dps is None:
  305. raise NotImplementedError()
  306. return self._swing_mode_dps.get_value(self._device)
  307. @property
  308. def swing_modes(self):
  309. """Return the list of swing modes that this device supports."""
  310. if self._swing_mode_dps is None:
  311. return None
  312. return self._swing_mode_dps.values(self._device)
  313. async def async_set_swing_mode(self, swing_mode):
  314. """Set the preset mode."""
  315. if self._swing_mode_dps is None:
  316. raise NotImplementedError()
  317. await self._swing_mode_dps.async_set_value(self._device, swing_mode)
  318. @property
  319. def fan_mode(self):
  320. """Return the current fan mode."""
  321. if self._fan_mode_dps is None:
  322. raise NotImplementedError()
  323. return self._fan_mode_dps.get_value(self._device)
  324. @property
  325. def fan_modes(self):
  326. """Return the list of fan modes that this device supports."""
  327. if self._fan_mode_dps is None:
  328. return None
  329. return self._fan_mode_dps.values(self._device)
  330. async def async_set_fan_mode(self, fan_mode):
  331. """Set the fan mode."""
  332. if self._fan_mode_dps is None:
  333. raise NotImplementedError()
  334. await self._fan_mode_dps.async_set_value(self._device, fan_mode)