Public Access
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:
1 parent
a91d2550d7
commit
e9f3c16c78
13 files changed
+176
-12
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 177 KiB |
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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++)
|
||||||
|
if (!_slow || !b.items[i].skip_slow_mode)
|
||||||
items[count++] = {b.items[i].page, b.items[i].seconds * 1000UL, ShowReason::Schedule, -1};
|
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};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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){}}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user