climate.py 13 KB

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