diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 6568f552fa6..bafb4d01c3f 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -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: diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 1cabaeb11ff..99a8ba467f6 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -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