Reconnect robustness, expose connection state. (#5869)

* Reconnect robustness, expose connection state.

- Expose connection status as rflink.connection_status state.
- Handle alternative timeout scenario.
- Explicitly set a timeout for connections.
- Error when trying to send commands if disconnected.
- Do not block component setup on gateway connection.

* Don't use coroutine where none is needed.

* Test disconnected behaviour.

* Use proper conventions for task creation.

* Possibly fix test race condition?

* Update hass import style
pull/6074/head
Johan Bloemberg 2017-02-15 16:10:19 +01:00 committed by Pascal Vizeli
parent b1fa178df4
commit 2d33ee6258
2 changed files with 73 additions and 9 deletions

View File

@ -9,12 +9,14 @@ from collections import defaultdict
import functools as ft
import logging
import async_timeout
import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP,
STATE_UNKNOWN)
from homeassistant.core import CoreState, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@ -39,6 +41,7 @@ DATA_DEVICE_REGISTER = 'rflink_device_register'
DATA_ENTITY_LOOKUP = 'rflink_entity_lookup'
DEFAULT_RECONNECT_INTERVAL = 10
DEFAULT_SIGNAL_REPETITIONS = 1
CONNECTION_TIMEOUT = 10
EVENT_BUTTON_PRESSED = 'button_pressed'
EVENT_KEY_COMMAND = 'command'
@ -148,7 +151,10 @@ def async_setup(hass, config):
@asyncio.coroutine
def connect():
"""Set up connection and hook it into HA for reconnect/shutdown."""
_LOGGER.info("Initiating Rflink connection")
_LOGGER.info('Initiating Rflink connection')
hass.states.async_set(
'{domain}.connection_status'.format(
domain=DOMAIN), 'connecting')
# Rflink create_rflink_connection decides based on the value of host
# (string or None) if serial or tcp mode should be used
@ -164,13 +170,19 @@ def async_setup(hass, config):
)
try:
with async_timeout.timeout(CONNECTION_TIMEOUT,
loop=hass.loop):
transport, protocol = yield from connection
except (serial.serialutil.SerialException, ConnectionRefusedError,
TimeoutError) as exc:
TimeoutError, OSError, asyncio.TimeoutError) as exc:
reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL]
_LOGGER.exception(
"Error connecting to Rflink, reconnecting in %s",
reconnect_interval)
hass.states.async_set(
'{domain}.connection_status'.format(
domain=DOMAIN), 'error')
hass.loop.call_later(reconnect_interval, reconnect, exc)
return
@ -182,9 +194,12 @@ def async_setup(hass, config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
lambda x: transport.close())
_LOGGER.info("Connected to Rflink")
_LOGGER.info('Connected to Rflink')
hass.states.async_set(
'{domain}.connection_status'.format(
domain=DOMAIN), 'connected')
yield from connect()
hass.async_add_job(connect)
return True
@ -279,6 +294,8 @@ class RflinkCommand(RflinkDevice):
# are sent
_repetition_task = None
_protocol = None
@classmethod
def set_rflink_protocol(cls, protocol, wait_ack=None):
"""Set the Rflink asyncio protocol as a class variable."""
@ -286,6 +303,11 @@ class RflinkCommand(RflinkDevice):
if wait_ack is not None:
cls._wait_ack = wait_ack
@classmethod
def is_connected(cls):
"""Return connection status."""
return bool(cls._protocol)
@asyncio.coroutine
def _async_handle_command(self, command, *args):
"""Do bookkeeping for command, send it to rflink and update state."""
@ -329,6 +351,9 @@ class RflinkCommand(RflinkDevice):
_LOGGER.debug(
"Sending command: %s to Rflink device: %s", cmd, self._device_id)
if not self.is_connected():
raise HomeAssistantError('Cannot send command, not connected!')
if self._wait_ack:
# Puts command on outgoing buffer then waits for Rflink to confirm
# the command has been send out in the ether.
@ -359,12 +384,10 @@ class SwitchableRflinkDevice(RflinkCommand):
elif command == 'off':
self._state = False
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn the device on."""
yield from self._async_handle_command('turn_on')
return self._async_handle_command("turn_on")
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the device off."""
yield from self._async_handle_command('turn_off')
return self._async_handle_command("turn_off")

View File

@ -176,3 +176,44 @@ def test_reconnecting_after_failure(hass, monkeypatch):
# we expect 3 calls, the initial and 2 reconnects
assert mock_create.call_count == 3
@asyncio.coroutine
def test_error_when_not_connected(hass, monkeypatch):
"""Sending command should error when not connected."""
domain = 'switch'
config = {
'rflink': {
'port': '/dev/ttyABC0',
CONF_RECONNECT_INTERVAL: 0,
},
domain: {
'platform': 'rflink',
'devices': {
'protocol_0_0': {
'name': 'test',
'aliasses': ['test_alias_0_0'],
},
},
},
}
# success first time but fail second
failures = [False, True, False]
# setup mocking rflink module
_, mock_create, _, disconnect_callback = yield from mock_rflink(
hass, config, domain, monkeypatch, failures=failures)
assert hass.states.get('rflink.connection_status').state == 'connected'
# rflink initiated disconnect
disconnect_callback(None)
yield from asyncio.sleep(0, loop=hass.loop)
assert hass.states.get('rflink.connection_status').state == 'error'
success = yield from hass.services.async_call(
domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: 'switch.test'})
assert not success, 'changing state should not succeed when disconnected'