Oil Boiler Instrumentation

This project shows how I instrumented my oil boiler and what I did with the readings.

First off, I wanted to know what the temperatures of the Out and Return flows were and secondly, by logging when it was actually fired up, how much oil I was using. I have a new boiler that is set to give 21.5kW at an oil flow rate of 2.28l/h. But I have no power near the boiler other than when it is being demanded!

I decided to use an ESP 8266, namely a Wemos D1 with integral 18650 battery and charging circuit.

Sensors

Temperature - 3 x DS18B20 One Wire sensors to measure Output, Return and Cabinet temperatures. The one wire fed into D4. (I should have added a fourth to measure the room temperature!)

Boiler Demand (ie from the CH/HW controller) - I found a point on the board that went from 1V0 to 4V7 when the demand was present, so I fed this through a 47k/100k resistor chain to get near 3V3 as an input to D6.

Boiler Thermostat Demand (ie when the boiler is actually burning as demanded by its own internal thermostat) - The logic within the boiler is all at mains voltage so I bought a mains sensor which opto isolates the digital output. This I fed into D7.

Battery voltage - I put the battery voltage through a 22k/100k resistor chain to drop it to 3V3 max for feeding into the A2D converter.

During the summer when Boiler demand isn’t that great I found the Battery would go flat so I put the ESP into sleep mode after an hour of no demand. The code includes some redundant features, namely to sense the difference between the boiler’s thermostat demanding and the oil flow solenoid kicking in after the safety checks. In the end I just added a delay of 8 seconds within the templates in Home Assistant. Also lots of Serial.prints to aid debugging but not in live use. Here’s the code:

/*  This will be the definitive Sketch for the Boiler sensor running on the
 *  WeMos8266 board with integrated battery. It also runs on the generic ESP8266
 *  development board in the breadboard.
 */
#include <SPI.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 2 // Signal from Temperature sensors D4
#define LED_BUILTIN 16
#define BURN 14 // Mains sensor circuit on solenoid cable. D5
#define THERM 13 // Mains sensor circuit on boiler thermostat output cable. D7
#define POWER 12 // pick up from board sensing power is on. D6
#define ADIN A0 // Analogue to Digital converter pin for reading the temperature. AD

IPAddress server(192, 168, x, x);
const char* mqtt_user = "user";
const char* mqtt_pwd = "pwd";
boolean charging; //HIGH if the unit is being charged (Boiler is being demanded)
boolean burning; // HIGH if the boiler is fired up
boolean lastBurning = false; // Previous state
boolean therming; // HIGH if boiler thermostat is demanding heating
boolean lastTherming; // Previous state
boolean noChange = true; // Flag for no state change
float tempOut; // Temperature of boiler water outflow
float tempIn; // Temperature of returning water
float tempCab; // Temperature inside the boiler cabinet
float batteryV; // Battery voltage
int upTime = 0; // Up Time in Seconds
int secsSincePower; // Seconds since Power was on
int modeStartTime; // Time at which it enters either of the while loops in loop()
byte mac[6];
char* topic;
String payload;
char output[180];
int numberOfDevices; // Number of temperature devices found - Should be 3
long rssi; // WiFi RSSI Strength

// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);

// We'll use this variable to store a found device address
DeviceAddress tempDeviceAddress; // Device address is a 16 character Hex number or 8 bytes

void callback(char* topic, byte* payload, unsigned int length) {
} // Not used

WiFiClient ethClient;
PubSubClient client(server, 1883, callback, ethClient);

void initWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin("SSID", "PASSWORD");
  while ((WiFi.status() != WL_CONNECTED)) {
     digitalWrite(LED_BUILTIN, LOW); // Turn on 
     delay(60);
     digitalWrite(LED_BUILTIN, HIGH); // Turn off 
     delay(340);
     Serial.print(".");
  }
  digitalWrite(LED_BUILTIN, HIGH); // Turn off 
  Serial.println("");
  Serial.print("WiFi connected, IP address: "); Serial.println(WiFi.localIP());
  WiFi.macAddress(mac);
  Serial.print("MAC: ");
  for(int i=0; i<5; i++){ // Print the first 5 with :
    if (mac[i] < 16) Serial.print("0");
    Serial.print(mac[i],HEX);
    Serial.print(":");
  }
  if (mac[5] < 16) Serial.print("0"); // Print the 6th
  Serial.println(mac[5],HEX);
}

