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
+177
-13
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
|
||||
(`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.
|
||||
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 Recovery` button: restarts into the [recovery app](recovery.md) for 10
|
||||
minutes.
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
: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.
|
||||
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)
|
||||
and power is cut after 30 seconds.
|
||||
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
|
||||
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.
|
||||
[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;
|
||||
if (!time_valid)
|
||||
return;
|
||||
selector.setSlowMode(settings.slow_mode.enabled);
|
||||
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))
|
||||
|
||||
+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_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)
|
||||
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||
|
||||
@@ -25,6 +25,7 @@ struct ScheduleItem
|
||||
{
|
||||
char page[PAGE_NAME_SIZE] = "";
|
||||
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
|
||||
@@ -66,6 +67,7 @@ struct TriggerRule
|
||||
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_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
|
||||
|
||||
@@ -131,15 +131,16 @@ PageChoice PageSelector::update(int minute, uint32_t now)
|
||||
const ScheduleBlock &b = _schedule->blocks[_block];
|
||||
effect = b.effect;
|
||||
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};
|
||||
for (int i = 0; i < rule_count; i++)
|
||||
{
|
||||
const TriggerRule &r = _schedule->rules[i];
|
||||
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};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// 3. the schedule: the pages of the current block rotate, plus the pages of active
|
||||
// AddToRotation rules
|
||||
// 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().
|
||||
|
||||
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
|
||||
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)
|
||||
void setRuleState(int rule, bool active, uint32_t now);
|
||||
bool ruleActive(int rule) const;
|
||||
@@ -63,6 +68,7 @@ private:
|
||||
const PageSchedule *_schedule = nullptr;
|
||||
char _chosen[PAGE_NAME_SIZE] = "";
|
||||
bool _picked = false;
|
||||
bool _slow = false;
|
||||
int _block = -1;
|
||||
int _last_minute = -1;
|
||||
int _item = 0;
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
static const char *NAMESPACE = "flipdot";
|
||||
static const char *KEY = "schedule";
|
||||
// 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
|
||||
// a change, add a frozen copy of the old layout and convert it (see git history).
|
||||
static const uint16_t VERSION = 4;
|
||||
// schedule) unless converted below. To keep the schedule over a change, add a frozen copy
|
||||
// of the old layout and convert it.
|
||||
static const uint16_t VERSION = 5;
|
||||
|
||||
struct StoredSchedule
|
||||
{
|
||||
@@ -17,6 +17,96 @@ struct StoredSchedule
|
||||
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)
|
||||
{
|
||||
schedule = PageSchedule();
|
||||
@@ -32,6 +122,20 @@ void loadSchedule(PageSchedule &schedule)
|
||||
schedule = stored->schedule;
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ void scheduleToJson(const PageSchedule &s, JsonObject out)
|
||||
JsonObject item = items.add<JsonObject>();
|
||||
item["page"] = b.items[j].page;
|
||||
item["seconds"] = b.items[j].seconds;
|
||||
item["skip_slow_mode"] = b.items[j].skip_slow_mode;
|
||||
}
|
||||
}
|
||||
JsonArray rules = out["rules"].to<JsonArray>();
|
||||
@@ -61,6 +62,7 @@ void scheduleToJson(const PageSchedule &s, JsonObject out)
|
||||
o["interval"] = r.interval;
|
||||
o["over_manual"] = r.over_manual;
|
||||
o["over_slow_mode"] = r.over_slow_mode;
|
||||
o["skip_slow_mode"] = r.skip_slow_mode;
|
||||
JsonArray in_blocks = o["blocks"].to<JsonArray>();
|
||||
for (int b = 0; b < s.block_count; b++)
|
||||
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)))
|
||||
return "Page name too long";
|
||||
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.over_manual = o["over_manual"] | true;
|
||||
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)
|
||||
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 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})}}
|
||||
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='';
|
||||
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 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')));
|
||||
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'))));
|
||||
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()})));
|
||||
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,skip_slow_mode:false});schedDirty=true;schedBuild()})));
|
||||
d.append(box)});
|
||||
if(!sched.blocks.length)d.append(mk('p',{className:'hint',textContent:'No blocks: the chosen page is shown.'}));
|
||||
const rd=$('sc_rules');rd.innerHTML='';
|
||||
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 skip=skipSlow(ru);skip.style.display=ru.mode=='rotate'?'':'none';
|
||||
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}),
|
||||
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)})),
|
||||
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')),
|
||||
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)'})),
|
||||
@@ -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}
|
||||
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}
|
||||
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 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){}}
|
||||
|
||||
@@ -2122,6 +2122,41 @@ void test_selector_add_to_rotation()
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -3271,6 +3306,7 @@ int main()
|
||||
RUN_TEST(test_selector_rule_over_slow_mode);
|
||||
RUN_TEST(test_selector_rule_can_leave_a_manual_pick_alone);
|
||||
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_duration);
|
||||
RUN_TEST(test_ha_timestamps);
|
||||
|
||||
Reference in new issue
Block a user