climate.py 16 KB

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