hakkoso/dist/server.js

522 lines
16 KiB
JavaScript

var state = {};
var localstate = {
manual: {}, recipe: 0, page: ['General', 'Dashboard'],
maxpoints: 1000,
editing: {}
};
var menu_items = [
{name: "General",
subitems:[
{name: "Dashboard"},
{name: "Sensors & Plots"}
]
},
{name: "Recipes",
subitems:[
{name: "Recipes Editor", link: 'edit-recipe'},
{name: "New Recipe"}
]
},
{name: "Configuration",
subitems:[
{name: "Settings"}
]
}
];
function applyState(newstate) {
if (newstate !== undefined) {
state = JSON.parse(JSON.stringify(newstate));
}
// preprocess
state.manual["dismissed"] = localstate.manual["dismissed"];
let selected_recipe = null;
for (let i = 0; i < state.recipes.length; i++) {
state.recipes[i].idx = i;
if (localstate.recipe == i) selected_recipe = state.recipes[i];
state.recipes[i].selected = localstate.recipe == i;
}
state.selected_recipe = JSON.parse(JSON.stringify(selected_recipe));
for (let i = 0; i < menu_items.length; i++) {
let group_match = menu_items[i].name == localstate.page[0];
for (let sub = 0; sub < menu_items[i].subitems.length; sub++) {
let sub_match = menu_items[i].subitems[sub].name == localstate.page[1];
menu_items[i].subitems[sub].current = group_match && sub_match;
menu_items[i].subitems[sub].path = JSON.stringify([
menu_items[i].name, menu_items[i].subitems[sub].name]);
}
}
state.menu_items = menu_items;
console.log('Apply state '+JSON.stringify(state));
// apply
render_actuators(state.actuators);
render_sidebar(state);
manual_modal(state.manual);
current_recipe(Object.assign({}, state.state, 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;
var plot_data = {};
function toggle_update_plot() {
enable_draw = !enable_draw;
}
function pause_plotting() {enable_draw = false;}
function resume_plotting() {enable_draw = true;}
function set_plot_points(value) {
let current = localstate.maxpoints;
if (value === undefined) {
value = parseInt(document.getElementById('point-number').value);
}
localstate.maxpoints = value;
if (value <= current) {
// we alredy have all the points we want just set it and let other
// function discard extra points
return;
}
// Ask for more data
socket.emit('get sensors history', value);
}
function add_plot_data(newpoints, render=true) {
for (var i = 0; i < newpoints.length; ++i) {
let point = newpoints[i]
let key = point[0];
if (!(key in plot_data)) {
plot_data[key] = {
x: [],
y: [],
type: 'scatter',
name: key
}
}
plot_data[key].x.push(point[1]);
plot_data[key].y.push(point[2]);
// This is different since we want maxpoints to be the absolute max, not
// for each variable, but good enough for now (or the opposite?)
while (plot_data[key].x.length > localstate.maxpoints) {
plot_data[key].x.shift();
plot_data[key].y.shift();
}
}
if (render) render_plot();
}
function render_plot() {
if (enable_draw && document.getElementById("data-plot") !== null) {
Plotly.newPlot('data-plot', Object.values(plot_data));
}
}
function render_actuators(data) {
let html = document.getElementById("actuator-list");
if (data === undefined || html === null) return;
let template = `<div class="content">
Total Consumption: {{total}}Wh<br>
{{#consumption}}
{{name}}: {{Wh}}Wh</br>
{{/consumption}}
</div>`;
let total = 0;
let out = [];
for (var key in data.consumption){
let val = data.consumption[key];
total += val;
out.push({name: key, Wh: val.toFixed(2)});
}
html.innerHTML = Mustache.render(template, { total: total.toFixed(2),
consumption: out });
}
function render_sensors(sensordata) {
let html = document.getElementById("sensor-list");
if (state.sensors === undefined || html === null) return;
let template = `<div class="content">
{{#sensors}}
<h4>{{what}}</h4>
{{^data}}No sensors found{{/data}}
<ul>
{{#data}}
<li ondblclick="alert('rename')">{{0}} -- {{1}} {{unit}}</li>
{{/data}}
</ul>
{{/sensors}}
</div>`;
let data = [];
for (let i = 0, k = Object.keys(state.sensors.units); i < k.length; ++i) {
let what = k[i];
let unit = state.sensors.units[k[i]];
let sensors = Object.keys(
Object.keys(state.sensors.found).reduce(function (filtered, key) {
if (state.sensors.found[key] == what) filtered[key] = state.sensors.found[key];
return filtered;
}, {}));
let req_data = [];
for (let j = 0; j < sensordata.length; ++j) {
if (sensors.includes(sensordata[j][0])) {
req_data.push([sensordata[j][0], sensordata[j][2]]);
}
}
data.push({what: what, unit: unit, data: req_data});
}
html.innerHTML = Mustache.render(template, {sensors:data});
}
function navigate(path, link) {
if (link !== undefined) {
window.location.href = "/" + link
return;
}
localstate.page = path;
applyState();
}
function render_sidebar(data) {
let html = document.getElementById("sidebar");
let template = `<aside class="menu">
{{#menu_items}}
<p class="menu-label">{{name}}</p>
<ul class="menu-list">
{{#subitems}}
<li><a onclick="navigate({{path}}, '{{link}}')"
class="{{#current}}is-active{{/current}}">{{name}}</a></li>
{{/subitems}}
</ul>
{{/menu_items}}
</aside>`
html.innerHTML = Mustache.render(template, data);
}
function respond_manual(response) {
socket.emit('manual response', response);
// Hide the modal, it will appear again if needed
manual_modal({'dismissed': true});
}
function eval_scheme(id) {
socket.emit('eval scheme', document.getElementById(id).value)
}
function select_recipe(idx) {
localstate.recipe = idx;
applyState();
}
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');
}
function maybe_stop_recipe() {
question_modal({required: true, dismissed: false, label: "Stoppiamo?", options: [
{action: "stop_recipe();", tag:"Sicuro"},
{action: "alert('non interrompiamo')", tag:"No"}]});
}
function next_phase() {
socket.emit('next phase');
}
function dismiss_modal() {
applyState(state);
}
function reload_recipes() {
socket.emit('reload recipes');
}
function render_load_recipe(data) {
let html = document.getElementById("load-recipe");
if (html === null) return;
let template = `<div class="card">
<header class="card-header">
<p class="card-header-title">
Load Recipe
</p>
<button class="button is-link"
onclick="reload_recipes();">Reload config</button>
</header>
<div class="card-content">
<div class="tabs is-centered">
<ul>
{{#recipes}}
<li class="{{#selected}}is-active{{/selected}}">
<a onclick="select_recipe({{idx}})">{{name}}</a>
</li>
{{/recipes}}
</ul>
</div>
{{#selected_recipe}}
{{description}}
<br/><br/>
Controllers:<br/>
<ul>
{{#controllers}}
<li>{{.}}</li>
{{/controllers}}
</ul>
{{#phases}}
<br><br>
<div class="content"><h4>{{name}}</h4></div>
{{text}}<br>
On Load: {{onload}}<br>
Exit When: {{nextcond}}<br>
On Exit: {{onexit}}<br>
{{/phases}}
{{/selected_recipe}}
</div>
<footer class="card-footer">
<a onclick="load_recipe()"
class="card-footer-item is-primary">Load</a>
<a onclick="edit_recipe()"
class="card-footer-item">Edit</a>
</footer>
</div>`
html.innerHTML = Mustache.render(template, data);
}
function stodate(value) {
var date = new Date(0);
date.setSeconds(value);
return date.toISOString().substring(11, 19);
}
function current_recipe(data) {
let html = document.getElementById("current-recipe");
if (html === null) return;
if (data === null || data.phases === undefined) {
html.innerHTML = '';
return;
}
data.current_phase = data.phases.find(function (p) { return p.current; });
let template = `
<div class="card">
<header class="card-header">
<p class="card-header-title">
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}}{{#current}} ({{phase-time}}){{/current}}</a></li>
{{/phases}}
</ul>
</div>
{{current_phase.text}}
<br/>
{{phase-consumption}}
<br/>
<br/>
Next Cond: {{current_phase.nextcond}}
<br/>
On Load: {{current_phase.onload}}
<br/>
On Exit: {{current_phase.onexit}}
</div>
<footer class="card-footer">
<!-- <a class="card-footer-item is-primary">Pause</a> -->
<a onclick="next_phase()"
class="card-footer-item">Next Phase</a>
<a onclick="maybe_stop_recipe()"
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);
}
function manual_modal(data) {
let modal = document.getElementById("manual-modal");
let template = `<div class="modal {{^dismissed}}{{#required}}is-active{{/required}}{{/dismissed}}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Manual Intervention Required</p>
<button class="delete" aria-label="close" onclick="dismiss_modal()"></button>
</header>
<section class="modal-card-body">
{{label}}
</section>
<footer class="modal-card-foot">
{{#options}}
<button class="button" onclick="respond_manual('{{value}}')">
{{tag}}
</button>
{{/options}}
</footer>
</div>
</div>`;
modal.innerHTML = Mustache.render(template, data);
}
function question_modal(data) {
let modal = document.getElementById("question-modal");
let template = `<div class="modal {{^dismissed}}{{#required}}is-active{{/required}}{{/dismissed}}">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Manual Intervention Required</p>
<button class="delete" aria-label="close" onclick="dismiss_modal()"></button>
</header>
<section class="modal-card-body">
{{label}}
</section>
<footer class="modal-card-foot">
{{#options}}
<button class="button" onclick="{{action}}">
{{tag}}
</button>
{{/options}}
</footer>
</div>
</div>`;
modal.innerHTML = Mustache.render(template, data);
}
let items = document.querySelectorAll('#items-list > li')
items.forEach(item => {
$(item).prop('draggable', true)
item.addEventListener('dragstart', dragStart)
item.addEventListener('drop', dropped)
item.addEventListener('dragenter', cancelDefault)
item.addEventListener('dragover', cancelDefault)
})
function dragStart (e) {
var index = $(e.target).index()
e.dataTransfer.setData('text/plain', index)
}
function dropped (e) {
cancelDefault(e)
// get new and old index
let oldIndex = e.dataTransfer.getData('text/plain')
let target = $(e.target)
let newIndex = target.index()
// remove dropped items at old place
let dropped = $(this).parent().children().eq(oldIndex).remove()
// insert the dropped items at new place
if (newIndex < oldIndex) {
target.before(dropped)
} else {
target.after(dropped)
}
}
function cancelDefault (e) {
e.preventDefault()
e.stopPropagation()
return false
}