Source code for bluetooth_numbers.utils
"""Module with utility functions for Bluetooth numbers."""
import re
from uuid import UUID
from bluetooth_numbers.exceptions import (
No16BitIntegerError,
NonStandardUUIDError,
WrongOUIFormatError,
)
BASE_UUID: UUID = UUID("00000000-0000-1000-8000-00805F9B34FB")
"""Base UUID defined by the Bluetooth SIG."""
_OUI_RE = re.compile(r"^([0-9A-F]{2})[-:]*([0-9A-F]{2})[-:]*([0-9A-F]{2})$")
_NORMALIZED_OUI_RE = re.compile(r"^[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}$")
[docs]def is_normalized_oui(oui: str) -> bool:
"""Check whether the argument is a normalized OUI.
A normalized OUI is a string with format "XX:YY:ZZ" where XX, YY and ZZ are
uppercase hexadecimal digits.
Args:
oui (str): The string to check.
Returns:
bool: ``True`` if `oui` is a normalized OUI, ``False`` otherwise.
Examples:
>>> from bluetooth_numbers.utils import is_normalized_oui
>>> is_normalized_oui("98-e7-43")
False
>>> is_normalized_oui("98:E7:43")
True
>>> is_normalized_oui("FOOBAR")
False
"""
return bool(_NORMALIZED_OUI_RE.match(oui))
[docs]def normalize_oui(oui: str) -> str:
"""Normalize an OUI.
This changes the letters in decimal digits to uppercase and places a colon
between the OUI's bytes.
Args:
oui (str): The OUI to normalize.
Raises:
WrongOUIFormatError: If `oui` doesn't have the right format.
Returns:
str: `oui` as a normalized OUI.
Examples:
>>> from bluetooth_numbers.utils import normalize_oui
>>> normalize_oui("98-e7-43")
'98:E7:43'
>>> normalize_oui("98e743")
'98:E7:43'
>>> normalize_oui("FOOBAR")
Traceback (most recent call last):
bluetooth_numbers.exceptions.WrongOUIFormatError: 'FOOBAR'
"""
oui_parts = _OUI_RE.match(oui.upper())
if oui_parts:
return oui_parts.group(1) + ":" + oui_parts.group(2) + ":" + oui_parts.group(3)
raise WrongOUIFormatError(oui)
[docs]def uuid128_to_uuid16(uuid128: UUID) -> int:
"""Convert a 128-bit standard Bluetooth UUID to a 16-bit UUID.
Args:
uuid128 (~uuid.UUID): A 128-bit Bluetooth UUID.
Raises:
NonStandardUUIDError: If `uuid128` is not a 128-bit standard Bluetooth UUID.
Returns:
int: A 16-bit UUID that is the short UUID of `uuid128`.
Example:
>>> from bluetooth_numbers.utils import uuid128_to_uuid16, uint16_to_hex
>>> from uuid import UUID
>>> uint16_to_hex(uuid128_to_uuid16(UUID('00001800-0000-1000-8000-00805f9b34fb')))
'0x1800'
""" # noqa: E501
if is_standard_uuid128(uuid128):
# Extract the 16-bit UUID
return int.from_bytes(uuid128.bytes[2:4], "big")
raise NonStandardUUIDError(uuid128)
[docs]def is_standard_uuid128(uuid128: UUID) -> bool:
"""Check whether a 128-bit Bluetooth UUID is a standard UUID.
Args:
uuid128 (~uuid.UUID): A 128-bit Bluetooth UUID.
Returns:
bool: ``True`` if `uuid128` is a standard Bluetooth UUID, ``False`` otherwise.
Examples:
>>> from bluetooth_numbers.utils import is_standard_uuid128
>>> from uuid import UUID
>>> is_standard_uuid128(UUID('00001800-0000-1000-8000-00805f9b34fb'))
True
>>> is_standard_uuid128(UUID('bfc46884-ea75-416b-8154-29c5d0b0a087'))
False
.. versionadded:: 1.1.0
"""
uuid128_bytearray = bytearray(uuid128.bytes)
uuid128_bytearray[2:4] = b"\x00\x00"
uuid128_masked = UUID(bytes=bytes(uuid128_bytearray))
return uuid128_masked == BASE_UUID
[docs]def uuid16_to_uuid128(uuid16: int) -> UUID:
"""Convert a 16-bit UUID to a 128-bit UUID with the Bluetooth base UUID.
Args:
uuid16 (int): A 16-bit UUID.
Raises:
No16BitIntegerError: If `uuid16` is not an integer from 0 to 65535.
Returns:
~uuid.UUID: A 128-bit UUID that is the full UUID of `uuid16`.
Example:
>>> from bluetooth_numbers.utils import uuid16_to_uuid128
>>> uuid16_to_uuid128(0x1800)
UUID('00001800-0000-1000-8000-00805f9b34fb')
"""
if not is_uint16(uuid16):
raise No16BitIntegerError(uuid16)
uuid128 = bytearray(BASE_UUID.bytes)
# Insert the 16-bit UUID in the third and fourth bytes of the 128-bit UUID.
# For example:
# 00000000-0000-1000-8000-00805f9b34fb
# \ /
# 1800
uuid128[2:4] = uuid16.to_bytes(2, "big")
return UUID(bytes=bytes(uuid128))
[docs]def uint16_to_hex(number: int) -> str:
"""Convert a 16-bit UUID or Company ID to a hexadecimal string.
Args:
number (int): A 16-bit number.
Raises:
No16BitIntegerError: If `number` is not an integer from 0 to 65535.
Returns:
str: A hexadecimal string representation of `number`.
Example:
>>> from bluetooth_numbers.utils import uint16_to_hex
>>> uint16_to_hex(0xFD6F)
'0xfd6f'
"""
if not is_uint16(number):
raise No16BitIntegerError(number)
return f"{number:#0{6}x}"
[docs]def is_uint16(number: int) -> bool:
"""Check whether a number is a 16-bit unsigned integer.
Args:
number (int): The number to check.
Returns:
bool: ``True`` if `number` is a 16-bit unsigned integer, ``False`` otherwise.
Examples:
>>> from bluetooth_numbers.utils import is_uint16
>>> is_uint16(0x1800)
True
>>> is_uint16(-1)
False
"""
return isinstance(number, int) and 0 <= number <= 0xFFFF # noqa: PLR2004