Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove modbus pragma no cover and solve nan #99221

Merged
merged 7 commits into from
Sep 12, 2023
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
25 changes: 15 additions & 10 deletions homeassistant/components/modbus/base_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,14 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
registers.reverse()
return registers

def __process_raw_value(self, entry: float | int | str) -> float | int | str | None:
def __process_raw_value(
self, entry: float | int | str | bytes
) -> float | int | str | bytes | None:
"""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 None
if isinstance(entry, bytes):
return entry
val: float | int = self._scale * entry + self._offset
if self._min_value is not None and val < self._min_value:
return self._min_value
Expand Down Expand Up @@ -232,14 +236,20 @@ def unpack_structure_result(self, registers: list[int]) -> str | None:
if isinstance(v_temp, int) and self._precision == 0:
v_result.append(str(v_temp))
elif v_temp is None:
v_result.append("") # pragma: no cover
v_result.append("0")
elif v_temp != v_temp: # noqa: PLR0124
# NaN float detection replace with None
v_result.append("nan") # pragma: no cover
v_result.append("0")
else:
v_result.append(f"{float(v_temp):.{self._precision}f}")
return ",".join(map(str, v_result))

# NaN float detection replace with None
if val[0] != val[0]: # noqa: PLR0124
return None
if byte_string == b"nan\x00":
return None

# Apply scale, precision, limits to floats and ints
val_result = self.__process_raw_value(val[0])

Expand All @@ -249,15 +259,10 @@ def unpack_structure_result(self, registers: list[int]) -> str | None:

if val_result is None:
return None
# NaN float detection replace with None
if val_result != val_result: # noqa: PLR0124
return None # pragma: no cover
if isinstance(val_result, int) and self._precision == 0:
return str(val_result)
if isinstance(val_result, str):
if val_result == "nan":
val_result = None # pragma: no cover
return val_result
if isinstance(val_result, bytes):
return val_result.decode()
return f"{float(val_result):.{self._precision}f}"


Expand Down
119 changes: 116 additions & 3 deletions tests/components/modbus/test_sensor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""The tests for the Modbus sensor component."""
import struct

from freezegun.api import FrozenDateTimeFactory
import pytest

Expand Down Expand Up @@ -654,6 +656,21 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
@pytest.mark.parametrize(
("config_addon", "register_words", "do_exception", "expected"),
[
(
{
CONF_SLAVE_COUNT: 1,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.FLOAT32,
},
[
0x5102,
0x0304,
int.from_bytes(struct.pack(">f", float("nan"))[0:2]),
int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
],
False,
["34899771392", "0"],
),
(
{
CONF_SLAVE_COUNT: 0,
Expand Down Expand Up @@ -930,6 +947,65 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None:
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE


@pytest.mark.parametrize(
"do_config",
[
{
CONF_SENSORS: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 51,
CONF_SCAN_INTERVAL: 1,
},
],
},
],
)
@pytest.mark.parametrize(
("config_addon", "register_words", "expected"),
[
(
{
CONF_DATA_TYPE: DataType.FLOAT32,
},
[
int.from_bytes(struct.pack(">f", float("nan"))[0:2]),
int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
],
STATE_UNAVAILABLE,
),
(
{
CONF_DATA_TYPE: DataType.FLOAT32,
},
[0x6E61, 0x6E00],
STATE_UNAVAILABLE,
),
(
{
CONF_DATA_TYPE: DataType.CUSTOM,
CONF_COUNT: 2,
CONF_STRUCTURE: "4s",
},
[0x6E61, 0x6E00],
STATE_UNAVAILABLE,
),
(
{
CONF_DATA_TYPE: DataType.CUSTOM,
CONF_COUNT: 2,
CONF_STRUCTURE: "4s",
},
[0x6161, 0x6100],
"aaa\x00",
),
],
)
async def test_unpack_ok(hass: HomeAssistant, mock_do_cycle, expected) -> None:
"""Run test for sensor."""
assert hass.states.get(ENTITY_ID).state == expected


@pytest.mark.parametrize(
"do_config",
[
Expand Down Expand Up @@ -993,10 +1069,35 @@ async def test_lazy_error_sensor(
CONF_DATA_TYPE: DataType.CUSTOM,
CONF_STRUCTURE: ">4f",
},
# floats: 7.931250095367432, 10.600000381469727,
# floats: nan, 10.600000381469727,
# 1.000879611487865e-28, 10.566553115844727
[0x40FD, 0xCCCD, 0x4129, 0x999A, 0x10FD, 0xC0CD, 0x4129, 0x109A],
"7.93,10.60,0.00,10.57",
[
int.from_bytes(struct.pack(">f", float("nan"))[0:2]),
int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
0x4129,
0x999A,
0x10FD,
0xC0CD,
0x4129,
0x109A,
],
"0,10.60,0.00,10.57",
),
(
{
CONF_COUNT: 4,
CONF_DATA_TYPE: DataType.CUSTOM,
CONF_STRUCTURE: ">2i",
CONF_NAN_VALUE: 0x0000000F,
},
# int: nan, 10,
[
0x0000,
0x000F,
0x0000,
0x000A,
],
"0,10",
),
(
{
Expand All @@ -1016,6 +1117,18 @@ async def test_lazy_error_sensor(
[0x0101],
"257",
),
(
{
CONF_COUNT: 8,
CONF_PRECISION: 2,
CONF_DATA_TYPE: DataType.CUSTOM,
CONF_STRUCTURE: ">4f",
},
# floats: 7.931250095367432, 10.600000381469727,
# 1.000879611487865e-28, 10.566553115844727
[0x40FD, 0xCCCD, 0x4129, 0x999A, 0x10FD, 0xC0CD, 0x4129, 0x109A],
"7.93,10.60,0.00,10.57",
),
],
)
async def test_struct_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
Expand Down