first commit
This commit is contained in:
commit
0ef2017d18
28 files changed
+1882
No files matched your search
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef AMBIENT_WEATHER_H
|
||||
#define AMBIENT_WEATHER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/*
|
||||
* Fields mirror the Ambient Weather device data spec:
|
||||
* https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs
|
||||
*
|
||||
* Note the API reports no textual "condition" or "icon" -- it is a personal
|
||||
* weather station feed, not a forecast service.
|
||||
*/
|
||||
typedef struct {
|
||||
float temp_f;
|
||||
float humidity; /* percent */
|
||||
float wind_mph;
|
||||
bool has_temp;
|
||||
bool has_humidity;
|
||||
bool has_wind;
|
||||
time_t last_update;
|
||||
} weather_data_t;
|
||||
|
||||
esp_err_t weather_init(void);
|
||||
esp_err_t weather_update(weather_data_t *data);
|
||||
void weather_get_string(const weather_data_t *data, char *buf, size_t len);
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user