using Google Maps API to calculate correct time zone
Posted: Thu Nov 23, 2017 1:13 pm
This is an example program I wrote to demonstrate configuring NTP and automatically setting the time zone based on the ESP's public facing IP address, using HTTPclient and ArduinoJson. This project is designed to run on a NodeMCU 1.0 compatible ESP8266 development board. I'm using the github version of Arduino ESP8266, and pull#2821 to bypass SSL verification, because google requires TLS and this is not sensitive information.
https://github.com/esp8266/Arduino#using-git-version
https://github.com/esp8266/Arduino/pull/2821 merge using 'git pull origin pull/2821/head'
I'm using http://freegeoip.net/ to determine the location of the IP address. If the IP address does not correspond to the desired location, just set the location string to your postal code, or city/state/country address fragment, and it will use that location, instead. The address fragment is converted to a location, using the Google Maps API. The location, from geoIP or address, is translated to a time zone, also using Google Maps API. Use of the Google Maps API requires an account, which is available free, for limited use.
https://developers.google.com/maps/documentation/timezone/intro
Passing parameters through a URL requires substituting some characters with %HEX codes, which is not handled by HTTPclient, so I wrote the included UrlEncode function to handle the task. The sketch will output the current time (and free heap) to Serial, every minute. It will also recalculate the correct timezone every day at 2:00am, to automatically adapt for daylight savings changes. All this is likely overkill for many projects, but at least demonstrates use of some interesting skills and capabilities. For most purposes, using/storing time codes in UTC is likely adequate or desirable.
https://github.com/esp8266/Arduino#using-git-version
https://github.com/esp8266/Arduino/pull/2821 merge using 'git pull origin pull/2821/head'
I'm using http://freegeoip.net/ to determine the location of the IP address. If the IP address does not correspond to the desired location, just set the location string to your postal code, or city/state/country address fragment, and it will use that location, instead. The address fragment is converted to a location, using the Google Maps API. The location, from geoIP or address, is translated to a time zone, also using Google Maps API. Use of the Google Maps API requires an account, which is available free, for limited use.
https://developers.google.com/maps/documentation/timezone/intro
Passing parameters through a URL requires substituting some characters with %HEX codes, which is not handled by HTTPclient, so I wrote the included UrlEncode function to handle the task. The sketch will output the current time (and free heap) to Serial, every minute. It will also recalculate the correct timezone every day at 2:00am, to automatically adapt for daylight savings changes. All this is likely overkill for many projects, but at least demonstrates use of some interesting skills and capabilities. For most purposes, using/storing time codes in UTC is likely adequate or desirable.
Code: Select all
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoOTA.h>
#include <time.h>
#include <ArduinoJson.h> // install using library manager, https://github.com/bblanchon/ArduinoJson/
#define WIFI_SSID "SSID"
#define WIFI_PASS "PASS"
const char* UserAgent = "SolarGuardn/1.0 (Arduino ESP8266)";
// openssl s_client -connect maps.googleapis.com:443 | openssl x509 -fingerprint -noout
const char* gMapsCrt = "67:7B:99:A4:E5:A7:AE:E4:F0:92:01:EF:F5:58:B8:0B:49:CF:53:D4";
const char* gMapsKey = "KEY"; // https://developers.google.com/maps/documentation/timezone/intro
time_t TWOAM;
String location = ""; // set to postal code to bypass geoIP lookup
String UrlEncode(const String url) {
String e;
for (int i = 0; i < url.length(); i++) {
char c = url.charAt(i);
if (c == 0x20) {
e += "%20"; // "+" only valid for some uses
} else if (isalnum(c)) {
e += c;
} else {
e += "%";
if (c < 0x10) e += "0";
e += String(c, HEX);
}
}
return e;
}
String getIPlocation() { // Using freegeoip.net to map public IP's location
HTTPClient http;
String URL = "http://freegeoip.net/json/"; // no host or IP specified returns client's public IP info
String payload;
String loc;
http.setUserAgent(UserAgent);
if (!http.begin(URL)) {
Serial.println(F("getIPlocation: [HTTP] connect failed!"));
} else {
int stat = http.GET();
if (stat > 0) {
if (stat == HTTP_CODE_OK) {
payload = http.getString();
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(payload);
if (root.success()) {
String region = root["region_name"];
String country = root["country_code"];
String lat = root["latitude"];
String lng = root["longitude"];
loc = lat + "," + lng;
Serial.println("getIPlocation: " + region + ", " + country);
} else {
Serial.println(F("getIPlocation: JSON parse failed!"));
Serial.println(payload);
}
} else {
Serial.printf("getIPlocation: [HTTP] GET reply %d\r\n", stat);
}
} else {
Serial.printf("getIPlocation: [HTTP] GET failed: %s\r\n", http.errorToString(stat).c_str());
}
}
http.end();
return loc;
} // getIPlocation
String getLocation(const String address, const char* key) { // using google maps API, return location for provided Postal Code
HTTPClient http;
String URL = "https://maps.googleapis.com/maps/api/geocode/json?address="
+ UrlEncode(address) + "&key=" + String(key);
String payload;
String loc;
http.setIgnoreTLSVerifyFailure(true); // https://github.com/esp8266/Arduino/pull/2821
http.setUserAgent(UserAgent);
if (!http.begin(URL, gMapsCrt)) {
Serial.println(F("getLocation: [HTTP] connect failed!"));
} else {
int stat = http.GET();
if (stat > 0) {
if (stat == HTTP_CODE_OK) {
payload = http.getString();
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(payload);
if (root.success()) {
JsonObject& results = root["results"][0];
JsonObject& results_geometry = results["geometry"];
String address = results["formatted_address"];
String lat = results_geometry["location"]["lat"];
String lng = results_geometry["location"]["lng"];
loc = lat + "," + lng;
Serial.print(F("getLocation: "));
Serial.println(address);
} else {
Serial.println(F("getLocation: JSON parse failed!"));
Serial.println(payload);
}
} else {
Serial.printf("getLocation: [HTTP] GET reply %d\r\n", stat);
}
} else {
Serial.printf("getLocation: [HTTP] GET failed: %s\r\n", http.errorToString(stat).c_str());
}
}
http.end();
return loc;
} // getLocation
int getTimeZone(time_t now, String loc, const char* key) { // using google maps API, return TimeZone for provided timestamp
HTTPClient http;
int tz = false;
String URL = "https://maps.googleapis.com/maps/api/timezone/json?location="
+ UrlEncode(loc) + "×tamp=" + String(now) + "&key=" + String(key);
String payload;
http.setIgnoreTLSVerifyFailure(true); // https://github.com/esp8266/Arduino/pull/2821
http.setUserAgent(UserAgent);
if (!http.begin(URL, gMapsCrt)) {
Serial.println(F("getTimeZone: [HTTP] connect failed!"));
} else {
int stat = http.GET();
if (stat > 0) {
if (stat == HTTP_CODE_OK) {
payload = http.getString();
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(payload);
if (root.success()) {
tz = (int (root["rawOffset"]) + int (root["dstOffset"])) / 3600; // combine Offset and dstOffset
const char* tzname = root["timeZoneName"];
Serial.printf("getTimeZone: %s (%d)\r\n", tzname, tz);
} else {
Serial.println(F("getTimeZone: JSON parse failed!"));
Serial.println(payload);
}
} else {
Serial.printf("getTimeZone: [HTTP] GET reply %d\r\n", stat);
}
} else {
Serial.printf("getTimeZone: [HTTP] GET failed: %s\r\n", http.errorToString(stat).c_str());
}
}
http.end();
return tz;
} // getTimeZone
void setNTP () {
int TZ = getTimeZone(time(nullptr), location, gMapsKey);
Serial.print(F("Synchronize NTP ..."));
configTime((TZ * 3600), 0, "pool.ntp.org", "time.nist.gov");
while (!time(nullptr)) {
delay(1000);
Serial.print(F("."));
}
delay(5000);
Serial.println(" OK");
time_t now = time(nullptr);
struct tm * calendar;
calendar = localtime(&now);
calendar->tm_mday++;
calendar->tm_hour = 2;
calendar->tm_min = 0;
calendar->tm_sec = 0;
TWOAM = mktime(calendar);
String t = ctime(&TWOAM);
t.trim();
Serial.print("next timezone check @ ");
Serial.println(t);
}
void setup() {
Serial.begin(115200);
//Serial.setDebugOutput(true);
while (!Serial);
Serial.print(F("\033[H\033[2J")); // clear screen (not supported in serial monitor)
Serial.print(F("Connecting to "));
Serial.print(WIFI_SSID);
Serial.print(F("..."));
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(F("."));
delay(500);
}
Serial.println(" OK");
if (location == "") {
location = getIPlocation();
} else {
location = getLocation(location, gMapsKey);
}
setNTP();
ArduinoOTA.onStart([]() {
Serial.println("\nOTA Start");
} );
ArduinoOTA.onEnd([]() {
Serial.println("\nOTA End");
delay(500);
} );
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.print("OTA Progress: " + String((progress / (total / 100))) + " \r");
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.print("\nError[" + String(error) + "]: ");
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
else Serial.println("unknown error");
delay(500);
ESP.restart();
});
ArduinoOTA.begin();
pinMode(BUILTIN_LED, OUTPUT);
Serial.println();
Serial.print(F("Last reset reason: "));
Serial.println(ESP.getResetReason());
Serial.print(F("WiFi Hostname: "));
Serial.println(WiFi.hostname());
Serial.print(F("WiFi IP addr: "));
Serial.println(WiFi.localIP());
Serial.print(F("WiFi MAC addr: "));
Serial.println(WiFi.macAddress());
Serial.print(F("ESP Sketch size: "));
Serial.println(ESP.getSketchSize());
Serial.print(F("ESP Flash free: "));
Serial.println(ESP.getFreeSketchSpace());
Serial.print(F("ESP Flash Size: "));
Serial.println(ESP.getFlashChipRealSize());
}
void loop() {
ArduinoOTA.handle();
analogWrite(BUILTIN_LED, 1);
int heap = ESP.getFreeHeap();
time_t now = time(nullptr);
if (now > TWOAM) setNTP();
String t = ctime(&now);
t.trim();
t = t + " (" + String(heap) + ")";
Serial.println(t);
for (int i = 23; i < 1023; i++) { // pulse LED with PWM
analogWrite(BUILTIN_LED, i);
delay(2);
}
analogWrite(BUILTIN_LED, 0);
digitalWrite(BUILTIN_LED, HIGH);
delay(3000);
for (int i = 0; i < 11 ; i++) { // loop once per minute
ArduinoOTA.handle(); // but continue to handle OTA
delay(5000);
}
}