Skip to content

Commit

Permalink
Merge pull request #6 from Bluetooth-Devices/BTHome_name
Browse files Browse the repository at this point in the history
feat!: bthome with capital h
  • Loading branch information
Ernst79 authored Sep 1, 2022
2 parents 5edaad5 + cf3c702 commit ca7733d
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 262 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Look through the GitHub issues for features. Anything tagged with "enhancement"

### Write Documentation

BThome BLE could always use more documentation, whether as part of the official BThome BLE docs, in docstrings, or even on the web in blog posts, articles, and such.
BTHome BLE could always use more documentation, whether as part of the official BTHome BLE docs, in docstrings, or even on the web in blog posts, articles, and such.

### Submit Feedback

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# BThome BLE
# BTHome BLE

<p align="center">
<a href="https://github.com/bluetooth-devices/bthome-ble/actions?query=workflow%3ACI">
Expand Down Expand Up @@ -30,7 +30,7 @@
<img src="https://img.shields.io/pypi/l/bthome-ble.svg?style=flat-square" alt="License">
</p>

BLE parser for sensors that support the BThome BLE format.
BLE parser for sensors that support the BTHome BLE format.

## Installation

Expand All @@ -40,7 +40,7 @@ Install this via pip (or your favourite package manager):

## BThome BLE format

