Weather usermod, more blaz recommendations

- add WEATHER_DEBUG 
- move globals to class statics => create forwards, move effect to end of file, refer to it as <class>::<var>
- move http get to httpGet utility function
- add errorMessage variable and show in Info
- increase JSON doc from 20000 to 24000 bytes
This commit is contained in:
Ewowi
2022-08-11 18:15:51 +02:00
parent cb425d0711
commit 1e53b2bcf5

View File

@@ -2,7 +2,12 @@
#include "wled.h"
#define FX_MODE_2DWEATHER 185 // can we do this here? Can we also increase modeCount here?
// #define WEATHER_DEBUG
//forward declarations as usermod class needs them
uint16_t mode_2DWeather(void);
//would like _data_FX_MODE_2DWEATHER after effect declaration but due to class static vars not possible...
static const char _data_FX_MODE_2DWEATHER[] PROGMEM = "Weather@;!;!;pal=54,2d"; //temperature palette
//utility function, move somewhere else???
void epochToString(time_t time, char *timeString) {
@@ -11,92 +16,61 @@ void epochToString(time_t time, char *timeString) {
sprintf(timeString, "%04d-%02d-%02d %02d:%02d:%02d", tm.Year + 1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
}
//globals used in effect
static uint32_t pushLoop = 0; //effect pushes loop to execute. might be interesting for audioreactive too
static uint8_t units = 1; //metric (celsius) is default. (Standard=Kelvin, Imperial is Fahrenheit)
static float temps[100]; //array of temperatures
static time_t times[100]; //array of corresponding times
static float minTemp = 0; //config var
static float maxTemp = 40; //config var
//effect function
uint16_t mode_2DWeather(void) {
pushLoop = millis(); //will be reset to 0 in usermod loop
SEGMENT.fadeToBlackBy(10);
float currentTemp = 0;
// time_t currentTime = 0;
for (int x=0; x<SEGMENT.virtualWidth(); x++) {
CRGB color;
currentTemp = temps[0];
// currentTime = times[0];
if (times[x%100] < localTime && times[(x+1)%100] >= localTime) {
color = RED;
currentTemp = map(localTime, times[x%100], times[(x+1)%100], temps[x%100] * 1000, temps[(x+1)%100] * 1000) / 1000.0;
// currentTime = localTime;
}
else
color = ColorFromPalette(SEGPALETTE, map((uint8_t)temps[x%100], 0, 40, 0, 255), 255, LINEARBLEND);
for (int y=0; y<SEGMENT.virtualHeight() * (temps[x%100]-minTemp)/(maxTemp - minTemp); y++) {
SEGMENT.setPixelColorXY(x, SEGMENT.virtualHeight() - y, color);
}
// Serial.print(" ");
// Serial.print(temps[x%16]);
}
if (localTime < times[0])
currentTemp = map(localTime, times[0], times[1], temps[0] * 1000, temps[1] * 1000) / 1000.0;
// Serial.print(" time ");
// char timeString[64];
// epochToString(currentTime, timeString);
// Serial.print(timeString);
// Serial.print(" temp ");
char tempString[5] = "";
sprintf(tempString, "%5.2f", currentTemp);
// Serial.println();
CRGB color = ColorFromPalette(SEGPALETTE, map((uint8_t)currentTemp, 0, 40, 0, 255), 255, LINEARBLEND);
if (SEGMENT.virtualWidth() < 16) {
SEGMENT.drawCharacter(tempString[0], 0, -2, 5, 8, color);
SEGMENT.drawCharacter(tempString[1], 4, -2, 5, 8, color);
SEGMENT.setPixelColorXY(8, 4, color);
SEGMENT.drawCharacter(tempString[3], 9, -2, 5, 8, color);
//utility function, move somewhere else???
void httpGet(WiFiClient &client, const char *url, char *errorMessage) {
//https://arduinojson.org/v6/example/http-client/
//is this the most compact way to do http get and put it in arduinojson object???
//would like async response ... ???
client.setTimeout(10000);
if (!client.connect("api.openweathermap.org", 80)) {
strcat(errorMessage, PSTR("Connection failed"));
}
else {
SEGMENT.drawCharacter(tempString[0], 0, -2, 5, 8, color);
SEGMENT.drawCharacter(tempString[1], 5, -2, 5, 8, color);
SEGMENT.setPixelColorXY(10, 4, color);
SEGMENT.drawCharacter(tempString[3], 12, -2, 5, 8, color);
SEGMENT.drawCharacter(tempString[4], 17, -2, 5, 8, color);
// Send HTTP request
client.println(url);
client.println(F("Host: api.openweathermap.org"));
client.println(F("Connection: close"));
if (client.println() == 0) {
strcat(errorMessage, PSTR("Failed to send request"));
}
else {
// Check HTTP status
char status[32] = {0};
client.readBytesUntil('\r', status, sizeof(status));
if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
strcat(errorMessage, PSTR("Unexpected response: "));
strcat(errorMessage, status);
}
else {
// Skip HTTP headers
char endOfHeaders[] = "\r\n\r\n";
if (!client.find(endOfHeaders)) {
strcat(errorMessage, PSTR("Invalid response"));
}
}
}
}
return FRAMETIME;
}
static const char _data_FX_MODE_2DWEATHER[] PROGMEM = "Weather@;!;!;pal=54,2d"; //temperature palette
class WeatherUsermod : public Usermod {
private:
unsigned long lastTime = 0; //will be used to download new forecast every hour
// strings to reduce flash memory usage (used more than twice)
static const char _name[]; //usermod name
static String apiKey; //config var
//config variables
String apiKey = "";
unsigned long lastTime = 0; //will be used to download new forecast every hour
char errorMessage[100] = "";
const char *cityName;
const char *countryName;
public:
//declare class static variables used in weather effect
static uint8_t units; //config var metric (celsius) is default. (Standard=Kelvin, Imperial is Fahrenheit)
static float minTemp; //config var
static float maxTemp; //config var
static uint32_t pushLoop; //effect pushes loop to execute. might be interesting for audioreactive too
static float temps[100]; //array of temperatures
static time_t times[100]; //array of corresponding times
void setup() {
strip.addEffect(255, &mode_2DWeather, _data_FX_MODE_2DWEATHER);
}
@@ -112,106 +86,74 @@ class WeatherUsermod : public Usermod {
char url[180];
sprintf(url, "GET /data/2.5/forecast?lat=%f&lon=%f&appid=%s&units=%s HTTP/1.0", latitude, longitude, apiKey.c_str(), units==0?"standard":units==1?"metric":"imperial");
#ifdef WLED_DEBUG
#ifdef WEATHER_DEBUG
Serial.println(url);
#endif
//https://arduinojson.org/v6/example/http-client/
//is this the most compact way to do http get and put it in arduinojson object???
httpGet(client, url, errorMessage);
client.setTimeout(10000);
if (!client.connect("api.openweathermap.org", 80)) {
Serial.println(F("Connection failed"));
return;
}
if (strcmp(errorMessage, "") == 0) {
Serial.println("after httpget");
// Allocate the JSON document
// Use arduinojson.org/v6/assistant to compute the capacity.
// const size_t capacity = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60;
DynamicJsonDocument weatherDoc(24000); //in practive about 20.000
// Send HTTP request
client.println(url);
client.println(F("Host: api.openweathermap.org"));
client.println(F("Connection: close"));
if (client.println() == 0) {
Serial.println(F("Failed to send request"));
client.stop();
return;
}
// Parse JSON object
DeserializationError error = deserializeJson(weatherDoc, client);
if (error) {
#ifdef WEATHER_DEBUG
Serial.printf("weatherDoc %u / %u%% (%u %u %u)\n", (unsigned int)weatherDoc.memoryUsage(), 100 * weatherDoc.memoryUsage() / weatherDoc.capacity(), (unsigned int)weatherDoc.size(), weatherDoc.overflowed(), (unsigned int)weatherDoc.nesting());
#endif
strcat(errorMessage, PSTR("deserializeJson() failed: "));
strcat(errorMessage, error.c_str());
}
else { //everything successful!!
JsonObject weatherDocObject = weatherDoc.as<JsonObject>();
JsonArray list = weatherDocObject[F("list")];
JsonObject city = weatherDocObject["city"];
strcat(errorMessage, city["name"]); //api succesfull
strcat(errorMessage, city["country"]);
// Check HTTP status
char status[32] = {0};
client.readBytesUntil('\r', status, sizeof(status));
if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
Serial.print(F("Unexpected response: "));
Serial.println(status);
client.stop();
return;
}
uint8_t i = 0;
for (JsonObject listElement: list) {
times[i%100] = listElement["dt"];
// Skip HTTP headers
char endOfHeaders[] = "\r\n\r\n";
if (!client.find(endOfHeaders)) {
Serial.println(F("Invalid response"));
client.stop();
return;
}
JsonObject main = listElement["main"];
temps[i%100] = main["temp"];
// Allocate the JSON document
// Use arduinojson.org/v6/assistant to compute the capacity.
// const size_t capacity = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60;
DynamicJsonDocument weatherDoc(20000);
#ifdef WEATHER_DEBUG
char timeString[64];
epochToString(listElement["dt"], timeString);
Serial.print(timeString);
// Parse JSON object
DeserializationError error = deserializeJson(weatherDoc, client);
if (error) {
Serial.print(F("deserializeJson() failed: "));
Serial.println(error.f_str());
client.stop();
return;
Serial.print(" temp ");
Serial.print(temps[i%100]);
Serial.print(" city ");
Serial.print(errorMessage);
Serial.print(" sunrise ");
char sunriseString[64];
epochToString(city["sunrise"], sunriseString);
Serial.print(sunriseString);
Serial.print(" localtime ");
char localTimeString[64];
epochToString(localTime, localTimeString);
Serial.print(localTimeString);
Serial.println();
#endif
i++;
}
}
}
// Disconnect
client.stop();
JsonObject weatherDocObject = weatherDoc.as<JsonObject>();
JsonArray list = weatherDocObject[F("list")];
JsonObject city = weatherDocObject["city"];
cityName = city["name"];
countryName = city["country"];
uint8_t i = 0;
for (JsonObject listElement: list) {
times[i%100] = listElement["dt"];
JsonObject main = listElement["main"];
temps[i%100] = main["temp"];
#ifdef WLED_DEBUG
char timeString[64];
epochToString(listElement["dt"], timeString);
Serial.print(timeString);
Serial.print(" temp ");
Serial.print(main["temp"]);
Serial.print(" city ");
Serial.print(cityName);
Serial.print(" - ");
Serial.print(countryName);
Serial.print(" sunrise ");
char sunriseString[64];
epochToString(city["sunrise"], sunriseString);
Serial.print(sunriseString);
Serial.print(" localtime ");
char localTimeString[64];
epochToString(localTime, localTimeString);
Serial.print(localTimeString);
Serial.println();
#endif
i++;
}
}
pushLoop = 0;
}
@@ -227,8 +169,8 @@ class WeatherUsermod : public Usermod {
if (user.isNull()) user = root.createNestedObject("u");
JsonArray infoArr = user.createNestedArray(FPSTR(_name));
infoArr.add(cityName); //value
infoArr.add(countryName); //unit
infoArr.add(errorMessage); //value
// infoArr.add(""); //unit
}
@@ -286,7 +228,7 @@ class WeatherUsermod : public Usermod {
oappend(SET_F("addOption(dd,'Fahrenheit',2);"));
oappend(SET_F("addInfo('WeatherUserMod:units',1,'<i>Set time and location in time settings</i>');"));
oappend(SET_F("addInfo('WeatherUserMod:apiKey',1,'<i>Create acount on openweathermap.org and copy the key</i>');"));
oappend(SET_F("addInfo('WeatherUserMod:minTemp',1,'<i>Reboot if you change apiKey to reload forecast</i>');"));
oappend(SET_F("addInfo('WeatherUserMod:minTemp',1,'<i>Changing values: Reboot to (re)load forecast</i>');"));
}
/*
@@ -312,3 +254,74 @@ class WeatherUsermod : public Usermod {
// strings to reduce flash memory usage (used more than twice)
const char WeatherUsermod::_name[] PROGMEM = "WeatherUserMod";
//define class static variables used in weather effect
String WeatherUsermod::apiKey = ""; //config var
float WeatherUsermod::minTemp = 0; //config var
float WeatherUsermod::maxTemp = 40; //config var
uint8_t WeatherUsermod::units = 1; //config var: metric (celsius) is default. (Standard=Kelvin, Imperial is Fahrenheit)
uint32_t WeatherUsermod::pushLoop = 0;
float WeatherUsermod::temps[100]; //array of temperatures
time_t WeatherUsermod::times[100]; //array of corresponding times
//effect function
uint16_t mode_2DWeather(void) {
WeatherUsermod::pushLoop = millis(); //will be reset to 0 in usermod loop
SEGMENT.fadeToBlackBy(10);
float currentTemp = 0;
// time_t currentTime = 0;
for (int x=0; x<SEGMENT.virtualWidth(); x++) {
CRGB color;
currentTemp = WeatherUsermod::temps[0];
// currentTime = times[0];
if (WeatherUsermod::times[x%100] < localTime && WeatherUsermod::times[(x+1)%100] >= localTime) {
color = RED;
currentTemp = map(localTime, WeatherUsermod::times[x%100], WeatherUsermod::times[(x+1)%100], WeatherUsermod::temps[x%100] * 1000, WeatherUsermod::temps[(x+1)%100] * 1000) / 1000.0;
// currentTime = localTime;
}
else
color = ColorFromPalette(SEGPALETTE, map((uint8_t)WeatherUsermod::temps[x%100], 0, 40, 0, 255), 255, LINEARBLEND);
for (int y=0; y<SEGMENT.virtualHeight() * (WeatherUsermod::temps[x%100]-WeatherUsermod::minTemp)/(WeatherUsermod::maxTemp - WeatherUsermod::minTemp); y++) {
SEGMENT.setPixelColorXY(x, SEGMENT.virtualHeight() - y, color);
}
// Serial.print(" ");
// Serial.print(temps[x%16]);
}
if (localTime < WeatherUsermod::times[0])
currentTemp = map(localTime, WeatherUsermod::times[0], WeatherUsermod::times[1], WeatherUsermod::temps[0] * 1000, WeatherUsermod::temps[1] * 1000) / 1000.0;
// Serial.print(" time ");
// char timeString[64];
// epochToString(currentTime, timeString);
// Serial.print(timeString);
// Serial.print(" temp ");
char tempString[5] = "";
sprintf(tempString, "%5.2f", currentTemp);
// Serial.println();
CRGB color = ColorFromPalette(SEGPALETTE, map((uint8_t)currentTemp, 0, 40, 0, 255), 255, LINEARBLEND);
if (SEGMENT.virtualWidth() < 16) {
SEGMENT.drawCharacter(tempString[0], 0, -2, 5, 8, color);
SEGMENT.drawCharacter(tempString[1], 4, -2, 5, 8, color);
SEGMENT.setPixelColorXY(8, 4, color);
SEGMENT.drawCharacter(tempString[3], 9, -2, 5, 8, color);
}
else {
SEGMENT.drawCharacter(tempString[0], 0, -2, 5, 8, color);
SEGMENT.drawCharacter(tempString[1], 5, -2, 5, 8, color);
SEGMENT.setPixelColorXY(10, 4, color);
SEGMENT.drawCharacter(tempString[3], 12, -2, 5, 8, color);
SEGMENT.drawCharacter(tempString[4], 17, -2, 5, 8, color);
}
return FRAMETIME;
}