From 9c20cbe7967aaf80d84662605e14c702d5a2cad6 Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Sat, 28 Dec 2024 13:24:42 +0100 Subject: [PATCH 1/5] add home by bridge api to my tado - including tests --- PyTado/http.py | 7 ++-- PyTado/interface/api/my_tado.py | 30 ++++++++++++++++ ..._bridge.boiler_max_output_temperature.json | 1 + ...idge.boiler_wiring_installation_state.json | 18 ++++++++++ tests/test_my_tado.py | 36 ++++++++++++++----- 5 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/home_by_bridge.boiler_max_output_temperature.json create mode 100644 tests/fixtures/home_by_bridge.boiler_wiring_installation_state.json diff --git a/PyTado/http.py b/PyTado/http.py index 360be08..f93fdc0 100644 --- a/PyTado/http.py +++ b/PyTado/http.py @@ -37,6 +37,7 @@ class Domain(enum.StrEnum): HOME = "homes" DEVICES = "devices" ME = "me" + HOME_BY_BRIDGE = "homeByBridge" class Action(enum.StrEnum): @@ -65,7 +66,7 @@ def __init__( action: Action | str = Action.GET, payload: dict[str, Any] | None = None, domain: Domain = Domain.HOME, - device: int | None = None, + device: int | str | None = None, mode: Mode = Mode.OBJECT, params: dict[str, Any] | None = None, ) -> None: @@ -89,7 +90,7 @@ def __init__( action: Action | str = Action.GET, payload: dict[str, Any] | None = None, domain: Domain = Domain.HOME, - device: int | None = None, + device: int | str | None = None, mode: Mode = Mode.OBJECT, params: dict[str, Any] | None = None, ) -> None: @@ -213,7 +214,7 @@ def request(self, request: TadoRequest) -> dict[str, Any]: def _configure_url(self, request: TadoRequest) -> str: if request.endpoint == Endpoint.MOBILE: url = f"{request.endpoint}{request.command}" - elif request.domain == Domain.DEVICES: + elif request.domain == Domain.DEVICES or request.domain == Domain.HOME_BY_BRIDGE: url = f"{request.endpoint}{request.domain}/{request.device}/{request.command}" elif request.domain == Domain.ME: url = f"{request.endpoint}{request.domain}" diff --git a/PyTado/interface/api/my_tado.py b/PyTado/interface/api/my_tado.py index 091549c..653ccf6 100644 --- a/PyTado/interface/api/my_tado.py +++ b/PyTado/interface/api/my_tado.py @@ -606,3 +606,33 @@ def get_running_times(self, date=datetime.datetime.now().strftime("%Y-%m-%d")) - request.params = {"from": date} return self._http.request(request) + + def get_boiler_output_temperature(self, bridge_id: str): + """ + Get the bridge details + """ + + request = TadoRequest() + request.action = Action.GET + request.domain = Domain.HOME_BY_BRIDGE + request.device = bridge_id + + request.command = "boilerWiringInstallationState" + install_state = self._http.request(request) + + return { + "boiler": install_state["boiler"], + } + + def get_boiler_max_output_temperature(self, bridge_id: str): + """ + Get the bridge details + """ + + request = TadoRequest() + request.action = Action.GET + request.domain = Domain.HOME_BY_BRIDGE + request.device = bridge_id + request.command = "boilerMaxOutputTemperature" + + return self._http.request(request) diff --git a/tests/fixtures/home_by_bridge.boiler_max_output_temperature.json b/tests/fixtures/home_by_bridge.boiler_max_output_temperature.json new file mode 100644 index 0000000..609c8ef --- /dev/null +++ b/tests/fixtures/home_by_bridge.boiler_max_output_temperature.json @@ -0,0 +1 @@ +{"boilerMaxOutputTemperatureInCelsius":50} diff --git a/tests/fixtures/home_by_bridge.boiler_wiring_installation_state.json b/tests/fixtures/home_by_bridge.boiler_wiring_installation_state.json new file mode 100644 index 0000000..c45fbf4 --- /dev/null +++ b/tests/fixtures/home_by_bridge.boiler_wiring_installation_state.json @@ -0,0 +1,18 @@ +{ + "state": "INSTALLATION_COMPLETED", + "deviceWiredToBoiler": { + "type": "RU02B", + "serialNo": "RUXXXXXXXXXX", + "thermInterfaceType": "OPENTHERM", + "connected": true, + "lastRequestTimestamp": "2024-12-28T10:36:47.533Z" + }, + "bridgeConnected": true, + "hotWaterZonePresent": false, + "boiler": { + "outputTemperature": { + "celsius": 38.01, + "timestamp": "2024-12-28T10:36:54.000Z" + } + } +} diff --git a/tests/test_my_tado.py b/tests/test_my_tado.py index d96ada2..12ff30f 100644 --- a/tests/test_my_tado.py +++ b/tests/test_my_tado.py @@ -15,9 +15,7 @@ class TadoTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() - login_patch = mock.patch( - "PyTado.http.Http._login", return_value=(1, "foo") - ) + login_patch = mock.patch("PyTado.http.Http._login", return_value=(1, "foo")) get_me_patch = mock.patch("PyTado.interface.api.Tado.get_me") login_patch.start() get_me_patch.start() @@ -35,9 +33,7 @@ def test_home_set_to_manual_mode( with mock.patch( "PyTado.http.Http.request", return_value=json.loads( - common.load_fixture( - "tadov2.home_state.auto_supported.manual_mode.json" - ) + common.load_fixture("tadov2.home_state.auto_supported.manual_mode.json") ), ): self.tado_client.get_home_state() @@ -53,9 +49,7 @@ def test_home_already_set_to_auto_mode( with mock.patch( "PyTado.http.Http.request", return_value=json.loads( - common.load_fixture( - "tadov2.home_state.auto_supported.auto_mode.json" - ) + common.load_fixture("tadov2.home_state.auto_supported.auto_mode.json") ), ): self.tado_client.get_home_state() @@ -92,3 +86,27 @@ def test_get_running_times(self): assert self.tado_client._http.request.called assert running_times["lastUpdated"] == "2023-08-05T19:50:21Z" assert running_times["runningTimes"][0]["zones"][0]["id"] == 1 + + def test_get_boiler_output_temperature(self): + with mock.patch( + "PyTado.http.Http.request", + return_value=json.loads( + common.load_fixture("home_by_bridge.boiler_wiring_installation_state.json") + ), + ): + boiler_temperature = self.tado_client.get_boiler_output_temperature("IB123456789") + + assert self.tado_client._http.request.called + assert boiler_temperature["boiler"]["outputTemperature"]["celsius"] == 38.01 + + def test_get_boiler_max_output_temperature(self): + with mock.patch( + "PyTado.http.Http.request", + return_value=json.loads( + common.load_fixture("home_by_bridge.boiler_max_output_temperature.json") + ), + ): + boiler_temperature = self.tado_client.get_boiler_max_output_temperature("IB123456789") + + assert self.tado_client._http.request.called + assert boiler_temperature["boilerMaxOutputTemperatureInCelsius"] == 50.0 From a96f677b64afada18b1289967d74617aadb863f5 Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Sat, 28 Dec 2024 14:06:16 +0100 Subject: [PATCH 2/5] changed signature - added logging to all http requests --- PyTado/http.py | 16 ++++++++-------- PyTado/interface/api/my_tado.py | 16 +++++++--------- tests/test_my_tado.py | 10 +++++++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/PyTado/http.py b/PyTado/http.py index f93fdc0..442c6a3 100644 --- a/PyTado/http.py +++ b/PyTado/http.py @@ -182,6 +182,7 @@ def request(self, request: TadoRequest) -> dict[str, Any]: http_request = requests.Request(method=request.action, url=url, headers=headers, data=data) prepped = http_request.prepare() + prepped.hooks["response"].append(self._log_response) retries = _DEFAULT_RETRIES @@ -197,6 +198,7 @@ def request(self, request: TadoRequest) -> dict[str, Any]: _LOGGER.warning("Connection error: %s", e) self._session.close() self._session = requests.Session() + self._session.hooks["response"].append(self._log_response) retries -= 1 else: _LOGGER.error( @@ -204,7 +206,7 @@ def request(self, request: TadoRequest) -> dict[str, Any]: _DEFAULT_RETRIES, e, ) - raise TadoException(e) + raise TadoException(e) from e if response.text is None or response.text == "": return {} @@ -218,15 +220,13 @@ def _configure_url(self, request: TadoRequest) -> str: url = f"{request.endpoint}{request.domain}/{request.device}/{request.command}" elif request.domain == Domain.ME: url = f"{request.endpoint}{request.domain}" - elif request.endpoint == Endpoint.MINDER: - params = request.params if request.params is not None else {} - - url = ( - f"{request.endpoint}{request.domain}/{self._id:d}/{request.command}" - f"?{urlencode(params)}" - ) else: url = f"{request.endpoint}{request.domain}/{self._id:d}/{request.command}" + + if request.params is not None: + params = request.params + url += f"?{urlencode(params)}" + return url def _configure_payload(self, headers: dict[str, str], request: TadoRequest) -> bytes: diff --git a/PyTado/interface/api/my_tado.py b/PyTado/interface/api/my_tado.py index 653ccf6..adf9605 100644 --- a/PyTado/interface/api/my_tado.py +++ b/PyTado/interface/api/my_tado.py @@ -607,26 +607,23 @@ def get_running_times(self, date=datetime.datetime.now().strftime("%Y-%m-%d")) - return self._http.request(request) - def get_boiler_output_temperature(self, bridge_id: str): + def get_boiler_install_state(self, bridge_id: str, auth_key: str): """ - Get the bridge details + Get the boiler wiring installation state from home by bridge endpoint """ request = TadoRequest() request.action = Action.GET request.domain = Domain.HOME_BY_BRIDGE request.device = bridge_id - request.command = "boilerWiringInstallationState" - install_state = self._http.request(request) + request.params = {"authKey": auth_key} - return { - "boiler": install_state["boiler"], - } + return self._http.request(request) - def get_boiler_max_output_temperature(self, bridge_id: str): + def get_boiler_max_output_temperature(self, bridge_id: str, auth_key: str): """ - Get the bridge details + Get the boiler max output temperature from home by bridge endpoint """ request = TadoRequest() @@ -634,5 +631,6 @@ def get_boiler_max_output_temperature(self, bridge_id: str): request.domain = Domain.HOME_BY_BRIDGE request.device = bridge_id request.command = "boilerMaxOutputTemperature" + request.params = {"authKey": auth_key} return self._http.request(request) diff --git a/tests/test_my_tado.py b/tests/test_my_tado.py index 12ff30f..a6f1962 100644 --- a/tests/test_my_tado.py +++ b/tests/test_my_tado.py @@ -87,14 +87,16 @@ def test_get_running_times(self): assert running_times["lastUpdated"] == "2023-08-05T19:50:21Z" assert running_times["runningTimes"][0]["zones"][0]["id"] == 1 - def test_get_boiler_output_temperature(self): + def test_get_boiler_install_state(self): with mock.patch( "PyTado.http.Http.request", return_value=json.loads( common.load_fixture("home_by_bridge.boiler_wiring_installation_state.json") ), ): - boiler_temperature = self.tado_client.get_boiler_output_temperature("IB123456789") + boiler_temperature = self.tado_client.get_boiler_install_state( + "IB123456789", "authcode" + ) assert self.tado_client._http.request.called assert boiler_temperature["boiler"]["outputTemperature"]["celsius"] == 38.01 @@ -106,7 +108,9 @@ def test_get_boiler_max_output_temperature(self): common.load_fixture("home_by_bridge.boiler_max_output_temperature.json") ), ): - boiler_temperature = self.tado_client.get_boiler_max_output_temperature("IB123456789") + boiler_temperature = self.tado_client.get_boiler_max_output_temperature( + "IB123456789", "authcode" + ) assert self.tado_client._http.request.called assert boiler_temperature["boilerMaxOutputTemperatureInCelsius"] == 50.0 From 7430c96c9d5890e19f6f46dcfe2e513704571928 Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Sat, 28 Dec 2024 16:02:40 +0100 Subject: [PATCH 3/5] add set boiler max temperature - with proper testing --- PyTado/interface/api/my_tado.py | 17 +++++++++++++++ tests/test_my_tado.py | 37 ++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/PyTado/interface/api/my_tado.py b/PyTado/interface/api/my_tado.py index adf9605..6f87d05 100644 --- a/PyTado/interface/api/my_tado.py +++ b/PyTado/interface/api/my_tado.py @@ -634,3 +634,20 @@ def get_boiler_max_output_temperature(self, bridge_id: str, auth_key: str): request.params = {"authKey": auth_key} return self._http.request(request) + + def set_boiler_max_output_temperature( + self, bridge_id: str, auth_key: str, temperature_in_celcius: int + ): + """ + Set the boiler max output temperature with home by bridge endpoint + """ + + request = TadoRequest() + request.action = Action.CHANGE + request.domain = Domain.HOME_BY_BRIDGE + request.device = bridge_id + request.command = "boilerMaxOutputTemperature" + request.params = {"authKey": auth_key} + request.payload = {"boilerMaxOutputTemperatureInCelsius": temperature_in_celcius} + + return self._http.request(request) diff --git a/tests/test_my_tado.py b/tests/test_my_tado.py index a6f1962..c806055 100644 --- a/tests/test_my_tado.py +++ b/tests/test_my_tado.py @@ -6,7 +6,7 @@ from . import common -from PyTado.http import Http +from PyTado.http import Http, TadoRequest from PyTado.interface.api import Tado @@ -80,10 +80,11 @@ def test_get_running_times(self): with mock.patch( "PyTado.http.Http.request", return_value=json.loads(common.load_fixture("running_times.json")), - ): + ) as mock_request: running_times = self.tado_client.get_running_times("2023-08-01") - assert self.tado_client._http.request.called + mock_request.assert_called_once() + assert running_times["lastUpdated"] == "2023-08-05T19:50:21Z" assert running_times["runningTimes"][0]["zones"][0]["id"] == 1 @@ -93,12 +94,13 @@ def test_get_boiler_install_state(self): return_value=json.loads( common.load_fixture("home_by_bridge.boiler_wiring_installation_state.json") ), - ): + ) as mock_request: boiler_temperature = self.tado_client.get_boiler_install_state( "IB123456789", "authcode" ) - assert self.tado_client._http.request.called + mock_request.assert_called_once() + assert boiler_temperature["boiler"]["outputTemperature"]["celsius"] == 38.01 def test_get_boiler_max_output_temperature(self): @@ -107,10 +109,31 @@ def test_get_boiler_max_output_temperature(self): return_value=json.loads( common.load_fixture("home_by_bridge.boiler_max_output_temperature.json") ), - ): + ) as mock_request: boiler_temperature = self.tado_client.get_boiler_max_output_temperature( "IB123456789", "authcode" ) - assert self.tado_client._http.request.called + mock_request.assert_called_once() + assert boiler_temperature["boilerMaxOutputTemperatureInCelsius"] == 50.0 + + def test_set_boiler_max_output_temperature(self): + with mock.patch( + "PyTado.http.Http.request", + return_value={"success": True}, + ) as mock_request: + response = self.tado_client.set_boiler_max_output_temperature( + "IB123456789", "authcode", 75 + ) + + mock_request.assert_called_once() + args, _ = mock_request.call_args + request: TadoRequest = args[0] + + self.assertEqual(request.command, "boilerMaxOutputTemperature") + self.assertEqual(request.action, "PUT") + self.assertEqual(request.payload, {"boilerMaxOutputTemperatureInCelsius": 75}) + + # Verify the response + self.assertTrue(response["success"]) From 8ca7610f808d16891054d45412733bf37ec8c127 Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Sat, 28 Dec 2024 18:13:02 +0100 Subject: [PATCH 4/5] changed temperatur to float --- PyTado/interface/api/my_tado.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyTado/interface/api/my_tado.py b/PyTado/interface/api/my_tado.py index 6f87d05..4044b85 100644 --- a/PyTado/interface/api/my_tado.py +++ b/PyTado/interface/api/my_tado.py @@ -636,7 +636,7 @@ def get_boiler_max_output_temperature(self, bridge_id: str, auth_key: str): return self._http.request(request) def set_boiler_max_output_temperature( - self, bridge_id: str, auth_key: str, temperature_in_celcius: int + self, bridge_id: str, auth_key: str, temperature_in_celcius: float ): """ Set the boiler max output temperature with home by bridge endpoint From 6055d5a19926ca3b868ffffc5a055c82dcf4d830 Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Sat, 28 Dec 2024 20:03:05 +0100 Subject: [PATCH 5/5] error in debug output fixed --- PyTado/http.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/PyTado/http.py b/PyTado/http.py index 442c6a3..21cc86b 100644 --- a/PyTado/http.py +++ b/PyTado/http.py @@ -164,12 +164,18 @@ def _log_response(self, response: requests.Response, *args, **kwargs) -> None: og_request_url = response.request.url og_request_headers = response.request.headers response_status = response.status_code + + if response.text is None or response.text == "": + response_data = {} + else: + response_data = response.json() + _LOGGER.debug( f"\nRequest:\n\tMethod:{og_request_method}" f"\n\tURL: {og_request_url}" f"\n\tHeaders: {pprint.pformat(og_request_headers)}" f"\nResponse:\n\tStatusCode: {response_status}" - f"\n\tData: {response.json()}" + f"\n\tData: {response_data}" ) def request(self, request: TadoRequest) -> dict[str, Any]: