From d4c5a93b638356686d003321919a1639a4c6f4e6 Mon Sep 17 00:00:00 2001
From: FredericMa <FredericMa@users.noreply.github.com>
Date: Mon, 13 Nov 2023 12:24:20 +0100
Subject: [PATCH] Add Risco communication delay (#101349)

* Add optional communication delay in case a panel responds slow.

* Add migration test

* Connect by increasing the communication delay

* Update init to use option instead of config

* Updated strings

* Fix migration and tests

* Review changes

* Update connect strategy

* Update tests

* Changes after review

* Change typing from object to Any.

* Add test to validate communication delay update.

* Move test to separate file

* Change filename.
---
 homeassistant/components/risco/__init__.py    | 36 ++++++++++++++-----
 homeassistant/components/risco/config_flow.py | 32 ++++++++++++++---
 homeassistant/components/risco/const.py       |  3 ++
 homeassistant/components/risco/manifest.json  |  2 +-
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 tests/components/risco/conftest.py            | 10 ++++++
 tests/components/risco/test_config_flow.py    |  7 ++--
 tests/components/risco/test_init.py           | 21 +++++++++++
 9 files changed, 96 insertions(+), 19 deletions(-)
 create mode 100644 tests/components/risco/test_init.py

diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py
index 88f8ba9bdfa..9c62447ee04 100644
--- a/homeassistant/components/risco/__init__.py
+++ b/homeassistant/components/risco/__init__.py
@@ -35,10 +35,12 @@ from homeassistant.helpers.storage import Store
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
 from .const import (
+    CONF_COMMUNICATION_DELAY,
     DATA_COORDINATOR,
     DEFAULT_SCAN_INTERVAL,
     DOMAIN,
     EVENTS_COORDINATOR,
+    MAX_COMMUNICATION_DELAY,
     TYPE_LOCAL,
 )
 
@@ -81,15 +83,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
 async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     data = entry.data
-    risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
-
-    try:
-        await risco.connect()
-    except CannotConnectError as error:
-        raise ConfigEntryNotReady() from error
-    except UnauthorizedError:
-        _LOGGER.exception("Failed to login to Risco cloud")
-        return False
+    comm_delay = initial_delay = data.get(CONF_COMMUNICATION_DELAY, 0)
+
+    while True:
+        risco = RiscoLocal(
+            data[CONF_HOST],
+            data[CONF_PORT],
+            data[CONF_PIN],
+            communication_delay=comm_delay,
+        )
+        try:
+            await risco.connect()
+        except CannotConnectError as error:
+            if comm_delay >= MAX_COMMUNICATION_DELAY:
+                raise ConfigEntryNotReady() from error
+            comm_delay += 1
+        except UnauthorizedError:
+            _LOGGER.exception("Failed to login to Risco cloud")
+            return False
+        else:
+            break
+
+    if comm_delay > initial_delay:
+        new_data = data.copy()
+        new_data[CONF_COMMUNICATION_DELAY] = comm_delay
+        hass.config_entries.async_update_entry(entry, data=new_data)
 
     async def _error(error: Exception) -> None:
         _LOGGER.error("Error in Risco library: %s", error)
diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py
index 0f532a376a1..ef96714742d 100644
--- a/homeassistant/components/risco/config_flow.py
+++ b/homeassistant/components/risco/config_flow.py
@@ -28,10 +28,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from .const import (
     CONF_CODE_ARM_REQUIRED,
     CONF_CODE_DISARM_REQUIRED,
+    CONF_COMMUNICATION_DELAY,
     CONF_HA_STATES_TO_RISCO,
     CONF_RISCO_STATES_TO_HA,
     DEFAULT_OPTIONS,
     DOMAIN,
+    MAX_COMMUNICATION_DELAY,
     RISCO_STATES,
     TYPE_LOCAL,
 )
@@ -78,16 +80,31 @@ async def validate_cloud_input(hass: core.HomeAssistant, data) -> dict[str, str]
 
 async def validate_local_input(
     hass: core.HomeAssistant, data: Mapping[str, str]
-) -> dict[str, str]:
+) -> dict[str, Any]:
     """Validate the user input allows us to connect to a local panel.
 
     Data has the keys from LOCAL_SCHEMA with values provided by the user.
     """
-    risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
-    await risco.connect()
+    comm_delay = 0
+    while True:
+        risco = RiscoLocal(
+            data[CONF_HOST],
+            data[CONF_PORT],
+            data[CONF_PIN],
+            communication_delay=comm_delay,
+        )
+        try:
+            await risco.connect()
+        except CannotConnectError as e:
+            if comm_delay >= MAX_COMMUNICATION_DELAY:
+                raise e
+            comm_delay += 1
+        else:
+            break
+
     site_id = risco.id
     await risco.disconnect()
