Merge pull request #1 from Manuel83/master

Merge
This commit is contained in:
IndyJoeA
2017-07-13 12:09:47 -04:00
committed by GitHub
46 changed files with 6911 additions and 226 deletions
+4
View File
@@ -8,3 +8,7 @@ craftbeerpi.db
modules/plugins/*
!modules/plugins/__init__.py
*.pyc
*.js
modules/ui/package.json
modules/ui/.babelrc
+6
View File
@@ -18,6 +18,12 @@ Type <code>cd craftbeerpi3</code> to navigate into the craftbeerpi folder.
Type <code>sudo ./install.sh</code>
## Hardware Wiring
Here you will find a guide how to wire everything up.
http://web.craftbeerpi.com/hardware/
## ATTENTION
CraftBeerPi 3.0 is a complete rewrite. Server as well as user interface. I recommend to use a second SD card for testing.
+18 -15
View File
@@ -1,4 +1,4 @@
CREATE TABLE kettle
CREATE TABLE IF NOT EXISTS kettle
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80),
@@ -13,7 +13,7 @@ CREATE TABLE kettle
diameter INTEGER
);
CREATE TABLE step
CREATE TABLE IF NOT EXISTS step
(
id INTEGER PRIMARY KEY NOT NULL,
"order" INTEGER,
@@ -27,7 +27,7 @@ CREATE TABLE step
kettleid INTEGER
);
CREATE TABLE sensor
CREATE TABLE IF NOT EXISTS sensor
(
id INTEGER PRIMARY KEY NOT NULL,
type VARCHAR(100),
@@ -36,7 +36,7 @@ CREATE TABLE sensor
hide BOOLEAN
);
CREATE TABLE fermenter_step
CREATE TABLE IF NOT EXISTS fermenter_step
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80),
@@ -54,7 +54,7 @@ CREATE TABLE fermenter_step
FOREIGN KEY (fermenter_id) REFERENCES fermenter (id)
);
CREATE TABLE fermenter
CREATE TABLE IF NOT EXISTS fermenter
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80),
@@ -69,7 +69,7 @@ CREATE TABLE fermenter
target_temp INTEGER
);
CREATE TABLE config
CREATE TABLE IF NOT EXISTS config
(
name VARCHAR(50) PRIMARY KEY NOT NULL,
value VARCHAR(255),
@@ -79,19 +79,22 @@ CREATE TABLE config
);
INSERT INTO config VALUES ('kettle_cols', 4, 'select', 'Adjust the width of a kettle widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
INSERT INTO config VALUES ('actor_cols', 4, 'select', 'Adjust the width of a actor widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
INSERT INTO config VALUES ('sensor_cols', 4, 'select', 'Adjust the width of a sensor widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
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 OR IGNORE INTO config VALUES ('kettle_cols', 4, 'select', 'Adjust the width of a kettle widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
INSERT OR IGNORE INTO config VALUES ('actor_cols', 4, 'select', 'Adjust the width of a actor widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
INSERT OR IGNORE INTO config VALUES ('sensor_cols', 4, 'select', 'Adjust the width of a sensor widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
INSERT OR IGNORE INTO config VALUES ('unit', 'C', 'select', 'Temperature Unit', '["C","F"]');
INSERT OR IGNORE INTO config VALUES ('brewery_name', 'My Home Brewery', 'text', 'Your brewery name', NULL );
INSERT OR IGNORE INTO config VALUES ('buzzer', 16, 'select', 'Buzzer GPIO', '[16,17,18,19,20]');
INSERT OR IGNORE INTO config VALUES ('setup', 'YES', 'select', 'Show the Setup dialog', '["YES","NO"]');
INSERT OR IGNORE INTO config VALUES ('brew_name', '', 'text', 'Brew Name', NULL);
INSERT OR IGNORE INTO config VALUES ('donation_notification', 'YES', 'select', 'Disable Donation Notification', '["YES","NO"]');
CREATE TABLE actor
CREATE TABLE IF NOT EXISTS actor
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(50),
type VARCHAR(100),
config VARCHAR(500),
hide BOOLEAN
);
);
+1
View File
@@ -0,0 +1 @@
3.0.2
+5
View File
@@ -35,6 +35,8 @@ import modules.system
import modules.buzzer
import modules.stats
import modules.kettle
import modules.recipe_import
import modules.core.db_mirgrate
from app_config import cbpi
# Build the database:
# This will create the database file using SQLAlchemy
@@ -63,3 +65,6 @@ cbpi.run_background_processes()
app.logger.info("##########################################")
app.logger.info("### STARTUP COMPLETE")
app.logger.info("##########################################")
+9 -4
View File
@@ -17,10 +17,10 @@ class ActorView(BaseView):
obj.state = 0
obj.power = 100
def post_post_callback(self, m):
def _post_post_callback(self, m):
self.api.init_actor(m.id)
def post_put_callback(self, m):
def _post_put_callback(self, m):
self.api.init_actor(m.id)
@@ -60,10 +60,15 @@ class ActorView(BaseView):
t = self.api.socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t)
return ('', 204)
@route('<int:id>/action/<method>', methods=["POST"])
def action(self, id, method):
cbpi.cache.get("actors").get(id).instance.__getattribute__(method)()
return ('', 204)
@cbpi.initalizer(order=1000)
def init(cbpi):
print "INITIALIZE ACTOR MODULE"
cbpi.app.logger.info("INITIALIZE ACTOR MODULE")
ActorView.register(cbpi.app, route_base='/api/actor')
ActorView.init_cache()
cbpi.init_actors()
+6 -4
View File
@@ -166,7 +166,9 @@ def update_addon(name):
def loadCorePlugins():
for filename in os.listdir("./modules/base_plugins"):
if filename.endswith(".py") or filename.endswith(".pyc"):
if os.path.isdir("./modules/base_plugins/"+filename) is False:
continue
try:
modules[filename] = import_module("modules.base_plugins.%s" % (filename))
@@ -178,7 +180,7 @@ def loadCorePlugins():
def loadPlugins():
for filename in os.listdir("./modules/plugins"):
if filename.endswith(".py") or filename.endswith(".pyc"):
if os.path.isdir("./modules/plugins/" + filename) is False:
continue
try:
modules[filename] = import_module("modules.plugins.%s" % (filename))
@@ -188,11 +190,11 @@ def loadPlugins():
@cbpi.initalizer(order=1)
def initPlugins(app):
print "INITIALIZE CUSTOM PLUGINS"
loadCorePlugins()
loadPlugins()
@cbpi.initalizer(order=2)
def init(cbpi):
print "INITIALIZE ADDON MODULE"
cbpi.app.register_blueprint(blueprint, url_prefix='/api/editor')
+4 -2
View File
@@ -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("##########################################")
+132 -6
View File
@@ -7,15 +7,16 @@ from modules.core.step import StepBase
from modules import cbpi
@cbpi.step
class MashStep(StepBase):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True)
kettle = StepProperty.Kettle("Kettle")
timer = Property.Number("Timer in Minutes", configurable=True)
temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step")
kettle = StepProperty.Kettle("Kettle", description="Kettle in which the mashing takes place")
timer = Property.Number("Timer in Minutes", configurable=True, description="Timer is started when the target temperature is reached")
def init(self):
'''
@@ -49,7 +50,7 @@ class MashStep(StepBase):
'''
# Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= int(self.temp):
if self.get_kettle_temp(self.kettle) >= float(self.temp):
# Check if Timer is Running
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
@@ -65,10 +66,14 @@ class MashInStep(StepBase):
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True)
kettle = StepProperty.Kettle("Kettle")
temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step")
kettle = StepProperty.Kettle("Kettle", description="Kettle in which the mashing takes place")
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,124 @@ class MashInStep(StepBase):
@cbpi.step
class ChilStep(StepBase):
timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")
@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", description="Pump actor gets toogled")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")
@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, description="Target temperature for boiling")
kettle = StepProperty.Kettle("Kettle", description="Kettle in which the boiling step takes place")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=90, description="Timer is started when target temperature is reached")
hop_1 = Property.Number("Hop 1 Addition", configurable=True, description="Fist Hop alert")
hop_1_added = Property.Number("",default_value=None)
hop_2 = Property.Number("Hop 2 Addition", configurable=True, description="Second Hop alert")
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, description="Second Hop alert")
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()
+24 -7
View File
@@ -10,21 +10,38 @@ from modules.core.props import Property
@cbpi.sensor
class DummyTempSensor(SensorActive):
temp = Property.Number("Temperature", configurable=True, default_value=5)
temp = Property.Number("Temperature", configurable=True, default_value=5, description="Dummy Temperature as decimal value")
@cbpi.action("My Custom Action")
def my_action(self):
print "HELLO WORLD"
pass
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):
pass
SensorActive.stop(self)
def execute(self):
while self.is_running():
'''
Active sensor has to handle his own loop
:return:
'''
while self.is_running() is True:
self.data_received(self.temp)
socketio.sleep(5)
self.sleep(5)
@classmethod
def init_global(cls):
'''
Called one at the startup for all sensors
:return:
'''
@@ -6,10 +6,10 @@ from modules.core.props import Property
@cbpi.fermentation_controller
class Hysteresis(FermenterController):
heater_offset_min = Property.Number("Heater Offset min", True, 0)
heater_offset_max = Property.Number("Heater Offset max", True, 0)
cooler_offset_min = Property.Number("Cooler Offset min", True, 0)
cooler_offset_max = Property.Number("Cooler Offset max", True, 0)
heater_offset_min = Property.Number("Heater Offset ON", True, 0, description="Offset as decimal number when the heater is switched on. Should be greater then 'Heater Offset OFF'. For example a value of 2 switches on the heater if the current temperature is 2 degrees below the target temperature")
heater_offset_max = Property.Number("Heater Offset OFF", True, 0, description="Offset as decimal number when the heater is switched off. Should be smaller then 'Heater Offset ON'. For example a value of 1 switches off the heater if the current temperature is 1 degree below the target temperature")
cooler_offset_min = Property.Number("Cooler Offset ON", True, 0, description="Offset as decimal number when the cooler is switched on. Should be greater then 'Cooler Offset OFF'. For example a value of 2 switches on the cooler if the current temperature is 2 degrees above the target temperature")
cooler_offset_max = Property.Number("Cooler Offset OFF", True, 0, description="Offset as decimal number when the cooler is switched off. Should be less then 'Cooler Offset ON'. For example a value of 1 switches off the cooler if the current temperature is 1 degree above the target temperature")
def stop(self):
super(FermenterController, self).stop()
@@ -23,16 +23,16 @@ class Hysteresis(FermenterController):
target_temp = self.get_target_temp()
temp = self.get_temp()
if temp + int(self.heater_offset_min) < target_temp:
if temp + float(self.heater_offset_min) <= target_temp:
self.heater_on(100)
if temp + int(self.heater_offset_max) > target_temp:
if temp + float(self.heater_offset_max) >= target_temp:
self.heater_off()
if temp > target_temp + int(self.cooler_offset_min):
if temp >= target_temp + float(self.cooler_offset_min):
self.cooler_on(100)
if temp < target_temp + int(self.cooler_offset_max):
if temp <= target_temp + float(self.cooler_offset_max):
self.cooler_off()
self.sleep(1)
self.sleep(1)
+17 -3
View File
@@ -18,7 +18,7 @@ except Exception as e:
@cbpi.actor
class GPIOSimple(ActorBase):
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27])
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27], description="GPIO to which the actor is connected")
def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT)
@@ -35,7 +35,7 @@ class GPIOSimple(ActorBase):
@cbpi.actor
class GPIOPWM(ActorBase):
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27])
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27], description="GPIO to which the actor is connected")
duty_cylce = Property.Number("Duty Cycle", configurable=True)
p = None
@@ -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)
@@ -69,7 +74,7 @@ class GPIOPWM(ActorBase):
@cbpi.actor
class RelayBoard(ActorBase):
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27])
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27], description="GPIO to which the actor is connected")
def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT)
@@ -86,8 +91,17 @@ class RelayBoard(ActorBase):
@cbpi.actor
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"
+4 -6
View File
@@ -8,8 +8,8 @@ class Hysteresis(KettleController):
# Custom Properties
on = Property.Number("Offset On", True, 0)
off = Property.Number("Offset Off", True, 0)
on = Property.Number("Offset On", True, 0, description="Offset below target temp when heater should switched on. Should be bigger then Offset Off")
off = Property.Number("Offset Off", True, 0, description="Offset below target temp when heater should switched off. Should be smaller then Offset Off")
def stop(self):
'''
@@ -30,11 +30,9 @@ class Hysteresis(KettleController):
'''
while self.is_running():
self.actor_power(50)
if self.get_temp() < self.get_target_temp() - int(self.on):
if self.get_temp() < self.get_target_temp() - float(self.on):
self.heater_on(100)
elif self.get_temp() >= self.get_target_temp() - int(self.off):
elif self.get_temp() >= self.get_target_temp() - float(self.off):
self.heater_off()
else:
self.heater_off()
+9 -4
View File
@@ -66,7 +66,8 @@ class myThread (threading.Thread):
@cbpi.sensor
class ONE_WIRE_SENSOR(SensorPassive):
sensor_name = Property.Select("Sensor", getSensors())
sensor_name = Property.Select("Sensor", getSensors(), description="The OneWire sensor address.")
offset = Property.Number("Offset", True, 0, description="Offset which is added to the received sensor data. Positive and negative values are both allowed.")
def init(self):
@@ -86,10 +87,14 @@ class ONE_WIRE_SENSOR(SensorPassive):
def read(self):
if self.get_config_parameter("unit", "C") == "C":
self.data_received(round(self.t.value, 2))
self.data_received(round(self.t.value + self.offset_value(), 2))
else:
self.data_received(round(9.0 / 5.0 * self.t.value + 32, 2))
self.data_received(round(9.0 / 5.0 * self.t.value + 32 + self.offset_value(), 2))
@cbpi.try_catch(0)
def offset_value(self):
return float(self.offset)
@classmethod
def init_global(self):
try:
@@ -108,5 +113,5 @@ def set_temp(t):
@cbpi.initalizer()
def init(cbpi):
cbpi.app.logger.info("INITIALIZE ONE WIRE MODULE")
cbpi.app.register_blueprint(blueprint, url_prefix='/api/one_wire')
+1 -3
View File
@@ -41,10 +41,8 @@ class Buzzer(object):
start_new_thread(play, (self.sound,))
@cbpi.initalizer(order=0)
@cbpi.initalizer(order=1)
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()
+3 -5
View File
@@ -24,10 +24,9 @@ class ConfigView(BaseView):
update_data = {"name": data["name"], "value": data["value"]}
if self.api.cache.get(self.cache_key) is not None:
#self.pre_post_callback(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])
self._post_put_callback(self.api.cache.get(self.cache_key)[name])
return json.dumps(self.api.cache.get(self.cache_key)[name].__dict__)
@route('/<id>', methods=["GET"])
@@ -51,9 +50,8 @@ class ConfigView(BaseView):
cls.post_init_callback(value)
cls.api.cache[cls.cache_key][value.name] = value
@cbpi.initalizer(order=1)
@cbpi.initalizer(order=0)
def init(cbpi):
print "INITIALIZE CONFIG MODULE"
ConfigView.register(cbpi.app, route_base='/api/config')
ConfigView.init_cache()
+15 -15
View File
@@ -9,7 +9,7 @@ class BaseView(FlaskView):
cache_key = None
api = cbpi
@route('/<int:id>')
@route('/<int:id>', methods=["GET"])
def getOne(self, id):
if self.api.cache.get(self.cache_key) is not None:
@@ -17,36 +17,36 @@ class BaseView(FlaskView):
else:
return json.dumps(self.model.get_one(id))
@route('/')
@route('/', methods=["GET"])
def getAll(self):
if self.api.cache.get(self.cache_key) is not None:
return json.dumps(self.api.cache.get(self.cache_key))
else:
return json.dumps(self.model.get_all())
def pre_post_callback(self, data):
def _pre_post_callback(self, data):
pass
def post_post_callback(self, m):
def _post_post_callback(self, m):
pass
@route('/', methods=["POST"])
def post(self):
data = request.json
self.pre_post_callback(data)
self._pre_post_callback(data)
m = self.model.insert(**data)
if self.api.cache.get(self.cache_key) is not None:
self.api.cache.get(self.cache_key)[m.id] = m
self.post_post_callback(m)
self._post_post_callback(m)
return json.dumps(m)
def pre_put_callback(self, m):
def _pre_put_callback(self, m):
pass
def post_put_callback(self, m):
def _post_put_callback(self, m):
pass
@@ -59,32 +59,32 @@ class BaseView(FlaskView):
except:
pass
if self.api.cache.get(self.cache_key) is not None:
self.pre_put_callback(self.api.cache.get(self.cache_key)[id])
self._pre_put_callback(self.api.cache.get(self.cache_key)[id])
self.api.cache.get(self.cache_key)[id].__dict__.update(**data)
m = self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
self.post_put_callback(self.api.cache.get(self.cache_key)[id])
self._post_put_callback(self.api.cache.get(self.cache_key)[id])
return json.dumps(self.api.cache.get(self.cache_key)[id])
else:
m = self.model.update(**data)
self.post_put_callback(m)
self._post_put_callback(m)
return json.dumps(m)
def pre_delete_callback(self, m):
def _pre_delete_callback(self, m):
pass
def post_delete_callback(self, id):
def _post_delete_callback(self, id):
pass
@route('/<int:id>', methods=["DELETE"])
def delete(self, id):
if self.api.cache.get(self.cache_key) is not None:
self.pre_delete_callback(self.api.cache.get(self.cache_key)[id])
self._pre_delete_callback(self.api.cache.get(self.cache_key)[id])
del self.api.cache.get(self.cache_key)[id]
m = self.model.delete(id)
def post_delete_callback(self, id):
def _post_delete_callback(self, id):
pass
return ('',204)
+2 -2
View File
@@ -135,13 +135,13 @@ class FermenterController(ControllerBase, ActorController, SensorController):
@cbpi.try_catch(None)
def cooler_on(self, power=100):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None:
if f.cooler is not None:
self.actor_on(power, int(f.cooler))
@cbpi.try_catch(None)
def cooler_off(self):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None:
if f.cooler is not None:
self.actor_off(int(f.cooler))
@cbpi.try_catch(None)
+56 -23
View File
@@ -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):
@@ -161,9 +159,8 @@ class SensorAPI(object):
def log_action(self, text):
filename = "./logs/action.log"
formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
with open(filename, "a") as file:
text = text.encode("utf-8")
file.write("%s,%s\n" % (formatted_time, text))
def shutdown_sensor(self, id):
@@ -173,7 +170,7 @@ class SensorAPI(object):
def get_sensor_value(self, id):
try:
id = int(id)
return int(self.cache.get("sensors")[id].instance.last_value)
return float(self.cache.get("sensors")[id].instance.last_value)
except Exception as e:
return None
@@ -226,12 +223,9 @@ class CraftBeerPi(ActorAPI, SensorAPI):
def notify(self, headline, message, type="success", timeout=5000):
self.beep()
msg = {"id": str(uuid.uuid1()), "type": type, "headline": headline, "message": message, "timeout": timeout}
if timeout is None:
self.cache["messages"].append(msg)
self.emit("NOTIFY", msg)
self.emit_message(msg)
def beep(self):
if self.buzzer is not None:
self.buzzer.beep()
@@ -248,6 +242,15 @@ class CraftBeerPi(ActorAPI, SensorAPI):
else:
return cfg.value
def set_config_parameter(self, name, value):
from modules.config import Config
with self.app.app_context():
update_data = {"name": name, "value": value}
self.cache.get("config")[name].__dict__.update(**update_data)
c = Config.update(**update_data)
self.emit("UPDATE_CONFIG", c)
def add_config_parameter(self, name, value, type, description, options=None):
from modules.config import Config
with self.app.app_context():
@@ -264,28 +267,56 @@ class CraftBeerPi(ActorAPI, SensorAPI):
# helper method for parsing props
def __parseProps(self, key, cls):
name = cls.__name__
self.cache[key][name] = {"name": name, "class": cls, "properties": []}
self.cache[key][name] = {"name": name, "class": cls, "properties": [], "actions": []}
tmpObj = cls()
members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")]
for m in members:
if isinstance(tmpObj.__getattribute__(m), Property.Number):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append(
{"name": m, "label": t.label, "type": "number", "configurable": t.configurable})
{"name": m, "label": t.label, "type": "number", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Text):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append(
{"name": m, "label": t.label, "type": "text", "configurable": t.configurable})
{"name": m, "label": t.label, "type": "text", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Select):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append(
{"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options})
{"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Actor):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Sensor):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Kettle):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable, "description": t.description})
for name, method in cls.__dict__.iteritems():
if hasattr(method, "action"):
label = method.__getattribute__("label")
self.cache[key][cls.__name__]["actions"].append({"method": name, "label": label})
return cls
def actor(self, cls):
return self.__parseProps("actor_types", cls)
def actor2(self, description="", power=True, **options):
def decorator(f):
print f()
print f
print options
print description
return f
return decorator
def sensor(self, cls):
return self.__parseProps("sensor_types", cls)
@@ -300,6 +331,8 @@ class CraftBeerPi(ActorAPI, SensorAPI):
def get_fermentation_controller(self, name):
return self.cache["fermentation_controller_types"].get(name)
# Step action
def action(self,label):
def real_decorator(func):
@@ -320,22 +353,22 @@ 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})
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable, "default_value": t.default_value, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Text):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "text", "configurable": t.configurable})
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "text", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Select):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "select", "options": t.options})
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Actor):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": t.configurable})
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Sensor):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": t.configurable})
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Kettle):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable})
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable, "description": t.description})
for name, method in cls.__dict__.iteritems():
if hasattr(method, "action"):
@@ -420,7 +453,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)
@@ -451,7 +484,7 @@ class CraftBeerPi(ActorAPI, SensorAPI):
def job(interval, method):
while True:
try:
method()
method(self)
except Exception as e:
self.app.logger.error("Exception" + method.__name__ + ": " + str(e))
self.socketio.sleep(interval)
+4 -2
View File
@@ -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
+47
View File
@@ -0,0 +1,47 @@
import sqlite3
import os
from modules import cbpi
from db import get_db
def execute_file(curernt_version, data):
if curernt_version >= data["version"]:
cbpi.app.logger.info("SKIP DB FILE: %s" % data["file"])
return
try:
with sqlite3.connect("craftbeerpi.db") as conn:
with open('./update/%s' % data["file"], 'r') as f:
d = f.read()
sqlCommands = d.split(";")
cur = conn.cursor()
for s in sqlCommands:
cur.execute(s)
cur.execute("INSERT INTO schema_info (version,filename) values (?,?)", (data["version"], data["file"]))
conn.commit()
except sqlite3.OperationalError as err:
print "EXCEPT"
print err
@cbpi.initalizer(order=-9999)
def init(app=None):
with cbpi.app.app_context():
conn = get_db()
cur = conn.cursor()
current_version = None
try:
cur.execute("SELECT max(version) as m FROM schema_info")
m = cur.fetchone()
current_version = m["m"]
except:
pass
result = []
for filename in os.listdir("./update"):
if filename.endswith(".sql"):
d = {"version": int(filename[:filename.index('_')]), "file": filename}
result.append(d)
execute_file(current_version, d)
+11 -7
View File
@@ -5,16 +5,19 @@ class Base(object):
@classmethod
def init_global(cls):
print "GLOBAL ACTOR INIT"
pass
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"
pass
def stop(self):
print "STOP HARDWARE"
pass
def update(self, **kwds):
pass
@@ -67,15 +70,16 @@ class SensorActive(SensorBase):
__running = False
def is_running(self):
return self.__running
def init(self):
self.__running = True
print "INIT ACTIVE SENSOR"
def stop(self):
self.__running = False
def execute(self):
pass
@@ -95,10 +99,10 @@ class ActorBase(Base):
return 1
def set_power(self, power):
print "SET POWER TO %s" % power
pass
def on(self, power=0):
print "ON"
pass
def off(self):
print "OFF"
pass
+36 -7
View File
@@ -3,36 +3,65 @@ class PropertyType(object):
class Property(object):
class Select(PropertyType):
def __init__(self, label, options):
def __init__(self, label, options, description=""):
PropertyType.__init__(self)
self.label = label
self.options = options
self.description = description
class Number(PropertyType):
def __init__(self, label, configurable=False, default_value=0, unit=""):
def __init__(self, label, configurable=False, default_value=None, unit="", description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = configurable
self.default_value = default_value
self.description = description
class Text(PropertyType):
def __init__(self, label, configurable=False, default_value=""):
def __init__(self, label, configurable=False, default_value="", description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = configurable
self.description = description
class Actor(PropertyType):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class Sensor(PropertyType):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class Kettle(PropertyType):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class StepProperty(Property):
class Actor(PropertyType):
def __init__(self, label):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class Sensor(PropertyType):
def __init__(self, label):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class Kettle(PropertyType):
def __init__(self, label):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.configurable = True
self.description = description
+5 -5
View File
@@ -44,7 +44,7 @@ class KettleAPI(NotificationAPI):
return self.api.cache.get("kettle").get(id).target_temp
def set_target_temp(self, temp, id=None):
temp = int(temp)
temp = float(temp)
try:
if id is None:
@@ -59,7 +59,7 @@ class Timer(object):
timer_end = Property.Number("TIMER_END", configurable=False)
def start_timer(self, timer):
print "START TIMER NEW"
if self.timer_end is not None:
return
self.timer_end = int(time.time()) + timer
@@ -100,13 +100,13 @@ class StepBase(Timer, ActorAPI, SensorAPI, KettleAPI):
self.n = True
def init(self):
print "INIT STEP"
pass
def finish(self):
print "FINSIH STEP"
pass
def reset(self):
print "REST STEP"
pass
def execute(self):
print "-------------"
+7 -6
View File
@@ -63,18 +63,19 @@ class FermenterView(BaseView):
model = Fermenter
cache_key = "fermenter"
def post_post_callback(self, m):
def _post_post_callback(self, m):
m.state = False
m.steps = []
def pre_put_callback(self, m):
def _pre_put_callback(self, m):
m.state = False
try:
m.instance.stop()
except:
pass
def post_put_callback(self, m):
def _post_put_callback(self, m):
m.state = False
@route('/<int:id>/targettemp/<temp>', methods=['POST'])
@@ -282,7 +283,7 @@ class FermenterView(BaseView):
@cbpi.backgroundtask(key="read_target_temps_fermenter", interval=5)
def read_target_temps():
def read_target_temps(api):
"""
background process that reads all passive sensors in interval of 1 second
:return: None
@@ -295,7 +296,7 @@ def read_target_temps():
instance = FermenterView()
@cbpi.backgroundtask(key="fermentation_task", interval=1)
def execute_fermentation_step():
def execute_fermentation_step(api):
with cbpi.app.app_context():
instance.check_step()
@@ -310,6 +311,6 @@ def init_active_steps():
@cbpi.initalizer(order=1)
def init(cbpi):
print "INITIALIZE CONFIG MODULE"
FermenterView.register(cbpi.app, route_base='/api/fermenter')
FermenterView.init_cache()
+6 -6
View File
@@ -15,7 +15,7 @@ class Kettle2View(BaseView):
cache_key = "kettle"
@classmethod
def pre_post_callback(self, data):
def _pre_post_callback(self, data):
data["target_temp"] = 0
@classmethod
@@ -23,16 +23,16 @@ class Kettle2View(BaseView):
obj.state = False
def post_post_callback(self, m):
def _post_post_callback(self, m):
m.state = False
def pre_put_callback(self, m):
def _pre_put_callback(self, m):
try:
m.instance.stop()
except:
pass
def post_put_callback(self, m):
def _post_put_callback(self, m):
m.state = False
@route('/<int:id>/targettemp/<temp>', methods=['POST'])
@@ -76,11 +76,11 @@ def set_target_temp(id, temp):
:param temp: target temp to set
:return: None
'''
print "GOT EVENT %s %s" % (id, temp)
Kettle2View().postTargetTemp(id,temp)
@cbpi.backgroundtask(key="read_target_temps", interval=5)
def read_target_temps():
def read_target_temps(api):
"""
background process that reads all passive sensors in interval of 1 second
:return: None
-1
View File
@@ -105,5 +105,4 @@ def init(app):
:param app: the flask app
:return: None
"""
print "INITIALIZE LOG MODULE"
LogView.register(cbpi.app, route_base='/api/logs')
+6 -8
View File
@@ -32,10 +32,9 @@ def messageEvent(message, **kwargs):
:param kwargs: other parameter
:return: None
"""
msg = {"id": len(cbpi.cache["messages"]), "type": "info", "message": message, "read": False}
cbpi.cache["messages"].append(msg)
cbpi.emit('MESSAGE', msg,)
if message["timeout"] is None:
cbpi.cache["messages"].append(message)
cbpi.emit("NOTIFY", message)
@cbpi.initalizer(order=2)
def init(cbpi):
@@ -44,9 +43,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')
+3
View File
@@ -0,0 +1,3 @@
import beerxml
import kbh
import restapi
+110
View File
@@ -0,0 +1,110 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules.app_config import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.steps import Step,StepView
import xml.etree.ElementTree
class BeerXMLImport(FlaskView):
BEER_XML_FILE = "./upload/beer.xml"
@route('/', methods=['GET'])
def get(self):
if not os.path.exists(self.BEER_XML_FILE):
self.api.notify(headline="File Not Found", message="Please upload a Beer.xml File",
type="danger")
return ('', 404)
result = []
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
result = []
for idx, val in enumerate(e.findall('RECIPE')):
result.append({"id": idx+1, "name": val.find("NAME").text})
return json.dumps(result)
def allowed_file(self, filename):
return '.' in filename and filename.rsplit('.', 1)[1] in set(['xml'])
@route('/upload', methods=['POST'])
def upload_file(self):
try:
if request.method == 'POST':
file = request.files['file']
if file and self.allowed_file(file.filename):
file.save(os.path.join(self.api.app.config['UPLOAD_FOLDER'], "beer.xml"))
self.api.notify(headline="Upload Successful", message="The Beer XML file was uploaded succesfully")
return ('', 204)
return ('', 404)
except Exception as e:
self.api.notify(headline="Upload Failed", message="Failed to upload Beer xml", type="danger")
return ('', 500)
@route('/<int:id>', methods=['POST'])
def load(self, id):
steps = self.getSteps(id)
name = self.getRecipeName(id)
self.api.set_config_parameter("brew_name", name)
boil_time = self.getBoilTime(id)
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
try:
for row in steps:
Step.insert(**{"name": row.get("name"), "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": float(row.get("temp")), "timer": row.get("timer")}})
Step.insert(**{"name": "ChilStep", "type": "ChilStep", "config": {"timer": 15}})
## Add cooking step
Step.insert(**{"name": "Boil", "type": boilstep_type, "config": {"kettle": boil_kettle, "temp": boil_temp, "timer": boil_time}})
## Add Whirlpool step
Step.insert(**{"name": "Whirlpool", "type": "ChilStep", "config": {"timer": 15}})
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", message=e.message, type="danger")
return ('', 500)
return ('', 204)
def getRecipeName(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
return e.find('./RECIPE[%s]/NAME' % (str(id))).text
def getBoilTime(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
return float(e.find('./RECIPE[%s]/BOIL_TIME' % (str(id))).text)
def getSteps(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
steps = []
for e in e.findall('./RECIPE[%s]/MASH/MASH_STEPS/MASH_STEP' % (str(id))):
if self.api.get_config_parameter("unit", "C") == "C":
temp = float(e.find("STEP_TEMP").text)
else:
temp = round(9.0 / 5.0 * float(e.find("STEP_TEMP").text) + 32, 2)
steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)})
return steps
@cbpi.initalizer()
def init(cbpi):
BeerXMLImport.api = cbpi
BeerXMLImport.register(cbpi.app, route_base='/api/beerxml')
+109
View File
@@ -0,0 +1,109 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules.app_config import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.steps import Step, StepView
class KBH(FlaskView):
@route('/', methods=['GET'])
def get(self):
conn = None
try:
if not os.path.exists(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db'):
self.api.notify(headline="File Not Found", message="Please upload a Kleiner Brauhelfer Database", type="danger")
return ('', 404)
conn = sqlite3.connect(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db')
c = conn.cursor()
c.execute('SELECT ID, Sudname, BierWurdeGebraut FROM Sud')
data = c.fetchall()
result = []
for row in data:
result.append({"id": row[0], "name": row[1], "brewed": row[2]})
return json.dumps(result)
except Exception as e:
print e
self.api.notify(headline="Failed to load KHB database", message="ERROR", type="danger")
return ('', 500)
finally:
if conn:
conn.close()
def allowed_file(self, filename):
return '.' in filename and filename.rsplit('.', 1)[1] in set(['sqlite'])
@route('/upload', methods=['POST'])
def upload_file(self):
try:
if request.method == 'POST':
file = request.files['file']
if file and self.allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(self.api.app.config['UPLOAD_FOLDER'], "kbh.db"))
self.api.notify(headline="Upload Successful", message="The Kleiner Brauhelfer Database was uploaded succesfully")
return ('', 204)
return ('', 404)
except Exception as e:
self.api.notify(headline="Upload Failed", message="Failed to upload Kleiner Brauhelfer", type="danger")
return ('', 500)
@route('/<int:id>', methods=['POST'])
def load(self, id):
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mashinstep_type = cbpi.get_config_parameter("step_mashin", "MashInStep")
chilstep_type = cbpi.get_config_parameter("step_chil", "ChilStep")
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
conn = None
try:
conn = sqlite3.connect(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db')
c = conn.cursor()
c.execute('SELECT EinmaischenTemp, Sudname FROM Sud WHERE ID = ?', (id,))
row = c.fetchone()
name = row[1]
self.api.set_config_parameter("brew_name", name)
Step.insert(**{"name": "MashIn", "type": mashinstep_type, "config": {"kettle": mash_kettle, "temp": row[0]}})
### add rest step
for row in c.execute('SELECT * FROM Rasten WHERE SudID = ?', (id,)):
Step.insert(**{"name": row[5], "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": row[3], "timer": row[4]}})
Step.insert(**{"name": "Chil", "type": chilstep_type, "config": {"timer": 15}})
## Add cooking step
c.execute('SELECT max(Zeit) FROM Hopfengaben WHERE SudID = ?', (id,))
row = c.fetchone()
Step.insert(**{"name": "Boil", "type": boilstep_type, "config": {"kettle": boil_kettle, "temp": boil_temp, "timer": row[0]}})
## Add Whirlpool step
Step.insert(**{"name": "Whirlpool", "type": chilstep_type, "config": {"timer": 15}})
#setBrewName(name)
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", message=e.message, type="danger")
return ('', 500)
finally:
if conn:
conn.close()
return ('', 204)
@cbpi.initalizer()
def init(cbpi):
KBH.api = cbpi
KBH.register(cbpi.app, route_base='/api/kbh')
+62
View File
@@ -0,0 +1,62 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules.app_config import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.steps import Step,StepView
import xml.etree.ElementTree
class RESTImport(FlaskView):
@route('/', methods=['POST'])
def load(self):
try:
data = request.json
name = data.get("name", "No Name")
self.api.set_config_parameter("brew_name", name)
chilstep_type = cbpi.get_config_parameter("step_chil", "ChilStep")
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
for step in data.get("steps"):
if step.get("type", None) == "MASH":
Step.insert(**{"name": step.get("name","Mash Step"), "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": step.get("temp",0), "timer": step.get("timer",0)}})
elif step.get("type", None) == "CHIL":
Step.insert(**{"name": step.get("name","Chil"), "type": chilstep_type, "config": {"timer": step.get("timer")}})
elif step.get("type", None) == "BOIL":
Step.insert(**{"name": step.get("name", "Boil"), "type": boilstep_type, "config": {"kettle": boil_kettle, "timer": step.get("timer"), "temp": boil_temp}})
else:
pass
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", type="danger", message=str(e))
m = str(e.message)
return (str(e), 500)
return ('', 204)
@cbpi.initalizer()
def init(cbpi):
RESTImport.api = cbpi
RESTImport.register(cbpi.app, route_base='/api/recipe/import/v1')
+10 -5
View File
@@ -12,27 +12,32 @@ class SensorView(BaseView):
model = Sensor
cache_key = "sensors"
@route('<int:id>/action/<method>', methods=["POST"])
def action(self, id, method):
def post_post_callback(self, m):
cbpi.cache.get("sensors").get(id).instance.__getattribute__(method)()
return ('', 204)
def _post_post_callback(self, m):
cbpi.init_sensor(m.id)
def post_put_callback(self, m):
def _post_put_callback(self, m):
cbpi.stop_sensor(m.id)
cbpi.init_sensor(m.id)
def pre_delete_callback(self, m):
def _pre_delete_callback(self, m):
cbpi.stop_sensor(m.id)
@cbpi.initalizer(order=1000)
def init(cbpi):
print "INITIALIZE SENSOR MODULE"
SensorView.register(cbpi.app, route_base='/api/sensor')
SensorView.init_cache()
cbpi.init_sensors()
@cbpi.backgroundtask(key="read_passiv_sensor", interval=5)
def read_passive_sensor():
def read_passive_sensor(api):
"""
background process that reads all passive sensors in interval of 1 second
:return: None
+1 -1
View File
@@ -16,7 +16,7 @@ def getserial():
@cbpi.initalizer(order=9999)
def sendStats(cbpi):
print "INITIALIZE STATS"
try:
serial = getserial()
+24 -15
View File
@@ -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,13 +54,29 @@ 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):
def _pre_post_callback(self, data):
order = self.model.get_max_order()
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()
@@ -69,16 +85,12 @@ class StepView(BaseView):
@route('/action/<method>', methods=["POST"])
def action(self, method):
cbpi.cache["active_step"].__getattribute__(method)()
return ('', 204)
@route('/reset', methods=["POST"])
def reset(self):
self.model.reset_all_steps()
#db.session.commit()
self.stop_step()
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all())
return ('', 204)
@@ -115,7 +127,6 @@ class StepView(BaseView):
return ('', 204)
def init_step(self, step):
cbpi.log_action("Start Step %s" % step.name)
type_cfg = cbpi.cache.get("step_types").get(step.type)
if type_cfg is None:
@@ -128,7 +139,6 @@ class StepView(BaseView):
cfg.update(dict(name=step.name, api=cbpi, id=step.id, timer_end=None, managed_fields=get_manged_fields_as_array(type_cfg)))
instance = type_cfg.get("class")(**cfg)
instance.init()
# set step instance to ache
cbpi.cache["active_step"] = instance
@@ -185,15 +195,16 @@ def init_after_startup():
if type_cfg is None:
# step type not found. cant restart step
return
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
@cbpi.initalizer(order=2000)
def init(cbpi):
print "INITIALIZE STEPS MODULE"
StepView.register(cbpi.app, route_base='/api/step')
def get_all():
@@ -205,7 +216,7 @@ def init(cbpi):
cbpi.add_cache_callback("steps", get_all)
@cbpi.backgroundtask(key="step_task", interval=0.1)
def execute_step():
def execute_step(api):
'''
Background job which executes the step
:return:
@@ -214,9 +225,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)
+40 -2
View File
@@ -1,4 +1,5 @@
from flask import json
import yaml
from flask import json, url_for, Response
from flask_classy import FlaskView, route
from git import Repo, Git
@@ -101,7 +102,44 @@ class SystemView(FlaskView):
def dump(self):
return json.dumps(cbpi.cache)
@route('/endpoints', methods=['GET'])
def endpoints(self):
import urllib
output = []
vf = self.api.app.view_functions
for f in self.api.app.view_functions:
print f
endpoints = {}
re = {
"swagger": "2.0",
"host": "",
"info": {
"description":"",
"version": "",
"title": "CraftBeerPi"
},
"schemes": ["http"],
"paths": endpoints}
for rule in self.api.app.url_map.iter_rules():
r = rule
endpoints[rule.rule] = {}
if "HEAD" in r.methods: r.methods.remove("HEAD")
if "OPTIONS" in r.methods: r.methods.remove("OPTIONS")
for m in rule.methods:
endpoints[rule.rule][m] = dict(summary="", description="", consumes=["application/json"],produces=["application/json"])
with open("config/version.yaml", 'r') as stream:
y = yaml.load(stream)
pprint.pprint(y)
pprint.pprint(re)
return Response(yaml.dump(re), mimetype='text/yaml')
@cbpi.initalizer()
def init(cbpi):
print "INITIALIZE SYSTEM MODULE"
SystemView.api = cbpi
SystemView.register(cbpi.app, route_base='/api/system')
+3 -2
View File
@@ -5,11 +5,12 @@ react = Blueprint('react', __name__, template_folder='templates', static_folder=
@cbpi.initalizer(order=10)
def init(cbpi):
print "INITIALIZE UI"
cbpi.app.register_blueprint(react, url_prefix='/ui')
@react.route('/')
@react.route('/', methods=["GET"])
def index():
return react.send_static_file("index.html")
+6006
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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;
}
+5
View File
@@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -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;
@@ -50,11 +52,15 @@
/* Einschränken des sichtbaren Ausschnitts */
</style>
<title>CraftBeerPi 3.0</title>
</head>
<body>
<div id="root"></div>
<div id="root" ></div>
<script src="static/bundle.js" type="text/javascript"></script>
<!--
This HTML file is a template.
+1 -2
View File
@@ -11,5 +11,4 @@ requests==2.11.0
Werkzeug==0.11.10
httplib2==0.9.2
flask-classy==0.6.10
GitPython==2.1.3
GitPython==2.1.3
+18
View File
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS schema_info
(
id INTEGER PRIMARY KEY NOT NULL,
version INTEGER,
filename VARCHAR(80),
creation_data DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO config VALUES ('step_mashin', 'MashStep', 'step', 'Default MashIn Step type for import', NULL );
INSERT OR IGNORE INTO config VALUES ('step_mash', 'MashStep', 'step', 'Default Mash Step type for import', NULL);
INSERT OR IGNORE INTO config VALUES ('step_boil', 'BoilStep', 'step', 'Default Boil Step type for import', NULL);
INSERT OR IGNORE INTO config VALUES ('step_chil', 'ChilStep', 'step', 'Default Chil Step type for import', NULL);
INSERT OR IGNORE INTO config VALUES ('step_mash_kettle', 1, 'kettle', 'Default Mash Tun', NULL);
INSERT OR IGNORE INTO config VALUES ('step_boil_kettle', 1, 'kettle', 'Default Boil Tun ', NULL);
+1
View File
@@ -0,0 +1 @@
UPDATE config set `options` = '[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]' WHERE name ='buzzer';
View File