"""Modbus Client Common."""
from __future__ import annotations

import enum
import struct
from typing import Generic, Literal, TypeVar, cast

from ..constants import ModbusStatus
from ..exceptions import ModbusException
from ..pdu import bit_message as pdu_bit
from ..pdu import diag_message as pdu_diag
from ..pdu import file_message as pdu_file_msg
from ..pdu import mei_message as pdu_mei
from ..pdu import other_message as pdu_other_msg
from ..pdu import register_message as pdu_reg
from ..pdu.pdu import ModbusPDU
from ..pdu.utils import pack_bitstring, unpack_bitstring


T = TypeVar("T", covariant=False)


class ModbusClientMixin(Generic[T]):  # pylint: disable=too-many-public-methods
    """**ModbusClientMixin**.

    This is an interface class to facilitate the sending requests/receiving responses like read_coils.
    execute() allows to make a call with non-standard or user defined function codes (remember to add a PDU
    in the transport class to interpret the request/response).

    Simple modbus message call::

        response = client.read_coils(1, 10)
        # or
        response = await client.read_coils(1, 10)

    Advanced modbus message call::

        request = ReadCoilsRequest(1,10)
        response = client.execute(False, request)
        # or
        request = ReadCoilsRequest(1,10)
        response = await client.execute(False, request)

    .. tip::
        All methods can be used directly (synchronous) or
        with await <method> (asynchronous) depending on the client used.
    """

    def __init__(self):
        """Initialize."""

    def execute(self, no_response_expected: bool, request: ModbusPDU) -> T:
        """Execute request."""
        _ = no_response_expected, request
        return cast(T, None)

    def read_coils(self, address: int, *, count: int = 1, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Read coils (code 0x01).

        :param address: Start address to read from
        :param count: (optional) Number of coils to read
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        reads from 1 to 2000 contiguous in a remote device.

        Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0).
        """
        return self.execute(no_response_expected, pdu_bit.ReadCoilsRequest(address=address, count=count, dev_id=device_id))

    def read_discrete_inputs(self,
                             address: int,
                             *,
                             count: int = 1,
                             device_id: int = 1,
                             no_response_expected: bool = False) -> T:
        """Read discrete inputs (code 0x02).

        :param address: Start address to read from
        :param count: (optional) Number of coils to read
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        read from 1 to 2000(0x7d0) discrete inputs (bits) in a remote device.

        Discrete Inputs are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0).
        """
        pdu = pdu_bit.ReadDiscreteInputsRequest(address=address, count=count, dev_id=device_id)
        return self.execute(no_response_expected, pdu)

    def read_holding_registers(self,
                               address: int,
                               *,
                               count: int = 1,
                               device_id: int = 1,
                               no_response_expected: bool = False) -> T:
        """Read holding registers (code 0x03).

        :param address: Start address to read from
        :param count: (optional) Number of registers to read
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to read the contents of a contiguous block
        of holding registers in a remote device. The Request specifies the
        starting register address and the number of registers.

        Registers are addressed starting at zero.
        Therefore devices that specify 1-16 are addressed as 0-15.
        """
        return self.execute(no_response_expected, pdu_reg.ReadHoldingRegistersRequest(address=address, count=count, dev_id=device_id))

    def read_input_registers(self,
                             address: int,
                             *,
                             count: int = 1,
                             device_id: int = 1,
                             no_response_expected: bool = False) -> T:
        """Read input registers (code 0x04).

        :param address: Start address to read from
        :param count: (optional) Number of registers to read
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to read from 1 to approx. 125 contiguous
        input registers in a remote device. The Request specifies the
        starting register address and the number of registers.

        Registers are addressed starting at zero.
        Therefore devices that specify 1-16 are addressed as 0-15.
        """
        return self.execute(no_response_expected, pdu_reg.ReadInputRegistersRequest(address=address, count=count, dev_id=device_id))

    def write_coil(self, address: int, value: bool, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Write single coil (code 0x05).

        :param address: Address to write to
        :param value: Boolean to write
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        write ON/OFF to a single coil in a remote device.

        Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0).
        """
        pdu = pdu_bit.WriteSingleCoilRequest(address=address, bits=[value], dev_id=device_id)
        return self.execute(no_response_expected, pdu)

    def write_register(self, address: int, value: int, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Write register (code 0x06).

        :param address: Address to write to
        :param value: Value to write
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to write a single holding register in a remote device.

        The Request specifies the address of the register to be written.

        Registers are addressed starting at zero. Therefore register
        numbered 1 is addressed as 0.
        """
        return self.execute(no_response_expected, pdu_reg.WriteSingleRegisterRequest(address=address, registers=[value], dev_id=device_id))

    def read_exception_status(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Read Exception Status (code 0x07).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to read the contents of eight Exception Status outputs in a remote device.

        The function provides a simple method for
        accessing this information, because the Exception Output references are
        known (no output reference is needed in the function).
        """
        return self.execute(no_response_expected, pdu_other_msg.ReadExceptionStatusRequest(dev_id=device_id))

    def diag_query_data(self, msg: bytes, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose query data (code 0x08 sub 0x00).

        :param msg: Message to be returned
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The data passed in the request data field is to be returned (looped back)
        in the response. The entire response message should be identical to the
        request.
        """
        return self.execute(no_response_expected, pdu_diag.ReturnQueryDataRequest(msg, dev_id=device_id))

    def diag_restart_communication(self, toggle: bool, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose restart communication (code 0x08 sub 0x01).

        :param toggle: True if toggled.
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The remote device serial line port must be initialized and restarted, and
        all of its communications event counters are cleared. If the port is
        currently in Listen Only Mode, no response is returned. This function is
        the only one that brings the port out of Listen Only Mode. If the port is
        not currently in Listen Only Mode, a normal response is returned.
        """
        msg = ModbusStatus.ON if toggle else ModbusStatus.OFF
        return self.execute(no_response_expected, pdu_diag.RestartCommunicationsOptionRequest(message=msg, dev_id=device_id))

    def diag_read_diagnostic_register(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read diagnostic register (code 0x08 sub 0x02).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The contents of the remote device's 16-bit diagnostic register are returned in the response.
        """
        return self.execute(no_response_expected, pdu_diag.ReturnDiagnosticRegisterRequest(dev_id=device_id))

    def diag_change_ascii_input_delimeter(self, *, delimiter: int = 0x0a, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose change ASCII input delimiter (code 0x08 sub 0x03).

        :param delimiter: char to replace LF
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The character passed in the request becomes the end of
        message delimiter for future messages (replacing the default LF
        character). This function is useful in cases of a Line Feed is not
        required at the end of ASCII messages.
        """
        return self.execute(no_response_expected, pdu_diag.ChangeAsciiInputDelimiterRequest(message=delimiter, dev_id=device_id))

    def diag_force_listen_only(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose force listen only (code 0x08 sub 0x04).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        Forces the addressed remote device to its Listen Only Mode for MODBUS communications.

        This isolates it from the other devices on the network,
        allowing them to continue communicating without interruption from the
        addressed remote device. No response is returned.
        """
        return self.execute(no_response_expected, pdu_diag.ForceListenOnlyModeRequest(dev_id=device_id))

    def diag_clear_counters(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose clear counters (code 0x08 sub 0x0A).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        Clear ll counters and the diagnostic register. Also, counters are cleared upon power-up
        """
        return self.execute(no_response_expected, pdu_diag.ClearCountersRequest(dev_id=device_id))

    def diag_read_bus_message_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read bus message count (code 0x08 sub 0x0B).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of messages that the
        remote device has detected on the communications systems since its last
        restart, clear counters operation, or power-up
        """
        return self.execute(no_response_expected, pdu_diag.ReturnBusMessageCountRequest(dev_id=device_id))

    def diag_read_bus_comm_error_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of CRC errors encountered
        by the remote device since its last restart, clear counter operation, or
        power-up
        """
        return self.execute(no_response_expected, pdu_diag.ReturnBusCommunicationErrorCountRequest(dev_id=device_id))

    def diag_read_bus_exception_error_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of modbus exception
        responses returned by the remote device since its last restart,
        clear counters operation, or power-up
        """
        return self.execute(no_response_expected, pdu_diag.ReturnBusExceptionErrorCountRequest(dev_id=device_id))

    def diag_read_device_message_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read device Message Count (code 0x08 sub 0x0E).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of messages addressed to the
        remote device, that the remote device has processed since
        its last restart, clear counters operation, or power-up
        """
        return self.execute(no_response_expected, pdu_diag.ReturnDeviceMessageCountRequest(dev_id=device_id))

    def diag_read_device_no_response_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read device No Response Count (code 0x08 sub 0x0F).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of messages addressed to the
        remote device, that the remote device has processed since
        its last restart, clear counters operation, or power-up.
        """
        return self.execute(no_response_expected, pdu_diag.ReturnDeviceNoResponseCountRequest(dev_id=device_id))

    def diag_read_device_nak_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read device NAK Count (code 0x08 sub 0x10).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of messages addressed to the
        remote device for which it returned a Negative ACKNOWLEDGE (NAK) exception
        response, since its last restart, clear counters operation, or power-up.
        Exception responses are described and listed in section 7 .
        """
        return self.execute(no_response_expected, pdu_diag.ReturnDeviceNAKCountRequest(dev_id=device_id))

    def diag_read_device_busy_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read device Busy Count (code 0x08 sub 0x11).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of messages addressed to the
        remote device for which it returned device Busy exception response,
        since its last restart, clear counters operation, or power-up.
        """
        return self.execute(no_response_expected, pdu_diag.ReturnDeviceBusyCountRequest(dev_id=device_id))

    def diag_read_bus_char_overrun_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        The response data field returns the quantity of messages addressed to the
        remote device that it could not handle due to a character overrun condition,
        since its last restart, clear counters operation, or power-up. A character
        overrun is caused by data characters arriving at the port faster than they
        can be stored, or by the loss of a character due to a hardware malfunction.
        """
        return self.execute(no_response_expected, pdu_diag.ReturnDeviceBusCharacterOverrunCountRequest(dev_id=device_id))

    def diag_read_iop_overrun_count(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose read Iop overrun count (code 0x08 sub 0x13).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        An IOP overrun is caused by data characters arriving at the port
        faster than they can be stored, or by the loss of a character due
        to a hardware malfunction.  This function is specific to the 884.
        """
        return self.execute(no_response_expected, pdu_diag.ReturnIopOverrunCountRequest(dev_id=device_id))

    def diag_clear_overrun_counter(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        An error flag should be cleared, but nothing else in the
        specification mentions is, so it is ignored.
        """
        return self.execute(no_response_expected, pdu_diag.ClearOverrunCountRequest(dev_id=device_id))

    def diag_getclear_modbus_response(self, *, data: int = 0, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15).

        :param data: "Get Statistics" or "Clear Statistics"
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        In addition to the Function code (08) and Subfunction code
        (00 15 hex) in the query, a two-byte Operation field is used
        to specify either a "Get Statistics" or a "Clear Statistics"
        operation.  The two operations are exclusive - the "Get"
        operation cannot clear the statistics, and the "Clear"
        operation does not return statistics prior to clearing
        them. Statistics are also cleared on power-up of the device,
        """
        return self.execute(no_response_expected, pdu_diag.GetClearModbusPlusRequest(message=data, dev_id=device_id))

    def diag_get_comm_event_counter(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose get event counter (code 0x0B).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to get a status word and an event count from the remote device.

        By fetching the current count before and after a series of messages, a
        client can determine whether the messages were handled normally by the
        remote device.

        The device's event counter is incremented once for each successful
        message completion. It is not incremented for exception responses,
        poll commands, or fetch event counter commands.

        The event counter can be reset by means of the Diagnostics function
        Restart Communications or Clear Counters and Diagnostic Register.
        """
        return self.execute(no_response_expected, pdu_other_msg.GetCommEventCounterRequest(dev_id=device_id))

    def diag_get_comm_event_log(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Diagnose get event counter (code 0x0C).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to get a status word.

        Event count, message count, and a field of event bytes from the remote device.

        The status word and event counts are identical to that returned by
        the Get Communications Event Counter function.

        The message counter contains the quantity of messages processed by the
        remote device since its last restart, clear counters operation, or
        power-up. This count is identical to that returned by the Diagnostic
        function Return Bus Message Count.

        The event bytes field contains 0-64 bytes, with each byte corresponding
        to the status of one MODBUS send or receive operation for the remote
        device. The remote device enters the events into the field in
        chronological order. Byte 0 is the most recent event. Each new byte
        flushes the oldest byte from the field.
        """
        return self.execute(no_response_expected, pdu_other_msg.GetCommEventLogRequest(dev_id=device_id))

    def write_coils(
        self,
        address: int,
        values: list[bool],
        *,
        device_id: int = 1,
        no_response_expected: bool = False
    ) -> T:
        """Write coils (code 0x0F).

        :param address: Start address to write to
        :param values: List of booleans to write, or a single boolean to write
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        write ON/OFF to multiple coils in a remote device.

        Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0).
        """
        pdu = pdu_bit.WriteMultipleCoilsRequest(address=address, bits=values, dev_id=device_id)
        return self.execute(no_response_expected, pdu)

    def write_registers(
        self,
        address: int,
        values: list[int],
        *,
        device_id: int = 1,
        no_response_expected: bool = False
    ) -> T:
        """Write registers (code 0x10).

        :param address: Start address to write to
        :param values: List of values to write
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to write a block of contiguous registers
        (1 to approx. 120 registers) in a remote device.
        """
        return self.execute(no_response_expected, pdu_reg.WriteMultipleRegistersRequest(address=address, registers=values,dev_id=device_id))

    def report_device_id(self, *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Report device ID (code 0x11).

        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to read the description of the type, the current status
        and other information specific to a remote device.
        """
        return self.execute(no_response_expected, pdu_other_msg.ReportDeviceIdRequest(dev_id=device_id))

    def read_file_record(self, records: list[pdu_file_msg.FileRecord], *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Read file record (code 0x14).

        :param records: List of FileRecord (Reference type, File number, Record Number)
        :param device_id: device id
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to perform a file record read. All request
        data lengths are provided in terms of number of bytes and all record
        lengths are provided in terms of registers.

        A file is an organization of records. Each file contains 10000 records,
        addressed 0000 to 9999 decimal or 0x0000 to 0x270f. For example, record
        12 is addressed as 12. The function can read multiple groups of
        references. The groups can be separating (non-contiguous), but the
        references within each group must be sequential. Each group is defined
        in a separate "sub-request" field that contains seven bytes::

            The reference type: 1 byte
            The file number: 2 bytes
            The starting record number within the file: 2 bytes
            The length of the record to be read: 2 bytes

        The quantity of registers to be read, combined with all other fields
        in the expected response, must not exceed the allowable length of the
        MODBUS PDU: 235 bytes.
        """
        return self.execute(no_response_expected, pdu_file_msg.ReadFileRecordRequest(records, dev_id=device_id))

    def write_file_record(self, records: list[pdu_file_msg.FileRecord], *, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Write file record (code 0x15).

        :param records: List of File_record (Reference type, File number, Record Number, Record Length, Record Data)
        :param device_id: (optional) Device id
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to perform a file record write. All
        request data lengths are provided in terms of number of bytes
        and all record lengths are provided in terms of the number of 16
        bit words.
        """
        return self.execute(no_response_expected, pdu_file_msg.WriteFileRecordRequest(records=records, dev_id=device_id))

    def mask_write_register(
        self,
        *,
        address: int = 0x0000,
        and_mask: int = 0xFFFF,
        or_mask: int = 0x0000,
        device_id: int = 1,
        no_response_expected: bool = False
    ) -> T:
        """Mask write register (code 0x16).

        :param address: The mask pointer address (0x0000 to 0xffff)
        :param and_mask: The and bitmask to apply to the register address
        :param or_mask: The or bitmask to apply to the register address
        :param device_id: (optional) device id
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function is used to modify the contents of a specified holding register
        using a combination of an AND mask, an OR mask, and the register's current contents.

        The function can be used to set or clear individual bits in the register.
        """
        return self.execute(no_response_expected, pdu_reg.MaskWriteRegisterRequest(address=address, and_mask=and_mask, or_mask=or_mask, dev_id=device_id))

    def readwrite_registers(
        self,
        *,
        read_address: int = 0,
        read_count: int = 0,
        write_address: int = 0,
        address: int | None = None,
        values: list[int] | None = None,
        device_id: int = 1,
        no_response_expected: bool = False
    ) -> T:
        """Read/Write registers (code 0x17).

        :param read_address: The address to start reading from
        :param read_count: The number of registers to read from address
        :param write_address: The address to start writing to
        :param address: (optional) use as read/write address
        :param values: List of values to write, or a single value to write
        :param device_id: (optional) Modbus device ID
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function performs a combination of one read operation and one
        write operation in a single MODBUS transaction. The write
        operation is performed before the read.

        Holding registers are addressed starting at zero. Therefore holding
        registers 1-16 are addressed in the PDU as 0-15.
        """
        if not values:
            values = []
        if address:
            read_address = address
            write_address = address
        return self.execute(no_response_expected, pdu_reg.ReadWriteMultipleRegistersRequest( read_address=read_address, read_count=read_count, write_address=write_address, write_registers=values,dev_id=device_id))

    def read_fifo_queue(self, *, address: int = 0x0000, device_id: int = 1, no_response_expected: bool = False) -> T:
        """Read FIFO queue (code 0x18).

        :param address: The address to start reading from
        :param device_id: (optional) device id
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function allows to read the contents of a First-In-First-Out
        (FIFO) queue of register in a remote device. The function returns a
        count of the registers in the queue, followed by the queued data.
        Up to 32 registers can be read: the count, plus up to 31 queued data
        registers.

        The queue count register is returned first, followed by the queued data
        registers.  The function reads the queue contents, but does not clear
        them.
        """
        return self.execute(no_response_expected, pdu_file_msg.ReadFifoQueueRequest(address, dev_id=device_id))

    # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED

    def read_device_information(self, *, read_code: int | None = None,
                                object_id: int = 0x00,
                                device_id: int = 1,
                                no_response_expected: bool = False) -> T:
        """Read FIFO queue (code 0x2B sub 0x0E).

        :param read_code: The device information read code
        :param object_id: The object to read from
        :param device_id: (optional) Device id
        :param no_response_expected: (optional) The client will not expect a response to the request
        :raises ModbusException:

        This function allows reading the identification and additional
        information relative to the physical and functional description of a
        remote device, only.

        The Read Device Identification interface is modeled as an address space
        composed of a set of addressable data elements. The data elements are
        called objects and an object Id identifies them.
        """
        return self.execute(no_response_expected, pdu_mei.ReadDeviceInformationRequest(read_code, object_id, dev_id=device_id))

    # ------------------
    # Converter methods
    # ------------------

    class DATATYPE(enum.Enum):
        """Datatype enum (name and internal data), used for convert_* calls."""

        INT16 = ("h", 1)
        UINT16 = ("H", 1)
        INT32 = ("i", 2)
        UINT32 = ("I", 2)
        INT64 = ("q", 4)
        UINT64 = ("Q", 4)
        FLOAT32 = ("f", 2)
        FLOAT64 = ("d", 4)
        STRING = ("s", 0)
        BITS = ("bits", 0)

    @classmethod
    def convert_from_registers(  # noqa: C901
        cls, registers: list[int], data_type: DATATYPE, word_order: Literal["big", "little"] = "big", string_encoding: str = "utf-8"
    ) -> int | float | str | list[bool] | list[int] | list[float]:
        """Convert registers to int/float/str.

        :param registers: list of registers received from e.g. read_holding_registers()
        :param data_type: data type to convert to
        :param word_order: "big"/"little" order of words/registers
        :param string_encoding: The encoding with which to decode the bytearray, only used when data_type=DATATYPE.STRING
        :returns: scalar or array of "data_type"
        :raises TypeError: when data_type is not DATATYPE
        :raises ModbusException: when size of registers is not a multiple of data_type
        :raises ParameterException: when the specified string encoding is not supported
        """
        if not isinstance(data_type, cls.DATATYPE):
            raise TypeError(
                "Illegal data_type, please use <client>.DATATYPE!"
            )

        if not (data_len := data_type.value[1]):
            byte_list = bytearray()
            if word_order == "little":
                registers.reverse()
            for x in registers:
                byte_list.extend(int.to_bytes(x, 2, "big"))
            if data_type == cls.DATATYPE.STRING:
                trailing_nulls_begin = len(byte_list)
                while trailing_nulls_begin > 0 and not byte_list[trailing_nulls_begin - 1]:
                    trailing_nulls_begin -= 1
                byte_list = byte_list[:trailing_nulls_begin]
                return byte_list.decode(string_encoding)
            return unpack_bitstring(byte_list)
        if (reg_len := len(registers)) % data_len:
            raise ModbusException(
                f"Registers illegal size ({len(registers)}) expected multiple of {data_len}!"
            )

        result = []
        for i in range(0, reg_len, data_len):
            regs = registers[i:i+data_len]
            if word_order == "little":
                regs.reverse()
            byte_list = bytearray()
            for x in regs:
                byte_list.extend(int.to_bytes(x, 2, "big"))
            result.append(struct.unpack(f">{data_type.value[0]}", byte_list)[0])
        return result if len(result) != 1 else result[0]

    @classmethod
    def convert_to_registers(  # noqa: C901
        cls, value: int | float | str | list[bool] | list[int] | list[float], data_type: DATATYPE, word_order: Literal["big", "little"] = "big", string_encoding: str = "utf-8"
    ) -> list[int]:
        """Convert int/float/str to registers (16/32/64 bit).

        :param value: value to be converted
        :param data_type: data type to convert from
        :param word_order: "big"/"little" order of words/registers
        :param string_encoding: The encoding with which to encode the bytearray, only used when data_type=DATATYPE.STRING
        :returns: List of registers, can be used directly in e.g. write_registers()
        :raises TypeError: when there is a mismatch between data_type and value or data_type is not DATATYPE
        :raises ParameterException: when the specified string encoding is not supported
        """
        if not isinstance(data_type, cls.DATATYPE):
            raise TypeError("Illegal data_type, please use <client>.DATATYPE!")

        if data_type == cls.DATATYPE.BITS:
            if not isinstance(value, list):
                raise TypeError(f"Value should be list of bool but is {type(value)}.")
            value = cast(list[bool], value)
            if (missing := len(value) % 16):
                value = value + [False] * (16 - missing)
            byte_list = pack_bitstring(value)
        elif data_type == cls.DATATYPE.STRING:
            if not isinstance(value, str):
                raise TypeError(f"Value should be string but is {type(value)}.")
            byte_list = value.encode(string_encoding)
            if len(byte_list) % 2:
                byte_list += b"\x00"
        else:
            if not isinstance(value, list):
                value = cast(list[int], [value])
            byte_list = bytearray()
            for v in value:
                byte_list.extend(struct.pack(f">{data_type.value[0]}", v))
        regs = [
            int.from_bytes(byte_list[x : x + 2], "big")
            for x in range(0, len(byte_list), 2)
        ]
        if word_order == "little":
            return cls._get_reversed_registers(regs, data_type)

        return regs

    @classmethod
    def _get_reversed_registers(cls, regs: list[int], data_type: DATATYPE) -> list[int]:
        if not (data_type_len := data_type.value[1]):
            regs.reverse()
            return regs

        reversed_regs: list[int] = []
        for x in range(0, len(regs), data_type_len):
            single_value_regs = regs[x: x + data_type_len]
            single_value_regs.reverse()
            reversed_regs = reversed_regs + single_value_regs
        return reversed_regs
