148 Commits
Author SHA1 Message Date
Manuel83andGitHub ea5ee44295 Update install.sh 2019-08-19 06:32:56 +02:00
Manuel83andGitHub 16ad8daf44 Change Target Temp for Fermentation to 50
Change Target Temp for Fermentation to 50
2019-06-18 22:21:28 +02:00
Manuel83andGitHub 67a103d1f3 Update schema.sql 2019-05-18 10:09:31 +02:00
Manuel83andGitHub 274f282e11 Update __init__.py 2019-05-18 10:08:32 +02:00
manuel83 4e432e05bd pyyaml update 2019-02-07 21:24:31 +01:00
Manuel83 eda209422f Update package because of vulnerability
Flask:
CVE-2018-1000656 More information
moderate severity
Vulnerable versions: < 0.12.3
Patched version: 0.12.3
The Pallets Project flask version Before 0.12.3 contains a CWE-20:
Improper Input Validation vulnerability in flask that can result in
Large amount of memory usage possibly leading to denial of service.
This attack appear to be exploitable via Attacker provides JSON data in
incorrect encoding. This vulnerability appears to have been fixed in
0.12.3.

Requests:
CVE-2018-18074 More information
moderate severity
Vulnerable versions: <= 2.19.1
Patched version: 2.20.0
The Requests package through 2.19.1 before 2018-09-14 for Python sends
an HTTP Authorization header to an http URI upon receiving a
same-hostname https-to-http redirect, which makes it easier for remote
attackers to discover credentials by sniffing the network.
2018-10-29 22:15:03 +01:00
Manuel83andGitHub 5b47b94833 Merge pull request #213 from cgspeck/fix-typo
Fix typo in README
2018-09-04 06:32:32 +02:00
Manuel83andGitHub 98409d9895 Merge pull request #206 from swimIan/swimIan-patch-1
Remove "Steps started on xml imported" bug
2018-09-04 06:32:15 +02:00
Chris Speck 433b1c30f1 Fix typo which resulted in reference to incorrect port 2018-08-15 22:47:36 +10:00
Manuel83andGitHub a477cb83a8 Merge pull request #211 from cgspeck/docker-dev-env
Docker files for ease of development on PCs/macs
2018-08-15 07:00:19 +02:00
Chris Speck a22dcba77c Docker files for ease of development on PCs/macs 2018-08-13 23:13:13 +10:00
swimIanandGitHub c1d94a8f61 Remove "Steps started on xml imported" bug 2018-08-03 09:12:45 -07:00
swimIanandGitHub dd64f4ee16 Merge pull request #16 from Manuel83/master
August 2 update
2018-08-03 08:40:43 -07:00
Manuel83andGitHub 1414b1ed3b Merge pull request #166 from DrRataplan/master
Import hop alerts from BeerXML
2018-08-02 12:54:14 +02:00
Manuel83andGitHub acd404622b Merge pull request #182 from archonic/patch-1
Remove duplicate line
2018-05-23 10:19:17 +02:00
Manuel83andGitHub d657077570 Merge pull request #181 from clearwaterbrewer/patch-1
Fix hysteresis turning off heater
2018-05-23 10:18:41 +02:00
JoshandGitHub 0e6da89cfd Remove duplicate line 2018-04-05 08:53:19 -07:00
clearwaterbrewerandGitHub faa2b9d32c Update __init__.py
removed lines 37 and 38 to make hysteresis work properly.
2018-04-05 10:49:35 -04:00
Martin 83699f1933 Import boil time alerts from BeerXML
By reading the HOP and MISC elements, the 'please add your hop' alerts may be imported.
2018-01-07 11:45:15 +00:00
swimIanandGitHub fea3e85053 Merge pull request #15 from Manuel83/master
Update swimIan master
2017-11-15 09:31:46 -08:00
Manuel83andGitHub 5efa1df350 Merge pull request #133 from JamFfm/JamFfm-install
Added a combinaton of stop-del logfiles-start
2017-11-08 19:45:08 +01:00
Manuel83andGitHub dd5b5f456a Merge pull request #151 from mb-tec/master
Buzzer Beep Level
2017-11-08 19:44:32 +01:00
Matthias e0e3aff929 Buzzer Beep Level 2017-10-28 22:08:30 +02:00
Manuel83andGitHub 2a02979e37 Merge pull request #142 from Manuel83/revert-105-patch2
Revert "Updates the boilstep and beerxml recipe import"
2017-10-15 14:48:45 +02:00
Manuel83andGitHub a1740074ac Revert "Updates the boilstep and beerxml recipe import" 2017-10-15 14:48:24 +02:00
swimIanandGitHub 1b3042a082 Merge pull request #13 from Manuel83/master
Update
2017-10-14 07:55:06 -07:00
Manuel83andGitHub b279dcf347 Merge pull request #105 from swimIan/patch2
Updates the boilstep and beerxml recipe import
2017-10-13 21:56:17 +02:00
JamFfmandGitHub 211edc1bbc Added a combinaton of stop-del logfiles-start
This one makes it easier to test plugins.
2017-10-01 14:11:52 +02:00
swimIanandGitHub b56c10a8a5 Merge branch 'master' into patch2 2017-09-29 13:43:51 -07:00
swimIan 8d2c5c916a Updated again 2017-09-29 13:40:55 -07:00
swimIan d637c08eba update to reflect changes in the master branch 2017-09-29 13:12:03 -07:00
swimIanandGitHub 0447621895 Merge pull request #10 from Manuel83/master
Most recent
2017-09-28 21:44:17 -07:00
Manuel83andGitHub 03395647ed Merge pull request #130 from bertah/master
GPIOPWM: Fixed misleading label and support decimal frequencies
2017-09-25 13:22:46 +02:00
bertah 65833b1249 GPIOPWM: Fixed misleading label "duty cycle" to "frequency", and handle decimal frequency values. Tested working with Rpi.GPIO 2017-09-23 10:03:48 -04:00
Manuel83andGitHub 25ace8024c Merge pull request #127 from mrillies/patch-1
Add reset to post put call (Fermenter update)
2017-09-22 13:55:10 +02:00
Manuel83 820061cf09 - Two new hop steps added. Now you can set 5 hop alerts 2017-09-22 13:52:00 +02:00
mrilliesandGitHub c1c5faa1f3 Add reset to post put call (Fermenter update)
Added a call to reset the fermenter after an update.

