Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/bthome_ble/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ def parse_event_properties(


class BTHomeData:
"""Data for BTHome Bluetooth devices."""

def __init__(
self,
service_info: BluetoothServiceInfoBleak,
Expand Down Expand Up @@ -188,8 +190,21 @@ def _is_encrypted(self) -> bool:
def _is_mac_included(self) -> bool:
"""Checks if the MAC is included flag is set."""
# If True, the first 6 bytes contain the mac address
# This is an undocumented deprecated and risky feature, due to privacy reasons
# It will not make it into the official documentation.
# It was implemented to address issue #42
# https://github.com/Bluetooth-Devices/bthome-ble/issues/42
# Do not use it. Use "is_mac_included_safe" instead.
return bool(self._get_adv_info() & (1 << 1)) # bit 1

def _is_mac_included_privately(self) -> bool:
"""Checks if the MAC is included privatly flag is set."""
# If True, the first 6 bytes of the decrypted payload contain the mac address
# and mac address is not used for nonce anymore. It is recommended to use that
# feature only in combination with "is_encrypted".
# NOT YET FULLY IMPLEMENTED.
return bool(self._get_adv_info() & (1 << 3)) # bit 3

def _is_sleepy_device(self) -> bool:
"""Checks if device has sleepy flag set."""
# If True, the device is only updating when trigger
Expand Down Expand Up @@ -279,6 +294,16 @@ def is_sleepy_device(self) -> bool:
case _:
raise ValueError

def is_mac_included_privately(self) -> bool:
"""Checks if the MAC is included privatly flag is set."""
match self.get_bthome_version():
case BTHomeVersion.V1:
return False
case BTHomeVersion.V2:
return self._is_mac_included_privately()
case _:
raise ValueError

def get_nounce_uuid(self) -> bytes:
"""Returns the UUID for the nounce."""
match self.get_bthome_version():
Expand Down Expand Up @@ -329,9 +354,13 @@ def get_encrypted_payload(self) -> bytes:
def get_nonce(self) -> bytes:
"""Creates the nounce for decryption."""
counter = self.get_counter()
uuid = self.get_nounce_uuid()
# TODO tests are needed
if self.is_mac_included_privately():
# nonce: uuid16 [2 (v1) or 3 (v2)], counter [4]
return b"".join([uuid, counter])
mac_readable = self.get_mac_readable()
bthome_mac = bytes.fromhex(mac_readable.replace(":", ""))
uuid = self.get_nounce_uuid()
# nonce: mac [6], uuid16 [2 (v1) or 3 (v2)], counter [4]
return b"".join([bthome_mac, uuid, counter])

Expand All @@ -347,7 +376,7 @@ def get_minimum_payload_length(self) -> int:


class BTHomeBluetoothDeviceData(BluetoothData):
"""Data for BTHome Bluetooth devices."""
"""Processing for BTHome Bluetooth devices."""

def __init__(self, bindkey: bytes | None = None) -> None:
super().__init__()
Expand Down Expand Up @@ -567,6 +596,11 @@ def _parse_bthome(
payload = self._get_decrypted_payload(bthome_data)
if payload is None:
return True
# TODO tests are needed
if bthome_data.is_mac_included_privately():
# TODO the mac itself (payload[:6]) is not usable anymore
payload = payload[6:]

return self._parse_payload(payload, bthome_data.get_time())

def _skip_old_or_duplicated_advertisement(
Expand Down
Loading