test_device.py 23 KB

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