Manuel83 8 роки тому
джерело
коміт
58e927b0bf
57 змінених файлів з 8540 додано та 1 видалено
  1. +13
    -0
      .gitignore
  2. +21
    -1
      README.md
  3. +62
    -0
      config/craftbeerpiboot
  4. +97
    -0
      config/schema.sql
  5. +149
    -0
      install.sh
  6. +0
    -0
      logs/empty
  7. +65
    -0
      modules/__init__.py
  8. +69
    -0
      modules/actor/__init__.py
  9. +1
    -0
      modules/addon/__init__.py
  10. +202
    -0
      modules/addon/endpoints.py
  11. +57
    -0
      modules/app_config.py
  12. +0
    -0
      modules/base_plugins/__init__.py
  13. +95
    -0
      modules/base_plugins/brew_steps/__init__.py
  14. +36
    -0
      modules/base_plugins/dummy_temp/__init__.py
  15. +27
    -0
      modules/base_plugins/fermenter_hysteresis/__init__.py
  16. +93
    -0
      modules/base_plugins/gpio_actor/__init__.py
  17. +39
    -0
      modules/base_plugins/hysteresis/__init__.py
  18. +112
    -0
      modules/base_plugins/one_wire/__init__.py
  19. +51
    -0
      modules/buzzer/__init__.py
  20. +59
    -0
      modules/config/__init__.py
  21. +0
    -0
      modules/core/__init__.py
  22. +109
    -0
      modules/core/baseview.py
  23. +134
    -0
      modules/core/controller.py
  24. +452
    -0
      modules/core/core.py
  25. +115
    -0
      modules/core/db.py
  26. +104
    -0
      modules/core/hardware.py
  27. +38
    -0
      modules/core/props.py
  28. +132
    -0
      modules/core/step.py
  29. +306
    -0
      modules/fermenter/__init__.py
  30. +88
    -0
      modules/kettle/__init__.py
  31. +1
    -0
      modules/logs/__init__.py
  32. +109
    -0
      modules/logs/endpoints.py
  33. +52
    -0
      modules/notification/__init__.py
  34. +43
    -0
      modules/sensors/__init__.py
  35. +36
    -0
      modules/stats/__init__.py
  36. +230
    -0
      modules/steps/__init__.py
  37. +1
    -0
      modules/system/__init__.py
  38. +107
    -0
      modules/system/endpoints.py
  39. +1
    -0
      modules/ui/__init__.py
  40. +23
    -0
      modules/ui/endpoints.py
  41. +11
    -0
      modules/ui/static/bootstrap.dark.css
  42. +6
    -0
      modules/ui/static/bootstrap.min.css
  43. +91
    -0
      modules/ui/static/bundle.js
  44. +2337
    -0
      modules/ui/static/css/font-awesome.css
  45. +4
    -0
      modules/ui/static/css/font-awesome.min.css
  46. BIN
      modules/ui/static/favicon.ico
  47. BIN
      modules/ui/static/fonts/FontAwesome.otf
  48. BIN
      modules/ui/static/fonts/fontawesome-webfont.eot
  49. +2671
    -0
      modules/ui/static/fonts/fontawesome-webfont.svg
  50. BIN
      modules/ui/static/fonts/fontawesome-webfont.ttf
  51. BIN
      modules/ui/static/fonts/fontawesome-webfont.woff
  52. BIN
      modules/ui/static/fonts/fontawesome-webfont.woff2
  53. +70
    -0
      modules/ui/static/index.html
  54. BIN
      modules/ui/static/logo.png
  55. +15
    -0
      requirements.txt
  56. +6
    -0
      run.py
  57. +0
    -0
      upload/empty

+ 13
- 0
.gitignore Переглянути файл

@@ -0,0 +1,13 @@
.idea/
*.log
craftbeerpi.db
**/node_modules/*
*.pyc
*.templog

modules/plugins/*
!modules/plugins/my_plugin/__init__.py
!modules/plugins/cbpi**/__init__.py
*.pyc



+ 21
- 1
README.md Переглянути файл

@@ -1 +1,21 @@
# craftbeerpi3
#CraftBeerPi V3.0

This is CraftBeerPi version 3.0. It's currently in beta status.

## Installation

Open a terminal window on Raspberry Pi and type:

<code> git clone http://github.com/craftbeerpi/craftbeerpi</code>

This will download (clone) the software to your local Raspberry Pi.

Type <code>cd craftbeerpi</code> to navigate into the craftbeerpi folder.

Type <code>sudo ./install.sh</code>

## ATTENTION

CraftBeerPi 3.0 is a complete rewrite. Server as well as user interface. I recommend to use a second SD card for testing.



+ 62
- 0
config/craftbeerpiboot Переглянути файл

@@ -0,0 +1,62 @@
#!/bin/sh

### BEGIN INIT INFO
# Provides: craftbeerpi
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Put a short description of the service here
# Description: Put a long description of the service here
### END INIT INFO

# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=#DIR#
DAEMON=$DIR/run.py
DAEMON_NAME=CraftBeerPI

# Add any command line options for your daemon here
DAEMON_OPTS=""

# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=root

# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid

. /lib/lsb/init-functions

do_start () {
log_daemon_msg "Starting system $DAEMON_NAME daemon"
start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --chdir $DIR --startas $DAEMON -- $DAEMON_OPTS
log_end_msg $?
}
do_stop () {
log_daemon_msg "Stopping system $DAEMON_NAME daemon"
start-stop-daemon --stop --pidfile $PIDFILE --retry 10
log_end_msg $?
}

case "$1" in

start|stop)
do_${1}
;;

restart|reload|force-reload)
do_stop
do_start
;;

status)
status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
;;

*)
echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
exit 1
;;

esac
exit 0

+ 97
- 0
config/schema.sql Переглянути файл

@@ -0,0 +1,97 @@
CREATE TABLE kettle
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80),
sensor VARCHAR(80),
heater VARCHAR(10),
automatic VARCHAR(255),
logic VARCHAR(50),
config VARCHAR(1000),
agitator VARCHAR(10),
target_temp INTEGER,
height INTEGER,
diameter INTEGER
);

CREATE TABLE step
(
id INTEGER PRIMARY KEY NOT NULL,
"order" INTEGER,
name VARCHAR(80),
type VARCHAR(100),
stepstate VARCHAR(255),
state VARCHAR(1),
start INTEGER,
end INTEGER,
config VARCHAR(255),
kettleid INTEGER
);

CREATE TABLE sensor
(
id INTEGER PRIMARY KEY NOT NULL,
type VARCHAR(100),
name VARCHAR(80),
config VARCHAR(500),
hide BOOLEAN
);

CREATE TABLE fermenter_step
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80),
hours INTEGER,
minutes INTEGER,
days INTEGER,
temp INTEGER,
direction VARCHAR(1),
"order" INTEGER,
state VARCHAR(1),
start INTEGER,
timer_start INTEGER,
end INTEGER,
fermenter_id INTEGER,
FOREIGN KEY (fermenter_id) REFERENCES fermenter (id)
);

CREATE TABLE fermenter
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80),
brewname VARCHAR(80),
sensor VARCHAR(80),
sensor2 VARCHAR(80),
sensor3 VARCHAR(80),
heater VARCHAR(10),
logic VARCHAR(50),
config VARCHAR(1000),
cooler VARCHAR(10),
target_temp INTEGER
);

CREATE TABLE config
(
name VARCHAR(50) PRIMARY KEY NOT NULL,
value VARCHAR(255),
type VARCHAR(50),
description VARCHAR(255),
options VARCHAR(255)
);


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"]');

CREATE TABLE actor
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(50),
type VARCHAR(100),
config VARCHAR(500),
hide BOOLEAN
);

+ 149
- 0
install.sh Переглянути файл

@@ -0,0 +1,149 @@
#!/bin/bash
#CraftBeerPi Installer
# Copy 2017 Manuel Fritsch

confirmAnswer () {
whiptail --title "Confirmation" --yes-button "Yes" --no-button "No" --defaultno --yesno "$1" 10 56
return $?
}

show_menu () {
# We show the host name right in the menu title so we know which Pi we are connected to
OPTION=$(whiptail --title "CraftBeerPi 3.0" --menu "Choose your option:" 15 56 7 \
"1" "Install CraftBeerPi" \
"2" "Clear Database" \
"3" "Add To Autostart" \
"4" "Remove From Autostart" \
"5" "Start CraftBeerPi" \
"6" "Stop CraftBeerPi" \
"7" "Software Update (git pull)" \
"8" "Reset File Changes (git reset --hard)" \
"9" "Clear all logs" \
"10" "Reboot Raspberry Pi" 3>&1 1>&2 2>&3)

BUTTON=$?
# Exit if user pressed cancel or escape
if [[ ($BUTTON -eq 1) || ($BUTTON -eq 255) ]]; then
exit 1
fi
if [ $BUTTON -eq 0 ]; then
case $OPTION in
1)
confirmAnswer "Would you like run apt-get update & apt-get upgrade?"
if [ $? = 0 ]; then
apt-get -y update; apt-get -y upgrade;
fi

confirmAnswer "Would you like to install wiringPI? This is required to control the GPIO"
if [ $? = 0 ]; then
git clone git://git.drogon.net/wiringPi;
cd wiringPi;
./build; cd ..;
rm -rf wiringPi;
fi

apt-get -y install python-setuptools
easy_install pip
apt-get -y install python-dev
apt-get -y install libpcre3-dev
pip install -r requirements.txt

confirmAnswer "Would you like to add active 1-wire support at your Raspberry PI now? IMPORTANT: The 1-wire thermometer must be conneted to GPIO 4!"
if [ $? = 0 ]; then
#apt-get -y update; apt-get -y upgrade;
echo '# CraftBeerPi 1-wire support' >> "/boot/config.txt"
echo 'dtoverlay=w1-gpio,gpiopin=4,pullup=on' >> "/boot/config.txt"

fi

