Oven Reminders with Node Red & ESPHome

Changes

2022-06-06 V1.0 First Upload
2022-09-09 V1.1 Issues with Esphome device not displaying pzem data, added stop_bits:1 to config, seems to be better?
2022-12-24 V1.2 Issues with voice announcements with longer times (forgot about hours vs minutes)
2024-05-16 V1..3 Improved ESPHome YAML and added lots of comments. Also, changed the board type to esp8285 which solved a couple of issues

Summary

A Node red flow (and Javascript) to

  • Monitor power from an electric Oven (done directly at the DB, no need to touch the oven. Note that the oven needs to be on it’s own circuit in the DB, or at least have the feed to the DB directly)
  • check when it is operating (averaging regular power measurements)
  • Notify (by voice) when it is up to temperature (first time element turns off)
  • Remind that the oven is on (voice) every X minutes (15 by default)
  • calculate cost of use based on 2 power tariffs
  • notify when complete (via various methods, and with cost/power data)
  • formats notifications nicely, eg $, c, wH, kWH etc
  • store power/cost/runtime in files (csv)
  • Allow the oven to be turned off completely (using a NC contactor)

Getting instantaneous power use from an appliance

There are a number of ways of measuring power usage from an appliance/device but in this case I’m using a PEM-004T at the distribution board. Below are other methods I’ve used for monitoring appliances.

  • Sonoff Pow modules with Tasmota (they report to MQTT) (Used for my washer & dryer)
  • Sonoff Basic, with PEM-004T, using ESP Home and Home Assistant (used here for my Oven)
  • Pulse counting a check meter with a Wemos D1 Mini (and Tasmota again) (Used for EV charging)
  • A plug in Tuya style power monitoring mains switch, converted (with software if possible, or else hardware) (Used for my Dishwashers)

Goals

Use a Node red flow and a simple power monitoring setup to give me Oven notifications. Also… be able to turn the oven off if it has been on for a long time (eg it has been forgotten about, OR there is smoke detection). This could be especially useful if it needs to be turned off remotely.

This expands on some previous power monitoring and appliance notification, but the code is tidied up significantly, and specifics added for the oven announcements.

This flow is WAY more complicated than it probably needs to be for you, as I have a number of announcement methods, and a full debug/test setup. All you really need is a node for the settings, a node that pulls in the oven power, the MAIN OPERATION node script and some sort of announcement/notification method.

Node Red Flow. All the announcement & notification options are on the right and most activity is in the Main Loop. There are also injection nodes to test the flow with power values.

Node Red flow to download

Note this is Version 1.0. You may need to tweak this download when other versions are released.

Hardware

  • Sonoff Basic running ESPHome
  • PZEM-004T, connected to serial UART of a sonoff basic (and current transformer around the oven supply phase). Setup with a resistor mod as documented in
    https://zorruno.com/2020/whole-house-3-phase-power-with-pzem-004t/
  • 25A contactor, connected to the Oven supply (phase) and driven by the sonoff relay. Contactor is set as Normally Closed (NC) and driven to break power to oven. Not fail safe, but will at least ensure the oven is usable if the sonoff or this setup fails (it is important for me to make home automation systems ‘fail normal’, so that if the house is sold the new owner has a ‘normal house’ to deal with if they like).

ESPHome Configuration

#############################################
#############################################
# OVEN POWER MONITOR
# Monitoring power of an oven (at the main 
# switchboard) using a sonoff basic (esp8266) 
# and a PZEM-004T. The relay also allows disabling
# of the oven with a contactor (eg on smoke detection)
# https://zorruno.com/2022/nodered-oven-notifications/
##############################################
#############################################

#############################################
# Variable Substitutions
#############################################
substitutions:
  devicename: "esp-mainovenmonitor"
  friendly_name: "esp-mainovenmonitor"
  description_comment: "Oven power monitoring and disable, with a sonoff basic"
  api_key: !secret esp-mainovenmonitor_api_key #unfortunately you can't use substitutions in secrets names
  ota_pass: !secret esp-mainovenmonitor_ota_pass #unfortunately you can't use substitutions in secrets names
  mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like
  #update_time: 30s #update time for for temp sensors etc
  