void light_sleep(uint32_t sleep_time_in_ms){ // If sleep time is 0 then an indefinate interrupt wakeup is set
  wifi_station_disconnect();
  wifi_set_opmode_current(NULL_MODE);
  wifi_fpm_set_sleep_type(LIGHT_SLEEP_T); // set sleep type, the above posters wifi_set_sleep_type() didnt seem to work for me although it did let me compile and upload with no errors 
  wifi_fpm_open(); // Enables force sleep
  gpio_pin_wakeup_enable(GPIO_ID_PIN(12), GPIO_PIN_INTR_HILEVEL); // Pin D6
  // GPIO_ID_PIN(2) corresponds to GPIO2 on ESP8266-01 , GPIO_PIN_INTR_LOLEVEL for a logic low, can also do other interrupts, see gpio.h above
  if (sleep_time_in_ms == 0){
    Serial.println("Going to sleep to be woken by interupt on GPIO 2 or D4");
    delay(200); // Give time to print before sleeping
    wifi_fpm_do_sleep(0xFFFFFFF); // Sleep for longest possible time
    delay(100); // This is necessary to ensure it goes to sleep!
    Serial.println("Wake up by interupt");
  }
  else{
    Serial.print("Going to sleep now for "); Serial.print(sleep_time_in_ms/1000); Serial.println(" seconds.");
    delay(200);
    wifi_fpm_do_sleep(sleep_time_in_ms * 1000); // Sleep for set time (us)
    delay(sleep_time_in_ms + 1);
    Serial.println("Woken up after 10s");
  }
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Boiler", mqtt_user, mqtt_pwd)) {
      Serial.println("connected");
      client.setBufferSize(512); // Default is 256 which was too short for the config messages.
      // Subscribe
        }
      else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void publishData(){
  // Create a JSON string containing the 2 temoeratures, the POWER and BURN states and maybe the WiFi status
  topic = "tele/boiler"; // Delete Breadboard for live system
  payload = "{\"Outflow Temperature\":";
  payload += String(tempOut);
  payload += ",\"Inflow Temperature\":";
  payload += String(tempIn);
  payload += ",\"Cabinet Temperature\":";
  payload += String(tempCab);
  payload += ",\"Battery Voltage\":";
  payload += String(batteryV);
  payload += ",\"RSSI\":";
  payload += String(rssi);
  payload += ",\"Demand\":";
  payload += charging ? "\"On\",\"Therm\":" : "\"Off\",\"Therm\":"; 
  payload += therming ? "\"On\",\"Burn\":" : "\"Off\",\"Burn\":"; 
  payload += burning ? "\"On\"}" : "\"Off\"}"; 
  if (!client.connected()) reconnect(); // Reconnect to to the MQTT Broker
  for (int i = 0; i <= payload.length(); i++) output[i] = payload[i];
  client.publish(topic, output);
  Serial.println(payload);
}

void getTemps(){
  // Read the temperature voltages **********************************************************************************  
  sensors.requestTemperatures(); // Send the command to get temperatures
  
  // Get temperature for each device - May need to adjust these indexes
  if(sensors.getAddress(tempDeviceAddress, 0)){ // Cabinet
    tempOut = sensors.getTempC(tempDeviceAddress);
  }
  if(sensors.getAddress(tempDeviceAddress, 1)){ // Output
    tempCab = sensors.getTempC(tempDeviceAddress);
  }
  if(sensors.getAddress(tempDeviceAddress, 2)){ // Input (or Return)
    tempIn = sensors.getTempCByIndex(2); //(tempDeviceAddress);
  }
  Serial.print("Boiler Output: "); Serial.print(tempOut); Serial.print("°C Input: "); Serial.print(tempIn);
  Serial.print("°C Cabinet: "); Serial.print(tempCab); Serial.println("°C");
}

