mirror of
https://github.com/JADE-Jerry/jcalendar.git
synced 2025-12-10 03:18:12 +08:00
300 lines
12 KiB
C++
Executable File
300 lines
12 KiB
C++
Executable File
#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* host, const char* key, const char* locid) {
|
|
// return getRestfulAPI("https://www.baidu.com", [&result](JsonDocument& json) {
|
|
return getRestfulAPI(
|
|
"https://" + String(host) + "/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* host, const char* key, const char* locid) {
|
|
return getRestfulAPI("https://" + String(host) + "/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* host, const char* key, const char* locid) {
|
|
return getRestfulAPI("https://" + String(host) + "/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__
|