From 5c02a365485ee2fb1c5729b4c0d90d6b37c88ae1 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie <35773677+BeatsuDev@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:52:37 +0200 Subject: [PATCH 01/26] test: remove a lot of unnecessary checks --- tests/realtime/test_live_measurements.py | 25 ------------------------ 1 file changed, 25 deletions(-) diff --git a/tests/realtime/test_live_measurements.py b/tests/realtime/test_live_measurements.py index a27378b..4964b24 100644 --- a/tests/realtime/test_live_measurements.py +++ b/tests/realtime/test_live_measurements.py @@ -37,31 +37,6 @@ async def callback(data): timestamp = timestamp.replace(tzinfo=None) now = datetime.now().replace(tzinfo=None) assert timestamp > now - timedelta(seconds=30) - assert data.power > 0 - assert data.last_meter_consumption > 0 - assert data.accumulated_consumption > 0 - assert isinstance(data.accumulated_production, (int, float)) - assert data.accumulated_consumption_last_hour > 0 - assert isinstance(data.accumulated_production_last_hour, (int, float)) - assert data.accumulated_cost > 0 - assert isinstance(data.accumulated_reward, (int, float)) - assert data.currency == "SEK" - assert data.min_power >= 0 - assert data.max_power > 0 - assert data.average_power > 0 - assert isinstance(data.power_production, (int, float)) - assert isinstance(data.power_reactive, (int, float)) - assert data.power_production_reactive > 0 - assert isinstance(data.min_power_production, (int, float)) - assert isinstance(data.max_power_production, (int, float)) - assert data.last_meter_production > 0 - assert data.power_factor > 0 - assert data.voltage_phase_1 > 0 - assert data.voltage_phase_2 > 0 - assert data.voltage_phase_3 > 0 - assert data.currentL1 > 0 - assert data.currentL2 > 0 - assert data.currentL3 > 0 # Return immediately after the first callback home.start_live_feed(f"tibber.py-tests/{__version__}", exit_condition = lambda data: True) From c90297bdc5dfc987fb02eb06dde52996cea68111 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie <35773677+BeatsuDev@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:54:12 +0200 Subject: [PATCH 02/26] test: add timeouts for realtime tests --- tests/realtime/test_live_measurements.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/realtime/test_live_measurements.py b/tests/realtime/test_live_measurements.py index 4964b24..e845bf8 100644 --- a/tests/realtime/test_live_measurements.py +++ b/tests/realtime/test_live_measurements.py @@ -14,6 +14,7 @@ def test_adding_listener_with_unknown_event_raises_exception(home): async def callback(data): print(data) +@pytest.mark.timeout(60) def test_starting_live_feed_with_no_listeners_shows_warning(caplog): account = tibber.Account(tibber.DEMO_TOKEN) home = account.homes[0] @@ -22,12 +23,14 @@ def test_starting_live_feed_with_no_listeners_shows_warning(caplog): home.start_live_feed(f"tibber.py-tests/{__version__}", exit_condition = lambda data: True) assert "The event that was broadcasted has no listeners / callbacks! Nothing was run." in caplog.text +@pytest.mark.timeout(60) def test_retrieving_live_measurements(): account = tibber.Account(tibber.DEMO_TOKEN) home = account.homes[0] global callback_was_run callback_was_run = False + @home.event("live_measurement") async def callback(data): global callback_was_run From 7a0de657d55ccabe62d33be664bd2572be1a056c Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Wed, 8 Feb 2023 20:43:14 +0100 Subject: [PATCH 03/26] Added on_exception argument to start_live_feed() --- tibber/types/home.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 468af30..71e32cf 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -249,6 +249,7 @@ def start_live_feed( exit_condition: Callable[[LiveMeasurement], bool] = None, retries: int = 3, retry_interval: Union[float, int] = 10, + on_exception: Callable[[Exception], None] = None, **kwargs, ) -> None: """Creates a websocket and starts pushing data out to registered callbacks. @@ -278,11 +279,11 @@ def start_live_feed( try: if loop and loop.is_running(): loop.run_until_complete( - self.start_websocket_loop(exit_condition, retries=retries, **kwargs) + self.start_websocket_loop(exit_condition, retries=retries, on_exception=on_exception, **kwargs) ) else: asyncio.run( - self.start_websocket_loop(exit_condition, retries=retries, **kwargs) + self.start_websocket_loop(exit_condition, retries=retries, on_exception=on_exception, **kwargs) ) except KeyboardInterrupt: _logger.info("Keyboard interrupt detected. Websocket should be closed now.") @@ -292,6 +293,7 @@ async def start_websocket_loop( exit_condition: Callable[[LiveMeasurement], bool] = None, retries: int = 3, retry_interval: Union[float, int] = 10, + on_exception: Callable[[Exception], None] = None, **kwargs, ) -> None: """Starts a websocket to subscribe for live measurements. @@ -323,6 +325,16 @@ async def start_websocket_loop( fetch_schema_from_transport=True, ) + # Define exception handlers for the retry mechanism (using the backoff library) + if not on_exception: + on_exception = lambda details: _logger.warning( + "Retrying to connect with backoff. Running {target} in {wait:.1f} seconds after {tries} tries.".format( + **details + ) + ) + + + # Construct the retry mechanism for reconnecting to the websocket retry_connect = backoff.on_exception( backoff.expo, [ @@ -331,16 +343,13 @@ async def start_websocket_loop( ], max_value=100, max_tries=retries, - on_backoff=lambda details: _logger.warning( - "Retrying to connect with backoff. Running {target} in {wait:.1f} seconds after {tries} tries.".format( - **details - ) - ), + on_backoff=on_exception, jitter=backoff.full_jitter, giveup=lambda e: isinstance(e, TransportQueryError) or isinstance(e, ValueError), ) + # Construct the retry mechanism for subscribing to the websocket retry_subscribe = backoff.on_exception( backoff.expo, Exception, @@ -356,11 +365,14 @@ async def start_websocket_loop( or isinstance(e, ValueError), ) + # Connect to the websocket _logger.debug("connecting to websocket") session = await self._websocket_client.connect_async( reconnecting=True, retry_connect=retry_connect, ) + + # Subscribe to the websocket _logger.info("Connected to websocket.") await retry_subscribe(self.run_websocket_loop)(session, exit_condition) From 98b302b3aa1232fc4497715eba3730a98260f441 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sat, 11 Feb 2023 19:23:59 +0100 Subject: [PATCH 04/26] Removed backoff from home.py --- tibber/types/home.py | 70 ++++---------------------------------------- 1 file changed, 6 insertions(+), 64 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 71e32cf..1440a7a 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -6,7 +6,6 @@ import logging from typing import TYPE_CHECKING, Callable, Union -import backoff import gql import websockets from gql.transport.exceptions import TransportQueryError @@ -249,11 +248,12 @@ def start_live_feed( exit_condition: Callable[[LiveMeasurement], bool] = None, retries: int = 3, retry_interval: Union[float, int] = 10, - on_exception: Callable[[Exception], None] = None, + on_error: Callable[[Exception], None] = None, **kwargs, ) -> None: """Creates a websocket and starts pushing data out to registered callbacks. + :param user_agent: The user agent to use when connecting to the websocket. :param exit_condition: A function that takes a LiveMeasurement as input and returns a boolean. If the function returns True, the websocket will be closed. :param retries: The number of times to retry connecting to the websocket if it fails. @@ -291,7 +291,7 @@ def start_live_feed( async def start_websocket_loop( self, exit_condition: Callable[[LiveMeasurement], bool] = None, - retries: int = 3, + retries: int = 5, retry_interval: Union[float, int] = 10, on_exception: Callable[[Exception], None] = None, **kwargs, @@ -302,6 +302,7 @@ async def start_websocket_loop( If the function returns True, the websocket will be closed. :param retries: The number of times to retry connecting to the websocket if it fails. :param retry_interval: The interval in seconds to wait before retrying to connect to the websocket. + :param kwargs: Additional arguments to pass to the websocket (gql.transport.WebsocketsTransport). """ if retry_interval < 1: @@ -325,46 +326,6 @@ async def start_websocket_loop( fetch_schema_from_transport=True, ) - # Define exception handlers for the retry mechanism (using the backoff library) - if not on_exception: - on_exception = lambda details: _logger.warning( - "Retrying to connect with backoff. Running {target} in {wait:.1f} seconds after {tries} tries.".format( - **details - ) - ) - - - # Construct the retry mechanism for reconnecting to the websocket - retry_connect = backoff.on_exception( - backoff.expo, - [ - gql.transport.exceptions.TransportClosed, - websockets.exceptions.ConnectionClosedError, - ], - max_value=100, - max_tries=retries, - on_backoff=on_exception, - jitter=backoff.full_jitter, - giveup=lambda e: isinstance(e, TransportQueryError) - or isinstance(e, ValueError), - ) - - # Construct the retry mechanism for subscribing to the websocket - retry_subscribe = backoff.on_exception( - backoff.expo, - Exception, - max_value=100, - max_tries=retries, - on_backoff=lambda details: _logger.warning( - "Retrying to subscribe with backoff. Running {target} in {wait:.1f} seconds after {tries} tries.".format( - **details - ) - ), - jitter=backoff.full_jitter, - giveup=lambda e: isinstance(e, TransportQueryError) - or isinstance(e, ValueError), - ) - # Connect to the websocket _logger.debug("connecting to websocket") session = await self._websocket_client.connect_async( @@ -374,7 +335,8 @@ async def start_websocket_loop( # Subscribe to the websocket _logger.info("Connected to websocket.") - await retry_subscribe(self.run_websocket_loop)(session, exit_condition) + await self.run_websocket_loop(session, exit_condition) + await session.close_async() async def run_websocket_loop(self, session, exit_condition) -> None: # Check if real time consumption is enabled @@ -405,8 +367,6 @@ async def run_websocket_loop(self, session, exit_condition) -> None: _logger.info("Exit condition met. The live loop is now exiting.") break - await self.close_websocket_connection() - async def process_websocket_response( self, data: dict, exit_condition: Callable[[LiveMeasurement], bool] = None ) -> bool: @@ -440,24 +400,6 @@ async def broadcast_event(self, event, data) -> None: await asyncio.gather(*[c(data) for c in self._callbacks[event]]) - async def close_websocket_connection(self) -> None: - _logger.debug("attempting to close websocket connection") - if self.websocket_running: - try: - await self._websocket_client.close_async() - self._websocket_client = None # Dereference for gc - _logger.info("Websocket connection closed.") - except KeyboardInterrupt as e: - _logger.warning( - "Keyboard interrupt detected while closing wbsocket connection. This may cause the websocket to be left open." - ) - raise e - else: - _logger.info( - "The websocket was not running when attempting to close the websocket." - + " The invocation of close_websocket_connection() therefore did nothing..." - ) - @property def websocket_running(self) -> bool: """Returns True if the websocket is running. False otherwise.""" From c507a3149dd21b60dca69789c878aeff5db948a4 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sat, 11 Feb 2023 19:24:50 +0100 Subject: [PATCH 05/26] Removed retry interval (this is set automatically with backoff) --- tibber/types/home.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 1440a7a..d4bfe17 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -247,7 +247,6 @@ def start_live_feed( user_agent=None, exit_condition: Callable[[LiveMeasurement], bool] = None, retries: int = 3, - retry_interval: Union[float, int] = 10, on_error: Callable[[Exception], None] = None, **kwargs, ) -> None: @@ -257,7 +256,6 @@ def start_live_feed( :param exit_condition: A function that takes a LiveMeasurement as input and returns a boolean. If the function returns True, the websocket will be closed. :param retries: The number of times to retry connecting to the websocket if it fails. - :param retry_interval: The interval in seconds to wait before retrying to connect to the websocket. :param kwargs: Additional arguments to pass to the websocket (gql.transport.WebsocketsTransport). """ if not self.features.real_time_consumption_enabled: @@ -292,7 +290,6 @@ async def start_websocket_loop( self, exit_condition: Callable[[LiveMeasurement], bool] = None, retries: int = 5, - retry_interval: Union[float, int] = 10, on_exception: Callable[[Exception], None] = None, **kwargs, ) -> None: @@ -301,13 +298,9 @@ async def start_websocket_loop( :param exit_condition: A function that takes a LiveMeasurement as input and returns a boolean. If the function returns True, the websocket will be closed. :param retries: The number of times to retry connecting to the websocket if it fails. - :param retry_interval: The interval in seconds to wait before retrying to connect to the websocket. :param kwargs: Additional arguments to pass to the websocket (gql.transport.WebsocketsTransport). """ - if retry_interval < 1: - raise ValueError("The retry interval must be at least 1 second.") - # Create the websocket transport = WebsocketsTransport( **kwargs, From 562505745c19d84e29263bc8dc97c3065f9b583c Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sat, 11 Feb 2023 19:26:12 +0100 Subject: [PATCH 06/26] Removed ping interval and ping timeout --- tibber/types/home.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index d4bfe17..28ff648 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -310,8 +310,6 @@ async def start_websocket_loop( headers={ "User-Agent": f"{self.tibber_client.user_agent} tibber.py/{__version__}" }, - ping_interval=10, - pong_timeout=10, ) self._websocket_client = gql.Client( From 79c7d5386dd073af0ad52f67fdf1082ab658c7d7 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sat, 11 Feb 2023 19:28:13 +0100 Subject: [PATCH 07/26] Set default retries value to 5 --- tibber/types/home.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 28ff648..95cb2af 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -1,6 +1,7 @@ from __future__ import annotations """Classes representing the Home type from the GraphQL Tibber API.""" +import random import asyncio import inspect import logging @@ -246,7 +247,7 @@ def start_live_feed( self, user_agent=None, exit_condition: Callable[[LiveMeasurement], bool] = None, - retries: int = 3, + retries: int = 5, on_error: Callable[[Exception], None] = None, **kwargs, ) -> None: @@ -318,14 +319,17 @@ async def start_websocket_loop( ) # Connect to the websocket + retry_count = 0 + while retry_count < retries: + _logger.debug("connecting to websocket") session = await self._websocket_client.connect_async( reconnecting=True, retry_connect=retry_connect, ) + _logger.info("Connected to websocket.") # Subscribe to the websocket - _logger.info("Connected to websocket.") await self.run_websocket_loop(session, exit_condition) await session.close_async() From 50eea94bb19dfd70e1d296e0a3d6ae9dd732dad6 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sat, 11 Feb 2023 21:00:39 +0100 Subject: [PATCH 08/26] Removed while loop --- tibber/types/home.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 95cb2af..5c7718a 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -319,9 +319,6 @@ async def start_websocket_loop( ) # Connect to the websocket - retry_count = 0 - while retry_count < retries: - _logger.debug("connecting to websocket") session = await self._websocket_client.connect_async( reconnecting=True, From 2660216c2c6d0d590fcf4430fe2a51f3ac54fa93 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sat, 11 Feb 2023 21:56:57 +0100 Subject: [PATCH 09/26] Added on_connect and on_query exception handler parameters --- tibber/types/home.py | 45 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 5c7718a..67d23db 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -248,7 +248,8 @@ def start_live_feed( user_agent=None, exit_condition: Callable[[LiveMeasurement], bool] = None, retries: int = 5, - on_error: Callable[[Exception], None] = None, + on_connection_error: Callable[[Exception], None] = None, + on_query_error: Callable[[Exception], None] = None, **kwargs, ) -> None: """Creates a websocket and starts pushing data out to registered callbacks. @@ -257,6 +258,10 @@ def start_live_feed( :param exit_condition: A function that takes a LiveMeasurement as input and returns a boolean. If the function returns True, the websocket will be closed. :param retries: The number of times to retry connecting to the websocket if it fails. + :param on_connection_error: A function that is run when an error occurs while connecting to the websocket. + This will be called every time a connection attempt fails. + :param on_query_error: A function that is run when query to the websocket returns an error. + This will be called every time a query fails. :param kwargs: Additional arguments to pass to the websocket (gql.transport.WebsocketsTransport). """ if not self.features.real_time_consumption_enabled: @@ -270,28 +275,27 @@ def start_live_feed( self.tibber_client.user_agent = user_agent or self.tibber_client.user_agent # The folllowing code is just to run the websocket loop in the correct loop. + websocket_loop_coroutine = self.start_websocket_loop( + exit_condition, retries=retries, on_exception=on_exception, **kwargs + ) + _run_async_in_correct_event_loop(websocket_loop_coroutine) + + def _run_async_in_correct_event_loop(self, coroutine): try: loop = asyncio.get_running_loop() except RuntimeError: loop = None - try: - if loop and loop.is_running(): - loop.run_until_complete( - self.start_websocket_loop(exit_condition, retries=retries, on_exception=on_exception, **kwargs) - ) - else: - asyncio.run( - self.start_websocket_loop(exit_condition, retries=retries, on_exception=on_exception, **kwargs) - ) - except KeyboardInterrupt: - _logger.info("Keyboard interrupt detected. Websocket should be closed now.") + if loop and loop.is_running(): + return loop.run_until_complete(coroutine) + else: + return asyncio.run(coroutine) async def start_websocket_loop( self, exit_condition: Callable[[LiveMeasurement], bool] = None, retries: int = 5, - on_exception: Callable[[Exception], None] = None, + on_query_error: Callable[[Exception], None] = None, **kwargs, ) -> None: """Starts a websocket to subscribe for live measurements. @@ -299,7 +303,8 @@ async def start_websocket_loop( :param exit_condition: A function that takes a LiveMeasurement as input and returns a boolean. If the function returns True, the websocket will be closed. :param retries: The number of times to retry connecting to the websocket if it fails. - + :param on_query_error: A function that is run when query to the websocket returns an error. + This will be called every time a query fails. :param kwargs: Additional arguments to pass to the websocket (gql.transport.WebsocketsTransport). """ # Create the websocket @@ -327,10 +332,10 @@ async def start_websocket_loop( _logger.info("Connected to websocket.") # Subscribe to the websocket - await self.run_websocket_loop(session, exit_condition) + await self._run_websocket_loop(session, exit_condition) await session.close_async() - async def run_websocket_loop(self, session, exit_condition) -> None: + async def _run_websocket_loop(self, session, exit_condition) -> None: # Check if real time consumption is enabled _logger.info( "Updating home information to check if real time consumption is enabled." @@ -352,14 +357,14 @@ async def run_websocket_loop(self, session, exit_condition) -> None: _logger.debug("real time data received.") # Returns True if exit condition is met - exit_condition_met = await self.process_websocket_response( + exit_condition_met = await self._process_websocket_response( data, exit_condition=exit_condition ) if exit_condition_met: _logger.info("Exit condition met. The live loop is now exiting.") break - async def process_websocket_response( + async def _process_websocket_response( self, data: dict, exit_condition: Callable[[LiveMeasurement], bool] = None ) -> bool: """Processes a response with data from the live data websocket. This function will call all registered callbacks @@ -371,14 +376,14 @@ async def process_websocket_response( # Broadcast the event # TODO: Differentiate between consumption data, production data and other data. cleaned_data = LiveMeasurement(data["liveMeasurement"], self.tibber_client) - await self.broadcast_event("live_measurement", cleaned_data) + await self._broadcast_event("live_measurement", cleaned_data) # Check if the exit condition is met if exit_condition and exit_condition(cleaned_data): return True return False - async def broadcast_event(self, event, data) -> None: + async def _broadcast_event(self, event, data) -> None: if event not in self._callbacks: _logger.warning( f'The event "{event}" was attempted emitted, but does not exist. Nothing was run.' From 2d2ea814c506038dd46f29f7f5b46b8773e3f8f0 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sun, 12 Feb 2023 16:10:29 +0100 Subject: [PATCH 10/26] Added a section about error handling to the README --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e50e21e..2bd0953 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > Note that this is NOT [pyTibber](https://github.com/Danielhiversen/pyTibber) which is the official python wrapper endorsed by Tibber themselves. tibber.py is an unofficial python wrapper package for communication with the [Tibber API](https://developer.tibber.com/). -This package aims to cover all functionalities of the Tibber API in the most beginner-friendly modern Pythonic way. You can read all the capabilites of the API and explore it +This package aims to cover all functionalities of the Tibber API in the most beginner-friendly modern Pythonic way. You can read all the capabilites of the API and explore it with [Tibbers' API explorer](https://developer.tibber.com/explorer). For documentation on how to use tibber.py head over to https://tibberpy.readthedocs.io/en/latest/. Every field of the API types should be found in the corresponding `tibber.type` (e.g. the `size: Int` field of `Home` @@ -21,21 +21,24 @@ docs (located on the right side of the Tibber API explorer). [![Pytest Python 3.7 / 3.11](https://github.com/BeatsuDev/tibber.py/actions/workflows/pytests.yml/badge.svg)](https://github.com/BeatsuDev/tibber.py/actions/workflows/pytests.yml) ![Publish to PyPi status](https://github.com/BeatsuDev/tibber.py/actions/workflows/publish-to-pypi.yml/badge.svg) - - Do you want to ask a question, report an issue, or even showcase your project that uses tibber.py? 🤩
Find out where to post by [checking out this overview](https://github.com/BeatsuDev/tibber.py/discussions/46). - ## Installation + ### Install via pip + ``` python -m pip install tibber.py ``` + ### Requirements + tibber.py depends on `gql`, `gql[aiohttp]`, `gql[websockets]` and `graphql-core`. tibber.py supports Python versions 3.7 and up! ## Examples + ### Getting basic account data + ```python import tibber @@ -56,6 +59,7 @@ print(account.name) ``` ### Getting basic home data + ```python import tibber @@ -75,6 +79,7 @@ print(home.main_fuse_size) # 25 ``` ### Reading historical data + ```python import tibber @@ -99,8 +104,10 @@ for hour in hour_data: ``` ### Reading live measurements + Note how you can register multiple callbacks for the same event. These will be run in asynchronously (at the same time)! + ```python import tibber @@ -115,11 +122,52 @@ async def show_current_power(data): @home.event("live_measurement") async def show_accumulated_cost(data): print(f"{data.accumulated_cost} {data.currency}") - + def when_to_stop(data): return data.power < 1500 # Start the live feed. This runs until data.power is less than 1500. # If a user agent was not defined earlier, this will be required here -home.start_live_feed(user_agent = "UserAgent/0.0.1", exit_condition = when_to_stop) +home.start_live_feed(user_agent = "UserAgent/0.0.1", exit_condition = when_to_stop) +``` + +### Handling errors in a websocket connection + +```python +import tibber + +from tibber.exceptions import ConnectionErrorList +from tibber.exceptions import QueryErrorList +from tibber.exceptions import MalformedQueryException + +account = tibber.Account(tibber.DEMO_TOKEN) +home = account.homes[0] + +@home.event("live_measurement") +async def show_current_power(data): + print(data.power) + + +# Define error handlers: +def connection_error_handler(errors: ConnectionErrorList, additional_data: dict): + for error in errors: + print(error) + + # Returning true will make tibber.py continue retrying the connection + # to the websocket until max retries has been reached. + # Returning False will exit the real time data feed regardless of + # how many connection attempts has already been done. + return True + +def query_error_handler(errors: QueryErrorList, additional_data: dict): + if isinstance(error, MalformedQueryException): + print(data.query) # Prints the query that failed + return False # Exit the real time data feed when a MalformedQueryException happens + return True + +home.start_live_feed( + user_agent = "UserAgent/0.0.1", + on_connection_error = connection_error_handler # Runs when the websocket fails to connect + on_query_error = query_error_handler # Run when the subscription query returns an error +) ``` From e0dd44f1b447870f37270ca4433abdd59d477e99 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Sun, 12 Feb 2023 16:19:05 +0100 Subject: [PATCH 11/26] Implemented starts of on_connection_error handler --- tibber/types/home.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 67d23db..a92e965 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -210,6 +210,10 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._websocket_client = None self._callbacks = {"live_measurement": []} + self._connection_retries = ( + 0 # The amount of times the websocket connection has been retried. + ) + self._query_retries = 0 # The amount of times the query has been retried. def event(self, event_to_listen_for) -> Callable: """Returns a decorator that registers the function being @@ -274,11 +278,18 @@ def start_live_feed( self.tibber_client.user_agent = user_agent or self.tibber_client.user_agent - # The folllowing code is just to run the websocket loop in the correct loop. - websocket_loop_coroutine = self.start_websocket_loop( - exit_condition, retries=retries, on_exception=on_exception, **kwargs - ) - _run_async_in_correct_event_loop(websocket_loop_coroutine) + # Keep trying to connect to the websocket until it succeeds or has tried `retries` times. + while self._connection_retries < retries: + try: + websocket_loop_coroutine = self.start_websocket_loop( + exit_condition, retries=retries, on_exception=on_exception, **kwargs + ) + self._run_async_in_correct_event_loop(websocket_loop_coroutine) + except Exception as e: + if not on_connection_error: + raise e + on_connection_error(e) + self._connection_retries += 1 def _run_async_in_correct_event_loop(self, coroutine): try: @@ -325,12 +336,11 @@ async def start_websocket_loop( # Connect to the websocket _logger.debug("connecting to websocket") - session = await self._websocket_client.connect_async( - reconnecting=True, - retry_connect=retry_connect, - ) + session = await self._websocket_client.connect_async() _logger.info("Connected to websocket.") + self._connection_retries = 0 # Connection was successful. Reset the counter. + # Subscribe to the websocket await self._run_websocket_loop(session, exit_condition) await session.close_async() From dd968d63c055e1767109c50d0bb8427ab21c39c2 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 01:46:41 +0100 Subject: [PATCH 12/26] Added connection_retries and query_retries --- tibber/types/home.py | 55 ++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index a92e965..d269266 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -5,11 +5,9 @@ import asyncio import inspect import logging -from typing import TYPE_CHECKING, Callable, Union +from typing import TYPE_CHECKING, Callable import gql -import websockets -from gql.transport.exceptions import TransportQueryError from gql.transport.websockets import WebsocketsTransport from graphql import parse @@ -210,10 +208,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._websocket_client = None self._callbacks = {"live_measurement": []} - self._connection_retries = ( + self._connection_retry_attempts = ( 0 # The amount of times the websocket connection has been retried. ) - self._query_retries = 0 # The amount of times the query has been retried. + self._query_retry_attempts = ( + 0 # The amount of times the query has been retried. + ) def event(self, event_to_listen_for) -> Callable: """Returns a decorator that registers the function being @@ -235,7 +235,7 @@ def decorator(callback): ) # If the key is not found - the event is not a valid event! - # Valid events will be added directly to the line where _callbacks is initialized. + # In the future, valid events will be added directly to the line where _callbacks is initialized. try: self._callbacks[event_to_listen_for].append(callback) except KeyError: @@ -251,7 +251,8 @@ def start_live_feed( self, user_agent=None, exit_condition: Callable[[LiveMeasurement], bool] = None, - retries: int = 5, + connection_retries: int = 5, + query_retries: int = 5, on_connection_error: Callable[[Exception], None] = None, on_query_error: Callable[[Exception], None] = None, **kwargs, @@ -261,7 +262,8 @@ def start_live_feed( :param user_agent: The user agent to use when connecting to the websocket. :param exit_condition: A function that takes a LiveMeasurement as input and returns a boolean. If the function returns True, the websocket will be closed. - :param retries: The number of times to retry connecting to the websocket if it fails. + :param connection_retries: The number of times to retry connecting to the websocket if it fails. + :param query_retries: The number of times to retry sending a query to the websocket if it fails. :param on_connection_error: A function that is run when an error occurs while connecting to the websocket. This will be called every time a connection attempt fails. :param on_query_error: A function that is run when query to the websocket returns an error. @@ -279,17 +281,24 @@ def start_live_feed( self.tibber_client.user_agent = user_agent or self.tibber_client.user_agent # Keep trying to connect to the websocket until it succeeds or has tried `retries` times. - while self._connection_retries < retries: + while self._connection_retry_attempts < connection_retries: + await asyncio.sleep( + min((2**self._connection_retry_attempts - 1) * random.random()), 100 + ) try: websocket_loop_coroutine = self.start_websocket_loop( - exit_condition, retries=retries, on_exception=on_exception, **kwargs + exit_condition, + query_retries=query_retries, + on_query_error=on_query_error, + **kwargs, ) self._run_async_in_correct_event_loop(websocket_loop_coroutine) except Exception as e: + self._connection_retry_attempts += 1 + # Raise the exception if no connection error handler is specified. if not on_connection_error: raise e on_connection_error(e) - self._connection_retries += 1 def _run_async_in_correct_event_loop(self, coroutine): try: @@ -305,15 +314,15 @@ def _run_async_in_correct_event_loop(self, coroutine): async def start_websocket_loop( self, exit_condition: Callable[[LiveMeasurement], bool] = None, - retries: int = 5, + query_retries: int = 5, on_query_error: Callable[[Exception], None] = None, **kwargs, ) -> None: - """Starts a websocket to subscribe for live measurements. + """Connects a websocket for live measurements. :param exit_condition: A function that takes a LiveMeasurement as input and returns a boolean. If the function returns True, the websocket will be closed. - :param retries: The number of times to retry connecting to the websocket if it fails. + :param query_retries: The number of times to retry connecting to the websocket if it fails. :param on_query_error: A function that is run when query to the websocket returns an error. This will be called every time a query fails. :param kwargs: Additional arguments to pass to the websocket (gql.transport.WebsocketsTransport). @@ -339,11 +348,23 @@ async def start_websocket_loop( session = await self._websocket_client.connect_async() _logger.info("Connected to websocket.") - self._connection_retries = 0 # Connection was successful. Reset the counter. + self._connection_retry_attempts = ( + 0 # Connection was successful. Reset the counter. + ) # Subscribe to the websocket - await self._run_websocket_loop(session, exit_condition) - await session.close_async() + while self._query_retry_attempts < query_retries: + await asyncio.sleep( + min((2**self._query_retry_attempts - 1) * random.random()), 100 + ) + try: + await self._run_websocket_loop(session, exit_condition) + except Exception as e: + if not on_query_error: + raise e + on_query_error(e) + self._query_retry_attempts += 1 + await self._websocket_client.close_async() async def _run_websocket_loop(self, session, exit_condition) -> None: # Check if real time consumption is enabled From 9e9202327e94171d5b5a2923f1cb39a873b4184b Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 01:48:48 +0100 Subject: [PATCH 13/26] Changed asyncio.sleep to time.sleep --- tibber/types/home.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index d269266..bee9bc0 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -1,10 +1,11 @@ from __future__ import annotations """Classes representing the Home type from the GraphQL Tibber API.""" -import random import asyncio import inspect import logging +import random +import time from typing import TYPE_CHECKING, Callable import gql @@ -282,7 +283,7 @@ def start_live_feed( # Keep trying to connect to the websocket until it succeeds or has tried `retries` times. while self._connection_retry_attempts < connection_retries: - await asyncio.sleep( + time.sleep( min((2**self._connection_retry_attempts - 1) * random.random()), 100 ) try: From c5968a204a801471f77483c491360624a6b36e80 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 01:50:45 +0100 Subject: [PATCH 14/26] Moved 100 value into min function --- tibber/types/home.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index bee9bc0..873674b 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -284,7 +284,7 @@ def start_live_feed( # Keep trying to connect to the websocket until it succeeds or has tried `retries` times. while self._connection_retry_attempts < connection_retries: time.sleep( - min((2**self._connection_retry_attempts - 1) * random.random()), 100 + min((2**self._connection_retry_attempts - 1) * random.random(), 100) ) try: websocket_loop_coroutine = self.start_websocket_loop( @@ -356,7 +356,7 @@ async def start_websocket_loop( # Subscribe to the websocket while self._query_retry_attempts < query_retries: await asyncio.sleep( - min((2**self._query_retry_attempts - 1) * random.random()), 100 + min((2**self._query_retry_attempts - 1) * random.random(), 100) ) try: await self._run_websocket_loop(session, exit_condition) From b7d88d8ab714c35ced8b8359d46efd08be3172c4 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:03:02 +0100 Subject: [PATCH 15/26] Added a running property to home and set it to False when exit condition is met --- tibber/types/home.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 873674b..d9e0235 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -207,6 +207,7 @@ class TibberHome(NonDecoratedTibberHome): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.running = False self._websocket_client = None self._callbacks = {"live_measurement": []} self._connection_retry_attempts = ( @@ -281,8 +282,9 @@ def start_live_feed( self.tibber_client.user_agent = user_agent or self.tibber_client.user_agent - # Keep trying to connect to the websocket until it succeeds or has tried `retries` times. - while self._connection_retry_attempts < connection_retries: + self.running = True + # Keep trying to connect to the websocket until it succeeds or has tried `retries` times + while self._connection_retry_attempts < connection_retries and self.running: time.sleep( min((2**self._connection_retry_attempts - 1) * random.random(), 100) ) @@ -354,7 +356,7 @@ async def start_websocket_loop( ) # Subscribe to the websocket - while self._query_retry_attempts < query_retries: + while self._query_retry_attempts < query_retries and self.running: await asyncio.sleep( min((2**self._query_retry_attempts - 1) * random.random(), 100) ) @@ -394,6 +396,7 @@ async def _run_websocket_loop(self, session, exit_condition) -> None: ) if exit_condition_met: _logger.info("Exit condition met. The live loop is now exiting.") + self.running = False break async def _process_websocket_response( From 607ddac7090e2b165159f4efd21e73a27b0eda2b Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:16:28 +0100 Subject: [PATCH 16/26] Added logging and removed throwing exception if no error handler is defined --- tibber/types/home.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index d9e0235..725a4f9 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -285,9 +285,11 @@ def start_live_feed( self.running = True # Keep trying to connect to the websocket until it succeeds or has tried `retries` times while self._connection_retry_attempts < connection_retries and self.running: - time.sleep( - min((2**self._connection_retry_attempts - 1) * random.random(), 100) - ) + to_sleep = min((2**self._connection_retry_attempts - 1) * random.random(), 100) + if self._connection_retry_attempts > 0: + _logger.warning(f"Retrying to CONNECT in {to_sleep:.1f} seconds. This is retry number {self._connection_retry_attempts}.") + time.sleep(to_sleep) + try: websocket_loop_coroutine = self.start_websocket_loop( exit_condition, @@ -298,10 +300,10 @@ def start_live_feed( self._run_async_in_correct_event_loop(websocket_loop_coroutine) except Exception as e: self._connection_retry_attempts += 1 + _logger.warning("Exception occured when attempting to CONNECT to the websocket!: " + str(e)) # Raise the exception if no connection error handler is specified. - if not on_connection_error: - raise e - on_connection_error(e) + if on_connection_error: + on_connection_error(e) def _run_async_in_correct_event_loop(self, coroutine): try: @@ -357,16 +359,18 @@ async def start_websocket_loop( # Subscribe to the websocket while self._query_retry_attempts < query_retries and self.running: - await asyncio.sleep( - min((2**self._query_retry_attempts - 1) * random.random(), 100) - ) + to_sleep = min((2**self._connection_retry_attempts - 1) * random.random(), 100) + if self._connection_retry_attempts > 0: + _logger.warning(f"Retrying QUERY in {to_sleep:.1f} seconds. This is retry number {self._connection_retry_attempts}.") + await asyncio.sleep(to_sleep) try: await self._run_websocket_loop(session, exit_condition) except Exception as e: - if not on_query_error: - raise e - on_query_error(e) self._query_retry_attempts += 1 + _logger.warning("Exception occured when attempting to send subscription QUERY!: " + str(e)) + if on_query_error: + on_query_error(e) + await self._websocket_client.close_async() async def _run_websocket_loop(self, session, exit_condition) -> None: From ba0cd41f23333020a109d2e76760eeb2f7729ebf Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:24:04 +0100 Subject: [PATCH 17/26] Show the exception type in logger warning --- tibber/types/home.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 725a4f9..5bc9f80 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -300,7 +300,7 @@ def start_live_feed( self._run_async_in_correct_event_loop(websocket_loop_coroutine) except Exception as e: self._connection_retry_attempts += 1 - _logger.warning("Exception occured when attempting to CONNECT to the websocket!: " + str(e)) + _logger.warning("Exception occured when attempting to CONNECT to the websocket!: " + e.__class__.__name__ + str(e)) # Raise the exception if no connection error handler is specified. if on_connection_error: on_connection_error(e) @@ -367,7 +367,7 @@ async def start_websocket_loop( await self._run_websocket_loop(session, exit_condition) except Exception as e: self._query_retry_attempts += 1 - _logger.warning("Exception occured when attempting to send subscription QUERY!: " + str(e)) + _logger.warning("Exception occured when attempting to send subscription QUERY!: " + e.__class__.__name__ + str(e)) if on_query_error: on_query_error(e) From 8642069ce76c3afd02b14a73fcafd40990f0ade8 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:27:00 +0100 Subject: [PATCH 18/26] Reset the query retry counter --- tibber/types/home.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tibber/types/home.py b/tibber/types/home.py index 5bc9f80..88ea9c2 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -356,6 +356,7 @@ async def start_websocket_loop( self._connection_retry_attempts = ( 0 # Connection was successful. Reset the counter. ) + self._query_retry_attempts = 0 # Subscribe to the websocket while self._query_retry_attempts < query_retries and self.running: @@ -393,6 +394,7 @@ async def _run_websocket_loop(self, session, exit_condition) -> None: _logger.info("Subscribing to websocket.") async for data in session.subscribe(document_node_query): _logger.debug("real time data received.") + self._query_retry_attempts = 0 # Query was successful. Reset the counter. # Returns True if exit condition is met exit_condition_met = await self._process_websocket_response( From 41c9ca616fc49b1aa40bf7b13dbdb4fa89be9cc6 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:29:12 +0100 Subject: [PATCH 19/26] Added space after exception name --- tibber/types/home.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 88ea9c2..42e5e08 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -300,7 +300,7 @@ def start_live_feed( self._run_async_in_correct_event_loop(websocket_loop_coroutine) except Exception as e: self._connection_retry_attempts += 1 - _logger.warning("Exception occured when attempting to CONNECT to the websocket!: " + e.__class__.__name__ + str(e)) + _logger.warning("Exception occured when attempting to CONNECT to the websocket!: [" + e.__class__.__name__ + "] " + str(e)) # Raise the exception if no connection error handler is specified. if on_connection_error: on_connection_error(e) @@ -368,7 +368,7 @@ async def start_websocket_loop( await self._run_websocket_loop(session, exit_condition) except Exception as e: self._query_retry_attempts += 1 - _logger.warning("Exception occured when attempting to send subscription QUERY!: " + e.__class__.__name__ + str(e)) + _logger.warning("Exception occured when attempting to send subscription QUERY!: [" + e.__class__.__name__ + "] " + str(e)) if on_query_error: on_query_error(e) From ee1761b9ff9f0e84d518c929c922f297ceb4b2b0 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:30:37 +0100 Subject: [PATCH 20/26] Fixed logger to output when QUERY will retry --- tibber/types/home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 42e5e08..2ca156d 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -361,7 +361,7 @@ async def start_websocket_loop( # Subscribe to the websocket while self._query_retry_attempts < query_retries and self.running: to_sleep = min((2**self._connection_retry_attempts - 1) * random.random(), 100) - if self._connection_retry_attempts > 0: + if self._query_retry_attempts > 0: _logger.warning(f"Retrying QUERY in {to_sleep:.1f} seconds. This is retry number {self._connection_retry_attempts}.") await asyncio.sleep(to_sleep) try: From 10bdf3e6eec4b66bfa7768fdae1f384c94d5037a Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:43:37 +0100 Subject: [PATCH 21/26] Retry connection if query fails --- tibber/types/home.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index 2ca156d..a1129cd 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -304,6 +304,9 @@ def start_live_feed( # Raise the exception if no connection error handler is specified. if on_connection_error: on_connection_error(e) + + if self._connection_retry_attempts >= connection_retries: + _logger.error(f"Failed to CONNECT to the websocket after {self._connection_retry_attempts} retries.") def _run_async_in_correct_event_loop(self, coroutine): try: @@ -356,13 +359,12 @@ async def start_websocket_loop( self._connection_retry_attempts = ( 0 # Connection was successful. Reset the counter. ) - self._query_retry_attempts = 0 # Subscribe to the websocket while self._query_retry_attempts < query_retries and self.running: - to_sleep = min((2**self._connection_retry_attempts - 1) * random.random(), 100) + to_sleep = min((2**self._query_retry_attempts - 1) * random.random(), 100) if self._query_retry_attempts > 0: - _logger.warning(f"Retrying QUERY in {to_sleep:.1f} seconds. This is retry number {self._connection_retry_attempts}.") + _logger.warning(f"Retrying QUERY in {to_sleep:.1f} seconds. This is retry number {self._query_retry_attempts}.") await asyncio.sleep(to_sleep) try: await self._run_websocket_loop(session, exit_condition) @@ -371,6 +373,13 @@ async def start_websocket_loop( _logger.warning("Exception occured when attempting to send subscription QUERY!: [" + e.__class__.__name__ + "] " + str(e)) if on_query_error: on_query_error(e) + + # If the query fails, we want to close the websocket and reconnect before trying again. + # Next time around, if the query fails again, we will try the query again but with a longer delay. + # (the query retry counter will stay incremented and will not reset until a successful query is made) + + # TODO: Note to future self: It might not be necessary to retry queries. Retrying the connection should be enough. + break await self._websocket_client.close_async() From d6fdd0e6349fb3fec5ec1edd14816ca75369c0e4 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:50:53 +0100 Subject: [PATCH 22/26] Make error messages how correct retries made --- tibber/types/home.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index a1129cd..3bfaa4a 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -305,8 +305,9 @@ def start_live_feed( if on_connection_error: on_connection_error(e) + # Show error message if the connection failed too many times. if self._connection_retry_attempts >= connection_retries: - _logger.error(f"Failed to CONNECT to the websocket after {self._connection_retry_attempts} retries.") + _logger.error(f"Could not connect to the websocket after {connection_retries - 1} retries.") def _run_async_in_correct_event_loop(self, coroutine): try: From 494b6222ae9e997271780f54dfbf35f593e32ae8 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:52:13 +0100 Subject: [PATCH 23/26] Added KeyboardInterrupt catch --- tibber/types/home.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tibber/types/home.py b/tibber/types/home.py index 3bfaa4a..4420580 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -298,6 +298,9 @@ def start_live_feed( **kwargs, ) self._run_async_in_correct_event_loop(websocket_loop_coroutine) + except KeyboardInterrupt: + self.running = False + _logger.info("Keyboard interrupt detected. Stopping live feed.") except Exception as e: self._connection_retry_attempts += 1 _logger.warning("Exception occured when attempting to CONNECT to the websocket!: [" + e.__class__.__name__ + "] " + str(e)) @@ -369,6 +372,9 @@ async def start_websocket_loop( await asyncio.sleep(to_sleep) try: await self._run_websocket_loop(session, exit_condition) + except KeyboardInterrupt: + self.running = False + _logger.info("Keyboard interrupt detected. Stopping live feed.") except Exception as e: self._query_retry_attempts += 1 _logger.warning("Exception occured when attempting to send subscription QUERY!: [" + e.__class__.__name__ + "] " + str(e)) From 9e653a7709da64049186c55ffcc6b4a85a0fc28e Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie Date: Thu, 23 Feb 2023 02:55:10 +0100 Subject: [PATCH 24/26] Added break statement to keyboardinterrupt catch --- tibber/types/home.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tibber/types/home.py b/tibber/types/home.py index 4420580..a2747b4 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -301,6 +301,7 @@ def start_live_feed( except KeyboardInterrupt: self.running = False _logger.info("Keyboard interrupt detected. Stopping live feed.") + break except Exception as e: self._connection_retry_attempts += 1 _logger.warning("Exception occured when attempting to CONNECT to the websocket!: [" + e.__class__.__name__ + "] " + str(e)) @@ -375,6 +376,7 @@ async def start_websocket_loop( except KeyboardInterrupt: self.running = False _logger.info("Keyboard interrupt detected. Stopping live feed.") + break except Exception as e: self._query_retry_attempts += 1 _logger.warning("Exception occured when attempting to send subscription QUERY!: [" + e.__class__.__name__ + "] " + str(e)) From 48d0aee02b868c3eec131b36841c5d1921959714 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie <35773677+BeatsuDev@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:08:22 +0200 Subject: [PATCH 25/26] test: remove a lot of unnecessary checks --- tests/realtime/test_live_measurements.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/realtime/test_live_measurements.py b/tests/realtime/test_live_measurements.py index e845bf8..f48fbdc 100644 --- a/tests/realtime/test_live_measurements.py +++ b/tests/realtime/test_live_measurements.py @@ -14,7 +14,6 @@ def test_adding_listener_with_unknown_event_raises_exception(home): async def callback(data): print(data) -@pytest.mark.timeout(60) def test_starting_live_feed_with_no_listeners_shows_warning(caplog): account = tibber.Account(tibber.DEMO_TOKEN) home = account.homes[0] @@ -23,7 +22,7 @@ def test_starting_live_feed_with_no_listeners_shows_warning(caplog): home.start_live_feed(f"tibber.py-tests/{__version__}", exit_condition = lambda data: True) assert "The event that was broadcasted has no listeners / callbacks! Nothing was run." in caplog.text -@pytest.mark.timeout(60) + def test_retrieving_live_measurements(): account = tibber.Account(tibber.DEMO_TOKEN) home = account.homes[0] From e19842f4db7d56e9089a64610f8c9282aee9cf97 Mon Sep 17 00:00:00 2001 From: Eric Bieszczad-Stie <35773677+BeatsuDev@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:18:33 +0200 Subject: [PATCH 26/26] chore: fix format errors --- tibber/types/home.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/tibber/types/home.py b/tibber/types/home.py index a2747b4..29b399a 100644 --- a/tibber/types/home.py +++ b/tibber/types/home.py @@ -285,9 +285,13 @@ def start_live_feed( self.running = True # Keep trying to connect to the websocket until it succeeds or has tried `retries` times while self._connection_retry_attempts < connection_retries and self.running: - to_sleep = min((2**self._connection_retry_attempts - 1) * random.random(), 100) + to_sleep = min( + (2**self._connection_retry_attempts - 1) * random.random(), 100 + ) if self._connection_retry_attempts > 0: - _logger.warning(f"Retrying to CONNECT in {to_sleep:.1f} seconds. This is retry number {self._connection_retry_attempts}.") + _logger.warning( + f"Retrying to CONNECT in {to_sleep:.1f} seconds. This is retry number {self._connection_retry_attempts}." + ) time.sleep(to_sleep) try: @@ -304,14 +308,21 @@ def start_live_feed( break except Exception as e: self._connection_retry_attempts += 1 - _logger.warning("Exception occured when attempting to CONNECT to the websocket!: [" + e.__class__.__name__ + "] " + str(e)) + _logger.warning( + "Exception occured when attempting to CONNECT to the websocket!: [" + + e.__class__.__name__ + + "] " + + str(e) + ) # Raise the exception if no connection error handler is specified. if on_connection_error: on_connection_error(e) - + # Show error message if the connection failed too many times. if self._connection_retry_attempts >= connection_retries: - _logger.error(f"Could not connect to the websocket after {connection_retries - 1} retries.") + _logger.error( + f"Could not connect to the websocket after {connection_retries - 1} retries." + ) def _run_async_in_correct_event_loop(self, coroutine): try: @@ -369,7 +380,9 @@ async def start_websocket_loop( while self._query_retry_attempts < query_retries and self.running: to_sleep = min((2**self._query_retry_attempts - 1) * random.random(), 100) if self._query_retry_attempts > 0: - _logger.warning(f"Retrying QUERY in {to_sleep:.1f} seconds. This is retry number {self._query_retry_attempts}.") + _logger.warning( + f"Retrying QUERY in {to_sleep:.1f} seconds. This is retry number {self._query_retry_attempts}." + ) await asyncio.sleep(to_sleep) try: await self._run_websocket_loop(session, exit_condition) @@ -379,10 +392,15 @@ async def start_websocket_loop( break except Exception as e: self._query_retry_attempts += 1 - _logger.warning("Exception occured when attempting to send subscription QUERY!: [" + e.__class__.__name__ + "] " + str(e)) + _logger.warning( + "Exception occured when attempting to send subscription QUERY!: [" + + e.__class__.__name__ + + "] " + + str(e) + ) if on_query_error: - on_query_error(e) - + on_query_error(e) + # If the query fails, we want to close the websocket and reconnect before trying again. # Next time around, if the query fails again, we will try the query again but with a longer delay. # (the query retry counter will stay incremented and will not reset until a successful query is made)