diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0ee9148..ce13692 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,12 +15,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies 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 25ddd52..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 @@ -390,7 +427,17 @@ def setAway(self): payload = { "homePresence": "AWAY" } data = self._apiCall(cmd, "PUT", payload) return data - + + def setAuto(self): + """Sets HomeState to AUTO """ + # 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""" data = self.getState(zone)['openWindow'] diff --git a/setup.py b/setup.py index 0b686d5..7061efb 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ requirements = [x.strip() for x in open(here('requirements.txt')).readlines()] setup(name='python-tado', - version='0.13.0', + version='0.14.0', description='PyTado from chrism0dwk, modfied by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi', long_description=readme, keywords='tado', 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