diff --git a/README.md b/README.md index 83230f4..55e028f 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ python example.py --help ## Running the bundled tests -To run the bundled tests you must create the `.\remootio_devices.configuration.json` file with a content according +To run the bundled tests you must create the `.\remootio_device.configuration.json` file with a content according to the following template. ``` diff --git a/src/aioremootio/constants.py b/src/aioremootio/constants.py index 7dd13f1..fe9b374 100644 --- a/src/aioremootio/constants.py +++ b/src/aioremootio/constants.py @@ -55,29 +55,35 @@ }, extra=REMOVE_EXTRA) CONNECTION_OPTION_DEFAULT_VALUE_CONNECT_AUTOMATICALLY = True -MESSAGE_HANDLER_HEARTBEAT = 5 -PING_SENDER_HEARTBEAT = 60 -WAITING_DELAY = 1 -WAITING_FOR_SAID_HELLO_DELAY = WAITING_DELAY -WAITING_FOR_DEVICE_ANSWERED_TO_HELLO_DELAY = WAITING_DELAY -LOCK_DELAY = 0.5 -LIFECYCLE_LOCK_DELAY = LOCK_DELAY -CONNECTING_LOCK_DELAY = LOCK_DELAY -DISCONNECTING_LOCK_DELAY = LOCK_DELAY -AUTHENTICATING_LOCK_DELAY = LOCK_DELAY -TERMINATING_LOCK_DELAY = LOCK_DELAY -INITIALIZING_LOCK_DELAY = LOCK_DELAY -SAYING_HELLO_LOCK_DELAY = LOCK_DELAY -UPDATING_LAST_ACTION_ID_LOCK_DELAY = LOCK_DELAY -INVOKING_STATE_CHANGE_LISTENERS_LOCK_DELAY = LOCK_DELAY -ADDING_STATE_CHANGE_LISTENER_LOCK_DELAY = LOCK_DELAY -INVOKING_EVENT_LISTENERS_LOCK_DELAY = LOCK_DELAY -ADDING_EVENT_LISTENERS_LOCK_DELAY = LOCK_DELAY + +HEARTBEAT_MESSAGE_HANDLER = 5 +HEARTBEAT_PING_SENDER = 60 + +TIMEOUT_DEFAULT = 30 +TIMEOUT_CONNECTING = TIMEOUT_DEFAULT +TIMEOUT_SAY_HELLO = TIMEOUT_DEFAULT +TIMEOUT_DISCONNECTING = TIMEOUT_DEFAULT + +WAITING_DELAY_DEFAULT = 1 +WAITING_DELAY_SAID_HELLO = WAITING_DELAY_DEFAULT +WAITING_DELAY_DEVICE_ANSWERED_TO_HELLO = WAITING_DELAY_DEFAULT + +LOCK_RELEASE_DELAY_DEFAULT = 0.5 +LOCK_RELEASE_DELAY_LIFECYCLE = LOCK_RELEASE_DELAY_DEFAULT +LOCK_RELEASE_DELAY_UPDATING_LAST_ACTION_ID = LOCK_RELEASE_DELAY_DEFAULT +LOCK_RELEASE_DELAY_MODIFYING_STATE_CHANGE_LISTENERS = LOCK_RELEASE_DELAY_DEFAULT +LOCK_RELEASE_TIMEOUT_MODIFYING_STATE_CHANGE_LISTENERS = TIMEOUT_DEFAULT +LOCK_RELEASE_DELAY_MODIFYING_EVENT_LISTENERS = LOCK_RELEASE_DELAY_DEFAULT +LOCK_RELEASE_TIMEOUT_MODIFYING_EVENT_LISTENERS = TIMEOUT_DEFAULT + ENCODING = "latin-1" + TASK_NAME_MESSAGE_RECEIVER_AND_HANDLER = "MessageReceiverAndHandler" TASK_NAME_PING_SENDER = "PingSender" TASK_NAME_CONNECTOR = "Connector" TASK_NAME_DISCONNECTOR = "Disconnector" -TASK_STOPPED_DELAY = 5 -TASK_STARTED_DELAY = 0.5 -TASK_STOPPED_TIMEOUT = 10 \ No newline at end of file + +TASK_STARTING_DELAY = 0.5 +TASK_STARTING_TIMEOUT = 10 +TASK_STOPPING_DELAY = 5 +TASK_STOPPING_TIMEOUT = 10 \ No newline at end of file diff --git a/src/aioremootio/remootioclient.py b/src/aioremootio/remootioclient.py index a17cf30..7fd2b08 100644 --- a/src/aioremootio/remootioclient.py +++ b/src/aioremootio/remootioclient.py @@ -17,7 +17,7 @@ import aiohttp import logging import json -from typing import Optional, NoReturn, Union, List +from typing import Optional, NoReturn, Union, List, Callable, Any from async_class import AsyncClass, TaskStore from aiohttp import ClientWebSocketResponse, WSMsgType from base64 import b64encode, b64decode @@ -53,8 +53,8 @@ retrieve_frame_type, AbstractJSONHolderFrame, PingFrame from .enums import State, FrameType, ActionType, EventType, ErrorCode, ErrorType, EventSource from .constants import \ - MESSAGE_HANDLER_HEARTBEAT, \ - PING_SENDER_HEARTBEAT, \ + HEARTBEAT_MESSAGE_HANDLER, \ + HEARTBEAT_PING_SENDER, \ CONNECTION_OPTION_KEY_HOST, \ CONNECTION_OPTION_KEY_API_AUTH_KEY, \ CONNECTION_OPTION_KEY_API_SECRET_KEY, \ @@ -62,19 +62,25 @@ CONNECTION_OPTIONS_VOLUPTUOUS_SCHEMA, \ CONNECTION_OPTION_DEFAULT_VALUE_CONNECT_AUTOMATICALLY, \ ENCODING, \ - LIFECYCLE_LOCK_DELAY, \ - UPDATING_LAST_ACTION_ID_LOCK_DELAY, \ - WAITING_FOR_SAID_HELLO_DELAY, \ - WAITING_FOR_DEVICE_ANSWERED_TO_HELLO_DELAY, \ + LOCK_RELEASE_DELAY_LIFECYCLE, \ + LOCK_RELEASE_DELAY_UPDATING_LAST_ACTION_ID, \ + WAITING_DELAY_SAID_HELLO, \ + WAITING_DELAY_DEVICE_ANSWERED_TO_HELLO, \ TASK_NAME_PING_SENDER, \ TASK_NAME_MESSAGE_RECEIVER_AND_HANDLER, \ TASK_NAME_CONNECTOR, \ TASK_NAME_DISCONNECTOR, \ - TASK_STOPPED_DELAY, \ - TASK_STARTED_DELAY, \ - TASK_STOPPED_TIMEOUT, \ - ADDING_STATE_CHANGE_LISTENER_LOCK_DELAY, \ - ADDING_EVENT_LISTENERS_LOCK_DELAY + TASK_STARTING_DELAY, \ + TASK_STARTING_TIMEOUT, \ + TASK_STOPPING_DELAY, \ + TASK_STOPPING_TIMEOUT, \ + LOCK_RELEASE_DELAY_MODIFYING_STATE_CHANGE_LISTENERS, \ + LOCK_RELEASE_DELAY_MODIFYING_EVENT_LISTENERS, \ + TIMEOUT_CONNECTING, \ + TIMEOUT_DISCONNECTING, \ + TIMEOUT_SAY_HELLO, \ + LOCK_RELEASE_TIMEOUT_MODIFYING_STATE_CHANGE_LISTENERS, \ + LOCK_RELEASE_TIMEOUT_MODIFYING_EVENT_LISTENERS class RemootioClient(AsyncClass): @@ -315,10 +321,7 @@ async def __initialize(self) -> bool: async with self.__lifecycle: try: connecting_task: asyncio.Task = await self.__start_connecting() - await self.__lifecycle.wait_for(lambda: self.connected) - - if connecting_task.exception() is not None: - raise connecting_task.exception() + await self.__wait_for_task_and_handle_result(connecting_task) if self.connected: await self.__start_tasks() @@ -341,7 +344,7 @@ async def __wait_for_initializing(self) -> bool: while self.__lifecycle.locked() and self.__initializing: self.__logger.debug("This client does currently initializing. " "Waiting until the progress is done.") - await asyncio.sleep(LIFECYCLE_LOCK_DELAY) + await asyncio.sleep(LOCK_RELEASE_DELAY_LIFECYCLE) return self.__initialized @@ -356,8 +359,8 @@ async def __terminate(self) -> bool: await self.remove_state_change_listeners() await self.remove_event_listeners() - await self.__start_disconnecting() - await self.__lifecycle.wait_for(lambda: not self.connected) + disconnecting_task: asyncio.Task = await self.__start_disconnecting() + await self.__wait_for_task_and_handle_result(disconnecting_task, True) await self.__task_store.close() finally: @@ -376,7 +379,7 @@ async def __wait_for_terminating(self) -> bool: while self.__lifecycle.locked() and self.__terminating: self.__logger.debug("This client does currently terminating. " "Waiting until the progress is done.") - await asyncio.sleep(LIFECYCLE_LOCK_DELAY) + await asyncio.sleep(LOCK_RELEASE_DELAY_LIFECYCLE) return self.__terminated @@ -393,34 +396,35 @@ async def __connect(self, handle_connection_error: bool = True) -> ClientWebSock if not self.connected: self.__connecting = True - try: + try: async with self.__lifecycle: try: if self.__is_ws_connected(self.__ws) and not self.__authenticated: self.__logger.warning( "Living client connection to the device will be closed now, " "because this client isn't authenticated by the device.") - await self.__start_disconnecting() - await self.__lifecycle.wait_for(lambda: not self.connected) - - if self.__ws is None: - # Establish connection to the device - self.__logger.info("Establishing websocket connection to the device...") - try: - self.__ws = await self.__client_session.ws_connect( - f"ws://{self.__connection_options.host}:8080/") - self.__logger.info("Websocket connection to the device has been established successfully.") - except BaseException as ex: - self.__ws = None - if handle_connection_error: - self.__logger.exception("Unable to establish websocket connection to the device.") - else: - raise RemootioClientConnectionEstablishmentError( - self, "Unable to establish websocket connection to the device.") from ex + disconnecting_task: asyncio.Task = await self.__start_disconnecting() + await self.__wait_for_task_and_handle_result(disconnecting_task) + + async with asyncio.timeout(TIMEOUT_CONNECTING): + if self.__ws is None: + # Establish connection to the device + self.__logger.info("Establishing websocket connection to the device...") + try: + self.__ws = await self.__client_session.ws_connect( + f"ws://{self.__connection_options.host}:8080/") + self.__logger.info("Websocket connection to the device has been established successfully.") + except BaseException as ex: + self.__ws = None + if handle_connection_error: + self.__logger.exception("Unable to establish websocket connection to the device.") + else: + raise RemootioClientConnectionEstablishmentError( + self, "Unable to establish websocket connection to the device.") from ex - if self.__is_ws_connected(self.__ws): - # Authenticate this client by the device - await self.__authenticate(self.__ws) + if self.__is_ws_connected(self.__ws): + # Authenticate this client by the device + await self.__authenticate(self.__ws) if self.__is_ws_connected(self.__ws) and self.__authenticated: # Say hello to the device @@ -439,7 +443,7 @@ async def __wait_for_connecting(self) -> bool: while self.__lifecycle.locked() and self.__connecting: self.__logger.debug("This client does currently connecting to the device. " "Waiting until the progress is done.") - await asyncio.sleep(LIFECYCLE_LOCK_DELAY) + await asyncio.sleep(LOCK_RELEASE_DELAY_LIFECYCLE) return self.connected @@ -522,32 +526,33 @@ async def __wait_for_said_hello(self): while not self.__said_hello: self.__logger.debug("This client has not yet said hello to the device. " "Waiting until it has sid hello.") - await asyncio.sleep(WAITING_FOR_SAID_HELLO_DELAY) + await asyncio.sleep(WAITING_DELAY_SAID_HELLO) async def __wait_for_device_has_answered_to_hello(self): while not self.__device_answered_to_hello: self.__logger.debug("The device has not yet answered to the hello from this client. " "Waiting until it has answered.") - await asyncio.sleep(WAITING_FOR_DEVICE_ANSWERED_TO_HELLO_DELAY) + await asyncio.sleep(WAITING_DELAY_DEVICE_ANSWERED_TO_HELLO) async def __disconnect(self) -> NoReturn: self.__disconnecting = True try: async with self.__lifecycle: try: - if self.__ws is not None and not self.__ws.closed: - try: - self.__logger.info("Closing websocket connection to the device...") - await self.__ws.close() - except BaseException: - self.__logger.warning("Unable to close websocket connection to the device because of an error.", - exc_info=(self.__logger.getEffectiveLevel() == logging.DEBUG)) + async with asyncio.timeout(TIMEOUT_DISCONNECTING): + if self.__ws is not None and not self.__ws.closed: + try: + self.__logger.info("Closing websocket connection to the device...") + await self.__ws.close() + except BaseException: + self.__logger.warning("Unable to close websocket connection to the device because of an error.", + exc_info=(self.__logger.getEffectiveLevel() == logging.DEBUG)) - self.__session_key = None - self.__last_action_id = None - self.__said_hello = False - self.__authenticated = False - self.__ws = None + self.__session_key = None + self.__last_action_id = None + self.__said_hello = False + self.__authenticated = False + self.__ws = None if not self.connected: await self.__invoke_event_listeners(Event(EventSource.CLIENT, EventType.DISCONNECTED, None)) @@ -560,7 +565,7 @@ async def __wait_for_disconnecting(self) -> bool: while self.__lifecycle.locked() and self.__disconnecting: self.__logger.debug("This client does currently disconnecting from the device. " "Waiting until the progress is done.") - await asyncio.sleep(LIFECYCLE_LOCK_DELAY) + await asyncio.sleep(LOCK_RELEASE_DELAY_LIFECYCLE) return not self.connected @@ -587,7 +592,7 @@ async def __start_receiving_and_handling_of_messages(self, force: bool = False) self.__task_store \ .create_task(self.__receive_and_handle_messages(), name=TASK_NAME_MESSAGE_RECEIVER_AND_HANDLER) \ .add_done_callback(self.__handle_task_done) - result = await self.__wait_for_task_started(TASK_NAME_MESSAGE_RECEIVER_AND_HANDLER) + result = await asyncio.wait_for(self.__wait_for_task_started(TASK_NAME_MESSAGE_RECEIVER_AND_HANDLER), TASK_STARTING_TIMEOUT) return result @@ -601,7 +606,7 @@ async def __start_sending_of_pings(self, force: bool = False) -> bool: self.__task_store \ .create_task(self.__send_pings(), name=TASK_NAME_PING_SENDER) \ .add_done_callback(self.__handle_task_done) - result = await self.__wait_for_task_started(TASK_NAME_PING_SENDER) + result = await asyncio.wait_for(self.__wait_for_task_started(TASK_NAME_PING_SENDER), TASK_STARTING_TIMEOUT) return result @@ -610,7 +615,7 @@ async def __start_connecting(self) -> asyncio.Task: .create_task(self.__connect(False), name=TASK_NAME_CONNECTOR) result.add_done_callback(self.__handle_task_done) - await self.__wait_for_task_started(TASK_NAME_CONNECTOR) + await asyncio.wait_for(self.__wait_for_task_started(TASK_NAME_CONNECTOR), TASK_STARTING_TIMEOUT) return result @@ -619,7 +624,7 @@ async def __start_disconnecting(self) -> asyncio.Task: .create_task(self.__disconnect(), name=TASK_NAME_DISCONNECTOR) result.add_done_callback(self.__handle_task_done) - await self.__wait_for_task_started(TASK_NAME_DISCONNECTOR) + await asyncio.wait_for(self.__wait_for_task_started(TASK_NAME_DISCONNECTOR), TASK_STARTING_TIMEOUT) return result @@ -630,28 +635,28 @@ async def __wait_for_task_started(self, task_name: str) -> bool: while not self.__receives_and_handles_messages: self.__logger.debug("Task to receive and handle messages isn't started yet. " "Waiting as long as it is started.") - await asyncio.sleep(TASK_STARTED_DELAY) + await asyncio.sleep(TASK_STARTING_DELAY) result = self.__receives_and_handles_messages elif task_name == TASK_NAME_PING_SENDER: while not self.__sends_pings: self.__logger.debug("Task to sending PINGs isn't started yet. " "Waiting as long as it is started.") - await asyncio.sleep(TASK_STARTED_DELAY) + await asyncio.sleep(TASK_STARTING_DELAY) result = self.__sends_pings elif task_name == TASK_NAME_CONNECTOR: while not self.__connecting: self.__logger.debug("Task to connect to the device isn't started yet. " "Waiting as long as it is started.") - await asyncio.sleep(TASK_STARTED_DELAY) + await asyncio.sleep(TASK_STARTING_DELAY) result = self.__connecting elif task_name == TASK_NAME_DISCONNECTOR: while not self.__disconnecting: self.__logger.debug("Task to disconnect from the device isn't started yet. " "Waiting as long as it is started.") - await asyncio.sleep(TASK_STARTED_DELAY) + await asyncio.sleep(TASK_STARTING_DELAY) result = self.__disconnecting @@ -675,7 +680,7 @@ async def __stop_task(self, name: str) -> bool: task: Optional[asyncio.Task] = self.__get_task(name) if task is not None: task.cancel() - result = await asyncio.wait_for(self.__wait_for_task_stopped(task), TASK_STOPPED_TIMEOUT) + result = await asyncio.wait_for(self.__wait_for_task_stopped(task), TASK_STOPPING_TIMEOUT) return result @@ -688,7 +693,7 @@ async def __wait_for_task_stopped(self, task: Union[str, asyncio.Task]) -> bool: if task is not None: while not task.done(): self.__logger.debug("Task \"%s\" isn't sopped yet. Waiting as long as it is stopped.", task.get_name()) - await asyncio.sleep(TASK_STOPPED_DELAY) + await asyncio.sleep(TASK_STOPPING_DELAY) result = task.done() @@ -716,7 +721,7 @@ async def __send_pings(self) -> NoReturn: except BaseException: self.__logger.warning("Sending PINGs by this client will be delayed " "because connection to the device can't be established.") - await asyncio.sleep(PING_SENDER_HEARTBEAT) + await asyncio.sleep(HEARTBEAT_PING_SENDER) continue if self.__is_ws_connected(ws): @@ -728,7 +733,7 @@ async def __send_pings(self) -> NoReturn: self.__logger.warning("Sending PINGs by this client will be delayed " "because connection to the device can't be established.") - await asyncio.sleep(PING_SENDER_HEARTBEAT) + await asyncio.sleep(HEARTBEAT_PING_SENDER) else: self.__logger.info("Sending PINGs by this client will be now stopped.") return @@ -751,7 +756,7 @@ async def __receive_and_handle_messages(self) -> NoReturn: except BaseException: self.__logger.warning("Receiving and handling of messages by this client will be delayed " "because connection to the device can't be established.") - await asyncio.sleep(MESSAGE_HANDLER_HEARTBEAT) + await asyncio.sleep(HEARTBEAT_MESSAGE_HANDLER) continue if self.__is_ws_connected(ws): @@ -795,11 +800,11 @@ async def __receive_and_handle_messages(self) -> NoReturn: except BaseException: self.__logger.error("Failed to handle received message.") else: - await asyncio.sleep(MESSAGE_HANDLER_HEARTBEAT) + await asyncio.sleep(HEARTBEAT_MESSAGE_HANDLER) else: self.__logger.warning("Receiving and handling of messages by this client will be delayed " "because connection to the device can't be established.") - await asyncio.sleep(MESSAGE_HANDLER_HEARTBEAT) + await asyncio.sleep(HEARTBEAT_MESSAGE_HANDLER) else: self.__logger.info("Receiving and handling of messages by this client will be now stopped.") return @@ -813,6 +818,8 @@ async def __receive_and_handle_messages(self) -> NoReturn: raise def __handle_task_done(self, task: asyncio.Task) -> NoReturn: + self.__logger.debug("Execution of task \"%s\" is done.", task.get_name()) + if task.get_name() == TASK_NAME_MESSAGE_RECEIVER_AND_HANDLER: self.__receives_and_handles_messages = False elif task.get_name() == TASK_NAME_PING_SENDER: @@ -822,6 +829,20 @@ def __handle_task_done(self, task: asyncio.Task) -> NoReturn: elif task.get_name() == TASK_NAME_DISCONNECTOR: self.__disconnecting = False + async def __wait_for_task_and_handle_result(self, task: asyncio.Task, handle_error: bool = False) -> Any: + await self.__lifecycle.wait_for(lambda: task.done()) + + try: + if task.exception() is not None: + raise task.exception() + except BaseException as ex: + if handle_error: + self.__logger.exception("Execution of task \"%s\" has been failed.") + else: + raise + + return task.result() + # ------------------------------- # Sending of frames to the device # ------------------------------- @@ -836,8 +857,9 @@ async def __send_frame( if isinstance(frame, HelloFrame) or isinstance(frame, PingFrame) or self.__authenticating: pass else: - await self.__wait_for_said_hello() - await self.__wait_for_device_has_answered_to_hello() + async with asyncio.timeout(TIMEOUT_SAY_HELLO): + await self.__wait_for_said_hello() + await self.__wait_for_device_has_answered_to_hello() if self.__logger.getEffectiveLevel() == logging.DEBUG: self.__logger.info("Sending frame... Frame [%s]", json.dumps(frame.json)) @@ -1097,10 +1119,11 @@ async def __invoke_state_change_listeners(self, state_change: StateChange) -> No exc_info=True) async def __wait_for_modifying_state_change_listeners(self): - while self.__modifying_state_change_listeners_lock.locked(): - self.__logger.debug("List of state change listeners will be currently modified. " - "Waiting until the progress is done.") - await asyncio.sleep(ADDING_STATE_CHANGE_LISTENER_LOCK_DELAY) + async with asyncio.timeout(LOCK_RELEASE_TIMEOUT_MODIFYING_STATE_CHANGE_LISTENERS): + while self.__modifying_state_change_listeners_lock.locked(): + self.__logger.debug("List of state change listeners will be currently modified. " + "Waiting until the progress is done.") + await asyncio.sleep(LOCK_RELEASE_DELAY_MODIFYING_STATE_CHANGE_LISTENERS) # --------------- # Event listeners @@ -1122,10 +1145,11 @@ async def __invoke_event_listeners(self, event: Event) -> NoReturn: exc_info=True) async def __wait_for_modifying_event_listeners(self): - while self.__modifying_event_listeners_lock.locked(): - self.__logger.debug("List of event listeners will be currently modified. " - "Waiting until the progress is done.") - await asyncio.sleep(ADDING_EVENT_LISTENERS_LOCK_DELAY) + async with asyncio.timeout(LOCK_RELEASE_TIMEOUT_MODIFYING_EVENT_LISTENERS): + while self.__modifying_event_listeners_lock.locked(): + self.__logger.debug("List of event listeners will be currently modified. " + "Waiting until the progress is done.") + await asyncio.sleep(LOCK_RELEASE_DELAY_MODIFYING_EVENT_LISTENERS) # -------------- # State handling @@ -1238,7 +1262,7 @@ async def __wait_for_updating_last_action_id(self): while self.__updating_last_action_id_lock.locked(): self.__logger.debug("This client is currently updating the last action id. " "Waiting until the progress is done.") - await asyncio.sleep(UPDATING_LAST_ACTION_ID_LOCK_DELAY) + await asyncio.sleep(LOCK_RELEASE_DELAY_UPDATING_LAST_ACTION_ID) async def __trigger(self, action_type: ActionType) -> NoReturn: if await self.__wait_for_terminating(): @@ -1291,7 +1315,7 @@ def __is_ws_connected(self, ws: Optional[ClientWebSocketResponse] = None) -> boo def connected(self) -> bool: """ Determines whether this client is connected to the device. That mains a WebSocket connection is successfully - established to the device, the client was successfully authenticated by the device, furthermore the client + established to the device, the client was successfully authenticated by the device, the client has said hello to the device. :return: ``true`` if this client is connected to the device, otherwise ``false`` """ diff --git a/tests/test_remootio_client.py b/tests/test_remootio_client.py index 0b9c706..7ac0b1e 100644 --- a/tests/test_remootio_client.py +++ b/tests/test_remootio_client.py @@ -22,6 +22,7 @@ import tests import asyncio import aiohttp +from test.support import busy_retry, SHORT_TIMEOUT class RemootioClientTestStateChangeListener(aioremootio.Listener[aioremootio.StateChange]): @@ -39,7 +40,7 @@ def invoke_count(self) -> int: return self.__invoke_count -class RemootioClientTestCase(unittest.TestCase): +class RemootioClientTestCase(unittest.IsolatedAsyncioTestCase): TIMEOUT = 200 DEFAULT_MAXIMUM_ATTEMPTS = 200 WAIT_TIME_BETWEEN_ATTEMPTS = 1 @@ -63,227 +64,232 @@ def setUpClass(cls) -> NoReturn: def setUp(self) -> NoReturn: self.__state_change_listener = RemootioClientTestStateChangeListener() - def test_remootio_client_0001(self): + async def test_remootio_client_0001(self): self.__logger.info("ENTRY: %s", "test_remootio_client_0001") if self.__remootio_device_configuration is not None: - asyncio.get_event_loop().run_until_complete(self.__test_remootio_client_0001()) - else: - self.__logger.warning("Tests will be skipped because of missing Remootio device configuration.") - - self.__logger.info("RETURN: %s", "test_remootio_client_0001") + async with aiohttp.ClientSession() as client_session: + remootio_client: aioremootio.RemootioClient = \ + await aioremootio.RemootioClient( + self.__remootio_device_configuration, + client_session, + LoggerConfiguration(logger=self.__logger), + [self.__state_change_listener] + ) - def test_remootio_client_0002(self): - self.__logger.info("ENTRY: %s", "test_remootio_client_0002") - - if self.__remootio_device_configuration is not None: - asyncio.get_event_loop().run_until_complete(self.__test_remootio_client_0002()) - else: - self.__logger.warning("Tests will be skipped because of missing Remootio device configuration.") - - self.__logger.info("RETURN: %s", "test_remootio_client_0002") + try: + await asyncio.wait_for( + self.__condition_is_met(self.__has_device_answered_to_hello, remootio_client, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) + + api_version: int = remootio_client.api_version - async def __test_remootio_client_0001(self): - async with aiohttp.ClientSession() as client_session: - remootio_client: aioremootio.RemootioClient = \ - await aioremootio.RemootioClient( - self.__remootio_device_configuration, - client_session, - LoggerConfiguration(logger=self.__logger), - [self.__state_change_listener] - ) + self.assertEqual(api_version, self.__remootio_device_configuration.api_version, + "API version isn't the expected.") - try: - api_version: int = remootio_client.api_version + if api_version >= 2: + serial_number: str = remootio_client.serial_number - self.assertEqual(api_version, self.__remootio_device_configuration.api_version, - "API version isn't the expected.") - - if api_version >= 2: - serial_number: str = remootio_client.serial_number + self.assertIsNotNone( + serial_number, + "By devices with API version >= 2 serial number must be set after successful initialization of " + "the client.") - self.assertIsNotNone( - serial_number, - "By devices with API version >= 2 serial number must be set after successful initialization of " - "the client.") + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=2, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=2, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + self.assertNotEqual(remootio_client.state, State.UNKNOWN, "State isn't the expected.") - self.assertNotEqual(remootio_client.state, State.UNKNOWN, "State isn't the expected.") + if remootio_client.state != State.NO_SENSOR_INSTALLED: + if remootio_client.state == State.OPEN: + await remootio_client.trigger_close() - if remootio_client.state != State.NO_SENSOR_INSTALLED: - if remootio_client.state == State.OPEN: - await remootio_client.trigger_close() + self.__logger.info("Waiting for that the gate / garage door is closed...") - self.__logger.info("Waiting for that the gate / garage door is closed...") + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=4, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=4, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") - self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") + await remootio_client.trigger_open() - await remootio_client.trigger_open() + self.__logger.info("Waiting for that the gate / garage door is open...") - self.__logger.info("Waiting for that the gate / garage door is open...") + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=6, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=6, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") + elif remootio_client.state == State.CLOSED: + await remootio_client.trigger_open() - self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") - elif remootio_client.state == State.CLOSED: - await remootio_client.trigger_open() + self.__logger.info("Waiting for that the gate / garage door is open...") - self.__logger.info("Waiting for that the gate / garage door is open...") + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=4, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=4, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") - self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") + await remootio_client.trigger_close() - await remootio_client.trigger_close() + self.__logger.info("Waiting for that the gate / garage door is closed...") - self.__logger.info("Waiting for that the gate / garage door is closed...") + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=6, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=6, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") + else: + self.__logger.warning("Further functional tests will be skipped because the Remootio device " + "hasn't a sensor installed.") + finally: + await remootio_client.terminate() + else: + self.__logger.warning("Tests will be skipped because of missing Remootio device configuration.") + + self.__logger.info("RETURN: %s", "test_remootio_client_0001") - self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") - else: - self.__logger.warning("Further functional tests will be skipped because the Remootio device " - "hasn't a sensor installed.") - finally: - await remootio_client.terminate() + async def test_remootio_client_0002(self): + self.__logger.info("ENTRY: %s", "test_remootio_client_0002") + + if self.__remootio_device_configuration is not None: + async with aiohttp.ClientSession() as client_session: + remootio_client: aioremootio.RemootioClient = \ + await aioremootio.RemootioClient( + self.__remootio_device_configuration, + client_session, + LoggerConfiguration(logger=self.__logger), + [self.__state_change_listener] + ) - async def __test_remootio_client_0002(self): - async with aiohttp.ClientSession() as client_session: - remootio_client: aioremootio.RemootioClient = \ - await aioremootio.RemootioClient( - self.__remootio_device_configuration, - client_session, - LoggerConfiguration(logger=self.__logger), - [self.__state_change_listener] - ) + try: + await asyncio.wait_for( + self.__condition_is_met(self.__has_device_answered_to_hello, remootio_client, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - try: - api_version: int = remootio_client.api_version + api_version: int = remootio_client.api_version - self.assertEqual(api_version, self.__remootio_device_configuration.api_version, - "API version isn't the expected.") + self.assertEqual(api_version, self.__remootio_device_configuration.api_version, + "API version isn't the expected.") - if api_version >= 2: - serial_number: str = remootio_client.serial_number + if api_version >= 2: + serial_number: str = remootio_client.serial_number - self.assertIsNotNone( - serial_number, - "By devices with API version >= 2 serial number must be set after successful initialization of " - "the client.") + self.assertIsNotNone( + serial_number, + "By devices with API version >= 2 serial number must be set after successful initialization of " + "the client.") - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=2, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=2, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - self.assertNotEqual(remootio_client.state, State.UNKNOWN, "State isn't the expected.") + self.assertNotEqual(remootio_client.state, State.UNKNOWN, "State isn't the expected.") - if remootio_client.state != State.NO_SENSOR_INSTALLED: - if remootio_client.state == State.OPEN: - await remootio_client.trigger_close() + if remootio_client.state != State.NO_SENSOR_INSTALLED: + if remootio_client.state == State.OPEN: + await remootio_client.trigger_close() - self.__logger.info("Waiting for that the gate / garage door is closed...") + self.__logger.info("Waiting for that the gate / garage door is closed...") - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=4, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=4, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") + self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") - await remootio_client.disconnect() + await remootio_client.disconnect() - self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") + self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") - await remootio_client.trigger_open() + await remootio_client.trigger_open() - self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") + self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") - self.__logger.info("Waiting for that the gate / garage door is open...") + self.__logger.info("Waiting for that the gate / garage door is open...") - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=6, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=6, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") + self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") - await remootio_client.disconnect() + await remootio_client.disconnect() - self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") + self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") - await remootio_client.connect() + await remootio_client.connect() - self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") - elif remootio_client.state == State.CLOSED: - await remootio_client.trigger_open() + self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") + elif remootio_client.state == State.CLOSED: + await remootio_client.trigger_open() - self.__logger.info("Waiting for that the gate / garage door is open...") + self.__logger.info("Waiting for that the gate / garage door is open...") - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=4, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=4, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") + self.assertEqual(remootio_client.state, State.OPEN, "State isn't the expected.") - await remootio_client.disconnect() + await remootio_client.disconnect() - self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") + self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") - await remootio_client.trigger_close() + await remootio_client.trigger_close() - self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") + self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") - self.__logger.info("Waiting for that the gate / garage door is closed...") + self.__logger.info("Waiting for that the gate / garage door is closed...") - await asyncio.wait_for( - self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, - expected_invoke_count=6, logger=self.__logger), - timeout=RemootioClientTestCase.TIMEOUT - ) + await asyncio.wait_for( + self.__condition_is_met(self.__is_state_change_listener_invoked, remootio_client, + expected_invoke_count=6, logger=self.__logger), + timeout=RemootioClientTestCase.TIMEOUT + ) - self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") + self.assertEqual(remootio_client.state, State.CLOSED, "State isn't the expected.") - await remootio_client.disconnect() + await remootio_client.disconnect() - self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") + self.assertTrue(not remootio_client.connected, "Client appears still connected to the device.") - await remootio_client.connect() + await remootio_client.connect() - self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") - else: - self.__logger.warning("Further functional tests will be skipped because the Remootio device " - "hasn't a sensor installed.") - finally: - await remootio_client.terminate() + self.assertTrue(remootio_client.connected, "Client appears still disconnected from the device.") + else: + self.__logger.warning("Further functional tests will be skipped because the Remootio device " + "hasn't a sensor installed.") + finally: + await remootio_client.terminate() + else: + self.__logger.warning("Tests will be skipped because of missing Remootio device configuration.") + + self.__logger.info("RETURN: %s", "test_remootio_client_0002") + async def __condition_is_met(self, condition_callback: Callable, remootio_client: aioremootio.RemootioClient, maximum_attempts: int = DEFAULT_MAXIMUM_ATTEMPTS, **kwargs) -> bool: @@ -304,6 +310,11 @@ def __is_state_change_listener_invoked(self, remootio_client: aioremootio.Remoot logger.info("Checking state change listener's invocation count. Actual [%s] Expected [%s]", self.__state_change_listener.invoke_count, kwargs["expected_invoke_count"]) return self.__state_change_listener.invoke_count == kwargs["expected_invoke_count"] + + def __has_device_answered_to_hello(self, remootio_client: aioremootio.RemootioClient, **kwargs) -> bool: + logger: logging.Logger = kwargs["logger"] + logger.info("Checking whether the device has answered to the client's \"Hello\".") + return remootio_client.api_version is not None if __name__ == '__main__':