base_device_tests.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. from unittest import IsolatedAsyncioTestCase
  2. from unittest.mock import AsyncMock, patch, PropertyMock
  3. from uuid import uuid4
  4. from homeassistant.components.light import COLOR_MODE_ONOFF
  5. from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
  6. from homeassistant.components.switch import DEVICE_CLASS_SWITCH
  7. from homeassistant.const import STATE_UNAVAILABLE
  8. from custom_components.tuya_local.generic.binary_sensor import TuyaLocalBinarySensor
  9. from custom_components.tuya_local.generic.climate import TuyaLocalClimate
  10. from custom_components.tuya_local.generic.fan import TuyaLocalFan
  11. from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
  12. from custom_components.tuya_local.generic.light import TuyaLocalLight
  13. from custom_components.tuya_local.generic.lock import TuyaLocalLock
  14. from custom_components.tuya_local.generic.number import TuyaLocalNumber
  15. from custom_components.tuya_local.generic.select import TuyaLocalSelect
  16. from custom_components.tuya_local.generic.sensor import TuyaLocalSensor
  17. from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
  18. from custom_components.tuya_local.helpers.device_config import (
  19. TuyaDeviceConfig,
  20. possible_matches,
  21. )
  22. from ..helpers import assert_device_properties_set
  23. DEVICE_TYPES = {
  24. "binary_sensor": TuyaLocalBinarySensor,
  25. "climate": TuyaLocalClimate,
  26. "fan": TuyaLocalFan,
  27. "humidifier": TuyaLocalHumidifier,
  28. "light": TuyaLocalLight,
  29. "lock": TuyaLocalLock,
  30. "number": TuyaLocalNumber,
  31. "switch": TuyaLocalSwitch,
  32. "select": TuyaLocalSelect,
  33. "sensor": TuyaLocalSensor,
  34. }
  35. class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
  36. __test__ = False
  37. def setUpForConfig(self, config_file, payload):
  38. """Perform setup tasks for every test."""
  39. device_patcher = patch("custom_components.tuya_local.device.TuyaLocalDevice")
  40. self.addCleanup(device_patcher.stop)
  41. self.mock_device = device_patcher.start()
  42. self.dps = payload.copy()
  43. self.mock_device.get_property.side_effect = lambda id: self.dps[id]
  44. cfg = TuyaDeviceConfig(config_file)
  45. self.conf_type = cfg.legacy_type
  46. type(self.mock_device).has_returned_state = PropertyMock(return_value=True)
  47. type(self.mock_device).unique_id = PropertyMock(return_value=str(uuid4()))
  48. self.mock_device.name = cfg.name
  49. self.entities = {}
  50. self.primary_entity = cfg.primary_entity.config_id
  51. self.entities[self.primary_entity] = self.create_entity(cfg.primary_entity)
  52. self.names = {}
  53. self.names[cfg.primary_entity.config_id] = cfg.primary_entity.name(cfg.name)
  54. for e in cfg.secondary_entities():
  55. self.entities[e.config_id] = self.create_entity(e)
  56. self.names[e.config_id] = e.name(cfg.name)
  57. def create_entity(self, config):
  58. """Create an entity to match the config"""
  59. dev_type = DEVICE_TYPES[config.entity]
  60. if dev_type:
  61. return dev_type(self.mock_device, config)
  62. def test_config_matched(self):
  63. for cfg in possible_matches(self.dps):
  64. if cfg.legacy_type == self.conf_type:
  65. self.assertEqual(cfg.match_quality(self.dps), 100.0)
  66. return
  67. self.fail()
  68. def test_should_poll(self):
  69. for e in self.entities.values():
  70. self.assertTrue(e.should_poll)
  71. def test_available(self):
  72. for e in self.entities.values():
  73. self.assertTrue(e.available)
  74. def test_entity_category(self):
  75. for k in self.entities:
  76. e = self.entities[k]
  77. if k == self.primary_entity:
  78. self.assertIsNone(e.entity_category)
  79. elif type(e) in [TuyaLocalBinarySensor, TuyaLocalSensor]:
  80. self.assertEqual(e.entity_category, "diagnostic")
  81. else:
  82. self.assertEqual(e.entity_category, "config")
  83. def test_name_returns_device_name(self):
  84. for e in self.entities:
  85. self.assertEqual(self.entities[e].name, self.names[e])
  86. def test_unique_id_contains_device_unique_id(self):
  87. entities = {}
  88. for e in self.entities.values():
  89. self.assertIn(self.mock_device.unique_id, e.unique_id)
  90. if type(e) not in entities:
  91. entities[type(e)] = []
  92. entities[type(e)].append(e.unique_id)
  93. for e in entities.values():
  94. self.assertCountEqual(e, set(e))
  95. def test_device_info_returns_device_info_from_device(self):
  96. for e in self.entities.values():
  97. self.assertEqual(e.device_info, self.mock_device.device_info)
  98. async def test_update(self):
  99. for e in self.entities.values():
  100. result = AsyncMock()
  101. self.mock_device.async_refresh.return_value = result()
  102. self.mock_device.async_refresh.reset_mock()
  103. await e.async_update()
  104. self.mock_device.async_refresh.assert_called_once()
  105. result.assert_awaited()
  106. # Mixins for common test functions
  107. class SwitchableTests:
  108. def setUpSwitchable(self, dps, subject):
  109. self.switch_dps = dps
  110. self.switch_subject = subject
  111. def test_switchable_is_on(self):
  112. self.dps[self.switch_dps] = True
  113. self.assertTrue(self.switch_subject.is_on)
  114. self.dps[self.switch_dps] = False
  115. self.assertFalse(self.switch_subject.is_on)
  116. self.dps[self.switch_dps] = None
  117. self.assertIsNone(self.switch_subject.is_on)
  118. async def test_switchable_turn_on(self):
  119. async with assert_device_properties_set(
  120. self.switch_subject._device, {self.switch_dps: True}
  121. ):
  122. await self.switch_subject.async_turn_on()
  123. async def test_switchable_turn_off(self):
  124. async with assert_device_properties_set(
  125. self.switch_subject._device, {self.switch_dps: False}
  126. ):
  127. await self.switch_subject.async_turn_off()
  128. async def test_switchable_toggle(self):
  129. self.dps[self.switch_dps] = False
  130. async with assert_device_properties_set(
  131. self.switch_subject._device, {self.switch_dps: True}
  132. ):
  133. await self.switch_subject.async_toggle()
  134. self.dps[self.switch_dps] = True
  135. async with assert_device_properties_set(
  136. self.switch_subject._device, {self.switch_dps: False}
  137. ):
  138. await self.switch_subject.async_toggle()
  139. class BasicLightTests:
  140. def setUpBasicLight(self, dps, subject):
  141. self.basicLight = subject
  142. self.basicLightDps = dps
  143. def test_basic_light_supported_features(self):
  144. self.assertEqual(self.basicLight.supported_features, 0)
  145. def test_basic_light_supported_color_modes(self):
  146. self.assertCountEqual(
  147. self.basicLight.supported_color_modes,
  148. [COLOR_MODE_ONOFF],
  149. )
  150. def test_basic_light_color_mode(self):
  151. self.assertEqual(self.basicLight.color_mode, COLOR_MODE_ONOFF)
  152. def test_light_has_no_brightness(self):
  153. self.assertIsNone(self.basicLight.brightness)
  154. def test_light_has_no_effects(self):
  155. self.assertIsNone(self.basicLight.effect_list)
  156. self.assertIsNone(self.basicLight.effect)
  157. def test_basic_light_is_on(self):
  158. self.dps[self.basicLightDps] = True
  159. self.assertTrue(self.basicLight.is_on)
  160. self.dps[self.basicLightDps] = False
  161. self.assertFalse(self.basicLight.is_on)
  162. async def test_basic_light_turn_on(self):
  163. async with assert_device_properties_set(
  164. self.basicLight._device, {self.basicLightDps: True}
  165. ):
  166. await self.basicLight.async_turn_on()
  167. async def test_basic_light_turn_off(self):
  168. async with assert_device_properties_set(
  169. self.basicLight._device, {self.basicLightDps: False}
  170. ):
  171. await self.basicLight.async_turn_off()
  172. async def test_basic_light_toggle_turns_on_when_it_was_off(self):
  173. self.dps[self.basicLightDps] = False
  174. async with assert_device_properties_set(
  175. self.basicLight._device,
  176. {self.basicLightDps: True},
  177. ):
  178. await self.basicLight.async_toggle()
  179. async def test_basic_light_toggle_turns_off_when_it_was_on(self):
  180. self.dps[self.basicLightDps] = True
  181. async with assert_device_properties_set(
  182. self.basicLight._device,
  183. {self.basicLightDps: False},
  184. ):
  185. await self.basicLight.async_toggle()
  186. def test_basic_light_state_attributes(self):
  187. self.assertEqual(self.basicLight.device_state_attributes, {})
  188. class BasicLockTests:
  189. def setUpBasicLock(self, dps, subject):
  190. self.basicLock = subject
  191. self.basicLockDps = dps
  192. def test_basic_lock_state(self):
  193. self.dps[self.basicLockDps] = True
  194. self.assertEqual(self.basicLock.state, STATE_LOCKED)
  195. self.dps[self.basicLockDps] = False
  196. self.assertEqual(self.basicLock.state, STATE_UNLOCKED)
  197. self.dps[self.basicLockDps] = None
  198. self.assertEqual(self.basicLock.state, STATE_UNAVAILABLE)
  199. def test_basic_lock_is_locked(self):
  200. self.dps[self.basicLockDps] = True
  201. self.assertTrue(self.basicLock.is_locked)
  202. self.dps[self.basicLockDps] = False
  203. self.assertFalse(self.basicLock.is_locked)
  204. self.dps[self.basicLockDps] = None
  205. self.assertFalse(self.basicLock.is_locked)
  206. async def test_basic_lock_locks(self):
  207. async with assert_device_properties_set(
  208. self.basicLock._device,
  209. {self.basicLockDps: True},
  210. ):
  211. await self.basicLock.async_lock()
  212. async def test_basic_lock_unlocks(self):
  213. async with assert_device_properties_set(
  214. self.basicLock._device,
  215. {self.basicLockDps: False},
  216. ):
  217. await self.basicLock.async_unlock()
  218. def test_basic_lock_state_attributes(self):
  219. self.assertEqual(self.basicLock.device_state_attributes, {})
  220. class BasicSwitchTests:
  221. def setUpBasicSwitch(self, dps, subject):
  222. self.basicSwitch = subject
  223. self.basicSwitchDps = dps
  224. def test_basic_switch_is_on(self):
  225. self.dps[self.basicSwitchDps] = True
  226. self.assertEqual(self.basicSwitch.is_on, True)
  227. self.dps[self.basicSwitchDps] = False
  228. self.assertEqual(self.basicSwitch.is_on, False)
  229. async def test_basic_switch_turn_on(self):
  230. async with assert_device_properties_set(
  231. self.basicSwitch._device, {self.basicSwitchDps: True}
  232. ):
  233. await self.basicSwitch.async_turn_on()
  234. async def test_basic_switch_turn_off(self):
  235. async with assert_device_properties_set(
  236. self.basicSwitch._device, {self.basicSwitchDps: False}
  237. ):
  238. await self.basicSwitch.async_turn_off()
  239. async def test_basic_switch_toggle_turns_on_when_it_was_off(self):
  240. self.dps[self.basicSwitchDps] = False
  241. async with assert_device_properties_set(
  242. self.basicSwitch._device, {self.basicSwitchDps: True}
  243. ):
  244. await self.basicSwitch.async_toggle()
  245. async def test_basic_switch_toggle_turns_off_when_it_was_on(self):
  246. self.dps[self.basicSwitchDps] = True
  247. async with assert_device_properties_set(
  248. self.basicSwitch._device, {self.basicSwitchDps: False}
  249. ):
  250. await self.basicSwitch.async_toggle()
  251. def test_basic_switch_class_is_switch(self):
  252. self.assertEqual(self.basicSwitch.device_class, DEVICE_CLASS_SWITCH)
  253. def test_basic_switch_has_no_power_monitoring(self):
  254. self.assertIsNone(self.basicSwitch.current_power_w)
  255. def test_basic_switch_state_attributes(self):
  256. self.assertEqual(self.basicSwitch.device_state_attributes, {})