From de6c5a503be125606170f74156acad1443c4e7bf Mon Sep 17 00:00:00 2001
From: iandday <iandday@gmail.com>
Date: Thu, 1 Dec 2016 15:48:08 -0500
Subject: [PATCH] Remote Component and Harmony Platform (#4254)

* Initial Harmony device support, working current activity sensor and switch for each activity
TODO: add new device per hub to send device specific activity

 Changes to be committed:
	new file:   homeassistant/components/harmony.py
	new file:   homeassistant/components/sensor/harmony.py
	new file:   homeassistant/components/switch/harmony.py

* ready for beta, I think

 Changes to be committed:
	modified:   homeassistant/components/harmony.py
	modified:   homeassistant/components/sensor/harmony.py
	modified:   homeassistant/components/switch/harmony.py

*  Changes to be committed:
	modified:   homeassistant/components/harmony.py
	new file:   homeassistant/components/remote/__init__.py
	new file:   homeassistant/components/remote/harmony.py
	new file:   homeassistant/components/remote/services.yaml
	modified:   homeassistant/components/sensor/harmony.py
	modified:   homeassistant/components/switch/harmony.py
Implemented remote component and harmony platform

* streamlined harmony support

* typo

* Initial Harmony device support, working current activity sensor and switch for each activity
TODO: add new device per hub to send device specific activity

 Changes to be committed:
	new file:   homeassistant/components/harmony.py
	new file:   homeassistant/components/sensor/harmony.py
	new file:   homeassistant/components/switch/harmony.py

* ready for beta, I think

 Changes to be committed:
	modified:   homeassistant/components/harmony.py
	modified:   homeassistant/components/sensor/harmony.py
	modified:   homeassistant/components/switch/harmony.py

*  Changes to be committed:
	modified:   homeassistant/components/harmony.py
	new file:   homeassistant/components/remote/__init__.py
	new file:   homeassistant/components/remote/harmony.py
	new file:   homeassistant/components/remote/services.yaml
	modified:   homeassistant/components/sensor/harmony.py
	modified:   homeassistant/components/switch/harmony.py
Implemented remote component and harmony platform

* streamlined harmony support

* typo

* reworked token generation

* delete

* Initial Harmony device support, working current activity sensor and switch for each activity
TODO: add new device per hub to send device specific activity

 Changes to be committed:
	new file:   homeassistant/components/harmony.py
	new file:   homeassistant/components/sensor/harmony.py
	new file:   homeassistant/components/switch/harmony.py

* Initial Harmony device support, working current activity sensor and switch for each activity
TODO: add new device per hub to send device specific activity

 Changes to be committed:
	new file:   homeassistant/components/harmony.py
	new file:   homeassistant/components/sensor/harmony.py
	new file:   homeassistant/components/switch/harmony.py

* ready for beta, I think

 Changes to be committed:
	modified:   homeassistant/components/harmony.py
	modified:   homeassistant/components/sensor/harmony.py
	modified:   homeassistant/components/switch/harmony.py

* ready for beta, I think

 Changes to be committed:
	modified:   homeassistant/components/harmony.py
	modified:   homeassistant/components/sensor/harmony.py
	modified:   homeassistant/components/switch/harmony.py

*  Changes to be committed:
	modified:   homeassistant/components/harmony.py
	new file:   homeassistant/components/remote/__init__.py
	new file:   homeassistant/components/remote/harmony.py
	new file:   homeassistant/components/remote/services.yaml
	modified:   homeassistant/components/sensor/harmony.py
	modified:   homeassistant/components/switch/harmony.py
Implemented remote component and harmony platform

* streamlined harmony support

* typo

* reworked token generation

* delete

* readded after rebase

* cleaning up style errors

* modified .coveragerc

* moved import statements

* added more debug logging

* Added URL encoding of token received from Logitech

* Corrected import for python 3

* new pyharmony version

* new pyharmony version

* remote tests

* only write config file if not present or sync service is called

* more tests

* more tests

* bumped pyharmony version to work with new auth

* bumped pyharmony version to work with new auth

* style corrections

* harmony local auth and remote demo platform

* style fix

* PR refinements and permission issues

* forgot a blank line

* removed sync test from test_init

* removed sync test from test_init

* visual indent

* send_command test in demo platform
---
 .coveragerc                                   |   1 +
 homeassistant/components/remote/__init__.py   | 142 +++++++++++++
 homeassistant/components/remote/demo.py       |  57 +++++
 homeassistant/components/remote/harmony.py    | 198 ++++++++++++++++++
 homeassistant/components/remote/services.yaml |  42 ++++
 requirements_all.txt                          |   3 +
 tests/components/remote/__init__.py           |   1 +
 tests/components/remote/test_demo.py          | 117 +++++++++++
 tests/components/remote/test_init.py          |  99 +++++++++
 9 files changed, 660 insertions(+)
 create mode 100755 homeassistant/components/remote/__init__.py
 create mode 100644 homeassistant/components/remote/demo.py
 create mode 100755 homeassistant/components/remote/harmony.py
 create mode 100644 homeassistant/components/remote/services.yaml
 create mode 100755 tests/components/remote/__init__.py
 create mode 100755 tests/components/remote/test_demo.py
 create mode 100755 tests/components/remote/test_init.py

diff --git a/.coveragerc b/.coveragerc
index 37d412b63d4..d0967918a60 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -241,6 +241,7 @@ omit =
     homeassistant/components/notify/xmpp.py
     homeassistant/components/nuimo_controller.py
     homeassistant/components/openalpr.py
+    homeassistant/components/remote/harmony.py
     homeassistant/components/scene/hunterdouglas_powerview.py
     homeassistant/components/sensor/arest.py
     homeassistant/components/sensor/arwn.py
diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py
new file mode 100755
index 00000000000..57d816fd0c9
--- /dev/null
+++ b/homeassistant/components/remote/__init__.py
@@ -0,0 +1,142 @@
+"""
+Component to interface with universal remote control devices.
+
+For more details about this component, please refer to the documentation
+at https://home-assistant.io/components/remote/
+"""
+from datetime import timedelta
+import logging
+import os
+
+import voluptuous as vol
+from homeassistant.config import load_yaml_config_file
+from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity import ToggleEntity
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (
+    STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
+from homeassistant.components import group
+from homeassistant.helpers.config_validation import PLATFORM_SCHEMA  # noqa
+
+ATTR_DEVICE = 'device'
+ATTR_COMMAND = 'command'
+ATTR_ACTIVITY = 'activity'
+SERVICE_SEND_COMMAND = 'send_command'
+SERVICE_SYNC = 'sync'
+
+DOMAIN = 'remote'
+SCAN_INTERVAL = 30
+
+GROUP_NAME_ALL_REMOTES = 'all remotes'
+ENTITY_ID_ALL_REMOTES = group.ENTITY_ID_FORMAT.format('all_remotes')
+
+ENTITY_ID_FORMAT = DOMAIN + '.{}'
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+
+REMOTE_SERVICE_SCHEMA = vol.Schema({
+    vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+})
+
+REMOTE_SERVICE_TURN_ON_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
+    vol.Optional(ATTR_ACTIVITY): cv.string
+})
+
+REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
+    vol.Required(ATTR_DEVICE): cv.string,
+    vol.Required(ATTR_COMMAND): cv.string,
+})
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def is_on(hass, entity_id=None):
+    """Return if the remote is on based on the statemachine."""
+    entity_id = entity_id or ENTITY_ID_ALL_REMOTES
+    return hass.states.is_state(entity_id, STATE_ON)
+
+
+def turn_on(hass, activity=None, entity_id=None):
+    """Turn all or specified remote on."""
+    data = {ATTR_ACTIVITY: activity}
+    if entity_id:
+        data[ATTR_ENTITY_ID] = entity_id
+    hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
+
+
+def turn_off(hass, entity_id=None):
+    """Turn all or specified remote off."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
+
+
+def send_command(hass, device, command, entity_id=None):
+    """Send a command to a device."""
+    data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command}
+    if entity_id:
+        data[ATTR_ENTITY_ID] = entity_id
+    hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
+
+
+def sync(hass, entity_id=None):
+    """Sync remote device."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_SYNC, data)
+
+
+def setup(hass, config):
+    """Track states and offer events for remotes."""
+    component = EntityComponent(
+        _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES)
+    component.setup(config)
+
+    def handle_remote_service(service):
+        """Handle calls to the remote services."""
+        target_remotes = component.extract_from_service(service)
+
+        activity_id = service.data.get(ATTR_ACTIVITY)
+        device = service.data.get(ATTR_DEVICE)
+        command = service.data.get(ATTR_COMMAND)
+
+        for remote in target_remotes:
+            if service.service == SERVICE_TURN_ON:
+                remote.turn_on(activity=activity_id)
+            elif service.service == SERVICE_SEND_COMMAND:
+                remote.send_command(device=device, command=command)
+            elif service.service == SERVICE_SYNC:
+                remote.sync()
+            else:
+                remote.turn_off()
+
+            if remote.should_poll:
+                remote.update_ha_state(True)
+
+    descriptions = load_yaml_config_file(
+        os.path.join(os.path.dirname(__file__), 'services.yaml'))
+    hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_remote_service,
+                           descriptions.get(SERVICE_TURN_OFF),
+                           schema=REMOTE_SERVICE_SCHEMA)
+    hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_remote_service,
+                           descriptions.get(SERVICE_TURN_ON),
+                           schema=REMOTE_SERVICE_TURN_ON_SCHEMA)
+    hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, handle_remote_service,
+                           descriptions.get(SERVICE_SEND_COMMAND),
+                           schema=REMOTE_SERVICE_SEND_COMMAND_SCHEMA)
+
+    return True
+
+
+class RemoteDevice(ToggleEntity):
+    """Representation of a remote."""
+
+    def turn_on(self, **kwargs):
+        """Turn a device on with the remote."""
+        raise NotImplementedError()
+
+    def turn_off(self, **kwargs):
+        """Turn a device off with the remote."""
+        raise NotImplementedError()
+
+    def send_command(self, **kwargs):
+        """Send a command to a device."""
+        raise NotImplementedError()
diff --git a/homeassistant/components/remote/demo.py b/homeassistant/components/remote/demo.py
new file mode 100644
index 00000000000..90c691a3d3c
--- /dev/null
+++ b/homeassistant/components/remote/demo.py
@@ -0,0 +1,57 @@
+"""
+Demo platform that has two fake remotes.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/demo/
+"""
+from homeassistant.components.remote import RemoteDevice
+from homeassistant.const import DEVICE_DEFAULT_NAME
+
+
+# pylint: disable=unused-argument
+def setup_platform(hass, config, add_devices_callback, discovery_info=None):
+    """Setup the demo remotes."""
+    add_devices_callback([
+        DemoRemote('Remote One', False, None),
+        DemoRemote('Remote Two', True, 'mdi:remote'),
+    ])
+
+
+class DemoRemote(RemoteDevice):
+    """Representation of a demo remote."""
+
+    def __init__(self, name, state, icon):
+        """Initialize the Demo Remote."""
+        self._name = name or DEVICE_DEFAULT_NAME
+        self._state = state
+        self._icon = icon
+
+    @property
+    def should_poll(self):
+        """No polling needed for a demo remote."""
+        return False
+
+    @property
+    def name(self):
+        """Return the name of the device if any."""
+        return self._name
+
+    @property
+    def icon(self):
+        """Return the icon to use for device if any."""
+        return self._icon
+
+    @property
+    def is_on(self):
+        """Return true if remote is on."""
+        return self._state
+
+    def turn_on(self, **kwargs):
+        """Turn the remote on."""
+        self._state = True
+        self.schedule_update_ha_state()
+
+    def turn_off(self, **kwargs):
+        """Turn the remote off."""
+        self._state = False
+        self.schedule_update_ha_state()
diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py
new file mode 100755
index 00000000000..9e799fab066
--- /dev/null
+++ b/homeassistant/components/remote/harmony.py
@@ -0,0 +1,198 @@
+"""
+Support for Harmony Hub devices.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/remote.harmony/
+
+"""
+
+import logging
+from os import path
+import urllib.parse
+from homeassistant.const import (
+    CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
+from homeassistant.components.remote import PLATFORM_SCHEMA, DOMAIN
+from homeassistant.util import slugify
+from homeassistant.config import load_yaml_config_file
+import homeassistant.components.remote as remote
+import homeassistant.helpers.config_validation as cv
+import voluptuous as vol
+
+
+REQUIREMENTS = ['pyharmony==1.0.12']
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_DEVICE = 'device'
+ATTR_COMMAND = 'command'
+ATTR_ACTIVITY = 'activity'
+
+SERVICE_SYNC = 'harmony_sync'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Required(CONF_NAME): cv.string,
+    vol.Required(CONF_HOST): cv.string,
+    vol.Required(CONF_PORT): cv.string,
+    vol.Required(ATTR_ACTIVITY, default=None): cv.string,
+})
+
+HARMONY_SYNC_SCHEMA = vol.Schema({
+    vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+})
+
+# List of devices that have been registered
+DEVICES = []
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Setup Harmony platform."""
+    import pyharmony
+    global DEVICES
+
+    name = config.get(CONF_NAME)
+    host = config.get(CONF_HOST)
+    port = config.get(CONF_PORT)
+    _LOGGER.info('Loading Harmony platform: ' + name)
+
+    harmony_conf_file = hass.config.path('harmony_' + slugify(name) + '.conf')
+
+    try:
+        _LOGGER.debug('calling pyharmony.ha_get_token for remote at: ' +
+                      host + ':' + port)
+        token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
+    except ValueError as err:
+        _LOGGER.critical(err.args[0] + ' for remote: ' + name)
+        return False
+
+    _LOGGER.debug('received token: ' + token)
+    DEVICES = [HarmonyRemote(config.get(CONF_NAME),
+                             config.get(CONF_HOST),
+                             config.get(CONF_PORT),
+                             config.get(ATTR_ACTIVITY),
+                             harmony_conf_file,
+                             token)]
+    add_devices(DEVICES, True)
+    register_services(hass)
+    return True
+
+
+def register_services(hass):
+    """Register all services for harmony devices."""
+    descriptions = load_yaml_config_file(
+        path.join(path.dirname(__file__), 'services.yaml'))
+
+    hass.services.register(DOMAIN, SERVICE_SYNC,
+                           _sync_service,
+                           descriptions.get(SERVICE_SYNC),
+                           schema=HARMONY_SYNC_SCHEMA)
+
+
+def _apply_service(service, service_func, *service_func_args):
+    """Internal func for applying a service."""
+    entity_ids = service.data.get('entity_id')
+
+    if entity_ids:
+        _devices = [device for device in DEVICES
+                    if device.entity_id in entity_ids]
+    else:
+        _devices = DEVICES
+
+    for device in _devices:
+        service_func(device, *service_func_args)
+        device.update_ha_state(True)
+
+
+def _sync_service(service):
+    _apply_service(service, HarmonyRemote.sync)
+
+
+class HarmonyRemote(remote.RemoteDevice):
+    """Remote representation used to control a Harmony device."""
+
+    def __init__(self, name, host, port, activity, out_path, token):
+        """Initialize HarmonyRemote class."""
+        import pyharmony
+        from pathlib import Path
+
+        _LOGGER.debug('HarmonyRemote device init started for: ' + name)
+        self._name = name
+        self._ip = host
+        self._port = port
+        self._state = None
+        self._current_activity = None
+        self._default_activity = activity
+        self._token = token
+        self._config_path = out_path
+        _LOGGER.debug('retrieving harmony config using token: ' + token)
+        self._config = pyharmony.ha_get_config(self._token, host, port)
+        if not Path(self._config_path).is_file():
+            _LOGGER.debug('writing harmony configuration to file: ' + out_path)
+            pyharmony.ha_write_config_file(self._config, self._config_path)
+
+    @property
+    def name(self):
+        """Return the Harmony device's name."""
+        return self._name
+
+    @property
+    def device_state_attributes(self):
+        """Add platform specific attributes."""
+        return {'current_activity': self._current_activity}
+
+    @property
+    def is_on(self):
+        """Return False if PowerOff is the current activity, otherwise True."""
+        return self._current_activity != 'PowerOff'
+
+    def update(self):
+        """Return current activity."""
+        import pyharmony
+        name = self._name
+        _LOGGER.debug('polling ' + name + ' for current activity')
+        state = pyharmony.ha_get_current_activity(self._token,
+                                                  self._config,
+                                                  self._ip,
+                                                  self._port)
+        _LOGGER.debug(name + '\'s current activity reported as: ' + state)
+        self._current_activity = state
+        self._state = bool(state != 'PowerOff')
+
+    def turn_on(self, **kwargs):
+        """Start an activity from the Harmony device."""
+        import pyharmony
+        if kwargs[ATTR_ACTIVITY]:
+            activity = kwargs[ATTR_ACTIVITY]
+        else:
+            activity = self._default_activity
+
+        if activity:
+            pyharmony.ha_start_activity(self._token,
+                                        self._ip,
+                                        self._port,
+                                        self._config,
+                                        activity)
+            self._state = True
+        else:
+            _LOGGER.error('No activity specified with turn_on service')
+
+    def turn_off(self):
+        """Start the PowerOff activity."""
+        import pyharmony
+        pyharmony.ha_power_off(self._token, self._ip, self._port)
+
+    def send_command(self, **kwargs):
+        """Send a command to one device."""
+        import pyharmony
+        pyharmony.ha_send_command(self._token, self._ip,
+                                  self._port, kwargs[ATTR_DEVICE],
+                                  kwargs[ATTR_COMMAND])
+
+    def sync(self):
+        """Sync the Harmony device with the web service."""
+        import pyharmony
+        _LOGGER.debug('syncing hub with Harmony servers')
+        pyharmony.ha_sync(self._token, self._ip, self._port)
+        self._config = pyharmony.ha_get_config(self._token,
+                                               self._ip,
+                                               self._port)
+        _LOGGER.debug('writing hub config to file: ' + self._config_path)
+        pyharmony.ha_write_config_file(self._config, self._config_path)
diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml
new file mode 100644
index 00000000000..2023588fcc2
--- /dev/null
+++ b/homeassistant/components/remote/services.yaml
@@ -0,0 +1,42 @@
+# Describes the format for available remote services
+
+turn_on:
+  description: Semds the Power On Command
+
+  fields:
+    entity_id:
+      description: Name(s) of entities to turn on
+      example: 'remote.family_room'
+    activity:
+      description: Activity ID or Activity Name to start
+      example: 'BedroomTV'
+
+turn_off:
+  description: Sends the Power Off Command
+
+  fields:
+    entity_id:
+      description: Name(s) of entities to turn off
+      example: 'remote.family_room'
+
+send_command:
+  description: Semds a single command to a single device
+
+  fields:
+    entity_id:
+      description: Name(s) of entities to send command from
+      example: 'remote.family_room'
+    device:
+      description: Device ID to send command to
+      example: '32756745'
+    command:
+      description: Command to send
+      example: 'Play'
+
+harmony_sync:
+  description: Syncs the remote's configuration
+
+  fields:
+    entity_id:
+      description: Name(s) of entities to sync
+      example: 'remote.family_room'
diff --git a/requirements_all.txt b/requirements_all.txt
index 8da898fb3d0..0d3709509fb 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -392,6 +392,9 @@ pyenvisalink==1.9
 # homeassistant.components.ifttt
 pyfttt==0.3
 
