climate.py 15 KB

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