diff --git a/platypush/plugins/bluetooth/_ble/_event_handler.py b/platypush/plugins/bluetooth/_ble/_event_handler.py
index 7b9f67944..fc1125fba 100644
--- a/platypush/plugins/bluetooth/_ble/_event_handler.py
+++ b/platypush/plugins/bluetooth/_ble/_event_handler.py
@@ -19,6 +19,7 @@ from platypush.message.event.bluetooth import (
 
 from .._cache import EntityCache
 from .._model import ServiceClass
+from .._plugins import BaseBluetoothPlugin
 from ._cache import DeviceCache
 from ._mappers import device_to_entity
 
@@ -89,7 +90,7 @@ event_matchers: Dict[
     or (old.reachable is False and new.reachable is True),
     BluetoothDeviceSignalUpdateEvent: lambda old, new: (
         (new.rssi is not None or new.tx_power is not None)
-        and (old and old.rssi)
+        and bool(old and old.rssi)
         and (
             _has_changed(old, new, 'rssi', tolerance=5)
             or _has_changed(old, new, 'tx_power', tolerance=5)
@@ -115,6 +116,7 @@ class EventHandler:
         device_queue: Queue,
         device_cache: DeviceCache,
         entity_cache: EntityCache,
+        plugins: Collection[BaseBluetoothPlugin],
         exclude_known_noisy_beacons: bool,
     ):
         """
@@ -126,6 +128,7 @@ class EventHandler:
         self._device_queue = device_queue
         self._device_cache = device_cache
         self._entity_cache = entity_cache
+        self._plugins = plugins
         self._exclude_known_noisy_beacons = exclude_known_noisy_beacons
 
     def __call__(self, device: BLEDevice, data: AdvertisementData):
@@ -155,6 +158,10 @@ class EventHandler:
             )
             return
 
+        # Extend the new entity with children entities added by the plugins
+        for plugin in self._plugins:
+            plugin.extend_device(new_entity)
+
         events: List[BluetoothDeviceEvent] = []
         existing_entity = self._entity_cache.get(device.address)
         events += [
@@ -195,7 +202,7 @@ class EventHandler:
             return False
 
         mapped_uuids = [
-            int(str(srv.uuid).split('-')[0], 16) & 0xFFFF
+            int(str(srv.uuid).split('-', maxsplit=1)[0], 16) & 0xFFFF
             if isinstance(srv.uuid, UUID)
             else srv.uuid
             for srv in device.services
diff --git a/platypush/plugins/bluetooth/_ble/_mappers.py b/platypush/plugins/bluetooth/_ble/_mappers.py
index de227d8bc..0eda4570c 100644
--- a/platypush/plugins/bluetooth/_ble/_mappers.py
+++ b/platypush/plugins/bluetooth/_ble/_mappers.py
@@ -210,7 +210,7 @@ def _parse_services(
         srv_cls = ServiceClass.get(uuid)
         services.append(
             BluetoothService(
-                id=f'{device.address}:{uuid}',
+                id=f'{device.address}::{uuid}',
                 uuid=uuid,
                 name=f'[{uuid}]' if srv_cls == ServiceClass.UNKNOWN else str(srv_cls),
                 protocol=Protocol.L2CAP,
diff --git a/platypush/plugins/bluetooth/_ble/_plugins/__init__.py b/platypush/plugins/bluetooth/_ble/_plugins/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/platypush/plugins/bluetooth/_manager.py b/platypush/plugins/bluetooth/_manager.py
index 7c1e4689a..5941315e2 100644
--- a/platypush/plugins/bluetooth/_manager.py
+++ b/platypush/plugins/bluetooth/_manager.py
@@ -43,6 +43,8 @@ class BaseBluetoothManager(ABC, threading.Thread):
         :param device_cache: Cache used to keep track of discovered devices.
         :param exclude_known_noisy_beacons: Exclude known noisy beacons.
         """
+        from ._plugins import scan_plugins
+
         kwargs['name'] = f'Bluetooth:{self.__class__.__name__}'
         super().__init__(**kwargs)
 
@@ -59,6 +61,8 @@ class BaseBluetoothManager(ABC, threading.Thread):
 
         self._cache = device_cache or EntityCache()
         """ Cache of discovered devices. """
+        self._plugins = scan_plugins(self)
+        """ Plugins compatible with this manager. """
 
     def notify(
         self, event_type: Type[BluetoothDeviceEvent], device: BluetoothDevice, **kwargs
@@ -71,6 +75,10 @@ class BaseBluetoothManager(ABC, threading.Thread):
         get_bus().post(event_type.from_device(device=device, **kwargs))
         self._device_queue.put_nowait(device)
 
+    @property
+    def plugins(self):
+        return self._plugins
+
     def should_stop(self) -> bool:
         return self._stop_event.is_set()
 
diff --git a/platypush/plugins/bluetooth/_plugins/__init__.py b/platypush/plugins/bluetooth/_plugins/__init__.py
new file mode 100644
index 000000000..ff7e2dc2a
--- /dev/null
+++ b/platypush/plugins/bluetooth/_plugins/__init__.py
@@ -0,0 +1,4 @@
+from ._base import BaseBluetoothPlugin
+from ._scanner import scan_plugins
+
+__all__ = ['BaseBluetoothPlugin', 'scan_plugins']
diff --git a/platypush/plugins/bluetooth/_plugins/_base.py b/platypush/plugins/bluetooth/_plugins/_base.py
new file mode 100644
index 000000000..51b03eeed
--- /dev/null
+++ b/platypush/plugins/bluetooth/_plugins/_base.py
@@ -0,0 +1,39 @@
+from abc import ABC, abstractmethod
+from typing import Iterable
+
+from platypush.entities import Entity
+from platypush.entities.bluetooth import BluetoothDevice
+from platypush.plugins.bluetooth._manager import BaseBluetoothManager
+
+
+class BaseBluetoothPlugin(ABC):
+    """
+    Base class for Bluetooth plugins, like Switchbot or HID integrations.
+    """
+
+    def __init__(self, manager: BaseBluetoothManager):
+        self._manager = manager
+
+    @abstractmethod
+    def supports_device(self, device: BluetoothDevice) -> bool:
+        """
+        Returns True if the given device matches this plugin.
+        """
+
+    @abstractmethod
+    def _extract_entities(self, device: BluetoothDevice) -> Iterable[Entity]:
+        """
+        If :meth:`_matches_device` returns True, this method should return an
+        iterable of entities that will be used to extend the existing device -
+        like sensors, switches etc.
+        """
+
+    def extend_device(self, device: BluetoothDevice):
+        """
+        Extends the given device with entities extracted through
+        :meth:`_extract_entities`, if :meth:`_matches_device` returns True.
+        """
+        if not self.supports_device(device):
+            return
+
+        device.children.extend(self._extract_entities(device))
diff --git a/platypush/plugins/bluetooth/_plugins/_scanner.py b/platypush/plugins/bluetooth/_plugins/_scanner.py
new file mode 100644
index 000000000..b078e97f9
--- /dev/null
+++ b/platypush/plugins/bluetooth/_plugins/_scanner.py
@@ -0,0 +1,42 @@
+import importlib
+import inspect
+import logging
+import os
+import pkgutil
+from typing import List
+
+from platypush.plugins.bluetooth._plugins import BaseBluetoothPlugin
+
+logger = logging.getLogger(__name__)
+
+
+def scan_plugins(manager) -> List[BaseBluetoothPlugin]:
+    """
+    Initializes all the plugins associated to the given BluetoothManager by
+    scanning all the modules under the ``_plugins`` folder in the manager's
+    package.
+    """
+    plugins = {}
+    base_dir = os.path.dirname(inspect.getfile(manager.__class__))
+    module = inspect.getmodule(manager.__class__)
+    assert module is not None
+    package = module.__package__
+    assert package is not None
+
+    for _, mod_name, _ in pkgutil.walk_packages([base_dir], prefix=package + '.'):
+        try:
+            module = importlib.import_module(mod_name)
+        except Exception as e:
+            logger.warning('Could not import module %s', mod_name)
+            logger.exception(e)
+            continue
+
+        for _, obj in inspect.getmembers(module):
+            if (
+                inspect.isclass(obj)
+                and not inspect.isabstract(obj)
+                and issubclass(obj, BaseBluetoothPlugin)
+            ):
+                plugins[obj] = obj(manager)
+
+    return list(plugins.values())