#############################################
# ESPHome
#############################################
esphome:
  name: ${devicename}
  comment: ${description_comment} #appears on the esphome page in HA

#############################################
# ESP Platform and Framework
# https://esphome.io/components/esp8266.html
# https://esphome.io/components/esp32.html
#############################################
esp8266:
  #board: sonoff_basic
  #board: esp01_1m
  board: esp8285
  framework:
    version: latest #recommended, latest or dev
#esp32:
#  board: nodemcu-32s
#  framework:
#    type: arduino
#    #type: esp-idf  #Suggested Use ESP-IDF Framework, or Plug Out the UART Cable Might Cause ESP32 Hang. 
#    version: recommended #recommended, latest or dev

#############################################    
# ESPHome Logging Enable
# https://esphome.io/components/logger.html
############################################# 
logger:
  level: INFO  #INFO Level suggested, or DEBUG for testing
  baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM)
  #esp8266_store_log_strings_in_flash: false
  #tx_buffer_size: 64

#############################################    
# Enable the Home Assistant API
# https://esphome.io/components/api.html
#############################################  
api:
  encryption:
    key: ${api_key}

#############################################    
# Enable Over the Air Update Capability
# https://esphome.io/components/ota.html?highlight=ota
#############################################  
ota:
  safe_mode: true  #Safe mode will detect boot loops
  password: ${ota_pass}

#############################################    
# Wifi Settings 
# https://esphome.io/components/wifi.html
#############################################  
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  #power_save_mode: LIGHT #https://esphome.io/components/wifi.html#wifi-power-save-mode
  #manual_ip: #optional static IP address
    #static_ip: 192.168.x.x
    #gateway: 192.168.X.x
    #subnet: 255.255.255.0
  ap:   #Details for fallback hotspot (captive portal) in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode
    ssid: $devicename fallback AP
    password: !secret fallback_ap_password
    ap_timeout: 5min #default is 1min
captive_portal: # Fallback captive portal https://esphome.io/components/captive_portal.html

#############################################    
# Web Portal for display and monitoring
#############################################   
web_server:
  port: 80
  version: 2
  include_internal: true
  ota: false
#  auth:
#    username: !secret web_server_username
#    password: !secret web_server_password

#############################################
# MQTT Monitoring
# https://esphome.io/components/mqtt.html?highlight=mqtt
# MUST also have api enabled if you enable MQTT
#############################################
mqtt:
  broker: !secret mqtt_server
  topic_prefix: ${mqtt_topic}/${devicename}
  #username: !secret mqtt_username
  #password: !secret mqtt_password


#############################################  
#############################################
# MAIN SENSORS
#############################################
#############################################
# PEZEM4 Power Monitoring
# https://esphome.io/components/sensor/pzemac.html
#############################################

uart:
  rx_pin: GPIO3
  tx_pin: GPIO1
  #tx_pin: RX
  #rx_pin: TX
  baud_rate: 9600
  stop_bits: 1
  
modbus:

sensor:
  - platform: pzemac
    current:
      name: "Main Oven Current"
    voltage:
      name: "Main Oven Voltage"
    energy:
      name: "Main Oven Energy"
    power:
      name: "Main Oven Power"
    frequency:
      name: "Main Oven Mains Frequency"
    power_factor:
      name: "Main Oven Power Factor"
    update_interval: 10s
    
########################################
########################################
    
# Relay Output (on sonoff basic)    
switch:
  - platform: gpio
    name: "Main Oven Disable"
    pin: GPIO12

button:
  - platform: restart
    name: "Main Oven ESPHome Restart"


# LED Flashes on errors or warnings
# https://esphome.io/components/status_led.html
status_led:
  pin:
    number: GPIO13
    inverted: true

Photos

Electronics mounted in box beside DB
A few options I considered initially for oven switching. A solid state relay (SSR), a 16A Sonoff POW and a large 230V relay. None I thought were safe enough, and the SSR is not ‘fail closed. The Sonoff Pow is really only suitable for 10A continual power in my opinion.

Push Notifications

An example notification (using Pushover) on Android and iphone. It could equally be used over email, twitter, text messaging or another push notification system
Pushover: https://pushover.net/ (non free, but only a one off cost for each device)

