diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index f08abf5fd4a..568ea8ece32 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -24,6 +24,8 @@ ATTR_DEVICE = 'device' ATTR_NUM_REPEATS = 'num_repeats' ATTR_DELAY_SECS = 'delay_secs' ATTR_HOLD_SECS = 'hold_secs' +ATTR_ALTERNATIVE = 'alternative' +ATTR_TIMEOUT = 'timeout' DOMAIN = 'remote' SCAN_INTERVAL = timedelta(seconds=30) @@ -36,12 +38,15 @@ GROUP_NAME_ALL_REMOTES = 'all remotes' MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SERVICE_SEND_COMMAND = 'send_command' +SERVICE_LEARN_COMMAND = 'learn_command' SERVICE_SYNC = 'sync' DEFAULT_NUM_REPEATS = 1 DEFAULT_DELAY_SECS = 0.4 DEFAULT_HOLD_SECS = 0 +SUPPORT_LEARN_COMMAND = 1 + REMOTE_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) @@ -59,6 +64,13 @@ REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({ vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), }) +REMOTE_SERVICE_LEARN_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({ + vol.Optional(ATTR_DEVICE): cv.string, + vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ALTERNATIVE): cv.boolean, + vol.Optional(ATTR_TIMEOUT): cv.positive_int +}) + @bind_hass def is_on(hass, entity_id=None): @@ -93,12 +105,22 @@ async def async_setup(hass, config): 'async_send_command' ) + component.async_register_entity_service( + SERVICE_LEARN_COMMAND, REMOTE_SERVICE_LEARN_COMMAND_SCHEMA, + 'async_learn_command' + ) + return True class RemoteDevice(ToggleEntity): """Representation of a remote.""" + @property + def supported_features(self): + """Flag supported features.""" + return 0 + def send_command(self, command, **kwargs): """Send a command to a device.""" raise NotImplementedError() @@ -108,5 +130,17 @@ class RemoteDevice(ToggleEntity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(ft.partial( - self.send_command, command, **kwargs)) + return self.hass.async_add_executor_job( + ft.partial(self.send_command, command, **kwargs)) + + def learn_command(self, **kwargs): + """Learn a command from a device.""" + raise NotImplementedError() + + def async_learn_command(self, **kwargs): + """Learn a command from a device. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_executor_job( + ft.partial(self.learn_command, **kwargs)) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 62615f28714..a551ba18ed4 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -25,7 +25,7 @@ turn_off: example: 'remote.family_room' send_command: - description: Sends a single command to a single device. + description: Sends a command or a list of commands to a device. fields: entity_id: description: Name(s) of entities to send command from. @@ -46,6 +46,25 @@ send_command: description: An optional value that specifies that number of seconds you want to have it held before the release is send. If not specified, the release will be send immediately after the press. example: '2.5' +learn_command: + description: Learns a command or a list of commands from a device. + fields: + entity_id: + description: Name(s) of entities to learn command from. + example: 'remote.bedroom' + device: + description: Device ID to learn command from. + example: 'television' + command: + description: A single command or a list of commands to learn. + example: 'Turn on' + alternative: + description: If code must be stored as alternative (useful for discrete remotes). + example: 'True' + timeout: + description: Timeout, in seconds, for the command to be learned. + example: '30' + harmony_sync: description: Syncs the remote's configuration. diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py index c68e34ddc18..e5db98c381b 100644 --- a/tests/components/demo/test_remote.py +++ b/tests/components/demo/test_remote.py @@ -48,5 +48,8 @@ class TestDemoRemote(unittest.TestCase): common.send_command(self.hass, 'test', entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) - assert state.attributes == \ - {'friendly_name': 'Remote One', 'last_command_sent': 'test'} + assert state.attributes == { + 'friendly_name': 'Remote One', + 'last_command_sent': 'test', + 'supported_features': 0 + } diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py index d03cf5d6d16..30b158bca4b 100644 --- a/tests/components/remote/common.py +++ b/tests/components/remote/common.py @@ -4,8 +4,9 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ from homeassistant.components.remote import ( - ATTR_ACTIVITY, ATTR_COMMAND, ATTR_DELAY_SECS, ATTR_DEVICE, - ATTR_NUM_REPEATS, DOMAIN, SERVICE_SEND_COMMAND) + ATTR_ACTIVITY, ATTR_ALTERNATIVE, ATTR_COMMAND, ATTR_DELAY_SECS, + ATTR_DEVICE, ATTR_NUM_REPEATS, ATTR_TIMEOUT, DOMAIN, + SERVICE_LEARN_COMMAND, SERVICE_SEND_COMMAND) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) from homeassistant.loader import bind_hass @@ -53,3 +54,26 @@ def send_command(hass, command, entity_id=None, device=None, data[ATTR_DELAY_SECS] = delay_secs hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) + + +@bind_hass +def learn_command(hass, entity_id=None, device=None, command=None, + alternative=None, timeout=None): + """Learn a command from a device.""" + data = {} + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + if device: + data[ATTR_DEVICE] = device + + if command: + data[ATTR_COMMAND] = command + + if alternative: + data[ATTR_ALTERNATIVE] = alternative + + if timeout: + data[ATTR_TIMEOUT] = timeout + + hass.services.call(DOMAIN, SERVICE_LEARN_COMMAND, data) diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 2315dc1cf64..2d1419c66ae 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -13,6 +13,7 @@ from tests.components.remote import common TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}} SERVICE_SEND_COMMAND = 'send_command' +SERVICE_LEARN_COMMAND = 'learn_command' class TestRemote(unittest.TestCase): @@ -53,7 +54,7 @@ class TestRemote(unittest.TestCase): self.hass.block_till_done() - assert 1 == len(turn_on_calls) + assert len(turn_on_calls) == 1 call = turn_on_calls[-1] assert remote.DOMAIN == call.domain @@ -68,12 +69,12 @@ class TestRemote(unittest.TestCase): self.hass.block_till_done() - assert 1 == len(turn_off_calls) + assert len(turn_off_calls) == 1 call = turn_off_calls[-1] - assert remote.DOMAIN == call.domain - assert SERVICE_TURN_OFF == call.service - assert 'entity_id_val' == call.data[ATTR_ENTITY_ID] + assert call.domain == remote.DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data[ATTR_ENTITY_ID] == 'entity_id_val' def test_send_command(self): """Test send_command.""" @@ -87,9 +88,28 @@ class TestRemote(unittest.TestCase): self.hass.block_till_done() - assert 1 == len(send_command_calls) + assert len(send_command_calls) == 1 call = send_command_calls[-1] - assert remote.DOMAIN == call.domain - assert SERVICE_SEND_COMMAND == call.service - assert 'entity_id_val' == call.data[ATTR_ENTITY_ID] + assert call.domain == remote.DOMAIN + assert call.service == SERVICE_SEND_COMMAND + assert call.data[ATTR_ENTITY_ID] == 'entity_id_val' + + def test_learn_command(self): + """Test learn_command.""" + learn_command_calls = mock_service( + self.hass, remote.DOMAIN, SERVICE_LEARN_COMMAND) + + common.learn_command( + self.hass, entity_id='entity_id_val', + device='test_device', command=['test_command'], + alternative=True, timeout=20) + + self.hass.block_till_done() + + assert len(learn_command_calls) == 1 + call = learn_command_calls[-1] + + assert call.domain == remote.DOMAIN + assert call.service == SERVICE_LEARN_COMMAND + assert call.data[ATTR_ENTITY_ID] == 'entity_id_val'