Skip to content

Instantly share code, notes, and snippets.

@andysc
Last active March 15, 2019 21:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andysc/212932ef36a46a62b3362524502f01fa to your computer and use it in GitHub Desktop.
Save andysc/212932ef36a46a62b3362524502f01fa to your computer and use it in GitHub Desktop.
Tide model calculator for @DrLucyRogers' SeeTide IoT device

This is the back-end for a device that displays how high the tide is at a specific location in real-time.

It is the code referred to in this blog post by @DrLucyRogers: SeeTide.

The blog post explains how you find your coastal location and plug it into the HTTP input node.

Tidal data is scraped from the UK Hydrographic Easy Tide web site once a day, and the flow creates a number of sine-wave models of the day's tide, peaking and troughing at the correct high and low tide times.

The model is then run every 5 minutes to compute how far in or out the tide currently is, and formats a string of RGB colours to send over MQTT to a neopixel display device that displays the green/blue mix to show land and sea.

The flow also uses cos (the first derivative of sin) to work out whether the tide is coming in or going out, and adds that onto the front of the colour string as a flag, which the device uses to light an LED indicating "in" or "out" direction.

Please note these calculations are simplified approximations of the tidal flows for the given location, and must under no circumstances be used for navigation or for any purpose that could in any way endanger life.

