"""Modbus TCP transport implementation.

This module provides the ModbusTransport class for direct local
communication with inverters via Modbus TCP (typically through
a Waveshare RS485-to-Ethernet adapter).

IMPORTANT: Single-Client Limitation
------------------------------------
Modbus TCP supports only ONE concurrent connection per gateway/inverter.
Running multiple clients (e.g., Home Assistant + custom script) causes:
- Transaction ID desynchronization
- "Request cancelled outside pymodbus" errors
- Intermittent timeouts and data corruption

Ensure only ONE integration/script connects to each inverter at a time.
Disable other Modbus integrations before using this transport.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from pymodbus.exceptions import ModbusIOException

from .capabilities import MODBUS_CAPABILITIES, TransportCapabilities
from .data import (
    BatteryBankData,
    InverterEnergyData,
    InverterRuntimeData,
    MidboxRuntimeData,
)
from .exceptions import (
    TransportConnectionError,
    TransportReadError,
    TransportTimeoutError,
    TransportWriteError,
)
from .protocol import BaseTransport

if TYPE_CHECKING:
    from pymodbus.client import AsyncModbusTcpClient

    from pylxpweb.devices.inverters._features import InverterFamily
    from pylxpweb.transports.register_maps import (
        EnergyRegisterMap,
        RuntimeRegisterMap,
    )

# Device type codes read from register 19
DEVICE_TYPE_REGISTER = 19
DEVICE_TYPE_MIDBOX = 50  # GridBOSS / MID device

_LOGGER = logging.getLogger(__name__)

# Register group definitions for efficient reading
# Based on Modbus 40-register per call limit
# Source: EG4-18KPV-12LV Modbus Protocol + eg4-modbus-monitor + Yippy's BMS docs
INPUT_REGISTER_GROUPS = {
    "power_energy": (0, 32),  # Registers 0-31: Power, voltage, SOC/SOH, current
    "status_energy": (32, 32),  # Registers 32-63: Status, energy, fault/warning codes
    "temperatures": (64, 16),  # Registers 64-79: Temperatures, currents, fault history
    "bms_data": (80, 33),  # Registers 80-112: BMS passthrough data (Yippy's docs)
}

HOLD_REGISTER_GROUPS = {
    "system": (0, 25),  # Registers 0-24: System config
    "grid_protection": (25, 35),  # Registers 25-59: Grid protection
    "charging": (60, 30),  # Registers 60-89: Charging config
    "battery": (90, 40),  # Registers 90-129: Battery config
}

# Serial number is stored in input registers 115-119 (5 registers, 10 ASCII chars)
# Each register contains 2 ASCII characters: low byte = char[0], high byte = char[1]
SERIAL_NUMBER_START_REGISTER = 115
SERIAL_NUMBER_REGISTER_COUNT = 5


class ModbusTransport(BaseTransport):
    """Modbus TCP transport for local inverter communication.

    This transport connects directly to the inverter via a Modbus TCP
    gateway (e.g., Waveshare RS485-to-Ethernet adapter).

    IMPORTANT: Single-Client Limitation
    ------------------------------------
    Modbus TCP supports only ONE concurrent connection per gateway/inverter.
    Running multiple clients (e.g., Home Assistant + custom script) causes:
    - Transaction ID desynchronization
    - "Request cancelled outside pymodbus" errors
    - Intermittent timeouts and data corruption

    Ensure only ONE integration/script connects to each inverter at a time.
    Disable other Modbus integrations before using this transport.

    Example:
        transport = ModbusTransport(
            host="192.168.1.100",
            port=502,
            serial="CE12345678",
        )
        await transport.connect()

        runtime = await transport.read_runtime()
        print(f"PV Power: {runtime.pv_total_power}W")

    Note:
        Requires the `pymodbus` package to be installed:
        uv add pymodbus
    """

    def __init__(
        self,
        host: str,
        port: int = 502,
        unit_id: int = 1,
        serial: str = "",
        timeout: float = 10.0,
        inverter_family: InverterFamily | None = None,
    ) -> None:
        """Initialize Modbus transport.

        Args:
            host: IP address or hostname of Modbus TCP gateway
            port: TCP port (default 502 for Modbus)
            unit_id: Modbus unit/slave ID (default 1)
            serial: Inverter serial number (for identification)
            timeout: Connection and operation timeout in seconds
            inverter_family: Inverter model family for correct register mapping.
                If None, defaults to PV_SERIES (EG4-18KPV) for backward
                compatibility. Use InverterFamily.LXP_EU for LXP-EU models.
        """
        import asyncio

        super().__init__(serial)
        self._host = host
        self._port = port
        self._unit_id = unit_id
        self._timeout = timeout
        self._inverter_family = inverter_family
        self._client: AsyncModbusTcpClient | None = None
        self._lock = asyncio.Lock()

    @property
    def capabilities(self) -> TransportCapabilities:
        """Get Modbus transport capabilities."""
        return MODBUS_CAPABILITIES

    @property
    def host(self) -> str:
        """Get the Modbus gateway host."""
        return self._host

    @property
    def port(self) -> int:
        """Get the Modbus gateway port."""
        return self._port

    @property
    def unit_id(self) -> int:
        """Get the Modbus unit/slave ID."""
        return self._unit_id

    @property
    def inverter_family(self) -> InverterFamily | None:
        """Get the inverter family for register mapping."""
        return self._inverter_family

    @property
    def runtime_register_map(self) -> RuntimeRegisterMap:
        """Get the runtime register map for this inverter family."""
        from pylxpweb.transports.register_maps import get_runtime_map

        return get_runtime_map(self._inverter_family)

    @property
    def energy_register_map(self) -> EnergyRegisterMap:
        """Get the energy register map for this inverter family."""
        from pylxpweb.transports.register_maps import get_energy_map

        return get_energy_map(self._inverter_family)

    async def connect(self) -> None:
        """Establish Modbus TCP connection.

        Raises:
            TransportConnectionError: If connection fails
        """
        try:
            # Import pymodbus here to make it optional
            from pymodbus.client import AsyncModbusTcpClient

            self._client = AsyncModbusTcpClient(
                host=self._host,
                port=self._port,
                timeout=self._timeout,
            )

            connected = await self._client.connect()
            if not connected:
                raise TransportConnectionError(
                    f"Failed to connect to Modbus gateway at {self._host}:{self._port}"
                )

            self._connected = True
            _LOGGER.info(
                "Modbus transport connected to %s:%s (unit %s) for %s",
                self._host,
                self._port,
                self._unit_id,
                self._serial,
            )

        except ImportError as err:
            raise TransportConnectionError(
                "pymodbus package not installed. Install with: uv add pymodbus"
            ) from err
        except (TimeoutError, OSError) as err:
            _LOGGER.error(
                "Failed to connect to Modbus gateway at %s:%s: %s",
                self._host,
                self._port,
                err,
            )
            raise TransportConnectionError(
                f"Failed to connect to {self._host}:{self._port}: {err}. "
                "Verify: (1) IP address is correct, (2) port 502 is not blocked, "
                "(3) Modbus TCP is enabled on the inverter/datalogger."
            ) from err

    async def disconnect(self) -> None:
        """Close Modbus TCP connection."""
        if self._client:
            self._client.close()
            self._client = None

        self._connected = False
        _LOGGER.debug("Modbus transport disconnected for %s", self._serial)

    async def _read_input_registers(
        self,
        address: int,
        count: int,
    ) -> list[int]:
        """Read input registers (read-only runtime data).

        Args:
            address: Starting register address
            count: Number of registers to read (max 40)

        Returns:
            List of register values

        Raises:
            TransportReadError: If read fails
            TransportTimeoutError: If operation times out

        Note:
            Timeout handling is delegated to pymodbus internally. We don't use
            asyncio.wait_for() because the double-timeout causes transaction ID
            desynchronization issues with pymodbus when timeouts occur.
        """
        self._ensure_connected()

        if self._client is None:
            raise TransportConnectionError("Modbus client not initialized")

        async with self._lock:
            try:
                # Let pymodbus handle timeout internally (set at client init)
                # Do NOT wrap with asyncio.wait_for() - causes transaction ID issues
                result = await self._client.read_input_registers(
                    address=address,
                    count=min(count, 40),
                    device_id=self._unit_id,
                )

                if result.isError():
                    _LOGGER.error(
                        "Modbus error reading input registers at %d: %s",
                        address,
                        result,
                    )
                    raise TransportReadError(f"Modbus read error at address {address}: {result}")

                if not hasattr(result, "registers") or result.registers is None:
                    _LOGGER.error(
                        "Invalid Modbus response at address %d: no registers",
                        address,
                    )
                    raise TransportReadError(
                        f"Invalid Modbus response at address {address}: no registers in response"
                    )

                return list(result.registers)

            except ModbusIOException as err:
                # pymodbus raises ModbusIOException for timeouts and connection issues
                error_msg = str(err)
                if "timeout" in error_msg.lower():
                    _LOGGER.error("Timeout reading input registers at %d", address)
                    raise TransportTimeoutError(
                        f"Timeout reading input registers at {address}"
                    ) from err
                _LOGGER.error("Failed to read input registers at %d: %s", address, err)
                raise TransportReadError(
                    f"Failed to read input registers at {address}: {err}"
                ) from err
            except TimeoutError as err:
                _LOGGER.error("Timeout reading input registers at %d", address)
                raise TransportTimeoutError(
                    f"Timeout reading input registers at {address}"
                ) from err
            except OSError as err:
                _LOGGER.error("Failed to read input registers at %d: %s", address, err)
                raise TransportReadError(
                    f"Failed to read input registers at {address}: {err}"
                ) from err

    async def _read_holding_registers(
        self,
        address: int,
        count: int,
    ) -> list[int]:
        """Read holding registers (configuration parameters).

        Args:
            address: Starting register address
            count: Number of registers to read (max 40)

        Returns:
            List of register values

        Raises:
            TransportReadError: If read fails
            TransportTimeoutError: If operation times out
        """
        self._ensure_connected()

        if self._client is None:
            raise TransportConnectionError("Modbus client not initialized")

        async with self._lock:
            try:
                # Let pymodbus handle timeout internally
                result = await self._client.read_holding_registers(
                    address=address,
                    count=min(count, 40),
                    device_id=self._unit_id,
                )

                if result.isError():
                    _LOGGER.error(
                        "Modbus error reading holding registers at %d: %s",
                        address,
                        result,
                    )
                    raise TransportReadError(f"Modbus read error at address {address}: {result}")

                if not hasattr(result, "registers") or result.registers is None:
                    _LOGGER.error(
                        "Invalid Modbus response at address %d: no registers",
                        address,
                    )
                    raise TransportReadError(
                        f"Invalid Modbus response at address {address}: no registers in response"
                    )

                return list(result.registers)

            except ModbusIOException as err:
                error_msg = str(err)
                if "timeout" in error_msg.lower():
                    _LOGGER.error("Timeout reading holding registers at %d", address)
                    raise TransportTimeoutError(
                        f"Timeout reading holding registers at {address}"
                    ) from err
                _LOGGER.error("Failed to read holding registers at %d: %s", address, err)
                raise TransportReadError(
                    f"Failed to read holding registers at {address}: {err}"
                ) from err
            except TimeoutError as err:
                _LOGGER.error("Timeout reading holding registers at %d", address)
                raise TransportTimeoutError(
                    f"Timeout reading holding registers at {address}"
                ) from err
            except OSError as err:
                _LOGGER.error("Failed to read holding registers at %d: %s", address, err)
                raise TransportReadError(
                    f"Failed to read holding registers at {address}: {err}"
                ) from err

    async def _write_holding_registers(
        self,
        address: int,
        values: list[int],
    ) -> bool:
        """Write holding registers.

        Args:
            address: Starting register address
            values: List of values to write

        Returns:
            True if write succeeded

        Raises:
            TransportWriteError: If write fails
            TransportTimeoutError: If operation times out
        """
        self._ensure_connected()

        if self._client is None:
            raise TransportConnectionError("Modbus client not initialized")

        async with self._lock:
            try:
                # Let pymodbus handle timeout internally
                result = await self._client.write_registers(
                    address=address,
                    values=values,
                    device_id=self._unit_id,
                )

                if result.isError():
                    _LOGGER.error(
                        "Modbus error writing registers at %d: %s",
                        address,
                        result,
                    )
                    raise TransportWriteError(f"Modbus write error at address {address}: {result}")

                return True

            except ModbusIOException as err:
                error_msg = str(err)
                if "timeout" in error_msg.lower():
                    _LOGGER.error("Timeout writing registers at %d", address)
                    raise TransportTimeoutError(f"Timeout writing registers at {address}") from err
                _LOGGER.error("Failed to write registers at %d: %s", address, err)
                raise TransportWriteError(f"Failed to write registers at {address}: {err}") from err
            except TimeoutError as err:
                _LOGGER.error("Timeout writing registers at %d", address)
                raise TransportTimeoutError(f"Timeout writing registers at {address}") from err
            except OSError as err:
                _LOGGER.error("Failed to write registers at %d: %s", address, err)
                raise TransportWriteError(f"Failed to write registers at {address}: {err}") from err

    async def read_runtime(self) -> InverterRuntimeData:
        """Read runtime data via Modbus input registers.

        Uses the appropriate register map based on the inverter_family parameter
        set during transport initialization. Different inverter families have
        different register layouts (e.g., PV_SERIES uses 32-bit power values,
        LXP_EU uses 16-bit power values with offset addresses).

        Note: Register reads are serialized (not concurrent) to prevent
        transaction ID desynchronization issues with pymodbus and some
        Modbus TCP gateways (e.g., Waveshare RS485-to-Ethernet adapters).

        Returns:
            Runtime data with all values properly scaled

        Raises:
            TransportReadError: If read operation fails
        """
        # Read register groups sequentially to avoid transaction ID issues
        # See: https://github.com/joyfulhouse/pylxpweb/issues/95
        input_registers: dict[int, int] = {}

        for group_name, (start, count) in INPUT_REGISTER_GROUPS.items():
            try:
                values = await self._read_input_registers(start, count)
                for offset, value in enumerate(values):
                    input_registers[start + offset] = value
            except Exception as e:
                _LOGGER.error(
                    "Failed to read register group '%s': %s",
                    group_name,
                    e,
                )
                raise TransportReadError(
                    f"Failed to read register group '{group_name}': {e}"
                ) from e

        return InverterRuntimeData.from_modbus_registers(input_registers, self.runtime_register_map)

    async def read_energy(self) -> InverterEnergyData:
        """Read energy statistics via Modbus input registers.

        Uses the appropriate energy register map based on the inverter_family
        parameter. Different models have different register layouts for energy
        data (e.g., LXP_EU uses 16-bit daily values vs 32-bit for PV_SERIES).

        Note: Register reads are serialized to prevent transaction ID issues.

        Returns:
            Energy data with all values in kWh

        Raises:
            TransportReadError: If read operation fails
        """
        # Read energy-related register groups sequentially
        # power_energy (0-31) contains PV daily energy at registers 28-30
        # status_energy (32-63) contains daily/lifetime energy counters
        groups_needed = ["power_energy", "status_energy", "bms_data"]
        input_registers: dict[int, int] = {}

        for group_name, (start, count) in INPUT_REGISTER_GROUPS.items():
            if group_name not in groups_needed:
                continue

            try:
                values = await self._read_input_registers(start, count)
                for offset, value in enumerate(values):
                    input_registers[start + offset] = value
            except Exception as e:
                _LOGGER.error(
                    "Failed to read energy register group '%s': %s",
                    group_name,
                    e,
                )
                raise TransportReadError(
                    f"Failed to read energy register group '{group_name}': {e}"
                ) from e

        return InverterEnergyData.from_modbus_registers(input_registers, self.energy_register_map)

    async def read_battery(self) -> BatteryBankData | None:
        """Read battery information via Modbus.

        Reads core battery data (registers 0-31) and BMS passthrough data
        (registers 80-112) for comprehensive battery monitoring.

        Uses the register map for correct register addresses and scaling,
        ensuring extensibility for different inverter families.

        Note: Individual battery module data is not available via Modbus.

        Returns:
            Battery bank data with available information, None if no battery

        Raises:
            TransportReadError: If read operation fails
        """
        # Read power/energy registers (0-31) and BMS registers (80-112)
        # Combine into single dict for factory method
        all_registers: dict[int, int] = {}

        # Read core battery registers (0-31)
        power_regs = await self._read_input_registers(0, 32)
        for i, value in enumerate(power_regs):
            all_registers[i] = value

        # Read BMS registers (80-112)
        try:
            bms_values = await self._read_input_registers(80, 33)
            for offset, value in enumerate(bms_values):
                all_registers[80 + offset] = value
        except Exception as e:
            _LOGGER.warning("Failed to read BMS registers 80-112: %s", e)
            # Continue with basic battery data even if BMS read fails

        # Use factory method with register map for proper extensibility
        result = BatteryBankData.from_modbus_registers(all_registers, self.runtime_register_map)

        if result is None:
            _LOGGER.debug(
                "Battery voltage below threshold, assuming no battery present. "
                "If batteries are installed, check Modbus register mapping."
            )

        return result

    async def read_parameters(
        self,
        start_address: int,
        count: int,
    ) -> dict[int, int]:
        """Read configuration parameters via Modbus holding registers.

        Args:
            start_address: Starting register address
            count: Number of registers to read (max 40 per call)

        Returns:
            Dict mapping register address to raw integer value

        Raises:
            TransportReadError: If read operation fails
        """
        result: dict[int, int] = {}

        # Read in chunks of 40 registers (Modbus limit)
        remaining = count
        current_address = start_address

        while remaining > 0:
            chunk_size = min(remaining, 40)
            values = await self._read_holding_registers(current_address, chunk_size)

            for offset, value in enumerate(values):
                result[current_address + offset] = value

            current_address += chunk_size
            remaining -= chunk_size

        return result

    async def write_parameters(
        self,
        parameters: dict[int, int],
    ) -> bool:
        """Write configuration parameters via Modbus holding registers.

        Args:
            parameters: Dict mapping register address to value to write

        Returns:
            True if all writes succeeded

        Raises:
            TransportWriteError: If any write operation fails
        """
        # Sort parameters by address for efficient writing
        sorted_params = sorted(parameters.items())

        # Group consecutive addresses for batch writing
        groups: list[tuple[int, list[int]]] = []
        current_start: int | None = None
        current_values: list[int] = []

        for address, value in sorted_params:
            if current_start is None:
                current_start = address
                current_values = [value]
            elif address == current_start + len(current_values):
                # Consecutive address, add to current group
                current_values.append(value)
            else:
                # Non-consecutive, save current group and start new one
                groups.append((current_start, current_values))
                current_start = address
                current_values = [value]

        # Don't forget the last group
        if current_start is not None and current_values:
            groups.append((current_start, current_values))

        # Write each group
        for start_address, values in groups:
            await self._write_holding_registers(start_address, values)

        return True

    async def read_serial_number(self) -> str:
        """Read inverter serial number from input registers 115-119.

        The serial number is stored as 10 ASCII characters across 5 registers.
        Each register contains 2 characters: low byte = char[0], high byte = char[1].

        This can be used to:
        - Validate the user-entered serial matches the actual device
        - Auto-discover the serial during setup
        - Detect cable swaps in multi-inverter setups

        Returns:
            10-character serial number string (e.g., "BA12345678")

        Raises:
            TransportReadError: If read operation fails

        Example:
            >>> transport = ModbusTransport(host="192.168.1.100", serial="")
            >>> await transport.connect()
            >>> actual_serial = await transport.read_serial_number()
            >>> print(f"Connected to inverter: {actual_serial}")
        """
        values = await self._read_input_registers(
            SERIAL_NUMBER_START_REGISTER, SERIAL_NUMBER_REGISTER_COUNT
        )

        # Decode ASCII characters from register values
        chars: list[str] = []
        for value in values:
            low_byte = value & 0xFF
            high_byte = (value >> 8) & 0xFF
            # Filter out non-printable characters
            if 32 <= low_byte <= 126:
                chars.append(chr(low_byte))
            if 32 <= high_byte <= 126:
                chars.append(chr(high_byte))

        serial = "".join(chars)
        _LOGGER.debug("Read serial number from Modbus: %s", serial)
        return serial

    async def read_firmware_version(self) -> str:
        """Read full firmware version code from holding registers 7-10.

        The firmware information is stored in a specific format:
        - Registers 7-8: Firmware prefix as byte-swapped ASCII (e.g., "FAAB")
        - Registers 9-10: Version bytes with special encoding

        Byte layout discovered via diagnostic register reading:
        - Reg 7: 0x4146 → low byte 'F', high byte 'A' → byte-swapped = "FA"
        - Reg 8: 0x4241 → low byte 'A', high byte 'B' → byte-swapped = "AB"
        - Reg 9: v1 is in high byte (e.g., 0x2503 → v1 = 0x25 = 37)
        - Reg 10: v2 is in low byte (e.g., 0x0125 → v2 = 0x25 = 37)

        The web API returns fwCode like "FAAB-2525" where:
        - "FAAB" is the device standard/prefix from registers 7-8
        - "2525" is {v1:02X}{v2:02X} (hex-encoded version bytes)

        Returns:
            Full firmware code string (e.g., "FAAB-2525")
            Returns empty string if read fails.
        """
        try:
            # Read holding registers 7-10 (prefix + version)
            regs = await self._read_holding_registers(7, 4)
            if len(regs) >= 4:
                # Extract firmware prefix from registers 7-8 (byte-swapped ASCII)
                # Each register has low byte first, high byte second
                prefix_chars = [
                    chr(regs[0] & 0xFF),  # Reg 7 low byte
                    chr((regs[0] >> 8) & 0xFF),  # Reg 7 high byte
                    chr(regs[1] & 0xFF),  # Reg 8 low byte
                    chr((regs[1] >> 8) & 0xFF),  # Reg 8 high byte
                ]
                prefix = "".join(prefix_chars)

                # Extract version bytes with special encoding
                # v1 is the HIGH byte of register 9
                # v2 is the LOW byte of register 10
                v1 = (regs[2] >> 8) & 0xFF  # High byte of register 9
                v2 = regs[3] & 0xFF  # Low byte of register 10

                firmware = f"{prefix}-{v1:02X}{v2:02X}"
                _LOGGER.debug("Read firmware version: %s", firmware)
                return firmware
        except Exception as err:
            _LOGGER.debug("Failed to read firmware version: %s", err)
        return ""

    async def validate_serial(self, expected_serial: str) -> bool:
        """Validate that the connected inverter matches the expected serial.

        Args:
            expected_serial: The serial number the user expects to connect to

        Returns:
            True if serials match, False otherwise

        Raises:
            TransportReadError: If read operation fails
        """
        actual_serial = await self.read_serial_number()
        matches = actual_serial == expected_serial

        if not matches:
            _LOGGER.warning(
                "Serial mismatch: expected %s, got %s",
                expected_serial,
                actual_serial,
            )

        return matches

    async def read_device_type(self) -> int:
        """Read device type code from register 19.

        This can be used to detect whether the connected device is an
        inverter or a MID/GridBOSS device.

        Known device type codes:
        - 50: MID/GridBOSS (Microgrid Interconnect Device)
        - 54: SNA Series
        - 2092: PV Series (18KPV)
        - 10284: FlexBOSS21/FlexBOSS18

        Returns:
            Device type code integer

        Raises:
            TransportReadError: If read operation fails
        """
        # Read register 19 which contains the device type code
        # Use holding register read (function 0x03) as this is a parameter
        values = await self._read_holding_registers(DEVICE_TYPE_REGISTER, 1)
        if not values:
            raise TransportReadError("Failed to read device type register")
        return values[0]

    def is_midbox_device(self, device_type_code: int) -> bool:
        """Check if device type code indicates a MID/GridBOSS device.

        Args:
            device_type_code: Device type code from read_device_type()

        Returns:
            True if device is a MID/GridBOSS, False for inverters
        """
        return device_type_code == DEVICE_TYPE_MIDBOX

    async def read_midbox_runtime(self) -> MidboxRuntimeData:
        """Read runtime data from a MID/GridBOSS device.

        MID devices use HOLDING registers (function 0x03) for runtime data,
        unlike inverters which use INPUT registers (function 0x04).

        Returns:
            MidboxRuntimeData with all values properly scaled:
            - Voltages in V (no scaling)
            - Currents in A (scaled /100)
            - Power in W (no scaling, signed)
            - Frequency in Hz (scaled /100)

        Raises:
            TransportReadError: If read operation fails
        """
        from pylxpweb.transports.register_maps import GRIDBOSS_RUNTIME_MAP

        # Read holding registers in groups
        # Group 1: Registers 0-41 (voltages, currents, power, smart load power)
        # Group 2: Registers 128-131 (frequencies)
        holding_registers: dict[int, int] = {}

        try:
            # Read voltages, currents, power, and smart load power (registers 0-41)
            values = await self._read_holding_registers(0, 42)
            for offset, value in enumerate(values):
                holding_registers[offset] = value

            # Read frequencies (registers 128-131)
            freq_values = await self._read_holding_registers(128, 4)
            for offset, value in enumerate(freq_values):
                holding_registers[128 + offset] = value

        except Exception as e:
            _LOGGER.error("Failed to read MID holding registers: %s", e)
            raise TransportReadError(f"Failed to read MID registers: {e}") from e

        return MidboxRuntimeData.from_modbus_registers(holding_registers, GRIDBOSS_RUNTIME_MAP)