-    return {"title": site_id}
+    return {"title": site_id, "comm_delay": comm_delay}
 
 
 class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -170,7 +187,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
                 self._abort_if_unique_id_configured()
 
                 return self.async_create_entry(
-                    title=info["title"], data={**user_input, **{CONF_TYPE: TYPE_LOCAL}}
+                    title=info["title"],
+                    data={
+                        **user_input,
+                        **{CONF_TYPE: TYPE_LOCAL},
+                        **{CONF_COMMUNICATION_DELAY: info["comm_delay"]},
+                    },
                 )
 
         return self.async_show_form(
diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py
index 9f0e71701c6..800003d2384 100644
--- a/homeassistant/components/risco/const.py
+++ b/homeassistant/components/risco/const.py
@@ -17,10 +17,13 @@ DEFAULT_SCAN_INTERVAL = 30
 
 TYPE_LOCAL = "local"
 
+MAX_COMMUNICATION_DELAY = 3
+
 CONF_CODE_ARM_REQUIRED = "code_arm_required"
 CONF_CODE_DISARM_REQUIRED = "code_disarm_required"
 CONF_RISCO_STATES_TO_HA = "risco_states_to_ha"
 CONF_HA_STATES_TO_RISCO = "ha_states_to_risco"
+CONF_COMMUNICATION_DELAY = "communication_delay"
 
 RISCO_GROUPS = ["A", "B", "C", "D"]
 RISCO_ARM = "arm"
diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json
index 5b208d1fc18..ca28af3d8e5 100644
--- a/homeassistant/components/risco/manifest.json
+++ b/homeassistant/components/risco/manifest.json
@@ -7,5 +7,5 @@
   "iot_class": "local_push",
   "loggers": ["pyrisco"],
   "quality_scale": "platinum",
-  "requirements": ["pyrisco==0.5.7"]
+  "requirements": ["pyrisco==0.5.8"]
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index 3cb6eaff697..3a2758e3a9a 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2001,7 +2001,7 @@ pyrecswitch==1.0.2
 pyrepetierng==0.1.0
 
 # homeassistant.components.risco
-pyrisco==0.5.7
+pyrisco==0.5.8
 
 # homeassistant.components.rituals_perfume_genie
 pyrituals==0.0.6
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 5ece8f95a86..aa187d8c886 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1508,7 +1508,7 @@ pyqwikswitch==0.93
 pyrainbird==4.0.0
 
 # homeassistant.components.risco
-pyrisco==0.5.7
+pyrisco==0.5.8
 
 # homeassistant.components.rituals_perfume_genie
 pyrituals==0.0.6
diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py
index cb3b3dd929e..325e787bb4f 100644
--- a/tests/components/risco/conftest.py
+++ b/tests/components/risco/conftest.py
@@ -171,6 +171,16 @@ def connect_with_error(exception):
         yield
 
 
+@pytest.fixture
+def connect_with_single_error(exception):
+    """Fixture to simulate error on connect."""
+    with patch(
+        "homeassistant.components.risco.RiscoLocal.connect",
+        side_effect=[exception, None],
+    ):
+        yield
+
+
 @pytest.fixture
 async def setup_risco_local(hass, local_config_entry):
     """Set up a local Risco integration for testing."""
diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py
index 5a9b60ed130..fdb51c65dda 100644
--- a/tests/components/risco/test_config_flow.py
+++ b/tests/components/risco/test_config_flow.py
@@ -9,7 +9,7 @@ from homeassistant.components.risco.config_flow import (
     CannotConnectError,
     UnauthorizedError,
 )
-from homeassistant.components.risco.const import DOMAIN
+from homeassistant.components.risco.const import CONF_COMMUNICATION_DELAY, DOMAIN
 from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
 from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResultType
@@ -246,7 +246,10 @@ async def test_local_form(hass: HomeAssistant) -> None:
         )
         await hass.async_block_till_done()
 
-    expected_data = {**TEST_LOCAL_DATA, **{"type": "local"}}
+    expected_data = {
+        **TEST_LOCAL_DATA,
+        **{"type": "local", CONF_COMMUNICATION_DELAY: 0},
+    }
     assert result3["type"] == FlowResultType.CREATE_ENTRY
     assert result3["title"] == TEST_SITE_NAME
     assert result3["data"] == expected_data
diff --git a/tests/components/risco/test_init.py b/tests/components/risco/test_init.py
new file mode 100644
index 00000000000..a1a9e3bd6a7
--- /dev/null
+++ b/tests/components/risco/test_init.py
@@ -0,0 +1,21 @@
+"""Tests for the Risco initialization."""
+import pytest
+
+from homeassistant.components.risco import CannotConnectError
+from homeassistant.components.risco.const import CONF_COMMUNICATION_DELAY
+from homeassistant.core import HomeAssistant
+
+
+@pytest.mark.parametrize("exception", [CannotConnectError])
+async def test_single_error_on_connect(
+    hass: HomeAssistant, connect_with_single_error, local_config_entry
+) -> None:
+    """Test single error on connect to validate communication delay update from 0 (default) to 1."""
+    expected_data = {
+        **local_config_entry.data,
+        **{"type": "local", CONF_COMMUNICATION_DELAY: 1},
+    }
+
+    await hass.config_entries.async_setup(local_config_entry.entry_id)
+    await hass.async_block_till_done()
+    assert local_config_entry.data == expected_data
-- 
GitLab