/* * Ambient Weather REST API client. * * GET https://rt.ambientweather.net/v1/devices/{macAddress} * ?apiKey=...&applicationKey=...&limit=1 * * Returns a JSON array of data records, newest first. */ #include #include #include #include #include "cJSON.h" #include "esp_crt_bundle.h" #include "esp_http_client.h" #include "esp_log.h" #include "app_config.h" #include "ambient_weather.h" static const char *TAG = "ambient_weather"; #define WEATHER_URL_MAX 320 #define WEATHER_RESP_MAX 4096 /* A first TLS handshake on this hardware can take well over 10s. */ #define WEATHER_TIMEOUT_MS 30000 static char resp_buf[WEATHER_RESP_MAX]; static size_t resp_len = 0; static esp_err_t http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_CONNECTED: resp_len = 0; break; case HTTP_EVENT_ON_DATA: /* * Accumulate into a fixed buffer. The response is chunked, so * Content-Length is often -1 -- sizing an allocation from it (as this * previously did) yields a zero-length buffer and a heap overflow. */ if (resp_len + evt->data_len < sizeof(resp_buf)) { memcpy(resp_buf + resp_len, evt->data, evt->data_len); resp_len += evt->data_len; } else { ESP_LOGW(TAG, "Response exceeds %d bytes, truncating", WEATHER_RESP_MAX); } break; default: break; } return ESP_OK; } esp_err_t weather_init(void) { if (AMBIENT_WEATHER_API_KEY[0] == '\0' || AMBIENT_WEATHER_APP_KEY[0] == '\0' || AMBIENT_WEATHER_DEVICE_ID[0] == '\0') { ESP_LOGW(TAG, "API credentials not set in main/app_config.h -- " "weather updates will be skipped"); return ESP_ERR_INVALID_STATE; } return ESP_OK; } static void parse_record(const cJSON *record, weather_data_t *data) { const cJSON *tempf = cJSON_GetObjectItemCaseSensitive(record, "tempf"); if (cJSON_IsNumber(tempf)) { data->temp_f = (float)tempf->valuedouble; data->has_temp = true; } const cJSON *humidity = cJSON_GetObjectItemCaseSensitive(record, "humidity"); if (cJSON_IsNumber(humidity)) { data->humidity = (float)humidity->valuedouble; data->has_humidity = true; } const cJSON *wind = cJSON_GetObjectItemCaseSensitive(record, "windspeedmph"); if (cJSON_IsNumber(wind)) { data->wind_mph = (float)wind->valuedouble; data->has_wind = true; } } esp_err_t weather_update(weather_data_t *data) { if (data == NULL) { return ESP_ERR_INVALID_ARG; } /* Caller may reuse the struct across polls; start from a known state so a * partial response cannot leave stale values looking fresh. */ memset(data, 0, sizeof(*data)); char url[WEATHER_URL_MAX]; const int written = snprintf(url, sizeof(url), "https://rt.ambientweather.net/v1/devices/%s" "?apiKey=%s&applicationKey=%s&limit=1", AMBIENT_WEATHER_DEVICE_ID, AMBIENT_WEATHER_API_KEY, AMBIENT_WEATHER_APP_KEY); if (written < 0 || written >= (int)sizeof(url)) { ESP_LOGE(TAG, "URL truncated -- check credential lengths"); return ESP_ERR_INVALID_SIZE; } const esp_http_client_config_t config = { .url = url, .method = HTTP_METHOD_GET, .timeout_ms = WEATHER_TIMEOUT_MS, .event_handler = http_event_handler, .crt_bundle_attach = esp_crt_bundle_attach, }; esp_http_client_handle_t client = esp_http_client_init(&config); if (client == NULL) { return ESP_ERR_NO_MEM; } esp_err_t err = esp_http_client_perform(client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return err; } const int status = esp_http_client_get_status_code(client); esp_http_client_cleanup(client); if (status != 200) { ESP_LOGE(TAG, "HTTP %d from Ambient Weather", status); return ESP_FAIL; } if (resp_len == 0) { ESP_LOGE(TAG, "Empty response"); return ESP_FAIL; } resp_buf[resp_len] = '\0'; cJSON *root = cJSON_Parse(resp_buf); if (root == NULL) { ESP_LOGE(TAG, "Failed to parse JSON response"); return ESP_FAIL; } err = ESP_FAIL; if (cJSON_IsArray(root) && cJSON_GetArraySize(root) > 0) { parse_record(cJSON_GetArrayItem(root, 0), data); if (data->has_temp) { data->last_update = time(NULL); err = ESP_OK; } else { ESP_LOGW(TAG, "Response contained no tempf field"); } } else { ESP_LOGW(TAG, "Response was not a non-empty array"); } cJSON_Delete(root); return err; } void weather_get_string(const weather_data_t *data, char *buf, size_t len) { if (data == NULL || buf == NULL || len == 0) { return; } int n = snprintf(buf, len, "%.1f°F", data->has_temp ? data->temp_f : 0.0f); if (data->has_humidity && n > 0 && (size_t)n < len) { n += snprintf(buf + n, len - n, " · %.0f%% RH", data->humidity); } if (data->has_wind && n > 0 && (size_t)n < len) { snprintf(buf + n, len - n, " · %.1f mph", data->wind_mph); } }