diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index e809430d2104e3ac7ee76c3bf3487bd2da325119..10ccf32068ff95d92f8169d517c8b1d1c2227403 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -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: - transport, protocol = yield from connection + 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") diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py index ad5e7f91b2f346ce0324f742d106f8d6e773bc04..555ec9372ba0dc51b1db6a787fb09ae4e79c7b81 100644 --- a/tests/components/test_rflink.py +++ b/tests/components/test_rflink.py @@ -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'