Reset builds step parameter with FerementerStep objects from database.
Reset all clears current ferment steps - this may be positive or negative
2017-09-22 21:41:19 +10:00
Manuel83andGitHub 837b906af1 Merge pull request #126 from mrillies/patch-1
Fix Fermentor Heater power
2017-09-20 23:41:43 +02:00
mrilliesandGitHub 0f42153591 Update controller.py
Fix for issue #125: Missing 'power' in call to actor_on had heater id number set as power
2017-09-21 02:14:56 +10:00
swimIan 5fc5684e56 update boilstep and reorder steps in beerxml recipe import 2017-09-09 10:51:37 -07:00
swimIanandGitHub dc908268dd Merge pull request #9 from swimIan/master
update 9/6/17
2017-09-06 06:10:43 -07:00
swimIanandGitHub 14e4e6d61a Merge pull request #8 from Manuel83/master
Update 9/6/17
2017-09-06 06:08:22 -07: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
swimIan ae7e3a2e7b Adjust check_hop_timer function to alert the user to add the hops at the expected values in the boil process (ex. 60= bittering, 20=aroma, etc), instead of the reverse (ex. 0=bittering, 40=aroma, etc). 2017-08-08 23:36:18 -07:00
swimIan 3eee952a04 Add mashinstep, import mashin temp(infuse_temp) from beerxml, and put boil step ahead of whirlpool and chill steps. 2017-08-08 22:48:23 -07:00
swimIanandGitHub 88b360a8d7 Merge pull request #7 from Manuel83/master
- Add On - Link to Plugin Website
2017-07-29 07:13:20 -07: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
swimIanandGitHub 3761713219 Merge pull request #6 from swimIan/patch2
Merge pull request #5 from swimIan/master
2017-07-22 07:55:19 -07:00
swimIanandGitHub a96c678dd9 Merge pull request #5 from swimIan/master
Update
2017-07-22 07:53:16 -07:00
swimIanandGitHub e7765a2c4c Merge pull request #4 from Manuel83/master
Update
2017-07-22 07:51:27 -07: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
swimIanandGitHub 74b56937e7 Merge pull request #3 from Manuel83/master
Update
2017-07-18 21:43:34 -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
50 changed files with 1116 additions and 325 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
+14
View File
@@ -0,0 +1,14 @@
# Dockerfile for development on a pc/mac
FROM python:2
EXPOSE 5000
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "run.py"]
+21
View File
@@ -18,10 +18,31 @@ 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.
## Docker-based development
For developing this application or its plugins on a PC/Mac you can use the docker-compose file:
``` shell
$ docker-compose up
...
Starting craftbeerpi3_app_1 ... done
Attaching to craftbeerpi3_app_1
app_1 | [2018-08-13 12:54:44,264] ERROR in __init__: BUZZER not working
app_1 | (1) wsgi starting up on http://0.0.0.0:5000
```
The contents of this folder will be mounted to `/usr/src/craftbeerpi3` and the server will be accesible on `localhost:5000`.
## Donation ## Donation
CraftBeerPi is a free & open source project. If you like to support the project I happy about a donation: CraftBeerPi is a free & open source project. If you like to support the project I happy about a donation:
+18 -14
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,15 +79,19 @@ 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,21,22,23]');
INSERT INTO config VALUES ('setup', 'YES', 'select', 'Show the Setup dialog', '["YES","NO"]'); INSERT OR IGNORE INTO config VALUES ('buzzer_beep_level', 'HIGH', 'select', 'Buzzer Logic Beep Level', '["HIGH", "LOW"]');
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),
+1
View File
@@ -0,0 +1 @@
3.0.2
+9
View File
@@ -0,0 +1,9 @@
# docker-compose file for development on a pc/mac
version: '3.3'
services:
app:
build: .
ports:
- 5000:5000
volumes:
- $PWD:/usr/src/app
+14 -10
View File
@@ -19,7 +19,8 @@ show_menu () {
"7" "Software Update (git pull)" \ "7" "Software Update (git pull)" \
"8" "Reset File Changes (git reset --hard)" \ "8" "Reset File Changes (git reset --hard)" \
"9" "Clear all logs" \ "9" "Clear all logs" \
"10" "Reboot Raspberry Pi" 3>&1 1>&2 2>&3) "10" "Reboot Raspberry Pi" \
"11" "Stop CraftBeerPi, Clear logs, Start CraftBeerPi" 3>&1 1>&2 2>&3)
BUTTON=$? BUTTON=$?
# Exit if user pressed cancel or escape # Exit if user pressed cancel or escape
@@ -34,14 +35,6 @@ show_menu () {
apt-get -y update; apt-get -y upgrade; apt-get -y update; apt-get -y upgrade;
fi fi
confirmAnswer "Would you like to install wiringPI? This is required to control the GPIO"
if [ $? = 0 ]; then
git clone git://git.drogon.net/wiringPi;
cd wiringPi;
./build; cd ..;
rm -rf wiringPi;
fi
apt-get -y install python-setuptools apt-get -y install python-setuptools
easy_install pip easy_install pip
apt-get -y install python-dev apt-get -y install python-dev
@@ -136,13 +129,24 @@ 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
show_menu show_menu
fi fi
;; ;;
11)
confirmAnswer "Are you sure you want to reboot CraftBeerPi and delete all log files?"
if [ $? = 0 ]; then
sudo /etc/init.d/craftbeerpiboot stop
sudo rm -rf logs/*.log
sudo /etc/init.d/craftbeerpiboot start
show_menu
else
show_menu
fi
;;
esac esac
fi fi
} }
+8 -1
View File
@@ -29,12 +29,15 @@ import modules.sensors
import modules.actor import modules.actor
import modules.notification import modules.notification
import modules.fermenter import modules.fermenter
import modules.addon from modules.addon.endpoints import initPlugins
import modules.ui import modules.ui
import modules.system import modules.system
import modules.buzzer import modules.buzzer
import modules.stats import modules.stats
import modules.kettle import modules.kettle
import modules.recipe_import
import modules.core.db_mirgrate
from app_config import cbpi from app_config import cbpi
# Build the database: # Build the database:
# This will create the database file using SQLAlchemy # This will create the database file using SQLAlchemy
@@ -57,9 +60,13 @@ def init_db():
pass pass
init_db() init_db()
initPlugins()
cbpi.run_init() cbpi.run_init()
cbpi.run_background_processes() cbpi.run_background_processes()
app.logger.info("##########################################")
app.logger.info("### STARTUP COMPLETE")
app.logger.info("##########################################")
+9 -4
View File
@@ -17,10 +17,10 @@ class ActorView(BaseView):
obj.state = 0 obj.state = 0
obj.power = 100 obj.power = 100
def post_post_callback(self, m): def _post_post_callback(self, m):
self.api.init_actor(m.id) self.api.init_actor(m.id)
def post_put_callback(self, m): def _post_put_callback(self, m):
self.api.init_actor(m.id) self.api.init_actor(m.id)
@@ -60,10 +60,15 @@ class ActorView(BaseView):
t = self.api.socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t) t = self.api.socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t)
return ('', 204) return ('', 204)
@route('<int:id>/action/<method>', methods=["POST"])
def action(self, id, method):
cbpi.cache.get("actors").get(id).instance.__getattribute__(method)()
return ('', 204)
@cbpi.initalizer(order=1000) @cbpi.initalizer(order=1000)
def init(cbpi): def init(cbpi):
print "INITIALIZE ACTOR MODULE"
cbpi.app.logger.info("INITIALIZE ACTOR MODULE")
ActorView.register(cbpi.app, route_base='/api/actor') ActorView.register(cbpi.app, route_base='/api/actor')
ActorView.init_cache() ActorView.init_cache()
cbpi.init_actors() cbpi.init_actors()
+7 -6
View File
@@ -166,7 +166,9 @@ def update_addon(name):
def loadCorePlugins(): def loadCorePlugins():
for filename in os.listdir("./modules/base_plugins"): for filename in os.listdir("./modules/base_plugins"):
if filename.endswith(".py") or filename.endswith(".pyc"):
if os.path.isdir("./modules/base_plugins/"+filename) is False:
continue continue
try: try:
modules[filename] = import_module("modules.base_plugins.%s" % (filename)) modules[filename] = import_module("modules.base_plugins.%s" % (filename))
@@ -178,7 +180,7 @@ def loadCorePlugins():
def loadPlugins(): def loadPlugins():
for filename in os.listdir("./modules/plugins"): for filename in os.listdir("./modules/plugins"):
if filename.endswith(".py") or filename.endswith(".pyc"): if os.path.isdir("./modules/plugins/" + filename) is False:
continue continue
try: try:
modules[filename] = import_module("modules.plugins.%s" % (filename)) modules[filename] = import_module("modules.plugins.%s" % (filename))
@@ -186,13 +188,12 @@ def loadPlugins():
cbpi.notify("Failed to load plugin %s " % filename, str(e), type="danger", timeout=None) cbpi.notify("Failed to load plugin %s " % filename, str(e), type="danger", timeout=None)
cbpi.app.logger.error(e) cbpi.app.logger.error(e)
@cbpi.initalizer(order=1) #@cbpi.initalizer(order=1)
def initPlugins(app): def initPlugins():
print "INITIALIZE CUSTOM PLUGINS"
loadCorePlugins() loadCorePlugins()
loadPlugins() loadPlugins()
@cbpi.initalizer(order=2) @cbpi.initalizer(order=2)
def init(cbpi): def init(cbpi):
print "INITIALIZE ADDON MODULE"
cbpi.app.register_blueprint(blueprint, url_prefix='/api/editor') cbpi.app.register_blueprint(blueprint, url_prefix='/api/editor')
+4 -2
View File
@@ -15,7 +15,9 @@ from modules.core.db import DBModel
app = Flask(__name__) app = Flask(__name__)
logging.basicConfig(filename='./logs/app.log',level=logging.INFO) FORMAT = '%(asctime)-15s - %(levelname)s - %(message)s'
logging.basicConfig(filename='./logs/app.log',level=logging.INFO, format=FORMAT)
app.config['SECRET_KEY'] = 'craftbeerpi' app.config['SECRET_KEY'] = 'craftbeerpi'
app.config['UPLOAD_FOLDER'] = './upload' app.config['UPLOAD_FOLDER'] = './upload'
@@ -45,7 +47,7 @@ class ComplexEncoder(json.JSONEncoder):
return None return None
app.json_encoder = ComplexEncoder app.json_encoder = ComplexEncoder
socketio = SocketIO(app, json=json) socketio = SocketIO(app, json=json, logging=False)
cbpi = CraftBeerPi(app, socketio) cbpi = CraftBeerPi(app, socketio)
app.logger.info("##########################################") app.logger.info("##########################################")
+141 -7
View File
@@ -7,15 +7,16 @@ from modules.core.step import StepBase
from modules import cbpi from modules import cbpi
@cbpi.step @cbpi.step
class MashStep(StepBase): class MashStep(StepBase):
''' '''
Just put the decorator @cbpi.step on top of a method Just put the decorator @cbpi.step on top of a method
''' '''
# Properties # Properties
temp = Property.Number("Temperature", configurable=True) temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step")
kettle = StepProperty.Kettle("Kettle") kettle = StepProperty.Kettle("Kettle", description="Kettle in which the mashing takes place")
timer = Property.Number("Timer in Minutes", configurable=True) timer = Property.Number("Timer in Minutes", configurable=True, description="Timer is started when the target temperature is reached")
def init(self): def init(self):
''' '''
@@ -49,13 +50,14 @@ class MashStep(StepBase):
''' '''
# Check if Target Temp is reached # Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= int(self.temp): if self.get_kettle_temp(self.kettle) >= float(self.temp):
# Check if Timer is Running # Check if Timer is Running
if self.is_timer_finished() is None: if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60) self.start_timer(int(self.timer) * 60)
# Check if timer finished and go to next step # Check if timer finished and go to next step
if self.is_timer_finished() == True: if self.is_timer_finished() == True:
self.notify("Mash Step Completed!", "Starting the next step", timeout=None)
self.next() self.next()
@@ -65,10 +67,14 @@ class MashInStep(StepBase):
Just put the decorator @cbpi.step on top of a method Just put the decorator @cbpi.step on top of a method
''' '''
# Properties # Properties
temp = Property.Number("Temperature", configurable=True) temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step")
kettle = StepProperty.Kettle("Kettle") kettle = StepProperty.Kettle("Kettle", description="Kettle in which the mashing takes place")
s = False s = False
@cbpi.action("Change Power")
def change_power(self):
self.actor_power(1, 50)
def init(self): def init(self):
''' '''
Initialize Step. This method is called once at the beginning of the step Initialize Step. This method is called once at the beginning of the step
@@ -87,9 +93,137 @@ class MashInStep(StepBase):
''' '''
# Check if Target Temp is reached # Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= int(self.temp) and self.s is False: if self.get_kettle_temp(self.kettle) >= float(self.temp) and self.s is False:
self.s = True self.s = True
self.notify("Step Temp Reached!", "Please press the next button to continue", timeout=None) self.notify("Step Temp Reached!", "Please press the next button to continue", timeout=None)
@cbpi.step
class ChilStep(StepBase):
timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")
@cbpi.action("Stat Timer")
def start(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
def reset(self):
self.stop_timer()
def finish(self):
pass
def execute(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
if self.is_timer_finished() == True:
self.next()
@cbpi.step
class PumpStep(StepBase):
pump = StepProperty.Actor("Pump", description="Pump actor gets toogled")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")
@cbpi.action("Stat Timer")
def start(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
def reset(self):
self.stop_timer()
def finish(self):
self.actor_off(int(self.pump))
def init(self):
self.actor_on(int(self.pump))
def execute(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
if self.is_timer_finished() == True:
self.next()
@cbpi.step
class BoilStep(StepBase):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True, default_value=100, description="Target temperature for boiling")
kettle = StepProperty.Kettle("Kettle", description="Kettle in which the boiling step takes place")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=90, description="Timer is started when target temperature is reached")
hop_1 = Property.Number("Hop 1 Addition", configurable=True, description="Fist Hop alert")
hop_1_added = Property.Number("",default_value=None)
hop_2 = Property.Number("Hop 2 Addition", configurable=True, description="Second Hop alert")
hop_2_added = Property.Number("", default_value=None)
hop_3 = Property.Number("Hop 3 Addition", configurable=True)
hop_3_added = Property.Number("", default_value=None, description="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.action("Start Timer Now")
def start(self):
'''
Custom Action which can be execute form the brewing dashboard.
All method with decorator @cbpi.action("YOUR CUSTOM NAME") will be available in the user interface
:return:
'''
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
def reset(self):
self.stop_timer()
self.set_target_temp(self.temp, self.kettle)
def finish(self):
self.set_target_temp(0, self.kettle)
def check_hop_timer(self, number, value):
if self.__getattribute__("hop_%s_added" % number) is not True and time.time() > (
self.timer_end - (int(self.timer) * 60 - int(value) * 60)):
self.__setattr__("hop_%s_added" % number, True)
self.notify("Hop Alert", "Please add Hop %s" % number, timeout=None)
def execute(self):
'''
This method is execute in an interval
:return:
'''
# Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= 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()
+24 -7
View File
@@ -10,21 +10,38 @@ from modules.core.props import Property
@cbpi.sensor @cbpi.sensor
class DummyTempSensor(SensorActive): class DummyTempSensor(SensorActive):
temp = Property.Number("Temperature", configurable=True, default_value=5)
temp = Property.Number("Temperature", configurable=True, default_value=5, description="Dummy Temperature as decimal value")
@cbpi.action("My Custom Action")
def my_action(self):
print "HELLO WORLD"
pass
def get_unit(self): def get_unit(self):
'''
:return: Unit of the sensor as string. Should not be longer than 3 characters
'''
return "°C" if self.get_config_parameter("unit", "C") == "C" else "°F" return "°C" if self.get_config_parameter("unit", "C") == "C" else "°F"
def stop(self): def stop(self):
pass SensorActive.stop(self)
def execute(self): def execute(self):
while self.is_running(): '''
Active sensor has to handle his own loop
:return:
'''
while self.is_running() is True:
self.data_received(self.temp) self.data_received(self.temp)
socketio.sleep(5) self.sleep(5)
@classmethod
def init_global(cls):
'''
Called one at the startup for all sensors
:return:
'''
@@ -6,10 +6,10 @@ from modules.core.props import Property
@cbpi.fermentation_controller @cbpi.fermentation_controller
class Hysteresis(FermenterController): class Hysteresis(FermenterController):
heater_offset_min = Property.Number("Heater Offset min", True, 0) heater_offset_min = Property.Number("Heater Offset ON", True, 0, description="Offset as decimal number when the heater is switched on. Should be greater then 'Heater Offset OFF'. For example a value of 2 switches on the heater if the current temperature is 2 degrees below the target temperature")
heater_offset_max = Property.Number("Heater Offset max", True, 0) heater_offset_max = Property.Number("Heater Offset OFF", True, 0, description="Offset as decimal number when the heater is switched off. Should be smaller then 'Heater Offset ON'. For example a value of 1 switches off the heater if the current temperature is 1 degree below the target temperature")
cooler_offset_min = Property.Number("Cooler Offset min", True, 0) cooler_offset_min = Property.Number("Cooler Offset ON", True, 0, description="Offset as decimal number when the cooler is switched on. Should be greater then 'Cooler Offset OFF'. For example a value of 2 switches on the cooler if the current temperature is 2 degrees above the target temperature")
cooler_offset_max = Property.Number("Cooler Offset max", True, 0) cooler_offset_max = Property.Number("Cooler Offset OFF", True, 0, description="Offset as decimal number when the cooler is switched off. Should be less then 'Cooler Offset ON'. For example a value of 1 switches off the cooler if the current temperature is 1 degree above the target temperature")
def stop(self): def stop(self):
super(FermenterController, self).stop() super(FermenterController, self).stop()
@@ -23,16 +23,16 @@ class Hysteresis(FermenterController):
target_temp = self.get_target_temp() target_temp = self.get_target_temp()
temp = self.get_temp() temp = self.get_temp()
if temp + int(self.heater_offset_min) < target_temp: if temp + float(self.heater_offset_min) <= target_temp:
self.heater_on(100) self.heater_on(100)
if temp + int(self.heater_offset_max) > target_temp: if temp + float(self.heater_offset_max) >= target_temp:
self.heater_off() self.heater_off()
if temp > target_temp + int(self.cooler_offset_min): if temp >= target_temp + float(self.cooler_offset_min):
self.cooler_on(100) self.cooler_on(100)
if temp < target_temp + int(self.cooler_offset_max): if temp <= target_temp + float(self.cooler_offset_max):
self.cooler_off() self.cooler_off()
self.sleep(1) self.sleep(1)
+22 -8
View File
@@ -18,7 +18,7 @@ except Exception as e:
@cbpi.actor @cbpi.actor
class GPIOSimple(ActorBase): class GPIOSimple(ActorBase):
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]) gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27], description="GPIO to which the actor is connected")
def init(self): def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT) GPIO.setup(int(self.gpio), GPIO.OUT)
@@ -35,11 +35,11 @@ class GPIOSimple(ActorBase):
@cbpi.actor @cbpi.actor
class GPIOPWM(ActorBase): class GPIOPWM(ActorBase):
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]) gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27], description="GPIO to which the actor is connected")
duty_cylce = Property.Number("Duty Cycle", configurable=True) frequency = Property.Number("Frequency (Hz)", configurable=True)
p = None p = None
power = 100 power = 100 # duty cycle
def init(self): def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT) GPIO.setup(int(self.gpio), GPIO.OUT)
@@ -50,13 +50,18 @@ class GPIOPWM(ActorBase):
if power is not None: if power is not None:
self.power = int(power) self.power = int(power)
if self.duty_cylce is None: if self.frequency is None:
duty_cylce = 50 self.frequency = 0.5 # 2 sec
self.p = GPIO.PWM(int(self.gpio), int(self.duty_cylce)) self.p = GPIO.PWM(int(self.gpio), float(self.frequency))
self.p.start(int(self.power)) self.p.start(int(self.power))
def set_power(self, power): def set_power(self, power):
'''
Optional: Set the power of your actor
:param power: int value between 0 - 100
:return:
'''
if power is not None: if power is not None:
self.power = int(power) self.power = int(power)
self.p.ChangeDutyCycle(self.power) self.p.ChangeDutyCycle(self.power)
@@ -69,7 +74,7 @@ class GPIOPWM(ActorBase):
@cbpi.actor @cbpi.actor
class RelayBoard(ActorBase): class RelayBoard(ActorBase):
gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]) gpio = Property.Select("GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27], description="GPIO to which the actor is connected")
def init(self): def init(self):
GPIO.setup(int(self.gpio), GPIO.OUT) GPIO.setup(int(self.gpio), GPIO.OUT)
@@ -86,8 +91,17 @@ class RelayBoard(ActorBase):
@cbpi.actor @cbpi.actor
class Dummy(ActorBase): class Dummy(ActorBase):
def on(self, power=100): def on(self, power=100):
'''
Code to switch on the actor
:param power: int value between 0 - 100
:return:
'''
print "ON" print "ON"
def off(self): def off(self):
print "OFF" print "OFF"
+5 -6
View File
@@ -8,8 +8,8 @@ class Hysteresis(KettleController):
# Custom Properties # Custom Properties
on = Property.Number("Offset On", True, 0) on = Property.Number("Offset On", True, 0, description="Offset below target temp when heater should switched on. Should be bigger then Offset Off")
off = Property.Number("Offset Off", True, 0) off = Property.Number("Offset Off", True, 0, description="Offset below target temp when heater should switched off. Should be smaller then Offset Off")
def stop(self): def stop(self):
''' '''
@@ -29,11 +29,10 @@ class Hysteresis(KettleController):
:return: :return:
''' '''
while self.is_running(): while self.is_running():
if self.get_temp() < self.get_target_temp() - int(self.on):
if self.get_temp() < self.get_target_temp() - float(self.on):
self.heater_on(100) self.heater_on(100)
elif self.get_temp() >= self.get_target_temp() - int(self.off): elif self.get_temp() >= self.get_target_temp() - float(self.off):
self.heater_off()
else:
self.heater_off() self.heater_off()
self.sleep(1) self.sleep(1)
+10 -4
View File
@@ -18,6 +18,7 @@ def getSensors():
arr = [] arr = []
for dirname in os.listdir('/sys/bus/w1/devices'): for dirname in os.listdir('/sys/bus/w1/devices'):
if (dirname.startswith("28") or dirname.startswith("10")): if (dirname.startswith("28") or dirname.startswith("10")):
cbpi.app.logger.info("Device %s Found (Family: 28/10, Thermometer on GPIO4 (w1))" % dirname)
arr.append(dirname) arr.append(dirname)
return arr return arr
except: except:
@@ -66,7 +67,8 @@ class myThread (threading.Thread):
@cbpi.sensor @cbpi.sensor
class ONE_WIRE_SENSOR(SensorPassive): class ONE_WIRE_SENSOR(SensorPassive):
sensor_name = Property.Select("Sensor", getSensors()) sensor_name = Property.Select("Sensor", getSensors(), description="The OneWire sensor address.")
offset = Property.Number("Offset", True, 0, description="Offset which is added to the received sensor data. Positive and negative values are both allowed.")
def init(self): def init(self):
@@ -86,9 +88,13 @@ class ONE_WIRE_SENSOR(SensorPassive):
def read(self): def read(self):
if self.get_config_parameter("unit", "C") == "C": if self.get_config_parameter("unit", "C") == "C":
self.data_received(self.t.value) self.data_received(round(self.t.value + self.offset_value(), 2))
else: else:
self.data_received(format(9.0 / 5.0 * self.t.value + 32, '.2f')) self.data_received(round(9.0 / 5.0 * self.t.value + 32 + self.offset_value(), 2))
@cbpi.try_catch(0)
def offset_value(self):
return float(self.offset)
@classmethod @classmethod
def init_global(self): def init_global(self):
@@ -108,5 +114,5 @@ def set_temp(t):
@cbpi.initalizer() @cbpi.initalizer()
def init(cbpi): def init(cbpi):
cbpi.app.logger.info("INITIALIZE ONE WIRE MODULE")
cbpi.app.register_blueprint(blueprint, url_prefix='/api/one_wire') cbpi.app.register_blueprint(blueprint, url_prefix='/api/one_wire')
+17 -11
View File
@@ -9,16 +9,18 @@ except Exception as e:
class Buzzer(object): class Buzzer(object):
sound = ["H", 0.1, "L"] sound = ["H", 0.1, "L", 0.1, "H", 0.1, "L", 0.1, "H", 0.1, "L"]
def __init__(self, gpio): def __init__(self, gpio, beep_level):
try: try:
cbpi.app.logger.info("INIT BUZZER NOW GPIO%s" % gpio) cbpi.app.logger.info("INIT BUZZER NOW GPIO%s" % gpio)
self.gpio = gpio self.gpio = int(gpio)
self.beep_level = beep_level
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BCM)
GPIO.setup(gpio, GPIO.OUT) GPIO.setup(self.gpio, GPIO.OUT)
self.state = True self.state = True
cbpi.app.logger.info("BUZZER SETUP OK") cbpi.app.logger.info("BUZZER SETUP OK")
except Exception as e: except Exception as e:
cbpi.app.logger.info("BUZZER EXCEPTION %s" % str(e))
self.state = False self.state = False
def beep(self): def beep(self):
@@ -30,10 +32,14 @@ class Buzzer(object):
try: try:
for i in sound: for i in sound:
if (isinstance(i, str)): if (isinstance(i, str)):
if i == "H": if i == "H" and self.beep_level == "HIGH":
GPIO.output(int(self.gpio), GPIO.HIGH) GPIO.output(int(self.gpio), GPIO.HIGH)
else: elif i == "H" and self.beep_level != "HIGH":
GPIO.output(int(self.gpio), GPIO.LOW) GPIO.output(int(self.gpio), GPIO.LOW)
elif i == "L" and self.beep_level == "HIGH":
GPIO.output(int(self.gpio), GPIO.LOW)
else:
GPIO.output(int(self.gpio), GPIO.HIGH)
else: else:
time.sleep(i) time.sleep(i)
except Exception as e: except Exception as e:
@@ -41,11 +47,11 @@ class Buzzer(object):
start_new_thread(play, (self.sound,)) start_new_thread(play, (self.sound,))
@cbpi.initalizer(order=0) @cbpi.initalizer(order=1)
def init(cbpi): def init(cbpi):
print "INIT BUZZER" gpio = cbpi.get_config_parameter("buzzer", 22)
cbpi.app.logger.info("INIT BUZZER") beep_level = cbpi.get_config_parameter("buzzer_beep_level", "HIGH")
gpio = cbpi.get_config_parameter("buzzer", 16)
cbpi.buzzer = Buzzer(gpio) cbpi.buzzer = Buzzer(gpio, beep_level)
cbpi.beep() cbpi.beep()
cbpi.app.logger.info("INIT OK") cbpi.app.logger.info("INIT OK")
+3 -5
View File
@@ -24,10 +24,9 @@ class ConfigView(BaseView):
update_data = {"name": data["name"], "value": data["value"]} update_data = {"name": data["name"], "value": data["value"]}
if self.api.cache.get(self.cache_key) is not None: if self.api.cache.get(self.cache_key) is not None:
#self.pre_post_callback(self.api.cache.get(self.cache_key)[name])
self.api.cache.get(self.cache_key)[name].__dict__.update(**update_data) 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__) m = self.model.update(**self.api.cache.get(self.cache_key)[name].__dict__)
self.post_put_callback(self.api.cache.get(self.cache_key)[name]) self._post_put_callback(self.api.cache.get(self.cache_key)[name])
return json.dumps(self.api.cache.get(self.cache_key)[name].__dict__) return json.dumps(self.api.cache.get(self.cache_key)[name].__dict__)
@route('/<id>', methods=["GET"]) @route('/<id>', methods=["GET"])
@@ -51,9 +50,8 @@ class ConfigView(BaseView):
cls.post_init_callback(value) cls.post_init_callback(value)
cls.api.cache[cls.cache_key][value.name] = value cls.api.cache[cls.cache_key][value.name] = value
@cbpi.initalizer(order=0)
@cbpi.initalizer(order=1)
def init(cbpi): def init(cbpi):
print "INITIALIZE CONFIG MODULE"
ConfigView.register(cbpi.app, route_base='/api/config') ConfigView.register(cbpi.app, route_base='/api/config')
ConfigView.init_cache() ConfigView.init_cache()
+15 -15
View File
@@ -9,7 +9,7 @@ class BaseView(FlaskView):
cache_key = None cache_key = None
api = cbpi api = cbpi
@route('/<int:id>') @route('/<int:id>', methods=["GET"])
def getOne(self, id): def getOne(self, id):
if self.api.cache.get(self.cache_key) is not None: if self.api.cache.get(self.cache_key) is not None:
@@ -17,36 +17,36 @@ class BaseView(FlaskView):
else: else:
return json.dumps(self.model.get_one(id)) return json.dumps(self.model.get_one(id))
@route('/') @route('/', methods=["GET"])
def getAll(self): def getAll(self):
if self.api.cache.get(self.cache_key) is not None: if self.api.cache.get(self.cache_key) is not None:
return json.dumps(self.api.cache.get(self.cache_key)) return json.dumps(self.api.cache.get(self.cache_key))
else: else:
return json.dumps(self.model.get_all()) return json.dumps(self.model.get_all())
def pre_post_callback(self, data): def _pre_post_callback(self, data):
pass pass
def post_post_callback(self, m): def _post_post_callback(self, m):
pass pass
@route('/', methods=["POST"]) @route('/', methods=["POST"])
def post(self): def post(self):
data = request.json data = request.json
self.pre_post_callback(data) self._pre_post_callback(data)
m = self.model.insert(**data) m = self.model.insert(**data)
if self.api.cache.get(self.cache_key) is not None: if self.api.cache.get(self.cache_key) is not None:
self.api.cache.get(self.cache_key)[m.id] = m self.api.cache.get(self.cache_key)[m.id] = m
self.post_post_callback(m) self._post_post_callback(m)
return json.dumps(m) return json.dumps(m)
def pre_put_callback(self, m): def _pre_put_callback(self, m):
pass pass
def post_put_callback(self, m): def _post_put_callback(self, m):
pass pass
@@ -59,32 +59,32 @@ class BaseView(FlaskView):
except: except:
pass pass
if self.api.cache.get(self.cache_key) is not None: if self.api.cache.get(self.cache_key) is not None:
self.pre_put_callback(self.api.cache.get(self.cache_key)[id]) self._pre_put_callback(self.api.cache.get(self.cache_key)[id])
self.api.cache.get(self.cache_key)[id].__dict__.update(**data) self.api.cache.get(self.cache_key)[id].__dict__.update(**data)
m = self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__) m = self.model.update(**self.api.cache.get(self.cache_key)[id].__dict__)
self.post_put_callback(self.api.cache.get(self.cache_key)[id]) self._post_put_callback(self.api.cache.get(self.cache_key)[id])
return json.dumps(self.api.cache.get(self.cache_key)[id]) return json.dumps(self.api.cache.get(self.cache_key)[id])
else: else:
m = self.model.update(**data) m = self.model.update(**data)
self.post_put_callback(m) self._post_put_callback(m)
return json.dumps(m) return json.dumps(m)
def pre_delete_callback(self, m): def _pre_delete_callback(self, m):
pass pass
def post_delete_callback(self, id): def _post_delete_callback(self, id):
pass pass
@route('/<int:id>', methods=["DELETE"]) @route('/<int:id>', methods=["DELETE"])
def delete(self, id): def delete(self, id):
if self.api.cache.get(self.cache_key) is not None: if self.api.cache.get(self.cache_key) is not None:
self.pre_delete_callback(self.api.cache.get(self.cache_key)[id]) self._pre_delete_callback(self.api.cache.get(self.cache_key)[id])
del self.api.cache.get(self.cache_key)[id] del self.api.cache.get(self.cache_key)[id]
m = self.model.delete(id) m = self.model.delete(id)
def post_delete_callback(self, id): def _post_delete_callback(self, id):
pass pass
return ('',204) return ('',204)
+24 -3
View File
@@ -17,6 +17,12 @@ class ActorController(object):
self.api.switch_actor_off(int(id)) self.api.switch_actor_off(int(id))
@cbpi.try_catch(None)
def actor_power(self, power, id=None):
if id is None:
id = self.heater
self.api.actor_power(int(id), power)
class SensorController(object): class SensorController(object):
@@ -68,6 +74,14 @@ class ControllerBase(object):
class KettleController(ControllerBase, ActorController, SensorController): class KettleController(ControllerBase, ActorController, SensorController):
@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): def __init__(self, *args, **kwds):
ControllerBase.__init__(self, *args, **kwds) ControllerBase.__init__(self, *args, **kwds)
self.kettle_id = kwds.get("kettle_id") self.kettle_id = kwds.get("kettle_id")
@@ -101,6 +115,13 @@ class KettleController(ControllerBase, ActorController, SensorController):
class FermenterController(ControllerBase, ActorController, SensorController): class FermenterController(ControllerBase, ActorController, SensorController):
@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): def __init__(self, *args, **kwds):
ControllerBase.__init__(self, *args, **kwds) ControllerBase.__init__(self, *args, **kwds)
self.fermenter_id = kwds.get("fermenter_id") self.fermenter_id = kwds.get("fermenter_id")
@@ -118,7 +139,7 @@ class FermenterController(ControllerBase, ActorController, SensorController):
def heater_on(self, power=100): def heater_on(self, power=100):
f = self.api.cache.get("fermenter").get(self.fermenter_id) f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None: if f.heater is not None:
self.actor_on(int(f.heater)) self.actor_on(power, int(f.heater))
@cbpi.try_catch(None) @cbpi.try_catch(None)
def heater_off(self): def heater_off(self):
@@ -129,13 +150,13 @@ class FermenterController(ControllerBase, ActorController, SensorController):
@cbpi.try_catch(None) @cbpi.try_catch(None)
def cooler_on(self, power=100): def cooler_on(self, power=100):
f = self.api.cache.get("fermenter").get(self.fermenter_id) f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None: if f.cooler is not None:
self.actor_on(power, int(f.cooler)) self.actor_on(power, int(f.cooler))
@cbpi.try_catch(None) @cbpi.try_catch(None)
def cooler_off(self): def cooler_off(self):
f = self.api.cache.get("fermenter").get(self.fermenter_id) f = self.api.cache.get("fermenter").get(self.fermenter_id)
if f.heater is not None: if f.cooler is not None:
self.actor_off(int(f.cooler)) self.actor_off(int(f.cooler))
@cbpi.try_catch(None) @cbpi.try_catch(None)
+56 -24
View File
@@ -1,3 +1,4 @@
import inspect
import pprint import pprint
import sqlite3 import sqlite3
@@ -28,6 +29,7 @@ class ActorAPI(object):
self.app.logger.info("Init Actors") self.app.logger.info("Init Actors")
t = self.cache.get("actor_types") t = self.cache.get("actor_types")
for key, value in t.iteritems(): for key, value in t.iteritems():
value.get("class").api = self
value.get("class").init_global() value.get("class").init_global()
for key in self.cache.get("actors"): for key in self.cache.get("actors"):
@@ -38,7 +40,6 @@ class ActorAPI(object):
value = self.cache.get("actors").get(int(id)) value = self.cache.get("actors").get(int(id))
cfg = value.config.copy() cfg = value.config.copy()
cfg.update(dict(api=self, id=id, name=value.name)) cfg.update(dict(api=self, id=id, name=value.name))
cfg.update(dict(api=self, id=id, name=value.name))
clazz = self.cache.get("actor_types").get(value.type).get("class") clazz = self.cache.get("actor_types").get(value.type).get("class")
value.instance = clazz(**cfg) value.instance = clazz(**cfg)
value.instance.init() value.instance.init()
@@ -73,12 +74,8 @@ class ActorAPI(object):
if actor.state == 0: if actor.state == 0:
return return
actor.instance.off() actor.instance.off()
actor.state = 0 actor.state = 0
self.emit("SWITCH_ACTOR", actor) self.emit("SWITCH_ACTOR", actor)
class SensorAPI(object): class SensorAPI(object):
@@ -161,9 +158,8 @@ class SensorAPI(object):
def log_action(self, text): def log_action(self, text):
filename = "./logs/action.log" filename = "./logs/action.log"
formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime()) formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
with open(filename, "a") as file: with open(filename, "a") as file:
text = text.encode("utf-8")
file.write("%s,%s\n" % (formatted_time, text)) file.write("%s,%s\n" % (formatted_time, text))
def shutdown_sensor(self, id): def shutdown_sensor(self, id):
@@ -173,7 +169,7 @@ class SensorAPI(object):
def get_sensor_value(self, id): def get_sensor_value(self, id):
try: try:
id = int(id) id = int(id)
return int(self.cache.get("sensors")[id].instance.last_value) return float(self.cache.get("sensors")[id].instance.last_value)
except Exception as e: except Exception as e:
return None return None
@@ -226,12 +222,9 @@ class CraftBeerPi(ActorAPI, SensorAPI):
def notify(self, headline, message, type="success", timeout=5000): def notify(self, headline, message, type="success", timeout=5000):
self.beep() self.beep()
msg = {"id": str(uuid.uuid1()), "type": type, "headline": headline, "message": message, "timeout": timeout} msg = {"id": str(uuid.uuid1()), "type": type, "headline": headline, "message": message, "timeout": timeout}
if timeout is None: self.emit_message(msg)
self.cache["messages"].append(msg)
self.emit("NOTIFY", msg)
def beep(self): def beep(self):
if self.buzzer is not None: if self.buzzer is not None:
self.buzzer.beep() self.buzzer.beep()
@@ -248,6 +241,15 @@ class CraftBeerPi(ActorAPI, SensorAPI):
else: else:
return cfg.value return cfg.value
def set_config_parameter(self, name, value):
from modules.config import Config
with self.app.app_context():
update_data = {"name": name, "value": value}
self.cache.get("config")[name].__dict__.update(**update_data)
c = Config.update(**update_data)
self.emit("UPDATE_CONFIG", c)
def add_config_parameter(self, name, value, type, description, options=None): def add_config_parameter(self, name, value, type, description, options=None):
from modules.config import Config from modules.config import Config
with self.app.app_context(): with self.app.app_context():
@@ -264,28 +266,56 @@ class CraftBeerPi(ActorAPI, SensorAPI):
# helper method for parsing props # helper method for parsing props
def __parseProps(self, key, cls): def __parseProps(self, key, cls):
name = cls.__name__ name = cls.__name__
self.cache[key][name] = {"name": name, "class": cls, "properties": []} self.cache[key][name] = {"name": name, "class": cls, "properties": [], "actions": []}
tmpObj = cls() tmpObj = cls()
members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")] members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")]
for m in members: for m in members:
if isinstance(tmpObj.__getattribute__(m), Property.Number): if isinstance(tmpObj.__getattribute__(m), Property.Number):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append( self.cache[key][name]["properties"].append(
{"name": m, "label": t.label, "type": "number", "configurable": t.configurable}) {"name": m, "label": t.label, "type": "number", "configurable": t.configurable, "description": t.description, "default_value": t.default_value})
elif isinstance(tmpObj.__getattribute__(m), Property.Text): elif isinstance(tmpObj.__getattribute__(m), Property.Text):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append( self.cache[key][name]["properties"].append(
{"name": m, "label": t.label, "type": "text", "configurable": t.configurable}) {"name": m, "label": t.label, "type": "text", "configurable": t.configurable, "default_value": t.default_value, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Select): elif isinstance(tmpObj.__getattribute__(m), Property.Select):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append( self.cache[key][name]["properties"].append(
{"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options}) {"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Actor):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Sensor):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), Property.Kettle):
t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable, "description": t.description})
for name, method in cls.__dict__.iteritems():
if hasattr(method, "action"):
label = method.__getattribute__("label")
self.cache[key][cls.__name__]["actions"].append({"method": name, "label": label})
return cls return cls
def actor(self, cls): def actor(self, cls):
return self.__parseProps("actor_types", cls) return self.__parseProps("actor_types", cls)
def actor2(self, description="", power=True, **options):
def decorator(f):
print f()
print f
print options
print description
return f
return decorator
def sensor(self, cls): def sensor(self, cls):
return self.__parseProps("sensor_types", cls) return self.__parseProps("sensor_types", cls)
@@ -300,6 +330,8 @@ class CraftBeerPi(ActorAPI, SensorAPI):
def get_fermentation_controller(self, name): def get_fermentation_controller(self, name):
return self.cache["fermentation_controller_types"].get(name) return self.cache["fermentation_controller_types"].get(name)
# Step action # Step action
def action(self,label): def action(self,label):
def real_decorator(func): def real_decorator(func):
@@ -320,22 +352,22 @@ class CraftBeerPi(ActorAPI, SensorAPI):
for m in members: for m in members:
if isinstance(tmpObj.__getattribute__(m), StepProperty.Number): if isinstance(tmpObj.__getattribute__(m), StepProperty.Number):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable}) self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "number", "configurable": t.configurable, "default_value": t.default_value, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Text): elif isinstance(tmpObj.__getattribute__(m), StepProperty.Text):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "text", "configurable": t.configurable}) self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "text", "configurable": t.configurable, "default_value": t.default_value, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Select): elif isinstance(tmpObj.__getattribute__(m), StepProperty.Select):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "select", "options": t.options}) self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "select", "configurable": True, "options": t.options, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Actor): elif isinstance(tmpObj.__getattribute__(m), StepProperty.Actor):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": t.configurable}) self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "actor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Sensor): elif isinstance(tmpObj.__getattribute__(m), StepProperty.Sensor):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": t.configurable}) self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "sensor", "configurable": t.configurable, "description": t.description})
elif isinstance(tmpObj.__getattribute__(m), StepProperty.Kettle): elif isinstance(tmpObj.__getattribute__(m), StepProperty.Kettle):
t = tmpObj.__getattribute__(m) t = tmpObj.__getattribute__(m)
self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable}) self.cache[key][name]["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable, "description": t.description})
for name, method in cls.__dict__.iteritems(): for name, method in cls.__dict__.iteritems():
if hasattr(method, "action"): if hasattr(method, "action"):
@@ -420,7 +452,7 @@ class CraftBeerPi(ActorAPI, SensorAPI):
self.app.logger.info("Invoke Init") self.app.logger.info("Invoke Init")
self.cache["init"] = sorted(self.cache["init"], key=lambda k: k['order']) self.cache["init"] = sorted(self.cache["init"], key=lambda k: k['order'])
for i in self.cache.get("init"): for i in self.cache.get("init"):
self.app.logger.info("-> %s " % i.get("function").__name__) self.app.logger.info("INITIALIZER - METHOD %s PAHT %s: " % (i.get("function").__name__, str(inspect.getmodule(i.get("function")).__file__) ))
i.get("function")(self) i.get("function")(self)
@@ -451,7 +483,7 @@ class CraftBeerPi(ActorAPI, SensorAPI):
def job(interval, method): def job(interval, method):
while True: while True:
try: try:
method() method(self)
except Exception as e: except Exception as e:
self.app.logger.error("Exception" + method.__name__ + ": " + str(e)) self.app.logger.error("Exception" + method.__name__ + ": " + str(e))
self.socketio.sleep(interval) self.socketio.sleep(interval)
+4 -2
View File
@@ -42,13 +42,15 @@ class DBModel(object):
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)) result.append( cls(r))
else: else:
result = {} result = {}
@@ -104,7 +106,7 @@ class DBModel(object):
else: else:
data = data + (kwargs.get(f),) data = data + (kwargs.get(f),)
print query, data
cur.execute(query, data) cur.execute(query, data)
get_db().commit() get_db().commit()
i = cur.lastrowid i = cur.lastrowid
+47
View File
@@ -0,0 +1,47 @@
import sqlite3
import os
from modules import cbpi
from db import get_db
def execute_file(curernt_version, data):
if curernt_version >= data["version"]:
cbpi.app.logger.info("SKIP DB FILE: %s" % data["file"])
return
try:
with sqlite3.connect("craftbeerpi.db") as conn:
with open('./update/%s' % data["file"], 'r') as f:
d = f.read()
sqlCommands = d.split(";")
cur = conn.cursor()
for s in sqlCommands:
cur.execute(s)
cur.execute("INSERT INTO schema_info (version,filename) values (?,?)", (data["version"], data["file"]))
conn.commit()
except sqlite3.OperationalError as err:
print "EXCEPT"
print err
@cbpi.initalizer(order=-9999)
def init(app=None):
with cbpi.app.app_context():
conn = get_db()
cur = conn.cursor()
current_version = None
try:
cur.execute("SELECT max(version) as m FROM schema_info")
m = cur.fetchone()
current_version = m["m"]
except:
pass
result = []
for filename in os.listdir("./update"):
if filename.endswith(".sql"):
d = {"version": int(filename[:filename.index('_')]), "file": filename}
result.append(d)
execute_file(current_version, d)
+11 -7
View File
@@ -5,16 +5,19 @@ class Base(object):
@classmethod @classmethod
def init_global(cls): def init_global(cls):
print "GLOBAL ACTOR INIT" pass
def get_config_parameter(self, key, default_value): def get_config_parameter(self, key, default_value):
return self.api.get_config_parameter(key, default_value) return self.api.get_config_parameter(key, default_value)
def sleep(self, seconds):
self.api.socketio.sleep(seconds)
def init(self): def init(self):
print "INIT BASE" pass
def stop(self): def stop(self):
print "STOP HARDWARE" pass
def update(self, **kwds): def update(self, **kwds):
pass pass
@@ -67,15 +70,16 @@ class SensorActive(SensorBase):
__running = False __running = False
def is_running(self): def is_running(self):
return self.__running return self.__running
def init(self): def init(self):
self.__running = True self.__running = True
print "INIT ACTIVE SENSOR"
def stop(self): def stop(self):
self.__running = False self.__running = False
def execute(self): def execute(self):
pass pass
@@ -95,10 +99,10 @@ class ActorBase(Base):
return 1 return 1
def set_power(self, power): def set_power(self, power):
print "SET POWER TO %s" % power pass
def on(self, power=0): def on(self, power=0):
print "ON" pass
def off(self): def off(self):
print "OFF" pass
+36 -6
View File
@@ -3,36 +3,66 @@ class PropertyType(object):
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): class Number(PropertyType):
def __init__(self, label, configurable=False, default_value=0, unit=""): def __init__(self, label, configurable=False, default_value=None, unit="", description=""):
PropertyType.__init__(self) PropertyType.__init__(self)
self.label = label self.label = label
self.configurable = configurable self.configurable = configurable
self.default_value = default_value
self.description = description
class Text(PropertyType): class Text(PropertyType):
def __init__(self, label, configurable=False, default_value=""): def __init__(self, label, configurable=False, default_value="", description=""):
PropertyType.__init__(self) PropertyType.__init__(self)
self.label = label self.label = label
self.configurable = configurable self.configurable = configurable
self.default_value = default_value
self.description = description
class Actor(PropertyType):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class Sensor(PropertyType):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class Kettle(PropertyType):
def __init__(self, label, description=""):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
class StepProperty(Property): class StepProperty(Property):
class Actor(PropertyType): class Actor(PropertyType):
def __init__(self, label): def __init__(self, label, description=""):
PropertyType.__init__(self) PropertyType.__init__(self)
self.label = label self.label = label
self.configurable = True self.configurable = True
self.description = description
class Sensor(PropertyType): class Sensor(PropertyType):
def __init__(self, label): def __init__(self, label, description=""):
PropertyType.__init__(self) PropertyType.__init__(self)
self.label = label self.label = label
self.configurable = True self.configurable = True
self.description = description
class Kettle(PropertyType): class Kettle(PropertyType):
def __init__(self, label): def __init__(self, label, description=""):
PropertyType.__init__(self) PropertyType.__init__(self)
self.label = label self.label = label
self.configurable = True self.configurable = True
self.description = description
+19 -5
View File
@@ -17,6 +17,10 @@ class ActorAPI(NotificationAPI):
def actor_off(self, id): def actor_off(self, id):
self.api.switch_actor_off(int(id)) self.api.switch_actor_off(int(id))
@cbpi.try_catch(None)
def actor_power(self, id, power):
self.api.actor_power(int(id), power)
class SensorAPI(NotificationAPI): class SensorAPI(NotificationAPI):
@cbpi.try_catch(None) @cbpi.try_catch(None)
@@ -40,7 +44,7 @@ class KettleAPI(NotificationAPI):
return self.api.cache.get("kettle").get(id).target_temp return self.api.cache.get("kettle").get(id).target_temp
def set_target_temp(self, temp, id=None): def set_target_temp(self, temp, id=None):
temp = int(temp) temp = float(temp)
try: try:
if id is None: if id is None:
@@ -55,6 +59,7 @@ class Timer(object):
timer_end = Property.Number("TIMER_END", configurable=False) timer_end = Property.Number("TIMER_END", configurable=False)
def start_timer(self, timer): def start_timer(self, timer):
if self.timer_end is not None: if self.timer_end is not None:
return return
self.timer_end = int(time.time()) + timer self.timer_end = int(time.time()) + timer
@@ -70,6 +75,12 @@ class Timer(object):
else: else:
return False 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): def is_timer_finished(self):
if self.timer_end is None: if self.timer_end is None:
return None return None
@@ -89,13 +100,13 @@ class StepBase(Timer, ActorAPI, SensorAPI, KettleAPI):
self.n = True self.n = True
def init(self): def init(self):
print "INIT STEP" pass
def finish(self): def finish(self):
print "FINSIH STEP" pass
def reset(self): def reset(self):
print "REST STEP" pass
def execute(self): def execute(self):
print "-------------" print "-------------"
@@ -105,11 +116,14 @@ class StepBase(Timer, ActorAPI, SensorAPI, KettleAPI):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
for a in kwds: for a in kwds:
super(StepBase, self).__setattr__(a, kwds.get(a)) super(StepBase, self).__setattr__(a, kwds.get(a))
self.api = kwds.get("api") self.api = kwds.get("api")
self.id = kwds.get("id") self.id = kwds.get("id")
self.name = kwds.get("name") self.name = kwds.get("name")
self.kettle_id = kwds.get("kettleid") self.kettle_id = kwds.get("kettleid")
self.value = None self.value = None
+8 -6
View File
@@ -63,19 +63,21 @@ 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
self.reset(int(m.id))
@route('/<int:id>/targettemp/<temp>', methods=['POST']) @route('/<int:id>/targettemp/<temp>', methods=['POST'])
def postTargetTemp(self, id, temp): def postTargetTemp(self, id, temp):
@@ -282,7 +284,7 @@ class FermenterView(BaseView):
@cbpi.backgroundtask(key="read_target_temps_fermenter", interval=5) @cbpi.backgroundtask(key="read_target_temps_fermenter", interval=5)
def read_target_temps(): def read_target_temps(api):
""" """
background process that reads all passive sensors in interval of 1 second background process that reads all passive sensors in interval of 1 second
:return: None :return: None
@@ -295,7 +297,7 @@ def read_target_temps():
instance = FermenterView() instance = FermenterView()
@cbpi.backgroundtask(key="fermentation_task", interval=1) @cbpi.backgroundtask(key="fermentation_task", interval=1)
def execute_fermentation_step(): def execute_fermentation_step(api):
with cbpi.app.app_context(): with cbpi.app.app_context():
instance.check_step() instance.check_step()
@@ -310,6 +312,6 @@ def init_active_steps():
@cbpi.initalizer(order=1) @cbpi.initalizer(order=1)
def init(cbpi): def init(cbpi):
print "INITIALIZE CONFIG MODULE"
FermenterView.register(cbpi.app, route_base='/api/fermenter') FermenterView.register(cbpi.app, route_base='/api/fermenter')
FermenterView.init_cache() FermenterView.init_cache()
+6 -6
View File
@@ -15,7 +15,7 @@ class Kettle2View(BaseView):
cache_key = "kettle" cache_key = "kettle"
@classmethod @classmethod
def pre_post_callback(self, data): def _pre_post_callback(self, data):
data["target_temp"] = 0 data["target_temp"] = 0
@classmethod @classmethod
@@ -23,16 +23,16 @@ class Kettle2View(BaseView):
obj.state = False obj.state = False
def post_post_callback(self, m): def _post_post_callback(self, m):
m.state = False m.state = False
def pre_put_callback(self, m): def _pre_put_callback(self, m):
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'])
@@ -76,11 +76,11 @@ def set_target_temp(id, temp):
:param temp: target temp to set :param temp: target temp to set
:return: None :return: None
''' '''
print "GOT EVENT %s %s" % (id, temp)
Kettle2View().postTargetTemp(id,temp) Kettle2View().postTargetTemp(id,temp)
@cbpi.backgroundtask(key="read_target_temps", interval=5) @cbpi.backgroundtask(key="read_target_temps", interval=5)
def read_target_temps(): def read_target_temps(api):
""" """
background process that reads all passive sensors in interval of 1 second background process that reads all passive sensors in interval of 1 second
:return: None :return: None
+5 -5
View File
@@ -65,6 +65,8 @@ class LogView(FlaskView):
pass pass
return array 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"]) @route('/<t>/<int:id>', methods=["POST"])
def get_logs_as_json(self, t, id): def get_logs_as_json(self, t, id):
@@ -76,13 +78,12 @@ class LogView(FlaskView):
if t == "k": if t == "k":
kettle = cbpi.cache.get("kettle").get(id) kettle = cbpi.cache.get("kettle").get(id)
result.append({"name": "Temp", "data": self.read_log_as_json("sensor", kettle.sensor)}) result = map(self.convert_chart_data_to_json, cbpi.get_controller(kettle.logic).get("class").chart(kettle))
result.append({"name": "Target Temp", "data": self.read_log_as_json("kettle", kettle.id)})
if t == "f": if t == "f":
fermenter = cbpi.cache.get("fermenter").get(id) fermenter = cbpi.cache.get("fermenter").get(id)
result.append({"name": "Temp", "data": self.read_log_as_json("sensor", fermenter.sensor)}) result = map(self.convert_chart_data_to_json, cbpi.get_fermentation_controller(fermenter.logic).get("class").chart(fermenter))
result.append({"name": "Target Temp", "data": self.read_log_as_json("fermenter", fermenter.id)})
return json.dumps(result) return json.dumps(result)
@route('/download/<file>') @route('/download/<file>')
@@ -105,5 +106,4 @@ def init(app):
:param app: the flask app :param app: the flask app
:return: None :return: None
""" """
print "INITIALIZE LOG MODULE"
LogView.register(cbpi.app, route_base='/api/logs') LogView.register(cbpi.app, route_base='/api/logs')
+4 -6
View File
@@ -32,10 +32,9 @@ def messageEvent(message, **kwargs):
:param kwargs: other parameter :param kwargs: other parameter
:return: None :return: None
""" """
if message["timeout"] is None:
msg = {"id": len(cbpi.cache["messages"]), "type": "info", "message": message, "read": False} cbpi.cache["messages"].append(message)
cbpi.cache["messages"].append(msg) cbpi.emit("NOTIFY", message)
cbpi.emit('MESSAGE', msg,)
@cbpi.initalizer(order=2) @cbpi.initalizer(order=2)
def init(cbpi): def init(cbpi):
@@ -44,8 +43,7 @@ def init(cbpi):
: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} 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) cbpi.cache["messages"].append(msg)
+3
View File
@@ -0,0 +1,3 @@
import beerxml
import kbh
import restapi
+152
View File
@@ -0,0 +1,152 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules.app_config import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.steps import Step,StepView
import xml.etree.ElementTree
class BeerXMLImport(FlaskView):
BEER_XML_FILE = "./upload/beer.xml"
@route('/', methods=['GET'])
def get(self):
if not os.path.exists(self.BEER_XML_FILE):
self.api.notify(headline="File Not Found", message="Please upload a Beer.xml File",
type="danger")
return ('', 404)
result = []
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
result = []
for idx, val in enumerate(e.findall('RECIPE')):
result.append({"id": idx+1, "name": val.find("NAME").text})
return json.dumps(result)
def allowed_file(self, filename):
return '.' in filename and filename.rsplit('.', 1)[1] in set(['xml'])
@route('/upload', methods=['POST'])
def upload_file(self):
try:
if request.method == 'POST':
file = request.files['file']
if file and self.allowed_file(file.filename):
file.save(os.path.join(self.api.app.config['UPLOAD_FOLDER'], "beer.xml"))
self.api.notify(headline="Upload Successful", message="The Beer XML file was uploaded succesfully")
return ('', 204)
return ('', 404)
except Exception as e:
self.api.notify(headline="Upload Failed", message="Failed to upload Beer xml", type="danger")
return ('', 500)
@route('/<int:id>', methods=['POST'])
def load(self, id):
steps = self.getSteps(id)
boil_time_alerts = self.getBoilAlerts(id)
name = self.getRecipeName(id)
self.api.set_config_parameter("brew_name", name)
boil_time = self.getBoilTime(id)
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
try:
for row in steps:
Step.insert(**{"name": row.get("name"), "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": float(row.get("temp")), "timer": row.get("timer")}})
Step.insert(**{"name": "ChilStep", "type": "ChilStep", "config": {"timer": 15}})
## Add boiling step
Step.insert(**{
"name": "Boil",
"type": boilstep_type,
"config": {
"kettle": boil_kettle,
"temp": boil_temp,
"timer": boil_time,
## Beer XML defines additions as the total time spent in boiling,
## CBP defines it as time-until-alert
## Also, The model supports five boil-time additions.
## Set the rest to None to signal them being absent
"hop_1": boil_time - boil_time_alerts[0] if len(boil_time_alerts) >= 1 else None,
"hop_2": boil_time - boil_time_alerts[1] if len(boil_time_alerts) >= 2 else None,
"hop_3": boil_time - boil_time_alerts[2] if len(boil_time_alerts) >= 3 else None,
"hop_4": boil_time - boil_time_alerts[3] if len(boil_time_alerts) >= 4 else None,
"hop_5": boil_time - boil_time_alerts[4] if len(boil_time_alerts) >= 5 else None
}
})
## Add Whirlpool step
Step.insert(**{"name": "Whirlpool", "type": "ChilStep", "config": {"timer": 15}})
StepView().reset()
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", message=e.message, type="danger")
return ('', 500)
return ('', 204)
def getRecipeName(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
return e.find('./RECIPE[%s]/NAME' % (str(id))).text
def getBoilTime(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
return float(e.find('./RECIPE[%s]/BOIL_TIME' % (str(id))).text)
def getBoilAlerts(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
recipe = e.find('./RECIPE[%s]' % (str(id)))
alerts = []
for e in recipe.findall('./HOPS/HOP'):
use = e.find('USE').text
## Hops which are not used in the boil step should not cause alerts
if use != 'Aroma' and use != 'Boil':
continue
alerts.append(float(e.find('TIME').text))
## There might also be miscelaneous additions during boild time
for e in recipe.findall('MISCS/MISC[USE="Boil"]'):
alerts.append(float(e.find('TIME').text))
## Dedupe and order the additions by their time, to prevent multiple alerts at the same time
alerts = sorted(list(set(alerts)))
## CBP should have these additions in reverse
alerts.reverse()
return alerts
def getSteps(self, id):
e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
steps = []
for e in e.findall('./RECIPE[%s]/MASH/MASH_STEPS/MASH_STEP' % (str(id))):
if self.api.get_config_parameter("unit", "C") == "C":
temp = float(e.find("STEP_TEMP").text)
else:
temp = round(9.0 / 5.0 * float(e.find("STEP_TEMP").text) + 32, 2)
steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)})
return steps
@cbpi.initalizer()
def init(cbpi):
BeerXMLImport.api = cbpi
BeerXMLImport.register(cbpi.app, route_base='/api/beerxml')
+109
View File
@@ -0,0 +1,109 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules.app_config import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.steps import Step, StepView
class KBH(FlaskView):
@route('/', methods=['GET'])
def get(self):
conn = None
try:
if not os.path.exists(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db'):
self.api.notify(headline="File Not Found", message="Please upload a Kleiner Brauhelfer Database", type="danger")
return ('', 404)
conn = sqlite3.connect(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db')
c = conn.cursor()
c.execute('SELECT ID, Sudname, BierWurdeGebraut FROM Sud')
data = c.fetchall()
result = []
for row in data:
result.append({"id": row[0], "name": row[1], "brewed": row[2]})
return json.dumps(result)
except Exception as e:
print e
self.api.notify(headline="Failed to load KHB database", message="ERROR", type="danger")
return ('', 500)
finally:
if conn:
conn.close()
def allowed_file(self, filename):
return '.' in filename and filename.rsplit('.', 1)[1] in set(['sqlite'])
@route('/upload', methods=['POST'])
def upload_file(self):
try:
if request.method == 'POST':
file = request.files['file']
if file and self.allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(self.api.app.config['UPLOAD_FOLDER'], "kbh.db"))
self.api.notify(headline="Upload Successful", message="The Kleiner Brauhelfer Database was uploaded succesfully")
return ('', 204)
return ('', 404)
except Exception as e:
self.api.notify(headline="Upload Failed", message="Failed to upload Kleiner Brauhelfer", type="danger")
return ('', 500)
@route('/<int:id>', methods=['POST'])
def load(self, id):
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mashinstep_type = cbpi.get_config_parameter("step_mashin", "MashInStep")
chilstep_type = cbpi.get_config_parameter("step_chil", "ChilStep")
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
conn = None
try:
conn = sqlite3.connect(self.api.app.config['UPLOAD_FOLDER'] + '/kbh.db')
c = conn.cursor()
c.execute('SELECT EinmaischenTemp, Sudname FROM Sud WHERE ID = ?', (id,))
row = c.fetchone()
name = row[1]
self.api.set_config_parameter("brew_name", name)
Step.insert(**{"name": "MashIn", "type": mashinstep_type, "config": {"kettle": mash_kettle, "temp": row[0]}})
### add rest step
for row in c.execute('SELECT * FROM Rasten WHERE SudID = ?', (id,)):
Step.insert(**{"name": row[5], "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": row[3], "timer": row[4]}})
Step.insert(**{"name": "Chil", "type": chilstep_type, "config": {"timer": 15}})
## Add cooking step
c.execute('SELECT max(Zeit) FROM Hopfengaben WHERE SudID = ?', (id,))
row = c.fetchone()
Step.insert(**{"name": "Boil", "type": boilstep_type, "config": {"kettle": boil_kettle, "temp": boil_temp, "timer": row[0]}})
## Add Whirlpool step
Step.insert(**{"name": "Whirlpool", "type": chilstep_type, "config": {"timer": 15}})
#setBrewName(name)
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", message=e.message, type="danger")
return ('', 500)
finally:
if conn:
conn.close()
return ('', 204)
@cbpi.initalizer()
def init(cbpi):
KBH.api = cbpi
KBH.register(cbpi.app, route_base='/api/kbh')
+62
View File
@@ -0,0 +1,62 @@
from flask import json, request
from flask_classy import FlaskView, route
from git import Repo, Git
import sqlite3
from modules.app_config import cbpi
from werkzeug.utils import secure_filename
import pprint
import time
import os
from modules.steps import Step,StepView
import xml.etree.ElementTree
class RESTImport(FlaskView):
@route('/', methods=['POST'])
def load(self):
try:
data = request.json
name = data.get("name", "No Name")
self.api.set_config_parameter("brew_name", name)
chilstep_type = cbpi.get_config_parameter("step_chil", "ChilStep")
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
# READ KBH DATABASE
Step.delete_all()
StepView().reset()
for step in data.get("steps"):
if step.get("type", None) == "MASH":
Step.insert(**{"name": step.get("name","Mash Step"), "type": mashstep_type, "config": {"kettle": mash_kettle, "temp": step.get("temp",0), "timer": step.get("timer",0)}})
elif step.get("type", None) == "CHIL":
Step.insert(**{"name": step.get("name","Chil"), "type": chilstep_type, "config": {"timer": step.get("timer")}})
elif step.get("type", None) == "BOIL":
Step.insert(**{"name": step.get("name", "Boil"), "type": boilstep_type, "config": {"kettle": boil_kettle, "timer": step.get("timer"), "temp": boil_temp}})
else:
pass
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", type="danger", message=str(e))
m = str(e.message)
return (str(e), 500)
return ('', 204)
@cbpi.initalizer()
def init(cbpi):
RESTImport.api = cbpi
RESTImport.register(cbpi.app, route_base='/api/recipe/import/v1')
+10 -5
View File
@@ -12,27 +12,32 @@ class SensorView(BaseView):
model = Sensor model = Sensor
cache_key = "sensors" cache_key = "sensors"
@route('<int:id>/action/<method>', methods=["POST"])
def action(self, id, method):
def post_post_callback(self, m): cbpi.cache.get("sensors").get(id).instance.__getattribute__(method)()
return ('', 204)
def _post_post_callback(self, m):
cbpi.init_sensor(m.id) cbpi.init_sensor(m.id)
def post_put_callback(self, m): def _post_put_callback(self, m):
cbpi.stop_sensor(m.id) cbpi.stop_sensor(m.id)
cbpi.init_sensor(m.id) cbpi.init_sensor(m.id)
def pre_delete_callback(self, m): def _pre_delete_callback(self, m):
cbpi.stop_sensor(m.id) cbpi.stop_sensor(m.id)
@cbpi.initalizer(order=1000) @cbpi.initalizer(order=1000)
def init(cbpi): def init(cbpi):
print "INITIALIZE SENSOR MODULE"
SensorView.register(cbpi.app, route_base='/api/sensor') SensorView.register(cbpi.app, route_base='/api/sensor')
SensorView.init_cache() SensorView.init_cache()
cbpi.init_sensors() cbpi.init_sensors()
@cbpi.backgroundtask(key="read_passiv_sensor", interval=5) @cbpi.backgroundtask(key="read_passiv_sensor", interval=5)
def read_passive_sensor(): def read_passive_sensor(api):
""" """
background process that reads all passive sensors in interval of 1 second background process that reads all passive sensors in interval of 1 second
:return: None :return: None
+1 -1
View File
@@ -16,7 +16,7 @@ def getserial():
@cbpi.initalizer(order=9999) @cbpi.initalizer(order=9999)
def sendStats(cbpi): def sendStats(cbpi):
print "INITIALIZE STATS"
try: try:
serial = getserial() serial = getserial()
+24 -15
View File
@@ -1,5 +1,5 @@
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 import DBModel, cbpi, get_db
@@ -21,9 +21,9 @@ class Step(DBModel):
return r.get("order") return r.get("order")
@classmethod @classmethod
def get_by_state(cls, state): def get_by_state(cls, state, order=True):
cur = get_db().cursor() cur = get_db().cursor()
cur.execute("SELECT * FROM %s WHERE state = ?" % cls.__table_name__, state) cur.execute("SELECT * FROM %s WHERE state = ? ORDER BY %s.'order'" % (cls.__table_name__,cls.__table_name__,), state)
r = cur.fetchone() r = cur.fetchone()
if r is not None: if r is not None:
return cls(r) return cls(r)
@@ -54,13 +54,29 @@ class Step(DBModel):
cur.execute("UPDATE %s SET stepstate = ? WHERE id =?" % cls.__table_name__, (json.dumps(state),id)) cur.execute("UPDATE %s SET stepstate = ? WHERE id =?" % cls.__table_name__, (json.dumps(state),id))
get_db().commit() get_db().commit()
@classmethod
def sort(cls, new_order):
cur = get_db().cursor()
for e in new_order:
cur.execute("UPDATE %s SET '%s' = ? WHERE id = ?" % (cls.__table_name__, "order"), (e[1], e[0]))
get_db().commit()
class StepView(BaseView): class StepView(BaseView):
model = Step model = Step
def pre_post_callback(self, data): def _pre_post_callback(self, data):
order = self.model.get_max_order() order = self.model.get_max_order()
data["order"] = 1 if order is None else order + 1 data["order"] = 1 if order is None else order + 1
data["state"] = "I" data["state"] = "I"
@route('/sort', methods=["POST"])
def sort_steps(self):
Step.sort(request.json)
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all())
return ('', 204)
@route('/', methods=["DELETE"]) @route('/', methods=["DELETE"])
def deleteAll(self): def deleteAll(self):
self.model.delete_all() self.model.delete_all()
@@ -69,16 +85,12 @@ class StepView(BaseView):
@route('/action/<method>', methods=["POST"]) @route('/action/<method>', methods=["POST"])
def action(self, method): def action(self, method):
cbpi.cache["active_step"].__getattribute__(method)() cbpi.cache["active_step"].__getattribute__(method)()
return ('', 204) return ('', 204)
@route('/reset', methods=["POST"]) @route('/reset', methods=["POST"])
def reset(self): def reset(self):
self.model.reset_all_steps() self.model.reset_all_steps()
#db.session.commit()
self.stop_step() self.stop_step()
cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all()) cbpi.emit("UPDATE_ALL_STEPS", self.model.get_all())
return ('', 204) return ('', 204)
@@ -115,7 +127,6 @@ class StepView(BaseView):
return ('', 204) return ('', 204)
def init_step(self, step): def init_step(self, step):
cbpi.log_action("Start Step %s" % step.name) cbpi.log_action("Start Step %s" % step.name)
type_cfg = cbpi.cache.get("step_types").get(step.type) type_cfg = cbpi.cache.get("step_types").get(step.type)
if type_cfg is None: if type_cfg is None:
@@ -128,7 +139,6 @@ class StepView(BaseView):
cfg.update(dict(name=step.name, api=cbpi, id=step.id, timer_end=None, managed_fields=get_manged_fields_as_array(type_cfg))) cfg.update(dict(name=step.name, api=cbpi, id=step.id, timer_end=None, managed_fields=get_manged_fields_as_array(type_cfg)))
instance = type_cfg.get("class")(**cfg) instance = type_cfg.get("class")(**cfg)
instance.init() instance.init()
# set step instance to ache # set step instance to ache
cbpi.cache["active_step"] = instance cbpi.cache["active_step"] = instance
@@ -185,15 +195,16 @@ def init_after_startup():
if type_cfg is None: if type_cfg is None:
# step type not found. cant restart step # step type not found. cant restart step
return return
cfg = step.stepstate.copy() cfg = step.stepstate.copy()
cfg.update(dict(api=cbpi, id=step.id, timer_end=None, managed_fields=get_manged_fields_as_array(type_cfg))) cfg.update(dict(api=cbpi, id=step.id, managed_fields=get_manged_fields_as_array(type_cfg)))
instance = type_cfg.get("class")(**cfg) instance = type_cfg.get("class")(**cfg)
instance.init() instance.init()
cbpi.cache["active_step"] = instance cbpi.cache["active_step"] = instance
@cbpi.initalizer(order=2000) @cbpi.initalizer(order=2000)
def init(cbpi): def init(cbpi):
print "INITIALIZE STEPS MODULE"
StepView.register(cbpi.app, route_base='/api/step') StepView.register(cbpi.app, route_base='/api/step')
def get_all(): def get_all():
@@ -205,7 +216,7 @@ def init(cbpi):
cbpi.add_cache_callback("steps", get_all) cbpi.add_cache_callback("steps", get_all)
@cbpi.backgroundtask(key="step_task", interval=0.1) @cbpi.backgroundtask(key="step_task", interval=0.1)
def execute_step(): def execute_step(api):
''' '''
Background job which executes the step Background job which executes the step
:return: :return:
@@ -214,9 +225,7 @@ def execute_step():
step = cbpi.cache.get("active_step") step = cbpi.cache.get("active_step")
if step is not None: if step is not None:
step.execute() step.execute()
if step.is_dirty(): if step.is_dirty():
state = {} state = {}
for field in step.managed_fields: for field in step.managed_fields:
state[field] = step.__getattribute__(field) state[field] = step.__getattribute__(field)
+40 -2
View File
@@ -1,4 +1,5 @@
from flask import json import yaml
from flask import json, url_for, Response
from flask_classy import FlaskView, route from flask_classy import FlaskView, route
from git import Repo, Git from git import Repo, Git
@@ -101,7 +102,44 @@ class SystemView(FlaskView):
def dump(self): def dump(self):
return json.dumps(cbpi.cache) return json.dumps(cbpi.cache)
@route('/endpoints', methods=['GET'])
def endpoints(self):
import urllib
output = []
vf = self.api.app.view_functions
for f in self.api.app.view_functions:
print f
endpoints = {}
re = {
"swagger": "2.0",
"host": "",
"info": {
"description":"",
"version": "",
"title": "CraftBeerPi"
},
"schemes": ["http"],
"paths": endpoints}
for rule in self.api.app.url_map.iter_rules():
r = rule
endpoints[rule.rule] = {}
if "HEAD" in r.methods: r.methods.remove("HEAD")
if "OPTIONS" in r.methods: r.methods.remove("OPTIONS")
for m in rule.methods:
endpoints[rule.rule][m] = dict(summary="", description="", consumes=["application/json"],produces=["application/json"])
with open("config/version.yaml", 'r') as stream:
y = yaml.load(stream)
pprint.pprint(y)
pprint.pprint(re)
return Response(yaml.dump(re), mimetype='text/yaml')
@cbpi.initalizer() @cbpi.initalizer()
def init(cbpi): def init(cbpi):
print "INITIALIZE SYSTEM MODULE"
SystemView.api = cbpi
SystemView.register(cbpi.app, route_base='/api/system') SystemView.register(cbpi.app, route_base='/api/system')
+5 -3
View File
@@ -1,15 +1,17 @@
from flask import Blueprint, render_template, request from flask import Blueprint
from modules import cbpi from modules import cbpi
react = Blueprint('react', __name__, template_folder='templates', static_folder='static') react = Blueprint('react', __name__, template_folder='templates', static_folder='static')
@cbpi.initalizer(order=10) @cbpi.initalizer(order=10)
def init(cbpi): def init(cbpi):
print "INITIALIZE UI"
cbpi.app.register_blueprint(react, url_prefix='/ui') cbpi.app.register_blueprint(react, url_prefix='/ui')
@react.route('/')
@react.route('/', methods=["GET"])
def index(): def index():
return react.send_static_file("index.html") return react.send_static_file("index.html")
+33
View File
@@ -0,0 +1,33 @@
.container {
padding-right: 5px;
padding-left: 5px;
margin-right: auto;
margin-left: auto;
}
.container-fluid {
padding-right: 5px;
padding-left: 5px;
}
.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;
}
+5
View File
@@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
File diff suppressed because one or more lines are too long
-49
View File
@@ -10,61 +10,12 @@
<link rel="stylesheet" href="static/bootstrap.dark.css"> <link rel="stylesheet" href="static/bootstrap.dark.css">
<style>
.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
position: relative;
min-height: 1px;
padding-right: 8px;
padding-left: 8px;
}
.panel-heading {
padding: 5px 5px;
}
.panel-footer {
padding: 2px 2px;
}
.modal-footer {
padding: 5px;
}
.row {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex !important;
flex-wrap: wrap;
}
.row > [class*='col-'] {
display: flex;
flex-direction: column;
}
</style>
<title>CraftBeerPi 3.0</title> <title>CraftBeerPi 3.0</title>
</head> </head>
<body> <body>
<div id="root" ></div> <div id="root" ></div>
<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>
+3 -4
View File
@@ -1,4 +1,4 @@
Flask==0.11.1 Flask==0.12.4
Flask-SocketIO==2.6.2 Flask-SocketIO==2.6.2
eventlet==0.19.0 eventlet==0.19.0
greenlet==0.4.10 greenlet==0.4.10
@@ -6,10 +6,9 @@ python-dateutil==2.5.3
python-engineio==0.9.2 python-engineio==0.9.2
python-mimeparse==1.5.2 python-mimeparse==1.5.2
python-socketio==1.4.4 python-socketio==1.4.4
PyYAML==3.11 PyYAML==4.2b1
requests==2.11.0 requests==2.20.0
Werkzeug==0.11.10 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
+7 -2
View File
@@ -1,6 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
from modules import socketio, app from modules import socketio, app, cbpi
socketio.run(app, host='0.0.0.0') try:
port = int(cbpi.get_config_parameter('port', '5000'))
except ValueError:
port = 5000
socketio.run(app, host='0.0.0.0', port=port)
+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 );
View File