Home Assistant requests in a task, progress bar, Plex page, bitmap cache

- net/ha_api: the HTTP requests run in a FreeRTOS task on core 0; loop()
  only hands over copies through queues. The web UI's test button polls.
  No more 3-4 s loop() stalls once a minute; timeout raised to 5 s.
- Progress bar page element (page format 4, v3 pages are converted)
- Plex user setting (settings v6): {plex_user} in entity ids
- Plex page (examples): watchers, play/pause icon, title, progress
- Bitmaps cached in RAM: a LittleFS read took ~100 ms on every render
  and web preview; long JSON lists sent in 2 KB pieces
- Backup includes the refresh settings and the Plex user

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-16 16:17:23 +02:00
1 parent 090c2bf8ae
commit 490cefc360
42 files changed
+1074 -175

No files matched your search

+76
View File
@@ -25,10 +25,62 @@ static bool printable(const char *text, size_t size)
return true;
}
bool expandEntity(const char *entity, const char *user, char *out, size_t out_size)
{
const char *at = strstr(entity, PLEX_USER_PLACEHOLDER);
int n;
if (at)
n = snprintf(out, out_size, "%.*s%s%s", (int)(at - entity), entity, user,
at + strlen(PLEX_USER_PLACEHOLDER));
else
n = snprintf(out, out_size, "%s", entity);
return n >= 0 && (size_t)n < out_size;
}
void plexUserSlug(const char *name, char *out, size_t out_size)
{
size_t n = 0;
bool gap = false;
for (const char *c = name; *c && n + 1 < out_size; c++)
{
if (isalnum((unsigned char)*c))
{
if (gap && n > 0 && n + 2 < out_size)
out[n++] = '_';
gap = false;
out[n++] = tolower((unsigned char)*c);
}
else
gap = true;
}
out[n] = '\0';
}
bool elementUsesHa(const PageElement &element)
{
switch (element.type)
{
case ElementType::HaValue:
return true;
case ElementType::Image:
case ElementType::Bar:
return element.entity[0] != '\0';
default:
return false;
}
}
bool validEntityId(const char *entity)
{
if (entity == nullptr)
return false;
// Checked with a stand-in user name
if (strstr(entity, PLEX_USER_PLACEHOLDER))
{
char full[ELEMENT_ENTITY_SIZE];
return strlen(entity) < (size_t)ELEMENT_ENTITY_SIZE && !strstr(strstr(entity, "}") + 1, "{") &&
expandEntity(entity, "x", full, sizeof(full)) && validEntityId(full);
}
const char *dot = strchr(entity, '.');
if (dot == nullptr || dot == entity || dot[1] == '\0' || strchr(dot + 1, '.'))
return false;
@@ -125,6 +177,15 @@ const char *validatePage(const DynamicPage &page)
return "Scrolling: window up to 168 dots, speed 1-30 dots per second";
if (e.type == ElementType::HaValue && !validEntityId(e.entity))
return "Home Assistant values need an entity id like sensor.outside_temperature";
if (e.type == ElementType::Bar)
{
if (!validEntityId(e.entity))
return "Progress bars need an entity id like sensor.progress";
if (e.bar_width < 1 || e.bar_width > BITMAP_WIDTH || e.bar_height < 1 || e.bar_height > BITMAP_HEIGHT)
return "Progress bar: width 1-84, height 1-19 dots";
if (!isfinite(e.bar_max) || e.bar_max <= 0)
return "Progress bar: the full value must be a number above 0";
}
if (e.type == ElementType::Countdown && e.target >= MAX_TARGET)
return "The countdown date must be before 2100";
if (e.type == ElementType::Image)
@@ -645,6 +706,7 @@ void elementText(const PageElement &e, const LocalTime &t, const ElementValues &
formatStopwatch(e, values.stopwatch_ms, out, size);
break;
case ElementType::Image:
case ElementType::Bar:
out[0] = '\0';
break;
default:
@@ -653,6 +715,20 @@ void elementText(const PageElement &e, const LocalTime &t, const ElementValues &
}
}
int barFill(const PageElement &e, const char *ha_value, int fill_width)
{
double v;
if (!ha_value || !parseNumber(ha_value, v) || fill_width <= 0)
return 0;
double part = v * e.factor / e.bar_max;
if (!(part > 0)) // also NaN
return 0;
if (part >= 1)
return fill_width;
int n = (int)(part * fill_width + 0.5);
return n;
}
bool elementShowsSeconds(const PageElement &e, const LocalTime &t, bool stopwatch_running)
{
switch (e.type)