Allow multiread in modbus binary_sensor (#59886)
This commit is contained in:
parent
0f580af1d3
commit
cb877adb6a
5 changed files with 203 additions and 13 deletions
|
@ -722,6 +722,7 @@ omit =
|
||||||
homeassistant/components/mjpeg/util.py
|
homeassistant/components/mjpeg/util.py
|
||||||
homeassistant/components/mochad/*
|
homeassistant/components/mochad/*
|
||||||
homeassistant/components/modbus/climate.py
|
homeassistant/components/modbus/climate.py
|
||||||
|
homeassistant/components/modbus/binary_sensor.py
|
||||||
homeassistant/components/modem_callerid/sensor.py
|
homeassistant/components/modem_callerid/sensor.py
|
||||||
homeassistant/components/moehlenhoff_alpha2/__init__.py
|
homeassistant/components/moehlenhoff_alpha2/__init__.py
|
||||||
homeassistant/components/moehlenhoff_alpha2/climate.py
|
homeassistant/components/moehlenhoff_alpha2/climate.py
|
||||||
|
|
|
@ -74,6 +74,7 @@ from .const import (
|
||||||
CONF_RETRY_ON_EMPTY,
|
CONF_RETRY_ON_EMPTY,
|
||||||
CONF_REVERSE_ORDER,
|
CONF_REVERSE_ORDER,
|
||||||
CONF_SCALE,
|
CONF_SCALE,
|
||||||
|
CONF_SLAVE_COUNT,
|
||||||
CONF_STATE_CLOSED,
|
CONF_STATE_CLOSED,
|
||||||
CONF_STATE_CLOSING,
|
CONF_STATE_CLOSING,
|
||||||
CONF_STATE_OFF,
|
CONF_STATE_OFF,
|
||||||
|
@ -270,6 +271,7 @@ BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
|
||||||
vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
|
vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
|
||||||
[CALL_TYPE_COIL, CALL_TYPE_DISCRETE]
|
[CALL_TYPE_COIL, CALL_TYPE_DISCRETE]
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,31 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME, STATE_ON
|
from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant
|
CONF_BINARY_SENSORS,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_NAME,
|
||||||
|
STATE_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
from . import get_hub
|
from . import get_hub
|
||||||
from .base_platform import BasePlatform
|
from .base_platform import BasePlatform
|
||||||
|
from .const import CONF_SLAVE_COUNT
|
||||||
|
from .modbus import ModbusHub
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
@ -23,21 +38,51 @@ async def async_setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Modbus binary sensors."""
|
"""Set up the Modbus binary sensors."""
|
||||||
sensors = []
|
|
||||||
|
|
||||||
if discovery_info is None: # pragma: no cover
|
if discovery_info is None: # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
||||||
|
sensors: list[ModbusBinarySensor | SlaveSensor] = []
|
||||||
|
hub = get_hub(hass, discovery_info[CONF_NAME])
|
||||||
for entry in discovery_info[CONF_BINARY_SENSORS]:
|
for entry in discovery_info[CONF_BINARY_SENSORS]:
|
||||||
hub = get_hub(hass, discovery_info[CONF_NAME])
|
slave_count = entry.get(CONF_SLAVE_COUNT, 0)
|
||||||
sensors.append(ModbusBinarySensor(hub, entry))
|
sensor = ModbusBinarySensor(hub, entry, slave_count)
|
||||||
|
if slave_count > 0:
|
||||||
|
sensors.extend(await sensor.async_setup_slaves(hass, slave_count, entry))
|
||||||
|
sensors.append(sensor)
|
||||||
async_add_entities(sensors)
|
async_add_entities(sensors)
|
||||||
|
|
||||||
|
|
||||||
class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
|
class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
|
||||||
"""Modbus binary sensor."""
|
"""Modbus binary sensor."""
|
||||||
|
|
||||||
|
def __init__(self, hub: ModbusHub, entry: dict[str, Any], slave_count: int) -> None:
|
||||||
|
"""Initialize the Modbus binary sensor."""
|
||||||
|
self._count = slave_count + 1
|
||||||
|
self._coordinator: DataUpdateCoordinator[Any] | None = None
|
||||||
|
self._result = None
|
||||||
|
super().__init__(hub, entry)
|
||||||
|
|
||||||
|
async def async_setup_slaves(
|
||||||
|
self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any]
|
||||||
|
) -> list[SlaveSensor]:
|
||||||
|
"""Add slaves as needed (1 read for multiple sensors)."""
|
||||||
|
|
||||||
|
# Add a dataCoordinator for each sensor that have slaves
|
||||||
|
# this ensures that idx = bit position of value in result
|
||||||
|
# polling is done with the base class
|
||||||
|
name = self._attr_name if self._attr_name else "modbus_sensor"
|
||||||
|
self._coordinator = DataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
|
||||||
|
slaves: list[SlaveSensor] = []
|
||||||
|
for idx in range(0, slave_count):
|
||||||
|
slaves.append(SlaveSensor(self._coordinator, idx, entry))
|
||||||
|
return slaves
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Handle entity which will be added."""
|
"""Handle entity which will be added."""
|
||||||
await self.async_base_added_to_hass()
|
await self.async_base_added_to_hass()
|
||||||
|
@ -52,7 +97,7 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
|
||||||
return
|
return
|
||||||
self._call_active = True
|
self._call_active = True
|
||||||
result = await self._hub.async_pymodbus_call(
|
result = await self._hub.async_pymodbus_call(
|
||||||
self._slave, self._address, 1, self._input_type
|
self._slave, self._address, self._count, self._input_type
|
||||||
)
|
)
|
||||||
self._call_active = False
|
self._call_active = False
|
||||||
if result is None:
|
if result is None:
|
||||||
|
@ -61,10 +106,44 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
|
||||||
return
|
return
|
||||||
self._lazy_errors = self._lazy_error_count
|
self._lazy_errors = self._lazy_error_count
|
||||||
self._attr_available = False
|
self._attr_available = False
|
||||||
self.async_write_ha_state()
|
self._result = None
|
||||||
return
|
else:
|
||||||
|
self._lazy_errors = self._lazy_error_count
|
||||||
|
self._attr_is_on = result.bits[0] & 1
|
||||||
|
self._attr_available = True
|
||||||
|
self._result = result
|
||||||
|
|
||||||
self._lazy_errors = self._lazy_error_count
|
|
||||||
self._attr_is_on = result.bits[0] & 1
|
|
||||||
self._attr_available = True
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
if self._coordinator:
|
||||||
|
self._coordinator.async_set_updated_data(self._result)
|
||||||
|
|
||||||
|
|
||||||
|
class SlaveSensor(CoordinatorEntity, RestoreEntity, BinarySensorEntity):
|
||||||
|
"""Modbus slave binary sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Modbus binary sensor."""
|
||||||
|
idx += 1
|
||||||
|
self._attr_name = f"{entry[CONF_NAME]}_{idx}"
|
||||||
|
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
|
||||||
|
self._attr_available = False
|
||||||
|
self._result_inx = int(idx / 8)
|
||||||
|
self._result_bit = 2 ** (idx % 8)
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
if state := await self.async_get_last_state():
|
||||||
|
self._attr_is_on = state.state == STATE_ON
|
||||||
|
self.async_write_ha_state()
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
result = self.coordinator.data
|
||||||
|
if result:
|
||||||
|
self._attr_is_on = result.bits[self._result_inx] & self._result_bit
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
|
@ -37,6 +37,7 @@ CONF_RETRY_ON_EMPTY = "retry_on_empty"
|
||||||
CONF_REVERSE_ORDER = "reverse_order"
|
CONF_REVERSE_ORDER = "reverse_order"
|
||||||
CONF_PRECISION = "precision"
|
CONF_PRECISION = "precision"
|
||||||
CONF_SCALE = "scale"
|
CONF_SCALE = "scale"
|
||||||
|
CONF_SLAVE_COUNT = "slave_count"
|
||||||
CONF_STATE_CLOSED = "state_closed"
|
CONF_STATE_CLOSED = "state_closed"
|
||||||
CONF_STATE_CLOSING = "state_closing"
|
CONF_STATE_CLOSING = "state_closing"
|
||||||
CONF_STATE_OFF = "state_off"
|
CONF_STATE_OFF = "state_off"
|
||||||
|
|
|
@ -7,6 +7,7 @@ from homeassistant.components.modbus.const import (
|
||||||
CALL_TYPE_DISCRETE,
|
CALL_TYPE_DISCRETE,
|
||||||
CONF_INPUT_TYPE,
|
CONF_INPUT_TYPE,
|
||||||
CONF_LAZY_ERROR,
|
CONF_LAZY_ERROR,
|
||||||
|
CONF_SLAVE_COUNT,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
|
@ -188,9 +189,17 @@ async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha):
|
||||||
assert hass.states.get(ENTITY_ID).state == STATE_ON
|
assert hass.states.get(ENTITY_ID).state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
ENTITY_ID2 = f"{ENTITY_ID}_1"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"mock_test_state",
|
"mock_test_state",
|
||||||
[(State(ENTITY_ID, STATE_ON),)],
|
[
|
||||||
|
(
|
||||||
|
State(ENTITY_ID, STATE_ON),
|
||||||
|
State(ENTITY_ID2, STATE_OFF),
|
||||||
|
)
|
||||||
|
],
|
||||||
indirect=True,
|
indirect=True,
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -202,6 +211,7 @@ async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha):
|
||||||
CONF_NAME: TEST_ENTITY_NAME,
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
CONF_ADDRESS: 51,
|
CONF_ADDRESS: 51,
|
||||||
CONF_SCAN_INTERVAL: 0,
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_SLAVE_COUNT: 1,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -210,3 +220,100 @@ async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha):
|
||||||
async def test_restore_state_binary_sensor(hass, mock_test_state, mock_modbus):
|
async def test_restore_state_binary_sensor(hass, mock_test_state, mock_modbus):
|
||||||
"""Run test for binary sensor restore state."""
|
"""Run test for binary sensor restore state."""
|
||||||
assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state
|
assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state
|
||||||
|
assert hass.states.get(ENTITY_ID2).state == mock_test_state[1].state
|
||||||
|
|
||||||
|
|
||||||
|
TEST_NAME = "test_sensor"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"do_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
CONF_BINARY_SENSORS: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_ADDRESS: 51,
|
||||||
|
CONF_SLAVE_COUNT: 3,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_config_slave_binary_sensor(hass, mock_modbus):
|
||||||
|
"""Run config test for binary sensor."""
|
||||||
|
assert SENSOR_DOMAIN in hass.config.components
|
||||||
|
|
||||||
|
for addon in ["", "_1", "_2", "_3"]:
|
||||||
|
entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}{addon}"
|
||||||
|
assert hass.states.get(entity_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"do_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
CONF_BINARY_SENSORS: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_ADDRESS: 51,
|
||||||
|
CONF_SLAVE_COUNT: 8,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"register_words,expected, slaves",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[0x01, 0x00],
|
||||||
|
STATE_ON,
|
||||||
|
[
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0x02, 0x00],
|
||||||
|
STATE_OFF,
|
||||||
|
[
|
||||||
|
STATE_ON,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[0x01, 0x01],
|
||||||
|
STATE_ON,
|
||||||
|
[
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_slave_binary_sensor(hass, expected, slaves, mock_do_cycle):
|
||||||
|
"""Run test for given config."""
|
||||||
|
assert hass.states.get(ENTITY_ID).state == expected
|
||||||
|
|
||||||
|
for i in range(8):
|
||||||
|
entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}"
|
||||||
|
assert hass.states.get(entity_id).state == slaves[i]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue