diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 2cdce927cd8..fe3e954c571 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -8,11 +8,13 @@ import asyncio from collections import defaultdict import functools as ft import logging +import os import async_timeout +from homeassistant.config import load_yaml_config_file from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN) + ATTR_ENTITY_ID, CONF_COMMAND, 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 @@ -35,6 +37,7 @@ CONF_GROUP = 'group' CONF_NOGROUP_ALIASES = 'nogroup_aliases' CONF_NOGROUP_ALIASSES = 'nogroup_aliasses' CONF_DEVICE_DEFAULTS = 'device_defaults' +CONF_DEVICE_ID = 'device_id' CONF_DEVICES = 'devices' CONF_AUTOMATIC_ADD = 'automatic_add' CONF_FIRE_EVENT = 'fire_event' @@ -60,6 +63,8 @@ RFLINK_GROUP_COMMANDS = ['allon', 'alloff'] DOMAIN = 'rflink' +SERVICE_SEND_COMMAND = 'send_command' + DEVICE_DEFAULTS_SCHEMA = vol.Schema({ vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS, @@ -78,6 +83,11 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) +SEND_COMMAND_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_COMMAND): cv.string, +}) + def identify_event_type(event): """Look at event to determine type of device. @@ -111,6 +121,24 @@ def async_setup(hass, config): # Allow platform to specify function to register new unknown devices hass.data[DATA_DEVICE_REGISTER] = {} + @asyncio.coroutine + def async_send_command(call): + """Send Rflink command.""" + _LOGGER.debug('Rflink command for %s', str(call.data)) + if not (yield from RflinkCommand.send_command( + call.data.get(CONF_DEVICE_ID), + call.data.get(CONF_COMMAND))): + _LOGGER.error('Failed Rflink command for %s', str(call.data)) + + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml') + ) + + hass.services.async_register( + DOMAIN, SERVICE_SEND_COMMAND, async_send_command, + descriptions[DOMAIN][SERVICE_SEND_COMMAND], SEND_COMMAND_SCHEMA) + @callback def event_callback(event): """Handle incoming Rflink events. @@ -312,6 +340,12 @@ class RflinkCommand(RflinkDevice): """Return connection status.""" return bool(cls._protocol) + @classmethod + @asyncio.coroutine + def send_command(cls, device_id, action): + """Send device command to Rflink and wait for acknowledgement.""" + return (yield from cls._protocol.send_command_ack(device_id, action)) + @asyncio.coroutine def _async_handle_command(self, command, *args): """Do bookkeeping for command, send it to rflink and update state.""" diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 880a539927a..7315b6dc2d2 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -533,3 +533,16 @@ knx: data: description: KNX data to send example: 1 + +rflink: + send_command: + description: Send device command through RFLink + + fields: + device_id: + description: RFLink device ID + example: 'newkaku_0000c6c2_1' + + command: + description: The command to be sent + example: 'on' diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py index ce6b473b465..b4cdd96f817 100644 --- a/tests/components/test_rflink.py +++ b/tests/components/test_rflink.py @@ -4,13 +4,15 @@ import asyncio from unittest.mock import Mock from homeassistant.bootstrap import async_setup_component -from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL +from homeassistant.components.rflink import ( + CONF_RECONNECT_INTERVAL, SERVICE_SEND_COMMAND) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF from tests.common import assert_setup_component @asyncio.coroutine -def mock_rflink(hass, config, domain, monkeypatch, failures=None): +def mock_rflink(hass, config, domain, monkeypatch, failures=None, + platform_count=1): """Create mock Rflink asyncio protocol, test component setup.""" transport, protocol = (Mock(), Mock()) @@ -45,7 +47,7 @@ def mock_rflink(hass, config, domain, monkeypatch, failures=None): mock_create) # verify instanstiation of component with given config - with assert_setup_component(1, domain): + with assert_setup_component(platform_count, domain): yield from async_setup_component(hass, domain, config) # hook into mock config for injecting events @@ -117,6 +119,58 @@ def test_send_no_wait(hass, monkeypatch): assert protocol.send_command.call_args_list[0][0][1] == 'off' +@asyncio.coroutine +def test_send_command(hass, monkeypatch): + """Test send_command service.""" + domain = 'rflink' + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + } + + # setup mocking rflink module + _, _, protocol, _ = yield from mock_rflink( + hass, config, domain, monkeypatch, platform_count=5) + + hass.async_add_job( + hass.services.async_call(domain, SERVICE_SEND_COMMAND, + {'device_id': 'newkaku_0000c6c2_1', + 'command': 'on'})) + yield from hass.async_block_till_done() + assert (protocol.send_command_ack.call_args_list[0][0][0] + == 'newkaku_0000c6c2_1') + assert protocol.send_command_ack.call_args_list[0][0][1] == 'on' + + +@asyncio.coroutine +def test_send_command_invalid_arguments(hass, monkeypatch): + """Test send_command service.""" + domain = 'rflink' + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + } + + # setup mocking rflink module + _, _, protocol, _ = yield from mock_rflink( + hass, config, domain, monkeypatch, platform_count=5) + + # one argument missing + hass.async_add_job( + hass.services.async_call(domain, SERVICE_SEND_COMMAND, + {'command': 'on'})) + hass.async_add_job( + hass.services.async_call(domain, SERVICE_SEND_COMMAND, + {'device_id': 'newkaku_0000c6c2_1'})) + # no arguments + hass.async_add_job( + hass.services.async_call(domain, SERVICE_SEND_COMMAND, {})) + yield from hass.async_block_till_done() + assert protocol.send_command_ack.call_args_list == [] + + @asyncio.coroutine def test_reconnecting_after_disconnect(hass, monkeypatch): """An unexpected disconnect should cause a reconnect."""