ESPHome configuration for CONTEC PC-60FW Bluetooth pulse oximeter

ESPHome configuration for connecting the CONTEC PC-60FW Bluetooth pulse 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'