diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/BluetoothDevice.vue b/platypush/backend/http/webapp/src/components/panels/Entities/BluetoothDevice.vue
new file mode 120000
index 000000000..792c76953
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/BluetoothDevice.vue
@@ -0,0 +1 @@
+Device.vue
\ No newline at end of file
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
index 2957dbb4e..4263b1c2a 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json
@@ -31,6 +31,14 @@
     }
   },
 
+  "bluetooth_device": {
+    "name": "Device",
+    "name_plural": "Devices",
+    "icon": {
+      "class": "fab fa-bluetooth-b"
+    }
+  },
+
   "device": {
     "name": "Device",
     "name_plural": "Devices",
diff --git a/platypush/entities/bluetooth.py b/platypush/entities/bluetooth.py
new file mode 100644
index 000000000..5dcd2df2e
--- /dev/null
+++ b/platypush/entities/bluetooth.py
@@ -0,0 +1,24 @@
+from sqlalchemy import Column, Integer, Boolean, ForeignKey
+
+from platypush.common.db import Base
+
+from .devices import Device
+
+
+if 'bluetooth_device' not in Base.metadata:
+
+    class BluetoothDevice(Device):
+        """
+        Entity that represents a Bluetooth device.
+        """
+
+        __tablename__ = 'bluetooth_device'
+
+        id = Column(
+            Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
+        )
+        connected = Column(Boolean, default=False)
+
+        __mapper_args__ = {
+            'polymorphic_identity': __tablename__,
+        }
diff --git a/platypush/message/event/bluetooth/ble.py b/platypush/message/event/bluetooth/ble.py
index ba3df9873..3d8b4e296 100644
--- a/platypush/message/event/bluetooth/ble.py
+++ b/platypush/message/event/bluetooth/ble.py
@@ -5,7 +5,31 @@ from platypush.message.event import Event
 
 class BluetoothEvent(Event):
     """
-    Base class for Bluetooth Low-Energy device events.
+    Base class for Bluetooth events.
+    """
+
+
+class BluetoothScanPausedEvent(BluetoothEvent):
+    """
+    Event triggered when the Bluetooth scan is paused.
+    """
+
+    def __init__(self, *args, duration: Optional[float] = None, **kwargs):
+        super().__init__(*args, duration=duration, **kwargs)
+
+
+class BluetoothScanResumedEvent(BluetoothEvent):
+    """
+    Event triggered when the Bluetooth scan is resumed.
+    """
+
+    def __init__(self, *args, duration: Optional[float] = None, **kwargs):
+        super().__init__(*args, duration=duration, **kwargs)
+
+
+class BluetoothDeviceEvent(BluetoothEvent):
+    """
+    Base class for Bluetooth device events.
     """
 
     def __init__(
@@ -17,7 +41,7 @@ class BluetoothEvent(Event):
         trusted: bool,
         blocked: bool,
         name: Optional[str] = None,
-        service_uuids: Optional[Collection[str]] = None,
+        characteristics: Optional[Collection[str]] = None,
         **kwargs
     ):
         """
@@ -27,7 +51,8 @@ class BluetoothEvent(Event):
         :param trusted: Whether the device is trusted.
         :param blocked: Whether the device is blocked.
         :param name: The name of the device.
-        :param service_uuids: The service UUIDs of the device.
+        :param characteristics: The UUIDs of the characteristics exposed by the
+            device.
         """
         super().__init__(
             *args,
@@ -37,66 +62,66 @@ class BluetoothEvent(Event):
             paired=paired,
             blocked=blocked,
             trusted=trusted,
-            service_uuids=service_uuids or [],
+            characteristics=characteristics or [],
             **kwargs
         )
 
 
-class BluetoothDeviceFoundEvent(BluetoothEvent):
+class BluetoothDeviceFoundEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is discovered during a scan.
     """
 
 
-class BluetoothDeviceLostEvent(BluetoothEvent):
+class BluetoothDeviceLostEvent(BluetoothDeviceEvent):
     """
     Event triggered when a previously discovered Bluetooth device is lost.
     """
 
 
-class BluetoothDeviceConnectedEvent(BluetoothEvent):
+class BluetoothDeviceConnectedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is connected.
     """
 
 
-class BluetoothDeviceDisconnectedEvent(BluetoothEvent):
+class BluetoothDeviceDisconnectedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is disconnected.
     """
 
 
-class BluetoothDevicePairedEvent(BluetoothEvent):
+class BluetoothDevicePairedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is paired.
     """
 
 
-class BluetoothDeviceUnpairedEvent(BluetoothEvent):
+class BluetoothDeviceUnpairedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is unpaired.
     """
 
 
-class BluetoothDeviceBlockedEvent(BluetoothEvent):
+class BluetoothDeviceBlockedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is blocked.
     """
 
 
-class BluetoothDeviceUnblockedEvent(BluetoothEvent):
+class BluetoothDeviceUnblockedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is unblocked.
     """
 
 
-class BluetoothDeviceTrustedEvent(BluetoothEvent):
+class BluetoothDeviceTrustedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is trusted.
     """
 
 
-class BluetoothDeviceUntrustedEvent(BluetoothEvent):
+class BluetoothDeviceUntrustedEvent(BluetoothDeviceEvent):
     """
     Event triggered when a Bluetooth device is untrusted.
     """
diff --git a/platypush/plugins/__init__.py b/platypush/plugins/__init__.py
index e77540d48..ea271cd2d 100644
--- a/platypush/plugins/__init__.py
+++ b/platypush/plugins/__init__.py
@@ -167,7 +167,7 @@ class AsyncRunnablePlugin(RunnablePlugin, ABC):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        self._loop: Optional[asyncio.AbstractEventLoop] = None
+        self._loop: Optional[asyncio.AbstractEventLoop] = asyncio.new_event_loop()
         self._task: Optional[asyncio.Task] = None
 
     @property
@@ -204,7 +204,6 @@ class AsyncRunnablePlugin(RunnablePlugin, ABC):
         """
         Initialize an event loop and run the listener as a task.
         """
-        self._loop = asyncio.new_event_loop()
         asyncio.set_event_loop(self._loop)
 
         self._task = self._loop.create_task(self._listen())
diff --git a/platypush/plugins/bluetooth/ble/__init__.py b/platypush/plugins/bluetooth/ble/__init__.py
index ebb65ad74..601240d09 100644
--- a/platypush/plugins/bluetooth/ble/__init__.py
+++ b/platypush/plugins/bluetooth/ble/__init__.py
@@ -1,6 +1,7 @@
 import base64
+from asyncio import Event, ensure_future
 from contextlib import asynccontextmanager
-from threading import RLock
+from threading import RLock, Timer
 from typing import AsyncGenerator, Collection, List, Optional, Dict, Type, Union
 from uuid import UUID
 
@@ -10,7 +11,7 @@ from typing_extensions import override
 
 from platypush.context import get_bus, get_or_create_event_loop
 from platypush.entities import Entity, EntityManager
-from platypush.entities.devices import Device
+from platypush.entities.bluetooth import BluetoothDevice
 from platypush.message.event.bluetooth.ble import (
     BluetoothDeviceBlockedEvent,
     BluetoothDeviceConnectedEvent,
@@ -22,7 +23,9 @@ from platypush.message.event.bluetooth.ble import (
     BluetoothDeviceUnblockedEvent,
     BluetoothDeviceUnpairedEvent,
     BluetoothDeviceUntrustedEvent,
-    BluetoothEvent,
+    BluetoothDeviceEvent,
+    BluetoothScanPausedEvent,
+    BluetoothScanResumedEvent,
 )
 from platypush.plugins import AsyncRunnablePlugin, action
 
@@ -52,7 +55,8 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
         interface: Optional[str] = None,
         connect_timeout: float = _default_connect_timeout,
         device_names: Optional[Dict[str, str]] = None,
-        service_uuids: Optional[Collection[UUIDType]] = None,
+        characteristics: Optional[Collection[UUIDType]] = None,
+        scan_paused_on_start: bool = False,
         **kwargs,
     ):
         """
@@ -60,7 +64,8 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
             on Linux). Default: first available interface.
         :param connect_timeout: Timeout in seconds for the connection to a
             Bluetooth device. Default: 5 seconds.
-        :param service_uuids: List of service UUIDs to discover. Default: all.
+        :param characteristics: List of service/characteristic UUIDs to
+            discover. Default: all.
         :param device_names: Bluetooth address -> device name mapping. If not
             specified, the device's advertised name will be used, or its
             Bluetooth address. Example:
@@ -73,13 +78,19 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
                         "00:11:22:33:44:57": "Button"
                     }
 
+        :param scan_paused_on_start: If ``True``, the plugin will not the
+            scanning thread until :meth:`.scan_resume` is called (default:
+            ``False``).
+
         """
         super().__init__(**kwargs)
 
         self._interface = interface
         self._connect_timeout = connect_timeout
-        self._service_uuids = service_uuids
+        self._characteristics = characteristics
         self._scan_lock = RLock()
+        self._scan_enabled = Event()
+        self._scan_controller_timer: Optional[Timer] = None
         self._connections: Dict[str, BleakClient] = {}
         self._devices: Dict[str, BLEDevice] = {}
         self._device_name_by_addr = device_names or {}
@@ -87,6 +98,9 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
             name: addr for addr, name in self._device_name_by_addr.items()
         }
 