Oven Notification
Your oven cooking is complete.
It started at 17:37, finished at 18:53, and used 1.4kWh, taking 01h 15min at a cost of 29c

Data to CSV File

An example of how it pushes to a CSV file (which is added to each time)

{"date/time":"2021-04-08T03:01:54.027Z","Appliance":"Oven","Start Time":"15:01","Finish Time":"15:01","Cycle Time Formatted":"00min","Cycle Time (s)":13.295000076293945,"Cycle Power Use Formatted":"179.3Wh","Cycle Power Use (Wh)":179.33333333333331,"Cycle Cost Formatted":"3c","Cycle Cost ($)":0.033282}

What’s Next

Some additions to this I am planning (or have already implemented since this article)

  • The announcement are nice, and a great reminder to turn the oven on when needed… and to stop burning food! The can get a bit annoying however to I plan to add a ‘silence’ option to cut announcements for the current cycle.

Configuration Example

This is the javascript for the ‘On Start’ tab in the ‘Settings’ Function node.

// -----------------------------------------------------------------------
// OVEN APPLIANCE MONITOR AND REMINDER
// -----------------------------------------------------------------------
// Notify and store power use and cost for an OVEN appliance
// Full loop to calculate power average of appliance and
// work out when it is operating.  When operational,
// fill arrays with power used, and cost of power calculated.
//
// Announce when oven likely is up to temperature and also
// Every X minutes as a reminder that it is still on.
// -----------------------------------------------------------------------
// 2022-05-28 V1 - zorruno
// - Taken from previous appliance script and modified for Oven use.
// -----------------------------------------------------------------------
// Licenced as free under the GPL version 2 or any later version
// thanks to mat@notenoughtech.com for the initial code
// -----------------------------------------------------------------------

// Debugging nodes on (true/false)
flow.set("debugFlow", true) ;

// Appliance Names (for notifications)
flow.set("applianceName", "Oven") ;
flow.set("applianceAction", "oven cooking") ;

//flow.set("JSONPowerTopic", msg.payload.ENERGY.Power);

// Announcement text (for voice notifications)
// It is an array, and the announcement will be randomised.
flow.set("voiceAnnouncements", [
    "Hey, the oven is up to temperature",
    "Notice, the oven has reached temperature",
    "The oven is up to temperature",
    "The oven is now pre heated",
    "Hey, the oven has reached preheat temperature",
    ]) ;

// -----------------------------------------------------------------------
// Electrical tariff info  
// -----------------------------------------------------------------------
// Updated with Auckland Contact Energy Costs, Feb 2022
// cost is in cents per kWh and start and end are the times
// to change tariffs.
// Yes, free power between 9pm and 12am
var tariff = {"costDay": 0.2023,
              "costNight": 0.0,
              "start": 0,
              "end": 21} ;

// -----------------------------------------------------------------------
// applianceAction OPERATION Settings
// -----------------------------------------------------------------------
// offPower : Below this value, the applianceAction is "Off"
// standbyPower : between this and offPower, the applianceAction is "Standby"
// operatingPowerMinimum : between this and standbyPower, it is "Waiting"
//                       : above this, the appliance is "Operating"
// -----------------------------------------------------------------------

// applianceAction shows "Off" if average power is below offPower
flow.set("offPower", 0.2) ;
// applianceAction shows "Standby" if average power is below standbyPower
// It will show "Waiting" if average power is between standbyPower
// and operatingPowerMinimum
flow.set("standbyPower", 5) ;
// Minimum power when fully "Operating".
// This is averaged over 'resolution' values, so no problem
// if it drops below this value at times during operation.
flow.set("operatingPowerMinium", 35) ;
// How many times to do a power reading for rolling average.
flow.set("resolution", 6) ;
// How often does the appliance report back (seconds)
flow.set("metricFrequency", 10) ;

// -----------------------------------------------------------------------
// For Oven Specifically
// -----------------------------------------------------------------------
flow.set("applianceUpToTemperature", "No");  // Is the Oven up to temperature?
flow.set("reminderFrequency", 15);           // How often, in Mins to voice remind that it is on

