Building an API interface for airconditioning automation

#1

EDIT: The below question can be ignored as it turns out, seems I'm the dumby afterall and this just got a lot easier :smiley: . I'll leave this open if that's cool as I'm bound to have future issues/questions.


Ok so just to preface, I'm brand new to Node-Red (and js if I'm honest) so please bear with me as I'll definitely being doing a lot of this wrong or inefficiently :slight_smile: The fun is in the learning though.

Basically what I'm trying to do here is interface with my ducted airconditioning API so that I can automate some things in HomeAssistant. The company hasn't officially released the API and it doesn't sound like they're going to be much help (and their app is rubbish and unreliable), hence the tinkering.

I do have some of this working already, in that I can can turn the aircon on and off in HomeAssistant now (built in Node-Red).

What I need to do is:

Use a static access token (I already have this) to get a Bearer token from their API so that I can issue commands. This token expires after 72 hours, which is where most of my issue is coming from. I'm already able to grab a Bearer token using Node-Red, however it's pretty dumb at the moment as their system simply dishes out a new token to you whenever you ask, however none of the new tokens work until the initial token has expired so I need a way to determine that.

So what I'm trying to do is essentially grab a token, get the current time and add 72 hours to it and then write this to a file. Then next time I try and grab a token, check this file and if the current time is less than the time in the file, use the existing token, otherwise grab a new one.

My issue is that when I save the date to the file, it's saved as a string, When I then read this file later, it's still a string so I can't work out how to either convert that, or somehow just find a way to compare it to the current time.

Now there's likely a much better way of going about this so I'm all ears, honestly :slight_smile:

I've attached a modified flow below. I've stripped out all the API stuff and am just injecting a file at the start with what the API would be returning so you can see how it's working (or not).

[{"id":"fe10a1ed.44f0b","type":"tab","label":"Flow 8","disabled":false,"info":""},{"id":"eed6c365.c39fd","type":"file","z":"fe10a1ed.44f0b","name":"Token to file","filename":"/data/Bearer.txt","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":626.5,"y":160,"wires":[[]]},{"id":"b38d3d46.e321e","type":"debug","z":"fe10a1ed.44f0b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":758,"y":398,"wires":[]},{"id":"f1b4b359.ed134","type":"function","z":"fe10a1ed.44f0b","name":"Split Bearer Token","func":"var access = msg.payload.access_token;\nmsg.payload = access;\nreturn msg;\n","outputs":1,"noerr":0,"x":309,"y":164,"wires":[["b38d3d46.e321e"]]},{"id":"208d42c6.494eae","type":"function","z":"fe10a1ed.44f0b","name":"Split Expiry + add to current time","func":"var t = new Date();\nvar expiry = msg.payload.expires_in;\nt = new Date(t.getTime() + (1000 * expiry))\nmsg.payload = t\nreturn msg;","outputs":1,"noerr":0,"x":352,"y":269,"wires":[["b38d3d46.e321e"]]},{"id":"72ef7d48.3ad914","type":"file","z":"fe10a1ed.44f0b","name":"Expiry to file","filename":"/data/Expiry.txt","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":650.5,"y":271,"wires":[[]]},{"id":"e586db6f.e414a8","type":"inject","z":"fe10a1ed.44f0b","name":"","topic":"","payload":"{\"access_token\":\"gU8S6TqKLnUHGSojIs4fzclitepZkdCBBm-hD159i6tY_TfZKOL2_lTxoWrBo1Fk49_Y7S7ftb17ASkwRPhBm2Uz5VKmiaQmN-hFhI-LLObydy6FCK5WqmsZnLMcjY_i4AKoPZyTE-M1CaRZbCVrSLEg4jfKydNKx76dMGjB9gLLX1HfjF9AHDabntg2zOMz-FRbvBlR_O2MOFYOgTHikkpFG04-quTh6wm7_0d98XISYwWOM4xyOYa1euIsmz367lOWR04dowaq47xNq0RsQzWbtfwUVulyKW-fYJAUedM2zW0IhXDQAQZlG9fu3_8EihpgQOfm-faIA_9kwScBnk4QSQWcs0AinY0GRKHmkialJ0sQcWHRW2fKUbwejxQMrEff-TLyK2df-M6jg-iSFYANrOHsTAO6gsnw48YJvxcGGZKBaSzIwOOLLuUV3Ow4t-PXafRm03Q9o0FavWpZX_BWGshLgdzYJlQys0RTp888_xKFNLnaFLR8ZYD-r8iNIup_852mvlvUvGOkbNi1kg\",\"token_type\":\"bearer\",\"expires_in\":259199}","payloadType":"json","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":97,"y":227,"wires":[["208d42c6.494eae","f1b4b359.ed134"]]}]

