few improvments and fixes to consumption/time tracking
This commit is contained in:
parent
e94994f4fb
commit
e9713731b1
6
app.py
6
app.py
|
@ -131,20 +131,20 @@ def run_recipes():
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
def read_sensors():
|
def read_sensors():
|
||||||
periodi_counter = 0
|
periodic_counter = 0
|
||||||
scantime = 0.5
|
scantime = 0.5
|
||||||
periodic_state_every_s = 5
|
periodic_state_every_s = 5
|
||||||
periodic_state_every_loop = int(periodic_state_every_s/scantime)
|
periodic_state_every_loop = int(periodic_state_every_s/scantime)
|
||||||
while True:
|
while True:
|
||||||
sensors.read()
|
sensors.read()
|
||||||
socketio.emit('sensors', (sensors.get(),))
|
socketio.emit('sensors', (sensors.get(),))
|
||||||
if periodic_counter % periodic_state_every == 0:
|
if periodic_counter % periodic_state_every_loop == 0:
|
||||||
## peridoic refresh other data, too, like:
|
## peridoic refresh other data, too, like:
|
||||||
# - current phase (time in this phase)
|
# - current phase (time in this phase)
|
||||||
# - actuators (consumption)
|
# - actuators (consumption)
|
||||||
# Issuing a full state update is easiest
|
# Issuing a full state update is easiest
|
||||||
updateState()
|
updateState()
|
||||||
periodi_counter += 1
|
periodic_counter += 1
|
||||||
sleep(scantime)
|
sleep(scantime)
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
|
@ -9,6 +9,8 @@ class FixedDutyCycle(controllers.Controller):
|
||||||
self.set_duty(kwargs.pop('duty_perc'))
|
self.set_duty(kwargs.pop('duty_perc'))
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.last_time = None
|
self.last_time = None
|
||||||
|
self.total_failed_read = 0
|
||||||
|
self.failed_read = 0
|
||||||
|
|
||||||
def set_duty(self, duty=None):
|
def set_duty(self, duty=None):
|
||||||
# Updates the duty cycle to a point in time where the switch
|
# Updates the duty cycle to a point in time where the switch
|
||||||
|
@ -33,10 +35,27 @@ class FixedDutyCycle(controllers.Controller):
|
||||||
|
|
||||||
def one_cycle(self, time):
|
def one_cycle(self, time):
|
||||||
self.last_time = time
|
self.last_time = time
|
||||||
|
if self.input is None:
|
||||||
|
self.total_failed_read += 1
|
||||||
|
self.failed_read += 1
|
||||||
|
# TODO: WARN/EXIT IF WE HAVE TOO MANY FAILED READS?
|
||||||
|
return False
|
||||||
|
self.failed_read = 0
|
||||||
return (self.timepoint() < self.duty) if (
|
return (self.timepoint() < self.duty) if (
|
||||||
self.input < self.target) else False
|
self.input < self.target) else False
|
||||||
|
|
||||||
|
|
||||||
|
# 0. refractary_period is fixed (e.g. 30s, depends on the hardware)
|
||||||
|
# 1. period = refractary_period / max(min(duty, 1-duty), 0.01)
|
||||||
|
# this guarantees that the shortest period is always the refractary period
|
||||||
|
# Store a) ON/OFF HISTORY and the number of times we inhibit control
|
||||||
|
# The duty must be adjusted automatically
|
||||||
|
# a) Take the mean() of the history
|
||||||
|
# b) if mean << duty: current duty is too high, set it to mean
|
||||||
|
# c) if mean = duty: current duty is either too low (so we are always on, when we can) or it is fine.
|
||||||
|
# use the percentage of skipped periods over total number of tests
|
||||||
|
# HOW LONG MUST BE THE HISTORY?
|
||||||
|
|
||||||
def fixed_duty_wrapper(args):
|
def fixed_duty_wrapper(args):
|
||||||
name, duty, period = stringify(args.car, quote=False), args.cdr.car, args.cdr.cdr.car
|
name, duty, period = stringify(args.car, quote=False), args.cdr.car, args.cdr.cdr.car
|
||||||
print('name, duty, period', name, duty*100, period)
|
print('name, duty, period', name, duty*100, period)
|
||||||
|
|
|
@ -53,7 +53,7 @@ function applyState(newstate) {
|
||||||
render_actuators(state.actuators);
|
render_actuators(state.actuators);
|
||||||
render_sidebar(state);
|
render_sidebar(state);
|
||||||
manual_modal(state.manual);
|
manual_modal(state.manual);
|
||||||
current_recipe(state.recipe);
|
current_recipe(Object.assign({}, state.state, state.recipe));
|
||||||
render_load_recipe(state);
|
render_load_recipe(state);
|
||||||
render_plot();
|
render_plot();
|
||||||
localstate.editing.recipe = state.recipes.findIndex(
|
localstate.editing.recipe = state.recipes.findIndex(
|
||||||
|
@ -85,6 +85,14 @@ function add_plot_data(newpoints, render=true) {
|
||||||
if (render) render_plot();
|
if (render) render_plot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function set_plot_points(maxpoints) {
|
||||||
|
// let more = localstate.maxpoints > maxpoints;
|
||||||
|
// localstate.maxpoints = maxpoints;
|
||||||
|
// if (more) {
|
||||||
|
// for (key in plot_data.keys())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
function render_plot() {
|
function render_plot() {
|
||||||
if (enable_draw && document.getElementById("data-plot") !== null) {
|
if (enable_draw && document.getElementById("data-plot") !== null) {
|
||||||
Plotly.newPlot('data-plot', Object.values(plot_data));
|
Plotly.newPlot('data-plot', Object.values(plot_data));
|
||||||
|
@ -344,10 +352,17 @@ On Exit: {{onexit}}<br>
|
||||||
html.innerHTML = Mustache.render(template, data);
|
html.innerHTML = Mustache.render(template, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stodate(value) {
|
||||||
|
var date = new Date(0);
|
||||||
|
date.setSeconds(value);
|
||||||
|
var timeString = date.toISOString().substring(11, 19);
|
||||||
|
return timeString;
|
||||||
|
}
|
||||||
|
|
||||||
function current_recipe(data) {
|
function current_recipe(data) {
|
||||||
let html = document.getElementById("current-recipe");
|
let html = document.getElementById("current-recipe");
|
||||||
if (html === null) return;
|
if (html === null) return;
|
||||||
if (data === null) {
|
if (data === null || data.phases === undefined) {
|
||||||
html.innerHTML = '';
|
html.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -356,20 +371,26 @@ function current_recipe(data) {
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
Active Recipe: {{name}}
|
Active Recipe ({{recipe-time}}): {{name}}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{description}}
|
{{description}}
|
||||||
|
<br/>
|
||||||
|
{{recipe-consumption}}
|
||||||
|
<br/>
|
||||||
|
|
||||||
<div class="tabs is-centered">
|
<div class="tabs is-centered">
|
||||||
<ul>
|
<ul>
|
||||||
{{#phases}}
|
{{#phases}}
|
||||||
<li class="{{#current}}is-active{{/current}}"><a>{{name}}</a></li>
|
<li class="{{#current}}is-active{{/current}}"><a>{{name}}{{#current}} ({{phase-time}}){{/current}}</a></li>
|
||||||
{{/phases}}
|
{{/phases}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{current_phase.text}}
|
{{current_phase.text}}
|
||||||
<br/>
|
<br/>
|
||||||
|
{{phase-consumption}}
|
||||||
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
Next Cond: {{current_phase.nextcond}}
|
Next Cond: {{current_phase.nextcond}}
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -385,6 +406,8 @@ On Exit: {{current_phase.onexit}}
|
||||||
class="card-footer-item">Stop</a>
|
class="card-footer-item">Stop</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
data['recipe-time'] = stodate(data['recipe-time'])
|
||||||
|
data['phase-time'] = stodate(data['phase-time'])
|
||||||
html.innerHTML = Mustache.render(template, data);
|
html.innerHTML = Mustache.render(template, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
64
phasectrl.py
64
phasectrl.py
|
@ -7,11 +7,12 @@ from persona import ManualIntervention
|
||||||
from sensors import get_sensor_value
|
from sensors import get_sensor_value
|
||||||
from control import fixed_duty_wrapper
|
from control import fixed_duty_wrapper
|
||||||
from actuators import actuators
|
from actuators import actuators
|
||||||
from utils import consumption_diff, consumption_to_string
|
from utils import consumption_diff, consumption_to_string, stohms
|
||||||
|
|
||||||
MY_GLOBAL_ENV = (
|
MY_GLOBAL_ENV = (
|
||||||
_('make-duty-controller', 3, fixed_duty_wrapper,
|
_('make-duty-controller', 3, fixed_duty_wrapper,
|
||||||
GLOBAL_ENV))
|
_('format-time', 1, lambda x: stohms(x.car),
|
||||||
|
GLOBAL_ENV)))
|
||||||
|
|
||||||
def safe_eval(data, env):
|
def safe_eval(data, env):
|
||||||
try:
|
try:
|
||||||
|
@ -95,8 +96,10 @@ class State():
|
||||||
def getState(self):
|
def getState(self):
|
||||||
return {
|
return {
|
||||||
'phase': self.phase,
|
'phase': self.phase,
|
||||||
'phase-time': self.phase_time(),
|
'phase-time': int(self.phase_time()),
|
||||||
'phase-consumption': self.consumption()
|
'phase-consumption': consumption_to_string(self.phase_consumption()),
|
||||||
|
'recipe-time': int(self.recipe_time()),
|
||||||
|
'recipe-consumption': consumption_to_string(self.recipe_consumption())
|
||||||
}
|
}
|
||||||
|
|
||||||
def now(self):
|
def now(self):
|
||||||
|
@ -107,18 +110,21 @@ class State():
|
||||||
return
|
return
|
||||||
self.phase = 0
|
self.phase = 0
|
||||||
self.env = (
|
self.env = (
|
||||||
_('recipe-name', 0, lambda x: self.recipe.name,
|
_('recipe-name', 0, lambda _: self.recipe.name,
|
||||||
_('recipe-description', 0, lambda x: self.recipe.description,
|
_('recipe-description', 0, lambda _: self.recipe.description,
|
||||||
_('recipe-consumption', 0, lambda x: self.recipe.consumption(),
|
_('recipe-consumption', 0, lambda _: self.recipe_consumption(),
|
||||||
_('time-in-this-phase', 0, lambda x: self.phase_time(),
|
_('recipe-time', 0, lambda _: self.recipe_time(),
|
||||||
_('phase-consumption', 0, lambda x: self.consumption(),
|
_('time-in-this-phase', 0, lambda _: self.phase_time(),
|
||||||
self.baseenv))))))
|
_('phase-consumption', 0, lambda _: self.phase_consumption(),
|
||||||
|
_('phase-time', 0, lambda _: self.phase_time(),
|
||||||
|
self.baseenv))))))))
|
||||||
self.envdata['controllers'] = {}
|
self.envdata['controllers'] = {}
|
||||||
for controller in self.recipe.controllers:
|
for controller in self.recipe.controllers:
|
||||||
name, ctrl = safe_eval(controller, self.env)
|
name, ctrl = safe_eval(controller, self.env)
|
||||||
self.envdata['controllers'][name] = ctrl
|
self.envdata['controllers'][name] = ctrl
|
||||||
self.envdata['sensors'] = {}
|
self.envdata['sensors'] = {}
|
||||||
self.envdata['recipe-start-consumption'] = self.consumption()
|
self.envdata['recipe-start-consumption'] = self.get_total_consumption()
|
||||||
|
self.envdata['current-recipe-loadtime'] = self.now()
|
||||||
self.loadphase(self.phase)
|
self.loadphase(self.phase)
|
||||||
|
|
||||||
def recipeById(self, id):
|
def recipeById(self, id):
|
||||||
|
@ -190,7 +196,12 @@ class State():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def phase_time(self, arg=None):
|
def phase_time(self, arg=None):
|
||||||
return (self.now() - self.envdata.get('current-phase-loadtime', 0))
|
start = self.envdata.get('current-phase-loadtime', 0)
|
||||||
|
return start and self.now() - start
|
||||||
|
|
||||||
|
def recipe_time(self, arg=None):
|
||||||
|
start = self.envdata.get('current-recipe-loadtime', 0)
|
||||||
|
return start and self.now() - start
|
||||||
|
|
||||||
def loadphase(self, phaseidx):
|
def loadphase(self, phaseidx):
|
||||||
phase = self.recipe.phases[phaseidx]
|
phase = self.recipe.phases[phaseidx]
|
||||||
|
@ -240,11 +251,17 @@ class State():
|
||||||
return {actuator: actuators[actuator].consumption() for
|
return {actuator: actuators[actuator].consumption() for
|
||||||
actuator in actuators.keys()}
|
actuator in actuators.keys()}
|
||||||
|
|
||||||
def consumption(self):
|
def phase_consumption(self):
|
||||||
before = self.envdata['current-phase-start-consumption']
|
before = self.envdata.get('current-phase-start-consumption', {})
|
||||||
after = self.get_total_consumption()
|
after = self.get_total_consumption()
|
||||||
return consumption_diff(before, after)
|
return consumption_diff(before, after)
|
||||||
|
|
||||||
|
def recipe_consumption(self):
|
||||||
|
return consumption_diff(
|
||||||
|
self.envdata.get('recipe-start-consumption', {}),
|
||||||
|
self.get_total_consumption())
|
||||||
|
|
||||||
|
|
||||||
class Recipe():
|
class Recipe():
|
||||||
def __init__(self, name, description='',
|
def __init__(self, name, description='',
|
||||||
controllers=(),
|
controllers=(),
|
||||||
|
@ -278,16 +295,12 @@ class Recipe():
|
||||||
# 'phase': self.phase
|
# 'phase': self.phase
|
||||||
}
|
}
|
||||||
|
|
||||||
def consumption(self):
|
|
||||||
return consumption_to_string(consumption_diff(
|
|
||||||
self.envdata['recipe-start-consumption'],
|
|
||||||
self.envdata['recipe-end-consumption']))
|
|
||||||
|
|
||||||
def done(self, env, message=None):
|
def done(self, env, message=None):
|
||||||
message = message or '''
|
message = message or '''
|
||||||
(concat "Recipe " (recipe-name) " completed at " (now) "\n"
|
(concat "Recipe " (recipe-name) " completed at " (now)
|
||||||
"Consumption: " (recipe-consumption))
|
" in " (format-time (recipe-time)) ", Consumption: " (recipe-consumption))
|
||||||
'''
|
'''
|
||||||
|
#
|
||||||
safe_eval(f'(notify {message})', env)
|
safe_eval(f'(notify {message})', env)
|
||||||
|
|
||||||
class Phase():
|
class Phase():
|
||||||
|
@ -305,7 +318,14 @@ class Phase():
|
||||||
return safe_eval(self.nextcond, env)
|
return safe_eval(self.nextcond, env)
|
||||||
|
|
||||||
def exit(self, env):
|
def exit(self, env):
|
||||||
return safe_eval(self.onexit, env)
|
value = safe_eval(self.onexit, env)
|
||||||
|
message = '''
|
||||||
|
(notify
|
||||||
|
(concat "Phase done in " (format-time (phase-time))
|
||||||
|
", with consumption: " (phase-consumption)))
|
||||||
|
'''
|
||||||
|
safe_eval(message, env)
|
||||||
|
return value
|
||||||
|
|
||||||
def revert(self, env):
|
def revert(self, env):
|
||||||
# Undo state changes (attuators values)
|
# Undo state changes (attuators values)
|
||||||
|
|
|
@ -90,11 +90,11 @@ class SHT40(Sensor):
|
||||||
print("ERROR: invalid sensor value: ", what)
|
print("ERROR: invalid sensor value: ", what)
|
||||||
return
|
return
|
||||||
self.measure = what
|
self.measure = what
|
||||||
|
self.heat_every = every
|
||||||
|
if self.is_humidity():
|
||||||
|
self.last_heat = perf_counter()
|
||||||
if not enable_sht:
|
if not enable_sht:
|
||||||
return
|
return
|
||||||
self.heat_every = every
|
|
||||||
if self.measure != "Temperature":
|
|
||||||
self.last_heat = perf_counter()
|
|
||||||
# alternative: HIGHHEAT_1S
|
# alternative: HIGHHEAT_1S
|
||||||
self.heatmode = adafruit_sht4x.Mode.LOWHEAT_100MS
|
self.heatmode = adafruit_sht4x.Mode.LOWHEAT_100MS
|
||||||
self.standardmode = SHT40_DEFAULT
|
self.standardmode = SHT40_DEFAULT
|
||||||
|
@ -114,6 +114,8 @@ class SHT40(Sensor):
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
time = perf_counter()
|
time = perf_counter()
|
||||||
|
if not enable_sht:
|
||||||
|
return (time, None)
|
||||||
reset = False
|
reset = False
|
||||||
if self.is_humidity():
|
if self.is_humidity():
|
||||||
timediff = time - self.last_heat
|
timediff = time - self.last_heat
|
||||||
|
|
7
utils.py
7
utils.py
|
@ -1,7 +1,9 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
def consumption_diff(before, after):
|
def consumption_diff(before, after):
|
||||||
out = {}
|
out = {}
|
||||||
for key in after:
|
for key in after:
|
||||||
out[key] = before.get(key, 0) - after.get(key, 0)
|
out[key] = after.get(key, 0) - before.get(key, 0)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def consumption_to_string(dic):
|
def consumption_to_string(dic):
|
||||||
|
@ -10,3 +12,6 @@ def consumption_to_string(dic):
|
||||||
out += '{0:s}:\t{1:.2f}W\n'.format(key, dic[key])
|
out += '{0:s}:\t{1:.2f}W\n'.format(key, dic[key])
|
||||||
out += 'Total:\t{0:.2f}W'.format(sum(dic.values()))
|
out += 'Total:\t{0:.2f}W'.format(sum(dic.values()))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def stohms(s):
|
||||||
|
return str(timedelta(seconds = s))
|
||||||
|
|
Loading…
Reference in New Issue