Well, that title was longer than I'd expected.

I've recently become responsible for the tech and facility management at a small, 70 capacity, village theatre. One of our biggest problems is heating (it's 28kW of electric fans... hardly efficient in the modern world!) but more importantly it doesn't work well.

The first step to finding a solution for this is understanding the problem, and in understanding any problem the more data you have the better.

Part of this I achieved with a (relatively affordable) thermal imaging camera which quickly hilighted areas where we can improve insulation and one of the overhead vents wasn't working as expected.

Next, I set about finding a way to accurately monitor temperature and the rate it rises/falls at when the heating is in use, and how evenly the space is heated.

I stumbled upon ThermoBeacon devices, which have helpfully already been reverse engineered by others and the Brifit branded sensors on Amazon for £16 per pair at the time of writing.

These are perfect for what I needed, cheap, decent battery life, and BLE so sniffable rather than just using the standard app.

I started with the excellent thermobeacon.py on a pi that's already in the building and, helpfully, it instantly started spitting out data

Device: C Temperature: 10.3125 degC Humidity: 56.625% 
Uptime: 2 Days 1 Hours 47 Minutes 4 Seconds sec Voltage: 2.933V
Device: A Temperature: 10.875 degC Humidity: 53.3125% 
Uptime: 2 Days 4 Hours 12 Minutes 1 Seconds sec Voltage: 2.942V
Device: B Temperature: 11.5625 degC Humidity: 51.25% 
Uptime: 2 Days 1 Hours 46 Minutes 37 Seconds sec Voltage: 2.935V
Device: D Temperature: 10.25 degC Humidity: 56.0% 
Uptime: 2 Days 1 Hours 46 Minutes 20 Seconds sec Voltage: 2.936V

I've recently started using ESP32 microcontrollers for many things, so it seemed the next stage was making a simple display that could be mounted on a wall showing the current temperature of the space.

The little Heltec ESP32 board was ideal for this, with its integrated OLED display. The following code queries my four sensors and displays it on the display;

#include "Arduino.h"
#include "heltec.h"
#include "NimBLEDevice.h"
#include <ArduinoJson.h>
#include <WiFiMulti.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include "Hash.h"

String strTempA = "-";
String strTempB = "-";
String strTempC = "-";
String strTempD = "-";

int intExpCountA = 0;
int intExpCountB = 0;
int intExpCountC = 0;
int intExpCountD = 0;

void updateDisplay() {
  Heltec.display->clear();
  Heltec.display->setColor(WHITE);
  Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
  Heltec.display->drawString(26, 5, strTempA);
  Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
  Heltec.display->drawString(26, 40, strTempB);
  Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
  Heltec.display->drawString(101, 5, strTempC);
  Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
  Heltec.display->drawString(101, 40, strTempD);

  // invalid cross
  if (intExpCountA > 20) { Heltec.display->drawLine(2, 27, 52, 2); Heltec.display->drawLine(3, 27, 53, 2); }
  if (intExpCountB > 20) { Heltec.display->drawLine(2, 62, 52, 37); Heltec.display->drawLine(3, 62, 53, 37); }
  if (intExpCountC > 20) { Heltec.display->drawLine(76, 27, 126, 2); Heltec.display->drawLine(77, 27, 127, 2); }
  if (intExpCountD > 20) { Heltec.display->drawLine(76, 62, 126, 37); Heltec.display->drawLine(77, 62, 127, 37); }


  delay(300);
  Heltec.display->display();
}

class MyAdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks {
    void onResult(NimBLEAdvertisedDevice* advertisedDevice) {
      if (advertisedDevice->getName() == "ThermoBeacon") {
        BLEAddress address = advertisedDevice->getAddress();
        String mact = String(address.toString().c_str());

        uint8_t* MFRdata;
        MFRdata = (uint8_t*)advertisedDevice->getManufacturerData().data();
        int len = advertisedDevice->getManufacturerData().length();
        if (len != 20) {
          return;
        } 
        
        // get the values from this device
        float batt = static_cast<int16_t>(MFRdata[10] + (MFRdata[11] << 8));
        float temp = static_cast<int16_t>(MFRdata[12] + (MFRdata[13] << 8)) / 16.f;
        float humi = static_cast<int16_t>(MFRdata[14] + (MFRdata[15] << 8)) / 16.f;

        // remap them to strings for sprintf
        char strBatt[4];
        char strTemp[8];
        char strHumi[8];
        dtostrf(batt, 2, 0, strBatt);
        dtostrf(temp, 2, 2, strTemp);
        dtostrf(humi, 2, 2, strHumi);

        // update the local variables (this is horribly hacky but it works) 
        if (mact == "fd:38:00:00:1d:bd") { 
          altTempA = false;
          intExpCountA = 0;
          strTempA = strTemp; 
          if (strBtn == "true") {
            altTempA = true; 
          }
        }
        if (mact == "fd:38:00:00:15:89") { 
          altTempB = false;
          intExpCountB = 0;
          strTempB = strTemp; 
          if (strBtn == "true") {
            altTempB = true; 
          }
        } 
        if (mact == "fd:38:00:00:18:26") { 
          altTempC = false;
          intExpCountC = 0;
          strTempC = strTemp; 
          if (strBtn == "true") {
            altTempC = true; 
          }
        }
        if (mact == "fd:38:00:00:14:c2") { 
          altTempD = false;
          intExpCountD = 0;
          strTempD = strTemp; 
          if (strBtn == "true") {
            altTempD = true; 
          }
        }

        // trigger an update of the local display
        updateDisplay();
      }
    }
};

void setup() {
  // Display
  Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Disable*/, true /*Serial Enable*/);
  Heltec.display->clear();
  Heltec.display->flipScreenVertically();
  Heltec.display->setFont(ArialMT_Plain_16);

  delay(1000);
  Heltec.display->setColor(WHITE);
  Heltec.display->fillRect(0, 0, 128, 64);
  Heltec.display->display();
  Heltec.display->setColor(BLACK);
  Heltec.display->fillRect(0, 0, 128, 64);
  Heltec.display->display();
  Heltec.display->clear();

  // start the BLE scanner
  NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA_DEVICE);
  NimBLEDevice::setScanDuplicateCacheSize(20);
  NimBLEDevice::init("");
  pBLEScan = NimBLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), false);
  pBLEScan->setActiveScan(true); // Set active scanning
  pBLEScan->setInterval(97); 
  pBLEScan->setWindow(37);  
  pBLEScan->setMaxResults(0); // do not store results
}

void loop() {
  updateDisplay();
    
  // If an error occurs that stops the scan, it will be restarted here.
  if(pBLEScan->isScanning() == false) {
      pBLEScan->start(0, nullptr, false);
  }

  // primitive way of checking for stale readings  
  intExpCountA++;
  intExpCountB++;
  intExpCountC++;
  intExpCountD++;

  if (intExpCountA > 50) strTempA = "";
  if (intExpCountB > 50) strTempB = "";
  if (intExpCountC > 50) strTempC = "";
  if (intExpCountD > 50) strTempD = "";

  if (intExpCountA > 220 && intExpCountB > 220 && intExpCountC > 220 && intExpCountD > 220) {
    Serial.println("Watchdog Restarting...");
    esp_restart();
  }

  delay(3000);
}

Not the most elegant code, but it does exactly what was intended.

I later modified it to send an HTTP POST request to an endpoint each time a value changes, this is then stored in a database (to give us the history we want) and also was used to create a somewhat more visually appealing representation of the current temperature on a simple webpage.

The total cost of this setup was £32 for some sensors, and the ESP32 I already had lying around, even if I didn't, it would have been £50 total.

Whether or not this remains indefinitely at the theatre, I'm not sure. Hopefully at some point we'll have a new heating system potentially with an accurate thermostat, otherwise I may look at adapting this system to use it to control relays (e.g. a Shelly Pro) to provide the functionality of a thermostat.

I've also ordered some more sensors to recreate this at home... with one sensor per room.