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

+ Actuators +

+
+
+
+
+
+

diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..40bff80 --- /dev/null +++ b/utils.py @@ -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