Handle explicit Modbus NaN values (#90800)

Co-authored-by: jan iversen <jancasacondor@gmail.com>
This commit is contained in:
Johannes Wagner 2023-08-06 13:47:54 +02:00 committed by GitHub
parent 0511071757
commit c4a5373976
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 57 additions and 2 deletions

View file

@ -82,6 +82,7 @@ from .const import ( # noqa: F401
CONF_MIN_TEMP, CONF_MIN_TEMP,
CONF_MIN_VALUE, CONF_MIN_VALUE,
CONF_MSG_WAIT, CONF_MSG_WAIT,
CONF_NAN_VALUE,
CONF_PARITY, CONF_PARITY,
CONF_PRECISION, CONF_PRECISION,
CONF_RETRIES, CONF_RETRIES,
@ -123,6 +124,7 @@ from .modbus import ModbusHub, async_modbus_setup
from .validators import ( from .validators import (
duplicate_entity_validator, duplicate_entity_validator,
duplicate_modbus_validator, duplicate_modbus_validator,
nan_validator,
number_validator, number_validator,
scan_interval_validator, scan_interval_validator,
struct_validator, struct_validator,
@ -298,6 +300,7 @@ SENSOR_SCHEMA = vol.All(
vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int, vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int,
vol.Optional(CONF_MIN_VALUE): number_validator, vol.Optional(CONF_MIN_VALUE): number_validator,
vol.Optional(CONF_MAX_VALUE): number_validator, vol.Optional(CONF_MAX_VALUE): number_validator,
vol.Optional(CONF_NAN_VALUE): nan_validator,
vol.Optional(CONF_ZERO_SUPPRESS): number_validator, vol.Optional(CONF_ZERO_SUPPRESS): number_validator,
} }
), ),

View file

@ -22,6 +22,7 @@ from homeassistant.const import (
CONF_STRUCTURE, CONF_STRUCTURE,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -46,6 +47,7 @@ from .const import (
CONF_LAZY_ERROR, CONF_LAZY_ERROR,
CONF_MAX_VALUE, CONF_MAX_VALUE,
CONF_MIN_VALUE, CONF_MIN_VALUE,
CONF_NAN_VALUE,
CONF_PRECISION, CONF_PRECISION,
CONF_SCALE, CONF_SCALE,
CONF_STATE_OFF, CONF_STATE_OFF,
@ -101,6 +103,7 @@ class BasePlatform(Entity):
self._min_value = get_optional_numeric_config(CONF_MIN_VALUE) self._min_value = get_optional_numeric_config(CONF_MIN_VALUE)
self._max_value = get_optional_numeric_config(CONF_MAX_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) self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS)
@abstractmethod @abstractmethod
@ -173,8 +176,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
registers.reverse() registers.reverse()
return registers return registers
def __process_raw_value(self, entry: float | int) -> float | int: def __process_raw_value(self, entry: float | int | str) -> float | int | str:
"""Process value from sensor with scaling, offset, min/max etc.""" """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 val: float | int = self._scale * entry + self._offset
if self._min_value is not None and val < self._min_value: if self._min_value is not None and val < self._min_value:
return self._min_value return self._min_value
@ -225,6 +230,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
# the conversion only when it's absolutely necessary. # the conversion only when it's absolutely necessary.
if isinstance(val_result, int) and self._precision == 0: if isinstance(val_result, int) and self._precision == 0:
return str(val_result) 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}" return f"{float(val_result):.{self._precision}f}"

View file

@ -30,6 +30,7 @@ CONF_MAX_VALUE = "max_value"
CONF_MIN_TEMP = "min_temp" CONF_MIN_TEMP = "min_temp"
CONF_MIN_VALUE = "min_value" CONF_MIN_VALUE = "min_value"
CONF_MSG_WAIT = "message_wait_milliseconds" CONF_MSG_WAIT = "message_wait_milliseconds"
CONF_NAN_VALUE = "nan_value"
CONF_PARITY = "parity" CONF_PARITY = "parity"
CONF_REGISTER = "register" CONF_REGISTER = "register"
CONF_REGISTER_TYPE = "register_type" CONF_REGISTER_TYPE = "register_type"

View file

@ -139,6 +139,20 @@ def number_validator(value: Any) -> int | float:
raise vol.Invalid(f"invalid number {value}") from err 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: def scan_interval_validator(config: dict) -> dict:
"""Control scan_interval.""" """Control scan_interval."""
for hub in config: for hub in config:

View file

@ -64,6 +64,7 @@ from homeassistant.components.modbus.const import (
from homeassistant.components.modbus.validators import ( from homeassistant.components.modbus.validators import (
duplicate_entity_validator, duplicate_entity_validator,
duplicate_modbus_validator, duplicate_modbus_validator,
nan_validator,
number_validator, number_validator,
struct_validator, struct_validator,
) )
@ -141,6 +142,23 @@ async def test_number_validator() -> None:
pytest.fail("Number_validator not throwing exception") 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( @pytest.mark.parametrize(
"do_config", "do_config",
[ [

View file

@ -9,6 +9,7 @@ from homeassistant.components.modbus.const import (
CONF_LAZY_ERROR, CONF_LAZY_ERROR,
CONF_MAX_VALUE, CONF_MAX_VALUE,
CONF_MIN_VALUE, CONF_MIN_VALUE,
CONF_NAN_VALUE,
CONF_PRECISION, CONF_PRECISION,
CONF_SCALE, CONF_SCALE,
CONF_SLAVE_COUNT, CONF_SLAVE_COUNT,
@ -558,6 +559,15 @@ async def test_config_wrong_struct_sensor(
False, False,
str(int(0x02010404)), str(int(0x02010404)),
), ),
(
{
CONF_DATA_TYPE: DataType.INT32,
CONF_NAN_VALUE: "0x80000000",
},
[0x8000, 0x0000],
False,
STATE_UNAVAILABLE,
),
( (
{ {
CONF_DATA_TYPE: DataType.INT32, CONF_DATA_TYPE: DataType.INT32,