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:
parent
1450662aea
commit
ebc4c89143
@ -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():
|
||||
|
||||
48
lib/oven.py
48
lib/oven.py
@ -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()
|
||||
|
||||
|
||||
@ -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%);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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" >°C</span></div>
|
||||
<div class="display ds-num ds-target"><span id="target_temp">---</span><span class="ds-unit" id="target_temp_scale">°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">\</span><span class="ds-led" id="cool">l</span><span class="ds-led" id="air">[</span><span class="ds-led" id="hazard">I</span><span class="ds-led" id="door">♨</span></div>
|
||||
<div class="display pull-right ds-state" style="padding-right:0"><span class="ds-led" id="heat">\</span><span class="ds-led" id="cool">l</span><span class="ds-led" id="air">[</span><span class="ds-led" id="hazard">I</span><span class="ds-led" id="timer">⧖</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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user