2026-10-15 19:43:59 +02:00
|
|
|
#include "web/page_json.h"
|
|
|
|
|
|
|
|
|
|
#include <Arduino.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include "logic/settings.h"
|
|
|
|
|
|
2026-10-16 16:17:23 +02:00
|
|
|
static const char *const TYPES[] = {"clock", "date", "ha", "text", "countdown", "stopwatch", "image", "bar"};
|
2026-10-15 19:43:59 +02:00
|
|
|
static const char *const ALIGNS[] = {"left", "center", "right"};
|
|
|
|
|
static const char *const OVERLAYS[] = {"top", "box", "invert"};
|
|
|
|
|
static const char *const LANGUAGES[] = {"nl", "en"};
|
2026-10-15 21:50:19 +02:00
|
|
|
static const char *const SHOWS[] = {"number", "duration", "time", "map"};
|
|
|
|
|
static const char *const UNITS[] = {"s", "min", "h"};
|
2026-10-15 19:43:59 +02:00
|
|
|
|
|
|
|
|
template <size_t N> static int indexOf(const char *const (&names)[N], const char *name)
|
|
|
|
|
{
|
|
|
|
|
for (size_t i = 0; i < N; i++)
|
|
|
|
|
if (name && strcmp(names[i], name) == 0)
|
|
|
|
|
return i;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
static const char *DEGREE_UTF8 = "\xC2\xB0";
|
|
|
|
|
|
|
|
|
|
static String withDegrees(const char *text)
|
|
|
|
|
{
|
|
|
|
|
String s = text;
|
|
|
|
|
s.replace(String(DEGREE_SIGN), DEGREE_UTF8);
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 19:43:59 +02:00
|
|
|
void pageToJson(const DynamicPage &page, JsonObject out)
|
|
|
|
|
{
|
|
|
|
|
char time[6];
|
|
|
|
|
JsonArray slots = out["slots"].to<JsonArray>();
|
|
|
|
|
for (int i = 0; i < page.slot_count; i++)
|
|
|
|
|
{
|
|
|
|
|
JsonObject slot = slots.add<JsonObject>();
|
|
|
|
|
formatTime(page.slots[i].start, time);
|
|
|
|
|
slot["start"] = String(time);
|
|
|
|
|
slot["bitmap"] = page.slots[i].bitmap;
|
|
|
|
|
}
|
2026-10-17 16:57:31 +02:00
|
|
|
out["effect"] = page.effect == PAGE_EFFECT_DEFAULT ? -1 : page.effect; // -1: Hardware setting
|
2026-10-15 19:43:59 +02:00
|
|
|
JsonArray elements = out["elements"].to<JsonArray>();
|
|
|
|
|
for (int i = 0; i < page.element_count; i++)
|
|
|
|
|
{
|
|
|
|
|
const PageElement &e = page.elements[i];
|
|
|
|
|
JsonObject o = elements.add<JsonObject>();
|
|
|
|
|
o["type"] = TYPES[(int)e.type];
|
|
|
|
|
o["x"] = e.x;
|
|
|
|
|
o["y"] = e.y;
|
|
|
|
|
o["align"] = ALIGNS[(int)e.align];
|
|
|
|
|
o["overlay"] = OVERLAYS[(int)e.overlay];
|
|
|
|
|
o["font"] = e.font;
|
|
|
|
|
o["format"] = e.format;
|
|
|
|
|
o["language"] = LANGUAGES[(int)e.language];
|
|
|
|
|
o["decimals"] = e.decimals;
|
2026-10-15 21:01:14 +02:00
|
|
|
o["text"] = withDegrees(e.text);
|
|
|
|
|
o["suffix"] = withDegrees(e.suffix);
|
2026-10-15 19:43:59 +02:00
|
|
|
o["entity"] = e.entity;
|
|
|
|
|
o["attribute"] = e.attribute;
|
2026-10-15 21:01:14 +02:00
|
|
|
o["factor"] = e.factor;
|
|
|
|
|
o["scroll_width"] = e.scroll_width;
|
|
|
|
|
o["scroll_speed"] = e.scroll_speed;
|
|
|
|
|
char target[17];
|
|
|
|
|
formatTarget(e.target, target);
|
|
|
|
|
o["target"] = String(target);
|
2026-10-15 21:50:19 +02:00
|
|
|
o["show"] = SHOWS[(int)e.show];
|
|
|
|
|
o["input_unit"] = UNITS[e.input_unit];
|
|
|
|
|
o["auto_unit"] = e.auto_unit;
|
|
|
|
|
o["absolute"] = e.absolute;
|
|
|
|
|
o["map"] = withDegrees(e.map);
|
2026-10-16 16:17:23 +02:00
|
|
|
o["bar_width"] = e.bar_width;
|
|
|
|
|
o["bar_height"] = e.bar_height;
|
|
|
|
|
o["bar_max"] = e.bar_max;
|
|
|
|
|
o["bar_frame"] = e.bar_frame;
|
2026-10-16 22:36:29 +02:00
|
|
|
o["day_offset"] = e.day_offset;
|
2026-10-15 19:43:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
// False when the text does not fit. The degree sign (UTF-8) becomes DEGREE_SIGN.
|
2026-10-15 19:43:59 +02:00
|
|
|
static bool copyText(JsonVariantConst value, char *out, size_t size)
|
|
|
|
|
{
|
2026-10-15 21:01:14 +02:00
|
|
|
String text = value | "";
|
|
|
|
|
text.replace(DEGREE_UTF8, String(DEGREE_SIGN));
|
|
|
|
|
if (text.length() >= size)
|
2026-10-15 19:43:59 +02:00
|
|
|
return false;
|
2026-10-15 21:01:14 +02:00
|
|
|
strcpy(out, text.c_str());
|
2026-10-15 19:43:59 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *pageFromJson(JsonVariantConst in, DynamicPage &page)
|
|
|
|
|
{
|
2026-10-16 22:36:29 +02:00
|
|
|
clearPage(page);
|
2026-10-15 19:43:59 +02:00
|
|
|
JsonArrayConst slots = in["slots"];
|
|
|
|
|
JsonArrayConst elements = in["elements"];
|
|
|
|
|
if (slots.size() > (size_t)PAGE_SLOTS)
|
|
|
|
|
return "Too many time slots (8 at most)";
|
|
|
|
|
if (elements.size() > (size_t)PAGE_ELEMENTS)
|
2026-10-17 16:57:31 +02:00
|
|
|
return "Too many elements (16 at most)";
|
|
|
|
|
int effect = in["effect"] | -1;
|
|
|
|
|
if (effect < -1 || effect >= DRIVE_EFFECT_COUNT)
|
|
|
|
|
return "Unknown refresh effect";
|
|
|
|
|
page.effect = effect < 0 ? PAGE_EFFECT_DEFAULT : effect;
|
2026-10-15 19:43:59 +02:00
|
|
|
|
|
|
|
|
for (JsonVariantConst s : slots)
|
|
|
|
|
{
|
|
|
|
|
PageSlot &slot = page.slots[page.slot_count++];
|
|
|
|
|
if (!parseTime(s["start"] | "", slot.start))
|
|
|
|
|
return "Times must look like 08:30";
|
|
|
|
|
if (!copyText(s["bitmap"], slot.bitmap, sizeof(slot.bitmap)))
|
|
|
|
|
return "Bitmap name too long";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (JsonVariantConst o : elements)
|
|
|
|
|
{
|
|
|
|
|
PageElement &e = page.elements[page.element_count++];
|
|
|
|
|
int type = indexOf(TYPES, o["type"]), align = indexOf(ALIGNS, o["align"] | "left"),
|
|
|
|
|
overlay = indexOf(OVERLAYS, o["overlay"] | "top"), language = indexOf(LANGUAGES, o["language"] | "nl");
|
|
|
|
|
if (type < 0 || align < 0 || overlay < 0 || language < 0)
|
|
|
|
|
return "Unknown element setting";
|
|
|
|
|
e.type = (ElementType)type;
|
|
|
|
|
e.align = (Align)align;
|
|
|
|
|
e.overlay = (Overlay)overlay;
|
|
|
|
|
e.language = (Language)language;
|
|
|
|
|
// Range checks are in validatePage(); out of range numbers must not wrap
|
|
|
|
|
e.x = constrain(o["x"] | 0, -1000, 1000);
|
|
|
|
|
e.y = constrain(o["y"] | 0, -1000, 1000);
|
|
|
|
|
e.format = constrain(o["format"] | 0, 0, 255);
|
|
|
|
|
e.decimals = constrain(o["decimals"] | (int)DECIMALS_AS_IS, -100, 100);
|
2026-10-15 21:01:14 +02:00
|
|
|
e.factor = o["factor"] | 1.0f;
|
|
|
|
|
e.scroll_width = constrain(o["scroll_width"] | 0, 0, 255);
|
2026-10-16 16:23:16 +02:00
|
|
|
e.scroll_speed = o["scroll_speed"] | 8.0f; // checked in validatePage()
|
2026-10-15 21:50:19 +02:00
|
|
|
int show = indexOf(SHOWS, o["show"] | "number"), unit = indexOf(UNITS, o["input_unit"] | "s");
|
|
|
|
|
if (show < 0 || unit < 0)
|
|
|
|
|
return "Unknown Home Assistant value setting";
|
|
|
|
|
e.show = (HaShow)show;
|
|
|
|
|
e.input_unit = unit;
|
|
|
|
|
e.auto_unit = o["auto_unit"] | false;
|
|
|
|
|
e.absolute = o["absolute"] | false;
|
2026-10-16 16:17:23 +02:00
|
|
|
e.bar_width = constrain(o["bar_width"] | 40, 0, 255);
|
|
|
|
|
e.bar_height = constrain(o["bar_height"] | 3, 0, 255);
|
|
|
|
|
e.bar_max = o["bar_max"] | 100.0f;
|
|
|
|
|
e.bar_frame = o["bar_frame"] | true;
|
2026-10-16 22:36:29 +02:00
|
|
|
e.day_offset = constrain(o["day_offset"] | 0, -1, DAY_OFFSET_MAX + 1); // out of range: validatePage says so
|
2026-10-15 21:50:19 +02:00
|
|
|
if (!copyText(o["map"], e.map, sizeof(e.map)))
|
|
|
|
|
return "The list of texts is too long (63 characters)";
|
2026-10-15 21:01:14 +02:00
|
|
|
const char *target = o["target"] | "";
|
|
|
|
|
if (e.type == ElementType::Countdown && !parseTarget(target, e.target))
|
|
|
|
|
return "Countdown: enter a date and time";
|
2026-10-15 19:43:59 +02:00
|
|
|
if (!copyText(o["font"], e.font, sizeof(e.font)) || !copyText(o["text"], e.text, sizeof(e.text)) ||
|
|
|
|
|
!copyText(o["suffix"], e.suffix, sizeof(e.suffix)) || !copyText(o["entity"], e.entity, sizeof(e.entity)) ||
|
|
|
|
|
!copyText(o["attribute"], e.attribute, sizeof(e.attribute)))
|
|
|
|
|
return "Text too long (32 characters, unit 16, entity 64)";
|
|
|
|
|
}
|
|
|
|
|
return validatePage(page);
|
|
|
|
|
}
|