climate.py 15 KB

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