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)
|
||||
|
||||
def read_sensors():
|
||||
periodi_counter = 0
|
||||
periodic_counter = 0
|
||||
scantime = 0.5
|
||||
periodic_state_every_s = 5
|
||||
periodic_state_every_loop = int(periodic_state_every_s/scantime)
|
||||
while True:
|
||||
sensors.read()
|
||||
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:
|
||||
# - current phase (time in this phase)
|
||||
# - actuators (consumption)
|
||||
# Issuing a full state update is easiest
|
||||
updateState()
|
||||
periodi_counter += 1
|
||||
periodic_counter += 1
|
||||
sleep(scantime)
|
||||
|
||||
import threading
|
||||
|
|
|
@ -9,6 +9,8 @@ class FixedDutyCycle(controllers.Controller):
|
|||
self.set_duty(kwargs.pop('duty_perc'))
|
||||
super().__init__(*args, **kwargs)
|
||||
self.last_time = None
|
||||
self.total_failed_read = 0
|
||||
self.failed_read = 0
|
||||
|
||||
def set_duty(self, duty=None):
|
||||
# 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):
|
||||
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 (
|
||||
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):
|
||||
name, duty, period = stringify(args.car, quote=False), args.cdr.car, args.cdr.cdr.car
|
||||
print('name, duty, period', name, duty*100, period)
|
||||
|
|
|
@ -53,7 +53,7 @@ function applyState(newstate) {
|
|||
render_actuators(state.actuators);
|
||||
render_sidebar(state);
|
||||
manual_modal(state.manual);
|
||||
current_recipe(state.recipe);
|
||||
current_recipe(Object.assign({}, state.state, state.recipe));
|
||||
render_load_recipe(state);
|
||||
render_plot();
|
||||
localstate.editing.recipe = state.recipes.findIndex(
|
||||
|
@ -85,6 +85,14 @@ function add_plot_data(newpoints, render=true) {
|
|||
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() {
|
||||
if (enable_draw && document.getElementById("data-plot") !== null) {
|
||||
Plotly.newPlot('data-plot', Object.values(plot_data));
|
||||
|
@ -344,10 +352,17 @@ On Exit: {{onexit}}<br>
|
|||
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) {
|
||||
let html = document.getElementById("current-recipe");
|
||||
if (html === null) return;
|
||||
if (data === null) {
|
||||
if (data === null || data.phases === undefined) {
|
||||
html.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
@ -356,20 +371,26 @@ function current_recipe(data) {
|
|||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Active Recipe: {{name}}
|
||||
Active Recipe ({{recipe-time}}): {{name}}
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{{description}}
|
||||
<br/>
|
||||
{{recipe-consumption}}
|
||||
<br/>
|
||||
|
||||
<div class="tabs is-centered">
|
||||
<ul>
|
||||
{{#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}}
|
||||
</ul>
|
||||
</div>
|
||||
{{current_phase.text}}
|
||||
<br/>
|
||||
{{phase-consumption}}
|
||||
<br/>
|
||||
<br/>
|
||||
Next Cond: {{current_phase.nextcond}}
|
||||
<br/>
|
||||
|
@ -385,6 +406,8 @@ On Exit: {{current_phase.onexit}}
|
|||
class="card-footer-item">Stop</a>
|
||||
</footer>
|
||||
</div>`;
|
||||
data['recipe-time'] = stodate(data['recipe-time'])
|
||||
data['phase-time'] = stodate(data['phase-time'])
|
||||
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 control import fixed_duty_wrapper
|
||||
from actuators import actuators
|
||||
from utils import consumption_diff, consumption_to_string
|
||||
from utils import consumption_diff, consumption_to_string, stohms
|
||||
|
||||
MY_GLOBAL_ENV = (
|
||||
_('make-duty-controller', 3, fixed_duty_wrapper,
|
||||
GLOBAL_ENV))
|
||||
_('format-time', 1, lambda x: stohms(x.car),
|
||||
GLOBAL_ENV)))
|
||||
|
||||
def safe_eval(data, env):
|
||||
try:
|
||||
|
@ -95,8 +96,10 @@ class State():
|
|||
def getState(self):
|
||||
return {
|
||||
'phase': self.phase,
|
||||
'phase-time': self.phase_time(),
|
||||
'phase-consumption': self.consumption()
|
||||
'phase-time': int(self.phase_time()),
|
||||
'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):
|
||||
|
@ -107,18 +110,21 @@ class State():
|
|||
return
|
||||
self.phase = 0
|
||||
self.env = (
|
||||
_('recipe-name', 0, lambda x: self.recipe.name,
|
||||
_('recipe-description', 0, lambda x: self.recipe.description,
|
||||
_('recipe-consumption', 0, lambda x: self.recipe.consumption(),
|
||||
_('time-in-this-phase', 0, lambda x: self.phase_time(),
|
||||
_('phase-consumption', 0, lambda x: self.consumption(),
|
||||
self.baseenv))))))
|
||||
_('recipe-name', 0, lambda _: self.recipe.name,
|
||||
_('recipe-description', 0, lambda _: self.recipe.description,
|
||||
_('recipe-consumption', 0, lambda _: self.recipe_consumption(),
|
||||
_('recipe-time', 0, lambda _: self.recipe_time(),
|
||||
_('time-in-this-phase', 0, lambda _: self.phase_time(),
|
||||
_('phase-consumption', 0, lambda _: self.phase_consumption(),
|
||||
_('phase-time', 0, lambda _: self.phase_time(),
|
||||
self.baseenv))))))))
|
||||
self.envdata['controllers'] = {}
|
||||
for controller in self.recipe.controllers:
|
||||
name, ctrl = safe_eval(controller, self.env)
|
||||
self.envdata['controllers'][name] = ctrl
|
||||
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)
|
||||
|
||||
def recipeById(self, id):
|
||||
|
@ -190,7 +196,12 @@ class State():
|
|||
return 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):
|
||||
phase = self.recipe.phases[phaseidx]
|
||||
|
@ -240,11 +251,17 @@ class State():
|
|||
return {actuator: actuators[actuator].consumption() for
|
||||
actuator in actuators.keys()}
|
||||
|
||||
def consumption(self):
|
||||
before = self.envdata['current-phase-start-consumption']
|
||||
def phase_consumption(self):
|
||||
before = self.envdata.get('current-phase-start-consumption', {})
|
||||
after = self.get_total_consumption()
|
||||
return consumption_diff(before, after)
|
||||
|
||||
def recipe_consumption(self):
|
||||
return consumption_diff(
|
||||
self.envdata.get('recipe-start-consumption', {}),
|
||||
self.get_total_consumption())
|
||||
|
||||
|
||||
class Recipe():
|
||||
def __init__(self, name, description='',
|
||||
controllers=(),
|
||||
|
@ -278,16 +295,12 @@ class Recipe():
|
|||
# '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):
|
||||
message = message or '''
|
||||
(concat "Recipe " (recipe-name) " completed at " (now) "\n"
|
||||
"Consumption: " (recipe-consumption))
|
||||
(concat "Recipe " (recipe-name) " completed at " (now)
|
||||
" in " (format-time (recipe-time)) ", Consumption: " (recipe-consumption))
|
||||
'''
|
||||
#
|
||||
safe_eval(f'(notify {message})', env)
|
||||
|
||||
class Phase():
|
||||
|
@ -305,7 +318,14 @@ class Phase():
|
|||
return safe_eval(self.nextcond, 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):
|
||||
# Undo state changes (attuators values)
|
||||
|
|
|
@ -90,11 +90,11 @@ class SHT40(Sensor):
|
|||
print("ERROR: invalid sensor value: ", what)
|
||||
return
|
||||
self.measure = what
|
||||
self.heat_every = every
|
||||
if self.is_humidity():
|
||||
self.last_heat = perf_counter()
|
||||
if not enable_sht:
|
||||
return
|
||||
self.heat_every = every
|
||||
if self.measure != "Temperature":
|
||||
self.last_heat = perf_counter()
|
||||
# alternative: HIGHHEAT_1S
|
||||
self.heatmode = adafruit_sht4x.Mode.LOWHEAT_100MS
|
||||
self.standardmode = SHT40_DEFAULT
|
||||
|
@ -114,6 +114,8 @@ class SHT40(Sensor):
|
|||
|
||||
def read(self):
|
||||
time = perf_counter()
|
||||
if not enable_sht:
|
||||
return (time, None)
|
||||
reset = False
|
||||
if self.is_humidity():
|
||||
timediff = time - self.last_heat
|
||||
|
|
7
utils.py
7
utils.py
|
@ -1,7 +1,9 @@
|
|||
from datetime import timedelta
|
||||
|
||||
def consumption_diff(before, after):
|
||||
out = {}
|
||||
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
|
||||
|
||||
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 += 'Total:\t{0:.2f}W'.format(sum(dic.values()))
|
||||
return out
|
||||
|
||||
def stohms(s):
|
||||
return str(timedelta(seconds = s))
|
||||
|
|
Loading…
Reference in New Issue