Parcourir la source

- Login Added. Default Password is „beer“

- Several UI Adjustments - still work in progress
- New API Method to add custom JavaScript to CraftBeerPi
- Global Action Buttons added
- Register new Web Modules for complete custom UI
tags/3.1_alpha
Manuel83 il y a 8 ans
Parent
révision
813b2f0035
22 fichiers modifiés avec 350 ajouts et 135 suppressions
  1. +13
    -6
      modules/actor/__init__.py
  2. +0
    -1
      modules/base_plugins/actor.py
  3. +12
    -1
      modules/base_plugins/sensor.py
  4. +4
    -0
      modules/base_plugins/steps.py
  5. +8
    -0
      modules/core/baseapi.py
  6. +2
    -1
      modules/core/basetypes.py
  7. +7
    -1
      modules/core/baseview.py
  8. +53
    -1
      modules/core/core.py
  9. +44
    -0
      modules/core/db_migrate.py
  10. +0
    -36
      modules/core/login.py
  11. +1
    -31
      modules/kettle/__init__.py
  12. +57
    -0
      modules/login/__init__.py
  13. +1
    -1
      modules/sensor/__init__.py
  14. +25
    -0
      modules/system/__init__.py
  15. +11
    -2
      modules/ui/__init__.py
  16. +1
    -2
      modules/ui/src/App.css
  17. +1
    -3
      modules/ui/src/index.css
  18. +44
    -48
      modules/ui/static/bundle.js
  19. +29
    -0
      modules/ui/templates/404.html
  20. +33
    -0
      modules/ui/templates/index.html
  21. +3
    -1
      run.py
  22. +1
    -0
      update/4_login_password.sql

+ 13
- 6
modules/actor/__init__.py Voir le fichier

