test_datetime.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. """Tests for the datetime entity."""
  2. from datetime import datetime, timezone
  3. from unittest.mock import AsyncMock, MagicMock, Mock
  4. import pytest
  5. from pytest_homeassistant_custom_component.common import MockConfigEntry
  6. from custom_components.tuya_local.const import (
  7. CONF_DEVICE_ID,
  8. CONF_PROTOCOL_VERSION,
  9. CONF_TYPE,
  10. DOMAIN,
  11. )
  12. from custom_components.tuya_local.datetime import TuyaLocalDateTime, async_setup_entry
  13. @pytest.mark.asyncio
  14. async def test_init_entry(hass):
  15. """Test the initialisation."""
  16. entry = MockConfigEntry(
  17. domain=DOMAIN,
  18. data={
  19. CONF_TYPE: "elko_cfmtb_thermostat",
  20. CONF_DEVICE_ID: "dummy",
  21. CONF_PROTOCOL_VERSION: "auto",
  22. },
  23. )
  24. m_add_entities = Mock()
  25. m_device = AsyncMock()
  26. hass.data[DOMAIN] = {
  27. "dummy": {"device": m_device},
  28. }
  29. await async_setup_entry(hass, entry, m_add_entities)
  30. assert (
  31. type(hass.data[DOMAIN]["dummy"]["datetime_override_end"]) is TuyaLocalDateTime
  32. )
  33. m_add_entities.assert_called()
  34. @pytest.mark.asyncio
  35. async def test_init_entry_fails_if_device_has_no_time(hass):
  36. """Test initialisation when device has no matching entity"""
  37. entry = MockConfigEntry(
  38. domain=DOMAIN,
  39. data={
  40. CONF_TYPE: "simple_switch",
  41. CONF_DEVICE_ID: "dummy",
  42. CONF_PROTOCOL_VERSION: "auto",
  43. },
  44. )
  45. m_add_entities = Mock()
  46. m_device = AsyncMock()
  47. hass.data[DOMAIN] = {
  48. "dummy": {"device": m_device},
  49. }
  50. try:
  51. await async_setup_entry(hass, entry, m_add_entities)
  52. assert False
  53. except ValueError:
  54. pass
  55. m_add_entities.assert_not_called()
  56. @pytest.mark.asyncio
  57. async def test_init_entry_fails_if_config_is_missing(hass):
  58. """Test initialisation when device has no matching entity"""
  59. entry = MockConfigEntry(
  60. domain=DOMAIN,
  61. data={
  62. CONF_TYPE: "non_existing",
  63. CONF_DEVICE_ID: "dummy",
  64. CONF_PROTOCOL_VERSION: "auto",
  65. },
  66. )
  67. m_add_entities = Mock()
  68. m_device = AsyncMock()
  69. hass.data[DOMAIN] = {
  70. "dummy": {"device": m_device},
  71. }
  72. try:
  73. await async_setup_entry(hass, entry, m_add_entities)
  74. assert False
  75. except ValueError:
  76. pass
  77. m_add_entities.assert_not_called()
  78. def _make_dp(name, value=None):
  79. dp = MagicMock()
  80. dp.name = name
  81. dp.hidden = False
  82. dp.optional = True
  83. dp.get_value.return_value = value
  84. dp.get_values_to_set.side_effect = lambda dev, val, s: {name: val}
  85. return dp
  86. def _make_datetime(
  87. year=None,
  88. month=None,
  89. day=None,
  90. hour=None,
  91. minute=None,
  92. second=None,
  93. ):
  94. """Create a TuyaLocalDateTime with mocked internals."""
  95. device = MagicMock()
  96. config = MagicMock()
  97. config.name = "Test DateTime"
  98. config.translation_key = None
  99. config.translation_only_key = None
  100. config.translation_placeholders = None
  101. config.entity_category = None
  102. config.config_id = "test_datetime"
  103. dps = {}
  104. dp_list = []
  105. if year is not None:
  106. dp = _make_dp("year", year)
  107. dps["year"] = dp
  108. dp_list.append(dp)
  109. if month is not None:
  110. dp = _make_dp("month", month)
  111. dps["month"] = dp
  112. dp_list.append(dp)
  113. if day is not None:
  114. dp = _make_dp("day", day)
  115. dps["day"] = dp
  116. dp_list.append(dp)
  117. if hour is not None:
  118. dp = _make_dp("hour", hour)
  119. dps["hour"] = dp
  120. dp_list.append(dp)
  121. if minute is not None:
  122. dp = _make_dp("minute", minute)
  123. dps["minute"] = dp
  124. dp_list.append(dp)
  125. if second is not None:
  126. dp = _make_dp("second", second)
  127. dps["second"] = dp
  128. dp_list.append(dp)
  129. config.dps.return_value = dp_list
  130. entity = object.__new__(TuyaLocalDateTime)
  131. entity._device = device
  132. entity._config = config
  133. entity._attr_dps = []
  134. entity._attr_translation_key = None
  135. entity._attr_translation_placeholders = None
  136. entity._year_dps = dps.get("year")
  137. entity._month_dps = dps.get("month")
  138. entity._day_dps = dps.get("day")
  139. entity._hour_dps = dps.get("hour")
  140. entity._minute_dps = dps.get("minute")
  141. entity._second_dps = dps.get("second")
  142. return entity
  143. class TestNativeValue:
  144. def test_all_none_returns_none(self):
  145. # Create with dps present but returning None values
  146. entity = _make_datetime(hour=0, minute=0, second=0)
  147. entity._hour_dps.get_value.return_value = None
  148. entity._minute_dps.get_value.return_value = None
  149. entity._second_dps.get_value.return_value = None
  150. assert entity.native_value is None
  151. def test_hour_minute_second(self):
  152. entity = _make_datetime(hour=10, minute=30, second=45)
  153. result = entity.native_value
  154. assert result is not None
  155. assert result.hour == 10
  156. assert result.minute == 30
  157. assert result.second == 45
  158. def test_hour_only(self):
  159. entity = _make_datetime(hour=14)
  160. result = entity.native_value
  161. assert result is not None
  162. assert result.hour == 14
  163. assert result.minute == 0
  164. assert result.second == 0
  165. def test_minute_only(self):
  166. entity = _make_datetime(minute=45)
  167. result = entity.native_value
  168. assert result is not None
  169. assert result.minute == 45
  170. def test_defaults_when_partial(self):
  171. entity = _make_datetime(hour=5, minute=0, second=0)
  172. result = entity.native_value
  173. assert result.year == 1970
  174. assert result.month == 1
  175. class TestAsyncSetValue:
  176. @pytest.mark.asyncio
  177. async def test_set_hour_minute_second(self):
  178. entity = _make_datetime(hour=0, minute=0, second=0)
  179. entity._device.async_set_properties = AsyncMock()
  180. value = datetime(1970, 1, 1, 14, 30, 45, tzinfo=timezone.utc)
  181. await entity.async_set_value(value)
  182. entity._device.async_set_properties.assert_awaited_once()
  183. settings = entity._device.async_set_properties.call_args[0][0]
  184. assert settings["hour"] == 14
  185. assert settings["minute"] == 30
  186. assert settings["second"] == 45
  187. @pytest.mark.asyncio
  188. async def test_set_without_second_dp(self):
  189. entity = _make_datetime(hour=0, minute=0)
  190. entity._device.async_set_properties = AsyncMock()
  191. value = datetime(1970, 1, 1, 10, 20, 30, tzinfo=timezone.utc)
  192. await entity.async_set_value(value)
  193. settings = entity._device.async_set_properties.call_args[0][0]
  194. assert settings["hour"] == 10
  195. assert settings["minute"] == 20
  196. assert "second" not in settings
  197. @pytest.mark.asyncio
  198. async def test_set_without_hour_dp(self):
  199. entity = _make_datetime(minute=0, second=0)
  200. entity._device.async_set_properties = AsyncMock()
  201. value = datetime(1970, 1, 1, 2, 30, 15, tzinfo=timezone.utc)
  202. await entity.async_set_value(value)
  203. settings = entity._device.async_set_properties.call_args[0][0]
  204. # hour=2 should be folded into minutes: 2*60 + 30 = 150
  205. assert settings["minute"] == 150
  206. assert settings["second"] == 15
  207. @pytest.mark.asyncio
  208. async def test_set_without_minute_dp(self):
  209. entity = _make_datetime(hour=0, second=0)
  210. entity._device.async_set_properties = AsyncMock()
  211. value = datetime(1970, 1, 1, 1, 5, 30, tzinfo=timezone.utc)
  212. await entity.async_set_value(value)
  213. settings = entity._device.async_set_properties.call_args[0][0]
  214. assert settings["hour"] == 1
  215. # minute=5 folded into seconds: 5*60 + 30 = 330
  216. assert settings["second"] == 330
  217. @pytest.mark.asyncio
  218. async def test_set_without_day_dp(self):
  219. entity = _make_datetime(hour=0, minute=0, second=0)
  220. entity._device.async_set_properties = AsyncMock()
  221. value = datetime(1970, 1, 2, 3, 0, 0, tzinfo=timezone.utc)
  222. await entity.async_set_value(value)
  223. settings = entity._device.async_set_properties.call_args[0][0]
  224. # day=2 (1 extra day) should fold into hours: 1*24 + 3 = 27
  225. assert settings["hour"] == 27
  226. @pytest.mark.asyncio
  227. async def test_set_with_day_dp(self):
  228. entity = _make_datetime(day=0, hour=0, minute=0, second=0)
  229. entity._device.async_set_properties = AsyncMock()
  230. value = datetime(1970, 1, 15, 8, 30, 0, tzinfo=timezone.utc)
  231. await entity.async_set_value(value)
  232. settings = entity._device.async_set_properties.call_args[0][0]
  233. assert settings["hour"] == 8
  234. assert settings["minute"] == 30
  235. @pytest.mark.asyncio
  236. async def test_set_with_month_dp(self):
  237. entity = _make_datetime(month=0, day=0, hour=0, minute=0, second=0)
  238. entity._device.async_set_properties = AsyncMock()
  239. value = datetime(1970, 3, 5, 12, 0, 0, tzinfo=timezone.utc)
  240. await entity.async_set_value(value)
  241. settings = entity._device.async_set_properties.call_args[0][0]
  242. assert settings["month"] == 3
  243. assert settings["hour"] == 12