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

+43 -8
View File
@@ -1,6 +1,7 @@
#include "display/bitmap_store.h"
#include <LittleFS.h>
#include <map>
#include <strings.h>
#include "display/bitmaps.h"
@@ -22,6 +23,18 @@ static const BuiltinBitmap BUILTIN[] = {
static bool mounted = false;
static uint32_t revision = 0;
// Bitmaps read before, also the names that don't exist (images try a name per state, then
// "default"). Reading one from LittleFS takes about 100 ms, and pages are drawn again for
// every frame (scrolling, seconds) and for every preview in the web UI. Cleared when a
// bitmap is saved or deleted, and when it holds CACHE_MAX names.
struct CachedBitmap
{
bool exists;
uint8_t data[BITMAP_BYTES];
};
static std::map<String, CachedBitmap> cache;
static const size_t CACHE_MAX = 64;
static const BuiltinBitmap *builtin(const char *name)
{
for (const BuiltinBitmap &b : BUILTIN)
@@ -99,7 +112,8 @@ bool bitmapExists(const char *name)
{
if (builtin(name))
return true;
return mounted && validBitmapName(name) && LittleFS.exists(path(name));
uint8_t bitmap[BITMAP_BYTES];
return loadBitmap(name, bitmap); // through the cache
}
bool loadBitmap(const char *name, uint8_t *bitmap)
@@ -110,14 +124,33 @@ bool loadBitmap(const char *name, uint8_t *bitmap)
memcpy_P(bitmap, b->data, BITMAP_BYTES);
return true;
}
if (!mounted || !validBitmapName(name))
return false;
auto cached = cache.find(name);
if (cached != cache.end())
{
if (cached->second.exists)
memcpy(bitmap, cached->second.data, BITMAP_BYTES);
return cached->second.exists;
}
// Checked first: open() logs an error for a missing file (images try names by state)
if (!mounted || !validBitmapName(name) || !LittleFS.exists(path(name)))
return false;
File file = LittleFS.open(path(name), "r");
if (!file)
return false;
bool ok = file.read(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
file.close();
bool ok = false;
if (LittleFS.exists(path(name)))
{
File file = LittleFS.open(path(name), "r");
if (file)
{
ok = file.read(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
file.close();
}
}
if (cache.size() >= CACHE_MAX)
cache.clear();
CachedBitmap &entry = cache[name];
entry.exists = ok;
if (ok)
memcpy(entry.data, bitmap, BITMAP_BYTES);
return ok;
}
@@ -138,6 +171,7 @@ const char *saveBitmap(const char *name, const uint8_t *bitmap)
bool ok = file.write(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
file.close();
revision++;
cache.clear();
return ok ? nullptr : "Storage full";
}
@@ -150,5 +184,6 @@ const char *deleteBitmap(const char *name)
if (!LittleFS.remove(path(name)))
return "Could not delete the file";
revision++;
cache.clear();
return nullptr;
}
+73 -2
View File
@@ -8,7 +8,7 @@
static const char *DIR = "/pages";
static const char *EXTENSION = ".pg";
// Increase when DynamicPage changes; older files are ignored unless converted below
static const uint16_t VERSION = 3;
static const uint16_t VERSION = 4;
struct StoredPage
{
@@ -149,7 +149,66 @@ static void convert(const Page &old, DynamicPage &page)
}
} // namespace v2
static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(StoredPage),
// Version 3 (before the progress bar): the elements lacked the fields from `bar_width` on.
// The rest has the same layout, so it is copied as a block.
namespace v3
{
struct Element
{
ElementType type;
int16_t x, y;
Align align;
Overlay overlay;
char font[PAGE_NAME_SIZE];
uint8_t format;
Language language;
int8_t decimals;
char text[ELEMENT_TEXT_SIZE];
char suffix[ELEMENT_SUFFIX_SIZE];
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
float factor;
uint8_t scroll_width;
uint8_t scroll_speed;
uint32_t target;
HaShow show;
uint8_t input_unit;
bool auto_unit;
bool absolute;
char map[ELEMENT_MAP_SIZE];
};
struct Page
{
uint8_t slot_count;
PageSlot slots[PAGE_SLOTS];
uint8_t element_count;
Element elements[PAGE_ELEMENTS];
};
struct Stored
{
uint16_t version;
uint16_t size;
Page page;
};
static const size_t COMMON = offsetof(Element, map) + sizeof(Element::map);
static_assert(offsetof(PageElement, map) == offsetof(Element, map), "same layout up to map");
static void convert(const Page &old, DynamicPage &page)
{
page = DynamicPage();
page.slot_count = old.slot_count;
memcpy(page.slots, old.slots, sizeof(page.slots));
page.element_count = old.element_count;
for (int i = 0; i < PAGE_ELEMENTS; i++)
memcpy((void *)&page.elements[i], &old.elements[i], COMMON); // the bar fields keep their defaults
}
} // namespace v3
static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(v3::Stored) &&
sizeof(v3::Stored) != sizeof(StoredPage),
"the versions are told apart by their size");
static uint32_t revision = 0;
@@ -210,6 +269,18 @@ bool loadPage(const char *name, DynamicPage &page)
page = stored->page;
delete stored;
}
else if (size == sizeof(v3::Stored))
{
v3::Stored *stored = new v3::Stored;
ok = file.read((uint8_t *)stored, sizeof(v3::Stored)) == sizeof(v3::Stored) && stored->version == 3 &&
stored->size == sizeof(v3::Page);
if (ok)
{
v3::convert(stored->page, page);
ok = validatePage(page) == nullptr;
}
delete stored;
}
else if (size == sizeof(v2::Stored))
{
v2::Stored *stored = new v2::Stored;