@@ -1,5 +1,7 @@
import time
from flask_classy import route
from flask_login import login_required
from modules.core.db import DBModel
from modules.core.core import cbpi
from modules.core.baseview import BaseView
@@ -21,6 +23,7 @@ class ActorView(BaseView):
def _post_put_callback(self, m):
self.api.actor.init_one(m.id)
@login_required
@route("<int:id>/switch/on", methods=["POST"])
def on(self, id):
"""
@@ -42,6 +45,7 @@ class ActorView(BaseView):
self.api.actor.on(id)
return ('', 204)
@login_required
@route("<int:id>/switch/off", methods=["POST"])
def off(self, id):
"""
@@ -63,6 +67,7 @@ class ActorView(BaseView):
self.api.actor.off(id)
return ('', 204)
@login_required
@route("<int:id>/power/<int:power>", methods=["POST"])
def power(self, id, power):
"""
@@ -90,6 +95,7 @@ class ActorView(BaseView):
self.api.actor.power(id, power)
return ('', 204)
@login_required
@route("<int:id>/toggle", methods=["POST"])
def toggle(self, id):
"""
@@ -108,10 +114,7 @@ class ActorView(BaseView):
200:
description: Actor toggled
"""
if self.api.cache.get("actors").get(id).state == 0:
self.on(id)
else:
self.off(id)
cbpi.actor.toggle(id)
return ('', 204)
def toggleTimeJob(self, id, t):
@@ -121,6 +124,7 @@ class ActorView(BaseView):
self.api.cache.get("actors").get(int(id)).timer = None
self.toggle(int(id))
@login_required
@route("/<id>/toggle/<int:t>", methods=["POST"])
def toggleTime(self, id, t):
"""
@@ -145,9 +149,11 @@ class ActorView(BaseView):
200:
description: Actor toggled
"""
t = self.api._socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t)
self.api.actor.toggle_timeout(id, t)
#t = self.api._socketio.start_background_task(target=self.toggleTimeJob, id=id, t=t)
return ('', 204)
@login_required
@route('<int:id>/action/<method>', methods=["POST"])
def action(self, id, method):
"""
@@ -172,7 +178,8 @@ class ActorView(BaseView):
200:
description: Actor Action called
"""
cbpi.cache.get("actors").get(id).instance.__getattribute__(method)()
self.api.actor.action(id, method)
return ('', 204)


+ 0
- 1
modules/base_plugins/actor.py Voir le fichier

@@ -26,7 +26,6 @@ class Dummy(Actor):
@cbpi.addon.kettle.controller()
class MyController(KettleController):


def run(self):
while self.is_running():
print "HALLO"


+ 12
- 1
modules/base_plugins/sensor.py Voir le fichier

@@ -1,4 +1,8 @@
# -*- coding: utf-8 -*-
import os

from os.path import join

from modules.core.basetypes import Actor, Sensor
from modules.core.core import cbpi
from modules.core.proptypes import Property
@@ -33,4 +37,11 @@ class Dummy(Sensor):
@cbpi.addon.core.action(key="clear", label="Clear all Logs")
def woohoo(cbpi):
print "COOL"
cbpi.notify(headline="HELLO WORLD",message="")
dir = "./logs"
test = os.listdir(dir)

for item in test:

if item.endswith(".log"):
os.remove(join(dir, item))
cbpi.notify(headline="Logs Deleted",message="All Logs Cleared")

+ 4
- 0
modules/base_plugins/steps.py Voir le fichier

@@ -7,6 +7,10 @@ from modules.core.proptypes import Property
class Dummy(Step):


@cbpi.addon.step.action("WOHOO")
def myaction(self):
print "HALLO"

text = Property.Text(label="Text", configurable=True, description="WOHOOO")

def execute(self):


+ 8
- 0
modules/core/baseapi.py Voir le fichier

@@ -87,6 +87,9 @@ class StepAPI(BaseAPI):
return func
return real_decorator
class ActorAPI(BaseAPI):
key = "actor_types"
@@ -150,6 +153,8 @@ class CoreAPI(BaseAPI):
self.cbpi.cache["init"] = []
self.cbpi.cache["js"] = {}
self.cbpi.cache["background"] = []
self.cbpi.cache["web_menu"] =[]
def init(self):
self.cbpi.cache["init"] = sorted(self.cbpi.cache["init"], key=lambda k: k['order'])
@@ -173,6 +178,9 @@ class CoreAPI(BaseAPI):
def add_js(self, name, file):
self.cbpi.cache["js"][name] = file
def add_menu_link(self, name, path):
self.cbpi.cache["web_menu"].append(dict(name=name, path=path))
def initializer(self, order=0, **options):
def decorator(f):
self.cbpi.cache.get("init").append({"function": f, "order": order})


+ 2
- 1
modules/core/basetypes.py Voir le fichier

@@ -246,4 +246,5 @@ class Step(Base):
self.__dirty = True
super(Step, self).__setattr__(name, value)
else:
super(Step, self).__setattr__(name, value)
super(Step, self).__setattr__(name, value)

+ 7
- 1
modules/core/baseview.py Voir le fichier

@@ -1,5 +1,7 @@
from flask import request, json
from flask_classy import route, FlaskView
from flask_login import login_required
from modules.core.core import cbpi
@@ -9,6 +11,7 @@ class BaseView(FlaskView):
cache_key = None
api = cbpi
@login_required
@route('/<int:id>', methods=["GET"])
def getOne(self, id):
if self.api.cache.get(self.cache_key) is not None:
@@ -16,6 +19,7 @@ class BaseView(FlaskView):
else:
return json.dumps(self.model.get_one(id))
@login_required
@route('/', methods=["GET"])
def getAll(self):
if self.api.cache.get(self.cache_key) is not None:
@@ -30,6 +34,7 @@ class BaseView(FlaskView):
def _post_post_callback(self, m):
pass
@login_required
@route('/', methods=["POST"])
def post(self):
@@ -50,7 +55,7 @@ class BaseView(FlaskView):
def _post_put_callback(self, m):
pass
@login_required
@route('/<int:id>', methods=["PUT"])
def put(self, id):
data = request.json
@@ -78,6 +83,7 @@ class BaseView(FlaskView):
def _post_delete_callback(self, id):
pass
@login_required
@route('/<int:id>', methods=["DELETE"])
def delete(self, id):
if self.api.cache.get(self.cache_key) is not None:


+ 53
- 1
modules/core/core.py Voir le fichier

@@ -7,6 +7,7 @@ from datetime import datetime
from functools import wraps, update_wrapper
from importlib import import_module
from time import localtime, strftime
import time
from flask import Flask, redirect, json, g, make_response
from flask_socketio import SocketIO
@@ -17,6 +18,7 @@ from modules.core.basetypes import Sensor, Actor
from modules.database.dbmodel import Kettle
class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
try:
@@ -109,6 +111,12 @@ class ActorCore(object):
print e
return False
def toggle(self, id):
if self.cbpi.cache.get("actors").get(id).state == 0:
self.on(id)
else:
self.off(id)
def power(self, id, power):
try:
actor = self.cbpi.cache["actors"].get(int(id))
@@ -121,6 +129,20 @@ class ActorCore(object):
print e
return False
def action(self, id, method):
self.cbpi.cache.get("actors").get(id).instance.__getattribute__(method)()
def toggle_timeout(self, id, seconds):
def toggle( id, seconds):
self.cbpi.cache.get("actors").get(int(id)).timer = int(time.time()) + int(seconds)
self.toggle(int(id))
self.cbpi.sleep(seconds)
self.cbpi.cache.get("actors").get(int(id)).timer = None
self.toggle(int(id))
job = self.cbpi._socketio.start_background_task(target=toggle, id=id, seconds=seconds)
def get_state(self, actor_id):
print actor_id
print self.cbpi
@@ -183,6 +205,10 @@ class SensorCore(object):
with open(filename, "a") as file:
file.write(msg)
def action(self, id, method):
self.cbpi.cache.get("sensors").get(id).instance.__getattribute__(method)()
class BrewingCore(object):
def __init__(self, cbpi):
@@ -207,6 +233,32 @@ class BrewingCore(object):
self.cbpi.emit("SET_KETTLE_TARGET_TEMP", id=id, temp=temp)
def toggle_automatic(self, id):
kettle = self.cbpi.cache.get("kettle")[id]
if kettle.state is False:
# Start controller
if kettle.logic is not None:
cfg = kettle.config.copy()
cfg.update(dict(api=cbpi, kettle_id=kettle.id, heater=kettle.heater, sensor=kettle.sensor))
instance = self.get_controller(kettle.logic).get("class")(**cfg)
instance.init()
kettle.instance = instance
def run(instance):
instance.run()
t = self.cbpi._socketio.start_background_task(target=run, instance=instance)
kettle.state = not kettle.state
self.cbpi.ws_emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id))
self.cbpi.emit("KETTLE_CONTROLLER_STARTED", id=id)
else:
# Stop controller
kettle.instance.stop()
kettle.state = not kettle.state
self.cbpi.ws_emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id))
self.cbpi.emit("KETTLE_CONTROLLER_STOPPED", id=id)
class FermentationCore(object):
def __init__(self, cbpi):
self.cbpi = cbpi
@@ -313,7 +365,7 @@ class CraftBeerPI(object):
return cfg.value
def emit(self, key, **kwargs):
print key, kwargs
if self.eventbus.get(key) is not None:
for value in self.eventbus[key]:
if value["async"] is False:


+ 44
- 0
modules/core/db_migrate.py Voir le fichier

@@ -0,0 +1,44 @@
import sqlite3
import os
from modules.core.core import cbpi
from modules.core.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.addon.core.initializer(order=-9999)
def init(cbpi):

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"):
print filename
if filename.endswith(".sql"):
d = {"version": int(filename[:filename.index('_')]), "file": filename}
result.append(d)
execute_file(current_version, d)

+ 0
- 36
modules/core/login.py Voir le fichier

@@ -1,36 +0,0 @@
import flask_login
from modules.core.core import cbpi, addon
class User(flask_login.UserMixin):
pass
@addon.core.initializer(order=0)
def log(cbpi):
cbpi._login_manager = flask_login.LoginManager()
cbpi._login_manager.init_app(cbpi._app)
@cbpi._app.route('/login', methods=['GET', 'POST'])
def login():
user = User()
user.id = "manuel"
flask_login.login_user(user)
return "OK"
@cbpi._app.route('/logout')
def logout():
flask_login.logout_user()
return 'Logged out'
@cbpi._login_manager.user_loader
def user_loader(email):
if email not in cbpi.cache["users"]:
return
user = User()
user.id = email
return user
@cbpi._login_manager.unauthorized_handler
def unauthorized_handler():
return 'Unauthorized :-('

+ 1
- 31
modules/kettle/__init__.py Voir le fichier

@@ -214,39 +214,9 @@ class KettleView(BaseView):
description: User created
"""
kettle = cbpi.cache.get("kettle")[id]
if kettle.state is False:
# Start controller
if kettle.logic is not None:
cfg = kettle.config.copy()
cfg.update(dict(api=cbpi, kettle_id=kettle.id, heater=kettle.heater, sensor=kettle.sensor))
instance = cbpi.brewing.get_controller(kettle.logic).get("class")(**cfg)
instance.init()
kettle.instance = instance
def run(instance):
instance.run()
t = self.api._socketio.start_background_task(target=run, instance=instance)
kettle.state = not kettle.state
cbpi.ws_emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id))
cbpi.emit("KETTLE_CONTROLLER_STARTED", id=id)
else:
# Stop controller
kettle.instance.stop()
kettle.state = not kettle.state
cbpi.ws_emit("UPDATE_KETTLE", cbpi.cache.get("kettle").get(id))
cbpi.emit("KETTLE_CONTROLLER_STOPPED", id=id)
self.api.brewing.toggle_automatic(id)
return ('', 204)
#@cbpi.event("SET_TARGET_TEMP")
def set_target_temp(id, temp):
'''
Change Taget Temp Event
:param id: kettle id
:param temp: target temp to set
:return: None
'''
KettleView().postTargetTemp(id, temp)
@cbpi.addon.core.backgroundjob(key="read_target_temps", interval=5)
def read_target_temps(api):
"""


+ 57
- 0
modules/login/__init__.py Voir le fichier

@@ -0,0 +1,57 @@
import flask_login
from flask import request

from modules.core.core import cbpi, addon

class User(flask_login.UserMixin):
pass

@addon.core.initializer(order=0)
def log(cbpi):


cbpi._login_manager = flask_login.LoginManager()
cbpi._login_manager.init_app(cbpi._app)
@cbpi._app.route('/login', methods=['POST'])
def login():

data = request.json
password = cbpi.get_config_parameter("password", None)

if password is None:
return ('',500)

if password == data.get("password",""):
user = User()
user.id = "craftbeerpi"
flask_login.login_user(user)
return ('',204)
else:
return ('',401)


@cbpi._app.route('/logout', methods=['POST'])
def logout():
flask_login.logout_user()
return 'Logged out'

@cbpi._login_manager.user_loader
def user_loader(user):

print cbpi.get_config_parameter("password_security", "NO")
print user

if cbpi.get_config_parameter("password_security", "NO") == "YES":
if user != "craftbeerpi":
return
user = User()
user.id = user
return user
else:
user = User()
user.id = user
return user

@cbpi._login_manager.unauthorized_handler
def unauthorized_handler():
return ('Please login',401)

+ 1
- 1
modules/sensor/__init__.py Voir le fichier

@@ -34,7 +34,7 @@ class SensorView(BaseView):
200:
description: Sensor Action called
"""
cbpi.cache.get("sensors").get(id).instance.__getattribute__(method)()
cbpi.sensor.action(id, method)
return ('', 204)