void flash(int noOfTimes){
 for(int i=0; i<noOfTimes; i++){
   digitalWrite(LED_BUILTIN, LOW); //On
   delay(10);
   digitalWrite(LED_BUILTIN, HIGH); //Off
   if(i+1 < noOfTimes)delay(290); // No delay after last flash  
 }
}

void getBatteryV(){
  // Reads the Battery Voltage using the A2D converter. 
  int adcV = analogRead(ADIN);
  batteryV = adcV * 4.1 / 820; // with 47k/100k voltage divider
  Serial.print("Battery Voltage: "); Serial.print(batteryV); Serial.print(" (Reading: "); Serial.print(adcV);  Serial.println(")");
  rssi = WiFi.RSSI();
  Serial.print("WiFi RSSI: "); Serial.println(rssi);
}

// function to print a DS18B20 device address
void printAddress(DeviceAddress deviceAddress) {
  for (uint8_t i = 0; i < 8; i++){
    if (deviceAddress[i] < 16) Serial.print("0");
      Serial.print(deviceAddress[i], HEX);
  }
}

boolean isBurning(){ // Is the safety mechanism demanding the oil solenoid
  return(!digitalRead(BURN)); // Only one place to forget the !
}

boolean isTherming(){ // Is the boiler's thermostat demanding the burner
  return(!digitalRead(THERM)); // Only one place to forget the !
}

void setup()
{
  Serial.begin(115200);
  Serial.println();
  Serial.println();
  Serial.println("BoilerDS18B20LightSleepMQTT");
  Serial.println();
  gpio_init(); // Initilise GPIO pins
  WiFi.mode( WIFI_OFF ); // Why?
  WiFi.forceSleepBegin(); // Why?
  sensors.begin();   // Start up the Dallas and OneWire stuff
  // locate devices on the bus
  numberOfDevices = sensors.getDeviceCount();   // Grab a count of devices on the wire - should be 3
  Serial.print("Locating devices...");
  Serial.print("Found ");
  Serial.print(numberOfDevices, DEC);
  Serial.println(" devices.");
  // Loop through each device, print out address
  for(int i=0;i<numberOfDevices; i++){
    // Search the wire for address
    if(sensors.getAddress(tempDeviceAddress, i)){
      Serial.print("Found device ");
      Serial.print(i, DEC);
      Serial.print(" with address: ");
      printAddress(tempDeviceAddress);
      Serial.println();
    } else {
      Serial.print("Found ghost device at ");
      Serial.print(i, DEC);
      Serial.print(" but could not detect address. Check power and cabling");
    }
  }

  pinMode(LED_BUILTIN, OUTPUT); // LED to give visual indication of what is going on
  digitalWrite(LED_BUILTIN, HIGH); // Turn LED off
  pinMode(POWER, INPUT); // HIGH if Power is on
  pinMode(BURN, INPUT_PULLUP); // LOW if burning
  pinMode(THERM, INPUT_PULLUP); // LOW if Boiler Thermostat is demanding heating
  delay(1000); // Let things settle ?
  upTime = millis()/1000;
  Serial.print("Up Time (S):- "); 
  Serial.println(upTime);
  charging = digitalRead(POWER); // Read ahead just once
  noChange = true;
  burning = false; // Sensor not implemented so set false
  modeStartTime = millis()/1000; // Time at which loop() starts or repeats
}
 
