Skip to content

Commit 000abc3

Browse files
committed
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: ..."
1 parent 1450662 commit 000abc3

File tree

5 files changed

+169
-14
lines changed

5 files changed

+169
-14
lines changed

kiln-controller.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import logging
66
import json
7+
from datetime import datetime
78

89
import bottle
910
import gevent
@@ -60,7 +61,7 @@ def handle_api():
6061

6162
# start at a specific minute in the schedule
6263
# for restarting and skipping over early parts of a schedule
63-
startat = 0;
64+
startat = 0
6465
if 'startat' in bottle.request.json:
6566
startat = bottle.request.json['startat']
6667

@@ -72,8 +73,7 @@ def handle_api():
7273
# FIXME juggling of json should happen in the Profile class
7374
profile_json = json.dumps(profile)
7475
profile = Profile(profile_json)
75-
oven.run_profile(profile,startat=startat)
76-
ovenWatcher.record(profile)
76+
run_profile(profile,startat=startat)
7777

7878
if bottle.request.json['cmd'] == 'stop':
7979
log.info("api stop command received")
@@ -96,6 +96,11 @@ def find_profile(wanted):
9696
return profile
9797
return None
9898

99+
def run_profile(profile, startat=0):
100+
oven.run_profile(profile, startat)
101+
ovenWatcher.record(profile)
102+
103+
99104
@app.route('/picoreflow/:filename#.*#')
100105
def send_static(filename):
101106
log.debug("serving %s" % filename)
@@ -126,8 +131,26 @@ def handle_control():
126131
if profile_obj:
127132
profile_json = json.dumps(profile_obj)
128133
profile = Profile(profile_json)
129-
oven.run_profile(profile)
130-
ovenWatcher.record(profile)
134+
135+
run_profile(profile)
136+
137+
elif msgdict.get("cmd") == "SCHEDULED_RUN":
138+
log.info("SCHEDULED_RUN command received")
139+
scheduled_start_time = msgdict.get('scheduledStartTime')
140+
profile_obj = msgdict.get('profile')
141+
if profile_obj:
142+
profile_json = json.dumps(profile_obj)
143+
profile = Profile(profile_json)
144+
145+
start_datetime = datetime.fromisoformat(
146+
scheduled_start_time,
147+
)
148+
oven.scheduled_run(
149+
start_datetime,
150+
profile,
151+
lambda: ovenWatcher.record(profile),
152+
)
153+
131154
elif msgdict.get("cmd") == "SIMULATE":
132155
log.info("SIMULATE command received")
133156
#profile_obj = msgdict.get('profile')
@@ -260,7 +283,7 @@ def get_config():
260283
"time_scale_slope": config.time_scale_slope,
261284
"time_scale_profile": config.time_scale_profile,
262285
"kwh_rate": config.kwh_rate,
263-
"currency_type": config.currency_type})
286+
"currency_type": config.currency_type})
264287

265288

266289
def main():

lib/oven.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import json
77
import config
88

9+
from threading import Timer
10+
911
log = logging.getLogger(__name__)
1012

1113

@@ -169,10 +171,16 @@ def __init__(self):
169171
self.daemon = True
170172
self.temperature = 0
171173
self.time_step = config.sensor_time_wait
174+
self.scheduled_run_timer = None
175+
self.start_datetime = None
172176
self.reset()
173177

174178
def reset(self):
175179
self.state = "IDLE"
180+
if self.scheduled_run_timer and self.scheduled_run_timer.is_alive():
181+
log.info("Cancelling previously scheduled run")
182+
self.scheduled_run_timer.cancel()
183+
self.start_datetime = None
176184
self.profile = None
177185
self.start_time = 0
178186
self.runtime = 0
@@ -205,6 +213,32 @@ def run_profile(self, profile, startat=0):
205213
self.startat = startat * 60
206214
log.info("Starting")
207215

216+
def scheduled_run(self, start_datetime, profile, run_trigger, startat=0):
217+
self.reset()
218+
seconds_until_start = (
219+
start_datetime - datetime.datetime.now()
220+
).total_seconds()
221+
if seconds_until_start <= 0:
222+
return
223+
224+
self.state = "SCHEDULED"
225+
self.start_datetime = start_datetime
226+
self.scheduled_run_timer = Timer(
227+
seconds_until_start,
228+
self._timeout,
229+
args=[profile, run_trigger, startat],
230+
)
231+
self.scheduled_run_timer.start()
232+
log.info(
233+
"Scheduled to run the kiln at %s",
234+
self.start_datetime,
235+
)
236+
237+
def _timeout(self, profile, run_trigger, startat):
238+
self.run_profile(profile, startat)
239+
if run_trigger:
240+
run_trigger()
241+
208242
def abort_run(self):
209243
self.reset()
210244

