Skip to content

Commit ac25d6d

Browse files
authored
Client coverage 100%. (#1943)
1 parent 531983f commit ac25d6d

File tree

10 files changed

+123
-240
lines changed

10 files changed

+123
-240
lines changed

pymodbus/client/base.py

Lines changed: 12 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ async def async_execute(self, request=None):
163163
resp = b"Broadcast write sent - no response expected"
164164
break
165165
try:
166-
req = self._build_response(request.transaction_id)
166+
req = self.build_response(request.transaction_id)
167167
resp = await asyncio.wait_for(
168168
req, timeout=self.comm_params.timeout_connect
169169
)
@@ -186,14 +186,6 @@ def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
186186
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
187187
return len(data)
188188

189-
def callback_disconnected(self, _reason: Exception | None) -> None:
190-
"""Handle lost connection."""
191-
for tid in list(self.transaction):
192-
self.raise_future(
193-
self.transaction.getTransaction(tid),
194-
ConnectionException("Connection lost during request"),
195-
)
196-
197189
async def connect(self):
198190
"""Connect to the modbus remote host."""
199191

@@ -212,7 +204,7 @@ def _handle_response(self, reply, **_kwargs):
212204
else:
213205
Log.debug("Unrequested message: {}", reply, ":str")
214206

215-
def _build_response(self, tid):
207+
def build_response(self, tid):
216208
"""Return a deferred response for the current request."""
217209
my_future = asyncio.Future()
218210
if not self.transport:
@@ -229,26 +221,12 @@ def send(self, request):
229221
230222
:meta private:
231223
"""
232-
if self.state != ModbusTransactionState.RETRYING:
233-
Log.debug('New Transaction state "SENDING"')
234-
self.state = ModbusTransactionState.SENDING
235-
return request
236224

237225
def recv(self, size):
238226
"""Receive data.
239227
240228
:meta private:
241229
"""
242-
return size
243-
244-
@classmethod
245-
def _get_address_family(cls, address):
246-
"""Get the correct address family."""
247-
try:
248-
_ = socket.inet_pton(socket.AF_INET6, address)
249-
except OSError: # not a valid ipv6 address
250-
return socket.AF_INET
251-
return socket.AF_INET6
252230

253231
# ----------------------------------------------------------------------- #
254232
# The magic methods
@@ -259,8 +237,7 @@ def __enter__(self):
259237
:returns: The current instance of the client
260238
:raises ConnectionException:
261239
"""
262-
if not self.connect():
263-
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
240+
self.connect()
264241
return self
265242

266243
async def __aenter__(self):
@@ -269,8 +246,7 @@ async def __aenter__(self):
269246
:returns: The current instance of the client
270247
:raises ConnectionException:
271248
"""
272-
if not await self.connect():
273-
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
249+
await self.connect()
274250
return self
275251

276252
def __exit__(self, klass, value, traceback):
@@ -400,11 +376,6 @@ def __init__( # pylint: disable=too-many-arguments
400376
# ----------------------------------------------------------------------- #
401377
# Client external interface
402378
# ----------------------------------------------------------------------- #
403-
@property
404-
def connected(self) -> bool:
405-
"""Return state of connection."""
406-
return self.is_active()
407-
408379
def register(self, custom_response_class: ModbusResponse) -> None:
409380
"""Register a custom response class with the decoder (call **sync**).
410381
@@ -416,13 +387,6 @@ def register(self, custom_response_class: ModbusResponse) -> None:
416387
"""
417388
self.framer.decoder.register(custom_response_class)
418389

419-
def close(self, reconnect: bool = False) -> None:
420-
"""Close connection."""
421-
if reconnect:
422-
self.connection_lost(asyncio.TimeoutError("Server not responding"))
423-
else:
424-
self.transport_close()
425-
426390
def idle_time(self) -> float:
427391
"""Time before initiating next transaction (call **sync**).
428392
@@ -444,80 +408,6 @@ def execute(self, request: ModbusRequest | None = None) -> ModbusResponse:
444408
raise ConnectionException(f"Failed to connect[{self!s}]")
445409
return self.transaction.execute(request)
446410

447-
# ----------------------------------------------------------------------- #
448-
# Merged client methods
449-
# ----------------------------------------------------------------------- #
450-
async def async_execute(self, request=None):
451-
"""Execute requests asynchronously."""
452-
request.transaction_id = self.transaction.getNextTID()
453-
packet = self.framer.buildPacket(request)
454-
455-
count = 0
456-
while count <= self.params.retries:
457-
if not count or not self.no_resend_on_retry:
458-
self.transport_send(packet)
459-
if self.params.broadcast_enable and not request.slave_id:
460-
resp = b"Broadcast write sent - no response expected"
461-
break
462-
try:
463-
req = self._build_response(request.transaction_id)
464-
resp = await asyncio.wait_for(
465-
req, timeout=self.comm_params.timeout_connect
466-
)
467-
break
468-
except asyncio.exceptions.TimeoutError:
469-
count += 1
470-
if count > self.params.retries:
471-
self.close(reconnect=True)
472-
raise ModbusIOException(
473-
f"ERROR: No response received after {self.params.retries} retries"
474-
)
475-
476-
return resp
477-
478-
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
479-
"""Handle received data.
480-
481-
returns number of bytes consumed
482-
"""
483-
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
484-
return len(data)
485-
486-
def callback_disconnected(self, _reason: Exception | None) -> None:
487-
"""Handle lost connection."""
488-
for tid in list(self.transaction):
489-
self.raise_future(
490-
self.transaction.getTransaction(tid),
491-
ConnectionException("Connection lost during request"),
492-
)
493-
494-
async def connect(self):
495-
"""Connect to the modbus remote host."""
496-
497-
def raise_future(self, my_future, exc):
498-
"""Set exception of a future if not done."""
499-
if not my_future.done():
500-
my_future.set_exception(exc)
501-
502-
def _handle_response(self, reply, **_kwargs):
503-
"""Handle the processed response and link to correct deferred."""
504-
if reply is not None:
505-
tid = reply.transaction_id
506-
if handler := self.transaction.getTransaction(tid):
507-
if not handler.done():
508-
handler.set_result(reply)
509-
else:
510-
Log.debug("Unrequested message: {}", reply, ":str")
511-
512-
def _build_response(self, tid):
513-
"""Return a deferred response for the current request."""
514-
my_future = asyncio.Future()
515-
if not self.transport:
516-
self.raise_future(my_future, ConnectionException("Client is not connected"))
517-
else:
518-
self.transaction.addTransaction(my_future, tid)
519-
return my_future
520-
521411
# ----------------------------------------------------------------------- #
522412
# Internal methods
523413
# ----------------------------------------------------------------------- #
@@ -539,14 +429,20 @@ def recv(self, size):
539429
return size
540430

541431
@classmethod
542-
def _get_address_family(cls, address):
432+
def get_address_family(cls, address):
543433
"""Get the correct address family."""
544434
try:
545435
_ = socket.inet_pton(socket.AF_INET6, address)
546436
except OSError: # not a valid ipv6 address
547437
return socket.AF_INET
548438
return socket.AF_INET6
549439

440+
def connect(self):
441+
"""Connect to other end, overwritten."""
442+
443+
def close(self):
444+
"""Close connection, overwritten."""
445+
550446
# ----------------------------------------------------------------------- #
551447
# The magic methods
552448
# ----------------------------------------------------------------------- #
@@ -556,28 +452,13 @@ def __enter__(self):
556452
:returns: The current instance of the client
557453
:raises ConnectionException:
558454
"""
559-
if not self.connect():
560-
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
561-
return self
562-
563-
async def __aenter__(self):
564-
"""Implement the client with enter block.
565-
566-
:returns: The current instance of the client
567-
:raises ConnectionException:
568-
"""
569-
if not await self.connect():
570-
raise ConnectionException(f"Failed to connect[{self.__str__()}]")
455+
self.connect()
571456
return self
572457

573458
def __exit__(self, klass, value, traceback):
574459
"""Implement the client with exit block."""
575460
self.close()
576461

577-
async def __aexit__(self, klass, value, traceback):
578-
"""Implement the client with exit block."""
579-
self.close()
580-
581462
def __str__(self):
582463
"""Build a string representation of the connection.
583464

pymodbus/client/mixin.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import pymodbus.other_message as pdu_other_msg
1515
import pymodbus.register_read_message as pdu_reg_read
1616
import pymodbus.register_write_message as pdu_req_write
17-
from pymodbus.constants import INTERNAL_ERROR
1817
from pymodbus.exceptions import ModbusException
1918
from pymodbus.pdu import ModbusRequest, ModbusResponse
2019

@@ -49,19 +48,18 @@ def __init__(self):
4948
"""Initialize."""
5049

5150
def execute(
52-
self, request: ModbusRequest
51+
self, _request: ModbusRequest
5352
) -> ModbusResponse | Awaitable[ModbusResponse]:
5453
"""Execute request (code ???).
5554
56-
:param request: Request to send
5755
:raises ModbusException:
5856
5957
Call with custom function codes.
6058
6159
.. tip::
6260
Response is not interpreted.
6361
"""
64-
raise ModbusException(INTERNAL_ERROR)
62+
return ModbusResponse()
6563

6664
def read_coils(
6765
self, address: int, count: int = 1, slave: int = 0, **kwargs: Any

pymodbus/client/serial.py

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def connected(self):
198198
"""Connect internal."""
199199
return self.connect()
200200

201-
def connect(self): # pylint: disable=invalid-overridden-method
201+
def connect(self):
202202
"""Connect to the modbus serial server."""
203203
if self.socket:
204204
return True
@@ -219,21 +219,15 @@ def connect(self): # pylint: disable=invalid-overridden-method
219219
self.close()
220220
return self.socket is not None
221221

222-
def close(self): # pylint: disable=arguments-differ
222+
def close(self):
223223
"""Close the underlying socket connection."""
224224
if self.socket:
225225
self.socket.close()
226226
self.socket = None
227227

228228
def _in_waiting(self):
229-
"""Return _in_waiting."""
230-
in_waiting = "in_waiting" if hasattr(self.socket, "in_waiting") else "inWaiting"
231-
232-
if in_waiting == "in_waiting":
233-
waitingbytes = getattr(self.socket, in_waiting)
234-
else:
235-
waitingbytes = getattr(self.socket, in_waiting)()
236-
return waitingbytes
229+
"""Return waiting bytes."""
230+
return getattr(self.socket, "in_waiting") if hasattr(self.socket, "in_waiting") else getattr(self.socket, "inWaiting")()
237231

238232
def send(self, request):
239233
"""Send data on the underlying socket.
@@ -246,20 +240,9 @@ def send(self, request):
246240
if not self.socket:
247241
raise ConnectionException(str(self))
248242
if request:
249-
try:
250-
if waitingbytes := self._in_waiting():
251-
result = self.socket.read(waitingbytes)
252-
if self.state == ModbusTransactionState.RETRYING:
253-
Log.debug(
254-
"Sending available data in recv buffer {}", result, ":hex"
255-
)
256-
return result
257-
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
258-
except NotImplementedError:
259-
pass
260-
if self.state != ModbusTransactionState.SENDING:
261-
Log.debug('New Transaction state "SENDING"')
262-
self.state = ModbusTransactionState.SENDING
243+
if waitingbytes := self._in_waiting():
244+
result = self.socket.read(waitingbytes)
245+
Log.warning("Cleanup recv buffer before send: {}", result, ":hex")
263246
size = self.socket.write(request)
264247
return size
265248
return 0
@@ -268,16 +251,10 @@ def _wait_for_data(self):
268251
"""Wait for data."""
269252
size = 0
270253
more_data = False
271-
if (
272-
self.comm_params.timeout_connect is not None
273-
and self.comm_params.timeout_connect
274-
):
275-
condition = partial(
276-
lambda start, timeout: (time.time() - start) <= timeout,
277-
timeout=self.comm_params.timeout_connect,
278-
)
279-
else:
280-
condition = partial(lambda dummy1, dummy2: True, dummy2=None)
254+
condition = partial(
255+
lambda start, timeout: (time.time() - start) <= timeout,
256+
timeout=self.comm_params.timeout_connect,
257+
)
281258
start = time.time()
282259
while condition(start):
283260
available = self._in_waiting()
@@ -306,15 +283,9 @@ def recv(self, size):
306283
def is_socket_open(self):
307284
"""Check if socket is open."""
308285
if self.socket:
309-
if hasattr(self.socket, "is_open"):
310-
return self.socket.is_open
311-
return self.socket.isOpen()
286+
return self.socket.is_open if hasattr(self.socket, "is_open") else self.socket.isOpen()
312287
return False
313288

314-
def __str__(self):
315-
"""Build a string representation of the connection."""
316-
return f"ModbusSerialClient({self.framer} baud[{self.comm_params.baudrate}])"
317-
318289
def __repr__(self):
319290
"""Return string representation."""
320291
return (

0 commit comments

Comments
 (0)