whiptail --title "Installition Finished" --msgbox "CraftBeerPi installation finished! You must hit OK to continue." 8 78
show_menu
;;
2)
confirmAnswer "Are you sure you want to clear the CraftBeerPi. All hardware setting will be deleted"
if [ $? = 0 ]; then
sudo rm -f craftbeerpi.db
whiptail --title "Database Delted" --msgbox "The CraftBeerPi database was succesfully deleted. You must hit OK to continue." 8 78
show_menu
else
show_menu
fi
;;
3)
confirmAnswer "Are you sure you want to add CraftBeerPi to autostart"
if [ $? = 0 ]; then
sed "s@#DIR#@${PWD}@g" config/craftbeerpiboot > /etc/init.d/craftbeerpiboot
chmod 755 /etc/init.d/craftbeerpiboot;
update-rc.d craftbeerpiboot defaults;
whiptail --title "Added succesfull to autostart" --msgbox "The CraftBeerPi was added to autostart succesfully. You must hit OK to continue." 8 78
show_menu
else
show_menu
fi
;;
4)
confirmAnswer "Are you sure you want to remove CraftBeerPi from autostart"
if [ $? = 0 ]; then
sudo rm -f /etc/init.d/craftbeerpiboot
show_menu
else
show_menu
fi
;;
5)
sudo /etc/init.d/craftbeerpiboot start
whiptail --title "CraftBeerPi started" --msgbox "Please connect via Browser: http://<IP-ADDRESS>:5000" 8 78
show_menu
;;
6)
sudo /etc/init.d/craftbeerpiboot stop
whiptail --title "CraftBeerPi stoped" --msgbox "The software is stoped" 8 78
show_menu
;;
7)
confirmAnswer "Are you sure you want to pull a software update?"
if [ $? = 0 ]; then

