compute actuators consumption

utils.py: New file.
This commit is contained in:
Nicolò Balzarotti 2023-02-21 14:00:07 +01:00
parent b9575f8b6e
commit 6cfa299371
6 changed files with 167 additions and 30 deletions

View File

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

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

22
dist/server.js vendored
View File

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

View File

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

View File

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

12
utils.py Normal file
View File

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