void loop() {
  // This loop will run over and over again. Initially on module startup and then after being woken from light sleep by
  // the power coming back on, a change in state of charging or therming or every 30 seconds unless in light sleep
  
  // ******************** Charging Loop *********************
  while (charging){ // Stay in this loop until charging stops (ie boiler is no longer being demanded)
    // Don't bother going to sleep as running the unit does not reduce the charging current
    // The boiler will only be Therming in this loop
    // The boiler will only be burning if it is Therming
    // Therming is when the boiler thermostat is demanding the burner
    Serial.println("In Charging While loop");
    flash(3); // Give visual indication of bein in charging loop and about to publish
    if ((WiFi.status() != WL_CONNECTED)) initWifi();
    therming = isTherming();
    lastTherming = therming;
    getTemps();
    getBatteryV(); // Gets rssi too
    publishData();     // Publish Power, Therm, Burn and Temps
    noChange = true;
    // Wait 30 seconds reading the burn etc. Breakout if burn changes
    while (((millis()/1000 - modeStartTime) < 30) && noChange){
      Serial.println(millis()/1000 - modeStartTime);
      delay(1000); // Loop once every second
      charging = digitalRead(POWER);
      if (!charging){ // charging/demand has stopped!
        noChange = false;
        getTemps(); // This bit is to force a write of the data in Home Assistant
        getBatteryV(); // to give more of a square wave on the graphs by
        charging = true; // Not true so will need to reset to false after publishing
        // Note therming should be as was
        Serial.println("Charging change publish.");
        publishData(); // writing the old data 1 second before the new.
        charging = false;
        delay(1000);
      }
      else { // This bit only runs whilst charging/demand is on
        therming = isTherming();
        if (therming != lastTherming){
          noChange = false;
          getTemps(); // This bit is to force a write of the data in Home Assistant
          getBatteryV(); // to give more of a square wave on the graphs by
          therming = lastTherming; // Not true so will need to reset to false after publishing
          // Note charging should not have changed
          Serial.println("Therming change publish.");
          publishData(); // writing the old data 1 second before the new.
          therming = isTherming();
          delay(1000);
        }
      }
    } // ***************** End of 30 seconds or change loop ******
    modeStartTime = millis()/1000; // Time the while(charging) loop repeats
  } // ******************* End of Charging loop *************
  
  // ********************* Not Charging Loop ****************
  int chargeOffTime = millis()/1000; // Time we enter the Not Charging loop. To time the hour before sleep
  while(!charging && (millis()/1000 - chargeOffTime) < 3600){ // Stay in this loop for up to an hour while NOT charging
    Serial.println("In the Not Charging while loop");
    flash(1);
    // Publish every 30 seconds for 1 hour then go to light sleep to be woken by the POWER coming on
    // No need to test for THERM or BURN only POWER
    if ((WiFi.status() != WL_CONNECTED)) initWifi();
    getTemps();
    getBatteryV(); // Gets rssi too
    burning = 0;
    therming = 0;
    publishData();
    noChange = true;
    while((millis()/1000) - modeStartTime < 30 && noChange){
      Serial.println(millis()/1000 - modeStartTime);
      delay(1000); // Loop once per second
      charging = digitalRead(POWER);
      if (charging){ // ie Demand has come back on
        noChange = false;
        getTemps(); // This bit is to force a write of the data in Home Assistant
        getBatteryV(); // to give more of a square wave on the graphs by
        charging = false; // Not true so will need to reset to true after publishing
        Serial.println("Power back on publish"); 
        publishData(); // writing the old data 1 second before the new.
        charging = true;
        delay(1000);
      }
    } // ******************* End of 30 seconds or change loop ***************
    
    modeStartTime = millis()/1000; // Time the while(charging) loop repeats
  }  //******************* End of Not Charging or 60 minutes loop  **********
  
  // We get here if an hour has passed or the Power has come back on
  if((millis()/1000 - chargeOffTime) >= 3600 && !charging) light_sleep(0); // Hour up. Go into light_sleep mode until charging starts again
  // Woken up
  // Need to publish a point here too with charging off to get a square edge
  delay(100);
  if ((WiFi.status() != WL_CONNECTED)) initWifi();
  getTemps();
  getBatteryV();
  // charging and therming should still be false
  publishData();
  delay(1000);
  charging = digitalRead(POWER); // Should be charging having just woken up or charging has started within the hour
  
}  // ********  ************ End of loop() loop *****************

Here’s the YAML that pick up the readings within Home Assistant

