ESPHome Bluetooth Tracker for Medisana BU 575, BU584 Blood Pressure Monitor

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:

  1. Replace bp_mac with your Medisana BU 575, BU584 MAC address (find via Bluetooth scanner)
  2. Move WiFi credentials to secrets.yaml for security
  3. Generate new encryption/OTA passwords
  4. Adjust static IP settings for your network
  5. 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.

:white_check_mark: 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!

7 Likes

thanx, bro!

“I made a small correction to the config, so now user IDs are displayed correctly.”

Hi

Thanks a lot for the share, do you think it’ll work with the BU584 ? they look quite similar with Bluetooth too :wink:

Vincèn

Hi. I don’t know for sure, you’ll have to check. But I hope it works.

I confirm it works fine, time to adjust sketch of my ESP32 used for BLE proxy and do a measure with device and it’s all good :+1:

Thanks a lot for the share !

Fine! For my BPM it works perfect!

Yeah I get BPM, systole and diastole :+1: