diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 24555704ac0..043d7375ae3 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -78,7 +78,9 @@ from .const import ( # noqa: F401 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, @@ -104,6 +106,7 @@ from .const import ( # noqa: F401 CONF_TARGET_TEMP, CONF_VERIFY, CONF_WRITE_TYPE, + CONF_ZERO_SUPPRESS, DEFAULT_HUB, DEFAULT_SCAN_INTERVAL, DEFAULT_TEMP_UNIT, @@ -288,6 +291,9 @@ SENSOR_SCHEMA = vol.All( 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, } ), ) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 9463847a4ee..337919c81f7 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -44,6 +44,8 @@ from .const import ( CONF_DATA_TYPE, CONF_INPUT_TYPE, CONF_LAZY_ERROR, + CONF_MAX_VALUE, + CONF_MIN_VALUE, CONF_PRECISION, CONF_SCALE, CONF_STATE_OFF, @@ -54,6 +56,7 @@ from .const import ( CONF_SWAP_WORD_BYTE, CONF_VERIFY, CONF_WRITE_TYPE, + CONF_ZERO_SUPPRESS, SIGNAL_START_ENTITY, SIGNAL_STOP_ENTITY, DataType, @@ -92,6 +95,18 @@ class BasePlatform(Entity): 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.""" @@ -162,6 +177,17 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): 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.""" @@ -181,10 +207,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): # 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 @@ -195,8 +221,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): 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 diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 6ed52ae0544..b7fcfee9053 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -26,7 +26,9 @@ CONF_INPUTS = "inputs" 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" @@ -67,6 +69,7 @@ CONF_VERIFY = "verify" CONF_VERIFY_REGISTER = "verify_register" CONF_VERIFY_STATE = "verify_state" CONF_WRITE_TYPE = "write_type" +CONF_ZERO_SUPPRESS = "zero_suppress" RTUOVERTCP = "rtuovertcp" SERIAL = "serial" diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 4a6495d5b46..02f55b075fa 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -7,6 +7,8 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_INPUT_TYPE, CONF_LAZY_ERROR, + CONF_MAX_VALUE, + CONF_MIN_VALUE, CONF_PRECISION, CONF_SCALE, CONF_SLAVE_COUNT, @@ -15,6 +17,7 @@ from homeassistant.components.modbus.const import ( CONF_SWAP_NONE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_ZERO_SUPPRESS, MODBUS_DOMAIN, DataType, ) @@ -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,