@@ -263,6 +297,9 @@ def reset_if_schedule_ended(self):
263297
self.reset()
264298

265299
def get_state(self):
300+
scheduled_start = None
301+
if self.start_datetime:
302+
scheduled_start = self.start_datetime.strftime("%Y-%m-%d %H:%M")
266303
state = {
267304
'runtime': self.runtime,
268305
'temperature': self.board.temp_sensor.temperature + config.thermocouple_offset,
@@ -274,6 +311,7 @@ def get_state(self):
274311
'currency_type': config.currency_type,
275312
'profile': self.profile.name if self.profile else None,
276313
'pidstats': self.pid.pidstats,
314+
'scheduled_start': scheduled_start,
277315
}
278316
return state
279317

@@ -294,7 +332,8 @@ def run(self):
294332
class SimulatedOven(Oven):
295333

296334
def __init__(self):
297-
self.reset()
335+
# call parent init
336+
Oven.__init__(self)
298337
self.board = BoardSimulated()
299338

300339
self.t_env = config.sim_t_env
@@ -309,9 +348,6 @@ def __init__(self):
309348
self.t = self.t_env # deg C temp of oven
310349
self.t_h = self.t_env #deg C temp of heating element
311350

312-
# call parent init
313-
Oven.__init__(self)
314-
315351
# start thread
316352
self.start()
317353
log.info("SimulatedOven started")
@@ -380,11 +416,11 @@ class RealOven(Oven):
380416
def __init__(self):
381417
self.board = Board()
382418
self.output = Output()
383-
self.reset()
384-
385419
# call parent init
386420
Oven.__init__(self)
387421

422+
self.reset()
423+
388424
# start thread
389425
self.start()
390426

public/assets/css/picoreflow.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ body {
3232
box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.55);
3333
}
3434

35+
#schedule-status {
36+
width: auto;
37+
padding-right: 4px;
38+
}
39+
3540
.display {
3641
display: inline-block;
3742
text-align: right;
@@ -175,6 +180,23 @@ body {
175180
background: radial-gradient(ellipse at center, rgba(221,221,221,1) 0%,rgba(221,221,221,0.26) 100%); /* W3C */
176181
}
177182

183+
.ds-led-timer-active {
184+
color: rgb(74, 159, 255);
185+
animation: blinker 1s linear infinite;
186+
background: -moz-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%, rgba(48,144,209,0.26) 100%); /* FF3.6+ */
187+
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+ */
188+
background: -webkit-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Chrome10+,Safari5.1+ */
189+
background: -o-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Opera 12+ */
190+
background: -ms-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* IE10+ */
191+
background: radial-gradient(ellipse at center, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* W3C */
192+
}
193+
194+
@keyframes blinker {
195+
50% {
196+
opacity: 0;
197+
}
198+
}
199+
178200
.ds-trend {
179201
top: 0;
180202
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 {
352374
top: 10%;
353375
}
354376

377+
.schedule-group {
378+
display: flex;
379+
justify-content: flex-end;
380+
margin-top: 10px;
381+
}
382+
383+
.schedule-group > input {
384+
margin-right: 5px;
385+
text-align: right;
386+
}
387+
355388
.alert {
356389
background-image: -webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));
357390
background-image: -webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);

public/assets/js/picoreflow.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,24 @@ function runTask()
222222

223223
}
224224

225+
function scheduleTask()
226+
{
227+
const startTime = document.getElementById('scheduled-run-time').value;
228+
console.log(startTime);
229+
230+
var cmd =
231+
{
232+
"cmd": "SCHEDULED_RUN",
233+
"profile": profiles[selected_profile],
234+
"scheduledStartTime": startTime,
235+
}
236+
237+
graph.live.data = [];
238+
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
239+
240+
ws_control.send(JSON.stringify(cmd));
241+
}
242+
225243
function runTaskSimulation()
226244
{
227245
var cmd =
@@ -440,10 +458,37 @@ function getOptions()
440458

441459
}
442460

