diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py
index f0739f9a073c2290e470c8636a8b4c844e53d6be..7ef42ef7da7aa20561c84d14eb047fe8fc9a44c6 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
 https://home-assistant.io/components/zha/
 """
 
+import asyncio
 import logging
 import voluptuous as vol
 
@@ -14,6 +15,7 @@ from .core.const import (
     DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
     ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
     CLIENT_COMMANDS, SERVER_COMMANDS, SERVER, NAME, ATTR_ENDPOINT_ID)
+from .core.helpers import get_matched_clusters, async_is_bindable_target
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -26,11 +28,18 @@ DEVICE_INFO = 'device_info'
 ATTR_DURATION = 'duration'
 ATTR_IEEE_ADDRESS = 'ieee_address'
 ATTR_IEEE = 'ieee'
+ATTR_SOURCE_IEEE = 'source_ieee'
+ATTR_TARGET_IEEE = 'target_ieee'
+BIND_REQUEST = 0x0021
+UNBIND_REQUEST = 0x0022
 
 SERVICE_PERMIT = 'permit'
 SERVICE_REMOVE = 'remove'
 SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = 'set_zigbee_cluster_attribute'
 SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = 'issue_zigbee_cluster_command'
+SERVICE_DIRECT_ZIGBEE_BIND = 'issue_direct_zigbee_bind'
+SERVICE_DIRECT_ZIGBEE_UNBIND = 'issue_direct_zigbee_unbind'
+SERVICE_ZIGBEE_BIND = 'service_zigbee_bind'
 IEEE_SERVICE = 'ieee_based_service'
 
 SERVICE_SCHEMAS = {
@@ -110,6 +119,26 @@ SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
     vol.Required(ATTR_CLUSTER_TYPE): str
 })
 
+WS_BIND_DEVICE = 'zha/devices/bind'
+SCHEMA_WS_BIND_DEVICE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+    vol.Required(TYPE): WS_BIND_DEVICE,
+    vol.Required(ATTR_SOURCE_IEEE): str,
+    vol.Required(ATTR_TARGET_IEEE): str
+})
+
+WS_UNBIND_DEVICE = 'zha/devices/unbind'
+SCHEMA_WS_UNBIND_DEVICE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+    vol.Required(TYPE): WS_UNBIND_DEVICE,
+    vol.Required(ATTR_SOURCE_IEEE): str,
+    vol.Required(ATTR_TARGET_IEEE): str
+})
+
+WS_BINDABLE_DEVICES = 'zha/devices/bindable'
+SCHEMA_WS_BINDABLE_DEVICES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+    vol.Required(TYPE): WS_BINDABLE_DEVICES,
+    vol.Required(ATTR_IEEE): str
+})
+
 
 def async_load_api(hass, application_controller, zha_gateway):
     """Set up the web socket API."""
@@ -244,6 +273,103 @@ def async_load_api(hass, application_controller, zha_gateway):
         SCHEMA_WS_RECONFIGURE_NODE
     )
 
+    @websocket_api.async_response
+    async def websocket_get_bindable_devices(hass, connection, msg):
+        """Directly bind devices."""
+        source_ieee = msg[ATTR_IEEE]
+        source_device = zha_gateway.get_device(source_ieee)
+        devices = [
+            {
+                **device.device_info
+            } for device in zha_gateway.devices.values() if
+            async_is_bindable_target(source_device, device)
+        ]
+
+        _LOGGER.debug("Get bindable devices: %s %s",
+                      "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
+                      "{}: [{}]".format('bindable devices:', devices)
+                      )
+
+        connection.send_message(websocket_api.result_message(
+            msg[ID],
+            devices
+        ))
+
+    hass.components.websocket_api.async_register_command(
+        WS_BINDABLE_DEVICES, websocket_get_bindable_devices,
+        SCHEMA_WS_BINDABLE_DEVICES
+    )
+
+    @websocket_api.async_response
+    async def websocket_bind_devices(hass, connection, msg):
+        """Directly bind devices."""
+        source_ieee = msg[ATTR_SOURCE_IEEE]
+        target_ieee = msg[ATTR_TARGET_IEEE]
+        await async_binding_operation(
+            source_ieee, target_ieee, BIND_REQUEST)
+        _LOGGER.info("Issue bind devices: %s %s",
+                     "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
+                     "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee)
+                     )
+
+    hass.components.websocket_api.async_register_command(
+        WS_BIND_DEVICE, websocket_bind_devices,
+        SCHEMA_WS_BIND_DEVICE
+    )
+
+    @websocket_api.async_response
+    async def websocket_unbind_devices(hass, connection, msg):
+        """Remove a direct binding between devices."""
+        source_ieee = msg[ATTR_SOURCE_IEEE]
+        target_ieee = msg[ATTR_TARGET_IEEE]
+        await async_binding_operation(
+            source_ieee, target_ieee, UNBIND_REQUEST)
+        _LOGGER.info("Issue unbind devices: %s %s",
+                     "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
+                     "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee)
+                     )
+
+    hass.components.websocket_api.async_register_command(
+        WS_UNBIND_DEVICE, websocket_unbind_devices,
+        SCHEMA_WS_UNBIND_DEVICE
+    )
+
+    async def async_binding_operation(source_ieee, target_ieee,
+                                      operation):
+        """Create or remove a direct zigbee binding between 2 devices."""
+        from zigpy.zdo import types as zdo_types
+        source_device = zha_gateway.get_device(source_ieee)
+        target_device = zha_gateway.get_device(target_ieee)
+
+        clusters_to_bind = await get_matched_clusters(source_device,
+                                                      target_device)
+
+        bind_tasks = []
+        for cluster_pair in clusters_to_bind:
+            destination_address = zdo_types.MultiAddress()
+            destination_address.addrmode = 3
+            destination_address.ieee = target_device.ieee
+            destination_address.endpoint = \
+                cluster_pair.target_cluster.endpoint.endpoint_id
+
+            zdo = cluster_pair.source_cluster.endpoint.device.zdo
+
+            _LOGGER.debug("processing binding operation for: %s %s %s",
+                          "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee),
+                          "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee),
+                          "{}: {}".format(
+                              'cluster',
+                              cluster_pair.source_cluster.cluster_id)
+                          )
+            bind_tasks.append(zdo.request(
+                operation,
+                source_device.ieee,
+                cluster_pair.source_cluster.endpoint.endpoint_id,
+                cluster_pair.source_cluster.cluster_id,
+                destination_address
+            ))
+        await asyncio.gather(*bind_tasks)
+
     @websocket_api.async_response
     async def websocket_device_clusters(hass, connection, msg):
         """Return a list of device clusters."""
diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py
index d1001682c7b4e82ff5b3ee4819df3fc8d7ad02c8..3c8adb097482117f0deef03be126dd48d59396d2 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -114,6 +114,7 @@ CUSTOM_CLUSTER_MAPPINGS = {}
 COMPONENT_CLUSTERS = {}
 EVENT_RELAY_CLUSTERS = []
 NO_SENSOR_CLUSTERS = []
+BINDABLE_CLUSTERS = []
 
 REPORT_CONFIG_MAX_INT = 900
 REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index 102c9bed2d3703b1668c4f0d9cbbcb3ee884066e..182a08357b6cd09652a9c444d924a115c27028ee 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -242,6 +242,18 @@ class ZHADevice:
             if ep_id != 0
         }
 
+    @callback
+    def async_get_zha_clusters(self):
+        """Get zigbee home automation clusters for this device."""
+        from zigpy.profiles.zha import PROFILE_ID
+        return {
+            ep_id: {
+                IN: endpoint.in_clusters,
+                OUT: endpoint.out_clusters
+            } for (ep_id, endpoint) in self._zigpy_device.endpoints.items()
+            if ep_id != 0 and endpoint.profile_id == PROFILE_ID
+        }
+
     @callback
     def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=IN):
         """Get zigbee cluster from this entity."""
diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index 563543fa4bd8a1d60658abd9899c32a77edbd660..35a79311253e75eb9bbaa7c5962e3a132c71c285 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -18,11 +18,11 @@ from .const import (
     ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
     SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY,
     TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
-    GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, UNKNOWN,
-    OPENING, ZONE, OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE,
+    GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, UNKNOWN, OPENING, ZONE,
+    OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE,
     REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT,
-    REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS,
-    POWER_CONFIGURATION_CHANNEL)
+    REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE,
+    NO_SENSOR_CLUSTERS, POWER_CONFIGURATION_CHANNEL, BINDABLE_CLUSTERS)
 from .device import ZHADevice, DeviceStatus
 from ..device_entity import ZhaDeviceEntity
 from .channels import (
@@ -450,6 +450,8 @@ def establish_device_mappings():
     NO_SENSOR_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id)
     NO_SENSOR_CLUSTERS.append(
         zcl.clusters.general.PowerConfiguration.cluster_id)
+    BINDABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
+    BINDABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
 
     DEVICE_CLASS[zha.PROFILE_ID].update({
         zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py
index 643e44ada1bffdf1bb81634338feec80b3869163..d6e9cc32338d508bb5e94d93a8d246dffa8dc39e 100644
--- a/homeassistant/components/zha/core/helpers.py
+++ b/homeassistant/components/zha/core/helpers.py
@@ -5,14 +5,19 @@ For more details about this component, please refer to the documentation at
 https://home-assistant.io/components/zha/
 """
 import asyncio
