PyModbus
PyModbusDocs

PyModbus read_holding_registers - Complete Guide with Examples

Master PyModbus read_holding_registers with practical examples. Learn to read Modbus holding registers, handle responses, and troubleshoot common issues.

Reading Holding Registers with PyModbus

Master the most common Modbus operation - reading holding registers. This guide covers everything from basic usage to advanced techniques with real-world examples.

What are Holding Registers?

Holding registers are 16-bit read/write registers in Modbus devices (function code 0x03). They typically store:

  • Configuration parameters
  • Setpoints
  • Sensor values
  • Status information

Register Addressing: Holding registers use addresses 40001-49999 in Modbus notation, but PyModbus uses 0-based addressing (0-9998).

Basic Usage

Synchronous Client

from pymodbus.client import ModbusTcpClient

# Connect to Modbus TCP device
client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

# Read 10 holding registers starting at address 0
result = client.read_holding_registers(address=0, count=10, slave=1)

if not result.isError():
    registers = result.registers
    print(f"Register values: {registers}")
else:
    print(f"Error: {result}")

client.close()

Asynchronous Client

import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def read_registers():
    async with AsyncModbusTcpClient('192.168.1.100') as client:
        result = await client.read_holding_registers(
            address=0,
            count=10,
            slave=1
        )
        if not result.isError():
            return result.registers
        return None

# Run async function
registers = asyncio.run(read_registers())
print(f"Registers: {registers}")

Function Signature

read_holding_registers(
    address: int,           # Starting register address (0-based)
    count: int = 1,        # Number of registers to read (1-125)
    slave: int = 1,        # Slave/Unit ID
    **kwargs               # Additional parameters
) -> ModbusResponse

Real-World Examples

Reading Temperature from Sensor

from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

def read_temperature(client, address=100):
    """Read temperature value from Modbus sensor."""
    # Many sensors store float values across 2 registers
    result = client.read_holding_registers(address=address, count=2)
    
    if not result.isError():
        # Decode the registers as a 32-bit float
        decoder = BinaryPayloadDecoder.fromRegisters(
            result.registers,
            byteorder=Endian.BIG,
            wordorder=Endian.BIG
        )
        temperature = decoder.decode_32bit_float()
        return round(temperature, 2)
    return None

# Usage
client = ModbusTcpClient('192.168.1.100')
client.connect()
temp = read_temperature(client)
print(f"Temperature: {temp}°C")
client.close()

Reading Multiple Values Efficiently

def read_device_data(client):
    """Read multiple parameters from a Modbus device."""
    data = {}
    
    # Read different register blocks
    readings = [
        ('voltage', 0, 2),      # Address 0, 2 registers
        ('current', 2, 2),      # Address 2, 2 registers
        ('power', 4, 2),        # Address 4, 2 registers
        ('frequency', 6, 1),    # Address 6, 1 register
        ('status', 10, 1),      # Address 10, 1 register
    ]
    
    for name, address, count in readings:
        result = client.read_holding_registers(
            address=address,
            count=count,
            slave=1
        )
        if not result.isError():
            if count == 1:
                data[name] = result.registers[0]
            else:
                # Convert two registers to float
                decoder = BinaryPayloadDecoder.fromRegisters(
                    result.registers,
                    byteorder=Endian.BIG,
                    wordorder=Endian.BIG
                )
                data[name] = decoder.decode_32bit_float()
    
    return data

Reading with Error Handling

import time
from pymodbus.exceptions import ModbusException

def read_with_retry(client, address, count, max_retries=3):
    """Read registers with automatic retry on failure."""
    for attempt in range(max_retries):
        try:
            result = client.read_holding_registers(
                address=address,
                count=count,
                slave=1
            )
            
            if result.isError():
                print(f"Modbus error on attempt {attempt + 1}: {result}")
                if attempt < max_retries - 1:
                    time.sleep(0.5)  # Wait before retry
                    continue
            else:
                return result.registers
                
        except ModbusException as e:
            print(f"Exception on attempt {attempt + 1}: {e}")
            if attempt < max_retries - 1:
                time.sleep(0.5)
                continue
    
    return None  # Failed after all retries

Working with Different Data Types

Integer Values (16-bit)

# Read single 16-bit integer
result = client.read_holding_registers(address=0, count=1)
if not result.isError():
    value = result.registers[0]  # Direct 16-bit value
    signed_value = value if value < 32768 else value - 65536  # Convert to signed

Long Integers (32-bit)

