climate.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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. HVAC_MODE_OFF,
  24. SUPPORT_AUX_HEAT,
  25. SUPPORT_FAN_MODE,
  26. SUPPORT_PRESET_MODE,
  27. SUPPORT_SWING_MODE,
  28. SUPPORT_TARGET_HUMIDITY,
  29. SUPPORT_TARGET_TEMPERATURE,
  30. SUPPORT_TARGET_TEMPERATURE_RANGE,
  31. )
  32. from homeassistant.const import (
  33. ATTR_TEMPERATURE,
  34. STATE_UNAVAILABLE,
  35. TEMP_CELSIUS,
  36. TEMP_FAHRENHEIT,
  37. TEMP_KELVIN,
  38. )
  39. from ..device import TuyaLocalDevice
  40. from ..helpers.device_config import TuyaEntityConfig
  41. _LOGGER = logging.getLogger(__name__)
  42. class TuyaLocalClimate(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. self._device = device
  52. self._config = config
  53. self._support_flags = 0
  54. self._attr_dps = []
  55. dps_map = {c.name: c for c in config.dps()}
  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. for d in dps_map.values():
  70. if not d.hidden:
  71. self._attr_dps.append(d)
  72. if self._aux_heat_dps:
  73. self._support_flags |= SUPPORT_AUX_HEAT
  74. if self._fan_mode_dps:
  75. self._support_flags |= SUPPORT_FAN_MODE
  76. if self._humidity_dps:
  77. self._support_flags |= SUPPORT_TARGET_HUMIDITY
  78. if self._preset_mode_dps:
  79. self._support_flags |= SUPPORT_PRESET_MODE
  80. if self._swing_mode_dps:
  81. self._support_flags |= SUPPORT_SWING_MODE
  82. if self._temp_high_dps and self._temp_low_dps:
  83. self._support_flags |= SUPPORT_TARGET_TEMPERATURE_RANGE
  84. elif self._temperature_dps is not None:
  85. self._support_flags |= SUPPORT_TARGET_TEMPERATURE
  86. @property
  87. def supported_features(self):
  88. """Return the features supported by this climate device."""
  89. return self._support_flags
  90. @property
  91. def should_poll(self):
  92. """Return the polling state."""
  93. return True
  94. @property
  95. def name(self):
  96. """Return the name of the climate device."""
  97. return self._device.name
  98. @property
  99. def friendly_name(self):
  100. """Return the friendly name of the climate entity for the UI."""
  101. return self._config.name
  102. @property
  103. def unique_id(self):
  104. """Return the unique id for this climate device."""
  105. return self._device.unique_id
  106. @property
  107. def device_info(self):
  108. """Return device information about this heater."""
  109. return self._device.device_info
  110. @property
  111. def icon(self):
  112. """Return the icon to use in the frontend for this device."""
  113. icon = self._config.icon(self._device)
  114. if icon:
  115. return icon
  116. else:
  117. return super().icon
  118. @property
  119. def temperature_unit(self):
  120. """Return the unit of measurement."""
  121. # If there is a separate DPS that returns the units, use that
  122. if self._unit_dps is not None:
  123. unit = self._unit_dps.get_value(self._device)
  124. # Only return valid units
  125. if unit == "C":
  126. return TEMP_CELSIUS
  127. elif unit == "F":
  128. return TEMP_FAHRENHEIT
  129. elif unit == "K":
  130. return TEMP_KELVIN
  131. # If there unit attribute configured in the temperature dps, use that
  132. if self._temperature_dps:
  133. unit = self._temperature_dps.unit
  134. # Only return valid units
  135. if unit == "C":
  136. return TEMP_CELSIUS
  137. elif unit == "F":
  138. return TEMP_FAHRENHEIT
  139. elif unit == "K":
  140. return TEMP_KELVIN
  141. # Return the default unit from the device
  142. return self._device.temperature_unit
  143. @property
  144. def target_temperature(self):
  145. """Return the currently set target temperature."""
  146. if self._temperature_dps is None:
  147. raise NotImplementedError()
  148. return self._temperature_dps.get_value(self._device)
  149. @property
  150. def target_temperature_high(self):
  151. """Return the currently set high target temperature."""
  152. if self._temp_high_dps is None:
  153. raise NotImplementedError()
  154. return self._temp_high_dps.get_value(self._device)
  155. @property
  156. def target_temperature_low(self):
  157. """Return the currently set low target temperature."""
  158. if self._temp_low_dps is None:
  159. raise NotImplementedError()
  160. return self._temp_low_dps.get_value(self._device)
  161. @property
  162. def target_temperature_step(self):
  163. """Return the supported step of target temperature."""
  164. dps = self._temperature_dps
  165. if dps is None:
  166. dps = self._temp_high_dps
  167. if dps is None:
  168. dps = self._temp_low_dps
  169. if dps is None:
  170. return 1
  171. return dps.step(self._device)
  172. @property
  173. def min_temp(self):
  174. """Return the minimum supported target temperature."""
  175. if self._temperature_dps is None:
  176. if self._temp_low_dps is None:
  177. return None
  178. r = self._temp_low_dps.range(self._device)
  179. else:
  180. r = self._temperature_dps.range(self._device)
  181. return DEFAULT_MIN_TEMP if r is None else r["min"]
  182. @property
  183. def max_temp(self):
  184. """Return the maximum supported target temperature."""
  185. if self._temperature_dps is None:
  186. if self._temp_high_dps is None:
  187. return None
  188. r = self._temp_high_dps.range(self._device)
  189. else:
  190. r = self._temperature_dps.range(self._device)
  191. return DEFAULT_MAX_TEMP if r is None else r["max"]
  192. async def async_set_temperature(self, **kwargs):
  193. """Set new target temperature."""
  194. if kwargs.get(ATTR_PRESET_MODE) is not None:
  195. await self.async_set_preset_mode(kwargs.get(ATTR_PRESET_MODE))
  196. if kwargs.get(ATTR_TEMPERATURE) is not None:
  197. await self.async_set_target_temperature(kwargs.get(ATTR_TEMPERATURE))
  198. high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
  199. low = kwargs.get(ATTR_TARGET_TEMP_LOW)
  200. if high is not None or low is not None:
  201. await self.async_set_target_temperature_range(low, high)
  202. async def async_set_target_temperature(self, target_temperature):
  203. if self._temperature_dps is None:
  204. raise NotImplementedError()
  205. await self._temperature_dps.async_set_value(self._device, target_temperature)
  206. async def async_set_target_temperature_range(self, low, high):
  207. """Set the target temperature range."""
  208. dps_map = {}
  209. if low is not None and self._temp_low_dps is not None:
  210. dps_map.update(self._temp_low_dps.get_values_to_set(self._device, low))
  211. if high is not None and self._temp_high_dps is not None:
  212. dps_map.update(self._temp_high_dps.get_values_to_set(self._device, high))
  213. if dps_map:
  214. await self._device.async_set_properties(dps_map)
  215. @property
  216. def current_temperature(self):
  217. """Return the current measured temperature."""
  218. if self._current_temperature_dps is None:
  219. return None
  220. return self._current_temperature_dps.get_value(self._device)
  221. @property
  222. def target_humidity(self):
  223. """Return the currently set target humidity."""
  224. if self._humidity_dps is None:
  225. raise NotImplementedError()
  226. return self._humidity_dps.get_value(self._device)
  227. @property
  228. def min_humidity(self):
  229. """Return the minimum supported target humidity."""
  230. if self._humidity_dps is None:
  231. return None
  232. r = self._humidity_dps.range(self._device)
  233. return DEFAULT_MIN_HUMIDITY if r is None else r["min"]
  234. @property
  235. def max_humidity(self):
  236. """Return the maximum supported target humidity."""
  237. if self._humidity_dps is None:
  238. return None
  239. r = self._humidity_dps.range(self._device)
  240. return DEFAULT_MAX_HUMIDITY if r is None else r["max"]
  241. async def async_set_humidity(self, humidity: int):
  242. if self._humidity_dps is None:
  243. raise NotImplementedError()
  244. await self._humidity_dps.async_set_value(self._device, humidity)
  245. @property
  246. def current_humidity(self):
  247. """Return the current measured humidity."""
  248. if self._current_humidity_dps is None:
  249. return None
  250. return self._current_humidity_dps.get_value(self._device)
  251. @property
  252. def hvac_action(self):
  253. """Return the current HVAC action."""
  254. if self._hvac_action_dps is None:
  255. return None
  256. return self._hvac_action_dps.get_value(self._device)
  257. @property
  258. def hvac_mode(self):
  259. """Return current HVAC mode."""
  260. if self._hvac_mode_dps is None:
  261. return HVAC_MODE_AUTO
  262. hvac_mode = self._hvac_mode_dps.get_value(self._device)
  263. return STATE_UNAVAILABLE if hvac_mode is None else hvac_mode
  264. @property
  265. def hvac_modes(self):
  266. """Return available HVAC modes."""
  267. if self._hvac_mode_dps is None:
  268. return []
  269. else:
  270. return self._hvac_mode_dps.values(self._device)
  271. async def async_set_hvac_mode(self, hvac_mode):
  272. """Set new HVAC mode."""
  273. if self._hvac_mode_dps is None:
  274. raise NotImplementedError()
  275. await self._hvac_mode_dps.async_set_value(self._device, hvac_mode)
  276. @property
  277. def is_aux_heat(self):
  278. """Return state of aux heater"""
  279. if self._aux_heat_dps is None:
  280. return None
  281. else:
  282. return self._aux_heat_dps.get_value(self._device)
  283. async def async_turn_aux_heat_on(self):
  284. """Turn on aux heater."""
  285. if self._aux_heat_dps is None:
  286. raise NotImplementedError()
  287. await self._aux_heat_dps.async_set_value(self._device, True)
  288. async def async_turn_aux_heat_off(self):
  289. """Turn off aux heater."""
  290. if self._aux_heat_dps is None:
  291. raise NotImplementedError()
  292. await self._aux_heat_dps.async_set_value(self._device, False)
  293. @property
  294. def preset_mode(self):
  295. """Return the current preset mode."""
  296. if self._preset_mode_dps is None:
  297. raise NotImplementedError()
  298. return self._preset_mode_dps.get_value(self._device)
  299. @property
  300. def preset_modes(self):
  301. """Return the list of presets that this device supports."""
  302. if self._preset_mode_dps is None:
  303. return None
  304. return self._preset_mode_dps.values(self._device)
  305. async def async_set_preset_mode(self, preset_mode):
  306. """Set the preset mode."""
  307. if self._preset_mode_dps is None:
  308. raise NotImplementedError()
  309. await self._preset_mode_dps.async_set_value(self._device, preset_mode)
  310. @property
  311. def swing_mode(self):
  312. """Return the current swing mode."""
  313. if self._swing_mode_dps is None:
  314. raise NotImplementedError()
  315. return self._swing_mode_dps.get_value(self._device)
  316. @property
  317. def swing_modes(self):
  318. """Return the list of swing modes that this device supports."""
  319. if self._swing_mode_dps is None:
  320. return None
  321. return self._swing_mode_dps.values(self._device)
  322. async def async_set_swing_mode(self, swing_mode):
  323. """Set the preset mode."""
  324. if self._swing_mode_dps is None:
  325. raise NotImplementedError()
  326. await self._swing_mode_dps.async_set_value(self._device, swing_mode)
  327. @property
  328. def fan_mode(self):
  329. """Return the current fan mode."""
  330. if self._fan_mode_dps is None:
  331. raise NotImplementedError()
  332. return self._fan_mode_dps.get_value(self._device)
  333. @property
  334. def fan_modes(self):
  335. """Return the list of fan modes that this device supports."""
  336. if self._fan_mode_dps is None:
  337. return None
  338. return self._fan_mode_dps.values(self._device)
  339. async def async_set_fan_mode(self, fan_mode):
  340. """Set the fan mode."""
  341. if self._fan_mode_dps is None:
  342. raise NotImplementedError()
  343. await self._fan_mode_dps.async_set_value(self._device, fan_mode)
  344. @property
  345. def device_state_attributes(self):
  346. """Get additional attributes that the integration itself does not support."""
  347. attr = {}
  348. for a in self._attr_dps:
  349. attr[a.name] = a.get_value(self._device)
  350. return attr
  351. async def async_update(self):
  352. await self._device.async_refresh()