Add dynamic pages with clock, date, Home Assistant values and fonts

The hardcoded clock is now the built-in dynamic page "Clock". A dynamic page
has a background bitmap per time of day and up to 8 elements (clock, date,
Home Assistant value, text), each with position, alignment, font and overlay.
Pages are drawn on the ESP32, also for the web previews.

- Pages tab in the web UI: page editor with drag and drop, font upload
  (TTF/OTF converted in the browser to the FDF1 dot font format)
- Built-in fonts Small, Normal, Large and Full height (generated by
  tools/make_builtin_fonts.py), built-in bitmaps Pikachu and Pikachu yawning
- Home Assistant values over the REST API (URL and token in the HA tab)
- Settings version 4: evening settings removed (now time slots of the Clock
  page), old records are converted
- HA Mode dropdown lists the dynamic pages and bitmaps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-15 19:43:59 +02:00
1 parent 1d3b084de1
commit 0411804a2f
49 files changed
+3058 -331

No files matched your search

+56 -36
View File
@@ -12,6 +12,7 @@
#include "config.h"
#include "credentials.h"
#include "display/bitmap_store.h"
#include "display/page_store.h"
#include "net/ha_page_select.h"
#include "system/boot_target.h"
@@ -68,35 +69,51 @@ static void onPowerCommand(bool state, HASwitch *sender)
sender->setState(state);
}
// The bitmap names for the dropdowns: built-in ones first, then the uploaded ones
// alphabetically. Rebuilt when bitmaps were added or deleted.
static const std::vector<String> &bitmapNames()
static bool lessName(const String &a, const String &b)
{
static std::vector<String> names;
static bool loaded = false;
static uint32_t revision = 0;
if (loaded && revision == bitmapListRevision())
return names;
loaded = true;
revision = bitmapListRevision();
return strcasecmp(a.c_str(), b.c_str()) < 0;
}
std::vector<String> uploaded;
names.clear();
// The bitmap names for the dropdowns: built-in ones first, then the uploaded ones
// alphabetically
static std::vector<String> bitmapNames()
{
std::vector<String> names, uploaded;
for (const BitmapInfo &b : listBitmaps())
(b.builtin ? names : uploaded).push_back(b.name);
std::sort(uploaded.begin(), uploaded.end(),
[](const String &a, const String &b) { return strcasecmp(a.c_str(), b.c_str()) < 0; });
std::sort(uploaded.begin(), uploaded.end(), lessName);
names.insert(names.end(), uploaded.begin(), uploaded.end());
return names;
}
// A dropdown with one fixed option (the clock, or blank) followed by the bitmaps, for a
// setting that holds either the fixed value or a bitmap name
struct BitmapChoice
// The dynamic pages the same way, followed by the bitmaps
static std::vector<String> pageNames()
{
std::vector<String> names, stored;
for (const PageInfo &p : listPages())
(p.builtin ? names : stored).push_back(p.name);
std::sort(stored.begin(), stored.end(), lessName);
names.insert(names.end(), stored.begin(), stored.end());
std::vector<String> bitmaps = bitmapNames();
names.insert(names.end(), bitmaps.begin(), bitmaps.end());
return names;
}
// Changes when bitmaps or pages were added or deleted
static uint32_t listRevision()
{
return bitmapListRevision() + pageListRevision();
}
// A dropdown for a setting that holds a name from the list, or optionally a fixed value
// shown as an extra first option (the blank off image)
struct NameChoice
{
HAPageSelect &select;
const char *fixed_option; // shown in Home Assistant
const char *fixed_option; // shown in Home Assistant; nullptr: no fixed option
const char *fixed_value; // stored in the settings
const char *fallback; // shown when the setting's name is gone (what the display shows then)
std::vector<String> (*names)();
const char *(*setting)();
const char *(*choose)(const char *value); // returns nullptr or an error
@@ -106,28 +123,29 @@ struct BitmapChoice
char state_for[PAGE_NAME_SIZE]; // setting the state was published for
std::vector<String> options;
// The option for the setting; the fixed one when the bitmap is gone (the display
// falls back to the clock or blank then too)
const char *current()
{
const char *value = setting();
if (strcmp(value, fixed_value) != 0)
for (const String &option : options)
if (strcasecmp(option.c_str(), value) == 0)
return option.c_str();
return fixed_option;
if (fixed_option && strcmp(value, fixed_value) == 0)
return fixed_option;
for (const String &option : options)
if (strcasecmp(option.c_str(), value) == 0)
return option.c_str();
return fallback;
}
// Rebuilds the options when bitmaps were added or deleted and reports the setting
// Rebuilds the options when bitmaps or pages were added or deleted and reports the setting
void update()
{
if (!options_loaded || options_revision != bitmapListRevision())
if (!options_loaded || options_revision != listRevision())
{
const std::vector<String> &names = bitmapNames();
options_loaded = true;
options_revision = bitmapListRevision();
options.assign(1, fixed_option);
options.insert(options.end(), names.begin(), names.end());
options_revision = listRevision();
options.clear();
if (fixed_option)
options.push_back(fixed_option);
std::vector<String> list = names();
options.insert(options.end(), list.begin(), list.end());
select.setOptions(options);
state_known = false;
}
@@ -142,17 +160,19 @@ struct BitmapChoice
void command(const char *option)
{
const char *error = choose(strcmp(option, fixed_option) == 0 ? fixed_value : option);
const char *error = choose(fixed_option && strcmp(option, fixed_option) == 0 ? fixed_value : option);
if (error)
Serial.printf("Home Assistant \"%s\": %s\n", option, error);
update(); // report the (new) state back
}
};
// Bitmaps can't be called "clock", and the blank off image is stored as ""
static BitmapChoice mode = {modeSelect, "Clock", PAGE_CLOCK, []() { return appSettings().page; }, appSelectPage};
static BitmapChoice offImage = {offSelect, "Blank", OFF_PAGE_BLANK, []() { return appSettings().off_page; },
appSelectOffPage};
// "Clock" is the built-in page (it was a fixed option before, so automations keep working)
static NameChoice mode = {modeSelect, nullptr, nullptr, DEFAULT_PAGE, pageNames,
[]() { return appSettings().page; }, appSelectPage};
// The blank off image is stored as ""
static NameChoice offImage = {offSelect, "Blank", OFF_PAGE_BLANK, "Blank", bitmapNames,
[]() { return appSettings().off_page; }, appSelectOffPage};
static void clearRemovedEntities()
{