From 88aadf11457af689687ac46102acc3106f38b2b1 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Feb 2024 14:39:10 +0100 Subject: [PATCH 01/18] Rework transport to leave re-connect to user --- RFXtrx/__init__.py | 154 +++++++++++++++++++------------- tests/test_transport_network.py | 86 ++++++++++++++++++ 2 files changed, 180 insertions(+), 60 deletions(-) create mode 100644 tests/test_transport_network.py diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 95e4afa..cdb93b6 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -674,6 +674,19 @@ def __str__(self): return "{0} device=[{1}]".format( type(self), self.device) + +class ConnectionEvent(RFXtrxEvent): + """ Connection event """ + def __init__(self): + super().__init__(None) + +class ConnectionLost(ConnectionEvent): + """ Connection lost """ + +class ConnectionDone(ConnectionEvent): + """ Connection lost """ + + ############################################################################### # DummySerial class ############################################################################### @@ -730,6 +743,13 @@ def close(self): self._close_event.set() +############################################################################### +# RFXtrxTransportError class +############################################################################### + +class RFXtrxTransportError(Exception): + """ Connection error """ + ############################################################################### # RFXtrxTransport class ############################################################################### @@ -763,6 +783,13 @@ def reset(self): def close(self): """ close connection to rfxtrx device """ + def receive_blocking(self): + """ Wait until a packet is received and return with an RFXtrxEvent """ + + def send(self, data): + """ Send the given packet """ + + ############################################################################### # PySerialTransport class ############################################################################### @@ -774,44 +801,42 @@ class PySerialTransport(RFXtrxTransport): def __init__(self, port): self.port = port self.serial = None - self._run_event = threading.Event() - self._run_event.set() self.connect() def connect(self): """ Open a serial connexion """ try: - self.serial = serial.Serial(self.port, 38400, timeout=0.1) - except serial.serialutil.SerialException: - port = glob.glob('/dev/serial/by-id/usb-RFXCOM_*-port0') - if len(port) < 1: - return - self.serial = serial.Serial(port[0], 38400, timeout=0.1) + try: + self.serial = serial.Serial(self.port, 38400) + except serial.SerialException: + port = glob.glob('/dev/serial/by-id/usb-RFXCOM_*-port0') + if len(port) < 1: + raise + _LOGGER.debug("Attempting connection by name %s", port) + self.serial = serial.Serial(port[0], 38400) + except serial.SerialException as exception: + raise RFXtrxTransportError("Connection failed: {0}".format(exception)) from exception def receive_blocking(self): + try: + return self._receive_packet() + except serial.SerialException as exception: + raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception + + def _receive_packet(self): """ Wait until a packet is received and return with an RFXtrxEvent """ - data = None - while self._run_event.is_set(): - try: - data = self.serial.read() - except TypeError: - continue - except serial.serialutil.SerialException: - try: - self.connect() - except serial.serialutil.SerialException: - time.sleep(5) - continue - if not data or data == '\x00': - continue - pkt = bytearray(data) - data = self.serial.read(pkt[0]) + data = self.serial.read() + if data == '\x00': + return None + pkt = bytearray(data) + while len(pkt) < pkt[0]+1: + data = self.serial.read(pkt[0]+1 - len(pkt)) pkt.extend(bytearray(data)) - _LOGGER.debug( - "Recv: %s", - " ".join("0x{0:02x}".format(x) for x in pkt) - ) - return self.parse(pkt) + _LOGGER.debug( + "Recv: %s", + " ".join("0x{0:02x}".format(x) for x in pkt) + ) + return self.parse(pkt) def send(self, data): """ Send the given packet """ @@ -835,7 +860,6 @@ def reset(self): def close(self): """ close connection to rfxtrx device """ - self._run_event.clear() self.serial.close() @@ -850,43 +874,41 @@ class PyNetworkTransport(RFXtrxTransport): def __init__(self, hostport): self.hostport = hostport # must be a (host, port) tuple self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._run_event = threading.Event() - self._run_event.set() self.connect() def connect(self): """ Open a socket connection """ try: self.sock.connect(self.hostport) - _LOGGER.info("Connected to network socket") - except socket.error: - _LOGGER.error('Failed to create socket, check host port config') - # This may throw exception for use by caller: - self.sock.connect(self.hostport) + _LOGGER.debug("Connected to network socket") + except socket.error as exception: + raise RFXtrxTransportError("Connection failed: {0}".format(exception)) from exception def receive_blocking(self): """ Wait until a packet is received and return with an RFXtrxEvent """ - data = None - while self._run_event.is_set(): - try: - data = self.sock.recv(1) - except socket.error: - try: - self.connect() - except socket.error: - time.sleep(5) - continue - if not data or data == '\x00': - continue - pkt = bytearray(data) - while len(pkt) < pkt[0]+1: - data = self.sock.recv(pkt[0]+1 - len(pkt)) - pkt.extend(bytearray(data)) - _LOGGER.debug( - "Recv: %s", - " ".join("0x{0:02x}".format(x) for x in pkt) - ) - return self.parse(pkt) + try: + return self._receive_packet() + except socket.error as exception: + raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception + + def _receive_packet(self): + """ Wait until a packet is received and return with an RFXtrxEvent """ + data = self.sock.recv(1) + if data == b'': + raise RFXtrxTransportError("Server was shutdown") + if data == '\x00': + return None + pkt = bytearray(data) + while len(pkt) < pkt[0]+1: + data = self.sock.recv(pkt[0]+1 - len(pkt)) + if data == b'': + raise RFXtrxTransportError("Server was shutdown") + pkt.extend(bytearray(data)) + _LOGGER.debug( + "Recv: %s", + " ".join("0x{0:02x}".format(x) for x in pkt) + ) + return self.parse(pkt) def send(self, data): """ Send the given packet """ @@ -910,7 +932,6 @@ def reset(self): def close(self): """ close connection to rfxtrx device """ - self._run_event.clear() self.sock.shutdown(socket.SHUT_RDWR) self.sock.close() @@ -975,13 +996,24 @@ def __init__(self, device, event_callback=None, self._modes = modes self.event_callback = event_callback - self.transport = transport_protocol(device) + self.transport: RFXtrxTransport = transport_protocol(device) self._thread = threading.Thread(target=self._connect) self._thread.daemon = True self._thread.start() self._run_event.wait() def _connect(self): + try: + self._connect_internal() + except RFXtrxTransportError as exception: + _LOGGER.info("Connection lost %s", exception) + except Exception: + _LOGGER.exception("Unexpected exception from transport") + finally: + if self.event_callback: + self.event_callback(ConnectionLost()) + + def _connect_internal(self): """Connect """ self.transport.reset() self._status = self.send_get_status() @@ -998,6 +1030,8 @@ def _connect(self): self.send_start() self._run_event.set() + if self.event_callback: + self.event_callback(ConnectionDone()) while self._run_event.is_set(): event = self.transport.receive_blocking() diff --git a/tests/test_transport_network.py b/tests/test_transport_network.py new file mode 100644 index 0000000..e8e72b2 --- /dev/null +++ b/tests/test_transport_network.py @@ -0,0 +1,86 @@ + +import pytest +import RFXtrx + +import socket +import dataclasses +import threading + + +@pytest.fixture(name="server_socket") +def fixture_server_socket(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('127.0.0.1', 0)) + sock.listen(1) + try: + yield sock + finally: + sock.close() + +@dataclasses.dataclass +class Server: + address: tuple + connections: list[socket.socket] + event = threading.Event() + +@pytest.fixture(name="server") +def fixture_server(server_socket: socket.socket): + + server = Server(address=server_socket.getsockname(), connections=[]) + + def runner(): + while True: + try: + connection, address = server_socket.accept() + server.connections.append(connection) + server.event.set() + except socket.error: + return + thread = threading.Thread(target=runner, daemon=True) + thread.start() + try: + yield server + finally: + server_socket.close() + for connection in server.connections: + connection.close() + thread.join() + +def connected_transport(server: Server): + server.event.clear() + transport = RFXtrx.PyNetworkTransport(server.address) + transport.sock.settimeout(10) + assert server.event.wait(10) + return transport, server.connections[-1] + +def test_transport_shutdown_between_packet(server: Server): + transport, connection = connected_transport(server) + connection.sendall(bytes([0x09, 0x03, 0x01, 0x04, 0x28, 0x0a, 0xb7, 0x66, 0x04, 0x70])) + connection.shutdown(socket.SHUT_RDWR) + + pkt = transport.receive_blocking() + assert isinstance(pkt, RFXtrx.SensorEvent) + with pytest.raises(RFXtrx.RFXtrxTransportError): + transport.receive_blocking() + +def test_transport_shutdown_mid_packet(server: Server): + transport, connection = connected_transport(server) + connection.sendall(bytes([0x09, 0x03, 0x01, 0x04])) + connection.shutdown(socket.SHUT_RDWR) + + with pytest.raises(RFXtrx.RFXtrxTransportError): + transport.receive_blocking() + +def test_transport_close_mid_packet(server: Server): + transport, connection = connected_transport(server) + connection.sendall(bytes([0x09, 0x03, 0x01, 0x04])) + connection.close() + + with pytest.raises(RFXtrx.RFXtrxTransportError): + transport.receive_blocking() + +def test_transport_empty_packet(server: Server): + transport, connection = connected_transport(server) + connection.sendall(bytes([0x00])) + + assert transport.receive_blocking() is None From abcca453f76682effc06165e0f6804805720c8e3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Feb 2024 14:50:45 +0100 Subject: [PATCH 02/18] Make compatible with older python --- tests/test_transport_network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_transport_network.py b/tests/test_transport_network.py index e8e72b2..5d689fd 100644 --- a/tests/test_transport_network.py +++ b/tests/test_transport_network.py @@ -5,7 +5,7 @@ import socket import dataclasses import threading - +from typing import Tuple, List @pytest.fixture(name="server_socket") def fixture_server_socket(): @@ -19,8 +19,8 @@ def fixture_server_socket(): @dataclasses.dataclass class Server: - address: tuple - connections: list[socket.socket] + address: Tuple + connections: List[socket.socket] event = threading.Event() @pytest.fixture(name="server") From 713d09c1ed04f33a610ac9dbe46aef026f81c28d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Feb 2024 15:09:46 +0100 Subject: [PATCH 03/18] Don't signal connection loss when asked to close --- RFXtrx/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index cdb93b6..2769fac 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -1010,7 +1010,7 @@ def _connect(self): except Exception: _LOGGER.exception("Unexpected exception from transport") finally: - if self.event_callback: + if self.event_callback and self._run_event.is_set(): self.event_callback(ConnectionLost()) def _connect_internal(self): From 7801c0f25d9d37fd443ee5c64d940dc66ed11880 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Feb 2024 15:14:34 +0100 Subject: [PATCH 04/18] Avoid blocking teardown --- tests/test_transport_network.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_transport_network.py b/tests/test_transport_network.py index 5d689fd..6b820f8 100644 --- a/tests/test_transport_network.py +++ b/tests/test_transport_network.py @@ -11,6 +11,7 @@ def fixture_server_socket(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 0)) + sock.settimeout(1) sock.listen(1) try: yield sock @@ -34,6 +35,8 @@ def runner(): connection, address = server_socket.accept() server.connections.append(connection) server.event.set() + except socket.timeout: + continue except socket.error: return thread = threading.Thread(target=runner, daemon=True) From 011180868b0db829a392a2a8e33358c5087f7b69 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Feb 2024 15:35:54 +0100 Subject: [PATCH 05/18] Split connect from constructions --- RFXtrx/__init__.py | 17 ++++++++++++----- examples/receive.py | 1 + examples/send.py | 1 + tests/test_base.py | 7 +++++++ tests/test_transport_network.py | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 2769fac..bd8778c 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -777,6 +777,9 @@ def parse(data): return obj return None + def connect(self): + """ connect to device """ + def reset(self): """ reset the rfxtrx device """ @@ -801,7 +804,6 @@ class PySerialTransport(RFXtrxTransport): def __init__(self, port): self.port = port self.serial = None - self.connect() def connect(self): """ Open a serial connexion """ @@ -874,7 +876,6 @@ class PyNetworkTransport(RFXtrxTransport): def __init__(self, hostport): self.hostport = hostport # must be a (host, port) tuple self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.connect() def connect(self): """ Open a socket connection """ @@ -943,6 +944,9 @@ def __init__(self, device=""): self.device = device self._close_event = threading.Event() + def connect(self): + pass + def receive(self, data=None): """ Emulate a receive by parsing the given data """ if data is None: @@ -979,6 +983,8 @@ class DummyTransport2(PySerialTransport): def __init__(self, device=""): self.serial = _dummySerial(device, 38400, timeout=0.1) self._run_event = threading.Event() + + def connect(self): self._run_event.set() @@ -995,10 +1001,11 @@ def __init__(self, device, event_callback=None, self._status = None self._modes = modes self.event_callback = event_callback - self.transport: RFXtrxTransport = transport_protocol(device) - self._thread = threading.Thread(target=self._connect) - self._thread.daemon = True + + def connect(self): + self.transport.connect() + self._thread = threading.Thread(target=self._connect, daemon=True) self._thread.start() self._run_event.wait() diff --git a/examples/receive.py b/examples/receive.py index 4229f24..eed72b9 100644 --- a/examples/receive.py +++ b/examples/receive.py @@ -39,6 +39,7 @@ def main(): modes_list = sys.argv[2].split() if len(sys.argv) > 2 else None print ("modes: ", modes_list) core = RFXtrx.Core(rfxcom_device, print_callback, modes=modes_list) + core.connect() print (core) while True: diff --git a/examples/send.py b/examples/send.py index 8f2cfa9..4ebdb0d 100644 --- a/examples/send.py +++ b/examples/send.py @@ -27,6 +27,7 @@ from time import sleep transport = PySerialTransport('/dev/cu.usbserial-05VN8GHS') +transport.connect() transport.reset() while True: diff --git a/tests/test_base.py b/tests/test_base.py index dc573ab..bbacb6b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -20,6 +20,7 @@ def setUp(self): def test_constructor(self): global num_calbacks core = RFXtrx.Core(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport2) + core.connect() while num_calbacks < 7: time.sleep(0.1) @@ -31,12 +32,14 @@ def test_constructor(self): def test_invalid_packet(self): bytes_array = bytearray([0x09, 0x11, 0xd7, 0x00, 0x01, 0x1d, 0x14, 0x02, 0x79, 0x0a]) core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core.connect() event = core.transport.parse(bytes_array) self.assertIsNone(event) def test_format_packet(self): # Lighting1 core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core.connect() bytes_array = bytearray([0x07, 0x10, 0x00, 0x2a, 0x45, 0x05, 0x01, 0x70]) event = core.transport.parse(bytes_array) self.assertEqual(RFXtrx.ControlEvent, type(event)) @@ -359,6 +362,7 @@ def test_equal_check(self): def test_equal_device_check(self): core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core.connect() data1 = bytearray(b'\x11\x5A\x01\x00\x2E\xB2\x03\x00\x00' b'\x02\xB4\x00\x00\x0C\x46\xA8\x11\x69') energy = core.transport.receive(data1) @@ -392,6 +396,7 @@ def test_equal_device_check(self): def test_get_device(self): core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core.connect() # Lighting1 bytes_array = bytearray([0x07, 0x10, 0x00, 0x2a, 0x45, 0x05, 0x01, 0x70]) event = core.transport.parse(bytes_array) @@ -439,6 +444,7 @@ def test_get_device(self): def test_set_recmodes(self): core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core.connect() time.sleep(0.2) self.assertEqual(None, core._modes) @@ -460,6 +466,7 @@ def test_set_recmodes(self): def test_receive(self): core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core.connect() time.sleep(0.2) # Lighting1 bytes_array = bytearray([0x07, 0x10, 0x00, 0x2a, 0x45, 0x05, 0x01, 0x70]) diff --git a/tests/test_transport_network.py b/tests/test_transport_network.py index 6b820f8..569586e 100644 --- a/tests/test_transport_network.py +++ b/tests/test_transport_network.py @@ -53,6 +53,7 @@ def connected_transport(server: Server): server.event.clear() transport = RFXtrx.PyNetworkTransport(server.address) transport.sock.settimeout(10) + transport.connect() assert server.event.wait(10) return transport, server.connections[-1] From 701881e9d39b4d71d71913f66f522380bea51761 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Feb 2024 15:40:42 +0100 Subject: [PATCH 06/18] Allow setting a connection timeout --- RFXtrx/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index bd8778c..0fbb612 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -777,7 +777,7 @@ def parse(data): return obj return None - def connect(self): + def connect(self, timeout=None): """ connect to device """ def reset(self): @@ -805,7 +805,7 @@ def __init__(self, port): self.port = port self.serial = None - def connect(self): + def connect(self, timeout=None): """ Open a serial connexion """ try: try: @@ -877,10 +877,12 @@ def __init__(self, hostport): self.hostport = hostport # must be a (host, port) tuple self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - def connect(self): + def connect(self, timeout=None): """ Open a socket connection """ try: + self.sock.settimeout(timeout) self.sock.connect(self.hostport) + self.sock.settimeout(None) _LOGGER.debug("Connected to network socket") except socket.error as exception: raise RFXtrxTransportError("Connection failed: {0}".format(exception)) from exception @@ -944,7 +946,7 @@ def __init__(self, device=""): self.device = device self._close_event = threading.Event() - def connect(self): + def connect(self, timeout=None): pass def receive(self, data=None): @@ -984,7 +986,7 @@ def __init__(self, device=""): self.serial = _dummySerial(device, 38400, timeout=0.1) self._run_event = threading.Event() - def connect(self): + def connect(self, timeout=None): self._run_event.set() @@ -1003,11 +1005,12 @@ def __init__(self, device, event_callback=None, self.event_callback = event_callback self.transport: RFXtrxTransport = transport_protocol(device) - def connect(self): - self.transport.connect() + def connect(self, timeout=None): + self.transport.connect(timeout) self._thread = threading.Thread(target=self._connect, daemon=True) self._thread.start() - self._run_event.wait() + if not self._run_event.wait(timeout): + self.close_connection() def _connect(self): try: From 084deb00a43a8fb50daf8ee37631f045af65b972 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 10 Feb 2024 15:41:50 +0100 Subject: [PATCH 07/18] Raise timeout exception on timeout --- RFXtrx/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 0fbb612..220ecfa 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -1011,6 +1011,7 @@ def connect(self, timeout=None): self._thread.start() if not self._run_event.wait(timeout): self.close_connection() + raise TimeoutError() def _connect(self): try: From c018b99a86705e248dfaf7474271bdd21c997a8c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Feb 2024 21:20:26 +0100 Subject: [PATCH 08/18] Convert more errors --- RFXtrx/__init__.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 220ecfa..c92d37c 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -852,18 +852,26 @@ def send(self, data): "Send: %s", " ".join("0x{0:02x}".format(x) for x in pkt) ) - self.serial.write(pkt) + try: + self.serial.write(pkt) + except serial.SerialException as exception: + raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception def reset(self): """ Reset the RFXtrx """ - self.send(b'\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') - sleep(0.3) # Should work with 0.05, but not for me - self.serial.flushInput() + try: + self.send(b'\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + sleep(0.3) # Should work with 0.05, but not for me + self.serial.flushInput() + except serial.SerialException as exception: + raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception def close(self): """ close connection to rfxtrx device """ - self.serial.close() - + try: + self.serial.close() + except serial.SerialException as exception: + raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception ############################################################################### # PyNetworkTransport class @@ -925,18 +933,27 @@ def send(self, data): "Send: %s", " ".join("0x{0:02x}".format(x) for x in pkt) ) - self.sock.send(pkt) + try: + self.sock.send(pkt) + except socket.error as exception: + raise RFXtrxTransportError("Send failed: {0}".format(exception)) from exception def reset(self): """ Reset the RFXtrx """ - self.send(b'\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') - sleep(0.3) - self.sock.sendall(b'') + try: + self.send(b'\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + sleep(0.3) + self.sock.sendall(b'') + except socket.error as exception: + raise RFXtrxTransportError("Reset failed: {0}".format(exception)) from exception def close(self): """ close connection to rfxtrx device """ - self.sock.shutdown(socket.SHUT_RDWR) - self.sock.close() + try: + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + except socket.error as exception: + raise RFXtrxTransportError("Close failed: {0}".format(exception)) from exception class DummyTransport(RFXtrxTransport): From 5f5815c23458925e47df71a3e617e8fa3e8000ae Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Feb 2024 21:22:03 +0100 Subject: [PATCH 09/18] Correct linting --- RFXtrx/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index c92d37c..b444751 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -26,7 +26,6 @@ import glob import socket import threading -import time import logging from time import sleep @@ -680,9 +679,11 @@ class ConnectionEvent(RFXtrxEvent): def __init__(self): super().__init__(None) + class ConnectionLost(ConnectionEvent): """ Connection lost """ + class ConnectionDone(ConnectionEvent): """ Connection lost """ From 449a37a88a8c1d9ef06f68e7ea528a93d3d0ce61 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Feb 2024 21:25:15 +0100 Subject: [PATCH 10/18] Adjust some linting issues --- RFXtrx/__init__.py | 29 ++++++++++++++++++++--------- tests/test_transport_network.py | 13 +++++++++++-- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index b444751..33222af 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -748,6 +748,7 @@ def close(self): # RFXtrxTransportError class ############################################################################### + class RFXtrxTransportError(Exception): """ Connection error """ @@ -755,6 +756,7 @@ class RFXtrxTransportError(Exception): # RFXtrxTransport class ############################################################################### + class RFXtrxTransport: """ Abstract superclass for all transport mechanisms """ @@ -818,13 +820,15 @@ def connect(self, timeout=None): _LOGGER.debug("Attempting connection by name %s", port) self.serial = serial.Serial(port[0], 38400) except serial.SerialException as exception: - raise RFXtrxTransportError("Connection failed: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Connection failed: {0}".format(exception)) from exception def receive_blocking(self): try: return self._receive_packet() except serial.SerialException as exception: - raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Connection was lost: {0}".format(exception)) from exception def _receive_packet(self): """ Wait until a packet is received and return with an RFXtrxEvent """ @@ -856,7 +860,8 @@ def send(self, data): try: self.serial.write(pkt) except serial.SerialException as exception: - raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Connection was lost: {0}".format(exception)) from exception def reset(self): """ Reset the RFXtrx """ @@ -865,14 +870,16 @@ def reset(self): sleep(0.3) # Should work with 0.05, but not for me self.serial.flushInput() except serial.SerialException as exception: - raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Connection was lost: {0}".format(exception)) from exception def close(self): """ close connection to rfxtrx device """ try: self.serial.close() except serial.SerialException as exception: - raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Connection was lost: {0}".format(exception)) from exception ############################################################################### # PyNetworkTransport class @@ -901,7 +908,8 @@ def receive_blocking(self): try: return self._receive_packet() except socket.error as exception: - raise RFXtrxTransportError("Connection was lost: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Connection was lost: {0}".format(exception)) from exception def _receive_packet(self): """ Wait until a packet is received and return with an RFXtrxEvent """ @@ -937,7 +945,8 @@ def send(self, data): try: self.sock.send(pkt) except socket.error as exception: - raise RFXtrxTransportError("Send failed: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Send failed: {0}".format(exception)) from exception def reset(self): """ Reset the RFXtrx """ @@ -946,7 +955,8 @@ def reset(self): sleep(0.3) self.sock.sendall(b'') except socket.error as exception: - raise RFXtrxTransportError("Reset failed: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Reset failed: {0}".format(exception)) from exception def close(self): """ close connection to rfxtrx device """ @@ -954,7 +964,8 @@ def close(self): self.sock.shutdown(socket.SHUT_RDWR) self.sock.close() except socket.error as exception: - raise RFXtrxTransportError("Close failed: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Close failed: {0}".format(exception)) from exception class DummyTransport(RFXtrxTransport): diff --git a/tests/test_transport_network.py b/tests/test_transport_network.py index 569586e..48ed431 100644 --- a/tests/test_transport_network.py +++ b/tests/test_transport_network.py @@ -7,9 +7,10 @@ import threading from typing import Tuple, List + @pytest.fixture(name="server_socket") def fixture_server_socket(): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 0)) sock.settimeout(1) sock.listen(1) @@ -18,12 +19,14 @@ def fixture_server_socket(): finally: sock.close() + @dataclasses.dataclass class Server: address: Tuple connections: List[socket.socket] event = threading.Event() + @pytest.fixture(name="server") def fixture_server(server_socket: socket.socket): @@ -49,6 +52,7 @@ def runner(): connection.close() thread.join() + def connected_transport(server: Server): server.event.clear() transport = RFXtrx.PyNetworkTransport(server.address) @@ -57,9 +61,11 @@ def connected_transport(server: Server): assert server.event.wait(10) return transport, server.connections[-1] + def test_transport_shutdown_between_packet(server: Server): transport, connection = connected_transport(server) - connection.sendall(bytes([0x09, 0x03, 0x01, 0x04, 0x28, 0x0a, 0xb7, 0x66, 0x04, 0x70])) + connection.sendall(bytes([0x09, 0x03, 0x01, 0x04, 0x28, + 0x0a, 0xb7, 0x66, 0x04, 0x70])) connection.shutdown(socket.SHUT_RDWR) pkt = transport.receive_blocking() @@ -67,6 +73,7 @@ def test_transport_shutdown_between_packet(server: Server): with pytest.raises(RFXtrx.RFXtrxTransportError): transport.receive_blocking() + def test_transport_shutdown_mid_packet(server: Server): transport, connection = connected_transport(server) connection.sendall(bytes([0x09, 0x03, 0x01, 0x04])) @@ -75,6 +82,7 @@ def test_transport_shutdown_mid_packet(server: Server): with pytest.raises(RFXtrx.RFXtrxTransportError): transport.receive_blocking() + def test_transport_close_mid_packet(server: Server): transport, connection = connected_transport(server) connection.sendall(bytes([0x09, 0x03, 0x01, 0x04])) @@ -83,6 +91,7 @@ def test_transport_close_mid_packet(server: Server): with pytest.raises(RFXtrx.RFXtrxTransportError): transport.receive_blocking() + def test_transport_empty_packet(server: Server): transport, connection = connected_transport(server) connection.sendall(bytes([0x00])) From 749ea014ea151997521e19d39bf0a1b7c8913a64 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Feb 2024 21:29:23 +0100 Subject: [PATCH 11/18] More flake fixes --- RFXtrx/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 33222af..35c186b 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -861,17 +861,18 @@ def send(self, data): self.serial.write(pkt) except serial.SerialException as exception: raise RFXtrxTransportError( - "Connection was lost: {0}".format(exception)) from exception + "Send failed: {0}".format(exception)) from exception def reset(self): """ Reset the RFXtrx """ try: - self.send(b'\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + self.send(b'\x0D\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00') sleep(0.3) # Should work with 0.05, but not for me self.serial.flushInput() except serial.SerialException as exception: raise RFXtrxTransportError( - "Connection was lost: {0}".format(exception)) from exception + "Reset failed: {0}".format(exception)) from exception def close(self): """ close connection to rfxtrx device """ @@ -879,7 +880,7 @@ def close(self): self.serial.close() except serial.SerialException as exception: raise RFXtrxTransportError( - "Connection was lost: {0}".format(exception)) from exception + "Close failed: {0}".format(exception)) from exception ############################################################################### # PyNetworkTransport class @@ -901,7 +902,8 @@ def connect(self, timeout=None): self.sock.settimeout(None) _LOGGER.debug("Connected to network socket") except socket.error as exception: - raise RFXtrxTransportError("Connection failed: {0}".format(exception)) from exception + raise RFXtrxTransportError( + "Connection failed: {0}".format(exception)) from exception def receive_blocking(self): """ Wait until a packet is received and return with an RFXtrxEvent """ @@ -951,7 +953,8 @@ def send(self, data): def reset(self): """ Reset the RFXtrx """ try: - self.send(b'\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + self.send(b'\x0D\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00') sleep(0.3) self.sock.sendall(b'') except socket.error as exception: From 24f8878d44845c2eedca45e43c225774137b721d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Feb 2024 21:36:47 +0100 Subject: [PATCH 12/18] More lint fixes --- RFXtrx/__init__.py | 4 +++- RFXtrx/lowlevel.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 35c186b..ef536a2 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -1034,12 +1034,13 @@ def __init__(self, device, event_callback=None, self._sensors = {} self._status = None self._modes = modes + self._thread = threading.Thread(target=self._connect, daemon=True) self.event_callback = event_callback self.transport: RFXtrxTransport = transport_protocol(device) def connect(self, timeout=None): + """Connect to device.""" self.transport.connect(timeout) - self._thread = threading.Thread(target=self._connect, daemon=True) self._thread.start() if not self._run_event.wait(timeout): self.close_connection() @@ -1052,6 +1053,7 @@ def _connect(self): _LOGGER.info("Connection lost %s", exception) except Exception: _LOGGER.exception("Unexpected exception from transport") + raise finally: if self.event_callback and self._run_event.is_set(): self.event_callback(ConnectionLost()) diff --git a/RFXtrx/lowlevel.py b/RFXtrx/lowlevel.py index 0e91d45..6200e64 100644 --- a/RFXtrx/lowlevel.py +++ b/RFXtrx/lowlevel.py @@ -2278,7 +2278,7 @@ def load_receive(self, data): (data[10] << 8) + data[11]) self.prodwatthours = ((data[12] * pow(2, 24)) + (data[13] << 16) + (data[14] << 8) + data[15]) - self.tarif_num = (data[16] & 0x0f) + self.tarif_num = data[16] & 0x0f self.voltage = data[17] + 200 self.currentwatt = (data[18] << 8) + data[19] self.state_byte = data[20] @@ -2378,7 +2378,7 @@ def set_transmit(self, subtype, seqnbr, id1, id2, sound): self.id2 = id2 self.sound = sound self.rssi = 0 - self.rssi_byte = (self.rssi << 4) + self.rssi_byte = self.rssi << 4 self.data = bytearray([self.packetlength, self.packettype, self.subtype, self.seqnbr, self.id1, self.id2, self.sound, From f2675956a54cebfe145ceb07ecc88e531c64464e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Feb 2024 21:46:44 +0100 Subject: [PATCH 13/18] Inject constructed transport --- RFXtrx/__init__.py | 6 +++--- examples/receive.py | 2 +- tests/test_base.py | 15 +++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index ef536a2..bb9d986 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -1027,8 +1027,8 @@ class Connect: Has methods for sensors. """ # pylint: disable=too-many-instance-attributes, too-many-arguments - def __init__(self, device, event_callback=None, - transport_protocol=PySerialTransport, + def __init__(self, event_callback=None, + transport=None, modes=None): self._run_event = threading.Event() self._sensors = {} @@ -1036,7 +1036,7 @@ def __init__(self, device, event_callback=None, self._modes = modes self._thread = threading.Thread(target=self._connect, daemon=True) self.event_callback = event_callback - self.transport: RFXtrxTransport = transport_protocol(device) + self.transport: RFXtrxTransport = transport def connect(self, timeout=None): """Connect to device.""" diff --git a/examples/receive.py b/examples/receive.py index eed72b9..26c42f1 100644 --- a/examples/receive.py +++ b/examples/receive.py @@ -38,7 +38,7 @@ def main(): modes_list = sys.argv[2].split() if len(sys.argv) > 2 else None print ("modes: ", modes_list) - core = RFXtrx.Core(rfxcom_device, print_callback, modes=modes_list) + core = RFXtrx.Connect(print_callback, modes=modes_list, transport=RFXtrx.PySerialTransport(rfxcom_device)) core.connect() print (core) diff --git a/tests/test_base.py b/tests/test_base.py index bbacb6b..8c631e9 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -19,7 +19,7 @@ def setUp(self): def test_constructor(self): global num_calbacks - core = RFXtrx.Core(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport2) + core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport2(self.path)) core.connect() while num_calbacks < 7: time.sleep(0.1) @@ -31,14 +31,14 @@ def test_constructor(self): def test_invalid_packet(self): bytes_array = bytearray([0x09, 0x11, 0xd7, 0x00, 0x01, 0x1d, 0x14, 0x02, 0x79, 0x0a]) - core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) core.connect() event = core.transport.parse(bytes_array) self.assertIsNone(event) def test_format_packet(self): # Lighting1 - core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) core.connect() bytes_array = bytearray([0x07, 0x10, 0x00, 0x2a, 0x45, 0x05, 0x01, 0x70]) event = core.transport.parse(bytes_array) @@ -361,7 +361,7 @@ def test_equal_check(self): self.assertFalse(temphum==energy) def test_equal_device_check(self): - core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) core.connect() data1 = bytearray(b'\x11\x5A\x01\x00\x2E\xB2\x03\x00\x00' b'\x02\xB4\x00\x00\x0C\x46\xA8\x11\x69') @@ -395,7 +395,7 @@ def test_equal_device_check(self): core.close_connection() def test_get_device(self): - core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) core.connect() # Lighting1 bytes_array = bytearray([0x07, 0x10, 0x00, 0x2a, 0x45, 0x05, 0x01, 0x70]) @@ -442,8 +442,7 @@ def test_get_device(self): core.close_connection() def test_set_recmodes(self): - core = RFXtrx.Connect(self.path, event_callback=_callback, - transport_protocol=RFXtrx.DummyTransport) + core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) core.connect() time.sleep(0.2) self.assertEqual(None, core._modes) @@ -465,7 +464,7 @@ def test_set_recmodes(self): core.set_recmodes(['arc', 'oregon', 'unknown-mode']) def test_receive(self): - core = RFXtrx.Connect(self.path, event_callback=_callback, transport_protocol=RFXtrx.DummyTransport) + core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) core.connect() time.sleep(0.2) # Lighting1 From 77ff4e5978deaf75d66d8f9c78f8508417efb7df Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Feb 2024 21:49:53 +0100 Subject: [PATCH 14/18] Change init order --- RFXtrx/__init__.py | 3 +-- examples/receive.py | 2 +- tests/test_base.py | 14 +++++++------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index bb9d986..a06bc5e 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -1027,8 +1027,7 @@ class Connect: Has methods for sensors. """ # pylint: disable=too-many-instance-attributes, too-many-arguments - def __init__(self, event_callback=None, - transport=None, + def __init__(self, transport, event_callback=None, modes=None): self._run_event = threading.Event() self._sensors = {} diff --git a/examples/receive.py b/examples/receive.py index 26c42f1..576d7d0 100644 --- a/examples/receive.py +++ b/examples/receive.py @@ -38,7 +38,7 @@ def main(): modes_list = sys.argv[2].split() if len(sys.argv) > 2 else None print ("modes: ", modes_list) - core = RFXtrx.Connect(print_callback, modes=modes_list, transport=RFXtrx.PySerialTransport(rfxcom_device)) + core = RFXtrx.Connect(RFXtrx.PySerialTransport(rfxcom_device), print_callback, modes=modes_list) core.connect() print (core) diff --git a/tests/test_base.py b/tests/test_base.py index 8c631e9..108cb79 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -19,7 +19,7 @@ def setUp(self): def test_constructor(self): global num_calbacks - core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport2(self.path)) + core = RFXtrx.Connect(RFXtrx.DummyTransport2(self.path), event_callback=_callback) core.connect() while num_calbacks < 7: time.sleep(0.1) @@ -31,14 +31,14 @@ def test_constructor(self): def test_invalid_packet(self): bytes_array = bytearray([0x09, 0x11, 0xd7, 0x00, 0x01, 0x1d, 0x14, 0x02, 0x79, 0x0a]) - core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) + core = RFXtrx.Connect(RFXtrx.DummyTransport(self.path), event_callback=_callback) core.connect() event = core.transport.parse(bytes_array) self.assertIsNone(event) def test_format_packet(self): # Lighting1 - core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) + core = RFXtrx.Connect(RFXtrx.DummyTransport(self.path), event_callback=_callback) core.connect() bytes_array = bytearray([0x07, 0x10, 0x00, 0x2a, 0x45, 0x05, 0x01, 0x70]) event = core.transport.parse(bytes_array) @@ -361,7 +361,7 @@ def test_equal_check(self): self.assertFalse(temphum==energy) def test_equal_device_check(self): - core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) + core = RFXtrx.Connect(RFXtrx.DummyTransport(self.path), event_callback=_callback) core.connect() data1 = bytearray(b'\x11\x5A\x01\x00\x2E\xB2\x03\x00\x00' b'\x02\xB4\x00\x00\x0C\x46\xA8\x11\x69') @@ -395,7 +395,7 @@ def test_equal_device_check(self): core.close_connection() def test_get_device(self): - core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) + core = RFXtrx.Connect(RFXtrx.DummyTransport(self.path), event_callback=_callback) core.connect() # Lighting1 bytes_array = bytearray([0x07, 0x10, 0x00, 0x2a, 0x45, 0x05, 0x01, 0x70]) @@ -442,7 +442,7 @@ def test_get_device(self): core.close_connection() def test_set_recmodes(self): - core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) + core = RFXtrx.Connect(RFXtrx.DummyTransport(self.path), event_callback=_callback) core.connect() time.sleep(0.2) self.assertEqual(None, core._modes) @@ -464,7 +464,7 @@ def test_set_recmodes(self): core.set_recmodes(['arc', 'oregon', 'unknown-mode']) def test_receive(self): - core = RFXtrx.Connect(event_callback=_callback, transport=RFXtrx.DummyTransport(self.path)) + core = RFXtrx.Connect(RFXtrx.DummyTransport(self.path), event_callback=_callback) core.connect() time.sleep(0.2) # Lighting1 From 7f4e72a931085949bda68406c49355d5144630a7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 13 Feb 2024 20:56:22 +0100 Subject: [PATCH 15/18] Use decorators to hide low level exceptions --- RFXtrx/__init__.py | 122 ++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index a06bc5e..837220b 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -23,6 +23,7 @@ # pylint: disable=R0903, invalid-name # pylint: disable= too-many-lines +import functools import glob import socket import threading @@ -804,31 +805,38 @@ def send(self, data): class PySerialTransport(RFXtrxTransport): """ Implementation of a transport using PySerial """ + @staticmethod + def errors(message): + def _errors(func): + @functools.wraps(func) + def __errors(*args, **kargs): + try: + return func(*args, **kargs) + except (serial.SerialException, OSError) as exception: + raise RFXtrxTransportError( + "{0} failed: {1}".format(message, exception)) from exception + return __errors + return _errors + def __init__(self, port): self.port = port self.serial = None + @errors("connect") def connect(self, timeout=None): """ Open a serial connexion """ try: - try: - self.serial = serial.Serial(self.port, 38400) - except serial.SerialException: - port = glob.glob('/dev/serial/by-id/usb-RFXCOM_*-port0') - if len(port) < 1: - raise - _LOGGER.debug("Attempting connection by name %s", port) - self.serial = serial.Serial(port[0], 38400) - except serial.SerialException as exception: - raise RFXtrxTransportError( - "Connection failed: {0}".format(exception)) from exception - + self.serial = serial.Serial(self.port, 38400) + except serial.SerialException: + port = glob.glob('/dev/serial/by-id/usb-RFXCOM_*-port0') + if len(port) < 1: + raise + _LOGGER.debug("Attempting connection by name %s", port) + self.serial = serial.Serial(port[0], 38400) + + @errors("receive") def receive_blocking(self): - try: - return self._receive_packet() - except serial.SerialException as exception: - raise RFXtrxTransportError( - "Connection was lost: {0}".format(exception)) from exception + return self._receive_packet() def _receive_packet(self): """ Wait until a packet is received and return with an RFXtrxEvent """ @@ -845,6 +853,7 @@ def _receive_packet(self): ) return self.parse(pkt) + @errors("send") def send(self, data): """ Send the given packet """ if isinstance(data, bytearray): @@ -857,30 +866,20 @@ def send(self, data): "Send: %s", " ".join("0x{0:02x}".format(x) for x in pkt) ) - try: - self.serial.write(pkt) - except serial.SerialException as exception: - raise RFXtrxTransportError( - "Send failed: {0}".format(exception)) from exception + self.serial.write(pkt) + @errors("reset") def reset(self): """ Reset the RFXtrx """ - try: - self.send(b'\x0D\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00') - sleep(0.3) # Should work with 0.05, but not for me - self.serial.flushInput() - except serial.SerialException as exception: - raise RFXtrxTransportError( - "Reset failed: {0}".format(exception)) from exception + self.send(b'\x0D\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00') + sleep(0.3) # Should work with 0.05, but not for me + self.serial.flushInput() + @errors("close") def close(self): """ close connection to rfxtrx device """ - try: - self.serial.close() - except serial.SerialException as exception: - raise RFXtrxTransportError( - "Close failed: {0}".format(exception)) from exception + self.serial.close() ############################################################################### # PyNetworkTransport class @@ -890,28 +889,35 @@ def close(self): class PyNetworkTransport(RFXtrxTransport): """ Implementation of a transport using sockets """ + @staticmethod + def errors(message): + def _errors(func): + @functools.wraps(func) + def __errors(*args, **kargs): + try: + return func(*args, **kargs) + except socket.error as exception: + raise RFXtrxTransportError( + "{0} failed: {1}".format(message, exception)) from exception + return __errors + return _errors + def __init__(self, hostport): self.hostport = hostport # must be a (host, port) tuple self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + @errors("connect") def connect(self, timeout=None): """ Open a socket connection """ - try: - self.sock.settimeout(timeout) - self.sock.connect(self.hostport) - self.sock.settimeout(None) - _LOGGER.debug("Connected to network socket") - except socket.error as exception: - raise RFXtrxTransportError( - "Connection failed: {0}".format(exception)) from exception + self.sock.settimeout(timeout) + self.sock.connect(self.hostport) + self.sock.settimeout(None) + _LOGGER.debug("Connected to network socket") + @errors("receive") def receive_blocking(self): """ Wait until a packet is received and return with an RFXtrxEvent """ - try: - return self._receive_packet() - except socket.error as exception: - raise RFXtrxTransportError( - "Connection was lost: {0}".format(exception)) from exception + return self._receive_packet() def _receive_packet(self): """ Wait until a packet is received and return with an RFXtrxEvent """ @@ -932,6 +938,7 @@ def _receive_packet(self): ) return self.parse(pkt) + @errors("send") def send(self, data): """ Send the given packet """ if isinstance(data, bytearray): @@ -944,12 +951,9 @@ def send(self, data): "Send: %s", " ".join("0x{0:02x}".format(x) for x in pkt) ) - try: - self.sock.send(pkt) - except socket.error as exception: - raise RFXtrxTransportError( - "Send failed: {0}".format(exception)) from exception + self.sock.send(pkt) + @errors("reset") def reset(self): """ Reset the RFXtrx """ try: @@ -961,14 +965,11 @@ def reset(self): raise RFXtrxTransportError( "Reset failed: {0}".format(exception)) from exception + @errors("reset") def close(self): """ close connection to rfxtrx device """ - try: - self.sock.shutdown(socket.SHUT_RDWR) - self.sock.close() - except socket.error as exception: - raise RFXtrxTransportError( - "Close failed: {0}".format(exception)) from exception + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() class DummyTransport(RFXtrxTransport): @@ -1050,9 +1051,6 @@ def _connect(self): self._connect_internal() except RFXtrxTransportError as exception: _LOGGER.info("Connection lost %s", exception) - except Exception: - _LOGGER.exception("Unexpected exception from transport") - raise finally: if self.event_callback and self._run_event.is_set(): self.event_callback(ConnectionLost()) From c17833d62b1ceb6901d1e2c98644ec5d674ff05e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 13 Feb 2024 21:12:07 +0100 Subject: [PATCH 16/18] Make compatible with legacy python --- RFXtrx/__init__.py | 65 ++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 837220b..326fd71 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -797,6 +797,23 @@ def send(self, data): """ Send the given packet """ +def transport_errors(message): + """ Decorator to wrap low level errors in known error. """ + def _errors(func): + @functools.wraps(func) + def __errors(instance: RFXtrxTransport, *args, **kargs): + try: + return func(instance, *args, **kargs) + except (socket.error, + serial.SerialException, + OSError) as exception: + _LOGGER.debug("%s failed: %s", message, str(exception), exc_info=True) + raise RFXtrxTransportError( + "{0} failed: {1}".format(message, exception) + ) from exception + return __errors + return _errors + ############################################################################### # PySerialTransport class ############################################################################### @@ -805,24 +822,11 @@ def send(self, data): class PySerialTransport(RFXtrxTransport): """ Implementation of a transport using PySerial """ - @staticmethod - def errors(message): - def _errors(func): - @functools.wraps(func) - def __errors(*args, **kargs): - try: - return func(*args, **kargs) - except (serial.SerialException, OSError) as exception: - raise RFXtrxTransportError( - "{0} failed: {1}".format(message, exception)) from exception - return __errors - return _errors - def __init__(self, port): self.port = port self.serial = None - @errors("connect") + @transport_errors("connect") def connect(self, timeout=None): """ Open a serial connexion """ try: @@ -834,7 +838,7 @@ def connect(self, timeout=None): _LOGGER.debug("Attempting connection by name %s", port) self.serial = serial.Serial(port[0], 38400) - @errors("receive") + @transport_errors("receive") def receive_blocking(self): return self._receive_packet() @@ -853,7 +857,7 @@ def _receive_packet(self): ) return self.parse(pkt) - @errors("send") + @transport_errors("send") def send(self, data): """ Send the given packet """ if isinstance(data, bytearray): @@ -868,15 +872,15 @@ def send(self, data): ) self.serial.write(pkt) - @errors("reset") + @transport_errors("reset") def reset(self): """ Reset the RFXtrx """ self.send(b'\x0D\x00\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00') + b'\x00\x00\x00\x00\x00\x00\x00') sleep(0.3) # Should work with 0.05, but not for me self.serial.flushInput() - @errors("close") + @transport_errors("close") def close(self): """ close connection to rfxtrx device """ self.serial.close() @@ -889,24 +893,11 @@ def close(self): class PyNetworkTransport(RFXtrxTransport): """ Implementation of a transport using sockets """ - @staticmethod - def errors(message): - def _errors(func): - @functools.wraps(func) - def __errors(*args, **kargs): - try: - return func(*args, **kargs) - except socket.error as exception: - raise RFXtrxTransportError( - "{0} failed: {1}".format(message, exception)) from exception - return __errors - return _errors - def __init__(self, hostport): self.hostport = hostport # must be a (host, port) tuple self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - @errors("connect") + @transport_errors("connect") def connect(self, timeout=None): """ Open a socket connection """ self.sock.settimeout(timeout) @@ -914,7 +905,7 @@ def connect(self, timeout=None): self.sock.settimeout(None) _LOGGER.debug("Connected to network socket") - @errors("receive") + @transport_errors("receive") def receive_blocking(self): """ Wait until a packet is received and return with an RFXtrxEvent """ return self._receive_packet() @@ -938,7 +929,7 @@ def _receive_packet(self): ) return self.parse(pkt) - @errors("send") + @transport_errors("send") def send(self, data): """ Send the given packet """ if isinstance(data, bytearray): @@ -953,7 +944,7 @@ def send(self, data): ) self.sock.send(pkt) - @errors("reset") + @transport_errors("reset") def reset(self): """ Reset the RFXtrx """ try: @@ -965,7 +956,7 @@ def reset(self): raise RFXtrxTransportError( "Reset failed: {0}".format(exception)) from exception - @errors("reset") + @transport_errors("reset") def close(self): """ close connection to rfxtrx device """ self.sock.shutdown(socket.SHUT_RDWR) From 25df52192a63d6ce3ac3c4401533316f3e87c3f9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 13 Feb 2024 21:15:22 +0100 Subject: [PATCH 17/18] Fix lint --- RFXtrx/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 326fd71..83da61a 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -807,7 +807,8 @@ def __errors(instance: RFXtrxTransport, *args, **kargs): except (socket.error, serial.SerialException, OSError) as exception: - _LOGGER.debug("%s failed: %s", message, str(exception), exc_info=True) + _LOGGER.debug("%s failed: %s", message, + str(exception), exc_info=True) raise RFXtrxTransportError( "{0} failed: {1}".format(message, exception) ) from exception From 37160f92294fdd9efe22c318ab4ace977ce0f920 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 24 Feb 2024 10:49:07 +0100 Subject: [PATCH 18/18] Suppress errors on close --- RFXtrx/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/RFXtrx/__init__.py b/RFXtrx/__init__.py index 83da61a..9bafb96 100644 --- a/RFXtrx/__init__.py +++ b/RFXtrx/__init__.py @@ -28,6 +28,7 @@ import socket import threading import logging +from contextlib import suppress from time import sleep @@ -884,7 +885,8 @@ def reset(self): @transport_errors("close") def close(self): """ close connection to rfxtrx device """ - self.serial.close() + with suppress(serial.SerialException): + self.serial.close() ############################################################################### # PyNetworkTransport class @@ -957,11 +959,11 @@ def reset(self): raise RFXtrxTransportError( "Reset failed: {0}".format(exception)) from exception - @transport_errors("reset") + @transport_errors("close") def close(self): """ close connection to rfxtrx device """ - self.sock.shutdown(socket.SHUT_RDWR) - self.sock.close() + with suppress(socket.error): + self.sock.close() class DummyTransport(RFXtrxTransport):