ESPHome Bluetooth Tracker for Contec PC-60FW fingertip oximeter

ESPHome configuration for connecting the Contec PC-60FW Bluetooth fingertip oximeter to Home Assistant. It’s a dumbed down copy of more sophisticated code originally written by Jeffrey Kosowsky (puterboy).

Sensor Outputs:

  • :white_check_mark: SpO2 - Blood oxygen saturation (0-100%)
  • :white_check_mark: Pulse - Heart rate (BPM)
  • :white_check_mark: Perfusion Index - (0.0-25.5%)
  • :white_check_mark: Battery Level - Device battery status (0-3 scale: 0=Low, 1=Medium, 2=High, 3=Full)
substitutions:
  # Customize these
  device_name: "pulsometer"
  device_friendly_name: "Pulse Oximeter Bridge"

globals:
  - id: record_to_ha
    type: bool
    restore_value: yes
    initial_value: 'true'
  - id: spo2_current
    type: uint8_t
    restore_value: no
  - id: pulse_current
    type: uint8_t
    restore_value: no
  - id: perf_index_current
    type: uint8_t
    restore_value: no
  - id: battery_current
    type: uint8_t
    restore_value: no

esphome:
  name: ${device_name}
  friendly_name: ${device_friendly_name}

esp32:
  board: esp32dev
  framework:
    type: esp-idf

logger:
  level: INFO

api:
  # Generate your own key: esphome secrets

ota:
  # Add password in secrets

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # Optional: Configure static IP below if needed
  # manual_ip:
  #   static_ip: 192.168.x.x
  #   gateway: 192.168.x.x
  #   subnet: 255.255.255.0
  ap:
    ssid: "${device_friendly_name} Fallback"
    password: !secret fallback_password

captive_portal:

esp32_ble_tracker:
  scan_parameters:
    interval: 3200ms
    window: 160ms
    active: false

ble_client:
  - mac_address: "00:00:00:03:10:4E"  # Replace with your device MAC
    id: pc_60fw

sensor:
  - platform: ble_client
    type: characteristic
    id: internal_pulseox
    internal: true
    ble_client_id: pc_60fw
    service_uuid: '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
    characteristic_uuid: '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
    notify: true
    lambda: |-
      // Data packet (12 bytes): SpO2, Pulse, Perfusion Index
      if(x.size() == 12 && x[0] == 0xAA && x[1] == 0x55 && x[2] == 0x0F && x[3] == 0x08) {
        if(x[5] != 0) {
          id(spo2_current) = x[5];
          if(id(record_to_ha)) id(spo2).publish_state(id(spo2_current));
        }
        if(x[6] != 0) {
          id(pulse_current) = x[6];
          if(id(record_to_ha)) id(pulse).publish_state(id(pulse_current));
        }
        if(x[8] != 0) {
          float pi_value = x[8] / 10.0;
          id(perf_index_current) = x[8];
          if(id(record_to_ha)) id(perfindex).publish_state(pi_value);
        }
      // Battery packet (7 bytes)
      }else if (x.size() == 7 && x[0] == 0xAA && x[1] == 0x55 && x[2] == 0xF0 && x[3] == 0x3 && x[4] == 0x3) {
        if(x[5] != 0) {
          id(battery_current) = x[5];
          if(id(record_to_ha)) id(battery_level).publish_state(id(battery_current));
        }
      }
      return {};

  - platform: template
    id: spo2
    name: "SpO2"
    icon: 'mdi:lung'
    unit_of_measurement: '%'
    accuracy_decimals: 0
    filters:
      - delta: 0.5
      - throttle: 1s

  - platform: template
    id: pulse
    name: "Pulse"
    icon: 'mdi:heart-pulse'
    unit_of_measurement: 'bpm'
    accuracy_decimals: 0
    filters:
      - delta: 0.5
      - throttle: 1s

  - platform: template
    id: perfindex
    name: "Perfusion Index"
    icon: 'mdi:waves'
    unit_of_measurement: '%'
    accuracy_decimals: 1
    filters:
      - delta: 0.05
      - throttle: 1s

  - platform: template
    id: battery_level
    name: "Battery Level"
    icon: 'mdi:battery'
    unit_of_measurement: '%'
    accuracy_decimals: 0
    filters:
      - delta: 0.5
      - throttle: 1s

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "IP Address"

binary_sensor:
  - platform: status
    name: "Status"

switch:
  - platform: template
    name: "Record to Home Assistant"
    id: record_to_ha_switch
    lambda: |-
      return id(record_to_ha);
    turn_on_action:
      - globals.set:
          id: record_to_ha
          value: 'true'
    turn_off_action:
      - globals.set:
          id: record_to_ha
          value: 'false'

My current configuration

name: pulsometer
friendly_name: pulsometer

esp32:
  board: esp32-c3-devkitm-1  
  framework:
    type: esp-idf

logger:
  level: INFO

api:
  encryption:
    key: "YOUR_ENCRYPTION_KEY"

ota:
  - platform: esphome
    password: "YOUR_OTA_PASSWORD"

wifi:
  ssid: "YOUR_WIFI_SSID"
  password: "YOUR_WIFI_PASSWORD"  
  manual_ip:
    static_ip: YOUR_STATIC_IP
    gateway: YOUR_GATEWAY
    subnet: YOUR_SUBNET
    dns1: YOUR_DNS
  output_power: 9.5dB   
  fast_connect: true
  passive_scan: false
  
