Public Access
Add a page schedule with rotation, Home Assistant trigger rules and a Stopwatch tab
- Schedule: daily blocks from a start time, each with up to 6 rotating pages or bitmaps (own duration), switching with the effect or instantly. A manual pick (Show tab, HA Mode) lasts until the next block; Back to schedule ends it. - Rules: an HA state (=, is not, above, below) shows a page while true, adds it to the rotation while true, or shows it for N seconds when it becomes true. Each rule is polled at its own interval, with a 60 s back-off on errors. - PageSelector (pure logic, tested) decides the shown page; schedule stored in NVS; HA sensor Shown page and button Back to schedule. - Web UI: Schedule and Stopwatch tabs, bitmaps used by pages or rules in their own subsection, schedule in export/import. Version 2.6.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
47ef65ee7d
commit
93b9bec69a
25 files changed
+1217
-50
No files matched your search
+133
-14
@@ -9,9 +9,17 @@
|
||||
#include "display/font_store.h"
|
||||
#include "display/page_resources.h"
|
||||
#include "display/page_store.h"
|
||||
#include "logic/page_selector.h"
|
||||
#include "net/ha_api.h"
|
||||
#include "system/schedule_store.h"
|
||||
#include "system/settings_store.h"
|
||||
|
||||
static void copyName(char *out, const char *name)
|
||||
{
|
||||
strncpy(out, name, PAGE_NAME_SIZE - 1);
|
||||
out[PAGE_NAME_SIZE - 1] = '\0';
|
||||
}
|
||||
|
||||
NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
|
||||
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
|
||||
FlipdotFrames frames(display, pageResources);
|
||||
@@ -23,6 +31,12 @@ static bool restart_pending = false;
|
||||
static uint32_t restart_at = 0;
|
||||
// Not on the stack: about 1.7 KB
|
||||
static DynamicPage loaded_page;
|
||||
// What is shown when (about 2.7 KB), and which page that is now
|
||||
static PageSchedule page_schedule;
|
||||
static PageSelector selector;
|
||||
static char shown[PAGE_NAME_SIZE] = DEFAULT_PAGE;
|
||||
static PageChoice shown_choice;
|
||||
static bool time_known = false;
|
||||
|
||||
static void showDynamicPage()
|
||||
{
|
||||
@@ -31,23 +45,26 @@ static void showDynamicPage()
|
||||
controller.setPage(Page::Dynamic);
|
||||
}
|
||||
|
||||
// Load the page into the frames: a dynamic page or a bitmap; the Clock page when it is gone
|
||||
static void applyPage()
|
||||
// Load the shown page into the frames: a dynamic page or a bitmap; the Clock page when it is
|
||||
// gone. instant: the next frame without the refresh effect.
|
||||
static void applyPage(bool instant = false)
|
||||
{
|
||||
if (loadPage(settings.page, loaded_page))
|
||||
if (instant)
|
||||
frames.showNextInstant();
|
||||
if (loadPage(shown, loaded_page))
|
||||
{
|
||||
showDynamicPage();
|
||||
return;
|
||||
}
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
if (loadBitmap(settings.page, bitmap))
|
||||
if (loadBitmap(shown, bitmap))
|
||||
{
|
||||
frames.setPageBitmap(bitmap);
|
||||
haApiWatch(nullptr);
|
||||
controller.setPage(Page::Bitmap);
|
||||
return;
|
||||
}
|
||||
Serial.printf("Page \"%s\" not found, showing the clock\n", settings.page);
|
||||
Serial.printf("Page \"%s\" not found, showing the clock\n", shown);
|
||||
builtinPage(DEFAULT_PAGE, loaded_page);
|
||||
showDynamicPage();
|
||||
}
|
||||
@@ -92,6 +109,11 @@ const Settings &appSettings()
|
||||
void appLoadSettings()
|
||||
{
|
||||
loadSettings(settings);
|
||||
loadSchedule(page_schedule);
|
||||
selector.setSchedule(&page_schedule);
|
||||
selector.setChosen(settings.page);
|
||||
haApiWatchRules(&page_schedule);
|
||||
copyName(shown, settings.page); // until the time is known
|
||||
apply();
|
||||
}
|
||||
|
||||
@@ -118,28 +140,119 @@ const char *appSaveSettings(const Settings &new_settings, bool &restart_needed)
|
||||
a.effect_small_only != b.effect_small_only;
|
||||
settings = new_settings;
|
||||
saveSettings(settings);
|
||||
selector.setChosen(settings.page);
|
||||
apply();
|
||||
if (drive_changed)
|
||||
status.sendDrive();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void copyName(char *out, const char *name)
|
||||
|
||||
// The chosen page, without overriding the schedule
|
||||
static void setChosen(const char *name)
|
||||
{
|
||||
strncpy(out, name, PAGE_NAME_SIZE - 1);
|
||||
out[PAGE_NAME_SIZE - 1] = '\0';
|
||||
copyName(settings.page, name);
|
||||
saveSettings(settings);
|
||||
selector.setChosen(name);
|
||||
}
|
||||
|
||||
const char *appSelectPage(const char *name)
|
||||
{
|
||||
if (!pageExists(name) && !bitmapExists(name))
|
||||
return "Page not found";
|
||||
copyName(settings.page, name);
|
||||
saveSettings(settings);
|
||||
applyPage();
|
||||
setChosen(name);
|
||||
selector.pick(name);
|
||||
if (!time_known)
|
||||
{
|
||||
copyName(shown, name);
|
||||
applyPage();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appSaveSchedule(const PageSchedule &schedule)
|
||||
{
|
||||
const char *error = validateSchedule(schedule);
|
||||
if (error)
|
||||
return error;
|
||||
for (int i = 0; i < schedule.block_count; i++)
|
||||
for (int j = 0; j < schedule.blocks[i].item_count; j++)
|
||||
{
|
||||
const char *page = schedule.blocks[i].items[j].page;
|
||||
if (!pageExists(page) && !bitmapExists(page))
|
||||
return "A page of the schedule was not found";
|
||||
}
|
||||
for (int i = 0; i < schedule.rule_count; i++)
|
||||
if (!pageExists(schedule.rules[i].page) && !bitmapExists(schedule.rules[i].page))
|
||||
return "The page of a rule was not found";
|
||||
page_schedule = schedule;
|
||||
saveSchedule(page_schedule);
|
||||
selector.setSchedule(&page_schedule);
|
||||
haApiWatchRules(&page_schedule);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PageSchedule &appSchedule()
|
||||
{
|
||||
return page_schedule;
|
||||
}
|
||||
|
||||
void appBackToSchedule()
|
||||
{
|
||||
selector.backToSchedule();
|
||||
}
|
||||
|
||||
const char *appShownPage()
|
||||
{
|
||||
return shown;
|
||||
}
|
||||
|
||||
void appShowStatus(char *out, size_t size)
|
||||
{
|
||||
static const char *const MODES[] = {"while true", "in rotation", "for a while"};
|
||||
switch (shown_choice.reason)
|
||||
{
|
||||
case ShowReason::Trigger:
|
||||
case ShowReason::Rotation:
|
||||
if (shown_choice.rule >= 0 && shown_choice.rule < page_schedule.rule_count)
|
||||
{
|
||||
const TriggerRule &r = page_schedule.rules[shown_choice.rule];
|
||||
snprintf(out, size, "rule %d (%s, %s)", shown_choice.rule + 1, r.entity, MODES[(int)r.mode]);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case ShowReason::Schedule:
|
||||
snprintf(out, size, "schedule");
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
int until = selector.pickedUntil();
|
||||
if (until >= 0)
|
||||
snprintf(out, size, "chosen, until %02d:%02d", until / 60, until % 60);
|
||||
else
|
||||
snprintf(out, size, selector.scheduleActive() ? "chosen" : "chosen (no schedule)");
|
||||
}
|
||||
|
||||
// Which page now: the rules (from Home Assistant), the schedule, the chosen page
|
||||
void appPageLoop(bool time_valid, const LocalTime &time)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < page_schedule.rule_count; i++)
|
||||
{
|
||||
const TriggerRule &r = page_schedule.rules[i];
|
||||
selector.setRuleState(i, ruleMatches(r, haApiValue(r.entity, r.attribute)), now);
|
||||
}
|
||||
time_known = time_valid;
|
||||
if (!time_valid)
|
||||
return;
|
||||
shown_choice = selector.update(time.hour * 60 + time.minute, now);
|
||||
if (!shown_choice.changed && sameName(shown, shown_choice.page))
|
||||
return;
|
||||
copyName(shown, shown_choice.page);
|
||||
applyPage(shown_choice.instant);
|
||||
}
|
||||
|
||||
const char *appSelectOffPage(const char *name)
|
||||
{
|
||||
if (name[0] && !bitmapExists(name))
|
||||
@@ -172,8 +285,12 @@ static const char *usedBy(const String &page)
|
||||
return message;
|
||||
}
|
||||
|
||||
static const char *USED_BY_SCHEDULE = "Used by the schedule or a rule: change that first";
|
||||
|
||||
const char *appDeleteBitmap(const char *name)
|
||||
{
|
||||
if (scheduleUses(page_schedule, name))
|
||||
return USED_BY_SCHEDULE;
|
||||
String user = pageUsingBitmap(name);
|
||||
if (user.length())
|
||||
return usedBy(user);
|
||||
@@ -181,7 +298,7 @@ const char *appDeleteBitmap(const char *name)
|
||||
if (error)
|
||||
return error;
|
||||
if (sameName(settings.page, name))
|
||||
appSelectPage(DEFAULT_PAGE);
|
||||
setChosen(DEFAULT_PAGE);
|
||||
if (sameName(settings.off_page, name))
|
||||
appSelectOffPage(OFF_PAGE_BLANK);
|
||||
return nullptr;
|
||||
@@ -200,18 +317,20 @@ const char *appSavePage(const char *name, const DynamicPage &page)
|
||||
const char *error = savePage(name, page);
|
||||
if (error)
|
||||
return error;
|
||||
if (sameName(settings.page, name))
|
||||
if (sameName(shown, name))
|
||||
reloadPage();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appDeletePage(const char *name)
|
||||
{
|
||||
if (scheduleUses(page_schedule, name))
|
||||
return USED_BY_SCHEDULE;
|
||||
const char *error = deletePage(name);
|
||||
if (error)
|
||||
return error;
|
||||
if (sameName(settings.page, name))
|
||||
appSelectPage(DEFAULT_PAGE);
|
||||
setChosen(DEFAULT_PAGE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user