1212from homeassistant .core import HomeAssistant
1313from homeassistant .exceptions import HomeAssistantError
1414from pymodbus .client import AsyncModbusTcpClient
15- from pymodbus .constants import Endian
1615from pymodbus .exceptions import ConnectionException , ModbusException
17- from pymodbus .payload import BinaryPayloadBuilder
1816from pymodbus .client .mixin import ModbusClientMixin
1917from pymodbus import __version__ as pymodbus_version
2018
4341 DC_CHARGER_RUNNING_INFO_REGISTERS ,
4442 DC_CHARGER_PARAMETER_REGISTERS ,
4543)
46- from homeassistant .helpers .entity_registry import async_get as async_get_entity_registry
47- from .common import generate_device_id
48- from .const import DOMAIN
49- from homeassistant .helpers .entity_registry import async_get as async_get_entity_registry
50- from .common import generate_device_id
51- from .const import DOMAIN
5244
5345_LOGGER = logging .getLogger (__name__ )
5446
47+ # Pymodbus version compatibility handling
48+ def _get_pymodbus_version_info ():
49+ """Get pymodbus version information for compatibility handling."""
50+ try :
51+ # Parse version string like "3.6.8" or "3.10.0"
52+ version_parts = pymodbus_version .split ('.' )
53+ major = int (version_parts [0 ])
54+ minor = int (version_parts [1 ])
55+ return major , minor
56+ except (ValueError , IndexError ):
57+ # Fallback to assuming older version if parsing fails
58+ _LOGGER .warning ("Could not parse pymodbus version '%s', assuming < 3.10" , pymodbus_version )
59+ return 3 , 0
60+
61+ # Check if we need to use 'device_id' instead of 'slave'
62+ _PYMODBUS_MAJOR , _PYMODBUS_MINOR = _get_pymodbus_version_info ()
63+ _USE_DEVICE_ID = (_PYMODBUS_MAJOR > 3 ) or (_PYMODBUS_MAJOR == 3 and _PYMODBUS_MINOR >= 10 )
64+
65+ def _call_modbus_method_safe (client_method , * args , ** kwargs ):
66+ """
67+ Call a pymodbus client method with version-compatible parameter handling.
68+
69+ This function handles the parameter name change from 'slave' to 'device_id'
70+ that occurred in pymodbus 3.10+.
71+
72+ Args:
73+ client_method: The pymodbus client method to call
74+ *args: Positional arguments
75+ **kwargs: Keyword arguments, including 'slave' or 'device_id'
76+
77+ Returns:
78+ The result of the client method call
79+
80+ Raises:
81+ TypeError: If the method call fails due to parameter incompatibility
82+ Other exceptions from the underlying method
83+ """
84+ if _USE_DEVICE_ID :
85+ # For pymodbus >= 3.10, use 'device_id' parameter
86+ if 'slave' in kwargs :
87+ kwargs ['device_id' ] = kwargs .pop ('slave' )
88+
89+ try :
90+ return client_method (* args , ** kwargs )
91+ except TypeError as e :
92+ if 'device_id' in str (e ):
93+ # If device_id also fails, try falling back to positional args
94+ _LOGGER .debug ("device_id parameter failed, trying positional arguments" )
95+ # Remove device_id and try with positional args if possible
96+ kwargs_copy = kwargs .copy ()
97+ if 'device_id' in kwargs_copy :
98+ device_id = kwargs_copy .pop ('device_id' )
99+ # Try calling with device_id as positional argument
100+ try :
101+ return client_method (* args , device_id , ** kwargs_copy )
102+ except TypeError as inner_e :
103+ # If positional also fails, re-raise original error
104+ raise e from inner_e
105+ else :
106+ raise e
107+ else :
108+ raise e
109+ else :
110+ # For pymodbus < 3.10, use 'slave' parameter
111+ try :
112+ return client_method (* args , ** kwargs )
113+ except TypeError as e :
114+ if 'slave' in str (e ):
115+ # If slave fails, try falling back to positional args
116+ _LOGGER .debug ("slave parameter failed, trying positional arguments" )
117+ kwargs_copy = kwargs .copy ()
118+ if 'slave' in kwargs_copy :
119+ slave = kwargs_copy .pop ('slave' )
120+ try :
121+ return client_method (* args , slave , ** kwargs_copy )
122+ except TypeError as inner_e :
123+ raise e from inner_e
124+ else :
125+ raise e
126+ else :
127+ raise e
128+
55129@dataclass
56130class ModbusConnectionConfig :
57131 """Configuration for a Modbus connection."""
@@ -295,13 +369,15 @@ async def _probe_single_register(
295369
296370 with _suppress_pymodbus_logging (really_suppress = False if _LOGGER .isEnabledFor (logging .DEBUG ) else True ):
297371 if register .register_type == RegisterType .READ_ONLY :
298- result = await client .read_input_registers (
372+ result = await _call_modbus_method_safe (
373+ client .read_input_registers ,
299374 address = register .address ,
300375 count = register .count ,
301376 slave = slave_id
302377 )
303378 elif register .register_type == RegisterType .HOLDING :
304- result = await client .read_holding_registers (
379+ result = await _call_modbus_method_safe (
380+ client .read_holding_registers ,
305381 address = register .address ,
306382 count = register .count ,
307383 slave = slave_id
@@ -365,7 +441,6 @@ async def async_probe_registers(
365441 _LOGGER .debug ("No registers need probing for %s." , device_info_log )
366442 # If no probing is needed, still generate intervals from already known registers
367443 # This handles the case where probing was already done in a previous run.
368- pass
369444 else :
370445 _LOGGER .debug ("Probing %d registers concurrently for %s..." , len (tasks ), device_info_log )
371446
@@ -503,10 +578,8 @@ async def async_read_registers(
503578
504579 async with self ._locks [key ]:
505580 with _suppress_pymodbus_logging (really_suppress = False if _LOGGER .isEnabledFor (logging .DEBUG ) else True ):
506- result = await client .read_input_registers (
507- address = address , count = count , slave = slave_id
508- ) if register_type == RegisterType .READ_ONLY \
509- else await client .read_holding_registers (
581+ result = await _call_modbus_method_safe (
582+ client .read_input_registers if register_type == RegisterType .READ_ONLY else client .read_holding_registers ,
510583 address = address , count = count , slave = slave_id
511584 )
512585
@@ -592,13 +665,15 @@ async def async_write_register(
592665 )
593666
594667 if approach ["function" ] == "write_registers" :
595- result = await client .write_registers (
668+ result = await _call_modbus_method_safe (
669+ client .write_registers ,
596670 address = approach ["address" ],
597671 values = approach ["values" ],
598672 slave = slave_id
599673 )
600674 else : # write_register
601- result = await client .write_register (
675+ result = await _call_modbus_method_safe (
676+ client .write_register ,
602677 address = approach ["address" ],
603678 value = approach ["value" ],
604679 slave = slave_id
@@ -753,36 +828,43 @@ def _encode_value(
753828 ) -> List [int ]:
754829 """Encode value to register values based on data type."""
755830 # For simple U16 values like 0 or 1, just return the value directly
756- # This bypasses potential byte order issues with the BinaryPayloadBuilder
757831 if data_type == DataType .U16 and isinstance (value , int ) and 0 <= value <= 255 :
758832 _LOGGER .debug ("Using direct value encoding for simple U16 value: %s" , value )
759833 return [value ]
760834
761- # For other cases, use the BinaryPayloadBuilder
762- builder = BinaryPayloadBuilder (byteorder = Endian .BIG , wordorder = Endian .BIG )
763-
764835 # Apply gain for numeric values
765836 if isinstance (value , (int , float )) and gain != 1 and data_type != DataType .STRING :
766837 value = int (value * gain )
767838
768839 _LOGGER .debug ("Encoding value %s with data_type %s" , value , data_type )
769840
770841 if data_type == DataType .U16 :
771- builder .add_16bit_uint (int (value ))
842+ registers = ModbusClientMixin .convert_to_registers (
843+ value , data_type = ModbusClientMixin .DATATYPE .UINT16
844+ )
772845 elif data_type == DataType .S16 :
773- builder .add_16bit_int (int (value ))
846+ registers = ModbusClientMixin .convert_to_registers (
847+ value , data_type = ModbusClientMixin .DATATYPE .INT16
848+ )
774849 elif data_type == DataType .U32 :
775- builder .add_32bit_uint (int (value ))
850+ registers = ModbusClientMixin .convert_to_registers (
851+ value , data_type = ModbusClientMixin .DATATYPE .UINT32
852+ )
776853 elif data_type == DataType .S32 :
777- builder .add_32bit_int (int (value ))
854+ registers = ModbusClientMixin .convert_to_registers (
855+ value , data_type = ModbusClientMixin .DATATYPE .INT32
856+ )
778857 elif data_type == DataType .U64 :
779- builder .add_64bit_uint (int (value ))
858+ registers = ModbusClientMixin .convert_to_registers (
859+ value , data_type = ModbusClientMixin .DATATYPE .UINT64
860+ )
780861 elif data_type == DataType .STRING :
781- builder .add_string (str (value ))
862+ registers = ModbusClientMixin .convert_to_registers (
863+ str (value ), data_type = ModbusClientMixin .DATATYPE .STRING
864+ )
782865 else :
783866 raise SigenergyModbusError (f"Unsupported data type: { data_type } " )
784867
785- registers = builder .to_registers ()
786868 _LOGGER .debug ("Encoded registers: %s" , registers )
787869 return registers
788870
@@ -1203,4 +1285,4 @@ async def async_write_parameter(
12031285 except SigenergyModbusError as ex :
12041286 _LOGGER .error ("Failed to write %s parameter '%s' (device: %s): %s" ,
12051287 device_type , register_name , device_identifier or 'plant' , ex )
1206- raise # Re-raise the specific error
1288+ raise # Re-raise the specific error
0 commit comments