Fix NaN values in Modbus slaves sensors (#139969)

* Fix NaN values in Modbus slaves sensors

* fixXbdraco
This commit is contained in:
Claudio Ruggeri - CR-Tech 2025-05-26 21:04:38 +02:00 committed by GitHub
parent 2dc2b0ffac
commit b667fb2728
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 71 additions and 28 deletions

View File

@ -285,10 +285,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity):
v_result = [] v_result = []
for entry in val: for entry in val:
v_temp = self.__process_raw_value(entry) v_temp = self.__process_raw_value(entry)
if v_temp is None: if self._data_type != DataType.CUSTOM:
v_result.append("0")
else:
v_result.append(str(v_temp)) v_result.append(str(v_temp))
else:
v_result.append(str(v_temp) if v_temp is not None else "0")
return ",".join(map(str, v_result)) return ",".join(map(str, v_result))
# Apply scale, precision, limits to floats and ints # Apply scale, precision, limits to floats and ints

View File

@ -73,7 +73,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
super().__init__(hass, hub, entry) super().__init__(hass, hub, entry)
if slave_count: if slave_count:
self._count = self._count * (slave_count + 1) self._count = self._count * (slave_count + 1)
self._coordinator: DataUpdateCoordinator[list[float] | None] | None = None self._coordinator: DataUpdateCoordinator[list[float | None] | None] | None = (
None
)
self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_state_class = entry.get(CONF_STATE_CLASS)
self._attr_device_class = entry.get(CONF_DEVICE_CLASS) self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
@ -120,37 +122,45 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity):
self._coordinator.async_set_updated_data(None) self._coordinator.async_set_updated_data(None)
self.async_write_ha_state() self.async_write_ha_state()
return return
self._attr_available = True
result = self.unpack_structure_result(raw_result.registers) result = self.unpack_structure_result(raw_result.registers)
if self._coordinator: if self._coordinator:
result_array: list[float | None] = []
if result: if result:
result_array = list( for i in result.split(","):
map( if i != "None":
float if not self._value_is_int else int, result_array.append(
result.split(","), float(i) if not self._value_is_int else int(i)
)
) )
else:
result_array.append(None)
self._attr_native_value = result_array[0] self._attr_native_value = result_array[0]
self._coordinator.async_set_updated_data(result_array) self._coordinator.async_set_updated_data(result_array)
else: else:
self._attr_native_value = None self._attr_native_value = None
self._coordinator.async_set_updated_data(None) result_array = (self._slave_count + 1) * [None]
self._coordinator.async_set_updated_data(result_array)
else: else:
self._attr_native_value = result self._attr_native_value = result
self._attr_available = self._attr_native_value is not None
self.async_write_ha_state() self.async_write_ha_state()
class SlaveSensor( class SlaveSensor(
CoordinatorEntity[DataUpdateCoordinator[list[float] | None]], CoordinatorEntity[DataUpdateCoordinator[list[float | None] | None]],
RestoreSensor, RestoreSensor,
SensorEntity, SensorEntity,
): ):
"""Modbus slave register sensor.""" """Modbus slave register sensor."""
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._attr_available
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator[list[float] | None], coordinator: DataUpdateCoordinator[list[float | None] | None],
idx: int, idx: int,
entry: dict[str, Any], entry: dict[str, Any],
) -> None: ) -> None:
@ -178,4 +188,5 @@ class SlaveSensor(
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
result = self.coordinator.data result = self.coordinator.data
self._attr_native_value = result[self._idx] if result else None self._attr_native_value = result[self._idx] if result else None
self._attr_available = result is not None
super()._handle_coordinator_update() super()._handle_coordinator_update()

View File

@ -428,7 +428,7 @@ async def test_config_wrong_struct_sensor(
}, },
[0x89AB], [0x89AB],
False, False,
STATE_UNAVAILABLE, STATE_UNKNOWN,
), ),
( (
{ {
@ -631,7 +631,7 @@ async def test_config_wrong_struct_sensor(
}, },
[0x8000, 0x0000], [0x8000, 0x0000],
False, False,
STATE_UNAVAILABLE, STATE_UNKNOWN,
), ),
( (
{ {
@ -742,7 +742,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
int.from_bytes(struct.pack(">f", float("nan"))[2:4]), int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
], ],
False, False,
["34899771392.0", "0.0"], ["34899771392.0", STATE_UNKNOWN],
), ),
( (
{ {
@ -757,7 +757,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
int.from_bytes(struct.pack(">f", float("nan"))[2:4]), int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
], ],
False, False,
["34899771392.0", "0.0"], ["34899771392.0", STATE_UNKNOWN],
), ),
( (
{ {
@ -802,7 +802,11 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
}, },
[0x0102, 0x0304, 0x0403, 0x0201, 0x0403], [0x0102, 0x0304, 0x0403, 0x0201, 0x0403],
False, False,
[STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNKNOWN], [
STATE_UNKNOWN,
STATE_UNKNOWN,
STATE_UNKNOWN,
],
), ),
( (
{ {
@ -857,7 +861,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
}, },
[0x0102, 0x0304, 0x0403, 0x0201], [0x0102, 0x0304, 0x0403, 0x0201],
True, True,
[STATE_UNAVAILABLE, STATE_UNKNOWN], [STATE_UNAVAILABLE, STATE_UNAVAILABLE],
), ),
( (
{ {
@ -866,7 +870,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
}, },
[0x0102, 0x0304, 0x0403, 0x0201], [0x0102, 0x0304, 0x0403, 0x0201],
True, True,
[STATE_UNAVAILABLE, STATE_UNKNOWN], [STATE_UNAVAILABLE, STATE_UNAVAILABLE],
), ),
( (
{ {
@ -875,7 +879,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
}, },
[], [],
False, False,
[STATE_UNAVAILABLE, STATE_UNKNOWN], [STATE_UNKNOWN, STATE_UNKNOWN],
), ),
( (
{ {
@ -884,7 +888,35 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
}, },
[], [],
False, False,
[STATE_UNAVAILABLE, STATE_UNKNOWN], [STATE_UNKNOWN, STATE_UNKNOWN],
),
(
{
CONF_VIRTUAL_COUNT: 4,
CONF_UNIQUE_ID: SLAVE_UNIQUE_ID,
CONF_DATA_TYPE: DataType.INT32,
CONF_NAN_VALUE: "0x800000",
},
[
0x0,
0x35,
0x0,
0x38,
0x80,
0x0,
0x80,
0x0,
0xFFFF,
0xFFF6,
],
False,
[
"53",
"56",
STATE_UNKNOWN,
STATE_UNKNOWN,
"-10",
],
), ),
], ],
) )
@ -1103,7 +1135,7 @@ async def test_virtual_swap_sensor(
) )
async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None: async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None:
"""Run test for sensor.""" """Run test for sensor."""
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert hass.states.get(ENTITY_ID).state == STATE_UNKNOWN
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1131,14 +1163,14 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None:
int.from_bytes(struct.pack(">f", float("nan"))[0:2]), int.from_bytes(struct.pack(">f", float("nan"))[0:2]),
int.from_bytes(struct.pack(">f", float("nan"))[2:4]), int.from_bytes(struct.pack(">f", float("nan"))[2:4]),
], ],
STATE_UNAVAILABLE, STATE_UNKNOWN,
), ),
( (
{ {
CONF_DATA_TYPE: DataType.FLOAT32, CONF_DATA_TYPE: DataType.FLOAT32,
}, },
[0x6E61, 0x6E00], [0x6E61, 0x6E00],
STATE_UNAVAILABLE, STATE_UNKNOWN,
), ),
( (
{ {
@ -1147,7 +1179,7 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None:
CONF_STRUCTURE: "4s", CONF_STRUCTURE: "4s",
}, },
[0x6E61, 0x6E00], [0x6E61, 0x6E00],
STATE_UNAVAILABLE, STATE_UNKNOWN,
), ),
( (
{ {