Slow mode replaces the late-evening quarter hours
Slow mode (Home Assistant switch "FlipDot Display Slow mode", dropdown
"FlipDot Display Slow mode interval", web UI Settings) makes the display
change only every 1, 2, 5, 10, 15, 20, 30 or 60 minutes, on the clock, and
powers it off in between: less noise. Nothing else is drawn meanwhile, also
no page changes. It updates at once after boot, the night, the Power switch
and live mode, and when it is switched off. Rules can override it ("show at
once in slow mode"): while their page is shown the display works normally.
Settings layout 7 and schedule layout 4 (older records are converted; they
are stored again once the firmware is marked good, so a rollback still
finds the old ones). Pages in older formats are rewritten in the current
one, in time-boxed steps. /api/status reports old_data_left.
Version 2.15.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -98,9 +98,15 @@ See README.md for the overview.
|
|||||||
- All settings are in `config.h`. Keep it free of Arduino types for the same reason.
|
- All settings are in `config.h`. Keep it free of Arduino types for the same reason.
|
||||||
- Frames are only sent when the screen changes. The schedule uses 08:00 as the start of
|
- Frames are only sent when the screen changes. The schedule uses 08:00 as the start of
|
||||||
the day on purpose (that is what the old code did, and the README documents it).
|
the day on purpose (that is what the old code did, and the README documents it).
|
||||||
|
- Slow mode (`SlowModeSettings`, replaced the late-evening quarter hours) lives in
|
||||||
|
`DisplayController::_slowClock()`: one frame per update minute (`slowModeUpdate()`), power
|
||||||
|
off in between, nothing else drawn (also no page changes). It updates at once after boot,
|
||||||
|
night, the Power switch and live mode; rules with `over_slow_mode` suspend it
|
||||||
|
(`setSlowModeOverride()`, fed from `PageChoice` in `appPageLoop()`).
|
||||||
- Home Assistant switches are not optimistic: callbacks must call `sender->setState()`.
|
- Home Assistant switches are not optimistic: callbacks must call `sender->setState()`.
|
||||||
The unique ids (`FlipDotDisplayPower`, `FlipDotDisplayMode`, `FlipDotDisplayOffImage`,
|
The unique ids (`FlipDotDisplayPower`, `FlipDotDisplayMode`, `FlipDotDisplayOffImage`,
|
||||||
`FlipDotDisplayStopwatch`, ...) and `HA_DEVICE_ID` must stay
|
`FlipDotDisplayStopwatch`, `FlipDotDisplaySlowMode`, `FlipDotDisplaySlowModeInterval`,
|
||||||
|
...) and `HA_DEVICE_ID` must stay
|
||||||
the same, or Home Assistant creates new entities.
|
the same, or Home Assistant creates new entities.
|
||||||
|
|
||||||
## Keeping ESP32 OTA alive (most important requirement)
|
## Keeping ESP32 OTA alive (most important requirement)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 367 KiB After Width: | Height: | Size: 387 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 156 KiB |
@@ -23,6 +23,10 @@ The ESP32 connects to Home Assistant in two ways:
|
|||||||
stopwatch of the dynamic pages.
|
stopwatch of the dynamic pages.
|
||||||
- `FlipDot Display Shown page` sensor: what the display shows now (schedule, rule or
|
- `FlipDot Display Shown page` sensor: what the display shows now (schedule, rule or
|
||||||
chosen page). `FlipDot Display Back to schedule` button: ends a page chosen by hand.
|
chosen page). `FlipDot Display Back to schedule` button: ends a page chosen by hand.
|
||||||
|
- `FlipDot Display Slow mode` switch and `FlipDot Display Slow mode interval` dropdown
|
||||||
|
(`1 minute` ... `60 minutes`): the display only changes at those times, on the clock, and
|
||||||
|
is switched off in between (less noise, e.g. in the evening with an automation). Rules
|
||||||
|
with "show at once in slow mode" still show their page right away.
|
||||||
- `FlipDot Display Firmware` sensor: version and health, e.g. `2.7.2, ok, 0 crashes, recovery ok`.
|
- `FlipDot Display Firmware` sensor: version and health, e.g. `2.7.2, ok, 0 crashes, recovery ok`.
|
||||||
- `FlipDot Display Recovery` button: restarts into the [recovery app](recovery.md) for 10
|
- `FlipDot Display Recovery` button: restarts into the [recovery app](recovery.md) for 10
|
||||||
minutes.
|
minutes.
|
||||||
|
|||||||
@@ -10,11 +10,14 @@
|
|||||||
connects to MQTT.
|
connects to MQTT.
|
||||||
2. **Clock.** Once the time is synced, the ESP32 sends a new frame whenever the screen
|
2. **Clock.** Once the time is synced, the ESP32 sends a new frame whenever the screen
|
||||||
changes, mostly once a minute:
|
changes, mostly once a minute:
|
||||||
- **08:00–22:00:** the page: the chosen page (by default the built-in Clock page,
|
- **08:00–23:30:** the page: the chosen page (by default the built-in Clock page,
|
||||||
Pikachu with a thought bubble showing `HH:MM`, yawning from 17:00), or what the
|
Pikachu with a thought bubble showing `HH:MM`, yawning from 17:00), or what the
|
||||||
[schedule and rules](web-ui.md#schedule) pick.
|
[schedule and rules](web-ui.md#schedule) pick.
|
||||||
- **22:00–23:30:** the clock is only shown during the minutes :00, :15, :30 and :45.
|
- **Slow mode** (a switch in Home Assistant and the web UI, for less noise): the display
|
||||||
30 seconds after each of those minutes, the relay turns the display off.
|
only changes every 1, 2, 5, 10, 15, 20, 30 or 60 minutes, on the clock (15 minutes =
|
||||||
|
:00, :15, :30 and :45). 30 seconds after each update the relay turns the display off;
|
||||||
|
the dots keep the image. Page changes wait for the next update too, except for rules
|
||||||
|
with "show at once in slow mode": while their page is shown, the display works normally.
|
||||||
- **23:31–08:00:** the screen is cleared (or shows the off image chosen in the web UI)
|
- **23:31–08:00:** the screen is cleared (or shows the off image chosen in the web UI)
|
||||||
and power is cut after 30 seconds.
|
and power is cut after 30 seconds.
|
||||||
3. **Home Assistant**: switches, dropdowns and sensors over MQTT, see
|
3. **Home Assistant**: switches, dropdowns and sensors over MQTT, see
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ Rules bring a page to the front from a Home Assistant state (an entity or attrib
|
|||||||
while it is true, or show it for a while when it becomes true. Each rule is read at its own
|
while it is true, or show it for a while when it becomes true. Each rule is read at its own
|
||||||
interval (2-600 s) and can be limited to some of the blocks. The first rule that shows a
|
interval (2-600 s) and can be limited to some of the blocks. The first rule that shows a
|
||||||
page wins. A rule can leave a page chosen by hand alone (untick "also over a page chosen by
|
page wins. A rule can leave a page chosen by hand alone (untick "also over a page chosen by
|
||||||
hand"): it then waits until that pick ends (the next block or "Back to schedule"). The display schedule ([Settings](#settings)) still decides when the display is on.
|
hand"): it then waits until that pick ends (the next block or "Back to schedule"). A rule
|
||||||
|
with "show at once in slow mode" ticked brings its page right away in
|
||||||
|
[slow mode](#settings), and the display works normally while that page is shown. The display schedule ([Settings](#settings)) still decides when the display is on.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -114,8 +116,9 @@ on the dots. Save it as a bitmap with a name, or as an icon (only the area with
|
|||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
The display schedule (on/off, when the display is on, the late-evening quarter-hour mode,
|
The display schedule (on/off, when the display is on, when it switches off); slow mode
|
||||||
when it switches off); the "No WiFi" screen; the flip time, the pause after each pulse,
|
(also a Home Assistant switch: the display only changes every 1-60 minutes, on the clock,
|
||||||
|
and is switched off in between, for less noise); the "No WiFi" screen; the flip time, the pause after each pulse,
|
||||||
pulses per dot and the refresh effect (instant, sparkle, wipe, rain, from the middle out,
|
pulses per dot and the refresh effect (instant, sparkle, wipe, rain, from the middle out,
|
||||||
diagonal, spiral, flip all then reveal, push, random; optionally only for small changes
|
diagonal, spiral, flip all then reveal, push, random; optionally only for small changes
|
||||||
like the clock's minutes; pages can choose their own effect); how often every dot is flipped again ("Dot refresh", default
|
like the clock's minutes; pages can choose their own effect); how often every dot is flipped again ("Dot refresh", default
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ static void applyOffPage()
|
|||||||
|
|
||||||
static void apply()
|
static void apply()
|
||||||
{
|
{
|
||||||
controller.setSettings(settings.schedule, settings.clock);
|
controller.setSettings(settings.schedule, settings.clock, settings.slow_mode);
|
||||||
controller.setEffectTime(effectTime(settings.drive));
|
controller.setEffectTime(effectTime(settings.drive));
|
||||||
status.setDrive(settings.flip_time, settings.drive, settings.refresh.dots);
|
status.setDrive(settings.flip_time, settings.drive, settings.refresh.dots);
|
||||||
frames.setDrive(settings.flip_time, settings.drive);
|
frames.setDrive(settings.flip_time, settings.drive);
|
||||||
@@ -109,6 +109,9 @@ static void reloadPage()
|
|||||||
controller.pageChanged();
|
controller.pageChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool settings_converted = false;
|
||||||
|
static bool schedule_converted = false;
|
||||||
|
|
||||||
const Settings &appSettings()
|
const Settings &appSettings()
|
||||||
{
|
{
|
||||||
return settings;
|
return settings;
|
||||||
@@ -116,8 +119,8 @@ const Settings &appSettings()
|
|||||||
|
|
||||||
void appLoadSettings()
|
void appLoadSettings()
|
||||||
{
|
{
|
||||||
loadSettings(settings);
|
settings_converted = loadSettings(settings);
|
||||||
loadSchedule(page_schedule);
|
schedule_converted = loadSchedule(page_schedule);
|
||||||
selector.setSchedule(&page_schedule);
|
selector.setSchedule(&page_schedule);
|
||||||
selector.setChosen(settings.page);
|
selector.setChosen(settings.page);
|
||||||
haApiWatchRules(&page_schedule);
|
haApiWatchRules(&page_schedule);
|
||||||
@@ -157,6 +160,27 @@ const char *appSaveSettings(const Settings &new_settings, bool &restart_needed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void appSetSlowMode(bool enabled)
|
||||||
|
{
|
||||||
|
if (settings.slow_mode.enabled == enabled)
|
||||||
|
return;
|
||||||
|
settings.slow_mode.enabled = enabled;
|
||||||
|
saveSettings(settings);
|
||||||
|
controller.setSettings(settings.schedule, settings.clock, settings.slow_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *appSetSlowModeMinutes(int minutes)
|
||||||
|
{
|
||||||
|
if (!validSlowModeMinutes(minutes))
|
||||||
|
return "Unknown slow mode interval";
|
||||||
|
if (settings.slow_mode.minutes == minutes)
|
||||||
|
return nullptr;
|
||||||
|
settings.slow_mode.minutes = minutes;
|
||||||
|
saveSettings(settings);
|
||||||
|
controller.setSettings(settings.schedule, settings.clock, settings.slow_mode);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// The chosen page, without overriding the schedule
|
// The chosen page, without overriding the schedule
|
||||||
static void setChosen(const char *name)
|
static void setChosen(const char *name)
|
||||||
{
|
{
|
||||||
@@ -256,6 +280,7 @@ void appPageLoop(bool time_valid, const LocalTime &time)
|
|||||||
if (!time_valid)
|
if (!time_valid)
|
||||||
return;
|
return;
|
||||||
shown_choice = selector.update(time.hour * 60 + time.minute, now);
|
shown_choice = selector.update(time.hour * 60 + time.minute, now);
|
||||||
|
controller.setSlowModeOverride(shown_choice.over_slow_mode);
|
||||||
if (!shown_choice.changed && sameName(shown, shown_choice.page))
|
if (!shown_choice.changed && sameName(shown, shown_choice.page))
|
||||||
return;
|
return;
|
||||||
copyName(shown, shown_choice.page);
|
copyName(shown, shown_choice.page);
|
||||||
@@ -534,6 +559,62 @@ void appMigrateIcons()
|
|||||||
reloadPage();
|
reloadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same steps as the icon migration: one page per check, MIGRATION_STEP_MS per pass
|
||||||
|
static bool data_listed = false;
|
||||||
|
static bool data_done = false;
|
||||||
|
static std::vector<String> data_pages;
|
||||||
|
static size_t data_next = 0;
|
||||||
|
static int data_left = -1;
|
||||||
|
|
||||||
|
void appMigrateData()
|
||||||
|
{
|
||||||
|
if (data_done || migration != Migration::Done)
|
||||||
|
return;
|
||||||
|
uint32_t pass = millis();
|
||||||
|
if (settings_converted)
|
||||||
|
{
|
||||||
|
saveSettings(settings);
|
||||||
|
settings_converted = false;
|
||||||
|
Serial.println("Settings stored in the new layout");
|
||||||
|
}
|
||||||
|
if (schedule_converted)
|
||||||
|
{
|
||||||
|
saveSchedule(page_schedule);
|
||||||
|
schedule_converted = false;
|
||||||
|
Serial.println("Schedule stored in the new layout");
|
||||||
|
}
|
||||||
|
if (!data_listed)
|
||||||
|
{
|
||||||
|
data_pages = storedPageNames();
|
||||||
|
data_listed = true;
|
||||||
|
data_left = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DynamicPage *page = new DynamicPage;
|
||||||
|
while (data_next < data_pages.size() && millis() - pass < MIGRATION_STEP_MS)
|
||||||
|
{
|
||||||
|
const char *name = data_pages[data_next++].c_str();
|
||||||
|
if (!pageInOldFormat(name))
|
||||||
|
continue;
|
||||||
|
const char *error = loadPage(name, *page) ? savePage(name, *page) : "unreadable";
|
||||||
|
Serial.printf("Page \"%s\" in the new format: %s\n", name, error ? error : "ok");
|
||||||
|
if (error)
|
||||||
|
data_left++;
|
||||||
|
}
|
||||||
|
delete page;
|
||||||
|
if (data_next < data_pages.size())
|
||||||
|
return;
|
||||||
|
data_pages.clear();
|
||||||
|
data_done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int appOldDataLeft()
|
||||||
|
{
|
||||||
|
if (!data_done)
|
||||||
|
return -1;
|
||||||
|
return data_left + (settings_converted ? 1 : 0) + (schedule_converted ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
const char *appSaveFont(const char *name, const uint8_t *data, size_t size)
|
const char *appSaveFont(const char *name, const uint8_t *data, size_t size)
|
||||||
{
|
{
|
||||||
const char *error = saveFont(name, data, size);
|
const char *error = saveFont(name, data, size);
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ void appLoadSettings();
|
|||||||
// restart_needed when the change only works after a restart (Home Assistant MQTT).
|
// restart_needed when the change only works after a restart (Home Assistant MQTT).
|
||||||
const char *appSaveSettings(const Settings &settings, bool &restart_needed);
|
const char *appSaveSettings(const Settings &settings, bool &restart_needed);
|
||||||
|
|
||||||
|
// Slow mode (Home Assistant switch and dropdown): stored and used right away. minutes: one
|
||||||
|
// of SLOW_MODE_MINUTES. Return nullptr or an error message.
|
||||||
|
void appSetSlowMode(bool enabled);
|
||||||
|
const char *appSetSlowModeMinutes(int minutes);
|
||||||
|
|
||||||
// The chosen page (settings): a dynamic page or a bitmap. With a schedule it is shown until
|
// The chosen page (settings): a dynamic page or a bitmap. With a schedule it is shown until
|
||||||
// the next block starts (a manual pick), without one all the time.
|
// the next block starts (a manual pick), without one all the time.
|
||||||
const char *appSelectPage(const char *name);
|
const char *appSelectPage(const char *name);
|
||||||
@@ -63,6 +68,12 @@ const char *appIconToBitmap(const char *name);
|
|||||||
// Once: the uploaded bitmaps that only pages use become icons. Call from loop(); returns at
|
// Once: the uploaded bitmaps that only pages use become icons. Call from loop(); returns at
|
||||||
// once when it is done.
|
// once when it is done.
|
||||||
void appMigrateIcons();
|
void appMigrateIcons();
|
||||||
|
// Once per boot, after the firmware was marked good (the old firmware can't read the new
|
||||||
|
// formats): settings and schedule read in an older layout are stored again, and pages in an
|
||||||
|
// older format are rewritten. Call from loop(); returns at once when it is done.
|
||||||
|
void appMigrateData();
|
||||||
|
// Records still in an older format (-1 until appMigrateData() checked the pages)
|
||||||
|
int appOldDataLeft();
|
||||||
const char *appSavePage(const char *name, const DynamicPage &page);
|
const char *appSavePage(const char *name, const DynamicPage &page);
|
||||||
const char *appDeletePage(const char *name);
|
const char *appDeletePage(const char *name);
|
||||||
const char *appSaveFont(const char *name, const uint8_t *data, size_t size);
|
const char *appSaveFont(const char *name, const uint8_t *data, size_t size);
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ const uint32_t SETUP_AP_DELAY = 20000; // open the AP when WiFi fails thi
|
|||||||
const uint32_t WIFI_CONNECT_TIMEOUT = 30000;
|
const uint32_t WIFI_CONNECT_TIMEOUT = 30000;
|
||||||
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
||||||
|
|
||||||
#define FIRMWARE_VERSION "2.14.0"
|
#define FIRMWARE_VERSION "2.15.0"
|
||||||
|
|
||||||
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
|
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
|
||||||
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||||
|
|||||||
@@ -536,6 +536,23 @@ bool loadPage(const char *name, DynamicPage &page)
|
|||||||
return ok || builtinPage(name, page);
|
return ok || builtinPage(name, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<String> storedPageNames()
|
||||||
|
{
|
||||||
|
return storedNames(DIR, EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pageInOldFormat(const char *name)
|
||||||
|
{
|
||||||
|
if (!storedPage(name))
|
||||||
|
return false;
|
||||||
|
File file = LittleFS.open(path(name), "r");
|
||||||
|
if (!file)
|
||||||
|
return false;
|
||||||
|
bool old = file.size() != sizeof(StoredPage);
|
||||||
|
file.close();
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
|
||||||
const char *savePage(const char *name, const DynamicPage &page)
|
const char *savePage(const char *name, const DynamicPage &page)
|
||||||
{
|
{
|
||||||
if (!validBitmapName(name))
|
if (!validBitmapName(name))
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ bool pageExists(const char *name);
|
|||||||
// A built-in page that no stored page replaces
|
// A built-in page that no stored page replaces
|
||||||
bool builtinPageExists(const char *name);
|
bool builtinPageExists(const char *name);
|
||||||
bool loadPage(const char *name, DynamicPage &page);
|
bool loadPage(const char *name, DynamicPage &page);
|
||||||
|
// The stored pages (slow: lists the directory) and whether one is stored in an older
|
||||||
|
// format (loadPage() converts it, savePage() writes the current one)
|
||||||
|
std::vector<String> storedPageNames();
|
||||||
|
bool pageInOldFormat(const char *name);
|
||||||
|
|
||||||
// Return nullptr on success, otherwise a message for the user. savePage() validates the
|
// Return nullptr on success, otherwise a message for the user. savePage() validates the
|
||||||
// page, but does not check that its bitmaps and fonts exist.
|
// page, but does not check that its bitmaps and fonts exist.
|
||||||
|
|||||||
@@ -7,13 +7,28 @@ DisplayController::DisplayController(DisplayPower &power, Frames &frames) : _pow
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayController::setSettings(const ScheduleSettings &schedule, const ClockSettings &clock)
|
void DisplayController::setSettings(const ScheduleSettings &schedule, const ClockSettings &clock, const SlowModeSettings &slow)
|
||||||
{
|
{
|
||||||
|
if (slow.enabled && !_slow.enabled)
|
||||||
|
_slow_keep = true;
|
||||||
_schedule = schedule;
|
_schedule = schedule;
|
||||||
_clock = clock;
|
_clock = clock;
|
||||||
|
_slow = slow;
|
||||||
_content_revision++; // e.g. the No WiFi setting changed: redraw
|
_content_revision++; // e.g. the No WiFi setting changed: redraw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DisplayController::setSlowModeOverride(bool active)
|
||||||
|
{
|
||||||
|
if (_slow_override && !active)
|
||||||
|
_slow_last = -1;
|
||||||
|
_slow_override = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DisplayController::slowModeActive() const
|
||||||
|
{
|
||||||
|
return _slow.enabled && !_slow_override;
|
||||||
|
}
|
||||||
|
|
||||||
void DisplayController::setEffectTime(uint32_t ms)
|
void DisplayController::setEffectTime(uint32_t ms)
|
||||||
{
|
{
|
||||||
_effect_time = ms;
|
_effect_time = ms;
|
||||||
@@ -40,6 +55,7 @@ void DisplayController::showLive(uint32_t now)
|
|||||||
void DisplayController::stopLive()
|
void DisplayController::stopLive()
|
||||||
{
|
{
|
||||||
_live = false;
|
_live = false;
|
||||||
|
_slow_last = -1;
|
||||||
_content_revision++;
|
_content_revision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +66,8 @@ bool DisplayController::liveActive() const
|
|||||||
|
|
||||||
void DisplayController::setPowerEnabled(bool enabled)
|
void DisplayController::setPowerEnabled(bool enabled)
|
||||||
{
|
{
|
||||||
|
if (enabled && !_power_enabled)
|
||||||
|
_slow_last = -1;
|
||||||
_power_enabled = enabled;
|
_power_enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +127,8 @@ void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &t
|
|||||||
switch (scheduleFor(time.hour, time.minute, _schedule))
|
switch (scheduleFor(time.hour, time.minute, _schedule))
|
||||||
{
|
{
|
||||||
case ScheduleMode::Night:
|
case ScheduleMode::Night:
|
||||||
|
_slow_last = -1; // the morning starts with an update
|
||||||
|
_slow_keep = false;
|
||||||
if (_power.isOn())
|
if (_power.isOn())
|
||||||
{
|
{
|
||||||
_showOnce(Screen::Off);
|
_showOnce(Screen::Off);
|
||||||
@@ -116,6 +136,11 @@ void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &t
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ScheduleMode::Clock:
|
case ScheduleMode::Clock:
|
||||||
|
if (slowModeActive())
|
||||||
|
{
|
||||||
|
_slowClock(now, time, show_no_wifi);
|
||||||
|
break;
|
||||||
|
}
|
||||||
_off_timer.cancel();
|
_off_timer.cancel();
|
||||||
_ensureOn();
|
_ensureOn();
|
||||||
if (_page == Page::Dynamic)
|
if (_page == Page::Dynamic)
|
||||||
@@ -123,14 +148,38 @@ void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &t
|
|||||||
else
|
else
|
||||||
_showOnce(Screen::Page);
|
_showOnce(Screen::Page);
|
||||||
break;
|
break;
|
||||||
case ScheduleMode::Standby:
|
|
||||||
// Flipdots keep their image without power, so the last image stays visible
|
|
||||||
if (_power.isOn())
|
|
||||||
_off_timer.request(now, POWER_OFF_DELAY);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slow mode: one frame in each update minute, then the power goes off. Flipdots keep their
|
||||||
|
// image without power, so it stays visible until the next update.
|
||||||
|
void DisplayController::_slowClock(uint32_t now, const LocalTime &time, bool no_wifi)
|
||||||
|
{
|
||||||
|
int minute = time.hour * 60 + time.minute;
|
||||||
|
if (_slow_keep)
|
||||||
|
{
|
||||||
|
_slow_keep = false;
|
||||||
|
// Not the boot screen or an off screen: that is replaced right away
|
||||||
|
if (_power.isOn() && (_screen == Screen::Dynamic || _screen == Screen::NoWifi || _screen == Screen::Page))
|
||||||
|
_slow_last = minute;
|
||||||
|
}
|
||||||
|
bool due = _slow_last < 0 || (minute != _slow_last && slowModeUpdate(time.hour, time.minute, _slow.minutes));
|
||||||
|
if (due)
|
||||||
|
{
|
||||||
|
_slow_last = minute;
|
||||||
|
_off_timer.cancel();
|
||||||
|
_ensureOn();
|
||||||
|
_screen = Screen::Unknown; // the whole image, also when it looks the same
|
||||||
|
if (_page == Page::Dynamic)
|
||||||
|
_showDynamic(now, time, no_wifi);
|
||||||
|
else
|
||||||
|
_showOnce(Screen::Page);
|
||||||
|
_dynamic_moving = false; // moving pages stand still until the next update
|
||||||
|
}
|
||||||
|
if (_power.isOn())
|
||||||
|
_off_timer.request(now, POWER_OFF_DELAY + _effect_time);
|
||||||
|
}
|
||||||
|
|
||||||
void DisplayController::_ensureOn()
|
void DisplayController::_ensureOn()
|
||||||
{
|
{
|
||||||
if (_power.isOn())
|
if (_power.isOn())
|
||||||
|
|||||||
@@ -49,7 +49,15 @@ class DisplayController
|
|||||||
public:
|
public:
|
||||||
DisplayController(DisplayPower &power, Frames &frames);
|
DisplayController(DisplayPower &power, Frames &frames);
|
||||||
|
|
||||||
void setSettings(const ScheduleSettings &schedule, const ClockSettings &clock);
|
// Slow mode: the display only changes in the minutes of slowModeUpdate() (once, also
|
||||||
|
// when the page changed in between) and is powered off in between. Switching it on keeps
|
||||||
|
// the image until then; switching it off updates right away.
|
||||||
|
void setSettings(const ScheduleSettings &schedule, const ClockSettings &clock, const SlowModeSettings &slow);
|
||||||
|
// A rule that overrides slow mode shows its page: the display works normally meanwhile,
|
||||||
|
// and is updated right away when it ends
|
||||||
|
void setSlowModeOverride(bool active);
|
||||||
|
// Slow mode is switched on and no rule overrides it
|
||||||
|
bool slowModeActive() const;
|
||||||
// How long the Nano takes to show a frame with the refresh effect (ms, 0 = instant).
|
// How long the Nano takes to show a frame with the refresh effect (ms, 0 = instant).
|
||||||
// The power stays on that much longer after the last frame.
|
// The power stays on that much longer after the last frame.
|
||||||
void setEffectTime(uint32_t ms);
|
void setEffectTime(uint32_t ms);
|
||||||
@@ -91,6 +99,10 @@ private:
|
|||||||
|
|
||||||
ScheduleSettings _schedule;
|
ScheduleSettings _schedule;
|
||||||
ClockSettings _clock;
|
ClockSettings _clock;
|
||||||
|
SlowModeSettings _slow;
|
||||||
|
int _slow_last = -1; // slow mode: minute of the day of the last update, -1 = update now
|
||||||
|
bool _slow_keep = false; // slow mode was switched on: the current image counts as updated
|
||||||
|
bool _slow_override = false;
|
||||||
Page _page = Page::Dynamic;
|
Page _page = Page::Dynamic;
|
||||||
uint32_t _effect_time = 0;
|
uint32_t _effect_time = 0;
|
||||||
|
|
||||||
@@ -110,6 +122,7 @@ private:
|
|||||||
bool _wifi_lost = false;
|
bool _wifi_lost = false;
|
||||||
uint32_t _wifi_lost_since = 0;
|
uint32_t _wifi_lost_since = 0;
|
||||||
|
|
||||||
|
void _slowClock(uint32_t now, const LocalTime &time, bool no_wifi);
|
||||||
void _ensureOn();
|
void _ensureOn();
|
||||||
void _showDynamic(uint32_t now, const LocalTime &time, bool no_wifi);
|
void _showDynamic(uint32_t now, const LocalTime &time, bool no_wifi);
|
||||||
void _showOnce(Screen screen);
|
void _showOnce(Screen screen);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ struct TriggerRule
|
|||||||
uint16_t interval = 10; // read the entity this often (s)
|
uint16_t interval = 10; // read the entity this often (s)
|
||||||
uint8_t blocks = 0; // bit i: active during block i; 0: always (also without a schedule)
|
uint8_t blocks = 0; // bit i: active during block i; 0: always (also without a schedule)
|
||||||
bool over_manual = true; // also over a page picked by hand (false: it waits until the pick ends)
|
bool over_manual = true; // also over a page picked by hand (false: it waits until the pick ends)
|
||||||
|
bool over_slow_mode = false; // while its page is shown the display works normally (slow mode waits)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PageSchedule
|
struct PageSchedule
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ PageChoice PageSelector::update(int minute, uint32_t now)
|
|||||||
}
|
}
|
||||||
|
|
||||||
choice.changed = !sameName(choice.page, _shown);
|
choice.changed = !sameName(choice.page, _shown);
|
||||||
|
choice.over_slow_mode = choice.rule >= 0 && choice.rule < rule_count && _schedule->rules[choice.rule].over_slow_mode;
|
||||||
// Rules bring their page at once, and leave at once; the schedule as set per block
|
// Rules bring their page at once, and leave at once; the schedule as set per block
|
||||||
choice.instant = choice.changed && (choice.reason == ShowReason::Trigger || _shown_reason == ShowReason::Trigger ||
|
choice.instant = choice.changed && (choice.reason == ShowReason::Trigger || _shown_reason == ShowReason::Trigger ||
|
||||||
!effect);
|
!effect);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ struct PageChoice
|
|||||||
int rule = -1; // the rule (Rotation, Trigger)
|
int rule = -1; // the rule (Rotation, Trigger)
|
||||||
bool changed = false; // another page than at the last update()
|
bool changed = false; // another page than at the last update()
|
||||||
bool instant = false; // when changed: switch without the refresh effect
|
bool instant = false; // when changed: switch without the refresh effect
|
||||||
|
bool over_slow_mode = false; // shown by a rule that overrides slow mode
|
||||||
};
|
};
|
||||||
|
|
||||||
// The chosen page also rotates with AddToRotation pages, this long
|
// The chosen page also rotates with AddToRotation pages, this long
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &s)
|
|||||||
int now = hour * 60 + minute;
|
int now = hour * 60 + minute;
|
||||||
if (now < s.day_start || now >= s.night_start)
|
if (now < s.day_start || now >= s.night_start)
|
||||||
return ScheduleMode::Night;
|
return ScheduleMode::Night;
|
||||||
if (now < s.late_start || !s.quarter_hours || minute % 15 == 0)
|
|
||||||
return ScheduleMode::Clock;
|
return ScheduleMode::Clock;
|
||||||
return ScheduleMode::Standby;
|
}
|
||||||
|
|
||||||
|
bool slowModeUpdate(int hour, int minute, int interval)
|
||||||
|
{
|
||||||
|
return interval <= 1 || (hour * 60 + minute) % interval == 0;
|
||||||
}
|
}
|
||||||
@@ -6,16 +6,20 @@
|
|||||||
// settings:
|
// settings:
|
||||||
//
|
//
|
||||||
// 00:00 - 07:59 Night screen cleared, power off
|
// 00:00 - 07:59 Night screen cleared, power off
|
||||||
// 08:00 - 21:59 Clock page shown (a dynamic page is updated every minute)
|
// 08:00 - 23:30 Clock page shown (a dynamic page is updated every minute)
|
||||||
// 22:00 - 23:30 Clock only during the minutes :00, :15, :30 and :45
|
|
||||||
// Standby in between: power off, the last image stays on the dots
|
|
||||||
// 23:31 - 23:59 Night
|
// 23:31 - 23:59 Night
|
||||||
|
//
|
||||||
|
// Slow mode (SlowModeSettings, handled by DisplayController) only changes how often the
|
||||||
|
// page is drawn during Clock.
|
||||||
|
|
||||||
enum class ScheduleMode
|
enum class ScheduleMode
|
||||||
{
|
{
|
||||||
Night,
|
Night,
|
||||||
Clock,
|
Clock,
|
||||||
Standby,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &settings);
|
ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &settings);
|
||||||
|
|
||||||
|
// Slow mode: true in the minutes when the display is updated (interval: one of
|
||||||
|
// SLOW_MODE_MINUTES, counted from midnight, so 15 = :00 :15 :30 :45)
|
||||||
|
bool slowModeUpdate(int hour, int minute, int interval);
|
||||||
@@ -20,10 +20,12 @@ const char *validateSettings(const Settings &s)
|
|||||||
return "Unknown off image";
|
return "Unknown off image";
|
||||||
|
|
||||||
const ScheduleSettings &t = s.schedule;
|
const ScheduleSettings &t = s.schedule;
|
||||||
if (t.day_start >= MINUTES_PER_DAY || t.late_start > MINUTES_PER_DAY || t.night_start > MINUTES_PER_DAY)
|
if (t.day_start >= MINUTES_PER_DAY || t.night_start > MINUTES_PER_DAY)
|
||||||
return "Times must be between 00:00 and 24:00";
|
return "Times must be between 00:00 and 24:00";
|
||||||
if (t.day_start > t.late_start || t.late_start > t.night_start)
|
if (t.day_start > t.night_start)
|
||||||
return "The times must be in order: day start, late evening, night";
|
return "The night must start after the day";
|
||||||
|
if (!validSlowModeMinutes(s.slow_mode.minutes))
|
||||||
|
return "Unknown slow mode interval";
|
||||||
|
|
||||||
if (s.flip_time < SETTINGS_FLIP_TIME_MIN || s.flip_time > SETTINGS_FLIP_TIME_MAX)
|
if (s.flip_time < SETTINGS_FLIP_TIME_MIN || s.flip_time > SETTINGS_FLIP_TIME_MAX)
|
||||||
return "The flip time must be between 50 and 2000 us";
|
return "The flip time must be between 50 and 2000 us";
|
||||||
@@ -53,6 +55,14 @@ const char *validateSettings(const Settings &s)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool validSlowModeMinutes(int minutes)
|
||||||
|
{
|
||||||
|
for (uint8_t m : SLOW_MODE_MINUTES)
|
||||||
|
if (m == minutes)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t effectTime(const DriveSettings &drive)
|
uint32_t effectTime(const DriveSettings &drive)
|
||||||
{
|
{
|
||||||
if (drive.effect == DRIVE_EFFECT_INSTANT)
|
if (drive.effect == DRIVE_EFFECT_INSTANT)
|
||||||
|
|||||||
@@ -14,11 +14,21 @@ struct ScheduleSettings
|
|||||||
{
|
{
|
||||||
bool enabled = true; // false: the display is always on
|
bool enabled = true; // false: the display is always on
|
||||||
uint16_t day_start = 8 * 60; // display on from
|
uint16_t day_start = 8 * 60; // display on from
|
||||||
uint16_t late_start = 22 * 60; // from here only at the quarter hours (see quarter_hours)
|
|
||||||
bool quarter_hours = true; // false: on all the time until night_start
|
|
||||||
uint16_t night_start = 23 * 60 + 31; // display off from (up to 24:00 = never)
|
uint16_t night_start = 23 * 60 + 31; // display off from (up to 24:00 = never)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Slow mode (Home Assistant switch "FlipDot Display Slow mode", web UI): the display only
|
||||||
|
// changes every `minutes` minutes, on the clock (15: at :00 :15 :30 :45), and is powered
|
||||||
|
// off in between. Fewer flips, so less noise.
|
||||||
|
const uint8_t SLOW_MODE_MINUTES[] = {1, 2, 5, 10, 15, 20, 30, 60}; // all divide an hour
|
||||||
|
const int SLOW_MODE_MINUTES_COUNT = sizeof(SLOW_MODE_MINUTES) / sizeof(SLOW_MODE_MINUTES[0]);
|
||||||
|
struct SlowModeSettings
|
||||||
|
{
|
||||||
|
bool enabled = false;
|
||||||
|
uint8_t minutes = 15; // one of SLOW_MODE_MINUTES
|
||||||
|
};
|
||||||
|
bool validSlowModeMinutes(int minutes);
|
||||||
|
|
||||||
struct ClockSettings
|
struct ClockSettings
|
||||||
{
|
{
|
||||||
bool no_wifi_screen = true; // "No WiFi" instead of a dynamic page when WiFi is down
|
bool no_wifi_screen = true; // "No WiFi" instead of a dynamic page when WiFi is down
|
||||||
@@ -85,6 +95,7 @@ struct Settings
|
|||||||
RefreshSettings refresh;
|
RefreshSettings refresh;
|
||||||
// Plex user name for {plex_user} in entity ids (Tautulli's sensors of that user)
|
// Plex user name for {plex_user} in entity ids (Tautulli's sensors of that user)
|
||||||
char plex_user[PLEX_USER_SIZE] = "";
|
char plex_user[PLEX_USER_SIZE] = "";
|
||||||
|
SlowModeSettings slow_mode;
|
||||||
// New fields go here, at the end (see system/settings_store.cpp)
|
// New fields go here, at the end (see system/settings_store.cpp)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ void loop()
|
|||||||
appLoop();
|
appLoop();
|
||||||
// Once the firmware can't be rolled back anymore (the old one doesn't know icons)
|
// Once the firmware can't be rolled back anymore (the old one doesn't know icons)
|
||||||
if (!firmwareGuardPending())
|
if (!firmwareGuardPending())
|
||||||
|
{
|
||||||
appMigrateIcons();
|
appMigrateIcons();
|
||||||
|
appMigrateData();
|
||||||
|
}
|
||||||
// Home Assistant values of the shown page (read by a task, never blocks)
|
// Home Assistant values of the shown page (read by a task, never blocks)
|
||||||
if (haApiLoop(wifiConnected()))
|
if (haApiLoop(wifiConnected()))
|
||||||
controller.pageChanged();
|
controller.pageChanged();
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ static HASwitch stopwatchSwitch("FlipDotDisplayStopwatch");
|
|||||||
static HAButton stopwatchReset("FlipDotDisplayStopwatchReset");
|
static HAButton stopwatchReset("FlipDotDisplayStopwatchReset");
|
||||||
static HASensor shownSensor("FlipDotDisplayShownPage");
|
static HASensor shownSensor("FlipDotDisplayShownPage");
|
||||||
static HAButton backButton("FlipDotDisplayBackToSchedule");
|
static HAButton backButton("FlipDotDisplayBackToSchedule");
|
||||||
|
static HASwitch slowSwitch("FlipDotDisplaySlowMode");
|
||||||
|
static HAPageSelect slowSelect("FlipDotDisplaySlowModeInterval");
|
||||||
|
|
||||||
// Removed entities: their retained discovery config is cleared, so Home Assistant
|
// Removed entities: their retained discovery config is cleared, so Home Assistant
|
||||||
// deletes them instead of showing them as unavailable
|
// deletes them instead of showing them as unavailable
|
||||||
@@ -180,6 +182,29 @@ static NameChoice mode = {modeSelect, nullptr, nullptr, DEFAULT_PAGE, pageNames,
|
|||||||
static NameChoice offImage = {offSelect, "Blank", OFF_PAGE_BLANK, "Blank", bitmapNames,
|
static NameChoice offImage = {offSelect, "Blank", OFF_PAGE_BLANK, "Blank", bitmapNames,
|
||||||
[]() { return appSettings().off_page; }, appSelectOffPage};
|
[]() { return appSettings().off_page; }, appSelectOffPage};
|
||||||
|
|
||||||
|
// The slow mode interval as dropdown options ("15 minutes")
|
||||||
|
static void slowOption(int minutes, char *out, size_t size)
|
||||||
|
{
|
||||||
|
snprintf(out, size, minutes == 1 ? "%d minute" : "%d minutes", minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int slow_shown_minutes = -1;
|
||||||
|
|
||||||
|
// Reports slow mode (also when it was changed in the web UI)
|
||||||
|
static void slowModeUpdate()
|
||||||
|
{
|
||||||
|
const SlowModeSettings &slow = appSettings().slow_mode;
|
||||||
|
if (slowSwitch.getCurrentState() != slow.enabled)
|
||||||
|
slowSwitch.setState(slow.enabled);
|
||||||
|
if (slow_shown_minutes != slow.minutes)
|
||||||
|
{
|
||||||
|
slow_shown_minutes = slow.minutes;
|
||||||
|
char option[16];
|
||||||
|
slowOption(slow.minutes, option, sizeof(option));
|
||||||
|
slowSelect.setState(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void clearRemovedEntities()
|
static void clearRemovedEntities()
|
||||||
{
|
{
|
||||||
for (const char *id : REMOVED_SWITCHES)
|
for (const char *id : REMOVED_SWITCHES)
|
||||||
@@ -242,6 +267,34 @@ void homeAssistantBegin(DisplayController &controller, const HomeAssistantSettin
|
|||||||
backButton.setIcon("mdi:calendar-clock");
|
backButton.setIcon("mdi:calendar-clock");
|
||||||
backButton.onCommand([](HAButton *) { appBackToSchedule(); });
|
backButton.onCommand([](HAButton *) { appBackToSchedule(); });
|
||||||
|
|
||||||
|
slowSwitch.setName("FlipDot Display Slow mode");
|
||||||
|
slowSwitch.setIcon("mdi:snail");
|
||||||
|
slowSwitch.setCurrentState(appSettings().slow_mode.enabled);
|
||||||
|
slowSwitch.onCommand([](bool state, HASwitch *sender) {
|
||||||
|
appSetSlowMode(state);
|
||||||
|
sender->setState(appSettings().slow_mode.enabled);
|
||||||
|
});
|
||||||
|
slowSelect.setName("FlipDot Display Slow mode interval");
|
||||||
|
slowSelect.setIcon("mdi:timer-sand");
|
||||||
|
{
|
||||||
|
std::vector<String> options;
|
||||||
|
char option[16];
|
||||||
|
for (uint8_t minutes : SLOW_MODE_MINUTES)
|
||||||
|
{
|
||||||
|
slowOption(minutes, option, sizeof(option));
|
||||||
|
options.push_back(option);
|
||||||
|
}
|
||||||
|
slowSelect.setOptions(options);
|
||||||
|
}
|
||||||
|
slowSelect.onCommand([](const char *option) {
|
||||||
|
const char *error = appSetSlowModeMinutes(atoi(option));
|
||||||
|
if (error)
|
||||||
|
Serial.printf("Home Assistant \"%s\": %s\n", option, error);
|
||||||
|
slow_shown_minutes = -1; // report the (new) state back
|
||||||
|
slowModeUpdate();
|
||||||
|
});
|
||||||
|
slowModeUpdate();
|
||||||
|
|
||||||
firmwareSensor.setName("FlipDot Display Firmware");
|
firmwareSensor.setName("FlipDot Display Firmware");
|
||||||
firmwareSensor.setIcon("mdi:chip");
|
firmwareSensor.setIcon("mdi:chip");
|
||||||
|
|
||||||
@@ -286,6 +339,7 @@ void homeAssistantLoop()
|
|||||||
firmware.loop();
|
firmware.loop();
|
||||||
mode.update();
|
mode.update();
|
||||||
offImage.update();
|
offImage.update();
|
||||||
|
slowModeUpdate();
|
||||||
// Started or stopped in the web UI
|
// Started or stopped in the web UI
|
||||||
if (stopwatchSwitch.getCurrentState() != stopwatch.running())
|
if (stopwatchSwitch.getCurrentState() != stopwatch.running())
|
||||||
stopwatchSwitch.setState(stopwatch.running());
|
stopwatchSwitch.setState(stopwatch.running());
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
// FlipDot Display Stopwatch reset button: back to 0
|
// FlipDot Display Stopwatch reset button: back to 0
|
||||||
// FlipDot Display Shown page sensor: the page or bitmap on the display now
|
// FlipDot Display Shown page sensor: the page or bitmap on the display now
|
||||||
// FlipDot Display Back to schedule button: ends a manual pick (the Mode dropdown)
|
// FlipDot Display Back to schedule button: ends a manual pick (the Mode dropdown)
|
||||||
|
// FlipDot Display Slow mode switch: the display only changes every few minutes
|
||||||
|
// FlipDot Display Slow mode interval select: how often ("15 minutes"; settings.slow_mode)
|
||||||
//
|
//
|
||||||
// The broker comes from the settings (web UI); an empty host means BROKER_ADDR, MQTT_USER
|
// The broker comes from the settings (web UI); an empty host means BROKER_ADDR, MQTT_USER
|
||||||
// and MQTT_PASSWORD from secrets.h. Changes need a restart.
|
// and MQTT_PASSWORD from secrets.h. Changes need a restart.
|
||||||
|
|||||||
@@ -6,10 +6,7 @@
|
|||||||
static const char *NAMESPACE = "flipdot";
|
static const char *NAMESPACE = "flipdot";
|
||||||
static const char *KEY = "schedule";
|
static const char *KEY = "schedule";
|
||||||
// Increase when PageSchedule changes; older records are ignored unless converted below
|
// Increase when PageSchedule changes; older records are ignored unless converted below
|
||||||
static const uint16_t VERSION = 3;
|
static const uint16_t VERSION = 4;
|
||||||
// Version 2: the same layout, but TriggerRule::over_manual was padding (set to true)
|
|
||||||
static const uint16_t VERSION_WITHOUT_OVER_MANUAL = 2;
|
|
||||||
static_assert(sizeof(TriggerRule) == 164, "version 2 records are only read when TriggerRule kept its size");
|
|
||||||
|
|
||||||
struct StoredSchedule
|
struct StoredSchedule
|
||||||
{
|
{
|
||||||
@@ -72,11 +69,74 @@ static void convert(const Schedule &old, PageSchedule &s)
|
|||||||
}
|
}
|
||||||
} // namespace v1
|
} // namespace v1
|
||||||
|
|
||||||
static_assert(sizeof(v1::Stored) != sizeof(StoredSchedule), "the versions are told apart by their size");
|
// Versions 2 and 3: the rules had no over_slow_mode. Version 2 had the same layout as 3, but
|
||||||
|
// over_manual was padding (set to true). Frozen copy, converted.
|
||||||
|
namespace v3
|
||||||
|
{
|
||||||
|
struct Rule
|
||||||
|
{
|
||||||
|
char entity[ELEMENT_ENTITY_SIZE];
|
||||||
|
char attribute[ELEMENT_ATTRIBUTE_SIZE];
|
||||||
|
Compare compare;
|
||||||
|
char value[RULE_VALUE_SIZE];
|
||||||
|
char page[PAGE_NAME_SIZE];
|
||||||
|
TriggerMode mode;
|
||||||
|
uint16_t seconds;
|
||||||
|
uint16_t interval;
|
||||||
|
uint8_t blocks;
|
||||||
|
bool over_manual;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Rule) == 164, "the frozen layout of versions 2 and 3");
|
||||||
|
|
||||||
void loadSchedule(PageSchedule &schedule)
|
struct Schedule
|
||||||
|
{
|
||||||
|
bool enabled;
|
||||||
|
uint8_t block_count;
|
||||||
|
ScheduleBlock blocks[SCHEDULE_BLOCKS];
|
||||||
|
uint8_t rule_count;
|
||||||
|
Rule rules[TRIGGER_RULES];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Stored
|
||||||
|
{
|
||||||
|
uint16_t version;
|
||||||
|
uint16_t size;
|
||||||
|
Schedule schedule;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void convert(const Schedule &old, uint16_t version, PageSchedule &s)
|
||||||
|
{
|
||||||
|
s = PageSchedule();
|
||||||
|
s.enabled = old.enabled;
|
||||||
|
s.block_count = old.block_count;
|
||||||
|
memcpy(s.blocks, old.blocks, sizeof(s.blocks));
|
||||||
|
s.rule_count = old.rule_count;
|
||||||
|
for (int i = 0; i < TRIGGER_RULES; i++)
|
||||||
|
{
|
||||||
|
const Rule &o = old.rules[i];
|
||||||
|
TriggerRule &r = s.rules[i];
|
||||||
|
memcpy(r.entity, o.entity, sizeof(r.entity));
|
||||||
|
memcpy(r.attribute, o.attribute, sizeof(r.attribute));
|
||||||
|
r.compare = o.compare;
|
||||||
|
memcpy(r.value, o.value, sizeof(r.value));
|
||||||
|
memcpy(r.page, o.page, sizeof(r.page));
|
||||||
|
r.mode = o.mode;
|
||||||
|
r.seconds = o.seconds;
|
||||||
|
r.interval = o.interval;
|
||||||
|
r.blocks = o.blocks;
|
||||||
|
r.over_manual = version == 2 ? true : o.over_manual; // over_slow_mode: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace v3
|
||||||
|
|
||||||
|
static_assert(sizeof(v1::Stored) != sizeof(StoredSchedule) && sizeof(v3::Stored) != sizeof(StoredSchedule) &&
|
||||||
|
sizeof(v1::Stored) != sizeof(v3::Stored),
|
||||||
|
"the versions are told apart by their size");
|
||||||
|
|
||||||
|
bool loadSchedule(PageSchedule &schedule)
|
||||||
{
|
{
|
||||||
schedule = PageSchedule();
|
schedule = PageSchedule();
|
||||||
|
bool converted = false;
|
||||||
Preferences prefs;
|
Preferences prefs;
|
||||||
prefs.begin(NAMESPACE, true);
|
prefs.begin(NAMESPACE, true);
|
||||||
size_t length = prefs.isKey(KEY) ? prefs.getBytesLength(KEY) : 0;
|
size_t length = prefs.isKey(KEY) ? prefs.getBytesLength(KEY) : 0;
|
||||||
@@ -84,31 +144,47 @@ void loadSchedule(PageSchedule &schedule)
|
|||||||
{
|
{
|
||||||
StoredSchedule *stored = new StoredSchedule;
|
StoredSchedule *stored = new StoredSchedule;
|
||||||
if (prefs.getBytes(KEY, stored, sizeof(StoredSchedule)) == sizeof(StoredSchedule) &&
|
if (prefs.getBytes(KEY, stored, sizeof(StoredSchedule)) == sizeof(StoredSchedule) &&
|
||||||
(stored->version == VERSION || stored->version == VERSION_WITHOUT_OVER_MANUAL) &&
|
stored->version == VERSION && stored->size == sizeof(PageSchedule) &&
|
||||||
stored->size == sizeof(PageSchedule) && validateSchedule(stored->schedule) == nullptr)
|
validateSchedule(stored->schedule) == nullptr)
|
||||||
{
|
|
||||||
if (stored->version == VERSION_WITHOUT_OVER_MANUAL)
|
|
||||||
for (TriggerRule &r : stored->schedule.rules)
|
|
||||||
r.over_manual = true;
|
|
||||||
schedule = stored->schedule;
|
schedule = stored->schedule;
|
||||||
|
delete stored;
|
||||||
}
|
}
|
||||||
|
else if (length == sizeof(v3::Stored))
|
||||||
|
{
|
||||||
|
v3::Stored *stored = new v3::Stored;
|
||||||
|
PageSchedule *page = new PageSchedule;
|
||||||
|
if (prefs.getBytes(KEY, stored, sizeof(v3::Stored)) == sizeof(v3::Stored) &&
|
||||||
|
(stored->version == 2 || stored->version == 3) && stored->size == sizeof(v3::Schedule))
|
||||||
|
{
|
||||||
|
v3::convert(stored->schedule, stored->version, *page);
|
||||||
|
if (validateSchedule(*page) == nullptr)
|
||||||
|
{
|
||||||
|
schedule = *page;
|
||||||
|
converted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete page;
|
||||||
delete stored;
|
delete stored;
|
||||||
}
|
}
|
||||||
else if (length == sizeof(v1::Stored))
|
else if (length == sizeof(v1::Stored))
|
||||||
{
|
{
|
||||||
v1::Stored *stored = new v1::Stored;
|
v1::Stored *stored = new v1::Stored;
|
||||||
PageSchedule *converted = new PageSchedule;
|
PageSchedule *page = new PageSchedule;
|
||||||
if (prefs.getBytes(KEY, stored, sizeof(v1::Stored)) == sizeof(v1::Stored) && stored->version == 1 &&
|
if (prefs.getBytes(KEY, stored, sizeof(v1::Stored)) == sizeof(v1::Stored) && stored->version == 1 &&
|
||||||
stored->size == sizeof(v1::Schedule))
|
stored->size == sizeof(v1::Schedule))
|
||||||
{
|
{
|
||||||
v1::convert(stored->schedule, *converted);
|
v1::convert(stored->schedule, *page);
|
||||||
if (validateSchedule(*converted) == nullptr)
|
if (validateSchedule(*page) == nullptr)
|
||||||
schedule = *converted;
|
{
|
||||||
|
schedule = *page;
|
||||||
|
converted = true;
|
||||||
}
|
}
|
||||||
delete converted;
|
}
|
||||||
|
delete page;
|
||||||
delete stored;
|
delete stored;
|
||||||
}
|
}
|
||||||
prefs.end();
|
prefs.end();
|
||||||
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveSchedule(const PageSchedule &schedule)
|
void saveSchedule(const PageSchedule &schedule)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "logic/page_schedule.h"
|
#include "logic/page_schedule.h"
|
||||||
|
|
||||||
// The page schedule and trigger rules in NVS (namespace "flipdot", key "schedule"). A
|
// The page schedule and trigger rules in NVS (namespace "flipdot", key "schedule"). A
|
||||||
// missing, outdated or invalid record gives an empty schedule.
|
// missing, unknown or invalid record gives an empty schedule. Returns true when the record
|
||||||
void loadSchedule(PageSchedule &schedule);
|
// had an older layout and was converted (save it again to keep it).
|
||||||
|
bool loadSchedule(PageSchedule &schedule);
|
||||||
void saveSchedule(const PageSchedule &schedule);
|
void saveSchedule(const PageSchedule &schedule);
|
||||||
@@ -9,7 +9,7 @@ static const char *KEY = "settings";
|
|||||||
// Increase when the Settings struct changes; old records are then ignored (defaults),
|
// Increase when the Settings struct changes; old records are then ignored (defaults),
|
||||||
// unless the code below says how to read them. When fields are only added at the end,
|
// unless the code below says how to read them. When fields are only added at the end,
|
||||||
// an old record is the start of the new layout: add an OldVersion for it like v3 does.
|
// an old record is the start of the new layout: add an OldVersion for it like v3 does.
|
||||||
static const uint16_t VERSION = 6;
|
static const uint16_t VERSION = 7;
|
||||||
|
|
||||||
struct OldVersion
|
struct OldVersion
|
||||||
{
|
{
|
||||||
@@ -17,6 +17,59 @@ struct OldVersion
|
|||||||
size_t fields; // bytes of the settings struct that version had
|
size_t fields; // bytes of the settings struct that version had
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Versions 4-6: the layout before slow mode (version 7 removed the late-evening quarter
|
||||||
|
// hours of the schedule and added slow_mode). Frozen copy; versions 4 and 5 lacked refresh
|
||||||
|
// and plex_user (added at the end, so they are a prefix of it).
|
||||||
|
namespace v6
|
||||||
|
{
|
||||||
|
struct ScheduleSettings
|
||||||
|
{
|
||||||
|
bool enabled = true;
|
||||||
|
uint16_t day_start = 8 * 60;
|
||||||
|
uint16_t late_start = 22 * 60;
|
||||||
|
bool quarter_hours = true;
|
||||||
|
uint16_t night_start = 23 * 60 + 31;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Settings
|
||||||
|
{
|
||||||
|
char page[PAGE_NAME_SIZE] = DEFAULT_PAGE;
|
||||||
|
ScheduleSettings schedule;
|
||||||
|
ClockSettings clock;
|
||||||
|
HomeAssistantSettings home_assistant;
|
||||||
|
uint16_t flip_time = 225;
|
||||||
|
bool status_led = true;
|
||||||
|
char off_page[PAGE_NAME_SIZE] = OFF_PAGE_BLANK;
|
||||||
|
DriveSettings drive;
|
||||||
|
RefreshSettings refresh;
|
||||||
|
char plex_user[PLEX_USER_SIZE] = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
static const OldVersion VERSIONS[] = {
|
||||||
|
{4, offsetof(Settings, refresh)},
|
||||||
|
{5, offsetof(Settings, plex_user)},
|
||||||
|
{6, sizeof(Settings)},
|
||||||
|
};
|
||||||
|
|
||||||
|
static void convert(const Settings &old, ::Settings &s)
|
||||||
|
{
|
||||||
|
s = ::Settings();
|
||||||
|
memcpy(s.page, old.page, sizeof(s.page));
|
||||||
|
s.schedule.enabled = old.schedule.enabled;
|
||||||
|
s.schedule.day_start = old.schedule.day_start;
|
||||||
|
s.schedule.night_start = old.schedule.night_start;
|
||||||
|
s.clock = old.clock;
|
||||||
|
s.home_assistant = old.home_assistant;
|
||||||
|
s.flip_time = old.flip_time;
|
||||||
|
s.status_led = old.status_led;
|
||||||
|
memcpy(s.off_page, old.off_page, sizeof(s.off_page));
|
||||||
|
s.drive = old.drive;
|
||||||
|
s.refresh = old.refresh;
|
||||||
|
memcpy(s.plex_user, old.plex_user, sizeof(s.plex_user));
|
||||||
|
// The quarter hours are replaced by slow mode, which starts off
|
||||||
|
}
|
||||||
|
} // namespace v6
|
||||||
|
|
||||||
// Versions 1-3: the layout before the dynamic pages (version 4 removed the evening
|
// Versions 1-3: the layout before the dynamic pages (version 4 removed the evening
|
||||||
// settings of the clock and added the Home Assistant API). Frozen copy, read as a prefix
|
// settings of the clock and added the Home Assistant API). Frozen copy, read as a prefix
|
||||||
// (version 1 lacked off_page and drive, version 2 drive) and converted.
|
// (version 1 lacked off_page and drive, version 2 drive) and converted.
|
||||||
@@ -41,7 +94,7 @@ struct HomeAssistantSettings
|
|||||||
struct Settings
|
struct Settings
|
||||||
{
|
{
|
||||||
char page[25] = "clock";
|
char page[25] = "clock";
|
||||||
ScheduleSettings schedule;
|
v6::ScheduleSettings schedule;
|
||||||
ClockSettings clock;
|
ClockSettings clock;
|
||||||
HomeAssistantSettings home_assistant;
|
HomeAssistantSettings home_assistant;
|
||||||
uint16_t flip_time = 225;
|
uint16_t flip_time = 225;
|
||||||
@@ -56,9 +109,9 @@ static const OldVersion VERSIONS[] = {
|
|||||||
{3, sizeof(Settings)},
|
{3, sizeof(Settings)},
|
||||||
};
|
};
|
||||||
|
|
||||||
static void convert(const Settings &old, ::Settings &s)
|
static void convert(const Settings &old, v6::Settings &s)
|
||||||
{
|
{
|
||||||
s = ::Settings();
|
s = v6::Settings();
|
||||||
memcpy(s.page, old.page, sizeof(s.page));
|
memcpy(s.page, old.page, sizeof(s.page));
|
||||||
s.schedule = old.schedule;
|
s.schedule = old.schedule;
|
||||||
s.clock.no_wifi_screen = old.clock.no_wifi_screen;
|
s.clock.no_wifi_screen = old.clock.no_wifi_screen;
|
||||||
@@ -76,20 +129,19 @@ static void convert(const Settings &old, ::Settings &s)
|
|||||||
}
|
}
|
||||||
} // namespace v3
|
} // namespace v3
|
||||||
|
|
||||||
static_assert(sizeof(v3::Settings) <= sizeof(Settings), "the old record must fit the buffer");
|
static constexpr size_t maxSize(size_t a, size_t b)
|
||||||
|
{
|
||||||
|
return a > b ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
// Versions 4 and 5: the current layout without refresh and plex_user (added at the end, so
|
// Big enough for every layout (an old record can be longer than the current one)
|
||||||
// they are a prefix of it)
|
static const size_t RECORD_SIZE = maxSize(sizeof(Settings), maxSize(sizeof(v6::Settings), sizeof(v3::Settings)));
|
||||||
static const OldVersion PREFIXES[] = {
|
|
||||||
{4, offsetof(Settings, refresh)},
|
|
||||||
{5, offsetof(Settings, plex_user)},
|
|
||||||
};
|
|
||||||
|
|
||||||
struct StoredSettings
|
struct StoredSettings
|
||||||
{
|
{
|
||||||
uint16_t version;
|
uint16_t version;
|
||||||
uint16_t size;
|
uint16_t size;
|
||||||
Settings settings;
|
alignas(4) uint8_t settings[RECORD_SIZE];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reads a record whose first `fields` bytes have the layout of T
|
// Reads a record whose first `fields` bytes have the layout of T
|
||||||
@@ -98,11 +150,11 @@ template <typename T> static bool readOld(const StoredSettings &stored, size_t r
|
|||||||
if (stored.size < fields || stored.size > sizeof(T) || read != offsetof(StoredSettings, settings) + stored.size)
|
if (stored.size < fields || stored.size > sizeof(T) || read != offsetof(StoredSettings, settings) + stored.size)
|
||||||
return false;
|
return false;
|
||||||
out = T();
|
out = T();
|
||||||
memcpy((void *)&out, &stored.settings, fields);
|
memcpy((void *)&out, stored.settings, fields);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadSettings(Settings &settings)
|
bool loadSettings(Settings &settings)
|
||||||
{
|
{
|
||||||
settings = Settings();
|
settings = Settings();
|
||||||
StoredSettings stored;
|
StoredSettings stored;
|
||||||
@@ -113,33 +165,36 @@ void loadSettings(Settings &settings)
|
|||||||
prefs.end();
|
prefs.end();
|
||||||
|
|
||||||
Settings loaded;
|
Settings loaded;
|
||||||
if (read == sizeof(stored) && stored.version == VERSION && stored.size == sizeof(Settings))
|
bool converted = false;
|
||||||
loaded = stored.settings;
|
if (stored.version == VERSION)
|
||||||
else if (stored.version >= 4 && stored.version < VERSION)
|
|
||||||
{
|
{
|
||||||
bool ok = false;
|
if (stored.size != sizeof(Settings) || !readOld(stored, read, sizeof(Settings), loaded))
|
||||||
for (const OldVersion &o : PREFIXES)
|
return false;
|
||||||
if (o.version == stored.version)
|
|
||||||
ok = readOld(stored, read, o.fields, loaded);
|
|
||||||
if (!ok)
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
|
v6::Settings old;
|
||||||
|
for (const OldVersion &o : v6::VERSIONS)
|
||||||
|
if (o.version == stored.version)
|
||||||
|
ok = readOld(stored, read, o.fields, old);
|
||||||
for (const OldVersion &o : v3::VERSIONS)
|
for (const OldVersion &o : v3::VERSIONS)
|
||||||
if (o.version == stored.version)
|
if (o.version == stored.version)
|
||||||
{
|
{
|
||||||
v3::Settings old;
|
v3::Settings older;
|
||||||
ok = readOld(stored, read, o.fields, old);
|
ok = readOld(stored, read, o.fields, older);
|
||||||
if (ok)
|
if (ok)
|
||||||
v3::convert(old, loaded);
|
v3::convert(older, old);
|
||||||
}
|
}
|
||||||
if (!ok)
|
if (!ok)
|
||||||
return;
|
return false;
|
||||||
|
v6::convert(old, loaded);
|
||||||
|
converted = true;
|
||||||
}
|
}
|
||||||
if (validateSettings(loaded) == nullptr)
|
if (validateSettings(loaded) != nullptr)
|
||||||
|
return false;
|
||||||
settings = loaded;
|
settings = loaded;
|
||||||
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveSettings(const Settings &settings)
|
void saveSettings(const Settings &settings)
|
||||||
@@ -147,9 +202,9 @@ void saveSettings(const Settings &settings)
|
|||||||
StoredSettings stored;
|
StoredSettings stored;
|
||||||
stored.version = VERSION;
|
stored.version = VERSION;
|
||||||
stored.size = sizeof(Settings);
|
stored.size = sizeof(Settings);
|
||||||
stored.settings = settings;
|
memcpy(stored.settings, (const void *)&settings, sizeof(Settings));
|
||||||
Preferences prefs;
|
Preferences prefs;
|
||||||
prefs.begin(NAMESPACE, false);
|
prefs.begin(NAMESPACE, false);
|
||||||
prefs.putBytes(KEY, &stored, sizeof(stored));
|
prefs.putBytes(KEY, &stored, offsetof(StoredSettings, settings) + sizeof(Settings));
|
||||||
prefs.end();
|
prefs.end();
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "logic/settings.h"
|
#include "logic/settings.h"
|
||||||
|
|
||||||
// The web UI settings in NVS. A missing or outdated record gives the defaults.
|
// The web UI settings in NVS. A missing or unknown record gives the defaults. Returns true
|
||||||
void loadSettings(Settings &settings);
|
// when the record had an older layout and was converted (save it again to keep it).
|
||||||
|
bool loadSettings(Settings &settings);
|
||||||
void saveSettings(const Settings &settings);
|
void saveSettings(const Settings &settings);
|
||||||
@@ -60,6 +60,7 @@ void scheduleToJson(const PageSchedule &s, JsonObject out)
|
|||||||
o["seconds"] = r.seconds;
|
o["seconds"] = r.seconds;
|
||||||
o["interval"] = r.interval;
|
o["interval"] = r.interval;
|
||||||
o["over_manual"] = r.over_manual;
|
o["over_manual"] = r.over_manual;
|
||||||
|
o["over_slow_mode"] = r.over_slow_mode;
|
||||||
JsonArray in_blocks = o["blocks"].to<JsonArray>();
|
JsonArray in_blocks = o["blocks"].to<JsonArray>();
|
||||||
for (int b = 0; b < s.block_count; b++)
|
for (int b = 0; b < s.block_count; b++)
|
||||||
if ((r.blocks >> b) & 1)
|
if ((r.blocks >> b) & 1)
|
||||||
@@ -113,6 +114,7 @@ const char *scheduleFromJson(JsonVariantConst in, PageSchedule &s)
|
|||||||
r.seconds = constrain(o["seconds"] | 30, 0, 65535);
|
r.seconds = constrain(o["seconds"] | 30, 0, 65535);
|
||||||
r.interval = constrain(o["interval"] | 10, 0, 65535);
|
r.interval = constrain(o["interval"] | 10, 0, 65535);
|
||||||
r.over_manual = o["over_manual"] | true;
|
r.over_manual = o["over_manual"] | true;
|
||||||
|
r.over_slow_mode = o["over_slow_mode"] | false;
|
||||||
// The blocks by their start time (none: always)
|
// The blocks by their start time (none: always)
|
||||||
for (JsonVariantConst start : o["blocks"].as<JsonArrayConst>())
|
for (JsonVariantConst start : o["blocks"].as<JsonArrayConst>())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -150,10 +150,12 @@ details{margin-top:10px}summary{cursor:pointer;color:var(--dim);font-size:13px;m
|
|||||||
<section id="t-settings"><div class="card"><h2>Schedule</h2><div class="form">
|
<section id="t-settings"><div class="card"><h2>Schedule</h2><div class="form">
|
||||||
<label>Schedule</label><label><input type="checkbox" id="s_en"> on (off: display always on)</label>
|
<label>Schedule</label><label><input type="checkbox" id="s_en"> on (off: display always on)</label>
|
||||||
<label>Display on from</label><input type="time" id="s_day">
|
<label>Display on from</label><input type="time" id="s_day">
|
||||||
<label>Late evening from</label><input type="time" id="s_late">
|
|
||||||
<label>Quarter hours</label><label><input type="checkbox" id="s_q"> late evening: only at :00 :15 :30 :45</label>
|
|
||||||
<label>Display off from</label><input type="text" id="s_night" placeholder="HH:MM, 24:00 = never" size="8">
|
<label>Display off from</label><input type="text" id="s_night" placeholder="HH:MM, 24:00 = never" size="8">
|
||||||
</div></div>
|
</div></div>
|
||||||
|
<div class="card"><h2>Slow mode</h2><div class="form">
|
||||||
|
<label>Slow mode</label><label><input type="checkbox" id="s_slow"> on (also the Home Assistant switch)</label>
|
||||||
|
<label>Update every</label><select id="s_slowmin"><option value="1">minute</option><option value="2">2 minutes</option><option value="5">5 minutes</option><option value="10">10 minutes</option><option value="15">15 minutes</option><option value="20">20 minutes</option><option value="30">30 minutes</option><option value="60">hour</option></select>
|
||||||
|
</div><p class="hint">Fewer flips, less noise: the display only changes at these times (on the clock, 15 minutes = :00 :15 :30 :45) and is switched off in between; the dots keep the image. Page changes wait too, except for rules with "show at once in slow mode".</p></div>
|
||||||
<div class="card"><h2>Without WiFi</h2><div class="form">
|
<div class="card"><h2>Without WiFi</h2><div class="form">
|
||||||
<label>No WiFi screen</label><label><input type="checkbox" id="c_nw"> show "No WiFi" instead of a dynamic page when WiFi is down</label>
|
<label>No WiFi screen</label><label><input type="checkbox" id="c_nw"> show "No WiFi" instead of a dynamic page when WiFi is down</label>
|
||||||
</div><p class="hint">The backgrounds per time of day (like the yawning Pikachu) are set per page in the Pages tab.</p></div>
|
</div><p class="hint">The backgrounds per time of day (like the yawning Pikachu) are set per page in the Pages tab.</p></div>
|
||||||
@@ -344,6 +346,8 @@ function schedBuild(){if(!sched)return;schedLink();$('sc_en').checked=sched.enab
|
|||||||
'read every',sinp(ru,'interval','number',{style:'width:5em',min:2,max:600}),'s',state,btn('Remove',()=>{sched.rules.splice(i,1);schedDirty=true;schedBuild()},'d')),
|
'read every',sinp(ru,'interval','number',{style:'width:5em',min:2,max:600}),'s',state,btn('Remove',()=>{sched.rules.splice(i,1);schedDirty=true;schedBuild()},'d')),
|
||||||
mk('div',{className:'row'},(()=>{const c=mk('input',{type:'checkbox',checked:ru.over_manual!==false});c.onchange=()=>{ru.over_manual=c.checked;schedDirty=true};
|
mk('div',{className:'row'},(()=>{const c=mk('input',{type:'checkbox',checked:ru.over_manual!==false});c.onchange=()=>{ru.over_manual=c.checked;schedDirty=true};
|
||||||
return mk('label',{},c,' also over a page chosen by hand')})(),mk('span',{className:'hint',textContent:'(off: it waits until the chosen page ends: the next block or Back to schedule)'})),
|
return mk('label',{},c,' also over a page chosen by hand')})(),mk('span',{className:'hint',textContent:'(off: it waits until the chosen page ends: the next block or Back to schedule)'})),
|
||||||
|
mk('div',{className:'row'},(()=>{const c=mk('input',{type:'checkbox',checked:!!ru.over_slow_mode});c.onchange=()=>{ru.over_slow_mode=c.checked;schedDirty=true};
|
||||||
|
return mk('label',{},c,' show at once in slow mode')})(),mk('span',{className:'hint',textContent:'(the display works normally while this page is shown)'})),
|
||||||
sched.blocks.length?mk('div',{className:'row'},'only in the blocks',...sched.blocks.map(b=>{const c=mk('input',{type:'checkbox',checked:ru._b.includes(b)});
|
sched.blocks.length?mk('div',{className:'row'},'only in the blocks',...sched.blocks.map(b=>{const c=mk('input',{type:'checkbox',checked:ru._b.includes(b)});
|
||||||
c.onchange=()=>{ru._b=c.checked?ru._b.concat([b]):ru._b.filter(x=>x!==b);schedDirty=true};return mk('label',{},c,' '+b.start)}),
|
c.onchange=()=>{ru._b=c.checked?ru._b.concat([b]):ru._b.filter(x=>x!==b);schedDirty=true};return mk('label',{},c,' '+b.start)}),
|
||||||
mk('span',{className:'hint',textContent:'(none ticked: always)'})):''))});
|
mk('span',{className:'hint',textContent:'(none ticked: always)'})):''))});
|
||||||
@@ -351,7 +355,7 @@ function schedBuild(){if(!sched)return;schedLink();$('sc_en').checked=sched.enab
|
|||||||
function scAddBlock(){if(sched.blocks.length>=8){msg('8 blocks at most',true);return}
|
function scAddBlock(){if(sched.blocks.length>=8){msg('8 blocks at most',true);return}
|
||||||
sched.blocks.push({start:'12:00',effect:true,items:[{page:settings.page,seconds:30}]});sched.blocks.sort(bySlot);schedDirty=true;schedBuild()}
|
sched.blocks.push({start:'12:00',effect:true,items:[{page:settings.page,seconds:30}]});sched.blocks.sort(bySlot);schedDirty=true;schedBuild()}
|
||||||
function scAddRule(){if(sched.rules.length>=8){msg('8 rules at most',true);return}
|
function scAddRule(){if(sched.rules.length>=8){msg('8 rules at most',true);return}
|
||||||
sched.rules.push({entity:'',attribute:'',compare:'eq',value:'',page:settings.page,mode:'while',seconds:30,interval:10,over_manual:true});schedDirty=true;schedBuild()}
|
sched.rules.push({entity:'',attribute:'',compare:'eq',value:'',page:settings.page,mode:'while',seconds:30,interval:10,over_manual:true,over_slow_mode:false});schedDirty=true;schedBuild()}
|
||||||
async function scSave(){sched.blocks.sort(bySlot);sched=await post('/api/schedule',schedPayload());schedDirty=false;schedBuild();msg('Schedule saved');loadLists()}
|
async function scSave(){sched.blocks.sort(bySlot);sched=await post('/api/schedule',schedPayload());schedDirty=false;schedBuild();msg('Schedule saved');loadLists()}
|
||||||
async function backToSchedule(){await post('/api/schedule/back');msg('Back to the schedule');setTimeout(pollStatusOnce,500)}
|
async function backToSchedule(){await post('/api/schedule/back');msg('Back to the schedule');setTimeout(pollStatusOnce,500)}
|
||||||
async function pollStatusOnce(){try{status=await api('/api/status');$('sc_now').textContent='Showing: '+status.shown+' ('+status.shown_status+')';$('shownnow').textContent=$('sc_now').textContent}catch(e){}}
|
async function pollStatusOnce(){try{status=await api('/api/status');$('sc_now').textContent='Showing: '+status.shown+' ('+status.shown_status+')';$('shownnow').textContent=$('sc_now').textContent}catch(e){}}
|
||||||
@@ -635,15 +639,17 @@ async function cycleTest(){clearInterval(testTimer);let n=0;const step=()=>live(
|
|||||||
await step();testTimer=setInterval(step,Math.max(4000,(settings.drive.effect?settings.drive.effect_time:0)+2000))}
|
await step();testTimer=setInterval(step,Math.max(4000,(settings.drive.effect?settings.drive.effect_time:0)+2000))}
|
||||||
async function stopTest(){clearInterval(testTimer);testTimer=null;await stopLive()}
|
async function stopTest(){clearInterval(testTimer);testTimer=null;await stopLive()}
|
||||||
// settings
|
// settings
|
||||||
function fill(){const s=settings;$('s_en').checked=s.schedule.enabled;$('s_day').value=s.schedule.day_start;$('s_late').value=s.schedule.late_start;
|
function fill(){const s=settings;$('s_en').checked=s.schedule.enabled;$('s_day').value=s.schedule.day_start;
|
||||||
$('s_q').checked=s.schedule.quarter_hours;$('s_night').value=s.schedule.night_start;
|
$('s_night').value=s.schedule.night_start;$('s_slow').checked=s.slow_mode.enabled;$('s_slowmin').value=s.slow_mode.minutes;
|
||||||
$('c_nw').checked=s.clock.no_wifi_screen;$('h_flip').value=s.flip_time;$('h_led').checked=s.status_led;
|
$('c_nw').checked=s.clock.no_wifi_screen;$('h_flip').value=s.flip_time;$('h_led').checked=s.status_led;
|
||||||
$('h_gap').value=s.drive.flip_gap;$('h_pulses').value=s.drive.pulses;$('h_effect').value=s.drive.effect;$('h_efft').value=s.drive.effect_time;$('h_small').checked=s.drive.effect_small_only;$('h_rdots').value=s.refresh.dots;$('h_rimg').value=s.refresh.image;
|
$('h_gap').value=s.drive.flip_gap;$('h_pulses').value=s.drive.pulses;$('h_effect').value=s.drive.effect;$('h_efft').value=s.drive.effect_time;$('h_small').checked=s.drive.effect_small_only;$('h_rdots').value=s.refresh.dots;$('h_rimg').value=s.refresh.image;
|
||||||
$('ha_en').checked=s.home_assistant.enabled;$('ha_host').value=s.home_assistant.host;$('ha_port').value=s.home_assistant.port;$('ha_user').value=s.home_assistant.user;
|
$('ha_en').checked=s.home_assistant.enabled;$('ha_host').value=s.home_assistant.host;$('ha_port').value=s.home_assistant.port;$('ha_user').value=s.home_assistant.user;
|
||||||
$('ha_pass').placeholder=s.home_assistant.password_set?'unchanged':'none';$('ha_url').value=s.home_assistant.api_url;$('ha_plex').value=s.plex_user||'';
|
$('ha_pass').placeholder=s.home_assistant.password_set?'unchanged':'none';$('ha_url').value=s.home_assistant.api_url;$('ha_plex').value=s.plex_user||'';
|
||||||
$('ha_token').value='';$('ha_token').placeholder=s.home_assistant.token_set?'unchanged':'none'}
|
$('ha_token').value='';$('ha_token').placeholder=s.home_assistant.token_set?'unchanged':'none'}
|
||||||
function collect(){const s=JSON.parse(JSON.stringify(settings));
|
function collect(){const s=JSON.parse(JSON.stringify(settings));
|
||||||
Object.assign(s.schedule,{enabled:$('s_en').checked,day_start:$('s_day').value,late_start:$('s_late').value,quarter_hours:$('s_q').checked,night_start:$('s_night').value.trim()});
|
Object.assign(s.schedule,{enabled:$('s_en').checked,day_start:$('s_day').value,night_start:$('s_night').value.trim()});
|
||||||
|
const sm={enabled:$('s_slow').checked,minutes:+$('s_slowmin').value};
|
||||||
|
if(sm.enabled!=settings.slow_mode.enabled||sm.minutes!=settings.slow_mode.minutes)s.slow_mode=sm;else delete s.slow_mode; // HA may have switched it meanwhile
|
||||||
s.clock={no_wifi_screen:$('c_nw').checked};
|
s.clock={no_wifi_screen:$('c_nw').checked};
|
||||||
s.flip_time=+$('h_flip').value;s.status_led=$('h_led').checked;
|
s.flip_time=+$('h_flip').value;s.status_led=$('h_led').checked;
|
||||||
s.drive={flip_gap:+$('h_gap').value,pulses:+$('h_pulses').value,effect:+$('h_effect').value,effect_time:+$('h_efft').value,effect_small_only:$('h_small').checked};s.refresh={dots:+$('h_rdots').value,image:+$('h_rimg').value};
|
s.drive={flip_gap:+$('h_gap').value,pulses:+$('h_pulses').value,effect:+$('h_effect').value,effect_time:+$('h_efft').value,effect_small_only:$('h_small').checked};s.refresh={dots:+$('h_rdots').value,image:+$('h_rimg').value};
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ static void handleStatus()
|
|||||||
doc["shown_status"] = why;
|
doc["shown_status"] = why;
|
||||||
doc["off_page"] = appSettings().off_page;
|
doc["off_page"] = appSettings().off_page;
|
||||||
doc["power_enabled"] = controller.powerEnabled();
|
doc["power_enabled"] = controller.powerEnabled();
|
||||||
|
doc["slow_mode"] = controller.slowModeActive();
|
||||||
|
doc["old_data_left"] = appOldDataLeft();
|
||||||
JsonObject sw = doc["stopwatch"].to<JsonObject>();
|
JsonObject sw = doc["stopwatch"].to<JsonObject>();
|
||||||
sw["running"] = stopwatch.running();
|
sw["running"] = stopwatch.running();
|
||||||
sw["ms"] = stopwatch.elapsed(millis());
|
sw["ms"] = stopwatch.elapsed(millis());
|
||||||
@@ -687,12 +689,13 @@ static void settingsToJson(const Settings &s, JsonObject o)
|
|||||||
schedule["enabled"] = s.schedule.enabled;
|
schedule["enabled"] = s.schedule.enabled;
|
||||||
formatTime(s.schedule.day_start, time);
|
formatTime(s.schedule.day_start, time);
|
||||||
schedule["day_start"] = String(time);
|
schedule["day_start"] = String(time);
|
||||||
formatTime(s.schedule.late_start, time);
|
|
||||||
schedule["late_start"] = String(time);
|
|
||||||
schedule["quarter_hours"] = s.schedule.quarter_hours;
|
|
||||||
formatTime(s.schedule.night_start, time);
|
formatTime(s.schedule.night_start, time);
|
||||||
schedule["night_start"] = String(time);
|
schedule["night_start"] = String(time);
|
||||||
|
|
||||||
|
JsonObject slow = o["slow_mode"].to<JsonObject>();
|
||||||
|
slow["enabled"] = s.slow_mode.enabled;
|
||||||
|
slow["minutes"] = s.slow_mode.minutes;
|
||||||
|
|
||||||
JsonObject clock = o["clock"].to<JsonObject>();
|
JsonObject clock = o["clock"].to<JsonObject>();
|
||||||
clock["no_wifi_screen"] = s.clock.no_wifi_screen;
|
clock["no_wifi_screen"] = s.clock.no_wifi_screen;
|
||||||
|
|
||||||
@@ -754,10 +757,11 @@ static void handleSettingsPost()
|
|||||||
|
|
||||||
JsonVariantConst schedule = body["schedule"];
|
JsonVariantConst schedule = body["schedule"];
|
||||||
copyBool(schedule["enabled"], s.schedule.enabled);
|
copyBool(schedule["enabled"], s.schedule.enabled);
|
||||||
copyBool(schedule["quarter_hours"], s.schedule.quarter_hours);
|
|
||||||
copyBool(body["clock"]["no_wifi_screen"], s.clock.no_wifi_screen);
|
copyBool(body["clock"]["no_wifi_screen"], s.clock.no_wifi_screen);
|
||||||
if (!copyTime(schedule["day_start"], s.schedule.day_start) || !copyTime(schedule["late_start"], s.schedule.late_start) ||
|
copyBool(body["slow_mode"]["enabled"], s.slow_mode.enabled);
|
||||||
!copyTime(schedule["night_start"], s.schedule.night_start))
|
if (body["slow_mode"]["minutes"].is<int>())
|
||||||
|
s.slow_mode.minutes = constrain(body["slow_mode"]["minutes"].as<int>(), 0, 255);
|
||||||
|
if (!copyTime(schedule["day_start"], s.schedule.day_start) || !copyTime(schedule["night_start"], s.schedule.night_start))
|
||||||
{
|
{
|
||||||
sendError(400, "Times must look like 08:30 (00:00 - 24:00)");
|
sendError(400, "Times must look like 08:30 (00:00 - 24:00)");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -135,18 +135,27 @@ void test_schedule_clock_from_8_to_22()
|
|||||||
TEST_ASSERT_TRUE(scheduleFor(21, 59, defaults.schedule) == ScheduleMode::Clock);
|
TEST_ASSERT_TRUE(scheduleFor(21, 59, defaults.schedule) == ScheduleMode::Clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_schedule_quarter_hours_until_23_30()
|
void test_schedule_clock_until_23_30()
|
||||||
{
|
{
|
||||||
TEST_ASSERT_TRUE(scheduleFor(22, 0, defaults.schedule) == ScheduleMode::Clock);
|
TEST_ASSERT_TRUE(scheduleFor(22, 1, defaults.schedule) == ScheduleMode::Clock);
|
||||||
TEST_ASSERT_TRUE(scheduleFor(22, 1, defaults.schedule) == ScheduleMode::Standby);
|
TEST_ASSERT_TRUE(scheduleFor(23, 29, defaults.schedule) == ScheduleMode::Clock);
|
||||||
TEST_ASSERT_TRUE(scheduleFor(22, 14, defaults.schedule) == ScheduleMode::Standby);
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(22, 15, defaults.schedule) == ScheduleMode::Clock);
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(22, 45, defaults.schedule) == ScheduleMode::Clock);
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(23, 0, defaults.schedule) == ScheduleMode::Clock);
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(23, 29, defaults.schedule) == ScheduleMode::Standby);
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(23, 30, defaults.schedule) == ScheduleMode::Clock);
|
TEST_ASSERT_TRUE(scheduleFor(23, 30, defaults.schedule) == ScheduleMode::Clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_update_minutes()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(slowModeUpdate(22, 0, 15));
|
||||||
|
TEST_ASSERT_FALSE(slowModeUpdate(22, 1, 15));
|
||||||
|
TEST_ASSERT_FALSE(slowModeUpdate(22, 14, 15));
|
||||||
|
TEST_ASSERT_TRUE(slowModeUpdate(22, 15, 15));
|
||||||
|
TEST_ASSERT_TRUE(slowModeUpdate(22, 45, 15));
|
||||||
|
TEST_ASSERT_TRUE(slowModeUpdate(12, 7, 1));
|
||||||
|
TEST_ASSERT_TRUE(slowModeUpdate(12, 40, 20));
|
||||||
|
TEST_ASSERT_FALSE(slowModeUpdate(12, 30, 20));
|
||||||
|
TEST_ASSERT_TRUE(slowModeUpdate(13, 0, 60));
|
||||||
|
TEST_ASSERT_FALSE(slowModeUpdate(13, 30, 60));
|
||||||
|
}
|
||||||
|
|
||||||
void test_schedule_night_from_23_31()
|
void test_schedule_night_from_23_31()
|
||||||
{
|
{
|
||||||
TEST_ASSERT_TRUE(scheduleFor(23, 31, defaults.schedule) == ScheduleMode::Night);
|
TEST_ASSERT_TRUE(scheduleFor(23, 31, defaults.schedule) == ScheduleMode::Night);
|
||||||
@@ -387,36 +396,165 @@ void test_night_does_not_power_on()
|
|||||||
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_standby_keeps_the_last_time_and_powers_off()
|
static void slowMode(bool enabled, uint8_t minutes = 15)
|
||||||
{
|
{
|
||||||
|
SlowModeSettings slow;
|
||||||
|
slow.enabled = enabled;
|
||||||
|
slow.minutes = minutes;
|
||||||
|
controller->setSettings(defaults.schedule, defaults.clock, slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_keeps_the_last_time_and_powers_off()
|
||||||
|
{
|
||||||
|
slowMode(true);
|
||||||
run(22, 0);
|
run(22, 0);
|
||||||
run(22, 1, POWER_OFF_DELAY + 200);
|
run(22, 1, POWER_OFF_DELAY + 200);
|
||||||
TEST_ASSERT_FALSE(power.is_on);
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
TEST_ASSERT_EQUAL_STRING("clock 22:00", frames.last().c_str()); // no off screen
|
TEST_ASSERT_EQUAL_STRING("clock 22:00", frames.last().c_str()); // no off screen
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_quarter_hour_powers_on_again_and_redraws()
|
void test_slow_mode_powers_on_again_and_redraws()
|
||||||
{
|
{
|
||||||
|
slowMode(true);
|
||||||
|
run(22, 0);
|
||||||
run(22, 1, POWER_OFF_DELAY + 200);
|
run(22, 1, POWER_OFF_DELAY + 200);
|
||||||
run(22, 14);
|
run(22, 14);
|
||||||
TEST_ASSERT_FALSE(power.is_on);
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
run(22, 15);
|
run(22, 15);
|
||||||
TEST_ASSERT_TRUE(power.is_on);
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
TEST_ASSERT_EQUAL_STRING("clock 22:15", frames.last().c_str());
|
TEST_ASSERT_EQUAL_STRING("clock 22:15", frames.last().c_str());
|
||||||
|
TEST_ASSERT_TRUE(frames.last_redraw);
|
||||||
|
run(22, 15, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_other_intervals()
|
||||||
|
{
|
||||||
|
slowMode(true, 5);
|
||||||
|
run(12, 0);
|
||||||
|
run(12, 4, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
||||||
|
run(12, 5);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:05", frames.last().c_str());
|
||||||
|
slowMode(false);
|
||||||
|
slowMode(true, 1); // switched on again: keeps 12:05 until the next minute
|
||||||
|
run(12, 5, 1000);
|
||||||
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
||||||
|
run(12, 6);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:06", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_switched_on_keeps_the_image()
|
||||||
|
{
|
||||||
|
run(12, 3);
|
||||||
|
slowMode(true);
|
||||||
|
run(12, 4, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:03", frames.last().c_str());
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
||||||
|
run(12, 15);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:15", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_after_boot_updates_at_once()
|
||||||
|
{
|
||||||
|
slowMode(true); // stored setting, before the first update
|
||||||
|
run(12, 7);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:07", frames.last().c_str());
|
||||||
|
run(12, 8, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_page_changes_wait()
|
||||||
|
{
|
||||||
|
slowMode(true);
|
||||||
|
run(12, 0, POWER_OFF_DELAY + 200);
|
||||||
|
controller->setPage(Page::Bitmap);
|
||||||
|
run(12, 5, 1000);
|
||||||
|
controller->pageChanged();
|
||||||
|
run(12, 6, 1000);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
||||||
|
run(12, 15);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("page", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_freezes_moving_pages()
|
||||||
|
{
|
||||||
|
frames.wait = 1000; // a scrolling page
|
||||||
|
slowMode(true);
|
||||||
|
run(12, 0, 20000);
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_switched_off_updates_at_once()
|
||||||
|
{
|
||||||
|
slowMode(true);
|
||||||
|
run(12, 0, POWER_OFF_DELAY + 200);
|
||||||
|
run(12, 7);
|
||||||
|
slowMode(false);
|
||||||
|
run(12, 7);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:07", frames.last().c_str());
|
||||||
|
run(12, 8, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on); // normal again
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:08", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_power_switch_on_updates_at_once()
|
||||||
|
{
|
||||||
|
slowMode(true);
|
||||||
|
run(12, 0);
|
||||||
|
controller->setPowerEnabled(false);
|
||||||
|
run(12, 1, CLEAR_BEFORE_OFF_TIME + 200);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str());
|
||||||
|
controller->setPowerEnabled(true);
|
||||||
|
run(12, 2);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:02", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_slow_mode_override_by_a_rule()
|
||||||
|
{
|
||||||
|
slowMode(true);
|
||||||
|
run(12, 0, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_TRUE(controller->slowModeActive());
|
||||||
|
controller->setSlowModeOverride(true);
|
||||||
|
controller->setPage(Page::Bitmap); // the rule's page
|
||||||
|
run(12, 7);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("page", frames.last().c_str());
|
||||||
|
TEST_ASSERT_FALSE(controller->slowModeActive());
|
||||||
|
run(12, 8, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on); // works normally while the rule shows its page
|
||||||
|
|
||||||
|
controller->setSlowModeOverride(false);
|
||||||
|
controller->setPage(Page::Dynamic);
|
||||||
|
run(12, 9);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:09", frames.last().c_str()); // back at once
|
||||||
|
run(12, 10, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on); // then slow again
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:09", frames.last().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_clock_cancels_pending_power_off()
|
void test_clock_cancels_pending_power_off()
|
||||||
{
|
{
|
||||||
|
slowMode(true);
|
||||||
run(22, 0);
|
run(22, 0);
|
||||||
run(22, 14, 10000); // power off countdown running
|
run(22, 1, 10000); // power off countdown running
|
||||||
TEST_ASSERT_TRUE(power.is_on);
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
run(22, 15, POWER_OFF_DELAY);
|
slowMode(false);
|
||||||
|
run(22, 1, POWER_OFF_DELAY);
|
||||||
TEST_ASSERT_TRUE(power.is_on);
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
TEST_ASSERT_EQUAL(0, power.off_count);
|
TEST_ASSERT_EQUAL(0, power.off_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_power_switch_off_clears_and_powers_off_quickly()
|
void test_power_switch_off_clears_and_powers_off_quickly()
|
||||||
{
|
{
|
||||||
|
slowMode(true);
|
||||||
run(12, 0);
|
run(12, 0);
|
||||||
run(22, 14, 1000); // 30 s countdown already running
|
run(22, 14, 1000); // 30 s countdown already running
|
||||||
controller->setPowerEnabled(false);
|
controller->setPowerEnabled(false);
|
||||||
@@ -528,10 +666,7 @@ void test_default_settings_are_valid()
|
|||||||
void test_settings_times_must_be_in_order()
|
void test_settings_times_must_be_in_order()
|
||||||
{
|
{
|
||||||
Settings s;
|
Settings s;
|
||||||
s.schedule.late_start = 7 * 60; // before day_start
|
s.schedule.night_start = 7 * 60; // before day_start
|
||||||
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
|
||||||
s = Settings();
|
|
||||||
s.schedule.night_start = 21 * 60; // before late_start
|
|
||||||
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
||||||
s = Settings();
|
s = Settings();
|
||||||
s.schedule.night_start = MINUTES_PER_DAY; // never night: fine
|
s.schedule.night_start = MINUTES_PER_DAY; // never night: fine
|
||||||
@@ -540,6 +675,22 @@ void test_settings_times_must_be_in_order()
|
|||||||
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_settings_slow_mode_interval()
|
||||||
|
{
|
||||||
|
Settings s;
|
||||||
|
for (uint8_t m : SLOW_MODE_MINUTES)
|
||||||
|
{
|
||||||
|
s.slow_mode.minutes = m;
|
||||||
|
TEST_ASSERT_NULL(validateSettings(s));
|
||||||
|
}
|
||||||
|
s.slow_mode.minutes = 7;
|
||||||
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
||||||
|
s.slow_mode.minutes = 0;
|
||||||
|
TEST_ASSERT_NOT_NULL(validateSettings(s));
|
||||||
|
TEST_ASSERT_FALSE(Settings().slow_mode.enabled);
|
||||||
|
TEST_ASSERT_EQUAL(15, Settings().slow_mode.minutes);
|
||||||
|
}
|
||||||
|
|
||||||
void test_settings_drive_ranges()
|
void test_settings_drive_ranges()
|
||||||
{
|
{
|
||||||
Settings s;
|
Settings s;
|
||||||
@@ -713,23 +864,13 @@ void test_schedule_custom_times()
|
|||||||
{
|
{
|
||||||
ScheduleSettings s;
|
ScheduleSettings s;
|
||||||
s.day_start = 6 * 60 + 30;
|
s.day_start = 6 * 60 + 30;
|
||||||
s.late_start = 20 * 60;
|
|
||||||
s.night_start = 21 * 60;
|
s.night_start = 21 * 60;
|
||||||
TEST_ASSERT_TRUE(scheduleFor(6, 29, s) == ScheduleMode::Night);
|
TEST_ASSERT_TRUE(scheduleFor(6, 29, s) == ScheduleMode::Night);
|
||||||
TEST_ASSERT_TRUE(scheduleFor(6, 30, s) == ScheduleMode::Clock);
|
TEST_ASSERT_TRUE(scheduleFor(6, 30, s) == ScheduleMode::Clock);
|
||||||
TEST_ASSERT_TRUE(scheduleFor(20, 1, s) == ScheduleMode::Standby);
|
TEST_ASSERT_TRUE(scheduleFor(20, 1, s) == ScheduleMode::Clock);
|
||||||
TEST_ASSERT_TRUE(scheduleFor(20, 30, s) == ScheduleMode::Clock);
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(21, 0, s) == ScheduleMode::Night);
|
TEST_ASSERT_TRUE(scheduleFor(21, 0, s) == ScheduleMode::Night);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_schedule_without_quarter_hours_stays_on_until_night()
|
|
||||||
{
|
|
||||||
ScheduleSettings s;
|
|
||||||
s.quarter_hours = false;
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(22, 7, s) == ScheduleMode::Clock);
|
|
||||||
TEST_ASSERT_TRUE(scheduleFor(23, 31, s) == ScheduleMode::Night);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Pages and live mode
|
// Pages and live mode
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -783,7 +924,7 @@ void test_settings_change_redraws()
|
|||||||
run(18, 0);
|
run(18, 0);
|
||||||
ClockSettings c;
|
ClockSettings c;
|
||||||
c.no_wifi_screen = false;
|
c.no_wifi_screen = false;
|
||||||
controller->setSettings(defaults.schedule, c);
|
controller->setSettings(defaults.schedule, c, defaults.slow_mode);
|
||||||
run(18, 0);
|
run(18, 0);
|
||||||
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
||||||
}
|
}
|
||||||
@@ -1847,6 +1988,29 @@ void test_selector_take_over_rules()
|
|||||||
TEST_ASSERT_TRUE(c.instant); // a rule's page leaves at once
|
TEST_ASSERT_TRUE(c.instant); // a rule's page leaves at once
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_selector_rule_over_slow_mode()
|
||||||
|
{
|
||||||
|
PageSchedule s = twoBlocks();
|
||||||
|
s.rule_count = 2;
|
||||||
|
s.rules[0] = rule("Deurbel", TriggerMode::WhileTrue);
|
||||||
|
s.rules[0].over_slow_mode = true;
|
||||||
|
s.rules[1] = rule("3D printer", TriggerMode::WhileTrue);
|
||||||
|
PageSelector sel;
|
||||||
|
sel.setSchedule(&s);
|
||||||
|
TEST_ASSERT_FALSE(sel.update(8 * 60, 0).over_slow_mode);
|
||||||
|
sel.setRuleState(1, true, 0);
|
||||||
|
PageChoice c = sel.update(8 * 60, 0);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("3D printer", c.page);
|
||||||
|
TEST_ASSERT_FALSE(c.over_slow_mode);
|
||||||
|
sel.setRuleState(0, true, 0);
|
||||||
|
c = sel.update(8 * 60, 0);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Deurbel", c.page);
|
||||||
|
TEST_ASSERT_TRUE(c.over_slow_mode);
|
||||||
|
sel.setRuleState(0, false, 0);
|
||||||
|
TEST_ASSERT_FALSE(sel.update(8 * 60, 0).over_slow_mode);
|
||||||
|
TEST_ASSERT_FALSE(TriggerRule().over_slow_mode);
|
||||||
|
}
|
||||||
|
|
||||||
void test_selector_rule_can_leave_a_manual_pick_alone()
|
void test_selector_rule_can_leave_a_manual_pick_alone()
|
||||||
{
|
{
|
||||||
PageSchedule s = twoBlocks(); // 07:00 Weer + Datum, 17:00 Energie
|
PageSchedule s = twoBlocks(); // 07:00 Weer + Datum, 17:00 Energie
|
||||||
@@ -2400,7 +2564,8 @@ int main()
|
|||||||
|
|
||||||
RUN_TEST(test_schedule_night_until_8);
|
RUN_TEST(test_schedule_night_until_8);
|
||||||
RUN_TEST(test_schedule_clock_from_8_to_22);
|
RUN_TEST(test_schedule_clock_from_8_to_22);
|
||||||
RUN_TEST(test_schedule_quarter_hours_until_23_30);
|
RUN_TEST(test_schedule_clock_until_23_30);
|
||||||
|
RUN_TEST(test_slow_mode_update_minutes);
|
||||||
RUN_TEST(test_schedule_night_from_23_31);
|
RUN_TEST(test_schedule_night_from_23_31);
|
||||||
|
|
||||||
RUN_TEST(test_timer_expires_once_after_delay);
|
RUN_TEST(test_timer_expires_once_after_delay);
|
||||||
@@ -2432,8 +2597,16 @@ int main()
|
|||||||
RUN_TEST(test_clock_is_only_sent_when_the_minute_changes);
|
RUN_TEST(test_clock_is_only_sent_when_the_minute_changes);
|
||||||
RUN_TEST(test_night_clears_then_powers_off_after_delay);
|
RUN_TEST(test_night_clears_then_powers_off_after_delay);
|
||||||
RUN_TEST(test_night_does_not_power_on);
|
RUN_TEST(test_night_does_not_power_on);
|
||||||
RUN_TEST(test_standby_keeps_the_last_time_and_powers_off);
|
RUN_TEST(test_slow_mode_keeps_the_last_time_and_powers_off);
|
||||||
RUN_TEST(test_quarter_hour_powers_on_again_and_redraws);
|
RUN_TEST(test_slow_mode_powers_on_again_and_redraws);
|
||||||
|
RUN_TEST(test_slow_mode_other_intervals);
|
||||||
|
RUN_TEST(test_slow_mode_switched_on_keeps_the_image);
|
||||||
|
RUN_TEST(test_slow_mode_after_boot_updates_at_once);
|
||||||
|
RUN_TEST(test_slow_mode_page_changes_wait);
|
||||||
|
RUN_TEST(test_slow_mode_freezes_moving_pages);
|
||||||
|
RUN_TEST(test_slow_mode_switched_off_updates_at_once);
|
||||||
|
RUN_TEST(test_slow_mode_power_switch_on_updates_at_once);
|
||||||
|
RUN_TEST(test_slow_mode_override_by_a_rule);
|
||||||
RUN_TEST(test_clock_cancels_pending_power_off);
|
RUN_TEST(test_clock_cancels_pending_power_off);
|
||||||
RUN_TEST(test_power_switch_off_clears_and_powers_off_quickly);
|
RUN_TEST(test_power_switch_off_clears_and_powers_off_quickly);
|
||||||
RUN_TEST(test_power_switch_off_waits_for_the_refresh_effect);
|
RUN_TEST(test_power_switch_off_waits_for_the_refresh_effect);
|
||||||
@@ -2448,6 +2621,7 @@ int main()
|
|||||||
|
|
||||||
RUN_TEST(test_default_settings_are_valid);
|
RUN_TEST(test_default_settings_are_valid);
|
||||||
RUN_TEST(test_settings_times_must_be_in_order);
|
RUN_TEST(test_settings_times_must_be_in_order);
|
||||||
|
RUN_TEST(test_settings_slow_mode_interval);
|
||||||
RUN_TEST(test_settings_drive_ranges);
|
RUN_TEST(test_settings_drive_ranges);
|
||||||
RUN_TEST(test_effect_time_is_zero_when_instant);
|
RUN_TEST(test_effect_time_is_zero_when_instant);
|
||||||
RUN_TEST(test_settings_off_page_blank_or_bitmap_name);
|
RUN_TEST(test_settings_off_page_blank_or_bitmap_name);
|
||||||
@@ -2461,7 +2635,6 @@ int main()
|
|||||||
RUN_TEST(test_bitmap_names);
|
RUN_TEST(test_bitmap_names);
|
||||||
RUN_TEST(test_schedule_disabled_is_always_on);
|
RUN_TEST(test_schedule_disabled_is_always_on);
|
||||||
RUN_TEST(test_schedule_custom_times);
|
RUN_TEST(test_schedule_custom_times);
|
||||||
RUN_TEST(test_schedule_without_quarter_hours_stays_on_until_night);
|
|
||||||
RUN_TEST(test_bitmap_page_is_shown_once);
|
RUN_TEST(test_bitmap_page_is_shown_once);
|
||||||
RUN_TEST(test_bitmap_page_redrawn_when_changed);
|
RUN_TEST(test_bitmap_page_redrawn_when_changed);
|
||||||
RUN_TEST(test_switching_back_to_the_clock_page);
|
RUN_TEST(test_switching_back_to_the_clock_page);
|
||||||
@@ -2514,6 +2687,7 @@ int main()
|
|||||||
RUN_TEST(test_selector_one_block_starts_again_each_day);
|
RUN_TEST(test_selector_one_block_starts_again_each_day);
|
||||||
RUN_TEST(test_selector_take_over_rules);
|
RUN_TEST(test_selector_take_over_rules);
|
||||||
RUN_TEST(test_selector_rules_only_in_their_blocks);
|
RUN_TEST(test_selector_rules_only_in_their_blocks);
|
||||||
|
RUN_TEST(test_selector_rule_over_slow_mode);
|
||||||
RUN_TEST(test_selector_rule_can_leave_a_manual_pick_alone);
|
RUN_TEST(test_selector_rule_can_leave_a_manual_pick_alone);
|
||||||
RUN_TEST(test_selector_add_to_rotation);
|
RUN_TEST(test_selector_add_to_rotation);
|
||||||
RUN_TEST(test_ha_auto_unit_and_absolute);
|
RUN_TEST(test_ha_auto_unit_and_absolute);
|
||||||
|
|||||||