mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 14:57:09 +00:00
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
This commit is contained in:
parent
b1fa178df4
commit
2d33ee6258
@ -9,12 +9,14 @@ from collections import defaultdict
|
|||||||
import functools as ft
|
import functools as ft
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP,
|
ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_UNKNOWN)
|
STATE_UNKNOWN)
|
||||||
from homeassistant.core import CoreState, callback
|
from homeassistant.core import CoreState, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
@ -39,6 +41,7 @@ DATA_DEVICE_REGISTER = 'rflink_device_register'
|
|||||||
DATA_ENTITY_LOOKUP = 'rflink_entity_lookup'
|
DATA_ENTITY_LOOKUP = 'rflink_entity_lookup'
|
||||||
DEFAULT_RECONNECT_INTERVAL = 10
|
DEFAULT_RECONNECT_INTERVAL = 10
|
||||||
DEFAULT_SIGNAL_REPETITIONS = 1
|
DEFAULT_SIGNAL_REPETITIONS = 1
|
||||||
|
CONNECTION_TIMEOUT = 10
|
||||||
|
|
||||||
EVENT_BUTTON_PRESSED = 'button_pressed'
|
EVENT_BUTTON_PRESSED = 'button_pressed'
|
||||||
EVENT_KEY_COMMAND = 'command'
|
EVENT_KEY_COMMAND = 'command'
|
||||||
@ -148,7 +151,10 @@ def async_setup(hass, config):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def connect():
|
def connect():
|
||||||
"""Set up connection and hook it into HA for reconnect/shutdown."""
|
"""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
|
# Rflink create_rflink_connection decides based on the value of host
|
||||||
# (string or None) if serial or tcp mode should be used
|
# (string or None) if serial or tcp mode should be used
|
||||||
@ -164,13 +170,19 @@ def async_setup(hass, config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
transport, protocol = yield from connection
|
with async_timeout.timeout(CONNECTION_TIMEOUT,
|
||||||
|
loop=hass.loop):
|
||||||
|
transport, protocol = yield from connection
|
||||||
|
|
||||||
except (serial.serialutil.SerialException, ConnectionRefusedError,
|
except (serial.serialutil.SerialException, ConnectionRefusedError,
|
||||||
TimeoutError) as exc:
|
TimeoutError, OSError, asyncio.TimeoutError) as exc:
|
||||||
reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL]
|
reconnect_interval = config[DOMAIN][CONF_RECONNECT_INTERVAL]
|
||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"Error connecting to Rflink, reconnecting in %s",
|
"Error connecting to Rflink, reconnecting in %s",
|
||||||
reconnect_interval)
|
reconnect_interval)
|
||||||
|
hass.states.async_set(
|
||||||
|
'{domain}.connection_status'.format(
|
||||||
|
domain=DOMAIN), 'error')
|
||||||
hass.loop.call_later(reconnect_interval, reconnect, exc)
|
hass.loop.call_later(reconnect_interval, reconnect, exc)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -182,9 +194,12 @@ def async_setup(hass, config):
|
|||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||||
lambda x: transport.close())
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -279,6 +294,8 @@ class RflinkCommand(RflinkDevice):
|
|||||||
# are sent
|
# are sent
|
||||||
_repetition_task = None
|
_repetition_task = None
|
||||||
|
|
||||||
|
_protocol = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_rflink_protocol(cls, protocol, wait_ack=None):
|
def set_rflink_protocol(cls, protocol, wait_ack=None):
|
||||||
"""Set the Rflink asyncio protocol as a class variable."""
|
"""Set the Rflink asyncio protocol as a class variable."""
|
||||||
@ -286,6 +303,11 @@ class RflinkCommand(RflinkDevice):
|
|||||||
if wait_ack is not None:
|
if wait_ack is not None:
|
||||||
cls._wait_ack = wait_ack
|
cls._wait_ack = wait_ack
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_connected(cls):
|
||||||
|
"""Return connection status."""
|
||||||
|
return bool(cls._protocol)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def _async_handle_command(self, command, *args):
|
def _async_handle_command(self, command, *args):
|
||||||
"""Do bookkeeping for command, send it to rflink and update state."""
|
"""Do bookkeeping for command, send it to rflink and update state."""
|
||||||
@ -329,6 +351,9 @@ class RflinkCommand(RflinkDevice):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Sending command: %s to Rflink device: %s", cmd, self._device_id)
|
"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:
|
if self._wait_ack:
|
||||||
# Puts command on outgoing buffer then waits for Rflink to confirm
|
# Puts command on outgoing buffer then waits for Rflink to confirm
|
||||||
# the command has been send out in the ether.
|
# the command has been send out in the ether.
|
||||||
@ -359,12 +384,10 @@ class SwitchableRflinkDevice(RflinkCommand):
|
|||||||
elif command == 'off':
|
elif command == 'off':
|
||||||
self._state = False
|
self._state = False
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_turn_on(self, **kwargs):
|
def async_turn_on(self, **kwargs):
|
||||||
"""Turn the device on."""
|
"""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):
|
def async_turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
yield from self._async_handle_command('turn_off')
|
return self._async_handle_command("turn_off")
|
||||||
|
@ -176,3 +176,44 @@ def test_reconnecting_after_failure(hass, monkeypatch):
|
|||||||
|
|
||||||
# we expect 3 calls, the initial and 2 reconnects
|
# we expect 3 calls, the initial and 2 reconnects
|
||||||
assert mock_create.call_count == 3
|
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'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user