/** * @file weather_service.cc * @brief Weather API service implementation * @version 1.0 */ #include "weather_service.h" #include #include #include #include static const char *TAG = "WeatherService"; WeatherService::WeatherService() { city_code_ = "101010100"; // Default: Beijing auto_detect_enabled_ = false; } WeatherService::~WeatherService() { } void WeatherService::Initialize(const std::string& city_code) { if (city_code.empty()) { // Auto-detect city code by IP ESP_LOGI(TAG, "Auto-detecting city code by IP address..."); if (AutoDetectCityCode()) { ESP_LOGI(TAG, "Auto-detected city code: %s", city_code_.c_str()); auto_detect_enabled_ = true; } else { ESP_LOGW(TAG, "Failed to auto-detect city code, using default: Beijing (101010100)"); city_code_ = "101010100"; auto_detect_enabled_ = false; } } else { city_code_ = city_code; auto_detect_enabled_ = false; ESP_LOGI(TAG, "Weather service initialized with city code: %s", city_code_.c_str()); } } bool WeatherService::AutoDetectCityCode() { // Use weather.com.cn IP geolocation API time_t now; time(&now); char url[256]; snprintf(url, sizeof(url), "http://wgeo.weather.com.cn/ip/?_=%ld", (long)now); ESP_LOGI(TAG, "Fetching city code from: %s", url); // Allocate response buffer char *buffer = (char*)malloc(2048); if (!buffer) { ESP_LOGE(TAG, "Failed to allocate buffer"); return false; } memset(buffer, 0, 2048); // Configure HTTP client with larger buffer esp_http_client_config_t config = {}; config.url = url; config.timeout_ms = 15000; config.buffer_size = 2048; // Important: increase buffer size config.buffer_size_tx = 1024; config.disable_auto_redirect = false; config.max_redirection_count = 3; esp_http_client_handle_t client = esp_http_client_init(&config); if (!client) { ESP_LOGE(TAG, "Failed to init HTTP client"); free(buffer); return false; } // Set headers esp_http_client_set_header(client, "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); esp_http_client_set_header(client, "Referer", "http://www.weather.com.cn/"); esp_http_client_set_header(client, "Accept", "*/*"); bool success = false; // Open connection and read esp_err_t err = esp_http_client_open(client, 0); if (err == ESP_OK) { int content_length = esp_http_client_fetch_headers(client); int status_code = esp_http_client_get_status_code(client); ESP_LOGI(TAG, "HTTP Status = %d, Content-Length = %d", status_code, content_length); if (status_code == 200 || status_code == 302) { // Read response data int total_read = 0; int read_len; while (total_read < 2047) { read_len = esp_http_client_read(client, buffer + total_read, 2047 - total_read); if (read_len <= 0) { break; } total_read += read_len; } buffer[total_read] = '\0'; ESP_LOGI(TAG, "Read %d bytes from API", total_read); if (total_read > 0) { // Print first 200 chars for debugging char preview[201]; int preview_len = (total_read > 200) ? 200 : total_read; memcpy(preview, buffer, preview_len); preview[preview_len] = '\0'; ESP_LOGI(TAG, "Response preview: %s", preview); std::string response(buffer); // Try multiple parsing patterns size_t id_pos = response.find("id=\""); if (id_pos != std::string::npos) { // Pattern: id="101010100" (JavaScript variable with double quotes) size_t start = id_pos + 4; // Skip id=" size_t end = response.find("\"", start); if (end != std::string::npos && end > start) { city_code_ = response.substr(start, end - start); ESP_LOGI(TAG, "✅ Detected city code: %s", city_code_.c_str()); success = true; } } else if ((id_pos = response.find("id='")) != std::string::npos) { // Pattern: id='101010100' (JavaScript variable with single quotes) size_t start = id_pos + 4; size_t end = response.find("'", start); if (end != std::string::npos && end > start) { city_code_ = response.substr(start, end - start); ESP_LOGI(TAG, "✅ Detected city code: %s", city_code_.c_str()); success = true; } } else if ((id_pos = response.find("id\":\"")) != std::string::npos) { // Pattern: "id":"101010100" (JSON format) size_t start = id_pos + 5; size_t end = response.find("\"", start); if (end != std::string::npos && end > start) { city_code_ = response.substr(start, end - start); ESP_LOGI(TAG, "✅ Detected city code: %s", city_code_.c_str()); success = true; } } else { ESP_LOGW(TAG, "❌ City code pattern not found in response"); } } else { ESP_LOGW(TAG, "❌ No data read from API"); } } else { ESP_LOGW(TAG, "❌ HTTP status: %d", status_code); } esp_http_client_close(client); } else { ESP_LOGE(TAG, "❌ Failed to connect: %s", esp_err_to_name(err)); } esp_http_client_cleanup(client); free(buffer); return success; } void WeatherService::SetWeatherCallback(std::function callback) { weather_callback_ = callback; } void WeatherService::FetchWeather() { ESP_LOGI(TAG, "Fetching weather data for city: %s", city_code_.c_str()); // Construct URL time_t now; time(&now); char url[256]; snprintf(url, sizeof(url), "http://d1.weather.com.cn/weather_index/%s.html?_=%ld", city_code_.c_str(), (long)now); // Configure HTTP client (no event handler, read directly) esp_http_client_config_t config = {}; config.url = url; config.timeout_ms = 10000; esp_http_client_handle_t client = esp_http_client_init(&config); // Set User-Agent header esp_http_client_set_header(client, "User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38"); esp_http_client_set_header(client, "Referer", "http://www.weather.com.cn/"); // Open connection esp_err_t err = esp_http_client_open(client, 0); if (err == ESP_OK) { // Read response int content_length = esp_http_client_fetch_headers(client); int status_code = esp_http_client_get_status_code(client); ESP_LOGI(TAG, "HTTP GET Status = %d, Content-Length = %d", status_code, content_length); if (status_code == 200) { // Allocate buffer (use 8KB if content_length unknown or too large) int buffer_size = (content_length > 0 && content_length < 8192) ? content_length + 1 : 8192; char *buffer = (char*)malloc(buffer_size); if (buffer) { int total_read = 0; int read_len; // Read all data while ((read_len = esp_http_client_read(client, buffer + total_read, buffer_size - total_read - 1)) > 0) { total_read += read_len; if (total_read >= buffer_size - 1) { break; } } buffer[total_read] = '\0'; ESP_LOGI(TAG, "Read %d bytes of weather data", total_read); std::string response(buffer); ParseWeatherData(response); if (weather_callback_) { weather_callback_(last_weather_data_); } free(buffer); } else { ESP_LOGE(TAG, "Failed to allocate buffer for response"); } } else { ESP_LOGW(TAG, "HTTP request returned status code: %d", status_code); } } else { ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); } esp_http_client_close(client); esp_http_client_cleanup(client); } std::string WeatherService::ExtractJsonValue(const std::string& json, const std::string& key) { size_t key_pos = json.find("\"" + key + "\":"); if (key_pos == std::string::npos) { return ""; } size_t value_start = json.find("\"", key_pos + key.length() + 3); if (value_start == std::string::npos) { return ""; } value_start++; size_t value_end = json.find("\"", value_start); if (value_end == std::string::npos) { return ""; } return json.substr(value_start, value_end - value_start); } void WeatherService::ParseWeatherData(const std::string& response) { ESP_LOGI(TAG, "Parsing weather data..."); try { // Extract dataSK JSON section size_t sk_start = response.find("dataSK ="); size_t sk_end = response.find(";var dataZS"); if (sk_start != std::string::npos && sk_end != std::string::npos) { std::string dataSK = response.substr(sk_start + 8, sk_end - sk_start - 8); // Parse JSON using cJSON cJSON *root = cJSON_Parse(dataSK.c_str()); if (root) { cJSON *city = cJSON_GetObjectItem(root, "cityname"); cJSON *temp = cJSON_GetObjectItem(root, "temp"); cJSON *humidity = cJSON_GetObjectItem(root, "SD"); cJSON *weather = cJSON_GetObjectItem(root, "weather"); cJSON *wind_dir = cJSON_GetObjectItem(root, "WD"); cJSON *wind_speed = cJSON_GetObjectItem(root, "WS"); cJSON *aqi = cJSON_GetObjectItem(root, "aqi"); if (city && cJSON_IsString(city)) { last_weather_data_.city_name = city->valuestring; } if (temp && cJSON_IsString(temp)) { last_weather_data_.temperature = temp->valuestring; } if (humidity && cJSON_IsString(humidity)) { last_weather_data_.humidity = humidity->valuestring; } if (weather && cJSON_IsString(weather)) { last_weather_data_.weather_desc = weather->valuestring; } if (wind_dir && cJSON_IsString(wind_dir)) { last_weather_data_.wind_direction = wind_dir->valuestring; } if (wind_speed && cJSON_IsString(wind_speed)) { last_weather_data_.wind_speed = wind_speed->valuestring; } if (aqi && cJSON_IsNumber(aqi)) { last_weather_data_.aqi = aqi->valueint; // Determine AQI description if (last_weather_data_.aqi > 200) { last_weather_data_.aqi_desc = "重度"; } else if (last_weather_data_.aqi > 150) { last_weather_data_.aqi_desc = "中度"; } else if (last_weather_data_.aqi > 100) { last_weather_data_.aqi_desc = "轻度"; } else if (last_weather_data_.aqi > 50) { last_weather_data_.aqi_desc = "良"; } else { last_weather_data_.aqi_desc = "优"; } } cJSON_Delete(root); } } // Extract forecast data (f section) size_t fc_start = response.find("\"f\":["); size_t fc_end = response.find(",{\"fa", fc_start); if (fc_start != std::string::npos && fc_end != std::string::npos) { std::string dataFC = response.substr(fc_start + 5, fc_end - fc_start - 5); cJSON *root = cJSON_Parse(dataFC.c_str()); if (root) { cJSON *temp_low = cJSON_GetObjectItem(root, "fd"); cJSON *temp_high = cJSON_GetObjectItem(root, "fc"); if (temp_low && cJSON_IsString(temp_low)) { last_weather_data_.temp_low = temp_low->valuestring; } if (temp_high && cJSON_IsString(temp_high)) { last_weather_data_.temp_high = temp_high->valuestring; } cJSON_Delete(root); } } last_weather_data_.last_update_time = esp_timer_get_time() / 1000000; ESP_LOGI(TAG, "Weather parsed: City=%s, Temp=%s℃, Humidity=%s, AQI=%d(%s)", last_weather_data_.city_name.c_str(), last_weather_data_.temperature.c_str(), last_weather_data_.humidity.c_str(), last_weather_data_.aqi, last_weather_data_.aqi_desc.c_str()); } catch (...) { ESP_LOGE(TAG, "Failed to parse weather data"); } }