edit recipes
This commit is contained in:
parent
a2a28007f3
commit
1a15b3caf1
|
@ -30,7 +30,7 @@ class GPIOPin(Actuator):
|
|||
|
||||
def enable(self, enable=True):
|
||||
GPIO.output(self.pin, self.onstate) if enable else self.disable()
|
||||
|
||||
|
||||
|
||||
def disable(self):
|
||||
GPIO.output(self.pin, self.offstate)
|
||||
|
@ -53,7 +53,5 @@ class MockPIN(Actuator):
|
|||
# print('FAKE DISABLE')
|
||||
|
||||
actuators = {
|
||||
'heater': GPIOPin(
|
||||
pin=22, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH) if (
|
||||
rpi) else MockPIN(pin=22, initial=False, onstate=True, offstate=False),
|
||||
'heater': (GPIOPin if rpi else MockPIN)(pin=22, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH),
|
||||
}
|
||||
|
|
19
app.py
19
app.py
|
@ -51,9 +51,14 @@ def make_state():
|
|||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/recipe-editor')
|
||||
def recipe_editor():
|
||||
return render_template('recipe-editor.html')
|
||||
@app.route('/edit-recipe/<id>')
|
||||
def recipe_editor(id):
|
||||
return render_template('recipe-editor.html',
|
||||
recipe=statemachine.recipeById(id))
|
||||
|
||||
@app.route('/edit-recipe/')
|
||||
def recipes_list():
|
||||
return render_template('recipes-list.html', recipes=statemachine.recipes)
|
||||
|
||||
@app.route('/status')
|
||||
def status():
|
||||
|
@ -88,6 +93,14 @@ def handle_manual_response(response):
|
|||
def load_recipe_idx(idx):
|
||||
statemachine.loadByIdx(idx)
|
||||
|
||||
@socketio.on('update recipe phase')
|
||||
def update_recipe_phase(recipeid, phaseid, content):
|
||||
recipe = statemachine.recipeById(recipeid)
|
||||
recipe.phases[phaseid] = phasectrl.load_phase(content)
|
||||
statemachine.recipes[recipeid] = recipe
|
||||
phasectrl.store_recipes("recipes.json",
|
||||
[r.getState() for r in statemachine.recipes])
|
||||
|
||||
@socketio.on('stop recipe')
|
||||
def stop_recipe():
|
||||
statemachine.stop()
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
var state = {};
|
||||
var localstate = {
|
||||
manual: {}, recipe: 0, page: ['General', 'Dashboard'],
|
||||
maxpoints: 1000
|
||||
maxpoints: 1000,
|
||||
editing: {}
|
||||
};
|
||||
var menu_items = [
|
||||
{name: "General",
|
||||
|
@ -12,7 +13,7 @@ var menu_items = [
|
|||
},
|
||||
{name: "Recipes",
|
||||
subitems:[
|
||||
{name: "Recipes Editor", link: 'recipe-editor'},
|
||||
{name: "Recipes Editor", link: 'edit-recipe'},
|
||||
{name: "New Recipe"}
|
||||
]
|
||||
},
|
||||
|
@ -54,6 +55,8 @@ function applyState(newstate) {
|
|||
current_recipe(state.recipe);
|
||||
render_load_recipe(state);
|
||||
render_plot();
|
||||
localstate.editing.recipe = state.recipes.findIndex(
|
||||
function (el) {return el.name == localstate.editing.recipe_name;});
|
||||
}
|
||||
|
||||
var enable_draw = true;
|
||||
|
@ -124,7 +127,7 @@ function render_sensors(sensordata) {
|
|||
|
||||
function navigate(path, link) {
|
||||
if (link !== undefined) {
|
||||
window.location.href = "./" + link
|
||||
window.location.href = "/" + link
|
||||
return;
|
||||
}
|
||||
localstate.page = path;
|
||||
|
@ -160,6 +163,93 @@ function load_recipe() {
|
|||
socket.emit('load recipe idx', localstate.recipe);
|
||||
}
|
||||
|
||||
function edit_recipe(recipeid) {
|
||||
let recipe = recipeid === undefined ? localstate.recipe : recipeid;
|
||||
window.location.href = '/edit-recipe/'+recipe;
|
||||
}
|
||||
|
||||
function currently_editing_recipe(name) {
|
||||
localstate.editing.recipe_name = name;
|
||||
}
|
||||
|
||||
function update_field(fieldname, newvalue) {
|
||||
state.recipes[localstate.editing.recipe].phases[localstate.editing.phase][fieldname] = newvalue;
|
||||
console.log(state.recipes[localstate.editing.recipe].phases[localstate.editing.phase]);
|
||||
console.log(fieldname, newvalue);
|
||||
}
|
||||
|
||||
function save_phase() {
|
||||
let edited_phase = state.recipes[localstate.editing.recipe].phases[
|
||||
localstate.editing.phase];
|
||||
console.log(edited_phase);
|
||||
socket.emit('update recipe phase',
|
||||
localstate.editing.recipe, localstate.editing.phase, edited_phase)
|
||||
}
|
||||
|
||||
function edit_phase(phaseid) {
|
||||
console.log(localstate);
|
||||
let html = document.getElementById("phase-editor");
|
||||
if (html === null) return;
|
||||
localstate.editing.phase = phaseid;
|
||||
data = state.recipes[localstate.editing.recipe].phases[phaseid];
|
||||
console.log(data.text);
|
||||
let template = `<div class="field">
|
||||
<label class="label">Phase Name</label>
|
||||
<div class="control">
|
||||
<input class="input"
|
||||
oninput="update_field('name', this.value)"
|
||||
type="text" placeholder="Phase Name" value="{{name}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Phase Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Phase Description"
|
||||
oninput="update_field('text', this.value)">{{text}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">On Load</label>
|
||||
<div class="control">
|
||||
<textarea
|
||||
oninput="update_field('onload', this.value)"
|
||||
class="textarea" placeholder="#t">{{onload}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Next Condition</label>
|
||||
<div class="control">
|
||||
<textarea oninput="update_field('nextcond', this.value)"
|
||||
class="textarea" placeholder="#t">{{nextcond}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">On Exit</label>
|
||||
<div class="control">
|
||||
<textarea oninput="update_field('onexit', this.value)"
|
||||
class="textarea" placeholder="#t">{{onexit}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-link"
|
||||
onclick="save_phase();">Save</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-link is-light"
|
||||
onclick="stop_editing_phase();">Cancel</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-danger"
|
||||
onclick="delete_phase();">Delete</button>
|
||||
</div>
|
||||
</div>`;
|
||||
html.innerHTML = Mustache.render(template, data);
|
||||
}
|
||||
|
||||
function stop_recipe() {
|
||||
socket.emit('stop recipe');
|
||||
}
|
||||
|
@ -193,6 +283,7 @@ function render_load_recipe(data) {
|
|||
</div>
|
||||
{{#selected_recipe}}
|
||||
{{description}}
|
||||
<br/><br/>
|
||||
Controllers:<br/>
|
||||
<ul>
|
||||
{{#controllers}}
|
||||
|
@ -203,7 +294,6 @@ Controllers:<br/>
|
|||
<br><br>
|
||||
<div class="content"><h4>{{name}}</h4></div>
|
||||
{{text}}<br>
|
||||
Ranges: FIXME<br>
|
||||
On Load: {{onload}}<br>
|
||||
Exit When: {{nextcond}}<br>
|
||||
On Exit: {{onexit}}<br>
|
||||
|
@ -213,7 +303,8 @@ On Exit: {{onexit}}<br>
|
|||
<footer class="card-footer">
|
||||
<a onclick="load_recipe()"
|
||||
class="card-footer-item is-primary">Load</a>
|
||||
<a class="card-footer-item">Edit</a>
|
||||
<a onclick="edit_recipe()"
|
||||
class="card-footer-item">Edit</a>
|
||||
</footer>
|
||||
</div>`
|
||||
html.innerHTML = Mustache.render(template, data);
|
||||
|
|
44
phasectrl.py
44
phasectrl.py
|
@ -20,24 +20,30 @@ def safe_eval(data, env):
|
|||
chat.send_sync(err)
|
||||
return True
|
||||
|
||||
def load_phase(json):
|
||||
def fallback(v):
|
||||
return json.get(v, '#t')
|
||||
return Phase(
|
||||
json['name'], json['text'],
|
||||
onexit=fallback('on_exit'),
|
||||
onload=fallback('on_load'),
|
||||
nextcond=fallback('exit_condition'))
|
||||
|
||||
def load_recipe(json):
|
||||
def load_phase(json):
|
||||
def fallback(v):
|
||||
return json.get(v, '#t')
|
||||
return Phase(
|
||||
json['name'], json['description'],
|
||||
onexit=fallback('on_exit'),
|
||||
onload=fallback('on_load'),
|
||||
nextcond=fallback('exit_condition'))
|
||||
return Recipe(
|
||||
json['name'], json['variant'], json['description'],
|
||||
json['name'], json['description'],
|
||||
json.get('controllers', ()),
|
||||
tuple(load_phase(p) for p in json['phases']))
|
||||
[load_phase(p) for p in json['phases']])
|
||||
|
||||
def load_recipes(file):
|
||||
with open(file, "r") as f:
|
||||
recipes = json.loads(f.read())
|
||||
return tuple(load_recipe(recipe) for recipe in recipes)
|
||||
return [load_recipe(recipe) for recipe in recipes]
|
||||
|
||||
def store_recipes(file, recipes):
|
||||
print(file, recipes)
|
||||
with open(file, "w") as f:
|
||||
f.write(json.dumps(recipes, indent=2))
|
||||
|
||||
def manual_intervention_wrapper(state):
|
||||
def wrapper(args):
|
||||
|
@ -101,8 +107,18 @@ class State():
|
|||
self.envdata['sensors'] = {}
|
||||
self.loadphase(self.phase)
|
||||
|
||||
def recipeById(self, id):
|
||||
try:
|
||||
id = int(id)
|
||||
except:
|
||||
id = -1
|
||||
print(self.recipes)
|
||||
if id > -1 and id < len(self.recipes):
|
||||
return self.recipes[id]
|
||||
return None
|
||||
|
||||
def loadByIdx(self, recipe):
|
||||
self.recipe = self.recipes[recipe]
|
||||
self.recipe = self.recipeById(recipe)
|
||||
self.postload()
|
||||
|
||||
def stop(self):
|
||||
|
@ -192,9 +208,9 @@ class State():
|
|||
'(', self.envdata['controllers'][controller], ')')
|
||||
|
||||
class Recipe():
|
||||
def __init__(self, name, variant='default', description='',
|
||||
def __init__(self, name, description='',
|
||||
controllers=(),
|
||||
phases=()):
|
||||
phases=[]):
|
||||
self.name = name
|
||||
self.controllers = controllers
|
||||
self.description = description
|
||||
|
|
66
recipes.json
66
recipes.json
|
@ -1,90 +1,92 @@
|
|||
[
|
||||
{
|
||||
"name": "Tepache",
|
||||
"variant": "default",
|
||||
"description": "Tiene il tepache a ~23 gradi",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "Tiene il tepache a ~23 gradi",
|
||||
"phases": [
|
||||
{
|
||||
"name": "Hold",
|
||||
"description": "Keep 23°C",
|
||||
"on_load": "(begin (set-controller \"heater\" \"T_food\")\n(set-target \"T_food\" 23.0))",
|
||||
"exit_condition": "(> (time-in-this-phase) (hours 10))"
|
||||
"text": "Keep 23\u00b0C",
|
||||
"nextcond": "#t",
|
||||
"current": false,
|
||||
"onload": "#t",
|
||||
"onexit": "#t"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Yogurt",
|
||||
"variant": "default",
|
||||
"description": "Pastorizza lo yogurt a 82 gradi, lascia raffreddare per fare l'inoculo, mantiene a 42 gradi fino a che \u00e8 pronto.",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "Pastorizza lo yogurt a 82 gradi, lascia raffreddare per fare l'inoculo, mantiene a 42 gradi fino a che è pronto.",
|
||||
"phases": [
|
||||
{
|
||||
"name": "Preheat",
|
||||
"description": "Preheat yogurt to 82°",
|
||||
"on_load": "(begin (set-controller \"heater\" \"T_food\")\n(set-target \"T_food\" 82.0))",
|
||||
"exit_condition": "(and (>= (get-sensor \"T_food\") 81.5)\n(> (time-in-this-phase) (minutes 15)))"
|
||||
"text": "Preheat yogurt to 82\u00b0",
|
||||
"nextcond": "#t",
|
||||
"current": false,
|
||||
"onload": "#t",
|
||||
"onexit": "#t"
|
||||
},
|
||||
{
|
||||
"name": "Cool",
|
||||
"description": "Cool to 46°",
|
||||
"on_load": "(set-target \"T_food\" 46.0)",
|
||||
"exit_condition": "(and (< (get-sensor \"T_food\") 46.0)\n(> (get-sensor \"T_food\") 42.0))"
|
||||
"text": "Cool to 46\u00b0",
|
||||
"nextcond": "#t",
|
||||
"current": false,
|
||||
"onload": "#t",
|
||||
"onexit": "#t"
|
||||
},
|
||||
{
|
||||
"name": "Inoculate",
|
||||
"description": "Add 50ml Yogurt",
|
||||
"on_load": "(notify \"Inoculate 50g of yogurt NOW!\")",
|
||||
"exit_condition": "(manual-intervention \"Inocula 50g di yogurt\" '((\"Fatto\" #t)))",
|
||||
"on_exit": "(notify (concat \"Inoculated at \" (now)))"
|
||||
"name": "Inoculate (Backslopping)",
|
||||
"text": "Add 50ml of an old Yogurt batch",
|
||||
"nextcond": "#t",
|
||||
"current": false,
|
||||
"onload": "#t",
|
||||
"onexit": "#t"
|
||||
},
|
||||
{
|
||||
"name": "Hold",
|
||||
"description": "Keep at 42° for 3h",
|
||||
"on_load": "(set-target \"T_food\" 41.0)",
|
||||
"exit_condition": "(> (time-in-this-phase) (hours 3))",
|
||||
"on_exit": "(notify \"You can remove the yogurt from the heater\")"
|
||||
"text": "Keep at 42\u00b0 for 3h",
|
||||
"nextcond": "#t",
|
||||
"current": false,
|
||||
"onload": "#t",
|
||||
"onexit": "#t"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Nattō",
|
||||
"variant": "default",
|
||||
"name": "Natt\u014d",
|
||||
"description": "FIXME",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
},
|
||||
{
|
||||
"name": "Tempeh",
|
||||
"variant": "default",
|
||||
"description": "FIXME",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
},
|
||||
{
|
||||
"name": "Koji Rice",
|
||||
"variant": "default",
|
||||
"description": "FIXME",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
},
|
||||
{
|
||||
"name": "Gorgonzola",
|
||||
"variant": "default",
|
||||
"description": "FIXME",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
}
|
||||
]
|
||||
]
|
|
@ -5,86 +5,50 @@
|
|||
{% endblock %}
|
||||
{% block content %}
|
||||
<!-- https://codepen.io/PJCHENder/pen/PKBVRO/ -->
|
||||
<ul id="items-list" class="moveable">
|
||||
<li>One</li>
|
||||
<li>Two</li>
|
||||
<li>Three</li>
|
||||
<li>Four</li>
|
||||
</ul>
|
||||
<!-- <ul id="items-list" class="moveable"> -->
|
||||
<!-- <li>One</li> -->
|
||||
<!-- <li>Two</li> -->
|
||||
<!-- <li>Three</li> -->
|
||||
<!-- <li>Four</li> -->
|
||||
<!-- </ul> -->
|
||||
<script>currently_editing_recipe('{{recipe.name}}');</script>
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label">Recipe Name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Text input">
|
||||
<input class="input" type="text" placeholder="Text input"
|
||||
value="{{recipe.name}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Recipe Description"></textarea>
|
||||
<textarea class="textarea" placeholder="Recipe Description">
|
||||
{{recipe.description}}
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
Phase list:
|
||||
{% for phase in recipe.phases %}
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-vertical">
|
||||
<div class="tile">
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child notification is-primary">
|
||||
<article
|
||||
onclick="edit_phase({{loop.index0}});"
|
||||
class="tile is-child notification is-primary">
|
||||
<p class="title"></p>
|
||||
<p class="subtitle">Top tile</p>
|
||||
<p class="subtitle">{{ phase.name }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label">Phase Name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Phase Name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Phase Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Phase Description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">On Load</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="#t"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Next Condition</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="(> (time-in-this-phase) (minutes 1))"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">On Exit</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="#t"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-link">Submit</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-link is-light">Cancel</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half" id="phase-editor">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h1>{% block title %}Hakkoso{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<button class="button is-link">New Recipe</button>
|
||||
<br/>
|
||||
<br/>
|
||||
{% for recipe in recipes %}
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-vertical">
|
||||
<div class="tile">
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article
|
||||
onclick="edit_recipe({{loop.index0}});"
|
||||
class="tile is-child notification is-primary">
|
||||
<p class="title">{{ recipe.name }}</p>
|
||||
<p class="subtitle">
|
||||
{% for phase in recipe.phases %}
|
||||
{{ phase.name }}
|
||||
{% endfor %}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="column is-half" id="phase-editor">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue