| @@ -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 | |||
| @@ -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. | |||
| @@ -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 | |||
| @@ -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 | |||
| ); | |||
| @@ -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 +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() | |||
| @@ -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() | |||
| @@ -0,0 +1 @@ | |||
| import endpoints | |||
| @@ -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') | |||
| @@ -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 +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) | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -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" | |||
| @@ -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) | |||
| @@ -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') | |||
| @@ -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") | |||
| @@ -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 +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 | |||
| @@ -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)) | |||
| @@ -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")) | |||
| @@ -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) | |||
| @@ -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" | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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() | |||
| @@ -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() | |||
| @@ -0,0 +1 @@ | |||
| import endpoints | |||
| @@ -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') | |||
| @@ -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') | |||
| @@ -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() | |||
| @@ -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 | |||
| @@ -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()) | |||
| @@ -0,0 +1 @@ | |||
| import endpoints | |||
| @@ -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') | |||
| @@ -0,0 +1 @@ | |||
| import endpoints | |||
| @@ -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") | |||
| @@ -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> | |||
| @@ -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 | |||
| @@ -0,0 +1,6 @@ | |||
| #!/usr/bin/env python | |||
| from modules import socketio, app | |||
| socketio.run(app, host='0.0.0.0') | |||