113 Commits
Author SHA1 Message Date
Manuel83 9b7908c9c5 - Refactoring
- Action Button with Parameter
- Actor Action with Parameter
- Sensor Action with Parameter
2017-11-29 22:37:18 +01:00
Manuel83 13836a5680 - Recipe Book
- BugFixes
- Refactoring
2017-11-22 00:10:11 +01:00
Manuel83 6253cb9976 fix 2017-10-13 23:27:58 +02:00
Manuel83 616a9c7933 fix 2017-10-13 23:20:26 +02:00
Manuel83 1dc4f5ffd5 Update git status 2017-10-13 23:16:12 +02:00
Manuel83 ad21625f9b update 2017-10-13 22:36:18 +02:00
Manuel83 f07a2bdd7a Further git test code 2017-10-13 21:54:20 +02:00
Manuel83 a265d3a8cc Clean up print statements 2017-10-13 21:50:07 +02:00
Manuel83andGitHub 7779d00db3 Merge pull request #132 from carlallen/restore_port_for_31
Restore port config for 3.1
2017-09-28 17:34:50 +02:00
Manuel83 d9e79d0208 Change to Pritt print Cache 2017-09-28 07:40:06 +02:00
Carl Allen 707a576e16 Restore port config for 3.1 2017-09-27 15:15:19 -05:00
Manuel83 547804be2b -Custom Web View Examples Added
-UI Adjustment for small screens. Brew Steps are displayed above the
kettles.
-
2017-09-27 20:13:36 +02:00
Manuel83 813b2f0035 - Login Added. Default Password is „beer“
- Several UI Adjustments - still work in progress
- New API Method to add custom JavaScript to CraftBeerPi
- Global Action Buttons added
- Register new Web Modules for complete custom UI
2017-09-24 15:10:45 +02:00
Manuel83 42ed9f02af First Refactoring
- Major API Changes in Core Module
- Will be changed further. Still work in progress
2017-09-21 00:03:33 +02:00
Manuel83andGitHub bf3fbddbe2 Merge pull request #119 from carlallen/text_property_default_value_fix
Set default value for TextProperty to fix bug
2017-09-05 08:03:24 +02:00
Carl Allen 590ef4ca92 set default_value for Text Property 2017-09-04 21:16:23 -05:00
Manuel83andGitHub d3bc54001f Merge pull request #101 from carlallen/default_value_for_numbers
Default value for Property.Number
2017-09-04 16:34:53 +02:00
Manuel83andGitHub 25dead2e4d Merge pull request #115 from cow77/patch-3
Update __init__.py
2017-09-04 16:34:25 +02:00
Manuel83 f771289224 Loading all plugins before calling initializer methods. Now also
initializer which are defined in the plugin will be called. Before
these initializer where ignored.
2017-08-31 18:42:02 +02:00
Co de WaalandGitHub bf02c08677 Update __init__.py 2017-08-29 11:11:20 +02:00
Manuel83 c765589984 Chart boost added 2017-08-15 22:10:59 +02:00
Carl AllenandGitHub f62f9ba7ce Add default to text property for @IndyJoeA 2017-07-28 09:32:15 -05:00
Carl Allen 909a6aa0ea Add default values for Property.Number 2017-07-26 18:15:31 -05:00
Manuel83 40b7253a6d - Add On - Link to Plugin Website
- CSS Fix for Safari
2017-07-24 21:27:42 +02:00
Manuel83andGitHub 2fe3d1d68e Merge pull request #94 from carlallen/modify_charts_in_plugins
Allow modifying chart data in plugins
2017-07-22 09:13:10 +02:00
Manuel83andGitHub 1f1f84018d Merge pull request #98 from swimIan/patch2
Add notification(s) at the end of mashstep and boilstep
2017-07-22 09:11:33 +02:00
swimIan f68e0f46ee Merge branch 'patch2' of https://github.com/swimIan/craftbeerpi3 into patch2 2017-07-21 15:49:33 -07:00
swimIan b75926d23e Removes boiltemp notifications for earlier commit 2017-07-21 15:49:11 -07:00
swimIan aa3420efc8 Merge branch 'patch2' of https://github.com/swimIan/craftbeerpi3 into patch2 2017-07-21 14:58:52 -07:00
swimIan 124a03bd0f Add notification(s) at the end of a mashstep, boilstep, and upon reaching boil temperature. 2017-07-21 14:57:49 -07:00
swimIan 1c4e16e358 Add notification at the end of a mashstep. 2017-07-20 16:44:57 -07:00
Manuel83andGitHub 805b3fae1b Merge pull request #90 from swimIan/master
Add 2 more beeps to the Buzzer
2017-07-19 06:31:08 +02:00
Manuel83andGitHub f8d4015b6e Merge pull request #89 from carlallen/port_param
Allow setting port by param
2017-07-19 06:29:54 +02:00
Carl Allen 01997f741c Allow modifying chart data in plugins 2017-07-18 15:06:55 -05:00
swimIan 1184107e50 Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-16 16:59:15 -07:00
swimIan ea793996ab Merge branch 'master' into patch-1 2017-07-16 16:58:51 -07:00
swimIanandGitHub bbcf0a40bf Merge pull request #2 from swimIan/patch-1
Patch 1
2017-07-16 16:29:23 -07:00
swimIanandGitHub d6d023798a Merge branch 'master' into patch-1 2017-07-16 16:29:11 -07:00
swimIanandGitHub 518a139b5d Update __init__.py
Added a wait time between the beeps.
2017-07-16 16:12:46 -07:00
Carl Allen a97c5f4585 Allow setting port by param 2017-07-16 14:31:52 -05:00
swimIanandGitHub 9c44d3ff62 Merge pull request #1 from Manuel83/master
Update
2017-07-15 23:33:22 -07:00
swimIan 544e9bb72a Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 23:17:51 -07:00
swimIan eee5680ba9 Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 23:17:47 -07:00
swimIanandGitHub 42e91de9eb Add two more beeps to the buzzer sound
Makes the buzzer notifications more noticeable.
2017-07-15 23:04:44 -07:00
swimIan 3c541e0946 Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 22:54:04 -07:00
swimIan 4d08b17abd Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 22:53:59 -07:00
swimIan be8506d95b Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 22:51:29 -07:00
swimIan 98fbcaf910 Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 22:50:46 -07:00
swimIan b40e82a6bb Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 22:49:50 -07:00
swimIan a368213cd1 Update beerxml.py 2017-07-15 22:45:51 -07:00
swimIanandGitHub 58fd9a659e Update beerxml.py 2017-07-15 22:43:10 -07:00
swimIanandGitHub e750fbb70e Update beerxml.py 2017-07-15 22:38:42 -07:00
swimIan ed9bbed084 Testing 2017-07-15 22:26:08 -07:00
swimIan 15d7906850 Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 22:23:40 -07:00
swimIan b9b3fee8c9 Remove spaces 2017-07-15 22:21:27 -07:00
swimIan ad02e2490d Remove spaces 2017-07-15 22:08:52 -07:00
swimIan eb715fabb6 Merge branch 'master' of https://github.com/swimIan/craftbeerpi3 2017-07-15 22:05:01 -07:00
swimIan 5d6e32a57f test 2017-07-15 22:04:22 -07:00
swimIan 85f0c6cbff test 2017-07-15 22:04:10 -07:00
swimIanandGitHub f60658c84e Update beerxml.py 2017-07-15 21:54:46 -07:00
swimIanandGitHub b4ddc60c07 Update beerxml.py 2017-07-15 21:52:34 -07:00
swimIanandGitHub 6fd39a81a9 Update beerxml.py 2017-07-15 21:50:28 -07:00
swimIan bb6f6f2b36 Add mashin step including the infusion temp from a beerxml import 2017-07-15 11:34:17 -07:00
Manuel83andGitHub ca9575ac29 Merge pull request #70 from MikonosII/patch-3
fixed typos
2017-07-15 13:13:16 +02:00
Manuel83andGitHub 9a1981fa46 Merge pull request #81 from swimIan/master
Update all brew steps to allow decimal values
2017-07-15 13:12:59 +02:00
Manuel83andGitHub 46fc39c185 Merge pull request #80 from IndyJoeA/master
Use Integer for Buzzer GPIO
2017-07-15 13:12:44 +02:00
swimIanandGitHub 54dd31ea11 Update all brew steps to allow decimal values
Changes int() on mashin and boilsteps to a float() value to check if the Target Temp is reached.  This allows decimals value to not error out in the log.
2017-07-14 09:02:26 -07:00
IndyJoeAandGitHub 1d586b353e Make buzzer GPIO an Int
Fixes buzzer initialization errors
2017-07-13 20:37:16 -04:00
IndyJoeAandGitHub fc04ae01a7 Merge pull request #1 from Manuel83/master
Merge
2017-07-13 12:09:47 -04:00
Manuel83 929db1df81 Actor Action added 2017-07-12 15:18:14 +02:00
Manuel83andGitHub fd5ac5ce77 Merge pull request #77 from carlallen/patch-2
Improved descriptions for fermenter hysteresis
2017-07-11 22:11:25 +02:00
Carl AllenandGitHub 91c0dc9a59 fix geather to greater 2017-07-11 15:09:57 -05:00
Carl AllenandGitHub 8c0f03c139 Update __init__.py 2017-07-11 15:00:35 -05:00
Carl AllenandGitHub 2398ce360d Improved descriptions for fermenter hysteresis 2017-07-11 14:58:55 -05:00
Manuel83andGitHub 785e5c984c Merge pull request #71 from carlallen/patch-1
Add back in sensor_name and improve description for offset
2017-07-10 21:57:36 +02:00
Carl AllenandGitHub a978f43649 Add back in sensor_name and improve description for offset 2017-07-09 17:13:30 -05:00
Manuel83 da6b107e94 tool tip for config properties 2017-07-09 22:04:19 +02:00
Manuel83 47268d8d1c Allow to use actor, sensor and kettle dropdown for actor and sensor
config.
2017-07-08 17:16:27 +02:00
MikonosIIandGitHub 8916cd6054 fixed typos 2017-07-07 20:30:53 -07:00
Manuel83andGitHub ca7270b5ee Merge pull request #67 from carlallen/buzzer_gpio_options
allow buzzer on all gpio pins
2017-07-07 08:47:37 +02:00
Manuel83andGitHub 2c941e01d5 Merge pull request #66 from MikonosII/patch-2
Update __init__.py
2017-07-07 08:46:14 +02:00
Carl Allen 7591d47add allow buzzer on all gpio pins 2017-07-06 21:04:36 -05:00
MikonosIIandGitHub 20b535b293 Update __init__.py
swapped .PropertyNumber("Cooler Offset ON", True, 0) on lines 11 and 12.  On should match with min and Off should match with max on lines 32 and 35
2017-07-06 16:49:44 -07:00
Manuel83 3af76d0a1e Action Button for Sensors added 2017-07-05 21:16:40 +02:00
Manuel83andGitHub 62f31e2dd4 Merge pull request #55 from jalim/hotfix/buzzer_fix
Move buzzer init order to fall after config init.
2017-07-05 20:34:29 +02:00
Manuel83andGitHub 88a9989c13 Merge pull request #62 from carlallen/patch-6
Select properties configurable for steps
2017-07-05 20:33:32 +02:00
Manuel83 ba0e932239 Beer.xml convert to Fahrenheit 2017-07-05 20:31:56 +02:00
Carl AllenandGitHub 734f0c6693 Select properties configurable for steps 2017-07-05 11:30:02 -05:00
Manuel83 45c937825b -fix beer xml
-fix fermenter hysteresis
2017-07-05 00:34:35 +02:00
Manuel83 291f09dcd1 - Import KBH
- Import BeerXML
- Import REST API
- Migration DB
- Set Default Screen after Start
- Cleanup
2017-07-02 23:52:18 +02:00
Luke Mullan e7de4d8965 Move buzzer init order to fall after config init.
Buzzer can't initialise with correct gpio pin as the config hasn't been initialised yet.
2017-07-02 22:12:43 +10:00
Manuel83andGitHub c2aa5d8141 Update README.md 2017-06-29 15:13:57 +02:00
Manuel83andGitHub dc046e92c4 Merge pull request #50 from carlallen/patch-5
Allow blank value for offset on onewire sensor
2017-06-28 21:59:08 +02:00
Carl AllenandGitHub 4470767a1c Allow blank value for offset on onewire sensor
Fixes bug caused introduced when adding the offset (temps only reading 0)
2017-06-28 09:46:28 -05:00
Manuel83andGitHub e67baeabb0 Merge pull request #47 from jalim/hotfix/message-event-bus-patch
Change to notification to use message event bus
2017-06-28 09:32:44 +02:00
Luke Mullan 7d35538962 Fix **kwargs ommision. 2017-06-28 17:31:52 +10:00
Manuel83andManuel83 10a989cee9 Fix: Sensors are now stopping after update of config 2017-06-28 09:01:13 +02:00
Manuel83andGitHub 3396e1b379 Merge pull request #38 from carlallen/patch-3
Allow specifying an offset on onewire sensor
2017-06-28 08:42:07 +02:00
Manuel83andGitHub 0749c55478 Merge pull request #39 from IndyJoeA/patch-1
Replace int with float in get_sensor_value()
2017-06-28 08:41:43 +02:00
Manuel83andGitHub 3b60c8cfd1 Merge pull request #44 from carlallen/patch-4
Allow SQL file to be re-run
2017-06-28 08:41:05 +02:00
Manuel83andGitHub ef0f719c58 Merge pull request #41 from MikonosII/patch-1
Update controller.py
2017-06-28 08:38:02 +02:00
Manuel83andGitHub 1ec6b9acf1 Merge pull request #36 from carlallen/patch-2
Allow floats for offeset min/max
2017-06-28 08:37:39 +02:00
Luke Mullan fc945f093c Change to notification to use message even bus
This will allow any plugin to listen for a notification and respond accordingly.
2017-06-27 14:52:01 +10:00
Carl AllenandGitHub ebeb726dd9 Allow SQL file to be re-run
Allow the schema.sql file to be re-run if it changes
2017-06-25 18:36:58 -05:00
Manuel83 00739ea38f -Ui Changes
-Brew Steps added
-Clean AddOn Dialog
2017-06-25 21:29:45 +02:00
MikonosIIandGitHub 71449740fa Update controller.py
lines 138 and 144 were changed to f.cooler
2017-06-24 19:12:25 -07:00
IndyJoeAandGitHub f1ec635f25 Replace int with float in get_sensor_value()
I've noticed that sensor values are being handled as integers by this function, which was causing poor performance of the PIDArduino logic and another logic plugin I am testing.  I have changed the type to float, and now logic controllers can receive more accurate temperature values.
2017-06-23 22:47:05 -04:00
Carl AllenandGitHub 139f5e8f91 Allow specifying an offset on onewire sensor 2017-06-22 23:20:32 -05:00
Carl AllenandGitHub 7594aecaee Allow floats for offeset min/max 2017-06-21 16:59:20 -05:00
Manuel83andGitHub f0b7c6e640 Merge pull request #24 from alexpalmer/patch-1
Update __init__.py
2017-06-18 21:22:34 +02:00
Manuel83 26da7c66bc Add actor_power to Controller 2017-06-18 20:08:17 +02:00
Manuel83 167a0bb8f8 KettleController
- Method actor_power added to set the power without switching
- Method timer_remaining added to get the remaining time of a timer
2017-06-18 19:09:08 +02:00
alexpalmerandGitHub c77fff6ffa Update __init__.py
Correct datatypes for def F automode to work
2017-06-17 22:07:35 -04:00
90 changed files with 12468 additions and 2909 deletions
+9
View File
@@ -8,3 +8,12 @@ craftbeerpi.db
modules/plugins/* modules/plugins/*
!modules/plugins/__init__.py !modules/plugins/__init__.py
*.pyc *.pyc
*.js
*.css
modules/ui/package.json
modules/ui/.babelrc
yarn.lock
modules/ui/package-lock.json
+6
View File
@@ -18,6 +18,12 @@ Type <code>cd craftbeerpi3</code> to navigate into the craftbeerpi folder.
Type <code>sudo ./install.sh</code> Type <code>sudo ./install.sh</code>
## Hardware Wiring
Here you will find a guide how to wire everything up.
http://web.craftbeerpi.com/hardware/
## ATTENTION ## ATTENTION
CraftBeerPi 3.0 is a complete rewrite. Server as well as user interface. I recommend to use a second SD card for testing. CraftBeerPi 3.0 is a complete rewrite. Server as well as user interface. I recommend to use a second SD card for testing.
+18 -15
View File
@@ -1,4 +1,4 @@
CREATE TABLE kettle CREATE TABLE IF NOT EXISTS kettle
( (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80), name VARCHAR(80),
@@ -13,7 +13,7 @@ CREATE TABLE kettle
diameter INTEGER diameter INTEGER
); );
CREATE TABLE step CREATE TABLE IF NOT EXISTS step
( (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
"order" INTEGER, "order" INTEGER,
@@ -27,7 +27,7 @@ CREATE TABLE step
kettleid INTEGER kettleid INTEGER
); );
CREATE TABLE sensor CREATE TABLE IF NOT EXISTS sensor
( (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
type VARCHAR(100), type VARCHAR(100),
@@ -36,7 +36,7 @@ CREATE TABLE sensor
hide BOOLEAN hide BOOLEAN
); );
CREATE TABLE fermenter_step CREATE TABLE IF NOT EXISTS fermenter_step
( (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80), name VARCHAR(80),
@@ -54,7 +54,7 @@ CREATE TABLE fermenter_step
FOREIGN KEY (fermenter_id) REFERENCES fermenter (id) FOREIGN KEY (fermenter_id) REFERENCES fermenter (id)
); );
CREATE TABLE fermenter CREATE TABLE IF NOT EXISTS fermenter
( (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80), name VARCHAR(80),
@@ -69,7 +69,7 @@ CREATE TABLE fermenter
target_temp INTEGER target_temp INTEGER
); );
CREATE TABLE config CREATE TABLE IF NOT EXISTS config
( (
name VARCHAR(50) PRIMARY KEY NOT NULL, name VARCHAR(50) PRIMARY KEY NOT NULL,
value VARCHAR(255), value VARCHAR(255),
@@ -79,19 +79,22 @@ CREATE TABLE config
); );
INSERT INTO config VALUES ('kettle_cols', 4, 'select', 'Adjust the width of a kettle widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]'); INSERT OR IGNORE INTO config VALUES ('kettle_cols', 4, 'select', 'Adjust the width of a kettle widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
INSERT INTO config VALUES ('actor_cols', 4, 'select', 'Adjust the width of a actor widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]'); INSERT OR IGNORE INTO config VALUES ('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 OR IGNORE INTO config VALUES ('sensor_cols', 4, 'select', 'Adjust the width of a sensor widget on the brewing dashboard', '[1,2,3, 4, 5, 6, 7, 8, 9, 10, 11, 12]');
INSERT INTO config VALUES ('unit', 'C', 'select', 'Temperature Unit', '["C","F"]'); INSERT OR IGNORE 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 OR IGNORE 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 OR IGNORE INTO config VALUES ('buzzer', 16, 'select', 'Buzzer GPIO', '[16,17,18,19,20]');
INSERT INTO config VALUES ('setup', 'YES', 'select', 'Show the Setup dialog', '["YES","NO"]'); INSERT OR IGNORE INTO config VALUES ('setup', 'YES', 'select', 'Show the Setup dialog', '["YES","NO"]');
INSERT OR IGNORE INTO config VALUES ('brew_name', '', 'text', 'Brew Name', NULL);
INSERT OR IGNORE INTO config VALUES ('donation_notification', 'YES', 'select', 'Disable Donation Notification', '["YES","NO"]');
CREATE TABLE actor
CREATE TABLE IF NOT EXISTS actor
( (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(50), name VARCHAR(50),
type VARCHAR(100), type VARCHAR(100),
config VARCHAR(500), config VARCHAR(500),
hide BOOLEAN hide BOOLEAN
); );
+1
View File
@@ -0,0 +1 @@
3.0.2
+27
View File
@@ -0,0 +1,27 @@
import requests
from git import Repo, Git
repo = Repo('./')
branch = repo.active_branch
print branch.name
for remote in repo.remotes:
remote.fetch()
url = 'https://api.github.com/repos/manuel83/craftbeerpi3/releases'
response = requests.get(url)
result = {"branches":[], "releases": []}
result["branches"].append({"name": "master"})
for branch in repo.branches:
result["branches"].append({"name": branch.name})
for r in response.json():
result["releases"].append({"name": "tags/%s" % r.get("tag_name")})
print result
+1 -1
View File
@@ -136,7 +136,7 @@ show_menu () {
fi fi
;; ;;
10) 10)
confirmAnswer "Are you sure you want to reboo the Raspberry Pi?" confirmAnswer "Are you sure you want to reboot the Raspberry Pi?"
if [ $? = 0 ]; then if [ $? = 0 ]; then
sudo reboot sudo reboot
else else
+4 -65
View File
@@ -1,65 +1,4 @@
import json from modules.core.core import CraftBeerPI, Addon
import pprint
import sys, os cbpi = CraftBeerPI()
from flask import Flask, render_template, redirect cbpi.addon = Addon(cbpi)
from flask_socketio import SocketIO, emit
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()
+34
View File
@@ -0,0 +1,34 @@
import json
from flask import request
from flask_classy import FlaskView, route
from modules import cbpi
class ActionView(FlaskView):
@route('/<action>', methods=['POST'])
def action(self, action):
"""
Call global action button
---
tags:
- action
responses:
200:
description: action invoked
"""
data = request.json
obj = self.cbpi.cache["actions"][action]["class"](self.cbpi)
obj.execute(**data)
return ('',204)
@cbpi.addon.core.initializer()
def init(cbpi):
"""
Initializer for the message module
:param app: the flask app
:return: None
"""
ActionView.cbpi = cbpi
ActionView.register(cbpi.web, route_base='/api/action')
Regular → Executable
+198 -69
View File
@@ -1,69 +1,198 @@
import time import time
from flask_classy import route
from modules import DBModel, cbpi from flask import request
from modules.core.baseview import BaseView from flask_classy import route
from flask_login import login_required
class Actor(DBModel):
__fields__ = ["name","type", "config", "hide"] from modules.core.db import DBModel
__table_name__ = "actor" from modules import cbpi
__json_fields__ = ["config"] from modules.core.baseview import RestApi
from modules.database.dbmodel import Actor
class ActorView(BaseView):
model = Actor
cache_key = "actors" class ActorView(RestApi):
model = Actor
@classmethod cache_key = "actors"
def post_init_callback(self, obj):
obj.state = 0 @classmethod
obj.power = 100 def post_init_callback(self, obj):
def post_post_callback(self, m): obj.state = 0
self.api.init_actor(m.id) obj.power = 100
def post_put_callback(self, m): def _post_post_callback(self, m):
self.api.actor.init_one(m.id)
self.api.init_actor(m.id)
def _post_put_callback(self, m):
@route("<int:id>/switch/on", methods=["POST"]) self.api.actor.init_one(m.id)
def on(self, id):
self.api.switch_actor_on(id) @login_required
return ('', 204) @route("<int:id>/switch/on", methods=["POST"])
def on(self, id):
@route("<int:id>/switch/off", methods=["POST"]) """
def off(self, id): Switch actor on
self.api.switch_actor_off(id) ---
return ('', 204) tags:
- actor
@route("<int:id>/power/<int:power>", methods=["POST"]) parameters:
def power(self, id, power): - in: path
self.api.actor_power(id, power) name: id
return ('', 204) schema:
type: integer
@route("<int:id>/toggle", methods=["POST"]) required: true
def toggle(self, id): description: Numeric ID of the actor
responses:
if self.api.cache.get("actors").get(id).state == 0: 200:
self.on(id) description: Actor switched on
else: """
self.off(id) self.api.actor.on(id)
return ('', 204) return ('', 204)
def toggleTimeJob(self, id, t): @login_required
self.api.cache.get("actors").get(int(id)).timer = int(time.time()) + int(t) @route("<int:id>/switch/off", methods=["POST"])
self.toggle(int(id)) def off(self, id):
self.api.socketio.sleep(t) """
self.api.cache.get("actors").get(int(id)).timer = None Switch actor off
self.toggle(int(id)) ---
tags:
@route("/<id>/toggle/<int:t>", methods=["POST"]) - actor
def toggleTime(self, id, t): parameters:
t = self.api.socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t) - in: path
return ('', 204) name: id
schema:
@cbpi.initalizer(order=1000) type: integer
def init(cbpi): required: true
print "INITIALIZE ACTOR MODULE" description: Numeric ID of the actor
cbpi.app.logger.info("INITIALIZE ACTOR MODULE") responses:
ActorView.register(cbpi.app, route_base='/api/actor') 200:
ActorView.init_cache() description: Actor switched off
cbpi.init_actors() """
self.api.actor.off(id)
return ('', 204)
@login_required
@route("<int:id>/power/<int:power>", methods=["POST"])
def power(self, id, power):
"""
Set Actor Power
---
tags:
- actor
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of the actor
- in: path
name: power
schema:
type: integer
required: true
description: Power value between 0 - 100
responses:
200:
description: Actor power set
"""
self.api.actor.power(id, power)
return ('', 204)
@login_required
@route("<int:id>/toggle", methods=["POST"])
def toggle(self, id):
"""
Toggle Actor
---
tags:
- actor
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of the actor
responses:
200:
description: Actor toggled
"""
cbpi.actor.toggle(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.sleep(t)
self.api.cache.get("actors").get(int(id)).timer = None
self.toggle(int(id))
@login_required
@route("/<id>/toggle/<int:t>", methods=["POST"])
def toggleTime(self, id, t):
"""
Toggle Actor for a defined time
---
tags:
- actor
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of the actor
- in: path
name: time
schema:
type: integer
required: true
description: time in seconds
responses:
200:
description: Actor toggled
"""
self.api.actor.toggle_timeout(id, t)
#t = self.api._socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t)
return ('', 204)
@login_required
@route('<int:id>/action/<method>', methods=["POST"])
def action(self, id, method):
"""
Actor Action
---
tags:
- actor
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of the actor
- in: path
name: method
schema:
type: string
required: true
description: action method name
responses:
200:
description: Actor Action called
"""
data = request.json
if data:
cbpi.actor.action(id, method, **data)
else:
cbpi.actor.action(id, method)
return ('', 204)
@cbpi.addon.core.initializer(order=1000)
def init(cbpi):
ActorView.register(cbpi.web, route_base='/api/actor')
ActorView.init_cache()
#cbpi.init_actors()
-1
View File
@@ -1 +0,0 @@
import endpoints
-198
View File
@@ -1,198 +0,0 @@
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/Manuel83/craftbeerpi-plugins/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')
-53
View File
@@ -1,53 +0,0 @@
import json
import sys, os
from flask import Flask, render_template, redirect, json, g
from flask_socketio import SocketIO, emit
import logging
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['SECRET_KEY'] = 'craftbeerpi'
app.config['UPLOAD_FOLDER'] = './upload'
@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("##########################################")
+76
View File
@@ -0,0 +1,76 @@
from modules.core.baseapi import Buzzer
from modules.core.basetypes import Actor, KettleController, FermenterController
from modules import cbpi
from modules.core.proptypes import Property
@cbpi.addon.actor.type("Dummy Actor")
class Dummy(Actor):
# Decorator to create a parameter based action
@cbpi.addon.actor.action("Run until Temp reached", parameters={"t": Property.Text(label="Target Temp")})
def check_sensor_value(self, t=1):
def check(api, id, value):
'''
Background Prozess which checks the sensor value every second
:param api:
:param id:
:param value:
:return:
'''
while api.sensor.get_value(1) < value:
api.sleep(1)
api.actor.off(id)
target_value = int(t)
# Create notificaiton
self.api.notify(headline="Waiting", message="Waiting for temp %s" % target_value)
# Switch actor on
self.api.actor.on(self.id, 100)
# Start Background task
self.api.start_background_task(check, self.api, id=self.id, value=target_value)
def on(self, power=100):
'''
Code to switch on the actor
:param power: int value between 0 - 100
:return:
'''
print "ID %s ON" % self.id
def off(self):
print "ID %s OFF" % self.id
@cbpi.addon.kettle.controller()
class MyController(KettleController):
def run(self):
while self.is_running():
print "HALLO"
self.sleep(1)
@cbpi.addon.fermenter.controller()
class MyController2(FermenterController):
def run(self):
while self.is_running():
print "HALLO"
self.get_target_temp()
self.sleep(1)
@cbpi.addon.core.initializer(order=200)
def init(cbpi):
class MyBuzzer(Buzzer):
def beep(self):
print "BEEEEEP"
pass
cbpi.buzzer = MyBuzzer()
@@ -1,95 +0,0 @@
# -*- 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)
@@ -1,36 +0,0 @@
# -*- 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)
@@ -1,38 +0,0 @@
from modules import cbpi
from modules.core.controller import KettleController, FermenterController
from modules.core.props import Property
@cbpi.fermentation_controller
class Hysteresis(FermenterController):
heater_offset_min = Property.Number("Heater Offset min", True, 0)
heater_offset_max = Property.Number("Heater Offset max", True, 0)
cooler_offset_min = Property.Number("Cooler Offset min", True, 0)
cooler_offset_max = Property.Number("Cooler Offset max", True, 0)
def stop(self):
super(FermenterController, self).stop()
self.heater_off()
self.cooler_off()
def run(self):
while self.is_running():
target_temp = self.get_target_temp()
temp = self.get_temp()
if temp + int(self.heater_offset_min) < target_temp:
self.heater_on(100)
if temp + int(self.heater_offset_max) > target_temp:
self.heater_off()
if temp > target_temp + int(self.cooler_offset_min):
self.cooler_on(100)
if temp < target_temp + int(self.cooler_offset_max):
self.cooler_off()
self.sleep(1)
@@ -1,93 +0,0 @@
# -*- 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,27])
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,27])
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,27])
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"
@@ -1,39 +0,0 @@
from modules import cbpi
from modules.core.controller import KettleController
from modules.core.props import Property
@cbpi.controller
class Hysteresis(KettleController):
# Custom Properties
on = Property.Number("Offset On", True, 0)
off = Property.Number("Offset Off", True, 0)
def stop(self):
'''
Invoked when the automatic is stopped.
Normally you switch off the actors and clean up everything
:return: None
'''
super(KettleController, self).stop()
self.heater_off()
def run(self):
'''
Each controller is exectuted in its own thread. The run method is the entry point
:return:
'''
while self.is_running():
if self.get_temp() < self.get_target_temp() - int(self.on):
self.heater_on(100)
elif self.get_temp() >= self.get_target_temp() - int(self.off):
self.heater_off()
else:
self.heater_off()
self.sleep(1)
-112
View File
@@ -1,112 +0,0 @@
# -*- 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')
+67
View File
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
import os
from os.path import join
from modules.core.basetypes import Actor, Sensor, Action
from modules import cbpi
from modules.core.proptypes import Property
import random
@cbpi.addon.sensor.type("Dummy Sensor")
class Dummy(Sensor):
text = Property.Text(label="Text", required=True, description="This is a parameter", configurable=True)
p = Property.Select(label="hallo",options=[1,2,3])
def init(self):
if self.api.get_config_parameter("unit","C") == "C":
self.unit = "°C"
else:
self.unit = "°F"
@cbpi.addon.sensor.action(label="Set Dummy Temp", parameters={
"p1":Property.Select(label="Temp",options=[1,2,3]),
})
def myaction(self, p1):
self.text = p1
self.update_value(int(p1))
def execute(self):
while True:
try:
self.update_value(int(self.text))
except:
pass
self.api.sleep(5)
@cbpi.addon.core.action(name="Delete All Logs")
class ParameterAction(Action):
p1 = Property.Number("P1", configurable=True, description="Target Temperature of Mash Step", unit="C")
p2 = Property.Number("P2", configurable=True, description="Target Temperature of Mash Step", unit="C")
def execute(self, p1, p2, **kwargs):
for i in range(5):
cbpi.sleep(1)
cbpi.notify(headline="Woohoo", message="%s %s" % (p1, p2))
@cbpi.addon.core.action(name="Delete All Logs")
class DeleteAllLogs(Action):
def execute(self, **kwargs):
dir = "./logs"
test = os.listdir(dir)
for item in test:
if item.endswith(".log"):
os.remove(join(dir, item))
cbpi.notify(headline="Logs Deleted", message="All Logs Cleared")
+248
View File
@@ -0,0 +1,248 @@
from modules.core.basetypes import Step
from modules import cbpi
from modules.core.proptypes import Property
import time
@cbpi.addon.step.type("Dummy Step")
class Dummy(Step):
@cbpi.addon.step.action("WOHOO")
def myaction(self):
self.stop_timer()
self.start_timer(10)
text = Property.Text(label="Text", configurable=True, description="WOHOOO")
time = Property.Text(label="Text", configurable=True, description="WOHOOO")
def execute(self):
pass
def reset(self):
self.stop_timer()
# -*- coding: utf-8 -*-
@cbpi.addon.step.type("Dummy Step")
class MashStep(Step):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step", unit="C")
kettle = Property.Kettle("Kettle", description="Kettle in which the mashing takes place" )
timer = Property.Number("Timer in Minutes", configurable=True, description="Timer is started when the target temperature is reached")
def init(self):
'''
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.addon.step.action("Start Timer")
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) >= float(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.api.beep()
self.notify("Mash Step Completed!", "Starting the next step", timeout=None)
self.next()
@cbpi.addon.step.type("MashInStep")
class MashInStep(Step):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step")
kettle = Property.Kettle("Kettle", description="Kettle in which the mashing takes place")
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) >= float(self.temp) and self.s is False:
self.s = True
self.notify("Step Temp Reached!", "Please press the next button to continue", timeout=None)
@cbpi.addon.step.type("MashInStep")
class ChilStep(Step):
timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")
@cbpi.addon.step.action("Start Timer")
def start(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
def reset(self):
self.stop_timer()
def finish(self):
pass
def execute(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
if self.is_timer_finished() == True:
self.next()
@cbpi.addon.step.type("MashInStep")
class PumpStep(Step):
pump = Property.Actor("Pump", description="Pump actor gets toogled")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")
@cbpi.addon.step.action("Start Timer")
def start(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
def reset(self):
self.stop_timer()
def finish(self):
self.actor_off(int(self.pump))
def init(self):
self.actor_on(int(self.pump))
def execute(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
if self.is_timer_finished() == True:
self.next()
@cbpi.addon.step.type("MashInStep")
class BoilStep(Step):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True, default_value=100, description="Target temperature for boiling")
kettle = Property.Kettle("Kettle", description="Kettle in which the boiling step takes place")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=90, description="Timer is started when target temperature is reached")
hop_1 = Property.Number("Hop 1 Addition", configurable=True, description="Fist Hop alert")
hop_1_added = Property.Number("",default_value=None)
hop_2 = Property.Number("Hop 2 Addition", configurable=True, description="Second Hop alert")
hop_2_added = Property.Number("", default_value=None)
hop_3 = Property.Number("Hop 3 Addition", configurable=True)
hop_3_added = Property.Number("", default_value=None, description="Third Hop alert")
hop_4 = Property.Number("Hop 4 Addition", configurable=True)
hop_4_added = Property.Number("", default_value=None, description="Fourth Hop alert")
hop_5 = Property.Number("Hop 5 Addition", configurable=True)
hop_5_added = Property.Number("", default_value=None, description="Fives Hop alert")
def init(self):
'''
Initialize Step. This method is called once at the beginning of the step
:return:
'''
# set target tep
self.set_target_temp(self.temp, self.kettle)
@cbpi.addon.step.action("Start Timer")
def start(self):
'''
Custom Action which can be execute form the brewing dashboard.
All method with decorator @cbpi.action("YOUR CUSTOM NAME") will be available in the user interface
:return:
'''
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
def reset(self):
self.stop_timer()
self.set_target_temp(self.temp, self.kettle)
def finish(self):
self.set_target_temp(0, self.kettle)
def check_hop_timer(self, number, value):
if self.__getattribute__("hop_%s_added" % number) is not True and time.time() > (
self.timer_end - (int(self.timer) * 60 - int(value) * 60)):
self.__setattr__("hop_%s_added" % number, True)
self.notify("Hop Alert", "Please add Hop %s" % number, timeout=None)
def execute(self):
'''
This method is execute in an interval
:return:
'''
# Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= float(self.temp):
# Check if Timer is Running
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
else:
self.check_hop_timer(1, self.hop_1)
self.check_hop_timer(2, self.hop_2)
self.check_hop_timer(3, self.hop_3)
self.check_hop_timer(4, self.hop_4)
self.check_hop_timer(5, self.hop_5)
# Check if timer finished and go to next step
if self.is_timer_finished() == True:
self.notify("Boil Step Completed!", "Starting the next step", timeout=None)
self.next()
Regular → Executable
+53 -51
View File
@@ -1,51 +1,53 @@
import time import time
from thread import start_new_thread from thread import start_new_thread
from modules import cbpi
from modules.core.baseapi import Buzzer
try: from modules import cbpi
import RPi.GPIO as GPIO
except Exception as e: try:
pass import RPi.GPIO as GPIO
except Exception as e:
class Buzzer(object): pass
sound = ["H", 0.1, "L"] class GPIOBuzzer(Buzzer):
def __init__(self, gpio):
try: sound = ["H", 0.1, "L", 0.1, "H", 0.1, "L", 0.1, "H", 0.1, "L"]
cbpi.app.logger.info("INIT BUZZER NOW GPIO%s" % gpio)
self.gpio = gpio
GPIO.setmode(GPIO.BCM) def __init__(self, gpio):
GPIO.setup(gpio, GPIO.OUT) try:
self.state = True cbpi.web.logger.info("INIT BUZZER NOW GPIO%s" % gpio)
cbpi.app.logger.info("BUZZER SETUP OK") self.gpio = int(gpio)
except Exception as e: GPIO.setmode(GPIO.BCM)
self.state = False GPIO.setup(self.gpio, GPIO.OUT)
self.state = True
def beep(self): cbpi.web.logger.info("BUZZER SETUP OK")
if self.state is False: except Exception as e:
cbpi.app.logger.error("BUZZER not working") cbpi.web.logger.info("BUZZER EXCEPTION %s" % str(e))
return self.state = False
def play(sound): def beep(self):
try: if self.state is False:
for i in sound: cbpi.web.logger.error("BUZZER not working")
if (isinstance(i, str)): return
if i == "H":
GPIO.output(int(self.gpio), GPIO.HIGH) def play(sound):
else: try:
GPIO.output(int(self.gpio), GPIO.LOW) for i in sound:
else: if (isinstance(i, str)):
time.sleep(i) if i == "H":
except Exception as e: GPIO.output(int(self.gpio), GPIO.HIGH)
pass else:
GPIO.output(int(self.gpio), GPIO.LOW)
start_new_thread(play, (self.sound,)) else:
time.sleep(i)
@cbpi.initalizer(order=0) except Exception as e:
def init(cbpi): pass
print "INIT BUZZER"
cbpi.app.logger.info("INIT BUZZER") start_new_thread(play, (self.sound,))
gpio = cbpi.get_config_parameter("buzzer", 16)
cbpi.buzzer = Buzzer(gpio) @cbpi.addon.core.initializer(order=1)
cbpi.beep() def init(cbpi):
cbpi.app.logger.info("INIT OK") gpio = cbpi.get_config_parameter("buzzer", 16)
cbpi.buzzer = GPIOBuzzer(gpio)
Regular → Executable
+89 -59
View File
@@ -1,59 +1,89 @@
import time import time
from flask import json, request from flask import json, request
from flask_classy import route from flask_classy import route
from modules import DBModel, cbpi, get_db from modules import cbpi
from modules.core.baseview import BaseView from modules.core.db import DBModel
from modules.core.baseview import RestApi
class Config(DBModel): from modules.database.dbmodel import Config
__fields__ = ["type", "value", "description", "options"]
__table_name__ = "config"
__json_fields__ = ["options"] class ConfigView(RestApi):
__priamry_key__ = "name" model = Config
cache_key = "config"
class ConfigView(BaseView): @route('/<name>', methods=["PUT"])
model = Config def put(self, name):
cache_key = "config" """
Set new config value
@route('/<name>', methods=["PUT"]) ---
def put(self, name): tags:
- config
data = request.json responses:
data["name"] = name 204:
update_data = {"name": data["name"], "value": data["value"]} description: New config value set
"""
if self.api.cache.get(self.cache_key) is not None: data = request.json
#self.pre_post_callback(self.api.cache.get(self.cache_key)[name]) data["name"] = name
self.api.cache.get(self.cache_key)[name].__dict__.update(**update_data) update_data = {"name": data["name"], "value": data["value"]}
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]) if self.api.cache.get(self.cache_key) is not None:
return json.dumps(self.api.cache.get(self.cache_key)[name].__dict__) 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__)
@route('/<id>', methods=["GET"]) self._post_put_callback(self.api.cache.get(self.cache_key)[name])
def getOne(self, id):
return ('NOT SUPPORTED', 400) self.api.emit("CONFIG_UPDATE", name=name, data=data["value"])
return json.dumps(self.api.cache.get(self.cache_key)[name].__dict__)
@route('/<id>', methods=["DELETE"])
def delete(self, id): @route('/<id>', methods=["GET"])
return ('NOT SUPPORTED', 400) def getOne(self, id):
"""
@route('/', methods=["POST"]) Get config parameter
def post(self): ---
return ('NOT SUPPORTED', 400) tags:
- config
@classmethod responses:
def init_cache(cls): 400:
description: Get one config parameter via web api is not supported
with cls.api.app.app_context(): """
cls.api.cache[cls.cache_key] = {} return ('NOT SUPPORTED', 400)
for key, value in cls.model.get_all().iteritems():
cls.post_init_callback(value) @route('/<id>', methods=["DELETE"])
cls.api.cache[cls.cache_key][value.name] = value def delete(self, id):
"""
Delete config parameter
@cbpi.initalizer(order=1) ---
def init(cbpi): tags:
print "INITIALIZE CONFIG MODULE" - config
ConfigView.register(cbpi.app, route_base='/api/config') responses:
ConfigView.init_cache() 400:
description: Deleting config parameter via web api is not supported
"""
return ('NOT SUPPORTED', 400)
@route('/', methods=["POST"])
def post(self):
"""
Get config parameter
---
tags:
- config
responses:
400:
description: Adding new config parameter via web api is not supported
"""
return ('NOT SUPPORTED', 400)
@classmethod
def init_cache(cls):
with cls.api.web.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.addon.core.initializer(order=0)
def init(cbpi):
ConfigView.register(cbpi.web, route_base='/api/config')
ConfigView.init_cache()
Regular → Executable
View File
+260
View File
@@ -0,0 +1,260 @@
from proptypes import *
class BaseAPI(object):
def __init__(self, cbpi):
self.cbpi = cbpi
self.cbpi.cache[self.key] = {}
def init(self):
for name, value in self.cbpi.cache[self.key].iteritems():
value["class"].init_global()
def parseProps(self, key, cls, **options):
name = cls.__name__
tmpObj = cls()
try:
doc = tmpObj.__doc__.strip()
except:
doc = ""
if self.cbpi.cache.get(key) is None:
self.cbpi.cache[key] = {}
self.cbpi.logger.debug(name)
self.cbpi.cache.get(key)[name] = {"name": name, "class": cls, "description":doc, "properties": [], "actions": []}
self.cbpi.cache.get(key)[name].update(options)
members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")]
for m in members:
t = tmpObj.__getattribute__(m)
if isinstance(t, Property.Number):
self.cbpi.cache.get(key)[name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable, "description": t.description, "default_value": t.default_value, "unit": t.unit})
elif isinstance(t, Property.Text):
self.cbpi.cache.get(key)[name]["properties"].append({"name": m, "label": t.label, "type": "text", "required": t.required, "configurable": t.configurable, "description": t.description, "default_value": t.default_value})
elif isinstance(t, Property.Select):
self.cbpi.cache.get(key)[name]["properties"].append({"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options, "description": t.description})
elif isinstance(t, Property.Actor):
self.cbpi.cache.get(key)[name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": True, "description": t.description})
elif isinstance(t, Property.Sensor):
self.cbpi.cache.get(key)[name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": True, "description": t.description})
elif isinstance(t, Property.Kettle):
self.cbpi.cache.get(key)[name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": True, "description": t.description})
for method_name, method in cls.__dict__.iteritems():
if hasattr(method, "action"):
label = method.__getattribute__("label")
parameters = method.__getattribute__("parameters")
props = []
if parameters is not None:
for k, t in parameters.iteritems():
if isinstance(t, Property.Number):
props.append({"name": k, "label": t.label, "type": "number", "configurable": t.configurable, "description": t.description, "default_value": t.default_value, "unit": t.unit})
elif isinstance(t, Property.Text):
props.append({"name": k, "label": t.label, "type": "text", "required": t.required, "configurable": t.configurable, "description": t.description, "default_value": t.default_value})
elif isinstance(t, Property.Select):
props.append({"name": k, "label": t.label, "type": "select", "configurable": True, "options": t.options, "description": t.description})
elif isinstance(t, Property.Actor):
props.append({"name": k, "label": t.label, "type": "actor", "configurable": True, "description": t.description})
elif isinstance(t, Property.Sensor):
props.append({"name": k, "label": t.label, "type": "sensor", "configurable": True, "description": t.description})
elif isinstance(t, Property.Kettle):
props.append({"name": k, "label": t.label, "type": "kettle", "configurable": True, "description": t.description})
self.cbpi.cache.get(key)[name]["actions"].append({"method": method_name, "label": label, "properties":props })
return cls
class SensorAPI(BaseAPI):
key = "sensor_types"
def type(self, description="Step", **options):
def decorator(f):
BaseAPI.parseProps(self, self.key,f, description=description)
return f
return decorator
def action(self, label, parameters=None):
def real_decorator(func):
func.action = True
func.parameters = parameters
func.label = label
return func
return real_decorator
class StepAPI(BaseAPI):
key = "step_types"
def init(self):
pass
def type(self, description="Step", **options):
def decorator(f):
BaseAPI.parseProps(self, self.key,f, description=description)
return f
return decorator
def action(self, label, parameters=None):
def real_decorator(func):
func.action = True
func.label = label
func.parameters = parameters
return func
return real_decorator
class ActorAPI(BaseAPI):
key = "actor_types"
def type(self, description="", **options):
def decorator(f):
BaseAPI.parseProps(self, self.key, f, description=description)
return f
return decorator
def action(self, label, parameters=None):
def real_decorator(func):
func.action = True
func.label = label
func.parameters = parameters
return func
return real_decorator
class KettleAPI(BaseAPI):
key = "controller_types"
def controller(self, description="", **options):
def decorator(f):
BaseAPI.parseProps(self, self.key,f,description=description)
return f
return decorator
def action(self, label, parameters=None):
def real_decorator(func):
func.action = True
func.label = label
func.parameters = parameters
return func
return real_decorator
class FermenterAPI(BaseAPI):
key = "fermentation_controller_types"
def controller(self, description="Step", **options):
def decorator(f):
BaseAPI.parseProps(self, self.key,f,description=description)
return f
return decorator
def action(self, label, parameters=None):
def real_decorator(func):
func.action = True
func.label = label
func.parameters = parameters
return func
return real_decorator
class CoreAPI(BaseAPI):
key = "core"
def __init__(self, cbpi):
self.cbpi = cbpi
self.cbpi.cache["actions"] = {}
self.cbpi.cache["init"] = []
self.cbpi.cache["js"] = {}
self.cbpi.cache["background"] = []
self.cbpi.cache["web_menu"] =[]
def init(self):
self.cbpi.cache["init"] = sorted(self.cbpi.cache["init"], key=lambda k: k['order'])
for value in self.cbpi.cache.get("init"):
value["function"](self.cbpi)
def job(interval, method):
while True:
try:
method(self.cbpi)
except Exception as e:
self.cbpi.logger.error(e)
self.cbpi.sleep(interval)
for value in self.cbpi.cache.get("background"):
t = self.cbpi.start_background_task(target=job, interval=value.get("interval"), method=value.get("function"))
def action(self, **options):
def decorator(f):
BaseAPI.parseProps(self, "actions", f)
return f
return decorator
def add_js(self, name, file):
self.cbpi.cache["js"][name] = file
def add_menu_link(self, name, path):
self.cbpi.cache["web_menu"].append(dict(name=name, path=path))
def initializer(self, order=0, **options):
def decorator(f):
self.cbpi.cache.get("init").append({"function": f, "order": order})
return f
return decorator
def backgroundtask(self, key, interval, **options):
def decorator(f):
self.cbpi.cache.get("background").append({"function": f, "key": key, "interval": interval})
return f
return decorator
def listen(self, name, method=None, async=False):
if method is not None:
if self.cbpi.eventbus.get(name) is None:
self.cbpi.eventbus[name] = []
self.cbpi.eventbus[name].append({"function": method, "async": async})
else:
def real_decorator(function):
if self.cbpi.eventbus.get(name) is None:
self.cbpi.eventbus[name] = []
self.cbpi.eventbus[name].append({"function": function, "async": async})
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
return real_decorator
class Buzzer(object):
def beep(self):
pass
class Addon(object):
def __init__(self, cbpi):
self.step = StepAPI(cbpi)
self.actor = ActorAPI(cbpi)
self.sensor = SensorAPI(cbpi)
self.kettle = KettleAPI(cbpi)
self.fermenter = FermenterAPI(cbpi)
self.core = CoreAPI(cbpi)
def init(self):
self.core.init()
self.step.init()
self.actor.init()
self.sensor.init()
+305
View File
@@ -0,0 +1,305 @@
from modules.core.proptypes import Property
import time
class Base(object):
def __init__(self, *args, **kwds):
for a in kwds:
super(Base, self).__setattr__(a, kwds.get(a))
self.api = kwds.get("cbpi")
self.id = kwds.get("id")
self.value = None
self.__dirty = False
class Action(Base):
def execute(self):
pass
class Actor(Base):
@classmethod
def init_global(cls):
pass
def init(self):
pass
def shutdown(self):
pass
def on(self, power=100):
print "SWITCH ON"
pass
def off(self):
print "SWITCH OFF"
pass
def power(self, power):
print "SET POWER", power
pass
def state(self):
pass
class Sensor(Base):
unit = ""
@classmethod
def init_global(cls):
pass
def init(self):
pass
def get_unit(self):
pass
def stop(self):
pass
def update_value(self, value):
self.value = value
self.cbpi.sensor.write_log(self.id, value)
self.cbpi.emit("SENSOR_UPDATE", id=self.id, value=value)
self.cbpi.ws_emit("SENSOR_UPDATE", self.cbpi.cache["sensors"][self.id])
def execute(self):
print "EXECUTE"
pass
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.sleep(seconds)
def stop(self):
self.__running = False
def get_sensor_value(self, id):
return self.api.sensor.get_sensor_value(id)
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 actor_on(self,id, power=100):
self.api.actor.on(id, power=power)
def actor_off(self, id):
self.api.actor.off(id)
def actor_power(self, power, id=None):
self.api.actor.power(id, power)
def run(self):
pass
class KettleController(ControllerBase):
@staticmethod
def chart(kettle):
result = []
result.append({"name": "Temp", "data_type": "sensor", "data_id": kettle.sensor})
result.append({"name": "Target Temp", "data_type": "kettle", "data_id": kettle.id})
return result
def __init__(self, *args, **kwds):
ControllerBase.__init__(self, *args, **kwds)
self.kettle_id = kwds.get("kettle_id")
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(k.heater, power)
def heater_off(self):
k = self.api.cache.get("kettle").get(self.kettle_id)
if k.heater is not None:
self.actor_off(k.heater)
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))
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):
@staticmethod
def chart(fermenter):
result = []
result.append({"name": "Temp", "data_type": "sensor", "data_id": fermenter.sensor})
result.append({"name": "Target Temp", "data_type": "fermenter", "data_id": fermenter.id})
return result
def __init__(self, *args, **kwds):
ControllerBase.__init__(self, *args, **kwds)
self.fermenter_id = kwds.get("fermenter_id")
self.cooler = kwds.get("cooler")
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
def heater_on(self, power=100):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None:
self.actor_on(int(f.heater))
def heater_off(self):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None:
self.actor_off(int(f.heater))
def cooler_on(self, power=100):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.cooler is not None:
self.actor_on(power, int(f.cooler))
def cooler_off(self):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.cooler is not None:
self.actor_off(int(f.cooler))
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))
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 timer_remaining(self):
if self.timer_end is not None:
return self.timer_end - int(time.time())
else:
return None
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 Step(Base, Timer):
@classmethod
def init_global(cls):
pass
__dirty = False
managed_fields = []
n = False
def next(self):
self.n = True
def init(self):
pass
def finish(self):
pass
def reset(self):
pass
def execute(self):
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(Step, 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 set_target_temp(self, temp, id=None):
temp = float(temp)
try:
if id is None:
self.api.emit("SET_TARGET_TEMP", id=self.kettle_id, temp=temp)
else:
self.api.emit("SET_TARGET_TEMP", id=id, temp=temp)
except Exception as e:
self.api.notify("Faild to set Target Temp", "", type="warning")
def get_kettle_temp(self, id=None):
id = int(id)
if id is None:
id = self.kettle_id
return self.api.sensor.get_value(int(self.api.cache.get("kettle").get(id).sensor))
def is_dirty(self):
return self.__dirty
def reset_dirty(self):
self.__dirty = False
def notify(self, headline, messsage, timeout=None):
self.api.notify(headline, messsage, timeout)
def __setattr__(self, name, value):
if name != "_StepBase__dirty" and name in self.managed_fields:
self.__dirty = True
super(Step, self).__setattr__(name, value)
else:
super(Step, self).__setattr__(name, value)
Regular → Executable
+116 -109
View File
@@ -1,109 +1,116 @@
from flask import request, json from flask import request, json
from flask_classy import route, FlaskView from flask_classy import route, FlaskView
from modules import cbpi from flask_login import login_required
from modules import cbpi
class BaseView(FlaskView):
as_array = False class RestApi(FlaskView):
cache_key = None
api = cbpi as_array = False
cache_key = None
@route('/<int:id>') api = cbpi
def getOne(self, id):
@login_required
if self.api.cache.get(self.cache_key) is not None: @route('/<int:id>', methods=["GET"])
return json.dumps(self.api.cache.get(self.cache_key).get(id)) def getOne(self, id):
else: if self.api.cache.get(self.cache_key) is not None:
return json.dumps(self.model.get_one(id)) return json.dumps(self.api.cache.get(self.cache_key).get(id))
else:
@route('/') return json.dumps(self.model.get_one(id))
def getAll(self):
if self.api.cache.get(self.cache_key) is not None: @login_required
return json.dumps(self.api.cache.get(self.cache_key)) @route('/', methods=["GET"])
else: def getAll(self):
return json.dumps(self.model.get_all()) if self.api.cache.get(self.cache_key) is not None:
return json.dumps(self.api.cache.get(self.cache_key))
def pre_post_callback(self, data): else:
pass return json.dumps(self.model.get_all())
def _pre_post_callback(self, data):
def post_post_callback(self, m): pass
pass
@route('/', methods=["POST"]) def _post_post_callback(self, m):
def post(self): pass
data = request.json
self.pre_post_callback(data) @login_required
m = self.model.insert(**data) @route('/', methods=["POST"])
if self.api.cache.get(self.cache_key) is not None: def post(self):
self.api.cache.get(self.cache_key)[m.id] = m
data = request.json
self.post_post_callback(m) self.api.web.logger.info("INSERT Model %s", self.model.__name__)
self._pre_post_callback(data)
return json.dumps(m) m = self.model.insert(**data)
if self.api.cache.get(self.cache_key) is not None:
def pre_put_callback(self, m): self.api.cache.get(self.cache_key)[m.id] = m
pass
self._post_post_callback(m)
def post_put_callback(self, m):
pass return json.dumps(m)
def _pre_put_callback(self, m):
@route('/<int:id>', methods=["PUT"]) pass
def put(self, id):
data = request.json def _post_put_callback(self, m):
data["id"] = id pass
try:
del data["instance"] @login_required
except: @route('/<int:id>', methods=["PUT"])
pass def put(self, id):
if self.api.cache.get(self.cache_key) is not None: data = request.json
self.pre_put_callback(self.api.cache.get(self.cache_key)[id]) data["id"] = id
self.api.cache.get(self.cache_key)[id].__dict__.update(**data) try:
m = self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__) del data["instance"]
self.post_put_callback(self.api.cache.get(self.cache_key)[id]) except:
return json.dumps(self.api.cache.get(self.cache_key)[id]) pass
else: if self.api.cache.get(self.cache_key) is not None:
m = self.model.update(**data) self._pre_put_callback(self.api.cache.get(self.cache_key)[id])
self.api.cache.get(self.cache_key)[id].__dict__.update(**data)
self.post_put_callback(m) m = self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
return json.dumps(m) self._post_put_callback(self.api.cache.get(self.cache_key)[id])
return json.dumps(self.api.cache.get(self.cache_key)[id])
else:
def pre_delete_callback(self, m): m = self.model.update(**data)
pass
self._post_put_callback(m)
def post_delete_callback(self, id): return json.dumps(m)
pass
@route('/<int:id>', methods=["DELETE"]) def _pre_delete_callback(self, m):
def delete(self, id): pass
if self.api.cache.get(self.cache_key) is not None:
self.pre_delete_callback(self.api.cache.get(self.cache_key)[id]) def _post_delete_callback(self, id):
del self.api.cache.get(self.cache_key)[id] pass
m = self.model.delete(id)
@login_required
def post_delete_callback(self, id): @route('/<int:id>', methods=["DELETE"])
pass def delete(self, id):
return ('',204) if self.api.cache.get(self.cache_key) is not None:
self._pre_delete_callback(self.api.cache.get(self.cache_key)[id])
@classmethod del self.api.cache.get(self.cache_key)[id]
def post_init_callback(cls, obj): m = self.model.delete(id)
pass
def _post_delete_callback(self, id):
@classmethod pass
def init_cache(cls): return ('',204)
with cls.api.app.app_context():
@classmethod
if cls.model.__as_array__ is True: def post_init_callback(cls, obj):
cls.api.cache[cls.cache_key] = [] pass
for value in cls.model.get_all(): @classmethod
cls.post_init_callback(value) def init_cache(cls):
cls.api.cache[cls.cache_key].append(value) with cls.api.web.app_context():
else:
cls.api.cache[cls.cache_key] = {} if cls.model.__as_array__ is True:
for key, value in cls.model.get_all().iteritems(): cls.api.cache[cls.cache_key] = []
cls.post_init_callback(value)
cls.api.cache[cls.cache_key][key] = value 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
-147
View File
@@ -1,147 +0,0 @@
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 f.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 f.heater is not None:
self.actor_off(int(f.heater))
@cbpi.try_catch(None)
def cooler_on(self, power=100):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None:
self.actor_on(power, int(f.cooler))
@cbpi.try_catch(None)
def cooler_off(self):
f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None:
self.actor_off(int(f.cooler))
@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))
Regular → Executable
+405 -461
View File
@@ -1,461 +1,405 @@
import pprint import json
import logging
import sqlite3 import os
from flask import make_response, g import sqlite3
import datetime import uuid
from datetime import datetime from datetime import datetime
from flask.views import MethodView from functools import wraps, update_wrapper
from flask_classy import FlaskView, route from importlib import import_module
from time import localtime, strftime
from time import localtime, strftime import time
from functools import wraps, update_wrapper
from flask import Flask, redirect, json, g, make_response
from flask_socketio import SocketIO
from props import *
from baseapi import *
from hardware import * from db import DBModel
from modules.core.basetypes import Sensor, Actor
import time from modules.database.dbmodel import Kettle
import uuid
class NotificationAPI(object): class ComplexEncoder(json.JSONEncoder):
pass def default(self, obj):
try:
class ActorAPI(object): if isinstance(obj, DBModel):
return obj.__dict__
def init_actors(self): elif isinstance(obj, Actor):
self.app.logger.info("Init Actors") return {"state": obj.value}
t = self.cache.get("actor_types") elif isinstance(obj, Sensor):
for key, value in t.iteritems(): return {"value": obj.value, "unit": obj.unit}
value.get("class").init_global() elif hasattr(obj, "callback"):
return obj()
for key in self.cache.get("actors"): else:
self.init_actor(key) return None
return None
def init_actor(self, id): except TypeError as e:
try: pass
value = self.cache.get("actors").get(int(id)) return None
cfg = value.config.copy()
cfg.update(dict(api=self, id=id, name=value.name)) class ActorCore(object):
cfg.update(dict(api=self, id=id, name=value.name)) key = "actor_types"
clazz = self.cache.get("actor_types").get(value.type).get("class")
value.instance = clazz(**cfg) def __init__(self, cbpi):
value.instance.init() self.cbpi = cbpi
value.state = 0 self.cbpi.cache["actors"] = {}
value.power = 100 self.cbpi.cache[self.key] = {}
except Exception as e:
self.notify("Actor Error", "Failed to setup actor %s. Please check the configuraiton" % value.name, def init(self):
type="danger", timeout=None) for key, value in self.cbpi.cache["actors"].iteritems():
self.app.logger.error("Initializing of Actor %s failed" % id) self.init_one(key)
def switch_actor_on(self, id, power=None): def init_one(self, id):
actor = self.cache.get("actors").get(id) try:
if actor.state == 1: actor = self.cbpi.cache["actors"][id]
return clazz = self.cbpi.cache[self.key].get(actor.type)["class"]
cfg = actor.config.copy()
actor.instance.on(power=power) cfg.update(dict(cbpi=self.cbpi, id=id))
actor.state = 1 self.cbpi.cache["actors"][id].instance = clazz(**cfg)
if power is not None: actor.state = 0
actor.power = 100
actor.power = power self.cbpi.emit("INIT_ACTOR", id=id)
self.emit("SWITCH_ACTOR", actor) except Exception as e:
self.cbpi.web.logger.error(e)
def actor_power(self, id, power=100):
actor = self.cache.get("actors").get(id) def stop_one(self, id):
actor.instance.set_power(power=power) self.cbpi.cache["actors"][id]["instance"].stop()
actor.power = power self.cbpi.emit("STOP_ACTOR", id=id)
self.emit("SWITCH_ACTOR", actor)
def on(self, id, power=100):
def switch_actor_off(self, id): try:
actor = self.cache.get("actors").get(id) actor = self.cbpi.cache["actors"].get(int(id))
actor.instance.on()
if actor.state == 0: actor.state = 1
return actor.power = power
self.cbpi.ws_emit("SWITCH_ACTOR", actor)
self.cbpi.emit("SWITCH_ACTOR_ON", id=id, power=power)
actor.instance.off() return True
actor.state = 0 except Exception as e:
self.cbpi.logger.error(e)
return False
self.emit("SWITCH_ACTOR", actor)
def off(self, id):
class SensorAPI(object): try:
actor = self.cbpi.cache["actors"].get(int(id))
def init_sensors(self): actor.instance.off()
''' actor.state = 0
Initialize all sensors self.cbpi.ws_emit("SWITCH_ACTOR", actor)
:return: self.cbpi.emit("SWITCH_ACTOR_OFF", id=id)
''' return True
except Exception as e:
self.app.logger.info("Init Sensors") self.cbpi.logger.error(e)
return False
t = self.cache.get("sensor_types")
for key, value in t.iteritems(): def toggle(self, id):
value.get("class").init_global() if self.cbpi.cache.get("actors").get(id).state == 0:
self.on(id)
for key in self.cache.get("sensors"): else:
self.init_sensor(key) self.off(id)
def stop_sensor(self, id): def power(self, id, power):
try:
try: actor = self.cbpi.cache["actors"].get(int(id))
self.cache.get("sensors").get(id).instance.stop() actor.instance.power(power)
except Exception as e: actor.power = power
self.cbpi.ws_emit("SWITCH_ACTOR", actor)
self.app.logger.info("Stop Sensor Error") self.cbpi.emit("SWITCH_ACTOR_POWER_CHANGE", id=id, power=power)
pass return True
except Exception as e:
self.cbpi.logger.error(e)
def init_sensor(self, id): return False
'''
initialize sensor by id def action(self, id, method, **data):
:param id: self.cbpi.cache.get("actors").get(id).instance.__getattribute__(method)(**data)
:return:
'''
def toggle_timeout(self, id, seconds):
def start_active_sensor(instance):
''' def toggle( id, seconds):
start active sensors as background job self.cbpi.cache.get("actors").get(int(id)).timer = int(time.time()) + int(seconds)
:param instance: self.toggle(int(id))
:return: self.cbpi.sleep(seconds)
''' self.cbpi.cache.get("actors").get(int(id)).timer = None
instance.execute() self.toggle(int(id))
job = self.cbpi._socketio.start_background_task(target=toggle, id=id, seconds=seconds)
try:
if id in self.cache.get("sensor_instances"): def get_state(self, actor_id):
self.cache.get("sensor_instances").get(id).stop() pass
value = self.cache.get("sensors").get(id)
cfg = value.config.copy() class SensorCore(object):
cfg.update(dict(api=self, id=id, name=value.name)) key = "sensor_types"
clazz = self.cache.get("sensor_types").get(value.type).get("class")
value.instance = clazz(**cfg) def __init__(self, cbpi):
value.instance.init() self.cbpi = cbpi
if isinstance(value.instance, SensorPassive): self.cbpi.cache["sensors"] = {}
# Passive Sensors self.cbpi.cache["sensor_instances"] = {}
value.mode = "P" self.cbpi.cache["sensor_types"] = {}
else:
# Active Sensors def init(self):
value.mode = "A" for key, value in self.cbpi.cache["sensors"].iteritems():
t = self.socketio.start_background_task(target=start_active_sensor, instance=value.instance) self.init_one(key)
except Exception as e: def init_one(self, id):
try:
self.notify("Sensor Error", "Failed to setup Sensor %s. Please check the configuraiton" % value.name, type="danger", timeout=None) sensor = self.cbpi.cache["sensors"][id]
self.app.logger.error("Initializing of Sensor %s failed" % id) clazz = self.cbpi.cache[self.key].get(sensor.type)["class"]
cfg = sensor.config.copy()
def receive_sensor_value(self, id, value): cfg.update(dict(cbpi=self.cbpi, id=id))
self.emit("SENSOR_UPDATE", self.cache.get("sensors")[id]) self.cbpi.cache["sensors"][id].instance = clazz(**cfg)
self.save_to_file(id, value) self.cbpi.cache["sensors"][id].instance.init()
def save_to_file(self, id, value, prefix="sensor"): self.cbpi.emit("INIT_SENSOR", id=id)
filename = "./logs/%s_%s.log" % (prefix, str(id))
formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime()) def job(obj):
msg = str(formatted_time) + "," +str(value) + "\n" obj.execute()
with open(filename, "a") as file: t = self.cbpi._socketio.start_background_task(target=job, obj=self.cbpi.cache["sensors"][id].instance)
file.write(msg) self.cbpi.emit("INIT_SENSOR", id=id)
def log_action(self, text): except Exception as e:
filename = "./logs/action.log" print "ERROR"
formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime()) self.cbpi.web.logger.error(e)
def stop_one(self, id):
with open(filename, "a") as file:
file.write("%s,%s\n" % (formatted_time, text)) self.cbpi.cache["sensors"][id].instance.stop()
self.cbpi.emit("STOP_SENSOR", id=id)
def shutdown_sensor(self, id):
self.cache.get("sensors")[id].stop() def get_value(self, sensorid):
try:
return self.cbpi.cache["sensors"][sensorid].instance.value
def get_sensor_value(self, id): except:
try: return None
id = int(id)
return int(self.cache.get("sensors")[id].instance.last_value) def get_state(self, actor_id):
except Exception as e: pass
return None def write_log(self, id, value, prefix="sensor"):
filename = "./logs/%s_%s.log" % (prefix, str(id))
class CacheAPI(object): formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
msg = str(formatted_time) + "," + str(value) + "\n"
def get_sensor(self, id):
try: with open(filename, "a") as file:
return self.cache["sensors"][id] file.write(msg)
except:
return None def action(self, id, method, **data):
self.cbpi.cache.get("sensors").get(id).instance.__getattribute__(method)(**data)
def get_actor(self, id):
try:
return self.cache["actors"][id] class BrewingCore(object):
except:
return None def __init__(self, cbpi):
self.cbpi = cbpi
class CraftBeerPi(ActorAPI, SensorAPI): self.cbpi.cache["step_types"] = {}
self.cbpi.cache["controller_types"] = {}
cache = {
"init": {}, def log_action(self, text):
"config": {}, filename = "./logs/action.log"
"actor_types": {}, formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
"sensor_types": {}, with open(filename, "a") as file:
"sensors": {}, text = text.encode("utf-8")
"sensor_instances": {}, file.write("%s,%s\n" % (formatted_time, text))
"init": [],
"background":[], def get_controller(self, name):
"step_types": {}, return self.cbpi.cache["controller_types"].get(name)
"controller_types": {},
"messages": [], def set_target_temp(self, id, temp):
"plugins": {}, self.cbpi.cache.get("kettle")[id].target_temp = float(temp)
"fermentation_controller_types": {}, Kettle.update(**self.cbpi.cache.get("kettle")[id].__dict__)
"fermenter_task": {} self.cbpi.ws_emit("UPDATE_KETTLE_TARGET_TEMP", {"id": id, "target_temp": temp})
} self.cbpi.emit("SET_KETTLE_TARGET_TEMP", id=id, temp=temp)
buzzer = None
eventbus = {}
def toggle_automatic(self, id):
kettle = self.cbpi.cache.get("kettle")[id]
# constructor if kettle.state is False:
def __init__(self, app, socketio): # Start controller
self.app = app if kettle.logic is not None:
self.socketio = socketio cfg = kettle.config.copy()
cfg.update(dict(api=self.cbpi, kettle_id=kettle.id, heater=kettle.heater, sensor=kettle.sensor))
instance = self.get_controller(kettle.logic).get("class")(**cfg)
def emit(self, key, data): instance.init()
self.socketio.emit(key, data, namespace='/brew') kettle.instance = instance
def notify(self, headline, message, type="success", timeout=5000): def run(instance):
self.beep() instance.run()
msg = {"id": str(uuid.uuid1()), "type": type, "headline": headline, "message": message, "timeout": timeout}
if timeout is None: t = self.cbpi._socketio.start_background_task(target=run, instance=instance)
self.cache["messages"].append(msg) kettle.state = not kettle.state
self.emit("NOTIFY", msg) self.cbpi.ws_emit("UPDATE_KETTLE", self.cbpi.cache.get("kettle").get(id))
self.cbpi.emit("KETTLE_CONTROLLER_STARTED", id=id)
def beep(self): else:
# Stop controller
if self.buzzer is not None: kettle.instance.stop()
self.buzzer.beep() kettle.state = not kettle.state
self.cbpi.ws_emit("UPDATE_KETTLE", self.cbpi.cache.get("kettle").get(id))
self.cbpi.emit("KETTLE_CONTROLLER_STOPPED", id=id)
def add_cache_callback(self, key, method):
method.callback = True
self.cache[key] = method class FermentationCore(object):
def __init__(self, cbpi):
def get_config_parameter(self, key, default): self.cbpi = cbpi
cfg = self.cache.get("config").get(key) self.cbpi.cache["fermenter"] = {}
self.cbpi.cache["fermentation_controller_types"] = {}
if cfg is None:
return default def get_controller(self, name):
else: return self.cbpi.cache["fermentation_controller_types"].get(name)
return cfg.value
def add_config_parameter(self, name, value, type, description, options=None): class Logger(object):
from modules.config import Config def __init__(self, cbpi):
with self.app.app_context(): self.cbpi = cbpi
c = Config.insert(**{"name":name, "value": value, "type": type, "description": description, "options": options})
if self.cache.get("config") is not None: def error(self, msg, *args, **kwargs):
self.cache.get("config")[c.name] = c self.cbpi.web.logger.error(msg, *args, **kwargs)
def clear_cache(self, key, is_array=False): def info(self, msg, *args, **kwargs):
if is_array: self.cbpi.web.logger.info(msg, *args, **kwargs)
self.cache[key] = []
else: def debug(self, msg, *args, **kwargs):
self.cache[key] = {} self.cbpi.web.logger.debug(msg, *args, **kwargs)
# helper method for parsing props def warning(self, msg, *args, **kwargs):
def __parseProps(self, key, cls): self.cbpi.web.logger.warning(msg, *args, **kwargs)
name = cls.__name__
self.cache[key][name] = {"name": name, "class": cls, "properties": []} class CraftBeerPI(object):
tmpObj = cls() cache = {}
members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")] eventbus = {}
for m in members:
if isinstance(tmpObj.__getattribute__(m), Property.Number): def __init__(self):
t = tmpObj.__getattribute__(m) self.cache["messages"] = []
self.cache[key][name]["properties"].append( self.cache["version"] = "3.1"
{"name": m, "label": t.label, "type": "number", "configurable": t.configurable}) FORMAT = '%(asctime)-15s - %(levelname)s - %(message)s'
elif isinstance(tmpObj.__getattribute__(m), Property.Text): logging.basicConfig(filename='./logs/app.log', level=logging.INFO, format=FORMAT)
t = tmpObj.__getattribute__(m) logging.getLogger('socketio').setLevel(logging.ERROR)
self.cache[key][name]["properties"].append( logging.getLogger('engineio').setLevel(logging.ERROR)
{"name": m, "label": t.label, "type": "text", "configurable": t.configurable}) self.web = Flask(__name__)
elif isinstance(tmpObj.__getattribute__(m), Property.Select): self.logger = Logger(self)
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append( self.logger.info("###Startup CraftBeerPi %s ###" % self.cache.get("version"))
{"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options}) self.web.secret_key = 'Cr4ftB33rP1'
return cls self.web.json_encoder = ComplexEncoder
self._socketio = SocketIO(self.web, json=json, logging=False)
def actor(self, cls):
return self.__parseProps("actor_types", cls) self.modules = {}
self.addon = Addon(self)
def sensor(self, cls): self.actor = ActorCore(self)
return self.__parseProps("sensor_types", cls) self.sensor = SensorCore(self)
self.brewing = BrewingCore(self)
def controller(self, cls): self.fermentation = FermentationCore(self)
return self.__parseProps("controller_types", cls)
@self.web.route('/')
def fermentation_controller(self, cls): def index():
return self.__parseProps("fermentation_controller_types", cls) return redirect('ui')
def get_controller(self, name): def run(self):
return self.cache["controller_types"].get(name) self.__init_db()
self.loadPlugins()
def get_fermentation_controller(self, name): self.addon.init()
return self.cache["fermentation_controller_types"].get(name) self.sensor.init()
# Step action self.actor.init()
def action(self,label): self.beep()
def real_decorator(func): try:
func.action = True port = int(self.get_config_parameter('port', '5000'))
func.label = label except ValueError:
return func port = 5000
return real_decorator
self._socketio.run(self.web, host='0.0.0.0', port=port)
# step decorator
def step(self, cls): def beep(self):
self.buzzer.beep()
key = "step_types"
name = cls.__name__ def sleep(self, seconds):
self.cache[key][name] = {"name": name, "class": cls, "properties": [], "actions": []} self._socketio.sleep(seconds)
tmpObj = cls() def start_background_task(self, target, *args, **kwargs):
members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")]
for m in members: self._socketio.start_background_task(target, *args, **kwargs)
if isinstance(tmpObj.__getattribute__(m), StepProperty.Number):
t = tmpObj.__getattribute__(m) def notify(self, headline, message, type="success", timeout=5000):
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable}) msg = {"id": str(uuid.uuid1()), "type": type, "headline": headline, "message": message, "timeout": timeout}
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Text): self.ws_emit("NOTIFY", msg)
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "text", "configurable": t.configurable}) def ws_emit(self, key, data):
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Select): self._socketio.emit(key, data, namespace='/brew')
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "select", "options": t.options}) def __init_db(self, ):
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Actor):
t = tmpObj.__getattribute__(m) with self.web.app_context():
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": t.configurable}) db = self.get_db()
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Sensor): try:
t = tmpObj.__getattribute__(m) with self.web.open_resource('../../config/schema.sql', mode='r') as f:
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": t.configurable}) db.cursor().executescript(f.read())
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Kettle): db.commit()
t = tmpObj.__getattribute__(m) except Exception as e:
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable})
pass
for name, method in cls.__dict__.iteritems():
if hasattr(method, "action"): def nocache(self, view):
label = method.__getattribute__("label") @wraps(view)
self.cache[key][cls.__name__]["actions"].append({"method": name, "label": label}) def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs))
return cls 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'
# Event Bus response.headers['Pragma'] = 'no-cache'
def event(self, name, async=False): response.headers['Expires'] = '-1'
return response
def real_decorator(function):
if self.eventbus.get(name) is None: return update_wrapper(no_cache, view)
self.eventbus[name] = []
self.eventbus[name].append({"function": function, "async": async}) def get_db(self):
def wrapper(*args, **kwargs): db = getattr(g, '_database', None)
return function(*args, **kwargs) if db is None:
return wrapper def dict_factory(cursor, row):
return real_decorator d = {}
for idx, col in enumerate(cursor.description):
def emit_message(self, message): d[col[0]] = row[idx]
self.emit_event(name="MESSAGE", message=message) return d
def emit_event(self, name, **kwargs): db = g._database = sqlite3.connect('craftbeerpi.db')
for i in self.eventbus.get(name, []): db.row_factory = dict_factory
if i["async"] is False: return db
i["function"](**kwargs)
else: def add_cache_callback(self, key, method):
t = self.socketio.start_background_task(target=i["function"], **kwargs) method.callback = True
self.cache[key] = method
# initializer decorator
def initalizer(self, order=0): def get_config_parameter(self, key, default=None):
def real_decorator(function): cfg = self.cache["config"].get(key)
self.cache["init"].append({"function": function, "order": order}) if cfg is None:
def wrapper(*args, **kwargs): return default
return function(*args, **kwargs) else:
return wrapper return cfg.value
return real_decorator
def set_config_parameter(self, name, value):
from modules.config import Config
with self.web.app_context():
def try_catch(self, errorResult="ERROR"): update_data = {"name": name, "value": value}
def real_decorator(function): self.cache.get("config")[name].__dict__.update(**update_data)
def wrapper(*args, **kwargs): c = Config.update(**update_data)
try: self.ws_emit("UPDATE_CONFIG", c)
return function(*args, **kwargs)
except:
self.app.logger.error("Exception in function %s. Return default %s" % (function.__name__, errorResult)) def emit(self, key, **kwargs):
return errorResult if self.eventbus.get(key) is not None:
return wrapper for value in self.eventbus[key]:
if value["async"] is False:
return real_decorator value["function"](**kwargs)
else:
def nocache(self, view): t = self.cbpi._socketio.start_background_task(target=value["function"], **kwargs)
@wraps(view)
def no_cache(*args, **kwargs): def loadPlugins(self):
response = make_response(view(*args, **kwargs)) for filename in os.listdir("./modules/plugins"):
response.headers['Last-Modified'] = datetime.now()
response.headers[ if os.path.isdir("./modules/plugins/" + filename) is False:
'Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' continue
response.headers['Pragma'] = 'no-cache' try:
response.headers['Expires'] = '-1' self.modules[filename] = import_module("modules.plugins.%s" % (filename))
return response except Exception as e:
return update_wrapper(no_cache, view) self.notify("Failed to load plugin %s " % filename, str(e), type="danger", timeout=None)
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"))
Regular → Executable
+133 -132
View File
@@ -1,132 +1,133 @@
import sqlite3 import sqlite3
from flask import json, g
from flask import json, g
def get_db():
def get_db(): db = getattr(g, '_database', None)
db = getattr(g, '_database', None) if db is None:
if db is None: def dict_factory(cursor, row):
def dict_factory(cursor, row): d = {}
d = {} for idx, col in enumerate(cursor.description):
for idx, col in enumerate(cursor.description): d[col[0]] = row[idx]
d[col[0]] = row[idx] return d
return d db = g._database = sqlite3.connect('craftbeerpi.db')
db = g._database = sqlite3.connect('craftbeerpi.db') db.row_factory = dict_factory
db.row_factory = dict_factory return db
return db
class DBModel(object):
class DBModel(object):
__priamry_key__ = "id"
__priamry_key__ = "id" __as_array__ = False
__as_array__ = False __order_by__ = None
__order_by__ = None __json_fields__ = []
__json_fields__ = []
def __init__(self, args):
def __init__(self, args):
self.__setattr__(self.__priamry_key__, args.get(self.__priamry_key__))
self.__setattr__(self.__priamry_key__, args.get(self.__priamry_key__)) for f in self.__fields__:
for f in self.__fields__: if f in self.__json_fields__:
if f in self.__json_fields__: if args.get(f) is not None:
if args.get(f) is not None:
if isinstance(args.get(f) , dict) or isinstance(args.get(f) , list) :
if isinstance(args.get(f) , dict) or isinstance(args.get(f) , list) : self.__setattr__(f, args.get(f))
self.__setattr__(f, args.get(f)) else:
else: self.__setattr__(f, json.loads(args.get(f)))
self.__setattr__(f, json.loads(args.get(f))) else:
else: self.__setattr__(f, None)
self.__setattr__(f, None) else:
else: self.__setattr__(f, args.get(f))
self.__setattr__(f, args.get(f))
@classmethod
@classmethod def get_all(cls):
def get_all(cls): cur = get_db().cursor()
cur = get_db().cursor() if cls.__order_by__ is not None:
if cls.__order_by__ is not None:
cur.execute("SELECT * FROM %s ORDER BY '%s'" % (cls.__table_name__,cls.__order_by__)) cur.execute("SELECT * FROM %s ORDER BY %s.'%s'" % (cls.__table_name__,cls.__table_name__,cls.__order_by__))
else: else:
cur.execute("SELECT * FROM %s" % cls.__table_name__) cur.execute("SELECT * FROM %s" % cls.__table_name__)
if cls.__as_array__ is True: if cls.__as_array__ is True:
result = [] result = []
for r in cur.fetchall(): for r in cur.fetchall():
result.append( cls(r))
else: result.append( cls(r))
result = {} else:
for r in cur.fetchall(): result = {}
result[r.get(cls.__priamry_key__)] = cls(r) for r in cur.fetchall():
return result result[r.get(cls.__priamry_key__)] = cls(r)
return result
@classmethod
def get_one(cls, id): @classmethod
cur = get_db().cursor() def get_one(cls, id):
cur.execute("SELECT * FROM %s WHERE %s = ?" % (cls.__table_name__, cls.__priamry_key__), (id,)) cur = get_db().cursor()
r = cur.fetchone() cur.execute("SELECT * FROM %s WHERE %s = ?" % (cls.__table_name__, cls.__priamry_key__), (id,))
if r is not None: r = cur.fetchone()
return cls(r) if r is not None:
else: return cls(r)
return None else:
return None
@classmethod
def delete(cls, id): @classmethod
cur = get_db().cursor() def delete(cls, id):
cur.execute("DELETE FROM %s WHERE %s = ? " % (cls.__table_name__, cls.__priamry_key__), (id,)) cur = get_db().cursor()
get_db().commit() cur.execute("DELETE FROM %s WHERE %s = ? " % (cls.__table_name__, cls.__priamry_key__), (id,))
get_db().commit()
@classmethod
def insert(cls, **kwargs): @classmethod
cur = get_db().cursor() def insert(cls, **kwargs):
cur = get_db().cursor()
if cls.__priamry_key__ is not None and kwargs.has_key(cls.__priamry_key__):
query = "INSERT INTO %s (%s, %s) VALUES (?, %s)" % ( if cls.__priamry_key__ is not None and kwargs.has_key(cls.__priamry_key__):
cls.__table_name__, query = "INSERT INTO %s (%s, %s) VALUES (?, %s)" % (
cls.__priamry_key__, cls.__table_name__,
', '.join("'%s'" % str(x) for x in cls.__fields__), cls.__priamry_key__,
', '.join(['?'] * len(cls.__fields__))) ', '.join("'%s'" % str(x) for x in cls.__fields__),
data = () ', '.join(['?'] * len(cls.__fields__)))
data = data + (kwargs.get(cls.__priamry_key__),) data = ()
for f in cls.__fields__: data = data + (kwargs.get(cls.__priamry_key__),)
if f in cls.__json_fields__: for f in cls.__fields__:
data = data + (json.dumps(kwargs.get(f)),) if f in cls.__json_fields__:
else: data = data + (json.dumps(kwargs.get(f)),)
data = data + (kwargs.get(f),) else:
else: data = data + (kwargs.get(f),)
else:
query = 'INSERT INTO %s (%s) VALUES (%s)' % (
cls.__table_name__, query = 'INSERT INTO %s (%s) VALUES (%s)' % (
', '.join("'%s'" % str(x) for x in cls.__fields__), cls.__table_name__,
', '.join(['?'] * len(cls.__fields__))) ', '.join("'%s'" % str(x) for x in cls.__fields__),
', '.join(['?'] * len(cls.__fields__)))
data = ()
for f in cls.__fields__: data = ()
if f in cls.__json_fields__: for f in cls.__fields__:
data = data + (json.dumps(kwargs.get(f)),) if f in cls.__json_fields__:
else: data = data + (json.dumps(kwargs.get(f)),)
data = data + (kwargs.get(f),) else:
data = data + (kwargs.get(f),)
print query, data
cur.execute(query, data)
get_db().commit() cur.execute(query, data)
i = cur.lastrowid get_db().commit()
kwargs["id"] = i i = cur.lastrowid
kwargs["id"] = i
return cls(kwargs)
return cls(kwargs)
@classmethod
def update(cls, **kwargs): @classmethod
cur = get_db().cursor() def update(cls, **kwargs):
query = 'UPDATE %s SET %s WHERE %s = ?' % ( cur = get_db().cursor()
cls.__table_name__, query = 'UPDATE %s SET %s WHERE %s = ?' % (
', '.join("'%s' = ?" % str(x) for x in cls.__fields__),cls.__priamry_key__) cls.__table_name__,
', '.join("'%s' = ?" % str(x) for x in cls.__fields__),cls.__priamry_key__)
data = ()
for f in cls.__fields__: data = ()
if f in cls.__json_fields__: for f in cls.__fields__:
data = data + (json.dumps(kwargs.get(f)),) if f in cls.__json_fields__:
else: data = data + (json.dumps(kwargs.get(f)),)
data = data + (kwargs.get(f),) else:
data = data + (kwargs.get(f),)
data = data + (kwargs.get(cls.__priamry_key__),)
cur.execute(query, data) data = data + (kwargs.get(cls.__priamry_key__),)
get_db().commit() cur.execute(query, data)
return cls(kwargs) get_db().commit()
return cls(kwargs)
+43
View File
@@ -0,0 +1,43 @@
import sqlite3
import os
from modules import cbpi
from modules.core.db import get_db
def execute_file(curernt_version, data):
if curernt_version >= data["version"]:
cbpi.web.logger.info("SKIP DB FILE: %s" % data["file"])
return
try:
with sqlite3.connect("craftbeerpi.db") as conn:
with open('./update/%s' % data["file"], 'r') as f:
d = f.read()
sqlCommands = d.split(";")
cur = conn.cursor()
for s in sqlCommands:
cur.execute(s)
cur.execute("INSERT INTO schema_info (version,filename) values (?,?)", (data["version"], data["file"]))
conn.commit()
except sqlite3.OperationalError as e:
cbpi.logger.error(e)
@cbpi.addon.core.initializer(order=-9999)
def init(cbpi):
with cbpi.web.app_context():
conn = get_db()
cur = conn.cursor()
current_version = None
try:
cur.execute("SELECT max(version) as m FROM schema_info")
m = cur.fetchone()
current_version = m["m"]
except:
pass
result = []
for filename in os.listdir("./update"):
if filename.endswith(".sql"):
d = {"version": int(filename[:filename.index('_')]), "file": filename}
result.append(d)
execute_file(current_version, d)
-104
View File
@@ -1,104 +0,0 @@
# -*- 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"
+50 -38
View File
@@ -1,38 +1,50 @@
class PropertyType(object): class PropertyType(object):
pass pass
class Property(object): class Property(object):
class Select(PropertyType): class Select(PropertyType):
def __init__(self, label, options): def __init__(self, label, options, description=""):
PropertyType.__init__(self) PropertyType.__init__(self)
self.label = label self.label = label
self.options = options self.options = options
self.description = description
class Number(PropertyType):
def __init__(self, label, configurable=False, default_value=0, unit=""): class Number(PropertyType):
PropertyType.__init__(self) def __init__(self, label, configurable=False, default_value=None, unit="", description=""):
self.label = label PropertyType.__init__(self)
self.configurable = configurable self.label = label
self.configurable = configurable
class Text(PropertyType): self.default_value = default_value
def __init__(self, label, configurable=False, default_value=""): self.description = description
PropertyType.__init__(self) self.unit = unit
self.label = label
self.configurable = configurable class Text(PropertyType):
def __init__(self, label, configurable=False, required=False, default_value="", description=""):
class StepProperty(Property): PropertyType.__init__(self)
class Actor(PropertyType): self.label = label
def __init__(self, label): self.required = required
PropertyType.__init__(self) self.configurable = configurable
self.label = label self.default_value = default_value
self.configurable = True self.description = description
class Sensor(PropertyType):
def __init__(self, label): class Actor(PropertyType):
PropertyType.__init__(self) def __init__(self, label, description=""):
self.label = label PropertyType.__init__(self)
self.configurable = True self.label = label
class Kettle(PropertyType): self.configurable = True
def __init__(self, label): self.description = description
PropertyType.__init__(self)
self.label = label class Sensor(PropertyType):
self.configurable = True def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class Kettle(PropertyType):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.unit = ""
self.configurable = True
self.description = description
-132
View File
@@ -1,132 +0,0 @@
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)
View File
+135
View File
@@ -0,0 +1,135 @@
from modules.core.db import DBModel, get_db
from flask import json
class Kettle(DBModel):
__fields__ = ["name","sensor", "heater", "automatic", "logic", "config", "agitator", "target_temp"]
__table_name__ = "kettle"
__json_fields__ = ["config"]
class Sensor(DBModel):
__fields__ = ["name","type", "config", "hide"]
__table_name__ = "sensor"
__json_fields__ = ["config"]
class Config(DBModel):
__fields__ = ["type", "value", "description", "options"]
__table_name__ = "config"
__json_fields__ = ["options"]
__priamry_key__ = "name"
class Actor(DBModel):
__fields__ = ["name","type", "config", "hide"]
__table_name__ = "actor"
__json_fields__ = ["config"]
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, order=True):
cur = get_db().cursor()
cur.execute("SELECT * FROM %s WHERE state = ? ORDER BY %s.'order'" % (cls.__table_name__,cls.__table_name__,), state)
r = cur.fetchone()
if r is not None:
return cls(r)
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()
@classmethod
def sort(cls, new_order):
cur = get_db().cursor()
for e in new_order:
cur.execute("UPDATE %s SET '%s' = ? WHERE id = ?" % (cls.__table_name__, "order"), (e[1], e[0]))
get_db().commit()
class 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()
@@ -0,0 +1,19 @@
from flask import Blueprint
from modules import cbpi
from flask_swagger import swagger
from flask import json
from flask import Blueprint
@cbpi.addon.core.initializer(order=22)
def web(cbpi):
s = Blueprint('web_view', __name__, template_folder='templates', static_folder='static')
@s.route('/', methods=["GET"])
def index():
return s.send_static_file("index.html")
cbpi.addon.core.add_menu_link("JQuery View", "/web_view")
cbpi.web.register_blueprint(s, url_prefix='/web_view')
@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CraftBeerPi WebView</title>
</head>
<body>
<div id="root" >
<div id="kettle">
</div>
</div>
<!--script src="static/bundle.js" type="text/javascript"></script-->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
$(document).ready(function() {
$.ajax({
url: "/api/system/dump",
dataType: "json"
}).then(function(data) {
$.each(data.actors, function (i, val) {
$( "#kettle" ).append("<div>"+ val.name +"</div>")
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,3 @@
{
"presets" : ["es2015", "stage-0", "react"]
}
@@ -0,0 +1,19 @@
from flask import Blueprint
from modules import cbpi
from flask_swagger import swagger
from flask import json
from flask import Blueprint
@cbpi.addon.core.initializer(order=22)
def web(cbpi):
s = Blueprint('webviewreact', __name__, template_folder='templates', static_folder='static')
@s.route('/', methods=["GET"])
def index():
return s.send_static_file("index.html")
cbpi.addon.core.add_menu_link("ReactJS View", "/webviewreact")
cbpi.web.register_blueprint(s, url_prefix='/webviewreact')
@@ -0,0 +1,44 @@
{
"name": "react-app2",
"version": "0.1.0",
"private": true,
"devDependencies": {
"react-scripts": "0.9.5"
},
"dependencies": {
"axios": "^0.16.1",
"babel-core": "^6.18.2",
"babel-loader": "^6.2.8",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.22.0",
"classnames": "^2.2.5",
"highcharts-boost": "^0.1.2",
"highcharts-exporting": "^0.1.2",
"highcharts-more": "^0.1.2",
"immutability-helper": "^2.1.2",
"rc-slider": "^7.0.8",
"react": "^15.5.4",
"react-bootstrap": "^0.30.10",
"react-bs-notifier": "^4.3.2",
"react-dom": "^15.5.4",
"react-fileupload-progress": "^0.4.0",
"react-highcharts": "^12.0.0",
"react-js-diagrams": "^3.1.2",
"react-jsonschema-form": "^0.50.1",
"react-redux": "^5.0.4",
"react-router-dom": "^4.1.1",
"react-sortable-hoc": "^0.6.3",
"redux": "^3.6.0",
"redux-logger": "^3.0.1",
"redux-thunk": "^2.2.0",
"socket.io-client": "^1.7.3",
"webpack": "^1.13.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CraftBeerPi WebView</title>
</head>
<body>
<div id="root" >
</div>
<script src="static/bundle.js" type="text/javascript"></script>
</body>
</html>
View File
+23
View File
@@ -0,0 +1,23 @@
from modules import cbpi
from flask_swagger import swagger
from flask import json
from flask import Blueprint
@cbpi.addon.core.initializer(order=22)
def hello(cbpi):
s = Blueprint('react', __name__, template_folder='templates', static_folder='static')
@s.route('/', methods=["GET"])
def index():
return s.send_static_file("index.html")
@s.route('/swagger.json', methods=["GET"])
def spec():
swag = swagger(cbpi.web)
swag['info']['version'] = "3.0"
swag['info']['title'] = "CraftBeerPi"
return json.dumps(swag)
cbpi.addon.core.add_menu_link("Swagger API", "/swagger")
cbpi.web.register_blueprint(s, url_prefix='/swagger')
Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

+95
View File
@@ -0,0 +1,95 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./static/swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>
</defs>
</svg>
<div id="swagger-ui"></div>
<script src="./static/swagger-ui-bundle.js"> </script>
<script src="./static/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "http://" + window.location.hostname + ":" + window.location.port + "/swagger/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
window.ui = ui
}
</script>
</body>
</html>
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en-US">
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}
isValid = qp.state === sentState
if (oauth2.auth.schema.get("flow") === "accessCode" && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: "Authorization failed: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;;;;;AAu7LA;;;;;;AA65DA;;;;;;;;;;;;;;;;;;;;;;;;;;AA68TA;;;;;;;;;;;;;;AAs8JA;;;;;;;;;AA69pBA;;;;;AA81QA;AAm4DA;;;;;;AAo4YA;;;;;;AA0iaA;AA4lvBA","sourceRoot":""}
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui-standalone-preset.js","sources":["webpack:///swagger-ui-standalone-preset.js"],"mappings":"AAAA;;;;;AA80CA;;;;;;AAqpFA","sourceRoot":""}
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui.css","sources":[],"mappings":"","sourceRoot":""}
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AAokeA","sourceRoot":""}
Executable → Regular
+49 -93
View File
@@ -2,79 +2,29 @@ import time
from flask import request from flask import request
from flask_classy import route from flask_classy import route
from modules import DBModel, cbpi, get_db from modules import cbpi
from modules.core.baseview import BaseView from modules.core.db import get_db, DBModel
from modules.core.baseview import RestApi
from modules.database.dbmodel import Fermenter, FermenterStep
class Fermenter(DBModel): class FermenterView(RestApi):
__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 model = Fermenter
cache_key = "fermenter" cache_key = "fermenter"
def post_post_callback(self, m):
def _post_post_callback(self, m):
m.state = False m.state = False
m.steps = [] m.steps = []
def pre_put_callback(self, m): def _pre_put_callback(self, m):
m.state = False m.state = False
try: try:
m.instance.stop() m.instance.stop()
except: except:
pass pass
def post_put_callback(self, m): def _post_put_callback(self, m):
m.state = False m.state = False
@route('/<int:id>/targettemp/<temp>', methods=['POST']) @route('/<int:id>/targettemp/<temp>', methods=['POST'])
@@ -85,7 +35,7 @@ class FermenterView(BaseView):
temp = float(temp) temp = float(temp)
cbpi.cache.get(self.cache_key)[id].target_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__) self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
cbpi.emit("UPDATE_FERMENTER_TARGET_TEMP", {"id": id, "target_temp": temp}) cbpi.ws_emit("UPDATE_FERMENTER_TARGET_TEMP", {"id": id, "target_temp": temp})
return ('', 204) return ('', 204)
@route('/<int:id>/brewname', methods=['POST']) @route('/<int:id>/brewname', methods=['POST'])
@@ -94,7 +44,7 @@ class FermenterView(BaseView):
brewname = data.get("brewname") brewname = data.get("brewname")
cbpi.cache.get(self.cache_key)[id].brewname = brewname cbpi.cache.get(self.cache_key)[id].brewname = brewname
self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__) self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
cbpi.emit("UPDATE_FERMENTER_BREWNAME", {"id": id, "brewname": brewname}) cbpi.ws_emit("UPDATE_FERMENTER_BREWNAME", {"id": id, "brewname": brewname})
return ('', 204) return ('', 204)
@classmethod @classmethod
@@ -118,7 +68,7 @@ class FermenterView(BaseView):
cbpi.cache.get(self.cache_key)[id].steps.append(f) cbpi.cache.get(self.cache_key)[id].steps.append(f)
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id]) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204) return ('', 204)
@@ -140,7 +90,7 @@ class FermenterView(BaseView):
FermenterStep.update(**s.__dict__) FermenterStep.update(**s.__dict__)
break break
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id]) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204) return ('', 204)
@route('/<int:id>/step/<int:stepid>', methods=["DELETE"]) @route('/<int:id>/step/<int:stepid>', methods=["DELETE"])
@@ -151,11 +101,12 @@ class FermenterView(BaseView):
del cbpi.cache.get(self.cache_key)[id].steps[idx] del cbpi.cache.get(self.cache_key)[id].steps[idx]
FermenterStep.delete(s.id) FermenterStep.delete(s.id)
break break
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id]) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204) return ('', 204)
@route('/<int:id>/start', methods=['POST']) @route('/<int:id>/start', methods=['POST'])
def start_fermentation(self, id): def start_fermentation(self, id):
print "START"
active = None active = None
for idx, s in enumerate(cbpi.cache.get(self.cache_key)[id].steps): for idx, s in enumerate(cbpi.cache.get(self.cache_key)[id].steps):
if s.state == 'A': if s.state == 'A':
@@ -178,7 +129,8 @@ class FermenterView(BaseView):
if inactive is not None: if inactive is not None:
fermenter = self.get_fermenter(inactive.fermenter_id) fermenter = self.get_fermenter(inactive.fermenter_id)
current_temp = cbpi.get_sensor_value(int(fermenter.sensor))
current_temp = cbpi.sensor.get_value(int(fermenter.sensor))
inactive.state = 'A' inactive.state = 'A'
inactive.start = time.time() inactive.start = time.time()
@@ -189,7 +141,7 @@ class FermenterView(BaseView):
cbpi.cache["fermenter_task"][id] = inactive cbpi.cache["fermenter_task"][id] = inactive
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id]) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204) return ('', 204)
@route('/<int:id>/reset', methods=["POST"]) @route('/<int:id>/reset', methods=["POST"])
@@ -201,38 +153,40 @@ class FermenterView(BaseView):
if id in cbpi.cache["fermenter_task"]: if id in cbpi.cache["fermenter_task"]:
del cbpi.cache["fermenter_task"][id] del cbpi.cache["fermenter_task"][id]
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id]) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
return ('', 204) return ('', 204)
@route('/<int:id>/automatic', methods=['POST']) @route('/<int:id>/automatic', methods=['POST'])
def toggle(self, id): def toggle(self, id):
fermenter = cbpi.cache.get(self.cache_key)[id] fermenter = cbpi.cache.get(self.cache_key)[id]
try: try:
print fermenter.state
if fermenter.state is False: if fermenter.state is False:
# Start controller # Start controller
if fermenter.logic is not None: if fermenter.logic is not None:
cfg = fermenter.config.copy() cfg = fermenter.config.copy()
cfg.update( cfg.update(
dict(api=cbpi, fermenter_id=fermenter.id, heater=fermenter.heater, sensor=fermenter.sensor)) dict(api=cbpi, fermenter_id=fermenter.id, heater=fermenter.heater, sensor=fermenter.sensor))
instance = cbpi.get_fermentation_controller(fermenter.logic).get("class")(**cfg) instance = cbpi.fermentation.get_controller(fermenter.logic).get("class")(**cfg)
instance.init() instance.init()
fermenter.instance = instance fermenter.instance = instance
def run(instance): def run(instance):
instance.run() instance.run()
t = cbpi.socketio.start_background_task(target=run, instance=instance) t = cbpi._socketio.start_background_task(target=run, instance=instance)
fermenter.state = not fermenter.state fermenter.state = not fermenter.state
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key).get(id)) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key).get(id))
cbpi.emit("FERMENTER_CONTROLLER_STARTED", id=id)
else: else:
# Stop controller # Stop controller
fermenter.instance.stop() fermenter.instance.stop()
fermenter.state = not fermenter.state fermenter.state = not fermenter.state
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key).get(id)) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key).get(id))
cbpi.emit("FERMENTER_CONTROLLER_STOPPED", id=id)
except Exception as e: except Exception as e:
print e
cbpi.notify("Toogle Fementer Controller failed", "Pleae check the %s configuration" % fermenter.name, cbpi.notify("Toogle Fementer Controller failed", "Pleae check the %s configuration" % fermenter.name,
type="danger", timeout=None) type="danger", timeout=None)
return ('', 500) return ('', 500)
@@ -243,6 +197,7 @@ class FermenterView(BaseView):
return cbpi.cache["fermenter"].get(id) return cbpi.cache["fermenter"].get(id)
def target_temp_reached(self,id, step): def target_temp_reached(self,id, step):
print "TARGET TEMP REACHED"
timestamp = time.time() timestamp = time.time()
days = step.days * 24 * 60 * 60 days = step.days * 24 * 60 * 60
@@ -254,22 +209,24 @@ class FermenterView(BaseView):
step.timer_start = target_time step.timer_start = target_time
cbpi.emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id]) cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])
def check_step(self): def check_step(self):
for key, value in cbpi.cache["fermenter_task"].iteritems(): for key, value in cbpi.cache["fermenter_task"].iteritems():
try: try:
fermenter = self.get_fermenter(key) fermenter = self.get_fermenter(key)
current_temp = current_temp = cbpi.get_sensor_value(int(fermenter.sensor)) current_temp = current_temp = cbpi.sensor.get_value(int(fermenter.sensor))
if value.timer_start is None: if value.timer_start is None:
print "TIMER IS NONE"
if value.direction == "H" : if value.direction == "H" :
print "TIMER WATING FOR HEATING"
if current_temp >= value.temp: if current_temp >= value.temp:
self.target_temp_reached(key,value) self.target_temp_reached(key,value)
else: else:
print "TIMER WATING FOR COILING"
if current_temp <= value.temp: if current_temp <= value.temp:
self.target_temp_reached(key, value) self.target_temp_reached(key, value)
else: else:
@@ -278,38 +235,37 @@ class FermenterView(BaseView):
else: else:
pass pass
except Exception as e: except Exception as e:
self.api.looger.error(e)
pass pass
@cbpi.backgroundtask(key="read_target_temps_fermenter", interval=5) @cbpi.addon.core.backgroundtask(key="read_target_temps_fermenter", interval=5)
def read_target_temps(): def read_target_temps(cbpi):
""" """
background process that reads all passive sensors in interval of 1 second background process that reads all passive sensors in interval of 1 second
:return: None :return: None
""" """
result = {}
for key, value in cbpi.cache.get("fermenter").iteritems(): for key, value in cbpi.cache.get("fermenter").iteritems():
cbpi.save_to_file(key, value.target_temp, prefix="fermenter") cbpi.sensor.write_log(key, value.target_temp, prefix="fermenter")
instance = FermenterView() instance = FermenterView()
@cbpi.backgroundtask(key="fermentation_task", interval=1) @cbpi.addon.core.backgroundtask(key="fermentation_task", interval=1)
def execute_fermentation_step(): def execute_fermentation_step(cbpi):
with cbpi.app.app_context(): with cbpi.web.app_context():
instance.check_step() instance.check_step()
def init_active_steps(): def init_active_steps():
''' pass
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)
@cbpi.addon.core.initializer(order=1)
def init(cbpi): def init(cbpi):
print "INITIALIZE CONFIG MODULE"
FermenterView.register(cbpi.app, route_base='/api/fermenter') cbpi.cache["fermenter_task"] = {}
FermenterView.register(cbpi.web, route_base='/api/fermenter')
FermenterView.init_cache() FermenterView.init_cache()
Regular → Executable
+245 -96
View File
@@ -1,96 +1,245 @@
from flask import request from flask import request
from flask_classy import FlaskView, route from flask_classy import FlaskView, route
from modules import cbpi, socketio from modules import cbpi
from modules.core.baseview import BaseView from modules.core.baseview import RestApi
from modules.core.db import DBModel from modules.core.db import DBModel
from modules.database.dbmodel import Kettle
class Kettle(DBModel):
__fields__ = ["name","sensor", "heater", "automatic", "logic", "config", "agitator", "target_temp"] class KettleView(RestApi):
__table_name__ = "kettle" model = Kettle
__json_fields__ = ["config"] cache_key = "kettle"
@route('/', methods=["GET"])
class Kettle2View(BaseView): def getAll(self):
model = Kettle """
cache_key = "kettle" Get all Kettles
---
@classmethod tags:
def pre_post_callback(self, data): - kettle
data["target_temp"] = 0 responses:
200:
@classmethod description: List auf all Kettles
def post_init_callback(cls, obj): """
obj.state = False return super(KettleView, self).getAll()
@route('/', methods=["POST"])
def post_post_callback(self, m): def post(self):
m.state = False """
Create a new kettle
def pre_put_callback(self, m): ---
try: tags:
m.instance.stop() - kettle
except: parameters:
pass - in: body
name: body
def post_put_callback(self, m): schema:
m.state = False id: Kettle
required:
@route('/<int:id>/targettemp/<temp>', methods=['POST']) - name
def postTargetTemp(self, id, temp): properties:
id = int(id) name:
temp = float(temp) type: string
cbpi.cache.get("kettle")[id].target_temp = float(temp) description: name for user
self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__) sensor:
cbpi.emit("UPDATE_KETTLE_TARGET_TEMP", {"id": id, "target_temp": temp}) type: string
return ('', 204) description: name for user
heater:
@route('/<int:id>/automatic', methods=['POST']) type: string
def toggle(self, id): description: name for user
kettle = cbpi.cache.get("kettle")[id] automatic:
type: string
if kettle.state is False: description: name for user
# Start controller logic:
if kettle.logic is not None: type: string
cfg = kettle.config.copy() description: name for user
cfg.update(dict(api=cbpi, kettle_id=kettle.id, heater=kettle.heater, sensor=kettle.sensor)) config:
instance = cbpi.get_controller(kettle.logic).get("class")(**cfg) type: string
instance.init() description: name for user
kettle.instance = instance agitator:
def run(instance): type: string
instance.run() description: name for user
t = self.api.socketio.start_background_task(target=run, instance=instance) target_temp:
kettle.state = not kettle.state type: string
cbpi.emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id)) description: name for user
else: responses:
# Stop controller 200:
kettle.instance.stop() description: User created
kettle.state = not kettle.state """
cbpi.emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id)) return super(KettleView, self).post()
return ('', 204)
@cbpi.event("SET_TARGET_TEMP") @route('/<int:id>', methods=["PUT"])
def set_target_temp(id, temp): def put(self, id):
''' """
Change Taget Temp Event Update a kettle
:param id: kettle id ---
:param temp: target temp to set tags:
:return: None - kettle
''' parameters:
print "GOT EVENT %s %s" % (id, temp) - in: path
Kettle2View().postTargetTemp(id,temp) name: id
schema:
@cbpi.backgroundtask(key="read_target_temps", interval=5) type: integer
def read_target_temps(): required: true
""" description: Numeric ID of the Kettle
background process that reads all passive sensors in interval of 1 second - in: body
:return: None name: body
""" schema:
result = {} id: Kettle
for key, value in cbpi.cache.get("kettle").iteritems(): required:
cbpi.save_to_file(key, value.target_temp, prefix="kettle") - name
properties:
@cbpi.initalizer() name:
def init(cbpi): type: string
Kettle2View.api = cbpi description: name for user
Kettle2View.register(cbpi.app,route_base='/api/kettle') sensor:
Kettle2View.init_cache() type: string
description: name for user
heater:
type: string
description: name for user
automatic:
type: string
description: name for user
logic:
type: string
description: name for user
config:
type: string
description: name for user
agitator:
type: string
description: name for user
target_temp:
type: string
description: name for user
responses:
200:
description: User created
"""
return super(KettleView, self).put(id)
@route('/<int:id>', methods=["DELETE"])
def delete(self, id):
"""
Delete a kettle
---
tags:
- kettle
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of the Kettle
responses:
200:
description: User created
"""
return super(KettleView, self).delete(id)
@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
def _pre_put_callback(self, m):
try:
m.instance.stop()
except:
pass
def _post_put_callback(self, m):
m.state = False
@route('/<int:id>/targettemp/<temp>', methods=['POST'])
def postTargetTemp(self, id, temp):
"""
Set Target Temp
---
tags:
- kettle
parameters:
- required: true
type: string
description: ID of pet to return
in: path
name: id
- required: true
type: string
description: Temperature you like to set
in: path
name: temp
responses:
201:
description: User created
"""
id = int(id)
temp = float(temp)
cbpi.brewing.set_target_temp(id, temp)
return ('', 204)
@route('/<int:id>/automatic', methods=['POST'])
def toggle(self, id):
"""
Set Target Temp
---
tags:
- kettle
parameters:
- required: true
type: string
description: ID of pet to return
in: path
name: id
- required: true
type: string
description: Temperature you like to set
in: path
name: temp
responses:
201:
description: User created
"""
self.api.brewing.toggle_automatic(id)
return ('', 204)
@cbpi.addon.core.listen("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
'''
KettleView().postTargetTemp(id,temp)
@cbpi.addon.core.backgroundtask(key="read_target_temps", interval=5)
def read_target_temps(api):
"""
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.sensor.write_log(key, value.target_temp, prefix="kettle")
@cbpi.addon.core.initializer()
def init(cbpi):
KettleView.api = cbpi
KettleView.register(cbpi.web, route_base='/api/kettle')
KettleView.init_cache()
+73
View File
@@ -0,0 +1,73 @@
import flask_login
from flask import request
from modules import cbpi
class User(flask_login.UserMixin):
pass
@cbpi.addon.core.initializer(order=0)
def log(cbpi):
cbpi._login_manager = flask_login.LoginManager()
cbpi._login_manager.init_app(cbpi.web)
@cbpi.web.route('/login', methods=['POST'])
def login():
data = request.json
password = cbpi.get_config_parameter("password", None)
if password is None:
return ('',500)
if password == data.get("password",""):
user = User()
user.id = "craftbeerpi"
flask_login.login_user(user)
return ('',204)
else:
return ('',401)
@cbpi.web.route('/logout', methods=['POST'])
def logout():
flask_login.logout_user()
return 'Logged out'
@cbpi._login_manager.request_loader
def load_user_from_request(request):
api_key = request.args.get('api_key')
if cbpi.get_config_parameter("password_security", "NO") == "NO":
user = User()
user.id = "craftbeerpi"
return user
elif api_key == "123":
user = User()
user.id = "craftbeerpi"
return user
return None
@cbpi._login_manager.user_loader
def user_loader(user):
if cbpi.get_config_parameter("password_security", "NO") == "YES":
if user != "craftbeerpi":
return
user = User()
user.id = user
return user
else:
user = User()
user.id = user
return user
@cbpi._login_manager.unauthorized_handler
def unauthorized_handler():
return ('Please login',401)
Regular → Executable
+175 -1
View File
@@ -1 +1,175 @@
import endpoints 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):
"""
Get a list of all Log Files
---
tags:
- logs
responses:
200:
description: List of all log files
"""
result = []
for filename in os.listdir("./logs"):
if filename.endswith(".log"):
result.append(filename)
return json.dumps(result)
@route('/actions')
def actions(self):
"""
Get a list of all brewing actions
---
tags:
- logs
responses:
200:
description: List of all log files
"""
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):
"""
Delete a log file by name
---
tags:
- logs
parameters:
- in: path
name: file
schema:
type: string
required: true
description: File name
responses:
204:
description: Log deleted
"""
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", "")
return ('', 204)
else:
cbpi.notify("Failed to delete log", "", type="danger")
return ('', 404)
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
def convert_chart_data_to_json(self, chart_data):
return {"name": chart_data["name"], "data": self.read_log_as_json(chart_data["data_type"], chart_data["data_id"])}
@route('/<t>/<int:id>', methods=["POST"])
def get_logs_as_json(self, t, id):
"""
Get Log as json
---
tags:
- logs
parameters:
- in: path
name: id
schema:
type: string
required: true
description: id of the file
responses:
200:
description: Log File Data
"""
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 = map(self.convert_chart_data_to_json, cbpi.brewing.get_controller(kettle.logic).get("class").chart(kettle))
if t == "f":
fermenter = cbpi.cache.get("fermenter").get(id)
result = map(self.convert_chart_data_to_json, cbpi.fermentation.get_controller(fermenter.logic).get("class").chart(fermenter))
return json.dumps(result)
@route('/download/<file>')
@cbpi.nocache
def download(self, file):
"""
Download a log file by name
---
tags:
- logs
parameters:
- in: path
name: file
schema:
type: string
required: true
description: filename
responses:
200:
description: Log file downloaded
"""
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.addon.core.initializer()
def init(cbpi):
"""
Initializer for the message module
:param app: the flask app
:return: None
"""
LogView.register(cbpi.web, route_base='/api/logs')
-109
View File
@@ -1,109 +0,0 @@
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')
+31 -17
View File
@@ -1,30 +1,46 @@
import json import json
from flask_classy import FlaskView, route from flask_classy import FlaskView, route
from modules import cbpi from modules import cbpi
class NotificationView(FlaskView): class NotificationView(FlaskView):
@route('/', methods=['GET']) @route('/', methods=['GET'])
def getMessages(self): def getMessages(self):
""" """
Get all messages Get all Messages
:return: current messages ---
tags:
- notification
responses:
200:
description: All messages
""" """
return json.dumps(cbpi.cache["messages"]) return json.dumps(cbpi.cache["messages"])
@route('/<id>', methods=['DELETE']) @route('/<id>', methods=['DELETE'])
def dismiss(self, id): def dismiss(self, id):
""" """
Delete message from cache by id Delete Message
:param id: message id to be deleted ---
:return: empty response HTTP 204 tags:
- notification
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of the message
responses:
200:
description: Message deleted
""" """
for idx, m in enumerate(cbpi.cache.get("messages", [])): for idx, m in enumerate(cbpi.cache.get("messages", [])):
if (m.get("id") == id): if (m.get("id") == id):
cbpi.cache["messages"].pop(idx) cbpi.cache["messages"].pop(idx)
return ('', 204) return ('', 204)
@cbpi.event("MESSAGE", async=True) #@cbpi.event("MESSAGE", async=True)
def messageEvent(message, **kwargs): def messageEvent(message, **kwargs):
""" """
React on message event. add the message to the cache and push the message to the clients React on message event. add the message to the cache and push the message to the clients
@@ -32,21 +48,19 @@ def messageEvent(message, **kwargs):
:param kwargs: other parameter :param kwargs: other parameter
:return: None :return: None
""" """
if message["timeout"] is None:
cbpi.cache["messages"].append(message)
cbpi.emit("NOTIFY", message)
msg = {"id": len(cbpi.cache["messages"]), "type": "info", "message": message, "read": False} @cbpi.addon.core.initializer(order=2)
cbpi.cache["messages"].append(msg)
cbpi.emit('MESSAGE', msg,)
@cbpi.initalizer(order=2)
def init(cbpi): def init(cbpi):
""" """
Initializer for the message module Initializer for the message module
:param app: the flask app :param app: the flask app
:return: None :return: None
""" """
print "INITIALIZE MESSAGE MODULE" if cbpi.get_config_parameter("donation_notification", "YES") == "YES":
msg = {"id": len(cbpi.cache["messages"]), "type": "info", "headline": "Support CraftBeerPi with your donation", "message": "You will find the PayPay Donation button in the system menu" , "read": False}
cbpi.cache["messages"].append(msg)
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} NotificationView.register(cbpi.web, route_base='/api/notification')
cbpi.cache["messages"].append(msg)
NotificationView.register(cbpi.app, route_base='/api/notification')
+139
View File
@@ -0,0 +1,139 @@
import sys
from flask import request, send_from_directory, json
from importlib import import_module
from modules import cbpi
from git import Repo
import os
import requests
import yaml
import shutil
from flask_classy import FlaskView, route
modules = {}
class PluginView(FlaskView):
def merge(self, 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, {})
self.merge(value, node)
else:
destination[key] = value
return destination
@route('/', methods=['GET'])
def get(self):
"""
Get Plugin List
---
tags:
- plugin
responses:
200:
description: List of all plugins
"""
response = requests.get("https://raw.githubusercontent.com/Manuel83/craftbeerpi-plugins/master/plugins.yaml")
self.api.cache["plugins"] = self.merge(yaml.load(response.text), self.api.cache["plugins"])
for key, value in cbpi.cache["plugins"].iteritems():
value["installed"] = os.path.isdir("./plugins/%s/" % (key))
return json.dumps(cbpi.cache["plugins"])
@route('/<name>', methods=['DELETE'])
def delete(self,name):
"""
Delete Plugin
---
tags:
- plugin
parameters:
- in: path
name: name
schema:
type: string
required: true
description: Plugin name
responses:
200:
description: Plugin deleted
"""
if os.path.isdir("./plugins/"+name) is False:
return ('Dir not found', 500)
shutil.rmtree("./plugins/"+name)
cbpi.notify("Plugin deleted", "Plugin %s deleted successfully" % name)
return ('', 204)
@route('/<name>/download', methods=['POST'])
def download(self, name):
"""
Download Plugin
---
tags:
- plugin
parameters:
- in: path
name: name
schema:
type: string
required: true
description: Plugin name
responses:
200:
description: Plugin downloaded
"""
plugin = self.api.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))
self.api.notify("Download successful", "Plugin %s downloaded successfully" % name)
finally:
plugin["loading"] = False
return ('', 204)
@route('/<name>/update', methods=['POST'])
def update(self, name):
"""
Pull Plugin Update
---
tags:
- plugin
parameters:
- in: path
name: name
schema:
type: string
required: true
description: Plugin name
responses:
200:
description: Plugin updated
"""
repo = Repo("./modules/plugins/%s/" % (name))
o = repo.remotes.origin
info = o.pull()
self.api.notify("Plugin Updated", "Plugin %s updated successfully. Please restart the system" % name)
return ('', 204)
@cbpi.addon.core.initializer()
def init(cbpi):
cbpi.cache["plugins"] = {}
PluginView.api = cbpi
PluginView.register(cbpi.web, route_base='/api/plugin')
+146
View File
@@ -0,0 +1,146 @@
import os
import re
import time
import datetime
from flask import json, request, send_from_directory
from flask_classy import route, FlaskView
from modules.core.db import DBModel
from modules.core.baseview import RestApi
from modules import cbpi
from modules.database.dbmodel import Step
from yaml import Loader, Dumper
from yaml import load, dump
from modules.step import StepView
class RecipeBook(FlaskView):
@route('/load', methods=["POST"])
def load(self):
data = request.json
recipe_name = data.get("name")
if re.match("^[A-Za-z0-9_-]*$", recipe_name) is None:
return ('Recipie Name contains not allowed characters', 500)
with open("./recipes/%s.json" % recipe_name) as json_data:
d = json.load(json_data)
Step.delete_all()
StepView().reset()
for s in d["steps"]:
Step.insert(**{"name": s.get("name"), "type": s.get("type"), "config": s.get("config")})
self.api.ws_emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % recipe_name, message="")
return ('', 204)
@route('/download/<name>', methods=["GET"])
def download(self, name):
"""
Download a log file by name
---
tags:
- logs
parameters:
- in: path
name: file
schema:
type: string
required: true
description: filename
responses:
200:
description: Log file downloaded
"""
file = "%s.json" % name
if not self.check_filename(file):
return ('File Not Found111', 404)
return send_from_directory('../../recipes', file, as_attachment=True, attachment_filename=file)
def check_filename(self, name):
import re
print "CHECK"
pattern = re.compile('^([A-Za-z0-9-_])+.json$')
return True if pattern.match(name) else False
@route('/<name>', methods=["DELETE"])
def remove(self, name):
recipe_name = name
if re.match("^[A-Za-z0-9_-]*$", recipe_name) is None:
return ('Recipie Name contains not allowed characters', 500)
filename = "./recipes/%s.json" % recipe_name
if os.path.isfile(filename) == True:
os.remove(filename)
self.api.notify(headline="Recipe %s deleted successfully" % recipe_name, message="")
return ('', 204)
else:
self.api.notify(headline="Faild to delete Recipe %s deleted" % recipe_name, message="")
return ('', 404)
return ('', 204)
@route('/', methods=["GET"])
def get_all(self):
result = []
for filename in os.listdir("./recipes"):
if filename.endswith(".json"):
result.append({"id":filename.split(".")[0], "name": filename.split(".")[0], "change_date": self._modification_date('./recipes/%s' % filename)})
return json.dumps(result)
def _modification_date(self, filename):
t = os.path.getmtime(filename)
return datetime.datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S')
@route('/save', methods=["POST"])
def save(self):
"""
Save Recepie
---
tags:
- steps
responses:
204:
description: Recipe saved
"""
recipe_name = self.api.get_config_parameter("brew_name")
if recipe_name is None or len(recipe_name) <= 0:
self.api.notify(headline="Please set brew name", message="Recipe not saved!", type="danger")
return ('Recipie Name contains not allowed characters', 500)
if re.match("^[\sA-Za-z0-9_-]*$", recipe_name) is None:
self.api.notify(headline="Only alphanummeric charaters are allowd for recipe name", message="", type="danger")
return ('Recipie Name contains not allowed characters', 500)
recipe_data = {"name": recipe_name, "steps": Step.get_all()}
file_name = recipe_name.replace(" ", "_")
with open('./recipes/%s.json' % file_name, 'w') as outfile:
json.dump(recipe_data, outfile, indent=4)
self.api.notify(headline="Recipe %s saved successfully" % recipe_name, message="")
return ('', 204)
@cbpi.addon.core.initializer(order=2000)
def init(cbpi):
RecipeBook.api = cbpi
RecipeBook.register(cbpi.web, route_base='/api/recipebook')
+3
View File
@@ -0,0 +1,3 @@
import beerxml
import kbh
import restapi
+146
View File
@@ -0,0 +1,146 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.step import Step,StepView
import xml.etree.ElementTree
class BeerXMLImport(FlaskView):
BEER_XML_FILE = "./upload/beer.xml"
@route('/', methods=['GET'])
def get(self):
"""
Get BeerXML
---
tags:
- beerxml
responses:
200:
description: BeerXML file stored in CraftBeerPI
"""
if not os.path.exists(self.BEER_XML_FILE):
self.api.notify(headline="File Not Found", message="Please upload a Beer.xml File",
type="danger")
return ('', 404)
result = []
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
result = []
for idx, val in enumerate(e.findall('RECIPE')):
result.append({"id": idx+1, "name": val.find("NAME").text})
return json.dumps(result)
def allowed_file(self, filename):
return '.' in filename and filename.rsplit('.', 1)[1] in set(['xml'])
@route('/upload', methods=['POST'])
def upload_file(self):
"""
Upload BeerXML File
---
tags:
- beerxml
responses:
200:
description: BeerXML File Uploaded
"""
try:
if request.method == 'POST':
file = request.files['file']
if file and self.allowed_file(file.filename):
file.save(os.path.join(self.api.get_config_parameter('UPLOAD_FOLDER', "./upload"), "beer.xml"))
self.api.notify(headline="Upload Successful", message="The Beer XML file was uploaded succesfully")
return ('', 204)
return ('', 404)
except Exception as e:
self.api.logger.error(e)
self.api.notify(headline="Upload Failed", message="Failed to upload Beer xml", type="danger")
return ('', 500)
@route('/<int:id>', methods=['POST'])
def load(self, id):
"""
Load Recipe from BeerXML
---
tags:
- beerxml
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Recipe ID from BeerXML
responses:
200:
description: Recipe loaed
"""
steps = self.getSteps(id)
name = self.getRecipeName(id)
self.api.set_config_parameter("brew_name", name)
boil_time = self.getBoilTime(id)
mashstep_type = self.api.get_config_parameter("step_mash", "MashStep")
mash_kettle = self.api.get_config_parameter("step_mash_kettle", None)
boilstep_type = self.api.get_config_parameter("step_boil", "BoilStep")
boil_kettle = self.api.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if self.api.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
try:
for row in steps:
Step.insert(**{"name": row.get("name"), "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": float(row.get("temp")), "timer": row.get("timer")}})
Step.insert(**{"name": "ChilStep", "type": "ChilStep", "config": {"timer": 15}})
## Add cooking step
Step.insert(**{"name": "Boil", "type": boilstep_type, "config": {"kettle": boil_kettle, "temp": boil_temp, "timer": boil_time}})
## Add Whirlpool step
Step.insert(**{"name": "Whirlpool", "type": "ChilStep", "config": {"timer": 15}})
self.api.ws_emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", message=e.message, type="danger")
return ('', 500)
return ('', 204)
def getRecipeName(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
return e.find('./RECIPE[%s]/NAME' % (str(id))).text
def getBoilTime(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
return float(e.find('./RECIPE[%s]/BOIL_TIME' % (str(id))).text)
def getSteps(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
steps = []
for e in e.findall('./RECIPE[%s]/MASH/MASH_STEPS/MASH_STEP' % (str(id))):
if self.api.get_config_parameter("unit", "C") == "C":
temp = float(e.find("STEP_TEMP").text)
else:
temp = round(9.0 / 5.0 * float(e.find("STEP_TEMP").text) + 32, 2)
steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)})
return steps
@cbpi.addon.core.initializer()
def init(cbpi):
BeerXMLImport.api = cbpi
BeerXMLImport.register(cbpi.web, route_base='/api/beerxml')
+146
View File
@@ -0,0 +1,146 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.step import Step, StepView
class KBH(FlaskView):
@route('/', methods=['GET'])
def get(self):
"""
Get all recipes from uploaded kleinerbrauhelfer database
---
tags:
- kleinerbrauhelfer
responses:
200:
description: Recipes from kleinerbrauhelfer database
"""
conn = None
try:
if not os.path.exists(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db'):
self.api.notify(headline="File Not Found", message="Please upload a Kleiner Brauhelfer Database", type="danger")
return ('', 404)
conn = sqlite3.connect(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db')
c = conn.cursor()
c.execute('SELECT ID, Sudname, BierWurdeGebraut FROM Sud')
data = c.fetchall()
result = []
for row in data:
result.append({"id": row[0], "name": row[1], "brewed": row[2]})
return json.dumps(result)
except Exception as e:
self.api.logger.error(e)
self.api.notify(headline="Failed to load KHB database", message="ERROR", type="danger")
return ('', 500)
finally:
if conn:
conn.close()
def allowed_file(self, filename):
return '.' in filename and filename.rsplit('.', 1)[1] in set(['sqlite'])
@route('/upload', methods=['POST'])
def upload_file(self):
"""
Upload KleinerBrauhelfer Database File
---
tags:
- kleinerbrauhelfer
responses:
200:
description: File uploaed
"""
try:
if request.method == 'POST':
file = request.files['file']
if file and self.allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(self.api.app.config['UPLOAD_FOLDER'], "kbh.db"))
self.api.notify(headline="Upload Successful", message="The Kleiner Brauhelfer Database was uploaded succesfully")
return ('', 204)
return ('', 404)
except Exception as e:
self.api.notify(headline="Upload Failed", message="Failed to upload Kleiner Brauhelfer", type="danger")
return ('', 500)
@route('/<int:id>', methods=['POST'])
def load(self, id):
"""
Load Recipe from Kleinerbrauhelfer Database
---
tags:
- kleinerbrauhelfer
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of recipe
responses:
200:
description: Recipe loaded
"""
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mashinstep_type = cbpi.get_config_parameter("step_mashin", "MashInStep")
chilstep_type = cbpi.get_config_parameter("step_chil", "ChilStep")
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
conn = None
try:
conn = sqlite3.connect(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db')
c = conn.cursor()
c.execute('SELECT EinmaischenTemp, Sudname FROM Sud WHERE ID = ?', (id,))
row = c.fetchone()
name = row[1]
self.api.set_config_parameter("brew_name", name)
Step.insert(**{"name": "MashIn", "type": mashinstep_type, "config": {"kettle": mash_kettle, "temp": row[0]}})
### add rest step
for row in c.execute('SELECT * FROM Rasten WHERE SudID = ?', (id,)):
Step.insert(**{"name": row[5], "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": row[3], "timer": row[4]}})
Step.insert(**{"name": "Chil", "type": chilstep_type, "config": {"timer": 15}})
## Add cooking step
c.execute('SELECT max(Zeit) FROM Hopfengaben WHERE SudID = ?', (id,))
row = c.fetchone()
Step.insert(**{"name": "Boil", "type": boilstep_type, "config": {"kettle": boil_kettle, "temp": boil_temp, "timer": row[0]}})
## Add Whirlpool step
Step.insert(**{"name": "Whirlpool", "type": chilstep_type, "config": {"timer": 15}})
#setBrewName(name)
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", message=e.message, type="danger")
return ('', 500)
finally:
if conn:
conn.close()
return ('', 204)
@cbpi.addon.core.initializer()
def init(cbpi):
KBH.api = cbpi
KBH.register(cbpi.web, route_base='/api/kbh')
+62
View File
@@ -0,0 +1,62 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.step import Step,StepView
import xml.etree.ElementTree
class RESTImport(FlaskView):
@route('/', methods=['POST'])
def load(self):
try:
data = request.json
name = data.get("name", "No Name")
self.api.set_config_parameter("brew_name", name)
chilstep_type = cbpi.get_config_parameter("step_chil", "ChilStep")
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
for step in data.get("steps"):
if step.get("type", None) == "MASH":
Step.insert(**{"name": step.get("name","Mash Step"), "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": step.get("temp",0), "timer": step.get("timer",0)}})
elif step.get("type", None) == "CHIL":
Step.insert(**{"name": step.get("name","Chil"), "type": chilstep_type, "config": {"timer": step.get("timer")}})
elif step.get("type", None) == "BOIL":
Step.insert(**{"name": step.get("name", "Boil"), "type": boilstep_type, "config": {"kettle": boil_kettle, "timer": step.get("timer"), "temp": boil_temp}})
else:
pass
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", type="danger", message=str(e))
m = str(e.message)
return (str(e), 500)
return ('', 204)
@cbpi.addon.core.initializer()
def init(cbpi):
RESTImport.api = cbpi
RESTImport.register(cbpi.web, route_base='/api/recipe/import/v1')
+73
View File
@@ -0,0 +1,73 @@
import time
from flask_classy import route
from modules import cbpi
from modules.core.db import DBModel
from modules.core.baseview import RestApi
from modules.database.dbmodel import Sensor
from flask import request
class SensorView(RestApi):
model = Sensor
cache_key = "sensors"
@route('<int:id>/action/<method>', methods=["POST"])
def action(self, id, method):
"""
Sensor Action
---
tags:
- sensor
parameters:
- in: path
name: id
schema:
type: integer
required: true
description: Numeric ID of the sensor
- in: path
name: method
schema:
type: string
required: true
description: action method name
responses:
200:
description: Sensor Action called
"""
data = request.json
if data:
cbpi.sensor.action(id, method, **data)
else:
cbpi.sensor.action(id, method)
return ('', 204)
def _post_post_callback(self, m):
cbpi.sensor.init_one(m.id)
def _post_put_callback(self, m):
cbpi.sensor.stop_one(m.id)
cbpi.sensor.init_one(m.id)
def _pre_delete_callback(self, m):
cbpi.sensor.stop_one(m.id)
@cbpi.addon.core.initializer(order=1000)
def init(cbpi):
SensorView.register(cbpi.web, route_base='/api/sensor')
SensorView.init_cache()
#@cbpi.addon.core.backgroundtask(key="read_passiv_sensor", interval=5)
def read_passive_sensor(api):
"""
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()
-43
View File
@@ -1,43 +0,0 @@
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()
Regular → Executable
+35 -36
View File
@@ -1,36 +1,35 @@
from modules import cbpi from modules import cbpi
def getserial(): def getserial():
cpuserial = "0000000000000000" cpuserial = "0000000000000000"
try: try:
f = open('/proc/cpuinfo','r') f = open('/proc/cpuinfo','r')
for line in f: for line in f:
if line[0:6]=='Serial': if line[0:6]=='Serial':
cpuserial = line[10:26] cpuserial = line[10:26]
f.close() f.close()
except: except:
cpuserial = "0000000000000000" cpuserial = "0000000000000000"
return cpuserial return cpuserial
@cbpi.initalizer(order=9999) @cbpi.initalizer(order=9999)
def sendStats(cbpi): def sendStats(cbpi):
print "INITIALIZE STATS"
try: try:
serial = getserial() serial = getserial()
info = {
info = { "id": serial,
"id": serial, "version": "3.1",
"version": "3.0", "kettle": len(cbpi.cache.get("kettle")),
"kettle": len(cbpi.cache.get("kettle")), "hardware": len(cbpi.cache.get("actors")),
"hardware": len(cbpi.cache.get("actors")), "thermometer": "CBP3.0",
"thermometer": "CBP3.0", "hardware_control": "CBP3.0"
"hardware_control": "CBP3.0" }
}
import requests
import requests #r = requests.post('http://statistics.craftbeerpi.com', json=info)
r = requests.post('http://statistics.craftbeerpi.com', json=info)
except Exception as e:
except Exception as e: pass
pass
@@ -1,230 +1,246 @@
import time import time
from flask import json from flask import json, request
from flask_classy import route from flask_classy import route
from modules import DBModel, cbpi, get_db from modules.core.db import DBModel
from modules.core.baseview import BaseView from modules.core.baseview import RestApi
from modules import cbpi
from modules.database.dbmodel import Step
class Step(DBModel):
__fields__ = ["name","type", "stepstate", "state", "start", "end", "order", "config"]
__table_name__ = "step" class StepView(RestApi):
__json_fields__ = ["config", "stepstate"] model = Step
__order_by__ = "order" def _pre_post_callback(self, data):
__as_array__ = True order = self.model.get_max_order()
data["order"] = 1 if order is None else order + 1
@classmethod data["state"] = "I"
def get_max_order(cls):
cur = get_db().cursor() @route('/sort', methods=["POST"])
cur.execute("SELECT max(step.'order') as 'order' FROM %s" % cls.__table_name__) def sort_steps(self):
r = cur.fetchone() """
return r.get("order") Sort all steps
---
@classmethod tags:
def get_by_state(cls, state): - steps
cur = get_db().cursor() responses:
cur.execute("SELECT * FROM %s WHERE state = ?" % cls.__table_name__, state) 204:
r = cur.fetchone() description: Steps sorted. Update delivered via web socket
if r is not None: """
return cls(r) Step.sort(request.json)
else: cbpi.ws_emit("UPDATE_ALL_STEPS", self.model.get_all())
return None return ('', 204)
@classmethod @route('/', methods=["DELETE"])
def delete_all(cls): def deleteAll(self):
cur = get_db().cursor() """
cur.execute("DELETE FROM %s" % cls.__table_name__) Delete all Steps
get_db().commit() ---
tags:
@classmethod - steps
def reset_all_steps(cls): responses:
cur = get_db().cursor() 204:
cur.execute("UPDATE %s SET state = 'I', stepstate = NULL , start = NULL, end = NULL " % cls.__table_name__) description: All steps deleted
get_db().commit() """
self.model.delete_all()
@classmethod self.api.emit("ALL_BREWING_STEPS_DELETED")
def update_state(cls, id, state): self.api.set_config_parameter("brew_name", "")
cur = get_db().cursor() cbpi.ws_emit("UPDATE_ALL_STEPS", self.model.get_all())
cur.execute("UPDATE %s SET state = ? WHERE id =?" % cls.__table_name__, (state, id)) return ('', 204)
get_db().commit()
@route('/action/<method>', methods=["POST"])
@classmethod def action(self, method):
def update_step_state(cls, id, state): """
cur = get_db().cursor() Call Step Action
cur.execute("UPDATE %s SET stepstate = ? WHERE id =?" % cls.__table_name__, (json.dumps(state),id)) ---
get_db().commit() tags:
- steps
class StepView(BaseView): responses:
model = Step 204:
def pre_post_callback(self, data): description: Step action called
order = self.model.get_max_order() """
data["order"] = 1 if order is None else order + 1 self.api.emit("BREWING_STEP_ACTION_INVOKED", method=method)
data["state"] = "I" cbpi.cache["active_step"].__getattribute__(method)()
return ('', 204)
@route('/', methods=["DELETE"])
def deleteAll(self): @route('/reset', methods=["POST"])
self.model.delete_all() def reset(self):
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all())
return ('', 204) """
Reset All Steps
@route('/action/<method>', methods=["POST"]) ---
def action(self, method): tags:
- steps
cbpi.cache["active_step"].__getattribute__(method)() responses:
200:
return ('', 204) description: Steps reseted
"""
@route('/reset', methods=["POST"]) self.model.reset_all_steps()
def reset(self): self.stop_step()
self.api.emit("ALL_BREWING_STEPS_RESET")
self.model.reset_all_steps() cbpi.ws_emit("UPDATE_ALL_STEPS", self.model.get_all())
#db.session.commit() return ('', 204)
self.stop_step()
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all()) def stop_step(self):
return ('', 204) '''
stop active step
def stop_step(self): :return:
''' '''
stop active step step = cbpi.cache.get("active_step")
:return: cbpi.cache["active_step"] = None
''' self.api.emit("BREWING_STEPS_STOP")
step = cbpi.cache.get("active_step") if step is not None:
cbpi.cache["active_step"] = None step.finish()
if step is not None: @route('/reset/current', methods=['POST'])
step.finish() def resetCurrentStep(self):
"""
@route('/reset/current', methods=['POST']) Reset current Steps
def resetCurrentStep(self): ---
''' tags:
Reset current step - steps
:return: responses:
''' 200:
step = cbpi.cache.get("active_step") description: Current Steps reseted
"""
if step is not None: step = cbpi.cache.get("active_step")
step.reset()
if step.is_dirty(): if step is not None:
step.reset()
state = {} if step.is_dirty():
for field in step.managed_fields:
state[field] = step.__getattribute__(field) state = {}
Step.update_step_state(step.id, state) for field in step.managed_fields:
step.reset_dirty() state[field] = step.__getattribute__(field)
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all()) Step.update_step_state(step.id, state)
return ('', 204) step.reset_dirty()
self.api.emit("BREWING_STEPS_RESET_CURRENT")
def init_step(self, step): cbpi.ws_emit("UPDATE_ALL_STEPS", self.model.get_all())
return ('', 204)
cbpi.log_action("Start Step %s" % step.name)
type_cfg = cbpi.cache.get("step_types").get(step.type) def init_step(self, step):
if type_cfg is None:
# if type not found cbpi.brewing.log_action("Start Step %s" % step.name)
return type_cfg = cbpi.cache.get("step_types").get(step.type)
if type_cfg is None:
# copy config to stepstate # if type not found
# init step return
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))) # copy config to stepstate
instance = type_cfg.get("class")(**cfg) # init step
instance.init() 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)))
# set step instance to ache instance = type_cfg.get("class")(**cfg)
cbpi.cache["active_step"] = instance instance.init()
# set step instance to ache
@route('/next', methods=['POST']) cbpi.cache["active_step"] = instance
@route('/start', methods=['POST'])
def start(self): @route('/next', methods=['POST'])
active = Step.get_by_state("A") @route('/start', methods=['POST'])
inactive = Step.get_by_state('I') def start(self):
if (active is not None): """
active.state = 'D' Next Step
active.end = int(time.time()) ---
self.stop_step() tags:
Step.update(**active.__dict__) - steps
responses:
if (inactive is not None): 200:
self.init_step(inactive) description: Next Step
inactive.state = 'A' """
inactive.stepstate = inactive.config active = Step.get_by_state("A")
inactive.start = int(time.time()) inactive = Step.get_by_state('I')
Step.update(**inactive.__dict__)
else: if (active is not None):
cbpi.log_action("Brewing Finished") active.state = 'D'
cbpi.notify("Brewing Finished", "You are done!", timeout=None) active.end = int(time.time())
self.stop_step()
cbpi.emit("UPDATE_ALL_STEPS", Step.get_all()) Step.update(**active.__dict__)
return ('', 204) self.api.emit("BREWING_STEP_DONE")
def get_manged_fields_as_array(type_cfg): if (inactive is not None):
self.init_step(inactive)
result = [] inactive.state = 'A'
for f in type_cfg.get("properties"): inactive.stepstate = inactive.config
inactive.start = int(time.time())
result.append(f.get("name")) Step.update(**inactive.__dict__)
self.api.emit("BREWING_STEP_STARTED")
return result else:
cbpi.brewing.log_action("Brewing Finished")
@cbpi.try_catch(None) self.api.emit("BREWING_FINISHED")
def init_after_startup(): cbpi.notify("Brewing Finished", "You are done!", timeout=None)
'''
Restart after startup. Check is a step is in state A and reinitialize
:return: None cbpi.ws_emit("UPDATE_ALL_STEPS", Step.get_all())
'''
return ('', 204)
step = Step.get_by_state('A')
# We have an active step def get_manged_fields_as_array(type_cfg):
if step is not None:
result = []
# get the type for f in type_cfg.get("properties"):
result.append(f.get("name"))
type_cfg = cbpi.cache.get("step_types").get(step.type)
return result
if type_cfg is None:
# step type not found. cant restart step
return def init_after_startup():
cfg = step.stepstate.copy() '''
cfg.update(dict(api=cbpi, id=step.id, timer_end=None, managed_fields=get_manged_fields_as_array(type_cfg))) Restart after startup. Check is a step is in state A and reinitialize
instance = type_cfg.get("class")(**cfg) :return: None
instance.init() '''
cbpi.cache["active_step"] = instance step = Step.get_by_state('A')
# We have an active step
@cbpi.initalizer(order=2000) if step is not None:
def init(cbpi): # get the type
print "INITIALIZE STEPS MODULE" type_cfg = cbpi.cache.get("step_types").get(step.type)
StepView.register(cbpi.app, route_base='/api/step')
if type_cfg is None:
def get_all(): # step type not found. cant restart step
with cbpi.app.app_context(): return
return Step.get_all() cfg = step.stepstate.copy()
cfg.update(dict(api=cbpi, id=step.id, managed_fields=get_manged_fields_as_array(type_cfg)))
with cbpi.app.app_context(): instance = type_cfg.get("class")(**cfg)
init_after_startup() instance.init()
cbpi.add_cache_callback("steps", get_all) cbpi.cache["active_step"] = instance
@cbpi.backgroundtask(key="step_task", interval=0.1) @cbpi.addon.core.initializer(order=2000)
def execute_step(): def init(cbpi):
'''
Background job which executes the step StepView.register(cbpi.web, route_base='/api/step')
:return:
''' def get_all():
with cbpi.app.app_context(): with cbpi.web.app_context():
step = cbpi.cache.get("active_step") return Step.get_all()
if step is not None:
step.execute() with cbpi.web.app_context():
init_after_startup()
if step.is_dirty():
cbpi.add_cache_callback("steps", get_all)
state = {}
for field in step.managed_fields: @cbpi.addon.core.backgroundtask(key="step_task", interval=0.1)
state[field] = step.__getattribute__(field) def execute_step(api):
Step.update_step_state(step.id, state) '''
step.reset_dirty() Background job which executes the step
cbpi.emit("UPDATE_ALL_STEPS", Step.get_all()) :return:
'''
if step.n is True: with cbpi.web.app_context():
StepView().start() step = cbpi.cache.get("active_step")
cbpi.emit("UPDATE_ALL_STEPS", Step.get_all()) 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.ws_emit("UPDATE_ALL_STEPS", Step.get_all())
if step.n is True:
StepView().start()
cbpi.ws_emit("UPDATE_ALL_STEPS", Step.get_all())
+193 -1
View File
@@ -1 +1,193 @@
import endpoints import flask_login
import requests
import yaml
from flask import json, url_for, Response, request
from flask_classy import FlaskView, route
from flask_login import login_required, current_user
from git import Repo, Git
from modules import cbpi
import pprint
import time
from modules.login import User
class SystemView(FlaskView):
def doShutdown(self):
time.sleep(5)
from subprocess import call
call("halt")
@login_required
@route('/shutdown', methods=['POST'])
def shutdown(self):
"""
System Shutdown
---
tags:
- system
responses:
200:
description: Shutdown triggered
"""
self.doShutdown()
return ('', 204)
def doReboot(self):
time.sleep(5)
from subprocess import call
call("reboot")
@login_required
@route('/reboot', methods=['POST'])
def reboot(self):
"""
System Reboot
---
tags:
- system
responses:
200:
description: Reboot triggered
"""
self.doReboot()
return ('', 204)
@login_required
@route('/checkout', methods=['POST'])
def checkout_tag(self):
data = request.json
name = data.get("name")
if name is None:
return ('', 500)
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)
@login_required
@route('/git/status', methods=['GET'])
def git_status(self):
repo = Repo('./')
o = repo.remotes.origin
o.fetch()
branch = repo.active_branch
url = 'https://api.github.com/repos/manuel83/craftbeerpi3/releases'
response = requests.get(url)
result = {"current_branch": branch.name, "branches": [], "releases": []}
result["branches"].append({"name": "master"})
for branch in repo.branches:
result["branches"].append({"name": branch.name})
for r in response.json():
result["releases"].append({"name": "tags/%s" % r.get("tag_name")})
"""
Check for GIT status
---
tags:
- system
responses:
200:
description: Git Status
"""
return json.dumps(result)
@login_required
@route('/check_update', methods=['GET'])
def check_update(self):
"""
Check for GIT update
---
tags:
- system
responses:
200:
description: Git Changes
"""
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)
@login_required
@route('/git/pull', methods=['POST'])
def update(self):
"""
System Update
---
tags:
- system
responses:
200:
description: Git Pull Triggered
"""
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('/connect', methods=['GET'])
def connect(self):
"""
Connect
---
tags:
- system
responses:
200:
description: CraftBeerPi System Cache
"""
if cbpi.get_config_parameter("password_security", "NO") == "NO":
user = User()
user.id = "craftbeerpi"
flask_login.login_user(user)
if self.api.get_config_parameter("setup", "YES") == "YES":
return json.dumps(dict(setup=True, loggedin= current_user.is_authenticated ))
else:
return json.dumps(dict(setup=False, loggedin= current_user.is_authenticated))
@login_required
@route('/dump', methods=['GET'])
def dump(self):
"""
Dump Cache
---
tags:
- system
responses:
200:
description: CraftBeerPi System Cache
"""
return Response(response=json.dumps(cbpi.cache, sort_keys=True, indent=4), status=200, mimetype='application/json')
@cbpi.addon.core.initializer()
def init(cbpi):
SystemView.api = cbpi
SystemView.register(cbpi.web, route_base='/api/system')
-107
View File
@@ -1,107 +0,0 @@
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')
+32 -1
View File
@@ -1 +1,32 @@
import endpoints from flask import Blueprint,render_template
from modules import cbpi
react = Blueprint('ui', __name__, template_folder='templates', static_folder='static')
@cbpi.addon.core.initializer(order=10)
def init(cbpi):
cbpi.web.register_blueprint(react, url_prefix='/ui')
@react.route('/', methods=["GET"])
def index():
#return react.send_static_file("index.html")
js_files = []
for key, value in cbpi.cache["js"].iteritems():
js_files.append(value)
return render_template('index.html', js_files=js_files)
@cbpi.web.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
-23
View File
@@ -1,23 +0,0 @@
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")
+32
View File
@@ -0,0 +1,32 @@
.container {
padding-right: 5px;
padding-left: 5px;
margin-right: auto;
margin-left: auto;
}
.container-fluid {
}
.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;
}
+3
View File
@@ -0,0 +1,3 @@
body {
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

+7713 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -47
View File
@@ -12,59 +12,17 @@
<style> <style>
html {
.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 { background-image: url('static/bg.png');
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> </style>
<title>CraftBeerPi 3.0</title> <title>CraftBeerPi 3.0</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root" ></div>
HALLO
<script src="static/bundle.js" type="text/javascript"></script> <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> </body>
</html> </html>
+29
View File
@@ -0,0 +1,29 @@
<!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="/ui/static/bootstrap.min.css">
<link rel="stylesheet" href="/ui/static/css/font-awesome.min.css">
<link rel="stylesheet" href="/ui/static/bootstrap.dark.css">
<link href="https://fonts.googleapis.com/css?family=Dosis:200,500" rel="stylesheet">
<style>
body, h1, h2, h3, h4, h5, h6 {
font-family: 'Dosis', sans-serif;
}
</style>
<title>CraftBeerPi 3.1</title>
</head>
<body>
<h1>CraftBeerPi - Page not Found!</h1>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<!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/theme.css">
<link href="https://fonts.googleapis.com/css?family=Dosis:200,500" rel="stylesheet"/>
<title>CraftBeerPi 3.1</title>
</head>
<body>
<div id="root" ></div>
{% for file in js_files %}
<script src="{{ file }}" type="text/javascript"></script>
{% endfor %}
<script src="static/bundle.js" type="text/javascript"></script>
</body>
</html>
View File
+2 -1
View File
@@ -1,5 +1,7 @@
Flask==0.11.1 Flask==0.11.1
Flask-SocketIO==2.6.2 Flask-SocketIO==2.6.2
flask_login==0.4.0
flask_swagger==0.2.13
eventlet==0.19.0 eventlet==0.19.0
greenlet==0.4.10 greenlet==0.4.10
python-dateutil==2.5.3 python-dateutil==2.5.3
@@ -12,4 +14,3 @@ Werkzeug==0.11.10
httplib2==0.9.2 httplib2==0.9.2
flask-classy==0.6.10 flask-classy==0.6.10
GitPython==2.1.3 GitPython==2.1.3
+28 -2
View File
@@ -1,6 +1,32 @@
#!/usr/bin/env python #!/usr/bin/env python
from modules import socketio, app
socketio.run(app, host='0.0.0.0')
from modules import cbpi
from modules.core.db_migrate import *
from modules.buzzer import *
from modules.config import *
from modules.login import *
from modules.system import *
from modules.ui import *
from modules.step import *
from modules.kettle import *
from modules.actor import *
from modules.plugin import *
from modules.logs import *
from modules.notification import *
from modules.sensor import *
from modules.recipe_import import *
from modules.fermenter import *
from modules.action import *
from modules.base_plugins.actor import *
from modules.base_plugins.sensor import *
from modules.base_plugins.steps import *
from modules.example_plugins.WebViewJquery import *
from modules.example_plugins.WebViewReactJs import *
from modules.example_plugins.swagger import *
from modules.recipe_book import *
cbpi.run()
+18
View File
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS schema_info
(
id INTEGER PRIMARY KEY NOT NULL,
version INTEGER,
filename VARCHAR(80),
creation_data DEFAULT CURRENT_TIMESTAMP
);
INSERT OR IGNORE INTO config VALUES ('step_mashin', 'MashStep', 'step', 'Default MashIn Step type for import', NULL );
INSERT OR IGNORE INTO config VALUES ('step_mash', 'MashStep', 'step', 'Default Mash Step type for import', NULL);
INSERT OR IGNORE INTO config VALUES ('step_boil', 'BoilStep', 'step', 'Default Boil Step type for import', NULL);
INSERT OR IGNORE INTO config VALUES ('step_chil', 'ChilStep', 'step', 'Default Chil Step type for import', NULL);
INSERT OR IGNORE INTO config VALUES ('step_mash_kettle', 1, 'kettle', 'Default Mash Tun', NULL);
INSERT OR IGNORE INTO config VALUES ('step_boil_kettle', 1, 'kettle', 'Default Boil Tun ', NULL);
+1
View File
@@ -0,0 +1 @@
UPDATE config set `options` = '[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]' WHERE name ='buzzer';
+1
View File
@@ -0,0 +1 @@
INSERT OR IGNORE INTO config VALUES ('port', 5000, 'number', 'Port to run server (requires restart)', NULL );
+2
View File
@@ -0,0 +1,2 @@
INSERT OR IGNORE INTO config VALUES ('password', 'beer', 'text', 'LoginPassword', NULL );
INSERT OR IGNORE INTO config VALUES ('password_security', 'NO', 'select', 'Show Login Dialog', '["YES","NO"]');
View File
+173
View File
@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<RECIPES>
<RECIPE>
<NAME>Pale Ale</NAME>
<VERSION>1</VERSION>
<TYPE>All Grain</TYPE>
<BREWER></BREWER>
<DISPLAY_BATCH_SIZE>6.5 gal</DISPLAY_BATCH_SIZE>
<DISPLAY_BOIL_SIZE>7.5 gal</DISPLAY_BOIL_SIZE>
<BATCH_SIZE>24.60517657</BATCH_SIZE>
<BOIL_SIZE>28.39058835</BOIL_SIZE>
<BOIL_TIME>60</BOIL_TIME>
<EFFICIENCY>63</EFFICIENCY>
<NOTES>&#13;
</NOTES>
<PRIMARY_TEMP>20</PRIMARY_TEMP>
<EST_COLOR>8.75</EST_COLOR>
<IBU>62.22</IBU>
<IBU_METHOD>Tinseth</IBU_METHOD>
<EST_ABV>4.88</EST_ABV>
<EST_OG>1.047 sg</EST_OG>
<EST_FG>1.01 sg</EST_FG>
<OG>1.047</OG>
<FG>1.01</FG>
<PRIMING_SUGAR_NAME></PRIMING_SUGAR_NAME>
<CARBONATION_USED></CARBONATION_USED>
<BF_PRIMING_METHOD></BF_PRIMING_METHOD>
<BF_PRIMING_AMOUNT></BF_PRIMING_AMOUNT>
<BF_CO2_LEVEL></BF_CO2_LEVEL>
<BF_CO2_UNIT>Volumes</BF_CO2_UNIT>
<URL></URL>
<BATCH_SIZE_MODE>f</BATCH_SIZE_MODE>
<YEAST_STARTER>false</YEAST_STARTER>
<NO_CHILL_EXTRA_MINUTES></NO_CHILL_EXTRA_MINUTES>
<STARTING_MASH_THICKNESS>3.33880656</STARTING_MASH_THICKNESS>
<PITCH_RATE>0.35</PITCH_RATE>
<FERMENTABLES>
<FERMENTABLE>
<NAME>Pale 2-Row</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>4.08233133</AMOUNT>
<YIELD>80.43</YIELD>
<COLOR>1.8</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>American</ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Caramel / Crystal 60L</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>0.45359237</AMOUNT>
<YIELD>73.91</YIELD>
<COLOR>60</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>American</ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Rye</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>1.0205828325</AMOUNT>
<YIELD>82.61</YIELD>
<COLOR>3.5</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>American</ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Flaked Wheat</NAME>
<VERSION>1</VERSION>
<TYPE>Adjunct</TYPE>
<AMOUNT>0.226796185</AMOUNT>
<YIELD>73.91</YIELD>
<COLOR>2</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN></ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Carapils</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>0.226796185</AMOUNT>
<YIELD>76.09</YIELD>
<COLOR>1.3</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>German</ORIGIN>
</FERMENTABLE>
</FERMENTABLES>
<HOPS>
<HOP>
<NAME>El Dorado</NAME>
<VERSION>1</VERSION>
<ALPHA>15.7</ALPHA>
<AMOUNT>0.0283495231</AMOUNT>
<USE>First Wort</USE>
<USER_HOP_USE>First Wort</USER_HOP_USE>
<TIME>0</TIME>
<FORM>Leaf</FORM>
</HOP>
<HOP>
<NAME>El Dorado</NAME>
<VERSION>1</VERSION>
<ALPHA>15.7</ALPHA>
<AMOUNT>0.0283495231</AMOUNT>
<USE>Boil</USE>
<USER_HOP_USE>Boil</USER_HOP_USE>
<TIME>30</TIME>
<FORM>Leaf</FORM>
</HOP>
<HOP>
<NAME>El Dorado</NAME>
<VERSION>1</VERSION>
<ALPHA>15.7</ALPHA>
<AMOUNT>0.0283495231</AMOUNT>
<USE>Dry Hop</USE>
<USER_HOP_USE>Dry Hop</USER_HOP_USE>
<TIME>14400</TIME>
<FORM>Leaf</FORM>
</HOP>
</HOPS>
<MISCS/>
<MASH>
<NAME>Mash Steps</NAME>
<VERSION>1</VERSION>
<GRAIN_TEMP>20</GRAIN_TEMP>
<MASH_STEPS>
<MASH_STEP>
<NAME>STEP1</NAME>
<VERSION>1</VERSION>
<TYPE>Infusion</TYPE>
<STEP_TIME>60</STEP_TIME>
<INFUSE_AMOUNT>1.5141647136</INFUSE_AMOUNT>
<STEP_TEMP>65.555555555556</STEP_TEMP>
</MASH_STEP>
</MASH_STEPS>
</MASH>
<YEASTS>
<YEAST>
<NAME>California Ale Yeast WLP001</NAME>
<VERSION>1</VERSION>
<TYPE>Ale</TYPE>
<FORM>Liquid</FORM>
<AMOUNT>0.1</AMOUNT>
<PRODUCT_ID>WLP001</PRODUCT_ID>
<LABORATORY>White Labs</LABORATORY>
<ATTENUATION>76.5</ATTENUATION>
<FLOCCULATION>Medium</FLOCCULATION>
<MIN_TEMPERATURE>20</MIN_TEMPERATURE>
<MAX_TEMPERATURE>22.777777777778</MAX_TEMPERATURE>
</YEAST>
</YEASTS>
<WATERS/>
<STYLE>
<NAME>English IPA</NAME>
<VERSION>1</VERSION>
<CATEGORY>India Pale Ale (IPA)</CATEGORY>
<CATEGORY_NUMBER>14</CATEGORY_NUMBER>
<STYLE_LETTER>A</STYLE_LETTER>
<STYLE_GUIDE>BJCP</STYLE_GUIDE>
<TYPE>Ale</TYPE>
<OG_MIN>1.05</OG_MIN>
<OG_MAX>1.075</OG_MAX>
<FG_MIN>1.01</FG_MIN>
<FG_MAX>1.018</FG_MAX>
<ABV_MIN>5</ABV_MIN>
<ABV_MAX>7.5</ABV_MAX>
<IBU_MIN>40</IBU_MIN>
<IBU_MAX>60</IBU_MAX>
<COLOR_MIN>8</COLOR_MIN>
<COLOR_MAX>14</COLOR_MAX>
</STYLE>
</RECIPE>
</RECIPES>