137 lines
4.4 KiB
C
137 lines
4.4 KiB
C
/*
|
|
* The data contract between "fetching" and "drawing".
|
|
*
|
|
* ha_data_t is plain data with no ESP-IDF or network dependency, and
|
|
* ha_parse_json() is a pure function. That is what lets the desktop simulator
|
|
* run the real UI: sim/ supplies a JSON snapshot or a LAN fetch, the firmware
|
|
* supplies esp_http_client, and both hand the identical struct to the
|
|
* identical drawing code.
|
|
*
|
|
* The UI layer must never see HTTP, cJSON or an entity ID -- only this.
|
|
*/
|
|
#ifndef HA_DATA_H
|
|
#define HA_DATA_H
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
|
|
#include "ha_config.h"
|
|
|
|
/* Five days across the bottom strip. HA returns six; the sixth is dropped. */
|
|
#define HA_FORECAST_DAYS 5
|
|
|
|
/* Derived from HA_LIGHT_LIST so the two can never disagree. */
|
|
#define HA_LIGHT_COUNT_X(a, b, c) +1
|
|
#define HA_LIGHT_COUNT (0 HA_LIGHT_LIST(HA_LIGHT_COUNT_X))
|
|
|
|
/*
|
|
* A reading that may not exist.
|
|
*
|
|
* Sensors go unavailable routinely -- a dead anemometer, a flat battery, an
|
|
* integration reloading -- and a float alone cannot say so. Every consumer has
|
|
* to check `valid` before touching `value`; there is deliberately no sentinel
|
|
* to accidentally render as a real number.
|
|
*/
|
|
typedef struct {
|
|
float value;
|
|
bool valid;
|
|
} ha_value_t;
|
|
|
|
typedef enum {
|
|
HA_LIGHT_OFF = 0,
|
|
HA_LIGHT_ON,
|
|
HA_LIGHT_UNAVAILABLE, /* entity exists but HA cannot reach the device */
|
|
} ha_light_state_t;
|
|
|
|
typedef struct {
|
|
ha_light_state_t state;
|
|
/* 0-255 as HA reports it. Only meaningful when state is HA_LIGHT_ON and
|
|
* the entity is dimmable; -1 otherwise. */
|
|
int16_t brightness;
|
|
} ha_light_status_t;
|
|
|
|
typedef struct {
|
|
const char *entity_id;
|
|
const char *label;
|
|
bool dimmable;
|
|
} ha_light_cfg_t;
|
|
|
|
/* Generated from HA_LIGHT_LIST; index-aligned with ha_data_t.lights. */
|
|
extern const ha_light_cfg_t HA_LIGHTS[HA_LIGHT_COUNT];
|
|
|
|
typedef struct {
|
|
char day[4]; /* "Mon" -- derived from the ISO date, see below */
|
|
char condition[24]; /* met.no condition slug, e.g. "partlycloudy" */
|
|
ha_value_t high_f;
|
|
ha_value_t low_f;
|
|
} ha_forecast_day_t;
|
|
|
|
typedef struct {
|
|
/* Weather station */
|
|
ha_value_t outdoor_temp_f;
|
|
ha_value_t outdoor_humidity;
|
|
ha_value_t indoor_temp_f;
|
|
ha_value_t indoor_humidity;
|
|
ha_value_t dew_point_f;
|
|
ha_value_t pressure_inhg;
|
|
ha_value_t rain_today_in;
|
|
ha_value_t rain_rate_inhr;
|
|
ha_value_t wind_mph;
|
|
ha_value_t gust_mph;
|
|
ha_value_t wind_dir_deg;
|
|
|
|
/* Forecast / sun. These strings arrive pre-formatted in local time --
|
|
* HA owns the timezone, so the device needs no clock and no SNTP. */
|
|
char condition[24];
|
|
char sunrise[12]; /* "6:05 AM" */
|
|
char sunset[12]; /* "7:45 PM" */
|
|
char clock[12]; /* "10:21 AM" */
|
|
char date[16]; /* "Tue Aug 4" */
|
|
bool sun_up;
|
|
|
|
ha_light_status_t lights[HA_LIGHT_COUNT];
|
|
|
|
ha_forecast_day_t forecast[HA_FORECAST_DAYS];
|
|
uint8_t forecast_count; /* 0 when no forecast has been fetched yet */
|
|
} ha_data_t;
|
|
|
|
/*
|
|
* Parse the reply to the state template into `out`.
|
|
*
|
|
* Zeroes `out` first, so a field missing from the response reads as invalid
|
|
* rather than keeping a previous poll's value. Returns false only when the
|
|
* body is not parseable JSON -- individual missing or unavailable sensors are
|
|
* normal and leave their ha_value_t invalid.
|
|
*
|
|
* Does not touch the forecast fields; those come from ha_parse_forecast_json()
|
|
* on a slower poll and would otherwise be wiped every minute.
|
|
*/
|
|
bool ha_parse_json(const char *json, ha_data_t *out);
|
|
|
|
/*
|
|
* Parse a weather/get_forecasts service response into out->forecast.
|
|
* Fills at most HA_FORECAST_DAYS entries and sets forecast_count.
|
|
*/
|
|
bool ha_parse_forecast_json(const char *json, ha_data_t *out);
|
|
|
|
/*
|
|
* "light" or "switch" for entity_id -- the HA service domain to call. The
|
|
* patio plug is a switch, and calling light.turn_on on it silently does
|
|
* nothing.
|
|
*/
|
|
const char *ha_service_domain(const char *entity_id);
|
|
|
|
/* Human-readable form of a met.no condition slug ("partlycloudy" ->
|
|
* "Partly Cloudy"). Returns the slug unchanged if unrecognised, so an
|
|
* unhandled condition shows up on screen as the thing to go add. */
|
|
const char *ha_condition_label(const char *slug);
|
|
|
|
/* As above, abbreviated for the forecast strip's narrow columns
|
|
* ("partlycloudy" -> "Partly"). */
|
|
const char *ha_condition_short(const char *slug);
|
|
|
|
/* Compass point for a bearing in degrees ("NNE"). */
|
|
const char *ha_wind_cardinal(float degrees);
|
|
|
|
#endif
|