mqtt:
  sensor:
    - name: "Boiler outflow water temperature"
      unique_id: mqtt_0002
      state_topic: "tele/boiler"
      unit_of_measurement: '°C'
      value_template: "{{value_json['Outflow Temperature']}}"
    - name: "Boiler return water temperature"
      unique_id: mqtt_0003
      state_topic: "tele/boiler"
      unit_of_measurement: '°C'
      value_template: "{{value_json['Inflow Temperature']}}"
    - name: "Boiler cabinet temperature"
      unique_id: mqtt_0004
      state_topic: "tele/boiler"
      unit_of_measurement: '°C'
      value_template: "{{value_json['Cabinet Temperature']}}"
    - name: "Boiler Battery Voltage"
      unique_id: mqtt_0005
      state_topic: "tele/boiler"
      unit_of_measurement: 'V'
      value_template: "{{value_json['Battery Voltage']}}"
    - name: "Boiler Sensor RSSI"
      unique_id: mqtt_0006
      state_topic: "tele/boiler"
      unit_of_measurement: "dB"
      value_template: "{{value_json['RSSI']}}"
      force_update: true
  binary_sensor:
    - name: "Boiler demand"
      state_topic: "tele/boiler"
      value_template: "{{value_json['Demand']}}"
      payload_on: "On"
      payload_off: "Off"
      force_update: true
    - name: "Boiler thermostat demand"
      state_topic: "tele/boiler"
      value_template: "{{value_json['Therm']}}"
      payload_on: "On"
      payload_off: "Off"
      force_update: true

Here’s 1 days worth of History on a cold day in December…

Other than monitoring and graphing the temperatures, I don’t do much with that info. I was thinking I could have a routine that checks that the boiler_outflow_water_temperature is going up when the boiler_thermostat_demand is on and sends me an email if it thinks there’s a problem. I haven’t got it right yet as I get emails daily!

The main sensor I use for further processing is the boiler_thermostat_demand. I use this to calculate such things like the oil remaining in the tank, oil usage by day, litres used and costs. It also gets fed into the Energy Integration as kWh of Gas (Oil isn’t an option yet?!). So here’s how I do all that.

The first thing I do is adjust the “on” time to get a truer log of when the boiler is actually burning. From the time the boiler_thermostat_demand turns on, there is about an 8 second delay before it actually fires up. I think this is why it’s doing its safety checks. So I have an automation that turns a boolean helper on after an 8 second delay

alias: Turn boiler State On
trigger:
  - platform: state
    entity_id: binary_sensor.boiler_thermostat_demand
    from: "off"
    to: "on"
    for:
      hours: 0
      minutes: 0
      seconds: 8
condition: []
action:
  - service: input_boolean.turn_on
    data: {}
    target:
      entity_id: input_boolean.boiler_state
mode: single

and another that turns is off immediately.

alias: Turn Boiler State Off
description: ""
trigger:
  - platform: state
    entity_id: binary_sensor.boiler_thermostat_demand
    from: "on"
    to: "off"
condition: []
action:
  - service: input_boolean.turn_off
    data: {}
    target:
      entity_id: input_boolean.boiler_state
mode: single

So helper input_boolean.boiler_state tells me if it’s burning or not.

Next step was to determine today’s (and yesterday’s (don’t ask me why)) burn time so I used the history_stats platform

sensor:
  - platform: history_stats
    name: Correct Burn Time Today
    entity_id: input_boolean.boiler_state
    state: "on"
    type: time
    start: "{{ now().replace(hour=0, minute=0, second=0) }}"
    end: "{{ now() }}"
  - platform: history_stats
    name: Correct Burn Time Yesterday
    entity_id: input_boolean.boiler_state
    state: "on"
    type: time
    end: "{{ now().replace(hour=0, minute=0, second=0) }}"
    duration:
      hours: 24

I wanted to know the burn time since the new boiler was fitted so created the template entity sensor.boiler_hours_since_new. (I cannot remember why I needed the if statement)

sensor:
  - platform: template
    sensors:
      boiler_hours_since_new:
        friendly_name: "Boiler Hours Burning Since New"
        unit_of_measurement: "h"
        value_template: >-
          {% if states('sensor.correct_burn_time_today') | float == 0 %}
            {{ states('sensor.boiler_hours_since_new') }}
          {% else %}  
            {{states('input_number.total_boiler_burn_time_cop_yesterday') | float + states('sensor.correct_burn_time_today') | float }}
          {% endif %} 

The helper input_number.total_boiler_burn_time_cop_yesterday gets updated each day just after midnight (when the boiler should be off) by an automation

alias: Update Total Boiler Burn Time COP Yesterday
description: Runs just after midnight
trigger:
  - platform: time
    at: "00:01"
condition: []
action:
  - service: input_number.set_value
    target:
      entity_id: input_number.total_boiler_burn_time_cop_yesterday
    data_template:
      value: "{{states(\"sensor.boiler_hours_since_new\")}}"
mode: single


This is a Grafana panel showing the last 8 months’ worth of data. It took me a while to sort out the logic and I didn’t set it up straight away!

Now, moving on to oil usage, it can be expressed in Litres, kWh and cost. I have helpers to use in these conversions, namely input_number.oil_burn_rate set to 2.28l/m as per the boiler manual; input_number.boiler_rating set to 21.5kW/h and input_number.last_oil_price which changes from one delivery to the next and is currently 0.83ÂŁ/L.

Template sensors get me todays values…

sensor:
  - platform: template
    sensors:
      oil_litres_today:
        friendly_name: "Oil Used Today"
        unit_of_measurement: "L"
        value_template: "{{states('sensor.correct_burn_time_today') | float * states('input_number.oil_burn_rate') | float}}"
  - platform: template
    sensors:
      oil_cost_today:
        friendly_name: "Oil Cost Today"
        unit_of_measurement: "ÂŁ"
        value_template: "{{states('sensor.oil_litres_today') | float * states('input_number.last_oil_price') | float}}"
  - platform: template
    sensors:
      oil_kwh_today:
        friendly_name: "Oil kWh Today"
        unit_of_measurement: "kWh"
        value_template: "{{states('sensor.correct_burn_time_today') | float * states('input_number.boiler_rating') | float}}"

Next I have another helper with the current Oil Tank level - input_number.test_oil_remaining which I update through the developer tools when I have a delivery and gets updated after each burn through an Automation…

alias: Test Oil Remaining
description: Updates oil remaining as the History sensor.oil_litres_today increases
trigger:
  - platform: state
    entity_id: sensor.oil_litres_today
condition:
  - condition: numeric_state
    entity_id: sensor.oil_litres_today
    above: "0"
action:
  - delay:
      hours: 0
      minutes: 0
      seconds: 1
      milliseconds: 0
  - service: input_number.set_value
    data_template:
      value: >-
        {{ states("input_number.oil_remaining_yesterday") | float -
        states("sensor.oil_litres_today") | float }}
    target:
      entity_id: input_number.test_oil_tank_remaining
mode: single

Note that this relies on another helper input_number.oil_remaining_yesterday that gets updated 1 minute after midnight in the same automation that updates the burn time cop yesterday above. It is another Action

  - service: input_number.set_value
    target:
      entity_id: input_number.oil_remaining_yesterday
    data_template:
      value: "{{ (states(\"input_number.test_oil_tank_remaining\")) }}"

So I can now have Guages and Graphs on my dashboards. I could also have notifications of when I need to reorder, but I’ve not done that yet. Having burnt through 1000L of oil since running this system, the manufacturer’s claim of 2.28L/h seems pretty spot on.

The last thing I’m going to add here is how I get the data into the Energy Integration so that it can do the serious stuff of logging usage and presenting it by day, week, month etc. First off, there is no mention of Oil in the Energy Integration, but it does have Gas. I don’t use gas so thought this could be used instead. The only meaningful unit to provide the Integration is kWh, so that’s what i tried. However, despite having the template entity oil_kwh_today, it did not come up as an acceptable entity in the Energy Integration. I do not know why but if anyone else does then please tell me.
Knowing that an MQTT entity was acceptable for electricity (I’d already set that up) I gave that a go and it worked! To do it I used Node Red to send an MQTT message on a change in sensor.oil_kwh_today although now, I’m guessing, I could have done it through an automation and an MQTT service call. The Node Red flow is here…