[{"id":"6ec7b365.ffb2f4","type":"function","z":"fc0d5395.fce918","name":"add direction","func":"msg.payload = msg.direction + msg.payload; \nreturn msg;","outputs":1,"noerr":0,"x":1050,"y":420,"wires":[["e99bc2ff.97b23","7fe8b5a2.5f2f0c"]]},{"id":"dabb1e36.1c9a88","type":"inject","z":"fc0d5395.fce918","name":"daily at midnight","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"01 00 * * *","once":false,"onceDelay":0.1,"x":210,"y":160,"wires":[["2859837e.e639a4"]]},{"id":"2859837e.e639a4","type":"http request","z":"fc0d5395.fce918","name":"Get Tide Data","method":"GET","ret":"txt","url":"http://www.ukho.gov.uk/easytide/EasyTide/ShowPrediction.aspx?PortID=0068A&PredictionLength=2","tls":"","x":420,"y":160,"wires":[["172ffaad.1b739d"]]},{"id":"6790a535.f844f4","type":"html","z":"fc0d5395.fce918","name":"parse HWLW labels","property":"payload","outproperty":"payload","tag":".HWLWTableHWLWCell","ret":"text","as":"single","x":690,"y":200,"wires":[["c8a3ffec.f2a848"]]},{"id":"f4e6ba0f.6fbf08","type":"html","z":"fc0d5395.fce918","name":"parse tide times","property":"payload","outproperty":"payload","tag":".HWLWTableCell","ret":"text","as":"single","x":960,"y":240,"wires":[["8d24c477.0542b8"]]},{"id":"28fa46df.150bc2","type":"change","z":"fc0d5395.fce918","name":"save models","rules":[{"t":"set","p":"models","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1410,"y":240,"wires":[[]]},{"id":"3bdc762b.27bbda","type":"inject","z":"fc0d5395.fce918","name":"5 mins","topic":"","payload":"","payloadType":"date","repeat":"300","crontab":"","once":false,"onceDelay":0.1,"x":180,"y":420,"wires":[["c2b6ac46.cb641"]]},{"id":"c522d3ca.1c6ba","type":"function","z":"fc0d5395.fce918","name":"calculate LED and direction","func":"// number of LEDs we have\nmsg.LEDs = 8;\n\n// msg.payload contains an angle in degrees of where we \n// currently are on the sine wave\nvar sine = Math.sin(msg.payload * Math.PI / 180); // -1 to +1\nsine = (sine + 1)/2; // 0 to 1\n// how many pixels are covered by water (blue) at this point?\nvar pixel = Math.round(sine * msg.LEDs); // 0 .. LEDs\n\n// see if tide is going out or coming in\n// by looking at cos of the angle - if it's negative,\n// sine is on way down (i.e. going out), \n// otherwise if cos is +ve, sine is on way up (coming in)\nvar cos = Math.cos(msg.payload * Math.PI / 180);\n\nif (cos < 0) \n msg.direction = 0; // outwards\nelse\n msg.direction = 1; // inwards\n\nmsg.payload = pixel;\nreturn msg;","outputs":1,"noerr":0,"x":580,"y":420,"wires":[["71fd553a.e3e95c","d0d3a9f3.12ad48","b2772b4c.cd4758"]]},{"id":"71fd553a.e3e95c","type":"function","z":"fc0d5395.fce918","name":"compile colours","func":"// msg.payload is how many blue pixels to show\n\nvar LED = [];\n\n// paint them all green\nfor (var i = 0; i<msg.LEDs; i++)\n LED.push('000400');\n\n// paint the right number blue\nfor (var i = 0; i<msg.payload; i++)\n LED[i] = '000004';\n \n// create the string of colours\nmsg.payload = LED.join('');\n//msg.payload = LED.slice().reverse().join('');\n// (in case you need it the other way round!)\n\nreturn msg;","outputs":1,"noerr":0,"x":860,"y":420,"wires":[["6ec7b365.ffb2f4"]]},{"id":"d0d3a9f3.12ad48","type":"debug","z":"fc0d5395.fce918","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":850,"y":480,"wires":[]},{"id":"b2772b4c.cd4758","type":"debug","z":"fc0d5395.fce918","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"direction","x":860,"y":360,"wires":[]},{"id":"e99bc2ff.97b23","type":"rbe","z":"fc0d5395.fce918","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":1210,"y":420,"wires":[["c67e7ad7.3e79a8","d2f74f20.1c843"]]},{"id":"7fe8b5a2.5f2f0c","type":"debug","z":"fc0d5395.fce918","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1190,"y":480,"wires":[]},{"id":"172ffaad.1b739d","type":"change","z":"fc0d5395.fce918","name":"save HTML","rules":[{"t":"set","p":"HTML","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":160,"wires":[["6790a535.f844f4"]]},{"id":"c8a3ffec.f2a848","type":"change","z":"fc0d5395.fce918","name":"save / restore data","rules":[{"t":"set","p":"labels","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"HTML","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":910,"y":200,"wires":[["f4e6ba0f.6fbf08"]]},{"id":"3d51a548.9c5432","type":"debug","z":"fc0d5395.fce918","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1410,"y":280,"wires":[]},{"id":"c67e7ad7.3e79a8","type":"change","z":"fc0d5395.fce918","name":"store last sent","rules":[{"t":"set","p":"last_sent","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1420,"y":380,"wires":[[]]},{"id":"f3899839.d4fd","type":"change","z":"fc0d5395.fce918","name":"get last sent","rules":[{"t":"set","p":"payload","pt":"msg","to":"last_sent","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":970,"y":600,"wires":[["fcfa90bc.a0e178"]]},{"id":"fcfa90bc.a0e178","type":"function","z":"fc0d5395.fce918","name":"fix msg","func":"msg = {payload: msg.payload};\nreturn msg;","outputs":1,"noerr":0,"x":1140,"y":600,"wires":[["5f79229.1f6dedc","d2f74f20.1c843"]]},{"id":"5f79229.1f6dedc","type":"debug","z":"fc0d5395.fce918","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1330,"y":600,"wires":[]},{"id":"b6b62985.9ef9e8","type":"comment","z":"fc0d5395.fce918","name":"© Crown Copyright and/or database rights. ","info":"Reproduced by permission of the Controller of Her Majesty’s Stationery Office and the UK Hydrographic Office (www.GOV.uk/UKHO).","x":410,"y":240,"wires":[]},{"id":"c3fbe664.ad6318","type":"comment","z":"fc0d5395.fce918","name":"Replace the port ID ...","info":"Find the Port ID of your choice at http://www.ukho.gov.uk/easytide/EasyTide/SelectPort.aspx\nReplace \"PortID=0048\" in the http link below with the PortID of your port.\n\n","x":420,"y":200,"wires":[]},{"id":"d2f74f20.1c843","type":"ibmiot out","z":"fc0d5395.fce918","authentication":"boundService","apiKey":"","outputType":"cmd","deviceId":"yyy","deviceType":"xxx","eventCommandType":"command","format":"text","data":"{}","qos":0,"name":"send to device","service":"registered","x":1520,"y":480,"wires":[]},{"id":"bbfd829c.8f2f2","type":"ibmiot in","z":"fc0d5395.fce918","authentication":"boundService","apiKey":"","inputType":"evt","logicalInterface":"","ruleId":"","deviceId":"yyy","applicationId":"","deviceType":"xxx","eventType":"status","commandType":"","format":"text","name":"birth certificate","service":"registered","allDevices":"","allApplications":"","allDeviceTypes":false,"allLogicalInterfaces":"","allEvents":false,"allCommands":"","allFormats":"","qos":0,"x":800,"y":600,"wires":[["f3899839.d4fd"]]},{"id":"377d36b7.57b712","type":"comment","z":"fc0d5395.fce918","name":"update Device Type and Device ID","info":"","x":840,"y":660,"wires":[]},{"id":"8d24c477.0542b8","type":"function","z":"fc0d5395.fce918","name":"build array of models","func":"// are we on daylight saving time? (true/false)\nvar DST = false;\n\n\n// need to know the start time\n// the wavelength (or half the wavelength)\n// and whether it's low-high or high-low (phase)\n\n// copy out the times but not the wave heights (heights are e.g. \"4.3 m\"))\nvar times = [];\nfor (var i=0; i<msg.payload.length; i++) {\n if (!msg.payload[i].match(/m$/)) \n times.push(msg.payload[i]);\n}\n \nnode.warn(times);\n\nvar models = [];\n\n// just look at the first 4 entries: that must cover more than 24 hours\n// then get one more, so there's always a \"next\" one to look at, even if we never use it\n// so we don't run off the end of the array of models\nfor (var i=0; i<5;i++) {\n var time1 = parse_time(times[i]);\n var time2 = parse_time(times[i+1]);\n \n if (time2 < time1) {\n // we've wrapped to tomorrow\n time2 += 24;\n }\n \n // get the \"wavelength\" from low to high (about 6 hours)\n var cycle = Math.abs(time2 - time1);\n \n \n // see if the start time is less than the previous start time\n // (only if we've got at least one model already)\n // this is so we have an \"end stop\" that we never reach\n if (i>0 && time1 < models[i-1].start) {\n // we've wrapped to tomorrow\n time1 += 24;\n }\n //node.warn(time1+\" and \"+time2);\n \n \n var phase = 90; // high to low\n if (msg.labels[i] == \"LW\") {\n // low to high\n phase = -90;\n }\n\n if (DST)\n time1 += 1;\n \n models[i] = {start: time1, cycle: cycle, phase: phase};\n}\n\nmsg.payload = models;\nreturn msg;\n\n\n// convert a hh:mm time string into decimal time hh.mmm\nfunction parse_time(time) {\n var components = time.match(/(.*):(.*)/);\n var hours = Number(components[1]);\n var mins = Number(components[2]);\n return hours + mins / 60;\n}","outputs":1,"noerr":0,"x":1180,"y":240,"wires":[["28fa46df.150bc2","3d51a548.9c5432"]]},{"id":"c2b6ac46.cb641","type":"function","z":"fc0d5395.fce918","name":"compute tide","func":"// get current time\nvar now = new Date();\nvar time = now.getHours() + now.getMinutes()/60;\n\n// if we're on daylight saving, add that to shift it one hour to the right\ntime += now.getTimezoneOffset()/60;\n\nvar models = flow.get(\"models\");\nvar current = flow.get(\"current\");\n\n// find the correct current model to use\n// if there either isn't a current one, or the next one available could be in play\n// make the next one available the current one\n// there will always be one more than we need in a day, so there will always be\n// a models[0], and the last one will have a start time > 24, so it will never become\n// valid, so we'll stick with the last one of the previous day until the new day's\n// models come along\n//node.warn(\"model 0 start is \"+models[0].start);\n\nwhile (!current || models[0].start <= time) {\n // if we've just reset, mark this model as special\n if (!current)\n models[0].first = true;\n \n current = models.shift();\n flow.set(\"current\", current);\n node.warn(\"new current...\");\n node.warn(current);\n}\n\n// see if we're still running on last night's final model\n// that can't happen if we've just been reset, so should then use the first model of\n// the day, and extrapolate backwards\nif (!current.first && time < current.start) {\n time += 24;\n}\n\n// use cycle time to calculate deg/hour, 180 deg in cycle time (about 6h)\nvar deg_hour = 180 / current.cycle; // about 30\n\n// find out how far we are through the sine cycle\n// +90 if this is high tide, so it's at the top of the sine wave\n// -90 if low tide, so it's bottom of the sine wave\nvar angle = (time - current.start) * deg_hour + current.phase;\n\nmsg.payload = angle;\nreturn msg;","outputs":1,"noerr":0,"x":350,"y":420,"wires":[["c522d3ca.1c6ba"]]},{"id":"b8366923.3ab128","type":"comment","z":"fc0d5395.fce918","name":"update Device Type and Device ID","info":"","x":1580,"y":540,"wires":[]}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment