66import importlib
77import json
88import logging
9+ import struct
910import time as _mtime
1011from dataclasses import dataclass , replace
1112from datetime import timedelta
7677 REG_HOLDING ,
7778 REG_INPUT ,
7879 REGISTER_F32 ,
80+ REGISTER_INT_RANGES ,
7981 REGISTER_S16 ,
8082 REGISTER_S32 ,
8183 REGISTER_STR ,
84+ REGISTER_TYPE_WORDS ,
8285 REGISTER_U8H ,
8386 REGISTER_U8L ,
8487 REGISTER_U16 ,
@@ -492,6 +495,10 @@ class PendingWrite:
492495 register_data_type : str | None = None
493496
494497
498+ class RegisterEncodingError (HomeAssistantError ):
499+ """Raised when a value cannot be represented by its Modbus register type."""
500+
501+
495502class SolaXModbusHub :
496503 """Thread safe wrapper class for pymodbus."""
497504
@@ -1345,11 +1352,17 @@ def _validate_write_response(self, response: Any, *, unit: int, address: int, op
13451352 raise HomeAssistantError (f"{ self ._name } : { operation } was rejected by device { unit } at register 0x{ address :x} : { response } " )
13461353 return response
13471354
1348- def _encode_multi_write_payload (self , payload : list [tuple [Any , Any ]]) -> list [int ]:
1349- """Encode a complete multi-register payload before any data is sent."""
1350- if not isinstance (payload , list ) or not payload :
1351- raise HomeAssistantError (f"{ self ._name } : multi-register write requires a non-empty payload" )
1352-
1355+ def _encode_write_value (
1356+ self ,
1357+ payload : int | float ,
1358+ register_data_type : str | None ,
1359+ * ,
1360+ single_register : bool ,
1361+ ) -> list [int ]:
1362+ """Validate and encode one value before it reaches the transport."""
1363+ effective_type = register_data_type or (REGISTER_S16 if single_register else None )
1364+ if effective_type is None :
1365+ raise RegisterEncodingError (f"{ self ._name } : unsupported register data type { register_data_type } " )
13531366 data_type_enum = cast (Any , DataType )
13541367 data_types : dict [str , Any ] = {
13551368 REGISTER_U16 : data_type_enum .UINT16 ,
@@ -1358,13 +1371,44 @@ def _encode_multi_write_payload(self, payload: list[tuple[Any, Any]]) -> list[in
13581371 REGISTER_F32 : data_type_enum .FLOAT32 ,
13591372 REGISTER_S32 : data_type_enum .INT32 ,
13601373 }
1374+ data_type = data_types .get (effective_type )
1375+ word_count = REGISTER_TYPE_WORDS .get (effective_type )
1376+ if data_type is None or word_count is None :
1377+ raise RegisterEncodingError (f"{ self ._name } : unsupported register data type { register_data_type } " )
1378+ if single_register and word_count != 1 :
1379+ raise RegisterEncodingError (
1380+ f"{ self ._name } : register data type { effective_type } requires { word_count } registers and cannot be written as a single register"
1381+ )
1382+
1383+ try :
1384+ if effective_type == REGISTER_F32 :
1385+ value : int | float = float (payload )
1386+ else :
1387+ value = int (payload )
1388+ minimum , maximum = REGISTER_INT_RANGES [effective_type ]
1389+ if value < minimum or value > maximum :
1390+ raise RegisterEncodingError (f"{ self ._name } : value { value } is outside the { effective_type } register range { minimum } ..{ maximum } " )
1391+ registers = cast (list [int ], convert_to_registers (value , data_type , self .plugin .order32 ))
1392+ except RegisterEncodingError :
1393+ raise
1394+ except (OverflowError , TypeError , ValueError , struct .error ) as ex :
1395+ raise RegisterEncodingError (f"{ self ._name } : cannot encode value { payload !r} as { effective_type } : { ex } " ) from ex
1396+
1397+ if len (registers ) != word_count :
1398+ raise RegisterEncodingError (f"{ self ._name } : encoding { effective_type } produced { len (registers )} registers instead of { word_count } " )
1399+ return registers
1400+
1401+ def _encode_multi_write_payload (self , payload : list [tuple [Any , Any ]]) -> list [int ]:
1402+ """Encode a complete multi-register payload before any data is sent."""
1403+ if not isinstance (payload , list ) or not payload :
1404+ raise HomeAssistantError (f"{ self ._name } : multi-register write requires a non-empty payload" )
1405+
13611406 registers : list [int ] = []
13621407 for item in payload :
13631408 try :
13641409 key , value = item
13651410 if key .startswith ("_" ):
13661411 register_data_type = key
1367- value = int (value )
13681412 else :
13691413 descr = self .writeLocals [key ]
13701414 reverse_options = getattr (descr , "reverse_option_dict" , None )
@@ -1378,13 +1422,9 @@ def _encode_multi_write_payload(self, payload: list[tuple[Any, Any]]) -> list[in
13781422 value = descr .scale (value , descr , self .data )
13791423 else :
13801424 value = value * descr .scale
1381- value = int (value )
13821425 register_data_type = descr .register_data_type
13831426
1384- data_type = data_types .get (register_data_type )
1385- if data_type is None :
1386- raise ValueError (f"unsupported register data type { register_data_type } " )
1387- registers .extend (convert_to_registers (value , data_type , self .plugin .order32 ))
1427+ registers .extend (self ._encode_write_value (value , register_data_type , single_register = False ))
13881428 except Exception as ex :
13891429 raise HomeAssistantError (f"{ self ._name } : cannot encode multi-register write item { item !r} : { ex } " ) from ex
13901430
@@ -1417,11 +1457,8 @@ async def _async_transport_write(
14171457 )
14181458
14191459 async def async_lowlevel_write_register (self , unit : int , address : int , payload : int , register_data_type : str | None = None ) -> Any :
1420- if register_data_type == REGISTER_U16 :
1421- regs = convert_to_registers (int (payload ), DataType .UINT16 , self .plugin .order32 ) # type: ignore[attr-defined]
1422- else :
1423- regs = convert_to_registers (int (payload ), DataType .INT16 , self .plugin .order32 ) # type: ignore[attr-defined]
14241460 try :
1461+ regs = self ._encode_write_value (payload , register_data_type , single_register = True )
14251462 response = await self ._async_transport_write (
14261463 unit = unit ,
14271464 address = address ,
@@ -1458,6 +1495,8 @@ async def async_write_register(self, unit: int, address: int, payload: int, regi
14581495 payload ,
14591496 register_data_type = register_data_type ,
14601497 )
1498+ except RegisterEncodingError :
1499+ raise
14611500 except HomeAssistantError as ex :
14621501 self .writequeue [(unit , address )] = request
14631502 if self .wakeupButton :
@@ -1494,10 +1533,7 @@ async def async_write_registers_single(
14941533 self , unit : int , address : int , payload : int , register_data_type : str | None = None
14951534 ) -> Any : # Needs adapting for register queue
14961535 """Write registers multi, but write only one register of type 16bit"""
1497- if register_data_type == REGISTER_U16 :
1498- regs = convert_to_registers (int (payload ), DataType .UINT16 , self .plugin .order32 ) # type: ignore[attr-defined]
1499- else :
1500- regs = convert_to_registers (int (payload ), DataType .INT16 , self .plugin .order32 ) # type: ignore[attr-defined]
1536+ regs = self ._encode_write_value (payload , register_data_type , single_register = True )
15011537 return await self ._async_transport_write (
15021538 unit = unit ,
15031539 address = address ,
0 commit comments