770 lines
36 KiB
C++
770 lines
36 KiB
C++
#include "weather_api.h"
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <cctype>
|
||
#include <vector>
|
||
#include <time.h>
|
||
|
||
// ESP32 ESP-IDF headers
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
#include "esp_http_client.h"
|
||
#include "esp_log.h"
|
||
#include "cJSON.h"
|
||
#include "esp_crt_bundle.h"
|
||
#include "esp_wifi.h"
|
||
#include "nvs.h"
|
||
#include "nvs_flash.h"
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/task.h"
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
#include "zlib.h"
|
||
|
||
static const char* TAG = "WeatherApi";
|
||
|
||
// 天气代码映射表 - 将和风天气API的天气代码转换为中文描述
|
||
std::unordered_map<std::string, std::string> WeatherApi::WEATHER_CODE_MAP = {
|
||
{"100", "晴"}, {"101", "多云"}, {"102", "少云"}, {"103", "晴间多云"},
|
||
{"104", "阴"}, {"150", "晴"}, {"151", "多云"}, {"152", "少云"},
|
||
{"153", "晴间多云"}, {"300", "阵雨"}, {"301", "强阵雨"}, {"302", "雷阵雨"},
|
||
{"303", "强雷阵雨"}, {"304", "雷阵雨伴有冰雹"}, {"305", "小雨"}, {"306", "中雨"},
|
||
{"307", "大雨"}, {"308", "极端降雨"}, {"309", "毛毛雨/细雨"}, {"310", "暴雨"},
|
||
{"311", "大暴雨"}, {"312", "特大暴雨"}, {"313", "冻雨"}, {"314", "小到中雨"},
|
||
{"315", "中到大雨"}, {"316", "大到暴雨"}, {"317", "暴雨到大暴雨"}, {"318", "大暴雨到特大暴雨"},
|
||
{"350", "阵雨"}, {"351", "强阵雨"}, {"399", "雨"}, {"400", "小雪"},
|
||
{"401", "中雪"}, {"402", "大雪"}, {"403", "暴雪"}, {"404", "雨夹雪"},
|
||
{"405", "雨雪天气"}, {"406", "阵雨夹雪"}, {"407", "阵雪"}, {"408", "小到中雪"},
|
||
{"409", "中到大雪"}, {"410", "大到暴雪"}, {"456", "阵雨夹雪"}, {"457", "阵雪"},
|
||
{"499", "雪"}, {"500", "薄雾"}, {"501", "雾"}, {"502", "霾"},
|
||
{"503", "扬沙"}, {"504", "浮尘"}, {"507", "沙尘暴"}, {"508", "强沙尘暴"},
|
||
{"509", "浓雾"}, {"510", "强浓雾"}, {"511", "中度霾"}, {"512", "重度霾"},
|
||
{"513", "严重霾"}, {"514", "大雾"}, {"515", "特强浓雾"}, {"900", "热"},
|
||
{"901", "冷"}, {"999", "未知"}
|
||
};
|
||
|
||
static std::string MapLangCode(const std::string& lang) {
|
||
if (lang.empty()) return "zh";
|
||
if (lang == "zh_CN" || lang == "zh" || lang == "zh-Hans") return "zh";
|
||
if (lang == "zh_HK" || lang == "zh-TW" || lang == "zh-Hant") return "zh-Hant";
|
||
if (lang == "en_US" || lang == "en") return "en";
|
||
if (lang == "ja_JP" || lang == "ja") return "ja";
|
||
if (lang == "ko_KR" || lang == "ko") return "ko";
|
||
if (lang == "fr_FR" || lang == "fr") return "fr";
|
||
if (lang == "de_DE" || lang == "de") return "de";
|
||
if (lang == "es_ES" || lang == "es") return "es";
|
||
return "zh";
|
||
}
|
||
|
||
static bool IsGzipData(const std::string& data) {
|
||
if (data.size() < 2) return false;
|
||
const unsigned char* p = reinterpret_cast<const unsigned char*>(data.data());
|
||
return p[0] == 0x1F && p[1] == 0x8B;
|
||
}
|
||
|
||
static bool GzipDecompress(const std::string& in, std::string& out) {
|
||
if (in.empty()) return false;
|
||
z_stream strm;
|
||
memset(&strm, 0, sizeof(strm));
|
||
strm.next_in = (Bytef*)in.data();
|
||
strm.avail_in = (uInt)in.size();
|
||
int ret = inflateInit2(&strm, 16 + MAX_WBITS);
|
||
if (ret != Z_OK) {
|
||
ESP_LOGE(TAG, "inflateInit2失败: %d", ret);
|
||
return false;
|
||
}
|
||
std::vector<char> buf(1024);
|
||
while (true) {
|
||
strm.next_out = (Bytef*)buf.data();
|
||
strm.avail_out = (uInt)buf.size();
|
||
ret = inflate(&strm, Z_NO_FLUSH);
|
||
if (ret == Z_STREAM_END) {
|
||
size_t produced = buf.size() - strm.avail_out;
|
||
if (produced) out.append(buf.data(), produced);
|
||
break;
|
||
} else if (ret == Z_OK || ret == Z_BUF_ERROR) {
|
||
size_t produced = buf.size() - strm.avail_out;
|
||
if (produced) out.append(buf.data(), produced);
|
||
continue;
|
||
} else {
|
||
ESP_LOGE(TAG, "inflate失败: %d", ret);
|
||
inflateEnd(&strm);
|
||
return false;
|
||
}
|
||
}
|
||
inflateEnd(&strm);
|
||
return true;
|
||
}
|
||
|
||
// 缓存条目结构体,包含时间戳信息
|
||
typedef struct {
|
||
time_t timestamp;// 缓存条目时间戳
|
||
} CacheEntry;
|
||
|
||
// 缓存上限常量
|
||
#define MAX_WIFI_CITY_CACHE 5
|
||
|
||
// WeatherApi构造函数,初始化配置参数
|
||
WeatherApi::WeatherApi() {
|
||
// 硬编码配置参数 - 从get_weather.py文件中提取的配置,与小智server保持一致
|
||
api_host_ = "kq3aapg9h5.re.qweatherapi.com";
|
||
api_key_ = "aa5ec0859c144ac7b33966e25eef5580";
|
||
default_location_ = "北京"; // 默认城市设置为北京
|
||
kid_ = "T45F5GTR8Y";
|
||
project_id_ = "4N855TEVNN";
|
||
private_key_ = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIA26lz31HoaZV17EjIGcyo9YNGGQ77/gOZU8Chw8wlWq\n-----END PRIVATE KEY-----";
|
||
|
||
ESP_LOGI(TAG, "初始化天气API配置 - 默认城市: %s", default_location_.c_str());
|
||
ESP_LOGI(TAG, "WiFi位置缓存限制已设置为: %d 条", MAX_WIFI_CITY_CACHE);
|
||
}
|
||
|
||
// 生成JWT令牌(预留接口,当前版本暂不实现完整JWT认证)
|
||
std::string WeatherApi::GenerateJwtToken() {
|
||
ESP_LOGI(TAG, "JWT令牌生成预留接口");
|
||
return "";
|
||
}
|
||
|
||
// 封装HTTP GET请求
|
||
bool WeatherApi::HttpGet(const std::string& url, const std::string& headers, std::string& response) {
|
||
ESP_LOGI(TAG, "HTTP请求: %s", url.c_str());
|
||
|
||
esp_http_client_config_t config = {};
|
||
config.url = url.c_str();
|
||
config.method = HTTP_METHOD_GET;
|
||
config.transport_type = HTTP_TRANSPORT_OVER_SSL;
|
||
#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||
#else
|
||
ESP_LOGE(TAG, "证书包未启用,无法进行服务器证书验证。请在 menuconfig 启用 ESP-TLS Certificate Bundle");
|
||
#endif
|
||
|
||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||
if (!client) {
|
||
ESP_LOGE(TAG, "HTTP客户端初始化失败");
|
||
return false;
|
||
}
|
||
|
||
if (!headers.empty()) {
|
||
esp_http_client_set_header(client, "Authorization", headers.c_str());
|
||
}
|
||
|
||
esp_http_client_set_header(client, "User-Agent",
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||
"(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36");
|
||
esp_http_client_set_header(client, "Accept", "application/json");
|
||
// esp_http_client_set_header(client, "Accept-Encoding", "identity");// 禁用压缩,防止解压失败
|
||
|
||
esp_err_t err = esp_http_client_open(client, 0);
|
||
bool success = false;
|
||
|
||
if (err == ESP_OK) {
|
||
int64_t headers_len = esp_http_client_fetch_headers(client);
|
||
int status_code = esp_http_client_get_status_code(client);
|
||
ESP_LOGI(TAG, "HTTP状态码: %d, 头长度: %lld", status_code, (long long)headers_len);
|
||
if (status_code == 200) {
|
||
char buf[512];
|
||
int read_len;
|
||
int total = 0;
|
||
while ((read_len = esp_http_client_read(client, buf, sizeof(buf))) > 0) {
|
||
response.append(buf, read_len);
|
||
total += read_len;
|
||
}
|
||
if (IsGzipData(response)) {
|
||
std::string decompressed;
|
||
if (GzipDecompress(response, decompressed)) {
|
||
response.swap(decompressed);
|
||
ESP_LOGI(TAG, "GZIP解压成功,解压后长度: %d", (int)response.size());
|
||
} else {
|
||
ESP_LOGE(TAG, "GZIP解压失败,保留原始响应");
|
||
}
|
||
}
|
||
success = (total > 0);
|
||
ESP_LOGI(TAG, "读取完成,累计长度: %d", total);
|
||
if (!success) {
|
||
ESP_LOGE(TAG, "读取响应内容失败");
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "HTTP请求失败,状态码: %d", status_code);
|
||
}
|
||
esp_http_client_close(client);
|
||
} else {
|
||
ESP_LOGE(TAG, "HTTP请求执行失败: %s", esp_err_to_name(err));
|
||
}
|
||
|
||
esp_http_client_cleanup(client);
|
||
|
||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||
|
||
return success;
|
||
}
|
||
|
||
// URL编码函数
|
||
std::string UrlEncode(const std::string& str) {
|
||
std::string encoded;
|
||
for (char c : str) {
|
||
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||
encoded += c;
|
||
} else {
|
||
encoded += '%';
|
||
encoded += "0123456789ABCDEF"[static_cast<unsigned char>(c) / 16];
|
||
encoded += "0123456789ABCDEF"[static_cast<unsigned char>(c) % 16];
|
||
}
|
||
}
|
||
return encoded;
|
||
}
|
||
|
||
// 获取城市信息
|
||
std::string WeatherApi::FetchCityInfo(const std::string& location, const std::string& lang) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 开始获取城市信息,location参数: '%s', 长度: %zu", location.c_str(), location.length());
|
||
|
||
// 对location参数进行URL编码
|
||
std::string encoded_location = UrlEncode(location);
|
||
ESP_LOGI(TAG, "[FetchCityInfo] URL编码后: '%s'", encoded_location.c_str());
|
||
|
||
// 构建城市信息查询URL
|
||
std::string q_lang = MapLangCode(lang);
|
||
std::string url = "https://" + api_host_ + "/geo/v2/city/lookup?key=" + api_key_ +
|
||
"\u0026location=" + encoded_location + "\u0026lang=" + q_lang;
|
||
std::string response;
|
||
|
||
ESP_LOGI(TAG, "[FetchCityInfo] API主机: '%s', API密钥长度: %zu", api_host_.c_str(), api_key_.length());
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 查询城市信息URL: '%s'", url.c_str());
|
||
|
||
// 发送HTTP请求
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 开始发送HTTP GET请求");
|
||
if (HttpGet(url, "", response)) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] HTTP请求成功,响应长度: %zu 字节", response.length());
|
||
ESP_LOGD(TAG, "[FetchCityInfo] 响应内容前100字节: '%s'", response.substr(0, std::min(size_t(100), response.length())).c_str());
|
||
|
||
// 解析JSON响应
|
||
cJSON* root = cJSON_Parse(response.c_str());
|
||
if (root) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] JSON解析成功");
|
||
|
||
// 检查响应状态
|
||
cJSON* code = cJSON_GetObjectItem(root, "code");
|
||
if (code) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 响应状态码存在: '%s'", code->valuestring);
|
||
if (cJSON_IsString(code) && strcmp(code->valuestring, "200") == 0) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 响应状态码为200,成功");
|
||
|
||
// 获取location数组
|
||
cJSON* location_array = cJSON_GetObjectItem(root, "location");
|
||
if (location_array) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] location数组存在");
|
||
if (cJSON_IsArray(location_array)) {
|
||
int array_size = cJSON_GetArraySize(location_array);
|
||
ESP_LOGI(TAG, "[FetchCityInfo] location数组大小: %d", array_size);
|
||
|
||
if (array_size > 0) {
|
||
// 获取第一个城市信息
|
||
cJSON* first_location = cJSON_GetArrayItem(location_array, 0);
|
||
if (first_location) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 获取到第一个城市信息对象");
|
||
|
||
// 获取城市ID
|
||
cJSON* id = cJSON_GetObjectItem(first_location, "id");
|
||
if (id) {
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 城市ID字段存在");
|
||
if (cJSON_IsString(id)) {
|
||
std::string city_id = id->valuestring;
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 成功获取城市ID: '%s'", city_id.c_str());
|
||
cJSON_Delete(root);
|
||
return city_id;
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] 城市ID不是字符串类型");
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] 未能获取城市ID字段");
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] 未能获取第一个城市信息对象");
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] location数组为空");
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] location不是数组类型");
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] 响应中没有location数组");
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] 城市信息API返回错误码: '%s'", code->valuestring);
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] 响应中没有code字段");
|
||
}
|
||
cJSON_Delete(root);
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] 解析城市信息JSON失败");
|
||
ESP_LOGD(TAG, "[FetchCityInfo] 失败的JSON响应: '%s'", response.c_str());
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[FetchCityInfo] HTTP请求失败");
|
||
}
|
||
|
||
ESP_LOGI(TAG, "[FetchCityInfo] 函数结束,返回空字符串");
|
||
return "";
|
||
}
|
||
|
||
// 解析并打印IP位置信息- IP查询
|
||
static bool ParseAndPrintIpInfo(const std::string& response) {
|
||
ESP_LOGI(TAG, "开始解析IP位置信息JSON");
|
||
|
||
// 解析JSON响应
|
||
cJSON* root = cJSON_Parse(response.c_str());
|
||
if (!root) {
|
||
const char* error_ptr = cJSON_GetErrorPtr();
|
||
ESP_LOGE(TAG, "解析IP信息JSON失败: %s", error_ptr ? error_ptr : "未知错误");
|
||
return false;
|
||
}
|
||
|
||
// 提取并打印各个字段(ip-api.com 格式)
|
||
cJSON* status = cJSON_GetObjectItem(root, "status");
|
||
cJSON* ip = cJSON_GetObjectItem(root, "query");
|
||
cJSON* country = cJSON_GetObjectItem(root, "country");
|
||
cJSON* region = cJSON_GetObjectItem(root, "region");
|
||
cJSON* regionName = cJSON_GetObjectItem(root, "regionName");
|
||
cJSON* city = cJSON_GetObjectItem(root, "city");
|
||
cJSON* zip = cJSON_GetObjectItem(root, "zip");
|
||
cJSON* lat = cJSON_GetObjectItem(root, "lat");
|
||
cJSON* lon = cJSON_GetObjectItem(root, "lon");
|
||
cJSON* timezone = cJSON_GetObjectItem(root, "timezone");
|
||
cJSON* isp = cJSON_GetObjectItem(root, "isp");
|
||
cJSON* org = cJSON_GetObjectItem(root, "org");
|
||
cJSON* asn = cJSON_GetObjectItem(root, "as");
|
||
|
||
// 打印解析结果
|
||
ESP_LOGI(TAG, "=============== IP位置信息 ===============");
|
||
if (status && cJSON_IsString(status)) {
|
||
ESP_LOGI(TAG, "状态: %s", status->valuestring);
|
||
}
|
||
if (ip && cJSON_IsString(ip)) {
|
||
ESP_LOGI(TAG, "IP: %s", ip->valuestring);
|
||
}
|
||
if (country && cJSON_IsString(country)) {
|
||
ESP_LOGI(TAG, "国家: %s", country->valuestring);
|
||
}
|
||
if (region && cJSON_IsString(region)) {
|
||
ESP_LOGI(TAG, "区域代码: %s", region->valuestring);
|
||
}
|
||
if (regionName && cJSON_IsString(regionName)) {
|
||
ESP_LOGI(TAG, "省份: %s", regionName->valuestring);
|
||
}
|
||
if (city && cJSON_IsString(city)) {
|
||
ESP_LOGI(TAG, "城市: %s", city->valuestring);
|
||
}
|
||
if (zip && cJSON_IsString(zip)) {
|
||
ESP_LOGI(TAG, "邮编: %s", zip->valuestring);
|
||
}
|
||
if (lat && cJSON_IsNumber(lat)) {
|
||
ESP_LOGI(TAG, "纬度: %.4f", lat->valuedouble);
|
||
}
|
||
if (lon && cJSON_IsNumber(lon)) {
|
||
ESP_LOGI(TAG, "经度: %.4f", lon->valuedouble);
|
||
}
|
||
if (timezone && cJSON_IsString(timezone)) {
|
||
ESP_LOGI(TAG, "时区: %s", timezone->valuestring);
|
||
}
|
||
if (isp && cJSON_IsString(isp)) {
|
||
ESP_LOGI(TAG, "运营商: %s", isp->valuestring);
|
||
}
|
||
if (org && cJSON_IsString(org)) {
|
||
ESP_LOGI(TAG, "组织: %s", org->valuestring);
|
||
}
|
||
if (asn && cJSON_IsString(asn)) {
|
||
ESP_LOGI(TAG, "AS号: %s", asn->valuestring);
|
||
}
|
||
ESP_LOGI(TAG, "======================================");
|
||
|
||
// 释放JSON对象
|
||
cJSON_Delete(root);
|
||
return true;
|
||
}
|
||
|
||
// 获取IP位置信息(ip-api.com)
|
||
// 自动获取设备当前IP的地理位置信息
|
||
std::string WeatherApi::GetIpInfo() {
|
||
ESP_LOGI(TAG, "[GetIpInfo] 开始获取IP位置信息");
|
||
|
||
// 构建IP查询API的URL
|
||
std::string url = "http://ip-api.com/json/?lang=zh-CN";
|
||
ESP_LOGI(TAG, "[GetIpInfo] 查询URL: %s", url.c_str());
|
||
std::string response;
|
||
std::string city_info = default_location_; // 默认返回默认位置
|
||
|
||
// 使用HTTP
|
||
esp_http_client_config_t config = {};
|
||
config.url = url.c_str();
|
||
config.method = HTTP_METHOD_GET;
|
||
config.transport_type = HTTP_TRANSPORT_OVER_TCP; // 使用HTTP而非HTTPS
|
||
|
||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||
if (!client) {
|
||
ESP_LOGE(TAG, "[GetIpInfo] HTTP客户端初始化失败");
|
||
return city_info;
|
||
}
|
||
|
||
// 设置请求头
|
||
esp_http_client_set_header(client, "User-Agent",
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||
"(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36");
|
||
|
||
esp_http_client_set_header(client, "Accept-Encoding", "identity");
|
||
esp_err_t err = esp_http_client_open(client, 0);
|
||
if (err == ESP_OK) {
|
||
int64_t headers_len = esp_http_client_fetch_headers(client);
|
||
int status_code = esp_http_client_get_status_code(client);
|
||
ESP_LOGI(TAG, "[GetIpInfo] HTTP状态码: %d, 头长度: %lld", status_code, (long long)headers_len);
|
||
if (status_code == 200) {
|
||
char buf[512];
|
||
int read_len;
|
||
while ((read_len = esp_http_client_read(client, buf, sizeof(buf))) > 0) {
|
||
response.append(buf, read_len);
|
||
}
|
||
|
||
ESP_LOGI(TAG, "[GetIpInfo] 获取到响应,长度: %zu 字节", response.length());
|
||
|
||
if (ParseAndPrintIpInfo(response)) {
|
||
cJSON* root = cJSON_Parse(response.c_str());
|
||
if (root) {
|
||
cJSON* city = cJSON_GetObjectItem(root, "city");
|
||
cJSON* regionName = cJSON_GetObjectItem(root, "regionName");
|
||
if (city && cJSON_IsString(city) && city->valuestring[0] != '\0') {
|
||
city_info = city->valuestring;
|
||
} else if (regionName && cJSON_IsString(regionName)) {
|
||
city_info = regionName->valuestring;
|
||
}
|
||
cJSON_Delete(root);
|
||
}
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[GetIpInfo] HTTP请求失败,状态码: %d", status_code);
|
||
}
|
||
|
||
esp_http_client_close(client);// 关闭HTTP客户端连接,释放资源
|
||
} else {
|
||
ESP_LOGE(TAG, "[GetIpInfo] HTTP请求执行失败: %s", esp_err_to_name(err));
|
||
}
|
||
|
||
esp_http_client_cleanup(client);// 清理HTTP客户端资源,释放内存
|
||
vTaskDelay(100 / portTICK_PERIOD_MS);// 延时100ms,确保资源释放完成
|
||
|
||
ESP_LOGI(TAG, "[GetIpInfo] 返回城市信息: %s", city_info.c_str());
|
||
return city_info;// 返回提取的城市信息
|
||
}
|
||
|
||
// 获取天气信息主函数
|
||
std::string WeatherApi::GetWeather(const std::string& location, const std::string& lang) {
|
||
ESP_LOGI(TAG, "[GetWeather] ===== 开始获取天气信息 =====");
|
||
ESP_LOGI(TAG, "[GetWeather] 输入参数: location='%s' (长度: %zu), lang='%s'",
|
||
location.c_str(), location.length(), lang.c_str());
|
||
|
||
// 确定查询位置 - 当location为空或为"None"字符串时,使用默认城市
|
||
std::string query_location = (location.empty() || location == "None") ? default_location_ : location;
|
||
ESP_LOGI(TAG, "[GetWeather] 查询位置: '%s'", query_location.c_str());
|
||
|
||
// 检查缓存 - 简单的内存缓存
|
||
std::string cache_key = "weather_" + query_location;
|
||
ESP_LOGI(TAG, "[GetWeather] 缓存键: '%s'", cache_key.c_str());
|
||
|
||
auto it = weather_cache_.find(cache_key);
|
||
if (it != weather_cache_.end()) {
|
||
ESP_LOGI(TAG, "[GetWeather] 使用缓存的天气信息: '%s'", query_location.c_str());
|
||
ESP_LOGI(TAG, "[GetWeather] ===== 天气信息获取完成 (缓存命中) =====");
|
||
return it->second; // 返回缓存的结果
|
||
}
|
||
|
||
ESP_LOGI(TAG, "[GetWeather] 缓存未命中,开始获取天气信息: '%s'", query_location.c_str());
|
||
|
||
// 获取城市ID
|
||
ESP_LOGI(TAG, "[GetWeather] 开始获取城市ID");
|
||
std::string city_id = FetchCityInfo(query_location, lang);
|
||
ESP_LOGI(TAG, "[GetWeather] FetchCityInfo返回结果: city_id='%s' (长度: %zu)",
|
||
city_id.c_str(), city_id.length());
|
||
|
||
if (city_id.empty()) {
|
||
ESP_LOGI(TAG, "[GetWeather] 未找到相关城市信息,请确认城市名称是否正确");
|
||
ESP_LOGI(TAG, "[GetWeather] ===== 天气信息获取失败 (城市ID为空) =====");
|
||
return "未找到相关城市信息,请确认城市名称是否正确";
|
||
}
|
||
|
||
std::string q_lang = MapLangCode(lang);// 映射语言代码为API需要的格式
|
||
std::string weather_url = "https://" + api_host_ + "/v7/weather/now?key=" + api_key_ + "\u0026location=" + city_id + "\u0026lang=" + q_lang;
|
||
std::string weather_response;// 存储天气API响应的字符串
|
||
|
||
ESP_LOGI(TAG, "[GetWeather] 构建天气查询URL: '%s'", weather_url.c_str());
|
||
std::string result = "";// 存储最终格式化的天气信息
|
||
|
||
// 发送天气查询请求
|
||
ESP_LOGI(TAG, "[GetWeather] 开始发送天气查询HTTP请求");
|
||
if (HttpGet(weather_url, "", weather_response)) {
|
||
ESP_LOGI(TAG, "[GetWeather] HTTP请求成功,响应长度: %zu 字节", weather_response.length());
|
||
|
||
// 解析天气JSON响应
|
||
ESP_LOGI(TAG, "[GetWeather] 开始解析天气JSON响应");
|
||
cJSON* root = cJSON_Parse(weather_response.c_str());
|
||
if (root) {
|
||
ESP_LOGI(TAG, "[GetWeather] JSON解析成功");
|
||
|
||
// 检查响应状态
|
||
cJSON* code = cJSON_GetObjectItem(root, "code");
|
||
if (code && cJSON_IsString(code) && strcmp(code->valuestring, "200") == 0) {
|
||
ESP_LOGI(TAG, "[GetWeather] 响应状态码: 200 (成功)");
|
||
|
||
// 获取天气数据
|
||
cJSON* now = cJSON_GetObjectItem(root, "now");
|
||
if (now) {
|
||
ESP_LOGI(TAG, "[GetWeather] 获取到now字段,开始构建天气报告");
|
||
|
||
// 构建天气报告
|
||
result = "前天气情况 (" + query_location + ")\n\n";
|
||
|
||
// 获取天气描述
|
||
cJSON* text = cJSON_GetObjectItem(now, "text");
|
||
if (text && cJSON_IsString(text)) {
|
||
result += "天气状况: " + std::string(text->valuestring) + "\n";
|
||
ESP_LOGI(TAG, "[GetWeather] 天气状况: '%s'", text->valuestring);
|
||
} else {
|
||
ESP_LOGW(TAG, "[GetWeather] 未能获取天气状况");
|
||
}
|
||
|
||
// 获取温度
|
||
cJSON* temp = cJSON_GetObjectItem(now, "temp");
|
||
if (temp && cJSON_IsString(temp)) {
|
||
result += "当前温度: " + std::string(temp->valuestring) + "摄氏度\n";
|
||
ESP_LOGI(TAG, "[GetWeather] 当前温度: '%s'摄氏度", temp->valuestring);
|
||
} else {
|
||
ESP_LOGW(TAG, "[GetWeather] 未能获取当前温度");
|
||
}
|
||
|
||
// 获取体感温度
|
||
cJSON* feelsLike = cJSON_GetObjectItem(now, "feelsLike");
|
||
if (feelsLike && cJSON_IsString(feelsLike)) {
|
||
result += "体感温度: " + std::string(feelsLike->valuestring) + "摄氏度\n";
|
||
ESP_LOGI(TAG, "[GetWeather] 体感温度: '%s'摄氏度", feelsLike->valuestring);
|
||
} else {
|
||
ESP_LOGW(TAG, "[GetWeather] 未能获取体感温度");
|
||
}
|
||
|
||
// 获取风向风速
|
||
cJSON* windDir = cJSON_GetObjectItem(now, "windDir");
|
||
cJSON* windScale = cJSON_GetObjectItem(now, "windScale");
|
||
if (windDir && cJSON_IsString(windDir) && windScale && cJSON_IsString(windScale)) {
|
||
result += "风向风速: " + std::string(windDir->valuestring) + " " +
|
||
std::string(windScale->valuestring) + "级\n";
|
||
ESP_LOGI(TAG, "[GetWeather] 风向风速: '%s' %s级", windDir->valuestring, windScale->valuestring);
|
||
} else {
|
||
ESP_LOGW(TAG, "[GetWeather] 未能获取风向风速信息");
|
||
}
|
||
|
||
// 获取湿度
|
||
cJSON* humidity = cJSON_GetObjectItem(now, "humidity");
|
||
if (humidity && cJSON_IsString(humidity)) {
|
||
result += "相对湿度: " + std::string(humidity->valuestring) + "%\n";
|
||
ESP_LOGI(TAG, "[GetWeather] 相对湿度: '%s'%%", humidity->valuestring);
|
||
} else {
|
||
ESP_LOGW(TAG, "[GetWeather] 未能获取相对湿度");
|
||
}
|
||
|
||
// 获取气压
|
||
cJSON* pressure = cJSON_GetObjectItem(now, "pressure");
|
||
if (pressure && cJSON_IsString(pressure)) {
|
||
result += "大气压强: " + std::string(pressure->valuestring) + "hPa\n";
|
||
ESP_LOGI(TAG, "[GetWeather] 大气压强: '%s'hPa", pressure->valuestring);
|
||
} else {
|
||
ESP_LOGW(TAG, "[GetWeather] 未能获取大气压强");
|
||
}
|
||
|
||
// 添加更新时间
|
||
result += std::string("\n数据更新时间: ") + __DATE__ + " " + __TIME__;
|
||
ESP_LOGI(TAG, "[GetWeather] 数据更新时间: %s %s", __DATE__, __TIME__);
|
||
|
||
} else {
|
||
ESP_LOGE(TAG, "[GetWeather] 无法获取当前天气数据(now字段为空)");
|
||
result = "无法获取当前天气数据";
|
||
}
|
||
} else {
|
||
const char* error_code = code && cJSON_IsString(code) ? code->valuestring : "未知";
|
||
ESP_LOGE(TAG, "[GetWeather] 天气API请求失败,错误码: '%s'", error_code);
|
||
result = "天气API请求失败,错误码: " + std::string(error_code);
|
||
}
|
||
cJSON_Delete(root);
|
||
} else {
|
||
ESP_LOGE(TAG, "[GetWeather] 解析天气数据失败,请稍后重试");
|
||
result = "解析天气数据失败,请稍后重试";
|
||
}
|
||
} else {
|
||
ESP_LOGE(TAG, "[GetWeather] 网络请求失败,无法获取天气信息");
|
||
result = "网络请求失败,无法获取天气信息";
|
||
}
|
||
|
||
// 缓存结果 - 简单实现,实际项目中应考虑缓存过期时间
|
||
if (!result.empty()) {
|
||
ESP_LOGI(TAG, "[GetWeather] 开始缓存天气结果,当前缓存大小: %zu", weather_cache_.size());
|
||
weather_cache_[cache_key] = result;
|
||
|
||
// 限制缓存大小
|
||
if (weather_cache_.size() > 10) {
|
||
ESP_LOGI(TAG, "[GetWeather] 缓存大小超过10,移除最旧的缓存项");
|
||
weather_cache_.erase(weather_cache_.begin());
|
||
}
|
||
ESP_LOGI(TAG, "[GetWeather] 缓存完成,当前缓存大小: %zu", weather_cache_.size());
|
||
}
|
||
|
||
ESP_LOGI(TAG, "[GetWeather] 天气信息获取完成");
|
||
ESP_LOGI(TAG, "[GetWeather] ===== 天气信息获取流程结束 =====");
|
||
return result;
|
||
}
|
||
|
||
// 创建全局WeatherApi实例
|
||
WeatherApi g_weather_api;
|
||
|
||
// 实现全局GetWeatherInfo函数,用于获取天气信息// 全局函数,供外部调用
|
||
std::string GetWeatherInfo(const std::string& location, const std::string& lang) {
|
||
ESP_LOGI(TAG, "[GetWeatherInfo] ===== 全局函数调用开始 =====");
|
||
ESP_LOGI(TAG, "[GetWeatherInfo] 收到调用请求,location='%s' (长度: %zu), lang='%s'",location.c_str(), location.length(), lang.c_str());
|
||
|
||
// 调用g_weather_api单例的GetWeather方法
|
||
ESP_LOGI(TAG, "[GetWeatherInfo] 准备调用g_weather_api.GetWeather");
|
||
std::string result = g_weather_api.GetWeather(location, lang);// 调用单例实例的GetWeather方法获取天气信息
|
||
|
||
ESP_LOGI(TAG, "[GetWeatherInfo] g_weather_api.GetWeather调用完成,结果长度: %zu 字节", result.length());
|
||
ESP_LOGD(TAG, "[GetWeatherInfo] 返回结果前100字节: '%s'", result.substr(0, std::min(size_t(100), result.length())).c_str());
|
||
ESP_LOGI(TAG, "[GetWeatherInfo] ===== 全局函数调用结束 =====");
|
||
return result;
|
||
}
|
||
|
||
std::string WeatherApi::GetDefaultLocation() const {
|
||
return default_location_;// 返回默认位置
|
||
}
|
||
|
||
// 新增方法:自动检测NVS中是否存在当前位置的城市信息,并设置当前位置
|
||
void WeatherApi::AutoDetectLocation() {
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] ===== 开始自动检测位置 =====");
|
||
wifi_config_t wc{};// 定义WiFi配置结构体
|
||
esp_wifi_get_config(WIFI_IF_STA, &wc);// 获取当前WiFi配置
|
||
std::string ssid = std::string(reinterpret_cast<char*>(wc.sta.ssid));// 转换SSID为字符串
|
||
wifi_ap_record_t ap{};// 定义AP记录结构体
|
||
std::string bssid;// 定义BSSID字符串
|
||
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
|
||
char buf[18];// 定义BSSID缓冲区
|
||
snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x",
|
||
ap.bssid[0], ap.bssid[1], ap.bssid[2],
|
||
ap.bssid[3], ap.bssid[4], ap.bssid[5]);// 格式化BSSID为字符串
|
||
bssid.assign(buf);// 赋值给BSSID字符串
|
||
}
|
||
nvs_handle_t h;// 定义NVS句柄
|
||
// 打开NVS命名空间"wifi_city_map",读写模式
|
||
if (nvs_open("wifi_city_map", NVS_READWRITE, &h) == ESP_OK) {
|
||
auto try_get = [&](const std::string& key)->std::string{
|
||
size_t len = 0;
|
||
// 尝试获取NVS中存储的城市字符串
|
||
if (nvs_get_str(h, key.c_str(), NULL, &len) == ESP_OK && len > 0) {
|
||
std::vector<char> buf(len);// 定义缓冲区
|
||
// 尝试从NVS获取城市字符串
|
||
if (nvs_get_str(h, key.c_str(), buf.data(), &len) == ESP_OK) {
|
||
return std::string(buf.data());// 返回城市字符串
|
||
}
|
||
}
|
||
return std::string();// 未找到对应城市,返回空字符串
|
||
};
|
||
std::string city;// 定义城市字符串
|
||
if (!ssid.empty()) {
|
||
if (!bssid.empty()) {
|
||
city = try_get(ssid + "|" + bssid);// 先尝试SSID+BSSID
|
||
}
|
||
if (city.empty()) {
|
||
city = try_get(ssid);// 如果未找到,再尝试SSID
|
||
}
|
||
}
|
||
// 如果仍未从NVS中找到城市信息,将调用位置API获取城市信息
|
||
if (city.empty()) {
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 未从NVS命中城市信息,将调用位置API获取城市信息");
|
||
std::string detected = GetIpInfo();// 获取IP位置信息 调用位置查询API获取当前位置的城市信息
|
||
if (!detected.empty()) {
|
||
default_location_ = detected;// 更新默认城市为检测到的位置
|
||
if (!ssid.empty()) {
|
||
size_t cache_count = 0;// 初始化缓存条目数量
|
||
std::vector<std::string> cache_entries; // 定义缓存条目向量,存储所有键
|
||
std::vector<std::string> deletable_entries; // 存储可删除的条目(排除当前使用的SSID和SSID+BSSID)
|
||
|
||
// 遍历NVS中的所有键
|
||
nvs_iterator_t it = NULL;
|
||
esp_err_t res = nvs_entry_find(NULL, "wifi_city_map", NVS_TYPE_STR, &it);// 查找所有字符串类型的条目
|
||
while (res == ESP_OK) {
|
||
nvs_entry_info_t info; // 定义NVS条目信息结构体
|
||
nvs_entry_info(it, &info);// 获取当前迭代器指向的条目信息
|
||
|
||
std::string key = info.key; // 获取当前条目的键名
|
||
cache_entries.push_back(key);// 将键添加到缓存向量中
|
||
cache_count++;// 增加缓存条目数量
|
||
|
||
// 检查是否为当前使用的键,如果不是则添加到可删除列表
|
||
std::string current_full_key = ssid;
|
||
if (!bssid.empty()) {current_full_key = ssid + "|" + bssid;}// 组合SSID和BSSID作为完整键
|
||
|
||
// 排除当前正在使用的SSID和SSID+BSSID键
|
||
if (key != ssid && key != current_full_key) {deletable_entries.push_back(key);}// 将键添加到可删除列表
|
||
res = nvs_entry_next(&it);// 移动到下一个条目
|
||
}
|
||
nvs_release_iterator(it);// 释放NVS迭代器
|
||
|
||
// 如果缓存数量超过限制且有可删除的条目,随机删除一个
|
||
if (cache_count >= MAX_WIFI_CITY_CACHE && !deletable_entries.empty()) {
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] WiFi位置缓存数量(%zu)达到限制(%d),开始随机删除策略",cache_count, MAX_WIFI_CITY_CACHE);
|
||
|
||
// 随机选择一个条目删除(使用更简单的方式选择索引),ESP32平台可以使用esp_random()获得更好的随机性,但这里保持简单实现
|
||
int random_index = (int)(esp_timer_get_time() % deletable_entries.size());// 生成随机索引
|
||
const std::string& key_to_delete = deletable_entries[random_index];// 获取随机选择的键名
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 随机删除缓存条目: %s", key_to_delete.c_str());
|
||
nvs_erase_key(h, key_to_delete.c_str());// 删除城市字符串键
|
||
}
|
||
|
||
// 保存新的位置信息
|
||
if (!bssid.empty()) {
|
||
const std::string full_key = ssid + "|" + bssid;// 组合SSID和BSSID作为完整键
|
||
nvs_set_str(h, full_key.c_str(), detected.c_str());// 缓存SSID+BSSID到城市
|
||
}
|
||
nvs_set_str(h, ssid.c_str(), detected.c_str());// 缓存SSID到城市
|
||
|
||
if (nvs_commit(h) == ESP_OK) {
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 城市信息保存到NVS成功!");
|
||
} else {
|
||
ESP_LOGW(TAG, "[AutoDetectLocation] 城市信息保存到NVS失败!");
|
||
}
|
||
}
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 自动检测到位置: '%s',已更新默认城市", detected.c_str());
|
||
} else {
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 位置检测失败或未变化,保持默认城市: '%s'", default_location_.c_str());
|
||
}
|
||
}
|
||
// 如果从NVS命中城市,找到了城市信息
|
||
else {
|
||
default_location_ = city;// 更新默认城市为NVS中命中的城市
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 从NVS命中位置: '%s',已更新默认城市", city.c_str());
|
||
}
|
||
nvs_close(h);// 关闭NVS句柄
|
||
}
|
||
// 如果打开NVS检索城市信息失败
|
||
else {
|
||
std::string detected = GetIpInfo();// 调用位置API获取城市信息
|
||
if (!detected.empty()) {
|
||
default_location_ = detected;// 更新默认城市为检测到的位置
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 自动检测到位置: '%s',已更新默认城市", detected.c_str());
|
||
} else {
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] 位置检测失败或未变化,保持默认城市: '%s'", default_location_.c_str());
|
||
}
|
||
}
|
||
ESP_LOGI(TAG, "[AutoDetectLocation] ===== 位置检测完成 =====");
|
||
}
|
||
|
||
// 全局函数:供应用程序网络连接后调用,用于自动检测位置
|
||
void AutoDetectAndSetLocation() {
|
||
ESP_LOGI(TAG, "[AutoDetectAndSetLocation] 调用全局函数自动检测位置");
|
||
g_weather_api.AutoDetectLocation();// 自动检测NVS中是否存在当前位置的城市信息,并设置当前位置
|
||
}
|