From c4a5373976e9df201801a6da7ce76274225f0113 Mon Sep 17 00:00:00 2001 From: Johannes Wagner <7691102+joanwa@users.noreply.github.com> Date: Sun, 6 Aug 2023 13:47:54 +0200 Subject: [PATCH] Handle explicit Modbus NaN values (#90800) Co-authored-by: jan iversen --- homeassistant/components/modbus/__init__.py | 3 +++ .../components/modbus/base_platform.py | 13 +++++++++++-- homeassistant/components/modbus/const.py | 1 + homeassistant/components/modbus/validators.py | 14 ++++++++++++++ tests/components/modbus/test_init.py | 18 ++++++++++++++++++ tests/components/modbus/test_sensor.py | 10 ++++++++++ 6 files changed, 57 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index d9e81b74ce9..0108c37a10b 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -82,6 +82,7 @@ from .const import ( # noqa: F401 CONF_MIN_TEMP, CONF_MIN_VALUE, CONF_MSG_WAIT, + CONF_NAN_VALUE, CONF_PARITY, CONF_PRECISION, CONF_RETRIES, @@ -123,6 +124,7 @@ from .modbus import ModbusHub, async_modbus_setup from .validators import ( duplicate_entity_validator, duplicate_modbus_validator, + nan_validator, number_validator, scan_interval_validator, struct_validator, @@ -298,6 +300,7 @@ SENSOR_SCHEMA = vol.All( 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_NAN_VALUE): nan_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 c936773bea7..343d5a36b26 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_UNIQUE_ID, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -46,6 +47,7 @@ from .const import ( CONF_LAZY_ERROR, CONF_MAX_VALUE, CONF_MIN_VALUE, + CONF_NAN_VALUE, CONF_PRECISION, CONF_SCALE, CONF_STATE_OFF, @@ -101,6 +103,7 @@ class BasePlatform(Entity): self._min_value = get_optional_numeric_config(CONF_MIN_VALUE) self._max_value = get_optional_numeric_config(CONF_MAX_VALUE) + self._nan_value = entry.get(CONF_NAN_VALUE, None) self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS) @abstractmethod @@ -173,8 +176,10 @@ 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.""" + def __process_raw_value(self, entry: float | int | str) -> float | int | str: + """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" + if self._nan_value and entry in (self._nan_value, -self._nan_value): + return STATE_UNAVAILABLE val: float | int = self._scale * entry + self._offset if self._min_value is not None and val < self._min_value: return self._min_value @@ -225,6 +230,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): # the conversion only when it's absolutely necessary. if isinstance(val_result, int) and self._precision == 0: return str(val_result) + if isinstance(val_result, str): + if val_result == "nan": + val_result = STATE_UNAVAILABLE # pragma: no cover + return val_result return f"{float(val_result):.{self._precision}f}" diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 264268f323e..3b565e91f92 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -30,6 +30,7 @@ CONF_MAX_VALUE = "max_value" CONF_MIN_TEMP = "min_temp" CONF_MIN_VALUE = "min_value" CONF_MSG_WAIT = "message_wait_milliseconds" +CONF_NAN_VALUE = "nan_value" CONF_PARITY = "parity" CONF_REGISTER = "register" CONF_REGISTER_TYPE = "register_type" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index a583b93ea80..ee9d40dd874 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -139,6 +139,20 @@ def number_validator(value: Any) -> int | float: raise vol.Invalid(f"invalid number {value}") from err +def nan_validator(value: Any) -> int: + """Convert nan string to number (can be hex string or int).""" + if isinstance(value, int): + return value + try: + return int(value) + except (TypeError, ValueError): + pass + try: + return int(value, 16) + except (TypeError, ValueError) as err: + raise vol.Invalid(f"invalid number {value}") from err + + def scan_interval_validator(config: dict) -> dict: """Control scan_interval.""" for hub in config: diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 2daf722bb05..d9d3b035c94 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -64,6 +64,7 @@ from homeassistant.components.modbus.const import ( from homeassistant.components.modbus.validators import ( duplicate_entity_validator, duplicate_modbus_validator, + nan_validator, number_validator, struct_validator, ) @@ -141,6 +142,23 @@ async def test_number_validator() -> None: pytest.fail("Number_validator not throwing exception") +async def test_nan_validator() -> None: + """Test number validator.""" + + for value, value_type in ( + (15, int), + ("15", int), + ("abcdef", int), + ("0xabcdef", int), + ): + assert isinstance(nan_validator(value), value_type) + + with pytest.raises(vol.Invalid): + nan_validator("x15") + with pytest.raises(vol.Invalid): + nan_validator("not a hex string") + + @pytest.mark.parametrize( "do_config", [ diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index be9ea95d86a..48a081ef637 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.modbus.const import ( CONF_LAZY_ERROR, CONF_MAX_VALUE, CONF_MIN_VALUE, + CONF_NAN_VALUE, CONF_PRECISION, CONF_SCALE, CONF_SLAVE_COUNT, @@ -558,6 +559,15 @@ async def test_config_wrong_struct_sensor( False, str(int(0x02010404)), ), + ( + { + CONF_DATA_TYPE: DataType.INT32, + CONF_NAN_VALUE: "0x80000000", + }, + [0x8000, 0x0000], + False, + STATE_UNAVAILABLE, + ), ( { CONF_DATA_TYPE: DataType.INT32,