Add ability to schedule when the kiln starts

This adds the possibility to use a datepicker in the modal after
clicking the start button to schedule when the kiln should start
running by itself automatically.
The timer is implemented in the backend and the start is triggered
there so closing or refreshing the browser does not stop it.
In the state when it's "waiting to start", the frontend state
changes so that the glowing timer icon is now shown instead of the
previously unused door icon. The state is also displayed as
SCHEDULED and above it the info states when it's due to start:
"Start at: ..."
This commit is contained in:
Marko Burazin 2021-12-17 22:42:36 +01:00
parent 1450662aea
commit ebc4c89143
5 changed files with 168 additions and 14 deletions

View File

@ -4,6 +4,7 @@ import os
import sys
import logging
import json
from datetime import datetime
import bottle
import gevent
@ -60,7 +61,7 @@ def handle_api():
# start at a specific minute in the schedule
# for restarting and skipping over early parts of a schedule
startat = 0;
startat = 0
if 'startat' in bottle.request.json:
startat = bottle.request.json['startat']
@ -72,8 +73,7 @@ def handle_api():
# FIXME juggling of json should happen in the Profile class
profile_json = json.dumps(profile)
profile = Profile(profile_json)
oven.run_profile(profile,startat=startat)
ovenWatcher.record(profile)
run_profile(profile,startat=startat)
if bottle.request.json['cmd'] == 'stop':
log.info("api stop command received")
@ -96,6 +96,11 @@ def find_profile(wanted):
return profile
return None
def run_profile(profile, startat=0):
oven.run_profile(profile, startat)
ovenWatcher.record(profile)
@app.route('/picoreflow/:filename#.*#')
def send_static(filename):
log.debug("serving %s" % filename)
@ -126,8 +131,26 @@ def handle_control():
if profile_obj:
profile_json = json.dumps(profile_obj)
profile = Profile(profile_json)
oven.run_profile(profile)
ovenWatcher.record(profile)
run_profile(profile)
elif msgdict.get("cmd") == "SCHEDULED_RUN":
log.info("SCHEDULED_RUN command received")
scheduled_start_time = msgdict.get('scheduledStartTime')
profile_obj = msgdict.get('profile')
if profile_obj:
profile_json = json.dumps(profile_obj)
profile = Profile(profile_json)
start_datetime = datetime.fromisoformat(
scheduled_start_time,
)
oven.scheduled_run(
start_datetime,
profile,
lambda: ovenWatcher.record(profile),
)
elif msgdict.get("cmd") == "SIMULATE":
log.info("SIMULATE command received")
#profile_obj = msgdict.get('profile')
@ -260,7 +283,7 @@ def get_config():
"time_scale_slope": config.time_scale_slope,
"time_scale_profile": config.time_scale_profile,
"kwh_rate": config.kwh_rate,
"currency_type": config.currency_type})
"currency_type": config.currency_type})
def main():

View File

