#!/usr/bin/env python3
# pylint: disable=missing-type-doc
"""Pymodbus Synchronous Client Examples.

The following is an example of how to use the synchronous modbus client
implementation from pymodbus::

    with ModbusClient("127.0.0.1") as client:
        result = client.read_coils(1,10)
        print result

"""
import asyncio
import struct

from pymodbus import FramerType
from pymodbus.client import AsyncModbusTcpClient as ModbusClient
from pymodbus.datastore import ModbusServerContext
from pymodbus.exceptions import ModbusIOException
from pymodbus.pdu import ModbusPDU
from pymodbus.pdu.bit_message import ReadCoilsRequest
from pymodbus.server import ServerAsyncStop, StartAsyncTcpServer
from pymodbus.simulator import DataType, SimData, SimDevice


# --------------------------------------------------------------------------- #
# create your custom message
# --------------------------------------------------------------------------- #
# The following is simply a read coil request that always reads 16 coils.
# Since the function code is already registered with the decoder,
# this will be decoded as a read coil response. If you implement a new
# method that is not currently implemented, you must register the request
# and response with the active DecodePDU object.
# --------------------------------------------------------------------------- #


class CustomModbusResponse(ModbusPDU):
    """Custom modbus response."""

    function_code = 55
    rtu_byte_count_pos = 2

    def __init__(self, values: list[int] | None = None, device_id=1, transaction=0):
        """Initialize."""
        super().__init__(dev_id=device_id, transaction_id=transaction)
        self.values: list[int] = values or []

    def encode(self):
        """Encode response pdu.

        :returns: The encoded packet message
        """
        res = struct.pack(">B", len(self.values) * 2)
        for register in self.values:  # pragma: no cover
            res += struct.pack(">H", register)
        return res

    def decode(self, data):
        """Decode response pdu.

        :param data: The packet data to decode
        """
        byte_count = int(data[0])
        self.values = []
        for i in range(1, byte_count + 1, 2):  # pragma: no cover
            self.values.append(struct.unpack(">H", data[i : i + 2])[0])


class CustomRequest(ModbusPDU):
    """Custom modbus request."""

    function_code = 55
    rtu_frame_size = 8

    def __init__(self, address=None, device_id=1, transaction=0):
        """Initialize."""
        super().__init__(dev_id=device_id, transaction_id=transaction)
        self.address = address
        self.count = 2

    def encode(self):
        """Encode."""
        return struct.pack(">HH", self.address, self.count)

    def decode(self, data):
        """Decode."""
        self.address, self.count = struct.unpack(">HH", data)

    async def datastore_update(self, context: ModbusServerContext, device_id: int) -> ModbusPDU:
        """Update diagnostic request on the given device."""
        _ = context, device_id
        return CustomModbusResponse()


# --------------------------------------------------------------------------- #
# This could also have been defined as
# --------------------------------------------------------------------------- #


class Read16CoilsRequest(ReadCoilsRequest):
    """Read 16 coils in one request."""

    def __init__(self, address, device_id=1, transaction=0):
        """Initialize a new instance.

        :param address: The address to start reading from
        """
        super().__init__(address=address, count=16, dev_id=device_id, transaction_id=transaction)


# --------------------------------------------------------------------------- #
# execute the request with your client
# --------------------------------------------------------------------------- #
# using the with context, the client will automatically be connected
# and closed when it leaves the current scope.
# --------------------------------------------------------------------------- #


async def main(host="localhost", port=5020):
    """Run versions of read coil."""
    task = asyncio.create_task(StartAsyncTcpServer(
        context=SimDevice(0, SimData(0, datatype=DataType.REGISTERS, values=[17]*100)),
        address=(host, port),
        custom_pdu=[CustomRequest])
    )
    await asyncio.sleep(0.1)
    async with ModbusClient(host=host, port=port, framer=FramerType.SOCKET) as client:
        await client.connect()

        # add new modbus function code.
        client.register(CustomModbusResponse)
        device_id=1
        request1 = CustomRequest(32, device_id=device_id)
        try:
            result = await client.execute(False, request1)
        except ModbusIOException:  # pragma: no cover
            print("Server do not support CustomRequest.")
        else:
            print(result)

        # inherited request
        request2 = Read16CoilsRequest(32, device_id)
        result = await client.execute(False, request2)
        print(result)
    await ServerAsyncStop()
    task.cancel()
    await task


if __name__ == "__main__":
    asyncio.run(main())
