This commit is contained in:
Jerry
2025-02-25 17:52:00 +08:00
parent 2aab05ba64
commit 689e4a08c9
37 changed files with 3936 additions and 11 deletions

298
src/API.hpp Executable file
View File

@@ -0,0 +1,298 @@
#ifndef __API_HPP__
#define __API_HPP__
#include <Arduino.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <esp_http_client.h>
#include <ArduinoUZlib.h> // 解压gzip
struct Weather {
String time;
int8_t temp;
int8_t humidity;
int16_t wind360;
String windDir;
int8_t windScale;
uint8_t windSpeed;
uint16_t icon;
String text;
String updateTime;
};
struct DailyWeather {
String date;
String sunrise;
String sunset;
String moonPhase;
uint16_t moonPhaseIcon;
int8_t tempMax;
int8_t tempMin;
int8_t humidity;
uint16_t iconDay;
String textDay;
uint16_t iconNight;
String textNight;
int16_t wind360Day;
String windDirDay;
int8_t windScaleDay;
uint8_t windSpeedDay;
int16_t wind360Night;
String windDirNight;
int8_t windScaleNight;
uint8_t windSpeedNight;
};
struct HourlyForecast {
Weather* weather;
uint8_t length;
uint8_t interval;
};
struct DailyForecast {
DailyWeather* weather;
uint8_t length;
String updateTime;
};
struct Hitokoto {
String sentence;
String from;
String from_who;
};
struct Bilibili {
uint64_t follower;
uint64_t view;
uint64_t likes;
};
template<uint8_t MAX_RETRY = 3>
class API {
using callback = std::function<bool(JsonDocument&)>;
using precall = std::function<void()>;
private:
HTTPClient http;
WiFiClientSecure wifiClient;
bool getRestfulAPI(String url, callback cb, precall pre = precall()) {
// Serial.printf("Request Url: %s\n", url.c_str());
JsonDocument doc;
for (uint8_t i = 0; i < MAX_RETRY; i++) {
bool shouldRetry = false;
if (http.begin(wifiClient, url)) {
if (pre) pre();
// Serial.printf("Before GET\n");
int httpCode = http.GET();
// Serial.printf("GET %d\n", httpCode);
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_NOT_MODIFIED) {
bool isGzip = false;
int headers = http.headers();
for (int j = 0; j < headers; j++) {
String headerName = http.headerName(j);
String headerValue = http.header(j);
// Serial.println(headerName + ": " + headerValue);
if (headerName.equalsIgnoreCase("Content-Encoding") && headerValue.equalsIgnoreCase("gzip")) {
isGzip = true;
break;
}
}
String s = http.getString();
DeserializationError error;
if (isGzip) {
// gzip解压缩
uint8_t* outBuf = NULL;
size_t outLen = 0;
ArduinoUZlib::decompress((uint8_t*)s.c_str(), (uint32_t)s.length(), outBuf, outLen);
error = deserializeJson(doc, (char*)outBuf, outLen);
} else {
error = deserializeJson(doc, s);
}
if (!error) {
wifiClient.flush();
http.end();
return cb(doc);
} else {
Serial.print(F("Parse JSON failed, error: "));
Serial.println(error.c_str());
shouldRetry = error == DeserializationError::IncompleteInput;
}
} else {
Serial.print(F("Get failed, error: "));
if (httpCode < 0) {
Serial.println(http.errorToString(httpCode));
shouldRetry = httpCode == HTTPC_ERROR_CONNECTION_REFUSED || httpCode == HTTPC_ERROR_CONNECTION_LOST || httpCode == HTTPC_ERROR_READ_TIMEOUT;
} else {
Serial.println(httpCode);
}
}
wifiClient.flush();
http.end();
} else {
Serial.println(F("Unable to connect"));
}
if (!shouldRetry) break;
Serial.println(F("Retry after 10 second"));
delay(5000);
}
return false;
}
public:
API() {
// http.setTimeout(10000);
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
const char* encoding = "Content-Encoding";
const char* headerKeys[1] = {};
headerKeys[0] = encoding;
http.collectHeaders(headerKeys, 1);
wifiClient.setInsecure();
}
~API() {}
// 获取 HTTPClient
HTTPClient& httpClient() {
return http;
}
// 和风天气 - 实时天气: https://dev.qweather.com/docs/api/weather/weather-now/
bool getWeatherNow(Weather& result, const char* key, const char* locid) {
// return getRestfulAPI("https://www.baidu.com", [&result](JsonDocument& json) {
return getRestfulAPI(
"https://devapi.qweather.com/v7/weather/now?key=" + String(key) + "&location=" + String(locid), [&result](JsonDocument& json) {
if (strcmp(json["code"], "200") != 0) {
Serial.print(F("Get weather failed, error: "));
Serial.println(json["code"].as<const char*>());
return false;
}
result.updateTime = String(json["updateTime"].as<const char*>());
JsonObject now = json["now"];
result.time = now["obsTime"].as<const char*>();
result.temp = atoi(now["temp"]);
result.humidity = atoi(now["humidity"]);
result.wind360 = atoi(now["wind360"]);
result.windDir = now["windDir"].as<const char*>();
result.windScale = atoi(now["windScale"]);
result.windSpeed = atoi(now["windSpeed"]);
result.icon = atoi(now["icon"]);
result.text = now["text"].as<const char*>();
return true;
});
}
// 和风天气 - 逐小时天气预报: https://dev.qweather.com/docs/api/weather/weather-hourly-forecast/
bool getForecastHourly(HourlyForecast& result, const char* key, const char* locid) {
return getRestfulAPI("https://devapi.qweather.com/v7/weather/24h?key=" + String(key) + "&location=" + String(locid), [&result](JsonDocument& json) {
if (strcmp(json["code"], "200") != 0) {
Serial.print(F("Get hourly forecast failed, error: "));
Serial.println(json["code"].as<const char*>());
return false;
}
uint8_t i, hours = json["hourly"].size();
for (i = 0; i < result.length; i++) {
if (i * result.interval >= hours) break;
Weather& weather = result.weather[i];
JsonObject hourly = json["hourly"][i * result.interval];
weather.time = hourly["fxTime"].as<const char*>();
weather.temp = atoi(hourly["temp"]);
weather.humidity = atoi(hourly["humidity"]);
weather.wind360 = atoi(hourly["wind360"]);
weather.windDir = hourly["windDir"].as<const char*>();
weather.windScale = atoi(hourly["windScale"]);
weather.windSpeed = atoi(hourly["windSpeed"]);
weather.icon = atoi(hourly["icon"]);
weather.text = hourly["text"].as<const char*>();
}
result.length = i;
return true;
});
}
// 和风天气 - 逐天天气预报: https://dev.qweather.com/docs/api/weather/weather-daily-forecast/
bool getForecastDaily(DailyForecast& result, const char* key, const char* locid) {
return getRestfulAPI("https://devapi.qweather.com/v7/weather/3d?key=" + String(key) + "&location=" + String(locid), [&result](JsonDocument& json) {
if (strcmp(json["code"], "200") != 0) {
Serial.print(F("Get daily forecast failed, error: "));
Serial.println(json["code"].as<const char*>());
return false;
}
result.updateTime = String(json["updateTime"].as<const char*>());
uint8_t i;
for (i = 0; i < result.length; i++) {
DailyWeather& weather = result.weather[i];
JsonObject daily = json["daily"][i];
weather.date = daily["fxDate"].as<const char*>();
weather.sunrise = daily["sunrise"].as<const char*>();
weather.sunset = daily["sunset"].as<const char*>();
weather.moonPhase = daily["moonPhase"].as<const char*>();
weather.moonPhaseIcon = atoi(daily["moonPhaseIcon"]);
weather.tempMax = atoi(daily["tempMax"]);
weather.tempMin = atoi(daily["tempMin"]);
weather.humidity = atoi(daily["humidity"]);
weather.iconDay = atoi(daily["iconDay"]);
weather.textDay = daily["textDay"].as<const char*>();
weather.iconNight = atoi(daily["iconNight"]);
weather.textNight = daily["textNight"].as<const char*>();
weather.wind360Day = atoi(daily["wind360Day"]);
weather.windDirDay = daily["windDirDay"].as<const char*>();
weather.windScaleDay = atoi(daily["windScaleDay"]);
weather.windSpeedDay = atoi(daily["windSpeedDay"]);
weather.wind360Night = atoi(daily["wind360Night"]);
weather.windDirNight = daily["windDirNight"].as<const char*>();
weather.windScaleNight = atoi(daily["windScaleNight"]);
weather.windSpeedNight = atoi(daily["windSpeedNight"]);
}
result.length = i;
return true;
});
}
// 一言: https://developer.hitokoto.cn/sentence/
bool getHitokoto(Hitokoto& result) {
return getRestfulAPI("https://v1.hitokoto.cn/?max_length=15", [&result](JsonDocument& json) {
result.sentence = json["hitokoto"].as<const char*>();
result.from = json["from"].as<const char*>();
result.from_who = json["from_who"].as<const char*>();
return true;
});
}
// B站粉丝
bool getFollower(Bilibili& result, uint32_t uid) {
return getRestfulAPI("https://api.bilibili.com/x/relation/stat?vmid=" + String(uid), [&result](JsonDocument& json) {
if (json["code"] != 0) {
Serial.print(F("Get bilibili follower failed, error: "));
Serial.println(json["message"].as<const char*>());
return false;
}
result.follower = json["data"]["follower"];
return true;
});
}
// B站总播放量和点赞数
bool getLikes(Bilibili& result, uint32_t uid, const char* cookie) {
return getRestfulAPI(
"https://api.bilibili.com/x/space/upstat?mid=" + String(uid), [&result](JsonDocument& json) {
if (json["code"] != 0) {
Serial.print(F("Get bilibili likes failed, error: "));
Serial.println(json["message"].as<const char*>());
return false;
}
result.view = json["data"]["archive"]["view"];
result.likes = json["data"]["likes"];
return true;
},
[this, &cookie]() {
http.addHeader("Cookie", String("SESSDATA=") + cookie + ";");
});
}
};
#endif // __API_HPP__