Public Access
Add dynamic pages with clock, date, Home Assistant values and fonts
The hardcoded clock is now the built-in dynamic page "Clock". A dynamic page has a background bitmap per time of day and up to 8 elements (clock, date, Home Assistant value, text), each with position, alignment, font and overlay. Pages are drawn on the ESP32, also for the web previews. - Pages tab in the web UI: page editor with drag and drop, font upload (TTF/OTF converted in the browser to the FDF1 dot font format) - Built-in fonts Small, Normal, Large and Full height (generated by tools/make_builtin_fonts.py), built-in bitmaps Pikachu and Pikachu yawning - Home Assistant values over the REST API (URL and token in the HA tab) - Settings version 4: evening settings removed (now time slots of the Clock page), old records are converted - HA Mode dropdown lists the dynamic pages and bitmaps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d3b084de1
commit
0411804a2f
49 files changed
+3058
-331
No files matched your search
@@ -0,0 +1,104 @@
|
||||
#include "web/page_json.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "logic/settings.h"
|
||||
|
||||
static const char *const TYPES[] = {"clock", "date", "ha", "text"};
|
||||
static const char *const ALIGNS[] = {"left", "center", "right"};
|
||||
static const char *const OVERLAYS[] = {"top", "box", "invert"};
|
||||
static const char *const LANGUAGES[] = {"nl", "en"};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
o["text"] = e.text;
|
||||
o["suffix"] = e.suffix;
|
||||
o["entity"] = e.entity;
|
||||
o["attribute"] = e.attribute;
|
||||
}
|
||||
}
|
||||
|
||||
// False when the text does not fit
|
||||
static bool copyText(JsonVariantConst value, char *out, size_t size)
|
||||
{
|
||||
const char *text = value | "";
|
||||
if (strlen(text) >= size)
|
||||
return false;
|
||||
strcpy(out, text);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *pageFromJson(JsonVariantConst in, DynamicPage &page)
|
||||
{
|
||||
page = DynamicPage();
|
||||
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)
|
||||
return "Too many elements (8 at most)";
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user