As application for the X-Ring RGB Strip I came up with the following idea: querying the next event in my Google Calendar tagged with #wecker and then visualize the remaining time. I have illustrated the structure of all components in the following figure:
Google Calendar
The Wemos D1 mini queries the next event by accessing my Google Calendar. This is done by executing a Google Apps Script. Google Apps Script is very powerful because you can use Google Services with ease – everything is well documented, there are so many examples and you can even debug your scripts. Directly accessing the google calendar with read-access as specific user requires authentication (currently with an OAUTH mechanism). I was not able to find a way to register my microcontroller at this OAUTH server easily. So I decided to use a Google Apps Script as proxy – it requires authentication when executing it at the first time. Then it has read-access to your google calendar. This is super simple and flexible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | /** * This script is licenced under CC BY-NC-SA 4.0 (https://creativecommons.org/licenses/by-nc-sa/4.0/). * Author: No3x * Fetch events from google calendar. * * Useful links used while development * @ref https://developers.google.com/apps-script/reference/calendar/calendar-event#getstarttime * @ref https://developers.google.com/apps-script/reference/calendar/calendar * @ref https://developers.google.com/apps-script/advanced/calendar * * This script is deployed as Web-App. * The script contains also quint tests - they can be accessed by appending ?test when calling the script. * */ TIME_DELTA_HOURS = 72; SEARCHTAG = '#wecker'; function doGet(e) { if( e != null && e.queryString != null && e.queryString.indexOf("test") !== -1 ) { Logger.log("App called with params: " + e.queryString ); return qunit(e); } else { return app(); } } function app() { const now = getNow(); Logger.log("Now: " + now.toISOString() ) const until = new Date(now.getTime() + (TIME_DELTA_HOURS * 60 * 60 * 1000)); const allCalendars = CalendarApp.getAllCalendars(); var events = Array(); for each (var calendar in allCalendars) { var allEventsOfCalendar = calendar.getEvents(now, until,{search: SEARCHTAG}); for each (var event in allEventsOfCalendar) { events.push(event); } Logger.log("Feteched events from calendar '" + calendar.getName() +"':"); printEvents( allEventsOfCalendar ); } const response = buildResponse(events); return ContentService.createTextOutput(JSON.stringify(response)) .setMimeType(ContentService.MimeType.JSON); } function buildResponse( events ) { if( 0 > events.length ) { Logger.log("Found no events!"); } else { var events = events.sort(sortByStartTime); Logger.log("Sorted events by startTime ASC:"); printEvents( events ); } return { "status": 0 < events.length ? 200 : 204, "nextEvent": 0 < events.length ? createDateAsUTC(events[0].getStartTime()).toISOString() : "", "timeDiff": 0 < events.length ? timeDiff( createDateAsUTC(events[0].getStartTime()), getNow()) : "" }; } function getNow() { return createDateAsUTC(new Date()); } function createDateAsUTC(date) { return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds())); } function convertDateToUTC(date) { return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); } function timeDiff( eventDate, compareDate ) { return Math.round( Math.max( ( eventDate.getTime() - compareDate.getTime() ) / (60*60*1000), 0) ); } function printEvents( events ) { for each(var event in events) { Logger.log( event.getTitle() + ": " + event.getStartTime() ); } } function sortByStartTime(a,b) { if (a.getStartTime().getTime()===b.getStartTime().getTime()){ return 0; } else if(a.getStartTime().getTime()>b.getStartTime().getTime()){ return 1; } else if(a.getStartTime().getTime()<b.getStartTime().getTime()){ return -1; } }; function testFunctions() { testingSortByStartTime(); testingBuildResponse(); testingTimeDiff(); } function qunit(e) { QUnit.urlParams( e.parameter ); QUnit.config({ title: "QUnit for Google Apps Script - Test suite" // Sets the title of the test page. }); QUnit.helpers( this ); QUnit.load( testFunctions ); return QUnit.getHtml(); } function testingSortByStartTime() { var a = { getStartTime: function() { return createDateAsUTC(new Date()); } }; var b = { getStartTime: function() { return createDateAsUTC(new Date(new Date().getTime() + 2000)); } }; QUnit.test( "sortByStartTime testing", function() { ok( sortByStartTime(a,a) == 0, "Test for equality" ); ok( sortByStartTime(a,b) == -1, "Test for a newer than b" ); ok( sortByStartTime(b,a) == 1, "Test for a newer than b" ); }); } function testingBuildResponse() { var a = { getStartTime: function() { return convertDateToUTC(new Date("2017-06-19T07:45:00.000Z")); }, getTitle: function() { return "Event A"; } }; var b = { getStartTime: function() { return convertDateToUTC(new Date(new Date("2017-06-19T07:45:00.000Z").getTime() + 2000)); }, getTitle: function() { return "Event B"; } }; QUnit.test( "BuildResponse testing", function() { ok( buildResponse( Array() ) != null, "Test empty array returns response" ); equal( buildResponse( Array() ).status,"204", "Test empty array returns response status 204" ); equal( buildResponse( Array(a,b) ).status, "200","Test array returns response status 200" ); equal( buildResponse( Array(a,b) ).nextEvent, "2017-06-19T07:45:00.000Z", "Test proper array returns a (2017-06-19T07:45:00.000Z)" ); }); } function testingTimeDiff() { const a = createDateAsUTC(new Date("2017-06-19T07:45:00.000Z")); const b = createDateAsUTC(new Date("2017-06-19T10:00:00.000Z")); QUnit.test( "TimeDiff testing", function() { equal( timeDiff( a,a ), 0, "Test difference of same date is 0" ); equal( timeDiff( a,b ), 0, "Test difference of dates in hours is 0 (actually negative but 0)" ); equal( timeDiff( b,a ), 2, "Test difference of dates in hours is 2 (swapped parameter returns same)" ); }); } |
The script depends on the following libraries:
Moment – https://script.google.com/macros/library/versions/d/15hgNOjKHUG4UtyZl9clqBbl23sDvWMS8pfDJOyIapZk5RBqwL3i-rlCo
QUnit – https://script.google.com/macros/library/versions/d/13agWuzcPH32W4JJvOqOEYqeNHGihS63P2V-a-Vxz-c9WPIzZYBvIhs3m
If you want to run the script you have to add them to your script.
Then you can publish the script as Webapp.
The script contains a little testbench to do some unit-tests. You can execute them by adding ?test to the exec URL of the webapp.
You can use your favourite HTTP Client to get the result from your webapp. Please notice that there is a 302 HTTP redirect when you call your webapp. This is visualized in the following figure.
For testing purposes on your computer you can use curl for example. -L to enable the location header flag to follow redirects
1 | curl -L https://script.google.com/macros/s/<id>/exec |
Then you are redirected to the final destination which is the output of the Google Apps Script.
Responses are JSON encoded:
No event currently:
1 2 3 4 5 | { "status": 204, "nextEvent": "", "timeDiff": "" } |
Next event:
1 2 3 4 5 | { "status": 200, "nextEvent": "2017-07-20T10:15:00.000Z", "timeDiff": 16 } |
If you want to query the Google Apps Script from Arduino make sure to use a HTTP Client that follows redirects.
E.g.: https://github.com/cvonk/esp8266-WiFiClientSecureRedirect
MQTT
I like event-driven architectures especially in the IoT-Context. You can build your own eventbus for your home. The MQTT architecture is flexible, adaptive and robust – it perfectly fits my needs. You may ask yourself why MQTT is used at all in this application, because I could just take this event and visualize it on the X-Ring. Of course – but what if I want to add another device using this event information? I would have to refactor my X-Ring Google Calendar Application. I just took this effort already to make it even less coupled and hereby flexible.
Visualization
The app queries for events every 10 minutes. When it does an update it animates a color wheel. Then all LED black. Then it lightens up N LED representing the hours until the next event. I have created two openprocessing script visualizing the colorwheel animation:
Result
The is a video of the result. It is not true-color.
I’m looking forward to develop more apps using the X-Ring module. I can recommend it for use with any arduino and microcontroller in general.
Great little project. I acquired an X-ring module a few days ago and like everyone else, I couldn’t find documentation. Then I came across your blog. Many thanks for you hard work.
But then I came across this implementation. Amazing. Although I love the pretty lights that come from RGB LEDS, I’ve never really found much to do with them. This is an original idea and beautifully implemented. Welll done!
Hi Avi, thanks for your feedback. Make sure to read https://no3x.de/projects/x-ring/x-ring-missing-documentation and please post your own project when finished.
It looks like that site is down and just gives a WordPress error link :/
What’s not working for you? I just did some maintenance some minutes ago. Is everything fine now?