Skip to content

Commit

Permalink
Add support for learning new commands (home-assistant#23888)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
felipediel authored and alandtse committed Aug 6, 2019
1 parent a5b83cc commit 779cb70
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 16 deletions.
38 changes: 36 additions & 2 deletions homeassistant/components/remote/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
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)
Expand All @@ -36,12 +38,15 @@
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,
})
Expand All @@ -59,6 +64,13 @@
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):
Expand Down Expand Up @@ -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()
Expand All @@ -108,5 +130,17 @@ def async_send_command(self, command, **kwargs):
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))
21 changes: 20 additions & 1 deletion homeassistant/components/remote/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions tests/components/demo/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@ def test_methods(self):
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
}
28 changes: 26 additions & 2 deletions tests/components/remote/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
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
Expand Down Expand Up @@ -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)
38 changes: 29 additions & 9 deletions tests/components/remote/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}}
SERVICE_SEND_COMMAND = 'send_command'
SERVICE_LEARN_COMMAND = 'learn_command'


class TestRemote(unittest.TestCase):
Expand Down Expand Up @@ -53,7 +54,7 @@ def test_turn_on(self):

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
Expand All @@ -68,12 +69,12 @@ def test_turn_off(self):

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."""
Expand All @@ -87,9 +88,28 @@ def test_send_command(self):

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'

0 comments on commit 779cb70

Please sign in to comment.