zorruno wikki: Appliance Completion Notification

Appliance Completion Notification


Changes

2021-04-12 V1.3 - zorruno
- Fixed loop not detecting Finished-> Standby
- Moved some flow variables to context
- Changed csv filename to reflect appliance

Summary

Node red flow to
- Monitor power from an appliance
- check when it is operating
- calculate cost of use
- 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)

Goals

This was mostly a lesson in Javascript and Node Red. I previously used some simple rules in a sonoff POW running tasmota to monitor when my washing machine and dryer completed (see LaundryTasmota). I wanted also to expand this to items like my EV charger, and dishwasher etc.

The tasmota rule method wasn't particularly reliable (occasionally falsely told me the laundry had finished in the middle of the night), so I looked at doing the same thing in Node Red to make it reliable and adding features such as calculating the cost of the power used.

I found this project from notenoughtech, but it really didn't do what I wanted. It was a great start however and gave me the opportunity to learn more Javascript and how Node Red function nodes can be built. I found what he had done a little confusing, not greatly documented (well for my pedantic mind anyway) and parts of the code seemed to be there from previous iterations (and did nothing). Finally, I learned about context and flow variables and simplified as much as I could, and only using flow variables where needed.

text

What does it do

The main crux of the code monitors the power from MQTT (from something like a Sonoff Pow V2, or in my case also a pulse based check meter for my EV charging) and takes an average over a few readings. If it looks like the appliance has started up, it will start calculating the power usage and cost of power (based on $/kWh, and can use peak/offpeak pricing). If the power usage average looks like the appliance is finished, it notifies (via various methods such as a google home random voice message, pushover phone push message or what have you) of completion, and the cost of power used.

The code is (now) well documented and there is a settings node that allows you to tweak pretty much all settings in one place, including such things as the type of appliance (washer, dryer etc... so the notifications are correct), the words to use to notify, the approx standby power of the device, how many readings to use for an average, and the electricity day/night tariff.

There are also a bunch of debug nodes and inject nodes for testing (mainly the JSON and notifications etc) as I wouldn't have been able to figure it all out without sensible debugging.

There is certainly more efficiencies to be had and improvements to the code, but I'm quite happy with it and it has taught me a lot. There is still small amounts of redundant code, and I have ideas to extend it. Node Red is great, but function nodes with Javascript are pretty much limitless in what you can achieve.

Push Notifications

An example notification (using Pushover) on Android and iphone:
Pushover: https://pushover.net/ (non free, but only a one off cost for each device)

"Dryer Notification
Your drying is complete.
It started at 21:20, finished at 21:41, and used 856.3Wh, taking 51min at a cost of 37c"


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":"EV Charger","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}


Configuration Example

// -----------------------------------------
// Notify and store power use and cost for an appliance
// FLOW VARIABLES
// Set these for the specific appliance you are measuring
// -----------------------------------------
// 2021-04-12 V1.3 - zorruno
// - Fixed loop not detecting Finished-> Standby
// - Moved some flow variables to context
// - Changed csv filename to reflect appliance
// -----------------------------------------

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

// Appliance Names (for notifications)
flow.set("applianceName", "Washer");
flow.set("applianceAction", "washing");

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

// Announcement text (for voice notifications)
flow.set("voiceAnnouncements", [
    "Hey, the washing machine is now complete",
    "Hey, your washing is complete",
    "The washing is done",
    "The washing machine has finished",
    "Washing has now finished",
    ]);

// Electrical tariff info  
// Updated with Auckland Contact Energy Costs, Feb 2021
// cost is in cents per kWh and start and end are the times
// to change tariffs.
var tariff = {"costDay": 0.1849,
              "costNight": 0.14792,
              "start": 7,
              "end": 21};

//---------------------------------------
// Appliance power settings
//---------------------------------------

// Average power in standby mode (it will show off if average
// power is above this and below the operatingPowerMinimum)
// Currently this just affects showing 'standby' vs 'off'
// so actual value is not critical.
flow.set("standbyPower", 2);    
// Minimum power when actually operating.
// This is averaged over 'resolution' values, so no problem
// if it drops below this value at times during operation.
flow.set("operatingPowerMinium", 10);  
// 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", 60);        


Main javascript Function Code

All of this code (and the configuration above) is in the Node Red Code below, but is here for reference
// -----------------------------------------
// Notify and store power use and cost for an appliance
// MAIN LOOP
// 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.
// -----------------------------------------
// 2021-04-12 V1.3 - zorruno
// - Fixed loop not detecting Finished-> Standby
// - Moved some flow variables to context
// - Changed csv filename to reflect appliance
// -----------------------------------------

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

// 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 opPowerMin = flow.get("operatingPowerMinium");
var currentCycle = flow.get("currentApplianceCycle");

// Get flow variables into local variables
var operation = context.get("operation") || "Off";
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();

// -----------------------------------------
// Fill power array, and calculate average
// power.  Do this on every loop.
// -----------------------------------------

// Function add for reduce array
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


// -----------------------------------------
// Fill cycle power array, and calculate average power.
// Only do this when cycle is occurring.
// Note this method doesn't capture EVERY data point -
// we'll miss the first few as we are calculating an
// average.  We could capture more, but that is less efficient.
// -----------------------------------------
if(operation === "Operating"){
    // 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
}

// -----------------------------------------
// Appliance is off
// -----------------------------------------
//if(average === 0){
if(average < standby){
  context.set("operation", "Off");
}

// -----------------------------------------
// Appliance has gone into Standby from Off
// -----------------------------------------
if(average >= standby && operation === "Off"){
  context.set("operation", "Standby");
}

// -----------------------------------------
// Appliance has gone into Standby from Finished
// -----------------------------------------
if(average >= standby && operation === "Finished"){
  context.set("operation", "Standby");
}

// -----------------------------------------
// Appliance has started its Operating cycle
// from Standby or Off
// -----------------------------------------
if((average > opPowerMin && operation === "Standby") || (average > opPowerMin && operation === "Off")){
  context.set("operation", "Operating");
  currentCycle.cycleTimeStart = dateS;
  cycleCostArray = [0]; // Clear array to start cycle
  flow.set("cycleCostArray",cycleCostArray);
  cyclePowerArray = [0]; // Clear array to start cycle
  flow.set("cyclePowerArray",cyclePowerArray);
}

// -----------------------------------------
// Appliance was in Operating cycle,
// but now has Finished
// -----------------------------------------
if(average < opPowerMin && operation === "Operating"){
    context.set("operation", "Finished");
    currentCycle.cycleTimeStop = dateS;

    // 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 ;

    msg.payload = context.get("operation") ;
    return [msg,msg]
}


// -----------------------------------------
// Output debug stuff on each loop
// -----------------------------------------
if(flow.get("debugFlow") === true){

    flow.set("debugAverage",average) ;

    // 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",cycleCostArray) ;
    flow.set("debugCycleCostArray",cyclePowerArray) ;
    flow.set("debugRecentPowerArray",recentPowerArray) ;

    msg.payload = context.get("operation") ;
    return [null,msg]
}


Node Red Code (EV Charger Notifications )

