Files
Flipdot-display/src/esp32/logic/page.cpp
T

809 lines
25 KiB
C++
Raw Normal View History

#include "logic/page.h"
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
static const uint16_t MINUTES_PER_DAY = 24 * 60;
static const uint32_t MAX_TARGET = 36525UL * MINUTES_PER_DAY; // 2100-01-01
static bool terminated(const char *text, size_t size)
{
return memchr(text, '\0', size) != nullptr;
}
// Plain ASCII and the degree sign 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 > DEGREE_SIGN)
return false;
return true;
}
bool expandEntity(const char *entity, const char *user, char *out, size_t out_size)
{
const char *at = strstr(entity, PLEX_USER_PLACEHOLDER);
int n;
if (at)
n = snprintf(out, out_size, "%.*s%s%s", (int)(at - entity), entity, user,
at + strlen(PLEX_USER_PLACEHOLDER));
else
n = snprintf(out, out_size, "%s", entity);
return n >= 0 && (size_t)n < out_size;
}
void plexUserSlug(const char *name, char *out, size_t out_size)
{
size_t n = 0;
bool gap = false;
for (const char *c = name; *c && n + 1 < out_size; c++)
{
if (isalnum((unsigned char)*c))
{
if (gap && n > 0 && n + 2 < out_size)
out[n++] = '_';
gap = false;
out[n++] = tolower((unsigned char)*c);
}
else
gap = true;
}
out[n] = '\0';
}
bool elementUsesHa(const PageElement &element)
{
switch (element.type)
{
case ElementType::HaValue:
return true;
case ElementType::Image:
case ElementType::Bar:
return element.entity[0] != '\0';
default:
return false;
}
}
bool pageUsesImage(const DynamicPage &page, const char *name, bool by_state)
{
for (int i = 0; i < page.slot_count; i++)
if (sameName(page.slots[i].bitmap, name))
return true;
for (int i = 0; i < page.element_count; i++)
{
const PageElement &e = page.elements[i];
if (e.type != ElementType::Image)
continue;
if (!e.entity[0])
{
if (sameName(e.text, name))
return true;
continue;
}
size_t start = strlen(e.text);
if (strncasecmp(name, e.text, start) != 0)
continue;
if (by_state ? name[start] != '\0' : strcasecmp(name + start, "default") == 0)
return true;
}
return false;
}
bool validEntityId(const char *entity)
{
if (entity == nullptr)
return false;
// Checked with a stand-in user name
if (strstr(entity, PLEX_USER_PLACEHOLDER))
{
char full[ELEMENT_ENTITY_SIZE];
return strlen(entity) < (size_t)ELEMENT_ENTITY_SIZE && !strstr(strstr(entity, "}") + 1, "{") &&
expandEntity(entity, "x", full, sizeof(full)) && validEntityId(full);
}
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 strncasecmpAscii(const char *a, const char *b, size_t n)
{
for (size_t i = 0; i < n; i++)
if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i]))
return false;
return true;
}
static bool validAttribute(const char *attribute)
{
for (const char *c = attribute; *c; c++)
if (!isalnum((unsigned char)*c) && *c != '_' && *c != '.') // dots: a path, e.g. forecast.1.condition
return false;
return true;
}
static uint8_t formatCount(const PageElement &e)
{
switch (e.type)
{
case ElementType::HaValue:
if (e.show == HaShow::Duration)
return DURATION_FORMAT_COUNT;
if (e.show == HaShow::Time)
return TIMESTAMP_FORMAT_COUNT;
return 255; // not used
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
}
}
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";
2026-10-17 16:57:31 +02:00
if (page.effect != PAGE_EFFECT_DEFAULT && page.effect >= PAGE_EFFECT_COUNT)
return "Unknown refresh effect";
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) and the degree sign, 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.show >= HaShow::Count || e.input_unit >= UNIT_COUNT)
return "Unknown Home Assistant value setting";
if (!printable(e.map, sizeof(e.map)))
return "The list of texts can only contain plain characters, up to 63";
if (e.format >= formatCount(e))
return "Unknown format";
if (e.day_offset < 0 || e.day_offset > DAY_OFFSET_MAX)
return "Date: 0-7 days after today";
if (!isfinite(e.factor) || e.factor == 0)
return "The multiplier must be a number other than 0";
if (e.scroll_width > 2 * BITMAP_WIDTH ||
2026-10-16 16:23:16 +02:00
(e.scroll_width && !(e.scroll_speed >= SCROLL_SPEED_MIN && e.scroll_speed <= SCROLL_SPEED_MAX)))
return "Scrolling: window up to 168 dots, speed 0.05-30 dots per second";
if (e.type == ElementType::HaValue && !validEntityId(e.entity))
return "Home Assistant values need an entity id like sensor.outside_temperature";
if (e.type == ElementType::Bar)
{
if (!validEntityId(e.entity))
return "Progress bars need an entity id like sensor.progress";
if (e.bar_width < 1 || e.bar_width > BITMAP_WIDTH || e.bar_height < 1 || e.bar_height > BITMAP_HEIGHT)
return "Progress bar: width 1-84, height 1-19 dots";
if (!isfinite(e.bar_max) || e.bar_max <= 0)
return "Progress bar: the full value must be a number above 0";
}
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)";
}
}
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;
}
// ---------------------------------------------------------------------------
// 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 parseTimestamp(const char *text, int64_t &seconds)
{
if (text == nullptr)
return false;
int y, mo, d, h, mi, s;
char sep;
int used = 0;
if (sscanf(text, "%4d-%2d-%2d%c%2d:%2d:%2d%n", &y, &mo, &d, &sep, &h, &mi, &s, &used) != 7 ||
(sep != 'T' && sep != ' '))
return false;
if (y < 2000 || y > 2099 || mo < 1 || mo > 12 || d < 1 || d > 31 || h > 23 || mi > 59 || s > 60)
return false;
const char *rest = text + used;
if (*rest == '.') // fractions of a second
for (rest++; isdigit((unsigned char)*rest); rest++)
;
int offset = 0; // minutes east of UTC
if (*rest == '+' || *rest == '-')
{
int oh, om;
if (sscanf(rest + 1, "%2d:%2d", &oh, &om) != 2)
return false;
offset = (oh * 60 + om) * (*rest == '-' ? -1 : 1);
rest += 6;
}
else if (*rest == 'Z')
rest++;
if (*rest != '\0')
return false;
int64_t days = daysFromCivil(y, mo, d) - DAYS_1970_TO_2000;
seconds = days * 86400 + h * 3600 + mi * 60 + s - offset * 60;
return true;
}
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);
}
// ---------------------------------------------------------------------------
// 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"},
};
// 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;
}
static int daysInMonth(int year, int month)
{
static const int DAYS[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
bool leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
return month == 2 && leap ? 29 : DAYS[(month - 1 + 12) % 12];
}
LocalTime addDays(const LocalTime &time, int days)
{
LocalTime t = time;
if (t.month < 1 || t.month > 12)
return t;
for (int i = 0; i < days; i++)
{
t.weekday = (t.weekday + 1) % 7;
if (++t.day > daysInMonth(t.year, t.month))
{
t.day = 1;
if (++t.month > 12)
{
t.month = 1;
t.year++;
}
}
}
return t;
}
static void formatDate(const PageElement &e, const LocalTime &today, char *out, size_t size)
{
LocalTime t = addDays(today, e.day_offset);
int lang = language(e);
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;
}
}
static bool parseNumber(const char *text, double &v)
{
char *end;
v = strtod(text, &end);
return end != text && *end == '\0';
}
static void formatNumber(double v, int decimals, char *out, size_t size)
{
if (decimals >= 0)
snprintf(out, size, "%.*f", decimals, v);
else
snprintf(out, size, "%.6g", v);
// -0.0 after rounding: no minus sign
if (out[0] == '-' && strspn(out + 1, "0.") == strlen(out + 1))
memmove(out, out + 1, strlen(out));
}
// Units that get k (1000x) and M (1000000x) in front with auto unit
static const char *const SCALED_UNITS[] = {"W", "Wh", "VA", "g", "m", "B", "Hz"};
static bool scaledUnit(const char *unit)
{
for (const char *u : SCALED_UNITS)
if (strcmp(unit, u) == 0)
return true;
return false;
}
// Number: multiplier, auto unit, no minus sign, decimals. unit: in/out, the text after.
static void showNumber(const PageElement &e, double v, char *out, size_t size, char *unit, size_t unit_size)
{
v *= e.factor;
if (e.absolute && v < 0)
v = -v;
int decimals = e.decimals;
if (e.auto_unit)
{
// The unit is the text after, without the spaces before it
size_t spaces = strspn(unit, " ");
char base[ELEMENT_SUFFIX_SIZE];
snprintf(base, sizeof(base), "%s", unit + spaces);
double a = v < 0 ? -v : v;
char prefix = 0;
const char *rest = base;
if ((base[0] == 'k' || base[0] == 'M') && scaledUnit(base + 1))
{
prefix = base[0];
rest = base + 1;
}
else if (!scaledUnit(base))
rest = nullptr;
if (rest)
{
char next = prefix;
if (a >= 1000 && prefix != 'M')
{
v /= 1000;
next = prefix ? 'M' : 'k';
}
else if (a < 1 && a > 0 && prefix)
{
v *= 1000;
next = prefix == 'M' ? 'k' : 0;
}
if (next != prefix)
{
char scaled[ELEMENT_SUFFIX_SIZE + 1];
const char new_prefix[2] = {next, '\0'};
snprintf(scaled, sizeof(scaled), "%.*s%s%s", (int)spaces, unit, new_prefix, rest);
snprintf(unit, unit_size, "%s", scaled);
if (decimals < 0)
decimals = 1;
}
}
}
formatNumber(v, decimals, out, size);
}
static const char *const DAYS_SHORT_WORD[] = {"d", "d"};
// Duration: a number of seconds, minutes or hours
static void showDuration(const PageElement &e, double v, char *out, size_t size)
{
static const double UNITS[] = {1, 60, 3600};
v *= e.factor * UNITS[e.input_unit < UNIT_COUNT ? e.input_unit : 0];
const char *sign = v < 0 ? "-" : "";
long long total = (long long)(v < 0 ? -v : v);
long long days = total / 86400, hours = total % 86400 / 3600, minutes = total % 3600 / 60, seconds = total % 60;
int lang = e.language < Language::Count ? (int)e.language : 0;
switch (e.format)
{
case DURATION_H_MM_SS:
snprintf(out, size, "%s%lld:%02lld:%02lld", sign, total / 3600, minutes, seconds);
break;
case DURATION_H_MM:
snprintf(out, size, "%s%lld:%02lld", sign, total / 3600, minutes);
break;
case DURATION_SMART:
if (days)
snprintf(out, size, "%s%lld %s", sign, days, DAYS_WORD[lang][days != 1]);
else if (hours)
snprintf(out, size, "%s%lld %s", sign, hours, HOURS_WORD[lang][hours != 1]);
else if (minutes)
snprintf(out, size, "%s%lld min", sign, minutes);
else
snprintf(out, size, "%s%lld s", sign, seconds);
break;
default: // the parts that are not 0: 2d 3h 15m; below a minute 45s
{
if (total < 60)
{
snprintf(out, size, "%s%llds", sign, seconds);
break;
}
size_t n = snprintf(out, size, "%s", sign);
if (days && n < size)
n += snprintf(out + n, size - n, "%lld%s", days, DAYS_SHORT_WORD[lang]);
if (hours && n < size)
n += snprintf(out + n, size - n, "%s%lldh", days ? " " : "", hours);
if (minutes && n < size)
snprintf(out + n, size - n, "%s%lldm", days || hours ? " " : "", minutes);
break;
}
}
}
// Time: a timestamp in local time, or how long until / ago
static bool showTimestamp(const PageElement &e, const char *value, const LocalTime &t, char *out, size_t size)
{
int64_t utc;
if (!parseTimestamp(value, utc))
return false;
int lang = e.language < Language::Count ? (int)e.language : 0;
if (e.format == TIMESTAMP_RELATIVE)
{
int64_t now = (int64_t)minutesSince2000(t) * 60 + t.second - t.utc_offset * 60;
int64_t diff = utc - now;
int64_t a = diff < 0 ? -diff : diff;
if (a < 60)
{
snprintf(out, size, "%s", lang ? "now" : "nu");
return true;
}
char amount[24];
if (a < 3600)
snprintf(amount, sizeof(amount), "%lld min", (long long)(a / 60));
else if (a < 86400)
snprintf(amount, sizeof(amount), "%lld %s", (long long)(a / 3600), HOURS_WORD[lang][a / 3600 != 1]);
else
snprintf(amount, sizeof(amount), "%lld %s", (long long)(a / 86400), DAYS_WORD[lang][a / 86400 != 1]);
if (diff > 0)
snprintf(out, size, "%s %s", lang ? "in" : "over", amount);
else
snprintf(out, size, "%s %s", amount, lang ? "ago" : "geleden");
return true;
}
int64_t local = utc + t.utc_offset * 60;
int64_t days = local >= 0 ? local / 86400 : (local - 86399) / 86400;
int minute_of_day = (int)((local - days * 86400) / 60);
int y, m, d;
civilFromDays((int32_t)(days + DAYS_1970_TO_2000), y, m, d);
int weekday = (int)((days + DAYS_1970_TO_2000 + 4) % 7); // 1970-01-01 was a Thursday
switch (e.format)
{
case TIMESTAMP_WEEKDAY_H_MM:
snprintf(out, size, "%s %02d:%02d", WEEKDAYS_SHORT[lang][weekday], minute_of_day / 60, minute_of_day % 60);
break;
case TIMESTAMP_DATE_H_MM:
snprintf(out, size, "%02d-%02d %02d:%02d", d, m, minute_of_day / 60, minute_of_day % 60);
break;
default:
snprintf(out, size, "%02d:%02d", minute_of_day / 60, minute_of_day % 60);
break;
}
return true;
}
// Map: "state=text, state=text"; false when the state is not in it
static bool showMapped(const PageElement &e, const char *value, char *out, size_t size)
{
const char *p = e.map;
while (*p)
{
const char *end = strchr(p, ',');
if (!end)
end = p + strlen(p);
const char *eq = (const char *)memchr(p, '=', end - p);
if (eq)
{
const char *k = p;
while (k < eq && *k == ' ')
k++;
const char *k_end = eq;
while (k_end > k && k_end[-1] == ' ')
k_end--;
size_t len = k_end - k;
if (len == strlen(value) && strncasecmpAscii(k, value, len))
{
const char *v = eq + 1;
while (v < end && *v == ' ')
v++;
const char *v_end = end;
while (v_end > v && v_end[-1] == ' ')
v_end--;
snprintf(out, size, "%.*s", (int)(v_end - v), v);
return true;
}
}
p = *end ? end + 1 : end;
}
return false;
}
// "--" when unknown
static void formatHaValue(const PageElement &e, const char *value, const LocalTime &t, char *out, size_t size)
{
char unit[ELEMENT_SUFFIX_SIZE + 2];
snprintf(unit, sizeof(unit), "%s", e.suffix);
char shown[64];
double v;
if (value == nullptr || value[0] == '\0' || strcmp(value, "unavailable") == 0 || strcmp(value, "unknown") == 0)
snprintf(shown, sizeof(shown), "--");
else if (e.show == HaShow::Map)
{
if (!showMapped(e, value, shown, sizeof(shown)))
snprintf(shown, sizeof(shown), "%s", value);
}
else if (e.show == HaShow::Time)
{
if (!showTimestamp(e, value, t, shown, sizeof(shown)))
snprintf(shown, sizeof(shown), "%s", value);
}
else if (e.show == HaShow::Duration && parseNumber(value, v))
showDuration(e, v, shown, sizeof(shown));
else if (e.show == HaShow::Number && parseNumber(value, v) &&
(e.decimals >= 0 || e.factor != 1 || e.auto_unit || e.absolute))
showNumber(e, v, shown, sizeof(shown), unit, sizeof(unit));
else
snprintf(shown, sizeof(shown), "%s", value); // not a number: as is
snprintf(out, size, "%s%s%s", e.text, shown, unit);
}
// 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)
{
switch (e.type)
{
case ElementType::Clock:
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);
break;
case ElementType::Date:
formatDate(e, t, out, size);
break;
case ElementType::HaValue:
formatHaValue(e, values.ha_value, t, 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:
case ElementType::Bar:
out[0] = '\0';
break;
default:
snprintf(out, size, "%s", e.text);
break;
}
}
int barFill(const PageElement &e, const char *ha_value, int fill_width)
{
double v;
if (!ha_value || !parseNumber(ha_value, v) || fill_width <= 0)
return 0;
double part = v * e.factor / e.bar_max;
if (!(part > 0)) // also NaN
return 0;
if (part >= 1)
return fill_width;
int n = (int)(part * fill_width + 0.5);
return n;
}
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;
}
}