From b0885962054066ff5670b3b4694a58ece1ccb998 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 19 Sep 2021 23:43:17 +0200 Subject: [PATCH] Perform initial update only using the sysinfo query (#199) Some devices are known to fail when trying to query non-supported modules like emeter information. This commit makes the initial update() only request the sysinfo, followed up by a second query if emeter is supported by the device. --- kasa/smartdevice.py | 25 ++++++++++++++++++++----- kasa/tests/conftest.py | 2 +- kasa/tests/test_smartdevice.py | 21 +++++++++++++++++++-- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 3b21aa01..cfca4c44 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -291,18 +291,33 @@ class SmartDevice: return await self._query_helper("system", "get_sysinfo") async def update(self): - """Update some of the attributes. + """Query the device to update the data. - Needed for methods that are decorated with `requires_update`. + Needed for properties that are decorated with `requires_update`. """ req = {} req.update(self._create_request("system", "get_sysinfo")) - # Check for emeter if we were never updated, or if the device has emeter - if self._last_update is None or self.has_emeter: + # If this is the initial update, check only for the sysinfo + # This is necessary as some devices crash on unexpected modules + # See #105, #120, #161 + if self._last_update is None: + _LOGGER.debug("Performing the initial update to obtain sysinfo") + self._last_update = await self.protocol.query(self.host, req) + self._sys_info = self._last_update["system"]["get_sysinfo"] + # If the device has no emeter, we are done for the initial update + # Otherwise we will follow the regular code path to also query + # the emeter data also during the initial update + if not self.has_emeter: + return + + if self.has_emeter: + _LOGGER.debug( + "The device has emeter, querying its information along sysinfo" + ) req.update(self._create_emeter_request()) + self._last_update = await self.protocol.query(self.host, req) - # TODO: keep accessible for tests self._sys_info = self._last_update["system"]["get_sysinfo"] def update_from_discover_info(self, info): diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py index ba1179cd..c3cb683f 100644 --- a/kasa/tests/conftest.py +++ b/kasa/tests/conftest.py @@ -35,7 +35,7 @@ STRIPS = {"HS107", "HS300", "KP303", "KP400"} DIMMERS = {"HS220"} DIMMABLE = {*BULBS, *DIMMERS} -WITH_EMETER = {"HS110", "HS300", *BULBS, *STRIPS} +WITH_EMETER = {"HS110", "HS300", *BULBS} ALL_DEVICES = BULBS.union(PLUGS).union(STRIPS).union(DIMMERS) diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index 67081b03..622a2126 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -5,7 +5,7 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342 from kasa import SmartDeviceException -from .conftest import handle_turn_on, pytestmark, turn_on +from .conftest import handle_turn_on, has_emeter, no_emeter, pytestmark, turn_on from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol @@ -17,7 +17,24 @@ async def test_invalid_connection(dev): with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException): with pytest.raises(SmartDeviceException): await dev.update() - dev.is_on + + +@has_emeter +async def test_initial_update_emeter(dev, mocker): + """Test that the initial update performs second query if emeter is available.""" + dev._last_update = None + spy = mocker.spy(dev.protocol, "query") + await dev.update() + assert spy.call_count == 2 + + +@no_emeter +async def test_initial_update_no_emeter(dev, mocker): + """Test that the initial update performs second query if emeter is available.""" + dev._last_update = None + spy = mocker.spy(dev.protocol, "query") + await dev.update() + assert spy.call_count == 1 async def test_query_helper(dev):