globals:
  - id: pulsometer_spo2
    type: int
    restore_value: yes
    initial_value: '0'
  - id: pulsometer_pulse
    type: int
    restore_value: yes
    initial_value: '0'
  - id: pulsometer_perf_index
    type: float
    restore_value: yes
    initial_value: '0.0'
  - id: pulsometer_battery_current
    type: int
    restore_value: yes
    initial_value: '0'
  - id: pulsometer_ble_connected
    type: bool
    restore_value: no
    initial_value: 'false'

esp32_ble_tracker:
  scan_parameters:
    interval: 1200ms
    window: 400ms
    active: false
    
ble_client:
  - mac_address: "YOUR_DEVICE_MAC_ADDRESS"
    id: pc_60fw
    auto_connect: false
    on_connect:
      then:
        - lambda: |-
            id(pulsometer_ble_connected) = true;
    on_disconnect:
      then:
        - lambda: |-
            id(pulsometer_ble_connected) = false;

interval:
  - interval: 3s
    then:
      - text_sensor.template.publish:
          id: pulsometer_status
          state: !lambda |-
            if (id(pulsometer_ble_connected)) {
              return "Connected";
            } else {
              return "Waiting";
            }      
      - lambda: |-
          static uint64_t last_connect_attempt = 0;
          uint64_t now = millis();          
          if (!id(pc_60fw)->connected() && 
              id(pulsometer_detected).state &&
              (now - last_connect_attempt > 10000)) { 
            last_connect_attempt = now;
            id(pc_60fw)->connect();
          }
          if (id(pc_60fw)->connected() && 
              !id(pulsometer_detected).state) {
            static uint64_t disconnect_timer = 0;
            if (disconnect_timer == 0) {
              disconnect_timer = now;
            } else if (now - disconnect_timer > 5000) {  
              id(pc_60fw)->disconnect();
              disconnect_timer = 0;
            }
          } else {
            static uint64_t disconnect_timer = 0;
            disconnect_timer = 0; 
          }

sensor:
  - platform: ble_client
    type: characteristic
    id: internal_pulseox
    internal: true
    ble_client_id: pc_60fw
    service_uuid: '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
    characteristic_uuid: '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
    notify: true
    lambda: |-
      if(x.size() == 12 && x[0] == 0xAA && x[1] == 0x55 && x[2] == 0x0F && x[3] == 0x08) {
        if(x[5] != 0) {
          id(pulsometer_spo2) = x[5];
          id(spo2).publish_state((float)id(pulsometer_spo2));
        }
        if(x[6] != 0) {
          id(pulsometer_pulse) = x[6];
          id(pulse).publish_state((float)id(pulsometer_pulse));
        }
        if(x[8] != 0) {         
          id(pulsometer_perf_index) = (float)x[8] / 10.0f;
          id(perf_index).publish_state(id(pulsometer_perf_index));
        }
      }else if (x.size() == 7 && x[0] == 0xAA && x[1] == 0x55 && x[2] == 0xF0 && x[3] == 0x3 && x[4] == 0x3) {
        if(x[5] != 0) {
          id(pulsometer_battery_current) = x[5];
          float battery_percent = ((float)id(pulsometer_battery_current) / 3.0f) * 100.0f;
          if(battery_percent < 0.0f) battery_percent = 0.0f;
          if(battery_percent > 100.0f) battery_percent = 100.0f;
          id(pulsometer_battery).publish_state(battery_percent);
        }
      }
      return {};

  - platform: template
    id: spo2
    name: "SpO2"
    icon: 'mdi:lung'
    unit_of_measurement: '%'
    accuracy_decimals: 0
    filters:
      - throttle: 1s
    lambda: 'return id(pulsometer_spo2);'  

  - platform: template
    id: pulse
    name: "Pulse"
    icon: 'mdi:heart-pulse'
    unit_of_measurement: 'bpm'
    accuracy_decimals: 0
    filters:
      - throttle: 1s
    lambda: 'return id(pulsometer_pulse);'  

  - platform: template
    id: perf_index
    name: "Perf Index"
    icon: 'mdi:waves'
    unit_of_measurement: '%'
    accuracy_decimals: 1
    filters:
      - throttle: 1s
    lambda: 'return id(pulsometer_perf_index);'  

  - platform: template
    id: pulsometer_battery
    name: "Pulsometer Battery"
    icon: 'mdi:battery'
    unit_of_measurement: '%'
    accuracy_decimals: 0
    filters:
      - throttle: 1s
    lambda: |-
      if (id(pulsometer_battery_current) == 0) {
        return 0.0f;
      }
      float battery_percent = ((float)id(pulsometer_battery_current) / 3.0f) * 100.0f;
      if (battery_percent < 0.0f) battery_percent = 0.0f;
      if (battery_percent > 100.0f) battery_percent = 100.0f;
      return battery_percent; 

text_sensor:
  - platform: template
    id: pulsometer_status
    name: "Pulsometer Status"
    icon: 'mdi:bluetooth'
  
  - platform: wifi_info
    ip_address:
      name: "IP Address"
    ssid:
      name: "SSID"
    mac_address:
      name: "Mac Address"

binary_sensor:
  - platform: status
    name: "Status"

  - platform: ble_presence
    mac_address: "YOUR_DEVICE_MAC_ADDRESS"
    name: "Pulsometer Detected"
    id: pulsometer_detected