diff --git a/.idea/craftbeerpi4.iml b/.idea/craftbeerpi4.iml
new file mode 100644
index 0000000..d7cfe33
--- /dev/null
+++ b/.idea/craftbeerpi4.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..c23ecac
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0532208
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..87e58e4
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..964274c
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,347 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ DEFINITION_ORDER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1541098050947
+
+
+ 1541098050947
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/__init__.py b/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/__pycache__/__init__.cpython-36.pyc b/core/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..bdfdffd
Binary files /dev/null and b/core/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/__pycache__/cbpi.cpython-36.pyc b/core/__pycache__/cbpi.cpython-36.pyc
new file mode 100644
index 0000000..a5406ff
Binary files /dev/null and b/core/__pycache__/cbpi.cpython-36.pyc differ
diff --git a/core/__pycache__/eventbus.cpython-36.pyc b/core/__pycache__/eventbus.cpython-36.pyc
new file mode 100644
index 0000000..d66f301
Binary files /dev/null and b/core/__pycache__/eventbus.cpython-36.pyc differ
diff --git a/core/__pycache__/plugin.cpython-36.pyc b/core/__pycache__/plugin.cpython-36.pyc
new file mode 100644
index 0000000..e2fd4b0
Binary files /dev/null and b/core/__pycache__/plugin.cpython-36.pyc differ
diff --git a/core/__pycache__/websocket.cpython-36.pyc b/core/__pycache__/websocket.cpython-36.pyc
new file mode 100644
index 0000000..561bf69
Binary files /dev/null and b/core/__pycache__/websocket.cpython-36.pyc differ
diff --git a/core/api/__init__.py b/core/api/__init__.py
new file mode 100644
index 0000000..027ad5e
--- /dev/null
+++ b/core/api/__init__.py
@@ -0,0 +1 @@
+__all__ = ["actor", "property", "sensor"]
\ No newline at end of file
diff --git a/core/api/__pycache__/__init__.cpython-36.pyc b/core/api/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..adef98c
Binary files /dev/null and b/core/api/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/api/__pycache__/actor.cpython-36.pyc b/core/api/__pycache__/actor.cpython-36.pyc
new file mode 100644
index 0000000..df979be
Binary files /dev/null and b/core/api/__pycache__/actor.cpython-36.pyc differ
diff --git a/core/api/__pycache__/decorator.cpython-36.pyc b/core/api/__pycache__/decorator.cpython-36.pyc
new file mode 100644
index 0000000..da470d7
Binary files /dev/null and b/core/api/__pycache__/decorator.cpython-36.pyc differ
diff --git a/core/api/__pycache__/property.cpython-36.pyc b/core/api/__pycache__/property.cpython-36.pyc
new file mode 100644
index 0000000..481a8ba
Binary files /dev/null and b/core/api/__pycache__/property.cpython-36.pyc differ
diff --git a/core/api/__pycache__/sensor.cpython-36.pyc b/core/api/__pycache__/sensor.cpython-36.pyc
new file mode 100644
index 0000000..949a74b
Binary files /dev/null and b/core/api/__pycache__/sensor.cpython-36.pyc differ
diff --git a/core/api/actor.py b/core/api/actor.py
new file mode 100644
index 0000000..1f4e505
--- /dev/null
+++ b/core/api/actor.py
@@ -0,0 +1,16 @@
+
+
+class Actor():
+
+ def __init__(self):
+ self.id = "";
+ self.name = ""
+
+ def on(self, power):
+ pass
+
+ def off(self):
+ pass
+
+ def state(self):
+ pass
\ No newline at end of file
diff --git a/core/api/decorator.py b/core/api/decorator.py
new file mode 100644
index 0000000..14eb58f
--- /dev/null
+++ b/core/api/decorator.py
@@ -0,0 +1,82 @@
+from aiohttp_auth import auth
+
+def composed(*decs):
+ def deco(f):
+ for dec in reversed(decs):
+ f = dec(f)
+ return f
+ return deco
+
+def request_mapping(path, name=None, method="GET", auth_required=True):
+
+ def on_http_request(path, name=None):
+ def real_decorator(func):
+ func.route = True
+ func.path = path
+ func.name = name
+ func.method = method
+ return func
+
+ return real_decorator
+
+ if auth_required is True:
+ return composed(
+ on_http_request(path, name),
+ auth.auth_required
+ )
+ else:
+ return composed(
+ on_http_request(path, name)
+ )
+
+def on_websocket_message(path, name=None):
+ def real_decorator(func):
+ func.ws = True
+ func.key = path
+ func.name = name
+ return func
+
+ return real_decorator
+
+def on_event(topic):
+ def real_decorator(func):
+ func.eventbus = True
+ func.topic = topic
+ return func
+
+ return real_decorator
+
+def action(key, parameters):
+ def real_decorator(func):
+ func.action = True
+ func.key = key
+ func.parameters = parameters
+ return func
+
+ return real_decorator
+
+def on_mqtt_message(topic):
+ def real_decorator(func):
+ func.mqtt = True
+ func.topic = topic
+ return func
+
+ return real_decorator
+
+
+def background_task(name, interval):
+ def real_decorator(func):
+ func.background_task = True
+ func.name = name
+ func.interval = interval
+ return func
+
+ return real_decorator
+
+
+def entry_exit(f):
+ def new_f():
+ print("Entering", f.__name__)
+ f()
+ print("Exited", f.__name__)
+ return new_f
\ No newline at end of file
diff --git a/core/api/property.py b/core/api/property.py
new file mode 100644
index 0000000..727b753
--- /dev/null
+++ b/core/api/property.py
@@ -0,0 +1,47 @@
+class PropertyType(object):
+ pass
+
+class Property(object):
+ class Select(PropertyType):
+ def __init__(self, label, options, description=""):
+ PropertyType.__init__(self)
+ self.label = label
+ self.options = options
+ self.description = description
+
+ class Number(PropertyType):
+ def __init__(self, label, configurable=False, default_value=None, unit="", description=""):
+ PropertyType.__init__(self)
+ self.label = label
+ self.configurable = configurable
+ self.default_value = default_value
+ self.description = description
+
+ class Text(PropertyType):
+ def __init__(self, label, configurable=False, default_value="", description=""):
+ PropertyType.__init__(self)
+ self.label = label
+ 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
\ No newline at end of file
diff --git a/core/api/sensor.py b/core/api/sensor.py
new file mode 100644
index 0000000..a9fd077
--- /dev/null
+++ b/core/api/sensor.py
@@ -0,0 +1,14 @@
+class Sensor():
+
+ def __init__(self):
+ self.id = "";
+ self.name = ""
+
+ def on(self):
+ pass
+
+ def off(self):
+ pass
+
+ def state(self):
+ pass
\ No newline at end of file
diff --git a/core/cbpi.py b/core/cbpi.py
new file mode 100644
index 0000000..a1a2130
--- /dev/null
+++ b/core/cbpi.py
@@ -0,0 +1,152 @@
+import asyncio
+import importlib
+import logging
+from os import urandom
+
+import yaml
+from aiohttp import web
+from aiohttp_auth import auth
+from aiohttp_session import session_middleware
+from aiohttp_session.cookie_storage import EncryptedCookieStorage
+from aiohttp_swagger import setup_swagger
+from aiojobs.aiohttp import setup, get_scheduler_from_app
+
+from core.controller.actor_controller import ActorController
+from core.controller.system_controller import SystemController
+from core.database.model import DBModel
+from core.eventbus import EventBus
+
+
+from core.http_endpoints.http_login import Login
+from core.controller.sensor_controller import SensorController
+from core.websocket import WebSocket
+
+logger = logging.getLogger(__file__)
+logging.basicConfig(level=logging.INFO)
+
+
+class CraftBeerPi():
+
+ def __init__(self):
+
+ logger.info("Init CraftBeerPI")
+ policy = auth.SessionTktAuthentication(urandom(32), 60, include_ip=True)
+ middlewares = [session_middleware(EncryptedCookieStorage(urandom(32))), auth.auth_middleware(policy)]
+ self.app = web.Application(middlewares=middlewares)
+
+ setup(self.app)
+ self.bus = EventBus()
+ self.ws = WebSocket(self)
+ self.actor = ActorController(self)
+ self.sensor = SensorController(self)
+ self.system = SystemController(self)
+ self.login = Login(self)
+
+ def register_events(self, obj):
+
+ for method in [getattr(obj, f) for f in dir(obj) if callable(getattr(obj, f)) and hasattr(getattr(obj, f),"eventbus")]:
+ print(method.__getattribute__("topic"), method)
+
+ doc = None
+ if method.__doc__ is not None:
+ doc = yaml.load(method.__doc__)
+ doc["topic"] = method.__getattribute__("topic")
+ self.bus.register(method.__getattribute__("topic"), method, doc)
+
+
+ def register_background_task(self, obj):
+ for method in [getattr(obj, f) for f in dir(obj) if callable(getattr(obj, f)) and hasattr(getattr(obj, f), "background_task")]:
+ name = method.__getattribute__("name")
+ interval = method.__getattribute__("interval")
+
+ async def job_loop(app, name, interval, method):
+ logger.info("Start Background Task %s Interval %s Method %s" % (name,interval, method))
+ while True:
+ logger.info("Execute Task %s - interval(%s second(s)" % (name, interval))
+ await asyncio.sleep(interval)
+ await method()
+
+ async def spawn_job(app):
+ scheduler = get_scheduler_from_app(self.app)
+ await scheduler.spawn(job_loop(self.app, name, interval, method))
+
+
+ self.app.on_startup.append(spawn_job)
+
+
+
+ def register_ws(self, obj):
+ if self.ws is None:
+ return
+
+ for method in [getattr(obj, f) for f in dir(obj) if callable(getattr(obj, f)) and hasattr(getattr(obj, f),"ws")]:
+ self.ws.add_callback(method, method.__getattribute__("key"))
+
+ def register(self, obj, subapp=None):
+ self.register_http_endpoints(obj, subapp)
+ self.register_events(obj)
+ self.register_ws(obj)
+ self.register_background_task(obj)
+
+
+ def register_http_endpoints(self, obj, subapp=None):
+ routes = []
+ for method in [getattr(obj, f) for f in dir(obj) if callable(getattr(obj, f)) and hasattr(getattr(obj, f), "route")]:
+
+ http_method = method.__getattribute__("method")
+ path = method.__getattribute__("path")
+
+ def add_post():
+ routes.append(web.post(method.__getattribute__("path"), method))
+
+ def add_get():
+ routes.append(web.get(method.__getattribute__("path"), method))
+
+ def add_delete():
+ routes.append(web.delete(path, method))
+
+ def add_put():
+ routes.append(web.put(path, method))
+ switcher = {
+ "POST": add_post,
+ "GET": add_get,
+ "DELETE": add_delete,
+ "PUT": add_put
+ }
+
+ switcher[http_method]()
+
+ if subapp is not None:
+ sub = web.Application()
+ sub.add_routes(routes)
+ self.app.add_subapp(subapp, sub)
+ else:
+ self.app.add_routes(routes)
+
+ async def _load_extensions(self, app):
+ extension_list = ["core.extension.dummy"]
+
+ for extension in extension_list:
+ logger.info("LOADING PUGIN %s" % extension)
+ my_module = importlib.import_module(extension)
+ my_module.setup(self)
+
+
+ def start(self):
+
+ async def init_database(app):
+ await DBModel.test_connection()
+
+ async def init_controller(app):
+ await self.actor.init()
+
+
+
+
+ self.app.on_startup.append(init_database)
+ self.app.on_startup.append(self._load_extensions)
+ self.app.on_startup.append(init_controller)
+ setup_swagger(self.app)
+ web.run_app(self.app)
+
+
diff --git a/core/controller/__init__.py b/core/controller/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/controller/__pycache__/__init__.cpython-36.pyc b/core/controller/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..4835777
Binary files /dev/null and b/core/controller/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/controller/__pycache__/actor_controller.cpython-36.pyc b/core/controller/__pycache__/actor_controller.cpython-36.pyc
new file mode 100644
index 0000000..a500f8b
Binary files /dev/null and b/core/controller/__pycache__/actor_controller.cpython-36.pyc differ
diff --git a/core/controller/__pycache__/crud_controller.cpython-36.pyc b/core/controller/__pycache__/crud_controller.cpython-36.pyc
new file mode 100644
index 0000000..050551b
Binary files /dev/null and b/core/controller/__pycache__/crud_controller.cpython-36.pyc differ
diff --git a/core/controller/__pycache__/sensor_controller.cpython-36.pyc b/core/controller/__pycache__/sensor_controller.cpython-36.pyc
new file mode 100644
index 0000000..89bb99e
Binary files /dev/null and b/core/controller/__pycache__/sensor_controller.cpython-36.pyc differ
diff --git a/core/controller/__pycache__/system_controller.cpython-36.pyc b/core/controller/__pycache__/system_controller.cpython-36.pyc
new file mode 100644
index 0000000..4e5aa82
Binary files /dev/null and b/core/controller/__pycache__/system_controller.cpython-36.pyc differ
diff --git a/core/controller/actor_controller.py b/core/controller/actor_controller.py
new file mode 100644
index 0000000..ed9127a
--- /dev/null
+++ b/core/controller/actor_controller.py
@@ -0,0 +1,71 @@
+from aiohttp import web
+from aiohttp_auth.auth.decorators import auth_required
+
+from core.api.decorator import on_event, request_mapping
+from core.controller.crud_controller import CRUDController
+from core.database.model import ActorModel
+from core.http_endpoints.http_api import HttpAPI
+from core.plugin import PluginAPI
+
+
+class ActorController(HttpAPI, CRUDController, PluginAPI):
+
+
+ model = ActorModel
+
+
+ def __init__(self, cbpi):
+ super(ActorController, self).__init__(cbpi)
+ self.cbpi = cbpi
+ self.state = False;
+
+ self.cbpi.register(self, "/actor")
+ self.types = {}
+ self.actors = {}
+
+
+ async def init(self):
+
+ await super(ActorController, self).init()
+ for name, clazz in self.types.items():
+ print("Type", name)
+ for id, value in self.cache.items():
+
+ if value.type in self.types:
+ clazz = self.types[value.type];
+ self.actors[id] = clazz(self.cbpi)
+ print(value.type)
+ print("CACHE", self.cache)
+ print("ACTORS", self.actors)
+
+ @request_mapping(path="/{id}/on",auth_required=False)
+ async def http_on(self, request) -> web.Response:
+ self.cbpi.bus.fire(event="actor/1/on", id=1, power=99)
+ return web.Response(status=204)
+
+ @on_event(topic="actor/+/on")
+ def on(self, id, power=100) -> None:
+ print("ON-------------", id, power)
+ if id in self.actors:
+ i = self.actors[id]
+ i.on(power)
+
+ @on_event(topic="actor/+/on")
+ def on2(self, id, **kwargs) -> None:
+ print("POWERED ON", id, kwargs)
+
+
+
+ def register(self, name, clazz) -> None:
+ '''
+ Register a new actor type
+ :param name: actor name
+ :param clazz: actor class
+ :return: None
+ '''
+ self._parse_props(clazz)
+ self.types[name] = clazz
+
+
+
+
diff --git a/core/controller/crud_controller.py b/core/controller/crud_controller.py
new file mode 100644
index 0000000..7079969
--- /dev/null
+++ b/core/controller/crud_controller.py
@@ -0,0 +1,86 @@
+class CRUDController(object):
+
+
+ cache = {}
+ caching = True
+
+ def __init__(self, core):
+ self.cbpi = core
+ self.cache = {}
+
+ async def init(self):
+ if self.caching is True:
+ self.cache = await self.model.get_all()
+
+ async def get_all(self, force_db_update=False):
+
+ if self.caching is False or force_db_update:
+ self.cache = await self.model.get_all()
+
+ return self.cache
+
+ async def get_one(self, id):
+
+ return self.cache.get(id)
+
+
+
+
+ async def _pre_add_callback(self, data):
+ pass
+
+ async def _post_add_callback(self, m):
+ pass
+
+ async def add(self, **data):
+ await self._pre_add_callback(data)
+ m = await self.model.insert(**data)
+ await self._post_add_callback(m)
+ self.cache[m.id] = m
+ return m
+
+ async def _pre_update_callback(self, id):
+ pass
+
+ async def _post_update_callback(self, m):
+ pass
+
+ async def update(self, id, **data):
+
+ await self._pre_update_callback(id)
+ data["id"] = id
+ try:
+ del data["instance"]
+ except:
+ pass
+ m = await self.model.update(**data)
+ #self.core.push_ws("UPDATE_%s" % self.key, m)
+
+ await self._post_update_callback(m)
+ if self.caching is True:
+ self.cache[m.id] = m
+ return m
+
+ async def _pre_delete_callback(self, m):
+ pass
+
+ async def _post_delete_callback(self, id):
+ pass
+
+ async def delete(self, id):
+ await self._pre_delete_callback(id)
+ m = await self.model.delete(id)
+ await self._post_delete_callback(id)
+ try:
+ if self.caching is True:
+ del self.cache[id]
+ except Exception as e:
+ pass
+
+ #self.core.push("DELETE_%s" % self.key, id)
+
+ async def delete_all(self):
+ self.model.delete_all()
+ if self.caching is True:
+ self.cache = {}
+ #self.core.push_ws("DELETE_ALL_%s" % self.key, None)
\ No newline at end of file
diff --git a/core/controller/fermentation_controller.py b/core/controller/fermentation_controller.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/controller/sensor_controller.py b/core/controller/sensor_controller.py
new file mode 100644
index 0000000..32f6b41
--- /dev/null
+++ b/core/controller/sensor_controller.py
@@ -0,0 +1,38 @@
+import logging
+from logging.handlers import TimedRotatingFileHandler
+
+
+from core.api.decorator import background_task
+from core.controller.crud_controller import CRUDController
+
+from core.database.model import SensorModel
+from core.http_endpoints.http_api import HttpAPI
+
+
+class SensorController(CRUDController, HttpAPI):
+
+ model = SensorModel
+
+ def __init__(self, core):
+ self.core = core
+ self.core.register(self, "/sensor")
+ self.service = self
+
+ self.sensors = {"S1": "S1", "S2": "S2"}
+ handler = TimedRotatingFileHandler("./logs/first_logfile2.log", when="m", interval=1, backupCount=5)
+ #handler = RotatingFileHandler("first_logfile.log", mode='a', maxBytes=300, backupCount=2, encoding=None, delay=0)
+ formatter = logging.Formatter('%(asctime)s,%(sensor)s,%(message)s')
+ handler.setFormatter(formatter)
+
+ self.logger = logging.getLogger("SensorController")
+ self.logger.setLevel(logging.INFO)
+ self.logger.propagate = False
+ self.logger.addHandler(handler)
+
+ async def pre_get_one(self, id):
+ pass
+
+ @background_task(name="test", interval=1)
+ async def hallo(self):
+
+ self.logger.info("WOOHO", extra={"sensor": 1})
diff --git a/core/controller/step_controller.py b/core/controller/step_controller.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/controller/system_controller.py b/core/controller/system_controller.py
new file mode 100644
index 0000000..d4d7031
--- /dev/null
+++ b/core/controller/system_controller.py
@@ -0,0 +1,23 @@
+from aiohttp import web
+from aiojobs.aiohttp import get_scheduler_from_app
+
+from core.api.decorator import request_mapping
+
+
+class SystemController():
+ name = "Manuel"
+
+ def __init__(self, core):
+ self.core = core
+ self.service = core.actor
+ self.core.register(self, "/system")
+
+ @request_mapping("/jobs", method="GET", name="get_jobs", auth_required=True)
+ def get_all_jobs(self, request):
+ scheduler = get_scheduler_from_app(self.core.app)
+ print(scheduler.active_count, scheduler.pending_limit)
+ for j in scheduler:
+ print(j)
+
+ # await j.close()
+ return web.Response(text="HALLO")
\ No newline at end of file
diff --git a/core/database/__init__.py b/core/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/database/__pycache__/__init__.cpython-36.pyc b/core/database/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..de3003f
Binary files /dev/null and b/core/database/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/database/__pycache__/model.cpython-36.pyc b/core/database/__pycache__/model.cpython-36.pyc
new file mode 100644
index 0000000..b12c5d4
Binary files /dev/null and b/core/database/__pycache__/model.cpython-36.pyc differ
diff --git a/core/database/model.py b/core/database/model.py
new file mode 100644
index 0000000..f1c8894
--- /dev/null
+++ b/core/database/model.py
@@ -0,0 +1,156 @@
+import json
+
+import aiosqlite
+
+TEST_DB = "./craftbeerpi.db"
+
+
+
+class DBModel(object):
+
+ __priamry_key__ = "id"
+ __as_array__ = False
+ __order_by__ = None
+ __json_fields__ = []
+
+ def __init__(self, args):
+
+ self.__setattr__(self.__priamry_key__, args[self.__priamry_key__])
+ for f in self.__fields__:
+ if f in self.__json_fields__:
+ if args[f] is not None:
+
+ if isinstance(args[f], dict) or isinstance(args[f], list):
+ self.__setattr__(f, args[f])
+ else:
+ self.__setattr__(f, json.loads(args[f]))
+ else:
+ self.__setattr__(f, None)
+ else:
+ print(f,args[f])
+ self.__setattr__(f, args[f])
+
+ @classmethod
+ async def test_connection(self):
+
+ print("CREATE DATABSE")
+ async with aiosqlite.connect(TEST_DB) as db:
+ print("DB OK")
+ assert isinstance(db, aiosqlite.Connection)
+ qry = open('./core/sql/create_table_user.sql', 'r').read()
+ cursor = await db.executescript(qry)
+
+
+
+
+
+ @classmethod
+ async def get_all(cls):
+ print("GET ALL")
+ if cls.__as_array__ is True:
+ result = []
+ else:
+ result = {}
+ async with aiosqlite.connect(TEST_DB) as db:
+
+ if cls.__order_by__ is not None:
+ sql = "SELECT * FROM %s ORDER BY %s.'%s'" % (cls.__table_name__,cls.__table_name__,cls.__order_by__)
+ else:
+ sql = "SELECT * FROM %s" % cls.__table_name__
+
+ db.row_factory = aiosqlite.Row
+ async with db.execute(sql) as cursor:
+ async for row in cursor:
+ if cls.__as_array__ is True:
+ result.append(cls(row))
+ else:
+ result[row[0]] = cls(row)
+ await cursor.close()
+
+ return result
+
+ @classmethod
+ async def get_one(cls, id):
+ async with aiosqlite.connect(TEST_DB) as db:
+ db.row_factory = aiosqlite.Row
+ async with db.execute("SELECT * FROM %s WHERE %s = ?" % (cls.__table_name__, cls.__priamry_key__), (id,)) as cursor:
+ row = await cursor.fetchone()
+ if row is not None:
+ return cls(row)
+ else:
+ return None
+
+ @classmethod
+ async def delete(cls, id):
+ async with aiosqlite.connect(TEST_DB) as db:
+ await db.execute("DELETE FROM %s WHERE %s = ? " % (cls.__table_name__, cls.__priamry_key__), (id,))
+ await db.commit()
+
+ @classmethod
+ async def insert(cls, **kwargs):
+
+ async with aiosqlite.connect(TEST_DB) as db:
+ if cls.__priamry_key__ is not None and cls.__priamry_key__ in kwargs:
+ query = "INSERT INTO %s (%s, %s) VALUES (?, %s)" % (
+ cls.__table_name__,
+ cls.__priamry_key__,
+ ', '.join("'%s'" % str(x) for x in cls.__fields__),
+ ', '.join(['?'] * len(cls.__fields__)))
+ data = ()
+ data = data + (kwargs.get(cls.__priamry_key__),)
+ for f in cls.__fields__:
+ if f in cls.__json_fields__:
+ data = data + (json.dumps(kwargs.get(f)),)
+ else:
+ data = data + (kwargs.get(f),)
+ else:
+
+ query = 'INSERT INTO %s (%s) VALUES (%s)' % (
+ cls.__table_name__,
+ ', '.join("'%s'" % str(x) for x in cls.__fields__),
+ ', '.join(['?'] * len(cls.__fields__)))
+
+ data = ()
+ for f in cls.__fields__:
+ if f in cls.__json_fields__:
+ data = data + (json.dumps(kwargs.get(f)),)
+ else:
+ data = data + (kwargs.get(f),)
+
+ print(query, data)
+ cursor = await db.execute(query, data)
+ await db.commit()
+
+ i = cursor.lastrowid
+ kwargs["id"] = i
+
+ return cls(kwargs)
+
+ @classmethod
+ async def update(cls, **kwargs):
+ async with aiosqlite.connect(TEST_DB) as db:
+ query = 'UPDATE %s SET %s WHERE %s = ?' % (cls.__table_name__, ', '.join("'%s' = ?" % str(x) for x in cls.__fields__), cls.__priamry_key__)
+
+ data = ()
+ for f in cls.__fields__:
+ if f in cls.__json_fields__:
+ data = data + (json.dumps(kwargs.get(f)),)
+ else:
+ data = data + (kwargs.get(f),)
+
+ data = data + (kwargs.get(cls.__priamry_key__),)
+ cursor = await db.execute(query, data)
+ await db.commit()
+ return cls(kwargs)
+
+
+class ActorModel(DBModel):
+ __fields__ = ["name","type","config"]
+ __table_name__ = "actor"
+ __json_fields__ = ["config"]
+
+
+class SensorModel(DBModel):
+ __fields__ = ["name","type", "config"]
+ __table_name__ = "sensor"
+ __json_fields__ = ["config"]
\ No newline at end of file
diff --git a/core/database/orm_framework.py b/core/database/orm_framework.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/eventbus.py b/core/eventbus.py
new file mode 100644
index 0000000..5935bab
--- /dev/null
+++ b/core/eventbus.py
@@ -0,0 +1,92 @@
+import logging
+
+
+class EventBus(object):
+ class Node(object):
+ __slots__ = '_children', '_content'
+
+ def __init__(self):
+ self._children = {}
+ self._content = None
+
+ def register(self, key, value, doc=None):
+
+ if doc is not None:
+ self.docs[key] = doc
+ self.logger.info("key %s", key)
+ node = self._root
+ for sym in key.split('/'):
+ node = node._children.setdefault(sym, self.Node())
+
+ if not isinstance(node._content, list):
+ node._content = []
+ node._content.append(value)
+
+ def get_callbacks(self, key):
+ try:
+ node = self._root
+ for sym in key.split('/'):
+ node = node._children[sym]
+ if node._content is None:
+ raise KeyError(key)
+ return node._content
+ except KeyError:
+ raise KeyError(key)
+
+ def unregister(self, key, method=None):
+
+ lst = []
+ try:
+ parent, node = None, self._root
+ for k in key.split('/'):
+ parent, node = node, node._children[k]
+ lst.append((parent, k, node))
+ # TODO
+ print(node._content)
+ if method is not None:
+ node._content = None
+ else:
+ node._content = None
+ except KeyError:
+ raise KeyError(key)
+ else: # cleanup
+ for parent, k, node in reversed(lst):
+ if node._children or node._content is not None:
+ break
+ del parent._children[k]
+
+ def __init__(self):
+ self.logger = logging.getLogger(__name__)
+ self._root = self.Node()
+ self.docs = {}
+
+ def fire(self, event: str, **kwargs) -> None:
+ self.logger.info("EMIT EVENT %s", event)
+ for methods in self.iter_match(event):
+ for f in methods:
+ print("METHOD: ", f)
+ f(**kwargs)
+
+ def iter_match(self, topic):
+
+ lst = topic.split('/')
+ normal = not topic.startswith('$')
+
+ def rec(node, i=0):
+ if i == len(lst):
+ if node._content is not None:
+ yield node._content
+ else:
+ part = lst[i]
+ if part in node._children:
+ for content in rec(node._children[part], i + 1):
+ yield content
+ if '+' in node._children and (normal or i > 0):
+ for content in rec(node._children['+'], i + 1):
+ yield content
+ if '#' in node._children and (normal or i > 0):
+ content = node._children['#']._content
+ if content is not None:
+ yield content
+
+ return rec(self._root)
diff --git a/core/extension/__init__.py b/core/extension/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/extension/__pycache__/__init__.cpython-36.pyc b/core/extension/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..73cfcbe
Binary files /dev/null and b/core/extension/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/extension/dummy/__init__.py b/core/extension/dummy/__init__.py
new file mode 100644
index 0000000..e13f96d
--- /dev/null
+++ b/core/extension/dummy/__init__.py
@@ -0,0 +1,45 @@
+from core.database.model import ActorModel
+from core.api.decorator import action, background_task
+from core.api.property import Property
+print("##################")
+
+from core.api.actor import Actor
+import logging
+
+
+class MyActor(Actor):
+
+
+ name = Property.Number(label="Test")
+ name1 = Property.Text(label="Test")
+ name2 = Property.Kettle(label="Test")
+
+ @background_task("s1", interval=2)
+ async def bg_job(self):
+ print("WOOH BG")
+
+ @action(key="name", parameters={})
+ def myAction(self):
+ print("HALLO")
+
+ def state(self):
+ super().state()
+
+ def off(self):
+ super().off()
+
+ def on(self, power=100):
+ super().on(power)
+
+ def __init__(self):
+ pass
+
+ def __init__(self, core=None):
+ self.logger = logging.getLogger(__name__)
+ self.logger.info("WOOHOO MY ACTOR")
+ self.core = None
+
+
+def setup(cbpi):
+
+ cbpi.actor.register("MyActor", MyActor)
\ No newline at end of file
diff --git a/core/extension/dummy/__pycache__/__init__.cpython-36.pyc b/core/extension/dummy/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..0a07862
Binary files /dev/null and b/core/extension/dummy/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/helper/__init__.py b/core/helper/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/helper/__pycache__/__init__.cpython-36.pyc b/core/helper/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..569e48e
Binary files /dev/null and b/core/helper/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/helper/__pycache__/jsondump.cpython-36.pyc b/core/helper/__pycache__/jsondump.cpython-36.pyc
new file mode 100644
index 0000000..d5e9f2c
Binary files /dev/null and b/core/helper/__pycache__/jsondump.cpython-36.pyc differ
diff --git a/core/helper/jsondump.py b/core/helper/jsondump.py
new file mode 100644
index 0000000..762524d
--- /dev/null
+++ b/core/helper/jsondump.py
@@ -0,0 +1,26 @@
+import json
+from json import JSONEncoder
+
+from core.database.model import DBModel, ActorModel
+
+
+class ComplexEncoder(JSONEncoder):
+ def default(self, obj):
+
+ try:
+ if isinstance(obj, DBModel):
+ return obj.__dict__
+
+ elif isinstance(obj, ActorModel):
+ return None
+
+ elif hasattr(obj, "callback"):
+ return obj()
+ else:
+ return None
+ except TypeError as e:
+ pass
+ return None
+
+def json_dumps(obj):
+ return json.dumps(obj, cls=ComplexEncoder)
\ No newline at end of file
diff --git a/core/http_endpoints/__init__.py b/core/http_endpoints/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/http_endpoints/__pycache__/__init__.cpython-36.pyc b/core/http_endpoints/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..05452e4
Binary files /dev/null and b/core/http_endpoints/__pycache__/__init__.cpython-36.pyc differ
diff --git a/core/http_endpoints/__pycache__/http_api.cpython-36.pyc b/core/http_endpoints/__pycache__/http_api.cpython-36.pyc
new file mode 100644
index 0000000..477fda1
Binary files /dev/null and b/core/http_endpoints/__pycache__/http_api.cpython-36.pyc differ
diff --git a/core/http_endpoints/__pycache__/http_login.cpython-36.pyc b/core/http_endpoints/__pycache__/http_login.cpython-36.pyc
new file mode 100644
index 0000000..ec92fba
Binary files /dev/null and b/core/http_endpoints/__pycache__/http_login.cpython-36.pyc differ
diff --git a/core/http_endpoints/http_api.py b/core/http_endpoints/http_api.py
new file mode 100644
index 0000000..e91a3c1
--- /dev/null
+++ b/core/http_endpoints/http_api.py
@@ -0,0 +1,30 @@
+import logging
+from aiohttp import web
+from aiojobs.aiohttp import get_scheduler_from_app
+
+from core.api.decorator import request_mapping
+from core.helper.jsondump import json_dumps
+
+class HttpAPI():
+
+ def __init__(self, core):
+ self.logger = logging.getLogger(__name__)
+ self.logger.info("WOOHOO MY ACTOR")
+ self.cbpi = core
+
+ @request_mapping(path="/", auth_required=False)
+ async def http_get_all(self, request):
+ return web.json_response(await self.get_all(force_db_update=True), dumps=json_dumps)
+
+ @request_mapping(path="/{id}", auth_required=False)
+ async def http_get_one(self, request):
+ id = int(request.match_info['id'])
+ return web.json_response(await self.get_one(id), dumps=json_dumps)
+
+
+ @request_mapping(path="/{id}'", method="POST", auth_required=False)
+ async def http_add_one(self, request):
+ id = request.match_info['id']
+ await self.get_all(force_db_update=True)
+ return web.json_response(await self.get_one(id), dumps=json_dumps)
+
diff --git a/core/http_endpoints/http_login.py b/core/http_endpoints/http_login.py
new file mode 100644
index 0000000..8b3d48b
--- /dev/null
+++ b/core/http_endpoints/http_login.py
@@ -0,0 +1,29 @@
+from aiohttp import web
+from aiohttp_auth import auth
+
+class Login():
+
+ def __init__(self, core):
+ core.app.router.add_route('POST', '/login', self.login_view)
+ core.app.router.add_route('GET', '/logout', self.logout_view)
+ self.db = {'user': 'password', 'super_user': 'super_password'}
+
+ @auth.auth_required
+ async def logout_view(self, request):
+ await auth.forget(request)
+ return web.Response(body='OK'.encode('utf-8'))
+
+ async def login_view(self, request):
+ params = await request.post()
+ print("HALLO LOGIN")
+ print(params.get('username', None), params.get('password', None))
+ user = params.get('username', None)
+ if (user in self.db and
+ params.get('password', None) == self.db[user]):
+
+ # User is in our database, remember their login details
+ await auth.remember(request, user)
+ return web.Response(body='OK'.encode('utf-8'))
+
+ raise web.HTTPForbidden()
+
diff --git a/core/mqtt/__init__.py b/core/mqtt/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/mqtt/mqtt.py b/core/mqtt/mqtt.py
new file mode 100644
index 0000000..7c7c47b
--- /dev/null
+++ b/core/mqtt/mqtt.py
@@ -0,0 +1,90 @@
+from aiojobs.aiohttp import get_scheduler_from_app
+from hbmqtt.broker import Broker
+from hbmqtt.client import MQTTClient
+from hbmqtt.mqtt.constants import QOS_1, QOS_0
+from typing import Callable
+
+from core.mqtt_matcher import MQTTMatcher
+
+
+class MQTT():
+ def __init__(self, core):
+
+ self.config = {
+ 'listeners': {
+ 'default': {
+ 'type': 'tcp',
+ 'bind': '0.0.0.0:1885',
+ },
+ 'ws': {
+ 'bind': '0.0.0.0:8081',
+ 'type': 'ws'
+ }
+ },
+ 'sys_interval': 10,
+ 'topic-check': {
+ 'enabled': True,
+ 'plugins': [
+ 'topic_taboo'
+ ]
+ },
+ 'auth': {
+ 'allow-anonymous': True,
+ 'password-file': '/Users/manuelfritsch/github/aio_sample/core/user.txt'
+ }
+ }
+
+ self.core = core
+ self.broker = Broker(self.config, plugin_namespace="hbmqtt.broker.plugins")
+ self.client = MQTTClient()
+ self.matcher = MQTTMatcher()
+ self.mqtt_methods = {"test": self.ok_msg, "$SYS/broker/#": self.sysmsg}
+ self.core.app.on_startup.append(self.start_broker)
+ self.count = 0
+
+ def sysmsg(self, msg):
+
+ print("SYS", msg)
+
+ def ok_msg(self, msg):
+ self.count = self.count + 1
+ print("MSFG", msg, self.count)
+
+ def publish(self, topic, message):
+ print("PUSH NOW", topic)
+ self.core.app.loop.create_task(self.client.publish(topic, str.encode(message), QOS_0))
+
+ def register_callback(self, func: Callable, topic) -> None:
+
+ self.mqtt_methods[topic] = func
+
+ async def on_message(self):
+ while True:
+
+ message = await self.client.deliver_message()
+ matched = False
+ packet = message.publish_packet
+ print(message.topic)
+ #print(message.topic.split('/'))
+ data = packet.payload.data.decode("utf-8")
+
+ for callback in self.matcher.iter_match(message.topic):
+ print("MATCH")
+ callback(data)
+ matched = True
+
+ if matched == False:
+ print("NO HANDLER", data)
+
+ async def start_broker(self, app):
+
+ await self.broker.start()
+ #
+ await self.client.connect('mqtt://username:manuel@localhost:1885')
+ # await self.client.connect('mqtt://broker.hivemq.com:1883')
+
+ for k, v in self.mqtt_methods.items():
+ print("############MQTT Subscribe:", k, v)
+ await self.client.subscribe([(k, QOS_1)])
+ self.matcher[k] = v
+ await get_scheduler_from_app(app).spawn(self.on_message())
diff --git a/core/mqtt/mqtt_matcher.py b/core/mqtt/mqtt_matcher.py
new file mode 100644
index 0000000..51bb22f
--- /dev/null
+++ b/core/mqtt/mqtt_matcher.py
@@ -0,0 +1,167 @@
+class MQTTMatcher(object):
+
+
+ class Node(object):
+ __slots__ = '_children', '_content'
+
+ def __init__(self):
+ self._children = {}
+ self._content = None
+
+
+ def register(self, key, value):
+ node = self._root
+ for sym in key.split('/'):
+ node = node._children.setdefault(sym, self.Node())
+
+ if not isinstance(node._content, list):
+ node._content = []
+ node._content.append(value)
+
+ def get_callbacks(self, key):
+ try:
+ node = self._root
+ for sym in key.split('/'):
+ node = node._children[sym]
+ if node._content is None:
+ raise KeyError(key)
+ return node._content
+ except KeyError:
+ raise KeyError(key)
+
+ def unregister(self, key, method=None):
+
+ lst = []
+ try:
+ parent, node = None, self._root
+ for k in key.split('/'):
+ parent, node = node, node._children[k]
+ lst.append((parent, k, node))
+ # TODO
+ print(node._content)
+ if method is not None:
+ node._content = None
+ else:
+ node._content = None
+ except KeyError:
+ raise KeyError(key)
+ else: # cleanup
+ for parent, k, node in reversed(lst):
+ if node._children or node._content is not None:
+ break
+ del parent._children[k]
+
+ def __init__(self):
+ self._root = self.Node()
+
+ def __setitem__(self, key, value):
+ print("...",key, value)
+ node = self._root
+ for sym in key.split('/'):
+ print(sym)
+ node = node._children.setdefault(sym, self.Node())
+ print(node)
+ if not isinstance(node._content, list):
+ #print("new array")
+ node._content = []
+ node._content.append(value)
+ #node._content = value
+
+ def __getitem__(self, key):
+ try:
+ node = self._root
+ for sym in key.split('/'):
+ node = node._children[sym]
+ if node._content is None:
+ raise KeyError(key)
+ return node._content
+ except KeyError:
+ raise KeyError(key)
+ '''
+
+ def __delitem__(self, thekey):
+ print("DELETE")
+
+ if isinstance(thekey, tuple):
+ key = thekey[1]
+ methods = thekey[0]
+ print(methods.__module__, methods.__name__)
+ else:
+ methods = None
+ key = thekey
+
+ lst = []
+ try:
+ parent, node = None, self._root
+ for k in key.split('/'):
+ parent, node = node, node._children[k]
+ lst.append((parent, k, node))
+ # TODO
+ print(node._content)
+ if methods is not None:
+
+ node._content = None
+ else:
+ node._content = None
+ except KeyError:
+ raise KeyError(key)
+ else: # cleanup
+ for parent, k, node in reversed(lst):
+ if node._children or node._content is not None:
+ break
+ del parent._children[k]
+ '''
+ def iter_match(self, topic):
+
+ lst = topic.split('/')
+ normal = not topic.startswith('$')
+ def rec(node, i=0):
+ if i == len(lst):
+ if node._content is not None:
+ yield node._content
+ else:
+ part = lst[i]
+ if part in node._children:
+ for content in rec(node._children[part], i + 1):
+ yield content
+ if '+' in node._children and (normal or i > 0):
+ for content in rec(node._children['+'], i + 1):
+ yield content
+ if '#' in node._children and (normal or i > 0):
+ content = node._children['#']._content
+ if content is not None:
+ yield content
+ return rec(self._root)
+
+
+if __name__ == "__main__":
+ m = MQTTMatcher()
+
+ def test_name():
+ print("actor/1/on")
+
+ def test_name2():
+ print("actor/2/on")
+
+ def test_name3():
+ print("actor/#")
+
+ def test_name4():
+ print("actor/+/on")
+
+
+
+ m.register("actor/1/on", test_name)
+ m.register("actor/1/on", test_name)
+ m.register("actor/1/on", test_name)
+
+ print(m.get_callbacks("actor/1/on"))
+
+
+ m.unregister("actor/1/on")
+
+ for methods in m.iter_match("actor/1/on"):
+
+ for f in methods:
+ f()
+
diff --git a/core/plugin.py b/core/plugin.py
new file mode 100644
index 0000000..cfdab18
--- /dev/null
+++ b/core/plugin.py
@@ -0,0 +1,46 @@
+from pprint import pprint
+
+from core.api.property import Property
+
+
+class PluginAPI():
+
+
+ def _parse_props(self, cls):
+
+ name = cls.__name__
+
+ result = {"name": name, "class": cls, "properties": [], "actions": []}
+
+
+ tmpObj = cls()
+ members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")]
+ for m in members:
+ if isinstance(tmpObj.__getattribute__(m), Property.Number):
+ t = tmpObj.__getattribute__(m)
+ result["properties"].append(
+ {"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):
+ t = tmpObj.__getattribute__(m)
+ result["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), Property.Select):
+ t = tmpObj.__getattribute__(m)
+ result["properties"].append(
+ {"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)
+ result["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)
+ result["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)
+ result["properties"].append({"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable, "description": t.description})
+
+ for method_name, method in cls.__dict__.items():
+ if hasattr(method, "action"):
+ key = method.__getattribute__("key")
+ parameters = method.__getattribute__("parameters")
+ result["actions"].append({"method": method_name, "label": key, "parameters": parameters})
+ pprint(result, width=200)
\ No newline at end of file
diff --git a/core/sql/create_table_user.sql b/core/sql/create_table_user.sql
new file mode 100644
index 0000000..8d65c4d
--- /dev/null
+++ b/core/sql/create_table_user.sql
@@ -0,0 +1,104 @@
+CREATE TABLE IF NOT EXISTS dashboard
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ name VARCHAR(80)
+
+);
+
+CREATE TABLE IF NOT EXISTS dashboard_content
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ dbid INTEGER(80),
+ element_id INTEGER,
+ type VARCHAR(80),
+ x INTEGER(5),
+ y INTEGER(5),
+ config VARCHAR(3000)
+);
+
+CREATE TABLE IF NOT EXISTS actor
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ name VARCHAR(80),
+ type VARCHAR(80),
+ config VARCHAR(3000)
+
+);
+
+CREATE TABLE IF NOT EXISTS sensor
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ name VARCHAR(80),
+ type VARCHAR(80),
+ config VARCHAR(3000)
+
+);
+
+CREATE TABLE IF NOT EXISTS kettle
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ name VARCHAR(80),
+ sensor VARCHAR(80),
+ heater VARCHAR(10),
+ automatic VARCHAR(255),
+ logic VARCHAR(50),
+ config VARCHAR(1000),
+ agitator VARCHAR(10),
+ target_temp INTEGER,
+ height INTEGER,
+ diameter INTEGER
+);
+
+CREATE TABLE IF NOT EXISTS config
+(
+ name VARCHAR(50) PRIMARY KEY NOT NULL,
+ value VARCHAR(255),
+ type VARCHAR(50),
+ description VARCHAR(255),
+ options VARCHAR(255)
+);
+
+CREATE TABLE IF NOT EXISTS sensor
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ type VARCHAR(100),
+ name VARCHAR(80),
+ config VARCHAR(3000)
+);
+
+CREATE TABLE IF NOT EXISTS step
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ "order" INTEGER,
+ name VARCHAR(80),
+ type VARCHAR(100),
+ stepstate VARCHAR(255),
+ state VARCHAR(1),
+ start INTEGER,
+ end INTEGER,
+ config VARCHAR(255),
+ kettleid INTEGER
+);
+
+CREATE TABLE IF NOT EXISTS tank
+(
+ id INTEGER PRIMARY KEY NOT NULL,
+ name VARCHAR(80),
+ brewname VARCHAR(80),
+ sensor VARCHAR(80),
+ sensor2 VARCHAR(80),
+ sensor3 VARCHAR(80),
+ heater VARCHAR(10),
+ logic VARCHAR(50),
+ config VARCHAR(1000),
+ cooler VARCHAR(10),
+ target_temp INTEGER
+);
+
+CREATE TABLE IF NOT EXISTS translation
+(
+ language_code VARCHAR(3) NOT NULL,
+ key VARCHAR(80) NOT NULL,
+ text VARCHAR(100) NOT NULL,
+ PRIMARY KEY (language_code, key)
+);
\ No newline at end of file
diff --git a/core/test.db b/core/test.db
new file mode 100644
index 0000000..c6756e4
Binary files /dev/null and b/core/test.db differ
diff --git a/core/websocket.py b/core/websocket.py
new file mode 100644
index 0000000..0157d37
--- /dev/null
+++ b/core/websocket.py
@@ -0,0 +1,92 @@
+import logging
+import weakref
+from collections import defaultdict
+
+import aiohttp
+from aiohttp import web
+from typing import Iterable, Callable
+
+
+
+
+class WebSocket:
+
+ def __init__(self, core) -> None:
+ self.core = core
+ self._callbacks = defaultdict(set)
+ self._clients = weakref.WeakSet()
+ self.logger = logging.getLogger(__name__)
+ self.core.app.add_routes([web.get('/ws', self.websocket_handler)])
+
+ def add_callback(self, func: Callable, event: str) -> None:
+ self._callbacks[event].add(func)
+
+ async def emit(self, event: str, *args, **kwargs) -> None:
+ for func in self._event_funcs(event):
+ await func(*args, **kwargs)
+
+ def _event_funcs(self, event: str) -> Iterable[Callable]:
+ for func in self._callbacks[event]:
+ yield func
+
+ async def websocket_handler(self, request):
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+
+ self._clients.add(ws)
+
+ c = len(self._clients) - 1
+
+ self.logger.info(ws)
+ self.logger.info(c)
+ try:
+ async for msg in ws:
+ if msg.type == aiohttp.WSMsgType.TEXT:
+ if msg.data == 'close':
+
+ await ws.close()
+ self.logger.info("WS Close")
+ else:
+ msg_obj = msg.json()
+
+
+
+ self.core.bus.fire(msg_obj["topic"], id=1, power=22)
+ #await self.fire(msg_obj["key"], ws, msg)
+
+ #await ws.send_str(msg.data)
+ elif msg.type == aiohttp.WSMsgType.ERROR:
+ self.logger.error('ws connection closed with exception %s' % ws.exception())
+
+ finally:
+ self._clients.discard(ws)
+
+ self.logger.info("Web Socket Close")
+
+ return ws
+
+
+async def websocket_handler(request):
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+
+ _ws.append(ws)
+
+ c = len(_ws) - 1
+
+ async for msg in ws:
+
+ if msg.type == aiohttp.WSMsgType.TEXT:
+ if msg.data == 'close':
+ await ws.close()
+ else:
+
+ await ws.send_str(msg.data)
+ elif msg.type == aiohttp.WSMsgType.ERROR:
+ print('ws connection closed with exception %s' %
+ ws.exception())
+
+ del _ws[c]
+ print('websocket connection closed')
+
+ return ws
\ No newline at end of file
diff --git a/craftbeerpi.db b/craftbeerpi.db
new file mode 100644
index 0000000..eb7b074
Binary files /dev/null and b/craftbeerpi.db differ
diff --git a/create_password.py b/create_password.py
new file mode 100644
index 0000000..32a5f3e
--- /dev/null
+++ b/create_password.py
@@ -0,0 +1,6 @@
+import sys
+from getpass import getpass
+from passlib.hash import sha512_crypt
+
+passwd = input() if not sys.stdin.isatty() else getpass()
+print(sha512_crypt.encrypt(passwd))
\ No newline at end of file
diff --git a/logs/first_logfile2.log b/logs/first_logfile2.log
new file mode 100644
index 0000000..cb67ea3
--- /dev/null
+++ b/logs/first_logfile2.log
@@ -0,0 +1 @@
+2018-11-01 19:49:28,690,1,WOOHO
diff --git a/logs/first_logfile2.log.2018-11-01_17-21 b/logs/first_logfile2.log.2018-11-01_17-21
new file mode 100644
index 0000000..78e4ad5
--- /dev/null
+++ b/logs/first_logfile2.log.2018-11-01_17-21
@@ -0,0 +1,4 @@
+2018-11-01 17:20:45,660,1,WOOHO
+2018-11-01 17:20:46,666,1,WOOHO
+2018-11-01 17:21:25,509,1,WOOHO
+2018-11-01 17:21:26,512,1,WOOHO
diff --git a/logs/first_logfile2.log.2018-11-01_17-23 b/logs/first_logfile2.log.2018-11-01_17-23
new file mode 100644
index 0000000..bb81557
--- /dev/null
+++ b/logs/first_logfile2.log.2018-11-01_17-23
@@ -0,0 +1,2 @@
+2018-11-01 17:23:12,685,1,WOOHO
+2018-11-01 17:23:13,691,1,WOOHO
diff --git a/logs/first_logfile2.log.2018-11-01_17-24 b/logs/first_logfile2.log.2018-11-01_17-24
new file mode 100644
index 0000000..d02b320
--- /dev/null
+++ b/logs/first_logfile2.log.2018-11-01_17-24
@@ -0,0 +1 @@
+2018-11-01 17:24:28,706,1,WOOHO
diff --git a/logs/first_logfile2.log.2018-11-01_17-27 b/logs/first_logfile2.log.2018-11-01_17-27
new file mode 100644
index 0000000..ea071cf
--- /dev/null
+++ b/logs/first_logfile2.log.2018-11-01_17-27
@@ -0,0 +1,2 @@
+2018-11-01 17:27:21,293,1,WOOHO
+2018-11-01 17:27:22,302,1,WOOHO
diff --git a/logs/first_logfile2.log.2018-11-01_17-32 b/logs/first_logfile2.log.2018-11-01_17-32
new file mode 100644
index 0000000..a10c009
--- /dev/null
+++ b/logs/first_logfile2.log.2018-11-01_17-32
@@ -0,0 +1,61 @@
+2018-11-01 17:31:36,985,1,WOOHO
+2018-11-01 17:31:37,991,1,WOOHO
+2018-11-01 17:31:38,993,1,WOOHO
+2018-11-01 17:31:39,994,1,WOOHO
+2018-11-01 17:31:40,996,1,WOOHO
+2018-11-01 17:31:42,000,1,WOOHO
+2018-11-01 17:31:43,001,1,WOOHO
+2018-11-01 17:31:44,005,1,WOOHO
+2018-11-01 17:31:45,007,1,WOOHO
+2018-11-01 17:31:46,013,1,WOOHO
+2018-11-01 17:31:47,014,1,WOOHO
+2018-11-01 17:31:48,020,1,WOOHO
+2018-11-01 17:31:49,024,1,WOOHO
+2018-11-01 17:31:50,028,1,WOOHO
+2018-11-01 17:31:51,032,1,WOOHO
+2018-11-01 17:31:52,040,1,WOOHO
+2018-11-01 17:31:53,045,1,WOOHO
+2018-11-01 17:31:54,050,1,WOOHO
+2018-11-01 17:31:55,054,1,WOOHO
+2018-11-01 17:31:56,057,1,WOOHO
+2018-11-01 17:31:57,060,1,WOOHO
+2018-11-01 17:31:58,061,1,WOOHO
+2018-11-01 17:31:59,066,1,WOOHO
+2018-11-01 17:32:00,070,1,WOOHO
+2018-11-01 17:32:01,075,1,WOOHO
+2018-11-01 17:32:02,079,1,WOOHO
+2018-11-01 17:32:03,083,1,WOOHO
+2018-11-01 17:32:04,087,1,WOOHO
+2018-11-01 17:32:05,089,1,WOOHO
+2018-11-01 17:32:06,094,1,WOOHO
+2018-11-01 17:32:07,098,1,WOOHO
+2018-11-01 17:32:08,101,1,WOOHO
+2018-11-01 17:32:09,104,1,WOOHO
+2018-11-01 17:32:10,106,1,WOOHO
+2018-11-01 17:32:11,108,1,WOOHO
+2018-11-01 17:32:12,112,1,WOOHO
+2018-11-01 17:32:13,115,1,WOOHO
+2018-11-01 17:32:14,116,1,WOOHO
+2018-11-01 17:32:15,119,1,WOOHO
+2018-11-01 17:32:16,121,1,WOOHO
+2018-11-01 17:32:17,122,1,WOOHO
+2018-11-01 17:32:18,127,1,WOOHO
+2018-11-01 17:32:19,130,1,WOOHO
+2018-11-01 17:32:20,135,1,WOOHO
+2018-11-01 17:32:21,136,1,WOOHO
+2018-11-01 17:32:22,142,1,WOOHO
+2018-11-01 17:32:23,145,1,WOOHO
+2018-11-01 17:32:24,148,1,WOOHO
+2018-11-01 17:32:25,153,1,WOOHO
+2018-11-01 17:32:26,159,1,WOOHO
+2018-11-01 17:32:27,162,1,WOOHO
+2018-11-01 17:32:28,166,1,WOOHO
+2018-11-01 17:32:29,169,1,WOOHO
+2018-11-01 17:32:32,214,1,WOOHO
+2018-11-01 17:32:33,215,1,WOOHO
+2018-11-01 17:32:34,220,1,WOOHO
+2018-11-01 17:32:35,224,1,WOOHO
+2018-11-01 17:32:36,229,1,WOOHO
+2018-11-01 17:32:37,235,1,WOOHO
+2018-11-01 17:32:38,238,1,WOOHO
+2018-11-01 17:32:39,242,1,WOOHO
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..d61cbcf
--- /dev/null
+++ b/main.py
@@ -0,0 +1,192 @@
+import aiohttp
+import aiosqlite
+from aiohttp import web
+from aiohttp_swagger import *
+from aiojobs.aiohttp import setup, spawn, get_scheduler_from_app
+from hbmqtt.broker import Broker
+from hbmqtt.client import MQTTClient
+from hbmqtt.mqtt.constants import QOS_1
+
+from core.matcher import MQTTMatcher
+from core.websocket import websocket_handler
+
+TEST_DB = "test.db"
+c = MQTTClient()
+import asyncio
+
+matcher = MQTTMatcher()
+
+config = {
+ 'listeners': {
+ 'default': {
+ 'type': 'tcp',
+ 'bind': '0.0.0.0:1885',
+ },
+ 'my-ws-1': {
+ 'bind': '0.0.0.0:8888',
+ 'type': 'ws'
+ }
+ },
+
+ 'sys_interval': 10,
+ 'auth': {
+ 'allow-anonymous': True,
+ }
+}
+
+broker = Broker(config, plugin_namespace="hbmqtt.test.plugins")
+
+
+async def test2(name):
+ while True:
+ print(name)
+ await asyncio.sleep(1)
+
+
+
+
+
+async def handle(request):
+ name = request.match_info.get('name', "Anonymous")
+ text = "Hello, " + name
+
+ return web.Response(text=text)
+
+
+async def test_connection():
+ async with aiosqlite.connect(TEST_DB) as db:
+ print("DB OK")
+ assert isinstance(db, aiosqlite.Connection)
+
+
+app = web.Application()
+
+
+async def listen_to_redis(app):
+ while True:
+ await asyncio.sleep(1)
+
+ #for w in _ws:
+ # pass
+ # await w.send_str("HALLO")
+
+ # print(w)
+
+
+async def myjob(app):
+ while True:
+ await asyncio.sleep(1)
+ print("JOB")
+
+
+def ok_msg(msg):
+ print("OK", msg)
+
+
+def ok_msg1(msg):
+ print("OK1", msg)
+
+
+def ok_msg2(msg):
+ print("OK2", msg)
+
+
+mqtt_methods = {"test": ok_msg, "test/+/ab": ok_msg1, "test/+": ok_msg2}
+
+
+async def on_message():
+ while True:
+ message = await c.deliver_message()
+ matched = False
+ packet = message.publish_packet
+ print(message.topic)
+ print(message.topic.split('/'))
+ data = packet.payload.data.decode("utf-8")
+
+ for callback in matcher.iter_match(message.topic):
+ print("MATCH")
+ callback(data)
+ matched = True
+
+ if matched == False:
+ print("NO HANDLER", data)
+
+ #for w in _ws:
+ # await w.send_str(data)
+
+
+async def start_background_tasks(app):
+ app['redis_listener'] = app.loop.create_task(listen_to_redis(app))
+
+
+async def start_broker(app):
+ print(app)
+ await broker.start()
+
+ await c.connect('mqtt://localhost:1885')
+
+ for k, v in mqtt_methods.items():
+ print(k, v)
+ await c.subscribe([(k, QOS_1)])
+
+ matcher[k] = v
+ # await c.subscribe([('/test', QOS_1),('/hallo', QOS_1)])
+
+
+ await get_scheduler_from_app(app).spawn(on_message())
+
+
+job = None
+
+
+async def start_task(request):
+ global job
+ job = await spawn(request, myjob(app))
+ await test_connection()
+ return web.Response(text="OK")
+
+
+async def stop_task(request):
+ await job.close()
+ return web.Response(text="OK")
+
+
+async def stats(request):
+ s = get_scheduler_from_app(app)
+
+ return web.Response(text="%s" % s.active_count)
+
+
+
+
+
+setup(app)
+
+
+def start_bg(app, name, method):
+ print("HALLO111")
+
+ async def start(app):
+ app[name] = app.loop.create_task(method(name))
+
+ app.on_startup.append(start)
+
+
+# start_bg(app, "test", test2)
+# start_bg(app, "test2", test2)
+
+#app.on_startup.append(start_background_tasks)
+app.on_startup.append(start_broker)
+
+app.add_routes([web.get('/', handle),
+ web.get('/stop', stop_task),
+ web.get('/start', start_task),
+ web.get('/stats', stats),
+ web.get('/ws', websocket_handler),
+ web.get('/{name}', handle)
+
+ ])
+
+setup_swagger(app)
+
+web.run_app(app)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..ccd26c8
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,37 @@
+aiohttp==3.4.4
+aiohttp-auth==0.1.1
+aiohttp-route-decorator==0.1.4
+aiohttp-security==0.4.0
+aiohttp-session==2.7.0
+aiohttp-swagger==1.0.5
+aiojobs==0.2.2
+aiosqlite==0.7.0
+asn1crypto==0.24.0
+async-timeout==3.0.1
+atomicwrites==1.2.1
+attrs==18.2.0
+cffi==1.11.5
+chardet==3.0.4
+cryptography==2.3.1
+docopt==0.6.2
+hbmqtt==0.9.4
+idna==2.7
+idna-ssl==1.1.0
+Jinja2==2.10
+MarkupSafe==1.0
+more-itertools==4.3.0
+multidict==4.4.2
+passlib==1.7.1
+pluggy==0.7.1
+py==1.7.0
+pycparser==2.19
+pync==2.0.3
+pytest==3.8.2
+pytest-aiohttp==0.3.0
+python-dateutil==2.7.5
+PyYAML==3.13
+six==1.11.0
+ticket-auth==0.1.4
+transitions==0.6.8
+websockets==6.0
+yarl==1.2.6
diff --git a/run.py b/run.py
new file mode 100644
index 0000000..0a391c3
--- /dev/null
+++ b/run.py
@@ -0,0 +1,12 @@
+import importlib
+
+from aiohttp import web
+from aiohttp_auth import auth
+from core.cbpi import CraftBeerPi
+
+cbpi = CraftBeerPi()
+
+
+
+
+cbpi.start()
\ No newline at end of file
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_app.py b/tests/test_app.py
new file mode 100644
index 0000000..853472f
--- /dev/null
+++ b/tests/test_app.py
@@ -0,0 +1,70 @@
+from pprint import pprint
+
+from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
+from aiohttp import web
+import json
+
+from hbmqtt.broker import Broker
+
+from core.cbpi import CraftBeerPi
+from core.database.model import ActorModel
+
+
+class MyAppTestCase(AioHTTPTestCase):
+
+
+
+
+ async def get_application(self):
+ self.cbpi = CraftBeerPi()
+ return self.cbpi.app
+
+
+ @unittest_run_loop
+ async def test_example(self):
+
+ resp = await self.client.request("GET", "/actor/1/on")
+ print(resp.status)
+ assert resp.status == 204
+
+ resp = await self.client.request("GET", "/actor/")
+ print(resp.status)
+ assert resp.status == 200
+
+ text = await resp.json()
+ pprint(text)
+ '''
+ resp = await self.client.request("GET", "/actor/2")
+ print(resp.status)
+ assert resp.status == 200
+ text = await resp.json()
+ pprint(text)
+ '''
+ #ws = await self.client.ws_connect("/ws");
+ #await ws.send_str(json.dumps({"key": "test"}))
+
+
+'''
+ @unittest_run_loop
+ async def test_example2(self):
+ print("TEST2222")
+
+ print("CLIENT ###### ", self.client)
+
+
+
+
+ ws = await self.client.ws_connect("/ws");
+ await ws.send_str(json.dumps({"topic": "test"}))
+
+
+
+ #resp = await ws.receive()
+
+ #print("##### REPSONE", resp)
+ assert "Manuel" in await self.cbpi.actor.get_name(), "OH NOW"
+
+ await self.client.close()
+
+'''
+