Description:
I’ve created an ESPHome-based Bluetooth tracker for the Medisana BU 575, BU584 blood pressure monitor that automatically integrates readings into Home Assistant. This project allows you to capture systolic/diastolic pressure, mean arterial pressure (MAP), pulse, user id and measurement timestamps without using the official app.
Features:
- Automatic connection and data parsing from Medisana BU 575, BU584
- Publishes all key metrics as Home Assistant sensors
- Tracks measurement status and timestamps
- Auto-reconnection with fallback hotspot
- Status indicators for monitoring connection health
- 2 id for 2 users
Configuration:
Here’s my ESPHome YAML configuration with sensitive data replaced:
# Replace these values with your own
substitutions:
bp_mac: "AA:BB:CC:DD:EE:FF" # Replace with your tonometer's MAC address
device_name: "tonometer"
friendly_name: "Tonometer"
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: INFO
# Generate your own encryption key with: esphome generate_encryption_key
api:
encryption:
key: "GENERATE_YOUR_OWN_ENCRYPTION_KEY_HERE"
# Generate your own OTA password
ota:
- platform: esphome
password: "GENERATE_YOUR_OWN_OTA_PASSWORD_HERE"
wifi:
ssid: "YOUR_WIFI_SSID" # Replace with your WiFi SSID
password: "YOUR_WIFI_PASSWORD" # Replace with your WiFi password
min_auth_mode: WPA2
# Configure IP settings for your network, or remove for DHCP
manual_ip:
static_ip: 192.168.1.100
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 192.168.1.1
# Fallback hotspot when WiFi is unavailable
ap:
ssid: "${friendly_name} Fallback Hotspot"
password: "GENERATE_FALLBACK_PASSWORD_HERE"
# Global variables to store measurement data
globals:
- id: last_systolic
type: int
initial_value: '0'
- id: last_diastolic
type: int
initial_value: '0'
- id: last_map
type: int
initial_value: '0'
- id: last_pulse
type: int
initial_value: '0'
- id: last_user_id
type: int
initial_value: '0'
- id: last_measurement_year
type: int
initial_value: '0'
- id: last_measurement_month
type: int
initial_value: '0'
- id: last_measurement_day
type: int
initial_value: '0'
- id: last_measurement_hour
type: int
initial_value: '0'
- id: last_measurement_minute
type: int
initial_value: '0'
- id: last_packet_time
type: int
initial_value: '0'
# Flag to track if measurement is complete
- id: measurement_complete
type: bool
initial_value: 'false'
# BLE client for connecting to Medisana BU575 tonometer
ble_client:
- mac_address: "${bp_mac}"
id: medisana_bu575
on_connect:
then:
- lambda: |-
ESP_LOGI("main", "Connected to tonometer");
id(measurement_complete) = false;
on_disconnect:
then:
- lambda: |-
ESP_LOGI("main", "Disconnected from tonometer");
# If measurement wasn't marked complete, publish final values
if (!id(measurement_complete)) {
ESP_LOGI("main", "Publishing final measurement values");
id(sys_sensor).publish_state(id(last_systolic));
id(dia_sensor).publish_state(id(last_diastolic));
id(map_sensor).publish_state(id(last_map));
id(pulse_sensor).publish_state(id(last_pulse));
id(user_id_sensor).publish_state(id(last_user_id));
id(measurement_complete) = true;
}
- delay: 15s # Wait before reconnecting
- lambda: 'id(medisana_bu575)->connect();'
sensor:
# Main BLE characteristic that receives blood pressure data
- platform: ble_client
type: characteristic
ble_client_id: medisana_bu575
service_uuid: "00001810-0000-1000-8000-00805f9b34fb" # Blood Pressure Service
characteristic_uuid: "00002a35-0000-1000-8000-00805f9b34fb" # Blood Pressure Measurement
name: "Blood Pressure Data"
internal: true # This sensor doesn't publish to HA, only processes data internally
notify: true # Enable notifications from the device
# Lambda function to decode BLE blood pressure data
lambda: |-
if (x.size() < 19) {
return {};
}
# Helper function to decode IEEE 11073 16-bit SFLOAT format
auto decode_sfloat = [](uint16_t raw) -> float {
int8_t exponent = (raw >> 12) & 0x0F;
if (exponent >= 0x08) {
exponent = -((0x0F + 1) - exponent);
}
int16_t mantissa = raw & 0x0FFF;
if (mantissa >= 0x0800) {
mantissa = -((0x0FFF + 1) - mantissa);
}
return mantissa * pow(10, exponent);
};
# Parse flags byte to determine which data is present
uint8_t flags = x[0];
bool timestamp_present = flags & 0x02;
bool pulse_rate_present = flags & 0x04;
bool user_id_present = flags & 0x08;
int offset = 1;
# Read systolic, diastolic, and mean arterial pressure
uint16_t systolic_raw = (x[offset + 1] << 8) | x[offset];
offset += 2;
uint16_t diastolic_raw = (x[offset + 1] << 8) | x[offset];
offset += 2;
uint16_t map_raw = (x[offset + 1] << 8) | x[offset];
offset += 2;
# Decode values from SFLOAT format
float systolic_f = decode_sfloat(systolic_raw);
float diastolic_f = decode_sfloat(diastolic_raw);
float mean_ap_f = decode_sfloat(map_raw);
uint16_t systolic = (uint16_t)systolic_f;
uint16_t diastolic = (uint16_t)diastolic_f;
uint16_t mean_ap = (uint16_t)mean_ap_f;
# Initialize time variables
uint16_t year = 0;
uint8_t month = 0;
uint8_t day = 0;
uint8_t hour = 0;
uint8_t minute = 0;
# Parse timestamp if present
if (timestamp_present) {
year = (x[offset + 1] << 8) | x[offset];
offset += 2;
month = x[offset++];
day = x[offset++];
hour = x[offset++];
minute = x[offset++];
offset++; # Skip seconds field
}
# Parse pulse rate if present
uint8_t pulse = 0;
if (pulse_rate_present) {
uint16_t pulse_raw = (x[offset + 1] << 8) | x[offset];
pulse = (uint8_t)decode_sfloat(pulse_raw);
offset += 2;
}
# Parse user ID if present
uint8_t user_id = 0;
if (user_id_present && offset < x.size()) {
user_id = x[offset];
}
# Store values in global variables
id(last_systolic) = systolic;
id(last_diastolic) = diastolic;
id(last_map) = mean_ap;
id(last_pulse) = pulse;
id(last_user_id) = user_id;
id(last_measurement_year) = year;
id(last_measurement_month) = month;
id(last_measurement_day) = day;
id(last_measurement_hour) = hour;
id(last_measurement_minute) = minute;
id(last_packet_time) = millis();
return {};
# Template sensors that publish to Home Assistant
- platform: template
name: "${friendly_name} Systolic"
id: sys_sensor
unit_of_measurement: "mmHg"
icon: "mdi:heart"
accuracy_decimals: 0
lambda: 'return id(last_systolic);'
- platform: template
name: "${friendly_name} Diastolic"
id: dia_sensor
unit_of_measurement: "mmHg"
icon: "mdi:heart-outline"
accuracy_decimals: 0
lambda: 'return id(last_diastolic);'
- platform: template
name: "${friendly_name} Mean Arterial Pressure"
id: map_sensor
unit_of_measurement: "mmHg"
icon: "mdi:heart-box"
accuracy_decimals: 0
lambda: 'return id(last_map);'
- platform: template
name: "${friendly_name} Pulse"
id: pulse_sensor
unit_of_measurement: "bpm"
icon: "mdi:heart-pulse"
accuracy_decimals: 0
lambda: 'return id(last_pulse);'
- platform: template
name: "${friendly_name} User ID"
id: user_id_sensor
icon: "mdi:account"
accuracy_decimals: 0
lambda: 'return id(last_user_id);'
text_sensor:
# WiFi information sensors
- platform: wifi_info
ip_address:
name: "${friendly_name} IP Address"
ssid:
name: "${friendly_name} SSID"
mac_address:
name: "${friendly_name} Mac Address"
# Measurement status indicator
- platform: template
name: "${friendly_name} Measurement Status"
id: measurement_status_text
icon: "mdi:progress-clock"
lambda: |-
if (id(measurement_complete)) {
return std::string("Complete");
} else {
return std::string("In Progress");
}
update_interval: 5s
# Formatted timestamp of last measurement
- platform: template
name: "${friendly_name} Last Measurement Time"
id: last_measurement_time
icon: "mdi:clock"
lambda: |-
if (id(last_measurement_year) == 0) {
return std::string("No measurement");
}
char buffer[20];
sprintf(buffer, "%04d-%02d-%02d %02d:%02d",
id(last_measurement_year),
id(last_measurement_month),
id(last_measurement_day),
id(last_measurement_hour),
id(last_measurement_minute));
return std::string(buffer);
# Device status binary sensor
binary_sensor:
- platform: status
name: "${friendly_name} Status"
# Interval to reset measurement completion flag if no new data
interval:
- interval: 120s
then:
- lambda: |-
# If no data received for 3 minutes, reset completion flag
if (id(last_packet_time) > 0 && (millis() - id(last_packet_time)) > 180000) {
ESP_LOGI("main", "Resetting measurement completion flag");
id(measurement_complete) = false;
}
Setup Instructions:
- Replace
bp_macwith your Medisana BU 575, BU584 MAC address (find via Bluetooth scanner) - Move WiFi credentials to
secrets.yamlfor security - Generate new encryption/OTA passwords
- Adjust static IP settings for your network
- Flash to an ESP32 board
How it works:
The device connects via Bluetooth LE to the tonometer and parses the data packet (19 bytes) from the blood pressure service (00001810-... ). It extracts all measurements and timestamps, then publishes them as template sensors. The tracker detects when measurements are complete and handles disconnection/reconnection automatically.
Supported Devices
Currently, this bt tracker has been tested and confirmed to work with:
● Medisana BU 575
● Medisana BU 584
If you successfully use this tracker with another Medisana device, please let me to know to help extend the supported list.
Hope this helps others integrate their blood pressure monitors into Home Assistant!