def _post_post_callback(self, m):


+ 25
- 0
modules/system/__init__.py Voir le fichier

@@ -1,6 +1,7 @@
import yaml
from flask import json, url_for, Response
from flask_classy import FlaskView, route
from flask_login import login_required, current_user
from git import Repo, Git
from modules.core.core import cbpi
import pprint
@@ -13,6 +14,7 @@ class SystemView(FlaskView):
from subprocess import call
call("halt")
@login_required
@route('/shutdown', methods=['POST'])
def shutdown(self):
"""
@@ -33,6 +35,7 @@ class SystemView(FlaskView):
from subprocess import call
call("reboot")
@login_required
@route('/reboot', methods=['POST'])
def reboot(self):
"""
@@ -47,6 +50,7 @@ class SystemView(FlaskView):
self.doReboot()
return ('', 204)
@login_required
@route('/tags/<name>', methods=['GET'])
def checkout_tag(self,name):
repo = Repo('./')
@@ -58,6 +62,7 @@ class SystemView(FlaskView):
cbpi.notify("Checkout successful", "Please restart the system")
return ('', 204)
@login_required
@route('/git/status', methods=['GET'])
def git_status(self):
"""
@@ -92,6 +97,7 @@ class SystemView(FlaskView):
return json.dumps({"tags": tags, "headcommit": str(repo.head.commit), "branchname": branch_name,
"master": {"changes": changes}})
@login_required
@route('/check_update', methods=['GET'])
def check_update(self):
"""
@@ -114,6 +120,7 @@ class SystemView(FlaskView):
return json.dumps(changes)
@login_required
@route('/git/pull', methods=['POST'])
def update(self):
"""
@@ -131,6 +138,24 @@ class SystemView(FlaskView):
cbpi.notify("Pull successful", "The lasted updated was downloaded. Please restart the system")
return ('', 204)
@route('/connect', methods=['GET'])
def connect(self):
"""
Connect
---
tags:
- system
responses:
200:
description: CraftBeerPi System Cache
"""
if self.api.get_config_parameter("setup", "YES") == "YES":
return json.dumps(dict(setup=True, loggedin= current_user.is_authenticated ))
else:
return json.dumps(dict(setup=False, loggedin= current_user.is_authenticated))
@login_required
@route('/dump', methods=['GET'])
def dump(self):
"""


+ 11
- 2
modules/ui/__init__.py Voir le fichier

@@ -1,4 +1,4 @@
from flask import Blueprint
from flask import Blueprint,render_template

from modules.core.core import cbpi

@@ -11,12 +11,21 @@ def init(cbpi):

@react.route('/', methods=["GET"])
def index():
return react.send_static_file("index.html")
#return react.send_static_file("index.html")

js_files = []
for key, value in cbpi.cache["js"].iteritems():
js_files.append(value)

print js_files
return render_template('index.html', js_files=js_files)



@cbpi._app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404






+ 1
- 2
modules/ui/src/App.css Voir le fichier

@@ -7,8 +7,7 @@
}

.container-fluid {
padding-right: 5px;
padding-left: 5px;


}



+ 1
- 3
modules/ui/src/index.css Voir le fichier

@@ -1,5 +1,3 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;

}

+ 44
- 48
modules/ui/static/bundle.js
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 29
- 0
modules/ui/templates/404.html Voir le fichier

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

<link rel="stylesheet" href="/ui/static/bootstrap.min.css">
<link rel="stylesheet" href="/ui/static/css/font-awesome.min.css">
<link rel="stylesheet" href="/ui/static/bootstrap.dark.css">
<link href="https://fonts.googleapis.com/css?family=Dosis:200,500" rel="stylesheet">
<style>

