From 1da251860e1061224cdb72cb12f698d5e72d8172 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Sun, 21 Nov 2021 21:32:03 -0700
Subject: [PATCH] Fix bugs causing SimpliSafe entities to incorrectly show
 `unavailable` (#59955)

---
 .../components/simplisafe/__init__.py         | 61 ++++++++++++++-----
 .../simplisafe/alarm_control_panel.py         | 12 +++-
 homeassistant/components/simplisafe/lock.py   | 15 +++--
 .../components/simplisafe/manifest.json       |  2 +-
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 6 files changed, 66 insertions(+), 28 deletions(-)

diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py
index a773304896d..044bd3651bc 100644
--- a/homeassistant/components/simplisafe/__init__.py
+++ b/homeassistant/components/simplisafe/__init__.py
@@ -107,7 +107,7 @@ ATTR_TIMESTAMP = "timestamp"
 
 DEFAULT_ENTITY_MODEL = "alarm_control_panel"
 DEFAULT_ENTITY_NAME = "Alarm Control Panel"
-DEFAULT_REST_API_ERROR_COUNT = 2
+DEFAULT_ERROR_THRESHOLD = 2
 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
 DEFAULT_SOCKET_MIN_RETRY = 15
 
@@ -231,7 +231,9 @@ def _async_register_base_station(
     )
 
 
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+async def async_setup_entry(  # noqa: C901
+    hass: HomeAssistant, entry: ConfigEntry
+) -> bool:
     """Set up SimpliSafe as config entry."""
     _async_standardize_config_entry(hass, entry)
 
@@ -351,6 +353,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     ):
         async_register_admin_service(hass, DOMAIN, service, method, schema=schema)
 
+    current_options = {**entry.options}
+
+    async def async_reload_entry(_: HomeAssistant, updated_entry: ConfigEntry) -> None:
+        """Handle an options update.
+
+        This method will get called in two scenarios:
+          1. When SimpliSafeOptionsFlowHandler is initiated
+          2. When a new refresh token is saved to the config entry data
+
+        We only want #1 to trigger an actual reload.
+        """
+        nonlocal current_options
+        updated_options = {**updated_entry.options}
+
+        if updated_options == current_options:
+            return
+
+        await hass.config_entries.async_reload(entry.entry_id)
+
     entry.async_on_unload(entry.add_update_listener(async_reload_entry))
 
     return True
@@ -365,11 +386,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     return unload_ok
 
 
-async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
-    """Handle an options update."""
-    await hass.config_entries.async_reload(entry.entry_id)
-
-
 class SimpliSafe:
     """Define a SimpliSafe data object."""
 
@@ -564,7 +580,11 @@ class SimpliSafeEntity(CoordinatorEntity):
         assert simplisafe.coordinator
         super().__init__(simplisafe.coordinator)
 
-        self._rest_api_errors = 0
+        # SimpliSafe can incorrectly return an error state when there isn't any
+        # error. This can lead to entities having an unknown state frequently.
+        # To protect against that, we measure an error count for each entity and only
+        # mark the state as unavailable if we detect a few in a row:
+        self._error_count = 0
 
         if device:
             model = device.type.name
@@ -630,7 +650,7 @@ class SimpliSafeEntity(CoordinatorEntity):
             system_offline = False
 
         return (
-            self._rest_api_errors < DEFAULT_REST_API_ERROR_COUNT
+            self._error_count < DEFAULT_ERROR_THRESHOLD
             and self._online
             and not system_offline
         )
@@ -638,14 +658,10 @@ class SimpliSafeEntity(CoordinatorEntity):
     @callback
     def _handle_coordinator_update(self) -> None:
         """Update the entity with new REST API data."""
-        # SimpliSafe can incorrectly return an error state when there isn't any
-        # error. This can lead to the system having an unknown state frequently.
-        # To protect against that, we measure how many "error states" we receive
-        # and only alter the state if we detect a few in a row:
         if self.coordinator.last_update_success:
-            self._rest_api_errors = 0
+            self.async_reset_error_count()
         else:
-            self._rest_api_errors += 1
+            self.async_increment_error_count()
 
         self.async_update_from_rest_api()
         self.async_write_ha_state()
@@ -713,6 +729,21 @@ class SimpliSafeEntity(CoordinatorEntity):
 
         self.async_update_from_rest_api()
 
+    @callback
+    def async_increment_error_count(self) -> None:
+        """Increment this entity's error count."""
+        LOGGER.debug('Error for entity "%s" (total: %s)', self.name, self._error_count)
+        self._error_count += 1
+
+    @callback
+    def async_reset_error_count(self) -> None:
+        """Reset this entity's error count."""
+        if self._error_count == 0:
+            return
+
+        LOGGER.debug('Resetting error count for "%s"', self.name)
+        self._error_count = 0
+
     @callback
     def async_update_from_rest_api(self) -> None:
         """Update the entity when new data comes from the REST API."""
diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py
index 7a09db18b07..e610b9139b8 100644
--- a/homeassistant/components/simplisafe/alarm_control_panel.py
+++ b/homeassistant/components/simplisafe/alarm_control_panel.py
@@ -154,11 +154,14 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
         """Set the state based on the latest REST API data."""
         if self._system.alarm_going_off:
             self._attr_state = STATE_ALARM_TRIGGERED
+        elif self._system.state == SystemStates.ERROR:
+            self.async_increment_error_count()
         elif state := STATE_MAP_FROM_REST_API.get(self._system.state):
             self._attr_state = state
+            self.async_reset_error_count()
         else:
             LOGGER.error("Unknown system state (REST API): %s", self._system.state)
-            self._attr_state = None
+            self.async_increment_error_count()
 
     async def async_alarm_disarm(self, code: str | None = None) -> None:
         """Send disarm command."""
@@ -237,4 +240,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
         self._attr_changed_by = event.changed_by
         if TYPE_CHECKING:
             assert event.event_type
-        self._attr_state = STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type)
+        if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type):
+            self._attr_state = state
+            self.async_reset_error_count()
+        else:
+            LOGGER.error("Unknown alarm websocket event: %s", event.event_type)
+            self.async_increment_error_count()
diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py
index 12d06c1028a..15757fde2e9 100644
--- a/homeassistant/components/simplisafe/lock.py
+++ b/homeassistant/components/simplisafe/lock.py
@@ -6,12 +6,7 @@ from typing import TYPE_CHECKING, Any
 from simplipy.device.lock import Lock, LockStates
 from simplipy.errors import SimplipyError
 from simplipy.system.v3 import SystemV3
-from simplipy.websocket import (
-    EVENT_LOCK_ERROR,
-    EVENT_LOCK_LOCKED,
-    EVENT_LOCK_UNLOCKED,
-    WebsocketEvent,
-)
+from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, WebsocketEvent
 
 from homeassistant.components.lock import LockEntity
 from homeassistant.config_entries import ConfigEntry
@@ -25,7 +20,6 @@ ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
 ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery"
 
 STATE_MAP_FROM_WEBSOCKET_EVENT = {
-    EVENT_LOCK_ERROR: None,
     EVENT_LOCK_LOCKED: True,
     EVENT_LOCK_UNLOCKED: False,
 }
@@ -105,4 +99,9 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity):
         """Update the entity when new data comes from the websocket."""
         if TYPE_CHECKING:
             assert event.event_type
-        self._attr_is_locked = STATE_MAP_FROM_WEBSOCKET_EVENT[event.event_type]
+        if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type):
+            self._attr_is_locked = state
+            self.async_reset_error_count()
+        else:
+            LOGGER.error("Unknown lock websocket event: %s", event.event_type)
+            self.async_increment_error_count()
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index ecc4578d878..81cb5b7febc 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -3,7 +3,7 @@
   "name": "SimpliSafe",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/simplisafe",
-  "requirements": ["simplisafe-python==2021.11.0"],
+  "requirements": ["simplisafe-python==2021.11.2"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
   "dhcp": [
diff --git a/requirements_all.txt b/requirements_all.txt
index de2f298f927..08043ffa094 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2146,7 +2146,7 @@ simplehound==0.3
 simplepush==1.1.4
 
 # homeassistant.components.simplisafe
-simplisafe-python==2021.11.0
+simplisafe-python==2021.11.2
 
 # homeassistant.components.sisyphus
 sisyphus-control==3.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index eef41267bb7..f66a2594459 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1263,7 +1263,7 @@ sharkiqpy==0.1.8
 simplehound==0.3
 
 # homeassistant.components.simplisafe
-simplisafe-python==2021.11.0
+simplisafe-python==2021.11.2
 
 # homeassistant.components.slack
 slackclient==2.5.0
-- 
GitLab