@ -6,6 +6,8 @@ import logging
import json
import config
from threading import Timer
log = logging.getLogger(__name__)
@ -169,10 +171,16 @@ class Oven(threading.Thread):
self.daemon = True
self.temperature = 0
self.time_step = config.sensor_time_wait
self.scheduled_run_timer = None
self.start_datetime = None
self.reset()
def reset(self):
self.state = "IDLE"
if self.scheduled_run_timer and self.scheduled_run_timer.is_alive():
log.info("Cancelling previously scheduled run")
self.scheduled_run_timer.cancel()
self.start_datetime = None
self.profile = None
self.start_time = 0
self.runtime = 0
@ -205,6 +213,32 @@ class Oven(threading.Thread):
self.startat = startat * 60
log.info("Starting")
def scheduled_run(self, start_datetime, profile, run_trigger, startat=0):
self.reset()
seconds_until_start = (
start_datetime - datetime.datetime.now()
).total_seconds()
if seconds_until_start <= 0:
return
self.state = "SCHEDULED"
self.start_datetime = start_datetime
self.scheduled_run_timer = Timer(
seconds_until_start,
self._timeout,
args=[profile, run_trigger, startat],
)
self.scheduled_run_timer.start()
log.info(
"Scheduled to run the kiln at %s",
self.start_datetime,
)
def _timeout(self, profile, run_trigger, startat):
self.run_profile(profile, startat)
if run_trigger:
run_trigger()
def abort_run(self):
self.reset()
@ -263,6 +297,9 @@ class Oven(threading.Thread):
self.reset()
def get_state(self):
scheduled_start = None
if self.start_datetime:
scheduled_start = self.start_datetime.strftime("%Y-%m-%d %H:%M")
state = {
'runtime': self.runtime,
'temperature': self.board.temp_sensor.temperature + config.thermocouple_offset,
@ -274,6 +311,7 @@ class Oven(threading.Thread):
'currency_type': config.currency_type,
'profile': self.profile.name if self.profile else None,
'pidstats': self.pid.pidstats,
'scheduled_start': scheduled_start,
}
return state
@ -294,7 +332,8 @@ class Oven(threading.Thread):
class SimulatedOven(Oven):
def __init__(self):
self.reset()
# call parent init
Oven.__init__(self)
self.board = BoardSimulated()
self.t_env = config.sim_t_env
@ -309,9 +348,6 @@ class SimulatedOven(Oven):
self.t = self.t_env # deg C temp of oven
self.t_h = self.t_env #deg C temp of heating element
# call parent init
Oven.__init__(self)
# start thread
self.start()
log.info("SimulatedOven started")
@ -380,11 +416,11 @@ class RealOven(Oven):
def __init__(self):
self.board = Board()
self.output = Output()
self.reset()
# call parent init
Oven.__init__(self)
self.reset()
# start thread
self.start()

View File

@ -32,6 +32,11 @@ body {
box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.55);
}
#schedule-status {
width: auto;
padding-right: 4px;
}
.display {
display: inline-block;
text-align: right;
@ -175,6 +180,23 @@ body {
background: radial-gradient(ellipse at center, rgba(221,221,221,1) 0%,rgba(221,221,221,0.26) 100%); /* W3C */
}
.ds-led-timer-active {
color: rgb(74, 159, 255);
animation: blinker 1s linear infinite;
background: -moz-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%, rgba(48,144,209,0.26) 100%); /* FF3.6+ */
background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(124,197,239,1)), color-stop(100%,rgba(48,144,209,0.26))); /* Chrome,Safari4+ */
background: -webkit-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Chrome10+,Safari5.1+ */
background: -o-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Opera 12+ */
background: -ms-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* IE10+ */
background: radial-gradient(ellipse at center, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* W3C */
}
@keyframes blinker {
50% {
opacity: 0;
}
}
.ds-trend {
top: 0;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.25), -1px -1px 0 rgba(0, 0, 0, 0.4);
@ -352,6 +374,17 @@ body {
top: 10%;
}
.schedule-group {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.schedule-group > input {
margin-right: 5px;
text-align: right;
}
.alert {
background-image: -webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));
background-image: -webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);

View File

