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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user