Thanks a lot for your help, Node-Red is pretty damn awesome...

Summary

This text will be hidden

0 Likes

#2

Since the fun is in the learning (I agree) I have two suggestions for you

  1. save the time in milliseconds (see https://www.w3schools.com/jsref/jsref_gettime.asp)
  2. read about context variables in functions (see https://nodered.org/docs/writing-functions#storing-data) and this A guide to understanding 'Persistent Context'

Using miliseconds will made the comparrisons easier and the two links should explain how to save and get the data.

If you have questions after reading them please ask.

"Tell me and I forget, teach me and I may remember, involve me and I learn." - Benjamin Franklin

0 Likes

#3

Thanks, I'll definitely read those shortly :slight_smile:

In the meantime, I came back to delete this as it seems their end isn't so "dumb" afterall and it's perfectly happy to accept a new Bearer token every request. I've no idea why that didn't work when I was first playing around, but it seems to be fine now so that's saved me heaps of time :slight_smile:

What I might do, if the etiquette is ok, is change the title of this post and leave it open for future questions, as I'm absolutely going to have them!

0 Likes

#4

@zenofmud

Thanks for the links, good reading and I'm already making use of the persistent context. I've hit a slight roadblock but it's more to do with how NR works I think, and I don't have enough knowledge about the whole thing yet to know how to proceed.

Long story short, I'm now storing the bearer token in a flow context and reusing that for each call until it expires. My flow goes like this:

  1. Trigger (from HomeAssistant)
  2. Switch node to see if my context variable is empty or not
  3. If not empty, use the context token and turn on the aircon
  4. If empty, request a new token and split out just the token itself from the response and store it in the context for next time
  5. Start the 72 hour timer (https://www.npmjs.com/package/node-red-contrib-mytimeout) and delete the context once that runs out

My issue is that I need to return a message at the end of the "Split Bearer Token" function so that the flow continues and "Turn On Aircon" is triggered. However, in doing this, it triggers that timer to start again every time as well, even if Step 3 is triggered above. Basically it seems a message is sent along the entire path every single time even if part of that path isn't actually triggered. Hopefully that makes sense :smiley:

So I suppose I either need to somehow block the flow of messages at the Switch node (to stop it from even looking at the "request token" path if it doesn't need to), or find another way to send a continue message to Turn On Aircon. Or perhaps I should be thinking about it differently and decoupling the timer from this flow and somehow calling that and triggering it another way?

0 Likes

#5

instead of a timer, you could use a flow variable with a timestamp set. Then periodically (every minute) check the timestamp. If 72h had passed, perform your operation.

This has 2 advantages ...

  1. you're only using built in nodes.
  2. If you store flow context with persistence, a reboot won't be a problem.
0 Likes

#6

What would be the best way to do this?

0 Likes

#7

Use an inject node with repeat set to 1 minute.

0 Likes

#8

Oh wire the inject to a function block that does a bit simple math...

var timestamp = flow.get("timestamp");
var diffMS = Date.now() - timestamp;
const maxMS = 72*60*60*1000;//72h in ms
if(diffMS >= maxMS){
  return msg;//trigger next node
}
return null;//don't trigger next node

^ untested code - but you should get the idea. ^

0 Likes

#9

Haha yep thanks, I was just putting that together (this'll make it much quicker though, I have to google all my js :D)

Thanks :slight_smile:

0 Likes

#10

Actually while I have you, is there a dashboard setting to show data currently stored in contexts? I'm using a small flow to retrieve them but it'd be easier if I could just view everything that's stored on the dash somewhere. Thanks!

0 Likes

#11

This works great, thanks. Definitely a nice way to do it, as it doesn't matter how much I deploy etc for testing, it doesn't affect anything as the timestamp is static obviously and it doesn't really matter how often the inject rechecks it.

I put a bit of code in "Split Bearer Token" to see if it should ignore the flow variable or reset it, and that seems to be working great. Thanks again, I'm sure I'll be back haha

var check = flow.get("time");
if (check===undefined){
   flow.set("time", new Date())
}
0 Likes

#12

of course

2019-04-18_08-19-24

0 Likes

#13

Ah brilliant, I totally missed that little dropdown in the top right :slight_smile:

0 Likes