Blue Charm Beacons, BLE and ESPHome

All I wanted to do, was get the temperature and battery reading off of a beacon. I didn't think it would take me down this rabbit hole for a week and a half. I was being stubborn and had it in my head that I was going to use ESPHome to do it. No other hardware, that was it. I had purchased a Blue Charm Beacon a while ago and I wanted to make use of the built-in thermometer.

I had to do a little research on the beacons themselves. Blue Charm beacons are very versatile. There is an app, KBeacon Pro, available on mobile, that allows you to reconfigure it. You can change what is broadcast in it's 5 slots and when it is broadcast. It can be advertised on motion, or all the time.

I strongly recommend going to their Quick Start Guide and following their instructions there. This will set you up to be comfortable with the app and know what you are doing to get the data you need. I used the BC04P Beacons,

This is where I could have made my life easier. Blue Charm does have a gateway that accepts the data from the beacon and will broadcast it via MQTT to wherever you need it to go. I decided not to. I chose to turn on TLM and the KSensor functionality and try to parse the data from that. I would have to create sensors in the ESPHome YAML config files to make it available.

RSSI Sensor

The first sensor I created was for signal strength. This one is straightforward, there is a built-in platform for RSSI:

- platform: ble_rssi
    mac_address: DD:34:02:XX:XX:XX
    name: "BLE BCPro_204664 RSSI"

BLE UUID Data Scanning

I started running into problems when I tried to get the battery and temperature data though. Blue Charm doesn't use the standard service and characteristic IDs for these. I had to use a script that would display in the live logs what the beacon was advertising. I tried a couple different apps but could not get anything to show me this on my phone.

on_ble_advertise:
    - mac_address: DD:34:02:XX:XX:XX
      then:
        - lambda: |-
              for (auto data : x.get_service_datas())
                ESP_LOGI("main", "Data Update UUID: %s Data: %s", data.uuid.to_string().c_str(), format_hex_pretty(data.data).c_str());

This filtered for the MAC of the beacon, then for any service data it provided a print out of what it was receiving:

[00:33:56.020][I][main:052]: Data Update UUID: 0xFEAA Data: 21.00.03.0B.AF.02.00 (7) [00:33:56.962][I][main:052]: Data Update UUID: 0xFEAA Data: 20.00.0B.AF.02.00.00.2F.49.F1.0F.07.29.EA (14) [00:33:57.052][I][main:052]: Data Update UUID: 0xFEAA Data: 21.00.03.0B.AF.02.00 (7)

In this example you can see the service data FEAA was exporting two different bit-lengths of data, 7 and 14. It took some research, but I was able to discover that FEAA was the TLM data. The TLM fields described here show that the one I needed started with 32, or h(20). I used the TLM field guide and bit bench to help analyze the real readings I was getting and it slowly started making sense.

Temperature Sensor

In Bit Bench I created this template for TLM. Fields 4 and 5 are combined to make the degrees in C, and 5 is divided by 256. The temp in the example above, AF 02, is 2C or 35.6F.

Now, to program that.

- mac_address: DD:34:02:XX:XX:XX
      service_uuid: 'FEAA'
      then:
          - lambda: |-
              if  (x[0]==32) {
                // 8.8 Notation: x[4] is the whole number, x[5] is the fraction
                // Combine them into a signed 16-bit integer to handle negatives
                int16_t raw_val = (int16_t)((x[4] << 8) | x[5]);
                // Convert to float by dividing by 2^8 (256.0)
                float tempc = raw_val / 256.0;
                // Convert from C to F
                float tempf = (((9*tempc) / 5) + 32);
                // Update the template sensor
                id(ble_temp).publish_state(tempf);
              }

Since there is more than one data type I'm filtering first for lines that start with 32. Then combining the 4th and 5th sets to create a 16 bit value. Because I am in the US I had to use the conversion formula to convert C to F, then publish that value to the template sensor ble_temp.

- platform: template
    name: "BLE BCPro_204664 Temperature"
    id: ble_temp
    icon: 'mdi:thermometer'
    unit_of_measurement: 'F'
    device_class: "Temperature"
    accuracy_decimals: 2

Battery Sensor

I also wanted to get the battery data. TLM only really transmits the voltage, so I would have to work with that. Using the TLM field guide and Bit Bench I referenced above, I started working on it. I now know that I need the line starting with 32. The reference pointed me to fields 2 and 3, this time. They are to combined as a 16-bit value like the temperature. This would give me a total bit value, and there is 1 bit per mV.

    - mac_address: DD:34:02:XX:XX:XX
      service_uuid: 'FEAA'
      then:
        - lambda: |-
            // only get lines that start with 32
            if (x[0]==32) {
              int16_t raw_val = (int16_t)((x[2] << 8) | x[3]);
              float battva = raw_val;
              id(ble_battery).publish_state(battva);
            }

So my template would take this published amount and set it as the total mV value of the battery.

  - platform: template
    name: "BLE BCPro_204664 Battery"
    id: ble_battery
    icon: 'mdi:battery'
    unit_of_measurement: 'mV'
    device_class: "Battery"
    entity_category: "diagnostic"

BLE Active Scan

Now that I am getting all of that data, the beacon is getting tired. It needs to rest. Rather than scanning it constantly I wanted to set up a script to only scan it every 10 minutes or so. This is a fairly straightforward script, but you will notice there is an id in there you may not have seen yet, ble_hub.

script:
  - id: ble_active_scan
    mode: restart
    then:
      - while:
          condition:
            lambda: 'return true;'
          then:
            - lambda: 'id(ble_hub).start_scan();'
            - logger.log: "BLE Scanning Started"
            - delay: 30s
            - lambda: 'id(ble_hub).stop_scan();'
            - logger.log: "BLE Scanning Stopped"
            - delay: 10min

ble_hub needs to be added as the id for the esp32_ble_tracker. An id isn't typically required for the ble_tracker, unless you're going to call it from a script. Also set active to false if it is already set to true.

esp32_ble_tracker:
  scan_parameters:
    interval: 10s
    window: 1100ms
    active: false
  id: ble_hub

The script needs to be started on boot so this needs to be added to the top of your config file.

esphome:
  name: my-esphome
  friendly_name: My ESPHome
  on_boot:
    then:
      - script.execute: ble_active_scan

And, if you want to be able to get the data on demand you can add a button.

button:
  - platform: template
    name: "BLE Active Scan"
    id: run_active_scan
    on_press:
    - script.execute: ble_active_scan

Dashboard

I still need to set up the Beacon to advertise on Motion, and the ESP32 needs programmed to receive it, but I'll save that for another day. For now, I'm going to create a dashboard so we can put it all together.


Here, I used an Apex chart for the temperature, and included the National Weather Service temperature for comparison. The battery reading is shown in the default gauge card, where is set the minimum display to 2550 and maximum to 3100. I added the needle and created arbitrary settings for green(2900), yellow(2700) and red(0). I'm not actually sure at what point the bacon would lose power but this is easy to adjust.

I decided to add the RSSI and the Active Scan button as badges. They won't get used or viewed much so it's nice to have them out of the way.

This was a fun project to bang my brain on for a while.



References

Blue Charm beacons has the best BLE beacons in my opinion.


triq.org has some amazing tools for SDR and this template for TLM is one I recreated and was able to share from Bit Bench.



Apex Charts are available in Home Assistant's HACS menu.

Comments

Popular posts from this blog

Blue Charm beacons, TLM data via ESPHome

Tracking BMs with Home Assistant AKA Poop Button

Mason Bees