water_heater.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """
  2. Setup for different kinds of Tuya water heater devices
  3. """
  4. import logging
  5. from homeassistant.components.water_heater import (
  6. ATTR_AWAY_MODE,
  7. ATTR_CURRENT_TEMPERATURE,
  8. ATTR_OPERATION_MODE,
  9. WaterHeaterEntity,
  10. WaterHeaterEntityFeature,
  11. )
  12. from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
  13. from .device import TuyaLocalDevice
  14. from .entity import TuyaLocalEntity, unit_from_ascii
  15. from .helpers.config import async_tuya_setup_platform
  16. from .helpers.device_config import TuyaEntityConfig
  17. _LOGGER = logging.getLogger(__name__)
  18. async def async_setup_entry(hass, config_entry, async_add_entities):
  19. config = {**config_entry.data, **config_entry.options}
  20. await async_tuya_setup_platform(
  21. hass,
  22. async_add_entities,
  23. config,
  24. "water_heater",
  25. TuyaLocalWaterHeater,
  26. )
  27. def validate_temp_unit(unit):
  28. translated_unit = unit_from_ascii(unit)
  29. try:
  30. return UnitOfTemperature(translated_unit)
  31. except ValueError:
  32. _LOGGER.warning("%s is not a valid temperature unit", unit)
  33. class TuyaLocalWaterHeater(TuyaLocalEntity, WaterHeaterEntity):
  34. """Representation of a Tuya water heater entity."""
  35. def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
  36. """
  37. Initialise the water heater device.
  38. Args:
  39. device (TuyaLocalDevice): The device API instance.
  40. config (TuyaEntityConfig): The entity config.
  41. """
  42. super().__init__()
  43. dps_map = self._init_begin(device, config)
  44. self._current_temperature_dps = dps_map.pop(
  45. ATTR_CURRENT_TEMPERATURE,
  46. None,
  47. )
  48. self._temperature_dps = dps_map.pop(ATTR_TEMPERATURE, None)
  49. self._unit_dps = dps_map.pop("temperature_unit", None)
  50. self._mintemp_dps = dps_map.pop("min_temperature", None)
  51. self._maxtemp_dps = dps_map.pop("max_temperature", None)
  52. self._operation_mode_dps = dps_map.pop(ATTR_OPERATION_MODE, None)
  53. self._away_mode_dps = dps_map.pop(ATTR_AWAY_MODE, None)
  54. self._init_end(dps_map)
  55. self._support_flags = WaterHeaterEntityFeature(0)
  56. if self._operation_mode_dps:
  57. self._support_flags |= WaterHeaterEntityFeature.OPERATION_MODE
  58. if self._operation_mode_dps.type is bool:
  59. self._support_flags |= WaterHeaterEntityFeature.ON_OFF
  60. if "away" in self._operation_mode_dps.values(device):
  61. self._support_flags |= WaterHeaterEntityFeature.AWAY_MODE
  62. if self._temperature_dps and not self._temperature_dps.readonly:
  63. self._support_flags |= WaterHeaterEntityFeature.TARGET_TEMPERATURE
  64. if self._away_mode_dps:
  65. self._support_flags |= WaterHeaterEntityFeature.AWAY_MODE
  66. @property
  67. def supported_features(self):
  68. """Return the features supported by this climate device."""
  69. return self._support_flags
  70. @property
  71. def temperature_unit(self):
  72. """Return the unit of measurement."""
  73. # If there is a separate DPS that returns the units, use that
  74. if self._unit_dps:
  75. unit = validate_temp_unit(self._unit_dps.get_value(self._device))
  76. # Only return valid units
  77. if unit:
  78. return unit
  79. # If there unit attribute configured in the temperature dps, use that
  80. if self._temperature_dps and self._temperature_dps.unit:
  81. unit = validate_temp_unit(self._temperature_dps.unit)
  82. if unit:
  83. return unit
  84. if self._current_temperature_dps and self._current_temperature_dps.unit:
  85. unit = validate_temp_unit(self._current_temperature_dps.unit)
  86. if unit:
  87. return unit
  88. # Return the default unit
  89. return UnitOfTemperature.CELSIUS
  90. @property
  91. def precision(self):
  92. """Return the precision of the temperature setting."""
  93. # unlike sensor, this is a decimal of the smallest unit that can be
  94. # represented, not a number of decimal places.
  95. if self._temperature_dps is None:
  96. return None
  97. return 1.0 / max(
  98. self._temperature_dps.scale(self._device),
  99. (
  100. self._current_temperature_dps.scale(self._device)
  101. if self._current_temperature_dps
  102. else 1.0
  103. ),
  104. )
  105. @property
  106. def current_operation(self):
  107. """Return current operation ie. eco, electric, performance, ..."""
  108. if self._operation_mode_dps is None:
  109. return None
  110. return self._operation_mode_dps.get_value(self._device)
  111. @property
  112. def operation_list(self):
  113. """Return the list of available operation modes."""
  114. if self._operation_mode_dps is None:
  115. return []
  116. else:
  117. return self._operation_mode_dps.values(self._device)
  118. @property
  119. def is_away_mode_on(self):
  120. if self._away_mode_dps:
  121. return self._away_mode_dps.get_value(self._device)
  122. elif self._operation_mode_dps and (
  123. "away" in self._operation_mode_dps.values(self._device)
  124. ):
  125. return self.current_operation == "away"
  126. @property
  127. def current_temperature(self):
  128. """Return the current temperature."""
  129. if self._current_temperature_dps:
  130. return self._current_temperature_dps.get_value(self._device)
  131. @property
  132. def target_temperature(self):
  133. """Return the temperature we try to reach."""
  134. if self._temperature_dps is None:
  135. return None
  136. return self._temperature_dps.get_value(self._device)
  137. @property
  138. def target_temperature_step(self):
  139. """Return the supported step of target temperature."""
  140. dps = self._temperature_dps
  141. if dps is None:
  142. return 1
  143. return dps.step(self._device)
  144. async def async_set_temperature(self, **kwargs):
  145. """Set the target temperature of the water heater."""
  146. if kwargs.get(ATTR_OPERATION_MODE) is not None:
  147. if self._operation_mode_dps is None:
  148. raise NotImplementedError()
  149. await self.async_set_operation_mode(
  150. kwargs.get(ATTR_OPERATION_MODE),
  151. )
  152. if kwargs.get(ATTR_TEMPERATURE) is not None:
  153. if self._temperature_dps is None:
  154. raise NotImplementedError()
  155. await self._temperature_dps.async_set_value(
  156. self._device, kwargs.get(ATTR_TEMPERATURE)
  157. )
  158. async def async_set_operation_mode(self, operation_mode):
  159. """Set new target operation mode."""
  160. if self._operation_mode_dps is None:
  161. raise NotImplementedError()
  162. await self._operation_mode_dps.async_set_value(
  163. self._device,
  164. operation_mode,
  165. )
  166. async def async_turn_away_mode_on(self):
  167. """Turn away mode on"""
  168. if self._away_mode_dps:
  169. await self._away_mode_dps.async_set_value(self._device, True)
  170. elif self._operation_mode_dps and (
  171. "away" in self._operation_mode_dps.values(self._device)
  172. ):
  173. await self.async_set_operation_mode("away")
  174. else:
  175. raise NotImplementedError()
  176. async def async_turn_away_mode_off(self):
  177. """Turn away mode off"""
  178. if self._away_mode_dps:
  179. await self._away_mode_dps.async_set_value(self._device, False)
  180. elif self._operation_mode_dps and (
  181. "away" in self._operation_mode_dps.values(self._device)
  182. ):
  183. # switch to the default mode
  184. await self.async_set_operation_mode(
  185. self._operation_mode_dps.default,
  186. )
  187. else:
  188. raise NotImplementedError()
  189. @property
  190. def min_temp(self):
  191. """Return the minimum supported target temperature."""
  192. # if a separate min_temperature dps is specified, the device tells us.
  193. if self._mintemp_dps is not None:
  194. return self._mintemp_dps.get_value(self._device)
  195. if self._temperature_dps:
  196. r = self._temperature_dps.range(self._device)
  197. return r[0]
  198. @property
  199. def max_temp(self):
  200. """Return the maximum supported target temperature."""
  201. # if a separate max_temperature dps is specified, the device tells us.
  202. if self._maxtemp_dps is not None:
  203. return self._maxtemp_dps.get_value(self._device)
  204. if self._temperature_dps:
  205. r = self._temperature_dps.range(self._device)
  206. return r[1]
  207. async def async_turn_on(self):
  208. """
  209. Turn on the water heater. Works only if operation_mode is a
  210. boolean dp.
  211. """
  212. if self._operation_mode_dps and self._operation_mode_dps.type is bool:
  213. await self._device.async_set_property(
  214. self._operation_mode_dps.id,
  215. True,
  216. )
  217. async def async_turn_off(self):
  218. """
  219. Turn off the water heater. Works only if operation_mode is a
  220. boolean dp.
  221. """
  222. if self._operation_mode_dps and self._operation_mode_dps.type is bool:
  223. await self._device.async_set_property(
  224. self._operation_mode_dps.id,
  225. False,
  226. )