body, h1, h2, h3, h4, h5, h6 {
font-family: 'Dosis', sans-serif;
}

</style>


<title>CraftBeerPi 3.1</title>
</head>
<body>

<h1>CraftBeerPi - Page not Found!</h1>


</body>
</html>

+ 33
- 0
modules/ui/templates/index.html Voir le fichier

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

<link rel="stylesheet" href="static/bootstrap.min.css">
<link rel="stylesheet" href="static/css/font-awesome.min.css">
<link rel="stylesheet" href="static/bootstrap.dark.css">
<link href="https://fonts.googleapis.com/css?family=Dosis:200,500" rel="stylesheet">
<style>

body, h1, h2, h3, h4, h5, h6 {
font-family: 'Dosis', sans-serif;
}

</style>


<title>CraftBeerPi 3.1</title>
</head>
<body>
<div id="root" ></div>
{% for file in js_files %}

<script src="{{ file }}" type="text/javascript"></script>
{% endfor %}
<script src="static/bundle.js" type="text/javascript"></script>


</body>
</html>

+ 3
- 1
run.py Voir le fichier

@@ -6,9 +6,11 @@ cbpi = CraftBeerPI()

addon = cbpi.addon


from modules.core.db_migrate import *
from modules.buzzer import *
from modules.config import *
from modules.core.login import *
from modules.login import *
from modules.system import *
from modules.ui import *
from modules.step import *


+ 1
- 0
update/4_login_password.sql Voir le fichier

@@ -0,0 +1 @@
INSERT OR IGNORE INTO config VALUES ('password', 'beer', 'text', 'LoginPassword', NULL );

Chargement…
Annuler
Enregistrer