diff --git a/examples/conf/config.yaml b/examples/conf/config.yaml
index 72d831d7f..fdcb16791 100644
--- a/examples/conf/config.yaml
+++ b/examples/conf/config.yaml
@@ -21,6 +21,12 @@ include:
     - include/media.yaml
     - include/sensors.yaml
 
+# platypush logs on stdout by default. You can use the logging section to specify
+# an alternative file or change the logging level.
+logging:
+    filename: ~/.local/log/platypush/platypush.log
+    level: INFO
+
 # The device_id is used by many components of platypush and it should uniquely
 # identify a device in your network. If nothing is specified then the hostname
 # will be used.
diff --git a/platypush/backend/http/app/routes/index.py b/platypush/backend/http/app/routes/index.py
index 375344a2c..ea3e64248 100644
--- a/platypush/backend/http/app/routes/index.py
+++ b/platypush/backend/http/app/routes/index.py
@@ -9,7 +9,6 @@ from platypush.backend.http.app.utils import authenticate, get_websocket_port
 from platypush.backend.http.utils import HttpUtils
 from platypush.config import Config
 
-
 index = Blueprint('index', __name__, template_folder=template_folder)
 
 # Declare routes list
diff --git a/platypush/message/response/__init__.py b/platypush/message/response/__init__.py
index 7faa8c99c..d6227fe9e 100644
--- a/platypush/message/response/__init__.py
+++ b/platypush/message/response/__init__.py
@@ -3,10 +3,11 @@ import time
 
 from platypush.message import Message
 
+
 class Response(Message):
     """ Response message class """
 
