Merge pull request #99 from 2Grey/feature/yandex-weather

This commit is contained in:
Ewoud
2023-12-22 14:35:56 +01:00
committed by GitHub
4 changed files with 708 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
# Yandex Weaher Usermod
V2 Usermod for getting weather data from [Yandex Weather](https://yandex.ru/pogoda) service
## Web interface
The info page in the web interface shows:
- Remaining time until new weather request
- Current temperature (°C)
- Feels temperature (°C)
- Wind speed (m/s)
- Wind direction
## Usage
Compile the source with the buildflag `-D USERMOD_YA_WEATHER` added.
```ini
build_flags = ${env:esp32dev.build_flags}
-D USERMOD_YA_WEATHER
```
## API for other Usermods
- `void setEnable(bool enable)` To change enable state
- `bool isEnable()` To query enable state
- `WeatherInfo *currentWeather()` To query current weather struct
### Access from other Usermod
There are two options to get access to the usermod instance:
1. Include `yandex-weather-usermod-v2.h` before your Usermod in _usermods_list.cpp_
1. Use `#include "yandex-weather-usermod-v2.h"` at the top of the your Usermod
### Usage example
```cpp
#include "wled.h"
#include "yandex-weather-usermod-v2.h"
class MyUsermode: public Usermod {
void foo() {
#ifdef USERMOD_ID_YA_WEATHER
YandexWeatherUsermod *weatherUserMode = (YandexWeatherUsermod *)usermods.lookup(USERMOD_ID_YA_WEATHER);
if (weatherUserMode != nullptr) {
weatherUserMode->setEnable(true);
weatherUserMode->currentWeather();
}
#endif
}
}
```
## Settings
### Enabled:
Checkbox to enable or disable the Usermod
### ApiKey
ApiKey for access to Yandex Weather API.
You can get it [here](https://yandex.ru/dev/weather/doc/dg/concepts/about.html).
### UpdateInterval
API access frequency in minutes.
*Note:* It should be greater or equal to 30, because free APIKey has a limit of 50 requests per day.
### ApiLanguage
The language and country combinations for which weather wording data will be returned.
More info [here](https://yandex.ru/dev/weather/doc/dg/concepts/forecast-info.html)
### CustomCoordinates
Enables the use of custom coordinates (more on them below) instead of the system ones from WLED.
### CityLatitude
Custom city latitude coordinate (works only with `CustomCoordinates` enabled)
### CityLongitude
Custom city longitude coordinate (works only with `CustomCoordinates` enabled)
### ShowInInfo
Enable display of weather information in the "Info" page (on the WLED main scree)
### PostToMQTT
Enable post weather data to MQTT topic (/yandexWeather)
## MQTT Topics
| Topic | Unit | Description |
|:------------------------|:--------|:------------------------|
| yandexWeather/date | String | Weather received date |
| yandexWeather/tempReal | °C | Real temperature |
| yandexWeather/tempFeels | °C | Feels like temperature |
| yandexWeather/windSpeed | m/s | Wind speed |
| yandexWeather/windDir | String | Wind direction |
## Additional Build Flags
| Flag | Description |
|:------------------------------|:----------------------------------------------------------------------------------------------|
| YA_WEATHER_DEBUG | Show debug message with _[YandexWeatherUsermod]_ prefix |
| YA_WEATHER_ALLOW_ALL_TIMEOUT | Allows you to set UpdateInterval to less than 30 (Use for tests or if you have a paid ApiKey) |
| YA_WEATHER_HIDE_REMAINING | Hide the remaining time to update in Info |
-----
Author:
2Grey | [Github](https://github.com/2Grey)

View File

@@ -0,0 +1,596 @@
#pragma once
#include "wled.h"
#include "time.h"
// #define YA_WEATHER_DEBUG // Show debug message with [YandexWeatherUsermod] prefix
// #define YA_WEATHER_ALLOW_ALL_TIMEOUT // Allows you to set UpdateInterval to less than 30
// #define YA_WEATHER_HIDE_REMAINING // Hide the remaining time to update in Info
class YandexWeatherUsermod: public Usermod
{
public:
// Enums
enum class WeatherInfoResult {
Success = 0, // Data fetch is Ok
Timeout, // The time hasn't come yet (Update interval)
InternalError, // ApiKey or coordinate missed
ServerError, // Some error from server
};
// Weather info
struct WeatherInfo {
unsigned long date; // Server time (Unixtime)
int tempReal; // Real temperature (°C)
int tempFeels; // Feels temperature (°C)
float windSpeed; // Wind speed (kmp)
String windDir; // Wind directions
WeatherInfo(unsigned long dt, int tr, int tf, float ws, String wd): date(dt), tempReal(tr), tempFeels(tf), windSpeed(ws), windDir(wd) {}
};
private:
// Internal
#ifndef _MoonModules_WLED_
bool initDone = false;
bool enabled = false;
unsigned long lastTime = 0;
#endif
bool isConnected = false;
char errorMessage[100] = "";
WeatherInfo *_weatherInfo;
// configurable parameters
String _apiKey = "";
int _updateInterval = 30;
int _apiLanguage = 0;
bool _isUseCustomCoord = false;
bool _isShowInInfo = false;
bool _isPostToMQTT = false;
float _apiLat = 0;
float _apiLon = 0;
// Const chars
static const char _cfg_key_enabled[];
static const char _cfg_key_apiKey[];
static const char _cfg_key_updateInterval[];
static const char _cfg_key_apiLang[];
static const char _cfg_key_custom_coord[];
static const char _cfg_key_apiLat[];
static const char _cfg_key_apiLon[];
static const char _cfg_key_showInfo[];
static const char _cfg_key_postMQTT[];
#ifndef _MoonModules_WLED_
static const char _name[];
#endif
// Inlines
inline bool isValidCoordinate(float lat, float lon) { return fabs(lat) > __FLT_EPSILON__ && fabs(lon) > __FLT_EPSILON__; }
inline bool isValidCoordinatePair(std::pair<float, float> coord) { return isValidCoordinate(coord.first, coord.second); }
inline bool isCharEmpty(String val) { return strcmp(val.c_str(), "") == 0; }
inline long unsigned getUpdateInterval() { return (_updateInterval * 60 * 1000); }
inline String uppercasedString(String str) { str.toUpperCase(); return str; }
inline const char* apiLanguageByIdx(int idx) {
switch (idx) {
case 1: return "ru_RU";
case 2: return "ru_UA";
case 3: return "uk_UA";
case 4: return "be_BY";
case 5: return "kk_KZ";
case 6: return "tr_TR";
default: return "en_US";
}
}
inline std::string windCharByString(std::string wd) {
if (wd.compare("nw") == 0) { return ""; }
if (wd.compare("n") == 0) { return "⬇︎"; }
if (wd.compare("ne") == 0) { return ""; }
if (wd.compare("e") == 0) { return "⬅︎"; }
if (wd.compare("se") == 0) { return ""; }
if (wd.compare("s") == 0) { return "⬆︎"; }
if (wd.compare("sw") == 0) { return ""; }
if (wd.compare("w") == 0) { return ""; }
return "";
}
inline String firstWeatherLine() {
if (_weatherInfo == nullptr) return String(F(""));
char r[20];
sprintf_P(r, "%d (like %d)", _weatherInfo->tempReal, _weatherInfo->tempFeels);
return String(r);
}
inline String secondWeatherLine() {
if (_weatherInfo == nullptr) return String(F(""));
char r[15];
sprintf(r, "%.2f m/s", _weatherInfo->windSpeed);
if (!_weatherInfo->windDir.isEmpty()) {
char wd[4];
sprintf(wd, "(%s)", windCharByString(_weatherInfo->windDir.c_str()).c_str());
strcat(r, wd);
}
return String(r);
}
// Helping functions
std::pair<float, float> getCoordinates()
{
std::pair<float, float> res { 0, 0};
if (_isUseCustomCoord) {
if (isValidCoordinate(_apiLat, _apiLon)) {
res.first = _apiLat;
res.second = _apiLon;
}
} else if (isValidCoordinate(latitude, longitude)) {
res.first = latitude;
res.second = longitude;
}
return res;
}
String epochToStirng(time_t time) {
tmElements_t tm;
breakTime(time, tm);
char buf[20];
sprintf(buf, "%04d-%02d-%02d %02d:%02d:%02d", tm.Year + 1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
return String(buf);
}
String remainingHumanString() {
unsigned long remainingTimeSec = (getUpdateInterval() - (millis() - lastTime)) / 1000;
int h = remainingTimeSec / 3600;
int m = (remainingTimeSec % 3600) / 60;
int s = remainingTimeSec % 60;
char buf[8];
sprintf(buf, "%02d:%02d:%02d", h, m, s);
return String(buf);
}
void resetLastTime() {
lastTime = millis() - getUpdateInterval();
}
// API Calls
bool parseReponse(WiFiClient client)
{
bool isSuccess = false;
StaticJsonDocument<256> filter;
filter["now"] = true;
filter["fact"]["temp"] = true;
filter["fact"]["feels_like"] = true;
filter["fact"]["wind_speed"] = true;
filter["fact"]["wind_dir"] = true;
PSRAMDynamicJsonDocument weatherDoc(4096);
DeserializationError parseError = deserializeJson(weatherDoc, client, DeserializationOption::Filter(filter));
if (parseError) {
strcat(errorMessage, PSTR("deserializeJson() failed: "));
strcat(errorMessage, parseError.c_str());
} else {
isSuccess = true;
unsigned long nowDate = 0;
getJsonValue(weatherDoc["now"], nowDate);
int tempReal;
int tempFeels;
float windSpeed;
String windDir;
JsonObject weatherDocObject = weatherDoc.as<JsonObject>();
JsonObject weatherFactObject = weatherDocObject["fact"];
getJsonValue(weatherFactObject["temp"], tempReal);
getJsonValue(weatherFactObject["feels_like"], tempFeels);
getJsonValue(weatherFactObject["wind_speed"], windSpeed);
getJsonValue(weatherFactObject["wind_dir"], windDir);
if (_weatherInfo) {
_weatherInfo->date = nowDate;
_weatherInfo->tempReal = tempReal;
_weatherInfo->tempFeels = tempFeels;
_weatherInfo->windSpeed = windSpeed;
_weatherInfo->windDir = windDir;
} else {
_weatherInfo = new WeatherInfo(nowDate, tempReal, tempFeels, windSpeed, windDir);
}
if (_weatherInfo) {
publishMQTT(_weatherInfo);
} else {
isSuccess = false;
}
}
return isSuccess;
}
WeatherInfoResult apiGetWeather(char *errorMessage)
{
WiFiClient client;
client.setTimeout(10000);
if(!client.connect("api.weather.yandex.ru", 80)) {
strcat(errorMessage, PSTR("Connection failed"));
#ifdef YA_WEATHER_DEBUG
DEBUG_PRINTLN(F("[YandexWeatherUsermod] Connection failed"));
#endif
return WeatherInfoResult::ServerError;
}
if (client.connected()) {
char url[180];
std::pair<float, float> coords = getCoordinates();
sprintf(url, "GET /v2/informers?lat=%f&lon=%f&lang=%s HTTP/1.0", coords.first, coords.second, apiLanguageByIdx(_apiLanguage));
client.println(url);
client.println(F("Host: api.weather.yandex.ru"));
client.printf("X-Yandex-API-Key: %s\n", _apiKey.c_str());
client.println(F("Connection: close"));
}
bool isSuccess = false;
if (client.println() == 0) {
strcat(errorMessage, PSTR("Failed to send request"));
#ifdef YA_WEATHER_DEBUG
DEBUG_PRINTLN(F("[YandexWeatherUsermod] Failed to send request"));
#endif
} else {
char status[32] = {0};
client.readBytesUntil('\r', status, sizeof(status));
if (strcmp(status, "HTTP/1.0 200 OK") != 0) {
strcat(errorMessage, PSTR("Unexpected response: "));
strcat(errorMessage, status);
#ifdef YA_WEATHER_DEBUG
DEBUG_PRINTF("[YandexWeatherUsermod] Unexpected response: %s\n", status);
#endif
} else {
char endOfHeaders[] = "\r\n\r\n";
if (!client.find(endOfHeaders)) {
strcat(errorMessage, PSTR("Invalid response"));
#ifdef YA_WEATHER_DEBUG
DEBUG_PRINTLN(F("[YandexWeatherUsermod] Invalid response"));
#endif
} else {
isSuccess |= parseReponse(client);
}
}
}
client.stop();
return isSuccess ? WeatherInfoResult::Success : WeatherInfoResult::ServerError;
}
// MQTT
void publishMQTT(WeatherInfo *wi) {
if (!wi || !_isPostToMQTT) return;
#ifndef WLED_DISABLE_MQTT
if (WLED_MQTT_CONNECTED) {
String topicName = mqttDeviceTopic;
topicName += F("/yandexWeather");
mqtt->publish((topicName + F("/date")).c_str(), 0, false, epochToStirng(_weatherInfo->date).c_str());
char buf[10];
sprintf(buf, "%d", _weatherInfo->tempReal);
mqtt->publish((topicName + F("/tempReal")).c_str(), 0, false, buf);
sprintf(buf, "%d", _weatherInfo->tempFeels);
mqtt->publish((topicName + F("/tempFeels")).c_str(), 0, false, buf);
sprintf(buf, "%.2f", _weatherInfo->windSpeed);
mqtt->publish((topicName + F("/windSpeed")).c_str(), 0, false, buf);
mqtt->publish((topicName + F("/windDir")).c_str(), 0, false, _weatherInfo->windDir.c_str());
}
#endif
}
public:
// Class Constructor/Destructor
#ifdef _MoonModules_WLED_
YandexWeatherUsermod(const char *name, bool enabled) : Usermod(name, enabled), _weatherInfo(nullptr) {
lastTime = 0;
}
#else
YandexWeatherUsermod() : _weatherInfo(nullptr) {
lastTime = 0;
}
#endif
~YandexWeatherUsermod() {
if (_weatherInfo) { delete _weatherInfo; _weatherInfo = nullptr; }
}
// Public methods
/**
* Togle enabled for Usermod
* @param enable New state of enabled of Usermod
*/
void setEnable(bool enable) { enabled = enable; }
/**
* @return Current enabled state of Usermod
*/
bool isEnable() { return enabled;}
/**
* @return Current weather struct (WeatherInfo)
*/
WeatherInfo *currentWeather() { return _weatherInfo; };
/**
* Try to fetch new weather data from server
* @param isForce If `true` ignore update interval
* @return Status result of fetching data (WeatherInfoResult)
*/
WeatherInfoResult tryGetWeather(bool isForce)
{
if (!isForce && millis() - lastTime < getUpdateInterval()) {
return WeatherInfoResult::Timeout;
}
#ifdef YA_WEATHER_DEBUG
DEBUG_PRINTLN(F("[YandexWeatherUsermod] Trying to get weather..."));
#endif
lastTime = millis();
strcpy(errorMessage, "");
if (isCharEmpty(_apiKey)) {
strcpy(errorMessage, PSTR("No API Key set"));
#ifdef YA_WEATHER_DEBUG
DEBUG_PRINTLN(F("[YandexWeatherUsermod] No API Key set"));
#endif
return WeatherInfoResult::InternalError;
}
std::pair<float, float> coords = getCoordinates();
if (!isValidCoordinatePair(coords)) {
strcpy(errorMessage, PSTR("No coordinates set"));
#ifdef YA_WEATHER_DEBUG
DEBUG_PRINTLN(F("[YandexWeatherUsermod] No coordinates set"));
#endif
return WeatherInfoResult::InternalError;
}
return apiGetWeather(errorMessage);
}
// WLED lyfecycle
void setup() {
initDone = true;
}
void connected() {
isConnected = true;
}
void loop() {
if (!initDone) return;
if (!enabled || !isConnected || strip.isUpdating()) return;
WeatherInfoResult res = tryGetWeather(false);
switch (res) {
case WeatherInfoResult::Success:
lastTime = millis();
break;
case WeatherInfoResult::ServerError:
lastTime = millis() - getUpdateInterval() + 10000;
break;
default:
break;
}
}
// MQTT
void onMqttConnect(bool sessionPresent) {
publishMQTT(_weatherInfo);
}
// Info page
void addToJsonInfo(JsonObject &root)
{
if (!initDone) return;
JsonObject user = root["u"];
if (user.isNull()) user = root.createNestedObject("u");
JsonArray infoArr = user.createNestedArray(FPSTR(_name)); // name
#ifndef YA_WEATHER_HIDE_REMAINING
if (enabled) {
infoArr.add(remainingHumanString());
}
#endif
String uiDomString = F("<button class=\"btn btn-xs\" onclick=\"requestJson({");
uiDomString += FPSTR(_name);
uiDomString += F(":{");
uiDomString += FPSTR("enabled");
uiDomString += enabled ? F(":false}});\">") : F(":true}});\">");
uiDomString += F("<i class=\"icons ");
uiDomString += enabled ? "on" : "off";
uiDomString += F("\">&#xe08f;</i></button>");
infoArr.add(uiDomString);
if (!enabled || !_isShowInInfo) return;
if (!isCharEmpty(errorMessage)) {
infoArr = user.createNestedArray(F("Weather error"));
String errorString = F("<b style=\"color:red;\">");
errorString += FPSTR(errorMessage);
errorString += F("</b>");
infoArr.add(errorString);
} else if (!_weatherInfo) {
infoArr = user.createNestedArray(F("Weather info"));
infoArr.add(F("no weather data"));
} else {
infoArr = user.createNestedArray(F("Weather date"));
infoArr.add(epochToStirng(_weatherInfo->date));
infoArr.add(F(" (UTC)"));
infoArr = user.createNestedArray(F("Temperature"));
infoArr.add(firstWeatherLine());
infoArr = user.createNestedArray(F("Wind"));
infoArr.add(secondWeatherLine());
}
}
// JSON State
void readFromJsonState(JsonObject& root)
{
if (!initDone) return;
bool en = enabled;
JsonObject um = root[FPSTR(_name)];
if (!um.isNull()) {
if (um[FPSTR(_cfg_key_enabled)].is<bool>()) {
en = um[FPSTR(_cfg_key_enabled)].as<bool>();
} else {
String str = um[FPSTR(_cfg_key_enabled)];
en = (bool)(str!="off");
}
}
if (en != enabled) {
enabled = en;
if (enabled) resetLastTime();
}
}
// Config
void addToConfig(JsonObject &root)
{
#ifdef _MoonModules_WLED_
Usermod::addToConfig(root);
JsonObject top = root[FPSTR(_name)];
#else
JsonObject top = root.createNestedObject(FPSTR(_name)); // WLEDMM: set enabled and _name
top[FPSTR("enabled")] = enabled;
#endif
top[FPSTR(_cfg_key_apiKey)] = _apiKey;
top[FPSTR(_cfg_key_updateInterval)] = _updateInterval;
top[FPSTR(_cfg_key_apiLang)] = _apiLanguage;
top[FPSTR(_cfg_key_custom_coord)] = _isUseCustomCoord;
if (isValidCoordinate(_apiLat, _apiLon)) {
top[FPSTR(_cfg_key_apiLat)] = _apiLat;
top[FPSTR(_cfg_key_apiLon)] = _apiLon;
} else {
top[FPSTR(_cfg_key_apiLat)] = 0;
top[FPSTR(_cfg_key_apiLon)] = 0;
}
top[FPSTR(_cfg_key_showInfo)] = _isShowInInfo;
top[FPSTR(_cfg_key_postMQTT)] = _isPostToMQTT;
}
void appendConfigData()
{
oappend(SET_F("addHB('YandexWeather');"));
oappend(SET_F("addInfo('"));
oappend(String(FPSTR(_name)).c_str());
oappend((":" + String(FPSTR(_cfg_key_updateInterval))).c_str());
#ifdef YA_WEATHER_ALLOW_ALL_TIMEOUT
oappend(SET_F("', 1,'minutes <i>(should be ≥ 1)</i>');"));
#else
oappend(SET_F("', 1,'minutes <i>(should be ≥ 30)</i>');"));
#endif
oappend(SET_F("dd=addDropdown('"));
oappend(String(FPSTR(_name)).c_str());
oappend(("', '" + String(FPSTR(_cfg_key_apiLang)) + "');").c_str());
oappend(SET_F("addOption(dd,'English',0);"));
oappend(SET_F("addOption(dd,'Russian (Russian domain)',1);"));
oappend(SET_F("addOption(dd,'Russian (Ukrainian domain)',2);"));
oappend(SET_F("addOption(dd,'Ukrainian (Ukrainian domain)',3);"));
oappend(SET_F("addOption(dd,'Belarusian',4);"));
oappend(SET_F("addOption(dd,'Kazakh',5);"));
oappend(SET_F("addOption(dd,'Turkish',6);"));
}
virtual bool readFromConfig(JsonObject &root)
{
#ifdef _MoonModules_WLED_
bool configComplete = Usermod::readFromConfig(root);
JsonObject top = root[FPSTR(_name)];
#else
bool configComplete = true;
JsonObject top = root[FPSTR(_name)];
if (!top.isNull()) {
configComplete &= getJsonValue(top[FPSTR("enabled")], enabled);
}
#endif
if (top.isNull()) {
return false;
}
// Old values (for re-call api for case when something changed)
bool oldEnabledState = enabled;
String oldAPIKey = _apiKey;
int oldApiLanguage = _apiLanguage;
bool oldIsUseCustomCoord = _isUseCustomCoord;
std::pair<float, float> oldCoords { _apiLat, _apiLon};
// Config logic
configComplete &= getJsonValue(top[FPSTR(_cfg_key_updateInterval)], _updateInterval);
#ifdef YA_WEATHER_ALLOW_ALL_TIMEOUT
_updateInterval = max(1, _updateInterval);
#else
_updateInterval = max(30, _updateInterval);
#endif
configComplete &= getJsonValue(top[FPSTR(_cfg_key_apiKey)], _apiKey);
configComplete &= getJsonValue(top[FPSTR(_cfg_key_apiLang)], _apiLanguage);
_apiLanguage = max(0, min(6, _apiLanguage));
configComplete &= getJsonValue(top[FPSTR(_cfg_key_custom_coord)], _isUseCustomCoord, false);
configComplete &= getJsonValue(top[FPSTR(_cfg_key_apiLat)], _apiLat);
configComplete &= getJsonValue(top[FPSTR(_cfg_key_apiLon)], _apiLon);
configComplete &= getJsonValue(top[FPSTR(_cfg_key_showInfo)], _isShowInInfo, false);
configComplete &= getJsonValue(top[FPSTR(_cfg_key_postMQTT)], _isPostToMQTT, false);
//
if (enabled) {
bool isEnabledChanged = (oldEnabledState == false);
bool isApiKeyChanged = (oldAPIKey.compareTo(_apiKey) != 0);
bool isApiLanguageChanged = (oldApiLanguage != _apiLanguage);
bool isUseCustomCoordChanged = (oldIsUseCustomCoord != _isUseCustomCoord);
bool isCoordinateChanges = (oldCoords.first != _apiLat || oldCoords.second != _apiLon);
if (isEnabledChanged || isApiKeyChanged || isApiLanguageChanged || isUseCustomCoordChanged) resetLastTime();
if (isUseCustomCoordChanged && isCoordinateChanges) resetLastTime();
} else {
strcpy(errorMessage, "");
}
return configComplete;
}
// Usermode ID
uint16_t getId() {
return USERMOD_ID_YA_WEATHER;
}
};
// strings to reduce flash memory usage (used more than twice)
const char YandexWeatherUsermod::_cfg_key_enabled[] PROGMEM = "enabled";
const char YandexWeatherUsermod::_cfg_key_apiKey[] PROGMEM = "apiKey";
const char YandexWeatherUsermod::_cfg_key_updateInterval[] PROGMEM = "updateInterval";
const char YandexWeatherUsermod::_cfg_key_apiLang[] PROGMEM = "apiLanguage";
const char YandexWeatherUsermod::_cfg_key_custom_coord[] PROGMEM = "customCoordinates";
const char YandexWeatherUsermod::_cfg_key_apiLat[] PROGMEM = "cityLatitude";
const char YandexWeatherUsermod::_cfg_key_apiLon[] PROGMEM = "cityLongitude";
const char YandexWeatherUsermod::_cfg_key_showInfo[] PROGMEM = "showInInfo";
const char YandexWeatherUsermod::_cfg_key_postMQTT[] PROGMEM = "postToMQTT";
#ifndef _MoonModules_WLED_
const char YandexWeatherUsermod::_name[] PROGMEM = "YandexWeather";
#endif

View File

@@ -142,6 +142,7 @@
#define USERMOD_ID_WEATHER 91 //Usermod "usermod_v2_weather.h"
#define USERMOD_ID_GAMES 92 //Usermod "usermod_v2_games.h"
#define USERMOD_ID_ANIMARTRIX 93 //Usermod "usermod_v2_animartrix.h"
#define USERMOD_ID_YA_WEATHER 94
//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot

View File

@@ -216,6 +216,10 @@
#include "../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h"
#endif
#ifdef USERMOD_YA_WEATHER
#include "../usermods/usermod_v2_yandex_weather/usermod_v2_yandex_weather.h"
#endif
void registerUsermods()
{
/*
@@ -418,4 +422,8 @@ void registerUsermods()
usermods.add(new AnimartrixUsermod("Animartrix", false));
#endif
#ifdef USERMOD_YA_WEATHER
usermods.add(new YandexWeatherUsermod("YandexWeather", false));
#endif
}