Skip to content

Commit cb10ad6

Browse files
authored
Add wifi_mac_to_bluetooth_mac() helper for ESP32 MAC address conversion (#1392)
1 parent bcae542 commit cb10ad6

File tree

3 files changed

+85
-1
lines changed

3 files changed

+85
-1
lines changed

aioesphomeapi/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
EncryptionHelloAPIError,
2323
SocketAPIError,
2424
BadMACAddressAPIError,
25+
wifi_mac_to_bluetooth_mac,
2526
)
2627
from .model import *
2728
from .reconnect_logic import ReconnectLogic

aioesphomeapi/core.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,45 @@ def to_human_readable_address(address: int) -> str:
304304
return ":".join(TWO_CHAR.findall(f"{address:012X}"))
305305

306306

307+
def wifi_mac_to_bluetooth_mac(wifi_mac: str) -> str:
308+
"""Convert a WiFi MAC address to a Bluetooth MAC address.
309+
310+
ESP32 devices use a single base MAC address and derive other interface
311+
MAC addresses from it. The Bluetooth MAC is calculated as base_mac + 2
312+
to the last octet.
313+
314+
Args:
315+
wifi_mac: WiFi MAC address in format "AABBCCDDEEFF" or "aa:bb:cc:dd:ee:ff"
316+
317+
Returns:
318+
Bluetooth MAC address in uppercase with colons (e.g., "AA:BB:CC:DD:EE:01")
319+
320+
Examples:
321+
>>> wifi_mac_to_bluetooth_mac("AABBCCDDEEFF")
322+
"AA:BB:CC:DD:EE:01"
323+
>>> wifi_mac_to_bluetooth_mac("aa:bb:cc:dd:ee:ff")
324+
"AA:BB:CC:DD:EE:01"
325+
>>> wifi_mac_to_bluetooth_mac("AA:BB:CC:DD:EE:FE")
326+
"AA:BB:CC:DD:EE:00"
327+
"""
328+
# Remove colons and convert to uppercase
329+
clean_mac = wifi_mac.replace(":", "").upper()
330+
331+
# Validate MAC address format
332+
if len(clean_mac) != 12 or not all(c in "0123456789ABCDEF" for c in clean_mac):
333+
raise ValueError(f"Invalid MAC address format: {wifi_mac}")
334+
335+
# Extract the last octet and add 2
336+
last_octet = int(clean_mac[-2:], 16)
337+
new_last_octet = (last_octet + 2) % 256
338+
339+
# Build the new MAC address
340+
bt_mac = clean_mac[:-2] + f"{new_last_octet:02X}"
341+
342+
# Always return with colons
343+
return ":".join(TWO_CHAR.findall(bt_mac))
344+
345+
307346
def to_human_readable_gatt_error(error: int) -> str:
308347
"""Convert a GATT error to a human readable format."""
309348
return ESPHOME_GATT_ERRORS.get(error, "Unknown error")

tests/test_core.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
11
from __future__ import annotations
22

3-
from aioesphomeapi.core import MESSAGE_TYPE_TO_PROTO
3+
import pytest
4+
5+
from aioesphomeapi.core import MESSAGE_TYPE_TO_PROTO, wifi_mac_to_bluetooth_mac
46

57

68
def test_order_and_no_missing_numbers_in_message_type_to_proto():
79
"""Test that MESSAGE_TYPE_TO_PROTO has no missing numbers."""
810
for idx, (k, v) in enumerate(MESSAGE_TYPE_TO_PROTO.items()):
911
assert idx + 1 == k
12+
13+
14+
def test_wifi_mac_to_bluetooth_mac():
15+
"""Test converting WiFi MAC address to Bluetooth MAC address."""
16+
# Test with uppercase MAC without colons
17+
assert wifi_mac_to_bluetooth_mac("AABBCCDDEEFF") == "AA:BB:CC:DD:EE:01"
18+
19+
# Test with lowercase MAC with colons
20+
assert wifi_mac_to_bluetooth_mac("aa:bb:cc:dd:ee:ff") == "AA:BB:CC:DD:EE:01"
21+
22+
# Test with uppercase MAC with colons
23+
assert wifi_mac_to_bluetooth_mac("AA:BB:CC:DD:EE:FF") == "AA:BB:CC:DD:EE:01"
24+
25+
# Test with mixed case
26+
assert wifi_mac_to_bluetooth_mac("Aa:Bb:Cc:Dd:Ee:Ff") == "AA:BB:CC:DD:EE:01"
27+
28+
# Test rollover (FE + 2 = 00, wraps around)
29+
assert wifi_mac_to_bluetooth_mac("AA:BB:CC:DD:EE:FE") == "AA:BB:CC:DD:EE:00"
30+
31+
# Test edge case with FF (FF + 2 = 01, wraps around)
32+
assert wifi_mac_to_bluetooth_mac("AA:BB:CC:DD:EE:FF") == "AA:BB:CC:DD:EE:01"
33+
34+
# Test with 00
35+
assert wifi_mac_to_bluetooth_mac("AA:BB:CC:DD:EE:00") == "AA:BB:CC:DD:EE:02"
36+
37+
# Test with FD (FD + 2 = FF)
38+
assert wifi_mac_to_bluetooth_mac("AA:BB:CC:DD:EE:FD") == "AA:BB:CC:DD:EE:FF"
39+
40+
41+
def test_wifi_mac_to_bluetooth_mac_invalid():
42+
"""Test wifi_mac_to_bluetooth_mac with invalid inputs."""
43+
# Test with invalid length
44+
with pytest.raises(ValueError, match="Invalid MAC address format"):
45+
wifi_mac_to_bluetooth_mac("AA:BB:CC:DD:EE")
46+
47+
# Test with invalid characters
48+
with pytest.raises(ValueError, match="Invalid MAC address format"):
49+
wifi_mac_to_bluetooth_mac("GG:BB:CC:DD:EE:FF")
50+
51+
# Test with completely invalid input
52+
with pytest.raises(ValueError, match="Invalid MAC address format"):
53+
wifi_mac_to_bluetooth_mac("not a mac address")

0 commit comments

Comments
 (0)