Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions homeassistant/components/modbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@
CONF_INPUT_TYPE,
CONF_LAZY_ERROR,
CONF_MAX_TEMP,
CONF_MAX_VALUE,
CONF_MIN_TEMP,
CONF_MIN_VALUE,
CONF_MSG_WAIT,
CONF_PARITY,
CONF_PRECISION,
Expand All @@ -104,6 +106,7 @@
CONF_TARGET_TEMP,
CONF_VERIFY,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
DEFAULT_HUB,
DEFAULT_SCAN_INTERVAL,
DEFAULT_TEMP_UNIT,
Expand Down Expand Up @@ -285,6 +288,9 @@
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int,
vol.Optional(CONF_MIN_VALUE): number_validator,
vol.Optional(CONF_MAX_VALUE): number_validator,
vol.Optional(CONF_ZERO_SUPPRESS): number_validator,
}
),
)
Expand Down
34 changes: 30 additions & 4 deletions homeassistant/components/modbus/base_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
CONF_DATA_TYPE,
CONF_INPUT_TYPE,
CONF_LAZY_ERROR,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_PRECISION,
CONF_SCALE,
CONF_STATE_OFF,
Expand All @@ -54,6 +56,7 @@
CONF_SWAP_WORD_BYTE,
CONF_VERIFY,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
SIGNAL_START_ENTITY,
SIGNAL_STOP_ENTITY,
DataType,
Expand Down Expand Up @@ -92,6 +95,18 @@ def __init__(self, hub: ModbusHub, entry: dict[str, Any]) -> None:
self._lazy_error_count = entry[CONF_LAZY_ERROR]
self._lazy_errors = self._lazy_error_count

def get_optional_numeric_config(config_name: str) -> int | float | None:
if (val := entry.get(config_name)) is None:
return None
assert isinstance(
val, (float, int)
), f"Expected float or int but {config_name} was {type(val)}"
return val

self._min_value = get_optional_numeric_config(CONF_MIN_VALUE)
self._max_value = get_optional_numeric_config(CONF_MAX_VALUE)
self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS)

@abstractmethod
async def async_update(self, now: datetime | None = None) -> None:
"""Virtual function to be overwritten."""
Expand Down Expand Up @@ -162,6 +177,17 @@ def _swap_registers(self, registers: list[int]) -> list[int]:
registers.reverse()
return registers

def __process_raw_value(self, entry: float | int) -> float | int:
"""Process value from sensor with scaling, offset, min/max etc."""
val: float | int = self._scale * entry + self._offset
if self._min_value is not None and val < self._min_value:
return self._min_value
if self._max_value is not None and val > self._max_value:
return self._max_value
if self._zero_suppress is not None and abs(val) <= self._zero_suppress:
return 0
return val

def unpack_structure_result(self, registers: list[int]) -> str | None:
"""Convert registers to proper result."""

Expand All @@ -181,10 +207,10 @@ def unpack_structure_result(self, registers: list[int]) -> str | None:
# If unpack() returns a tuple greater than 1, don't try to process the value.
# Instead, return the values of unpack(...) separated by commas.
if len(val) > 1:
# Apply scale and precision to floats and ints
# Apply scale, precision, limits to floats and ints
v_result = []
for entry in val:
v_temp = self._scale * entry + self._offset
v_temp = self.__process_raw_value(entry)

# We could convert int to float, and the code would still work; however
# we lose some precision, and unit tests will fail. Therefore, we do
Expand All @@ -195,8 +221,8 @@ def unpack_structure_result(self, registers: list[int]) -> str | None:
v_result.append(f"{float(v_temp):.{self._precision}f}")
return ",".join(map(str, v_result))

# Apply scale and precision to floats and ints
val_result: float | int = self._scale * val[0] + self._offset
# Apply scale, precision, limits to floats and ints
val_result = self.__process_raw_value(val[0])

# We could convert int to float, and the code would still work; however
# we lose some precision, and unit tests will fail. Therefore, we do
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/modbus/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
CONF_INPUT_TYPE = "input_type"
CONF_LAZY_ERROR = "lazy_error_count"
CONF_MAX_TEMP = "max_temp"
CONF_MAX_VALUE = "max_value"
CONF_MIN_TEMP = "min_temp"
CONF_MIN_VALUE = "min_value"
CONF_MSG_WAIT = "message_wait_milliseconds"
CONF_PARITY = "parity"
CONF_REGISTER = "register"
Expand Down Expand Up @@ -67,6 +69,7 @@
CONF_VERIFY_REGISTER = "verify_register"
CONF_VERIFY_STATE = "verify_state"
CONF_WRITE_TYPE = "write_type"
CONF_ZERO_SUPPRESS = "zero_suppress"

RTUOVERTCP = "rtuovertcp"
SERIAL = "serial"
Expand Down
39 changes: 39 additions & 0 deletions tests/components/modbus/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
CONF_DATA_TYPE,
CONF_INPUT_TYPE,
CONF_LAZY_ERROR,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_PRECISION,
CONF_SCALE,
CONF_SLAVE_COUNT,
Expand All @@ -15,6 +17,7 @@
CONF_SWAP_NONE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
CONF_ZERO_SUPPRESS,
MODBUS_DOMAIN,
DataType,
)
Expand Down Expand Up @@ -535,6 +538,42 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl
False,
str(int(0x04030201)),
),
(
{
CONF_DATA_TYPE: DataType.INT32,
CONF_MAX_VALUE: int(0x02010400),
},
[0x0201, 0x0403],
False,
str(int(0x02010400)),
),
(
{
CONF_DATA_TYPE: DataType.INT32,
CONF_MIN_VALUE: int(0x02010404),
},
[0x0201, 0x0403],
False,
str(int(0x02010404)),
),
(
{
CONF_DATA_TYPE: DataType.INT32,
CONF_ZERO_SUPPRESS: int(0x00000001),
},
[0x0000, 0x0002],
False,
str(int(0x00000002)),
),
(
{
CONF_DATA_TYPE: DataType.INT32,
CONF_ZERO_SUPPRESS: int(0x00000002),
},
[0x0000, 0x0002],
False,
str(int(0)),
),
(
{
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
Expand Down