Skip to content

Commit

Permalink
fix: Allowed sending of device address on all transport. Raising exce…
Browse files Browse the repository at this point in the history
…ption if a transport requires address and non is given.

Added typings to transports.py
Fixes #9
  • Loading branch information
Krolken committed May 18, 2020
1 parent 3690dae commit adc659c
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 36 deletions.
15 changes: 8 additions & 7 deletions iec62056_21/client.py
Expand Up @@ -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):
"""
Expand All @@ -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())

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions iec62056_21/exceptions.py
Expand Up @@ -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"""

Expand Down
85 changes: 56 additions & 29 deletions iec62056_21/transports.py
@@ -1,5 +1,6 @@
import time
import logging
from typing import Tuple, Union, Optional

import serial
import socket
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -176,31 +182,31 @@ 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.
:param data:
"""
raise NotImplemented("Must be defined in subclass")

def recv(self, chars):
def recv(self, chars: int) -> bytes:
"""
Will receive data over the transport.
:param chars:
"""
return self._recv(chars)

def _recv(self, chars):
def _recv(self, chars: int) -> bytes:
"""
Transport dependant sending functionality.
:param 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.
Expand All @@ -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.
"""
Expand All @@ -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,
Expand Down Expand Up @@ -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.
"""
Expand All @@ -316,42 +334,51 @@ 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.
:param baud:
"""
pass

def _get_socket(self):
def _get_socket(self) -> socket.socket:
"""
Create a correct socket.
"""
Expand Down
22 changes: 22 additions & 0 deletions 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")

0 comments on commit adc659c

Please sign in to comment.