#include "logic/page.h" #include #include #include #include static const uint16_t MINUTES_PER_DAY = 24 * 60; static bool terminated(const char *text, size_t size) { return memchr(text, '\0', size) != nullptr; } // Printable ASCII only: the fonts have no other characters static bool printable(const char *text, size_t size) { if (!terminated(text, size)) return false; for (const char *c = text; *c; c++) if (*c < 32 || *c > 126) 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; } 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))) return "Texts can only contain plain characters (no accents), up to 32"; 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"; if (e.type == ElementType::Clock && e.format >= CLOCK_FORMAT_COUNT) return "Unknown clock format"; if (e.type == ElementType::Date && e.format >= DATE_FORMAT_COUNT) return "Unknown date format"; if (e.type == ElementType::HaValue && !validEntityId(e.entity)) return "Home Assistant values need an entity id like sensor.outside_temperature"; } 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; } // --------------------------------------------------------------------------- // 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"}, }; static void formatDate(const PageElement &e, const LocalTime &t, char *out, size_t size) { int lang = e.language < Language::Count ? (int)e.language : 0; 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; } } // Rounded to the element's decimals when it is a number; "--" when unknown 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]; if (e.decimals >= 0 && strcmp(value, "--") != 0) { char *end; double v = strtod(value, &end); if (end != value && *end == '\0') { snprintf(number, sizeof(number), "%.*f", e.decimals, v); if (strcmp(number, "-0") == 0 || strncmp(number, "-0.", 3) == 0) { // -0.0 after rounding: no minus sign bool zero = true; for (const char *c = number + 1; *c; c++) if (*c != '0' && *c != '.') zero = false; if (zero) memmove(number, number + 1, strlen(number)); } value = number; } } snprintf(out, size, "%s%s%s", e.text, value, e.suffix); } void elementText(const PageElement &e, const LocalTime &t, const char *ha_value, char *out, size_t size) { switch (e.type) { case ElementType::Clock: snprintf(out, size, e.format == CLOCK_H_MM ? "%d:%02d" : "%02d:%02d", t.hour, t.minute); break; case ElementType::Date: formatDate(e, t, out, size); break; case ElementType::HaValue: formatHaValue(e, ha_value, out, size); break; default: snprintf(out, size, "%s", e.text); break; } }