+        if not scan_paused_on_start:
+            self._scan_enabled.set()
+
     async def _get_device(self, device: str) -> BLEDevice:
         """
         Utility method to get a device by name or address.
@@ -113,7 +127,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
         )
 
     def _post_event(
-        self, event_type: Type[BluetoothEvent], device: BLEDevice, **kwargs
+        self, event_type: Type[BluetoothDeviceEvent], device: BLEDevice, **kwargs
     ):
         props = device.details.get('props', {})
         get_bus().post(
@@ -124,13 +138,13 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
                 paired=props.get('Paired', False),
                 blocked=props.get('Blocked', False),
                 trusted=props.get('Trusted', False),
-                service_uuids=device.metadata.get('uuids', []),
+                characteristics=device.metadata.get('uuids', []),
                 **kwargs,
             )
         )
 
     def _on_device_event(self, device: BLEDevice, _):
-        event_types: List[Type[BluetoothEvent]] = []
+        event_types: List[Type[BluetoothDeviceEvent]] = []
         existing_device = self._devices.get(device.address)
 
         if existing_device:
@@ -168,6 +182,9 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
             event_types.append(BluetoothDeviceFoundEvent)
 
         self._devices[device.address] = device
+        if device.name:
+            self._device_name_by_addr[device.address] = device.name
+            self._device_addr_by_name[device.name] = device.address
 
         if event_types:
             for event_type in event_types:
@@ -217,7 +234,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
     async def _scan(
         self,
         duration: Optional[float] = None,
-        service_uuids: Optional[Collection[UUIDType]] = None,
+        characteristics: Optional[Collection[UUIDType]] = None,
         publish_entities: bool = False,
     ) -> Collection[Entity]:
         with self._scan_lock:
@@ -226,7 +243,7 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
                 adapter=self._interface,
                 timeout=timeout,
                 service_uuids=list(
-                    map(str, service_uuids or self._service_uuids or [])
+                    map(str, characteristics or self._characteristics or [])
                 ),
                 detection_callback=self._on_device_event,
             )
@@ -234,29 +251,75 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
             # TODO Infer type from device.metadata['manufacturer_data']
 
         self._devices.update({dev.address: dev for dev in devices})
-        if publish_entities:
-            entities = self.publish_entities(devices)
-        else:
-            entities = self.transform_entities(devices)
+        return (
+            self.publish_entities(devices)
+            if publish_entities
+            else self.transform_entities(devices)
+        )
 
-        return entities
+    async def _scan_state_set(self, state: bool, duration: Optional[float] = None):
+        def timer_callback():
+            if state:
+                self.scan_pause()
+            else:
+                self.scan_resume()
+
+            self._scan_controller_timer = None
+
+        with self._scan_lock:
+            if not state and self._scan_enabled.is_set():
+                get_bus().post(BluetoothScanPausedEvent(duration=duration))
+            elif state and not self._scan_enabled.is_set():
+                get_bus().post(BluetoothScanResumedEvent(duration=duration))
+
+            if state:
+                self._scan_enabled.set()
+            else:
+                self._scan_enabled.clear()
+
+            if duration and not self._scan_controller_timer:
+                self._scan_controller_timer = Timer(duration, timer_callback)
+                self._scan_controller_timer.start()
+
+    @action
+    def scan_pause(self, duration: Optional[float] = None):
+        """
+        Pause the scanning thread.
+
+        :param duration: For how long the scanning thread should be paused
+            (default: null = indefinitely).
+        """
+        if self._loop:
+            ensure_future(self._scan_state_set(False, duration), loop=self._loop)
+
+    @action
+    def scan_resume(self, duration: Optional[float] = None):
+        """
+        Resume the scanning thread, if inactive.
+
+        :param duration: For how long the scanning thread should be running
+            (default: null = indefinitely).
+        """
+        if self._loop:
+            ensure_future(self._scan_state_set(True, duration), loop=self._loop)
 
     @action
     def scan(
         self,
         duration: Optional[float] = None,
-        service_uuids: Optional[Collection[UUIDType]] = None,
+        characteristics: Optional[Collection[UUIDType]] = None,
     ):
         """
