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

+406 -34
View File
@@ -13,8 +13,13 @@
#include "config.h"
#include "logic/bitmap_format.h"
#include "logic/builtin_fonts.h"
#include "logic/builtin_pages.h"
#include "logic/font.h"
#include "logic/boot_policy.h"
#include "logic/display_controller.h"
#include "logic/page.h"
#include "logic/page_render.h"
#include "logic/power_timer.h"
#include "logic/schedule.h"
#include "logic/settings.h"
@@ -49,10 +54,10 @@ class FakeFrames : public Frames
public:
std::vector<std::string> shown;
void clock(int hour, int minute, bool evening) override
void dynamicPage(const LocalTime &time) override
{
char s[24];
snprintf(s, sizeof(s), "clock %02d:%02d%s", hour, minute, evening ? " evening" : "");
snprintf(s, sizeof(s), "clock %02d:%02d", time.hour, time.minute);
shown.push_back(s);
}
void page() override { shown.push_back("page"); }
@@ -94,7 +99,10 @@ static void run(int hour, int minute, uint32_t ms = 0)
uint32_t end = now + ms;
do
{
controller->update(now, true, hour, minute, wifi);
LocalTime time;
time.hour = hour;
time.minute = minute;
controller->update(now, true, time, wifi);
now += 100;
} while ((int32_t)(end - now) >= 0);
}
@@ -136,13 +144,6 @@ void test_schedule_night_from_23_31()
TEST_ASSERT_TRUE(scheduleFor(23, 59, defaults.schedule) == ScheduleMode::Night);
}
void test_evening_from_17()
{
TEST_ASSERT_FALSE(isEvening(16, 59, defaults.clock));
TEST_ASSERT_TRUE(isEvening(17, 0, defaults.clock));
TEST_ASSERT_TRUE(isEvening(23, 0, defaults.clock));
}
// ---------------------------------------------------------------------------
// Power timer
// ---------------------------------------------------------------------------
@@ -336,7 +337,7 @@ void test_led_blinks_slow_with_wifi_and_fast_without()
void test_nothing_happens_until_time_is_synced()
{
power.is_on = true; // showing "Connecting..."
controller->update(now, false, 0, 0, true);
controller->update(now, false, LocalTime(), true);
TEST_ASSERT_EQUAL(0, power.off_count);
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
}
@@ -381,7 +382,7 @@ void test_standby_keeps_the_last_time_and_powers_off()
run(22, 0);
run(22, 1, POWER_OFF_DELAY + 200);
TEST_ASSERT_FALSE(power.is_on);
TEST_ASSERT_EQUAL_STRING("clock 22:00 evening", frames.last().c_str()); // no off screen
TEST_ASSERT_EQUAL_STRING("clock 22:00", frames.last().c_str()); // no off screen
}
void test_quarter_hour_powers_on_again_and_redraws()
@@ -391,7 +392,7 @@ void test_quarter_hour_powers_on_again_and_redraws()
TEST_ASSERT_FALSE(power.is_on);
run(22, 15);
TEST_ASSERT_TRUE(power.is_on);
TEST_ASSERT_EQUAL_STRING("clock 22:15 evening", frames.last().c_str());
TEST_ASSERT_EQUAL_STRING("clock 22:15", frames.last().c_str());
}
void test_clock_cancels_pending_power_off()
@@ -599,7 +600,7 @@ void test_settings_flip_time_and_port_ranges()
TEST_ASSERT_NOT_NULL(validateSettings(s));
}
void test_settings_page_must_be_clock_or_bitmap_name()
void test_settings_page_must_be_a_name()
{
Settings s;
strcpy(s.page, "My bitmap");
@@ -680,7 +681,8 @@ void test_bitmap_names()
TEST_ASSERT_FALSE(validBitmapName("trail "));
TEST_ASSERT_FALSE(validBitmapName("../etc"));
TEST_ASSERT_FALSE(validBitmapName("a/b"));
TEST_ASSERT_FALSE(validBitmapName("Clock")); // the clock page
TEST_ASSERT_TRUE(sameName("Clock", "clock"));
TEST_ASSERT_FALSE(sameName("Clock", "Clocks"));
TEST_ASSERT_TRUE(validBitmapName("123456789012345678901234"));
TEST_ASSERT_FALSE(validBitmapName("1234567890123456789012345"));
}
@@ -718,13 +720,6 @@ void test_schedule_without_quarter_hours_stays_on_until_night()
TEST_ASSERT_TRUE(scheduleFor(23, 31, s) == ScheduleMode::Night);
}
void test_evening_can_be_disabled()
{
ClockSettings c;
c.evening_enabled = false;
TEST_ASSERT_FALSE(isEvening(20, 0, c));
}
// ---------------------------------------------------------------------------
// Pages and live mode
// ---------------------------------------------------------------------------
@@ -751,7 +746,7 @@ void test_switching_back_to_the_clock_page()
{
controller->setPage(Page::Bitmap);
run(12, 0);
controller->setPage(Page::Clock);
controller->setPage(Page::Dynamic);
run(12, 0);
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
}
@@ -765,21 +760,22 @@ void test_bitmap_page_follows_the_schedule()
TEST_ASSERT_FALSE(power.is_on);
}
void test_evening_clock_redrawn_when_the_evening_starts()
void test_dynamic_page_redrawn_when_its_content_changes()
{
run(16, 59);
run(17, 0);
TEST_ASSERT_EQUAL_STRING("clock 17:00 evening", frames.last().c_str());
run(18, 0);
controller->pageChanged(); // e.g. a Home Assistant value changed
run(18, 0);
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
}
void test_settings_change_redraws()
{
run(18, 0);
ClockSettings c;
c.evening_enabled = false;
c.no_wifi_screen = false;
controller->setSettings(defaults.schedule, c);
run(18, 0);
TEST_ASSERT_EQUAL_STRING("clock 18:00", frames.last().c_str());
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
}
void test_live_is_shown_right_away_even_at_night()
@@ -794,7 +790,7 @@ void test_live_is_shown_right_away_even_at_night()
void test_live_works_before_the_time_is_synced()
{
controller->showLive(now);
controller->update(now, false, 0, 0, true);
controller->update(now, false, LocalTime(), true);
TEST_ASSERT_EQUAL_STRING("live", frames.last().c_str());
}
@@ -834,6 +830,367 @@ void test_live_respects_the_power_switch()
TEST_ASSERT_EQUAL(0, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("live")));
}
// ---------------------------------------------------------------------------
// Fonts
// ---------------------------------------------------------------------------
static Font fontNamed(const char *name)
{
const BuiltinFont *b = findBuiltinFont(name);
TEST_ASSERT_NOT_NULL(b);
return builtinFont(*b);
}
void test_builtin_fonts_are_valid()
{
for (int i = 0; i < BUILTIN_FONT_COUNT; i++)
TEST_ASSERT_TRUE_MESSAGE(validFont(BUILTIN_FONTS[i].data, BUILTIN_FONTS[i].size), BUILTIN_FONTS[i].name);
TEST_ASSERT_NOT_NULL(findBuiltinFont("normal"));
TEST_ASSERT_NULL(findBuiltinFont("Comic Sans"));
TEST_ASSERT_EQUAL(2, fontNamed("Large").scale);
}
void test_font_sizes()
{
// Monospaced digits: the clock doesn't move when the time changes
TEST_ASSERT_EQUAL(29, textWidth(fontNamed("Normal"), "12:34"));
TEST_ASSERT_EQUAL(29, textWidth(fontNamed("Normal"), "00:00"));
TEST_ASSERT_EQUAL(58, textWidth(fontNamed("Large"), "12:34"));
TEST_ASSERT_EQUAL(17, textWidth(fontNamed("Small"), "12:34"));
TEST_ASSERT_EQUAL(51, textWidth(fontNamed("Full height"), "12:34"));
TEST_ASSERT_EQUAL(8, fontHeight(fontNamed("Normal")));
TEST_ASSERT_EQUAL(16, fontHeight(fontNamed("Large")));
TEST_ASSERT_EQUAL(6, fontHeight(fontNamed("Small")));
TEST_ASSERT_EQUAL(19, fontHeight(fontNamed("Full height")));
// Characters that are not in the font are skipped
TEST_ASSERT_EQUAL(10, textWidth(fontNamed("Full height"), "a1b"));
TEST_ASSERT_EQUAL(0, textWidth(fontNamed("Normal"), ""));
}
void test_draw_text_uses_the_classic_glyphs()
{
uint8_t b[BITMAP_BYTES] = {};
drawText(b, fontNamed("Normal"), 10, 2, "1", DrawMode::Set);
// glcdfont '1': 0x00, 0x42, 0x7F, 0x40, 0x00 (columns, bit 0 = top)
for (int row = 0; row < 7; row++)
TEST_ASSERT_TRUE(bitmapPixel(b, 12, 2 + row));
TEST_ASSERT_FALSE(bitmapPixel(b, 12, 9));
TEST_ASSERT_TRUE(bitmapPixel(b, 11, 3));
TEST_ASSERT_FALSE(bitmapPixel(b, 11, 2));
TEST_ASSERT_TRUE(bitmapPixel(b, 13, 8));
TEST_ASSERT_FALSE(bitmapPixel(b, 10, 5));
}
void test_draw_text_scaled_and_inverted()
{
uint8_t b[BITMAP_BYTES];
memset(b, 0xFF, sizeof(b));
drawText(b, fontNamed("Large"), 0, 0, "1", DrawMode::Invert);
TEST_ASSERT_FALSE(bitmapPixel(b, 4, 0)); // column 2 doubled: 4 and 5
TEST_ASSERT_FALSE(bitmapPixel(b, 5, 13));
TEST_ASSERT_TRUE(bitmapPixel(b, 4, 14));
TEST_ASSERT_TRUE(bitmapPixel(b, 0, 0));
}
void test_draw_text_is_clipped()
{
uint8_t b[BITMAP_BYTES] = {};
drawText(b, fontNamed("Full height"), 80, 5, "88", DrawMode::Set); // must not write outside
drawText(b, fontNamed("Full height"), -5, -10, "88", DrawMode::Set);
TEST_ASSERT_TRUE(bitmapPixel(b, 81, 6));
}
void test_invalid_fonts_are_rejected()
{
const BuiltinFont *n = findBuiltinFont("Normal");
std::vector<uint8_t> f(n->data, n->data + n->size);
TEST_ASSERT_TRUE(validFont(f.data(), f.size()));
TEST_ASSERT_FALSE(validFont(f.data(), f.size() - 1));
f.push_back(0);
TEST_ASSERT_FALSE(validFont(f.data(), f.size()));
f.pop_back();
f[0] = 'X';
TEST_ASSERT_FALSE(validFont(f.data(), f.size()));
f[0] = 'F';
f[4] = 20; // too high
TEST_ASSERT_FALSE(validFont(f.data(), f.size()));
TEST_ASSERT_FALSE(validFont(nullptr, 0));
}
// ---------------------------------------------------------------------------
// Dynamic pages
// ---------------------------------------------------------------------------
static DynamicPage twoSlotPage()
{
DynamicPage page;
page.slot_count = 2;
page.slots[0].start = 8 * 60;
strcpy(page.slots[0].bitmap, "Day");
page.slots[1].start = 17 * 60;
strcpy(page.slots[1].bitmap, "Evening");
return page;
}
void test_active_slot_follows_the_time()
{
DynamicPage page = twoSlotPage();
TEST_ASSERT_EQUAL_STRING("Evening", activeSlot(page, 7 * 60)->bitmap); // from the day before
TEST_ASSERT_EQUAL_STRING("Day", activeSlot(page, 8 * 60)->bitmap);
TEST_ASSERT_EQUAL_STRING("Day", activeSlot(page, 17 * 60 - 1)->bitmap);
TEST_ASSERT_EQUAL_STRING("Evening", activeSlot(page, 17 * 60)->bitmap);
TEST_ASSERT_NULL(activeSlot(DynamicPage(), 600));
}
void test_page_validation()
{
DynamicPage page = twoSlotPage();
TEST_ASSERT_NULL(validatePage(page));
page.slots[1].start = 8 * 60;
TEST_ASSERT_NOT_NULL(validatePage(page)); // not in order
page = twoSlotPage();
page.slots[1].start = 24 * 60;
TEST_ASSERT_NOT_NULL(validatePage(page));
page = twoSlotPage();
strcpy(page.slots[0].bitmap, "../x");
TEST_ASSERT_NOT_NULL(validatePage(page));
page = DynamicPage();
page.element_count = 1;
PageElement &e = page.elements[0];
TEST_ASSERT_NULL(validatePage(page));
e.type = ElementType::HaValue;
TEST_ASSERT_NOT_NULL(validatePage(page)); // needs an entity
strcpy(e.entity, "sensor.outside_temperature");
TEST_ASSERT_NULL(validatePage(page));
strcpy(e.attribute, "bad attr");
TEST_ASSERT_NOT_NULL(validatePage(page));
e.attribute[0] = '\0';
strcpy(e.text, "caf\xc3\xa9");
TEST_ASSERT_NOT_NULL(validatePage(page)); // the fonts have no accents
e.text[0] = '\0';
e.decimals = 5;
TEST_ASSERT_NOT_NULL(validatePage(page));
e.decimals = 1;
e.type = ElementType::Date;
e.format = DATE_FORMAT_COUNT;
TEST_ASSERT_NOT_NULL(validatePage(page));
page.element_count = PAGE_ELEMENTS + 1;
TEST_ASSERT_NOT_NULL(validatePage(page));
TEST_ASSERT_TRUE(validEntityId("weather.home"));
TEST_ASSERT_FALSE(validEntityId("Sensor.x"));
TEST_ASSERT_FALSE(validEntityId("sensor"));
TEST_ASSERT_FALSE(validEntityId("a.b.c"));
TEST_ASSERT_FALSE(validEntityId(".x"));
}
static std::string textOf(const PageElement &e, const char *ha_value = nullptr)
{
LocalTime t; // Thursday 15 October 2026, 08:05
t.year = 2026;
t.month = 10;
t.day = 15;
t.weekday = 4;
t.hour = 8;
t.minute = 5;
char out[64];
elementText(e, t, ha_value, out, sizeof(out));
return out;
}
void test_clock_and_date_texts()
{
PageElement e;
TEST_ASSERT_EQUAL_STRING("08:05", textOf(e).c_str());
e.format = CLOCK_H_MM;
TEST_ASSERT_EQUAL_STRING("8:05", textOf(e).c_str());
e.type = ElementType::Date;
const char *dutch[] = {"do 15 okt", "15 okt", "15 oktober", "15-10", "15-10-2026", "donderdag", "do"};
const char *english[] = {"Thu 15 Oct", "15 Oct", "15 October", "15-10", "15-10-2026", "Thursday", "Thu"};
for (uint8_t f = 0; f < DATE_FORMAT_COUNT; f++)
{
e.format = f;
e.language = Language::Dutch;
TEST_ASSERT_EQUAL_STRING(dutch[f], textOf(e).c_str());
e.language = Language::English;
TEST_ASSERT_EQUAL_STRING(english[f], textOf(e).c_str());
}
e = PageElement();
e.type = ElementType::Text;
strcpy(e.text, "Hallo");
TEST_ASSERT_EQUAL_STRING("Hallo", textOf(e).c_str());
}
void test_home_assistant_value_texts()
{
PageElement e;
e.type = ElementType::HaValue;
strcpy(e.text, "T ");
strcpy(e.suffix, "C");
TEST_ASSERT_EQUAL_STRING("T 21.46C", textOf(e, "21.46").c_str());
TEST_ASSERT_EQUAL_STRING("T --C", textOf(e, nullptr).c_str());
TEST_ASSERT_EQUAL_STRING("T --C", textOf(e, "unavailable").c_str());
e.decimals = 1;
TEST_ASSERT_EQUAL_STRING("T 21.5C", textOf(e, "21.46").c_str());
TEST_ASSERT_EQUAL_STRING("T 0.0C", textOf(e, "-0.04").c_str());
TEST_ASSERT_EQUAL_STRING("T -1.0C", textOf(e, "-1").c_str());
TEST_ASSERT_EQUAL_STRING("T onC", textOf(e, "on").c_str()); // not a number: as is
e.decimals = 0;
TEST_ASSERT_EQUAL_STRING("T 21C", textOf(e, "21.46").c_str());
}
// Backgrounds: "Full" is all dots on, "Day"/"Evening" have one dot at x 0 / x 1
class FakeResources : public PageResources
{
public:
const char *value = "21.5";
bool background(const char *name, uint8_t *bitmap) override
{
memset(bitmap, 0, BITMAP_BYTES);
if (strcmp(name, "Full") == 0)
memset(bitmap, 0xFF, BITMAP_BYTES);
else if (strcmp(name, "Day") == 0)
setBitmapPixel(bitmap, 0, 0, true);
else if (strcmp(name, "Evening") == 0)
setBitmapPixel(bitmap, 1, 0, true);
else
{
memset(bitmap, 0xAA, BITMAP_BYTES); // garbage: must be cleared
return false;
}
return true;
}
Font font(const char *name) override
{
const BuiltinFont *b = findBuiltinFont(name);
return b ? builtinFont(*b) : Font();
}
const char *haValue(const PageElement &) override { return value; }
};
static LocalTime at(int hour, int minute)
{
LocalTime t;
t.hour = hour;
t.minute = minute;
return t;
}
void test_render_background_follows_the_slots()
{
FakeResources res;
DynamicPage page = twoSlotPage();
uint8_t b[BITMAP_BYTES];
renderPage(page, at(9, 0), res, b);
TEST_ASSERT_TRUE(bitmapPixel(b, 0, 0));
TEST_ASSERT_FALSE(bitmapPixel(b, 1, 0));
renderPage(page, at(18, 0), res, b);
TEST_ASSERT_TRUE(bitmapPixel(b, 1, 0));
strcpy(page.slots[1].bitmap, "Gone");
renderPage(page, at(18, 0), res, b);
for (int i = 0; i < BITMAP_BYTES; i++)
TEST_ASSERT_EQUAL_HEX8(0, b[i]); // blank
}
void test_render_overlays()
{
FakeResources res;
DynamicPage page;
page.slot_count = 1;
strcpy(page.slots[0].bitmap, "Full");
page.element_count = 1;
PageElement &e = page.elements[0];
e.type = ElementType::Text;
strcpy(e.text, "1");
e.x = 10;
e.y = 2;
uint8_t b[BITMAP_BYTES];
e.overlay = Overlay::OnTop; // on a full background nothing changes
renderPage(page, at(12, 0), res, b);
TEST_ASSERT_TRUE(bitmapPixel(b, 10, 2));
TEST_ASSERT_TRUE(bitmapPixel(b, 12, 2));
e.overlay = Overlay::Box; // cleared area with one dot around it, then the text
renderPage(page, at(12, 0), res, b);
TEST_ASSERT_FALSE(bitmapPixel(b, 9, 1));
TEST_ASSERT_FALSE(bitmapPixel(b, 10, 2));
TEST_ASSERT_TRUE(bitmapPixel(b, 12, 2));
TEST_ASSERT_FALSE(bitmapPixel(b, 15, 10));
TEST_ASSERT_TRUE(bitmapPixel(b, 16, 10));
TEST_ASSERT_TRUE(bitmapPixel(b, 8, 2));
e.overlay = Overlay::Invert;
renderPage(page, at(12, 0), res, b);
TEST_ASSERT_TRUE(bitmapPixel(b, 10, 2));
TEST_ASSERT_FALSE(bitmapPixel(b, 12, 2));
}
void test_render_alignment_and_boxes()
{
FakeResources res;
DynamicPage page;
page.element_count = 2;
page.elements[0].x = 84;
page.elements[0].align = Align::Right;
page.elements[1].type = ElementType::HaValue;
strcpy(page.elements[1].entity, "sensor.t");
page.elements[1].x = 42;
page.elements[1].y = 11;
page.elements[1].align = Align::Center;
ElementBox boxes[2];
uint8_t b[BITMAP_BYTES];
renderPage(page, at(12, 34), res, b, boxes);
TEST_ASSERT_EQUAL(84 - 29, boxes[0].x);
TEST_ASSERT_EQUAL(29, boxes[0].width);
TEST_ASSERT_EQUAL(8, boxes[0].height);
TEST_ASSERT_EQUAL(23, boxes[1].width); // "21.5"
TEST_ASSERT_EQUAL(42 - 11, boxes[1].x);
TEST_ASSERT_EQUAL(11, boxes[1].y);
}
void test_render_unknown_font_uses_normal()
{
FakeResources res;
DynamicPage page;
page.element_count = 1;
strcpy(page.elements[0].font, "Gone");
ElementBox box;
uint8_t b[BITMAP_BYTES];
renderPage(page, at(12, 34), res, b, &box);
TEST_ASSERT_EQUAL(29, box.width);
}
void test_builtin_clock_page_is_the_old_clock()
{
DynamicPage page;
TEST_ASSERT_TRUE(builtinPage("clock", page)); // old settings stored "clock"
TEST_ASSERT_NULL(validatePage(page));
TEST_ASSERT_EQUAL_STRING("Pikachu", activeSlot(page, 16 * 60 + 59)->bitmap);
TEST_ASSERT_EQUAL_STRING("Pikachu yawning", activeSlot(page, 17 * 60)->bitmap);
TEST_ASSERT_EQUAL_STRING(BUILTIN_PAGE_CLOCK, builtinPageName(0));
TEST_ASSERT_FALSE(builtinPage("Pikachu", page));
// The time where the old firmware printed it: cursor 46, 4 with the classic font
FakeResources res;
TEST_ASSERT_TRUE(builtinPage(BUILTIN_PAGE_CLOCK, page));
uint8_t b[BITMAP_BYTES];
renderPage(page, at(12, 34), res, b);
TEST_ASSERT_TRUE(bitmapPixel(b, 48, 4)); // the '1'
TEST_ASSERT_TRUE(bitmapPixel(b, 48, 10));
TEST_ASSERT_FALSE(bitmapPixel(b, 48, 11));
}
void test_default_page_is_the_builtin_clock()
{
DynamicPage page;
TEST_ASSERT_TRUE(builtinPage(defaults.page, page));
}
int main()
{
UNITY_BEGIN();
@@ -842,7 +1199,6 @@ int main()
RUN_TEST(test_schedule_clock_from_8_to_22);
RUN_TEST(test_schedule_quarter_hours_until_23_30);
RUN_TEST(test_schedule_night_from_23_31);
RUN_TEST(test_evening_from_17);
RUN_TEST(test_timer_expires_once_after_delay);
RUN_TEST(test_timer_keeps_earlier_deadline);
@@ -894,7 +1250,7 @@ int main()
RUN_TEST(test_settings_off_page_blank_or_bitmap_name);
RUN_TEST(test_changed_off_image_is_redrawn_while_the_display_is_on);
RUN_TEST(test_settings_flip_time_and_port_ranges);
RUN_TEST(test_settings_page_must_be_clock_or_bitmap_name);
RUN_TEST(test_settings_page_must_be_a_name);
RUN_TEST(test_time_parse_and_format);
RUN_TEST(test_bitmap_is_84_by_19_in_drawbitmap_format);
RUN_TEST(test_bitmap_hex_round_trip);
@@ -903,12 +1259,11 @@ int main()
RUN_TEST(test_schedule_disabled_is_always_on);
RUN_TEST(test_schedule_custom_times);
RUN_TEST(test_schedule_without_quarter_hours_stays_on_until_night);
RUN_TEST(test_evening_can_be_disabled);
RUN_TEST(test_bitmap_page_is_shown_once);
RUN_TEST(test_bitmap_page_redrawn_when_changed);
RUN_TEST(test_switching_back_to_the_clock_page);
RUN_TEST(test_bitmap_page_follows_the_schedule);
RUN_TEST(test_evening_clock_redrawn_when_the_evening_starts);
RUN_TEST(test_dynamic_page_redrawn_when_its_content_changes);
RUN_TEST(test_settings_change_redraws);
RUN_TEST(test_live_is_shown_right_away_even_at_night);
RUN_TEST(test_live_works_before_the_time_is_synced);
@@ -917,5 +1272,22 @@ int main()
RUN_TEST(test_live_ends_after_timeout);
RUN_TEST(test_live_respects_the_power_switch);
RUN_TEST(test_builtin_fonts_are_valid);
RUN_TEST(test_font_sizes);
RUN_TEST(test_draw_text_uses_the_classic_glyphs);
RUN_TEST(test_draw_text_scaled_and_inverted);
RUN_TEST(test_draw_text_is_clipped);
RUN_TEST(test_invalid_fonts_are_rejected);
RUN_TEST(test_active_slot_follows_the_time);
RUN_TEST(test_page_validation);
RUN_TEST(test_clock_and_date_texts);
RUN_TEST(test_home_assistant_value_texts);
RUN_TEST(test_render_background_follows_the_slots);
RUN_TEST(test_render_overlays);
RUN_TEST(test_render_alignment_and_boxes);
RUN_TEST(test_render_unknown_font_uses_normal);
RUN_TEST(test_builtin_clock_page_is_the_old_clock);
RUN_TEST(test_default_page_is_the_builtin_clock);
return UNITY_END();
}