edit recipes

master
Nicolò Balzarotti 2023-01-04 14:52:28 +01:00
parent a2a28007f3
commit 1a15b3caf1
7 changed files with 234 additions and 113 deletions

View File

@ -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
View File

@ -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()

101
dist/server.js vendored
View File

@ -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);

View File

@ -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

View File

@ -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": []
}
]
]

View File

@ -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>

View File

@ -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 %}