Ecobee integration into Node-RED...?

Hi all.

I'm still learning Node-RED, so I apologize if what I'm about to ask is obvious.

Short Version
I am trying to integrate my Ecobee3 Lite thermostat into my new Node-RED setup (so I can pull current settings, temps, etc, plus maybe sometime be able to remotely turn on/off the thermostat, change temps, etc). However, I'm running into difficulties. It looks like the Ecobee API has more security/authentication than Fort Knox, and that might be what's hanging me up. I spent a few hours last night trying to get it working, but never was 100% successful.

Longer Version
I first tried to contact the Ecobee via cURL (on my RPi). It took a lot of searching online, but I finally was able to get it to work for a little while. It was very clunky due to the negotiation/authentication process that is used to access the thermostat. Still, I was encouraged at this point since.

Turning to doing this in Node-RED, I searched the web, but I didn't find anything substantial, which surprised me. The closest I got was this:

https://github.com/mifbody/EcobeeNodeRed

However:

(1) It seems like the instructions are missing some stuff (e.g., it jumps from "First Run" to "Refresh Tokens" -- nothing in there about when to run Step 2, what to expect from it, etc), and...

(2) It is years-old, so perhaps my troubles are because the Ecobee API interface has changed...?

I was able to:

(1) Node-RED : Add the above flow to Node-RED.
(2) Ecobee website : Add myself as a developer (to the Ecobee site).
(3) Ecobee website : Go to "Developer", create a sample application on the Ecobee website and get an API key.
(4) Node-RED : Add the API key to the flow.
(5) Node-RED : Run "Step 1" and get the four character authorization code.
(6) Ecobee website : Go to "My Apps" and create a new application using the four digit code.
(7) Node-RED : Run "Step 2" and get the access token., since the above Github page doesn't really say what to do with the token that was generated, I kind of fumbled around and tried different things at this point. (I don't recall the exact steps, as I'm not in front of my home computer right now.)

I was stuck at the refresh -- no matter what I did, the token refreshing stuff didn't work, so I figured I had something misconfigured in the flow.

As a side note, there are 3 other Ecobee related topics in this forum, but none seem to really address the nuts-and-bolts of how to get the Ecobee working with Node-RED.

Any help on how to proceed would be greatly appreciated. Thank you for your time.

-Jon

That flow is a bit out of date and may need a bit of work to get running with the current incarnation of the ecobee API. I had something running loosely based on it awhile back but got sidetracked by other projects. I may take another stab at it and see what I can pull off. If I get it working I'll share.

1 Like

@JayDickson - Got it, I understand. Yes, please -- if you updated it, I'd really appreciate seeing a working flow. Thank you. :slight_smile:

And if I make any progress on the existing one, I'll let you (and everyone) know.

