test_device.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. import asyncio
  2. import logging
  3. from time import time
  4. import pytest
  5. # from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
  6. from custom_components.tuya_local.const import CONF_DEVICE_ID, DOMAIN
  7. from custom_components.tuya_local.device import TuyaLocalDevice, async_delete_device
  8. from .const import EUROM_600_HEATER_PAYLOAD
  9. @pytest.fixture
  10. def mock_api(mocker):
  11. mock = mocker.patch("tinytuya.Device")
  12. mock.parent = None
  13. yield mock
  14. @pytest.fixture
  15. def patched_hass(hass, mocker):
  16. hass.is_running = True
  17. hass.is_stopping = False
  18. hass.data = {"tuya_local": {}}
  19. async def job(func, *args):
  20. print(f"{args}")
  21. return func(*args)
  22. mocker.patch.object(hass, "async_add_executor_job", side_effect=job)
  23. mocker.patch.object(hass, "async_create_task")
  24. return hass
  25. @pytest.fixture
  26. def subject(patched_hass, mock_api, mocker):
  27. subject = TuyaLocalDevice(
  28. "Some name",
  29. "some_dev_id",
  30. "some.ip.address",
  31. "some_local_key",
  32. "auto",
  33. None,
  34. patched_hass,
  35. )
  36. # For most tests we want the protocol working
  37. subject._api_protocol_version_index = 0
  38. subject._api_protocol_working = True
  39. subject._protocol_configured = "auto"
  40. return subject
  41. def test_name(subject):
  42. """Returns the name given at instantiation."""
  43. assert subject.name == "Some name"
  44. def test_unique_id(subject, mock_api):
  45. """Returns the unique ID presented by the API class."""
  46. assert subject.unique_id is mock_api().id
  47. def test_subdevice_unique_id_is_scoped_by_gateway(patched_hass, mock_api):
  48. """Returns a gateway-scoped ID for a child device."""
  49. subject = TuyaLocalDevice(
  50. "Some name",
  51. "gateway_id",
  52. "some.ip.address",
  53. "some_local_key",
  54. "3.3",
  55. "child_id",
  56. patched_hass,
  57. )
  58. assert subject.unique_id == "gateway_id/child_id"
  59. def test_device_info(subject, mock_api):
  60. """Returns generic info plus the unique ID for categorisation."""
  61. assert subject.device_info == {
  62. "identifiers": {("tuya_local", mock_api().id)},
  63. "name": "Some name",
  64. "manufacturer": "Tuya",
  65. }
  66. @pytest.mark.asyncio
  67. async def test_delete_keeps_device_entry_when_stop_fails(hass, mocker):
  68. """Device cache should not be removed before stop succeeds."""
  69. device = mocker.MagicMock()
  70. device.async_stop = mocker.AsyncMock(side_effect=RuntimeError("stop failed"))
  71. hass.data[DOMAIN] = {
  72. "deviceid": {
  73. "device": device,
  74. "tuyadevice": mocker.MagicMock(),
  75. "tuyadevicelock": mocker.MagicMock(),
  76. }
  77. }
  78. with pytest.raises(RuntimeError, match="stop failed"):
  79. await async_delete_device(hass, {CONF_DEVICE_ID: "deviceid"})
  80. assert hass.data[DOMAIN]["deviceid"]["device"] is device
  81. def test_has_returned_state(subject):
  82. """Returns True if the device has returned its state."""
  83. subject._cached_state = EUROM_600_HEATER_PAYLOAD
  84. assert subject.has_returned_state
  85. subject._cached_state = {"updated_at": 0}
  86. assert not subject.has_returned_state
  87. @pytest.mark.asyncio
  88. async def test_refreshes_state_if_no_cached_state_exists(subject, mocker):
  89. subject._cached_state = {}
  90. subject.async_refresh = mocker.AsyncMock()
  91. await subject.async_inferred_type()
  92. subject.async_refresh.assert_awaited()
  93. @pytest.mark.asyncio
  94. async def test_detection_returns_none_when_device_type_not_detected(subject):
  95. subject._cached_state = {"192": False, "updated_at": time()}
  96. assert await subject.async_inferred_type() is None
  97. @pytest.mark.asyncio
  98. async def test_refresh_retries_up_to_eleven_times(subject, mock_api):
  99. subject._api_protocol_working = False
  100. mock_api().status.side_effect = [
  101. Exception("Error"),
  102. Exception("Error"),
  103. Exception("Error"),
  104. Exception("Error"),
  105. Exception("Error"),
  106. Exception("Error"),
  107. Exception("Error"),
  108. Exception("Error"),
  109. Exception("Error"),
  110. Exception("Error"),
  111. {"dps": {"1": False}},
  112. ]
  113. await subject.async_refresh()
  114. assert mock_api().status.call_count == 11
  115. assert subject._cached_state["1"] is False
  116. @pytest.mark.asyncio
  117. async def test_refresh_clears_cache_after_allowed_failures(subject, mock_api):
  118. subject._cached_state = {"1": True}
  119. subject._pending_updates = {
  120. "1": {"value": False, "updated_at": time(), "sent": True}
  121. }
  122. mock_api().status.side_effect = [
  123. Exception("Error"),
  124. Exception("Error"),
  125. Exception("Error"),
  126. ]
  127. await subject.async_refresh()
  128. assert mock_api().status.call_count == 3
  129. assert subject._cached_state == {"updated_at": 0}
  130. assert subject._pending_updates == {}
  131. @pytest.mark.asyncio
  132. async def test_api_protocol_version_is_rotated_with_each_failure(
  133. subject, mock_api, mocker
  134. ):
  135. subject._api_protocol_version_index = None
  136. subject._api_protocol_working = False
  137. mock_api().status.side_effect = [
  138. Exception("Error"),
  139. Exception("Error"),
  140. Exception("Error"),
  141. Exception("Error"),
  142. Exception("Error"),
  143. Exception("Error"),
  144. Exception("Error"),
  145. ]
  146. await subject.async_refresh()
  147. mock_api().set_version.assert_has_calls(
  148. [
  149. mocker.call(3.3),
  150. mocker.call(3.1),
  151. mocker.call(3.2),
  152. mocker.call(3.4),
  153. mocker.call(3.5),
  154. mocker.call(3.3),
  155. mocker.call(3.4),
  156. mocker.call(3.5),
  157. mocker.call(3.3),
  158. mocker.call(3.1),
  159. mocker.call(3.2),
  160. mocker.call(3.4),
  161. mocker.call(3.5),
  162. mocker.call(3.3),
  163. mocker.call(3.4),
  164. mocker.call(3.5),
  165. mocker.call(3.3),
  166. mocker.call(3.1),
  167. ]
  168. )
  169. @pytest.mark.asyncio
  170. async def test_api_protocol_version_is_stable_once_successful(
  171. subject, mock_api, mocker
  172. ):
  173. subject._api_protocol_version_index = None
  174. subject._api_protocol_working = False
  175. mock_api().status.side_effect = [
  176. Exception("Error"),
  177. Exception("Error"),
  178. Exception("Error"),
  179. {"dps": {"1": False}},
  180. {"dps": {"1": False}},
  181. Exception("Error"),
  182. Exception("Error"),
  183. {"dps": {"1": False}},
  184. ]
  185. await subject.async_refresh()
  186. assert subject._api_protocol_version_index == 3
  187. assert subject._api_protocol_working
  188. await subject.async_refresh()
  189. assert subject._api_protocol_version_index == 3
  190. await subject.async_refresh()
  191. assert subject._api_protocol_version_index == 3
  192. mock_api().set_version.assert_has_calls(
  193. [
  194. mocker.call(3.1),
  195. mocker.call(3.2),
  196. mocker.call(3.4),
  197. ]
  198. )
  199. @pytest.mark.asyncio
  200. async def test_api_protocol_version_is_not_rotated_when_not_auto(subject, mock_api):
  201. # Set up preconditions for the test
  202. subject._protocol_configured = 3.4
  203. subject._api_protocol_version_index = None
  204. subject._api_protocol_working = False
  205. mock_api().status.side_effect = [
  206. Exception("Error"),
  207. Exception("Error"),
  208. Exception("Error"),
  209. {"dps": {"1": False}},
  210. {"dps": {"1": False}},
  211. Exception("Error"),
  212. Exception("Error"),
  213. Exception("Error"),
  214. Exception("Error"),
  215. Exception("Error"),
  216. Exception("Error"),
  217. Exception("Error"),
  218. {"dps": {"1": False}},
  219. ]
  220. await subject._rotate_api_protocol_version()
  221. mock_api().set_version.assert_called_once_with(3.4)
  222. mock_api().set_version.reset_mock()
  223. await subject.async_refresh()
  224. assert subject._api_protocol_version_index == 3
  225. await subject.async_refresh()
  226. assert subject._api_protocol_version_index == 3
  227. await subject.async_refresh()
  228. assert subject._api_protocol_version_index == 3
  229. def test_reset_cached_state_clears_cached_state_and_pending_updates(subject):
  230. subject._cached_state = {"1": True, "updated_at": time()}
  231. subject._pending_updates = {
  232. "1": {"value": False, "updated_at": time(), "sent": True}
  233. }
  234. subject._reset_cached_state()
  235. assert subject._cached_state == {"updated_at": 0}
  236. assert subject._pending_updates == {}
  237. def test_get_property_returns_value_from_cached_state(subject):
  238. subject._cached_state = {"1": True}
  239. assert subject.get_property("1") is True
  240. def test_get_property_returns_pending_update_value(subject):
  241. subject._pending_updates = {
  242. "1": {"value": False, "updated_at": time() - 4, "sent": True}
  243. }
  244. assert subject.get_property("1") is False
  245. def test_pending_update_value_overrides_cached_value(subject):
  246. subject._cached_state = {"1": True}
  247. subject._pending_updates = {
  248. "1": {"value": False, "updated_at": time() - 4, "sent": True}
  249. }
  250. assert subject.get_property("1") is False
  251. def test_expired_pending_update_value_does_not_override_cached_value(subject):
  252. subject._cached_state = {"1": True}
  253. subject._pending_updates = {
  254. "1": {"value": False, "updated_at": time() - 5, "sent": True}
  255. }
  256. assert subject.get_property("1") is True
  257. def test_get_property_returns_none_when_value_does_not_exist(subject):
  258. subject._cached_state = {"1": True}
  259. assert subject.get_property("2") is None
  260. @pytest.mark.asyncio
  261. async def test_async_set_property_sends_to_api(subject, mock_api):
  262. await subject.async_set_property("1", False)
  263. mock_api().set_multiple_values.assert_called_once()
  264. @pytest.mark.asyncio
  265. async def test_set_property_immediately_stores_pending_updates(subject):
  266. subject._cached_state = {"1": True}
  267. await subject.async_set_property("1", False)
  268. assert not subject.get_property("1")
  269. @pytest.mark.asyncio
  270. async def test_set_properties_takes_no_action_when_nothing_provided(subject, mocker):
  271. mock = mocker.patch("asyncio.sleep")
  272. await subject.async_set_properties({})
  273. mock.assert_not_called()
  274. def test_anticipate_property_value_updates_cached_state(subject):
  275. subject._cached_state = {"1": True}
  276. subject.anticipate_property_value("1", False)
  277. assert subject._cached_state["1"] is False
  278. def test_get_key_for_value_returns_key_from_object_matching_value(subject):
  279. obj = {"key1": "value1", "key2": "value2"}
  280. assert TuyaLocalDevice.get_key_for_value(obj, "value1") == "key1"
  281. assert TuyaLocalDevice.get_key_for_value(obj, "value2") == "key2"
  282. def test_get_key_for_value_returns_fallback_when_value_not_found(subject):
  283. obj = {"key1": "value1", "key2": "value2"}
  284. assert TuyaLocalDevice.get_key_for_value(obj, "value3", fallback="fb") == "fb"
  285. def test_refresh_cached_state(subject, mock_api):
  286. # set up preconditions
  287. mock_api().status.return_value = {"dps": {"1": "CHANGED"}}
  288. subject._cached_state = {"1": "UNCHANGED", "updated_at": 123}
  289. # call the function under test
  290. subject._refresh_cached_state()
  291. # Did it call the API as expected?
  292. mock_api().status.assert_called_once()
  293. # Did it update the cached state?
  294. assert subject._cached_state == {"1": "CHANGED"} | subject._cached_state
  295. # Did it update the timestamp on the cached state?
  296. assert subject._cached_state["updated_at"] == pytest.approx(time(), abs=2)
  297. def test_set_values(subject, mock_api):
  298. # set up preconditions
  299. subject._pending_updates = {
  300. "1": {"value": "sample", "updated_at": time() - 2, "sent": False},
  301. }
  302. # call the function under test
  303. subject._set_values({"1": "sample"})
  304. # did it send what it was asked?
  305. mock_api().set_multiple_values.assert_called_once_with({"1": "sample"}, nowait=True)
  306. # did it mark the pending updates as sent?
  307. assert subject._pending_updates["1"]["sent"]
  308. # did it update the time on the pending updates?
  309. assert subject._pending_updates["1"]["updated_at"] == pytest.approx(time(), abs=2)
  310. # did it lock and unlock when sending
  311. # subject._lock.acquire.assert_called_once()
  312. # subject._lock.release.assert_called_once()
  313. def test_pending_updates_cleared_on_receipt(subject):
  314. # Set up the preconditions
  315. now = time()
  316. subject._pending_updates = {
  317. "1": {"value": True, "updated_at": now, "sent": True},
  318. "2": {"value": True, "updated_at": now, "sent": False}, # unsent
  319. "3": {"value": True, "updated_at": now, "sent": True}, # unmatched
  320. "4": {"value": True, "updated_at": now, "sent": True}, # not received
  321. }
  322. subject._remove_properties_from_pending_updates({"1": True, "2": True, "3": False})
  323. assert subject._pending_updates == {
  324. "2": {"value": True, "updated_at": now, "sent": False},
  325. "3": {"value": True, "updated_at": now, "sent": True},
  326. "4": {"value": True, "updated_at": now, "sent": True},
  327. }
  328. def test_actually_start(subject, mocker, patched_hass):
  329. # Set up the preconditions
  330. mocker.patch.object(subject, "receive_loop", return_value="LOOP")
  331. mocker.patch.object(subject, "_refresh_task", new=mocker.AsyncMock)
  332. subject._running = False
  333. mocker.patch.object(patched_hass, "async_create_task")
  334. # patched_hass.async_create_task = mocker.MagicMock()
  335. # patched_hass.bus.async_listen_once = mocker.AsyncMock()
  336. # patched_hass.bus.async_listen_once.return_value = "LISTENER"
  337. # run the function under test
  338. subject.actually_start()
  339. # did it register a listener for EVENT_HOMEASSISTANT_STOP?
  340. # patched_hass.bus.async_listen_once.assert_called_once_with(
  341. # EVENT_HOMEASSISTANT_STOP, subject.async_stop
  342. # )
  343. # assert subject._shutdown_listener == "LISTENER"
  344. # did it set the running flag?
  345. assert subject._running
  346. # did it schedule the loop?
  347. # task.assert_called_once()
  348. def test_start_starts_when_ha_running(subject, patched_hass, mocker):
  349. # Set up preconditions
  350. patched_hass.is_running = True
  351. listener = mocker.MagicMock()
  352. subject._startup_listener = listener
  353. subject.actually_start = mocker.MagicMock()
  354. # Call the function under test
  355. subject.start()
  356. # Did it actually start?
  357. subject.actually_start.assert_called_once()
  358. # Did it cancel the startup listener?
  359. assert subject._startup_listener is None
  360. listener.assert_called_once()
  361. def test_start_schedules_for_later_when_ha_starting(subject, patched_hass, mocker):
  362. # Set up preconditions
  363. patched_hass.is_running = False
  364. subject.actually_start = mocker.MagicMock()
  365. # Call the function under test
  366. subject.start()
  367. # Did it avoid actually starting?
  368. subject.actually_start.assert_not_called()
  369. # Did it register a listener?
  370. # assert subject._startup_listener == "LISTENER"
  371. # patched_hass.bus.async_listen_once.assert_called_once_with(
  372. # EVENT_HOMEASSISTANT_STARTED, subject.actually_start
  373. # )
  374. def test_start_does_nothing_when_ha_stopping(subject, patched_hass, mocker):
  375. # Set up preconditions
  376. patched_hass.is_running = True
  377. patched_hass.is_stopping = True
  378. subject.actually_start = mocker.MagicMock()
  379. # Call the function under test
  380. subject.start()
  381. # Did it avoid actually starting?
  382. subject.actually_start.assert_not_called()
  383. # Did it avoid registering a listener?
  384. # patched_hass.bus.async_listen_once.assert_not_called()
  385. assert subject._startup_listener is None
  386. @pytest.mark.asyncio
  387. async def test_async_stop(subject, mocker):
  388. # Set up preconditions
  389. listener = mocker.MagicMock()
  390. subject._refresh_task = None
  391. subject._shutdown_listener = listener
  392. subject._children = [1, 2, 3]
  393. # Call the function under test
  394. await subject.async_stop()
  395. # Shutdown listener doesn't get cancelled as HA does that
  396. listener.assert_not_called()
  397. # Were the child entities cleared?
  398. assert subject._children == []
  399. # Did it wait for the refresh task to finish then clear it?
  400. # This doesn't work because AsyncMock only mocks awaitable method calls
  401. # but we want an awaitable object
  402. # refresh.assert_awaited_once()
  403. assert subject._refresh_task is None
  404. @pytest.mark.asyncio
  405. async def test_async_stop_when_not_running(subject):
  406. # Set up preconditions
  407. _refresh_task = None
  408. subject._shutdown_listener = None
  409. subject._children = []
  410. # Call the function under test
  411. await subject.async_stop()
  412. # Was the shutdown listener left empty?
  413. assert subject._shutdown_listener is None
  414. # Were the child entities cleared?
  415. assert subject._children == []
  416. # Was the refresh task left empty?
  417. assert subject._refresh_task is None
  418. def test_register_first_entity_ha_running(subject, mocker):
  419. # Set up preconditions
  420. subject._children = []
  421. subject._running = False
  422. subject._startup_listener = None
  423. subject.start = mocker.MagicMock()
  424. entity = mocker.AsyncMock()
  425. entity._config = mocker.MagicMock()
  426. entity._config.dps.return_value = []
  427. # despite the name, the below HA function is not async and does not need to be awaited
  428. entity.async_schedule_update_ha_state = mocker.MagicMock()
  429. # Call the function under test
  430. subject.register_entity(entity)
  431. # Was the entity added to the list?
  432. assert subject._children == [entity]
  433. # Did we start the loop?
  434. subject.start.assert_called_once()
  435. def test_register_subsequent_entity_ha_running(subject, mocker):
  436. # Set up preconditions
  437. first = mocker.AsyncMock()
  438. second = mocker.AsyncMock()
  439. second._config = mocker.MagicMock()
  440. second._config.dps.return_value = []
  441. subject._children = [first]
  442. subject._running = True
  443. subject._startup_listener = None
  444. subject.start = mocker.MagicMock()
  445. # Call the function under test
  446. subject.register_entity(second)
  447. # Was the entity added to the list?
  448. assert set(subject._children) == set([first, second])
  449. # Did we avoid restarting the loop?
  450. subject.start.assert_not_called()
  451. def test_register_subsequent_entity_ha_starting(subject, mocker):
  452. # Set up preconditions
  453. first = mocker.AsyncMock()
  454. second = mocker.AsyncMock()
  455. second._config = mocker.MagicMock()
  456. second._config.dps.return_value = []
  457. subject._children = [first]
  458. subject._running = False
  459. subject._startup_listener = mocker.MagicMock()
  460. subject.start = mocker.MagicMock()
  461. # Call the function under test
  462. subject.register_entity(second)
  463. # Was the entity added to the list?
  464. assert set(subject._children) == set([first, second])
  465. # Did we avoid restarting the loop?
  466. subject.start.assert_not_called()
  467. @pytest.mark.asyncio
  468. async def test_unregister_one_of_many_entities(subject, mocker):
  469. # Set up preconditions
  470. subject._children = ["First", "Second"]
  471. subject.async_stop = mocker.AsyncMock()
  472. # Call the function under test
  473. await subject.async_unregister_entity("First")
  474. # Was the entity removed from the list?
  475. assert set(subject._children) == set(["Second"])
  476. # Is the loop still running?
  477. subject.async_stop.assert_not_called()
  478. @pytest.mark.asyncio
  479. async def test_unregister_last_entity(subject, mocker):
  480. # Set up preconditions
  481. subject._children = ["Last"]
  482. subject.async_stop = mocker.AsyncMock()
  483. # Call the function under test
  484. await subject.async_unregister_entity("Last")
  485. # Was the entity removed from the list?
  486. assert subject._children == []
  487. # Was the loop stopped?
  488. subject.async_stop.assert_called_once()
  489. @pytest.mark.asyncio
  490. async def test_async_receive(subject, mock_api, mocker):
  491. # Set up preconditions
  492. mock_api().status.return_value = {"dps": {"1": "INIT", "2": 2}}
  493. mock_api().receive.return_value = {"1": "UPDATED"}
  494. subject._running = True
  495. subject._cached_state = {"updated_at": 0}
  496. # Call the function under test
  497. print("starting test loop...")
  498. loop = subject.async_receive()
  499. print("getting first iteration...")
  500. result = await loop.__anext__()
  501. # Check that the loop was started, but without persistent connection
  502. # since there was no state returned yet and it might need to negotiate
  503. # version.
  504. mock_api().set_socketPersistent.assert_called_once_with(False)
  505. # Check that a full poll was done
  506. mock_api().status.assert_called_once()
  507. assert result == {"1": "INIT", "2": 2, "full_poll": True}
  508. # Prepare for next round
  509. subject._cached_state = subject._cached_state | result
  510. mock_api().status.reset_mock()
  511. mock_api().set_socketPersistent.reset_mock()
  512. print("getting second iteration...")
  513. result = await loop.__anext__()
  514. # Check that the connection was made persistent now that data has been
  515. # returned
  516. mock_api().set_socketPersistent.assert_called_once_with(True)
  517. mock_api().status.reset_mock()
  518. # Wait long enough to force a heartbeat poll on the next iteration
  519. subject._cached_state = subject._cached_state | {"updated_at": time()}
  520. await asyncio.sleep(10.1)
  521. print("getting third iteration...")
  522. # Call the function under test
  523. result = await loop.__anext__()
  524. # Check that a heartbeat poll was done
  525. mock_api().status.assert_not_called()
  526. mock_api().heartbeat.assert_called_once()
  527. mock_api().receive.assert_called_once()
  528. assert result == {"1": "UPDATED", "full_poll": False}
  529. # Prepare for next iteration
  530. subject._running = False
  531. mock_api().set_socketPersistent.reset_mock()
  532. # Call the function under test
  533. print("getting last iteration...")
  534. try:
  535. result = await loop.__anext__()
  536. pytest.fail("Should have raised an exception to quit the loop")
  537. # Check that the loop terminated
  538. except StopAsyncIteration:
  539. pass
  540. mock_api().set_socketPersistent.assert_called_once_with(False)
  541. def test_should_poll(subject):
  542. subject._cached_state = {"1": "sample", "updated_at": time()}
  543. subject._poll_only = False
  544. subject._temporary_poll = False
  545. # Test temporary poll via pause/resume
  546. assert not subject.should_poll
  547. subject.pause()
  548. assert subject.should_poll
  549. subject.resume()
  550. assert not subject.should_poll
  551. # Test configured polling
  552. subject._poll_only = True
  553. assert subject.should_poll
  554. subject._poll_only = False
  555. # Test initial polling
  556. subject._cached_state = {}
  557. assert subject.should_poll
  558. @pytest.mark.asyncio
  559. async def test_refresh_error_reports_device_error_code(subject, mock_api, caplog):
  560. """The error code and message returned by the device are logged."""
  561. subject._api_protocol_working = False
  562. subject._protocol_configured = "3.3"
  563. mock_api().status.return_value = {
  564. "Error": "Check device key or version",
  565. "Err": "914",
  566. "Payload": None,
  567. }
  568. with caplog.at_level(logging.ERROR):
  569. await subject.async_refresh()
  570. assert "914" in caplog.text
  571. assert "Check device key or version" in caplog.text
  572. # 914 is ambiguous, so the possible causes are spelled out
  573. assert "power cycled" in caplog.text
  574. @pytest.mark.asyncio
  575. async def test_refresh_error_without_error_code_is_unchanged(subject, mock_api, caplog):
  576. """A failure that is not a device error logs the bare message."""
  577. subject._api_protocol_working = False
  578. subject._protocol_configured = "3.3"
  579. mock_api().status.side_effect = Exception("connection refused")
  580. with caplog.at_level(logging.ERROR):
  581. await subject.async_refresh()
  582. assert "Failed to refresh device state for Some name." in caplog.text
  583. assert "Device reported error" not in caplog.text
  584. @pytest.mark.asyncio
  585. async def test_refresh_error_914_stays_quiet_when_rotating_protocols(
  586. subject, mock_api, caplog
  587. ):
  588. """914 is expected while auto-detecting, so it must not log at error."""
  589. subject._api_protocol_working = False
  590. subject._protocol_configured = "auto"
  591. mock_api().status.return_value = {
  592. "Error": "Check device key or version",
  593. "Err": "914",
  594. "Payload": None,
  595. }
  596. with caplog.at_level(logging.ERROR):
  597. await subject.async_refresh()
  598. assert caplog.text == ""