-        Scan for Bluetooth devices nearby.
+        Scan for Bluetooth devices nearby and return the results as a list of
+        entities.
 
         :param duration: Scan duration in seconds (default: same as the plugin's
             `poll_interval` configuration parameter)
-        :param service_uuids: List of service UUIDs to discover. Default: all.
+        :param characteristics: List of characteristic UUIDs to discover. Default: all.
         """
         loop = get_or_create_event_loop()
-        loop.run_until_complete(
-            self._scan(duration, service_uuids, publish_entities=True)
+        return loop.run_until_complete(
+            self._scan(duration, characteristics, publish_entities=True)
         )
 
     @action
@@ -319,9 +382,11 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
         return self.scan().output
 
     @override
-    def transform_entities(self, entities: Collection[BLEDevice]) -> Collection[Device]:
+    def transform_entities(
+        self, entities: Collection[BLEDevice]
+    ) -> Collection[BluetoothDevice]:
         return [
-            Device(
+            BluetoothDevice(
                 id=dev.address,
                 name=self._get_device_name(dev),
             )
@@ -333,7 +398,9 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
         device_addresses = set()
 
         while True:
+            await self._scan_enabled.wait()
             entities = await self._scan()
+
             new_device_addresses = {e.id for e in entities}
             missing_device_addresses = device_addresses - new_device_addresses
             missing_devices = [
@@ -348,5 +415,12 @@ class BluetoothBlePlugin(AsyncRunnablePlugin, EntityManager):
 
             device_addresses = new_device_addresses
 
+    @override
+    def stop(self):
+        if self._scan_controller_timer:
+            self._scan_controller_timer.cancel()
+
+        super().stop()
+
 
 # vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/switchbot/bluetooth/__init__.py b/platypush/plugins/switchbot/bluetooth/__init__.py
index 2663bbf6d..7a920a7fe 100644
--- a/platypush/plugins/switchbot/bluetooth/__init__.py
+++ b/platypush/plugins/switchbot/bluetooth/__init__.py
@@ -54,7 +54,7 @@ class SwitchbotBluetoothPlugin(BluetoothBlePlugin, EnumSwitchEntityManager):
     }
 
     def __init__(self, *args, **kwargs):
-        super().__init__(*args, service_uuids=self._uuids.values(), **kwargs)
+        super().__init__(*args, characteristics=self._uuids.values(), **kwargs)
 
     async def _run(
         self,