Add support for learning new commands (#23888)

* Add support for learning new commands

This update creates a generic service in the 'remote' component to enable remote control platforms to learn new commands.

* Update __init__.py with the proposed changes

- Add 'supported_features' property and a constant related to the 'learn_command' functionality.
- Redefine 'async_learn_command' function as a coroutine.

* Update __init__.py

* Fix assertion error

Adding the 'supported_features' attribute generated an assertion error on the 'Demo Remote' platform. This update fixes this.

* Fix duplicated 'hass' object

This update fixes a typo that occurred at the last update.
This commit is contained in:
Felipe Martins Diel 2019-06-05 06:32:59 -03:00 committed by Pascal Vizeli
parent 408ae44bdd
commit 0ed9e185b2
5 changed files with 116 additions and 16 deletions

View File

@ -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))

View File

@ -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.

View File

@ -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
}

View File

@ -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)

View File

@ -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'