# Read 32-bit integer from 2 registers
result = client.read_holding_registers(address=0, count=2)
if not result.isError():
    decoder = BinaryPayloadDecoder.fromRegisters(
        result.registers,
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    value = decoder.decode_32bit_uint()  # or decode_32bit_int() for signed

Float Values

# Read 32-bit float from 2 registers
result = client.read_holding_registers(address=0, count=2)
if not result.isError():
    decoder = BinaryPayloadDecoder.fromRegisters(
        result.registers,
        byteorder=Endian.BIG,
        wordorder=Endian.BIG
    )
    value = decoder.decode_32bit_float()

String Values

# Read string from multiple registers
result = client.read_holding_registers(address=0, count=8)  # 16 characters
if not result.isError():
    decoder = BinaryPayloadDecoder.fromRegisters(
        result.registers,
        byteorder=Endian.BIG
    )
    text = decoder.decode_string(16).decode('ascii').strip('\x00')

RTU vs TCP Examples

from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('192.168.1.100', port=502)
client.connect()

result = client.read_holding_registers(
    address=0,
    count=10,
    slave=1
)

client.close()
from pymodbus.client import ModbusSerialClient

client = ModbusSerialClient(
    port='/dev/ttyUSB0',  # or 'COM3' on Windows
    baudrate=9600,
    parity='N',
    stopbits=1,
    bytesize=8
)
client.connect()

result = client.read_holding_registers(
    address=0,
    count=10,
    slave=1
)

client.close()
from pymodbus.client import ModbusTcpClient
from pymodbus.framer import ModbusRtuFramer

client = ModbusTcpClient(
    '192.168.1.100',
    port=502,
    framer=ModbusRtuFramer
)
client.connect()

result = client.read_holding_registers(
    address=0,
    count=10,
    slave=1
)

client.close()

Common Issues and Solutions

Issue: None or Empty Response

# Check connection first
if not client.connect():
    print("Failed to connect")
    
# Verify slave ID
result = client.read_holding_registers(
    address=0,
    count=1,
    slave=1  # Make sure this matches your device
)

# Check for Modbus exception
if result.isError():
    print(f"Modbus exception: {result.exception_code}")
    # 1 = Illegal Function
    # 2 = Illegal Data Address
    # 3 = Illegal Data Value
    # 4 = Slave Device Failure

Issue: Wrong Values

# Try different byte/word order
for byte_order in [Endian.BIG, Endian.LITTLE]:
    for word_order in [Endian.BIG, Endian.LITTLE]:
        decoder = BinaryPayloadDecoder.fromRegisters(
            result.registers,
            byteorder=byte_order,
            wordorder=word_order
        )
        value = decoder.decode_32bit_float()
        print(f"Byte: {byte_order}, Word: {word_order}, Value: {value}")

Issue: Address Offset

# Some devices use Modbus notation (40001+)
# PyModbus uses 0-based addressing
modbus_address = 40001  # Device documentation
pymodbus_address = modbus_address - 40001  # Convert to 0-based

result = client.read_holding_registers(
    address=pymodbus_address,
    count=1
)

Performance Tips

  1. Read Multiple Registers at Once

    # Good - Single request
    result = client.read_holding_registers(address=0, count=10)
    
    # Bad - Multiple requests
    for i in range(10):
        result = client.read_holding_registers(address=i, count=1)
  2. Use Connection Pooling

    # Keep connection open for multiple reads
    with ModbusTcpClient('192.168.1.100') as client:
        for _ in range(100):
            result = client.read_holding_registers(0, 10)
            # Process result
            time.sleep(1)
  3. Async for Multiple Devices

    async def read_all_devices(devices):
        tasks = []
        for ip in devices:
            async with AsyncModbusTcpClient(ip) as client:
                task = client.read_holding_registers(0, 10)
                tasks.append(task)
        
        results = await asyncio.gather(*tasks)
        return results

Complete Example: Energy Meter Reader

#!/usr/bin/env python3
"""Read energy meter data using PyModbus."""

from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian
import time

class EnergyMeter:
    def __init__(self, host, port=502, slave=1):
        self.client = ModbusTcpClient(host, port=port)
        self.slave = slave
        
    def connect(self):
        return self.client.connect()
    
    def disconnect(self):
        self.client.close()
    
    def read_voltage(self):
        """Read voltage from registers 0-1 (32-bit float)."""
        result = self.client.read_holding_registers(
            address=0,
            count=2,
            slave=self.slave
        )
        if not result.isError():
            decoder = BinaryPayloadDecoder.fromRegisters(
                result.registers,
                byteorder=Endian.BIG,
                wordorder=Endian.BIG
            )
            return decoder.decode_32bit_float()
        return None
    
    def read_current(self):
        """Read current from registers 2-3 (32-bit float)."""
        result = self.client.read_holding_registers(
            address=2,
            count=2,
            slave=self.slave
        )
        if not result.isError():
            decoder = BinaryPayloadDecoder.fromRegisters(
                result.registers,
                byteorder=Endian.BIG,
                wordorder=Endian.BIG
            )
            return decoder.decode_32bit_float()
        return None
    
    def read_power(self):
        """Read power from registers 4-5 (32-bit float)."""
        result = self.client.read_holding_registers(
            address=4,
            count=2,
            slave=self.slave
        )
        if not result.isError():
            decoder = BinaryPayloadDecoder.fromRegisters(
                result.registers,
                byteorder=Endian.BIG,
                wordorder=Endian.BIG
            )
            return decoder.decode_32bit_float()
        return None
    
    def read_all(self):
        """Read all meter values."""
        return {
            'voltage': self.read_voltage(),
            'current': self.read_current(),
            'power': self.read_power(),
            'timestamp': time.time()
        }

# Usage
if __name__ == "__main__":
    meter = EnergyMeter('192.168.1.100')
    
    if meter.connect():
        print("Connected to energy meter")
        
        # Continuous monitoring
        while True:
            data = meter.read_all()
            print(f"Voltage: {data['voltage']:.2f} V")
            print(f"Current: {data['current']:.2f} A")
            print(f"Power: {data['power']:.2f} W")
            print("-" * 30)
            time.sleep(5)
    else:
        print("Failed to connect to meter")

Next Steps

How is this guide?