| @@ -8,3 +8,7 @@ craftbeerpi.db | |||
| modules/plugins/* | |||
| !modules/plugins/__init__.py | |||
| *.pyc | |||
| *.js | |||
| modules/ui/package.json | |||
| modules/ui/.babelrc | |||
| @@ -86,6 +86,9 @@ INSERT INTO config VALUES ('unit', 'C', 'select', 'Temperature Unit', '["C","F"] | |||
| INSERT INTO config VALUES ('brewery_name', 'My Home Brewery', 'text', 'Your brewery name', NULL ); | |||
| INSERT INTO config VALUES ('buzzer', 16, 'select', 'Buzzer GPIO', '[16,17,18,19,20]'); | |||
| INSERT INTO config VALUES ('setup', 'YES', 'select', 'Show the Setup dialog', '["YES","NO"]'); | |||
| INSERT INTO config VALUES ('brew_name', '', 'text', 'Brew Name', NULL); | |||
| INSERT INTO config VALUES ('donation_notification', 'YES', 'select', 'Disable Donation Notification', '["YES","NO"]'); | |||
| CREATE TABLE actor | |||
| ( | |||
| @@ -0,0 +1 @@ | |||
| 3.0.1 | |||
| @@ -63,3 +63,6 @@ cbpi.run_background_processes() | |||
| app.logger.info("##########################################") | |||
| app.logger.info("### STARTUP COMPLETE") | |||
| app.logger.info("##########################################") | |||
| @@ -15,7 +15,9 @@ from modules.core.db import DBModel | |||
| app = Flask(__name__) | |||
| logging.basicConfig(filename='./logs/app.log',level=logging.INFO) | |||
| FORMAT = '%(asctime)-15s - %(levelname)s - %(message)s' | |||
| logging.basicConfig(filename='./logs/app.log',level=logging.INFO, format=FORMAT) | |||
| app.config['SECRET_KEY'] = 'craftbeerpi' | |||
| app.config['UPLOAD_FOLDER'] = './upload' | |||
| @@ -45,7 +47,7 @@ class ComplexEncoder(json.JSONEncoder): | |||
| return None | |||
| app.json_encoder = ComplexEncoder | |||
| socketio = SocketIO(app, json=json) | |||
| socketio = SocketIO(app, json=json, logging=False) | |||
| cbpi = CraftBeerPi(app, socketio) | |||
| app.logger.info("##########################################") | |||
| @@ -7,6 +7,7 @@ from modules.core.step import StepBase | |||
| from modules import cbpi | |||
| @cbpi.step | |||
| class MashStep(StepBase): | |||
| ''' | |||
| @@ -69,6 +70,10 @@ class MashInStep(StepBase): | |||
| kettle = StepProperty.Kettle("Kettle") | |||
| s = False | |||
| @cbpi.action("Change Power") | |||
| def change_power(self): | |||
| self.actor_power(1, 50) | |||
| def init(self): | |||
| ''' | |||
| Initialize Step. This method is called once at the beginning of the step | |||
| @@ -93,3 +98,125 @@ class MashInStep(StepBase): | |||
| @cbpi.step | |||
| class ChilStep(StepBase): | |||
| timer = Property.Number("Timer in Minutes", configurable=True, default_value=0) | |||
| @cbpi.action("Stat Timer") | |||
| def start(self): | |||
| if self.is_timer_finished() is None: | |||
| self.start_timer(int(self.timer) * 60) | |||
| def reset(self): | |||
| self.stop_timer() | |||
| def finish(self): | |||
| pass | |||
| def execute(self): | |||
| if self.is_timer_finished() is None: | |||
| self.start_timer(int(self.timer) * 60) | |||
| if self.is_timer_finished() == True: | |||
| self.next() | |||
| @cbpi.step | |||
| class PumpStep(StepBase): | |||
| pump = StepProperty.Actor("Pump") | |||
| timer = Property.Number("Timer in Minutes", configurable=True, default_value=0) | |||
| @cbpi.action("Stat Timer") | |||
| def start(self): | |||
| if self.is_timer_finished() is None: | |||
| self.start_timer(int(self.timer) * 60) | |||
| def reset(self): | |||
| self.stop_timer() | |||
| def finish(self): | |||
| self.actor_off(int(self.pump)) | |||
| def init(self): | |||
| self.actor_on(int(self.pump)) | |||
| def execute(self): | |||
| if self.is_timer_finished() is None: | |||
| self.start_timer(int(self.timer) * 60) | |||
| if self.is_timer_finished() == True: | |||
| self.next() | |||
| @cbpi.step | |||
| class BoilStep(StepBase): | |||
| ''' | |||
| Just put the decorator @cbpi.step on top of a method | |||
| ''' | |||
| # Properties | |||
| temp = Property.Number("Temperature", configurable=True, default_value=100) | |||
| kettle = StepProperty.Kettle("Kettle") | |||
| timer = Property.Number("Timer in Minutes", configurable=True, default_value=90) | |||
| hop_1 = Property.Number("Hop 1 Addition", configurable=True) | |||
| hop_1_added = Property.Number("",default_value=None) | |||
| hop_2 = Property.Number("Hop 2 Addition", configurable=True) | |||
| hop_2_added = Property.Number("", default_value=None) | |||
| hop_3 = Property.Number("Hop 3 Addition", configurable=True) | |||
| hop_3_added = Property.Number("", default_value=None) | |||
| def init(self): | |||
| ''' | |||
| Initialize Step. This method is called once at the beginning of the step | |||
| :return: | |||
| ''' | |||
| # set target tep | |||
| self.set_target_temp(self.temp, self.kettle) | |||
| @cbpi.action("Start Timer Now") | |||
| def start(self): | |||
| ''' | |||
| Custom Action which can be execute form the brewing dashboard. | |||
| All method with decorator @cbpi.action("YOUR CUSTOM NAME") will be available in the user interface | |||
| :return: | |||
| ''' | |||
| if self.is_timer_finished() is None: | |||
| self.start_timer(int(self.timer) * 60) | |||
| def reset(self): | |||
| self.stop_timer() | |||
| self.set_target_temp(self.temp, self.kettle) | |||
| def finish(self): | |||
| self.set_target_temp(0, self.kettle) | |||
| def check_hop_timer(self, number, value): | |||
| if self.__getattribute__("hop_%s_added" % number) is not True and time.time() > ( | |||
| self.timer_end - (int(self.timer) * 60 - int(value) * 60)): | |||
| self.__setattr__("hop_%s_added" % number, True) | |||
| self.notify("Hop Alert", "Please add Hop %s" % number, timeout=None) | |||
| def execute(self): | |||
| ''' | |||
| This method is execute in an interval | |||
| :return: | |||
| ''' | |||
| # Check if Target Temp is reached | |||
| if self.get_kettle_temp(self.kettle) >= int(self.temp): | |||
| # Check if Timer is Running | |||
| if self.is_timer_finished() is None: | |||
| self.start_timer(int(self.timer) * 60) | |||
| else: | |||
| self.check_hop_timer(1, self.hop_1) | |||
| self.check_hop_timer(2, self.hop_2) | |||
| self.check_hop_timer(3, self.hop_3) | |||
| # Check if timer finished and go to next step | |||
| if self.is_timer_finished() == True: | |||
| self.next() | |||
| @@ -10,21 +10,37 @@ from modules.core.props import Property | |||
| @cbpi.sensor | |||
| class DummyTempSensor(SensorActive): | |||
| temp = Property.Number("Temperature", configurable=True, default_value=5) | |||
| def get_unit(self): | |||
| ''' | |||
| :return: Unit of the sensor as string. Should not be longer than 3 characters | |||
| ''' | |||
| return "°C" if self.get_config_parameter("unit", "C") == "C" else "°F" | |||
| def stop(self): | |||
| ''' | |||
| Stop the sensor. Is called when the sensor config is updated or the sensor is deleted | |||
| :return: | |||
| ''' | |||
| pass | |||
| def execute(self): | |||
| ''' | |||
| Active sensor has to handle his own loop | |||
| :return: | |||
| ''' | |||
| while self.is_running(): | |||
| self.data_received(self.temp) | |||
| socketio.sleep(5) | |||
| @classmethod | |||
| def init_global(cls): | |||
| ''' | |||
| Called one at the startup for all sensors | |||
| :return: | |||
| ''' | |||
| @@ -57,6 +57,11 @@ class GPIOPWM(ActorBase): | |||
| self.p.start(int(self.power)) | |||
| def set_power(self, power): | |||
| ''' | |||
| Optional: Set the power of your actor | |||
| :param power: int value between 0 - 100 | |||
| :return: | |||
| ''' | |||
| if power is not None: | |||
| self.power = int(power) | |||
| self.p.ChangeDutyCycle(self.power) | |||
| @@ -87,7 +92,13 @@ class RelayBoard(ActorBase): | |||
| class Dummy(ActorBase): | |||
| def on(self, power=100): | |||
| ''' | |||
| Code to switch on the actor | |||
| :param power: int value between 0 - 100 | |||
| :return: | |||
| ''' | |||
| print "ON" | |||
| def off(self): | |||
| print "OFF" | |||
| @@ -43,8 +43,6 @@ class Buzzer(object): | |||
| @cbpi.initalizer(order=0) | |||
| def init(cbpi): | |||
| print "INIT BUZZER" | |||
| cbpi.app.logger.info("INIT BUZZER") | |||
| gpio = cbpi.get_config_parameter("buzzer", 16) | |||
| cbpi.buzzer = Buzzer(gpio) | |||
| cbpi.beep() | |||
| @@ -25,6 +25,7 @@ class ConfigView(BaseView): | |||
| if self.api.cache.get(self.cache_key) is not None: | |||
| #self.pre_post_callback(self.api.cache.get(self.cache_key)[name]) | |||
| print self.api.cache.get(self.cache_key)[name] | |||
| self.api.cache.get(self.cache_key)[name].__dict__.update(**update_data) | |||
| m = self.model.update(**self.api.cache.get(self.cache_key)[name].__dict__) | |||
| self.post_put_callback(self.api.cache.get(self.cache_key)[name]) | |||
| @@ -51,7 +52,6 @@ class ConfigView(BaseView): | |||
| cls.post_init_callback(value) | |||
| cls.api.cache[cls.cache_key][value.name] = value | |||
| @cbpi.initalizer(order=1) | |||
| def init(cbpi): | |||
| print "INITIALIZE CONFIG MODULE" | |||
| @@ -1,3 +1,4 @@ | |||
| import inspect | |||
| import pprint | |||
| import sqlite3 | |||
| @@ -28,6 +29,7 @@ class ActorAPI(object): | |||
| self.app.logger.info("Init Actors") | |||
| t = self.cache.get("actor_types") | |||
| for key, value in t.iteritems(): | |||
| value.get("class").api = self | |||
| value.get("class").init_global() | |||
| for key in self.cache.get("actors"): | |||
| @@ -73,12 +75,8 @@ class ActorAPI(object): | |||
| if actor.state == 0: | |||
| return | |||
| actor.instance.off() | |||
| actor.state = 0 | |||
| self.emit("SWITCH_ACTOR", actor) | |||
| class SensorAPI(object): | |||
| @@ -231,7 +229,6 @@ class CraftBeerPi(ActorAPI, SensorAPI): | |||
| self.emit("NOTIFY", msg) | |||
| def beep(self): | |||
| if self.buzzer is not None: | |||
| self.buzzer.beep() | |||
| @@ -286,6 +283,15 @@ class CraftBeerPi(ActorAPI, SensorAPI): | |||
| def actor(self, cls): | |||
| return self.__parseProps("actor_types", cls) | |||
| def actor2(self, description="", power=True, **options): | |||
| def decorator(f): | |||
| print f() | |||
| print options | |||
| print description | |||
| return f | |||
| return decorator | |||
| def sensor(self, cls): | |||
| return self.__parseProps("sensor_types", cls) | |||
| @@ -320,9 +326,13 @@ class CraftBeerPi(ActorAPI, SensorAPI): | |||
| for m in members: | |||
| if isinstance(tmpObj.__getattribute__(m), StepProperty.Number): | |||
| t = tmpObj.__getattribute__(m) | |||
| self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable}) | |||
| print t.__dict__ | |||
| #self.cache[key][name]["properties"].append(t.__dict__) | |||
| self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable, "default_value": t.default_value}) | |||
| elif isinstance(tmpObj.__getattribute__(m), StepProperty.Text): | |||
| t = tmpObj.__getattribute__(m) | |||
| print t.__dict__ | |||
| #self.cache[key][name]["properties"].append(t.__dict__) | |||
| self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "text", "configurable": t.configurable}) | |||
| elif isinstance(tmpObj.__getattribute__(m), StepProperty.Select): | |||
| t = tmpObj.__getattribute__(m) | |||
| @@ -420,7 +430,7 @@ class CraftBeerPi(ActorAPI, SensorAPI): | |||
| self.app.logger.info("Invoke Init") | |||
| self.cache["init"] = sorted(self.cache["init"], key=lambda k: k['order']) | |||
| for i in self.cache.get("init"): | |||
| self.app.logger.info("-> %s " % i.get("function").__name__) | |||
| self.app.logger.info("INITIALIZER - METHOD %s PAHT %s: " % (i.get("function").__name__, str(inspect.getmodule(i.get("function")).__file__) )) | |||
| i.get("function")(self) | |||
| @@ -42,13 +42,15 @@ class DBModel(object): | |||
| def get_all(cls): | |||
| cur = get_db().cursor() | |||
| if cls.__order_by__ is not None: | |||
| cur.execute("SELECT * FROM %s ORDER BY '%s'" % (cls.__table_name__,cls.__order_by__)) | |||
| cur.execute("SELECT * FROM %s ORDER BY %s.'%s'" % (cls.__table_name__,cls.__table_name__,cls.__order_by__)) | |||
| else: | |||
| cur.execute("SELECT * FROM %s" % cls.__table_name__) | |||
| if cls.__as_array__ is True: | |||
| result = [] | |||
| for r in cur.fetchall(): | |||
| result.append( cls(r)) | |||
| else: | |||
| result = {} | |||
| @@ -104,7 +106,7 @@ class DBModel(object): | |||
| else: | |||
| data = data + (kwargs.get(f),) | |||
| print query, data | |||
| cur.execute(query, data) | |||
| get_db().commit() | |||
| i = cur.lastrowid | |||
| @@ -10,6 +10,9 @@ class Base(object): | |||
| def get_config_parameter(self, key, default_value): | |||
| return self.api.get_config_parameter(key, default_value) | |||
| def sleep(self, seconds): | |||
| self.api.socketio.sleep(seconds) | |||
| def init(self): | |||
| print "INIT BASE" | |||
| @@ -9,10 +9,11 @@ class Property(object): | |||
| self.options = options | |||
| class Number(PropertyType): | |||
| def __init__(self, label, configurable=False, default_value=0, unit=""): | |||
| def __init__(self, label, configurable=False, default_value=None, unit=""): | |||
| PropertyType.__init__(self) | |||
| self.label = label | |||
| self.configurable = configurable | |||
| self.default_value = default_value | |||
| class Text(PropertyType): | |||
| def __init__(self, label, configurable=False, default_value=""): | |||
| @@ -44,9 +44,8 @@ def init(cbpi): | |||
| :param app: the flask app | |||
| :return: None | |||
| """ | |||
| print "INITIALIZE MESSAGE MODULE" | |||
| msg = {"id": len(cbpi.cache["messages"]), "type": "info", "headline": "Support CraftBeerPi with your donation", "message": "You will find the PayPay Donation button in the system menu" , "read": False} | |||
| cbpi.cache["messages"].append(msg) | |||
| if cbpi.get_config_parameter("donation_notification", "YES") == "YES": | |||
| msg = {"id": len(cbpi.cache["messages"]), "type": "info", "headline": "Support CraftBeerPi with your donation", "message": "You will find the PayPay Donation button in the system menu" , "read": False} | |||
| cbpi.cache["messages"].append(msg) | |||
| NotificationView.register(cbpi.app, route_base='/api/notification') | |||
| @@ -1,5 +1,5 @@ | |||
| import time | |||
| from flask import json | |||
| from flask import json, request | |||
| from flask_classy import route | |||
| from modules import DBModel, cbpi, get_db | |||
| @@ -21,9 +21,9 @@ class Step(DBModel): | |||
| return r.get("order") | |||
| @classmethod | |||
| def get_by_state(cls, state): | |||
| def get_by_state(cls, state, order=True): | |||
| cur = get_db().cursor() | |||
| cur.execute("SELECT * FROM %s WHERE state = ?" % cls.__table_name__, state) | |||
| cur.execute("SELECT * FROM %s WHERE state = ? ORDER BY %s.'order'" % (cls.__table_name__,cls.__table_name__,), state) | |||
| r = cur.fetchone() | |||
| if r is not None: | |||
| return cls(r) | |||
| @@ -54,6 +54,16 @@ class Step(DBModel): | |||
| cur.execute("UPDATE %s SET stepstate = ? WHERE id =?" % cls.__table_name__, (json.dumps(state),id)) | |||
| get_db().commit() | |||
| @classmethod | |||
| def sort(cls, new_order): | |||
| cur = get_db().cursor() | |||
| for e in new_order: | |||
| cur.execute("UPDATE %s SET '%s' = ? WHERE id = ?" % (cls.__table_name__, "order"), (e[1], e[0])) | |||
| get_db().commit() | |||
| class StepView(BaseView): | |||
| model = Step | |||
| def pre_post_callback(self, data): | |||
| @@ -61,6 +71,14 @@ class StepView(BaseView): | |||
| data["order"] = 1 if order is None else order + 1 | |||
| data["state"] = "I" | |||
| @route('/sort', methods=["POST"]) | |||
| def sort_steps(self): | |||
| Step.sort(request.json) | |||
| cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all()) | |||
| return ('', 204) | |||
| @route('/', methods=["DELETE"]) | |||
| def deleteAll(self): | |||
| self.model.delete_all() | |||
| @@ -185,8 +203,10 @@ def init_after_startup(): | |||
| if type_cfg is None: | |||
| # step type not found. cant restart step | |||
| return | |||
| print "STEP SATE......", step.stepstate | |||
| cfg = step.stepstate.copy() | |||
| cfg.update(dict(api=cbpi, id=step.id, timer_end=None, managed_fields=get_manged_fields_as_array(type_cfg))) | |||
| cfg.update(dict(api=cbpi, id=step.id, managed_fields=get_manged_fields_as_array(type_cfg))) | |||
| instance = type_cfg.get("class")(**cfg) | |||
| instance.init() | |||
| cbpi.cache["active_step"] = instance | |||
| @@ -214,9 +234,7 @@ def execute_step(): | |||
| step = cbpi.cache.get("active_step") | |||
| if step is not None: | |||
| step.execute() | |||
| if step.is_dirty(): | |||
| state = {} | |||
| for field in step.managed_fields: | |||
| state[field] = step.__getattribute__(field) | |||
| @@ -9,6 +9,8 @@ def init(cbpi): | |||
| cbpi.app.register_blueprint(react, url_prefix='/ui') | |||
| @react.route('/') | |||
| def index(): | |||
| return react.send_static_file("index.html") | |||
| @@ -0,0 +1,16 @@ | |||
| .App { | |||
| } | |||
| .container { | |||
| padding-right: 5px; | |||
| padding-left: 5px; | |||
| margin-right: auto; | |||
| margin-left: auto; | |||
| } | |||
| .container-fluid { | |||
| padding-right: 5px; | |||
| padding-left: 5px; | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| body { | |||
| margin: 0; | |||
| padding: 0; | |||
| font-family: sans-serif; | |||
| } | |||
| @@ -13,6 +13,8 @@ | |||
| <style> | |||
| .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { | |||
| position: relative; | |||
| min-height: 1px; | |||