diff --git a/actuators.py b/actuators.py
index 55cbada..2c2bf46 100644
--- a/actuators.py
+++ b/actuators.py
@@ -1,23 +1,54 @@
import atexit
+from time import perf_counter
try:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
rpi = True
except:
rpi = False
+ class GPIO():
+ LOW = 0
+ HIGH = 1
class Actuator():
- def __init__(self):
- pass
+ def __init__(self, *args, **kwargs):
+ self.wattage = kwargs.pop('wattage')
+ self.total_seconds_on = 0
+ self.logging = False
+ self.started_at = 0
+ def log_on(self):
+ if self.logging:
+ return
+ self.started_at = perf_counter()
+ self.logging = True
+
+ def elapsed(self):
+ return perf_counter() - self.started_at
+
+ def log_off(self):
+ if not self.logging:
+ return
+ self.total_seconds_on += self.elapsed()
+ self.logging = False
+
+ def s_to_hr(self, s):
+ return s / 60 / 60
+
+ def consumption(self):
+ total_time = self.total_seconds_on
+ if self.logging:
+ # If we are on, count this time too
+ total_time += self.elapsed()
+ return self.s_to_hr(total_time) * self.wattage
class GPIOPin(Actuator):
def __init__(self, *args, **kwargs):
- self.pin = (kwargs.pop('pin'))
- self.initial = (kwargs.pop('initial'))
- self.onstate = (kwargs.pop('onstate'))
- self.offstate = (kwargs.pop('offstate'))
+ self.pin = kwargs.pop('pin')
+ self.initial = kwargs.pop('initial')
+ self.onstate = kwargs.pop('onstate')
+ self.offstate = kwargs.pop('offstate')
GPIO.setup(self.pin, GPIO.OUT, initial=self.initial)
atexit.register(self.deinit)
super().__init__(*args, **kwargs)
@@ -30,29 +61,42 @@ class GPIOPin(Actuator):
def enable(self, enable=True):
GPIO.output(self.pin, self.onstate) if enable else self.disable()
+ if enable:
+ self.log_on()
+ else:
+ self.log_off()
def disable(self):
GPIO.output(self.pin, self.offstate)
+ self.log_off()
class MockPIN(Actuator):
def __init__(self, *args, **kwargs):
- self.pin = (kwargs.pop('pin'))
- self.initial = (kwargs.pop('initial'))
- self.onstate = (kwargs.pop('onstate'))
- self.offstate = (kwargs.pop('offstate'))
+ self.pin = kwargs.pop('pin')
+ self.initial = kwargs.pop('initial')
+ self.onstate = kwargs.pop('onstate')
+ self.offstate = kwargs.pop('offstate')
super().__init__(*args, **kwargs)
def enable(self, enable=True):
- pass
+ if enable:
+ self.log_on()
+ else:
+ self.log_off()
# print('FAKE ENABLE') if enable else self.disable()
-
def disable(self):
- pass
+ self.log_off()
# print('FAKE DISABLE')
+def PIN(*args, **kwargs):
+ return (GPIOPin if rpi else MockPIN)(*args, **kwargs)
+
actuators = {
- 'heater': (GPIOPin if rpi else MockPIN)(pin=22, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH),
- 'fan': (GPIOPin if rpi else MockPIN)(pin=27, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH),
- 'humidifier': (GPIOPin if rpi else MockPIN)(pin=17, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH),
+ 'heater': PIN(pin=22, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH,
+ wattage=100),
+ 'fan': PIN(pin=27, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH,
+ wattage=12*0.4),
+ 'humidifier': PIN(pin=17, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH,
+ wattage=30),
}
diff --git a/app.py b/app.py
index 0018bff..c2fbb6a 100644
--- a/app.py
+++ b/app.py
@@ -32,7 +32,9 @@ statemachine = State('recipes.json', updateState)
def make_state():
return {
'manual': statemachine.manual.getState(),
+ # TODO: Add current recipe time, consumption
'recipes': tuple(r.getState() for r in statemachine.recipes),
+ # TODO: Add current phase time, consumption
'recipe': (statemachine.recipe.getState(statemachine.phase)
if statemachine.recipe else None),
'state': statemachine.getState(),
@@ -45,6 +47,9 @@ def make_state():
'Switch': 'On/Off',
},
'found': sensors.list()
+ },
+ 'actuators': {
+ 'consumption': statemachine.get_total_consumption()
}
}
diff --git a/dist/server.js b/dist/server.js
index f82c585..d06da94 100644
--- a/dist/server.js
+++ b/dist/server.js
@@ -50,6 +50,7 @@ function applyState(newstate) {
console.log('Apply state '+JSON.stringify(state));
// apply
+ render_actuators(state.actuators);
render_sidebar(state);
manual_modal(state.manual);
current_recipe(state.recipe);
@@ -91,6 +92,27 @@ function render_plot() {
}
+function render_actuators(data) {
+ let html = document.getElementById("actuator-list");
+ if (data === undefined || html === null) return;
+ let template = `
+ Total Consumption: {{total}}W
+ {{#consumption}}
+ {{name}}: {{W}}W
+ {{/consumption}}
+
`;
+ let total = 0;
+ let out = [];
+ for (var key in data.consumption){
+ let val = data.consumption[key];
+ total += val;
+ out.push({name: key, W: 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;
diff --git a/phasectrl.py b/phasectrl.py
index ba24437..dde9e85 100644
--- a/phasectrl.py
+++ b/phasectrl.py
@@ -7,6 +7,7 @@ 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
MY_GLOBAL_ENV = (
_('make-duty-controller', 3, fixed_duty_wrapper,
@@ -66,26 +67,36 @@ def manual_intervention_wrapper(state):
class State():
def __init__(self, recipe_file, onupdate=lambda: None):
self.recipes = load_recipes(recipe_file)
+ self.recipe_file = recipe_file
self.recipe = None
self.phase = None
self.manual = ManualIntervention()
self.baseenv = (
_('get-sensor', 1, lambda x: get_sensor_value(stringify(x.car, quote=False)),
- _('notify', 1, lambda x: chat.send_sync(stringify(x.car, quote=False)),
- _('hours', 1, lambda x: x.car * 60 * 60,
- _('minutes', 1, lambda x: x.car * 60,
- _('seconds', 1, lambda x: x.car,
- _('manual-intervention', -1, manual_intervention_wrapper(self),
- _('set-target', 2, self.set_target,
- _('set-controller', 2, self.set_controller,
- MY_GLOBAL_ENV)))))))))
+ _('set-actuator', 2, self.set_actuator,
+ _('notify', 1, lambda x: chat.send_sync(stringify(x.car, quote=False)),
+ _('hours', 1, lambda x: x.car * 60 * 60,
+ _('minutes', 1, lambda x: x.car * 60,
+ _('seconds', 1, lambda x: x.car,
+ _('manual-intervention', -1, manual_intervention_wrapper(self),
+ _('set-target', 2, self.set_target,
+ _('set-controller', 2, self.set_controller,
+ _('reload-recipes', 0, lambda x: self.reloadRecipes(),
+ MY_GLOBAL_ENV)))))))))))
self.env = self.baseenv
self.envdata = {}
self.onupdate = onupdate
+ def reloadRecipes(self):
+ # chat.send_sync("Reloading recipes!")
+ print("Reloading recipes!")
+ self.recipes = load_recipes(self.recipe_file)
+
def getState(self):
return {
- 'phase': self.phase
+ 'phase': self.phase,
+ 'phase-time': self.phase_time(),
+ 'phase-consumption': self.consumption()
}
def now(self):
@@ -98,13 +109,16 @@ class State():
self.env = (
_('recipe-name', 0, lambda x: self.recipe.name,
_('recipe-description', 0, lambda x: self.recipe.description,
- _('time-in-this-phase', 0, lambda x: self.phase_time(),
- self.baseenv))))
+ _('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))))))
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.loadphase(self.phase)
def recipeById(self, id):
@@ -155,8 +169,8 @@ class State():
continue
response = ctrl.apply(self.envdata['sensors'][ctrl.input_label][0])
actuators[controller].enable(response)
- except:
- print("UNKNOWN BUG HERE in run_step")
+ except Exception as e:
+ print(f"UNKNOWN BUG HERE in run_step {e}")
if self.check():
if self.next() is None:
return True
@@ -165,6 +179,7 @@ class State():
def next(self):
self.current_phase().exit(self.env)
+
if self.phase < len(self.recipe.phases) - 1:
self.phase += 1
phase = self.recipe.phases[self.phase]
@@ -180,11 +195,13 @@ class State():
def loadphase(self, phaseidx):
phase = self.recipe.phases[phaseidx]
self.envdata['current-phase-loadtime'] = self.now()
+ self.envdata['current-phase-start-consumption'] = self.get_total_consumption()
resp = safe_eval(phase.onload, self.env)
self.onupdate()
return resp
def done(self, message=None):
+ self.envdata['recipe-done-consumption'] = self.get_total_consumption()
self.recipe.done(self.env, message)
# Stop all actuators
for controller in self.envdata.get('controllers', {}).keys():
@@ -210,6 +227,24 @@ class State():
print('linking sensor', sensor, 'to controller', controller,
'(', self.envdata['controllers'][controller], ')')
+ def set_actuator(self, args):
+ actuator, value = (stringify(args.car, quote=False), args.cdr.car)
+ if value:
+ actuators[actuator].enable()
+ else:
+ actuators[actuator].disable()
+ print('setting actuator ', actuator, 'to value', value,
+ ', remember to change it manually if stopping?')
+
+ def get_total_consumption(self):
+ return {actuator: actuators[actuator].consumption() for
+ actuator in actuators.keys()}
+
+ def consumption(self):
+ before = self.envdata['current-phase-start-consumption']
+ after = self.get_total_consumption()
+ return consumption_diff(before, after)
+
class Recipe():
def __init__(self, name, description='',
controllers=(),
@@ -243,8 +278,16 @@ 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))'
+ message = message or '''
+ (concat "Recipe " (recipe-name) " completed at " (now) "\n"
+ "Consumption: " (recipe-consumption))
+ '''
safe_eval(f'(notify {message})', env)
class Phase():
diff --git a/templates/index.html b/templates/index.html
index e230286..3cf05d9 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -18,6 +18,17 @@
+
+