+# homeassistant.components.remote.harmony
+pyharmony==1.0.12
+
 # homeassistant.components.homematic
 pyhomematic==0.1.18
 
diff --git a/tests/components/remote/__init__.py b/tests/components/remote/__init__.py
new file mode 100755
index 00000000000..77870a11f20
--- /dev/null
+++ b/tests/components/remote/__init__.py
@@ -0,0 +1 @@
+"""The tests for Remote platforms."""
diff --git a/tests/components/remote/test_demo.py b/tests/components/remote/test_demo.py
new file mode 100755
index 00000000000..f43f9e8610c
--- /dev/null
+++ b/tests/components/remote/test_demo.py
@@ -0,0 +1,117 @@
+"""The tests for the demo remote component."""
+# pylint: disable=protected-access
+import unittest
+
+from homeassistant.bootstrap import setup_component
+import homeassistant.components.remote as remote
+from homeassistant.const import (
+    ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM,
+    SERVICE_TURN_ON, SERVICE_TURN_OFF)
+from tests.common import get_test_home_assistant, mock_service
+
+SERVICE_SYNC = 'sync'
+SERVICE_SEND_COMMAND = 'send_command'
+
+
+class TestDemoRemote(unittest.TestCase):
+    """Test the demo remote."""
+
+    # pylint: disable=invalid-name
+    def setUp(self):
+        """Setup things to be run when tests are started."""
+        self.hass = get_test_home_assistant()
+        self.assertTrue(setup_component(self.hass, remote.DOMAIN, {'remote': {
+            'platform': 'demo',
+        }}))
+
+    # pylint: disable=invalid-name
+    def tearDown(self):
+        """Stop down everything that was started."""
+        self.hass.stop()
+
+    def test_methods(self):
+        """Test if methods call the services as expected."""
+
+        self.assertTrue(
+            setup_component(self.hass, remote.DOMAIN,
+                            {remote.DOMAIN: {CONF_PLATFORM: 'demo'}}))
+
+        # Test is_on
+        self.hass.states.set('remote.demo', STATE_ON)
+        self.assertTrue(remote.is_on(self.hass, 'remote.demo'))
+
+        self.hass.states.set('remote.demo', STATE_OFF)
+        self.assertFalse(remote.is_on(self.hass, 'remote.demo'))
+
+        self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_ON)
+        self.assertTrue(remote.is_on(self.hass))
+
+        self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_OFF)
+        self.assertFalse(remote.is_on(self.hass))
+
+    def test_services(self):
+        """Test the provided services."""
+
+        # Test turn_on
+        turn_on_calls = mock_service(
+            self.hass, remote.DOMAIN, SERVICE_TURN_ON)
+
+        remote.turn_on(
+            self.hass,
+            entity_id='entity_id_val')
+
+        self.hass.block_till_done()
+
+        self.assertEqual(1, len(turn_on_calls))
+        call = turn_on_calls[-1]
+
+        self.assertEqual(remote.DOMAIN, call.domain)
+
+        # Test turn_off
+        turn_off_calls = mock_service(
+            self.hass, remote.DOMAIN, SERVICE_TURN_OFF)
+
+        remote.turn_off(
+            self.hass, entity_id='entity_id_val')
+
+        self.hass.block_till_done()
+
+        self.assertEqual(1, len(turn_off_calls))
+        call = turn_off_calls[-1]
+
+        self.assertEqual(remote.DOMAIN, call.domain)
+        self.assertEqual(SERVICE_TURN_OFF, call.service)
+        self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
+
+        # Test sync
+        sync_calls = mock_service(
+            self.hass, remote.DOMAIN, SERVICE_SYNC)
+
+        remote.sync(
+            self.hass, entity_id='entity_id_val')
+
+        self.hass.block_till_done()
+
+        self.assertEqual(1, len(sync_calls))
+        call = sync_calls[-1]
+
+        self.assertEqual(remote.DOMAIN, call.domain)
+        self.assertEqual(SERVICE_SYNC, call.service)
+        self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
+
+        # Test send_command
+        send_command_calls = mock_service(
+            self.hass, remote.DOMAIN, SERVICE_SEND_COMMAND)
+
+        remote.send_command(
+            self.hass, entity_id='entity_id_val',
+            device='test_device', command='test_command')
+
+        self.hass.block_till_done()
+
+        self.assertEqual(1, len(send_command_calls))
+        call = send_command_calls[-1]
+
+        self.assertEqual(remote.DOMAIN, call.domain)
+        self.assertEqual(SERVICE_SEND_COMMAND, call.service)
+        self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py
new file mode 100755
index 00000000000..799ed3b5ea7
--- /dev/null
+++ b/tests/components/remote/test_init.py
@@ -0,0 +1,99 @@
+"""The tests for the Remote component, adapted from Light Test."""
+# pylint: disable=protected-access
+
+import unittest
+
+from homeassistant.bootstrap import setup_component
+from homeassistant.const import (
+    ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM,
+    SERVICE_TURN_ON, SERVICE_TURN_OFF)
+import homeassistant.components.remote as remote
+
+from tests.common import mock_service, get_test_home_assistant
+TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}}
+SERVICE_SYNC = 'sync'
+SERVICE_SEND_COMMAND = 'send_command'
+
+
+class TestRemote(unittest.TestCase):
+    """Test the remote module."""
+
+    # pylint: disable=invalid-name
+    def setUp(self):
+        """Setup things to be run when tests are started."""
+        self.hass = get_test_home_assistant()
+
+    # pylint: disable=invalid-name
+    def tearDown(self):
+        """Stop everything that was started."""
+        self.hass.stop()
+
+    def test_is_on(self):
+        """ Test is_on"""
+        self.hass.states.set('remote.test', STATE_ON)
+        self.assertTrue(remote.is_on(self.hass, 'remote.test'))
+
+        self.hass.states.set('remote.test', STATE_OFF)
+        self.assertFalse(remote.is_on(self.hass, 'remote.test'))
+
+        self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_ON)
+        self.assertTrue(remote.is_on(self.hass))
+
+        self.hass.states.set(remote.ENTITY_ID_ALL_REMOTES, STATE_OFF)
+        self.assertFalse(remote.is_on(self.hass))
+
+    def test_turn_on(self):
+        """ Test turn_on"""
+        turn_on_calls = mock_service(
+            self.hass, remote.DOMAIN, SERVICE_TURN_ON)
+
+        remote.turn_on(
+            self.hass,
+            entity_id='entity_id_val')
+
+        self.hass.block_till_done()
+
+        self.assertEqual(1, len(turn_on_calls))
+        call = turn_on_calls[-1]
+
+        self.assertEqual(remote.DOMAIN, call.domain)
+
+    def test_turn_off(self):
+        """ Test turn_off"""
+        turn_off_calls = mock_service(
+            self.hass, remote.DOMAIN, SERVICE_TURN_OFF)
+
+        remote.turn_off(
+            self.hass, entity_id='entity_id_val')
+
+        self.hass.block_till_done()
+
+        self.assertEqual(1, len(turn_off_calls))
+        call = turn_off_calls[-1]
+
+        self.assertEqual(remote.DOMAIN, call.domain)
+        self.assertEqual(SERVICE_TURN_OFF, call.service)
+        self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
+
+    def test_send_command(self):
+        """ Test send_command"""
+        send_command_calls = mock_service(
+            self.hass, remote.DOMAIN, SERVICE_SEND_COMMAND)
+
+        remote.send_command(
+            self.hass, entity_id='entity_id_val',
+            device='test_device', command='test_command')
+
+        self.hass.block_till_done()
+
+        self.assertEqual(1, len(send_command_calls))
+        call = send_command_calls[-1]
+
+        self.assertEqual(remote.DOMAIN, call.domain)
+        self.assertEqual(SERVICE_SEND_COMMAND, call.service)
+        self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
+
+    def test_services(self):
+        """Test the provided services."""
+        self.assertTrue(setup_component(self.hass, remote.DOMAIN,
+                                        TEST_PLATFORM))
-- 
GitLab