@ -222,6 +222,24 @@ function runTask()
}
function scheduleTask()
{
const startTime = document.getElementById('scheduled-run-time').value;
console.log(startTime);
var cmd =
{
"cmd": "SCHEDULED_RUN",
"profile": profiles[selected_profile],
"scheduledStartTime": startTime,
}
graph.live.data = [];
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
ws_control.send(JSON.stringify(cmd));
}
function runTaskSimulation()
{
var cmd =
@ -440,10 +458,37 @@ function getOptions()
}
function formatDateInput(date)
{
var dd = date.getDate();
var mm = date.getMonth() + 1; //January is 0!
var yyyy = date.getFullYear();
var hh = date.getHours();
var mins = date.getMinutes();
if (dd < 10) {
dd = '0' + dd;
}
if (mm < 10) {
mm = '0' + mm;
}
const formattedDate = yyyy + '-' + mm + '-' + dd + 'T' + hh + ':' + mins;
return formattedDate;
}
function initDatetimePicker() {
const now = new Date();
const inThirtyMinutes = new Date();
inThirtyMinutes.setMinutes(inThirtyMinutes.getMinutes() + 10);
$('#scheduled-run-time').attr('value', formatDateInput(inThirtyMinutes));
$('#scheduled-run-time').attr('min', formatDateInput(now));
}
$(document).ready(function()
{
initDatetimePicker();
if(!("WebSocket" in window))
{
@ -538,6 +583,8 @@ $(document).ready(function()
{
$("#nav_start").hide();
$("#nav_stop").show();
$("#timer").removeClass("ds-led-timer-active");
$('#schedule-status').hide()
graph.live.data.push([x.runtime, x.temperature]);
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
@ -550,12 +597,22 @@ $(document).ready(function()
$('#target_temp').html(parseInt(x.target));
}
else if (state === "SCHEDULED") {
$("#nav_start").hide();
$("#nav_stop").show();
$('#timer').addClass("ds-led-timer-active"); // Start blinking timer symbol
$('#state').html('<p class="ds-text">'+state+'</p>');
$('#schedule-status').html('Start at: ' + x.scheduled_start);
$('#schedule-status').show()
}
else
{
$("#nav_start").show();
$("#nav_stop").hide();
$("#timer").removeClass("ds-led-timer-active");
$('#state').html('<p class="ds-text">'+state+'</p>');
$('#schedule-status').hide()
}
$('#act_temp').html(parseInt(x.temperature));

View File

@ -29,6 +29,7 @@
<div class="ds-title-panel">
<div class="ds-title">Sensor Temp</div>
<div class="ds-title">Target Temp</div>
<div id="schedule-status" class="ds-title"></div>
<div class="ds-title ds-state pull-right" style="border-left: 1px solid #ccc;">Status</div>
</div>
<div class="clearfix"></div>
@ -36,7 +37,7 @@
<div class="display ds-num"><span id="act_temp">25</span><span class="ds-unit" id="act_temp_scale" >&deg;C</span></div>
<div class="display ds-num ds-target"><span id="target_temp">---</span><span class="ds-unit" id="target_temp_scale">&deg;C</span></div>
<div class="display ds-num ds-text" id="state"></div>
<div class="display pull-right ds-state" style="padding-right:0"><span class="ds-led" id="heat">&#92;</span><span class="ds-led" id="cool">&#108;</span><span class="ds-led" id="air">&#91;</span><span class="ds-led" id="hazard">&#73;</span><span class="ds-led" id="door">&#9832;</span></div>
<div class="display pull-right ds-state" style="padding-right:0"><span class="ds-led" id="heat">&#92;</span><span class="ds-led" id="cool">&#108;</span><span class="ds-led" id="air">&#91;</span><span class="ds-led" id="hazard">&#73;</span><span class="ds-led" id="timer">&#x29D6;</span></div>
</div>
<div class="clearfix"></div>
<div>
@ -107,7 +108,11 @@
<div class="modal-footer">
<div class="btn-group" style="width: 100%">
<button type="button" class="btn btn-danger" style="width: 50%" data-dismiss="modal">No, take me back</button>
<button type="button" class="btn btn-success" style="width: 50%" data-dismiss="modal" onclick="runTask()">Yes, start the Run</button>
<button type="button" class="btn btn-success" style="width: 50%" data-dismiss="modal" onclick="runTask()">Yes, start the Run now</button>
</div>
<div class="schedule-group">
<input type="datetime-local" id="scheduled-run-time">
<button type="button" class="btn btn-primary" style="width: 50%" data-dismiss="modal" onclick="scheduleTask()">Schedule start for later</button>
</div>
</div>
</div>