I still use that flow. I modified it slightly through the years. So I use the credentials node to store the Client ID (when you export the credentials node it doesn't export your credentials and instead clears them.

Once the flow gets the access and refresh tokens I store those in the flow context:

flow.set("Ecobee.access_token",msg.payload.access_token);
flow.set("Ecobee.refresh_token",msg.payload.refresh_token);
// node.warn("Tokens refreshed successfully.");
msg.payload={
    "_id":"tokens",
    "_rev":flow.get("Ecobee._rev"),
    "access_token":flow.get("Ecobee.access_token"),
    "refresh_token":flow.get("Ecobee.refresh_token")
};
return msg;

and then retrieve them and pass them on to subsequent data requests:

if ((flow.get("Ecobee.refresh_token") !== null) && (flow.get("Ecobee.refresh_token") !== undefined)) {
    var newMsg={
        "url":"https://api.ecobee.com/1/thermostat?format=json&body=%7B%22selection%22%3A%7B%22selectionType%22%3A%22registered%22%2C%22selectionMatch%22%3A%22%22%2C%22includeEvents%22%3A%22false%22%2C%22includeEquipmentStatus%22%3A%22true%22%2C%22includeRuntime%22%3A%22true%22%2C%22includeSensors%22%3A%22true%22%2C%22includeWeather%22%3A%22true%22%7D%7D",
        "method": "GET",
        headers: {
            "Content-Type":"application/json;charset=UTF-8",
            "Authorization":"Bearer "+flow.get("Ecobee.access_token"),
        }
    }
} else {
    var newMsg=null; // don't do anything if authorization is not done
}
return newMsg;

Would you mind sharing an export of your flows?

You will need the credentials node.
Some of the nodes jump to other pages for MQTT, etc. so that might be confusing on import, but...
What you want is the result of the RBE node / debug msg node.

[{"id":"96a2c3b4.61f02","type":"inject","z":"50466fda.5d00f","name":"Request Data","topic":"","payload":"","payloadType":"str","repeat":"120","crontab":"","once":false,"onceDelay":"","x":120,"y":340,"wires":[["70a2249f.495bec"]]},{"id":"e31ca9c9.2a72d8","type":"function","z":"50466fda.5d00f","name":"Get ecobeePin Token","func":"// Ecobee API Key stored in variable:\n// context.global.Ecobee.ClientID=msg.payload;\n\nvar newMsg ={\n    \"url\":\"https://api.ecobee.com/authorize?response_type=ecobeePin&client_id=\"+flow.get(\"Ecobee.ClientID\")+\"&scope=smartWrite\",\n    \"method\": \"GET\",\n    headers: {\n    }\n};\nreturn newMsg;","outputs":1,"noerr":0,"x":420,"y":100,"wires":[["bc583db0.45f21"]]},{"id":"7215d04f.35c9b","type":"debug","z":"50466fda.5d00f","name":"","active":true,"console":"false","complete":"payload","x":970,"y":100,"wires":[]},{"id":"bc583db0.45f21","type":"http request","z":"50466fda.5d00f","name":"","method":"use","ret":"obj","url":"","tls":"74d52d37.cac784","x":610,"y":100,"wires":[["babf59ff.4ca048"]]},{"id":"7d405852.04f168","type":"function","z":"50466fda.5d00f","name":"Get Thermostats","func":"if ((flow.get(\"Ecobee.refresh_token\") !== null) && (flow.get(\"Ecobee.refresh_token\") !== undefined)) {\n    var newMsg={\n        \"url\":\"https://api.ecobee.com/1/thermostat?format=json&body=%7B%22selection%22%3A%7B%22selectionType%22%3A%22registered%22%2C%22selectionMatch%22%3A%22%22%2C%22includeEvents%22%3A%22false%22%2C%22includeEquipmentStatus%22%3A%22true%22%2C%22includeRuntime%22%3A%22true%22%2C%22includeSensors%22%3A%22true%22%2C%22includeWeather%22%3A%22true%22%7D%7D\",\n        \"method\": \"GET\",\n        headers: {\n            \"Content-Type\":\"application/json;charset=UTF-8\",\n            \"Authorization\":\"Bearer \"+flow.get(\"Ecobee.access_token\"),\n        }\n    }\n} else {\n    var newMsg=null; // don't do anything if authorization is not done\n}\nreturn newMsg;","outputs":1,"noerr":0,"x":450,"y":340,"wires":[["10830111.cc29df"]]},{"id":"10830111.cc29df","type":"http request","z":"50466fda.5d00f","name":"Get Thermostat Request","method":"use","ret":"obj","url":"","tls":"74d52d37.cac784","x":650,"y":340,"wires":[["b543311d.f82ef"]]},{"id":"1c63e8e6.23b8c7","type":"inject","z":"50466fda.5d00f","name":"Refresh token every 19 minutes","topic":"","payload":"","payloadType":"str","repeat":"1140","crontab":"","once":false,"onceDelay":"","x":180,"y":260,"wires":[["222cc17.3f3123e"]]},{"id":"222cc17.3f3123e","type":"function","z":"50466fda.5d00f","name":"Refresh Token","func":"if ((flow.get(\"Ecobee.refresh_token\") !== null) && (flow.get(\"Ecobee.refresh_token\") !== undefined)) {\n    var newMsg ={\n        \"url\":\"https://api.ecobee.com/token?grant_type=refresh_token&code=\"+flow.get(\"Ecobee.refresh_token\")+\"&client_id=\"+flow.get(\"Ecobee.ClientID\")+\"\",\n        \"method\": \"POST\",\n        headers: { }\n    }\n} else {\n    var newMsg=null; // don't do anything if authorization is not done\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"x":400,"y":260,"wires":[["8f53ba9e.62ee08"]]},{"id":"8f53ba9e.62ee08","type":"http request","z":"50466fda.5d00f","name":"","method":"use","ret":"obj","url":"","tls":"74d52d37.cac784","x":570,"y":260,"wires":[["89c2c2df.d2e5"]]},{"id":"9dc9e01e.3f92c","type":"function","z":"50466fda.5d00f","name":"Define Access and Refresh Token vars","func":"flow.set(\"Ecobee.access_token\",msg.payload.access_token);\nflow.set(\"Ecobee.refresh_token\",msg.payload.refresh_token);\n// node.warn(\"Tokens refreshed successfully.\");\nmsg.payload={\n    \"_id\":\"tokens\",\n    \"_rev\":flow.get(\"Ecobee._rev\"),\n    \"access_token\":flow.get(\"Ecobee.access_token\"),\n    \"refresh_token\":flow.get(\"Ecobee.refresh_token\")\n};\nreturn msg;","outputs":1,"noerr":0,"x":880,"y":220,"wires":[[]]},{"id":"c0a8cda1.94c0b","type":"debug","z":"50466fda.5d00f","name":"","active":true,"console":"false","complete":"payload","x":970,"y":260,"wires":[]},{"id":"dc855d88.96a13","type":"comment","z":"50466fda.5d00f","name":"Step #3 - Refresh Token Flow a few times for good measure.","info":"This flow will refresh your token every time the input button is pressed.\n\nGlobal variables set -- EcobeeAccessToken and EcobeeRefreshToken","x":240,"y":220,"wires":[]},{"id":"cefa9323.f506","type":"comment","z":"50466fda.5d00f","name":"Step #4 - Get Thermostat Data - 1 minute polling works but Ecobee says only poll every 3 minutes or more","info":"Get thermostat data\nEcobee reference to 3 minute interval:\nhttps://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostat-summary.shtml\n","x":380,"y":300,"wires":[]},{"id":"2aa0e254.0421ee","type":"inject","z":"50466fda.5d00f","name":"Step 1","topic":"","payload":"","payloadType":"none","repeat":"","crontab":"","once":false,"x":90,"y":100,"wires":[["2e8762d3.16a4de"]]},{"id":"2a204d64.b90b32","type":"comment","z":"50466fda.5d00f","name":"Step #1 - Requesting the 4-digit ecobeePin - Log in to Ecobee.com and go to your \"My Apps\" before clicking the Inject node.","info":"The ecobeePin must be entered into your \"My Apps\" in Ecobee.com within 10 minutes.","x":430,"y":20,"wires":[]},{"id":"5251da69.a11c34","type":"function","z":"50466fda.5d00f","name":"Get Authorization Token","func":"var newMsg ={\n    \"url\":\"https://api.ecobee.com/token?grant_type=ecobeePin&code=\"+flow.get(\"Ecobee.access_token\")+\"&client_id=\"+flow.get(\"Ecobee.ClientID\"),\n    \"method\": \"POST\",\n    headers: {\n    }\n}\nreturn newMsg;","outputs":1,"noerr":0,"x":270,"y":180,"wires":[["2ffd018.f9c37fe"]]},{"id":"2ffd018.f9c37fe","type":"http request","z":"50466fda.5d00f","name":"","method":"use","ret":"obj","url":"","tls":"74d52d37.cac784","x":490,"y":180,"wires":[["4f85e128.2eaf3"]]},{"id":"bc26abf2.7b2948","type":"comment","z":"50466fda.5d00f","name":"Step #2 - Requesting the first access token - you must run Step 2 within 10 minutes of running Step 1.","info":"Now that the app has been added to your \"My Apps\" - we need to request our first access token.","x":370,"y":140,"wires":[]},{"id":"babf59ff.4ca048","type":"function","z":"50466fda.5d00f","name":"Format Ecobee Pin","func":"flow.set(\"Ecobee.access_token\", msg.payload.code);\nvar msg = {\"payload\":\"Ecobee Pin: \"+msg.payload.ecobeePin};\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":100,"wires":[["7215d04f.35c9b"]]},{"id":"4f85e128.2eaf3","type":"function","z":"50466fda.5d00f","name":"Define Access and Refresh Tokens","func":"flow.set(\"Ecobee.refresh_token\", msg.payload.refresh_token);\nflow.set(\"Ecobee.access_token\", msg.payload.access_token);\nmsg.payload=flow.get(\"Ecobee.refresh_token\");\nreturn msg;","outputs":1,"noerr":0,"x":720,"y":180,"wires":[["67d81ca8.768654"]]},{"id":"67d81ca8.768654","type":"debug","z":"50466fda.5d00f","name":"","active":true,"console":"false","complete":"false","x":970,"y":180,"wires":[]},{"id":"89c2c2df.d2e5","type":"switch","z":"50466fda.5d00f","name":"Refresh Token","property":"payload.refresh_token","propertyType":"msg","rules":[{"t":"null"},{"t":"nnull"}],"checkall":"true","outputs":2,"x":740,"y":260,"wires":[["c0a8cda1.94c0b"],["9dc9e01e.3f92c","c0a8cda1.94c0b"]]},{"id":"70a2249f.495bec","type":"delay","z":"50466fda.5d00f","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"10","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":280,"y":340,"wires":[["7d405852.04f168"]]},{"id":"8d207c6b.d448b","type":"comment","z":"50466fda.5d00f","name":"You will have 2 minutes to enter the 4-digit pin into Ecobee.com and authorize the application. (Ecobee disgards the 4-pin code after 10 minutes!)","info":"","x":500,"y":60,"wires":[]},{"id":"897bdcda.4ecd5","type":"inject","z":"50466fda.5d00f","name":"Step 2","topic":"","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"x":90,"y":180,"wires":[["5251da69.a11c34"]]},{"id":"9cd45335.9c179","type":"link out","z":"50466fda.5d00f","name":"Ecobee side of Ecobee-to-Wonderware","links":["3a764f5e.e3f53"],"x":895,"y":300,"wires":[]},{"id":"b543311d.f82ef","type":"function","z":"50466fda.5d00f","name":"parse to [msg] array","func":"var timestampfromthermostat;//timestamp\nvar thermostat = [];\nvar thermostatname = \"\";\nvar msgarr = [];\nvar remotesensor = [];\nvar remotesensorname;\nvar capability = [];\nvar capabilitytype;\nvar capabilityvalue;\n\nfor (thermostat of msg.payload.thermostatList) {//array so use \"of\" not \"in\"\n    timestampfromthermostat = new Date(thermostat.utcTime+\" UTC\").getTime();\n    thermostatname = thermostat.name.replace(/\\s+/g, '').toLowerCase();//remove spaces, if any, and set to lowercase.\n    //node.warn(\"completed replace with thermostatname = \"+ thermostatname);\n\n    if (thermostat.equipmentStatus === \"\") {\n        thermostat.equipmentStatus = \"off\";\n    }\n    msgarr.push({\n        payload:(thermostat.equipmentStatus),\n        topic: thermostatname + \"/thermostat/status\",\n        timestamp : timestampfromthermostat\n    });    \n\n    msgarr.push({\n        payload:(thermostat.runtime.actualTemperature/10),\n        topic: thermostatname + \"/average/temperature\",\n        timestamp : timestampfromthermostat\n    });\n    \n    msgarr.push({\n        payload:(thermostat.runtime.desiredCool/10),\n        topic: thermostatname + \"/average/temperature/setpoint/cool\",\n        timestamp : timestampfromthermostat\n    });\n    msgarr.push({\n        payload:(thermostat.runtime.desiredHeat/10),\n        topic: thermostatname + \"/average/temperature/setpoint/heat\",\n        timestamp : timestampfromthermostat\n    });\n    msgarr.push({\n        payload:(thermostat.runtime.actualHumidity),\n        topic: thermostatname + \"/average/humidity\",\n        timestamp : timestampfromthermostat\n    });\n    \n    // for weather stuff we only want the current weather (array element zero)\n    msgarr.push({\n        payload:(thermostat.weather.forecasts[0].temperature/10),\n        topic: thermostatname + \"/outside/temperature\",\n        timestamp : timestampfromthermostat\n    });\n    msgarr.push({\n        payload:(thermostat.weather.forecasts[0].pressure/10),\n        topic: thermostatname + \"/outside/pressure\",\n        timestamp : timestampfromthermostat\n    });\n    msgarr.push({\n        payload:(thermostat.weather.forecasts[0].relativeHumidity),\n        topic: thermostatname + \"/outside/humidity\",\n        timestamp : timestampfromthermostat\n    });\n    msgarr.push({\n        payload:(thermostat.weather.forecasts[0].windSpeed/1000),\n        topic: thermostatname + \"/outside/wind/speed\",\n        timestamp : timestampfromthermostat\n    });\n    msgarr.push({\n        payload:(thermostat.weather.forecasts[0].windBearing/10),\n        topic: thermostatname + \"/outside/wind/bearing\",\n        timestamp : timestampfromthermostat\n    });\n\n    for (remotesensor of thermostat.remoteSensors) {//array so use \"of\" not \"in\"\n        remotesensorname = remotesensor.name.replace(/\\s+/g, '').toLowerCase();//remove spaces, if any, and set to lowercase.\n        if (remotesensorname==thermostatname) {\n            if (remotesensorname==\"home\") { //my ecobee main unit is in the dining room\n                remotesensorname=\"diningroom\";\n            } else {\n                remotesensorname = \"main\";\n            }\n        }\n        //node.warn(\"remotesensorname = \" + remotesensorname);\n        for (capability of remotesensor.capability) { //array so use \"of\" not \"in\"\n            capabilitytype = capability.type.replace(/\\s+/g, '').toLowerCase();//remove spaces, if any, and set to lowercase.\n            //node.warn(\"capabilitytype = \" + capabilitytype);\n            \n            capabilityvalue = capability.value;\n            if (capabilitytype==\"temperature\") {\n                capabilityvalue = capabilityvalue/10;//temperatures need decimal shifted place left\n            }\n\n            msgarr.push({\n                payload:capabilityvalue,\n                topic: thermostatname + \"/\" + remotesensorname + \"/\" + capabilitytype,\n                timestamp : timestampfromthermostat\n            });\n        }\n    }\n}\n\nreturn [msgarr];\n","outputs":1,"noerr":0,"x":860,"y":340,"wires":[["951eb9a4.4d2208"]]},{"id":"2e8762d3.16a4de","type":"credentials","z":"50466fda.5d00f","name":"","props":[{"value":"Ecobee.ClientID","type":"flow"}],"x":230,"y":100,"wires":[["e31ca9c9.2a72d8"]]},{"id":"e75b1948.c5f178","type":"debug","z":"50466fda.5d00f","name":"","active":true,"console":"false","complete":"true","x":990,"y":300,"wires":[]},{"id":"951eb9a4.4d2208","type":"rbe","z":"50466fda.5d00f","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":830,"y":300,"wires":[["9cd45335.9c179","e75b1948.c5f178"]]},{"id":"74d52d37.cac784","type":"tls-config","z":"","name":"TLS Config Generic","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","servername":"","verifyservercert":true}]