Schedule pages and rotation rules can be skipped in slow mode

Each page in a schedule block, and each rule that adds its page to the
rotation, has a "skip in slow mode" checkbox. While slow mode is on those
pages are left out of the rotation; when none of the block's pages are
left, the chosen page is shown. The rotation keeps its own timers.

Schedule format version 5; version 4 is converted when read and stored
again in version 5 on the next save.

Version 2.18.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-20 21:14:14 +02:00
1 parent a91d2550d7
commit e9f3c16c78
13 files changed
+177 -13

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 177 KiB

+2 -1
View File
@@ -26,7 +26,8 @@ The ESP32 connects to Home Assistant in two ways:
- `FlipDot Display Slow mode` switch and `FlipDot Display Slow mode interval` dropdown - `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 (`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 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. with "show at once in slow mode" still show their page right away; pages with "skip in
slow mode" are left out of the rotation.
- `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.
+2
View File
@@ -26,6 +26,8 @@
:00, :15, :30 and :45). 30 seconds after each update the relay turns the display off; :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 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. with "show at once in slow mode": while their page is shown, the display works normally.
Schedule pages and rotation rules with "skip in slow mode" are left out of the rotation
(none left in the block: the chosen page).
- **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
+5 -1
View File
@@ -70,7 +70,11 @@ interval (2-600 s) and can be limited to some of the blocks. The first rule that
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"). A rule 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 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. [slow mode](#settings), and the display works normally while that page is shown.
Pages in a block, and rules that add their page to the rotation, can be left out while slow
mode is on ("skip in slow mode"). When that leaves none of a block's pages, the chosen page
is shown instead. The display schedule ([Settings](#settings)) still decides when the display is on.
![Schedule](../Pictures/web-ui/schedule.png) ![Schedule](../Pictures/web-ui/schedule.png)
+1
View File
@@ -277,6 +277,7 @@ void appPageLoop(bool time_valid, const LocalTime &time)
time_known = time_valid; time_known = time_valid;
if (!time_valid) if (!time_valid)
return; return;
selector.setSlowMode(settings.slow_mode.enabled);
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); 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))
+1 -1
View File
@@ -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.17.0" #define FIRMWARE_VERSION "2.18.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};
+2
View File
@@ -25,6 +25,7 @@ struct ScheduleItem
{ {
char page[PAGE_NAME_SIZE] = ""; char page[PAGE_NAME_SIZE] = "";
uint16_t seconds = 30; uint16_t seconds = 30;
bool skip_slow_mode = false; // left out of the rotation while slow mode is on
}; };
// From `start` (minutes since midnight) until the next block // From `start` (minutes since midnight) until the next block
@@ -66,6 +67,7 @@ struct TriggerRule
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) bool over_slow_mode = false; // while its page is shown the display works normally (slow mode waits)
bool skip_slow_mode = false; // AddToRotation: left out of the rotation while slow mode is on
}; };
struct PageSchedule struct PageSchedule
+4 -3
View File
@@ -131,15 +131,16 @@ PageChoice PageSelector::update(int minute, uint32_t now)
const ScheduleBlock &b = _schedule->blocks[_block]; const ScheduleBlock &b = _schedule->blocks[_block];
effect = b.effect; effect = b.effect;
for (int i = 0; i < b.item_count; i++) for (int i = 0; i < b.item_count; i++)
items[count++] = {b.items[i].page, b.items[i].seconds * 1000UL, ShowReason::Schedule, -1}; if (!_slow || !b.items[i].skip_slow_mode)
items[count++] = {b.items[i].page, b.items[i].seconds * 1000UL, ShowReason::Schedule, -1};
} }
else if (count == 0) // no schedule, a manual pick, or slow mode skips all the block's pages
items[count++] = {_chosen, CHOSEN_PAGE_SECONDS * 1000UL, ShowReason::Chosen, -1}; items[count++] = {_chosen, CHOSEN_PAGE_SECONDS * 1000UL, ShowReason::Chosen, -1};
for (int i = 0; i < rule_count; i++) for (int i = 0; i < rule_count; i++)
{ {
const TriggerRule &r = _schedule->rules[i]; const TriggerRule &r = _schedule->rules[i];
if (r.mode == TriggerMode::AddToRotation && _rule_active[i] && ruleInBlock(r, _block) && if (r.mode == TriggerMode::AddToRotation && _rule_active[i] && ruleInBlock(r, _block) &&
(r.over_manual || !_picked)) (r.over_manual || !_picked) && (!_slow || !r.skip_slow_mode))
items[count++] = {r.page, r.seconds * 1000UL, ShowReason::Rotation, i}; items[count++] = {r.page, r.seconds * 1000UL, ShowReason::Rotation, i};
} }
+6
View File
@@ -12,6 +12,8 @@
// 3. the schedule: the pages of the current block rotate, plus the pages of active // 3. the schedule: the pages of the current block rotate, plus the pages of active
// AddToRotation rules // AddToRotation rules
// 4. without a schedule (none, or switched off): the chosen page, plus AddToRotation pages // 4. without a schedule (none, or switched off): the chosen page, plus AddToRotation pages
// In slow mode the items and AddToRotation rules with skip_slow_mode are left out of the
// rotation; when that leaves none of the block's pages, the chosen page is shown instead.
// Pure logic, tested in test/test_esp32. Times are millis(). // Pure logic, tested in test/test_esp32. Times are millis().
enum class ShowReason : uint8_t enum class ShowReason : uint8_t
@@ -53,6 +55,9 @@ public:
// Start of the block that ends a manual pick (minutes since midnight), -1 when none // Start of the block that ends a manual pick (minutes since midnight), -1 when none
int pickedUntil() const; int pickedUntil() const;
// Slow mode is switched on (SlowModeSettings::enabled): leave out the skip_slow_mode pages
void setSlowMode(bool on) { _slow = on; }
// Whether a rule's condition is true now (from Home Assistant) // Whether a rule's condition is true now (from Home Assistant)
void setRuleState(int rule, bool active, uint32_t now); void setRuleState(int rule, bool active, uint32_t now);
bool ruleActive(int rule) const; bool ruleActive(int rule) const;
@@ -63,6 +68,7 @@ private:
const PageSchedule *_schedule = nullptr; const PageSchedule *_schedule = nullptr;
char _chosen[PAGE_NAME_SIZE] = ""; char _chosen[PAGE_NAME_SIZE] = "";
bool _picked = false; bool _picked = false;
bool _slow = false;
int _block = -1; int _block = -1;
int _last_minute = -1; int _last_minute = -1;
int _item = 0; int _item = 0;
+107 -3
View File
@@ -6,9 +6,9 @@
static const char *NAMESPACE = "flipdot"; static const char *NAMESPACE = "flipdot";
static const char *KEY = "schedule"; static const char *KEY = "schedule";
// Increase when PageSchedule changes; records of other versions are ignored (an empty // Increase when PageSchedule changes; records of other versions are ignored (an empty
// schedule). 2.15.0 stored all older records again in version 4. To keep the schedule over // schedule) unless converted below. To keep the schedule over a change, add a frozen copy
// a change, add a frozen copy of the old layout and convert it (see git history). // of the old layout and convert it.
static const uint16_t VERSION = 4; static const uint16_t VERSION = 5;
struct StoredSchedule struct StoredSchedule
{ {
@@ -17,6 +17,96 @@ struct StoredSchedule
PageSchedule schedule; PageSchedule schedule;
}; };
// Version 4 (2.15.0 - 2.17.0): no skip_slow_mode. Frozen copy, converted when read; the
// converted schedule is only stored when it is saved again (a rollback can still read
// version 4). Drop this once the display has saved its schedule in version 5.
namespace v4
{
struct Item
{
char page[PAGE_NAME_SIZE];
uint16_t seconds;
};
struct Block
{
uint16_t start;
bool effect;
uint8_t item_count;
Item items[BLOCK_ITEMS];
};
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;
bool over_slow_mode;
};
struct Schedule
{
bool enabled;
uint8_t block_count;
Block 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, PageSchedule &s)
{
s = PageSchedule(); // skip_slow_mode: false
s.enabled = old.enabled;
s.block_count = old.block_count;
for (int i = 0; i < SCHEDULE_BLOCKS; i++)
{
const Block &o = old.blocks[i];
ScheduleBlock &b = s.blocks[i];
b.start = o.start;
b.effect = o.effect;
b.item_count = o.item_count;
for (int j = 0; j < BLOCK_ITEMS; j++)
{
memcpy(b.items[j].page, o.items[j].page, sizeof(b.items[j].page));
b.items[j].seconds = o.items[j].seconds;
}
}
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 = o.over_manual;
r.over_slow_mode = o.over_slow_mode;
}
}
} // namespace v4
static_assert(sizeof(v4::Stored) != sizeof(StoredSchedule), "the record sizes tell the versions apart");
void loadSchedule(PageSchedule &schedule) void loadSchedule(PageSchedule &schedule)
{ {
schedule = PageSchedule(); schedule = PageSchedule();
@@ -32,6 +122,20 @@ void loadSchedule(PageSchedule &schedule)
schedule = stored->schedule; schedule = stored->schedule;
delete stored; delete stored;
} }
else if (length == sizeof(v4::Stored))
{
v4::Stored *stored = new v4::Stored;
PageSchedule *converted = new PageSchedule;
if (prefs.getBytes(KEY, stored, sizeof(v4::Stored)) == sizeof(v4::Stored) && stored->version == 4 &&
stored->size == sizeof(v4::Schedule))
{
v4::convert(stored->schedule, *converted);
if (validateSchedule(*converted) == nullptr)
schedule = *converted;
}
delete converted;
delete stored;
}
prefs.end(); prefs.end();
} }
+4
View File
@@ -44,6 +44,7 @@ void scheduleToJson(const PageSchedule &s, JsonObject out)
JsonObject item = items.add<JsonObject>(); JsonObject item = items.add<JsonObject>();
item["page"] = b.items[j].page; item["page"] = b.items[j].page;
item["seconds"] = b.items[j].seconds; item["seconds"] = b.items[j].seconds;
item["skip_slow_mode"] = b.items[j].skip_slow_mode;
} }
} }
JsonArray rules = out["rules"].to<JsonArray>(); JsonArray rules = out["rules"].to<JsonArray>();
@@ -61,6 +62,7 @@ void scheduleToJson(const PageSchedule &s, JsonObject out)
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; o["over_slow_mode"] = r.over_slow_mode;
o["skip_slow_mode"] = r.skip_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)
@@ -97,6 +99,7 @@ const char *scheduleFromJson(JsonVariantConst in, PageSchedule &s)
if (!copyText(i["page"], item.page, sizeof(item.page))) if (!copyText(i["page"], item.page, sizeof(item.page)))
return "Page name too long"; return "Page name too long";
item.seconds = constrain(i["seconds"] | 30, 0, 65535); item.seconds = constrain(i["seconds"] | 30, 0, 65535);
item.skip_slow_mode = i["skip_slow_mode"] | false;
} }
} }
@@ -115,6 +118,7 @@ const char *scheduleFromJson(JsonVariantConst in, PageSchedule &s)
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; r.over_slow_mode = o["over_slow_mode"] | false;
r.skip_slow_mode = o["skip_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>())
{ {
+7 -4
View File
@@ -330,24 +330,27 @@ function pageOpts(){return pageList.map(p=>[p.name,p.name]).concat(bitmapList.ma
function schedLink(){sched.rules.forEach(r=>{if(!r._b)r._b=(r.blocks||[]).map(t=>sched.blocks.find(b=>b.start==t)).filter(Boolean)})} function schedLink(){sched.rules.forEach(r=>{if(!r._b)r._b=(r.blocks||[]).map(t=>sched.blocks.find(b=>b.start==t)).filter(Boolean)})}
function schedPayload(){return{enabled:sched.enabled,blocks:sched.blocks,rules:sched.rules.map(r=>{const o=Object.assign({},r); function schedPayload(){return{enabled:sched.enabled,blocks:sched.blocks,rules:sched.rules.map(r=>{const o=Object.assign({},r);
o.blocks=sched.blocks.filter(b=>(r._b||[]).includes(b)).map(b=>b.start);delete o._b;return o})}} o.blocks=sched.blocks.filter(b=>(r._b||[]).includes(b)).map(b=>b.start);delete o._b;return o})}}
function skipSlow(o){const c=mk('input',{type:'checkbox',checked:!!o.skip_slow_mode});c.onchange=()=>{o.skip_slow_mode=c.checked;schedDirty=true};
return mk('label',{title:'Left out of the rotation while slow mode is on'},c,' skip in slow mode')}
function schedBuild(){if(!sched)return;schedLink();$('sc_en').checked=sched.enabled;const d=$('sc_blocks'),opts=pageOpts();d.innerHTML=''; function schedBuild(){if(!sched)return;schedLink();$('sc_en').checked=sched.enabled;const d=$('sc_blocks'),opts=pageOpts();d.innerHTML='';
sched.blocks.forEach((b,i)=>{const t=sinp(b,'start','time');t.onchange=()=>{sched.blocks.sort(bySlot);schedDirty=true;schedBuild()}; sched.blocks.forEach((b,i)=>{const t=sinp(b,'start','time');t.onchange=()=>{sched.blocks.sort(bySlot);schedDirty=true;schedBuild()};
const ef=mk('input',{type:'checkbox',checked:b.effect});ef.onchange=()=>{b.effect=ef.checked;schedDirty=true}; const ef=mk('input',{type:'checkbox',checked:b.effect});ef.onchange=()=>{b.effect=ef.checked;schedDirty=true};
const box=mk('div',{className:'el'},mk('div',{className:'row'},mk('b',{textContent:'From'}),t,mk('label',{},ef,' switch with the refresh effect'), const box=mk('div',{className:'el'},mk('div',{className:'row'},mk('b',{textContent:'From'}),t,mk('label',{},ef,' switch with the refresh effect'),
btn('Remove block',()=>{sched.blocks.splice(i,1);schedDirty=true;schedBuild()},'d'))); btn('Remove block',()=>{sched.blocks.splice(i,1);schedDirty=true;schedBuild()},'d')));
b.items.forEach((it,j)=>box.append(mk('div',{className:'row'},ssel(it,'page',opts),'for',sinp(it,'seconds','number',{style:'width:5em',min:5,max:3600}),'s', b.items.forEach((it,j)=>box.append(mk('div',{className:'row'},ssel(it,'page',opts),'for',sinp(it,'seconds','number',{style:'width:5em',min:5,max:3600}),'s',
btn('Remove',()=>{b.items.splice(j,1);schedDirty=true;schedBuild()},'d')))); skipSlow(it),btn('Remove',()=>{b.items.splice(j,1);schedDirty=true;schedBuild()},'d'))));
if(b.items.length<6)box.append(mk('div',{className:'row'},btn('+ Page',()=>{b.items.push({page:opts.length?opts[0][0]:'',seconds:30});schedDirty=true;schedBuild()}))); if(b.items.length<6)box.append(mk('div',{className:'row'},btn('+ Page',()=>{b.items.push({page:opts.length?opts[0][0]:'',seconds:30,skip_slow_mode:false});schedDirty=true;schedBuild()})));
d.append(box)}); d.append(box)});
if(!sched.blocks.length)d.append(mk('p',{className:'hint',textContent:'No blocks: the chosen page is shown.'})); if(!sched.blocks.length)d.append(mk('p',{className:'hint',textContent:'No blocks: the chosen page is shown.'}));
const rd=$('sc_rules');rd.innerHTML=''; const rd=$('sc_rules');rd.innerHTML='';
sched.rules.forEach((ru,i)=>{const state=mk('span',{className:'hint'}); sched.rules.forEach((ru,i)=>{const state=mk('span',{className:'hint'});
const secs=mk('span',{},'for',sinp(ru,'seconds','number',{style:'width:5em',min:1,max:3600}),'s');secs.style.display=ru.mode=='while'?'none':''; const secs=mk('span',{},'for',sinp(ru,'seconds','number',{style:'width:5em',min:1,max:3600}),'s');secs.style.display=ru.mode=='while'?'none':'';
const skip=skipSlow(ru);skip.style.display=ru.mode=='rotate'?'':'none';
rd.append(mk('div',{className:'el'}, rd.append(mk('div',{className:'el'},
mk('div',{className:'row'},mk('b',{textContent:(i+1)+'. When'}),sinp(ru,'entity','text',{placeholder:'sensor.mainsailos_current_print_state',size:30,maxLength:64}), mk('div',{className:'row'},mk('b',{textContent:(i+1)+'. When'}),sinp(ru,'entity','text',{placeholder:'sensor.mainsailos_current_print_state',size:30,maxLength:64}),
sinp(ru,'attribute','text',{placeholder:'(the state)',size:10,maxLength:32}),ssel(ru,'compare',RCMP),sinp(ru,'value','text',{size:12,maxLength:32,placeholder:'printing'}), sinp(ru,'attribute','text',{placeholder:'(the state)',size:10,maxLength:32}),ssel(ru,'compare',RCMP),sinp(ru,'value','text',{size:12,maxLength:32,placeholder:'printing'}),
btn('Test',async()=>{state.textContent='now: '+await haValue(ru.entity,ru.attribute)})), btn('Test',async()=>{state.textContent='now: '+await haValue(ru.entity,ru.attribute)})),
mk('div',{className:'row'},'then',ssel(ru,'page',opts),ssel(ru,'mode',RMODE,()=>{secs.style.display=ru.mode=='while'?'none':''}),secs, mk('div',{className:'row'},'then',ssel(ru,'page',opts),ssel(ru,'mode',RMODE,()=>{secs.style.display=ru.mode=='while'?'none':'';skip.style.display=ru.mode=='rotate'?'':'none'}),secs,skip,
'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)'})),
@@ -360,7 +363,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,over_slow_mode:false});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,skip_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){}}
+36
View File
@@ -2122,6 +2122,41 @@ void test_selector_add_to_rotation()
TEST_ASSERT_EQUAL_STRING("3D printer", sel2.update(600, CHOSEN_PAGE_SECONDS * 1000).page); TEST_ASSERT_EQUAL_STRING("3D printer", sel2.update(600, CHOSEN_PAGE_SECONDS * 1000).page);
} }
void test_selector_skips_pages_in_slow_mode()
{
PageSchedule s = twoBlocks();
s.blocks[0].items[1].skip_slow_mode = true; // Datum
s.rule_count = 2;
s.rules[0] = rule("3D printer", TriggerMode::AddToRotation, 15);
s.rules[0].skip_slow_mode = true;
s.rules[1] = rule("Afval", TriggerMode::AddToRotation, 15);
PageSelector sel;
sel.setSchedule(&s);
sel.setChosen("Clock");
sel.setRuleState(0, true, 0);
sel.setRuleState(1, true, 0);
sel.setSlowMode(true);
TEST_ASSERT_EQUAL_STRING("Weer", sel.update(8 * 60, 0).page);
TEST_ASSERT_EQUAL_STRING("Afval", sel.update(8 * 60, 20000).page); // no Datum, no printer
TEST_ASSERT_EQUAL_STRING("Weer", sel.update(8 * 60, 35000).page);
// Off again: all of them
sel.setSlowMode(false);
TEST_ASSERT_EQUAL_STRING("Datum", sel.update(8 * 60, 55000).page);
TEST_ASSERT_EQUAL_STRING("3D printer", sel.update(8 * 60, 65000).page);
// All of the block's pages skipped: the chosen page, plus the rules that stay
s.blocks[0].items[0].skip_slow_mode = true;
sel.setRuleState(1, false, 70000);
sel.setSlowMode(true);
PageChoice c = sel.update(8 * 60, 80000);
TEST_ASSERT_EQUAL_STRING("Clock", c.page);
TEST_ASSERT_TRUE(c.reason == ShowReason::Chosen);
TEST_ASSERT_EQUAL_STRING("Clock", sel.update(8 * 60, 200000).page);
TEST_ASSERT_FALSE(ScheduleItem().skip_slow_mode);
TEST_ASSERT_FALSE(TriggerRule().skip_slow_mode);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Home Assistant value conversions // Home Assistant value conversions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -3271,6 +3306,7 @@ int main()
RUN_TEST(test_selector_rule_over_slow_mode); 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_selector_skips_pages_in_slow_mode);
RUN_TEST(test_ha_auto_unit_and_absolute); RUN_TEST(test_ha_auto_unit_and_absolute);
RUN_TEST(test_ha_duration); RUN_TEST(test_ha_duration);
RUN_TEST(test_ha_timestamps); RUN_TEST(test_ha_timestamps);