+import collections
 import logging
 from concurrent.futures import TimeoutError as Timeout
+from homeassistant.core import callback
 from .const import (
     DEFAULT_BAUDRATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT,
-    REPORT_CONFIG_RPT_CHANGE, RadioType)
+    REPORT_CONFIG_RPT_CHANGE, RadioType, IN, OUT, BINDABLE_CLUSTERS)
 
 _LOGGER = logging.getLogger(__name__)
 
+ClusterPair = collections.namedtuple(
+    'ClusterPair', 'source_cluster target_cluster')
+
 
 async def safe_read(cluster, attributes, allow_cache=True, only_cache=False,
                     manufacturer=None):
@@ -157,3 +162,44 @@ def get_attr_id_by_name(cluster, attr_name):
     """Get the attribute id for a cluster attribute by its name."""
     return next((attrid for attrid, (attrname, datatype) in
                  cluster.attributes.items() if attr_name == attrname), None)
+
+
+async def get_matched_clusters(source_zha_device, target_zha_device):
+    """Get matched input/output cluster pairs for 2 devices."""
+    source_clusters = source_zha_device.async_get_zha_clusters()
+    target_clusters = target_zha_device.async_get_zha_clusters()
+    clusters_to_bind = []
+
+    for endpoint_id in source_clusters:
+        for cluster_id in source_clusters[endpoint_id][OUT]:
+            if cluster_id not in BINDABLE_CLUSTERS:
+                continue
+            for t_endpoint_id in target_clusters:
+                if cluster_id in target_clusters[t_endpoint_id][IN]:
+                    cluster_pair = ClusterPair(
+                        source_cluster=source_clusters[
+                            endpoint_id][OUT][cluster_id],
+                        target_cluster=target_clusters[
+                            t_endpoint_id][IN][cluster_id]
+                    )
+                    clusters_to_bind.append(cluster_pair)
+    return clusters_to_bind
+
+
+@callback
+def async_is_bindable_target(source_zha_device, target_zha_device):
+    """Determine if target is bindable to source."""
+    source_clusters = source_zha_device.async_get_zha_clusters()
+    target_clusters = target_zha_device.async_get_zha_clusters()
+
+    bindables = set(BINDABLE_CLUSTERS)
+    for endpoint_id in source_clusters:
+        for t_endpoint_id in target_clusters:
+            matches = set(
+                source_clusters[endpoint_id][OUT].keys()
+                ).intersection(
+                    target_clusters[t_endpoint_id][IN].keys()
+                )
+            if any(bindable in bindables for bindable in matches):
+                return True
+    return False