1
mirror of https://github.com/home-assistant/core synced 2024-09-25 00:41:32 +02:00

Add configurable delay between connect and first request in modbus (#50124)

* Activate startup delay.

* Add removal of call_later if HA is stopped.

This is unlikely to happen, but just security measure.

* Removing timing interval.

async_fire_time_changed() needs to be called twice, first time the delay is
ended and second time update() is executed.

* Variable naming.
This commit is contained in:
jan iversen 2021-05-08 13:28:35 +02:00 committed by GitHub
parent e0de6752af
commit 29eb31e9da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 155 additions and 7 deletions

View File

@ -22,6 +22,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.event import async_call_later
from .const import (
ATTR_ADDRESS,
@ -59,7 +60,7 @@ def modbus_setup(
# modbus needs to be activated before components are loaded
# to avoid a racing problem
hub_collect[conf_hub[CONF_NAME]].setup()
hub_collect[conf_hub[CONF_NAME]].setup(hass)
# load platforms
for component, conf_key in (
@ -131,13 +132,14 @@ class ModbusHub:
# generic configuration
self._client = None
self._cancel_listener = None
self._in_error = False
self._lock = threading.Lock()
self._config_name = client_config[CONF_NAME]
self._config_type = client_config[CONF_TYPE]
self._config_port = client_config[CONF_PORT]
self._config_timeout = client_config[CONF_TIMEOUT]
self._config_delay = 0
self._config_delay = client_config[CONF_DELAY]
Defaults.Timeout = 10
if self._config_type == "serial":
@ -150,10 +152,6 @@ class ModbusHub:
else:
# network configuration
self._config_host = client_config[CONF_HOST]
self._config_delay = client_config[CONF_DELAY]
if self._config_delay > 0:
_LOGGER.warning("Parameter delay is accepted but not used in this version")
@property
def name(self):
@ -168,7 +166,7 @@ class ModbusHub:
_LOGGER.error(log_text)
self._in_error = error_state
def setup(self):
def setup(self, hass):
"""Set up pymodbus client."""
try:
if self._config_type == "serial":
@ -208,8 +206,22 @@ class ModbusHub:
# Connect device
self.connect()
# Start counting down to allow modbus requests.
if self._config_delay:
self._cancel_listener = async_call_later(
hass, self._config_delay, self.end_delay
)
def end_delay(self, args):
"""End startup delay."""
self._cancel_listener = None
self._config_delay = 0
def close(self):
"""Disconnect client."""
if self._cancel_listener:
self._cancel_listener()
self._cancel_listener = None
with self._lock:
try:
if self._client:
@ -230,6 +242,8 @@ class ModbusHub:
def read_coils(self, unit, address, count):
"""Read coils."""
if self._config_delay:
return None
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
@ -245,6 +259,8 @@ class ModbusHub:
def read_discrete_inputs(self, unit, address, count):
"""Read discrete inputs."""
if self._config_delay:
return None
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
@ -259,6 +275,8 @@ class ModbusHub:
def read_input_registers(self, unit, address, count):
"""Read input registers."""
if self._config_delay:
return None
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
@ -273,6 +291,8 @@ class ModbusHub:
def read_holding_registers(self, unit, address, count):
"""Read holding registers."""
if self._config_delay:
return None
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
@ -287,6 +307,8 @@ class ModbusHub:
def write_coil(self, unit, address, value) -> bool:
"""Write coil."""
if self._config_delay:
return False
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
@ -301,6 +323,8 @@ class ModbusHub:
def write_coils(self, unit, address, values) -> bool:
"""Write coil."""
if self._config_delay:
return False
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
@ -315,6 +339,8 @@ class ModbusHub:
def write_register(self, unit, address, value) -> bool:
"""Write register."""
if self._config_delay:
return False
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
@ -329,6 +355,8 @@ class ModbusHub:
def write_registers(self, unit, address, values) -> bool:
"""Write registers."""
if self._config_delay:
return False
with self._lock:
kwargs = {"unit": unit} if unit else {}
try:

View File

@ -39,6 +39,7 @@ from homeassistant.const import (
CONF_METHOD,
CONF_NAME,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SENSORS,
CONF_TIMEOUT,
CONF_TYPE,
@ -160,6 +161,12 @@ async def _config_helper(hass, do_config, caplog):
CONF_TIMEOUT: 30,
CONF_DELAY: 10,
},
{
CONF_TYPE: "tcp",
CONF_HOST: "modbusTestHost",
CONF_PORT: 5501,
CONF_DELAY: 5,
},
],
)
async def test_config_modbus(hass, caplog, do_config, mock_pymodbus):
@ -467,3 +474,116 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus):
await hass.async_block_till_done()
assert len(caplog.records) == 1
assert caplog.records[0].levelname == "ERROR"
async def test_delay(hass, mock_pymodbus):
"""Run test for different read."""
# the purpose of this test is to test startup delay
# We "hijiack" binary_sensor and sensor in order
# to make a proper blackbox test.
config = {
DOMAIN: [
{
CONF_TYPE: "tcp",
CONF_HOST: "modbusTestHost",
CONF_PORT: 5501,
CONF_NAME: TEST_MODBUS_NAME,
CONF_DELAY: 15,
CONF_BINARY_SENSORS: [
{
CONF_INPUT_TYPE: CALL_TYPE_COIL,
CONF_NAME: f"{TEST_SENSOR_NAME}_2",
CONF_ADDRESS: 52,
CONF_SCAN_INTERVAL: 5,
},
{
CONF_INPUT_TYPE: CALL_TYPE_DISCRETE,
CONF_NAME: f"{TEST_SENSOR_NAME}_1",
CONF_ADDRESS: 51,
CONF_SCAN_INTERVAL: 5,
},
],
CONF_SENSORS: [
{
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_NAME: f"{TEST_SENSOR_NAME}_3",
CONF_ADDRESS: 53,
CONF_SCAN_INTERVAL: 5,
},
{
CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT,
CONF_NAME: f"{TEST_SENSOR_NAME}_4",
CONF_ADDRESS: 54,
CONF_SCAN_INTERVAL: 5,
},
],
}
]
}
mock_pymodbus.read_coils.return_value = ReadResult([0x01])
mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01])
mock_pymodbus.read_holding_registers.return_value = ReadResult([7])
mock_pymodbus.read_input_registers.return_value = ReadResult([7])
now = dt_util.utcnow()
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
assert await async_setup_component(hass, DOMAIN, config) is True
await hass.async_block_till_done()
now = now + timedelta(seconds=10)
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
# Check states
entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_1"
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_2"
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_3"
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_4"
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
mock_pymodbus.reset_mock()
data = {
ATTR_HUB: TEST_MODBUS_NAME,
ATTR_UNIT: 17,
ATTR_ADDRESS: 16,
ATTR_STATE: False,
}
await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True)
assert not mock_pymodbus.write_coil.called
await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True)
assert not mock_pymodbus.write_coil.called
data[ATTR_STATE] = [True, False, True]
await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True)
assert not mock_pymodbus.write_coils.called
del data[ATTR_STATE]
data[ATTR_VALUE] = 15
await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True)
assert not mock_pymodbus.write_register.called
data[ATTR_VALUE] = [1, 2, 3]
await hass.services.async_call(DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True)
assert not mock_pymodbus.write_registers.called
# 2 times fire_changed is needed to secure "normal" update is called.
now = now + timedelta(seconds=6)
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
now = now + timedelta(seconds=10)
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
async_fire_time_changed(hass, now)
await hass.async_block_till_done()
# Check states
entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_1"
assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE
entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_2"
assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE
entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_3"
assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE
entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}_4"
assert not hass.states.get(entity_id).state == STATE_UNAVAILABLE