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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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() + +''' +