-    def __init__(self, target=None, origin=None, id=None, output=None, errors=[],
+    def __init__(self, target=None, origin=None, id=None, output=None, errors=None,
                  timestamp=None, disable_logging=False):
         """
         Params:
@@ -21,13 +22,13 @@ class Response(Message):
         super().__init__(timestamp=timestamp)
         self.target = target
         self.output = self._parse_msg(output)
-        self.errors = self._parse_msg(errors)
+        self.errors = self._parse_msg(errors or [])
         self.origin = origin
         self.id = id
         self.disable_logging = disable_logging
 
     def is_error(self):
-        """ Returns True if the respopnse has errors """
+        """ Returns True if the response has errors """
         return len(self.errors) != 0
 
     @classmethod
@@ -35,27 +36,30 @@ class Response(Message):
         if isinstance(msg, bytes) or isinstance(msg, bytearray):
             msg = msg.decode('utf-8')
         if isinstance(msg, str):
-            try: msg = json.loads(msg.strip())
-            except ValueError as e: pass
+            try:
+                msg = json.loads(msg.strip())
+            except ValueError:
+                pass
 
         return msg
 
-
     @classmethod
     def build(cls, msg):
         msg = super().parse(msg)
         args = {
-            'target' : msg['target'],
-            'output' : msg['response']['output'],
-            'errors' : msg['response']['errors'],
+            'target': msg['target'],
+            'output': msg['response']['output'],
+            'errors': msg['response']['errors'],
+            'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time(),
+            'disable_logging': msg.get('_disable_logging', False),
         }
 
-        args['timestamp'] = msg['_timestamp'] if '_timestamp' in msg else time.time()
-        args['disable_logging'] = msg.get('_disable_logging', False)
-        if 'id' in msg: args['id'] = msg['id']
-        if 'origin' in msg: args['origin'] = msg['origin']
-        return cls(**args)
+        if 'id' in msg:
+            args['id'] = msg['id']
+        if 'origin' in msg:
+            args['origin'] = msg['origin']
 
+        return cls(**args)
 
     def __str__(self):
         """
@@ -64,14 +68,14 @@ class Response(Message):
         """
 
         response_dict = {
-            'id'         : self.id,
-            'type'       : 'response',
-            'target'     : self.target if hasattr(self, 'target') else None,
-            'origin'     : self.origin if hasattr(self, 'origin') else None,
-            '_timestamp' : self.timestamp,
-            'response'   : {
-                'output' : self.output,
-                'errors' : self.errors,
+            'id': self.id,
+            'type': 'response',
+            'target': self.target if hasattr(self, 'target') else None,
+            'origin': self.origin if hasattr(self, 'origin') else None,
+            '_timestamp': self.timestamp,
+            'response': {
+                'output': self.output,
+                'errors': self.errors,
             },
         }
 
@@ -80,5 +84,4 @@ class Response(Message):
 
         return json.dumps(response_dict)
 
-
 # vim:sw=4:ts=4:et:
diff --git a/platypush/message/response/bluetooth.py b/platypush/message/response/bluetooth.py
new file mode 100644
index 000000000..69e3cc754
--- /dev/null
+++ b/platypush/message/response/bluetooth.py
@@ -0,0 +1,101 @@
+from platypush.message.response import Response
+
+
+class BluetoothResponse(Response):
+    pass
+
+
+class BluetoothScanResponse(BluetoothResponse):
+    def __init__(self, devices, *args, **kwargs):
+        if isinstance(devices, list):
+            self.devices = [
+                {
+                    'addr': dev[0],
+                    'name': dev[1] if len(dev) > 1 else None,
+                    'class': hex(dev[2]) if len(dev) > 2 else None,
+                }
+                for dev in devices
+            ]
+        elif isinstance(devices, dict):
+            self.devices = [
+                {
+                    'addr': addr,
+                    'name': name or None,
+                    'class': 'BLE',
+                }
+                for addr, name in devices.items()
+            ]
+        else:
+            raise ValueError('devices must be either a list of tuples or a dict')
+
+        super().__init__(output=self.devices, *args, **kwargs)
+
+
+class BluetoothLookupNameResponse(BluetoothResponse):
+    def __init__(self, addr: str, name: str, *args, **kwargs):
+        self.addr = addr
+        self.name = name
+        super().__init__(output={
+            'addr': self.addr,
+            'name': self.name
+        }, *args, **kwargs)
+
+
+class BluetoothLookupServiceResponse(BluetoothResponse):
+    """
+    Example services response output::
+
+        [
+          {
+            "service-classes": [
+              "1801"
+            ],
+            "profiles": [],
+            "name": "Service name #1",
+            "description": null,
+            "provider": null,
+            "service-id": null,
+            "protocol": "L2CAP",
+            "port": 31,
+            "host": "00:11:22:33:44:55"
+          },
+          {
+            "service-classes": [
+              "1800"
+            ],
+            "profiles": [],
+            "name": "Service name #2",
+            "description": null,
+            "provider": null,
+            "service-id": null,
+            "protocol": "L2CAP",
+            "port": 31,
+            "host": "00:11:22:33:44:56"
+          },
+          {
+            "service-classes": [
+              "1112",
+              "1203"
+            ],
+            "profiles": [
+              [
+                "1108",
+                258
+              ]
+            ],
+            "name": "Headset Gateway",
+            "description": null,
+            "provider": null,
+            "service-id": null,
+            "protocol": "RFCOMM",
+            "port": 2,
+            "host": "00:11:22:33:44:57"
+          }
+        ]
+    """
+    def __init__(self, services: list, *args, **kwargs):
+        self.services = services
+        super().__init__(output=self.services, *args, **kwargs)
+
+
+# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/bluetooth/__init__.py b/platypush/plugins/bluetooth/__init__.py
new file mode 100644
index 000000000..4c9824cc2
--- /dev/null
+++ b/platypush/plugins/bluetooth/__init__.py
@@ -0,0 +1,415 @@
+import base64
+import os
+import re
+import select
+
+from platypush.plugins import Plugin, action
+from platypush.message.response.bluetooth import BluetoothScanResponse, \
+    BluetoothLookupNameResponse, BluetoothLookupServiceResponse, BluetoothResponse
+
+
+class BluetoothPlugin(Plugin):
+    """
+    Bluetooth plugin
+
+    Requires:
+
+        * **pybluez** (``pip install pybluez``)
+        * **pyobex** (``pip install pyobex``) [optional] for file transfer support
+
+    """
+
+    import bluetooth
+
+    class _DeviceDiscoverer(bluetooth.DeviceDiscoverer):
+        def __init__(self, name, *args, **kwargs):
+            super().__init__(*args, **kwargs)
+            self.name = name
+            self.device = {}
+            self.done = True
+
+        def pre_inquiry(self):
+            self.done = False
+
+        def device_discovered(self, dev_addr, dev_class, rssi, dev_name):
+            dev_name = dev_name.decode()
+            if dev_name == self.name:
+                self.device = {
+                    'addr': dev_addr,
+                    'name': dev_name,
+                    'class': dev_class,
+                }
+
+                self.done = True
+
+        def inquiry_complete(self):
+            self.done = True
+
+    def __init__(self, device_id: int = -1, **kwargs):
+        """
+        :param device_id: Default adapter device_id to be used (default: -1, auto)
+        """
+        super().__init__(**kwargs)
+        self.device_id = device_id
+        self._devices = []
+        self._devices_by_addr = {}
+        self._devices_by_name = {}
+        self._port_and_protocol_by_addr_and_srv_uuid = {}
+        self._port_and_protocol_by_addr_and_srv_name = {}
+        self._socks = {}
+
+    def _get_device_addr(self, device):
+        if re.match('([0-9A-F]{2}:){5}[0-9A-F]{2}', device, re.IGNORECASE):
+            return device
+        if device in self._devices_by_name:
+            return self._devices_by_name[device]['addr']
+
+        return self.lookup_address(device).output['addr']
+
+    @action
+    def scan(self, device_id: int = None, duration: int = 10) -> BluetoothScanResponse:
+        """
+        Scan for nearby bluetooth devices
+
+        :param device_id: Bluetooth adapter ID to use (default configured if None)
+        :param duration: Scan duration in seconds
+        """
+        from bluetooth import discover_devices
+
+        if device_id is None:
+            device_id = self.device_id
+
+        self.logger.info('Discovering devices on adapter {}, duration: {} seconds'.format(
+            device_id, duration))
+
+        devices = discover_devices(duration=duration, lookup_names=True, lookup_class=True, device_id=device_id,
+                                   flush_cache=True)
+        response = BluetoothScanResponse(devices)
+
+        self._devices = response.devices
+        self._devices_by_addr = {dev['addr']: dev for dev in self._devices}
+        self._devices_by_name = {dev['name']: dev for dev in self._devices if dev.get('name')}
+        return response
+
+    @action
+    def lookup_name(self, addr: str, timeout: int = 10) -> BluetoothLookupNameResponse:
+        """
+        Look up the name of a nearby bluetooth device given the address
+
+        :param addr: Device address
+        :param timeout: Lookup timeout (default: 10 seconds)
+        """
+        from bluetooth import lookup_name
+
+        self.logger.info('Looking up name for device {}'.format(addr))
+        name = lookup_name(addr, timeout=timeout)
+
+        dev = {
+            'addr': addr,
+            'name': name,
+            'class': self._devices_by_addr.get(addr, {}).get('class'),
+        }
+
+        self._devices_by_addr[addr] = dev
+        if name:
+            self._devices_by_name[name] = dev
+
+        return BluetoothLookupNameResponse(addr=addr, name=name)
+
+    @action
+    def lookup_address(self, name: str, timeout: int = 10) -> BluetoothLookupNameResponse:
+        """
+        Look up the address of a nearby bluetooth device given the name
+
+        :param name: Device name
+        :param timeout: Lookup timeout (default: 10 seconds)
+        """
+
+        self.logger.info('Looking up address for device {}'.format(name))
+        discoverer = self._DeviceDiscoverer(name)
+        discoverer.find_devices(lookup_names=True, duration=timeout)
+        readfiles = [discoverer]
+
+        while True:
+            rfds = select.select(readfiles, [], [])[0]
+            if discoverer in rfds:
+                discoverer.process_event()
+
+            if discoverer.done:
+                break
+
+        dev = discoverer.device
+        if not dev:
+            raise RuntimeError('No such device: {}'.format(name))
+
+        addr = dev.get('addr')
+        self._devices_by_addr[addr] = dev
+        self._devices_by_name[name] = dev
+        return BluetoothLookupNameResponse(addr=addr, name=name)
+
+    @action
+    def find_service(self, name: str = None, addr: str = None, uuid: str = None) -> BluetoothLookupServiceResponse:
+        """
+        Look up for a service published by a nearby bluetooth device. If all the parameters are null then all the
+        published services on the nearby devices will be returned. See
+        `:class:platypush.message.response.bluetoothBluetoothLookupServiceResponse` for response structure reference.
+
+        :param name: Service name
+        :param addr: Service/device address
+        :param uuid: Service UUID
+        """
+
+        import bluetooth
+        from bluetooth import find_service
+        services = find_service(name=name, address=addr, uuid=uuid)
+
+        self._port_and_protocol_by_addr_and_srv_uuid.update({
+            (srv['host'], srv['service-id']): (srv['port'], getattr(bluetooth, srv['protocol']))
+            for srv in services if srv.get('service-id')
+        })
+
+        self._port_and_protocol_by_addr_and_srv_name.update({
+            (srv['host'], srv['name']): (srv['port'], getattr(bluetooth, srv['protocol']))
+            for srv in services if srv.get('name')
+        })
+
+        return BluetoothLookupServiceResponse(services)
+
+    def _get_sock(self, protocol=None, device: str = None, port: int = None, service_uuid: str = None,
+                  service_name: str = None, connect_if_closed=False):
+        sock = None
+        addr = self._get_device_addr(device)
+
+        if not (addr and port and protocol):
+            addr, port, protocol = self._get_addr_port_protocol(protocol=protocol, device=device, port=port,
+                                                                service_uuid=service_uuid, service_name=service_name)
+
+        if (addr, port) in self._socks:
+            sock = self._socks[(addr, port)]
+        elif connect_if_closed:
+            self.connect(protocol=protocol, device=device, port=port, service_uuid=service_uuid,
+                         service_name=service_name)
+            sock = self._socks[(addr, port)]
+
+        return sock
+
+    def _get_addr_port_protocol(self, protocol=None, device: str = None, port: int = None, service_uuid: str = None,
+                                service_name: str = None) -> tuple:
+        import bluetooth
+
+        addr = self._get_device_addr(device) if device else None
+        if service_uuid or service_name:
+            if addr:
+                if service_uuid:
+                    (port, protocol) = self._port_and_protocol_by_addr_and_srv_uuid[(addr, service_uuid)] \
+                        if (addr, service_uuid) in self._port_and_protocol_by_addr_and_srv_uuid else \
+                        (None, None)
+                else:
+                    (port, protocol) = self._port_and_protocol_by_addr_and_srv_name[(addr, service_name)] \
+                        if (addr, service_name) in self._port_and_protocol_by_addr_and_srv_name else \
+                        (None, None)
+
+            if not (addr and port):
+                self.logger.info('Discovering devices, service_name={name}, uuid={uuid}, address={addr}'.format(
+                    name=service_name, uuid=service_uuid, addr=addr))
+
+                services = [
+                    srv for srv in self.find_service().services
+                    if (service_name is None or srv.get('name') == service_name) and
+                       (addr is None or srv.get('host') == addr) and
+                       (service_uuid is None or srv.get('service-id') == service_uuid)
+                ]
+
+                if not services:
+                    raise RuntimeError('No such service: name={name} uuid={uuid} address={addr}'.format(
+                        name=service_name, uuid=service_uuid, addr=addr))
+
+                service = services[0]
+                addr = service['host']
+                port = service['port']
+                protocol = getattr(bluetooth, service['protocol'])
+        elif protocol:
+            if isinstance(protocol, str):
+                protocol = getattr(bluetooth, protocol)
+        else:
+            raise RuntimeError('No service name/UUID nor bluetooth protocol (RFCOMM/L2CAP) specified')
+
+        if not (addr and port):
+            raise RuntimeError('No valid device name/address, port, service name or UUID specified')
+
+        return addr, port, protocol
+
+    @action
+    def connect(self, protocol=None, device: str = None, port: int = None, service_uuid: str = None,
+                service_name: str = None):
+        """
+        Connect to a bluetooth device.
+        You can query the advertised services through ``find_service``.
+
+        :param protocol: Supported values: either 'RFCOMM'/'L2CAP' (str) or bluetooth.RFCOMM/bluetooth.L2CAP
+            int constants (int)
+        :param device: Device address or name
+        :param port: Port number
+        :param service_uuid: Service UUID
+        :param service_name: Service name
+        """
+        from bluetooth import BluetoothSocket
+
+        addr, port, protocol = self._get_addr_port_protocol(protocol=protocol, device=device, port=port,
+                                                            service_uuid=service_uuid, service_name=service_name)
+        sock = self._get_sock(protocol=protocol, device=addr, port=port)
+        if sock:
+            self.close(device=addr, port=port)
+
+        sock = BluetoothSocket(protocol)
+        self.logger.info('Opening connection to device {} on port {}'.format(addr, port))
+        sock.connect((addr, port))
+        self.logger.info('Connected to device {} on port {}'.format(addr, port))
+        self._socks[(addr, port)] = sock
+
+    @action
+    def close(self, device: str = None, port: int = None, service_uuid: str = None, service_name: str = None):
+        """
+        Close an active bluetooth connection
+
+        :param device: Device address or name
+        :param port: Port number
+        :param service_uuid: Service UUID
+        :param service_name: Service name
+        """
+        sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
+
+        if not sock:
+            self.logger.info('Close on device {}({}) that is not connected'.format(device, port))
+            return
+
+        try:
+            sock.close()
+        except Exception as e:
+            self.logger.warning('Exception while closing previous connection to {}({}): {}'.format(
+                device, port, str(e)))
+
+    @action
+    def send(self, data, device: str = None, port: int = None, service_uuid: str = None, service_name: str = None,
+             binary: bool = False):
+        """
+        Send data to an active bluetooth connection
+
+        :param data: Data to be sent
+        :param device: Device address or name
+        :param service_uuid: Service UUID
+        :param service_name: Service name
+        :param port: Port number
+        :param binary: Set to true if msg is a base64-encoded binary string
+        """
+        from bluetooth import BluetoothError
+
+        sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name,
+                              connect_if_closed=True)
+
+        if binary:
+            data = base64.decodebytes(data.encode() if isinstance(data, str) else data)
+
+        try:
+            sock.send(data)
+        except BluetoothError as e:
+            self.close(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
+            raise e
+
+    @action
+    def recv(self, device: str, port: int, service_uuid: str = None, service_name: str = None, size: int = 1024,
+             binary: bool = False) -> BluetoothResponse:
+        """
+        Send data to an active bluetooth connection
+
+        :param device: Device address or name
+        :param port: Port number
+        :param service_uuid: Service UUID
+        :param service_name: Service name
+        :param size: Maximum number of bytes to be read
+        :param binary: Set to true to return a base64-encoded binary string
+        """
+        from bluetooth import BluetoothError
+
+        sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name,
+                              connect_if_closed=True)
+
+        if not sock:
+            self.connect(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
+            sock = self._get_sock(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
+
+        try:
+            data = sock.recv(size)
+        except BluetoothError as e:
+            self.close(device=device, port=port, service_uuid=service_uuid, service_name=service_name)
+            raise e
+
+        if binary:
+            data = base64.encodebytes(data)
+
+        return BluetoothResponse(output=data.decode())
+
+    @action
+    def set_l2cap_mtu(self, mtu: int, device: str = None, port: int = None, service_name: str = None,
+                      service_uuid: str = None):
+        """
+        Set the L2CAP MTU (Maximum Transmission Unit) value for a connected bluetooth device.
+        Both the devices usually use the same MTU value over a connection.
+
+        :param device: Device address or name
+        :param port: Port number
+        :param service_uuid: Service UUID
+        :param service_name: Service name
+        :param mtu: New MTU value
+        """
+        from bluetooth import BluetoothError, set_l2cap_mtu, L2CAP
+
+        sock = self._get_sock(protocol=L2CAP, device=device, port=port, service_uuid=service_uuid,
+                              service_name=service_name, connect_if_closed=True)
+
+        if not sock:
+            raise RuntimeError('set_l2cap_mtu: device not connected')
+
+        try:
+            set_l2cap_mtu(sock, mtu)
+        except BluetoothError as e:
+            self.close(device=device, port=port, service_name=service_name, service_uuid=service_uuid)
+            raise e
+
+    @action
+    def send_file(self, filename: str, device: str, port: int = None, data=None, service_name='OBEX Object Push',
+                  binary: bool = False):
+        """
+        Send a local file to a device that exposes an OBEX Object Push service
+
+        :param filename: Path of the file to be sent
+        :param data: Alternatively to a file on disk you can send raw (string or binary) content
+        :param device: Device address or name
+        :param port: Port number
+        :param service_name: Service name
+        :param binary: Set to true if data is a base64-encoded binary string
+        """
+        from PyOBEX.client import Client
+
+        if not data:
+            filename = os.path.abspath(os.path.expanduser(filename))
+            with open(filename, 'r') as f:
+                data = f.read()
+            filename = os.path.basename(filename)
+        else:
+            if binary:
+                data = base64.decodebytes(data.encode() if isinstance(data, str) else data)
+
+        addr, port, protocol = self._get_addr_port_protocol(device=device, port=port,
+                                                            service_name=service_name)
+
+        client = Client(addr, port)
+        self.logger.info('Connecting to device {}'.format(addr))
+        client.connect()
+        self.logger.info('Sending file {} to device {}'.format(filename, addr))
+        client.put(filename, data)
+        self.logger.info('File {} sent to device {}'.format(filename, addr))
+        client.disconnect()
+
+
+# vim:sw=4:ts=4:et:
diff --git a/requirements.txt b/requirements.txt
index 62e8d0755..c4c348f48 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -103,9 +103,10 @@ paho-mqtt
 # Serial port support
 # pyserial
 
-# Switchbot devices support
+# Bluetooth devices support
 # pybluez
 # gattlib
+# git+https://github.com/BlackLight/PyOBEX
 
 # Support for TP-Link HS100 and similar smart switches/plugins
 # pyHS100
diff --git a/setup.py b/setup.py
index e48709e9c..159354a30 100755
--- a/setup.py
+++ b/setup.py
@@ -149,7 +149,7 @@ setup(
         # Support for Kafka backend and plugin
         'kafka': ['kafka-python'],
         # Support for Pushbullet backend and plugin
-        'pushbullet': ['pushbullet.py'],
+        'pushbullet': ['pushbullet.py @ https://github.com/rbrcsk/pushbullet.py'],
         # Support for HTTP backend
         'http': ['flask', 'websockets', 'python-dateutil', 'tz', 'frozendict', 'bcrypt'],
         # Support for uWSGI HTTP backend
@@ -237,8 +237,9 @@ setup(
         'flic': ['flic @ https://github.com/50ButtonsEach/fliclib-linux-hci/tarball/master'],
         # Support for Alexa/Echo plugin
         'alexa': ['avs @ https://github.com:BlackLight/avs/tarball/master'],
-        # Support for bluetooth and Switchbot plugin
-        'bluetooth': ['pybluez', 'gattlib'],
+        # Support for bluetooth devices
+        'bluetooth': ['pybluez', 'gattlib',
+                'pyobex @ https://github.com/BlackLight/PyOBEX'],
         # Support for TP-Link devices
         'tplink': ['pyHS100'],
         # Support for PWM3901 2-Dimensional Optical Flow Sensor