// -----------------------------------------------------------------------
// No need to change these.  
// Set up the Arrays, with their default values.
// -----------------------------------------------------------------------
flow.set("tariff", tariff);

flow.set("recentPowerArray", [0]);
flow.set("cycleCostArray", [0]);
flow.set("cyclePowerArray", [0]);

var cycle = {   "cycleTimeStart": null,
                "cycleTimeStop": null,
                "totalCycleCostFormatted": 0,
                "totalCycleCostDollars": 0,
                "totalCyclePowerFormatted": 0,
                "totalCyclePowerWattHours": 0,
            }

flow.set("currentApplianceCycle",cycle);

Main Loop

// ---------------------------------------------------
// Modified code from my other appliance monitoring.
// Specifically to measure and notify re an electric oven
// MAIN LOOP
// Calculate power average of appliance and 
// notify regularly when it is operating. Also notify when 
// it gets up to temperature (first time element switches off).  
// When operational it fill arrays with power used, 
// and cost of power is calculated.
// ---------------------------------------------------
// 2022-02-27 V1.0 - zorruno - initial
// 2022-12-24 V1.2 - zorruno - fixed time notifications (yes, should simplify using 'switch')
// ---------------------------------------------------

// The input message must be the current power in Watts
var power = parseInt(msg.payload) ;

// Get context variables into local variables
var operation = context.get("operation") || "Off" ; // current Operation
var applianceUpToTemperature = context.get("applianceUpToTemperature") || "No" ;

var reminderDue = context.get("reminderDue") ; // can't use the || trick for booleans
if (reminderDue === undefined) { reminderDue = true }

// Get flow user settings variables into local variables
var res   = flow.get("resolution") ;
var tariff = flow.get("tariff") ;
var metricsf = flow.get("metricFrequency") ;
var standby = flow.get("standbyPower") ;
var applianceName = flow.get("applianceName") ;
var opPowerMin = flow.get("operatingPowerMinium") ;
var currentCycle = flow.get("currentApplianceCycle") ;
var reminderFrequency = flow.get("reminderFrequency") ;  // How often, in Mins to voice remind that it is on

// Get flow arrays into local variables
var recentPowerArray = flow.get("recentPowerArray") || [0];
var cycleCostArray  = flow.get("cycleCostArray") || [0];
var cyclePowerArray  =  flow.get("cyclePowerArray") || [0];

// Get date, seconds and hours
var date = new Date();
var dateS = date.getTime()/1000;
var hour = date.getHours();

// ---------------------------------------------------
// AVERAGE POWER
// Fill power array, and calculate average 
// power.  Do this on every loop.
// ---------------------------------------------------
//
// Function add for reduce array
/**
* @param {any} accumulator
* @param {any} a
*/
function add(accumulator, a) 
    {
    return accumulator + a;
    }

// Push power into TotalPower array for average
recentPowerArray.unshift(power);

// Remove X element to get total resolution for average calc
if(recentPowerArray[res] === undefined) 
    {
    flow.set("recentPowerArray", recentPowerArray);
    }
else 
    {
    recentPowerArray.splice(res, 1);
    flow.set("recentPowerArray", recentPowerArray);
    }

// Calculate average power from array
var sum = recentPowerArray;
var average = (sum.reduce(add)/recentPowerArray.length);   // Average the array


// ---------------------------------------------------
// OFF
// Appliance is "Off"
// 0.2 is just an arbitrary low value, in case not exactly 0
// ---------------------------------------------------
if (average < 0.2)
    {
    context.set("operation", "Off") ;
    }
// ---------------------------------------------------


// ---------------------------------------------------
// STANDBY
// Appliance is "Standby" (i.e. powered up but no element
// ---------------------------------------------------
if (average >= 0.2 && average <= standby)
    {
    context.set("operation", "Standby") ;
    }
// ---------------------------------------------------

// ---------------------------------------------------
// OPERATING
// Appliance has just started its Operating cycle 
// from Standby or Off
// ---------------------------------------------------
//if((average > standby && operation === "Standby") 
//  || (average > standby && operation === "Off")  ) {
 