[{"id":"b9ab70d0a923d773","type":"server-state-changed","z":"4d3d0c02.dba494","name":"","server":"1ebc9f01.db1151","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"sensor.oil_kwh_today","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"output_only_on_state_change":true,"for":0,"forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"x":230,"y":120,"wires":[["e1e0e89e9e6f2959"]]},{"id":"e1e0e89e9e6f2959","type":"mqtt out","z":"4d3d0c02.dba494","name":"","topic":"tele/boiler/energy","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"91b9efe8.4a598","x":530,"y":120,"wires":[]},{"id":"1ebc9f01.db1151","type":"server","name":"Home Assistant","addon":true},{"id":"91b9efe8.4a598","type":"mqtt-broker","name":"MQTT","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"compatmode":false,"protocolVersion":4,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

And the YAML code to receive it being also a valid input to the Energy Integration is here…

mqtt:
  sensor:
    - name: "Oil Energy"
      unique_id: sunny_mqtt_0001
      state_topic: tele/boiler/energy
      unit_of_measurement: "kWh"
      device_class: energy
      state_class: total_increasing

And here is December’s usage. Where it say Gas read Oil - I hope one day the good people writing Home Assistant recognize that oil is out there as a fuel too.

That’s it. I do do more with the data putting it into Grafana but in truth that just duplicates what is going on in the Energy Integration.

6 Likes

Love it! I have an ESP all set up with some Dallas temperature sensors, and I plan to eventually move my existing sensors over to that at some point. Right now I just track run-time of the boiler’s burner and each zone valve using relays to open/close GPIO pins on the RPi running HA.

Since my domestic hot water is direct off the boiler, I’m trying to think of ways to monitor the input and output temperatures for that, too. I can put sensors on the pipes, but of course those readings would only have meaning after the water has been flowing. At all other times they’ll drift back to the ambient temperature surrounding the pipe.

Very interesting - I’ve done similar using a Shelly Uni to get the boiler flow, return and HW temps, and planning to use the switch input on a Shelly 1 relay parallel to the burner switch feed (as it’s 240V). Also have an OilPal tank level sensor, but the resolution of the reading is poor and fluctuates a lot, hence wanting to measure from the burner. Will go through your config in more detail, but good to see others with same issue. +1 for treating oil as a common fuel.

This has been super inspiring, and I have implemented most of it on my setup. Great workđź’Ş

Regarding the Energy dashboard, you can do it without looping it around MQTT. I am tracking my consumptions in liters ( 0.001 m3), and added the state class, device class and unit.

My config looks like this:

sensor:
  - platform: history_stats
    name: "Brænder tid"
    entity_id: binary_sensor.oliefyr_input
    state: "on"
    type: time
    start: "{{ now().replace(hour=0, minute=0, second=0) }}"
    end: "{{ now() }}"
  - platform: history_stats
    name: Brænder tid i går
    entity_id: binary_sensor.oliefyr_input
    state: "on"
    type: time
    end: "{{ now().replace(hour=0, minute=0, second=0) }}"
    duration:
      hours: 24
 
template: 
  - sensor:
      - name: oil_litres_today
        unique_id: "96f7ddf9-8027-4059-ab42-2252d950618e"
        unit_of_measurement: "L"
        state: "{{states('sensor.braender_tid') | float(0) * 2.25 | float}}"
        device_class: 'energy'
        state_class: 'total_increasing'
      - name: oil_m3_today
        unique_id: "76264150-f252-4e12-b42a-2c57d9dfd362"
        unit_of_measurement: "mÂł"
        state: "{{states('sensor.braender_tid') | float(0) * 0.00225 | float}}"
        device_class: 'gas'
        state_class: 'total_increasing'

It seems you are using the old template integration, which does not support these configurations, but they can also be added as a customization to your existing sensor by using the customize integration.

Thanks for all your great work :slight_smile:

Very nice. I have a waste oil burner with a Carlin 60200 controller. How did you wire yours to the controller? I’m looking to monitor oil usage and if it goes into Lock Out because of miss fire.