next iteration on display, still some artifacting issues, and ux with buttons states and updating
This commit is contained in:
1 parent
45998b6176
commit
ae5d12b3a2
14 files changed
+860
-398
No files matched your search
@@ -63,15 +63,19 @@
|
||||
* put the sofa icon on the patio. Only ui_manager.c expands it; the api-layer
|
||||
* consumers ignore the argument, so this header does not include ui_icons.h.
|
||||
*
|
||||
* Note light.main_room is a GROUP containing blue_lamp, brown_lamp and
|
||||
* landing_chandelier. Toggling it changes three of the other tiles; that is
|
||||
* expected, and the UI reflects it on the next poll.
|
||||
* light.main_room used to be here and was removed deliberately: it is a
|
||||
* GROUP of blue_lamp, brown_lamp and landing_chandelier -- three of the
|
||||
* other tiles. One tap restyled four tiles at once, which both confused the
|
||||
* optimistic-hold logic and triggered a redraw storm big enough to glitch
|
||||
* the panel (see README on PSRAM bandwidth). Groups spanning other tiles do
|
||||
* not belong in this list. light.tv_room is also a group (of 2), but of
|
||||
* lights that have no tile of their own, which is fine.
|
||||
*/
|
||||
/* Labels must fit one line of Montserrat 16 in a ~125px tile -- roughly 11
|
||||
* characters. Longer ones are truncated with an ellipsis, which reads as a
|
||||
* mistake. "Dining Room" is one character too wide, hence "Dining". */
|
||||
#define HA_LIGHT_LIST(X) \
|
||||
X("light.main_room", "Main Room", true, ICON_SOFA) \
|
||||
X("light.tv_room", "TV Room", true, ICON_TV) \
|
||||
X("light.dining_room", "Dining", true, ICON_DINING) \
|
||||
X("light.blue_lamp", "Blue Lamp", true, ICON_FLOOR_LAMP) \
|
||||
X("light.brown_lamp", "Brown Lamp", true, ICON_DESK_LAMP) \
|
||||
|
||||
@@ -305,6 +305,120 @@ const char *ha_condition_short(const char *slug)
|
||||
return (c != NULL) ? c->shrt : (slug != NULL ? slug : "--");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ trends */
|
||||
|
||||
/*
|
||||
* 20 slots, one kept per ~4 minutes, covers ~80 minutes. Feeding faster than
|
||||
* that overwrites the newest slot instead of consuming a new one, so a 60s
|
||||
* poll cadence still fills the ring at 4-minute spacing.
|
||||
*
|
||||
* Size matters here more than it looks: this array is .bss, which lives in
|
||||
* internal RAM on the S3 -- the same internal RAM the RGB panel's bounce
|
||||
* buffers come from, with only a few KB of slack (README, memory notes). A
|
||||
* naive 72-slot-per-poll ring cost 2.6KB and was enough to tip display_init
|
||||
* into "no mem for bounce buffer" on an unlucky boot. Keep this structure
|
||||
* small; if it ever needs to grow, it must move to PSRAM instead.
|
||||
*
|
||||
* Static per channel; a reboot forgets history, and the display shows no
|
||||
* arrow until enough accumulates again (see ha_data.h).
|
||||
*/
|
||||
#define TREND_SLOTS 20
|
||||
#define TREND_BUCKET_MS (4u * 60u * 1000u)
|
||||
|
||||
typedef struct {
|
||||
float value;
|
||||
uint32_t ms;
|
||||
bool used;
|
||||
} trend_sample_t;
|
||||
|
||||
static trend_sample_t trend_ring[HA_TREND_CHANNELS][TREND_SLOTS];
|
||||
static int trend_head[HA_TREND_CHANNELS]; /* slot most recently written */
|
||||
|
||||
void ha_trend_feed(int channel, float value, uint32_t now_ms)
|
||||
{
|
||||
if (channel < 0 || channel >= HA_TREND_CHANNELS) {
|
||||
return;
|
||||
}
|
||||
|
||||
trend_sample_t *newest = &trend_ring[channel][trend_head[channel]];
|
||||
|
||||
/* Inside the current bucket: refresh the newest sample in place, so
|
||||
* compute always sees the latest reading without spending a slot. */
|
||||
if (newest->used && (now_ms - newest->ms) < TREND_BUCKET_MS) {
|
||||
newest->value = value;
|
||||
return;
|
||||
}
|
||||
|
||||
const int next = newest->used ? (trend_head[channel] + 1) % TREND_SLOTS
|
||||
: trend_head[channel];
|
||||
trend_ring[channel][next] = (trend_sample_t){ value, now_ms, true };
|
||||
trend_head[channel] = next;
|
||||
}
|
||||
|
||||
ha_trend_t ha_trend_compute(int channel, float threshold_per_hour, uint32_t now_ms)
|
||||
{
|
||||
if (channel < 0 || channel >= HA_TREND_CHANNELS) {
|
||||
return HA_TREND_UNKNOWN;
|
||||
}
|
||||
|
||||
/* The newest sample is the one just fed; find the usable sample whose
|
||||
* age is closest to the window. Unsigned subtraction keeps this correct
|
||||
* across tick wrap. */
|
||||
const trend_sample_t *newest = NULL;
|
||||
const trend_sample_t *anchor = NULL;
|
||||
uint32_t best_dist = UINT32_MAX;
|
||||
|
||||
for (int i = 0; i < TREND_SLOTS; i++) {
|
||||
const trend_sample_t *s = &trend_ring[channel][i];
|
||||
if (!s->used) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t age = now_ms - s->ms;
|
||||
if (age > HA_TREND_WINDOW_MS + HA_TREND_MIN_AGE_MS) {
|
||||
continue; /* too old even for a sparse ring; ignore */
|
||||
}
|
||||
|
||||
if (newest == NULL || age < (now_ms - newest->ms)) {
|
||||
newest = s;
|
||||
}
|
||||
|
||||
if (age >= HA_TREND_MIN_AGE_MS) {
|
||||
const uint32_t dist = (age > HA_TREND_WINDOW_MS)
|
||||
? age - HA_TREND_WINDOW_MS
|
||||
: HA_TREND_WINDOW_MS - age;
|
||||
if (dist < best_dist) {
|
||||
best_dist = dist;
|
||||
anchor = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newest == NULL || anchor == NULL || newest == anchor) {
|
||||
return HA_TREND_UNKNOWN;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scale the threshold to the actual span so a 20-minute-old anchor uses
|
||||
* a proportionally smaller bar than a full hour would. Without this the
|
||||
* first hour after boot under-reports every trend.
|
||||
*/
|
||||
const uint32_t span_ms = anchor->ms == newest->ms
|
||||
? 1
|
||||
: newest->ms - anchor->ms;
|
||||
const float threshold = threshold_per_hour *
|
||||
((float)span_ms / (float)HA_TREND_WINDOW_MS);
|
||||
|
||||
const float delta = newest->value - anchor->value;
|
||||
if (delta > threshold) {
|
||||
return HA_TREND_RISING;
|
||||
}
|
||||
if (delta < -threshold) {
|
||||
return HA_TREND_FALLING;
|
||||
}
|
||||
return HA_TREND_FLAT;
|
||||
}
|
||||
|
||||
const char *ha_wind_cardinal(float degrees)
|
||||
{
|
||||
static const char *const points[16] = {
|
||||
|
||||
@@ -56,6 +56,27 @@ typedef struct {
|
||||
bool dimmable;
|
||||
} ha_light_cfg_t;
|
||||
|
||||
/*
|
||||
* Which way a reading has moved over the last hour.
|
||||
*
|
||||
* UNKNOWN is deliberately 0 so a freshly wiped ha_data_t renders no arrow --
|
||||
* after boot there is no history, and "no arrow" is the honest display until
|
||||
* at least HA_TREND_MIN_AGE_MS of samples exist.
|
||||
*/
|
||||
typedef enum {
|
||||
HA_TREND_UNKNOWN = 0,
|
||||
HA_TREND_FLAT,
|
||||
HA_TREND_RISING,
|
||||
HA_TREND_FALLING,
|
||||
} ha_trend_t;
|
||||
|
||||
enum {
|
||||
HA_TREND_CH_INDOOR = 0,
|
||||
HA_TREND_CH_OUTDOOR,
|
||||
HA_TREND_CH_PRESSURE,
|
||||
HA_TREND_CHANNELS,
|
||||
};
|
||||
|
||||
/* Generated from HA_LIGHT_LIST; index-aligned with ha_data_t.lights. */
|
||||
extern const ha_light_cfg_t HA_LIGHTS[HA_LIGHT_COUNT];
|
||||
|
||||
@@ -93,6 +114,16 @@ typedef struct {
|
||||
|
||||
ha_forecast_day_t forecast[HA_FORECAST_DAYS];
|
||||
uint8_t forecast_count; /* 0 when no forecast has been fetched yet */
|
||||
|
||||
/*
|
||||
* Filled by the CALLER after parsing, from ha_trend_feed/compute --
|
||||
* the parser wipes them to UNKNOWN. Local ring-buffer history, not an HA
|
||||
* query: an /api/history call returns hundreds of points per entity,
|
||||
* which is silly when the device already sees every reading each poll.
|
||||
*/
|
||||
ha_trend_t trend_indoor;
|
||||
ha_trend_t trend_outdoor;
|
||||
ha_trend_t trend_pressure;
|
||||
} ha_data_t;
|
||||
|
||||
/*
|
||||
@@ -133,4 +164,19 @@ const char *ha_condition_short(const char *slug);
|
||||
/* Compass point for a bearing in degrees ("NNE"). */
|
||||
const char *ha_wind_cardinal(float degrees);
|
||||
|
||||
/*
|
||||
* Trend tracking. Feed one sample per poll; compute compares the newest
|
||||
* sample against the one closest to an hour old. Timestamps come from the
|
||||
* caller (lv_tick_get() or xTaskGetTickCount-derived ms) so this file stays
|
||||
* free of LVGL and FreeRTOS. Wrap-safe on uint32 ms.
|
||||
*
|
||||
* compute returns UNKNOWN until the oldest usable sample is at least
|
||||
* HA_TREND_MIN_AGE_MS old -- "trending" over 90 seconds is noise.
|
||||
*/
|
||||
#define HA_TREND_WINDOW_MS (60u * 60u * 1000u)
|
||||
#define HA_TREND_MIN_AGE_MS (5u * 60u * 1000u)
|
||||
|
||||
void ha_trend_feed(int channel, float value, uint32_t now_ms);
|
||||
ha_trend_t ha_trend_compute(int channel, float threshold_per_hour, uint32_t now_ms);
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user