[{"id":"8cfcb1ff.b957e","type":"tab","label":"EV Charged Notification","disabled":false,"info":"# Appliance Notification\n\nThis is a vast derivation of the code originally found at the below. \n - [Complete instructions for **Google Home, Android & Windows**](https://notenoughtech.com/home-automation/washing-machine-notifications/)\n - [Complete instructions for **Alexa**](https://notenoughtech.com/home-automation/alexa-is-my-washing-ready/): \n\n# Settings\nSee Settings node in the setup menu - each setting had been explained in the comment.\n\n# Appliance data\nDepending on the MQTT message structure, you will need to adjust the power input to your needs. \n\n# Features\n\n**New & changed Stuff**:\n\n- Added debug code and nodes to help with testing\n- notifications also tell you power usage \n- added the ability to push to a csv file (unformatted and formatted times/costs/power)\n- made more generic for ANY appliance\n- notification tells how long appliance was going for as well as cost\n- removed any reference to the nagging until switched off. All my appliances stay on standby\n- uses power value from mqtt\n- most config is now in the settings function node, even for notifications\n- some efficencies, more still can be done\n- added Pushover notifications (push notification app for android and iphone)\n- lots more code comments\n- removed flow variables where context ones will do.\n\n**Original Features**:\n\n-   Google Home random voice notifications (Change IP address for device) \n-   DELETED: Android, Alexa, and Windows 10 Notifications \n-   No timers, nothing to set up    \n-   DELETED: Nagging mode for Google Home    \n-   Cost of washing (supports multi tarifs)  \n\n**Still to do**:\n\n- System currently relies on setting the right teleperiod or times that data gets pushed to match the code (eg 60 mins)\n- more efficient code in some places would be nice\n- Should really switch the appliance off when finished to save standby power (need an on switch somewhere)\n"},{"id":"3260cfab.0f6d8","type":"group","z":"8cfcb1ff.b957e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["91609baf.a3dae8","a369b5db.c72258","273b0c2b.3d3bd4","244dd341.1a034c","2316932d.1d489c"],"x":874,"y":79,"w":452,"h":202},{"id":"396b9814.6875d8","type":"group","z":"8cfcb1ff.b957e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["aa2c31f8.51871","7b713146.b36f5","1348c522.0f2c8b","18fe6e4f.4a1072","bfb51512.055008","59009dab.41fda4","3ad3c9fb.548a86","965e8b4.c68b778","4770b186.3f53c"],"x":34,"y":459,"w":712,"h":462},{"id":"4219661b.098d48","type":"group","z":"8cfcb1ff.b957e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["e007722f.02bb","be6a8a99.0b0958","af82b5e4.2f41d8","ddd5d398.9df2a","4baf4cc1.87d384","5e5a02a3.cc51dc","b468246f.61ebf8"],"x":874,"y":293,"w":452,"h":302},{"id":"7e5a8959.71df28","type":"group","z":"8cfcb1ff.b957e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["bc300e6c.85029","7b940ad4.7685e4","6b546f5a.74bbe","686cd8f1.f0fd28","d1ec5ba5.d7eeb8","3f6a0b44.15f7c4","369309f5.85c146","48013a77.fa2f14"],"x":36,"y":170,"w":452,"h":282},{"id":"85bfd9df.f70e58","type":"group","z":"8cfcb1ff.b957e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["72f9c1dd.60ad","2fff0056.25422","18a3cba.181d334","4ef56ac1.b18cb4","dd25bb24.d87028","99466a15.949de8","86042560.10bc58"],"x":874,"y":611,"w":472,"h":322},{"id":"e932d4ab.b99c08","type":"function","z":"8cfcb1ff.b957e","name":"Main Loop","func":"// -----------------------------------------\n// Notify and store power use and cost for an appliance\n// MAIN LOOP\n// Full loop to calculate power average of appliance and \n// work out when it is operating.  When operational\n// fill arrays with power used, and cost of power calculated.\n// -----------------------------------------\n// 2021-04-12 V1.3 - zorruno\n// - Fixed loop not detecting Finished-> Standby \n// - Moved some flow variables to context\n// - Changed csv filename to reflect appliance\n// -----------------------------------------\n\n// The input message must be the current power in Watts\nvar power = msg.payload;\n\n// Get flow user settings variables into local variables\nvar res   = flow.get(\"resolution\");\nvar tariff = flow.get(\"tariff\");\nvar metricsf = flow.get(\"metricFrequency\");\nvar standby = flow.get(\"standbyPower\");\nvar opPowerMin = flow.get(\"operatingPowerMinium\");\nvar currentCycle = flow.get(\"currentApplianceCycle\");\n\n// Get flow variables into local variables\nvar operation = context.get(\"operation\") || \"Off\";\nvar recentPowerArray = flow.get(\"recentPowerArray\") || [0];\nvar cycleCostArray  = flow.get(\"cycleCostArray\") || [0];\nvar cyclePowerArray  =  flow.get(\"cyclePowerArray\") || [0];\n\n// Get date, seconds and hours\nvar date = new Date();\nvar dateS = date.getTime()/1000;\nvar hour = date.getHours();\n\n// -----------------------------------------\n// Fill power array, and calculate average \n// power.  Do this on every loop.\n// -----------------------------------------\n\n// Function add for reduce array\nfunction add(accumulator, a) {\n    return accumulator + a;\n}\n// Push power into TotalPower array for average\nrecentPowerArray.unshift(power);\n// Remove X element to get total resolution for average calc\nif(recentPowerArray[res] === undefined) {\n    flow.set(\"recentPowerArray\", recentPowerArray);\n}\nelse {\n    recentPowerArray.splice(res, 1);\n    flow.set(\"recentPowerArray\", recentPowerArray);\n}\n// Calculate average power from array\nvar sum = recentPowerArray;\nvar average = (sum.reduce(add)/recentPowerArray.length);   // Average the array\n\n\n// -----------------------------------------\n// Fill cycle power array, and calculate average power.\n// Only do this when cycle is occurring.\n// Note this method doesn't capture EVERY data point - \n// we'll miss the first few as we are calculating an \n// average.  We could capture more, but that is less efficient.\n// -----------------------------------------\nif(operation === \"Operating\"){\n    // Push watthours into cyclePowerArray for cycle\n    var wattHoursNow = power / ( 60 * ( 60 / metricsf ));\n    cyclePowerArray.push(wattHoursNow);\n    flow.set(\"cyclePowerArray\", cyclePowerArray);\n\n    // Calculate the cost of power\n    var price;\n    if( hour >= tariff.start && hour < tariff.end ){\n        price = tariff.costDay; // Apply day tariff\n    }\n    if( hour < tariff.start || hour >= tariff.end ){\n        price = tariff.costNight; // Apply night tariff\n    }\n\n    // Fill cycleCostArray\n    var costPerMinute = power/1000 * price / (60* (60/metricsf));\n    cycleCostArray.push(costPerMinute);\n    flow.set(\"cycleCostArray\", cycleCostArray); // Add to cost array\n}\n\n// -----------------------------------------\n// Appliance is off\n// -----------------------------------------\n//if(average === 0){\nif(average < standby){\n  context.set(\"operation\", \"Off\");\n}\n\n// -----------------------------------------\n// Appliance has gone into Standby from Off\n// -----------------------------------------\nif(average >= standby && operation === \"Off\"){\n  context.set(\"operation\", \"Standby\");\n}\n\n// -----------------------------------------\n// Appliance has gone into Standby from Finished\n// -----------------------------------------\nif(average >= standby && operation === \"Finished\"){\n  context.set(\"operation\", \"Standby\");\n}\n\n// -----------------------------------------\n// Appliance has started its Operating cycle \n// from Standby or Off\n// -----------------------------------------\nif((average > opPowerMin && operation === \"Standby\") || (average > opPowerMin && operation === \"Off\")){\n  context.set(\"operation\", \"Operating\");\n  currentCycle.cycleTimeStart = dateS;\n  cycleCostArray = [0]; // Clear array to start cycle\n  flow.set(\"cycleCostArray\",cycleCostArray);\n  cyclePowerArray = [0]; // Clear array to start cycle\n  flow.set(\"cyclePowerArray\",cyclePowerArray);\n}\n\n// -----------------------------------------\n// Appliance was in Operating cycle, \n// but now has Finished\n// -----------------------------------------\nif(average < opPowerMin && operation === \"Operating\"){\n    context.set(\"operation\", \"Finished\");\n    currentCycle.cycleTimeStop = dateS;\n\n    // Calculate & format total cost of the entire cycle\n    var sumCost = flow.get(\"cycleCostArray\");\n    var costOfPower = sumCost.reduce(add);\n    currentCycle.totalCycleCostDollars = costOfPower;\n    \n    // Format as $ or cents\n    if ( costOfPower < 0.01 ){\n        costOfPower = '<1c';\n    } else if ( costOfPower < 1){\n        costOfPower = costOfPower * 100;\n        costOfPower = Math.round(costOfPower);\n        costOfPower = costOfPower.toString() + 'c';\n    } else {\n        costOfPower = costOfPower.toFixed(2);\n        costOfPower = '$' + costOfPower.toString();\n    }\n    currentCycle.totalCycleCostFormatted = costOfPower ;\n\n    // Calculate & format total power use of the entire cycle\n    var sumPower = flow.get(\"cyclePowerArray\");\n    var sumOfPower = sumPower.reduce(add);\n    currentCycle.totalCyclePowerWattHours = sumOfPower;\n\n   // Format as wH or kWh\n    if ( sumOfPower >= 1000 ){\n        sumOfPower = sumOfPower / 1000;\n        sumOfPower = sumOfPower.toFixed(1); // 1 decimal place\n        sumOfPower = sumOfPower.toString() + 'kWh';\n    } else if ( costOfPower < 1){\n       costOfPower = '<1Wh';\n    } else {\n        sumOfPower = sumOfPower.toFixed(1); // 1 decimal place\n        sumOfPower = sumOfPower.toString() + 'Wh';\n    }\n\n    currentCycle.totalCyclePowerFormatted = sumOfPower ;\n\n    msg.payload = context.get(\"operation\") ;\n    return [msg,msg] \n}\n\n\n// -----------------------------------------\n// Output debug stuff on each loop\n// -----------------------------------------\nif(flow.get(\"debugFlow\") === true){\n\n    flow.set(\"debugAverage\",average) ;\n\n    // Calculate total cost of the entire cycle so far\n    var costSoFar = flow.get(\"cycleCostArray\").reduce(add);\n    flow.set(\"debugCostSoFar\",costSoFar) ;\n\n    // Calculate total power use of the entire cycle so far\n    var powerSoFar = flow.get(\"cyclePowerArray\").reduce(add);\n    flow.set(\"debugPowerSoFar\",powerSoFar) ;\n\n    flow.set(\"debugCyclePowerArray\",cyclePowerArray) ;\n    flow.set(\"debugCycleCostArray\",cycleCostArray) ;\n    flow.set(\"debugRecentPowerArray\",recentPowerArray) ;\n\n    msg.payload = context.get(\"operation\") ;\n    return [null,msg]\n}\n\n\n \n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":670,"y":300,"wires":[["233b1321.0d520c"],["59009dab.41fda4"]]},{"id":"eab8b087.e8911","type":"comment","z":"8cfcb1ff.b957e","name":"Settings","info":"","x":120,"y":80,"wires":[]},{"id":"57b444a9.248eac","type":"function","z":"8cfcb1ff.b957e","name":"Settings","func":"//see Setup tab\n","outputs":1,"noerr":0,"initialize":"// -----------------------------------------\n// Notify and store power use and cost for an appliance\n// FLOW VARIABLES\n// Full loop to calculate power average of appliance and \n// work out when it is operating.  When operational\n// fill arrays with power used, and cost of power calculated.\n// -----------------------------------------\n// 2021-04-12 V1.3 - zorruno\n// - Fixed loop not detecting Finished-> Standby \n// - Moved some flow variables to context\n// - Changed csv filename to reflect appliance\n// -----------------------------------------\n\n// Debugging nodes\nflow.set(\"debugFlow\", true);\n\n// Appliance Names (for notifications)\n// ie. \"your ApplianceName is ApplianceAction\"\nflow.set(\"applianceName\", \"EV Charger\"); \nflow.set(\"applianceAction\", \"charging\"); \n\n// Announcement text (for voice notifications if used)\n// these are randomised later.\nflow.set(\"voiceAnnouncements\", [\n    \"Hey, the e v charging is now complete\",\n    \"Hey, your charging is complete\",\n    \"The car charging is done\",\n    \"The e v charging has finished\",\n    \"E V Charging has now finished\",\n    ]);\n\n\n// Electrical tariff info   \n// Updated with Auckland Contact Energy Costs, Feb 2021\n// cost is in cents per kWh and start and end are the times\n// to change tariffs.\nvar tariff = {\"costDay\": 0.1849, \n              \"costNight\": 0.14792,\n              \"start\": 7,\n              \"end\": 21};\n\n//---------------------------------------\n// Appliance power settings\n//---------------------------------------\n\n// Average power in standby mode (it will show off if average\n// power is above this and below the operatingPowerMinimum)\n// Currently this just affects showing 'standby' vs 'off'\n// so actual value is not critical.\nflow.set(\"standbyPower\", 2);       \n// Minimum power when actually operating.\n// This is averaged over 'resolution' values, so no problem\n// if it drops below this value at times during operation.\nflow.set(\"operatingPowerMinium\", 100);  \n// How many times to do a power reading for rolling average.\nflow.set(\"resolution\", 6);      \n// How often does the appliance report back (seconds)\nflow.set(\"metricFrequency\", 60);        \n\n//---------------------------------------\n// No need to change these \n//---------------------------------------\nflow.set(\"tariff\", tariff);\n\n//flow.set(\"operation\", \"Off\");\nflow.set(\"recentPowerArray\", [0]);\nflow.set(\"cycleCostArray\", [0]);\nflow.set(\"cyclePowerArray\", [0]);\n\nvar cycle = {   \"cycleTimeStart\": null,\n                \"cycleTimeStop\": null,\n                \"totalCycleCostFormatted\": 0,\n                \"totalCycleCostDollars\": 0,\n                \"totalCyclePowerFormatted\": 0,\n                \"totalCyclePowerWattHours\": 0,\n            }\n            \nflow.set(\"currentApplianceCycle\",cycle);\n","finalize":"","x":120,"y":120,"wires":[[]]},{"id":"e49748b3.532cc8","type":"mqtt in","z":"8cfcb1ff.b957e","name":"Appliance Data","topic":"stat/tasmo-wemosd1-7280-powermon-1/EnergyMeterCount","qos":"0","datatype":"json","broker":"e28b763a.77bd98","x":300,"y":120,"wires":[["279aedc1.461022","8978c85f.c239c8"]]},{"id":"233b1321.0d520c","type":"link out","z":"8cfcb1ff.b957e","name":"","links":["244dd341.1a034c","559a8885.34c1e8","4baf4cc1.87d384"],"x":795,"y":260,"wires":[]},{"id":"279aedc1.461022","type":"debug","z":"8cfcb1ff.b957e","name":"","active":false,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload.EVChargerWhCount","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":480,"y":71,"wires":[]},{"id":"aa2c31f8.51871","type":"debug","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Cycle: Cost Array","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugCycleCostArray')\t","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":550,"y":820,"wires":[]},{"id":"7b713146.b36f5","type":"debug","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Recent Power Readings","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugRecentPowerArray')\t","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":580,"y":880,"wires":[]},{"id":"4853b668.0ee738","type":"comment","z":"8cfcb1ff.b957e","name":"Operation","info":"","x":660,"y":260,"wires":[]},{"id":"eb3c3f47.e1d9b","type":"comment","z":"8cfcb1ff.b957e","name":"Notifications","info":"","x":946,"y":55,"wires":[]},{"id":"72f9c1dd.60ad","type":"cast-to-client","z":"8cfcb1ff.b957e","g":"85bfd9df.f70e58","name":"Notify A Google Home","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"50","x":1220,"y":892,"wires":[[]]},{"id":"2fff0056.25422","type":"play audio","z":"8cfcb1ff.b957e","g":"85bfd9df.f70e58","name":"","voice":"2","x":1060,"y":752,"wires":[]},{"id":"18a3cba.181d334","type":"function","z":"8cfcb1ff.b957e","g":"85bfd9df.f70e58","name":"Random Notification Selection","func":"// Get a set of statements and push one at random back to\n// the payload.  An example for a washing machine is below\n//\n// var announce = [\n//    \"The washing machine is now complete\",\n//    \"Your washing is complete\",\n//    \"The washing is done\",\n//    \"The washing machine has finished\",\n//    \"Washing has now finished\",\n//    ];\n    \nvar announce = flow.get(\"voiceAnnouncements\");\n    \nvar random = announce[Math.floor(Math.random() * announce.length)];\nmsg.payload = random;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1190,"y":712,"wires":[["2fff0056.25422","4ef56ac1.b18cb4"]]},{"id":"4ef56ac1.b18cb4","type":"change","z":"8cfcb1ff.b957e","g":"85bfd9df.f70e58","name":"Google Home Notification Details","rules":[{"t":"set","p":"ip","pt":"msg","to":"192.168.2.97","tot":"str"},{"t":"set","p":"language","pt":"msg","to":"En-gb","tot":"str"},{"t":"set","p":"volume","pt":"msg","to":"80","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1180,"y":832,"wires":[["72f9c1dd.60ad"]]},{"id":"dd25bb24.d87028","type":"link in","z":"8cfcb1ff.b957e","g":"85bfd9df.f70e58","name":"Appliance Audio Announcements","links":[],"x":915,"y":712,"wires":[["18a3cba.181d334"]]},{"id":"99466a15.949de8","type":"inject","z":"8cfcb1ff.b957e","g":"85bfd9df.f70e58","name":"Test Notification","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1240,"y":652,"wires":[["18a3cba.181d334"]]},{"id":"91609baf.a3dae8","type":"pushover","z":"8cfcb1ff.b957e","g":"3260cfab.0f6d8","name":"Pushover Phone Message","device":"galaxynote","title":"","priority":0,"sound":"classical","url":"","url_title":"","html":false,"x":1180,"y":240,"wires":[]},{"id":"a369b5db.c72258","type":"function","z":"8cfcb1ff.b957e","g":"3260cfab.0f6d8","name":"Notification Text","func":"// Get Current Cycle Symmary Array\nvar currentCycle = flow.get(\"currentApplianceCycle\");\n\nvar date = new Date();\nmsg.payload = {};\n\n// Function to convert unix epoch to X hours and Y mins\n// and format as 'Xh Xmin'.  Drop the hours if less than 1.\nfunction secondsToHms(d) {\n    d = Number(d);\n    var h = Math.floor(d / 3600);\n    var m = Math.floor(d % 3600 / 60);\n    if (h < 1 ) {\n        return ('0' + m).slice(-2)+\"min\";\n    }\n    else {\n        return ('0' + h).slice(-2) + \"h \" + ('0' + m).slice(-2)+\"min\";\n    }\n}\n\n// Function to convert unix epoch to 00:00\nfunction epochToFormattedTime(d) {\n    d = Date(d * 1000);\n    var h = d.gethours();\n    var m = \"0\" + d.getminutes();\n    return h + ':' + m.substr(-2);\n}\n\n\n// Extract start/stop time based on the unix timestamp\n// First multiply by 1000 so that the argument is in milliseconds, not seconds.\nvar startDateTime = new Date(currentCycle.cycleTimeStart * 1000);\nvar stopDateTime = new Date(currentCycle.cycleTimeStop * 1000);\nvar startDateHours = \"0\" + startDateTime.getHours();\nvar stopDateHours = \"0\" + stopDateTime.getHours();\nvar startDateMinutes = \"0\" + startDateTime.getMinutes();\nvar stopDateMinutes = \"0\" + stopDateTime.getMinutes();\n\n// Format times\n// Will display time in 10:30 format (no seconds)\nvar formattedStartTime = startDateHours.substr(-2) + ':' + startDateMinutes.substr(-2)\nvar formattedStopTime = stopDateHours.substr(-2) + ':' + stopDateMinutes.substr(-2)\n\n// Put together a notification ready for sending to notifying tools\nmsg.topic = flow.get(\"applianceName\") + \" Notification\";\nmsg.payload = \"Your \" + flow.get(\"applianceAction\") + \n              \" is complete.\\nIt started at \" + formattedStartTime +  \n              \", finished at \" + formattedStopTime + \n              \", and used \" + currentCycle.totalCyclePowerFormatted + \n              \", taking \" + secondsToHms(currentCycle.cycleTimeStop - currentCycle.cycleTimeStart) + \n              \" at a cost of \" + currentCycle.totalCycleCostFormatted;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1220,"y":180,"wires":[["91609baf.a3dae8"]]},{"id":"273b0c2b.3d3bd4","type":"inject","z":"8cfcb1ff.b957e","g":"3260cfab.0f6d8","name":"Test Notification","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1220,"y":120,"wires":[["a369b5db.c72258"]]},{"id":"244dd341.1a034c","type":"link in","z":"8cfcb1ff.b957e","g":"3260cfab.0f6d8","name":"Appliance Pushover Notification","links":["233b1321.0d520c"],"x":915,"y":180,"wires":[["a369b5db.c72258"]]},{"id":"bc300e6c.85029","type":"inject","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1800","payloadType":"num","x":132,"y":291,"wires":[["369309f5.85c146"]]},{"id":"7b940ad4.7685e4","type":"debug","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"","active":false,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":362,"y":351,"wires":[]},{"id":"6b546f5a.74bbe","type":"inject","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"300","payloadType":"num","x":132,"y":331,"wires":[["369309f5.85c146"]]},{"id":"686cd8f1.f0fd28","type":"inject","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"30","payloadType":"num","x":132,"y":371,"wires":[["369309f5.85c146"]]},{"id":"d1ec5ba5.d7eeb8","type":"comment","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"Inject Values For Testing Only","info":"Can be used to generate test power values into the input","x":182,"y":211,"wires":[]},{"id":"8978c85f.c239c8","type":"function","z":"8cfcb1ff.b957e","name":"Convert to Watts","func":"// We want the msg.payload to be the power now in Watts.\n// in this case, the payload is a count from a check meter\n// This check meter gives 2Wh per pulse so to convert to Watts\n// we multiply the count by two and multiply by 60 (minutes)\n\nmsg.payload = msg.payload.EVChargerWhCount;\nmsg.payload = msg.payload * 2 * 60;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":500,"y":120,"wires":[["e932d4ab.b99c08","5b6b9f9e.c16d9"]]},{"id":"5b6b9f9e.c16d9","type":"debug","z":"8cfcb1ff.b957e","name":"","active":false,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":680,"y":71,"wires":[]},{"id":"1348c522.0f2c8b","type":"debug","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Cycle: Power Array","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugCyclePowerArray')\t","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":560,"y":760,"wires":[]},{"id":"5bfac55b.863d9c","type":"comment","z":"8cfcb1ff.b957e","name":"Power Input","info":"","x":290,"y":80,"wires":[]},{"id":"18fe6e4f.4a1072","type":"comment","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"If Debugging On","info":"","x":140,"y":500,"wires":[]},{"id":"e007722f.02bb","type":"file","z":"8cfcb1ff.b957e","g":"4219661b.098d48","name":"Push to CSV File","filename":"","appendNewline":true,"createDir":true,"overwriteFile":"false","encoding":"none","x":1210,"y":494,"wires":[["ddd5d398.9df2a"]]},{"id":"be6a8a99.0b0958","type":"function","z":"8cfcb1ff.b957e","g":"4219661b.098d48","name":"Data Manipulation for CSV File","func":"// Get Current Cycle Symmary Array\nvar currentCycle = flow.get(\"currentApplianceCycle\");\n\nvar date = new Date();\nmsg.payload = {};\n\n// Function to convert unix epoch to X hours and Y mins\n// and format as 'Xh Xmin'.  Drop the hours if less than 1.\nfunction secondsToHms(d) {\n    d = Number(d);\n    var h = Math.floor(d / 3600);\n    var m = Math.floor(d % 3600 / 60);\n    if (h < 1 ) { \n        return ('0' + m).slice(-2)+\"min\"; \n    }\n    else {\n        return ('0' + h).slice(-2) + \"h \" + ('0' + m).slice(-2)+\"min\"; \n    }\n}\n\n// Function to convert unix epoch to 00:00\nfunction epochToFormattedTime(d) {\n    d = Date(d * 1000);\n    var h = d.gethours();\n    var m = \"0\" + d.getminutes();\n    return h + ':' + m.substr(-2);\n}\n\n\n// Extract start/stop time based on the unix timestamp\n// First multiply by 1000 so that the argument is in milliseconds, not seconds.\nvar startDateTime = new Date(currentCycle.cycleTimeStart * 1000);\nvar stopDateTime = new Date(currentCycle.cycleTimeStop * 1000);\nvar startDateHours = startDateTime.getHours();\nvar stopDateHours = stopDateTime.getHours();\nvar startDateMinutes = \"0\" + startDateTime.getMinutes();\nvar stopDateMinutes = \"0\" + stopDateTime.getMinutes();\n\n// Format times\n// Will display time in 10:30 format (no seconds)\nvar formattedStartTime = startDateHours + ':' + startDateMinutes.substr(-2)\nvar formattedStopTime = stopDateHours + ':' + stopDateMinutes.substr(-2)\n\ncsvPayload={\n    \"date/time\": date,\n    \"Appliance\": flow.get(\"applianceName\"), \n    \"Start Time\": formattedStartTime, \n    \"Finish Time\": formattedStopTime,\n    \"Cycle Time Formatted\": secondsToHms(currentCycle.cycleTimeStop - currentCycle.cycleTimeStart), \n    \"Cycle Time (s)\": currentCycle.cycleTimeStop - currentCycle.cycleTimeStart, \n    \"Cycle Power Use Formatted\": currentCycle.totalCyclePowerFormatted,        \n    \"Cycle Power Use (Wh)\": currentCycle.totalCyclePowerWattHours,        \n    \"Cycle Cost Formatted\": currentCycle.totalCycleCostFormatted,\n    \"Cycle Cost ($)\": currentCycle.totalCycleCostDollars,\n}\n\nmsg.payload=csvPayload;\nvar filename = flow.get(\"applianceName\"); \nmsg.filename = \"/data/\" + filename.replace(/\\s+/g, '') + \"_poweruse.csv\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1170,"y":394,"wires":[["af82b5e4.2f41d8"]]},{"id":"af82b5e4.2f41d8","type":"json","z":"8cfcb1ff.b957e","g":"4219661b.098d48","name":"Convert to JSON","property":"payload","action":"","pretty":false,"x":1210,"y":444,"wires":[["e007722f.02bb"]]},{"id":"ddd5d398.9df2a","type":"debug","z":"8cfcb1ff.b957e","g":"4219661b.098d48","name":"debug","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":1180,"y":554,"wires":[]},{"id":"4baf4cc1.87d384","type":"link in","z":"8cfcb1ff.b957e","g":"4219661b.098d48","name":"Appliance History CSV File Creation","links":["233b1321.0d520c"],"x":915,"y":394,"wires":[["be6a8a99.0b0958"]]},{"id":"5e5a02a3.cc51dc","type":"inject","z":"8cfcb1ff.b957e","g":"4219661b.098d48","name":"Test Notification","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1220,"y":334,"wires":[["be6a8a99.0b0958"]]},{"id":"3f6a0b44.15f7c4","type":"inject","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"3265","payloadType":"num","x":132,"y":251,"wires":[["369309f5.85c146"]]},{"id":"369309f5.85c146","type":"change","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"Passthrough","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":352,"y":291,"wires":[["7b940ad4.7685e4","e932d4ab.b99c08"]]},{"id":"48013a77.fa2f14","type":"inject","z":"8cfcb1ff.b957e","g":"7e5a8959.71df28","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":132,"y":411,"wires":[["369309f5.85c146"]]},{"id":"2316932d.1d489c","type":"comment","z":"8cfcb1ff.b957e","g":"3260cfab.0f6d8","name":"Pushover App","info":"","x":970,"y":120,"wires":[]},{"id":"86042560.10bc58","type":"comment","z":"8cfcb1ff.b957e","g":"85bfd9df.f70e58","name":"Google Home Announce","info":"","x":1010,"y":652,"wires":[]},{"id":"b468246f.61ebf8","type":"comment","z":"8cfcb1ff.b957e","g":"4219661b.098d48","name":"Data to CSV File","info":"","x":980,"y":334,"wires":[]},{"id":"bfb51512.055008","type":"debug","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Debug: Cost So Far","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugCostSoFar')","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":560,"y":640,"wires":[]},{"id":"59009dab.41fda4","type":"change","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Passthrough","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":190,"y":720,"wires":[["bfb51512.055008","3ad3c9fb.548a86","965e8b4.c68b778","1348c522.0f2c8b","aa2c31f8.51871","7b713146.b36f5","4770b186.3f53c"]]},{"id":"3ad3c9fb.548a86","type":"debug","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Debug: Power So Far","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugPowerSoFar')","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":570,"y":700,"wires":[]},{"id":"965e8b4.c68b778","type":"debug","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Debug: Average Now","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugAverage')","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":570,"y":580,"wires":[]},{"id":"4770b186.3f53c","type":"debug","z":"8cfcb1ff.b957e","g":"396b9814.6875d8","name":"Operation","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":530,"y":520,"wires":[]},{"id":"e28b763a.77bd98","type":"mqtt-broker","name":"Panda","broker":"192.168.X.X","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]


Node Red Code (Washing Machine Notifications)

[{"id":"3d2a02fd.1b421e","type":"tab","label":"Washer Notifications","disabled":false,"info":"# Appliance Notification\n\nThis is a vast derivation of the code originally found at the below. \n - [Complete instructions for **Google Home, Android & Windows**](https://notenoughtech.com/home-automation/washing-machine-notifications/)\n - [Complete instructions for **Alexa**](https://notenoughtech.com/home-automation/alexa-is-my-washing-ready/): \n\n# Settings\nSee Settings node in the setup menu - each setting had been explained in the comment.\n\n# Appliance data\nDepending on the MQTT message structure, you will need to adjust the power input to your needs. \n\n# Features\n\n**New & changed Stuff**:\n\n- Added debug code and nodes to help with testing\n- notifications also tell you power usage \n- added the ability to push to a csv file (unformatted and formatted times/costs/power)\n- made more generic for ANY appliance\n- notification tells how long appliance was going for as well as cost\n- removed any reference to the nagging until switched off. All my appliances stay on standby\n- uses power value from mqtt\n- most config is now in the settings function node, even for notifications\n- some efficencies, more still can be done\n- added Pushover notifications (push notification app for android and iphone)\n- lots more code comments\n- removed flow variables where context ones will do.\n\n**Original Features**:\n\n-   Google Home random voice notifications (Change IP address for device) \n-   DELETED: Android, Alexa, and Windows 10 Notifications \n-   No timers, nothing to set up    \n-   DELETED: Nagging mode for Google Home    \n-   Cost of washing (supports multi tarifs)  \n\n**Still to do**:\n\n- System currently relies on setting the right teleperiod or times that data gets pushed to match the code (eg 60 mins)\n- more efficient code in some places would be nice\n- Should really switch the appliance off when finished to save standby power (need an on switch somewhere)\n"},{"id":"27cca7b8.5d45c8","type":"group","z":"3d2a02fd.1b421e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["f268d769.2c4d78","b2a75ece.53e29","3cd0a6f9.014d7a","cfd2a083.7a6c6","62e5c924.6c8808"],"x":874,"y":79,"w":532,"h":202},{"id":"3b5c156a.9c65aa","type":"group","z":"3d2a02fd.1b421e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["d0f9e0a.534822","35eb3a08.3b6956","529e42d5.a356fc","306f0caf.042094","58bf6075.437f9","fc0ec529.c12c48","4b9423df.00159c","a33b3a29.9fd2c8","5f5749c0.0d0578"],"x":36,"y":170,"w":452,"h":291},{"id":"6f58a785.6fd898","type":"group","z":"3d2a02fd.1b421e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["2c074a44.483cf6","fc61477e.80b4e8","8578e8aa.b7a248","36b756a0.26d46a","ecba72be.fe705","bbc410c3.d6efa","67a1fc62.57c914","573ef317.5fa43c","37b6460f.1aebfa"],"x":34,"y":459,"w":712,"h":462},{"id":"72d8fb35.7c5474","type":"group","z":"3d2a02fd.1b421e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["5dde5b19.1e0774","7fd876a2.3da9b8","4797e8cb.dc10e8","b67b7d1c.b383","a555eaa9.ccb958","ae89197b.a085c8","1dcd014.fe6f2ff"],"x":874,"y":611,"w":472,"h":322},{"id":"dff1b97f.128698","type":"group","z":"3d2a02fd.1b421e","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["e28f85e7.64c748","f02aa0d9.8e46e","f465bfcd.8cc17","1edd0236.05616e","a5a48c9e.92e3e","bd54160c.9e3648","32e3a454.f3817c"],"x":874,"y":293,"w":452,"h":302},{"id":"7935febc.65d1f","type":"comment","z":"3d2a02fd.1b421e","name":"Settings","info":"","x":120,"y":80,"wires":[]},{"id":"564cc576.e394fc","type":"function","z":"3d2a02fd.1b421e","name":"Settings","func":"//see Setup tab\n","outputs":1,"noerr":0,"initialize":"// -----------------------------------------\n// Notify and store power use and cost for an appliance\n// FLOW VARIABLES\n// Full loop to calculate power average of appliance and \n// work out when it is operating.  When operational\n// fill arrays with power used, and cost of power calculated.\n// -----------------------------------------\n// 2021-04-12 V1.3 - zorruno\n// - Fixed loop not detecting Finished-> Standby \n// - Moved some flow variables to context\n// - Changed csv filename to reflect appliance\n// -----------------------------------------\n\n// Debugging nodes\nflow.set(\"debugFlow\", true);\n\n// Appliance Names (for notifications)\nflow.set(\"applianceName\", \"Washer\"); \nflow.set(\"applianceAction\", \"washing\"); \n\n//flow.set(\"JSONPowerTopic\", msg.payload.ENERGY.Power);\n\n// Announcement text (for voice notifications)\nflow.set(\"voiceAnnouncements\", [\n    \"Hey, the washing machine is now complete\",\n    \"Hey, your washing is complete\",\n    \"The washing is done\",\n    \"The washing machine has finished\",\n    \"Washing has now finished\",\n    ]);\n\n// Electrical tariff info   \n// Updated with Auckland Contact Energy Costs, Feb 2021\n// cost is in cents per kWh and start and end are the times\n// to change tariffs.\nvar tariff = {\"costDay\": 0.1849, \n              \"costNight\": 0.14792,\n              \"start\": 7,\n              \"end\": 21};\n\n//---------------------------------------\n// Appliance power settings\n//---------------------------------------\n\n// Average power in standby mode (it will show off if average\n// power is above this and below the operatingPowerMinimum)\n// Currently this just affects showing 'standby' vs 'off'\n// so actual value is not critical.\nflow.set(\"standbyPower\", 2);     \n// Minimum power when actually operating.\n// This is averaged over 'resolution' values, so no problem\n// if it drops below this value at times during operation.\nflow.set(\"operatingPowerMinium\", 20);  \n// How many times to do a power reading for rolling average.\nflow.set(\"resolution\", 6);      \n// How often does the appliance report back (seconds)\nflow.set(\"metricFrequency\", 60);        \n\n//---------------------------------------\n// No need to change these \n//---------------------------------------\nflow.set(\"tariff\", tariff);\n\nflow.set(\"recentPowerArray\", [0]);\nflow.set(\"cycleCostArray\", [0]);\nflow.set(\"cyclePowerArray\", [0]);\n\nvar cycle = {   \"cycleTimeStart\": null,\n                \"cycleTimeStop\": null,\n                \"totalCycleCostFormatted\": 0,\n                \"totalCycleCostDollars\": 0,\n                \"totalCyclePowerFormatted\": 0,\n                \"totalCyclePowerWattHours\": 0,\n            }\n\nflow.set(\"currentApplianceCycle\",cycle);\n\n\n\n","finalize":"","x":120,"y":120,"wires":[[]]},{"id":"59585eec.e98cb","type":"mqtt in","z":"3d2a02fd.1b421e","name":"Appliance Data","topic":"tele/tasmo-spow-1957-laundrywash/SENSOR","qos":"0","datatype":"json","broker":"e28b763a.77bd98","x":300,"y":120,"wires":[["d07beb23.9f8618","bb3215aa.7a5b88"]]},{"id":"189b9430.0574bc","type":"link out","z":"3d2a02fd.1b421e","name":"","links":["cfd2a083.7a6c6","559a8885.34c1e8","a5a48c9e.92e3e","a555eaa9.ccb958"],"x":795,"y":260,"wires":[]},{"id":"d07beb23.9f8618","type":"debug","z":"3d2a02fd.1b421e","name":"","active":false,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload.ENERGY.Power","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":480,"y":71,"wires":[]},{"id":"46013d1e.7c4b64","type":"comment","z":"3d2a02fd.1b421e","name":"Operation","info":"","x":660,"y":260,"wires":[]},{"id":"2829d97d.7e5ef6","type":"comment","z":"3d2a02fd.1b421e","name":"Notifications","info":"","x":946,"y":55,"wires":[]},{"id":"bb3215aa.7a5b88","type":"function","z":"3d2a02fd.1b421e","name":"Convert to Watts","func":"// We want the msg.payload to be the power now in Watts.\n\nmsg.payload = msg.payload.ENERGY.Power;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":500,"y":120,"wires":[["9d25581c.e09578","34891fda.0a794"]]},{"id":"9d25581c.e09578","type":"debug","z":"3d2a02fd.1b421e","name":"","active":false,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":740,"y":120,"wires":[]},{"id":"612b088f.178118","type":"comment","z":"3d2a02fd.1b421e","name":"Power Input","info":"","x":290,"y":80,"wires":[]},{"id":"f268d769.2c4d78","type":"pushover","z":"3d2a02fd.1b421e","g":"27cca7b8.5d45c8","name":"Pushover Phone galaxynote,iphone","device":"galaxynote,iphone","title":"","priority":0,"sound":"pianobar","url":"","url_title":"","html":false,"x":1220,"y":240,"wires":[]},{"id":"b2a75ece.53e29","type":"function","z":"3d2a02fd.1b421e","g":"27cca7b8.5d45c8","name":"Notification Text","func":"// Get Current Cycle Symmary Array\nvar currentCycle = flow.get(\"currentApplianceCycle\");\n\nvar date = new Date();\nmsg.payload = {};\n\n// Function to convert unix epoch to X hours and Y mins\n// and format as 'Xh Xmin'.  Drop the hours if less than 1.\nfunction secondsToHms(d) {\n    d = Number(d);\n    var h = Math.floor(d / 3600);\n    var m = Math.floor(d % 3600 / 60);\n    if (h < 1 ) {\n        return ('0' + m).slice(-2)+\"min\";\n    }\n    else {\n        return ('0' + h).slice(-2) + \"h \" + ('0' + m).slice(-2)+\"min\";\n    }\n}\n\n// Function to convert unix epoch to 00:00\nfunction epochToFormattedTime(d) {\n    d = Date(d * 1000);\n    var h = d.gethours();\n    var m = \"0\" + d.getminutes();\n    return h + ':' + m.substr(-2);\n}\n\n\n// Extract start/stop time based on the unix timestamp\n// First multiply by 1000 so that the argument is in milliseconds, not seconds.\nvar startDateTime = new Date(currentCycle.cycleTimeStart * 1000);\nvar stopDateTime = new Date(currentCycle.cycleTimeStop * 1000);\nvar startDateHours = \"0\" + startDateTime.getHours();\nvar stopDateHours = \"0\" + stopDateTime.getHours();\nvar startDateMinutes = \"0\" + startDateTime.getMinutes();\nvar stopDateMinutes = \"0\" + stopDateTime.getMinutes();\n\n// Format times\n// Will display time in 10:30 format (no seconds)\nvar formattedStartTime = startDateHours.substr(-2) + ':' + startDateMinutes.substr(-2)\nvar formattedStopTime = stopDateHours.substr(-2) + ':' + stopDateMinutes.substr(-2)\n\n// Put together a notification ready for sending to notifying tools\nmsg.topic = flow.get(\"applianceName\") + \" Notification\";\nmsg.payload = \"Your \" + flow.get(\"applianceAction\") + \n              \" is complete.\\nIt started at \" + formattedStartTime +  \n              \", finished at \" + formattedStopTime + \n              \", and used \" + currentCycle.totalCyclePowerFormatted + \n              \", taking \" + secondsToHms(currentCycle.cycleTimeStop - currentCycle.cycleTimeStart) + \n              \" at a cost of \" + currentCycle.totalCycleCostFormatted;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1220,"y":180,"wires":[["f268d769.2c4d78"]]},{"id":"3cd0a6f9.014d7a","type":"inject","z":"3d2a02fd.1b421e","g":"27cca7b8.5d45c8","name":"Test Notification","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1220,"y":120,"wires":[["b2a75ece.53e29"]]},{"id":"cfd2a083.7a6c6","type":"link in","z":"3d2a02fd.1b421e","g":"27cca7b8.5d45c8","name":"Appliance Pushover Notification","links":["189b9430.0574bc"],"x":915,"y":180,"wires":[["b2a75ece.53e29"]]},{"id":"62e5c924.6c8808","type":"comment","z":"3d2a02fd.1b421e","g":"27cca7b8.5d45c8","name":"Pushover App","info":"","x":970,"y":120,"wires":[]},{"id":"2c074a44.483cf6","type":"debug","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Cycle: Cost Array","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugCycleCostArray')\t","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":550,"y":820,"wires":[]},{"id":"fc61477e.80b4e8","type":"debug","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Recent Power Readings","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugRecentPowerArray')\t","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":580,"y":880,"wires":[]},{"id":"8578e8aa.b7a248","type":"debug","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Cycle: Power Array","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugCyclePowerArray')\t","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":560,"y":760,"wires":[]},{"id":"36b756a0.26d46a","type":"comment","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"If Debugging On","info":"","x":140,"y":500,"wires":[]},{"id":"ecba72be.fe705","type":"debug","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Debug: Cost So Far","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugCostSoFar')","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":560,"y":640,"wires":[]},{"id":"bbc410c3.d6efa","type":"change","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Passthrough","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":190,"y":720,"wires":[["ecba72be.fe705","67a1fc62.57c914","573ef317.5fa43c","8578e8aa.b7a248","2c074a44.483cf6","fc61477e.80b4e8","37b6460f.1aebfa"]]},{"id":"67a1fc62.57c914","type":"debug","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Debug: Power So Far","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugPowerSoFar')","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":570,"y":700,"wires":[]},{"id":"573ef317.5fa43c","type":"debug","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Debug: Average Now","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"$flowContext('debugAverage')","targetType":"jsonata","statusVal":"payload","statusType":"auto","x":570,"y":580,"wires":[]},{"id":"37b6460f.1aebfa","type":"debug","z":"3d2a02fd.1b421e","g":"6f58a785.6fd898","name":"Operation","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":530,"y":520,"wires":[]},{"id":"e28f85e7.64c748","type":"file","z":"3d2a02fd.1b421e","g":"dff1b97f.128698","name":"Push to CSV File","filename":"","appendNewline":true,"createDir":true,"overwriteFile":"false","encoding":"none","x":1210,"y":494,"wires":[["1edd0236.05616e"]]},{"id":"f02aa0d9.8e46e","type":"function","z":"3d2a02fd.1b421e","g":"dff1b97f.128698","name":"Data Manipulation for CSV File","func":"// Get Current Cycle Symmary Array\nvar currentCycle = flow.get(\"currentApplianceCycle\");\n\nvar date = new Date();\nmsg.payload = {};\n\n// Function to convert unix epoch to X hours and Y mins\n// and format as 'Xh Xmin'.  Drop the hours if less than 1.\nfunction secondsToHms(d) {\n    d = Number(d);\n    var h = Math.floor(d / 3600);\n    var m = Math.floor(d % 3600 / 60);\n    if (h < 1 ) { \n        return ('0' + m).slice(-2)+\"min\"; \n    }\n    else {\n        return ('0' + h).slice(-2) + \"h \" + ('0' + m).slice(-2)+\"min\"; \n    }\n}\n\n// Function to convert unix epoch to 00:00\nfunction epochToFormattedTime(d) {\n    d = Date(d * 1000);\n    var h = d.gethours();\n    var m = \"0\" + d.getminutes();\n    return h + ':' + m.substr(-2);\n}\n\n\n// Extract start/stop time based on the unix timestamp\n// First multiply by 1000 so that the argument is in milliseconds, not seconds.\nvar startDateTime = new Date(currentCycle.cycleTimeStart * 1000);\nvar stopDateTime = new Date(currentCycle.cycleTimeStop * 1000);\nvar startDateHours = startDateTime.getHours();\nvar stopDateHours = stopDateTime.getHours();\nvar startDateMinutes = \"0\" + startDateTime.getMinutes();\nvar stopDateMinutes = \"0\" + stopDateTime.getMinutes();\n\n// Format times\n// Will display time in 10:30 format (no seconds)\nvar formattedStartTime = startDateHours + ':' + startDateMinutes.substr(-2)\nvar formattedStopTime = stopDateHours + ':' + stopDateMinutes.substr(-2)\n\ncsvPayload={\n    \"date/time\": date,\n    \"Appliance\": flow.get(\"applianceName\"), \n    \"Start Time\": formattedStartTime, \n    \"Finish Time\": formattedStopTime,\n    \"Cycle Time Formatted\": secondsToHms(currentCycle.cycleTimeStop - currentCycle.cycleTimeStart), \n    \"Cycle Time (s)\": currentCycle.cycleTimeStop - currentCycle.cycleTimeStart, \n    \"Cycle Power Use Formatted\": currentCycle.totalCyclePowerFormatted,        \n    \"Cycle Power Use (Wh)\": currentCycle.totalCyclePowerWattHours,        \n    \"Cycle Cost Formatted\": currentCycle.totalCycleCostFormatted,\n    \"Cycle Cost ($)\": currentCycle.totalCycleCostDollars,\n}\n\nmsg.payload=csvPayload;\nvar filename = flow.get(\"applianceName\"); \nmsg.filename = \"/data/\" + filename.replace(/\\s+/g, '') + \"_poweruse.csv\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1170,"y":394,"wires":[["f465bfcd.8cc17"]]},{"id":"f465bfcd.8cc17","type":"json","z":"3d2a02fd.1b421e","g":"dff1b97f.128698","name":"Convert to JSON","property":"payload","action":"","pretty":false,"x":1210,"y":444,"wires":[["e28f85e7.64c748"]]},{"id":"1edd0236.05616e","type":"debug","z":"3d2a02fd.1b421e","g":"dff1b97f.128698","name":"debug","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":1180,"y":554,"wires":[]},{"id":"a5a48c9e.92e3e","type":"link in","z":"3d2a02fd.1b421e","g":"dff1b97f.128698","name":"Appliance History CSV File Creation","links":["189b9430.0574bc"],"x":915,"y":394,"wires":[["f02aa0d9.8e46e"]]},{"id":"bd54160c.9e3648","type":"inject","z":"3d2a02fd.1b421e","g":"dff1b97f.128698","name":"Test Notification","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1220,"y":334,"wires":[["f02aa0d9.8e46e"]]},{"id":"32e3a454.f3817c","type":"comment","z":"3d2a02fd.1b421e","g":"dff1b97f.128698","name":"Data to CSV File","info":"","x":980,"y":334,"wires":[]},{"id":"d0f9e0a.534822","type":"inject","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1800","payloadType":"num","x":132,"y":291,"wires":[["4b9423df.00159c"]]},{"id":"35eb3a08.3b6956","type":"debug","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"","active":false,"tosidebar":true,"console":true,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":362,"y":351,"wires":[]},{"id":"529e42d5.a356fc","type":"inject","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"300","payloadType":"num","x":132,"y":331,"wires":[["4b9423df.00159c"]]},{"id":"306f0caf.042094","type":"inject","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"30","payloadType":"num","x":132,"y":371,"wires":[["4b9423df.00159c"]]},{"id":"58bf6075.437f9","type":"comment","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"Inject Values For Testing Only","info":"Can be used to generate test power values into the input","x":182,"y":211,"wires":[]},{"id":"fc0ec529.c12c48","type":"inject","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"3265","payloadType":"num","x":132,"y":251,"wires":[["4b9423df.00159c"]]},{"id":"4b9423df.00159c","type":"change","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"Passthrough","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":352,"y":291,"wires":[["35eb3a08.3b6956","34891fda.0a794"]]},{"id":"a33b3a29.9fd2c8","type":"inject","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"4","payloadType":"num","x":132,"y":411,"wires":[["4b9423df.00159c"]]},{"id":"5dde5b19.1e0774","type":"cast-to-client","z":"3d2a02fd.1b421e","g":"72d8fb35.7c5474","name":"Notify A Google Home","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"50","x":1220,"y":892,"wires":[[]]},{"id":"7fd876a2.3da9b8","type":"play audio","z":"3d2a02fd.1b421e","g":"72d8fb35.7c5474","name":"","voice":"2","x":1060,"y":752,"wires":[]},{"id":"4797e8cb.dc10e8","type":"function","z":"3d2a02fd.1b421e","g":"72d8fb35.7c5474","name":"Random Notification Selection","func":"// Get a set of statements and push one at random back to\n// the payload.  An example for a washing machine is below\n//\n// var announce = [\n//    \"The washing machine is now complete\",\n//    \"Your washing is complete\",\n//    \"The washing is done\",\n//    \"The washing machine has finished\",\n//    \"Washing has now finished\",\n//    ];\n    \nvar announce = flow.get(\"voiceAnnouncements\");\n    \nvar random = announce[Math.floor(Math.random() * announce.length)];\nmsg.payload = random;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1190,"y":712,"wires":[["7fd876a2.3da9b8","b67b7d1c.b383"]]},{"id":"b67b7d1c.b383","type":"change","z":"3d2a02fd.1b421e","g":"72d8fb35.7c5474","name":"Google Home Notification Details","rules":[{"t":"set","p":"ip","pt":"msg","to":"192.168.2.97","tot":"str"},{"t":"set","p":"language","pt":"msg","to":"En-gb","tot":"str"},{"t":"set","p":"volume","pt":"msg","to":"80","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1180,"y":832,"wires":[["5dde5b19.1e0774"]]},{"id":"a555eaa9.ccb958","type":"link in","z":"3d2a02fd.1b421e","g":"72d8fb35.7c5474","name":"Appliance Audio Announcements","links":["189b9430.0574bc"],"x":915,"y":712,"wires":[["4797e8cb.dc10e8"]]},{"id":"ae89197b.a085c8","type":"inject","z":"3d2a02fd.1b421e","g":"72d8fb35.7c5474","name":"Test Notification","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1240,"y":652,"wires":[["4797e8cb.dc10e8"]]},{"id":"1dcd014.fe6f2ff","type":"comment","z":"3d2a02fd.1b421e","g":"72d8fb35.7c5474","name":"Google Home Announce","info":"","x":1010,"y":652,"wires":[]},{"id":"34891fda.0a794","type":"function","z":"3d2a02fd.1b421e","name":"Main Loop","func":"// -----------------------------------------\n// Notify and store power use and cost for an appliance\n// MAIN LOOP\n// Full loop to calculate power average of appliance and \n// work out when it is operating.  When operational\n// fill arrays with power used, and cost of power calculated.\n// -----------------------------------------\n// 2021-04-12 V1.3 - zorruno\n// - Fixed loop not detecting Finished-> Standby \n// - Moved some flow variables to context\n// - Changed csv filename to reflect appliance\n// -----------------------------------------\n\n// The input message must be the current power in Watts\nvar power = msg.payload;\n\n// Get flow user settings variables into local variables\nvar res   = flow.get(\"resolution\");\nvar tariff = flow.get(\"tariff\");\nvar metricsf = flow.get(\"metricFrequency\");\nvar standby = flow.get(\"standbyPower\");\nvar opPowerMin = flow.get(\"operatingPowerMinium\");\nvar currentCycle = flow.get(\"currentApplianceCycle\");\n\n// Get flow variables into local variables\nvar operation = context.get(\"operation\") || \"Off\";\nvar recentPowerArray = flow.get(\"recentPowerArray\") || [0];\nvar cycleCostArray  = flow.get(\"cycleCostArray\") || [0];\nvar cyclePowerArray  =  flow.get(\"cyclePowerArray\") || [0];\n\n// Get date, seconds and hours\nvar date = new Date();\nvar dateS = date.getTime()/1000;\nvar hour = date.getHours();\n\n// -----------------------------------------\n// Fill power array, and calculate average \n// power.  Do this on every loop.\n// -----------------------------------------\n\n// Function add for reduce array\nfunction add(accumulator, a) {\n    return accumulator + a;\n}\n// Push power into TotalPower array for average\nrecentPowerArray.unshift(power);\n// Remove X element to get total resolution for average calc\nif(recentPowerArray[res] === undefined) {\n    flow.set(\"recentPowerArray\", recentPowerArray);\n}\nelse {\n    recentPowerArray.splice(res, 1);\n    flow.set(\"recentPowerArray\", recentPowerArray);\n}\n// Calculate average power from array\nvar sum = recentPowerArray;\nvar average = (sum.reduce(add)/recentPowerArray.length);   // Average the array\n\n\n// -----------------------------------------\n// Fill cycle power array, and calculate average power.\n// Only do this when cycle is occurring.\n// Note this method doesn't capture EVERY data point - \n// we'll miss the first few as we are calculating an \n// average.  We could capture more, but that is less efficient.\n// -----------------------------------------\nif(operation === \"Operating\"){\n    // Push watthours into cyclePowerArray for cycle\n    var wattHoursNow = power / ( 60 * ( 60 / metricsf ));\n    cyclePowerArray.push(wattHoursNow);\n    flow.set(\"cyclePowerArray\", cyclePowerArray);\n\n    // Calculate the cost of power\n    var price;\n    if( hour >= tariff.start && hour < tariff.end ){\n        price = tariff.costDay; // Apply day tariff\n    }\n    if( hour < tariff.start || hour >= tariff.end ){\n        price = tariff.costNight; // Apply night tariff\n    }\n\n    // Fill cycleCostArray\n    var costPerMinute = power/1000 * price / (60* (60/metricsf));\n    cycleCostArray.push(costPerMinute);\n    flow.set(\"cycleCostArray\", cycleCostArray); // Add to cost array\n}\n\n// -----------------------------------------\n// Appliance is off\n// -----------------------------------------\n//if(average === 0){\nif(average < standby){\n  context.set(\"operation\", \"Off\");\n}\n\n// -----------------------------------------\n// Appliance has gone into Standby from Off\n// -----------------------------------------\nif(average >= standby && operation === \"Off\"){\n  context.set(\"operation\", \"Standby\");\n}\n\n// -----------------------------------------\n// Appliance has gone into Standby from Finished\n// -----------------------------------------\nif(average >= standby && operation === \"Finished\"){\n  context.set(\"operation\", \"Standby\");\n}\n\n// -----------------------------------------\n// Appliance has started its Operating cycle \n// from Standby or Off\n// -----------------------------------------\nif((average > opPowerMin && operation === \"Standby\") || (average > opPowerMin && operation === \"Off\")){\n  context.set(\"operation\", \"Operating\");\n  currentCycle.cycleTimeStart = dateS;\n  cycleCostArray = [0]; // Clear array to start cycle\n  flow.set(\"cycleCostArray\",cycleCostArray);\n  cyclePowerArray = [0]; // Clear array to start cycle\n  flow.set(\"cyclePowerArray\",cyclePowerArray);\n}\n\n// -----------------------------------------\n// Appliance was in Operating cycle, \n// but now has Finished\n// -----------------------------------------\nif(average < opPowerMin && operation === \"Operating\"){\n    context.set(\"operation\", \"Finished\");\n    currentCycle.cycleTimeStop = dateS;\n\n    // Calculate & format total cost of the entire cycle\n    var sumCost = flow.get(\"cycleCostArray\");\n    var costOfPower = sumCost.reduce(add);\n    currentCycle.totalCycleCostDollars = costOfPower;\n    \n    // Format as $ or cents\n    if ( costOfPower < 0.01 ){\n        costOfPower = '<1c';\n    } else if ( costOfPower < 1){\n        costOfPower = costOfPower * 100;\n        costOfPower = Math.round(costOfPower);\n        costOfPower = costOfPower.toString() + 'c';\n    } else {\n        costOfPower = costOfPower.toFixed(2);\n        costOfPower = '$' + costOfPower.toString();\n    }\n    currentCycle.totalCycleCostFormatted = costOfPower ;\n\n    // Calculate & format total power use of the entire cycle\n    var sumPower = flow.get(\"cyclePowerArray\");\n    var sumOfPower = sumPower.reduce(add);\n    currentCycle.totalCyclePowerWattHours = sumOfPower;\n\n   // Format as wH or kWh\n    if ( sumOfPower >= 1000 ){\n        sumOfPower = sumOfPower / 1000;\n        sumOfPower = sumOfPower.toFixed(1); // 1 decimal place\n        sumOfPower = sumOfPower.toString() + 'kWh';\n    } else if ( costOfPower < 1){\n       costOfPower = '<1Wh';\n    } else {\n        sumOfPower = sumOfPower.toFixed(1); // 1 decimal place\n        sumOfPower = sumOfPower.toString() + 'Wh';\n    }\n\n    currentCycle.totalCyclePowerFormatted = sumOfPower ;\n\n    msg.payload = context.get(\"operation\") ;\n    return [msg,msg] \n}\n\n\n// -----------------------------------------\n// Output debug stuff on each loop\n// -----------------------------------------\nif(flow.get(\"debugFlow\") === true){\n\n    flow.set(\"debugAverage\",average) ;\n\n    // Calculate total cost of the entire cycle so far\n    var costSoFar = flow.get(\"cycleCostArray\").reduce(add);\n    flow.set(\"debugCostSoFar\",costSoFar) ;\n\n    // Calculate total power use of the entire cycle so far\n    var powerSoFar = flow.get(\"cyclePowerArray\").reduce(add);\n    flow.set(\"debugPowerSoFar\",powerSoFar) ;\n\n    flow.set(\"debugCyclePowerArray\",cyclePowerArray) ;\n    flow.set(\"debugCycleCostArray\",cycleCostArray) ;\n    flow.set(\"debugRecentPowerArray\",recentPowerArray) ;\n\n    msg.payload = context.get(\"operation\") ;\n    return [null,msg]\n}\n\n\n \n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":670,"y":300,"wires":[["189b9430.0574bc"],["bbc410c3.d6efa"]]},{"id":"5f5749c0.0d0578","type":"inject","z":"3d2a02fd.1b421e","g":"3b5c156a.9c65aa","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":270,"y":420,"wires":[["4b9423df.00159c"]]},{"id":"e28b763a.77bd98","type":"mqtt-broker","name":"Panda","broker":"192.168.X.X","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]



Last edited by ZorrUno
Tue, 13 Apr 2021 04:08 UTC [diff]


--
CategoryHomeAutomation
CategoryNodered
CategoryMQTT
CategoryTasmota
CategoryElectricVehicles