parent
b9575f8b6e
commit
6cfa299371
76
actuators.py
76
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),
|
||||
}
|
||||
|
|
5
app.py
5
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = `<div class="content">
|
||||
Total Consumption: {{total}}W<br>
|
||||
{{#consumption}}
|
||||
{{name}}: {{W}}W</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, 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;
|
||||
|
|
71
phasectrl.py
71
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():
|
||||
|
|
|
@ -18,6 +18,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Actuators
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div id="actuator-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
def consumption_diff(before, after):
|
||||
out = {}
|
||||
for key in after:
|
||||
out[key] = before.get(key, 0) - after.get(key, 0)
|
||||
return out
|
||||
|
||||
def consumption_to_string(dic):
|
||||
out = ''
|
||||
for key in 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
|
Loading…
Reference in New Issue