diff --git a/src/pysaleryd/client.py b/src/pysaleryd/client.py index bb8b8d3..835f3ee 100644 --- a/src/pysaleryd/client.py +++ b/src/pysaleryd/client.py @@ -15,7 +15,10 @@ class Client: """Client to manage communication with HRV""" - def __init__(self, url: str, port: int, session: aiohttp.ClientSession): + def __init__( + self, url: str, port: int, session: aiohttp.ClientSession, update_interval=30 + ): + self._update_interval = update_interval self._url = url self._port = port self._session = session @@ -23,7 +26,19 @@ def __init__(self, url: str, port: int, session: aiohttp.ClientSession): self._error_cache = ErrorCache() self._handlers = [] self._socket = WSClient(self._session, self._url, self._port, self._handler) - self._parser = Parser() + self._message_handler_task = asyncio.create_task(self._message_handler()) + self._data_handler_task = asyncio.create_task(self._call_handlers()) + self._incoming_queue = asyncio.queues.Queue() + + @property + def state(self): + """Get internal socket state""" + return self._socket.state + + @property + def data(self): + """Get data from system""" + return self._data async def __aenter__(self): """Start socket and wait for connection""" @@ -33,39 +48,29 @@ async def __aenter__(self): async def __aexit__(self, _type, value, traceback): self.disconnect() - async def connect(self): - """Connect to system and wait for connection""" - - async def check_connection(): - while self._socket.state != State.RUNNING: - await asyncio.sleep(0.2) - - self._socket.start() - await asyncio.gather(check_connection()) - - def disconnect(self): - """Disconnect from system""" - self._socket.stop() - - def add_handler(self, handler: Callable[[str], None]): - """Add event handler""" - self._handlers.append(handler) - - def _call_handlers(self, data): + async def _call_handlers(self): """Call handlers with data""" - for handler in self._handlers: - try: - handler(data) - except Exception: - _LOGGER.warning("Failed to call handler", exc_info=True) + while True: + for handler in self._handlers: + try: + handler(self._data) + except Exception: + _LOGGER.warning("Failed to call handler", exc_info=True) + await asyncio.sleep(self._update_interval) async def _handler( - self, signal: Signal, data: str, state: State = None + self, signal: Signal, data: str, state: "State" = None ): # pylint: disable W0613 - """Call handlers if data""" if signal == Signal.DATA: + await self._incoming_queue.put(data) + + async def _message_handler(self): + while True: try: - (key, value) = self._parser.from_str(data) + msg = await self._incoming_queue.get() + # update state + # if ack force push state to handler + (key, value) = Parser.from_str(msg) if key in ["*EA", "*EB", "*EZ"]: if key == "*EA": @@ -75,26 +80,34 @@ async def _handler( if key == "*EZ": self._error_cache.end_frame() self._data["*EB"] = self._error_cache.data - self._call_handlers(("*EB", self._error_cache.data)) else: self._data[key] = value - self._call_handlers(data) except ParseError: pass - @property - def state(self): - """Get internal socket state""" - return self._socket.state + async def connect(self): + """Connect to system and wait for connection""" - @property - def data(self): - """Get data from system""" - return self._data + async def check_connection(): + while self._socket.state != State.RUNNING: + await asyncio.sleep(0.2) + + self._socket.start() + await asyncio.gather(check_connection()) + + def disconnect(self): + """Disconnect from system""" + self._socket.stop() + self._data_handler_task.cancel() + self._message_handler_task.cancel() + + def add_handler(self, handler: Callable[[str], None]): + """Add event handler""" + self._handlers.append(handler) async def send_command(self, key, value: str | int): """Send command to HRV""" - message = self._parser.to_str(key, value) + message = Parser.to_str(key, value) async def ack_command(): """Should probably ack command here, just sleep for now""" diff --git a/src/pysaleryd/utils.py b/src/pysaleryd/utils.py index 18ebb41..5004e7d 100644 --- a/src/pysaleryd/utils.py +++ b/src/pysaleryd/utils.py @@ -11,7 +11,8 @@ class ParseError(BaseException): class Parser: """Message parser. Parse HRV system messages""" - def to_str(self, key, value): + @staticmethod + def to_str(key, value): """Parse message to string Args: @@ -23,7 +24,8 @@ def to_str(self, key, value): """ return f"#{key}:{value}\r" - def from_str(self, msg: str): + @staticmethod + def from_str(msg: str): """Parse message string Args: diff --git a/src/pysaleryd/websocket.py b/src/pysaleryd/websocket.py index 8738a0e..5bd4034 100644 --- a/src/pysaleryd/websocket.py +++ b/src/pysaleryd/websocket.py @@ -132,7 +132,9 @@ async def _running(self) -> None: if msg.type == aiohttp.WSMsgType.TEXT: _LOGGER.debug("Received: %s", msg.data) asyncio.create_task( - self._session_handler_callback(Signal.DATA, data=msg.data) + self._session_handler_callback( + Signal.DATA, data=msg.data, state=self._state + ) ) continue diff --git a/tests/test_client.py b/tests/test_client.py index c1b24b3..d0ac794 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -20,7 +20,7 @@ async def _hrv_client(ws_server): """HRV Client""" async with aiohttp.ClientSession() as session: - async with Client("localhost", 3001, session) as client: + async with Client("localhost", 3001, session, 3) as client: yield client @@ -33,15 +33,22 @@ async def test_client_connect(hrv_client: "Client"): @pytest.mark.asyncio async def test_handler(hrv_client: "Client", mocker): """Test handler callback""" - handler = mocker.Mock() + + data = None # noqa: F841 + + def handler(_data): + nonlocal data + data = _data def broken_handler(data): raise Exception() # pylint: disable=W0719 hrv_client.add_handler(broken_handler) hrv_client.add_handler(handler) - await asyncio.sleep(3) - handler.assert_called() + await asyncio.sleep(5) + + assert isinstance(data, dict) + assert any(data.keys()) @pytest.mark.asyncio diff --git a/tests/test_parser.py b/tests/test_parser.py index afeb438..c22976f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -14,7 +14,7 @@ @pytest.fixture(name="parser") def _parser() -> Parser: - return Parser() + return Parser def test_parse_int_from_list_str(parser: Parser):