if (average > standby && ( operation === "Standby" || operation === "Off"))
    {
    context.set("operation", "Operating"); 
  
    // Put the start time into the currentCycle array
    currentCycle.cycleTimeStart = dateS;  
    flow.set("currentApplianceCycle",currentCycle);
 
    // Clear the power calc array to start calculating for new cycle
    cyclePowerArray = [0]; // Clear array to start cycle
    flow.set("cyclePowerArray",cyclePowerArray);
  
    // Clear the cost array to start calculating for new cycle
    cycleCostArray = [0]; 
    flow.set("cycleCostArray",cycleCostArray);
    }
// ---------------------------------------------------

// ---------------------------------------------------
// OPERATING
// Appliance is now Operating 
// ---------------------------------------------------
if (average >= opPowerMin && operation !== "Operating")
    {
    context.set("operation", "Operating") ;
    }
// ---------------------------------------------------

// ---------------------------------------------------
// WAITING
// Appliance is "Waiting" (i.e. is on or just up to temp)
// If has just switched to Waiting and was prevously "Operating"
// the it is "UpToTemperature"
// ---------------------------------------------------
if (average >= standby && average <= opPowerMin)
    {
    if (operation === "Operating" && applianceUpToTemperature != "Yes" ) 
        {
        context.set("operation", "UpToTemperature") ;
        context.set("applianceUpToTemperature", "Yes") ;
        }
    else 
        {
        context.set("operation", "Waiting") ;   
        }
    }
// ---------------------------------------------------

// ---------------------------------------------------
// POWER CALCULATION
// Calculate power and cost and put into array.
// Only do this when cycle is occurring (Operating, OR Waiting).
// Note this method doesn't capture EVERY data point - 
// eg we'll miss the first few as we are calculating an 
// average.  We could capture more, but that is less efficient.
// ---------------------------------------------------
if( (operation === "Operating") || (operation === "Waiting")  ) 
    {
    // Push watthours into cyclePowerArray for cycle
    var wattHoursNow = power / ( 60 * ( 60 / metricsf )) ;
    cyclePowerArray.push(wattHoursNow) ;
    flow.set("cyclePowerArray", cyclePowerArray) ;

    // Calculate the cost of power
    var price ;
    if ( hour >= tariff.start && hour < tariff.end )
        {
        price = tariff.costDay ;         // Apply day tariff
        }
    if ( hour < tariff.start || hour >= tariff.end )
        {
        price = tariff.costNight ;       // Apply night tariff
        }

    // Fill cycleCostArray
    var costPerMinute = power/1000 * price / (60* (60/metricsf)) ;
    cycleCostArray.push(costPerMinute) ;
    flow.set("cycleCostArray", cycleCostArray) ; // Add to cost array
    }
// ---------------------------------------------------

// ---------------------------------------------------
// FINISHED
// Appliance was in Operating cycle OR Waiting, 
// but now has now finished (i.e. switched off or into Standby)
// ---------------------------------------------------

if (average <= standby && ( operation === "Operating" || operation === "Waiting"))
    {
    context.set("operation", "Finished") ;          // we are Finished
    context.set("applianceUpToTemperature", "No") ; // reset this for next operation
    currentCycle.cycleTimeStop = dateS ;            // log the end time

    // Calculate & format total cost of the entire cycle
    var sumCost = flow.get("cycleCostArray") ;
    var costOfPower = sumCost.reduce(add) ;
    currentCycle.totalCycleCostDollars = costOfPower ;
    
    // Format as $ or cents
    if ( costOfPower < 0.01 ){
        costOfPower = '<1c' ;
    } else if ( costOfPower < 1){
        costOfPower = costOfPower * 100 ;
        costOfPower = Math.round(costOfPower) ;
        costOfPower = costOfPower.toString() + 'c' ;
    } else {
        costOfPower = costOfPower.toFixed(2) ;
        costOfPower = '$' + costOfPower.toString() ;
    }
    currentCycle.totalCycleCostFormatted = costOfPower ;

    // Calculate & format total power use of the entire cycle
    var sumPower = flow.get("cyclePowerArray") ;
    var sumOfPower = sumPower.reduce(add) ;
    currentCycle.totalCyclePowerWattHours = sumOfPower ;

   // Format as wH or kWh
    if ( sumOfPower >= 1000 ){
        sumOfPower = sumOfPower / 1000 ;
        sumOfPower = sumOfPower.toFixed(1); // 1 decimal place
        sumOfPower = sumOfPower.toString() + 'kWh' ;
    } else if ( costOfPower < 1){
       costOfPower = '<1Wh' ;
    } else {
        sumOfPower = sumOfPower.toFixed(1); // 1 decimal place
        sumOfPower = sumOfPower.toString() + 'Wh' ;
    }
    currentCycle.totalCyclePowerFormatted = sumOfPower ;
    
    flow.set("currentApplianceCycle",currentCycle);  // Store in flow variable
}

