diff --git a/.coveragerc b/.coveragerc
index d14bf41195e6fd0b07bac950f4bcf0a8cf1d3b22..b859e229e74082611c6fbd5b69f74cf42bed2a9a 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -358,7 +358,10 @@ omit =
     homeassistant/components/hangouts/hangouts_bot.py
     homeassistant/components/hangouts/hangups_utils.py
     homeassistant/components/harman_kardon_avr/media_player.py
-    homeassistant/components/harmony/*
+    homeassistant/components/harmony/const.py
+    homeassistant/components/harmony/data.py
+    homeassistant/components/harmony/remote.py
+    homeassistant/components/harmony/util.py
     homeassistant/components/haveibeenpwned/sensor.py
     homeassistant/components/hdmi_cec/*
     homeassistant/components/heatmiser/climate.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 8c9ffeea599a09c4bd1d081b9429b6eeef2006f1..03912a0dec0464997145a7946868da03eec6aee3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -180,7 +180,7 @@ homeassistant/components/griddy/* @bdraco
 homeassistant/components/group/* @home-assistant/core
 homeassistant/components/growatt_server/* @indykoning
 homeassistant/components/guardian/* @bachya
-homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco
+homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
 homeassistant/components/hassio/* @home-assistant/supervisor
 homeassistant/components/hdmi_cec/* @newAM
 homeassistant/components/heatmiser/* @andylockran
diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py
index e425b5ce94a3747750c3724ae2a12e5e92c70383..6ba63ee0f81eaa3e4d5286699248f4a4c56629f7 100644
--- a/homeassistant/components/harmony/__init__.py
+++ b/homeassistant/components/harmony/__init__.py
@@ -1,11 +1,7 @@
 """The Logitech Harmony Hub integration."""
 import asyncio
 
-from homeassistant.components.remote import (
-    ATTR_ACTIVITY,
-    ATTR_DELAY_SECS,
-    DEFAULT_DELAY_SECS,
-)
+from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_HOST, CONF_NAME
 from homeassistant.core import HomeAssistant, callback
@@ -13,7 +9,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
 from homeassistant.helpers.dispatcher import async_dispatcher_send
 
 from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS
-from .remote import HarmonyRemote
+from .data import HarmonyData
 
 
 async def async_setup(hass: HomeAssistant, config: dict):
@@ -33,22 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
 
     address = entry.data[CONF_HOST]
     name = entry.data[CONF_NAME]
-    activity = entry.options.get(ATTR_ACTIVITY)
-    delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
-
-    harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf")
+    data = HarmonyData(hass, address, name, entry.unique_id)
     try:
-        device = HarmonyRemote(
-            name, entry.unique_id, address, activity, harmony_conf_file, delay_secs
-        )
-        connected_ok = await device.connect()
+        connected_ok = await data.connect()
     except (asyncio.TimeoutError, ValueError, AttributeError) as err:
         raise ConfigEntryNotReady from err
 
     if not connected_ok:
         raise ConfigEntryNotReady
 
-    hass.data[DOMAIN][entry.entry_id] = device
+    hass.data[DOMAIN][entry.entry_id] = data
 
     entry.add_update_listener(_update_listener)
 
@@ -92,8 +82,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
     )
 
     # Shutdown a harmony remote for removal
-    device = hass.data[DOMAIN][entry.entry_id]
-    await device.shutdown()
+    data = hass.data[DOMAIN][entry.entry_id]
+    await data.shutdown()
 
     if unload_ok:
         hass.data[DOMAIN].pop(entry.entry_id)
diff --git a/homeassistant/components/harmony/connection_state.py b/homeassistant/components/harmony/connection_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..9706ba28776d0da3b272c1baa7e0837e07bbbdd5
--- /dev/null
+++ b/homeassistant/components/harmony/connection_state.py
@@ -0,0 +1,44 @@
+"""Mixin class for handling connection state changes."""
+import logging
+
+from homeassistant.helpers.event import async_call_later
+
+_LOGGER = logging.getLogger(__name__)
+
+TIME_MARK_DISCONNECTED = 10
+
+
+class ConnectionStateMixin:
+    """Base implementation for connection state handling."""
+
+    def __init__(self):
+        """Initialize this mixin instance."""
+        super().__init__()
+        self._unsub_mark_disconnected = None
+
+    async def got_connected(self, _=None):
+        """Notification that we're connected to the HUB."""
+        _LOGGER.debug("%s: connected to the HUB", self._name)
+        self.async_write_ha_state()
+
+        self._clear_disconnection_delay()
+
+    async def got_disconnected(self, _=None):
+        """Notification that we're disconnected from the HUB."""
+        _LOGGER.debug("%s: disconnected from the HUB", self._name)
+        # We're going to wait for 10 seconds before announcing we're
+        # unavailable, this to allow a reconnection to happen.
+        self._unsub_mark_disconnected = async_call_later(
+            self.hass, TIME_MARK_DISCONNECTED, self._mark_disconnected_if_unavailable
+        )
+
+    def _clear_disconnection_delay(self):
+        if self._unsub_mark_disconnected:
+            self._unsub_mark_disconnected()
+            self._unsub_mark_disconnected = None
+
+    def _mark_disconnected_if_unavailable(self, _):
+        self._unsub_mark_disconnected = None
+        if not self.available:
+            # Still disconnected. Let the state engine know.
+            self.async_write_ha_state()
diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py
index f6315b57b57a498337adeae8763ff0e2ba1340b9..ee4a454847e629fe67b7ace0e73402a6eea749f7 100644
--- a/homeassistant/components/harmony/const.py
+++ b/homeassistant/components/harmony/const.py
@@ -2,7 +2,7 @@
 DOMAIN = "harmony"
 SERVICE_SYNC = "sync"
 SERVICE_CHANGE_CHANNEL = "change_channel"
-PLATFORMS = ["remote"]
+PLATFORMS = ["remote", "switch"]
 UNIQUE_ID = "unique_id"
 ACTIVITY_POWER_OFF = "PowerOff"
 HARMONY_OPTIONS_UPDATE = "harmony_options_update"
diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c3ad874fa9d2ce56df967a21f9ea1322de41ab0
--- /dev/null
+++ b/homeassistant/components/harmony/data.py
@@ -0,0 +1,251 @@
+"""Harmony data object which contains the Harmony Client."""
+
+import logging
+from typing import Iterable
+
+from aioharmony.const import ClientCallbackType, SendCommandDevice
+import aioharmony.exceptions as aioexc
+from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient
+
+from .const import ACTIVITY_POWER_OFF
+from .subscriber import HarmonySubscriberMixin
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class HarmonyData(HarmonySubscriberMixin):
+    """HarmonyData registers for Harmony hub updates."""
+
+    def __init__(self, hass, address: str, name: str, unique_id: str):
+        """Initialize a data object."""
+        super().__init__(hass)
+        self._name = name
+        self._unique_id = unique_id
+        self._available = False
+
+        callbacks = {
+            "config_updated": self._config_updated,
+            "connect": self._connected,
+            "disconnect": self._disconnected,
+            "new_activity_starting": self._activity_starting,
+            "new_activity": self._activity_started,
+        }
+        self._client = HarmonyClient(
+            ip_address=address, callbacks=ClientCallbackType(**callbacks)
+        )
+
+    @property
+    def activity_names(self):
+        """Names of all the remotes activities."""
+        activity_infos = self._client.config.get("activity", [])
+        activities = [activity["label"] for activity in activity_infos]
+
+        # Remove both ways of representing PowerOff
+        if None in activities:
+            activities.remove(None)
+        if ACTIVITY_POWER_OFF in activities:
+            activities.remove(ACTIVITY_POWER_OFF)
+
+        return activities
+
+    @property
+    def device_names(self):
+        """Names of all of the devices connected to the hub."""
+        device_infos = self._client.config.get("device", [])
+        devices = [device["label"] for device in device_infos]
+
+        return devices
+
+    @property
+    def name(self):
+        """Return the Harmony device's name."""
+        return self._name
+
+    @property
+    def unique_id(self):
+        """Return the Harmony device's unique_id."""
+        return self._unique_id
+
+    @property
+    def json_config(self):
+        """Return the hub config as json."""
+        if self._client.config is None:
+            return None
+        return self._client.json_config
+
+    @property
+    def available(self) -> bool:
+        """Return if connected to the hub."""
+        return self._available
+
+    @property
+    def current_activity(self) -> tuple:
+        """Return the current activity tuple."""
+        return self._client.current_activity
+
+    def device_info(self, domain: str):
+        """Return hub device info."""
+        model = "Harmony Hub"
+        if "ethernetStatus" in self._client.hub_config.info:
+            model = "Harmony Hub Pro 2400"
+        return {
+            "identifiers": {(domain, self.unique_id)},
+            "manufacturer": "Logitech",
+            "sw_version": self._client.hub_config.info.get(
+                "hubSwVersion", self._client.fw_version
+            ),
+            "name": self.name,
+            "model": model,
+        }
+
+    async def connect(self) -> bool:
+        """Connect to the Harmony Hub."""
+        _LOGGER.debug("%s: Connecting", self._name)
+        try:
+            if not await self._client.connect():
+                _LOGGER.warning("%s: Unable to connect to HUB", self._name)
+                await self._client.close()
+                return False
+        except aioexc.TimeOut:
+            _LOGGER.warning("%s: Connection timed-out", self._name)
+            return False
+        return True
+
+    async def shutdown(self):
+        """Close connection on shutdown."""
+        _LOGGER.debug("%s: Closing Harmony Hub", self._name)
+        try:
+            await self._client.close()
+        except aioexc.TimeOut:
+            _LOGGER.warning("%s: Disconnect timed-out", self._name)
+
+    async def async_start_activity(self, activity: str):
+        """Start an activity from the Harmony device."""
+
+        if not activity:
+            _LOGGER.error("%s: No activity specified with turn_on service", self.name)
+            return
+
+        activity_id = None
+        activity_name = None
+
+        if activity.isdigit() or activity == "-1":
+            _LOGGER.debug("%s: Activity is numeric", self.name)
+            activity_name = self._client.get_activity_name(int(activity))
+            if activity_name:
+                activity_id = activity
+
+        if activity_id is None:
+            _LOGGER.debug("%s: Find activity ID based on name", self.name)
+            activity_name = str(activity)
+            activity_id = self._client.get_activity_id(activity_name)
+
+        if activity_id is None:
+            _LOGGER.error("%s: Activity %s is invalid", self.name, activity)
+            return
+
+        _, current_activity_name = self.current_activity
+        if current_activity_name == activity_name:
+            # Automations or HomeKit may turn the device on multiple times
+            # when the current activity is already active which will cause
+            # harmony to loose state.  This behavior is unexpected as turning
+            # the device on when its already on isn't expected to reset state.
+            _LOGGER.debug(
+                "%s: Current activity is already %s", self.name, activity_name
+            )
+            return
+
+        try:
+            await self._client.start_activity(activity_id)
+        except aioexc.TimeOut:
+            _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity)
+
+    async def async_power_off(self):
+        """Start the PowerOff activity."""
+        _LOGGER.debug("%s: Turn Off", self.name)
+        try:
+            await self._client.power_off()
+        except aioexc.TimeOut:
+            _LOGGER.error("%s: Powering off timed-out", self.name)
+
+    async def async_send_command(
+        self,
+        commands: Iterable[str],
+        device: str,
+        num_repeats: int,
+        delay_secs: float,
+        hold_secs: float,
+    ):
+        """Send a list of commands to one device."""
+        device_id = None
+        if device.isdigit():
+            _LOGGER.debug("%s: Device %s is numeric", self.name, device)
+            if self._client.get_device_name(int(device)):
+                device_id = device
+
+        if device_id is None:
+            _LOGGER.debug(
+                "%s: Find device ID %s based on device name", self.name, device
+            )
+            device_id = self._client.get_device_id(str(device).strip())
+
+        if device_id is None:
+            _LOGGER.error("%s: Device %s is invalid", self.name, device)
+            return
+
+        _LOGGER.debug(
+            "Sending commands to device %s holding for %s seconds "
+            "with a delay of %s seconds",
+            device,
+            hold_secs,
+            delay_secs,
+        )
+
+        # Creating list of commands to send.
+        snd_cmnd_list = []
+        for _ in range(num_repeats):
+            for single_command in commands:
+                send_command = SendCommandDevice(
+                    device=device_id, command=single_command, delay=hold_secs
+                )
+                snd_cmnd_list.append(send_command)
+                if delay_secs > 0:
+                    snd_cmnd_list.append(float(delay_secs))
+
+        _LOGGER.debug("%s: Sending commands", self.name)
+        try:
+            result_list = await self._client.send_commands(snd_cmnd_list)
+        except aioexc.TimeOut:
+            _LOGGER.error("%s: Sending commands timed-out", self.name)
+            return
+
+        for result in result_list:
+            _LOGGER.error(
+                "Sending command %s to device %s failed with code %s: %s",
+                result.command.command,
+                result.command.device,
+                result.code,
+                result.msg,
+            )
+
+    async def change_channel(self, channel: int):
+        """Change the channel using Harmony remote."""
+        _LOGGER.debug("%s: Changing channel to %s", self.name, channel)
+        try:
+            await self._client.change_channel(channel)
+        except aioexc.TimeOut:
+            _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel)
+
+    async def sync(self) -> bool:
+        """Sync the Harmony device with the web service.
+
+        Returns True if the sync was successful.
+        """
+        _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name)
+        try:
+            await self._client.sync()
+        except aioexc.TimeOut:
+            _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name)
+            return False
+        else:
+            return True
diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json
index 4d8b83f464319eb7f04e272c1206a8cdfa7b18b8..7509f3d4f4df6c5414e0beb3ff8d00287c38189a 100644
--- a/homeassistant/components/harmony/manifest.json
+++ b/homeassistant/components/harmony/manifest.json
@@ -3,12 +3,13 @@
   "name": "Logitech Harmony Hub",
   "documentation": "https://www.home-assistant.io/integrations/harmony",
   "requirements": ["aioharmony==0.2.6"],
-  "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco"],
+  "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco", "@mkeesey"],
   "ssdp": [
     {
       "manufacturer": "Logitech",
       "deviceType": "urn:myharmony-com:device:harmony:1"
     }
   ],
+  "dependencies": ["remote", "switch"],
   "config_flow": true
 }
diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py
index ff7825013e8f5d32bf55a8bab0e0e30d82a2b5f2..b9205a4befb2f33715af949d4c0cce429f297507 100644
--- a/homeassistant/components/harmony/remote.py
+++ b/homeassistant/components/harmony/remote.py
@@ -1,11 +1,7 @@
 """Support for Harmony Hub devices."""
-import asyncio
 import json
 import logging
 
-from aioharmony.const import ClientCallbackType
-import aioharmony.exceptions as aioexc
-from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient, SendCommandDevice
 import voluptuous as vol
 
 from homeassistant.components import remote
@@ -27,6 +23,7 @@ import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.restore_state import RestoreEntity
 
+from .connection_state import ConnectionStateMixin
 from .const import (
     ACTIVITY_POWER_OFF,
     ATTR_ACTIVITY_LIST,
@@ -41,12 +38,12 @@ from .const import (
     SERVICE_SYNC,
     UNIQUE_ID,
 )
+from .subscriber import HarmonyCallback
 from .util import (
     find_best_name_for_remote,
     find_matching_config_entries_for_host,
     find_unique_id_for_remote,
     get_harmony_client_if_available,
-    list_names_from_hublist,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -113,10 +110,15 @@ async def async_setup_entry(
 ):
     """Set up the Harmony config entry."""
 
-    device = hass.data[DOMAIN][entry.entry_id]
+    data = hass.data[DOMAIN][entry.entry_id]
 
-    _LOGGER.debug("Harmony Remote: %s", device)
+    _LOGGER.debug("HarmonyData : %s", data)
 
+    default_activity = entry.options.get(ATTR_ACTIVITY)
+    delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
+
+    harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf")
+    device = HarmonyRemote(data, default_activity, delay_secs, harmony_conf_file)
     async_add_entities([device])
 
     platform = entity_platform.current_platform.get()
@@ -131,37 +133,23 @@ async def async_setup_entry(
     )
 
 
-class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
+class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity):
     """Remote representation used to control a Harmony device."""
 
-    def __init__(self, name, unique_id, host, activity, out_path, delay_secs):
+    def __init__(self, data, activity, delay_secs, out_path):
         """Initialize HarmonyRemote class."""
-        self._name = name
-        self.host = host
+        super().__init__()
+        self._data = data
+        self._name = data.name
         self._state = None
         self._current_activity = ACTIVITY_POWER_OFF
         self.default_activity = activity
         self._activity_starting = None
         self._is_initial_update = True
-        self._client = HarmonyClient(ip_address=host)
-        self._config_path = out_path
         self.delay_secs = delay_secs
-        self._available = False
-        self._unique_id = unique_id
+        self._unique_id = data.unique_id
         self._last_activity = None
-
-    @property
-    def activity_names(self):
-        """Names of all the remotes activities."""
-        activities = [activity["label"] for activity in self._client.config["activity"]]
-
-        # Remove both ways of representing PowerOff
-        if None in activities:
-            activities.remove(None)
-        if ACTIVITY_POWER_OFF in activities:
-            activities.remove(ACTIVITY_POWER_OFF)
-
-        return activities
+        self._config_path = out_path
 
     async def _async_update_options(self, data):
         """Change options when the options flow does."""
@@ -171,15 +159,16 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
         if ATTR_ACTIVITY in data:
             self.default_activity = data[ATTR_ACTIVITY]
 
-    def _update_callbacks(self):
+    def _setup_callbacks(self):
         callbacks = {
+            "connected": self.got_connected,
+            "disconnected": self.got_disconnected,
             "config_updated": self.new_config,
-            "connect": self.got_connected,
-            "disconnect": self.got_disconnected,
-            "new_activity_starting": self.new_activity,
-            "new_activity": self._new_activity_finished,
+            "activity_starting": self.new_activity,
+            "activity_started": self._new_activity_finished,
         }
-        self._client.callbacks = ClientCallbackType(**callbacks)
+
+        self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks)))
 
     def _new_activity_finished(self, activity_info: tuple) -> None:
         """Call for finished updated current activity."""
@@ -191,8 +180,9 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
         await super().async_added_to_hass()
 
         _LOGGER.debug("%s: Harmony Hub added", self._name)
-        # Register the callbacks
-        self._update_callbacks()
+
+        self.async_on_remove(self._clear_disconnection_delay)
+        self._setup_callbacks()
 
         self.async_on_remove(
             async_dispatcher_connect(
@@ -219,29 +209,10 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
 
         self._last_activity = last_state.attributes[ATTR_LAST_ACTIVITY]
 
-    async def shutdown(self):
-        """Close connection on shutdown."""
-        _LOGGER.debug("%s: Closing Harmony Hub", self._name)
-        try:
-            await self._client.close()
-        except aioexc.TimeOut:
-            _LOGGER.warning("%s: Disconnect timed-out", self._name)
-
     @property
     def device_info(self):
         """Return device info."""
-        model = "Harmony Hub"
-        if "ethernetStatus" in self._client.hub_config.info:
-            model = "Harmony Hub Pro 2400"
-        return {
-            "identifiers": {(DOMAIN, self.unique_id)},
-            "manufacturer": "Logitech",
-            "sw_version": self._client.hub_config.info.get(
-                "hubSwVersion", self._client.fw_version
-            ),
-            "name": self.name,
-            "model": model,
-        }
+        self._data.device_info(DOMAIN)
 
     @property
     def unique_id(self):
@@ -264,10 +235,8 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
         return {
             ATTR_ACTIVITY_STARTING: self._activity_starting,
             ATTR_CURRENT_ACTIVITY: self._current_activity,
-            ATTR_ACTIVITY_LIST: list_names_from_hublist(
-                self._client.hub_config.activities
-            ),
-            ATTR_DEVICES_LIST: list_names_from_hublist(self._client.hub_config.devices),
+            ATTR_ACTIVITY_LIST: self._data.activity_names,
+            ATTR_DEVICES_LIST: self._data.device_names,
             ATTR_LAST_ACTIVITY: self._last_activity,
         }
 
@@ -279,20 +248,7 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
     @property
     def available(self):
         """Return True if connected to Hub, otherwise False."""
-        return self._available
-
-    async def connect(self):
-        """Connect to the Harmony HUB."""
-        _LOGGER.debug("%s: Connecting", self._name)
-        try:
-            if not await self._client.connect():
-                _LOGGER.warning("%s: Unable to connect to HUB", self._name)
-                await self._client.close()
-                return False
-        except aioexc.TimeOut:
-            _LOGGER.warning("%s: Connection timed-out", self._name)
-            return False
-        return True
+        return self._data.available
 
     def new_activity(self, activity_info: tuple) -> None:
         """Call for updating the current activity."""
@@ -309,34 +265,14 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
             # when turning on
             self._last_activity = activity_name
         self._state = bool(activity_id != -1)
-        self._available = True
         self.async_write_ha_state()
 
     async def new_config(self, _=None):
         """Call for updating the current activity."""
         _LOGGER.debug("%s: configuration has been updated", self._name)
-        self.new_activity(self._client.current_activity)
+        self.new_activity(self._data.current_activity)
         await self.hass.async_add_executor_job(self.write_config_file)
 
-    async def got_connected(self, _=None):
-        """Notification that we're connected to the HUB."""
-        _LOGGER.debug("%s: connected to the HUB", self._name)
-        if not self._available:
-            # We were disconnected before.
-            await self.new_config()
-
-    async def got_disconnected(self, _=None):
-        """Notification that we're disconnected from the HUB."""
-        _LOGGER.debug("%s: disconnected from the HUB", self._name)
-        self._available = False
-        # We're going to wait for 10 seconds before announcing we're
-        # unavailable, this to allow a reconnection to happen.
-        await asyncio.sleep(10)
-
-        if not self._available:
-            # Still disconnected. Let the state engine know.
-            self.async_write_ha_state()
-
     async def async_turn_on(self, **kwargs):
         """Start an activity from the Harmony device."""
         _LOGGER.debug("%s: Turn On", self.name)
@@ -347,55 +283,18 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
             if self._last_activity:
                 activity = self._last_activity
             else:
-                all_activities = list_names_from_hublist(
-                    self._client.hub_config.activities
-                )
+                all_activities = self._data.activity_names
                 if all_activities:
                     activity = all_activities[0]
 
         if activity:
-            activity_id = None
-            activity_name = None
-
-            if activity.isdigit() or activity == "-1":
-                _LOGGER.debug("%s: Activity is numeric", self.name)
-                activity_name = self._client.get_activity_name(int(activity))
-                if activity_name:
-                    activity_id = activity
-
-            if activity_id is None:
-                _LOGGER.debug("%s: Find activity ID based on name", self.name)
-                activity_name = str(activity)
-                activity_id = self._client.get_activity_id(activity_name)
-
-            if activity_id is None:
-                _LOGGER.error("%s: Activity %s is invalid", self.name, activity)
-                return
-
-            if self._current_activity == activity_name:
-                # Automations or HomeKit may turn the device on multiple times
-                # when the current activity is already active which will cause
-                # harmony to loose state.  This behavior is unexpected as turning
-                # the device on when its already on isn't expected to reset state.
-                _LOGGER.debug(
-                    "%s: Current activity is already %s", self.name, activity_name
-                )
-                return
-
-            try:
-                await self._client.start_activity(activity_id)
-            except aioexc.TimeOut:
-                _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity)
+            await self._data.async_start_activity(activity)
         else:
             _LOGGER.error("%s: No activity specified with turn_on service", self.name)
 
     async def async_turn_off(self, **kwargs):
         """Start the PowerOff activity."""
-        _LOGGER.debug("%s: Turn Off", self.name)
-        try:
-            await self._client.power_off()
-        except aioexc.TimeOut:
-            _LOGGER.error("%s: Powering off timed-out", self.name)
+        await self._data.async_power_off()
 
     async def async_send_command(self, command, **kwargs):
         """Send a list of commands to one device."""
@@ -405,90 +304,38 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity):
             _LOGGER.error("%s: Missing required argument: device", self.name)
             return
 
-        device_id = None
-        if device.isdigit():
-            _LOGGER.debug("%s: Device %s is numeric", self.name, device)
-            if self._client.get_device_name(int(device)):
-                device_id = device
-
-        if device_id is None:
-            _LOGGER.debug(
-                "%s: Find device ID %s based on device name", self.name, device
-            )
-            device_id = self._client.get_device_id(str(device).strip())
-
-        if device_id is None:
-            _LOGGER.error("%s: Device %s is invalid", self.name, device)
-            return
-
         num_repeats = kwargs[ATTR_NUM_REPEATS]
         delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secs)
         hold_secs = kwargs[ATTR_HOLD_SECS]
-        _LOGGER.debug(
-            "Sending commands to device %s holding for %s seconds "
-            "with a delay of %s seconds",
-            device,
-            hold_secs,
-            delay_secs,
+        await self._data.async_send_command(
+            command, device, num_repeats, delay_secs, hold_secs
         )
 
-        # Creating list of commands to send.
-        snd_cmnd_list = []
-        for _ in range(num_repeats):
-            for single_command in command:
-                send_command = SendCommandDevice(
-                    device=device_id, command=single_command, delay=hold_secs
-                )
-                snd_cmnd_list.append(send_command)
-                if delay_secs > 0:
-                    snd_cmnd_list.append(float(delay_secs))
-
-        _LOGGER.debug("%s: Sending commands", self.name)
-        try:
-            result_list = await self._client.send_commands(snd_cmnd_list)
-        except aioexc.TimeOut:
-            _LOGGER.error("%s: Sending commands timed-out", self.name)
-            return
-
-        for result in result_list:
-            _LOGGER.error(
-                "Sending command %s to device %s failed with code %s: %s",
-                result.command.command,
-                result.command.device,
-                result.code,
-                result.msg,
-            )
-
     async def change_channel(self, channel):
         """Change the channel using Harmony remote."""
-        _LOGGER.debug("%s: Changing channel to %s", self.name, channel)
-        try:
-            await self._client.change_channel(channel)
-        except aioexc.TimeOut:
-            _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel)
+        await self._data.change_channel(channel)
 
     async def sync(self):
         """Sync the Harmony device with the web service."""
-        _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name)
-        try:
-            await self._client.sync()
-        except aioexc.TimeOut:
-            _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name)
-        else:
+        if await self._data.sync():
             await self.hass.async_add_executor_job(self.write_config_file)
 
     def write_config_file(self):
-        """Write Harmony configuration file."""
+        """Write Harmony configuration file.
+
+        This is a handy way for users to figure out the available commands for automations.
+        """
         _LOGGER.debug(
             "%s: Writing hub configuration to file: %s", self.name, self._config_path
         )
-        if self._client.config is None:
+        json_config = self._data.json_config
+        if json_config is None:
             _LOGGER.warning("%s: No configuration received from hub", self.name)
             return
 
         try:
             with open(self._config_path, "w+", encoding="utf-8") as file_out:
-                json.dump(self._client.json_config, file_out, sort_keys=True, indent=4)
+                json.dump(json_config, file_out, sort_keys=True, indent=4)
         except OSError as exc:
             _LOGGER.error(
                 "%s: Unable to write HUB configuration to %s: %s",
diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3bed33d5606697734c38dc4769b616cbe8e199d
--- /dev/null
+++ b/homeassistant/components/harmony/subscriber.py
@@ -0,0 +1,77 @@
+"""Mixin class for handling harmony callback subscriptions."""
+
+import logging
+from typing import Any, Callable, NamedTuple, Optional
+
+from homeassistant.core import callback
+
+_LOGGER = logging.getLogger(__name__)
+
+NoParamCallback = Optional[Callable[[object], Any]]
+ActivityCallback = Optional[Callable[[object, tuple], Any]]
+
+
+class HarmonyCallback(NamedTuple):
+    """Callback type for Harmony Hub notifications."""
+
+    connected: NoParamCallback
+    disconnected: NoParamCallback
+    config_updated: NoParamCallback
+    activity_starting: ActivityCallback
+    activity_started: ActivityCallback
+
+
+class HarmonySubscriberMixin:
+    """Base implementation for a subscriber."""
+
+    def __init__(self, hass):
+        """Initialize an subscriber."""
+        super().__init__()
+        self._hass = hass
+        self._subscriptions = []
+
+    @callback
+    def async_subscribe(self, update_callbacks: HarmonyCallback) -> Callable:
+        """Add a callback subscriber."""
+        self._subscriptions.append(update_callbacks)
+
+        def _unsubscribe():
+            self.async_unsubscribe(update_callbacks)
+
+        return _unsubscribe
+
+    @callback
+    def async_unsubscribe(self, update_callback: HarmonyCallback):
+        """Remove a callback subscriber."""
+        self._subscriptions.remove(update_callback)
+
+    def _config_updated(self, _=None) -> None:
+        _LOGGER.debug("config_updated")
+        self._call_callbacks("config_updated")
+
+    def _connected(self, _=None) -> None:
+        _LOGGER.debug("connected")
+        self._available = True
+        self._call_callbacks("connected")
+
+    def _disconnected(self, _=None) -> None:
+        _LOGGER.debug("disconnected")
+        self._available = False
+        self._call_callbacks("disconnected")
+
+    def _activity_starting(self, activity_info: tuple) -> None:
+        _LOGGER.debug("activity %s starting", activity_info)
+        self._call_callbacks("activity_starting", activity_info)
+
+    def _activity_started(self, activity_info: tuple) -> None:
+        _LOGGER.debug("activity %s started", activity_info)
+        self._call_callbacks("activity_started", activity_info)
+
+    def _call_callbacks(self, callback_func_name: str, argument: tuple = None):
+        for subscription in self._subscriptions:
+            current_callback = getattr(subscription, callback_func_name)
+            if current_callback:
+                if argument:
+                    self._hass.async_run_job(current_callback, argument)
+                else:
+                    self._hass.async_run_job(current_callback)
diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py
new file mode 100644
index 0000000000000000000000000000000000000000..5fae07c431ba3524d405e795d404b6cde1d74dc4
--- /dev/null
+++ b/homeassistant/components/harmony/switch.py
@@ -0,0 +1,87 @@
+"""Support for Harmony Hub activities."""
+import logging
+
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.const import CONF_NAME
+
+from .connection_state import ConnectionStateMixin
+from .const import DOMAIN
+from .data import HarmonyData
+from .subscriber import HarmonyCallback
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+    """Set up harmony activity switches."""
+    data = hass.data[DOMAIN][entry.entry_id]
+    activities = data.activity_names
+
+    switches = []
+    for activity in activities:
+        _LOGGER.debug("creating switch for activity: %s", activity)
+        name = f"{entry.data[CONF_NAME]} {activity}"
+        switches.append(HarmonyActivitySwitch(name, activity, data))
+
+    async_add_entities(switches, True)
+
+
+class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity):
+    """Switch representation of a Harmony activity."""
+
+    def __init__(self, name: str, activity: str, data: HarmonyData):
+        """Initialize HarmonyActivitySwitch class."""
+        super().__init__()
+        self._name = name
+        self._activity = activity
+        self._data = data
+
+    @property
+    def name(self):
+        """Return the Harmony activity's name."""
+        return self._name
+
+    @property
+    def unique_id(self):
+        """Return the unique id."""
+        return f"{self._data.unique_id}-{self._activity}"
+
+    @property
+    def is_on(self):
+        """Return if the current activity is the one for this switch."""
+        _, activity_name = self._data.current_activity
+        return activity_name == self._activity
+
+    @property
+    def should_poll(self):
+        """Return that we shouldn't be polled."""
+        return False
+
+    @property
+    def available(self):
+        """Return True if we're connected to the Hub, otherwise False."""
+        return self._data.available
+
+    async def async_turn_on(self, **kwargs):
+        """Start this activity."""
+        await self._data.async_start_activity(self._activity)
+
+    async def async_turn_off(self, **kwargs):
+        """Stop this activity."""
+        await self._data.async_power_off()
+
+    async def async_added_to_hass(self):
+        """Call when entity is added to hass."""
+
+        callbacks = {
+            "connected": self.got_connected,
+            "disconnected": self.got_disconnected,
+            "activity_starting": self._activity_update,
+            "activity_started": self._activity_update,
+            "config_updated": None,
+        }
+
+        self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks)))
+
+    def _activity_update(self, activity_info: tuple):
+        self.async_write_ha_state()
diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..e758a2795a94e35a3578583bfb761c8d17243d45
--- /dev/null
+++ b/tests/components/harmony/conftest.py
@@ -0,0 +1,147 @@
+"""Fixtures for harmony tests."""
+import logging
+from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
+
+from aioharmony.const import ClientCallbackType
+import pytest
+
+from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF
+
+_LOGGER = logging.getLogger(__name__)
+
+WATCH_TV_ACTIVITY_ID = 123
+PLAY_MUSIC_ACTIVITY_ID = 456
+
+ACTIVITIES_TO_IDS = {
+    ACTIVITY_POWER_OFF: -1,
+    "Watch TV": WATCH_TV_ACTIVITY_ID,
+    "Play Music": PLAY_MUSIC_ACTIVITY_ID,
+}
+
+IDS_TO_ACTIVITIES = {
+    -1: ACTIVITY_POWER_OFF,
+    WATCH_TV_ACTIVITY_ID: "Watch TV",
+    PLAY_MUSIC_ACTIVITY_ID: "Play Music",
+}
+
+TV_DEVICE_ID = 1234
+TV_DEVICE_NAME = "My TV"
+
+DEVICES_TO_IDS = {
+    TV_DEVICE_NAME: TV_DEVICE_ID,
+}
+
+IDS_TO_DEVICES = {
+    TV_DEVICE_ID: TV_DEVICE_NAME,
+}
+
+
+class FakeHarmonyClient:
+    """FakeHarmonyClient to mock away network calls."""
+
+    def __init__(
+        self, ip_address: str = "", callbacks: ClientCallbackType = MagicMock()
+    ):
+        """Initialize FakeHarmonyClient class."""
+        self._activity_name = "Watch TV"
+        self.close = AsyncMock()
+        self.send_commands = AsyncMock()
+        self.change_channel = AsyncMock()
+        self.sync = AsyncMock()
+        self._callbacks = callbacks
+
+    async def connect(self):
+        """Connect and call the appropriate callbacks."""
+        self._callbacks.connect(None)
+        return AsyncMock(return_value=(True))
+
+    def get_activity_name(self, activity_id):
+        """Return the activity name with the given activity_id."""
+        return IDS_TO_ACTIVITIES.get(activity_id)
+
+    def get_activity_id(self, activity_name):
+        """Return the mapping of an activity name to the internal id."""
+        return ACTIVITIES_TO_IDS.get(activity_name)
+
+    def get_device_name(self, device_id):
+        """Return the device name with the given device_id."""
+        return IDS_TO_DEVICES.get(device_id)
+
+    def get_device_id(self, device_name):
+        """Return the device id with the given device_name."""
+        return DEVICES_TO_IDS.get(device_name)
+
+    async def start_activity(self, activity_id):
+        """Update the current activity and call the appropriate callbacks."""
+        self._activity_name = IDS_TO_ACTIVITIES.get(int(activity_id))
+        activity_tuple = (activity_id, self._activity_name)
+        self._callbacks.new_activity_starting(activity_tuple)
+        self._callbacks.new_activity(activity_tuple)
+
+        return AsyncMock(return_value=(True, "unused message"))
+
+    async def power_off(self):
+        """Power off all activities."""
+        await self.start_activity(-1)
+
+    @property
+    def current_activity(self):
+        """Return the current activity tuple."""
+        return (
+            self.get_activity_id(self._activity_name),
+            self._activity_name,
+        )
+
+    @property
+    def config(self):
+        """Return the config object."""
+        return self.hub_config.config
+
+    @property
+    def json_config(self):
+        """Return the json config as a dict."""
+        return {}
+
+    @property
+    def hub_config(self):
+        """Return the client_config type."""
+        config = MagicMock()
+        type(config).activities = PropertyMock(
+            return_value=[
+                {"name": "Watch TV", "id": WATCH_TV_ACTIVITY_ID},
+                {"name": "Play Music", "id": PLAY_MUSIC_ACTIVITY_ID},
+            ]
+        )
+        type(config).devices = PropertyMock(
+            return_value=[{"name": TV_DEVICE_NAME, "id": TV_DEVICE_ID}]
+        )
+        type(config).info = PropertyMock(return_value={})
+        type(config).hub_state = PropertyMock(return_value={})
+        type(config).config = PropertyMock(
+            return_value={
+                "activity": [
+                    {"id": WATCH_TV_ACTIVITY_ID, "label": "Watch TV"},
+                    {"id": PLAY_MUSIC_ACTIVITY_ID, "label": "Play Music"},
+                ]
+            }
+        )
+        return config
+
+
+@pytest.fixture()
+def mock_hc():
+    """Create a mock HarmonyClient."""
+    with patch(
+        "homeassistant.components.harmony.data.HarmonyClient",
+        side_effect=FakeHarmonyClient,
+    ) as fake:
+        yield fake
+
+
+@pytest.fixture()
+def mock_write_config():
+    """Patches write_config_file to remove side effects."""
+    with patch(
+        "homeassistant.components.harmony.remote.HarmonyRemote.write_config_file",
+    ) as mock:
+        yield mock
diff --git a/tests/components/harmony/const.py b/tests/components/harmony/const.py
new file mode 100644
index 0000000000000000000000000000000000000000..1911ea949aff078c24adb2eb360aaefc830cf232
--- /dev/null
+++ b/tests/components/harmony/const.py
@@ -0,0 +1,6 @@
+"""Constants for Logitch Harmony Hub tests."""
+
+HUB_NAME = "Guest Room"
+ENTITY_REMOTE = "remote.guest_room"
+ENTITY_WATCH_TV = "switch.guest_room_watch_tv"
+ENTITY_PLAY_MUSIC = "switch.guest_room_play_music"
diff --git a/tests/components/harmony/test_activity_changes.py b/tests/components/harmony/test_activity_changes.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff76c3ce998bacf211eace811cc8b59323c7f89b
--- /dev/null
+++ b/tests/components/harmony/test_activity_changes.py
@@ -0,0 +1,137 @@
+"""Test the Logitech Harmony Hub activity switches."""
+
+import logging
+
+from homeassistant.components.harmony.const import DOMAIN
+from homeassistant.components.remote import ATTR_ACTIVITY, DOMAIN as REMOTE_DOMAIN
+from homeassistant.components.switch import (
+    DOMAIN as SWITCH_DOMAIN,
+    SERVICE_TURN_OFF,
+    SERVICE_TURN_ON,
+)
+from homeassistant.const import (
+    ATTR_ENTITY_ID,
+    CONF_HOST,
+    CONF_NAME,
+    STATE_OFF,
+    STATE_ON,
+)
+
+from .conftest import ACTIVITIES_TO_IDS
+from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME
+
+from tests.common import MockConfigEntry
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def test_switch_toggles(mock_hc, hass, mock_write_config):
+    """Ensure calls to the switch modify the harmony state."""
+    entry = MockConfigEntry(
+        domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+    )
+
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    # mocks start with current activity == Watch TV
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    # turn off watch tv switch
+    await _toggle_switch_and_wait(hass, SERVICE_TURN_OFF, ENTITY_WATCH_TV)
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    # turn on play music switch
+    await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_PLAY_MUSIC)
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON)
+
+    # turn on watch tv switch
+    await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_WATCH_TV)
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+
+async def test_remote_toggles(mock_hc, hass, mock_write_config):
+    """Ensure calls to the remote also updates the switches."""
+    entry = MockConfigEntry(
+        domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+    )
+
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    # mocks start with current activity == Watch TV
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    # turn off remote
+    await hass.services.async_call(
+        REMOTE_DOMAIN,
+        SERVICE_TURN_OFF,
+        {ATTR_ENTITY_ID: ENTITY_REMOTE},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    # turn on remote, restoring the last activity
+    await hass.services.async_call(
+        REMOTE_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_ENTITY_ID: ENTITY_REMOTE},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    # send new activity command, with activity name
+    await hass.services.async_call(
+        REMOTE_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: "Play Music"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON)
+
+    # send new activity command, with activity id
+    await hass.services.async_call(
+        REMOTE_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: ACTIVITIES_TO_IDS["Watch TV"]},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+
+async def _toggle_switch_and_wait(hass, service_name, entity):
+    await hass.services.async_call(
+        SWITCH_DOMAIN,
+        service_name,
+        {ATTR_ENTITY_ID: entity},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
diff --git a/tests/components/harmony/test_commands.py b/tests/components/harmony/test_commands.py
new file mode 100644
index 0000000000000000000000000000000000000000..62056a08e1dff207d6cb98916ca5c96c921d2870
--- /dev/null
+++ b/tests/components/harmony/test_commands.py
@@ -0,0 +1,263 @@
+"""Test sending commands to the Harmony Hub remote."""
+
+from aioharmony.const import SendCommandDevice
+
+from homeassistant.components.harmony.const import (
+    DOMAIN,
+    SERVICE_CHANGE_CHANNEL,
+    SERVICE_SYNC,
+)
+from homeassistant.components.harmony.remote import ATTR_CHANNEL, ATTR_DELAY_SECS
+from homeassistant.components.remote import (
+    ATTR_COMMAND,
+    ATTR_DEVICE,
+    ATTR_NUM_REPEATS,
+    DEFAULT_DELAY_SECS,
+    DEFAULT_HOLD_SECS,
+    DOMAIN as REMOTE_DOMAIN,
+    SERVICE_SEND_COMMAND,
+)
+from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
+
+from .conftest import TV_DEVICE_ID, TV_DEVICE_NAME
+from .const import ENTITY_REMOTE, HUB_NAME
+
+from tests.common import MockConfigEntry
+
+PLAY_COMMAND = "Play"
+STOP_COMMAND = "Stop"
+
+
+async def test_async_send_command(mock_hc, hass, mock_write_config):
+    """Ensure calls to send remote commands properly propagate to devices."""
+    entry = MockConfigEntry(
+        domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+    )
+
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    data = hass.data[DOMAIN][entry.entry_id]
+    send_commands_mock = data._client.send_commands
+
+    # No device provided
+    await _send_commands_and_wait(
+        hass, {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_COMMAND: PLAY_COMMAND}
+    )
+    send_commands_mock.assert_not_awaited()
+
+    # Tell the TV to play by id
+    await _send_commands_and_wait(
+        hass,
+        {
+            ATTR_ENTITY_ID: ENTITY_REMOTE,
+            ATTR_COMMAND: PLAY_COMMAND,
+            ATTR_DEVICE: TV_DEVICE_ID,
+        },
+    )
+
+    send_commands_mock.assert_awaited_once_with(
+        [
+            SendCommandDevice(
+                device=str(TV_DEVICE_ID),
+                command=PLAY_COMMAND,
+                delay=float(DEFAULT_HOLD_SECS),
+            ),
+            DEFAULT_DELAY_SECS,
+        ]
+    )
+    send_commands_mock.reset_mock()
+
+    # Tell the TV to play by name
+    await _send_commands_and_wait(
+        hass,
+        {
+            ATTR_ENTITY_ID: ENTITY_REMOTE,
+            ATTR_COMMAND: PLAY_COMMAND,
+            ATTR_DEVICE: TV_DEVICE_NAME,
+        },
+    )
+
+    send_commands_mock.assert_awaited_once_with(
+        [
+            SendCommandDevice(
+                device=TV_DEVICE_ID,
+                command=PLAY_COMMAND,
+                delay=float(DEFAULT_HOLD_SECS),
+            ),
+            DEFAULT_DELAY_SECS,
+        ]
+    )
+    send_commands_mock.reset_mock()
+
+    # Tell the TV to play and stop by name
+    await _send_commands_and_wait(
+        hass,
+        {
+            ATTR_ENTITY_ID: ENTITY_REMOTE,
+            ATTR_COMMAND: [PLAY_COMMAND, STOP_COMMAND],
+            ATTR_DEVICE: TV_DEVICE_NAME,
+        },
+    )
+
+    send_commands_mock.assert_awaited_once_with(
+        [
+            SendCommandDevice(
+                device=TV_DEVICE_ID,
+                command=PLAY_COMMAND,
+                delay=float(DEFAULT_HOLD_SECS),
+            ),
+            DEFAULT_DELAY_SECS,
+            SendCommandDevice(
+                device=TV_DEVICE_ID,
+                command=STOP_COMMAND,
+                delay=float(DEFAULT_HOLD_SECS),
+            ),
+            DEFAULT_DELAY_SECS,
+        ]
+    )
+    send_commands_mock.reset_mock()
+
+    # Tell the TV to play by name multiple times
+    await _send_commands_and_wait(
+        hass,
+        {
+            ATTR_ENTITY_ID: ENTITY_REMOTE,
+            ATTR_COMMAND: PLAY_COMMAND,
+            ATTR_DEVICE: TV_DEVICE_NAME,
+            ATTR_NUM_REPEATS: 2,
+        },
+    )
+
+    send_commands_mock.assert_awaited_once_with(
+        [
+            SendCommandDevice(
+                device=TV_DEVICE_ID,
+                command=PLAY_COMMAND,
+                delay=float(DEFAULT_HOLD_SECS),
+            ),
+            DEFAULT_DELAY_SECS,
+            SendCommandDevice(
+                device=TV_DEVICE_ID,
+                command=PLAY_COMMAND,
+                delay=float(DEFAULT_HOLD_SECS),
+            ),
+            DEFAULT_DELAY_SECS,
+        ]
+    )
+    send_commands_mock.reset_mock()
+
+    # Send commands to an unknown device
+    await _send_commands_and_wait(
+        hass,
+        {
+            ATTR_ENTITY_ID: ENTITY_REMOTE,
+            ATTR_COMMAND: PLAY_COMMAND,
+            ATTR_DEVICE: "no-such-device",
+        },
+    )
+    send_commands_mock.assert_not_awaited()
+    send_commands_mock.reset_mock()
+
+
+async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config):
+    """Ensure calls to send remote commands properly propagate to devices with custom delays."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_HOST: "192.0.2.0",
+            CONF_NAME: HUB_NAME,
+            ATTR_DELAY_SECS: DEFAULT_DELAY_SECS + 2,
+        },
+    )
+
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    data = hass.data[DOMAIN][entry.entry_id]
+    send_commands_mock = data._client.send_commands
+
+    # Tell the TV to play by id
+    await _send_commands_and_wait(
+        hass,
+        {
+            ATTR_ENTITY_ID: ENTITY_REMOTE,
+            ATTR_COMMAND: PLAY_COMMAND,
+            ATTR_DEVICE: TV_DEVICE_ID,
+        },
+    )
+
+    send_commands_mock.assert_awaited_once_with(
+        [
+            SendCommandDevice(
+                device=str(TV_DEVICE_ID),
+                command=PLAY_COMMAND,
+                delay=float(DEFAULT_HOLD_SECS),
+            ),
+            DEFAULT_DELAY_SECS + 2,
+        ]
+    )
+    send_commands_mock.reset_mock()
+
+
+async def test_change_channel(mock_hc, hass, mock_write_config):
+    """Test change channel commands."""
+    entry = MockConfigEntry(
+        domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+    )
+
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    data = hass.data[DOMAIN][entry.entry_id]
+    change_channel_mock = data._client.change_channel
+
+    # Tell the remote to change channels
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_CHANGE_CHANNEL,
+        {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_CHANNEL: 100},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    change_channel_mock.assert_awaited_once_with(100)
+
+
+async def test_sync(mock_hc, mock_write_config, hass):
+    """Test the sync command."""
+    entry = MockConfigEntry(
+        domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+    )
+
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    data = hass.data[DOMAIN][entry.entry_id]
+    sync_mock = data._client.sync
+
+    # Tell the remote to change channels
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_SYNC,
+        {ATTR_ENTITY_ID: ENTITY_REMOTE},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    sync_mock.assert_awaited_once()
+    mock_write_config.assert_called()
+
+
+async def _send_commands_and_wait(hass, service_data):
+    await hass.services.async_call(
+        REMOTE_DOMAIN,
+        SERVICE_SEND_COMMAND,
+        service_data,
+        blocking=True,
+    )
+    await hass.async_block_till_done()
diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py
index f43c9f6b478db8ffa34e6a8027573c7ea5206cdf..e5d0c6f0570ace790f6a59a69b497d02417ff587 100644
--- a/tests/components/harmony/test_config_flow.py
+++ b/tests/components/harmony/test_config_flow.py
@@ -1,5 +1,5 @@
 """Test the Logitech Harmony Hub config flow."""
-from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
+from unittest.mock import AsyncMock, MagicMock, patch
 
 from homeassistant import config_entries, data_entry_flow, setup
 from homeassistant.components.harmony.config_flow import CannotConnect
@@ -17,23 +17,6 @@ def _get_mock_harmonyapi(connect=None, close=None):
     return harmonyapi_mock
 
 
-def _get_mock_harmonyclient():
-    harmonyclient_mock = MagicMock()
-    type(harmonyclient_mock).connect = AsyncMock()
-    type(harmonyclient_mock).close = AsyncMock()
-    type(harmonyclient_mock).get_activity_name = MagicMock(return_value="Watch TV")
-    type(harmonyclient_mock.hub_config).activities = PropertyMock(
-        return_value=[{"name": "Watch TV", "id": 123}]
-    )
-    type(harmonyclient_mock.hub_config).devices = PropertyMock(
-        return_value=[{"name": "My TV", "id": 1234}]
-    )
-    type(harmonyclient_mock.hub_config).info = PropertyMock(return_value={})
-    type(harmonyclient_mock.hub_config).hub_state = PropertyMock(return_value={})
-
-    return harmonyclient_mock
-
-
 async def test_user_form(hass):
     """Test we get the user form."""
     await setup.async_setup_component(hass, "persistent_notification", {})
@@ -213,9 +196,8 @@ async def test_form_cannot_connect(hass):
     assert result2["errors"] == {"base": "cannot_connect"}
 
 
-async def test_options_flow(hass):
+async def test_options_flow(hass, mock_hc):
     """Test config flow options."""
-
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         unique_id="abcde12345",
@@ -223,19 +205,13 @@ async def test_options_flow(hass):
         options={"activity": "Watch TV", "delay_secs": 0.5},
     )
 
-    harmony_client = _get_mock_harmonyclient()
-
-    with patch(
-        "aioharmony.harmonyapi.HarmonyClient",
-        return_value=harmony_client,
-    ), patch("homeassistant.components.harmony.remote.HarmonyRemote.write_config_file"):
-        config_entry.add_to_hass(hass)
-        assert await hass.config_entries.async_setup(config_entry.entry_id)
-        await hass.async_block_till_done()
-        result = await hass.config_entries.options.async_init(config_entry.entry_id)
-        await hass.async_block_till_done()
-        assert await hass.config_entries.async_unload(config_entry.entry_id)
-        await hass.async_block_till_done()
+    config_entry.add_to_hass(hass)
+    assert await hass.config_entries.async_setup(config_entry.entry_id)
+    await hass.async_block_till_done()
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    await hass.async_block_till_done()
+    assert await hass.config_entries.async_unload(config_entry.entry_id)
+    await hass.async_block_till_done()
 
     assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
     assert result["step_id"] == "init"
diff --git a/tests/components/harmony/test_connection_changes.py b/tests/components/harmony/test_connection_changes.py
new file mode 100644
index 0000000000000000000000000000000000000000..15d462988555a0095a87d43a45680aa4ef6fb054
--- /dev/null
+++ b/tests/components/harmony/test_connection_changes.py
@@ -0,0 +1,67 @@
+"""Test the Logitech Harmony Hub entities with connection state changes."""
+
+from datetime import timedelta
+
+from homeassistant.components.harmony.const import DOMAIN
+from homeassistant.const import (
+    CONF_HOST,
+    CONF_NAME,
+    STATE_OFF,
+    STATE_ON,
+    STATE_UNAVAILABLE,
+)
+from homeassistant.util import utcnow
+
+from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+async def test_connection_state_changes(mock_hc, hass, mock_write_config):
+    """Ensure connection changes are reflected in the switch states."""
+    entry = MockConfigEntry(
+        domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
+    )
+
+    entry.add_to_hass(hass)
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    data = hass.data[DOMAIN][entry.entry_id]
+
+    # mocks start with current activity == Watch TV
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    data._disconnected()
+    await hass.async_block_till_done()
+
+    # Entities do not immediately show as unavailable
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    future_time = utcnow() + timedelta(seconds=10)
+    async_fire_time_changed(hass, future_time)
+    await hass.async_block_till_done()
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_UNAVAILABLE)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE)
+
+    data._connected()
+    await hass.async_block_till_done()
+
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
+
+    data._disconnected()
+    data._connected()
+    future_time = utcnow() + timedelta(seconds=10)
+    async_fire_time_changed(hass, future_time)
+
+    await hass.async_block_till_done()
+    assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
+    assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON)
+    assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF)
diff --git a/tests/components/harmony/test_subscriber.py b/tests/components/harmony/test_subscriber.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c357bef82509d7ede2a0ea23787579d863563f7
--- /dev/null
+++ b/tests/components/harmony/test_subscriber.py
@@ -0,0 +1,143 @@
+"""Test the HarmonySubscriberMixin class."""
+
+import asyncio
+from unittest.mock import AsyncMock, MagicMock
+
+from homeassistant.components.harmony.subscriber import (
+    HarmonyCallback,
+    HarmonySubscriberMixin,
+)
+
+_NO_PARAM_CALLBACKS = {
+    "connected": "_connected",
+    "disconnected": "_disconnected",
+    "config_updated": "_config_updated",
+}
+
+_ACTIVITY_CALLBACKS = {
+    "activity_starting": "_activity_starting",
+    "activity_started": "_activity_started",
+}
+
+_ALL_CALLBACK_NAMES = list(_NO_PARAM_CALLBACKS.keys()) + list(
+    _ACTIVITY_CALLBACKS.keys()
+)
+
+_ACTIVITY_TUPLE = ("not", "used")
+
+
+async def test_no_callbacks(hass):
+    """Ensure we handle no subscriptions."""
+    subscriber = HarmonySubscriberMixin(hass)
+    _call_all_callbacks(subscriber)
+    await hass.async_block_till_done()
+
+
+async def test_empty_callbacks(hass):
+    """Ensure we handle a missing callback in a subscription."""
+    subscriber = HarmonySubscriberMixin(hass)
+
+    callbacks = {k: None for k in _ALL_CALLBACK_NAMES}
+    subscriber.async_subscribe(HarmonyCallback(**callbacks))
+    _call_all_callbacks(subscriber)
+    await hass.async_block_till_done()
+
+
+async def test_async_callbacks(hass):
+    """Ensure we handle async callbacks."""
+    subscriber = HarmonySubscriberMixin(hass)
+
+    callbacks = {k: AsyncMock() for k in _ALL_CALLBACK_NAMES}
+    subscriber.async_subscribe(HarmonyCallback(**callbacks))
+    _call_all_callbacks(subscriber)
+    await hass.async_block_till_done()
+
+    for callback_name in _NO_PARAM_CALLBACKS.keys():
+        callback_mock = callbacks[callback_name]
+        callback_mock.assert_awaited_once()
+
+    for callback_name in _ACTIVITY_CALLBACKS.keys():
+        callback_mock = callbacks[callback_name]
+        callback_mock.assert_awaited_once_with(_ACTIVITY_TUPLE)
+
+
+async def test_long_async_callbacks(hass):
+    """Ensure we handle async callbacks that may have sleeps."""
+    subscriber = HarmonySubscriberMixin(hass)
+
+    blocker_event = asyncio.Event()
+    notifier_event_one = asyncio.Event()
+    notifier_event_two = asyncio.Event()
+
+    async def blocks_until_notified():
+        await blocker_event.wait()
+        notifier_event_one.set()
+
+    async def notifies_when_called():
+        notifier_event_two.set()
+
+    callbacks_one = {k: blocks_until_notified for k in _ALL_CALLBACK_NAMES}
+    callbacks_two = {k: notifies_when_called for k in _ALL_CALLBACK_NAMES}
+    subscriber.async_subscribe(HarmonyCallback(**callbacks_one))
+    subscriber.async_subscribe(HarmonyCallback(**callbacks_two))
+
+    subscriber._connected()
+    await notifier_event_two.wait()
+    blocker_event.set()
+    await notifier_event_one.wait()
+
+
+async def test_callbacks(hass):
+    """Ensure we handle non-async callbacks."""
+    subscriber = HarmonySubscriberMixin(hass)
+
+    callbacks = {k: MagicMock() for k in _ALL_CALLBACK_NAMES}
+    subscriber.async_subscribe(HarmonyCallback(**callbacks))
+    _call_all_callbacks(subscriber)
+    await hass.async_block_till_done()
+
+    for callback_name in _NO_PARAM_CALLBACKS.keys():
+        callback_mock = callbacks[callback_name]
+        callback_mock.assert_called_once()
+
+    for callback_name in _ACTIVITY_CALLBACKS.keys():
+        callback_mock = callbacks[callback_name]
+        callback_mock.assert_called_once_with(_ACTIVITY_TUPLE)
+
+
+async def test_subscribe_unsubscribe(hass):
+    """Ensure we handle subscriptions and unsubscriptions correctly."""
+    subscriber = HarmonySubscriberMixin(hass)
+
+    callback_one = {k: MagicMock() for k in _ALL_CALLBACK_NAMES}
+    unsub_one = subscriber.async_subscribe(HarmonyCallback(**callback_one))
+    callback_two = {k: MagicMock() for k in _ALL_CALLBACK_NAMES}
+    _ = subscriber.async_subscribe(HarmonyCallback(**callback_two))
+    callback_three = {k: MagicMock() for k in _ALL_CALLBACK_NAMES}
+    unsub_three = subscriber.async_subscribe(HarmonyCallback(**callback_three))
+
+    unsub_one()
+    unsub_three()
+
+    _call_all_callbacks(subscriber)
+    await hass.async_block_till_done()
+
+    for callback_name in _NO_PARAM_CALLBACKS.keys():
+        callback_one[callback_name].assert_not_called()
+        callback_two[callback_name].assert_called_once()
+        callback_three[callback_name].assert_not_called()
+
+    for callback_name in _ACTIVITY_CALLBACKS.keys():
+        callback_one[callback_name].assert_not_called()
+        callback_two[callback_name].assert_called_once_with(_ACTIVITY_TUPLE)
+        callback_three[callback_name].assert_not_called()
+
+
+def _call_all_callbacks(subscriber):
+    for callback_method in _NO_PARAM_CALLBACKS.values():
+        to_call = getattr(subscriber, callback_method)
+        to_call()
+
+    for callback_method in _ACTIVITY_CALLBACKS.values():
+        to_call = getattr(subscriber, callback_method)
+        to_call(_ACTIVITY_TUPLE)