2026-10-15 19:43:59 +02:00
|
|
|
#include "logic/page.h"
|
|
|
|
|
|
|
|
|
|
#include <ctype.h>
|
2026-10-15 21:01:14 +02:00
|
|
|
#include <math.h>
|
2026-10-15 19:43:59 +02:00
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
static const uint16_t MINUTES_PER_DAY = 24 * 60;
|
2026-10-15 21:01:14 +02:00
|
|
|
static const uint32_t MAX_TARGET = 36525UL * MINUTES_PER_DAY; // 2100-01-01
|
2026-10-15 19:43:59 +02:00
|
|
|
|
|
|
|
|
static bool terminated(const char *text, size_t size)
|
|
|
|
|
{
|
|
|
|
|
return memchr(text, '\0', size) != nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
// Plain ASCII and the degree sign only: the fonts have no other characters
|
2026-10-15 19:43:59 +02:00
|
|
|
static bool printable(const char *text, size_t size)
|
|
|
|
|
{
|
|
|
|
|
if (!terminated(text, size))
|
|
|
|
|
return false;
|
|
|
|
|
for (const char *c = text; *c; c++)
|
2026-10-15 21:01:14 +02:00
|
|
|
if (*c < 32 || *c > DEGREE_SIGN)
|
2026-10-15 19:43:59 +02:00
|
|
|
return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool validEntityId(const char *entity)
|
|
|
|
|
{
|
|
|
|
|
if (entity == nullptr)
|
|
|
|
|
return false;
|
|
|
|
|
const char *dot = strchr(entity, '.');
|
|
|
|
|
if (dot == nullptr || dot == entity || dot[1] == '\0' || strchr(dot + 1, '.'))
|
|
|
|
|
return false;
|
|
|
|
|
for (const char *c = entity; *c; c++)
|
|
|
|
|
if (c != dot && !islower((unsigned char)*c) && !isdigit((unsigned char)*c) && *c != '_')
|
|
|
|
|
return false;
|
|
|
|
|
return strlen(entity) < (size_t)ELEMENT_ENTITY_SIZE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool validAttribute(const char *attribute)
|
|
|
|
|
{
|
|
|
|
|
for (const char *c = attribute; *c; c++)
|
|
|
|
|
if (!isalnum((unsigned char)*c) && *c != '_')
|
|
|
|
|
return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
static uint8_t formatCount(ElementType type)
|
|
|
|
|
{
|
|
|
|
|
switch (type)
|
|
|
|
|
{
|
|
|
|
|
case ElementType::Clock:
|
|
|
|
|
return CLOCK_FORMAT_COUNT;
|
|
|
|
|
case ElementType::Date:
|
|
|
|
|
return DATE_FORMAT_COUNT;
|
|
|
|
|
case ElementType::Countdown:
|
|
|
|
|
return COUNTDOWN_FORMAT_COUNT;
|
|
|
|
|
case ElementType::Stopwatch:
|
|
|
|
|
return STOPWATCH_FORMAT_COUNT;
|
|
|
|
|
default:
|
|
|
|
|
return 255; // not used
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
const char *validatePage(const DynamicPage &page)
|
|
|
|
|
{
|
|
|
|
|
if (page.slot_count > PAGE_SLOTS)
|
|
|
|
|
return "Too many time slots";
|
|
|
|
|
if (page.element_count > PAGE_ELEMENTS)
|
|
|
|
|
return "Too many elements";
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < page.slot_count; i++)
|
|
|
|
|
{
|
|
|
|
|
const PageSlot &slot = page.slots[i];
|
|
|
|
|
if (slot.start >= MINUTES_PER_DAY)
|
|
|
|
|
return "Time slots must start before 24:00";
|
|
|
|
|
if (i > 0 && slot.start <= page.slots[i - 1].start)
|
|
|
|
|
return "Time slots must be in order, each at a different time";
|
|
|
|
|
if (!terminated(slot.bitmap, sizeof(slot.bitmap)) || (slot.bitmap[0] && !validBitmapName(slot.bitmap)))
|
|
|
|
|
return "Invalid background bitmap name";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < page.element_count; i++)
|
|
|
|
|
{
|
|
|
|
|
const PageElement &e = page.elements[i];
|
|
|
|
|
if (e.type >= ElementType::Count || e.align >= Align::Count || e.overlay >= Overlay::Count ||
|
|
|
|
|
e.language >= Language::Count)
|
|
|
|
|
return "Unknown element setting";
|
|
|
|
|
if (e.x < -BITMAP_WIDTH || e.x > 2 * BITMAP_WIDTH || e.y < -BITMAP_HEIGHT || e.y > BITMAP_HEIGHT)
|
|
|
|
|
return "Element position out of range";
|
|
|
|
|
if (!terminated(e.font, sizeof(e.font)) || !validBitmapName(e.font))
|
|
|
|
|
return "Invalid font name";
|
|
|
|
|
if (!printable(e.text, sizeof(e.text)) || !printable(e.suffix, sizeof(e.suffix)))
|
2026-10-15 21:01:14 +02:00
|
|
|
return "Texts can only contain plain characters (no accents) and the degree sign, up to 32";
|
2026-10-15 19:43:59 +02:00
|
|
|
if (!terminated(e.entity, sizeof(e.entity)) || !terminated(e.attribute, sizeof(e.attribute)) ||
|
|
|
|
|
!validAttribute(e.attribute))
|
|
|
|
|
return "Invalid entity or attribute";
|
|
|
|
|
if (e.decimals < DECIMALS_AS_IS || e.decimals > DECIMALS_MAX)
|
|
|
|
|
return "Decimals must be 0-4, or as received";
|
2026-10-15 21:01:14 +02:00
|
|
|
if (e.format >= formatCount(e.type))
|
|
|
|
|
return "Unknown format";
|
|
|
|
|
if (!isfinite(e.factor) || e.factor == 0)
|
|
|
|
|
return "The multiplier must be a number other than 0";
|
|
|
|
|
if (e.scroll_width > 2 * BITMAP_WIDTH ||
|
|
|
|
|
(e.scroll_width && (e.scroll_speed < SCROLL_SPEED_MIN || e.scroll_speed > SCROLL_SPEED_MAX)))
|
|
|
|
|
return "Scrolling: window up to 168 dots, speed 1-30 dots per second";
|
2026-10-15 19:43:59 +02:00
|
|
|
if (e.type == ElementType::HaValue && !validEntityId(e.entity))
|
|
|
|
|
return "Home Assistant values need an entity id like sensor.outside_temperature";
|
2026-10-15 21:01:14 +02:00
|
|
|
if (e.type == ElementType::Countdown && e.target >= MAX_TARGET)
|
|
|
|
|
return "The countdown date must be before 2100";
|
|
|
|
|
if (e.type == ElementType::Image)
|
|
|
|
|
{
|
|
|
|
|
if (e.entity[0] && !validEntityId(e.entity))
|
|
|
|
|
return "Invalid entity id";
|
|
|
|
|
char name[ELEMENT_TEXT_SIZE + 1];
|
|
|
|
|
snprintf(name, sizeof(name), "%sx", e.text); // a prefix must be the start of a valid name
|
|
|
|
|
if (e.entity[0] ? (e.text[0] == ' ' || !validBitmapName(name)) : !validBitmapName(e.text))
|
|
|
|
|
return "Images need a bitmap name (or the start of one, with an entity)";
|
|
|
|
|
}
|
2026-10-15 19:43:59 +02:00
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PageSlot *activeSlot(const DynamicPage &page, int minute_of_day)
|
|
|
|
|
{
|
|
|
|
|
if (page.slot_count == 0)
|
|
|
|
|
return nullptr;
|
|
|
|
|
const PageSlot *active = &page.slots[page.slot_count - 1];
|
|
|
|
|
for (int i = 0; i < page.slot_count; i++)
|
|
|
|
|
if (page.slots[i].start <= minute_of_day)
|
|
|
|
|
active = &page.slots[i];
|
|
|
|
|
return active;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Dates
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Days since 1970-01-01 (proleptic Gregorian calendar)
|
|
|
|
|
static int32_t daysFromCivil(int y, int m, int d)
|
|
|
|
|
{
|
|
|
|
|
y -= m <= 2;
|
|
|
|
|
int32_t era = (y >= 0 ? y : y - 399) / 400;
|
|
|
|
|
int32_t yoe = y - era * 400;
|
|
|
|
|
int32_t doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
|
|
|
|
int32_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
|
|
|
return era * 146097 + doe - 719468;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void civilFromDays(int32_t z, int &y, int &m, int &d)
|
|
|
|
|
{
|
|
|
|
|
z += 719468;
|
|
|
|
|
int32_t era = (z >= 0 ? z : z - 146096) / 146097;
|
|
|
|
|
int32_t doe = z - era * 146097;
|
|
|
|
|
int32_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
|
|
|
int32_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
|
|
|
int32_t mp = (5 * doy + 2) / 153;
|
|
|
|
|
d = doy - (153 * mp + 2) / 5 + 1;
|
|
|
|
|
m = mp < 10 ? mp + 3 : mp - 9;
|
|
|
|
|
y = yoe + era * 400 + (m <= 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const int32_t DAYS_1970_TO_2000 = 10957;
|
|
|
|
|
|
|
|
|
|
uint32_t minutesSince2000(const LocalTime &t)
|
|
|
|
|
{
|
|
|
|
|
int32_t days = daysFromCivil(t.year, t.month, t.day) - DAYS_1970_TO_2000;
|
|
|
|
|
if (days < 0)
|
|
|
|
|
return 0;
|
|
|
|
|
return (uint32_t)days * MINUTES_PER_DAY + t.hour * 60 + t.minute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool parseTarget(const char *text, uint32_t &minutes)
|
|
|
|
|
{
|
|
|
|
|
if (text == nullptr || strlen(text) != 16)
|
|
|
|
|
return false;
|
|
|
|
|
int y, mo, d, h, mi;
|
|
|
|
|
char extra;
|
|
|
|
|
if (sscanf(text, "%4d-%2d-%2dT%2d:%2d%c", &y, &mo, &d, &h, &mi, &extra) != 5)
|
|
|
|
|
return false;
|
|
|
|
|
if (y < 2000 || y > 2099 || mo < 1 || mo > 12 || d < 1 || d > 31 || h > 23 || mi > 59 || h < 0 || mi < 0)
|
|
|
|
|
return false;
|
|
|
|
|
int cy, cm, cd; // rejects e.g. 31 February
|
|
|
|
|
int32_t days = daysFromCivil(y, mo, d);
|
|
|
|
|
civilFromDays(days, cy, cm, cd);
|
|
|
|
|
if (cd != d)
|
|
|
|
|
return false;
|
|
|
|
|
minutes = (uint32_t)(days - DAYS_1970_TO_2000) * MINUTES_PER_DAY + h * 60 + mi;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void formatTarget(uint32_t minutes, char *out)
|
|
|
|
|
{
|
|
|
|
|
int y, m, d;
|
|
|
|
|
civilFromDays(minutes / MINUTES_PER_DAY + DAYS_1970_TO_2000, y, m, d);
|
|
|
|
|
unsigned minute_of_day = minutes % MINUTES_PER_DAY;
|
|
|
|
|
snprintf(out, 17, "%04d-%02d-%02dT%02u:%02u", y % 10000, m, d, minute_of_day / 60, minute_of_day % 60);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Texts
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
static const char *const WEEKDAYS[][7] = {
|
|
|
|
|
{"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"},
|
|
|
|
|
{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
|
|
|
|
|
};
|
|
|
|
|
static const char *const WEEKDAYS_SHORT[][7] = {
|
|
|
|
|
{"zo", "ma", "di", "wo", "do", "vr", "za"},
|
|
|
|
|
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"},
|
|
|
|
|
};
|
|
|
|
|
static const char *const MONTHS[][12] = {
|
|
|
|
|
{"januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober",
|
|
|
|
|
"november", "december"},
|
|
|
|
|
{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October",
|
|
|
|
|
"November", "December"},
|
|
|
|
|
};
|
|
|
|
|
static const char *const MONTHS_SHORT[][12] = {
|
|
|
|
|
{"jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"},
|
|
|
|
|
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},
|
|
|
|
|
};
|
2026-10-15 21:01:14 +02:00
|
|
|
// Countdown units: singular, plural
|
|
|
|
|
static const char *const DAYS_WORD[][2] = {{"dag", "dagen"}, {"day", "days"}};
|
|
|
|
|
static const char *const HOURS_WORD[][2] = {{"uur", "uur"}, {"hour", "hours"}};
|
|
|
|
|
|
|
|
|
|
static int language(const PageElement &e)
|
|
|
|
|
{
|
|
|
|
|
return e.language < Language::Count ? (int)e.language : 0;
|
|
|
|
|
}
|
2026-10-15 19:43:59 +02:00
|
|
|
|
|
|
|
|
static void formatDate(const PageElement &e, const LocalTime &t, char *out, size_t size)
|
|
|
|
|
{
|
2026-10-15 21:01:14 +02:00
|
|
|
int lang = language(e);
|
2026-10-15 19:43:59 +02:00
|
|
|
int wd = t.weekday >= 0 && t.weekday < 7 ? t.weekday : 0;
|
|
|
|
|
int mo = t.month >= 1 && t.month <= 12 ? t.month - 1 : 0;
|
|
|
|
|
switch (e.format)
|
|
|
|
|
{
|
|
|
|
|
case DATE_WEEKDAY_DAY_MONTH:
|
|
|
|
|
snprintf(out, size, "%s %d %s", WEEKDAYS_SHORT[lang][wd], t.day, MONTHS_SHORT[lang][mo]);
|
|
|
|
|
break;
|
|
|
|
|
case DATE_DAY_MONTH:
|
|
|
|
|
snprintf(out, size, "%d %s", t.day, MONTHS_SHORT[lang][mo]);
|
|
|
|
|
break;
|
|
|
|
|
case DATE_DAY_MONTH_LONG:
|
|
|
|
|
snprintf(out, size, "%d %s", t.day, MONTHS[lang][mo]);
|
|
|
|
|
break;
|
|
|
|
|
case DATE_NUMERIC:
|
|
|
|
|
snprintf(out, size, "%02d-%02d", t.day, t.month);
|
|
|
|
|
break;
|
|
|
|
|
case DATE_NUMERIC_YEAR:
|
|
|
|
|
snprintf(out, size, "%02d-%02d-%04d", t.day, t.month, t.year);
|
|
|
|
|
break;
|
|
|
|
|
case DATE_WEEKDAY:
|
|
|
|
|
snprintf(out, size, "%s", WEEKDAYS[lang][wd]);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
snprintf(out, size, "%s", WEEKDAYS_SHORT[lang][wd]);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
// Rounded to the element's decimals (after the multiplier) when it is a number; "--" when unknown
|
2026-10-15 19:43:59 +02:00
|
|
|
static void formatHaValue(const PageElement &e, const char *value, char *out, size_t size)
|
|
|
|
|
{
|
|
|
|
|
if (value == nullptr || value[0] == '\0' || strcmp(value, "unavailable") == 0 || strcmp(value, "unknown") == 0)
|
|
|
|
|
value = "--";
|
|
|
|
|
char number[24];
|
2026-10-15 21:01:14 +02:00
|
|
|
char *end;
|
|
|
|
|
double v = strtod(value, &end);
|
|
|
|
|
if (strcmp(value, "--") != 0 && end != value && *end == '\0' && (e.decimals >= 0 || e.factor != 1))
|
2026-10-15 19:43:59 +02:00
|
|
|
{
|
2026-10-15 21:01:14 +02:00
|
|
|
v *= e.factor;
|
|
|
|
|
if (e.decimals >= 0)
|
2026-10-15 19:43:59 +02:00
|
|
|
snprintf(number, sizeof(number), "%.*f", e.decimals, v);
|
2026-10-15 21:01:14 +02:00
|
|
|
else
|
|
|
|
|
snprintf(number, sizeof(number), "%.6g", v);
|
|
|
|
|
// -0.0 after rounding: no minus sign
|
|
|
|
|
if (number[0] == '-' && strspn(number + 1, "0.") == strlen(number + 1))
|
|
|
|
|
memmove(number, number + 1, strlen(number));
|
|
|
|
|
value = number;
|
2026-10-15 19:43:59 +02:00
|
|
|
}
|
|
|
|
|
snprintf(out, size, "%s%s%s", e.text, value, e.suffix);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
// Seconds until the target; negative when it has passed
|
|
|
|
|
static int64_t secondsLeft(const PageElement &e, const LocalTime &t)
|
|
|
|
|
{
|
|
|
|
|
return (int64_t)e.target * 60 - ((int64_t)minutesSince2000(t) * 60 + t.second);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void formatCountdown(const PageElement &e, const LocalTime &t, char *out, size_t size)
|
|
|
|
|
{
|
|
|
|
|
int64_t left = secondsLeft(e, t);
|
|
|
|
|
if (left <= 0)
|
|
|
|
|
{
|
|
|
|
|
snprintf(out, size, "%s%s", e.text, e.suffix[0] ? e.suffix : "0");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
int lang = language(e);
|
|
|
|
|
long days = (long)(left / 86400), hours = (long)(left % 86400 / 3600), minutes = (long)(left % 3600 / 60),
|
|
|
|
|
seconds = (long)(left % 60);
|
|
|
|
|
char value[32];
|
|
|
|
|
switch (e.format)
|
|
|
|
|
{
|
|
|
|
|
case COUNTDOWN_DAYS:
|
|
|
|
|
{
|
|
|
|
|
// Calendar days: tomorrow is 1 day, whatever the time
|
|
|
|
|
long calendar = (long)(e.target / MINUTES_PER_DAY) - (long)(minutesSince2000(t) / MINUTES_PER_DAY);
|
|
|
|
|
snprintf(value, sizeof(value), "%ld %s", calendar, DAYS_WORD[lang][calendar != 1]);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case COUNTDOWN_DAYS_TIME:
|
|
|
|
|
if (days)
|
|
|
|
|
snprintf(value, sizeof(value), "%ldd %02ld:%02ld", days, hours, minutes);
|
|
|
|
|
else
|
|
|
|
|
snprintf(value, sizeof(value), "%02ld:%02ld", hours, minutes);
|
|
|
|
|
break;
|
|
|
|
|
case COUNTDOWN_SECONDS:
|
|
|
|
|
if (days)
|
|
|
|
|
snprintf(value, sizeof(value), "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
|
|
|
|
|
else
|
|
|
|
|
snprintf(value, sizeof(value), "%02ld:%02ld:%02ld", hours, minutes, seconds);
|
|
|
|
|
break;
|
|
|
|
|
default: // smart: the largest unit
|
|
|
|
|
if (days)
|
|
|
|
|
snprintf(value, sizeof(value), "%ld %s", days, DAYS_WORD[lang][days != 1]);
|
|
|
|
|
else if (hours)
|
|
|
|
|
snprintf(value, sizeof(value), "%ld %s", hours, HOURS_WORD[lang][hours != 1]);
|
|
|
|
|
else if (minutes)
|
|
|
|
|
snprintf(value, sizeof(value), "%ld min", minutes);
|
|
|
|
|
else
|
|
|
|
|
snprintf(value, sizeof(value), "0:%02ld", seconds);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
snprintf(out, size, "%s%s", e.text, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void formatStopwatch(const PageElement &e, uint32_t ms, char *out, size_t size)
|
|
|
|
|
{
|
|
|
|
|
unsigned long s = ms / 1000, h = s / 3600, m = s % 3600 / 60, sec = s % 60;
|
|
|
|
|
char value[24];
|
|
|
|
|
if (e.format == STOPWATCH_H_MM)
|
|
|
|
|
snprintf(value, sizeof(value), "%lu:%02lu", h, m);
|
|
|
|
|
else if (e.format == STOPWATCH_H_MM_SS || h)
|
|
|
|
|
snprintf(value, sizeof(value), "%lu:%02lu:%02lu", h, m, sec);
|
|
|
|
|
else
|
|
|
|
|
snprintf(value, sizeof(value), "%lu:%02lu", m, sec);
|
|
|
|
|
snprintf(out, size, "%s%s%s", e.text, value, e.suffix);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void elementText(const PageElement &e, const LocalTime &t, const ElementValues &values, char *out, size_t size)
|
2026-10-15 19:43:59 +02:00
|
|
|
{
|
|
|
|
|
switch (e.type)
|
|
|
|
|
{
|
|
|
|
|
case ElementType::Clock:
|
2026-10-15 21:01:14 +02:00
|
|
|
if (e.format == CLOCK_HH_MM_SS || e.format == CLOCK_H_MM_SS)
|
|
|
|
|
snprintf(out, size, e.format == CLOCK_H_MM_SS ? "%d:%02d:%02d" : "%02d:%02d:%02d", t.hour, t.minute,
|
|
|
|
|
t.second);
|
|
|
|
|
else
|
|
|
|
|
snprintf(out, size, e.format == CLOCK_H_MM ? "%d:%02d" : "%02d:%02d", t.hour, t.minute);
|
2026-10-15 19:43:59 +02:00
|
|
|
break;
|
|
|
|
|
case ElementType::Date:
|
|
|
|
|
formatDate(e, t, out, size);
|
|
|
|
|
break;
|
|
|
|
|
case ElementType::HaValue:
|
2026-10-15 21:01:14 +02:00
|
|
|
formatHaValue(e, values.ha_value, out, size);
|
|
|
|
|
break;
|
|
|
|
|
case ElementType::Countdown:
|
|
|
|
|
formatCountdown(e, t, out, size);
|
|
|
|
|
break;
|
|
|
|
|
case ElementType::Stopwatch:
|
|
|
|
|
formatStopwatch(e, values.stopwatch_ms, out, size);
|
|
|
|
|
break;
|
|
|
|
|
case ElementType::Image:
|
|
|
|
|
out[0] = '\0';
|
2026-10-15 19:43:59 +02:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
snprintf(out, size, "%s", e.text);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-15 21:01:14 +02:00
|
|
|
|
|
|
|
|
bool elementShowsSeconds(const PageElement &e, const LocalTime &t, bool stopwatch_running)
|
|
|
|
|
{
|
|
|
|
|
switch (e.type)
|
|
|
|
|
{
|
|
|
|
|
case ElementType::Clock:
|
|
|
|
|
return e.format == CLOCK_HH_MM_SS || e.format == CLOCK_H_MM_SS;
|
|
|
|
|
case ElementType::Stopwatch:
|
|
|
|
|
return stopwatch_running && e.format != STOPWATCH_H_MM;
|
|
|
|
|
case ElementType::Countdown:
|
|
|
|
|
{
|
|
|
|
|
int64_t left = secondsLeft(e, t);
|
|
|
|
|
if (left <= 0)
|
|
|
|
|
return false;
|
|
|
|
|
return e.format == COUNTDOWN_SECONDS || (e.format == COUNTDOWN_SMART && left < 60);
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|