// ---------------------------------------------------
// REMINDER
// Appliance is in Operating cycle OR Waiting, 
// and is still operating (e.e above standby power)
// Send reminder (to second output asynchrously)
// ---------------------------------------------------

if (average >= standby && ( operation === "Operating" || operation === "Waiting"))
    {
    //  Calculate how long has passed in minutes from cycle start
    var msg2 ;
    var diffMilliseconds = (date.getTime() - currentCycle.cycleTimeStart * 1000) ; // milliseconds between now & currentCycleStart
    var diffHoursFromStart = Math.floor(diffMilliseconds / 1000 / 60 / 60 ) ; // hours between now & currentCycleStart
    var diffMinsFromStart = Math.round(((diffMilliseconds % 86400000) % 3600000) / 60000) ; // minutes between now & currentCycleStart (without hours)
    if ( diffMinsFromStart > 0 && diffMinsFromStart % reminderFrequency == 0 )  // use modulus to see if a reminder is due
        {
        if (reminderDue && diffHoursFromStart == 1 && diffMinsFromStart != 60)
            {
            msg2 = {
                payload: "The " + applianceName 
                + " has been on for " + diffHoursFromStart + "hour, and " + diffMinsFromStart
                + " Minutes."
                } ;
            node.send([null, msg2]) ;
            reminderDue = false ;
            }  
        if (reminderDue && diffHoursFromStart > 1 && diffMinsFromStart != 60) {
            msg2 = {
                payload: "The " + applianceName
                    + " has been on for " + diffHoursFromStart + "hours, and " + diffMinsFromStart
                    + " Minutes."
            };
            node.send([null, msg2]);
            reminderDue = false;
            }  
        if (reminderDue && diffHoursFromStart >= 1 && diffMinsFromStart == 60) 
           {
            msg2 = {
                payload: "The " + applianceName
                    + " has been on for " + ( diffHoursFromStart + 1) + "hours." 
                    } ;
            node.send([null, msg2]);
            reminderDue = false;
            }  
        if (reminderDue && diffHoursFromStart < 1 && diffMinsFromStart <= 60) {
            msg2 = {
                payload: "The " + applianceName
                    + " has been on for " + diffMinsFromStart + "minutes."
                   } ;
            node.send([null, msg2]);
            reminderDue = false;
            }  
        }
        else  
        {
        reminderDue = true ;
        }
    context.set("reminderDue", reminderDue) ;  // set to node context for next round
    }
    
// ---------------------------------------------------
// Output debug stuff on each loop
// ---------------------------------------------------
if (flow.get("debugFlow") === true)
    {
    flow.set("debugAverage",average) ;
    flow.set("debugHours",hour) ;

    // Calculate total cost of the entire cycle so far
    var costSoFar = flow.get("cycleCostArray").reduce(add);
    flow.set("debugCostSoFar",costSoFar) ;

    // Calculate total power use of the entire cycle so far
    var powerSoFar = flow.get("cyclePowerArray").reduce(add);
    flow.set("debugPowerSoFar",powerSoFar) ;

    flow.set("debugCyclePowerArray",cyclePowerArray) ;
    flow.set("debugCycleCostArray",cycleCostArray) ;
    flow.set("debugRecentPowerArray",recentPowerArray) ;
    
    flow.set("debugReminderMins",diffMinsFromStart) ;
    flow.set("debugReminderHours", diffHoursFromStart) ;
    
    flow.set("debugRemindNow",reminderDue) ;
    }

msg.payload = context.get("operation") ;

return [ msg, null ] ;

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment