From 2ec295a6f8bdfa8d01fef2eb9baa7835160204b0 Mon Sep 17 00:00:00 2001
From: Johan Bloemberg <github@ijohan.nl>
Date: Sat, 16 Jun 2018 00:26:48 +0200
Subject: [PATCH] Add availability to Rflink entities. (#14977)

---
 homeassistant/components/rflink.py     | 31 ++++++++++++++++++++
 tests/components/sensor/test_rflink.py | 40 +++++++++++++++++++++++++-
 2 files changed, 70 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py
index 87e2a7a2331..272a5b868ec 100644
--- a/homeassistant/components/rflink.py
+++ b/homeassistant/components/rflink.py
@@ -20,6 +20,8 @@ from homeassistant.exceptions import HomeAssistantError
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.deprecation import get_deprecated
 from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.dispatcher import (
+    async_dispatcher_send, async_dispatcher_connect)
 
 
 REQUIREMENTS = ['rflink==0.0.37']
@@ -65,6 +67,8 @@ DOMAIN = 'rflink'
 
 SERVICE_SEND_COMMAND = 'send_command'
 
+SIGNAL_AVAILABILITY = 'rflink_device_available'
+
 DEVICE_DEFAULTS_SCHEMA = vol.Schema({
     vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
     vol.Optional(CONF_SIGNAL_REPETITIONS,
@@ -185,6 +189,8 @@ def async_setup(hass, config):
         # Reset protocol binding before starting reconnect
         RflinkCommand.set_rflink_protocol(None)
 
+        async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
+
         # If HA is not stopping, initiate new connection
         if hass.state != CoreState.stopping:
             _LOGGER.warning('disconnected from Rflink, reconnecting')
@@ -219,9 +225,16 @@ def async_setup(hass, config):
             _LOGGER.exception(
                 "Error connecting to Rflink, reconnecting in %s",
                 reconnect_interval)
+            # Connection to Rflink device is lost, make entities unavailable
+            async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
+
             hass.loop.call_later(reconnect_interval, reconnect, exc)
             return
 
+        # There is a valid connection to a Rflink device now so
+        # mark entities as available
+        async_dispatcher_send(hass, SIGNAL_AVAILABILITY, True)
+
         # Bind protocol to command class to allow entities to send commands
         RflinkCommand.set_rflink_protocol(
             protocol, config[DOMAIN][CONF_WAIT_FOR_ACK])
@@ -244,6 +257,7 @@ class RflinkDevice(Entity):
 
     platform = None
     _state = STATE_UNKNOWN
+    _available = True
 
     def __init__(self, device_id, hass, name=None, aliases=None, group=True,
                  group_aliases=None, nogroup_aliases=None, fire_event=False,
@@ -305,6 +319,23 @@ class RflinkDevice(Entity):
         """Assume device state until first device event sets state."""
         return self._state is STATE_UNKNOWN
 
+    @property
+    def available(self):
+        """Return True if entity is available."""
+        return self._available
+
+    @callback
+    def set_availability(self, availability):
+        """Update availability state."""
+        self._available = availability
+        self.async_schedule_update_ha_state()
+
+    @asyncio.coroutine
+    def async_added_to_hass(self):
+        """Register update callback."""
+        async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY,
+                                 self.set_availability)
+
 
 class RflinkCommand(RflinkDevice):
     """Singleton class to make Rflink command interface available to entities.
diff --git a/tests/components/sensor/test_rflink.py b/tests/components/sensor/test_rflink.py
index a99d14cc735..a250a75ab99 100644
--- a/tests/components/sensor/test_rflink.py
+++ b/tests/components/sensor/test_rflink.py
@@ -8,6 +8,9 @@ automatic sensor creation.
 import asyncio
 
 from ..test_rflink import mock_rflink
+from homeassistant.components.rflink import (
+    CONF_RECONNECT_INTERVAL)
+from homeassistant.const import STATE_UNKNOWN
 
 DOMAIN = 'sensor'
 
@@ -32,7 +35,7 @@ CONFIG = {
 def test_default_setup(hass, monkeypatch):
     """Test all basic functionality of the rflink sensor component."""
     # setup mocking rflink module
-    event_callback, create, _, _ = yield from mock_rflink(
+    event_callback, create, _, disconnect_callback = yield from mock_rflink(
         hass, CONFIG, DOMAIN, monkeypatch)
 
     # make sure arguments are passed
@@ -100,3 +103,38 @@ def test_disable_automatic_add(hass, monkeypatch):
 
     # make sure new device is not added
     assert not hass.states.get('sensor.test2')
+
+
+@asyncio.coroutine
+def test_entity_availability(hass, monkeypatch):
+    """If Rflink device is disconnected, entities should become unavailable."""
+    # Make sure Rflink mock does not 'recover' to quickly from the
+    # disconnect or else the unavailability cannot be measured
+    config = CONFIG
+    failures = [True, True]
+    config[CONF_RECONNECT_INTERVAL] = 60
+
+    # Create platform and entities
+    event_callback, create, _, disconnect_callback = yield from mock_rflink(
+        hass, config, DOMAIN, monkeypatch, failures=failures)
+
+    # Entities are available by default
+    assert hass.states.get('sensor.test').state == STATE_UNKNOWN
+
+    # Mock a disconnect of the Rflink device
+    disconnect_callback()
+
+    # Wait for dispatch events to propagate
+    yield from hass.async_block_till_done()
+
+    # Entity should be unavailable
+    assert hass.states.get('sensor.test').state == 'unavailable'
+
+    # Reconnect the Rflink device
+    disconnect_callback()
+
+    # Wait for dispatch events to propagate
+    yield from hass.async_block_till_done()
+
+    # Entities should be available again
+    assert hass.states.get('sensor.test').state == STATE_UNKNOWN
-- 
GitLab