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 ,
@@ -480,6 +483,10 @@ class PendingWrite:
480483 register_data_type : str | None = None
481484
482485
486+ class RegisterEncodingError (HomeAssistantError ):
487+ """Raised when a value cannot be represented by its Modbus register type."""
488+
489+
483490class SolaXModbusHub :
484491 """Thread safe wrapper class for pymodbus."""
485492
@@ -1366,11 +1373,17 @@ def _validate_write_response(self, response: Any, *, unit: int, address: int, op
13661373 raise HomeAssistantError (f"{ self ._name } : { operation } was rejected by device { unit } at register 0x{ address :x} : { response } " )
13671374 return response
13681375
1369- def _encode_multi_write_payload (self , payload : list [tuple [Any , Any ]]) -> list [int ]:
1370- """Encode a complete multi-register payload before any data is sent."""
1371- if not isinstance (payload , list ) or not payload :
1372- raise HomeAssistantError (f"{ self ._name } : multi-register write requires a non-empty payload" )
1373-
1376+ def _encode_write_value (
1377+ self ,
1378+ payload : int | float ,
1379+ register_data_type : str | None ,
1380+ * ,
1381+ single_register : bool ,
1382+ ) -> list [int ]:
1383+ """Validate and encode one value before it reaches the transport."""
1384+ effective_type = register_data_type or (REGISTER_S16 if single_register else None )
1385+ if effective_type is None :
1386+ raise RegisterEncodingError (f"{ self ._name } : unsupported register data type { register_data_type } " )
13741387 data_type_enum = cast (Any , DataType )
13751388 data_types : dict [str , Any ] = {
13761389 REGISTER_U16 : data_type_enum .UINT16 ,
@@ -1379,13 +1392,44 @@ def _encode_multi_write_payload(self, payload: list[tuple[Any, Any]]) -> list[in
13791392 REGISTER_F32 : data_type_enum .FLOAT32 ,
13801393 REGISTER_S32 : data_type_enum .INT32 ,
13811394 }
1395+ data_type = data_types .get (effective_type )
1396+ word_count = REGISTER_TYPE_WORDS .get (effective_type )
1397+ if data_type is None or word_count is None :
1398+ raise RegisterEncodingError (f"{ self ._name } : unsupported register data type { register_data_type } " )
1399+ if single_register and word_count != 1 :
1400+ raise RegisterEncodingError (
1401+ f"{ self ._name } : register data type { effective_type } requires { word_count } registers and cannot be written as a single register"
1402+ )
1403+
1404+ try :
1405+ if effective_type == REGISTER_F32 :
1406+ value : int | float = float (payload )
1407+ else :
1408+ value = int (payload )
1409+ minimum , maximum = REGISTER_INT_RANGES [effective_type ]
1410+ if value < minimum or value > maximum :
1411+ raise RegisterEncodingError (f"{ self ._name } : value { value } is outside the { effective_type } register range { minimum } ..{ maximum } " )
1412+ registers = cast (list [int ], convert_to_registers (value , data_type , self .plugin .order32 ))
1413+ except RegisterEncodingError :
1414+ raise
1415+ except (OverflowError , TypeError , ValueError , struct .error ) as ex :
1416+ raise RegisterEncodingError (f"{ self ._name } : cannot encode value { payload !r} as { effective_type } : { ex } " ) from ex
1417+
1418+ if len (registers ) != word_count :
1419+ raise RegisterEncodingError (f"{ self ._name } : encoding { effective_type } produced { len (registers )} registers instead of { word_count } " )
1420+ return registers
1421+
1422+ def _encode_multi_write_payload (self , payload : list [tuple [Any , Any ]]) -> list [int ]:
1423+ """Encode a complete multi-register payload before any data is sent."""
1424+ if not isinstance (payload , list ) or not payload :
1425+ raise HomeAssistantError (f"{ self ._name } : multi-register write requires a non-empty payload" )
1426+
13821427 registers : list [int ] = []
13831428 for item in payload :
13841429 try :
13851430 key , value = item
13861431 if key .startswith ("_" ):
13871432 register_data_type = key
1388- value = int (value )
13891433 else :
13901434 descr = self .writeLocals [key ]
13911435 reverse_options = getattr (descr , "reverse_option_dict" , None )
@@ -1399,13 +1443,9 @@ def _encode_multi_write_payload(self, payload: list[tuple[Any, Any]]) -> list[in
13991443 value = descr .scale (value , descr , self .data )
14001444 else :
14011445 value = value * descr .scale
1402- value = int (value )
14031446 register_data_type = descr .register_data_type
14041447
1405- data_type = data_types .get (register_data_type )
1406- if data_type is None :
1407- raise ValueError (f"unsupported register data type { register_data_type } " )
1408- registers .extend (convert_to_registers (value , data_type , self .plugin .order32 ))
1448+ registers .extend (self ._encode_write_value (value , register_data_type , single_register = False ))
14091449 except Exception as ex :
14101450 raise HomeAssistantError (f"{ self ._name } : cannot encode multi-register write item { item !r} : { ex } " ) from ex
14111451
@@ -1438,11 +1478,8 @@ async def _async_transport_write(
14381478 )
14391479
14401480 async def async_lowlevel_write_register (self , unit : int , address : int , payload : int , register_data_type : str | None = None ) -> Any :
1441- if register_data_type == REGISTER_U16 :
1442- regs = convert_to_registers (int (payload ), DataType .UINT16 , self .plugin .order32 ) # type: ignore[attr-defined]
1443- else :
1444- regs = convert_to_registers (int (payload ), DataType .INT16 , self .plugin .order32 ) # type: ignore[attr-defined]
14451481 try :
1482+ regs = self ._encode_write_value (payload , register_data_type , single_register = True )
14461483 response = await self ._async_transport_write (
14471484 unit = unit ,
14481485 address = address ,
@@ -1479,6 +1516,8 @@ async def async_write_register(self, unit: int, address: int, payload: int, regi
14791516 payload ,
14801517 register_data_type = register_data_type ,
14811518 )
1519+ except RegisterEncodingError :
1520+ raise
14821521 except HomeAssistantError as ex :
14831522 self .writequeue [(unit , address )] = request
14841523 if self .wakeupButton :
@@ -1515,10 +1554,7 @@ async def async_write_registers_single(
15151554 self , unit : int , address : int , payload : int , register_data_type : str | None = None
15161555 ) -> Any : # Needs adapting for register queue
15171556 """Write registers multi, but write only one register of type 16bit"""
1518- if register_data_type == REGISTER_U16 :
1519- regs = convert_to_registers (int (payload ), DataType .UINT16 , self .plugin .order32 ) # type: ignore[attr-defined]
1520- else :
1521- regs = convert_to_registers (int (payload ), DataType .INT16 , self .plugin .order32 ) # type: ignore[attr-defined]
1557+ regs = self ._encode_write_value (payload , register_data_type , single_register = True )
15221558 return await self ._async_transport_write (
15231559 unit = unit ,
15241560 address = address ,
0 commit comments