Add conversions for Home Assistant values: duration, time, own texts, auto unit

- Show as number (multiplier, decimals, auto unit W -> kW -> MW, without
  minus sign), duration (seconds/minutes/hours as 2h 21m, 2:21:00, 2:21 or
  smart), time (HA timestamps in local time, with weekday or date, or
  relative: over 34 min / 12 min geleden) or own texts per state.
- Page format 3; version 2 pages are converted. Version 2.7.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-15 21:50:19 +02:00
1 parent 93b9bec69a
commit 87e242bf89
11 files changed
+601 -30

No files matched your search

+293 -21
View File
@@ -38,6 +38,14 @@ bool validEntityId(const char *entity)
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++)
@@ -46,10 +54,16 @@ static bool validAttribute(const char *attribute)
return true;
}
static uint8_t formatCount(ElementType type)
static uint8_t formatCount(const PageElement &e)
{
switch (type)
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:
@@ -98,7 +112,11 @@ const char *validatePage(const DynamicPage &page)
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.format >= formatCount(e.type))
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 (!isfinite(e.factor) || e.factor == 0)
return "The multiplier must be a number other than 0";
@@ -171,6 +189,40 @@ uint32_t minutesSince2000(const LocalTime &t)
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)
@@ -260,27 +312,247 @@ static void formatDate(const PageElement &e, const LocalTime &t, char *out, size
}
}
// Rounded to the element's decimals (after the multiplier) when it is a number; "--" when unknown
static void formatHaValue(const PageElement &e, const char *value, char *out, size_t size)
static bool parseNumber(const char *text, double &v)
{
if (value == nullptr || value[0] == '\0' || strcmp(value, "unavailable") == 0 || strcmp(value, "unknown") == 0)
value = "--";
char number[24];
char *end;
double v = strtod(value, &end);
if (strcmp(value, "--") != 0 && end != value && *end == '\0' && (e.decimals >= 0 || e.factor != 1))
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)
{
v *= e.factor;
if (e.decimals >= 0)
snprintf(number, sizeof(number), "%.*f", e.decimals, v);
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;
// 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;
}
}
}
snprintf(out, size, "%s%s%s", e.text, value, e.suffix);
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
@@ -364,7 +636,7 @@ void elementText(const PageElement &e, const LocalTime &t, const ElementValues &
formatDate(e, t, out, size);
break;
case ElementType::HaValue:
formatHaValue(e, values.ha_value, out, size);
formatHaValue(e, values.ha_value, t, out, size);
break;
case ElementType::Countdown:
formatCountdown(e, t, out, size);