461+
function formatDateInput(date)
462+
{
463+
var dd = date.getDate();
464+
var mm = date.getMonth() + 1; //January is 0!
465+
var yyyy = date.getFullYear();
466+
var hh = date.getHours();
467+
var mins = date.getMinutes();
468+
469+
if (dd < 10) {
470+
dd = '0' + dd;
471+
}
472+
473+
if (mm < 10) {
474+
mm = '0' + mm;
475+
}
443476

477+
const formattedDate = yyyy + '-' + mm + '-' + dd + 'T' + hh + ':' + mins;
478+
return formattedDate;
479+
}
480+
481+
function initDatetimePicker() {
482+
const now = new Date();
483+
const inThirtyMinutes = new Date();
484+
inThirtyMinutes.setMinutes(inThirtyMinutes.getMinutes() + 10);
485+
$('#scheduled-run-time').attr('value', formatDateInput(inThirtyMinutes));
486+
$('#scheduled-run-time').attr('min', formatDateInput(now));
487+
}
444488

445489
$(document).ready(function()
446490
{
491+
initDatetimePicker();
447492

448493
if(!("WebSocket" in window))
449494
{
@@ -538,6 +583,8 @@ $(document).ready(function()
538583
{
539584
$("#nav_start").hide();
540585
$("#nav_stop").show();
586+
$("#timer").removeClass("ds-led-timer-active");
587+
$('#schedule-status').hide()
541588

542589
graph.live.data.push([x.runtime, x.temperature]);
543590
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
@@ -550,12 +597,22 @@ $(document).ready(function()
550597
$('#target_temp').html(parseInt(x.target));
551598

552599

600+
}
601+
else if (state === "SCHEDULED") {
602+
$("#nav_start").hide();
603+
$("#nav_stop").show();
604+
$('#timer').addClass("ds-led-timer-active"); // Start blinking timer symbol
605+
$('#state').html('<p class="ds-text">'+state+'</p>');
606+
$('#schedule-status').html('Start at: ' + x.scheduled_start);
607+
$('#schedule-status').show()
553608
}
554609
else
555610
{
556611
$("#nav_start").show();
557612
$("#nav_stop").hide();
613+
$("#timer").removeClass("ds-led-timer-active");
558614
$('#state').html('<p class="ds-text">'+state+'</p>');
615+
$('#schedule-status').hide()
559616
}
560617

561618
$('#act_temp').html(parseInt(x.temperature));

public/index.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@
2929
<div class="ds-title-panel">
3030
<div class="ds-title">Sensor Temp</div>
3131
<div class="ds-title">Target Temp</div>
32+
<div id="schedule-status" class="ds-title"></div>
3233
<div class="ds-title ds-state pull-right" style="border-left: 1px solid #ccc;">Status</div>
3334
</div>
3435
<div class="clearfix"></div>
3536
<div class="ds-panel">
3637
<div class="display ds-num"><span id="act_temp">25</span><span class="ds-unit" id="act_temp_scale" >&deg;C</span></div>
3738
<div class="display ds-num ds-target"><span id="target_temp">---</span><span class="ds-unit" id="target_temp_scale">&deg;C</span></div>
3839
<div class="display ds-num ds-text" id="state"></div>
39-
<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>
40+
<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>
4041
</div>
4142
<div class="clearfix"></div>
4243
<div>
@@ -107,7 +108,12 @@ <h3 class="modal-title" id="jobSummaryModalLabel">Task Overview</h3>
107108
<div class="modal-footer">
108109
<div class="btn-group" style="width: 100%">
109110
<button type="button" class="btn btn-danger" style="width: 50%" data-dismiss="modal">No, take me back</button>
110-
<button type="button" class="btn btn-success" style="width: 50%" data-dismiss="modal" onclick="runTask()">Yes, start the Run</button>
111+
<button type="button" class="btn btn-success" style="width: 50%" data-dismiss="modal" onclick="runTask()">Yes, start the Run now</button>
112+
</div>
113+
<div class="schedule-group">
114+
<input type="datetime-local" id="scheduled-run-time"
115+
min="2018-06-07T00:00" max="2022-06-14T00:00" stype="width: 50%">
116+
<button type="button" class="btn btn-primary" style="width: 50%" data-dismiss="modal" onclick="scheduleTask()">Schedule start for later</button>
111117
</div>
112118
</div>
113119
</div>

0 commit comments

Comments
 (0)