From ebc4c89143828dd0d7a116151b0eedd12552e286 Mon Sep 17 00:00:00 2001 From: Marko Burazin Date: Fri, 17 Dec 2021 22:42:36 +0100 Subject: [PATCH] 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: ..." --- kiln-controller.py | 35 ++++++++++++++++---- lib/oven.py | 48 +++++++++++++++++++++++---- public/assets/css/picoreflow.css | 33 ++++++++++++++++++ public/assets/js/picoreflow.js | 57 ++++++++++++++++++++++++++++++++ public/index.html | 9 +++-- 5 files changed, 168 insertions(+), 14 deletions(-) diff --git a/kiln-controller.py b/kiln-controller.py index 4482ba8..828c36f 100755 --- a/kiln-controller.py +++ b/kiln-controller.py @@ -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(): diff --git a/lib/oven.py b/lib/oven.py index af2b42f..0ca2d9d 100644 --- a/lib/oven.py +++ b/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() diff --git a/public/assets/css/picoreflow.css b/public/assets/css/picoreflow.css index 8e77db7..df7fded 100644 --- a/public/assets/css/picoreflow.css +++ b/public/assets/css/picoreflow.css @@ -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%); diff --git a/public/assets/js/picoreflow.js b/public/assets/js/picoreflow.js index 479b7b7..da1f653 100644 --- a/public/assets/js/picoreflow.js +++ b/public/assets/js/picoreflow.js @@ -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('

'+state+'

'); + $('#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('

'+state+'

'); + $('#schedule-status').hide() } $('#act_temp').html(parseInt(x.temperature)); diff --git a/public/index.html b/public/index.html index 08b2d69..a619c9d 100644 --- a/public/index.html +++ b/public/index.html @@ -29,6 +29,7 @@
Sensor Temp
Target Temp
+
Status
@@ -36,7 +37,7 @@
25°C
---°C
-
\l[I
+
\l[I
@@ -107,7 +108,11 @@