ESP32 e-Paper info screen (client part).
[weathermon.git] / weather_screen / weather_screen.ino
1 #include <FS.h>                   //this needs to be first, or it all crashes and burns...
2 #include "SPIFFS.h"
3 #include <WiFi.h>          //https://github.com/esp8266/Arduino
4 //needed for library
5 #include <DNSServer.h>
6 #include <WebServer.h>
7 #include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager
8
9 #include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson
10
11 #include <time.h>                     // Built-in
12 #include <SPI.h>                      // Built-in 
13
14 #include <HTTPClient.h>
15
16 #include <GxEPD2_BW.h>
17 #include <U8g2_for_Adafruit_GFX.h>
18
19 #include <time.h>
20 #include <sys/time.h>
21
22 #include "weather_types.h"
23
24 #define SCREEN_WIDTH  800             // Set for landscape mode
25 #define SCREEN_HEIGHT 480
26
27 enum alignment {LEFT, RIGHT, CENTER};
28
29 // Connections for e.g. Waveshare ESP32 e-Paper Driver Board
30 static const uint8_t EPD_BUSY = 25;
31 static const uint8_t EPD_CS   = 15;
32 static const uint8_t EPD_RST  = 26; 
33 static const uint8_t EPD_DC   = 27; 
34 static const uint8_t EPD_SCK  = 13;
35 static const uint8_t EPD_MISO = 12; // Master-In Slave-Out not used, as no data from display
36 static const uint8_t EPD_MOSI = 14;
37
38 GxEPD2_BW<GxEPD2_750_T7, GxEPD2_750_T7::HEIGHT> display(GxEPD2_750_T7(/*CS=*/ EPD_CS, /*DC=*/ EPD_DC, /*RST=*/ EPD_RST, /*BUSY=*/ EPD_BUSY));   // B/W display
39 //GxEPD2_3C<GxEPD2_750c, GxEPD2_750c::HEIGHT> display(GxEPD2_750(/*CS=*/ EPD_CS, /*DC=*/ EPD_DC, /*RST=*/ EPD_RST, /*BUSY=*/ EPD_BUSY)); // 3-colour displays
40 // use GxEPD_BLACK or GxEPD_WHITE or GxEPD_RED or GxEPD_YELLOW depending on display type
41
42 U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;
43
44 #include "fonts.h"
45
46 //define your default values here, if there are different values in config.json, they are overwritten.
47 char api_key[40]      = "";                      // See: https://openweathermap.org/  // It's free to get an API key, but don't take more than 60 readings/minute!
48 char latitude[8]      = "55.5";
49 char longitude[8]     = "37.5";
50 char language[4]      = "RU";                    // NOTE: Only the weather description is translated by OWM
51                                                  // Examples: Arabic (AR) Czech (CZ) English (EN) Greek (EL) Persian(Farsi) (FA) Galician (GL) Hungarian (HU) Japanese (JA)
52                                                  // Korean (KR) Latvian (LA) Lithuanian (LT) Macedonian (MK) Slovak (SK) Slovenian (SL) Vietnamese (VI)
53
54 char current_weather_server[255] = "";
55 char current_weather_uri[255] = "";
56 char current_weather_pressure[40] = "PRESSURE";
57 char current_weather_temperature[40] = "TEMPERATURE_C";
58 char current_weather_humidity[40] = "HUMIDITY";
59 char current_weather_windspeed[40] = "";
60 char current_weather_lux[40] = "LUX";
61
62 const char server[] = "api.openweathermap.org";
63
64 int SleepDuration = 10;
65
66 //flag for saving data
67 bool shouldSaveConfig = false;
68
69 //callback notifying us of the need to save config
70 void saveConfigCallback () {
71   Serial.println("Should save config");
72   shouldSaveConfig = true;
73 }
74
75 void DisplaySetup() {
76   display.init(115200, true, 2); // init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration, bool pulldown_rst_mode)
77   SPI.end();
78   SPI.begin(EPD_SCK, EPD_MISO, EPD_MOSI, EPD_CS);
79   u8g2Fonts.begin(display); // connect u8g2 procedures to Adafruit GFX
80   u8g2Fonts.setFontMode(1);                  // use u8g2 transparent mode (this is default)
81   u8g2Fonts.setFontDirection(0);             // left to right (this is default)
82   u8g2Fonts.setForegroundColor(GxEPD_BLACK); // apply Adafruit GFX color
83   u8g2Fonts.setBackgroundColor(GxEPD_WHITE); // apply Adafruit GFX color
84   SetCyrFont(10); 
85   display.fillScreen(GxEPD_WHITE);
86   display.setFullWindow();
87 }
88
89 void WiFiSetup() {
90
91   // The extra parameters to be configured (can be either global or just in the setup)
92   // After connecting, parameter.getValue() will get you the configured value
93   // id/name placeholder/prompt default length
94   WiFiManagerParameter custom_api_key("api_key", "OWN API key", api_key, 40);
95   WiFiManagerParameter custom_latitude("latitide", "Latitude", latitude, 8);
96   WiFiManagerParameter custom_longitude("longitude", "Longitude", longitude, 8);
97   WiFiManagerParameter custom_language("language", "Language", language, 4);
98   WiFiManagerParameter custom_current_weather_server("current_weather_server", "Weather server", current_weather_server, 255);
99   WiFiManagerParameter custom_current_weather_uri("current_weather_uri", "Weather URI", current_weather_uri, 255);
100   WiFiManagerParameter custom_current_weather_pressure("current_weather_pressure", "Pressure key", current_weather_pressure, 40);
101   WiFiManagerParameter custom_current_weather_temperature("current_weather_temperature", "Temperature key", current_weather_temperature, 40);
102   WiFiManagerParameter custom_current_weather_humidity("current_weather_humidity", "Humidity key", current_weather_humidity, 40);
103   WiFiManagerParameter custom_current_weather_windspeed("current_weather_windspeed", "Windspeed key", current_weather_windspeed, 40);
104   WiFiManagerParameter custom_current_weather_lux("current_weather_lux", "Lux key", current_weather_lux, 40);
105  
106   //WiFiManager
107   //Local intialization. Once its business is done, there is no need to keep it around
108   WiFiManager wifiManager;
109
110   //set config save notify callback
111   wifiManager.setSaveConfigCallback(saveConfigCallback);
112
113   //add all your parameters here
114   wifiManager.addParameter(&custom_api_key);
115   wifiManager.addParameter(&custom_latitude);
116   wifiManager.addParameter(&custom_longitude);
117   wifiManager.addParameter(&custom_language);
118   wifiManager.addParameter(&custom_current_weather_server);
119   wifiManager.addParameter(&custom_current_weather_uri);
120   wifiManager.addParameter(&custom_current_weather_pressure);
121   wifiManager.addParameter(&custom_current_weather_temperature);
122   wifiManager.addParameter(&custom_current_weather_humidity);
123   wifiManager.addParameter(&custom_current_weather_windspeed);
124   wifiManager.addParameter(&custom_current_weather_lux);
125
126   wifiManager.setTimeout(300);
127
128   if (!wifiManager.startConfigPortal()) {
129     Serial.println("failed to connect and hit timeout");
130     delay(3000);
131     //reset and try again, or maybe put it to deep sleep
132     ESP.restart();
133   }
134
135   //if you get here you have connected to the WiFi
136   Serial.println("connected...");
137
138   //read updated parameters
139   strcpy(api_key, custom_api_key.getValue());
140   strcpy(latitude, custom_latitude.getValue());
141   strcpy(longitude, custom_longitude.getValue());
142   strcpy(language, custom_language.getValue());
143   strcpy(current_weather_server, custom_current_weather_server.getValue());
144   strcpy(current_weather_uri, custom_current_weather_uri.getValue());
145   strcpy(current_weather_pressure, custom_current_weather_pressure.getValue());
146   strcpy(current_weather_temperature, custom_current_weather_temperature.getValue());
147   strcpy(current_weather_humidity, custom_current_weather_humidity.getValue());
148   strcpy(current_weather_windspeed, custom_current_weather_windspeed.getValue());
149   strcpy(current_weather_lux, custom_current_weather_lux.getValue());
150
151   //save the custom parameters to FS
152   if (shouldSaveConfig) {
153     Serial.println("saving config");
154     DynamicJsonDocument json(1024);
155     json["api_key"] = api_key;
156     json["latitude"] = latitude;
157     json["longitude"] = longitude;
158     json["language"] = language;
159     json["current_weather_server"] = current_weather_server;
160     json["current_weather_uri"] = current_weather_uri;
161     json["current_weather_pressure"] = current_weather_pressure;
162     json["current_weather_temperature"] = current_weather_temperature;
163     json["current_weather_humidity"] = current_weather_humidity;
164     json["current_weather_windspeed"] = current_weather_windspeed;
165     json["current_weather_lux"] = current_weather_lux;
166
167     File configFile = SPIFFS.open("/config.json", "w");
168     if (!configFile) {
169       Serial.println("failed to open config file for writing");
170     }
171
172     serializeJson(json, Serial);
173     serializeJson(json, configFile);
174     configFile.close();
175     //end save
176   }
177  
178 }
179
180 void setup() {
181   byte need_setup = false;
182   // put your setup code here, to run once:
183   Serial.begin(115200);
184   Serial.println();
185
186   //read configuration from FS json
187   Serial.println("mounting FS...");
188
189   if (SPIFFS.begin(true)) {
190     Serial.println("mounted file system");
191     if (SPIFFS.exists("/config.json")) {
192       //file exists, reading and loading
193       Serial.println("reading config file");
194       File configFile = SPIFFS.open("/config.json", "r");
195       if (configFile) {
196         Serial.println("opened config file");
197         size_t size = configFile.size();
198         // Allocate a buffer to store contents of the file.
199         std::unique_ptr<char[]> buf(new char[size]);
200
201         configFile.readBytes(buf.get(), size);
202
203         DynamicJsonDocument json(1024);
204         auto deserializeError = deserializeJson(json, buf.get());
205         serializeJson(json, Serial);
206         if ( ! deserializeError ) {
207           Serial.println("\nparsed json");
208           if (json.containsKey("api_key")) { strcpy(api_key, json["api_key"]); }
209           if (json.containsKey("latitude")) { strcpy(latitude, json["latitude"]); }
210           if (json.containsKey("longitude")) { strcpy(longitude, json["longitude"]); }
211           if (json.containsKey("language")) { strcpy(language, json["language"]); }
212           if (json.containsKey("current_weather_server")) { strcpy(current_weather_server, json["current_weather_server"]); }
213           if (json.containsKey("current_weather_uri")) { strcpy(current_weather_uri, json["current_weather_uri"]); }
214           if (json.containsKey("current_weather_pressure")) { strcpy(current_weather_pressure, json["current_weather_pressure"]); }
215           if (json.containsKey("current_weather_temperature")) { strcpy(current_weather_temperature, json["current_weather_temperature"]); }
216           if (json.containsKey("current_weather_humidity")) { strcpy(current_weather_humidity, json["current_weather_humidity"]); }
217           if (json.containsKey("current_weather_windspeed")) { strcpy(current_weather_windspeed, json["current_weather_windspeed"]); }
218           if (json.containsKey("current_weather_lux")) { strcpy(current_weather_lux, json["current_weather_lux"]); }
219         } else {
220           Serial.println("failed to load json config");
221         }
222         configFile.close();
223       } else {
224         need_setup = true;
225       }
226     } else {
227       need_setup = true;
228     }
229   } else {
230     Serial.println("failed to mount FS");
231   }
232   //end read
233
234   if (need_setup) {
235     WiFiSetup(); 
236   } else {
237     WiFiManager wifiManager;
238     wifiManager.setTimeout(10);
239     if(!wifiManager.autoConnect()) {
240       Serial.println("failed to connect and hit timeout");
241       delay(3000);
242       ESP.restart();
243     }  
244   }
245
246   Serial.println("\nlocal ip");
247   Serial.println(WiFi.localIP());
248
249   DisplaySetup();
250   
251 }
252
253 int TimeZoneOffset;
254 int SunRise, SunSet;
255
256 int HourlyDT[MaxHourlyFC];
257 float HourlyTemp[MaxHourlyFC];
258 float HourlyFeelsLike[MaxHourlyFC];
259 float HourlyPressure[MaxHourlyFC];
260 float HourlyHumidity[MaxHourlyFC];
261 float HourlyClouds[MaxHourlyFC];
262 float HourlyWindSpeed[MaxHourlyFC];
263 float HourlyRain[MaxHourlyFC];
264 float HourlySnow[MaxHourlyFC];
265 float HourlyPrecip[MaxHourlyFC];
266
267 int DailyDT[MaxDailyFC];
268 float DailyTempMin[MaxDailyFC];
269 float DailyTempMax[MaxDailyFC];
270 float DailyFeelsLikeMin[MaxDailyFC];
271 float DailyFeelsLikeMax[MaxDailyFC];
272 float DailyPressure[MaxDailyFC];
273 float DailyHumidity[MaxDailyFC];
274 float DailyClouds[MaxDailyFC];
275 float DailyWindSpeed[MaxDailyFC];
276 float DailyRain[MaxDailyFC];
277 float DailySnow[MaxDailyFC];
278 float DailyPrecip[MaxDailyFC];
279
280 String UnixTime(int unix_time) {
281   time_t tm = unix_time;
282   struct tm *now_tm = localtime(&tm);
283   char output[40];
284   strftime(output, sizeof(output), "%H:%M %d/%m/%y", now_tm);
285   return output;
286 }
287
288 String UnixTimeOnly(int unix_time) {
289   time_t tm = unix_time;
290   struct tm *now_tm = localtime(&tm);
291   char output[40];
292   strftime(output, sizeof(output), "%H:%M", now_tm);
293   return output;
294 }
295
296 String UnixDate(int unix_time) {
297   time_t tm = unix_time;
298   struct tm *now_tm = localtime(&tm);
299   char output[40];
300   strftime(output, sizeof(output), "%d/%m/%y", now_tm);
301   return output;
302 }
303
304 bool DecodeWeather(WiFiClient& json) {
305   DynamicJsonDocument doc(35 * 1024);
306   // Deserialize the JSON document
307   DeserializationError error = deserializeJson(doc, json);
308   // Test if parsing succeeds.
309   if (error) {
310     Serial.print(F("deserializeJson() failed: "));
311     Serial.println(error.c_str());
312     return false;
313   }
314   // convert it to a JsonObject
315   JsonObject root = doc.as<JsonObject>();
316
317   TimeZoneOffset = root["timezone_offset"].as<int>();
318   
319   SunRise = root["current"]["sunrise"].as<int>();
320   SunSet  = root["current"]["sunset"].as<int>();
321
322   CurrentWeather.dt           = root["current"]["dt"].as<int>();
323   CurrentWeather.temp         = root["current"]["temp"].as<float>();
324   CurrentWeather.feels_like   = root["current"]["feels_like"].as<float>();
325   CurrentWeather.pressure     = 0.75 * root["current"]["pressure"].as<float>();
326   CurrentWeather.humidity     = root["current"]["humidity"].as<float>();
327   CurrentWeather.clouds       = root["current"]["clouds"].as<float>();
328   CurrentWeather.wind_speed   = root["current"]["wind_speed"].as<float>();
329   CurrentWeather.wind_deg     = root["current"]["wind_deg"].as<float>();
330   CurrentWeather.rain         = root["current"]["rain"]["1h"].as<float>();
331   CurrentWeather.snow         = root["current"]["snow"]["1h"].as<float>();
332   CurrentWeather.description  = root["current"]["weather"][0]["description"].as<char*>();
333   CurrentWeather.icon         = root["current"]["weather"][0]["icon"].as<char*>();
334
335   struct timeval now = { .tv_sec =  CurrentWeather.dt };
336   settimeofday(&now, NULL);
337
338   JsonArray hourly = root["hourly"];
339   for (byte i=0; i<MaxHourlyFC; i++) {
340     HourlyDT[i] = HourlyForecasts[i].dt                  = hourly[i]["dt"].as<int>();
341     HourlyTemp[i] = HourlyForecasts[i].temp              = hourly[i]["temp"].as<float>();
342     HourlyFeelsLike[i] = HourlyForecasts[i].feels_like   = hourly[i]["feels_like"].as<float>();
343     HourlyPressure[i] = HourlyForecasts[i].pressure      = 0.75 * hourly[i]["pressure"].as<float>();
344     HourlyHumidity[i] = HourlyForecasts[i].humidity      = hourly[i]["humidity"].as<float>();
345     HourlyClouds[i] = HourlyForecasts[i].clouds          = hourly[i]["clouds"].as<float>();
346     HourlyWindSpeed[i] = HourlyForecasts[i].wind_speed   = hourly[i]["wind_speed"].as<float>();
347     HourlyForecasts[i].wind_deg                          = hourly[i]["wind_deg"].as<float>();
348     HourlyRain[i] = HourlyForecasts[i].rain              = hourly[i]["rain"]["1h"].as<float>();
349     HourlySnow[i] = HourlyForecasts[i].snow              = hourly[i]["snow"]["1h"].as<float>();
350     HourlyPrecip[i] = HourlyRain[i] + HourlySnow[i];
351     HourlyForecasts[i].description                       = hourly[i]["weather"][0]["description"].as<char*>();
352     HourlyForecasts[i].icon                              = hourly[i]["weather"][0]["icon"].as<char*>();
353   }
354
355   JsonArray daily = root["daily"];
356   for (byte i=0; i<MaxDailyFC; i++) {
357     DailyDT[i] = DailyForecasts[i].dt                        = daily[i]["dt"].as<int>();
358     DailyTempMin[i] = DailyForecasts[i].temp_min             = daily[i]["temp"]["min"].as<float>();
359     DailyFeelsLikeMin[i] = DailyForecasts[i].feels_like_min  = daily[i]["feels_like"]["min"].as<float>();
360     DailyTempMax[i] = DailyForecasts[i].temp_max             = daily[i]["temp"]["max"].as<float>();
361     DailyFeelsLikeMax[i] = DailyForecasts[i].feels_like_max  = daily[i]["feels_like"]["max"].as<float>();
362     DailyPressure[i] = DailyForecasts[i].pressure            = 0.75 * daily[i]["pressure"].as<float>();
363     DailyHumidity[i] = DailyForecasts[i].humidity            = daily[i]["humidity"].as<float>();
364     DailyClouds[i] = DailyForecasts[i].clouds                = daily[i]["clouds"].as<float>();
365     DailyWindSpeed[i] = DailyForecasts[i].wind_speed         = daily[i]["wind_speed"].as<float>();
366     DailyForecasts[i].wind_deg                               = daily[i]["wind_deg"].as<float>();
367     DailyRain[i] = DailyForecasts[i].rain                    = daily[i]["rain"].as<float>();
368     DailySnow[i] = DailyForecasts[i].snow                    = daily[i]["snow"].as<float>();
369     DailyPrecip[i] = DailyRain[i] + DailySnow[i];
370     DailyForecasts[i].description                            = daily[i]["weather"][0]["description"].as<char*>();
371     DailyForecasts[i].icon                                   = daily[i]["weather"][0]["icon"].as<char*>();
372   }
373   
374   return true;
375 }
376
377 bool GetWeather(WiFiClient& client) {
378   client.stop(); // close connection before sending a new request
379   HTTPClient http;
380   char uri[128];
381   sprintf(uri,"/data/2.5/onecall?lat=%s&lon=%s&appid=%s&lang=%s&units=metric",latitude,longitude,api_key,language);
382   http.useHTTP10(true);
383   Serial.println("Connecting...");
384   http.begin(client, server, 80, uri);
385   int httpCode = http.GET();
386   if(httpCode == HTTP_CODE_OK) {
387     if (!DecodeWeather(http.getStream())) return false;
388     client.stop();
389     http.end();
390     return true;
391   } else {
392     Serial.printf("connection failed, error: %s\n", http.errorToString(httpCode).c_str());
393     client.stop();
394     http.end();
395     return false;
396   }
397   http.end();
398   return true;
399 }
400
401 bool DecodeCurrentWeather(WiFiClient& json) {
402   DynamicJsonDocument doc(35 * 1024);
403   // Deserialize the JSON document
404   DeserializationError error = deserializeJson(doc, json);
405   // Test if parsing succeeds.
406   if (error) {
407     Serial.print(F("deserializeJson() failed: "));
408     Serial.println(error.c_str());
409     return false;
410   }
411   // convert it to a JsonObject
412   JsonObject root = doc.as<JsonObject>();
413
414   if (current_weather_pressure[0] && root.containsKey(current_weather_pressure)) {
415     CurrentWeather.pressure     = root[current_weather_pressure].as<float>();
416   }
417   if (current_weather_temperature[0] && root.containsKey(current_weather_temperature)) {
418     CurrentWeather.temp     = root[current_weather_temperature].as<float>();
419   }
420   if (current_weather_humidity[0] && root.containsKey(current_weather_humidity)) {
421     CurrentWeather.humidity     = root[current_weather_humidity].as<float>();
422   }
423   if (current_weather_windspeed[0] && root.containsKey(current_weather_windspeed)) {
424     CurrentWeather.wind_speed     = root[current_weather_windspeed].as<float>();
425   }
426
427   float Ro = (CurrentWeather.humidity/100) * 6.105 * pow(2.712828, 17.27 * CurrentWeather.temp/(237.7+CurrentWeather.temp));
428   CurrentWeather.feels_like = CurrentWeather.temp + 0.348*Ro - 0.70*CurrentWeather.wind_speed - 4.25;
429   if (current_weather_lux[0] && root.containsKey(current_weather_lux)) {
430     float wm2 = root[current_weather_lux].as<float>()/685;
431     CurrentWeather.feels_like = CurrentWeather.feels_like + 0.70*wm2/(CurrentWeather.wind_speed + 10);
432   }
433   Serial.print("Calculated feels like: ");
434   Serial.println(CurrentWeather.feels_like);
435   
436   return true;
437 }
438
439
440 bool GetCurrentWeather(WiFiClient& client) {
441   if (current_weather_server) {
442     client.stop(); // close connection before sending a new request
443     HTTPClient http;
444     http.useHTTP10(true);
445     Serial.println("Connecting...");
446     http.begin(client, current_weather_server, 80, current_weather_uri);
447     int httpCode = http.GET();
448     if(httpCode == HTTP_CODE_OK) {
449       if (!DecodeCurrentWeather(http.getStream())) return false;
450       client.stop();
451       http.end();
452       return true;
453     } else {
454       Serial.printf("connection failed, error: %s\n", http.errorToString(httpCode).c_str());
455       client.stop();
456       http.end();
457       return false;
458     }
459     http.end();
460   }
461   return true;
462 }
463
464 void arrow(int x, int y, int asize, float aangle, int pwidth, int plength) {
465   float dx = (asize) * cos((aangle - 90) * PI / 180) + x; // calculate X position
466   float dy = (asize) * sin((aangle - 90) * PI / 180) + y; // calculate Y position
467   float x1 = 0;         float y1 = plength;
468   float x2 = pwidth / 2;  float y2 = pwidth / 2;
469   float x3 = -pwidth / 2; float y3 = pwidth / 2;
470   float angle = aangle * PI / 180 - 135;
471   float xx1 = x1 * cos(angle) - y1 * sin(angle) + dx;
472   float yy1 = y1 * cos(angle) + x1 * sin(angle) + dy;
473   float xx2 = x2 * cos(angle) - y2 * sin(angle) + dx;
474   float yy2 = y2 * cos(angle) + x2 * sin(angle) + dy;
475   float xx3 = x3 * cos(angle) - y3 * sin(angle) + dx;
476   float yy3 = y3 * cos(angle) + x3 * sin(angle) + dy;
477   display.fillTriangle(xx1, yy1, xx3, yy3, xx2, yy2, GxEPD_BLACK);
478 }
479
480 void drawString(int x, int y, String text, alignment align) {
481   uint16_t w, h;
482   display.setTextWrap(false);
483   w = u8g2Fonts.getUTF8Width(text.c_str());
484   h = u8g2Fonts.getFontAscent();
485   if (align == RIGHT)  x = x - w;
486   if (align == CENTER) x = x - w / 2;
487   u8g2Fonts.setCursor(x, y + h / 2);
488   u8g2Fonts.print(text);
489 }
490
491 String WindDegToDirection(float winddirection) {
492   if (winddirection >= 337.5 || winddirection < 22.5)  return "С";
493   if (winddirection >=  22.5 && winddirection < 67.5)  return "СВ";
494   if (winddirection >=  67.5 && winddirection < 112.5) return "В";
495   if (winddirection >= 112.5 && winddirection < 157.5) return "ЮВ";
496   if (winddirection >= 157.5 && winddirection < 202.5) return "Ю";
497   if (winddirection >= 202.5 && winddirection < 247.5) return "ЮЗ";
498   if (winddirection >= 247.5 && winddirection < 292.5) return "З";
499   if (winddirection >= 292.5 && winddirection < 337.5) return "СЗ";
500   return "?";
501 }
502
503 void DrawBlock(int x1, int x2, int y1, int y2) {
504   display.fillRect(x1+3, y1+3, x2-x1, y2-y1, GxEPD_BLACK);
505   display.fillRect(x1, y1, x2-x1, y2-y1, GxEPD_WHITE);
506   display.drawRect(x1, y1, x2-x1, y2-y1, GxEPD_BLACK);
507 }
508
509 void DisplayCurrent(int x1, int x2, int y1, int y2, WeatherRecord weather) {
510
511   DrawBlock(x1,x2,y1,y2);
512
513   int x = (x1 + x2)/2;
514   int y = (y1 + y2)/2;
515   int h = (y2 - y1)/3;
516   int w = (x2 - x1)/4;
517   if (h<w) { w = h; }
518   SetNumFont(w);
519   drawString(x, y1+w*2/3, String(weather.temp,1) + "°", CENTER);
520   SetCyrFont(w/4);
521   drawString(x, y1+w*3/2, "ощущается как", CENTER);
522   SetNumFont(w*2/3);
523   drawString(x, y1+w*2, String(weather.feels_like,1) + "°", CENTER);
524   SetCyrFont(w/2);
525   drawString(x, y2-w/3, String(weather.humidity,0) + "% " + String(weather.pressure,0) + "мм", CENTER);
526 }
527
528 void DisplayWind(int x1, int x2, int y1, int y2, WeatherRecord weather) {
529
530   DrawBlock(x1,x2,y1,y2);
531
532   int r, dxo, dyo, dxi, dyi;
533   int x = (x1 + x2)/2;
534   int y = (y1 + y2)/2;
535   int r1 = (x2 - x1)/2;
536   int r2 = (y2 - y1)/2;
537   if (r1>r2) { r = r2; } else { r = r1; }
538   int w = r/5;
539   if (w>40) { w = 40; }
540   r = r - w;
541
542   String speedstr = String(weather.wind_speed,1);
543   speedstr.trim();
544
545   arrow (x, y, r-w, weather.wind_deg, w/2, w*7/8);
546
547   display.drawCircle(x, y, r, GxEPD_BLACK);     // Draw compass circle
548   display.drawCircle(x, y, r + 1, GxEPD_BLACK); // Draw compass circle
549   display.drawCircle(x, y, r - w, GxEPD_BLACK); // Draw compass inner circle
550   display.drawCircle(x, y, r - w + 1, GxEPD_BLACK); // Draw compass inner circle
551   for (float a = 0; a < 360; a = a + 30) {
552     dxo = r * cos((a - 90) * PI / 180);
553     dyo = r * sin((a - 90) * PI / 180);
554     dxi = dxo * 0.9;
555     dyi = dyo * 0.9;
556     display.drawLine(dxo + x, dyo + y, dxi + x, dyi + y, GxEPD_BLACK);
557   }
558
559   SetCyrFont(w*2/3);
560   drawString(x, y - r - w/2, "С", CENTER);
561   drawString(x, y + r + w/2, "Ю", CENTER);
562   drawString(x - r - w/2, y, "З", CENTER);
563   drawString(x + r + w/2, y, "В", CENTER);
564   drawString(x - (r + w/2)*10/14, y - (r + w/2)*10/14, "сз", CENTER);
565   drawString(x - (r + w/2)*10/14, y + (r + w/2)*10/14, "юз", CENTER);
566   drawString(x + (r + w/2)*10/14, y + (r + w/2)*10/14, "юв", CENTER);
567   drawString(x + (r + w/2)*10/14, y - (r + w/2)*10/14, "св", CENTER);
568   SetNumFont(r*5/8);
569   drawString(x, y-r/8, speedstr, CENTER);
570   SetCyrFont(w);
571   drawString(x, y+r*3/8, "м/с", CENTER);
572
573 }
574
575 void drawsun(int x, int y, int scale, int linesize) {
576   display.fillRect(x - scale * 2, y, scale * 4, linesize, GxEPD_BLACK);
577   display.fillRect(x, y - scale * 2, linesize, scale * 4, GxEPD_BLACK);
578   display.drawLine(x - scale * 1.3, y - scale * 1.3, x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
579   display.drawLine(x - scale * 1.3, y + scale * 1.3, x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
580   if (linesize>1) {
581     display.drawLine(1 + x - scale * 1.3, y - scale * 1.3, 1 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
582     display.drawLine(2 + x - scale * 1.3, y - scale * 1.3, 2 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
583     display.drawLine(3 + x - scale * 1.3, y - scale * 1.3, 3 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
584     display.drawLine(1 + x - scale * 1.3, y + scale * 1.3, 1 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
585     display.drawLine(2 + x - scale * 1.3, y + scale * 1.3, 2 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
586     display.drawLine(3 + x - scale * 1.3, y + scale * 1.3, 3 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
587   }
588   display.fillCircle(x, y, scale * 1.3, GxEPD_WHITE);
589   display.fillCircle(x, y, scale, GxEPD_BLACK);
590   display.fillCircle(x, y, scale - linesize, GxEPD_WHITE);
591 }
592
593 void drawcloud(int x, int y, int scale, int linesize) {
594   //Draw cloud outer
595   display.fillCircle(x - scale * 3, y, scale, GxEPD_BLACK);                // Left most circle
596   display.fillCircle(x + scale * 3, y, scale, GxEPD_BLACK);                // Right most circle
597   display.fillCircle(x - scale, y - scale, scale * 1.4, GxEPD_BLACK);    // left middle upper circle
598   display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75, GxEPD_BLACK); // Right middle upper circle
599   display.fillRect(x - scale * 3 - 1, y - scale, scale * 6, scale * 2 + 1, GxEPD_BLACK); // Upper and lower lines
600   //Clear cloud inner
601   display.fillCircle(x - scale * 3, y, scale - linesize, GxEPD_WHITE);            // Clear left most circle
602   display.fillCircle(x + scale * 3, y, scale - linesize, GxEPD_WHITE);            // Clear right most circle
603   display.fillCircle(x - scale, y - scale, scale * 1.4 - linesize, GxEPD_WHITE);  // left middle upper circle
604   display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75 - linesize, GxEPD_WHITE); // Right middle upper circle
605   display.fillRect(x - scale * 3 + 2, y - scale + linesize - 1, scale * 5.9, scale * 2 - linesize * 2 + 2, GxEPD_WHITE); // Upper and lower lines
606 }
607
608 void drawraindrop(int x, int y, int scale) {
609   display.fillCircle(x, y, scale / 4, GxEPD_BLACK);
610   display.fillTriangle(x - scale / 4, y, x, y - scale, x + scale / 4, y , GxEPD_BLACK);
611   x = x + scale * 1.5; y = y + scale / 2;
612   display.fillCircle(x, y, scale / 4, GxEPD_BLACK);
613   display.fillTriangle(x - scale / 4, y, x, y - scale, x + scale / 4, y , GxEPD_BLACK);
614 }
615
616 void drawrain(int x, int y, int scale) {
617   for (int d = 0; d < 4; d++) {
618     drawraindrop(x + scale * (7.8 - d * 1.85) - scale * 5.2, y + scale * 2 - scale / 6, scale / 1.6);
619   }
620 }
621
622 void drawsnow(int x, int y, int scale) {
623   int dxo, dyo, dxi, dyi;
624   int delta = -scale/5;
625   for (int flakes = 0; flakes < 5; flakes++) {
626     for (int i = 0; i < 360; i = i + 60) {
627       dxo = 0.5 * scale * cos((i - 90) * 3.14 / 180); dxi = dxo * 0.1;
628       dyo = 0.5 * scale * sin((i - 90) * 3.14 / 180); dyi = dyo * 0.1;
629       display.drawLine(dxo + x + flakes * 1.5 * scale - scale * 3, dyo + y + delta + scale * 1.8, dxi + x + 0 + flakes * 1.5 * scale - scale * 3, dyi + y + delta + scale * 1.8, GxEPD_BLACK);
630     }
631     delta = - delta;
632   }
633 }
634
635 void drawtstorm(int x, int y, int scale) {
636   y = y + scale / 2;
637   int delta = -scale/5;
638   for (int i = 0; i < 4; i++) {
639     delta = - delta;
640     display.drawLine(x - scale * 3 + scale * i * 1.5 + 0, y + delta + scale * 1.4, x - scale * 2.5 + scale * i * 1.5 + 0, y + delta + scale * 0.9, GxEPD_BLACK);
641     if (scale > 15) {
642       display.drawLine(x - scale * 3 + scale * i * 1.5 + 1, y + delta + scale * 1.4, x - scale * 2.5 + scale * i * 1.5 + 1, y + delta + scale * 0.9, GxEPD_BLACK);
643       display.drawLine(x - scale * 3 + scale * i * 1.5 + 2, y + delta + scale * 1.4, x - scale * 2.5 + scale * i * 1.5 + 2, y + delta + scale * 0.9, GxEPD_BLACK);
644     }
645     display.drawLine(x - scale * 3 + scale * i * 1.5, y + delta + scale * 1.4 + 0, x - scale * 2 + scale * i * 1.5 + 0, y + delta + scale * 1.4 + 0, GxEPD_BLACK);
646     if (scale > 15) {
647       display.drawLine(x - scale * 3 + scale * i * 1.5, y + delta + scale * 1.4 + 1, x - scale * 2 + scale * i * 1.5 + 0, y + delta + scale * 1.4 + 1, GxEPD_BLACK);
648       display.drawLine(x - scale * 3 + scale * i * 1.5, y + delta + scale * 1.4 + 2, x - scale * 2 + scale * i * 1.5 + 0, y + delta + scale * 1.4 + 2, GxEPD_BLACK);
649     }
650     display.drawLine(x - scale * 2.5 + scale * i * 1.4 + 0, y + delta + scale * 2.4, x - scale * 2 + scale * i * 1.5 + 0, y + delta + scale * 1.4, GxEPD_BLACK);
651     if (scale > 15) {
652       display.drawLine(x - scale * 2.5 + scale * i * 1.4 + 1, y + delta + scale * 2.4, x - scale * 2 + scale * i * 1.5 + 1, y + delta + scale * 1.4, GxEPD_BLACK);
653       display.drawLine(x - scale * 2.5 + scale * i * 1.4 + 2, y + delta + scale * 2.4, x - scale * 2 + scale * i * 1.5 + 2, y + delta + scale * 1.4, GxEPD_BLACK);
654     }
655   }
656 }
657
658 void drawfog(int x, int y, int scale, int linesize) {
659   if (scale < 15) {
660     y -= 10;
661     linesize = 1;
662   }
663   for (int i = 0; i < 6; i++) {
664     display.fillRect(x - scale * 3, y + scale * 1.5, scale * 6, linesize, GxEPD_BLACK);
665     display.fillRect(x - scale * 3, y + scale * 2.0, scale * 6, linesize, GxEPD_BLACK);
666     display.fillRect(x - scale * 3, y + scale * 2.5, scale * 6, linesize, GxEPD_BLACK);
667   }
668 }
669
670 void Sunny(int x, int y, int scale) {
671   int linesize = 1;
672   if (scale>15) linesize=3;
673   drawsun(x, y, scale*6/5, linesize);
674 }
675
676 void MostlySunny(int x, int y, int scale) {
677   int linesize = 1, offset = 5;
678   if (scale>15) {
679     linesize = 3;
680     offset = 15;
681   }
682   drawcloud(x, y + offset, scale, linesize);
683   drawsun(x - scale * 1.8, y - scale * 1.8 + offset, scale, linesize);
684 }
685
686 void MostlyCloudy(int x, int y, int scale) {
687   int linesize = 1, offset = 5;
688   if (scale>15) {
689     linesize = 3;
690     offset = 15;
691   }
692   drawsun(x - scale * 1.8, y - scale * 1.8 + offset, scale, linesize);
693   drawcloud(x, y + offset, scale, linesize);
694 }
695
696 void Cloudy(int x, int y, int scale) {
697   int linesize = 1;
698   if (scale<=15) {
699     drawcloud(x, y+5, scale, linesize);
700   }
701   else {
702     linesize = 5;
703     y += scale/2;
704     drawcloud(x + scale * 2.5, y - scale * 1.8, scale/1.6, linesize); // Cloud top right
705     drawcloud(x - scale * 2, y - scale * 2, scale/2, linesize); // Cloud top left
706     drawcloud(x, y, scale, linesize);       // Main cloud
707   }
708 }
709
710 void Rain(int x, int y, int scale) {
711   int linesize = 3;
712   if (scale<15) {
713     linesize = 1;
714   }
715   drawcloud(x, y-scale/3, scale, linesize);
716   drawrain(x, y-scale/3, scale);
717 }
718
719 void ExpectRain(int x, int y, int scale) {
720   int linesize = 3;
721   if (scale<15) {
722     linesize = 1;
723   }
724   drawsun(x - scale * 1.8, y - scale * 1.6, scale, linesize);
725   drawcloud(x, y+scale/5, scale, linesize);
726   drawrain(x, y, scale);
727 }
728
729 void Tstorms(int x, int y, int scale) {
730   int linesize = 3;
731   if (scale<15) {
732     linesize = 1;
733   }
734   drawcloud(x, y, scale, linesize);
735   drawtstorm(x, y, scale);
736 }
737
738 void Snow(int x, int y, int scale) {
739   int linesize = 3;
740   if (scale<15) {
741     linesize = 1;
742   }
743   drawcloud(x, y, scale, linesize);
744   drawsnow(x, y, scale);
745 }
746
747 void Fog(int x, int y, int scale) {
748   int linesize = 3;
749   if (scale<15) {
750     linesize = 1;
751   }
752   drawcloud(x, y - 5, scale, linesize);
753   drawfog(x, y - 5, scale, linesize);
754 }
755
756 void Haze(int x, int y, int scale) {
757   int linesize = 3;
758   if (scale<15) {
759     linesize = 1;
760   }
761   drawsun(x, y - 5, scale*6/5, linesize);
762   drawfog(x, y - 5, scale, linesize);
763 }
764
765 void DisplayIcon(int x1, int x2, int y1, int y2, WeatherRecord weather) {
766
767   DrawBlock(x1,x2,y1,y2);
768
769   int x = (x1 + x2)/2;
770   int y = (y1 + y2)/2;
771   int h = (y2 - y1);
772   int w = (x2 - x1);
773
774   int scale = w/14;
775   int fscale = w/12;
776
777   String icon = weather.icon;
778   String description = weather.description;
779
780   if      (icon == "01d" || icon == "01n")  
781     Sunny(x, y, scale);
782   else if (icon == "02d" || icon == "02n")  
783     MostlySunny(x, y, scale);
784   else if (icon == "03d" || icon == "03n")  
785     MostlyCloudy(x, y, scale);
786   else if (icon == "04d" || icon == "04n")  
787     Cloudy(x, y, scale);
788   else if (icon == "09d" || icon == "09n")  
789     Rain(x, y, scale);
790   else if (icon == "10d" || icon == "10n")  
791     ExpectRain(x, y, scale);
792   else if (icon == "11d" || icon == "11n")  
793     Tstorms(x, y, scale);
794   else if (icon == "13d" || icon == "13n")  
795     Snow(x, y, scale);
796   else if (icon == "50d")                   
797     Haze(x, y, scale);
798   else if (icon == "50n")                   
799     Fog(x, y, scale);
800
801   SetCyrFont(fscale);
802   drawString(x, y2-fscale*2/3, description, CENTER); 
803
804 }
805
806 void DisplayUpdate(int x1, int x2, int y1, int y2, WeatherRecord weather) {
807
808   DrawBlock(x1,x2,y1,y2);
809
810   int x = (x1 + x2)/2;
811   int y = (y1 + y2)/2;
812   int h = (y2 - y1)/2;
813   int w = (x2 - x1)/16;
814   if (h<w) { w = h; }
815   SetCyrFont(w);
816   drawString(x, y1+w, "Прогноз от " + UnixTime(weather.dt + TimeZoneOffset), CENTER);
817   
818 }
819
820 void DisplayForecast(int x1, int x2, int y1, int y2, WeatherRecord weather) {
821
822   DrawBlock(x1,x2,y1,y2);
823
824   int x = (x1 + x2)/2;
825   int y = (y1 + y2)/2;
826   int h = (y2 - y1);
827   int w = (x2 - x1);
828
829   int scale = w/12;
830   int fscale = w/6;
831   if (fscale<6) fscale = 6;
832
833   String icon = weather.icon;
834   String temperature = String(weather.temp,1);
835
836   if      (icon == "01d" || icon == "01n")  
837     Sunny(x, y, scale);
838   else if (icon == "02d" || icon == "02n")  
839     MostlySunny(x, y, scale);
840   else if (icon == "03d" || icon == "03n")  
841     MostlyCloudy(x, y, scale);
842   else if (icon == "04d" || icon == "04n")  
843     Cloudy(x, y, scale);
844   else if (icon == "09d" || icon == "09n")  
845     Rain(x, y, scale);
846   else if (icon == "10d" || icon == "10n")  
847     ExpectRain(x, y, scale);
848   else if (icon == "11d" || icon == "11n")  
849     Tstorms(x, y, scale);
850   else if (icon == "13d" || icon == "13n")  
851     Snow(x, y, scale);
852   else if (icon == "50d")                   
853     Haze(x, y, scale);
854   else if (icon == "50n")                   
855     Fog(x, y, scale);
856
857   SetCyrFont(fscale);
858   drawString(x, y1+fscale*2/3, UnixTimeOnly(weather.dt+ TimeZoneOffset), CENTER); 
859   drawString(x, y2-fscale*2/3, temperature, CENTER); 
860
861 }
862
863 void DisplayGraph(int x1, int x2, int y1, int y2, int TimeArray[], float DataArray[], int readings, boolean barchart_mode, String label) {
864
865   DrawBlock(x1,x2,y1,y2);
866
867   float maxY = -10000;
868   float minY =  10000;
869
870   int last_x, last_y;
871
872   int scale = (y2 - y1)/10;
873
874   SetCyrFont(scale);
875   drawString((x1+x2)/2, y1+scale*2/3, label, CENTER); 
876   
877   int x_pos = x1 + 3 + scale*2;
878   int y_pos = y2 - 3;
879
880   int h = y2 - y1 - 6 - scale;
881   int w = x2 - x1 - 8 - scale*2;
882
883   float x_new, y_new;
884   
885   for (int i = 1; i < readings; i++ ) {
886     if (DataArray[i] >= maxY) maxY = DataArray[i];
887     if (DataArray[i] <= minY) minY = DataArray[i];
888   }
889   maxY = round(maxY + 0.5);
890   minY = round(minY);
891
892   last_x = x_pos + 1;
893   last_y = y_pos - (constrain(DataArray[0], minY, maxY) - minY) / (maxY - minY) * h -1;
894   
895   display.drawRect(x_pos, y_pos-h, w, h, GxEPD_BLACK);
896
897   for (int gx = 1; gx < readings; gx++) {
898     x_new = x_pos + gx * w / (readings - 1) - 1 ;
899     y_new = y_pos - (constrain(DataArray[gx], minY, maxY) - minY) / (maxY - minY) * h -1;
900     if (barchart_mode) {
901       if (DataArray[gx]!=0) {
902         display.fillRect(x_new, y_new, (w / readings) - 1, y_pos - y_new, GxEPD_BLACK);
903       }
904     } else {
905       display.drawLine(last_x, last_y, x_new, y_new, GxEPD_BLACK);
906     }
907     last_x = x_new;
908     last_y = y_new;
909   }
910
911 #define y_minor_axis 5
912 #define number_of_dashes 20
913   SetNumFont(scale/1.3);
914   for (int spacing = 1; spacing < y_minor_axis; spacing++) {
915     for (int j = 0; j < number_of_dashes; j++) {
916       if (spacing < y_minor_axis) display.drawFastHLine((x_pos + 3 + j * w / number_of_dashes), y_pos - (h * spacing / y_minor_axis), w / (2 * number_of_dashes), GxEPD_BLACK);
917     }
918     drawString(x_pos - 1, y_pos - h * spacing / y_minor_axis, String((minY + (float)(maxY - minY) / y_minor_axis * spacing + 0.01), 1), RIGHT);
919   }
920 }
921
922 void DisplayWeather() {
923   Serial.println("Displaying...");
924   DisplayWind(20,235,20,225, CurrentWeather);
925   DisplayCurrent(240,515,20,225,CurrentWeather);
926   DisplayIcon(520,775,60,225,CurrentWeather);
927   DisplayUpdate(520,775,20,55,CurrentWeather);
928   int x=20;
929   for (int i=0; i<MaxHourlyFC; i+=3) {
930     DisplayForecast(x,x+90,235,305,HourlyForecasts[i]);
931     x = x + 95;
932     if (x+95>780) break;
933   }
934   DisplayGraph(20,268,315,460,HourlyDT,HourlyTemp,MaxHourlyFC,false,"Температура");
935   DisplayGraph(273,522,315,460,HourlyDT,HourlyPressure,MaxHourlyFC,false,"Давление");
936   DisplayGraph(527,775,315,460,HourlyDT,HourlyPrecip,MaxHourlyFC,true,"Осадки");
937   display.display();
938 }
939
940 void loop() {
941
942   delay(2000);
943   WiFiManager wifiManager;
944   wifiManager.setTimeout(10);
945   if(!wifiManager.autoConnect()) {
946
947     Serial.println("failed to connect and hit timeout");
948   
949   } else {
950
951     if (Serial.available() && Serial.read() == 'c') {
952       WiFiSetup();
953     }
954   
955     // put your main code here, to run repeatedly:
956     byte Attempts = 1;
957     WiFiClient client;   // wifi client object
958     for (byte i=1; i<=3; i++) {
959       if (GetWeather(client)) {
960         GetCurrentWeather(client);
961         WiFi.mode(WIFI_OFF);
962         btStop();
963         DisplayWeather();
964
965         int CurrentMin, CurrentSec;
966         struct tm timeinfo;
967         getLocalTime(&timeinfo, 10000);
968         CurrentMin  = timeinfo.tm_min;
969         CurrentSec  = timeinfo.tm_sec;
970         long SleepTimer = (SleepDuration * 60 - ((CurrentMin % SleepDuration) * 60 + CurrentSec));
971         Serial.print("Going to long sleep for ");
972         Serial.print(SleepTimer);
973         Serial.println(" seconds now");
974         delay(1000);
975         Serial.flush(); 
976
977         esp_sleep_enable_timer_wakeup((SleepTimer+5)*1000*1000);
978         esp_deep_sleep_start();
979       }
980     }
981
982   }  
983
984   Serial.println("Going to short sleep for 120 seconds now ");
985   delay(1000);
986   Serial.flush(); 
987   esp_sleep_enable_timer_wakeup(120*1000*1000);
988   esp_deep_sleep_start();
989
990 }
991
992