-
Notifications
You must be signed in to change notification settings - Fork 0
/
WeatherEmailFinalWifi.ino
945 lines (780 loc) · 34.6 KB
/
WeatherEmailFinalWifi.ino
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
/* Copyright (C) 2018 Samuel Trassare (https://github.com/tiogaplanet)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// This example sketch turns MiP into a weather person and provides the
// weather data to a web client.
#include <mip_esp8266.h>
#include <JsonListener.h>
#include <time.h>
#include <FS.h>
#include <ESP8266WebServer.h>
#include "OpenWeatherMapCurrent.h"
#include <WiFiClientSecure.h>
// The following four variables must be configured by the user for the sketch to work. ///////////////
// Enter the SSID for your wifi network.
const char* ssid = "..............";
// Enter your wifi password.
const char* password = "..............";
// Provide your OpenWeatherMap API key. See
// https://docs.thingpulse.com/how-tos/openweathermap-key/
// for more information.
const String OPEN_WEATHER_MAP_APP_ID = "your_openweathermap_api_key";
// The Base64 encoded version of your Gmail login credentials.
const char* credentials = "bWlwQGdtYWlsLmNvbTpBIHZlcnkgbG9uZyBwYXNzd29yZCwgaW5kZWVkLg==";
/////////////////////////////////////////////////////////////////////////////////////////////////////
// The following two variables can be configured to the user's preference. //////////////////////////
// Provide the OpenWeatherMap ID for your city. For example, the value for Naples, Italy
// is 3172394. Charleston, South Carolina is 4574324. Newcastle upon Tyne, GB is 2641673.
const String OPEN_WEATHER_MAP_LOCATION_ID = "3172394";
// Set any hostname you desire.
const char* hostname = "MiP-0x02";
/////////////////////////////////////////////////////////////////////////////////////////////////////
// No other changes need to be made.
// The weather data set is big and doesn't always come down in one request.
#define HTTP_RETRIES 3
// Define some on and off times to flash the chest LED.
#define ON_TIME 1000
#define OFF_TIME 700
// MiP variables.
MiP mip; // We need a single MiP object
bool connectResult; // Test whether a connection to MiP was established.
// For the chest LED.
uint8_t red, green, blue = 0;
bool chestValuesWritten = false;
// Don't update the eyes if they're already solid.
bool lastUpdatedToSolid = false;
bool extinguished = false;
// Interval in which to randomly write to the eyes, indicating rain.
const long eyesRainInterval = 500;
// A longer interval for drizzle.
const long eyesDrizzleInterval = 1000;
// An even longer interval for mist.
const long eyesMistInterval = 3000;
// Store the last time MiP's eyes were written to for animation.
unsigned long previousEyesMillis = 0;
// Track MiP's position. MiP roams while upright and reports the weather when on the kickstand.
MiPPosition lastPosition = (MiPPosition) - 1;
// These variables are for the weather station features.
// Initiate the client for the OpenWeatherMap API.
OpenWeatherMapCurrent client;
const String OPEN_WEATHER_MAP_LANGUAGE = "en";
const boolean IS_METRIC = false;
// Read the location code from SPIFFS
String locationLine;
// Store the last time OpenWeatherMap was queried.
unsigned long previousMillis = 0;
// Retrieve weather data every 15 minutes (900000 milliseconds).
const long interval = 900000;
// A place to store the data retrieved from OpenWeatherMap.
OpenWeatherMapCurrentData data;
// Set the web server port number to 80.
ESP8266WebServer server(80);
// Variable in which to store the HTTP request.
String header;
// Function prototypes for HTTP handlers.
void handleRoot();
void handleNotFound();
// For form validation when searching for a new city.
boolean searchError = false;
// The rest of these variables are for email notifications.
// The Gmail server.
const char* host = "mail.google.com";
// The Gmail feed URL.
const char* url = "/mail/feed/atom";
// The port to connect to the email server.
const int httpsPort = 443;
// Keep track of new and older, unread emails.
int latestUnread;
int lastUnread;
// Store the last time Gmail was queried.
unsigned long previousMailMillis = 0;
// Check Gmail every minute (60000 milliseconds).
const long mailInterval = 60000;
void setup() {
// Establish the WiFi connection and connect to MiP.
connectResult = mip.begin(ssid, password, hostname);
if (!connectResult) {
Serial1.println(F("Failed connecting to MiP."));
return;
}
// Quiet, MiP!
mip.writeVolume(MIP_VOLUME_OFF);
// We'll need a random number generator for a few things such as animating the eyes in conditions of rain
// and choosing random sounds.
randomSeed(analogRead(A0));
// Call the 'handleRoot' function when a client requests the URI "/".
server.on("/", handleRoot);
// When a client requests an unknown URI (i.e. something other than "/"), call function "handleNotFound."
server.onNotFound(handleNotFound);
// Start the web server.
server.begin();
// We'll be able to turn MiP's eyes and chest on and off with a single clap while on the kickstand and reporting the weather.
mip.enableClapEvents();
mip.writeClapDelay(1000);
SPIFFS.begin();
// Read the default weather location from SPIFFS.
locationLine = readLocation();
if (locationLine.length() == 0) {
Serial1.println(F("Using default location."));
saveLocation(OPEN_WEATHER_MAP_LOCATION_ID);
}
locationLine = readLocation();
updateWeatherById(locationLine);
// Check email for the first time.
latestUnread = lastUnread = getUnread();
previousMailMillis = millis();
}
void loop() {
// Handle OTA updates.
ArduinoOTA.handle();
// This block of code pulls data from OpenWeatherMap.org every 15 minutes regardless of MiP's position.
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
updateWeatherById(locationLine);
chestValuesWritten = false;
previousMillis = currentMillis;
}
// Check the mail too.
unsigned long currentMailMillis = millis();
if (currentMailMillis - previousMailMillis >= mailInterval) {
latestUnread = getUnread();
previousMailMillis = currentMailMillis;
}
MiPPosition currentPosition = mip.readPosition();
if (currentPosition != lastPosition) {
if (mip.isOnBack()) {
Serial1.println(F("Position: On Back"));
// Make a random sound and flash the chest LED.
randomFall();
}
if (mip.isFaceDown()) {
Serial1.println(F("Position: Face Down"));
// Make a random sound and flash the chest LED.
randomFall();
}
if (mip.isUpright()) {
Serial1.println(F("Position: Upright"));
// Set MiP into a custom roaming mode.
mip.disableClapEvents();
customRoamMode();
}
if (mip.isPickedUp()) {
Serial1.println(F("Position: Picked Up"));
// Stop the wheels from spinning freely and make a noise.
mip.stop();
mip.playSound(MIP_SOUND_MIP_WHOAH, MIP_VOLUME_1);
}
if (mip.isHandStanding()) {
Serial1.println(F("Position: Hand Stand"));
// No special handling for this position.
}
if (mip.isFaceDownOnTray()) {
Serial1.println(F("Position: Face Down on Tray"));
// Make a random sound and flash the chest LED.
randomFall();
}
if (mip.isOnBackWithKickstand()) {
Serial1.println(F("Position: On Back With Kickstand"));
// Make MiP ready to report the weather. Don't forget to plug MiP in at this point.
mip.stop();
mip.disableRadarMode();
mip.enableClapEvents();
mip.writeChestLED(red, green, blue);
}
lastPosition = currentPosition;
}
// MiP is on his kickstand - start reporting the weather.
if (mip.isOnBackWithKickstand()) {
// Listen for claps first. If a clap is detected, toggle the boolean extinguished variable.
while (mip.availableClapEvents() > 0) {
uint8_t clapCount = mip.readClapEvent();
Serial1.println("Detected " + String(clapCount) + " clap(s).");
if (clapCount > 0 && extinguished == false) {
Serial1.println(F("Clap detected, switching off."));
mip.writeHeadLEDs(MIP_HEAD_LED_OFF, MIP_HEAD_LED_OFF, MIP_HEAD_LED_OFF, MIP_HEAD_LED_OFF);
mip.writeChestLED(0, 0, 0);
extinguished = true;
lastUpdatedToSolid = false;
chestValuesWritten = false;
} else if (clapCount > 0 && extinguished == true) {
Serial1.println(F("Clap detected, switching on."));
extinguished = false;
}
}
// Unless MiP's chest and eyes have been extinguished with a clap, show weather and mail status.
if (!extinguished) {
if (data.description.indexOf("rain") >= 0) { // Animate the eyes to indicate rain.
// Randomly write values to MiP's eyes to indicate rain. Writes are done once every eyesRainInterval.
unsigned long eyesMillis = millis();
if (eyesMillis - previousEyesMillis >= eyesRainInterval) {
mip.unverifiedWriteHeadLEDs((MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2));
previousEyesMillis = eyesMillis;
}
lastUpdatedToSolid = false;
} else if (data.description.indexOf("drizzle") >= 0) {
// Randomly write values to MiP's eyes to indicate rain. Writes are done once every eyesRainInterval.
unsigned long eyesMillis = millis();
if (eyesMillis - previousEyesMillis >= eyesDrizzleInterval) {
mip.unverifiedWriteHeadLEDs((MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2));
previousEyesMillis = eyesMillis;
}
lastUpdatedToSolid = false;
} else if (data.description.indexOf("mist") >= 0) {
// Randomly write values to MiP's eyes to indicate rain. Writes are done once every eyesRainInterval.
unsigned long eyesMillis = millis();
if (eyesMillis - previousEyesMillis >= eyesMistInterval) {
mip.unverifiedWriteHeadLEDs((MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2), (MiPHeadLED)random(0, 2));
previousEyesMillis = eyesMillis;
}
lastUpdatedToSolid = false;
} else if (!lastUpdatedToSolid) { // If there is no rain, turn on the eyes without animation.
mip.writeHeadLEDs(MIP_HEAD_LED_ON, MIP_HEAD_LED_ON, MIP_HEAD_LED_ON, MIP_HEAD_LED_ON);
lastUpdatedToSolid = true;
}
// Update the chest LED.
if (!chestValuesWritten) {
chestValuesWritten = updateChestLEDForWeather();
}
// If latestUnread and lastUnread differ, the chest LED needs to be updated for mail.
if (latestUnread != lastUnread) {
updateChestLEDForMail();
}
}
}
// Listen for HTTP requests from clients.
server.handleClient();
}
// Play a randomly selected sound when MiP falls.
void randomFall() {
MiPSoundIndex fallSounds[6] = { MIP_SOUND_MIP_HURT, MIP_SOUND_MIP_GRUNT_1, MIP_SOUND_MIP_GRUNT_2, MIP_SOUND_MIP_GRUNT_3, MIP_SOUND_MIP_OUCH_1, MIP_SOUND_MIP_OUCH_2 };
mip.playSound(fallSounds[rand() % 6], MIP_VOLUME_1);
mip.writeChestLED(0xFF, 0x00, 0x00, 990, 980);
}
// A variation on my first sketch for MiP. MiP will roam and after encountering a defined number of near obstacles within a defined
// interval, MiP shows frustration. This function has its own loop so the main loop will be blocked until customRoamMode() returns.
void customRoamMode() {
// Set the chest LED to a violet color.
mip.writeChestLED(0xB6, 0x00, 0xFF);
mip.enableRadarMode();
// While MiP is roaming, he may get frustrated by too many obstacles. This is the interval in which MiP calms down.
const long cooldownInterval = 60000;
// Each obstruction within the cool down period increases MiP's frustration.
uint8_t frustrationLevel = 0;
// The number of obstructions MiP can tolerate within the cool down period before expressing frustration.
const uint8_t frustrationThreshold = 4;
MiPRadar lastRadar = MIP_RADAR_INVALID;
// As soon as MiP changes position, return execution to the main loop.
while (mip.isUpright()) {
// Start driving.
mip.continuousDrive(16, 0);
MiPRadar currentRadar = mip.readRadar();
unsigned long currentMillis = millis();
if (currentRadar != MIP_RADAR_INVALID && lastRadar != currentRadar) {
switch (currentRadar)
{
case MIP_RADAR_NONE:
// No obstruction, continue on happily.
break;
case MIP_RADAR_10CM_30CM:
// Distant obstruction detected, take evasive maneuvers.
randomEvasion();
mip.continuousDrive(16, 0);
break;
case MIP_RADAR_0CM_10CM:
// Near obstruction detected, reset the cool down clock and increase frustration level.
previousMillis = currentMillis;
frustrationLevel++;
if (frustrationLevel != frustrationThreshold) {
randomEvasion();
mip.continuousDrive(16, 0);
}
break;
}
lastRadar = currentRadar;
}
// This is it. If MiP has exceeded its frustration threshold, have a good ol' tantrum and go back to normal.
if (frustrationLevel >= frustrationThreshold) {
frustration();
previousMillis = currentMillis;
frustrationLevel = 0;
} else if (currentMillis - previousMillis >= cooldownInterval) { // Otherwise, if MiP has avoided near obstacles for
previousMillis = currentMillis; // the last minute, reset the frustration level.
frustrationLevel = 0;
}
// Listen for HTTP requests from clients while in roaming mode.
server.handleClient();
}
}
// This is the actual expression of frustration, called from customRoamMode().
void frustration() {
// Set the chest LED to red.
mip.writeChestLED(0xFF, 0x00, 0x00);
// Make an angry noise.
mip.beginSoundList();
mip.addEntryToSoundList(MIP_SOUND_VOLUME_4, 0);
mip.addEntryToSoundList(MIP_SOUND_MOOD_ANGRY, 1000);
mip.addEntryToSoundList(MIP_SOUND_VOLUME_OFF, 0);
mip.playSoundList(0);
// Flash the eyes angrily.
MiPHeadLEDs headLEDs;
headLEDs.led2 = headLEDs.led3 = MIP_HEAD_LED_BLINK_FAST;
headLEDs.led1 = headLEDs.led4 = MIP_HEAD_LED_BLINK_SLOW;
mip.writeHeadLEDs(headLEDs);
// Do three spins, each in a random direction for a random number of degrees at max speed, of course.
for (uint8_t i = 0; i < 3; i++) {
(random(0, 2)) ? mip.turnLeft(random(0, 1276), 24) : mip.turnRight(random(0, 1276), 24);
delay(1500);
}
// restore the eyes to normal.
headLEDs.led1 = headLEDs.led2 = headLEDs.led3 = headLEDs.led4 = MIP_HEAD_LED_ON;
mip.writeHeadLEDs(headLEDs);
// Make an "exhaustion" noise.
mip.beginSoundList();
mip.addEntryToSoundList(MIP_SOUND_VOLUME_4, 0);
mip.addEntryToSoundList(MIP_SOUND_ACTION_OUT_OF_BREATH, 0);
mip.addEntryToSoundList(MIP_SOUND_VOLUME_OFF, 0);
mip.playSoundList(0);
// Set the chest LED back to violet and get on with life.
mip.writeChestLED(0xB6, 0x00, 0xFF);
}
// Randomly turn right or left to avoid obstructions while in the custom roaming mode.
void randomEvasion() {
(random(0, 2) == 0) ? mip.turnLeft(90, 12) : mip.turnRight(90, 12);
delay(500);
}
// The rest of these functions support the weather station features.
// Save the location from the search field to SPIFFS.
void saveLocation(const String location) {
SPIFFS.remove("/location.txt");
File saveFile = SPIFFS.open("/location.txt", "w");
saveFile.println(location);
saveFile.close();
}
// Read the saved location from SPIFFS.
String readLocation() {
File saveFile = SPIFFS.open("/location.txt", "r");
String savedLocation = saveFile.readStringUntil('\n');
saveFile.close();
savedLocation.trim();
return savedLocation;
}
// Read the weather from OpenWeatherMap by city ID.
void updateWeatherById(const String cityId) {
// If the update fails we will restore the city name.
String holder = data.cityName;
data.cityName = "";
client.setLanguage(OPEN_WEATHER_MAP_LANGUAGE);
client.setMetric(IS_METRIC);
uint8_t i;
for (i = 0; i < HTTP_RETRIES ; i++) {
if (data.cityName.length() == 0) {
client.updateCurrentById(&data, OPEN_WEATHER_MAP_APP_ID, cityId);
} else {
Serial1.println("Found data for " + data.cityName + ", " + data.country + ".");
break;
}
}
if (i == HTTP_RETRIES && data.cityName.length() == 0) {
Serial1.println(F("Failed updating weather."));
data.cityName = holder;
}
}
// Read the weather from OpenWeatherMap by city name.
void updateWeatherByName(const String cityName) {
// If the update fails we will restore the city name.
String holder = data.cityName;
data.cityName = "";
client.setLanguage(OPEN_WEATHER_MAP_LANGUAGE);
client.setMetric(IS_METRIC);
uint8_t i;
for (i = 0; i < HTTP_RETRIES ; i++) {
if (data.cityName.length() == 0) {
client.updateCurrent(&data, OPEN_WEATHER_MAP_APP_ID, cityName);
} else {
Serial1.println("Found data for " + data.cityName + ".");
break;
}
}
if (i == HTTP_RETRIES && data.cityName.length() == 0) {
Serial1.println(F("Failed updating weather."));
data.cityName = holder;
}
}
// Set MiP's chest to indicate the current temperature using the algorithm, not the values, from:
// https://sjackm.wordpress.com/2012/03/26/visualizing-temperature-as-color-using-an-rgb-led-a-lm35-sensor-and-arduino/
bool updateChestLEDForWeather() {
if (data.temp) {
// Range of blue.
if (data.temp <= 32) {
blue = 255;
}
else if (data.temp > 32 && data.temp <= 72) {
blue = map(data.temp, 32, 72, 255, 0);
}
else if (data.temp > 72) {
blue = 0;
}
// Range of green.
if (data.temp <= 32) {
green = 0;
}
else if (data.temp > 32 && data.temp <= 60) {
green = map(data.temp, 32, 60, 0, 255);
}
else if (data.temp > 60 && data.temp <= 80) {
green = 255;
}
else if (data.temp > 80) {
green = map(data.temp, 80, 110, 255, 0);
}
// Range of red.
if (data.temp < 72) {
red = 0;
}
else if (data.temp >= 72 && data.temp <= 80) {
red = map(data.temp, 72, 80, 1, 255);
}
else if (data.temp > 80) {
red = 255;
}
MiPChestLED chestLED;
mip.readChestLED(chestLED);
// Write it to the chest LED.
if (chestLED.onTime == ON_TIME) {
mip.writeChestLED(red, green, blue, ON_TIME, OFF_TIME);
} else {
mip.writeChestLED(red, green, blue);
}
} else {
return false;
}
return true;
}
// Flash the chest LED if new mail arrives. This function preserves the color of the chest LED.
void updateChestLEDForMail() {
MiPChestLED chestLED;
mip.readChestLED(chestLED);
if (latestUnread > lastUnread) {
Serial1.printf("You have %d new email%s and %d older, unread email%s.\n",
latestUnread - lastUnread, latestUnread - lastUnread == 1 ? "" : "s", lastUnread, lastUnread == 1 ? "" : "s");
// If the chest is already flashing, don't write to it again.
if (chestLED.offTime != OFF_TIME) {
// flash the chest to indicate new email.
mip.writeChestLED(chestLED.red, chestLED.green, chestLED.blue, ON_TIME, OFF_TIME);
}
} else if (latestUnread < lastUnread) {
// User either read or deleted unread emails, so stop indicating new email.
Serial1.printf("You have %d unread message%s.\r\n", latestUnread, latestUnread == 1 ? "" : "s");
// Set the chest LED to solid green.
mip.writeChestLED(chestLED.red, chestLED.green, chestLED.blue);
} else if (latestUnread == -1) {
Serial1.println(F("I could not access your email. I'll try again in a minute."));
}
lastUnread = latestUnread;
}
// Handle calls from the web client to the web server.
void handleRoot() {
// In case the search fails.
String lastCity = data.cityName;
if (server.args() > 0 ) {
for ( uint8_t i = 0; i < server.args(); i++ ) {
if (server.argName(i) == "city") {
// Validate the data.
data.cityName = "";
updateWeatherById(server.arg(i));
if (data.cityName.length() == 0) {
Serial1.println(F("Failed update by ID, trying by name."));
updateWeatherByName(server.arg(i));
if (data.cityName.length() == 0) {
searchError = true;
// The search failed so put the old city back for the next page display.
data.cityName = lastCity;
} else {
// The update by name was successful but do the save using the city ID.
Serial1.println("Found data for " + data.cityName + ".");
saveLocation(data.cityId);
Serial1.println("Saved " + readLocation());
locationLine = data.cityId;
searchError = false;
if (!extinguished) {
updateChestLEDForWeather();
}
}
} else {
// The update by ID was successful so save the user's search string for the next time.
saveLocation(server.arg(i));
locationLine = server.arg(i);
searchError = false;
if (!extinguished) {
updateChestLEDForWeather();
}
}
}
}
}
// Send HTTP status 200 (Ok) and the page to the client.
server.send(200, "text/html", completePage());
}
// Handle "not found" calls from the web client to the web server.
void handleNotFound() {
// Send HTTP status 404 (Not Found) when there's no handler for the URI in the request.
char errorMessage[150];
sprintf(errorMessage,
"404: Not found.\n\nPlease use \"http://%s\" or \"http://%s.local\" to see the weather.\n Thank you.\n -MiP",
WiFi.localIP().toString().c_str(), hostname);
server.send(404, "text/plain", errorMessage);
}
// Beyond here lies the HTML served to the client from handleRoot().
String completePage() {
String htmlOutput = "<!DOCTYPE html>\n<html>\n";
htmlOutput += htmlHead();
htmlOutput += htmlBody() ;
htmlOutput += "</html>\n";
return htmlOutput;
}
String htmlHead() {
String head = "<head>\n";
// Use the weather condition icon as the favicon.
head += "<link rel=\"icon\" href=\"http://openweathermap.org/img/w/" + data.icon + ".png\">\n";
// Refresh every 15 minutes.
head += "<meta http-equiv=\"refresh\" content=\"900\">\n";
head += " <meta charset=\"UTF-8\">\n";
head += " <meta name=\"viewport\" content=\"user-scalable=no,width=device-width\" />\n";
head += "<style>\n";
head += " body {background-color: #";
// Use the weather condition icon to determine the appropriate background color. It's easier than checking for the
// plain language weather description.
if (data.icon.indexOf("01") >= 0) { // Clear sky.
(data.icon.indexOf('d') >= 0) ? head += "065ce5" : head += "04398e";
} else if (data.icon.indexOf("02") >= 0) { // Few clouds.
(data.icon.indexOf('d') >= 0) ? head += "2b64bf" : head += "17376b";
} else if (data.icon.indexOf("03") >= 0) { // Scattered clouds.
(data.icon.indexOf('d') >= 0) ? head += "3c6dbc" : head += "213e6d";
} else if (data.icon.indexOf("04") >= 0) { // Broken clouds.
(data.icon.indexOf('d') >= 0) ? head += "4c71ad" : head += "2f466d";
} else if (data.icon.indexOf("09") >= 0) { // Shower rain.
(data.icon.indexOf('d') >= 0) ? head += "4c6284" : head += "384860";
} else if (data.icon.indexOf("10") >= 0) { // Rain.
(data.icon.indexOf('d') >= 0) ? head += "43536b" : head += "313d4f";
} else if (data.icon.indexOf("11") >= 0) { // Thunderstorm.
(data.icon.indexOf('d') >= 0) ? head += "485260" : head += "333a44";
} else if (data.icon.indexOf("13") >= 0) { // Snow.
(data.icon.indexOf('d') >= 0) ? head += "f9fafc" : head += "666768";
} else if (data.icon.indexOf("50") >= 0) { // Mist.
(data.icon.indexOf('d') >= 0) ? head += "bbbdc1" : head += "38393a";
}
head += ";}\n";
// Build a "navigation bar" ...
head += " .navbar {overflow: hidden; background-color: #333; position: fixed; top: 0; left: 0; width: 100%; height: 40px; margin: auto; }\n";
// ...that contains a search box for other cities...
head += " .search-box,.close-icon,.search-wrapper {position: relative; padding: 5px 5px;}\n";
head += " .search-wrapper {width: 30%; float: left;}\n";
head += " .search-box {width: 200px; border: 1px solid #ccc; outline: 0; border-radius: 15px;}\n";
head += " .search-box:focus {box-shadow: 0 0 2px 2px #b0e0ee; border: 1px solid #bebede;}\n";
head += " .close-icon {border:1px solid transparent; background-color: transparent; display: inline-block; vertical-align: middle; outline: 0; cursor: pointer;}\n";
head += " .close-icon:after {content: \"X\"; display: block; width: 15px; height: 15px; position: absolute; background-color: #FA9595; z-index:1; right: 35px; top: 0; bottom: 0; margin: auto; padding: 2px; border-radius: 50%; text-align: center; color: white; font-weight: normal; font-size: 10px; box-shadow: 0 0 2px #E50F0F; cursor: pointer;}\n";
head += " .search-box:not(:valid) ~ .close-icon { display: none;}\n";
if (searchError) {
head += " ::placeholder {color: red; opacity: 1;}\n";
}
// ...and conveniently displays the user's local time.
head += " .menuclock {font-family: Arial, Helvetica, sans-serif; color: #ffffff; float: right; padding: 10px 5px; margin-left: 30%;}\n";
head += " h1 {color: white; font-family: Arial, Helvetica, sans-serif; font-size: 200%; text-align: center; line-height: 5px;}\n";
head += " h2 {color: white; font-family: Arial, Helvetica, sans-serif; font-size: 300%; text-align: center; line-height: 5px;}\n";
head += " h3 {color: white; font-family: Arial, Helvetica, sans-serif; font-size: 110%; text-align: center; line-height: 5px;}\n";
head += " h4 {color: white; font-family: Arial, Helvetica, sans-serif; font-size: 100%; text-align: center; line-height: 5px;}\n";
head += " hr {border-top: 1px solid white;}\n";
head += " p {color: white; font-family: Arial, Helvetica, sans-serif;}\n";
head += " .weather {margin: 0 auto; max-width: 350px; margin-top: 50px; border-radius: 20px; background: rgba(0, 0, 0, .5); padding: 10px;}\n";
head += " canvas {padding-left: 0; padding-right: 0; margin-left: auto; margin-right: auto; display: block;}\n";
head += " footer {color: #d26c22; text-align: center;}\n";
head += " </style>\n";
head += "<title>" + data.cityName + " Weather Conditions</title>\n";
head += " </head>\n";
return head;
}
// Bring together the major parts of the HTML body.
String htmlBody() {
String body = "<body>\n";
body += "<p/>\n";
body += htmlMenuBar();
body += "<div class=\"weather\">\n";
body += htmlHeader();
body += htmlWeatherData();
body += htmlFooter();
body += "</div>\n";
body += "<p/>\n";
body += "</body>\n";
return body;
}
String htmlMenuBar() {
String htmlMenuBar = "<div class=\"navbar\">\n";
htmlMenuBar += "<div class=\"search-wrapper\">\n";
htmlMenuBar += " <form action=\"/\" method=\"post\">\n";
htmlMenuBar += " <input type=\"text\" name=\"city\" required class=\"search-box\" placeholder=\"";
(searchError) ? htmlMenuBar += "Invalid city name or ID" : htmlMenuBar += "City name or ID";
htmlMenuBar += "\" />\n";
htmlMenuBar += " <button class=\"close-icon\" type=\"reset\"></button>\n";
htmlMenuBar += " </form>\n";
htmlMenuBar += "</div>\n";
htmlMenuBar += "<div id=\"clockbox\" class=\"menuclock\"></div>\n";
htmlMenuBar += "<script type=\"text/javascript\">\n";
htmlMenuBar += "var tday=[\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"];\n";
htmlMenuBar += "var tmonth=[\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"];\n";
htmlMenuBar += "function GetClock(){\n";
htmlMenuBar += "var d=new Date();\n";
htmlMenuBar += "var nday=d.getDay(),nmonth=d.getMonth(),ndate=d.getDate(),nyear=d.getFullYear();\n";
htmlMenuBar += "var nhour=d.getHours(),nmin=d.getMinutes();\n";
htmlMenuBar += "if(nmin<=9) nmin=\"0\"+nmin\n";
htmlMenuBar += "var clocktext=\"\"+tday[nday]+\", \"+ndate+\" \"+tmonth[nmonth]+\", \"+nyear+\" \"+nhour+\":\"+nmin+\"\";\n";
htmlMenuBar += "document.getElementById('clockbox').innerHTML=clocktext;\n";
htmlMenuBar += "}\n";
htmlMenuBar += "GetClock();\n";
htmlMenuBar += "setInterval(GetClock,1000);\n";
htmlMenuBar += "</script>\n";
htmlMenuBar += "</div>\n";
return htmlMenuBar;
}
// The header shows just the city, main weather description and the temperature.
String htmlHeader() {
String header = "<header>\n";
header += " <h1>" + data.cityName + "</h1> \n";
header += "<h4>" + data.main + "</h4>\n";
header += " <h2>" + String(round(data.temp)) + "°</h2> \n";
header += "</header>\n";
return header;
}
// The footer contains the acknowledgement link to OpenWeatherMap.
String htmlFooter() {
String footer = "<footer>\n";
footer += " <a title=\"OpenWeatherMap\" href=\"https://openweathermap.org\"><img src=\"https://openweathermap.org/themes/openweathermap/assets/vendor/owm/img/logo_OpenWeatherMap_orange.svg\" alt=\"OpenWeatherMap logo\" height=\"20\"></a>\n";
footer += "</footer>\n";
return footer;
}
// A nicely formatted HTML rendering of the weather data.
String htmlWeatherData() {
String htmlOutput = "<hr />";
htmlOutput += "<h4>Weather for " + data.cityName + ", " + data.country + "</h4>\n";
htmlOutput += "<p>\n";
time_t time = data.observationTime;
htmlOutput += "Observation time: " + String(ctime(&time)) + "<br>\n";
htmlOutput += "Description: " + data.description + "<br>\n";
htmlOutput += "IconMeteoCon: " + data.iconMeteoCon + "<br>\n";
htmlOutput += "Temperature: " + String(round(data.temp)) + "°<br>\n";
htmlOutput += "Pressure: " + String(data.pressure) + " hPa<br>\n";
htmlOutput += "Humidity: " + String(data.humidity) + "%<br>\n";
htmlOutput += "Temperature minimum: " + String(round(data.tempMin)) + "°<br>\n";
htmlOutput += "Temperature maximum: " + String(round(data.tempMax)) + "°<br>\n";
htmlOutput += "Wind speed: " + String(data.windSpeed) + " mph<br>\n";
htmlOutput += "Wind degrees: " + String(data.windDeg) + "<br>\n";
htmlOutput += "Clouds: " + String(data.clouds) + "%<br>\n";
time = data.sunrise;
htmlOutput += "Sunrise: " + String(ctime(&time)) + "<br>\n";
time = data.sunset;
htmlOutput += "Sunset: " + String(ctime(&time)) + "<br>\n";
htmlOutput += "</p>\n";
htmlOutput += "<hr />";
// Show the RGB values for the chest LED.
htmlOutput += "<h3>MiP";
if (mip.isUpright()) {
htmlOutput += " is roaming</h3>\n";
htmlOutput += chestHTML(0xB6, 0x00, 0xFF);
} else if (extinguished) {
htmlOutput += " is muted</h3>\n";
} else if (!extinguished) {
htmlOutput += "</h3>\n";
htmlOutput += chestHTML(red, green, blue);
MiPChestLED chestLED;
mip.readChestLED(chestLED);
if(chestLED.onTime == ON_TIME) {
htmlOutput += "<p>";
char mailMessage[30];
sprintf(mailMessage, "You have %d new email%s.", latestUnread, latestUnread == 1 ? "" : "s");
htmlOutput += mailMessage;
htmlOutput += "</p>\n";
}
}
htmlOutput += "<hr />";
return htmlOutput;
}
// This is an HTML5 canvas object used to display the color of MiP's chest LED.
String chestHTML(const uint8_t redHTML, const uint8_t greenHTML, const uint8_t blueHTML) {
String chestHTML = "<canvas id=\"imageView\" width=\"64\" height=\"64\"></canvas>\n";
chestHTML += "<script type=\"text/javascript\">\n";
chestHTML += "var canvas, context, canvaso, contexto;\n";
chestHTML += "canvaso = document.getElementById('imageView');\n";
chestHTML += "context = canvaso.getContext('2d');\n";
chestHTML += "context.rect(0, 0, 64, 64);\n";
chestHTML += "context.fillStyle=\"white\";\n";
chestHTML += "context.fill();\n";
chestHTML += "context.strokeStyle = '#a1a2a3';\n";
chestHTML += "context.save();\n";
chestHTML += "context.translate(32, 32);\n";
chestHTML += "context.scale(0.6363636363636364, 1);\n";
chestHTML += "context.beginPath();\n";
chestHTML += "context.arc(0, 0, 37, 0, 6.283185307179586, false);\n";
chestHTML += "context.fillStyle = '#";
char rgbValue[6];
sprintf(rgbValue, "%02X%02X%02X", redHTML, greenHTML, blueHTML);
chestHTML += rgbValue;
chestHTML += "';\n";
chestHTML += "context.fill();\n";
chestHTML += "context.stroke();\n";
chestHTML += "context.closePath();\n";
chestHTML += "context.restore();\n";
chestHTML += "context.strokeStyle = '#000000';\n";
chestHTML += "context.beginPath();\n";
chestHTML += "context.moveTo(0, 32);\n";
chestHTML += "context.lineTo(9, 32);\n";
chestHTML += "context.lineWidth=5;\n";
chestHTML += "context.stroke();\n";
chestHTML += "context.closePath();\n";
chestHTML += "context.strokeStyle = '#000000';\n";
chestHTML += "context.beginPath();\n";
chestHTML += "context.moveTo(55, 32);\n";
chestHTML += "context.lineTo(64, 32);\n";
chestHTML += "context.stroke();\n";
chestHTML += "context.closePath();\n";
chestHTML += "</script>\n";
return chestHTML;
}
// Get the number of unread emails in your Gmail inbox.
int getUnread() {
// Use WiFiClientSecure class to create a TLS (HTTPS) connection.
WiFiClientSecure client;
Serial1.printf("Connecting to %s:%d ... \r\n", host, httpsPort);
// Connect to the Gmail server on port 443.
if (!client.connect(host, httpsPort)) {
// If the connection fails, stop and return.
Serial1.println(F("Connection failed."));
return -1;
}
Serial1.printf("Requesting URL: %s%s\n", host, url);
// Send the HTTP request headers.
client.print(String("GET ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Authorization: Basic " + credentials + "\r\n" +
"User-Agent: ESP8266\r\n" +
"Connection: close\r\n\r\n");
Serial1.println(F("Request sent."));
int unread = -1;
while (client.connected()) { // Wait for the response. The response is in XML format
client.readStringUntil('<'); // read until the first XML tag
String tagname = client.readStringUntil('>'); // read until the end of this tag to get the tag name
if (tagname == "fullcount") { // if the tag is <fullcount>, the next string will be the number of unread emails
String unreadStr = client.readStringUntil('<'); // read until the closing tag (</fullcount>)
unread = unreadStr.toInt(); // convert from String to int
break; // stop reading
} // if the tag is not <fullcount>, repeat and read the next tag
}
Serial1.println(F("Connection closed."));
return unread;
}