Skip to content

Commit b19c039

Browse files
authored
Merge pull request #2222 from rosenrot00/fix/register-write-range-validation
Validate Modbus register ranges before writing
2 parents 19d30f8 + 85282ab commit b19c039

6 files changed

Lines changed: 264 additions & 85 deletions

File tree

custom_components/solax_modbus/__init__.py

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import importlib
77
import json
88
import logging
9+
import struct
910
import time as _mtime
1011
from dataclasses import dataclass, replace
1112
from datetime import timedelta
@@ -76,9 +77,11 @@
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+
495502
class 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,

custom_components/solax_modbus/const.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ class UnitOfReactivePower(StrEnum): # type: ignore[no-redef]
108108
REGISTER_WORDS = "_words" # nr or words must be specified in wordcount
109109
REGISTER_U8L = "_int8L"
110110
REGISTER_U8H = "_int8H"
111+
REGISTER_U16_MAX = 0xFFFF
112+
REGISTER_INT_RANGES: dict[str, tuple[int, int]] = {
113+
REGISTER_U16: (0, REGISTER_U16_MAX),
114+
REGISTER_S16: (-(1 << 15), (1 << 15) - 1),
115+
REGISTER_U32: (0, (1 << 32) - 1),
116+
REGISTER_S32: (-(1 << 31), (1 << 31) - 1),
117+
}
118+
REGISTER_TYPE_WORDS: dict[str, int] = {
119+
REGISTER_U16: 1,
120+
REGISTER_S16: 1,
121+
REGISTER_U32: 2,
122+
REGISTER_S32: 2,
123+
REGISTER_F32: 2,
124+
}
111125
WRITE_SINGLE_MODBUS = 1 # use write_single_modbus command
112126
WRITE_MULTISINGLE_MODBUS = 2 # use write_mutiple modbus command for single register
113127
WRITE_DATA_LOCAL = 3 # write only to local data storage (not persistent)

custom_components/solax_modbus/plugin_solis.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
REGISTER_S16,
3030
REGISTER_S32,
3131
REGISTER_U16,
32+
REGISTER_U16_MAX,
3233
REGISTER_U32,
3334
REGISTER_WORDS,
3435
SLEEPMODE_LASTAWAKE,
@@ -350,18 +351,7 @@ def value_function_pv4_power(initval: int, descr: Any, datadict: dict[str, Any])
350351

351352
# ================================= Number Declarations ============================================================
352353

353-
MAX_CURRENTS: list[tuple[str, int | float]] = [
354-
("0602", 62.5), # 3kW 48v
355-
("0102", 62.5), # 3kW 48v AC Only?
356-
("110F", 62.5), # 3.6kW 48v
357-
("160F3", 100), # 5kW 48v
358-
("160F4", 60), # 3.6kW 48v
359-
("160F5", 62.5), # 3.6kW 48v
360-
("1031", 100), # 5kW 48v
361-
("134F", 100), # 5kW 48v
362-
("6031", 100), # 6kW 48v
363-
("110C", 25), # 10kW HV
364-
]
354+
U16_CURRENT_MAX = REGISTER_U16_MAX * 0.1
365355

366356
NUMBER_TYPES = [
367357
###
@@ -862,13 +852,13 @@ def value_function_pv4_power(initval: int, descr: Any, datadict: dict[str, Any])
862852
register=43116,
863853
fmt="f",
864854
native_min_value=0,
865-
native_max_value=20,
855+
native_max_value=U16_CURRENT_MAX,
866856
native_step=1,
867857
scale=0.1,
868858
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
869859
device_class=NumberDeviceClass.CURRENT,
870860
allowedtypes=HYBRID,
871-
max_exceptions=MAX_CURRENTS,
861+
register_data_type=REGISTER_U16,
872862
entity_category=EntityCategory.CONFIG,
873863
),
874864
SolisModbusNumberEntityDescription(
@@ -877,13 +867,13 @@ def value_function_pv4_power(initval: int, descr: Any, datadict: dict[str, Any])
877867
register=43117,
878868
fmt="f",
879869
native_min_value=0,
880-
native_max_value=20,
870+
native_max_value=U16_CURRENT_MAX,
881871
native_step=1,
882872
scale=0.1,
883873
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
884874
device_class=NumberDeviceClass.CURRENT,
885875
allowedtypes=HYBRID,
886-
max_exceptions=MAX_CURRENTS,
876+
register_data_type=REGISTER_U16,
887877
entity_category=EntityCategory.CONFIG,
888878
),
889879
SolisModbusNumberEntityDescription(
@@ -892,13 +882,13 @@ def value_function_pv4_power(initval: int, descr: Any, datadict: dict[str, Any])
892882
register=43118,
893883
fmt="f",
894884
native_min_value=0,
895-
native_max_value=20,
885+
native_max_value=U16_CURRENT_MAX,
896886
native_step=1,
897887
scale=0.1,
898888
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
899889
device_class=NumberDeviceClass.CURRENT,
900890
allowedtypes=HYBRID,
901-
max_exceptions=MAX_CURRENTS,
891+
register_data_type=REGISTER_U16,
902892
entity_category=EntityCategory.CONFIG,
903893
),
904894
SolisModbusNumberEntityDescription(
@@ -907,13 +897,13 @@ def value_function_pv4_power(initval: int, descr: Any, datadict: dict[str, Any])
907897
register=43141,
908898
fmt="f",
909899
native_min_value=0,
910-
native_max_value=20,
900+
native_max_value=U16_CURRENT_MAX,
911901
native_step=1,
912902
scale=0.1,
913903
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
914904
device_class=NumberDeviceClass.CURRENT,
915905
allowedtypes=HYBRID,
916-
max_exceptions=MAX_CURRENTS,
906+
register_data_type=REGISTER_U16,
917907
entity_category=EntityCategory.CONFIG,
918908
),
919909
SolisModbusNumberEntityDescription(
@@ -922,13 +912,13 @@ def value_function_pv4_power(initval: int, descr: Any, datadict: dict[str, Any])
922912
register=43142,
923913
fmt="f",
924914
native_min_value=0,
925-
native_max_value=20,
915+
native_max_value=U16_CURRENT_MAX,
926916
native_step=1,
927917
scale=0.1,
928918
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
929919
device_class=NumberDeviceClass.CURRENT,
930920
allowedtypes=HYBRID,
931-
max_exceptions=MAX_CURRENTS,
921+
register_data_type=REGISTER_U16,
932922
entity_category=EntityCategory.CONFIG,
933923
),
934924
SolisModbusNumberEntityDescription(

0 commit comments

Comments
 (0)