Added test/read_registers.py for testing

This commit is contained in:
Vladyslav Doloman
2025-11-24 02:33:24 +02:00
parent 9f96a3e534
commit 7423d7c37e

486
test/read_registers.py Normal file
View File

@@ -0,0 +1,486 @@
"""
Script to read and write holding registers from a Solarman inverter using the pysolarmanv5 library.
Usage:
Read registers: python read_registers.py
Write to register 230: python read_registers.py --write 230 75
Write to register 293: python read_registers.py --write 293 5000
Write to multiple registers: python read_registers.py --write 230 75 --write 293 5000
Writable registers:
- Register 230: range 0-115
- Register 293: range 0-6500
"""
import sys
import os
import argparse
import ipaddress
# Import required libraries with error handling
try:
from pysolarmanv5 import PySolarmanV5
from pysolarmanv5.pysolarmanv5 import V5FrameError
import umodbus.exceptions
except ImportError as e:
print(f"ERROR: Required library not installed: {e}")
print("Install required libraries with: pip install pysolarmanv5 umodbus")
sys.exit(1)
# Helper functions for validated configuration
def get_env_int(name, default, min_val=None, max_val=None):
"""
Get integer from environment variable with validation.
Args:
name: Environment variable name
default: Default value if not set or invalid
min_val: Minimum allowed value (optional)
max_val: Maximum allowed value (optional)
Returns:
int: Validated integer value
"""
value_str = os.getenv(name)
if value_str is None:
return default
try:
value = int(value_str)
if min_val is not None and value < min_val:
print(f"WARNING: {name}={value} is below minimum ({min_val}). Using default: {default}")
return default
if max_val is not None and value > max_val:
print(f"WARNING: {name}={value} exceeds maximum ({max_val}). Using default: {default}")
return default
return value
except ValueError:
print(f"WARNING: Invalid {name}='{value_str}' (must be an integer). Using default: {default}")
return default
def get_env_ip(name, default):
"""
Get IP address from environment variable with validation.
Args:
name: Environment variable name
default: Default IP address
Returns:
str: Validated IP address
"""
value = os.getenv(name)
if value is None:
return default
try:
# Validate IP address format
ipaddress.ip_address(value)
return value
except ValueError:
print(f"WARNING: Invalid IP address in {name}='{value}'. Using default: {default}")
return default
# Configuration - Can be overridden with environment variables
INVERTER_IP = get_env_ip("INVERTER_IP", "192.168.0.203")
INVERTER_SERIAL = get_env_int("INVERTER_SERIAL", 2722455016, min_val=0, max_val=4294967295)
INVERTER_PORT = get_env_int("INVERTER_PORT", 8899, min_val=1, max_val=65535)
MODBUS_SLAVE_ID = get_env_int("MODBUS_SLAVE_ID", 1, min_val=0, max_val=247)
SOCKET_TIMEOUT = get_env_int("SOCKET_TIMEOUT", 5, min_val=1, max_val=60)
# Registers to read
REGISTERS = [227, 230, 292, 293]
# Writable registers whitelist with validation ranges
# Format: {register_number: (min_value, max_value)}
WRITABLE_REGISTERS = {
230: (0, 115), # Register 230: valid range 0-115
293: (0, 6500), # Register 293: valid range 0-6500
}
def validate_writable_registers():
"""Validate WRITABLE_REGISTERS configuration at startup."""
if not WRITABLE_REGISTERS:
raise ValueError("WRITABLE_REGISTERS cannot be empty. At least one register must be configured.")
for reg, (min_val, max_val) in WRITABLE_REGISTERS.items():
if not isinstance(reg, int) or reg < 0 or reg > 65535:
raise ValueError(f"Invalid register address: {reg}. Must be 0-65535.")
if not isinstance(min_val, int) or not isinstance(max_val, int):
raise ValueError(f"Register {reg} range values must be integers")
if min_val > max_val:
raise ValueError(f"Invalid range for register {reg}: min ({min_val}) > max ({max_val})")
if min_val < 0 or max_val > 65535:
raise ValueError(f"Register {reg} range [{min_val}, {max_val}] outside valid Modbus range [0, 65535]")
def validate_registers():
"""Validate REGISTERS configuration at startup."""
if not isinstance(REGISTERS, list):
raise ValueError("REGISTERS must be a list")
seen = set()
for reg in REGISTERS:
if not isinstance(reg, int):
raise ValueError(f"Invalid register address: {reg}. Must be an integer.")
if reg < 0 or reg > 65535:
raise ValueError(f"Invalid register address: {reg}. Must be 0-65535.")
if reg in seen:
raise ValueError(f"Duplicate register address: {reg}")
seen.add(reg)
# Validate configuration on module load
try:
validate_writable_registers()
validate_registers()
except ValueError as e:
print(f"ERROR: Configuration validation failed: {e}")
print("Please check WRITABLE_REGISTERS and REGISTERS configuration.")
sys.exit(1)
def validate_register_arg(value_str):
"""
Validate register address from command line.
Args:
value_str: String value from command line
Returns:
int: Validated register address
Raises:
ValueError: If value is invalid
"""
try:
int_val = int(value_str)
if int_val < 0 or int_val > 65535:
raise ValueError(f"Register address must be 0-65535, got {int_val}")
return int_val
except ValueError as e:
if "invalid literal" in str(e):
raise ValueError(f"Invalid integer: {value_str}")
raise
def validate_value_arg(value_str):
"""
Validate register value from command line.
Args:
value_str: String value from command line
Returns:
int: Validated register value
Raises:
ValueError: If value is invalid
"""
try:
int_val = int(value_str)
if int_val < 0 or int_val > 65535:
raise ValueError(f"Register value must be 0-65535, got {int_val}")
return int_val
except ValueError as e:
if "invalid literal" in str(e):
raise ValueError(f"Invalid integer: {value_str}")
raise
def validate_write_operations(write_operations):
"""
Validate all write operations before connecting to device.
Args:
write_operations: List of (register_addr, value) tuples
Returns:
bool: True if all operations are valid
Raises:
SystemExit: If any validation fails
"""
if not write_operations:
return True
print("Validating write operations...")
for register_addr, value in write_operations:
# Check if register is in the whitelist
if register_addr not in WRITABLE_REGISTERS:
print(f"\nERROR: Register {register_addr} is not in the writable registers whitelist")
print(f"Allowed registers: {list(WRITABLE_REGISTERS.keys())}")
sys.exit(1)
# Validate value is not negative
if value < 0:
print(f"\nERROR: Negative values not allowed. Got: {value}")
sys.exit(1)
# Validate value fits in Modbus register
if value > 65535:
print(f"\nERROR: Value {value} exceeds maximum Modbus register value (65535)")
sys.exit(1)
# Validate value range for this specific register
min_val, max_val = WRITABLE_REGISTERS[register_addr]
if not min_val <= value <= max_val:
print(f"\nERROR: Value {value} out of range for register {register_addr}")
print(f"Valid range: {min_val} to {max_val}")
sys.exit(1)
print("All write operations validated.\n")
return True
def write_register(modbus, register_addr, value):
"""
Write a value to a holding register.
Note: This function assumes the register address and value have already been
validated by validate_write_operations() before calling.
Args:
modbus: PySolarmanV5 instance
register_addr: Register address to write to (must be pre-validated)
value: Integer value to write (must be pre-validated)
Returns:
bool: True if write succeeded, False otherwise
"""
try:
print(f"\nWriting value {value} to register {register_addr}...")
# Read current value first
print("Reading current value...")
current = modbus.read_holding_registers(register_addr=register_addr, quantity=1)
# Check for empty response
if not current or len(current) == 0:
print("ERROR: Failed to read current value (empty response)")
return False
print(f"Current value: {current[0]}")
try:
result = modbus.write_multiple_holding_registers(register_addr, [value])
print(f"Write command sent (method: multi)({result})")
except V5FrameError as e:
print(f"V5FrameError: {e}")
return False
except umodbus.exceptions.ModbusError as e:
print(f"Modbus protocol error: {e}")
return False
# Verify the write succeeded
print("Verifying write...")
try:
updated = modbus.read_holding_registers(register_addr=register_addr, quantity=1)
# Check for empty response
if not updated or len(updated) == 0:
print("ERROR: Failed to verify write (empty response)")
return False
updated_value = updated[0]
if updated_value == value:
print(f"SUCCESS: Register {register_addr} = {updated_value}")
return True
else:
print(f"WARNING: Expected {value}, but register {register_addr} = {updated_value}")
print("Note: Value may have been modified by device or another client")
return False
except TimeoutError:
print(f"ERROR: Timeout reading back register {register_addr}")
return False
except Exception as e:
print(f"ERROR writing to register {register_addr}: {type(e).__name__}: {e}")
return False
def read_and_display_registers(modbus):
"""
Read and display the configured registers.
Args:
modbus: PySolarmanV5 instance
"""
try:
# Check if REGISTERS list is empty
if not REGISTERS:
print("WARNING: No registers configured to read")
return
# Read and display each register
print("Reading holding registers:")
print("-" * 50)
for register_addr in REGISTERS:
try:
# Read single register
result = modbus.read_holding_registers(
register_addr=register_addr,
quantity=1
)
# Check for empty response
if not result or len(result) == 0:
print(f"Register {register_addr:>3}: Error - empty response")
continue
# Extract value (result is a list)
value = result[0]
# Display the register and its value
print(f"Register {register_addr:>3}: {value:>5} (0x{value:04X})")
except (V5FrameError, umodbus.exceptions.IllegalDataAddressError) as e:
print(f"Register {register_addr:>3}: Error reading - {e}")
except Exception as e:
print(f"Register {register_addr:>3}: Unexpected error - {e}")
print("-" * 50)
print("\nRead complete!")
except Exception as e:
print(f"\nError reading registers: {e}")
def main():
"""Main function to parse arguments and execute operations."""
# Parse command line arguments
parser = argparse.ArgumentParser(
description="Read and write Solarman inverter holding registers",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
Examples:
Read registers: python read_registers.py
Write to register 230: python read_registers.py --write 230 75
Write to register 293: python read_registers.py --write 293 5000
Write to multiple registers: python read_registers.py --write 230 75 --write 293 5000
Enable verbose output: python read_registers.py --verbose
Writable registers and their valid ranges:
{chr(10).join(f' Register {reg}: {min_val}-{max_val}' for reg, (min_val, max_val) in WRITABLE_REGISTERS.items())}
Environment Variables:
INVERTER_IP - IP address of Solarman data logger (default: {INVERTER_IP})
INVERTER_SERIAL - Serial number of data logger (default: {INVERTER_SERIAL})
INVERTER_PORT - TCP port (default: {INVERTER_PORT})
MODBUS_SLAVE_ID - Modbus slave ID (default: {MODBUS_SLAVE_ID})
SOCKET_TIMEOUT - Socket timeout in seconds (default: {SOCKET_TIMEOUT})
"""
)
parser.add_argument(
'--write',
nargs=2,
type=str, # Parse as strings for better validation
metavar=('REGISTER', 'VALUE'),
action='append',
dest='write_operations',
help='Write VALUE to REGISTER (can be used multiple times)'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Enable verbose protocol output for debugging'
)
args = parser.parse_args()
# Validate and convert write operations
if args.write_operations:
validated_operations = []
for reg_str, val_str in args.write_operations:
try:
register_addr = validate_register_arg(reg_str)
value = validate_value_arg(val_str)
validated_operations.append((register_addr, value))
except ValueError as e:
print(f"ERROR: Invalid write argument: {e}")
sys.exit(1)
args.write_operations = validated_operations
print("=" * 50)
print("Solarman Register Reader/Writer")
print("=" * 50)
print()
# Pre-validate write operations before connecting
validate_write_operations(args.write_operations)
modbus = None
write_failures = []
try:
# Initialize connection
print(f"Connecting to {INVERTER_IP}:{INVERTER_PORT} (Serial: {INVERTER_SERIAL})...")
modbus = PySolarmanV5(
address=INVERTER_IP,
serial=INVERTER_SERIAL,
port=INVERTER_PORT,
mb_slave_id=MODBUS_SLAVE_ID,
socket_timeout=SOCKET_TIMEOUT,
verbose=args.verbose,
v5_error_correction=True
)
print("Connected successfully!")
# Validate connection with a test read
if REGISTERS:
try:
test_read = modbus.read_holding_registers(register_addr=REGISTERS[0], quantity=1)
if test_read and len(test_read) > 0:
print(f"Connection verified (read register {REGISTERS[0]})!\n")
else:
print(f"WARNING: Connection test to register {REGISTERS[0]} returned empty response\n")
except Exception as e:
print(f"WARNING: Connection test to register {REGISTERS[0]} failed: {e}")
print("This may indicate a connection issue or invalid register address.")
print("Continuing anyway...\n")
else:
print("Skipping connection test (no registers configured)\n")
# Perform write operations if requested
if args.write_operations:
for register_addr, value in args.write_operations:
success = write_register(modbus, register_addr, value)
if not success:
print(f"Write operation failed for register {register_addr}!")
write_failures.append(register_addr)
# Always read registers to show current values
print()
read_and_display_registers(modbus)
except Exception as e:
print(f"\nConnection error: {e}")
print("Please check your IP address, serial number, and network connection.")
sys.exit(1)
finally:
# Ensure disconnection
if modbus:
try:
modbus.disconnect()
print("\nDisconnected from inverter.")
except Exception as e:
# Log but don't raise - we're already cleaning up
print(f"\nWarning: Error during disconnect: {e}")
# Exit with error code if any write operations failed
if write_failures:
print(f"\nERROR: Failed to write to {len(write_failures)} register(s): {write_failures}")
sys.exit(1)
if __name__ == "__main__":
main()