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