whiptail --textbox /dev/stdin 20 50 <<<"$(git pull)"
show_menu
else
show_menu
fi
;;
8)
confirmAnswer "Are you sure you want to reset all file changes for this git respository (git reset --hard)?"
if [ $? = 0 ]; then
whiptail --textbox /dev/stdin 20 50 <<<"$(git reset --hard)"
show_menu
else
show_menu
fi
;;
9)
confirmAnswer "Are you sure you want to delete all CraftBeerPi log files"
if [ $? = 0 ]; then
sudo rm -rf logs/*.log
whiptail --title "Log files deleted" --msgbox "All CraftBeerPi Files are deleted. You must hit OK to continue." 8 78
show_menu
else
show_menu
fi
;;
10)
confirmAnswer "Are you sure you want to reboo the Raspberry Pi?"
if [ $? = 0 ]; then
sudo reboot
else
show_menu
fi
;;
esac
fi
}

if [ "$EUID" -ne 0 ]
then whiptail --title "Please run as super user (sudo)" --msgbox "Please run the install file -> sudo install.sh " 8 78
exit
fi

show_menu

+ 0
- 0
logs/empty Переглянути файл


+ 65
- 0
modules/__init__.py Переглянути файл

@@ -0,0 +1,65 @@
import json
import pprint
import sys, os
from flask import Flask, render_template, redirect
from flask_socketio import SocketIO, emit
from flask_sqlalchemy import SQLAlchemy
import logging
# Define the WSGI application object
from app_config import *
import pprint
from modules.core.db import get_db
@app.route('/')
def index():
return redirect('ui')
# Define the database object which is imported
# by modules and controllers
import modules.steps
import modules.config
import modules.logs
import modules.sensors
import modules.actor
import modules.notification
import modules.fermenter
import modules.addon
import modules.ui
import modules.system
import modules.buzzer
import modules.stats
import modules.kettle
from app_config import cbpi
# Build the database:
# This will create the database file using SQLAlchemy
pp = pprint.PrettyPrinter(indent=6)
def init_db():
print "INIT DB"
with app.app_context():
db = get_db()
try:
with app.open_resource('../config/schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
except Exception as e:
pass
init_db()
cbpi.run_init()
cbpi.run_background_processes()

+ 69
- 0
modules/actor/__init__.py Переглянути файл

@@ -0,0 +1,69 @@
import time
from flask_classy import route
from modules import DBModel, cbpi
from modules.core.baseview import BaseView

class Actor(DBModel):
__fields__ = ["name","type", "config", "hide"]
__table_name__ = "actor"
__json_fields__ = ["config"]

class ActorView(BaseView):
model = Actor
cache_key = "actors"

@classmethod
def post_init_callback(self, obj):
obj.state = 0
obj.power = 100

def post_post_callback(self, m):
self.api.init_actor(m.id)

def post_put_callback(self, m):

self.api.init_actor(m.id)

@route("<int:id>/switch/on", methods=["POST"])
def on(self, id):
self.api.switch_actor_on(id)
return ('', 204)

@route("<int:id>/switch/off", methods=["POST"])
def off(self, id):
self.api.switch_actor_off(id)
return ('', 204)

@route("<int:id>/power/<int:power>", methods=["POST"])
def power(self, id, power):
self.api.actor_power(id, power)
return ('', 204)

@route("<int:id>/toggle", methods=["POST"])
def toggle(self, id):

if self.api.cache.get("actors").get(id).state == 0:
self.on(id)
else:
self.off(id)
return ('', 204)

def toggleTimeJob(self, id, t):
self.api.cache.get("actors").get(int(id)).timer = int(time.time()) + int(t)
self.toggle(int(id))
self.api.socketio.sleep(t)
self.api.cache.get("actors").get(int(id)).timer = None
self.toggle(int(id))

@route("/<id>/toggle/<int:t>", methods=["POST"])
def toggleTime(self, id, t):
t = self.api.socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t)
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()

+ 1
- 0
modules/addon/__init__.py Переглянути файл

@@ -0,0 +1 @@
import endpoints

+ 202
- 0
modules/addon/endpoints.py Переглянути файл

@@ -0,0 +1,202 @@
import json

import sys
from flask import Blueprint, request, send_from_directory
from importlib import import_module
from modules import socketio, cbpi


from git import Repo
import os
import requests
import yaml
import shutil

blueprint = Blueprint('addon', __name__)


modules = {}

def merge(source, destination):
"""
Helper method to merge two dicts
:param source:
:param destination:
:return:
"""
for key, value in source.items():
if isinstance(value, dict):
# get node or create one
node = destination.setdefault(key, {})
merge(value, node)
else:
destination[key] = value

return destination

@blueprint.route('/', methods=['GET'])
def getPlugins():
"""
Endpoint for all plugins
:return:
"""
result = []
for filename in os.listdir("./modules/plugins"):
if filename.endswith(".DS_Store") or filename.endswith(".py") or filename.endswith(".pyc"):
continue
result.append(filename)

return json.dumps(result)

@blueprint.route('/<name>', methods=['GET'])
def getFile(name):
"""
Returns plugin code
:param name: plugin name
:return: the plugin code from __init__.py
"""
return send_from_directory('./plugins/'+name, "__init__.py")

@blueprint.route('/<name>', methods=['PUT'])
def createPlugin(name):

"""
Create a new plugin file
:param name: the plugin name
:return: empty http response 204
"""
if not os.path.exists("./modules/plugins/"+name):
os.makedirs("./modules/plugins/"+name)
with open("./modules/plugins/" + name + "/__init__.py", "wb") as fo:
fo.write("")
cbpi.emit_message("PLUGIN %s CREATED" % (name))
return ('', 204)
else:
cbpi.emit_message("Failed to create plugin %s. Name arlready in use" % (name))
return ('', 500)




@blueprint.route('/<name>', methods=['POST'])
def saveFile(name):

"""
save plugin code. code is provides via http body
:param name: the plugin name
:return: empty http reponse
"""
with open("./modules/plugins/"+name+"/__init__.py", "wb") as fo:
fo.write(request.get_data())
cbpi.emit_message("PLUGIN %s SAVED" % (name))

return ('', 204)

@blueprint.route('/<name>', methods=['DELETE'])
def deletePlugin(name):

"""
Delete plugin
:param name: plugin name
:return: HTTP 204 if ok - HTTP 500 if plugin not exists
"""
if os.path.isdir("./modules/plugins/"+name) is False:
return ('Dir Not found', 500)
shutil.rmtree("./modules/plugins/"+name)
cbpi.notify("Plugin deleted", "Plugin %s deleted successfully" % name)
return ('', 204)

@blueprint.route('/<name>/reload/', methods=['POST'])
def reload(name):
"""
hot reload plugnin
:param name:
:return:
"""
try:
if name in cache["modules"]:
reload(cache["modules"][name])
cbpi.emit_message("REALOD OF PLUGIN %s SUCCESSFUL" % (name))
return ('', 204)
else:
cache["modules"][name] = import_module("modules.plugins.%s" % (name))
return ('', 204)
except Exception as e:
cbpi.emit_message("REALOD OF PLUGIN %s FAILED" % (name))
return json.dumps(e.message)


@blueprint.route('/list', methods=['GET'])
def plugins():
"""
Read the central plugin yaml to get a list of all official plugins
:return:
"""
response = requests.get("https://raw.githubusercontent.com/craftbeerpi/pluginlist/master/plugins.yaml")
cbpi.cache["plugins"] = merge(yaml.load(response.text), cbpi.cache["plugins"])
for key, value in cbpi.cache["plugins"].iteritems():
value["installed"] = os.path.isdir("./modules/plugins/%s/" % (key))

return json.dumps(cbpi.cache["plugins"])


@blueprint.route('/<name>/download', methods=['POST'])
def download_addon(name):

plugin = cbpi.cache["plugins"].get(name)
plugin["loading"] = True
if plugin is None:
return ('', 404)
try:
Repo.clone_from(plugin.get("repo_url"), "./modules/plugins/%s/" % (name))
cbpi.notify("Download successful", "Plugin %s downloaded successfully" % name)
finally:
plugin["loading"] = False

return ('', 204)

@blueprint.route('/<name>/update', methods=['POST'])
def update_addon(name):
repo = Repo("./modules/plugins/%s/" % (name))
o = repo.remotes.origin
info = o.pull()
cbpi.notify("Plugin Updated", "Plugin %s updated successfully. Please restart the system" % name)
return ('', 204)


def loadCorePlugins():
for filename in os.listdir("./modules/base_plugins"):
if filename.endswith(".py") or filename.endswith(".pyc"):
continue
try:
modules[filename] = import_module("modules.base_plugins.%s" % (filename))
except Exception as e:


cbpi.notify("Failed to load plugin %s " % filename, str(e), type="danger", timeout=None)
cbpi.app.logger.error(e)

def loadPlugins():
for filename in os.listdir("./modules/plugins"):
if filename.endswith(".py") or filename.endswith(".pyc"):
continue
try:
modules[filename] = import_module("modules.plugins.%s" % (filename))
except Exception as e:
cbpi.notify("Failed to load plugin %s " % filename, str(e), type="danger", timeout=None)
cbpi.app.logger.error(e)

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





+ 57
- 0
modules/app_config.py Переглянути файл

@@ -0,0 +1,57 @@

import json
import sys, os
from flask import Flask, render_template, redirect, json, g


from flask_socketio import SocketIO, emit
from flask_sqlalchemy import SQLAlchemy
import logging

from sqlalchemy.ext.declarative import DeclarativeMeta

from modules.core.core import CraftBeerPi, ActorBase, SensorBase
from modules.core.db import DBModel

app = Flask(__name__)

logging.basicConfig(filename='./logs/app.log',level=logging.INFO)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../craftbeerpi.db'

#app.config['SQLALCHEMY_ECHO'] = False
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'craftbeerpi'
app.config['UPLOAD_FOLDER'] = './upload'
db = SQLAlchemy(app)

@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()


class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
try:
if isinstance(obj, DBModel):
return obj.__dict__
elif isinstance(obj, ActorBase):
return obj.state()
elif isinstance(obj, SensorBase):
return obj.get_value()
elif hasattr(obj, "callback"):
return obj()
else:
return None
except TypeError as e:
pass
return None

app.json_encoder = ComplexEncoder
socketio = SocketIO(app, json=json)
cbpi = CraftBeerPi(app, socketio)

app.logger.info("##########################################")
app.logger.info("### NEW STARTUP Version 3.0")
app.logger.info("##########################################")

+ 0
- 0
modules/base_plugins/__init__.py Переглянути файл


+ 95
- 0
modules/base_plugins/brew_steps/__init__.py Переглянути файл

@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
import time


from modules.core.props import Property, StepProperty
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)

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

# Check if timer finished and go to next step
if self.is_timer_finished() == True:
self.next()


@cbpi.step
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")
s = False

def init(self):
'''
Initialize Step. This method is called once at the beginning of the step
:return:
'''
# set target tep
self.s = False
self.set_target_temp(self.temp, self.kettle)



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) and self.s is False:
self.s = True
self.notify("Step Temp Reached!", "Please press the next button to continue", timeout=None)




+ 36
- 0
modules/base_plugins/dummy_temp/__init__.py Переглянути файл

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
import subprocess
import time

from modules import cbpi, socketio
from modules.core.hardware import SensorActive
from modules import cbpi
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 "°C" if self.get_config_parameter("unit", "C") == "C" else "°F"

def stop(self):
pass

def execute(self):
while self.is_running():

self.data_received(self.temp)
socketio.sleep(5)












+ 27
- 0
modules/base_plugins/fermenter_hysteresis/__init__.py Переглянути файл

@@ -0,0 +1,27 @@
from modules import cbpi
from modules.core.controller import KettleController, FermenterController
from modules.core.props import Property


@cbpi.fermentation_controller
class Hysteresis(FermenterController):

on = Property.Number("Offset On", True, 0)
off = Property.Number("Offset Off", True, 0)

def stop(self):

super(FermenterController, self).stop()
self.heater_off()

def run(self):
while self.is_running():
print "Temp %s" % self.get_temp()
if self.get_temp() < self.get_target_temp() - int(self.on):
self.heater_on(100)
elif self.get_temp() >= self.get_target_temp() - int(self.off):
self.heater_off()
else:
self.heater_off()
self.sleep(1)


+ 93
- 0
modules/base_plugins/gpio_actor/__init__.py Переглянути файл

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
import time

from modules import cbpi
from modules.core.hardware import ActorBase, SensorPassive, SensorActive
from modules.core.props import Property

try:
import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM)
except Exception as e:
print e
pass



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

def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT)
GPIO.output(int(self.gpio), 0)

def on(self, power=0):
print "GPIO ON %s" % str(self.gpio)
GPIO.output(int(self.gpio), 1)

def off(self):
print "GPIO OFF"
GPIO.output(int(self.gpio), 0)

@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])
duty_cylce = Property.Number("Duty Cycle", configurable=True)

p = None
power = 100

def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT)
GPIO.output(int(self.gpio), 0)


def on(self, power=None):
if power is not None:
self.power = int(power)

if self.duty_cylce is None:
duty_cylce = 50

self.p = GPIO.PWM(int(self.gpio), int(self.duty_cylce))
self.p.start(int(self.power))

def set_power(self, power):
if power is not None:
self.power = int(power)
self.p.ChangeDutyCycle(self.power)

def off(self):
print "GPIO OFF"
self.p.stop()


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

def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT)
GPIO.output(int(self.gpio), 1)

def on(self, power=0):

GPIO.output(int(self.gpio), 0)

def off(self):

GPIO.output(int(self.gpio), 1)

@cbpi.actor
class Dummy(ActorBase):

def on(self, power=100):
print "ON"

def off(self):
print "OFF"

+ 39
- 0
modules/base_plugins/hysteresis/__init__.py Переглянути файл

@@ -0,0 +1,39 @@
from modules import cbpi
from modules.core.controller import KettleController
from modules.core.props import Property


@cbpi.controller
class Hysteresis(KettleController):

# Custom Properties

on = Property.Number("Offset On", True, 0)
off = Property.Number("Offset Off", True, 0)

def stop(self):
'''
Invoked when the automatic is stopped.
Normally you switch off the actors and clean up everything
:return: None
'''
super(KettleController, self).stop()
self.heater_off()




def run(self):
'''
Each controller is exectuted in its own thread. The run method is the entry point
:return:
'''
while self.is_running():
if self.get_temp() < self.get_target_temp() - int(self.on):
self.heater_on(100)
elif self.get_temp() >= self.get_target_temp() - int(self.off):
self.heater_off()
else:
self.heater_off()
self.sleep(1)


+ 112
- 0
modules/base_plugins/one_wire/__init__.py Переглянути файл

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
import os
from subprocess import Popen, PIPE, call

from modules import cbpi, app
from modules.core.hardware import SensorPassive
import json
import os, re, threading, time
from flask import Blueprint, render_template, request
from modules.core.props import Property

blueprint = Blueprint('one_wire', __name__)

temp = 22

def getSensors():
try:
arr = []
for dirname in os.listdir('/sys/bus/w1/devices'):
if (dirname.startswith("28") or dirname.startswith("10")):
arr.append(dirname)
return arr
except:
return []




class myThread (threading.Thread):

value = 0


def __init__(self, sensor_name):
threading.Thread.__init__(self)
self.value = 0
self.sensor_name = sensor_name
self.runnig = True

def shutdown(self):
pass

def stop(self):
self.runnig = False

def run(self):

while self.runnig:
try:
app.logger.info("READ TEMP")
## Test Mode
if self.sensor_name is None:
return
with open('/sys/bus/w1/devices/w1_bus_master1/%s/w1_slave' % self.sensor_name, 'r') as content_file:
content = content_file.read()
if (content.split('\n')[0].split(' ')[11] == "YES"):
temp = float(content.split("=")[-1]) / 1000 # temp in Celcius
self.value = temp
except:
pass

time.sleep(4)



@cbpi.sensor
class ONE_WIRE_SENSOR(SensorPassive):

sensor_name = Property.Select("Sensor", getSensors())

def init(self):

self.t = myThread(self.sensor_name)

def shudown():
shudown.cb.shutdown()
shudown.cb = self.t

self.t.start()

def stop(self):
try:
self.t.stop()
except:
pass

def read(self):
if self.get_config_parameter("unit", "C") == "C":
self.data_received(self.t.value)
else:
self.data_received(format(9.0 / 5.0 * self.t.value + 32, '.2f'))

@classmethod
def init_global(self):
try:
call(["modprobe", "w1-gpio"])
call(["modprobe", "w1-therm"])
except Exception as e:
pass


@blueprint.route('/<int:t>', methods=['GET'])
def set_temp(t):
global temp
temp = t
return ('', 204)


@cbpi.initalizer()
def init(cbpi):
cbpi.app.logger.info("INITIALIZE ONE WIRE MODULE")
cbpi.app.register_blueprint(blueprint, url_prefix='/api/one_wire')

+ 51
- 0
modules/buzzer/__init__.py Переглянути файл

@@ -0,0 +1,51 @@
import time
from thread import start_new_thread
from modules import cbpi

try:
import RPi.GPIO as GPIO
except Exception as e:
pass

class Buzzer(object):

sound = ["H", 0.1, "L"]
def __init__(self, gpio):
try:
cbpi.app.logger.info("INIT BUZZER NOW GPIO%s" % gpio)
self.gpio = gpio
GPIO.setmode(GPIO.BCM)
GPIO.setup(gpio, GPIO.OUT)
self.state = True
cbpi.app.logger.info("BUZZER SETUP OK")
except Exception as e:
self.state = False

def beep(self):
if self.state is False:
cbpi.app.logger.error("BUZZER not working")
return

def play(sound):
try:
for i in sound:
if (isinstance(i, str)):
if i == "H":
GPIO.output(int(self.gpio), GPIO.HIGH)
else:
GPIO.output(int(self.gpio), GPIO.LOW)
else:
time.sleep(i)
except Exception as e:
pass

start_new_thread(play, (self.sound,))

@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()
cbpi.app.logger.info("INIT OK")

+ 59
- 0
modules/config/__init__.py Переглянути файл

@@ -0,0 +1,59 @@
import time

from flask import json, request
from flask_classy import route
from modules import DBModel, cbpi, get_db
from modules.core.baseview import BaseView

class Config(DBModel):
__fields__ = ["type", "value", "description", "options"]
__table_name__ = "config"
__json_fields__ = ["options"]
__priamry_key__ = "name"


class ConfigView(BaseView):
model = Config
cache_key = "config"

@route('/<name>', methods=["PUT"])
def put(self, name):

data = request.json
data["name"] = name
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])
return json.dumps(self.api.cache.get(self.cache_key)[name].__dict__)

@route('/<id>', methods=["GET"])
def getOne(self, id):
return ('NOT SUPPORTED', 400)

@route('/<id>', methods=["DELETE"])
def delete(self, id):
return ('NOT SUPPORTED', 400)

@route('/', methods=["POST"])
def post(self):
return ('NOT SUPPORTED', 400)

@classmethod
def init_cache(cls):

with cls.api.app.app_context():
cls.api.cache[cls.cache_key] = {}
for key, value in cls.model.get_all().iteritems():
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"
ConfigView.register(cbpi.app, route_base='/api/config')
ConfigView.init_cache()

+ 0
- 0
modules/core/__init__.py Переглянути файл


+ 109
- 0
modules/core/baseview.py Переглянути файл

@@ -0,0 +1,109 @@
from flask import request, json
from flask_classy import route, FlaskView
from modules import cbpi


class BaseView(FlaskView):

as_array = False
cache_key = None
api = cbpi

@route('/<int:id>')
def getOne(self, id):

if self.api.cache.get(self.cache_key) is not None:
return json.dumps(self.api.cache.get(self.cache_key).get(id))
else:
return json.dumps(self.model.get_one(id))

@route('/')
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):
pass


def post_post_callback(self, m):
pass

@route('/', methods=["POST"])
def post(self):
data = request.json
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)

return json.dumps(m)

def pre_put_callback(self, m):
pass

def post_put_callback(self, m):
pass


@route('/<int:id>', methods=["PUT"])
def put(self, id):
data = request.json
data["id"] = id
try:
del data["instance"]
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.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])
return json.dumps(self.api.cache.get(self.cache_key)[id])
else:
m = self.model.update(**data)

self.post_put_callback(m)
return json.dumps(m)


def pre_delete_callback(self, m):
pass

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])
del self.api.cache.get(self.cache_key)[id]
m = self.model.delete(id)

def post_delete_callback(self, id):
pass
return ('',204)

@classmethod
def post_init_callback(cls, obj):
pass

@classmethod
def init_cache(cls):
with cls.api.app.app_context():

if cls.model.__as_array__ is True:
cls.api.cache[cls.cache_key] = []

for value in cls.model.get_all():
cls.post_init_callback(value)
cls.api.cache[cls.cache_key].append(value)
else:
cls.api.cache[cls.cache_key] = {}
for key, value in cls.model.get_all().iteritems():
cls.post_init_callback(value)
cls.api.cache[cls.cache_key][key] = value

+ 134
- 0
modules/core/controller.py Переглянути файл

@@ -0,0 +1,134 @@
from modules import cbpi


class ActorController(object):

@cbpi.try_catch(None)
def actor_on(self, power=100, id=None):

if id is None:
id = self.heater
self.api.switch_actor_on(int(id), power=power)

@cbpi.try_catch(None)
def actor_off(self, id=None):
if id is None:
id = self.heater

self.api.switch_actor_off(int(id))


class SensorController(object):

@cbpi.try_catch(None)
def get_sensor_value(self, id=None):

if id is None:
id = self.sensor

return cbpi.get_sensor_value(id)



class ControllerBase(object):
__dirty = False
__running = False

@staticmethod
def init_global():
print "GLOBAL CONTROLLER INIT"

def notify(self, headline, message, type="success", timeout=5000):
self.api.notify(headline, message, type, timeout)

def is_running(self):
return self.__running

def init(self):
self.__running = True


def sleep(self, seconds):
self.api.socketio.sleep(seconds)

def stop(self):
self.__running = False


def __init__(self, *args, **kwds):
for a in kwds:
super(ControllerBase, self).__setattr__(a, kwds.get(a))
self.api = kwds.get("api")
self.heater = kwds.get("heater")
self.sensor = kwds.get("sensor")


def run(self):
pass

class KettleController(ControllerBase, ActorController, SensorController):

def __init__(self, *args, **kwds):
ControllerBase.__init__(self, *args, **kwds)
self.kettle_id = kwds.get("kettle_id")

@cbpi.try_catch(None)
def heater_on(self, power=100):
k = self.api.cache.get("kettle").get(self.kettle_id)
if k.heater is not None:
self.actor_on(power, int(k.heater))




@cbpi.try_catch(None)
def heater_off(self):
k = self.api.cache.get("kettle").get(self.kettle_id)
if k.heater is not None:
self.actor_off(int(k.heater))

@cbpi.try_catch(None)
def get_temp(self, id=None):
if id is None:
id = self.kettle_id
return self.get_sensor_value(int(self.api.cache.get("kettle").get(id).sensor))

@cbpi.try_catch(None)
def get_target_temp(self, id=None):
if id is None:
id = self.kettle_id
return self.api.cache.get("kettle").get(id).target_temp

class FermenterController(ControllerBase, ActorController, SensorController):

def __init__(self, *args, **kwds):
ControllerBase.__init__(self, *args, **kwds)
self.fermenter_id = kwds.get("fermenter_id")
self.cooler = kwds.get("cooler")

@cbpi.try_catch(None)
def get_target_temp(self, id=None):

if id is None:
id = self.fermenter_id
return self.api.cache.get("fermenter").get(id).target_temp

@cbpi.try_catch(None)
def heater_on(self, power=100):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if k.heater is not None:
self.actor_on(int(f.heater))

@cbpi.try_catch(None)
def heater_off(self):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if k.heater is not None:
self.actor_off(int(f.heater))

@cbpi.try_catch(None)
def get_temp(self, id=None):

if id is None:
id = self.fermenter_id
return self.get_sensor_value(int(self.api.cache.get("fermenter").get(id).sensor))


+ 452
- 0
modules/core/core.py Переглянути файл

@@ -0,0 +1,452 @@
import pprint

import sqlite3
from flask import make_response, g
import datetime
from datetime import datetime
from flask.views import MethodView
from flask_classy import FlaskView, route
from flask_sqlalchemy import SQLAlchemy
from time import localtime, strftime
from functools import wraps, update_wrapper
from props import *

from hardware import *

import time
import uuid


class NotificationAPI(object):
pass

class ActorAPI(object):

def init_actors(self):
self.app.logger.info("Init Actors")
t = self.cache.get("actor_types")
for key, value in t.iteritems():
value.get("class").init_global()

for key in self.cache.get("actors"):
self.init_actor(key)

def init_actor(self, id):
try:
value = self.cache.get("actors").get(int(id))
cfg = value.config.copy()
cfg.update(dict(api=self, id=id, name=value.name))
cfg.update(dict(api=self, id=id, name=value.name))
clazz = self.cache.get("actor_types").get(value.type).get("class")
value.instance = clazz(**cfg)
value.instance.init()
value.state = 0
value.power = 100
except Exception as e:
self.notify("Actor Error", "Failed to setup actor %s. Please check the configuraiton" % value.name,
type="danger", timeout=None)
self.app.logger.error("Initializing of Actor %s failed" % id)

def switch_actor_on(self, id, power=None):
actor = self.cache.get("actors").get(id)

if actor.state == 1:
return

actor.instance.on(power=power)
actor.state = 1
if power is not None:

actor.power = power
self.emit("SWITCH_ACTOR", actor)

def actor_power(self, id, power=100):
actor = self.cache.get("actors").get(id)
actor.instance.set_power(power=power)
actor.power = power
self.emit("SWITCH_ACTOR", actor)

def switch_actor_off(self, id):
actor = self.cache.get("actors").get(id)

if actor.state == 0:
return


actor.instance.off()
actor.state = 0


self.emit("SWITCH_ACTOR", actor)

class SensorAPI(object):

def init_sensors(self):
'''
Initialize all sensors
:return:
'''

self.app.logger.info("Init Sensors")

t = self.cache.get("sensor_types")
for key, value in t.iteritems():
value.get("class").init_global()

for key in self.cache.get("sensors"):
self.init_sensor(key)

def stop_sensor(self, id):

try:
self.cache.get("sensors").get(id).instance.stop()
except Exception as e:

self.app.logger.info("Stop Sensor Error")
pass


def init_sensor(self, id):
'''
initialize sensor by id
:param id:
:return:
'''

def start_active_sensor(instance):
'''
start active sensors as background job
:param instance:
:return:
'''
instance.execute()

try:
if id in self.cache.get("sensor_instances"):
self.cache.get("sensor_instances").get(id).stop()
value = self.cache.get("sensors").get(id)

cfg = value.config.copy()
cfg.update(dict(api=self, id=id, name=value.name))
clazz = self.cache.get("sensor_types").get(value.type).get("class")
value.instance = clazz(**cfg)
value.instance.init()
if isinstance(value.instance, SensorPassive):
# Passive Sensors
value.mode = "P"
else:
# Active Sensors
value.mode = "A"
t = self.socketio.start_background_task(target=start_active_sensor, instance=value.instance)

except Exception as e:

self.notify("Sensor Error", "Failed to setup Sensor %s. Please check the configuraiton" % value.name, type="danger", timeout=None)
self.app.logger.error("Initializing of Sensor %s failed" % id)

def receive_sensor_value(self, id, value):
self.emit("SENSOR_UPDATE", self.cache.get("sensors")[id])
self.save_to_file(id, value)

def save_to_file(self, id, value, prefix="sensor"):
filename = "./logs/%s_%s.log" % (prefix, str(id))
formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
msg = str(formatted_time) + "," +str(value) + "\n"

with open(filename, "a") as file:
file.write(msg)

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:
file.write("%s,%s\n" % (formatted_time, text))

def shutdown_sensor(self, id):
self.cache.get("sensors")[id].stop()


def get_sensor_value(self, id):
try:
id = int(id)
return int(self.cache.get("sensors")[id].instance.last_value)
except Exception as e:

return None

class CacheAPI(object):

def get_sensor(self, id):
try:
return self.cache["sensors"][id]
except:
return None

def get_actor(self, id):
try:
return self.cache["actors"][id]
except:
return None

class CraftBeerPi(ActorAPI, SensorAPI):

cache = {
"init": {},
"config": {},
"actor_types": {},
"sensor_types": {},
"sensors": {},
"sensor_instances": {},
"init": [],
"background":[],
"step_types": {},
"controller_types": {},
"messages": [],
"plugins": {},
"fermentation_controller_types": {},
"fermenter_task": {}
}
buzzer = None
eventbus = {}


# constructor
def __init__(self, app, socketio):
self.app = app
self.socketio = socketio


def emit(self, key, data):
self.socketio.emit(key, data, namespace='/brew')

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)

def beep(self):

if self.buzzer is not None:
self.buzzer.beep()


def add_cache_callback(self, key, method):
method.callback = True
self.cache[key] = method

def get_config_parameter(self, key, default):
cfg = self.cache.get("config").get(key)

if cfg is None:
return default
else:
return cfg.value

def clear_cache(self, key, is_array=False):
if is_array:
self.cache[key] = []
else:
self.cache[key] = {}

# helper method for parsing props
def __parseProps(self, key, cls):
name = cls.__name__
self.cache[key][name] = {"name": name, "class": cls, "properties": []}
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})
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})
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})
return cls


def actor(self, cls):
return self.__parseProps("actor_types", cls)

def sensor(self, cls):
return self.__parseProps("sensor_types", cls)

def controller(self, cls):
return self.__parseProps("controller_types", cls)

def fermentation_controller(self, cls):
return self.__parseProps("fermentation_controller_types", cls)

def get_controller(self, name):
return self.cache["controller_types"].get(name)

def get_fermentation_controller(self, name):
return self.cache["fermentation_controller_types"].get(name)
# Step action
def action(self,label):
def real_decorator(func):
func.action = True
func.label = label
return func
return real_decorator

# step decorator
def step(self, cls):

key = "step_types"
name = cls.__name__
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), StepProperty.Number):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable})
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})
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})
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})
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})
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})

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


# Event Bus
def event(self, name, async=False):

def real_decorator(function):
if self.eventbus.get(name) is None:
self.eventbus[name] = []
self.eventbus[name].append({"function": function, "async": async})
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
return real_decorator

def emit_message(self, message):
self.emit_event(name="MESSAGE", message=message)

def emit_event(self, name, **kwargs):
for i in self.eventbus.get(name, []):
if i["async"] is False:
i["function"](**kwargs)
else:
t = self.socketio.start_background_task(target=i["function"], **kwargs)

# initializer decorator
def initalizer(self, order=0):
def real_decorator(function):
self.cache["init"].append({"function": function, "order": order})
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
return real_decorator



def try_catch(self, errorResult="ERROR"):
def real_decorator(function):
def wrapper(*args, **kwargs):
try:
return function(*args, **kwargs)
except:
self.app.logger.error("Exception in function %s. Return default %s" % (function.__name__, errorResult))
return errorResult
return wrapper

return real_decorator

def nocache(self, view):
@wraps(view)
def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs))
response.headers['Last-Modified'] = datetime.now()
response.headers[
'Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
return response

return update_wrapper(no_cache, view)

def init_kettle(self, id):
try:
value = self.cache.get("kettle").get(id)
value["state"] = False
except:
self.notify("Kettle Setup Faild", "Please check %s configuration" % value.name, type="danger", timeout=None)
self.app.logger.error("Initializing of Kettle %s failed" % id)


def run_init(self):
'''
call all initialziers after startup
:return:
'''
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__)
i.get("function")(self)



def backgroundtask(self, key, interval, config_parameter=None):

'''
Background Task Decorator
:param key:
:param interval:
:param config_parameter:
:return:
'''
def real_decorator(function):
self.cache["background"].append({"function": function, "key": key, "interval": interval, "config_parameter": config_parameter})
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
return real_decorator

def run_background_processes(self):
'''
call all background task after startup
:return:
'''
self.app.logger.info("Start Background")

def job(interval, method):
while True:
try:
method()
except Exception as e:
self.app.logger.error("Exception" + method.__name__ + ": " + str(e))
self.socketio.sleep(interval)


for value in self.cache.get("background"):
t = self.socketio.start_background_task(target=job, interval=value.get("interval"), method=value.get("function"))

+ 115
- 0
modules/core/db.py Переглянути файл

@@ -0,0 +1,115 @@
import sqlite3

from flask import json, g


def get_db():
db = getattr(g, '_database', None)
if db is None:
def dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
db = g._database = sqlite3.connect('craftbeerpi.db')
db.row_factory = dict_factory
return db

class DBModel(object):

__priamry_key__ = "id"
__as_array__ = False
__order_by__ = None
__json_fields__ = []

def __init__(self, args):

self.__setattr__(self.__priamry_key__, args.get(self.__priamry_key__))
for f in self.__fields__:
if f in self.__json_fields__:
if args.get(f) is not None:

if isinstance(args.get(f) , dict) or isinstance(args.get(f) , list) :
self.__setattr__(f, args.get(f))
else:
self.__setattr__(f, json.loads(args.get(f)))
else:
self.__setattr__(f, None)
else:
self.__setattr__(f, args.get(f))

@classmethod
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__))
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 = {}
for r in cur.fetchall():
result[r.get(cls.__priamry_key__)] = cls(r)
return result

@classmethod
def get_one(cls, id):
cur = get_db().cursor()
cur.execute("SELECT * FROM %s WHERE %s = ?" % (cls.__table_name__, cls.__priamry_key__), (id,))
r = cur.fetchone()
if r is not None:
return cls(r)
else:
return None

@classmethod
def delete(cls, id):
cur = get_db().cursor()
cur.execute("DELETE FROM %s WHERE %s = ? " % (cls.__table_name__, cls.__priamry_key__), (id,))
get_db().commit()

@classmethod
def insert(cls, **kwargs):
cur = get_db().cursor()

query = 'INSERT INTO %s (%s) VALUES (%s)' % (
cls.__table_name__,
', '.join("'%s'" % str(x) for x in cls.__fields__),
', '.join(['?'] * len(cls.__fields__)))

data = ()
for f in cls.__fields__:
if f in cls.__json_fields__:
data = data + (json.dumps(kwargs.get(f)),)
else:
data = data + (kwargs.get(f),)

cur.execute(query, data)
get_db().commit()
i = cur.lastrowid
kwargs["id"] = i

return cls(kwargs)

@classmethod
def update(cls, **kwargs):
cur = get_db().cursor()
query = 'UPDATE %s SET %s WHERE %s = ?' % (
cls.__table_name__,
', '.join("'%s' = ?" % str(x) for x in cls.__fields__),cls.__priamry_key__)

data = ()
for f in cls.__fields__:
if f in cls.__json_fields__:
data = data + (json.dumps(kwargs.get(f)),)
else:
data = data + (kwargs.get(f),)

data = data + (kwargs.get(cls.__priamry_key__),)
cur.execute(query, data)
get_db().commit()
return cls(kwargs)

+ 104
- 0
modules/core/hardware.py Переглянути файл

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-

class Base(object):
__dirty = False

@classmethod
def init_global(cls):
print "GLOBAL ACTOR INIT"

def get_config_parameter(self, key, default_value):
return self.api.get_config_parameter(key, default_value)

def init(self):
print "INIT BASE"

def stop(self):
print "STOP HARDWARE"

def update(self, **kwds):
pass

def __init__(self, *args, **kwds):
for a in kwds:
super(Base, self).__setattr__(a, kwds.get(a))
self.api = kwds.get("api")
self.id = kwds.get("id")
self.value = None
self.__dirty = False

def __setattr__(self, name, value):

if name != "_Base__dirty":
self.__dirty = True
super(Base, self).__setattr__(name, value)
else:
super(Base, self).__setattr__(name, value)


class SensorBase(Base):

last_value = 0

def init(self):
print "INIT Base SENSOR"

def stop(self):
print "STOP SENSOR"

def data_received(self, data):


self.last_value = data
self.api.receive_sensor_value(self.id, data)

def get_unit(self):
if self.get_config_parameter("unit", "C") == "C":
return "°C"
else:
return "°F"

def get_value(self):

return {"value": self.last_value, "unit": self.get_unit()}

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


class SensorPassive(SensorBase):
def init(self):
print "INIT PASSIV SENSOR"
pass

def read(self):
return 0


class ActorBase(Base):

def state(self):
return 1

def set_power(self, power):
print "SET POWER TO %s" % power

def on(self, power=0):
print "ON"

def off(self):
print "OFF"

+ 38
- 0
modules/core/props.py Переглянути файл

@@ -0,0 +1,38 @@
class PropertyType(object):
pass

class Property(object):
class Select(PropertyType):
def __init__(self, label, options):
PropertyType.__init__(self)
self.label = label
self.options = options

class Number(PropertyType):
def __init__(self, label, configurable=False, default_value=0, unit=""):
PropertyType.__init__(self)
self.label = label
self.configurable = configurable

class Text(PropertyType):
def __init__(self, label, configurable=False, default_value=""):
PropertyType.__init__(self)
self.label = label
self.configurable = configurable

class StepProperty(Property):
class Actor(PropertyType):
def __init__(self, label):
PropertyType.__init__(self)
self.label = label
self.configurable = True
class Sensor(PropertyType):
def __init__(self, label):
PropertyType.__init__(self)
self.label = label
self.configurable = True
class Kettle(PropertyType):
def __init__(self, label):
PropertyType.__init__(self)
self.label = label
self.configurable = True

+ 132
- 0
modules/core/step.py Переглянути файл

@@ -0,0 +1,132 @@
from modules import cbpi
from modules.core.props import StepProperty, Property
import time


class NotificationAPI(object):
def notify(self, headline, message, type="success", timeout=5000):
self.api.notify(headline, message, type, timeout)

class ActorAPI(NotificationAPI):

@cbpi.try_catch(None)
def actor_on(self, id, power=100):
self.api.switch_actor_on(int(id), power=power)

@cbpi.try_catch(None)
def actor_off(self, id):
self.api.switch_actor_off(int(id))

class SensorAPI(NotificationAPI):

@cbpi.try_catch(None)
def get_sensor_value(self, id):
return cbpi.get_sensor_value(id)

class KettleAPI(NotificationAPI):

@cbpi.try_catch(None)
def get_kettle_temp(self, id=None):
id = int(id)
if id is None:
id = self.kettle_id
return cbpi.get_sensor_value(int(self.api.cache.get("kettle").get(id).sensor))

@cbpi.try_catch(None)
def get_target_temp(self, id=None):
id = int(id)
if id is None:
id = self.kettle_id
return self.api.cache.get("kettle").get(id).target_temp

def set_target_temp(self, temp, id=None):
temp = int(temp)

try:
if id is None:
self.api.emit_event("SET_TARGET_TEMP", id=self.kettle_id, temp=temp)
else:
self.api.emit_event("SET_TARGET_TEMP", id=id, temp=temp)
except Exception as e:

self.notify("Faild to set Target Temp", "", type="warning")

class Timer(object):
timer_end = Property.Number("TIMER_END", configurable=False)

def start_timer(self, timer):
if self.timer_end is not None:
return
self.timer_end = int(time.time()) + timer


def stop_timer(self):
if self.timer_end is not None:
self.timer_end = None

def is_timer_running(self):
if self.timer_end is not None:
return True
else:
return False

def is_timer_finished(self):
if self.timer_end is None:
return None
if self.timer_end <= int(time.time()):
return True
else:
return False


class StepBase(Timer, ActorAPI, SensorAPI, KettleAPI):

__dirty = False
managed_fields = []
n = False

def next(self):
self.n = True

def init(self):
print "INIT STEP"

def finish(self):
print "FINSIH STEP"

def reset(self):
print "REST STEP"

def execute(self):
print "-------------"
print "Step Info"
print "Kettle ID: %s" % self.kettle_id
print "ID: %s" % self.id


def __init__(self, *args, **kwds):
for a in kwds:
super(StepBase, self).__setattr__(a, kwds.get(a))
self.api = kwds.get("api")
self.id = kwds.get("id")

self.name = kwds.get("name")
self.kettle_id = kwds.get("kettleid")
self.value = None
self.__dirty = False


def is_dirty(self):
return self.__dirty

def reset_dirty(self):
self.__dirty = False

def __setattr__(self, name, value):

if name != "_StepBase__dirty" and name in self.managed_fields:
self.__dirty = True
super(StepBase, self).__setattr__(name, value)
else:
super(StepBase, self).__setattr__(name, value)


+ 306
- 0
modules/fermenter/__init__.py Переглянути файл

@@ -0,0 +1,306 @@
import time
from flask import request
from flask_classy import route

from modules import DBModel, cbpi, get_db
from modules.core.baseview import BaseView


class Fermenter(DBModel):
__fields__ = ["name", "brewname", "sensor", "sensor2", "sensor3", "heater", "cooler", "logic", "config", "target_temp"]
__table_name__ = "fermenter"
__json_fields__ = ["config"]

class FermenterStep(DBModel):
__fields__ = ["name", "days", "hours", "minutes", "temp", "direction", "order", "state", "start", "end", "timer_start", "fermenter_id"]
__table_name__ = "fermenter_step"

@classmethod
def get_by_fermenter_id(cls, id):
cur = get_db().cursor()
cur.execute("SELECT * FROM %s WHERE fermenter_id = ?" % cls.__table_name__,(id,))
result = []
for r in cur.fetchall():
result.append(cls(r))
return result

@classmethod
def get_max_order(cls,id):
cur = get_db().cursor()
cur.execute("SELECT max(fermenter_step.'order') as 'order' FROM %s WHERE fermenter_id = ?" % cls.__table_name__, (id,))
r = cur.fetchone()
return r.get("order")

@classmethod
def update_state(cls, id, state):
cur = get_db().cursor()
cur.execute("UPDATE %s SET state = ? WHERE id =?" % cls.__table_name__, (state, id))
get_db().commit()

@classmethod
def update_timer(cls, id, timer):
cur = get_db().cursor()
cur.execute("UPDATE %s SET timer_start = ? WHERE id =?" % cls.__table_name__, (timer, id))
get_db().commit()

@classmethod
def get_by_state(cls, state):
cur = get_db().cursor()
cur.execute("SELECT * FROM %s WHERE state = ?" % cls.__table_name__, state)
r = cur.fetchone()
if r is not None:
return cls(r)
else:
return None

@classmethod
def reset_all_steps(cls,id):
cur = get_db().cursor()
cur.execute("UPDATE %s SET state = 'I', start = NULL, end = NULL, timer_start = NULL WHERE fermenter_id = ?" % cls.__table_name__, (id,))
get_db().commit()

class FermenterView(BaseView):
model = Fermenter
cache_key = "fermenter"

def post_post_callback(self, m):
m.state = False
m.steps = []


@route('/<int:id>/targettemp/<temp>', methods=['POST'])
def postTargetTemp(self, id, temp):
if temp is None or not temp:
return ('', 500)
id = int(id)
temp = float(temp)
cbpi.cache.get(self.cache_key)[id].target_temp = float(temp)
self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
cbpi.emit("UPDATE_FERMENTER_TARGET_TEMP", {"id": id, "target_temp": temp})
return ('', 204)

@route('/<int:id>/brewname', methods=['POST'])
def postBrewName(self, id):
data = request.json
brewname = data.get("brewname")
cbpi.cache.get(self.cache_key)[id].brewname = brewname
self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
cbpi.emit("UPDATE_FERMENTER_BREWNAME", {"id": id, "brewname": brewname})
return ('', 204)

@classmethod
def post_init_callback(cls, obj):
obj.steps = FermenterStep.get_by_fermenter_id(obj.id)
obj.state = False

@route('/<int:id>/step', methods=['POST'])
def postStep(self, id):
data = request.json
order_max = FermenterStep.get_max_order(id)
order = order_max + 1 if order_max is not None else 1
data["order"] = order
data["days"] = 0 if data["days"] == "" else data["days"]
data["hours"] = 0 if data["hours"] == "" else data["hours"]
data["minutes"] = 0 if data["minutes"] == "" else data["minutes"]
data["temp"] = 0 if data["temp"] == "" else data["temp"]
data["state"] = "I"
data["name"] = "NO NAME" if data["name"] == "" else data["name"]
f = FermenterStep.insert(**data)

cbpi.cache.get(self.cache_key)[id].steps.append(f)

cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])

return ('', 204)

@route('/<int:id>/step/<int:stepid>', methods=["PUT"])
def putStep(self, id, stepid):
data = request.json
# Select modal

data["id"] = stepid
data["fermenter_id"] = id

data["days"] = 0 if data["days"] == "" else data["days"]
data["hours"] = 0 if data["hours"] == "" else data["hours"]
data["minutes"] = 0 if data["minutes"] == "" else data["minutes"]

for s in cbpi.cache.get(self.cache_key)[id].steps:
if s.id == stepid:
s.__dict__.update(**data)

FermenterStep.update(**s.__dict__)
break
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204)

@route('/<int:id>/step/<int:stepid>', methods=["DELETE"])
def deleteStep(self, id, stepid):

for idx, s in enumerate(cbpi.cache.get(self.cache_key)[id].steps):
if s.id == stepid:
del cbpi.cache.get(self.cache_key)[id].steps[idx]
FermenterStep.delete(s.id)
break
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204)

@route('/<int:id>/start', methods=['POST'])
def start_fermentation(self, id):
active = None
for idx, s in enumerate(cbpi.cache.get(self.cache_key)[id].steps):
if s.state == 'A':
active = s
break

inactive = None
for idx, s in enumerate(cbpi.cache.get(self.cache_key)[id].steps):
if s.state == 'I':
inactive = s
break

if active is not None:

active.state = 'D'
active.end = time.time()
FermenterStep.update(**active.__dict__)

del cbpi.cache["fermenter_task"][id]

if inactive is not None:
fermenter = self.get_fermenter(inactive.fermenter_id)
current_temp = cbpi.get_sensor_value(int(fermenter.sensor))

inactive.state = 'A'
inactive.start = time.time()
inactive.direction = "C" if current_temp >= inactive.temp else "H"
FermenterStep.update(**inactive.__dict__)

self.postTargetTemp(id, inactive.temp)

cbpi.cache["fermenter_task"][id] = inactive

cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204)

@route('/<int:id>/reset', methods=["POST"])
def reset(self, id):
FermenterStep.reset_all_steps(id)

cbpi.cache[self.cache_key][id].steps = FermenterStep.get_by_fermenter_id(id)

if id in cbpi.cache["fermenter_task"]:
del cbpi.cache["fermenter_task"][id]

cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204)

@route('/<int:id>/automatic', methods=['POST'])
def toggle(self, id):
fermenter = cbpi.cache.get(self.cache_key)[id]
try:

if fermenter.state is False:
# Start controller
if fermenter.logic is not None:
cfg = fermenter.config.copy()
cfg.update(
dict(api=cbpi, fermenter_id=fermenter.id, heater=fermenter.heater, sensor=fermenter.sensor))
instance = cbpi.get_fermentation_controller(fermenter.logic).get("class")(**cfg)
instance.init()
fermenter.controller_instance = instance

def run(instance):
instance.run()

t = cbpi.socketio.start_background_task(target=run, instance=instance)
fermenter.state = not fermenter.state
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key).get(id))
else:
# Stop controller
fermenter.controller_instance.stop()
fermenter.state = not fermenter.state
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key).get(id))

except Exception as e:

cbpi.notify("Toogle Fementer Controller failed", "Pleae check the %s configuration" % fermenter.name,
type="danger", timeout=None)
return ('', 500)

return ('', 204)

def get_fermenter(self, id):
return cbpi.cache["fermenter"].get(id)

def target_temp_reached(self,id, step):
timestamp = time.time()

days = step.days * 24 * 60 * 60
hours = step.hours * 60 * 60
minutes = step.minutes * 60
target_time = days + hours + minutes + timestamp

FermenterStep.update_timer(step.id, target_time)

step.timer_start = target_time

cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])

def check_step(self):
for key, value in cbpi.cache["fermenter_task"].iteritems():

try:
fermenter = self.get_fermenter(key)
current_temp = current_temp = cbpi.get_sensor_value(int(fermenter.sensor))

if value.timer_start is None:

if value.direction == "H" :

if current_temp >= value.temp:
self.target_temp_reached(key,value)
else:
if current_temp <= value.temp:
self.target_temp_reached(key, value)
else:
if time.time() >= value.timer_start:
self.start_fermentation(key)
else:
pass
except Exception as e:
pass


@cbpi.backgroundtask(key="read_target_temps_fermenter", interval=5)
def read_target_temps():
"""
background process that reads all passive sensors in interval of 1 second
:return: None
"""
result = {}
for key, value in cbpi.cache.get("fermenter").iteritems():
cbpi.save_to_file(key, value.target_temp, prefix="fermenter")


instance = FermenterView()

@cbpi.backgroundtask(key="fermentation_task", interval=1)
def execute_fermentation_step():
with cbpi.app.app_context():
instance.check_step()


def init_active_steps():
'''
active_steps = FermenterStep.query.filter_by(state='A')
for a in active_steps:
db.session.expunge(a)
cbpi.cache["fermenter_task"][a.fermenter_id] = a
'''

@cbpi.initalizer(order=1)
def init(cbpi):
print "INITIALIZE CONFIG MODULE"
FermenterView.register(cbpi.app, route_base='/api/fermenter')
FermenterView.init_cache()

+ 88
- 0
modules/kettle/__init__.py Переглянути файл

@@ -0,0 +1,88 @@
from flask import request
from flask_classy import FlaskView, route
from modules import cbpi, socketio
from modules.core.baseview import BaseView
from modules.core.db import DBModel

class Kettle(DBModel):
__fields__ = ["name","sensor", "heater", "automatic", "logic", "config", "agitator", "target_temp"]
__table_name__ = "kettle"
__json_fields__ = ["config"]


class Kettle2View(BaseView):
model = Kettle
cache_key = "kettle"

@classmethod
def pre_post_callback(self, data):
data["target_temp"] = 0

@classmethod
def post_init_callback(cls, obj):
obj.state = False


def post_post_callback(self, m):
m.state = False


@route('/<int:id>/targettemp/<temp>', methods=['POST'])
def postTargetTemp(self, id, temp):
id = int(id)
temp = float(temp)
cbpi.cache.get("kettle")[id].target_temp = float(temp)
self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
cbpi.emit("UPDATE_KETTLE_TARGET_TEMP", {"id": id, "target_temp": temp})
return ('', 204)

@route('/<int:id>/automatic', methods=['POST'])
def toggle(self, id):
kettle = cbpi.cache.get("kettle")[id]

if kettle.state is False:
# Start controller
if kettle.logic is not None:
cfg = kettle.config.copy()
cfg.update(dict(api=cbpi, kettle_id=kettle.id, heater=kettle.heater, sensor=kettle.sensor))
instance = cbpi.get_controller(kettle.logic).get("class")(**cfg)
instance.init()
kettle.controller_instance = instance
def run(instance):
instance.run()
t = self.api.socketio.start_background_task(target=run, instance=instance)
kettle.state = not kettle.state
cbpi.emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id))
else:
# Stop controller
kettle.controller_instance.stop()
kettle.state = not kettle.state
cbpi.emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id))
return ('', 204)

@cbpi.event("SET_TARGET_TEMP")
def set_target_temp(id, temp):
'''
Change Taget Temp Event
:param id: kettle id
: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():
"""
background process that reads all passive sensors in interval of 1 second
:return: None
"""
result = {}
for key, value in cbpi.cache.get("kettle").iteritems():
cbpi.save_to_file(key, value.target_temp, prefix="kettle")

@cbpi.initalizer()
def init(cbpi):
Kettle2View.api = cbpi
Kettle2View.register(cbpi.app,route_base='/api/kettle')
Kettle2View.init_cache()

+ 1
- 0
modules/logs/__init__.py Переглянути файл

@@ -0,0 +1 @@
import endpoints

+ 109
- 0
modules/logs/endpoints.py Переглянути файл

@@ -0,0 +1,109 @@
import datetime
import os
from flask import Blueprint, request, send_from_directory, json
from flask_classy import FlaskView, route
from modules import cbpi


class LogView(FlaskView):

@route('/', methods=['GET'])
def get_all_logfiles(self):
result = []
for filename in os.listdir("./logs"):
if filename.endswith(".log"):
result.append(filename)
return json.dumps(result)

@route('/actions')
def actions(self):
filename = "./logs/action.log"
if os.path.isfile(filename) == False:
return
import csv
array = []
with open(filename, 'rb') as f:
reader = csv.reader(f)
for row in reader:
try:
array.append([int((datetime.datetime.strptime(row[0], "%Y-%m-%d %H:%M:%S") - datetime.datetime(1970, 1, 1)).total_seconds()) * 1000, row[1]])
except:
pass
return json.dumps(array)

@route('/<file>', methods=["DELETE"])
def clearlog(self, file):
"""
Overload delete method to shutdown sensor before delete
:param id: sensor id
:return: HTTP 204
"""
if not self.check_filename(file):
return ('File Not Found', 404)

filename = "./logs/%s" % file
if os.path.isfile(filename) == True:
os.remove(filename)
cbpi.notify("log deleted succesfully", "")
else:
cbpi.notify("Failed to delete log", "", type="danger")
return ('', 204)

def read_log_as_json(self, type, id):
filename = "./logs/%s_%s.log" % (type, id)
if os.path.isfile(filename) == False:
return

import csv
array = []
with open(filename, 'rb') as f:
reader = csv.reader(f)
for row in reader:
try:
array.append([int((datetime.datetime.strptime(row[0], "%Y-%m-%d %H:%M:%S") - datetime.datetime(1970, 1, 1)).total_seconds()) * 1000, float(row[1])])
except:
pass
return array


@route('/<t>/<int:id>', methods=["POST"])
def get_logs_as_json(self, t, id):
data = request.json
result = []
if t == "s":
name = cbpi.cache.get("sensors").get(id).name
result.append({"name": name, "data": self.read_log_as_json("sensor", id)})

if t == "k":
kettle = cbpi.cache.get("kettle").get(id)
result.append({"name": "Temp", "data": self.read_log_as_json("sensor", kettle.sensor)})
result.append({"name": "Target Temp", "data": self.read_log_as_json("kettle", kettle.id)})

if t == "f":
fermenter = cbpi.cache.get("fermenter").get(id)
result.append({"name": "Temp", "data": self.read_log_as_json("sensor", fermenter.sensor)})
result.append({"name": "Target Temp", "data": self.read_log_as_json("fermenter", fermenter.id)})
return json.dumps(result)

@route('/download/<file>')
@cbpi.nocache
def download(self, file):
if not self.check_filename(file):
return ('File Not Found', 404)
return send_from_directory('../logs', file, as_attachment=True, attachment_filename=file)

def check_filename(self, name):
import re
pattern = re.compile('^([A-Za-z0-9-_])+.log$')

return True if pattern.match(name) else False

@cbpi.initalizer()
def init(app):
"""
Initializer for the message module
:param app: the flask app
:return: None
"""
print "INITIALIZE LOG MODULE"
LogView.register(cbpi.app, route_base='/api/logs')

+ 52
- 0
modules/notification/__init__.py Переглянути файл

@@ -0,0 +1,52 @@
import json
from flask_classy import FlaskView, route
from modules import cbpi
class NotificationView(FlaskView):

@route('/', methods=['GET'])
def getMessages(self):
"""
Get all messages
:return: current messages
"""

return json.dumps(cbpi.cache["messages"])

@route('/<id>', methods=['DELETE'])
def dismiss(self, id):
"""
Delete message from cache by id
:param id: message id to be deleted
:return: empty response HTTP 204
"""
for idx, m in enumerate(cbpi.cache.get("messages", [])):
if (m.get("id") == id):
cbpi.cache["messages"].pop(idx)
return ('', 204)

@cbpi.event("MESSAGE", async=True)
def messageEvent(message, **kwargs):
"""
React on message event. add the message to the cache and push the message to the clients
:param message: the message
: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,)

@cbpi.initalizer(order=2)
def init(cbpi):
"""
Initializer for the message module
: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)

NotificationView.register(cbpi.app, route_base='/api/notification')

+ 43
- 0
modules/sensors/__init__.py Переглянути файл

@@ -0,0 +1,43 @@
import time
from flask_classy import route
from modules import DBModel, cbpi
from modules.core.baseview import BaseView

class Sensor(DBModel):
__fields__ = ["name","type", "config", "hide"]
__table_name__ = "sensor"
__json_fields__ = ["config"]

class SensorView(BaseView):
model = Sensor
cache_key = "sensors"


def post_post_callback(self, m):
cbpi.init_sensor(m.id)

def post_put_callback(self, m):
cbpi.stop_sensor(m.id)
cbpi.init_sensor(m.id)

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():
"""
background process that reads all passive sensors in interval of 1 second
:return: None

"""
for key, value in cbpi.cache.get("sensors").iteritems():
if value.mode == "P":
value.instance.read()

+ 36
- 0
modules/stats/__init__.py Переглянути файл

@@ -0,0 +1,36 @@
from modules import cbpi


def getserial():
cpuserial = "0000000000000000"
try:
f = open('/proc/cpuinfo','r')
for line in f:
if line[0:6]=='Serial':
cpuserial = line[10:26]
f.close()
except:
cpuserial = "0000000000000000"
return cpuserial


@cbpi.initalizer(order=9999)
def sendStats(cbpi):
print "INITIALIZE STATS"
try:
serial = getserial()

info = {
"id": serial,
"version": "3.0",
"kettle": len(cbpi.cache.get("kettle")),
"hardware": len(cbpi.cache.get("actors")),
"thermometer": "CBP3.0",
"hardware_control": "CBP3.0"
}

import requests
r = requests.post('http://statistics.craftbeerpi.com', json=info)

except Exception as e:
pass

+ 230
- 0
modules/steps/__init__.py Переглянути файл

@@ -0,0 +1,230 @@
import time
from flask import json
from flask_classy import route

from modules import DBModel, cbpi, get_db
from modules.core.baseview import BaseView


class Step(DBModel):
__fields__ = ["name","type", "stepstate", "state", "start", "end", "order", "config"]
__table_name__ = "step"
__json_fields__ = ["config", "stepstate"]
__order_by__ = "order"
__as_array__ = True

@classmethod
def get_max_order(cls):
cur = get_db().cursor()
cur.execute("SELECT max(step.'order') as 'order' FROM %s" % cls.__table_name__)
r = cur.fetchone()
return r.get("order")

@classmethod
def get_by_state(cls, state):
cur = get_db().cursor()
cur.execute("SELECT * FROM %s WHERE state = ?" % cls.__table_name__, state)
r = cur.fetchone()
if r is not None:
return cls(r)
else:
return None

@classmethod
def delete_all(cls):
cur = get_db().cursor()
cur.execute("DELETE FROM %s" % cls.__table_name__)
get_db().commit()

@classmethod
def reset_all_steps(cls):
cur = get_db().cursor()
cur.execute("UPDATE %s SET state = 'I', stepstate = NULL , start = NULL, end = NULL " % cls.__table_name__)
get_db().commit()

@classmethod
def update_state(cls, id, state):
cur = get_db().cursor()
cur.execute("UPDATE %s SET state = ? WHERE id =?" % cls.__table_name__, (state, id))
get_db().commit()

@classmethod
def update_step_state(cls, id, state):
cur = get_db().cursor()
cur.execute("UPDATE %s SET stepstate = ? WHERE id =?" % cls.__table_name__, (json.dumps(state),id))
get_db().commit()

class StepView(BaseView):
model = Step
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('/', methods=["DELETE"])
def deleteAll(self):
self.model.delete_all()
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all())
return ('', 204)

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

def stop_step(self):
'''
stop active step
:return:
'''
step = cbpi.cache.get("active_step")
cbpi.cache["active_step"] = None

if step is not None:
step.finish()

@route('/reset/current', methods=['POST'])
def resetCurrentStep(self):
'''
Reset current step
:return:
'''
step = cbpi.cache.get("active_step")

if step is not None:
step.reset()
if step.is_dirty():

state = {}
for field in step.managed_fields:
state[field] = step.__getattribute__(field)
Step.update_step_state(step.id, state)
step.reset_dirty()
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all())
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:
# if type not found
return

# copy config to stepstate
# init step
cfg = step.config.copy()
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

@route('/next', methods=['POST'])
@route('/start', methods=['POST'])
def start(self):
active = Step.get_by_state("A")
inactive = Step.get_by_state('I')

if (active is not None):
active.state = 'D'
active.end = int(time.time())
self.stop_step()
Step.update(**active.__dict__)

if (inactive is not None):
self.init_step(inactive)
inactive.state = 'A'
inactive.stepstate = inactive.config
inactive.start = int(time.time())
Step.update(**inactive.__dict__)
else:
cbpi.log_action("Brewing Finished")
cbpi.notify("Brewing Finished", "You are done!", timeout=None)

cbpi.emit("UPDATE_ALL_STEPS", Step.get_all())
return ('', 204)

def get_manged_fields_as_array(type_cfg):

result = []
for f in type_cfg.get("properties"):

result.append(f.get("name"))

return result

@cbpi.try_catch(None)
def init_after_startup():
'''
Restart after startup. Check is a step is in state A and reinitialize
:return: None
'''

step = Step.get_by_state('A')
# We have an active step
if step is not None:

# get the type


type_cfg = cbpi.cache.get("step_types").get(step.type)

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)))
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():
with cbpi.app.app_context():
return Step.get_all()

with cbpi.app.app_context():
init_after_startup()
cbpi.add_cache_callback("steps", get_all)

@cbpi.backgroundtask(key="step_task", interval=0.1)
def execute_step():
'''
Background job which executes the step
:return:
'''
with cbpi.app.app_context():
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)
Step.update_step_state(step.id, state)
step.reset_dirty()
cbpi.emit("UPDATE_ALL_STEPS", Step.get_all())

if step.n is True:

StepView().start()
cbpi.emit("UPDATE_ALL_STEPS", Step.get_all())

+ 1
- 0
modules/system/__init__.py Переглянути файл

@@ -0,0 +1 @@
import endpoints

+ 107
- 0
modules/system/endpoints.py Переглянути файл

@@ -0,0 +1,107 @@
from flask import json
from flask_classy import FlaskView, route
from git import Repo, Git
from modules.app_config import cbpi
import pprint
import time
class SystemView(FlaskView):
def doShutdown(self):
time.sleep(5)
from subprocess import call
call("halt")
@route('/shutdown', methods=['POST'])
def shutdown(self):
"""
Shutdown hook
:return: HTTP 204
"""
self.doShutdown()
return ('', 204)
def doReboot(self):
time.sleep(5)
from subprocess import call
call("reboot")
@route('/reboot', methods=['POST'])
def reboot(self):
"""
Reboot hook
:return: HTTP 204
"""
self.doReboot()
return ('', 204)
@route('/tags/<name>', methods=['GET'])
def checkout_tag(self,name):
repo = Repo('./')
repo.git.reset('--hard')
o = repo.remotes.origin
o.fetch()
g = Git('./')
g.checkout(name)
cbpi.notify("Checkout successful", "Please restart the system")
return ('', 204)
@route('/git/status', methods=['GET'])
def git_status(self):
repo = Repo('./')
o = repo.remotes.origin
o.fetch()
# Tags
tags = []
for t in repo.tags:
tags.append({"name": t.name, "commit": str(t.commit), "date": t.commit.committed_date,
"committer": t.commit.committer.name, "message": t.commit.message})
try:
branch_name = repo.active_branch.name
# test1
except:
branch_name = None
changes = []
commits_behind = repo.iter_commits('master..origin/master')
for c in list(commits_behind):
changes.append({"committer": c.committer.name, "message": c.message})
return json.dumps({"tags": tags, "headcommit": str(repo.head.commit), "branchname": branch_name,
"master": {"changes": changes}})
@route('/check_update', methods=['GET'])
def check_update(self):
repo = Repo('./')
o = repo.remotes.origin
o.fetch()
changes = []
commits_behind = repo.iter_commits('master..origin/master')
for c in list(commits_behind):
changes.append({"committer": c.committer.name, "message": c.message})
return json.dumps(changes)
@route('/git/pull', methods=['POST'])
def update(self):
repo = Repo('./')
o = repo.remotes.origin
info = o.pull()
cbpi.notify("Pull successful", "The lasted updated was downloaded. Please restart the system")
return ('', 204)
@route('/dump', methods=['GET'])
def dump(self):
return json.dumps(cbpi.cache)
@cbpi.initalizer()
def init(cbpi):
print "INITIALIZE SYSTEM MODULE"
SystemView.register(cbpi.app, route_base='/api/system')

+ 1
- 0
modules/ui/__init__.py Переглянути файл

@@ -0,0 +1 @@
import endpoints

+ 23
- 0
modules/ui/endpoints.py Переглянути файл

@@ -0,0 +1,23 @@
from flask import Blueprint, render_template, request
from modules import cbpi

react = Blueprint('react', __name__, template_folder='templates', static_folder='static')

@cbpi.initalizer(order=10)
def init(cbpi):
print "INITIALIZE UI"
cbpi.app.register_blueprint(react, url_prefix='/ui')


@react.route('/')
def index():
return react.send_static_file("index.html")










+ 11
- 0
modules/ui/static/bootstrap.dark.css
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 6
- 0
modules/ui/static/bootstrap.min.css
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 91
- 0
modules/ui/static/bundle.js
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 2337
- 0
modules/ui/static/css/font-awesome.css
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 4
- 0
modules/ui/static/css/font-awesome.min.css
Різницю між файлами не показано, бо вона завелика
Переглянути файл


BIN
modules/ui/static/favicon.ico Переглянути файл

Перед Після

BIN
modules/ui/static/fonts/FontAwesome.otf Переглянути файл


BIN
modules/ui/static/fonts/fontawesome-webfont.eot Переглянути файл


+ 2671
- 0
modules/ui/static/fonts/fontawesome-webfont.svg
Різницю між файлами не показано, бо вона завелика
Переглянути файл


BIN
modules/ui/static/fonts/fontawesome-webfont.ttf Переглянути файл


BIN
modules/ui/static/fonts/fontawesome-webfont.woff Переглянути файл


BIN
modules/ui/static/fonts/fontawesome-webfont.woff2 Переглянути файл


+ 70
- 0
modules/ui/static/index.html Переглянути файл

@@ -0,0 +1,70 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">

<link rel="stylesheet" href="static/bootstrap.min.css">
<link rel="stylesheet" href="static/css/font-awesome.min.css">
<link rel="stylesheet" href="static/bootstrap.dark.css">


<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;
padding-right: 8px;
padding-left: 8px;
}

.panel-heading {
padding: 5px 5px;

}

.panel-footer {
padding: 2px 2px;

}

.modal-footer {
padding: 5px;


}
.row {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex !important;
flex-wrap: wrap;
}

.row > [class*='col-'] {
display: flex;
flex-direction: column;
}



</style>
<title>CraftBeerPi 3.0</title>
</head>
<body>
<div id="root"></div>
<script src="static/bundle.js" type="text/javascript"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>

BIN
modules/ui/static/logo.png Переглянути файл

Перед Після
Ширина: 200  |  Висота: 208  |  Розмір: 22KB

+ 15
- 0
requirements.txt Переглянути файл

@@ -0,0 +1,15 @@
Flask==0.11.1
Flask-SocketIO==2.6.2
eventlet==0.19.0
greenlet==0.4.10
python-dateutil==2.5.3
python-engineio==0.9.2
python-mimeparse==1.5.2
python-socketio==1.4.4
PyYAML==3.11
requests==2.11.0
Werkzeug==0.11.10
httplib2==0.9.2
flask-classy==0.6.10
GitPython==2.1.3

+ 6
- 0
run.py Переглянути файл

@@ -0,0 +1,6 @@
#!/usr/bin/env python

from modules import socketio, app

socketio.run(app, host='0.0.0.0')


+ 0
- 0
upload/empty Переглянути файл


Завантаження…
Відмінити
Зберегти