I wanted my BTHome temp sensors to publish their data directly to MQTT, and rather than setting up an automation in Home Assistant, I figured an ESP32 could do it directly. But the syntax was way beyond me. So I asked claude.ai and it spit it out for me almost perfectly. So much easier than trudging through forum posts trying to figure out indents and spacing and all the intricacies of YAML configs.
esphome:
name: esp32-bluetooth-proxy
friendly_name: ESP32 Bluetooth Proxy
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "your-api-encryption-key-here"
# OTA updates
ota:
- platform: esphome
password: "your-ota-password-here"
# WiFi configuration
wifi:
ssid: "Your_WiFi_SSID"
password: "Your_WiFi_Password"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "ESP32-Bluetooth-Proxy Fallback Hotspot"
password: "fallback-password"
captive_portal:
# Web server (optional - for debugging)
web_server:
port: 80
# MQTT configuration
mqtt:
broker: 192.168.1.100 # Replace with your MQTT broker IP
port: 1883
username: "your-mqtt-username"
password: "your-mqtt-password"
client_id: esp32_bluetooth_proxy
discovery: true
discovery_retain: false
topic_prefix: "homeassistant"
id: mqtt_client_component # ID for referencing in lambda functions
birth_message:
topic: "homeassistant/esp32_bluetooth_proxy/status"
payload: "online"
will_message:
topic: "homeassistant/esp32_bluetooth_proxy/status"
payload: "offline"
# Global variables for device name mapping
globals:
- id: device_names
type: std::map<std::string, std::string>
restore_value: no
initial_value: '{
{"AA:BB:CC:DD:EE:FF", "bedroom_sensor"},
{"11:22:33:44:55:66", "living_room_sensor"},
{"77:88:99:AA:BB:CC", "kitchen_thermometer"},
{"DD:EE:FF:00:11:22", "bathroom_humidity"},
{"33:44:55:66:77:88", "outdoor_weather"}
}'
# Bluetooth proxy and BTHome configuration
esp32_ble_tracker:
scan_parameters:
interval: 320ms
window: 30ms
active: false # Passive scanning for better performance with many devices
on_ble_advertise:
then:
- lambda: |-
// Log BLE advertisement for debugging
ESP_LOGD("ble_adv", "BLE Advertisement from %s (RSSI: %d)", x.address_str().c_str(), x.get_rssi());
// Check if this is a BTHome advertisement
for (auto service_data : x.get_service_datas()) {
if (service_data.uuid == esp32_ble::ESPBTUUID::from_uint16(0x181C) ||
service_data.uuid == esp32_ble::ESPBTUUID::from_uint16(0xFCD2)) {
ESP_LOGD("bthome", "BTHome advertisement detected from %s", x.address_str().c_str());
// Parse BTHome data and publish to MQTT
std::string mac_address = x.address_str();
std::string device_name = mac_address;
// Replace MAC with friendly name if found in device_names map
std::replace(device_name.begin(), device_name.end(), ':', '_');
if (id(device_names).count(mac_address) > 0) {
device_name = id(device_names)[mac_address];
}
// Parse BTHome format and publish individual sensors
if (service_data.data.size() >= 3) {
uint8_t bthome_info = service_data.data[0];
bool encrypted = (bthome_info & 0x01) != 0;
uint8_t version = (bthome_info >> 5) & 0x07;
if (!encrypted && version == 2) { // BTHome v2, unencrypted
// Parse sensor data starting from byte 1 and build JSON
std::string json_data = "{";
json_data += "\"device\":\"" + device_name + "\",";
json_data += "\"mac\":\"" + mac_address + "\",";
json_data += "\"rssi\":" + to_string(x.get_rssi()) + ",";
json_data += "\"timestamp\":" + to_string(millis()) + ",";
bool first_sensor = true;
for (int i = 1; i < service_data.data.size(); ) {
if (i >= service_data.data.size()) break;
uint8_t object_id = service_data.data[i++];
std::string sensor_key = "";
std::string sensor_value = "";
std::string sensor_unit = "";
switch (object_id) {
case 0x01: // Battery %
if (i < service_data.data.size()) {
sensor_key = "battery";
sensor_value = to_string(service_data.data[i++]);
sensor_unit = "%";
}
break;
case 0x02: // Temperature °C
if (i + 1 < service_data.data.size()) {
sensor_key = "temperature";
int16_t temp = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(temp / 100.0f);
sensor_unit = "°C";
i += 2;
}
break;
case 0x03: // Humidity %
if (i + 1 < service_data.data.size()) {
sensor_key = "humidity";
uint16_t hum = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(hum / 100.0f);
sensor_unit = "%";
i += 2;
}
break;
case 0x04: // Pressure hPa
if (i + 2 < service_data.data.size()) {
sensor_key = "pressure";
uint32_t press = (service_data.data[i+2] << 16) | (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(press / 100.0f);
sensor_unit = "hPa";
i += 3;
}
break;
case 0x05: // Illuminance lux
if (i + 2 < service_data.data.size()) {
sensor_key = "illuminance";
uint32_t lux = (service_data.data[i+2] << 16) | (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(lux / 100.0f);
sensor_unit = "lx";
i += 3;
}
break;
case 0x06: // Mass kg
if (i + 1 < service_data.data.size()) {
sensor_key = "mass";
uint16_t mass = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(mass / 100.0f);
sensor_unit = "kg";
i += 2;
}
break;
case 0x07: // Mass lb
if (i + 1 < service_data.data.size()) {
sensor_key = "mass_lb";
uint16_t mass = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(mass / 100.0f);
sensor_unit = "lb";
i += 2;
}
break;
case 0x08: // Moisture %
if (i + 1 < service_data.data.size()) {
sensor_key = "moisture";
uint16_t moist = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(moist / 100.0f);
sensor_unit = "%";
i += 2;
}
break;
case 0x0A: // CO2 ppm
if (i + 1 < service_data.data.size()) {
sensor_key = "co2";
uint16_t co2 = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(co2);
sensor_unit = "ppm";
i += 2;
}
break;
case 0x0D: // PM2.5 µg/m³
if (i + 1 < service_data.data.size()) {
sensor_key = "pm25";
uint16_t pm = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(pm);
sensor_unit = "µg/m³";
i += 2;
}
break;
case 0x0E: // PM10 µg/m³
if (i + 1 < service_data.data.size()) {
sensor_key = "pm10";
uint16_t pm = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(pm);
sensor_unit = "µg/m³";
i += 2;
}
break;
case 0x0F: // Binary sensor (generic)
if (i < service_data.data.size()) {
sensor_key = "binary_sensor";
sensor_value = service_data.data[i++] ? "true" : "false";
sensor_unit = "";
}
break;
case 0x10: // Power W
if (i + 2 < service_data.data.size()) {
sensor_key = "power";
uint32_t power = (service_data.data[i+2] << 16) | (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(power / 100.0f);
sensor_unit = "W";
i += 3;
}
break;
case 0x11: // Voltage V
if (i + 1 < service_data.data.size()) {
sensor_key = "voltage";
uint16_t volt = (service_data.data[i+1] << 8) | service_data.data[i];
sensor_value = to_string(volt / 1000.0f);
sensor_unit = "V";
i += 2;
}
break;
case 0x15: // Window/Door sensor
if (i < service_data.data.size()) {
sensor_key = "door";
sensor_value = service_data.data[i++] ? "true" : "false";
sensor_unit = "";
}
break;
case 0x16: // Motion sensor
if (i < service_data.data.size()) {
sensor_key = "motion";
sensor_value = service_data.data[i++] ? "true" : "false";
sensor_unit = "";
}
break;
default:
// Skip unknown sensor types
ESP_LOGW("bthome", "Unknown BTHome object ID: 0x%02X", object_id);
i++; // Skip at least one byte
break;
}
// Add sensor data to JSON if we parsed something
if (!sensor_key.empty() && !sensor_value.empty()) {
if (!first_sensor) {
json_data += ",";
}
json_data += "\"" + sensor_key + "\":{";
json_data += "\"value\":" + sensor_value;
if (!sensor_unit.empty()) {
json_data += ",\"unit\":\"" + sensor_unit + "\"";
}
json_data += "}";
first_sensor = false;
}
}
json_data += "}";
// Publish JSON data to single topic
std::string json_topic = "bthome/" + device_name + "/sensors";
id(mqtt_client_component).publish(json_topic, json_data);
ESP_LOGD("bthome", "Published JSON: %s", json_data.c_str());
}
}
break; // Only process first BTHome service data
}
}
bluetooth_proxy:
active: true
# Status LED (optional)
status_led:
pin: GPIO2
# Example sensors that publish to MQTT
sensor:
# WiFi signal strength
- platform: wifi_signal
name: "WiFi Signal Strength"
id: wifi_signal_db
update_interval: 60s
entity_category: "diagnostic"
on_value:
then:
- mqtt.publish:
topic: "esp32/wifi_signal"
payload: !lambda |-
return to_string(id(wifi_signal_db).state);
# Uptime sensor
- platform: uptime
name: "Uptime"
id: uptime_sensor
update_interval: 60s
on_value:
then:
- mqtt.publish:
topic: "esp32/uptime"
payload: !lambda |-
return to_string(id(uptime_sensor).state);
# Text sensors
text_sensor:
# Device info
- platform: wifi_info
ip_address:
name: "IP Address"
id: ip_address
on_value:
then:
- mqtt.publish:
topic: "esp32/ip_address"
payload: !lambda |-
return id(ip_address).state;
ssid:
name: "Connected SSID"
id: connected_ssid
on_value:
then:
- mqtt.publish:
topic: "esp32/ssid"
payload: !lambda |-
return id(connected_ssid).state;
# Binary sensors
binary_sensor:
# Connection status
- platform: status
name: "Status"
id: device_status
on_state:
then:
- mqtt.publish:
topic: "esp32/status"
payload: !lambda |-
return id(device_status).state ? "online" : "offline";
# Button for restart (optional)
button:
- platform: restart
name: "Restart"
id: restart_button
# Example: GPIO sensors/controls (customize as needed)
# Uncomment and modify based on your hardware setup
# switch:
# - platform: gpio
# pin: GPIO4
# name: "GPIO4 Switch"
# id: gpio4_switch
# on_turn_on:
# then:
# - mqtt.publish:
# topic: "esp32/gpio4"
# payload: "ON"
# on_turn_off:
# then:
# - mqtt.publish:
# topic: "esp32/gpio4"
# payload: "OFF"
# binary_sensor:
# - platform: gpio
# pin:
# number: GPIO5
# mode: INPUT_PULLUP
# name: "GPIO5 Button"
# id: gpio5_button
# on_press:
# then:
# - mqtt.publish:
# topic: "esp32/gpio5_button"
# payload: "PRESSED"
# on_release:
# then:
# - mqtt.publish:
# topic: "esp32/gpio5_button"
# payload: "RELEASED"