added weather forecast fetch from open-meteo
This commit is contained in:
+3
-1
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "am2302_sensor.h"
|
||||
#include "internet_time.h"
|
||||
#include "weather_forecast.h"
|
||||
#include "wifi_connection.h"
|
||||
|
||||
void setInternetTimeInfo(InternetTimeInfo timeInfo);
|
||||
@@ -13,6 +14,7 @@ void drawHelloWorldScreen(U8G2 &u8g2);
|
||||
void drawInternetConnectionScreen(U8G2 &u8g2, WifiConnectionInfo wifiInfo);
|
||||
void setEnvironmentReading(Am2302Reading reading);
|
||||
void drawEnvironmentScreen(U8G2 &u8g2);
|
||||
void drawWeatherIconScreen(U8G2 &u8g2);
|
||||
void setWeatherForecastInfo(WeatherForecastInfo forecastInfo);
|
||||
void drawWeatherForecastScreen(U8G2 &u8g2);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef WEATHER_FORECAST_H
|
||||
#define WEATHER_FORECAST_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
struct WeatherForecastInfo
|
||||
{
|
||||
bool valid;
|
||||
float currentTemperatureC;
|
||||
float minTemperatureC;
|
||||
float maxTemperatureC;
|
||||
float precipitationMm;
|
||||
uint8_t precipitationProbabilityPercent;
|
||||
int weatherCode;
|
||||
};
|
||||
|
||||
class WeatherForecast
|
||||
{
|
||||
public:
|
||||
WeatherForecast();
|
||||
void update(unsigned long now, bool wifiConnected);
|
||||
WeatherForecastInfo info() const;
|
||||
bool consumeDirty();
|
||||
|
||||
private:
|
||||
void fetch(unsigned long now);
|
||||
bool readResponse(String &body);
|
||||
bool parseForecast(const String &body, WeatherForecastInfo &forecast) const;
|
||||
bool parseFloatArray(const String &body, const char *name, float *values, size_t maxCount, size_t &count) const;
|
||||
bool parseIntArray(const String &body, const char *name, int *values, size_t maxCount, size_t &count) const;
|
||||
|
||||
WeatherForecastInfo current_;
|
||||
unsigned long nextAttempt_;
|
||||
bool dirty_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -7,4 +7,8 @@ const char WIFI_PASSWORD[] = "your-wifi-password";
|
||||
// Offset from UTC in seconds. Examples: Finland winter 7200, Finland summer 10800.
|
||||
#define TIME_UTC_OFFSET_SECONDS 0
|
||||
|
||||
// Coordinates used for the weather forecast.
|
||||
#define WEATHER_LATITUDE 60.1699
|
||||
#define WEATHER_LONGITUDE 24.9384
|
||||
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#include "carousel.h"
|
||||
|
||||
const unsigned long pageInterval = 5000;
|
||||
const unsigned long pageInterval = 30000;
|
||||
|
||||
Carousel::Carousel(Screen *screens, size_t screenCount)
|
||||
: screens_(screens),
|
||||
|
||||
+11
-1
@@ -7,6 +7,7 @@
|
||||
#include "internet_time.h"
|
||||
#include "menu.h"
|
||||
#include "screens.h"
|
||||
#include "weather_forecast.h"
|
||||
#include "wifi_connection.h"
|
||||
|
||||
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R2, U8X8_PIN_NONE);
|
||||
@@ -18,6 +19,7 @@ uint8_t buttonSelect = A5;
|
||||
Am2302Sensor am2302(A3);
|
||||
WifiConnection wifiConnection;
|
||||
InternetTime internetTime;
|
||||
WeatherForecast weatherForecast;
|
||||
|
||||
enum class AppMode
|
||||
{
|
||||
@@ -30,7 +32,7 @@ enum class AppMode
|
||||
Screen screens[] = {
|
||||
{ drawTimeScreen },
|
||||
{ drawEnvironmentScreen },
|
||||
{ drawWeatherIconScreen }
|
||||
{ drawWeatherForecastScreen }
|
||||
};
|
||||
Carousel carousel(screens, sizeof(screens) / sizeof(screens[0]));
|
||||
Menu menu;
|
||||
@@ -145,6 +147,7 @@ void loop(){
|
||||
const unsigned long now = millis();
|
||||
wifiConnection.update(now);
|
||||
internetTime.update(now, wifiConnection.info().connected);
|
||||
weatherForecast.update(now, wifiConnection.info().connected);
|
||||
|
||||
if (internetTime.consumeDirty()) {
|
||||
setInternetTimeInfo(internetTime.info());
|
||||
@@ -153,6 +156,13 @@ void loop(){
|
||||
}
|
||||
}
|
||||
|
||||
if (weatherForecast.consumeDirty()) {
|
||||
setWeatherForecastInfo(weatherForecast.info());
|
||||
if (appMode == AppMode::Carousel) {
|
||||
carousel.requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
if (appMode == AppMode::Carousel) {
|
||||
carousel.update(now);
|
||||
}
|
||||
|
||||
+78
-22
@@ -4,6 +4,7 @@ namespace
|
||||
{
|
||||
Am2302Reading environmentReading = {0.0F, 0.0F, false};
|
||||
InternetTimeInfo internetTimeInfo = {false, 0, 0, 0, 0, 0};
|
||||
WeatherForecastInfo weatherForecastInfo = {false, 0.0F, 0.0F, 0.0F, 0.0F, 0, 0};
|
||||
|
||||
void printTwoDigits(U8G2 &u8g2, uint8_t value)
|
||||
{
|
||||
@@ -13,26 +14,50 @@ void printTwoDigits(U8G2 &u8g2, uint8_t value)
|
||||
u8g2.print(value);
|
||||
}
|
||||
|
||||
void drawSun(U8G2 &u8g2, uint8_t x, uint8_t y)
|
||||
const char *weatherDescription(int code)
|
||||
{
|
||||
u8g2.drawDisc(x, y, 11);
|
||||
u8g2.drawLine(x, y - 18, x, y - 14);
|
||||
u8g2.drawLine(x, y + 14, x, y + 18);
|
||||
u8g2.drawLine(x - 18, y, x - 14, y);
|
||||
u8g2.drawLine(x + 14, y, x + 18, y);
|
||||
u8g2.drawLine(x - 13, y - 13, x - 10, y - 10);
|
||||
u8g2.drawLine(x + 10, y - 10, x + 13, y - 13);
|
||||
u8g2.drawLine(x - 13, y + 13, x - 10, y + 10);
|
||||
u8g2.drawLine(x + 10, y + 10, x + 13, y + 13);
|
||||
}
|
||||
|
||||
void drawCloud(U8G2 &u8g2, uint8_t x, uint8_t y)
|
||||
{
|
||||
u8g2.drawDisc(x + 18, y + 14, 12);
|
||||
u8g2.drawDisc(x + 34, y + 8, 17);
|
||||
u8g2.drawDisc(x + 52, y + 15, 13);
|
||||
u8g2.drawBox(x + 17, y + 15, 38, 14);
|
||||
u8g2.drawRBox(x + 8, y + 18, 58, 15, 7);
|
||||
switch (code) {
|
||||
case 0:
|
||||
return "Clear";
|
||||
case 1:
|
||||
case 2:
|
||||
return "Partly cloudy";
|
||||
case 3:
|
||||
return "Cloudy";
|
||||
case 45:
|
||||
case 48:
|
||||
return "Fog";
|
||||
case 51:
|
||||
case 53:
|
||||
case 55:
|
||||
case 56:
|
||||
case 57:
|
||||
return "Drizzle";
|
||||
case 61:
|
||||
case 63:
|
||||
case 65:
|
||||
case 66:
|
||||
case 67:
|
||||
return "Rain";
|
||||
case 71:
|
||||
case 73:
|
||||
case 75:
|
||||
case 77:
|
||||
return "Snow";
|
||||
case 80:
|
||||
case 81:
|
||||
case 82:
|
||||
return "Showers";
|
||||
case 85:
|
||||
case 86:
|
||||
return "Snow showers";
|
||||
case 95:
|
||||
case 96:
|
||||
case 99:
|
||||
return "Thunder";
|
||||
default:
|
||||
return "Forecast";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +144,39 @@ void drawEnvironmentScreen(U8G2 &u8g2)
|
||||
u8g2.print(" %");
|
||||
}
|
||||
|
||||
void drawWeatherIconScreen(U8G2 &u8g2)
|
||||
void setWeatherForecastInfo(WeatherForecastInfo forecastInfo)
|
||||
{
|
||||
drawSun(u8g2, 78, 22);
|
||||
drawCloud(u8g2, 29, 24);
|
||||
weatherForecastInfo = forecastInfo;
|
||||
}
|
||||
|
||||
void drawWeatherForecastScreen(U8G2 &u8g2)
|
||||
{
|
||||
u8g2.setFont(u8g2_font_6x12_tr);
|
||||
u8g2.drawStr(0, 12, "Weather next 12h");
|
||||
|
||||
if (!weatherForecastInfo.valid) {
|
||||
u8g2.drawStr(0, 34, "Weather has not");
|
||||
u8g2.drawStr(0, 50, "been updated");
|
||||
return;
|
||||
}
|
||||
|
||||
u8g2.setCursor(0, 28);
|
||||
u8g2.print("Now ");
|
||||
u8g2.print(weatherForecastInfo.currentTemperatureC, 1);
|
||||
u8g2.print(" C ");
|
||||
u8g2.print(weatherDescription(weatherForecastInfo.weatherCode));
|
||||
|
||||
u8g2.setCursor(0, 42);
|
||||
u8g2.print("Temp ");
|
||||
u8g2.print(weatherForecastInfo.minTemperatureC, 0);
|
||||
u8g2.print("-");
|
||||
u8g2.print(weatherForecastInfo.maxTemperatureC, 0);
|
||||
u8g2.print(" C");
|
||||
|
||||
u8g2.setCursor(0, 56);
|
||||
u8g2.print("Rain ");
|
||||
u8g2.print(weatherForecastInfo.precipitationProbabilityPercent);
|
||||
u8g2.print("% ");
|
||||
u8g2.print(weatherForecastInfo.precipitationMm, 1);
|
||||
u8g2.print(" mm");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
#include "weather_forecast.h"
|
||||
|
||||
#include <WiFi101.h>
|
||||
|
||||
#include "wifi_credentials.h"
|
||||
|
||||
#ifndef WEATHER_LATITUDE
|
||||
#define WEATHER_LATITUDE 0.0
|
||||
#endif
|
||||
|
||||
#ifndef WEATHER_LONGITUDE
|
||||
#define WEATHER_LONGITUDE 0.0
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
const char weatherHost[] = "api.open-meteo.com";
|
||||
const unsigned long retryInterval = 300000;
|
||||
const unsigned long refreshInterval = 3600000;
|
||||
const size_t forecastHours = 12;
|
||||
const size_t maxResponseLength = 3500;
|
||||
|
||||
const char *weatherRequestPath()
|
||||
{
|
||||
static String path;
|
||||
path = "/v1/forecast?latitude=";
|
||||
path += String(WEATHER_LATITUDE, 4);
|
||||
path += "&longitude=";
|
||||
path += String(WEATHER_LONGITUDE, 4);
|
||||
path += "&hourly=temperature_2m,precipitation_probability,precipitation,weather_code";
|
||||
path += "&forecast_hours=12&timezone=auto&forecast_days=1";
|
||||
return path.c_str();
|
||||
}
|
||||
|
||||
const char *arrayStartForName(const String &body, const char *name)
|
||||
{
|
||||
const String key = String("\"") + name + "\":[";
|
||||
const int start = body.indexOf(key);
|
||||
if (start < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return body.c_str() + start + key.length();
|
||||
}
|
||||
|
||||
void skipWhitespace(const char *&cursor)
|
||||
{
|
||||
while (*cursor == ' ' || *cursor == '\n' || *cursor == '\r' || *cursor == '\t') {
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WeatherForecast::WeatherForecast()
|
||||
: current_({false, 0.0F, 0.0F, 0.0F, 0.0F, 0, 0}),
|
||||
nextAttempt_(0),
|
||||
dirty_(true) {}
|
||||
|
||||
void WeatherForecast::update(unsigned long now, bool wifiConnected)
|
||||
{
|
||||
if (!wifiConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (now >= nextAttempt_) {
|
||||
fetch(now);
|
||||
}
|
||||
}
|
||||
|
||||
WeatherForecastInfo WeatherForecast::info() const
|
||||
{
|
||||
return current_;
|
||||
}
|
||||
|
||||
bool WeatherForecast::consumeDirty()
|
||||
{
|
||||
const bool d = dirty_;
|
||||
dirty_ = false;
|
||||
return d;
|
||||
}
|
||||
|
||||
void WeatherForecast::fetch(unsigned long now)
|
||||
{
|
||||
nextAttempt_ = now + retryInterval;
|
||||
|
||||
String body;
|
||||
if (!readResponse(body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WeatherForecastInfo forecast;
|
||||
if (!parseForecast(body, forecast)) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_ = forecast;
|
||||
dirty_ = true;
|
||||
nextAttempt_ = now + refreshInterval;
|
||||
}
|
||||
|
||||
bool WeatherForecast::readResponse(String &body)
|
||||
{
|
||||
WiFiClient client;
|
||||
if (!client.connect(weatherHost, 80)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
client.print("GET ");
|
||||
client.print(weatherRequestPath());
|
||||
client.println(" HTTP/1.0");
|
||||
client.print("Host: ");
|
||||
client.println(weatherHost);
|
||||
client.println("Connection: close");
|
||||
client.println();
|
||||
|
||||
const unsigned long started = millis();
|
||||
bool inBody = false;
|
||||
char headerTail[4] = {0, 0, 0, 0};
|
||||
while ((client.connected() || client.available()) && millis() - started < 10000) {
|
||||
while (client.available()) {
|
||||
const char c = client.read();
|
||||
|
||||
if (!inBody) {
|
||||
headerTail[0] = headerTail[1];
|
||||
headerTail[1] = headerTail[2];
|
||||
headerTail[2] = headerTail[3];
|
||||
headerTail[3] = c;
|
||||
inBody = headerTail[0] == '\r' &&
|
||||
headerTail[1] == '\n' &&
|
||||
headerTail[2] == '\r' &&
|
||||
headerTail[3] == '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (body.length() < maxResponseLength) {
|
||||
body += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.stop();
|
||||
return body.indexOf("\"hourly\"") >= 0;
|
||||
}
|
||||
|
||||
bool WeatherForecast::parseForecast(const String &body, WeatherForecastInfo &forecast) const
|
||||
{
|
||||
float temperatures[forecastHours];
|
||||
float precipitation[forecastHours];
|
||||
int precipitationProbability[forecastHours];
|
||||
int weatherCodes[forecastHours];
|
||||
size_t temperatureCount = 0;
|
||||
size_t precipitationCount = 0;
|
||||
size_t probabilityCount = 0;
|
||||
size_t weatherCodeCount = 0;
|
||||
|
||||
if (!parseFloatArray(body, "temperature_2m", temperatures, forecastHours, temperatureCount) ||
|
||||
!parseFloatArray(body, "precipitation", precipitation, forecastHours, precipitationCount) ||
|
||||
!parseIntArray(body, "precipitation_probability", precipitationProbability, forecastHours, probabilityCount) ||
|
||||
!parseIntArray(body, "weather_code", weatherCodes, forecastHours, weatherCodeCount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (temperatureCount == 0 ||
|
||||
precipitationCount == 0 ||
|
||||
probabilityCount == 0 ||
|
||||
weatherCodeCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float minTemperature = temperatures[0];
|
||||
float maxTemperature = temperatures[0];
|
||||
float totalPrecipitation = 0.0F;
|
||||
uint8_t maxProbability = 0;
|
||||
|
||||
for (size_t i = 0; i < temperatureCount; i++) {
|
||||
if (temperatures[i] < minTemperature) {
|
||||
minTemperature = temperatures[i];
|
||||
}
|
||||
if (temperatures[i] > maxTemperature) {
|
||||
maxTemperature = temperatures[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < precipitationCount; i++) {
|
||||
totalPrecipitation += precipitation[i];
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < probabilityCount; i++) {
|
||||
if (precipitationProbability[i] > maxProbability) {
|
||||
maxProbability = precipitationProbability[i];
|
||||
}
|
||||
}
|
||||
|
||||
forecast = {
|
||||
true,
|
||||
temperatures[0],
|
||||
minTemperature,
|
||||
maxTemperature,
|
||||
totalPrecipitation,
|
||||
maxProbability,
|
||||
weatherCodes[0]
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WeatherForecast::parseFloatArray(const String &body, const char *name, float *values, size_t maxCount, size_t &count) const
|
||||
{
|
||||
const char *cursor = arrayStartForName(body, name);
|
||||
if (cursor == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
count = 0;
|
||||
while (*cursor != '\0' && *cursor != ']' && count < maxCount) {
|
||||
skipWhitespace(cursor);
|
||||
values[count++] = atof(cursor);
|
||||
while (*cursor != '\0' && *cursor != ',' && *cursor != ']') {
|
||||
cursor++;
|
||||
}
|
||||
if (*cursor == ',') {
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
bool WeatherForecast::parseIntArray(const String &body, const char *name, int *values, size_t maxCount, size_t &count) const
|
||||
{
|
||||
const char *cursor = arrayStartForName(body, name);
|
||||
if (cursor == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
count = 0;
|
||||
while (*cursor != '\0' && *cursor != ']' && count < maxCount) {
|
||||
skipWhitespace(cursor);
|
||||
values[count++] = atoi(cursor);
|
||||
while (*cursor != '\0' && *cursor != ',' && *cursor != ']') {
|
||||
cursor++;
|
||||
}
|
||||
if (*cursor == ',') {
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user