Public Access
Rules can be limited to schedule blocks
Each rule has the blocks it acts in (none: always; without a running schedule always). Stored as a block mask (schedule format 2, version 1 records are converted), in JSON as the blocks' start times. Version 2.7.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87e242bf89
commit
53e9057d02
11 files changed
+166
-19
No files matched your search
@@ -59,6 +59,13 @@ void scheduleToJson(const PageSchedule &s, JsonObject out)
|
||||
o["mode"] = MODES[(int)r.mode];
|
||||
o["seconds"] = r.seconds;
|
||||
o["interval"] = r.interval;
|
||||
JsonArray in_blocks = o["blocks"].to<JsonArray>();
|
||||
for (int b = 0; b < s.block_count; b++)
|
||||
if ((r.blocks >> b) & 1)
|
||||
{
|
||||
formatTime(s.blocks[b].start, time);
|
||||
in_blocks.add(String(time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +111,19 @@ const char *scheduleFromJson(JsonVariantConst in, PageSchedule &s)
|
||||
return "Rule text too long";
|
||||
r.seconds = constrain(o["seconds"] | 30, 0, 65535);
|
||||
r.interval = constrain(o["interval"] | 10, 0, 65535);
|
||||
// The blocks by their start time (none: always)
|
||||
for (JsonVariantConst start : o["blocks"].as<JsonArrayConst>())
|
||||
{
|
||||
uint16_t minutes;
|
||||
int found = -1;
|
||||
if (parseTime(start | "", minutes))
|
||||
for (int b = 0; b < s.block_count; b++)
|
||||
if (s.blocks[b].start == minutes)
|
||||
found = b;
|
||||
if (found < 0)
|
||||
return "A rule is set for a block that doesn't exist";
|
||||
r.blocks |= 1 << found;
|
||||
}
|
||||
}
|
||||
return validateSchedule(s);
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
// {enabled,
|
||||
// blocks: [{start: "07:00", effect: true, items: [{page, seconds}]}],
|
||||
// rules: [{entity, attribute, compare: "eq"|"ne"|"gt"|"lt", value, page,
|
||||
// mode: "while"|"rotate"|"for", seconds, interval}]}
|
||||
// mode: "while"|"rotate"|"for", seconds, interval,
|
||||
// blocks: ["07:00"] (the start times of the blocks it acts in; empty: always)}]}
|
||||
|
||||
void scheduleToJson(const PageSchedule &schedule, JsonObject out);
|
||||
// Returns nullptr or a message for the user. Validates (validateSchedule()).
|
||||
|
||||
@@ -98,7 +98,7 @@ details{margin-top:10px}summary{cursor:pointer;color:var(--dim);font-size:13px;m
|
||||
<div id="sc_blocks"></div><div class="row"><button class="s" onclick="scAddBlock()">Add block</button></div>
|
||||
<p class="hint">Every day: a block runs from its time until the next block (the last one continues after midnight). Its pages rotate, each for its time. Without blocks, or switched off, the chosen page (Show tab) is shown. A page chosen by hand is shown until the next block starts.</p></div>
|
||||
<div class="card"><h2>Rules</h2><div id="sc_rules"></div><div class="row"><button class="s" onclick="scAddRule()">Add rule</button></div>
|
||||
<p class="hint">Home Assistant states that bring a page to the front (needs the token in the Home Assistant tab). The first rule that shows a page wins, over the schedule and a page chosen by hand. Texts are compared without case; above and below compare numbers. Rules only act while the display is on.</p></div>
|
||||
<p class="hint">Home Assistant states that bring a page to the front (needs the token in the Home Assistant tab). The first rule that shows a page wins, over the schedule and a page chosen by hand. Texts are compared without case; above and below compare numbers. Rules only act while the display is on, and only in the blocks ticked for them (none ticked: always; without a running schedule the blocks don't matter).</p></div>
|
||||
<button class="b" onclick="scSave()">Save schedule and rules</button></section>
|
||||
|
||||
<section id="t-stopwatch"><div class="card"><h2>Stopwatch</h2>
|
||||
@@ -267,7 +267,10 @@ function sinp(o,k,type,props){const i=mk('input',Object.assign({type:type||'text
|
||||
function ssel(o,k,opts,after){const s=mk('select');opts.forEach(([v,t])=>s.append(mk('option',{value:v,textContent:t})));if(!opts.some(x=>x[0]==o[k]))s.append(mk('option',{value:o[k],textContent:o[k]+' (gone)'}));
|
||||
s.value=o[k];s.onchange=()=>{o[k]=s.value;schedDirty=true;if(after)after()};return s}
|
||||
function pageOpts(){return pageList.map(p=>[p.name,p.name]).concat(bitmapList.map(b=>[b.name,b.name+' (bitmap)']))}
|
||||
function schedBuild(){if(!sched)return;$('sc_en').checked=sched.enabled;const d=$('sc_blocks'),opts=pageOpts();d.innerHTML='';
|
||||
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 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'),
|
||||
@@ -285,13 +288,16 @@ function schedBuild(){if(!sched)return;$('sc_en').checked=sched.enabled;const d=
|
||||
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()=>{const v=await api('/api/ha/value?entity='+encodeURIComponent(ru.entity)+'&attribute='+encodeURIComponent(ru.attribute));state.textContent='now: '+v.value})),
|
||||
mk('div',{className:'row'},'then',ssel(ru,'page',opts),ssel(ru,'mode',RMODE,()=>{secs.style.display=ru.mode=='while'?'none':''}),secs,
|
||||
'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')),
|
||||
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)}),
|
||||
mk('span',{className:'hint',textContent:'(none ticked: always)'})):''))});
|
||||
if(!sched.rules.length)rd.append(mk('p',{className:'hint',textContent:'No rules.'}))}
|
||||
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});schedDirty=true;schedBuild()}
|
||||
async function scSave(){sched.blocks.sort(bySlot);sched=await post('/api/schedule',sched);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 pollStatusOnce(){try{status=await api('/api/status');$('sc_now').textContent='Showing: '+status.shown+' ('+status.shown_status+')';$('shownnow').textContent=$('sc_now').textContent}catch(e){}}
|
||||
// page editor: the ESP32 draws the preview and returns the element boxes
|
||||
|
||||
Reference in new issue
Block a user