More detailed information about the BThome BLE format can be found on the [usage page](https://github.com/Bluetooth-Devices/bthome-ble/blob/main/docs/source/usage.md)
More detailed information about the BTHome BLE format can be found on the [usage page](https://github.com/Bluetooth-Devices/bthome-ble/blob/main/docs/source/usage.md)

## Contributors ✨

Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

# -- Project information -----------------------------------------------------

project = "BThome BLE"
project = "BTHome BLE"
copyright = "2022, E. Klamer"
author = "E. Klamer"

Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Welcome to BThome BLE documentation!
# Welcome to BTHome BLE documentation!

```{toctree}
:caption: Installation & Usage
Expand Down
161 changes: 2 additions & 159 deletions docs/source/usage.md
Original file line number Diff line number Diff line change
@@ -1,160 +1,3 @@
# Usage BThome for DIY sensors
# Usage BTHome for DIY sensors

## Introduction

The BThome BLE format was originally developed as HA BLE for the custom Home Assistant integration "BLE monitor", and is soon also available for the official Home Assistant Bluetooth integration.
The BThome BLE format allows you to create for your own DIY BLE sensors, which can be read by the Home Assistant Bluetooth integration.
This format tries to provide an energy effective, but still flexible BLE format that can be customized to your own needs.
A proof of concept is the latest [ATC firmware](https://github.com/pvvx/ATC_MiThermometer), which is supporting the BThome BLE format (firmware version 3.7 and up). Note that in firmware 3.7 it is called `HA BLE` format.

To use this package, import it:

```python
import bthome_ble
```

## The BThome BLE format

The `BThome BLE` format can best be explained with an example. A BLE advertisement is a long message with bytes (bytestring).

```
043E1B02010000A5808FE648540F0201060B161C182302C4090303BF13CC
```

This message is split up in three parts, the **header**, the **advertising payload** and an **RSSI value**

- Header: `043E1B02010000A5808FE648540F`
- Adverting payload `0201060B161C182302C4090303BF13`
- RSSI value `CC`

## Header

The first part `043E1B02010000A5808FE648540F` is the header of the message and contains, amongst others

- the length of the message in the 3rd byte (`0x1B` in hex is 27 in decimals, meaning 27 bytes after the third byte = 30 bytes in total)
- the MAC address in reversed order in byte 8-13 (`A5808FE64854`, in reversed order, this corresponds to a MAC address `54:48:E6:8F:80:A5`)
- the length of the advertising payload in byte 14 (`0x0F` = 15)

## Advertising payload

The second part `0201060B161C182302C4090303BF13` contains the advertising payload, and can consist of one or more **Advertising Data (AD) elements**. Each element contains the following:

- 1st byte: length of the element (excluding the length byte itself)
- 2nd byte: AD type – specifies what data is included in the element
- AD data – one or more bytes - the meaning is defined by the AD type

In the `BThome BLE` format, the advertising payload should contain the following two AD elements:

- Flags (`0x01`)
- Service Data - 16-bit UUID (`0x16`)

In the example, we have:

- First AD element: `020106`

- `0x02` = length (2 bytes)
`0x01` = Flags
`0x06` = in bits, this is `00000110`. Bit 1 and bit 2 are 1, meaning:
Bit 1: “LE General Discoverable Mode”
Bit 2: “BR/EDR Not Supported”
- This part always has to be added, and is always the same (`0x020106`)

- Second AD element: `0B161C182302C4090303BF13` (BThome BLE data)

- `0x0B` = length (11 bytes)
`0x16` = Service Data - 16-bit UUID
`0x1C182302C4090303BF13` = BThome BLE data

### BThome BLE data format (non-encrypted)

The BThome BLE data is the part that contains the data. The data can contain multiple measurements. We continue with the example from above.

- BThome BLE data = `0x1C182302C4090303BF13`
- `0x1C18` = The first two byte are the UUID16, which are assigned numbers that can be found in [this official document]https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf by the Bluetooth organization. For BThome BLE we use the so called `GATT Service` = `User Data` (`0x1C18`) for non-encrypted messages. For encrypted messages, we use `GATT Service` = `Bond Management` (`0x1E18`). More information about encryption can be found further down this page. This part should always be `0x1C18` (non-encrypted) or `0x1E18` (encrypted), as it is used to recognize a BThome BLE message.
- `0x2302C409` = Temperature packet
- `0x0303BF13` = Humidity packet

Let's explain how the last two data packets work. The temperature packet is used as example.

- The first byte `0x23` (in bits `00100011`) is giving information about:
- The object length (bit 0-4): `00011` = 3 bytes (excluding the length byte itself)
- The object format (bit 5-7) `001` = 1 = Signed Integer (see table below)

| type | bit 5-7 | format | Data type |
| ---- | ------- | ------ | ---------------------- |
| `0` | `000` | uint | unsingned integer |
| `1` | `001` | int | signed integer |
| `2` | `010` | float | float |
| `3` | `011` | string | string |
| `4` | `100` | MAC | MAC address (reversed) |

- The second byte `0x02` is defining the type of measurement (temperature, see table below)
- The remaining bytes `0xC409` is the object value (little endian), which will be multiplied with the factor in the table below to get a sufficient number of digits.
- The object length is telling us that the value is 2 bytes long (object length = 3 bytes minus the second byte) and the object format is telling us that the value is a Signed Integer (positive or negative integer).
- `0xC409` as unsigned integer in little endian is equal to 2500.
- The factor for a temperature measurement is 0.01, resulting in a temperature of 25.00°C

At the moment, the following sensors are supported. A preferred data type is given for your convenience, which should give you a short data message and at the same time a sufficient number of digits to display your data with high accuracy in Home Assistant. But you are free to use a different data type. If you want another sensor, let us know by creating a new issue on Github.

| Object id | Property | Preferred data type | Factor | example | result | Unit in HA | Notes |
| --------- | ----------- | ------------------- | ------ | ---------------- | ------------ | ---------- | ------- |
| `0x00` | packet id | uint8 (1 byte) | 1 | `020009` | 9 | | [1] [2] |
| `0x01` | battery | uint8 (1 byte) | 1 | `020161` | 97 | `%` | |
| `0x02` | temperature | sint16 (2 bytes) | 0.01 | `2302CA09` | 25.06 | `°C` | |
| `0x03` | humidity | uint16 (2 bytes) | 0.01 | `0303BF13` | 50.55 | `%` | |
| `0x04` | pressure | uint24 (3 bytes) | 0.01 | `0404138A01` | 1008.83 | `mbar` | |
| `0x05` | illuminance | uint24 (3 bytes) | 0.01 | `0405138A14` | 13460.67 | `lux` | |
| `0x06` | weight | uint16 (2 byte) | 0.01 | `03065E1F` | 80.3 | `kg` | [1] [3] |
| `0x07` | weight unit | string (2 bytes) | None | `63076B67` | "kg" | | [1] [3] |
| `0x08` | dewpoint | sint16 (2 bytes) | 0.01 | `2308CA06` | 17.386 | `°C` | [1] |
| `0x09` | count | uint | 1 | `020960` | 96 | | [1] |
| `0X0A` | energy | uint24 (3 bytes) | 0.001 | `040A138A14` | 1346.067 | `kWh` | |
| `0x0B` | power | uint24 (3 bytes) | 0.01 | `040B021B00` | 69.14 | `W` | |
| `0x0C` | voltage | uint16 (2 bytes) | 0.001 | `030C020C` | 3.074 | `V` | |
| `0x0D` | pm2.5 | uint16 (2 bytes) | 1 | `030D120C` | 3090 | `ug/m3` | |
| `0x0E` | pm10 | uint16 (2 bytes) | 1 | `030E021C` | 7170 | `ug/m3` | |
| `0x0F` | boolean | uint8 (1 byte) | 1 | `020F01` | 1 (True) | `True` | [1] |
| `0x10` | switch | uint8 (1 byte) | 1 | `021001` | 1 (True) | `on` | [1] |
| `0x11` | opening | uint8 (1 byte) | 1 | `021100` | 0 (false) | `closed` | [1] |
| `0x12` | co2 | uint16 (2 bytes) | 1 | `0312E204` | 1250 | `ppm` | |
| `0x13` | voc | uint16 (2 bytes) | 1 | `03133301` | 307 | `ug/m3` | |
| | mac | 6 bytes (reversed) | | `86A6808FE64854` | 5448E68F80A6 | | [1] [4] |

**Notes**

Full example payloads are given in the test tests.

**_1. Not supported measurement type_**

The sensors with [1] is the last column are not yet supported in the official HA integration and will be added soon.
For these measurement types, use [BLE monitor](https://github.com/custom_components/ble_monitor) for the time being.

**_2. packet id (not supported in HA integration yet)_**

The `packet id` is optional and was used to filter duplicate data in BLE monitor. This functionality is not supported in the official Home Assistant integration.

**_3. weight (unit) (not supported in HA integration yet)_**

The `weight unit` is in `kg` by default, but can be set with the weight unit property. Examples of `weight unit` packets are:

- kg (`63076B67`)
- lbs (`64076C6273`)
- jin (`64076A696E`)

**_4. mac (not supported in HA integration yet)_**

The `mac` functionality is not yet supported in the official Home Assistant integration, but was developed for BLE monitor.
We will most likely add this in a future update.

You don't have to specify the `mac` address in the advertising payload, as it is already included in the [header](#header).
However, you can overwrite the `mac` by specifying it in the advertising payload.
To do this, set the first byte to `0x86` (meaning: object type = 4 (`mac`) and object length = 6), followed by the MAC in reversed order. No Object id is needed.

### BThome BLE data format (encrypted)

You can also choose to send the data in an encrypted way, which gives you extra security.
Unencrypted BLE advertisements can be read by everyone, even your neighbour with Home Assistant and BLE Monitor should in theory be able to receive your BLE data.
However, when you use encryption, it will be useless for anyone else, as long as he or she doesn't have the encryption key.
The encryption key should be 16 bytes long key (32 characters).
More information on how to encrypt your messages is demonstrated in [this script](https://github.com/custom-components/ble_monitor/blob/master/custom_components/ble_monitor/ble_parser/ha_ble_encryption.py).
BThome is using an AES encryption (CCM mode). Don't forget to set the encryption key in your Home Assistant device settings.
Detailed information about the use of BTHome BLE can be found on our [website](https://bthome.io).
6 changes: 3 additions & 3 deletions src/bthome_ble/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Parser for BLE advertisements in BThome format."""
"""Parser for BLE advertisements in BTHome format."""
from __future__ import annotations

from sensor_state_data import (
Expand All @@ -11,12 +11,12 @@
Units,
)

from .parser import BThomeBluetoothDeviceData
from .parser import BTHomeBluetoothDeviceData

__version__ = "0.5.2"

__all__ = [
"BThomeBluetoothDeviceData",
"BTHomeBluetoothDeviceData",
"SensorDescription",
"SensorDeviceInfo",
"DeviceClass",
Expand Down
2 changes: 1 addition & 1 deletion src/bthome_ble/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Constants for BThome measurements."""
"""Constants for BTHome measurements."""
import dataclasses

from sensor_state_data import SensorLibrary, description
Expand Down
43 changes: 24 additions & 19 deletions src/bthome_ble/parser.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Parser for BLE advertisements in BThome format.
"""Parser for BLE advertisements in BTHome format.
This file is shamelessly copied from the following repository:
https://github.com/Ernst79/bleparser/blob/ac8757ad64f1fc17674dcd22111e547cdf2f205b/package/bleparser/ha_ble.py
BThome was originally developed as HA BLE for ble_monitor and been renamed to
BThome for Home Assistant Bluetooth integrations.
BTHome was originally developed as HA BLE for ble_monitor and has been renamed
to BTHome for the Home Assistant Bluetooth integration.
MIT License applies.
"""
Expand Down Expand Up @@ -66,7 +66,7 @@ def parse_float(data_obj: bytes, factor: float = 1.0) -> float | None:
elif len(data_obj) == 8:
[val] = struct.unpack("d", data_obj)
else:
_LOGGER.error("only 2, 4 or 8 byte long floats are supported in BThome BLE")
_LOGGER.error("only 2, 4 or 8 byte long floats are supported in BTHome BLE")
return None
return round(val * factor, decimal_places)

Expand All @@ -85,8 +85,8 @@ def parse_mac(data_obj: bytes) -> bytes | None:
return None


class BThomeBluetoothDeviceData(BluetoothData):
"""Date for BThome Bluetooth devices."""
class BTHomeBluetoothDeviceData(BluetoothData):
"""Date for BTHome Bluetooth devices."""

def __init__(self, bindkey: bytes | None = None) -> None:
super().__init__()
Expand Down Expand Up @@ -138,15 +138,15 @@ def supported(self, data: BluetoothServiceInfo) -> bool:

def _start_update(self, service_info: BluetoothServiceInfo) -> None:
"""Update from BLE advertisement data."""
_LOGGER.debug("Parsing BThome BLE advertisement data: %s", service_info)
_LOGGER.debug("Parsing BTHome BLE advertisement data: %s", service_info)
for uuid, data in service_info.service_data.items():
if self._parse_bthome(service_info, service_info.name, data):
self.last_service_info = service_info

def _parse_bthome(
self, service_info: BluetoothServiceInfo, name: str, data: bytes
) -> bool:
"""Parser for BThome sensors"""
"""Parser for BTHome sensors"""
identifier = short_address(service_info.address)

# Remove identifier from ATC sensors.
Expand All @@ -172,23 +172,23 @@ def _parse_bthome(

self.set_device_name(f"{name} {identifier}")
self.set_title(f"{name} {identifier}")
self.set_device_type("BThome sensor")
self.set_device_type("BTHome sensor")

uuid16 = list(service_info.service_data.keys())
if uuid16 == ["0000181c-0000-1000-8000-00805f9b34fb"]:
# Non-encrypted BThome BLE format
# Non-encrypted BTHome BLE format
self.encryption_scheme = EncryptionScheme.NONE
self.set_device_sw_version("BThome BLE")
self.set_device_sw_version("BTHome BLE")
payload = data
packet_id = None # noqa: F841
elif uuid16 == ["0000181e-0000-1000-8000-00805f9b34fb"]:
# Encrypted BThome BLE format
# Encrypted BTHome BLE format
self.encryption_scheme = EncryptionScheme.BTHOME_BINDKEY
self.set_device_sw_version("BThome BLE (encrypted)")
self.set_device_sw_version("BTHome BLE (encrypted)")

mac_readable = service_info.address
if len(mac_readable) != 17 and mac_readable[2] != ":":
# On macOS, we get a UUID, which is useless for BThome sensors
# On macOS, we get a UUID, which is useless for BTHome sensors
self.mac_known = False
return False
else:
Expand Down Expand Up @@ -235,14 +235,14 @@ def _parse_bthome(

elif obj_data_format > 4:
_LOGGER.error(
"UNKNOWN dataobject in BThome BLE payload! Adv: %s",
"UNKNOWN dataobject in BTHome BLE payload! Adv: %s",
data.hex(),
)
continue

if obj_meas_type not in MEAS_TYPES:
_LOGGER.debug(
"UNKNOWN measurement type in BThome BLE payload! Adv: %s",
"UNKNOWN measurement type in BTHome BLE payload! Adv: %s",
data.hex(),
)
continue
Expand All @@ -267,13 +267,18 @@ def _parse_bthome(
if meas_float is not None:
if meas_format.device_class in HA_SUPPORTED_DEVICE_CLASSES:
self.update_predefined_sensor(meas_format, meas_float)
else:
elif meas_format.device_class:
self.update_sensor(
key=meas_format.device_class,
native_unit_of_measurement=meas_format.native_unit_of_measurement,
native_value=meas_float,
device_class=None,
)
else:
_LOGGER.debug(
"UNKNOWN dataobject in BTHome BLE payload! Adv: %s",
data.hex(),
)
result = True
elif meas_str is not None:
_LOGGER.debug(
Expand All @@ -282,7 +287,7 @@ def _parse_bthome(
)
else:
_LOGGER.debug(
"UNKNOWN dataobject in BThome BLE payload! Adv: %s",
"UNKNOWN dataobject in BTHome BLE payload! Adv: %s",
data.hex(),
)

Expand All @@ -292,7 +297,7 @@ def _parse_bthome(
return True

def _decrypt_bthome(self, data: bytes, bthome_mac: bytes) -> bytes:
"""Decrypt encrypted BThome BLE advertisements"""
"""Decrypt encrypted BTHome BLE advertisements"""
if not self.bindkey:
self.bindkey_verified = False
_LOGGER.debug("Encryption key not set and adv is encrypted")
Expand Down
Loading

0 comments on commit ca7733d

Please sign in to comment.