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.pull/50512/head^2
parent
e0de6752af
commit
29eb31e9da
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue