From 8ff0cb1c7cceea8b771f5e12f394d2f5b0dfd878 Mon Sep 17 00:00:00 2001 From: chiefdragon <11260692+chiefdragon@users.noreply.github.com> Date: Sun, 16 Oct 2022 19:24:08 +0100 Subject: [PATCH] Add tests and check for auto geofencing support Add check to ensure auto geofencing mode can only be enabled if it is known to be supported, i.e. ensure no issues occur for users without the Auto Assist skill. Add a custom exception to handle the event something tries to set auto mode when it is not known to be supported Add autoGeofencingSupported flag to track this ability and an external function for getting it Add tests --- PyTado/exceptions.py | 10 +++ PyTado/interface.py | 49 +++++++++++- .../tadov2.home_state.auto_not_supported.json | 4 + ...2.home_state.auto_supported.auto_mode.json | 4 + ...home_state.auto_supported.manual_mode.json | 5 ++ tests/test_tado.py | 80 +++++++++++++++++++ 6 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 PyTado/exceptions.py create mode 100644 tests/fixtures/tadov2.home_state.auto_not_supported.json create mode 100644 tests/fixtures/tadov2.home_state.auto_supported.auto_mode.json create mode 100644 tests/fixtures/tadov2.home_state.auto_supported.manual_mode.json create mode 100644 tests/test_tado.py diff --git a/PyTado/exceptions.py b/PyTado/exceptions.py new file mode 100644 index 0000000..ef02f3f --- /dev/null +++ b/PyTado/exceptions.py @@ -0,0 +1,10 @@ +"""Tado exceptions.""" + + +class TadoException(Exception): + """Base exception class for Tado.""" + + +class TadoNotSupportedException(TadoException): + """Exception to indicate a requested action is not supported by Tado.""" + diff --git a/PyTado/interface.py b/PyTado/interface.py index f4a608f..76002f6 100644 --- a/PyTado/interface.py +++ b/PyTado/interface.py @@ -11,6 +11,7 @@ from enum import IntEnum from .zone import TadoZone +from .exceptions import TadoNotSupportedException _LOGGER = logging.getLogger(__name__) @@ -27,8 +28,13 @@ class Timetable(IntEnum): THREE_DAY = 1 SEVEN_DAY = 2 + # Instance-wide variables _debugCalls = False + # Track whether the user's Tado instance supports auto-geofencing, + # set to None until explicitly set + _autoGeofencingSupported = None + # Instance-wide constant info api2url = 'https://my.tado.com/api/v2/' mobi2url = 'https://my.tado.com/mobile/1.9/' @@ -205,10 +211,41 @@ def getHomeState(self): # but a button is shown in the app. showHomePresenceSwitchButton # is an indicator, that the homeState can be switched # {"presence":"HOME","showHomePresenceSwitchButton":true} + # With an auto assist skill, showSwitchToAutoGeofencingButton is + # present when geofencing has been disabled due to the user selecting + # a mode manually: + # {'presence': 'HOME', 'presenceLocked': False, + # 'showSwitchToAutoGeofencingButton': True} + # With an auto assist skill, showSwitchToAutoGeofencingButton is NOT + # present when geofencing has been enabled: + # {'presence': 'HOME', 'presenceLocked': True} + # In both scenarios with the auto assist skill, 'presenceLocked' + # indicates whether presence is current locked (manually set) to + # HOME or AWAY or not locked (automatically set based on geolocation) cmd = 'state' data = self._apiCall(cmd) + + # Check whether Auto Geofencing is permitted via the presence of + # showSwitchToAutoGeofencingButton or currently enabled via the + # presence of presenceLocked = False + if "showSwitchToAutoGeofencingButton" in data: + self._autoGeofencingSupported = data['showSwitchToAutoGeofencingButton'] + elif "presenceLocked" in data: + if not data['presenceLocked']: + self._autoGeofencingSupported = True + else: + self._autoGeofencingSupported = False + else: + self._autoGeofencingSupported = False + return data + def getAutoGeofencingSupported(self): + """Return whether the Tado Home supports auto geofencing""" + if self._autoGeofencingSupported is None: + self.getHomeState() + return self._autoGeofencingSupported + def getCapabilities(self, zone): """Gets current capabilities of Zone zone.""" # pylint: disable=C0103 @@ -370,7 +407,7 @@ def setZoneOverlay(self, zone, overlayMode, setTemp=None, duration=None, deviceT data = self._apiCall(cmd, "PUT", post_data) return data - + def getZoneOverlayDefault(self, zone): """Get current overlay default settings for zone.""" cmd = 'zones/%i/defaultOverlay' % zone @@ -393,9 +430,13 @@ def setAway(self): def setAuto(self): """Sets HomeState to AUTO """ - cmd = 'presenceLock' - data = self._apiCall(cmd, "DELETE") - return data + # Only attempt to set Auto Geofencing if it is believed to be supported + if self._autoGeofencingSupported: + cmd = 'presenceLock' + data = self._apiCall(cmd, "DELETE") + return data + else: + raise TadoNotSupportedException("Auto mode is not known to be supported.") def getWindowState(self, zone): """Returns the state of the window for Zone zone""" diff --git a/tests/fixtures/tadov2.home_state.auto_not_supported.json b/tests/fixtures/tadov2.home_state.auto_not_supported.json new file mode 100644 index 0000000..32228cc --- /dev/null +++ b/tests/fixtures/tadov2.home_state.auto_not_supported.json @@ -0,0 +1,4 @@ +{ + "presence": "HOME", + "presenceLocked": true + } \ No newline at end of file diff --git a/tests/fixtures/tadov2.home_state.auto_supported.auto_mode.json b/tests/fixtures/tadov2.home_state.auto_supported.auto_mode.json new file mode 100644 index 0000000..de02c9b --- /dev/null +++ b/tests/fixtures/tadov2.home_state.auto_supported.auto_mode.json @@ -0,0 +1,4 @@ +{ + "presence": "HOME", + "presenceLocked": false + } \ No newline at end of file diff --git a/tests/fixtures/tadov2.home_state.auto_supported.manual_mode.json b/tests/fixtures/tadov2.home_state.auto_supported.manual_mode.json new file mode 100644 index 0000000..62b5d87 --- /dev/null +++ b/tests/fixtures/tadov2.home_state.auto_supported.manual_mode.json @@ -0,0 +1,5 @@ +{ + "presence": "HOME", + "presenceLocked": true, + "showSwitchToAutoGeofencingButton": true +} \ No newline at end of file diff --git a/tests/test_tado.py b/tests/test_tado.py new file mode 100644 index 0000000..a3ffc66 --- /dev/null +++ b/tests/test_tado.py @@ -0,0 +1,80 @@ +"""Test the Tado object.""" + +import os +import json +from unittest.mock import patch + +from PyTado.interface import Tado + + +def load_fixture(filename): + """Load a fixture.""" + path = os.path.join(os.path.dirname(__file__), "fixtures", filename) + with open(path) as fptr: + return fptr.read() + + +def mock_tado(): + """Mock out a Tado object.""" + with patch("PyTado.interface.Tado._loginV2"), patch( + "PyTado.interface.Tado.getMe" + ): + tado = Tado("my@username.com", "mypassword") + return tado + + +def test_home_can_be_set_to_auto_when_home_supports_geofencing_and_home_set_to_manual_mode(): + """Test that the Tado home can be set to auto geofencing mode when it is supported and currently in manual mode.""" + tado = mock_tado() + with patch("PyTado.interface.Tado._apiCall", + return_value=json.loads(load_fixture("tadov2.home_state.auto_supported.manual_mode.json")), + ): + tado.getHomeState() + + with patch("PyTado.interface.Tado._apiCall"): + raised = False + try: + tado.setAuto() + except: + raised = True + + # An exception should NOT have been raised because geofencing is supported + assert raised is False + + +def test_home_remains_set_to_auto_when_home_supports_geofencing_and_home_already_set_to_auto_mode(): + """Test that the Tado home remains set to auto geofencing mode when it is supported, and already in auto mode.""" + tado = mock_tado() + with patch("PyTado.interface.Tado._apiCall", + return_value=json.loads(load_fixture("tadov2.home_state.auto_supported.auto_mode.json")), + ): + tado.getHomeState() + + with patch("PyTado.interface.Tado._apiCall"): + raised = False + try: + tado.setAuto() + except: + raised = True + + # An exception should NOT have been raised because geofencing is supported + assert raised is False + + +def test_home_cant_be_set_to_auto_when_home_does_not_support_geofencing(): + """Test that the Tado home can't be set to auto geofencing mode when it is not supported.""" + tado = mock_tado() + with patch("PyTado.interface.Tado._apiCall", + return_value=json.loads(load_fixture("tadov2.home_state.auto_not_supported.json")), + ): + tado.getHomeState() + + with patch("PyTado.interface.Tado._apiCall"): + raised = False + try: + tado.setAuto() + except: + raised = True + + # An exception should have been raised because geofencing is NOT supported + assert raised is True