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_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,
}
),

View file

@ -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}"

View file

@ -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"

View file

@ -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:

View file

@ -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",
[

View file

@ -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,