diff --git a/iec62056_21/client.py b/iec62056_21/client.py index 756d4ae..9220167 100644 --- a/iec62056_21/client.py +++ b/iec62056_21/client.py @@ -61,6 +61,12 @@ def __init__( self.error_parser = error_parser_class() self._current_baudrate: int = 300 + if self.transport.TRANSPORT_REQUIRES_ADDRESS and not self.device_address: + raise exceptions.Iec6205621ClientError( + f"The transported used ({self.transport}) requires a device address " + f"and none was supplied." + ) + @property def switchover_baudrate(self): """ @@ -82,7 +88,7 @@ def read_single_value(self, address, additional_data="1"): # TODO: When not using the additional data on an EMH meter we get an ack back. # a bit later we get the break message. Is the device waiting? - request = messages.CommandMessage.for_single_read(address) + request = messages.CommandMessage.for_single_read(address, additional_data) logger.info(f"Sending read request: {request}") self.transport.send(request.to_bytes()) @@ -151,7 +157,6 @@ def startup(self): # Setting the baudrate to the one propsed by the device. self._switchover_baudrate_char = ident_msg.switchover_baudrate_char - print(self.BAUDRATES_MODE_C[self._switchover_baudrate_char]) self.identification = ident_msg.identification self.manufacturer_id = ident_msg.manufacturer @@ -241,11 +246,7 @@ def send_init_request(self): you want to talk to by adding the address in the request. """ - if self.transport.TRANSPORT_REQUIRES_ADDRESS: - request = messages.RequestMessage(device_address=self.device_address) - else: - request = messages.RequestMessage() - + request = messages.RequestMessage(device_address=self.device_address) logger.info(f"Sending request message: {request}") self.transport.send(request.to_bytes()) self.rest() diff --git a/iec62056_21/exceptions.py b/iec62056_21/exceptions.py index 858a96f..c8eee46 100644 --- a/iec62056_21/exceptions.py +++ b/iec62056_21/exceptions.py @@ -2,6 +2,10 @@ class Iec6205621Exception(Exception): """General IEC62056-21 Exception""" +class Iec6205621ClientError(Iec6205621Exception): + """Client error""" + + class Iec6205621ParseError(Iec6205621Exception): """Error in parsing IEC62056-21 data""" diff --git a/iec62056_21/transports.py b/iec62056_21/transports.py index a2da05a..d2ecc1d 100644 --- a/iec62056_21/transports.py +++ b/iec62056_21/transports.py @@ -1,5 +1,6 @@ import time import logging +from typing import Tuple, Union, Optional import serial import socket @@ -17,18 +18,18 @@ class BaseTransport: Base transport class for IEC 62056-21 communication. """ - TRANSPORT_REQUIRES_ADDRESS = True + TRANSPORT_REQUIRES_ADDRESS: bool = True - def __init__(self, timeout=30): + def __init__(self, timeout: int = 30): self.timeout = timeout - def connect(self): + def connect(self) -> None: raise NotImplemented("Must be defined in subclass") - def disconnect(self): + def disconnect(self) -> None: raise NotImplemented("Must be defined in subclass") - def read(self, timeout=None): + def read(self, timeout: Optional[int] = None) -> bytes: """ Will read a normal readout. Supports both full and partial block readout. When using partial blocks it will recreate the messages as it was not sent with @@ -49,7 +50,7 @@ def read(self, timeout=None): while True: in_data = b"" - duration = 0 + duration: float = 0.0 start_time = time.time() while True: b = self.recv(1) @@ -130,7 +131,12 @@ def read(self, timeout=None): return total_data - def simple_read(self, start_char, end_char, timeout=None): + def simple_read( + self, + start_char: Union[str, bytes], + end_char: Union[str, bytes], + timeout: Optional[int] = None, + ) -> bytes: """ A more flexible read for use with some messages. """ @@ -140,7 +146,7 @@ def simple_read(self, start_char, end_char, timeout=None): in_data = b"" start_char_received = False timeout = timeout or self.timeout - duration = 0 + duration: float = 0.0 start_time = time.time() while True: b = self.recv(1) @@ -167,7 +173,7 @@ def simple_read(self, start_char, end_char, timeout=None): logger.debug(f"Received {in_data!r} over transport: {self.__class__.__name__}") return in_data - def send(self, data: bytes): + def send(self, data: bytes) -> None: """ Will send data over the transport @@ -176,7 +182,7 @@ def send(self, data: bytes): self._send(data) logger.debug(f"Sent {data!r} over transport: {self.__class__.__name__}") - def _send(self, data: bytes): + def _send(self, data: bytes) -> None: """ Transport dependant sending functionality. @@ -184,7 +190,7 @@ def _send(self, data: bytes): """ raise NotImplemented("Must be defined in subclass") - def recv(self, chars): + def recv(self, chars: int) -> bytes: """ Will receive data over the transport. @@ -192,7 +198,7 @@ def recv(self, chars): """ return self._recv(chars) - def _recv(self, chars): + def _recv(self, chars: int) -> bytes: """ Transport dependant sending functionality. @@ -200,7 +206,7 @@ def _recv(self, chars): """ raise NotImplemented("Must be defined in subclass") - def switch_baudrate(self, baud): + def switch_baudrate(self, baud: int) -> None: """ The protocol defines a baudrate switchover process. Though it might not be used in all available transports. @@ -219,13 +225,13 @@ class SerialTransport(BaseTransport): TRANSPORT_REQUIRES_ADDRESS = False - def __init__(self, port, timeout=10): + def __init__(self, port: str, timeout: int = 10): super().__init__(timeout=timeout) - self.port_name = port - self.port = None + self.port_name: str = port + self.port: Optional[serial.Serial] = None - def connect(self, baudrate: int = 300): + def connect(self, baudrate: int = 300) -> None: """ Creates a serial port. """ @@ -242,36 +248,48 @@ def connect(self, baudrate: int = 300): xonxoff=False, ) - def disconnect(self): + def disconnect(self) -> None: """ Closes and removes the serial port. """ + if self.port is None: + raise TransportError("Serial port is closed.") + self.port.close() self.port = None - def _send(self, data: bytes): + def _send(self, data: bytes) -> None: """ Sends data over the serial port. :param data: """ + if self.port is None: + raise TransportError("Serial port is closed.") + self.port.write(data) self.port.flush() - def _recv(self, chars=1): + def _recv(self, chars: int = 1) -> bytes: """ Receives data over the serial port. :param chars: """ + if self.port is None: + raise TransportError("Serial port is closed.") + return self.port.read(chars) - def switch_baudrate(self, baud): + def switch_baudrate(self, baud: int) -> None: """ Creates a new serial port with the correct baudrate. :param baud: """ + if self.port is None: + raise TransportError("Serial port is closed.") + logger.info(f"Switching baudrate to: {baud}") self.port = self.port = serial.Serial( self.port_name, @@ -300,13 +318,13 @@ class TcpTransport(BaseTransport): Transport class for TCP/IP communication. """ - def __init__(self, address, timeout=30): + def __init__(self, address: Tuple[str, int], timeout: int = 30): super().__init__(timeout=timeout) self.address = address - self.socket = self._get_socket() + self.socket: Optional[socket.socket] = self._get_socket() - def connect(self): + def connect(self) -> None: """ Connects the socket to the device network interface. """ @@ -316,34 +334,43 @@ def connect(self): logger.debug(f"Connecting to {self.address}") self.socket.connect(self.address) - def disconnect(self): + def disconnect(self) -> None: """ Closes and removes the socket. """ + if self.socket is None: + raise TransportError("Socket is closed") + self.socket.close() self.socket = None - def _send(self, data: bytes): + def _send(self, data: bytes) -> None: """ Sends data over the socket. :param data: """ + if self.socket is None: + raise TransportError("Socket is closed") + self.socket.sendall(data) - def _recv(self, chars=1): + def _recv(self, chars: int = 1) -> bytes: """ Receives data from the socket. :param chars: """ + if self.socket is None: + raise TransportError("Socket is closed") + try: b = self.socket.recv(chars) except (OSError, IOError, socket.timeout, socket.error) as e: raise TransportError from e return b - def switch_baudrate(self, baud): + def switch_baudrate(self, baud: int) -> None: """ Baudrate has not meaning in TCP/IP so we just dont do anything. @@ -351,7 +378,7 @@ def switch_baudrate(self, baud): """ pass - def _get_socket(self): + def _get_socket(self) -> socket.socket: """ Create a correct socket. """ diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..c69c8fc --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,22 @@ +import pytest +from iec62056_21 import exceptions, client, transports + + +class TestIec6205621Client: + def test_with_no_address_when_required_raises_client_error(self): + with pytest.raises(exceptions.Iec6205621ClientError): + c = client.Iec6205621Client.with_tcp_transport(("192.168.1.1", 5000)) + + def test_can_create_client_with_tcp_transport(self): + c = client.Iec6205621Client.with_tcp_transport( + "192.168.1.1", device_address="00000000" + ) + + def test_no_address_when_required_raises_client_error(self): + trans = transports.TcpTransport(address=("192.168.1.1", 5000)) + with pytest.raises(exceptions.Iec6205621ClientError): + c = client.Iec6205621Client(transport=trans) + + def test_can_create_client_tcp_transport(self): + trans = transports.TcpTransport(address=("192.168.1.1", 5000)) + c = client.Iec6205621Client(transport=trans, device_address="00000000")