Lots of changes

This commit is contained in:
manuel83 2019-07-27 21:08:19 +02:00
parent 0496b04608
commit 8ba6b4c506
33 changed files with 941 additions and 971 deletions

File diff suppressed because it is too large Load diff

View file

@ -45,5 +45,3 @@ class CBPiActor(CBPiExtension, metaclass=ABCMeta):
pass pass
def reprJSON(self):
return dict(state=True)

View file

@ -50,5 +50,3 @@ class CBPiExtension():
except: except:
logger.warning("Faild to load config %s/config.yaml" % path) logger.warning("Faild to load config %s/config.yaml" % path)

View file

@ -1,3 +1,4 @@
import json
import time import time
import asyncio import asyncio
import logging import logging
@ -6,22 +7,39 @@ from abc import abstractmethod,ABCMeta
class CBPiSimpleStep(metaclass=ABCMeta): class CBPiSimpleStep(metaclass=ABCMeta):
__dirty = False
managed_fields = [] managed_fields = []
_interval = 1
_max_exceptions = 2
_exception_count = 0
def __init__(self, *args, **kwargs): def __init__(self, cbpi="", managed_fields=[], id="", name="", *args, **kwargs):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
print(kwargs) self._exception_count = 0
for a in kwargs: self._interval = 0.1
super(CBPiSimpleStep, self).__setattr__(a, kwargs.get(a)) self._max_exceptions = 2
self.id = kwargs.get("id") self.__dirty = False
self.cbpi = cbpi
self.id = id
self.name = name
if managed_fields:
self.managed_fields = managed_fields
for a in managed_fields:
super(CBPiSimpleStep, self).__setattr__(a, kwargs.get(a, None))
self.is_stopped = False self.is_stopped = False
self.is_next = False self.is_next = False
self.start = time.time() self.start = time.time()
self.logger.info(self.__repr__())
def __repr__(self) -> str:
mf = {}
has_cbpi = True if self.cbpi is not None else False
for f in self.managed_fields:
mf[f] = super(CBPiSimpleStep, self).__getattribute__(f)
return json.dumps(dict(type=self.__class__.__name__, id=self.id, name=self.name, has_link_to_cbpi=has_cbpi, managed_fields=mf))
def get_status(self):
pass
def running(self): def running(self):
''' '''
Method checks if the step should continue running. Method checks if the step should continue running.
@ -39,6 +57,9 @@ class CBPiSimpleStep(metaclass=ABCMeta):
async def run(self): async def run(self):
#while self.running():
# print(".... Step %s ...." % self.id)
# await asyncio.sleep(0.1)
''' '''
This method in running in the background. It invokes the run_cycle method in the configured interval This method in running in the background. It invokes the run_cycle method in the configured interval
It checks if a managed variable was modified in the last exection cycle. If yes, the method will persisit the new value of the It checks if a managed variable was modified in the last exection cycle. If yes, the method will persisit the new value of the
@ -61,7 +82,6 @@ class CBPiSimpleStep(metaclass=ABCMeta):
await asyncio.sleep(self._interval) await asyncio.sleep(self._interval)
if self.is_dirty(): if self.is_dirty():
print("DIRTY")
# Now we have to store the managed props # Now we have to store the managed props
state = {} state = {}
for field in self.managed_fields: for field in self.managed_fields:
@ -72,6 +92,7 @@ class CBPiSimpleStep(metaclass=ABCMeta):
await self.cbpi.bus.fire("step/update") await self.cbpi.bus.fire("step/update")
self.reset_dirty() self.reset_dirty()
@abstractmethod @abstractmethod
async def run_cycle(self): async def run_cycle(self):
''' '''

View file

@ -1,5 +1,7 @@
import argparse import argparse
import logging import logging
import requests
import yaml
from cbpi.craftbeerpi import CraftBeerPi from cbpi.craftbeerpi import CraftBeerPi
import os import os
@ -13,6 +15,7 @@ def create_plugin_file():
srcfile = os.path.join(os.path.dirname(__file__), "config", "plugin_list.txt") srcfile = os.path.join(os.path.dirname(__file__), "config", "plugin_list.txt")
destfile = os.path.join(".", 'config') destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile) shutil.copy(srcfile, destfile)
print("Plugin Folder created")
def create_config_file(): def create_config_file():
import os.path import os.path
@ -20,19 +23,49 @@ def create_config_file():
srcfile = os.path.join(os.path.dirname(__file__), "config", "config.yaml") srcfile = os.path.join(os.path.dirname(__file__), "config", "config.yaml")
destfile = os.path.join(".", 'config') destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile) shutil.copy(srcfile, destfile)
print("Config Folder created")
def create_home_folder_structure(): def create_home_folder_structure():
pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(".", 'config')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(".", 'config')).mkdir(parents=True, exist_ok=True)
print("Log Folder created")
def copy_splash(): def copy_splash():
srcfile = os.path.join(os.path.dirname(__file__), "config", "splash.png") srcfile = os.path.join(os.path.dirname(__file__), "config", "splash.png")
destfile = os.path.join(".", 'config') destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile) shutil.copy(srcfile, destfile)
print("Splash Srceen created")
def check_for_setup():
if os.path.exists(os.path.join(".", "config", "config.yaml")) is False:
print("***************************************************")
print("CraftBeerPi Config File not found: %s" % os.path.join(".", "config", "config.yaml"))
print("Please run 'cbpi setup' before starting the server ")
print("***************************************************")
return False
else:
return True
def list_plugins():
print("***************************************************")
print("CraftBeerPi 4.x Plugin List")
print("***************************************************")
plugins_yaml = "https://raw.githubusercontent.com/Manuel83/craftbeerpi-plugins/master/plugins_v4.yaml"
r = requests.get(plugins_yaml)
data = yaml.load(r.content, Loader=yaml.FullLoader)
for name, value in data.items():
print(name)
def main(): def main():
parser = argparse.ArgumentParser(description='Welcome to CraftBeerPi 4') parser = argparse.ArgumentParser(description='Welcome to CraftBeerPi 4')
parser.add_argument("action", type=str, help="start,stop,restart,setup") parser.add_argument("action", type=str, help="start,stop,restart,setup,plugins")
args = parser.parse_args() args = parser.parse_args()
@ -40,15 +73,24 @@ def main():
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
if args.action == "setup": if args.action == "setup":
print("Setting up CBPi")
create_home_folder_structure() create_home_folder_structure()
create_plugin_file() create_plugin_file()
create_config_file() create_config_file()
copy_splash() copy_splash()
return
if args.action == "plugins":
list_plugins()
return
if args.action == "start": if args.action == "start":
if check_for_setup() is False:
return
cbpi = CraftBeerPi() cbpi = CraftBeerPi()
cbpi.start() cbpi.start()
return
parser.print_help() parser.print_help()

View file

@ -1,8 +1,8 @@
name: CraftBeerPi name: CraftBeerPi
version: 4.1 version: 4.0.1_alpha
#index_url: /myext #: /myext
port: 8080 port: 8080

View file

@ -1,8 +1,9 @@
import asyncio import asyncio
import logging import logging
from asyncio import Future
from cbpi.api import *
from voluptuous import Schema from voluptuous import Schema
from cbpi.api import *
from cbpi.controller.crud_controller import CRUDController from cbpi.controller.crud_controller import CRUDController
from cbpi.database.model import ActorModel from cbpi.database.model import ActorModel
@ -46,6 +47,7 @@ class ActorController(CRUDController):
self.cache[actor.id].instance = clazz(**cfg) self.cache[actor.id].instance = clazz(**cfg)
self.cache[actor.id].instance.init() self.cache[actor.id].instance.init()
await self.cbpi.bus.fire(topic="actor/%s/initialized" % actor.id, id=actor.id) await self.cbpi.bus.fire(topic="actor/%s/initialized" % actor.id, id=actor.id)
else: else:
@ -73,6 +75,7 @@ class ActorController(CRUDController):
actor_id = int(actor_id) actor_id = int(actor_id)
if actor_id in self.cache: if actor_id in self.cache:
self.logger.debug("ON %s" % actor_id) self.logger.debug("ON %s" % actor_id)
actor = self.cache[actor_id].instance actor = self.cache[actor_id].instance
actor.on(power) actor.on(power)
@ -141,7 +144,7 @@ class ActorController(CRUDController):
:param m: :param m:
:return: :return:
''' '''
print("INIT ACTION")
await self._init_actor(m) await self._init_actor(m)
pass pass

View file

@ -6,7 +6,7 @@ from cbpi.database.model import ConfigModel
from cbpi.utils import load_config from cbpi.utils import load_config
class ConfigController(): class ConfigController:
''' '''
The main actor controller The main actor controller
''' '''
@ -24,14 +24,13 @@ class ConfigController():
async def init(self): async def init(self):
this_directory = os.path.dirname(__file__) this_directory = os.path.dirname(__file__)
self.static = load_config("./config/config.yaml") self.static = load_config("./config/config.yaml")
items = await self.model.get_all() items = await self.model.get_all()
for key, value in items.items(): for key, value in items.items():
self.cache[value.name] = value self.cache[value.name] = value
def get(self, name, default=None): def get(self, name, default=None):
self.logger.info("GET CONFIG VALUE %s (default %s)" % (name, default)) self.logger.debug("GET CONFIG VALUE %s (default %s)" % (name, default))
if name in self.cache and self.cache[name].value is not None: if name in self.cache and self.cache[name].value is not None:
return self.cache[name].value return self.cache[name].value
else: else:

View file

@ -70,7 +70,7 @@ class CRUDController(metaclass=ABCMeta):
await self._pre_add_callback(data) await self._pre_add_callback(data)
print("INSSERT ADD", data)
m = await self.model.insert(**data) m = await self.model.insert(**data)
@ -96,10 +96,12 @@ class CRUDController(metaclass=ABCMeta):
''' '''
self.logger.debug("Update Sensor %s - %s " % (id, data)) self.logger.debug("Update Sensor %s - %s " % (id, data))
id = int(id) id = int(id)
if id not in self.cache: if self.caching is True and id not in self.cache:
self.logger.debug("Sensor %s Not in Cache" % (id,))
self.logger.debug("%s %s Not in Cache" % (self.name, id))
raise CBPiException("%s with id %s not found" % (self.name,id)) raise CBPiException("%s with id %s not found" % (self.name,id))
data["id"] = id data["id"] = id
@ -115,14 +117,10 @@ class CRUDController(metaclass=ABCMeta):
self.cache[id].__dict__.update(**data) self.cache[id].__dict__.update(**data)
m = self.cache[id] = await self.model.update(**self.cache[id].__dict__) m = self.cache[id] = await self.model.update(**self.cache[id].__dict__)
await self._post_update_callback(self.cache[id]) await self._post_update_callback(self.cache[id])
else: else:
m = await self.model.update(**data) m = await self.model.update(**data)
return m return m
async def _pre_delete_callback(self, m): async def _pre_delete_callback(self, m):
''' '''

View file

@ -1,7 +1,5 @@
import logging import logging
from voluptuous import Schema, MultipleInvalid
from cbpi.controller.crud_controller import CRUDController from cbpi.controller.crud_controller import CRUDController
from cbpi.database.model import DashboardModel, DashboardContentModel from cbpi.database.model import DashboardModel, DashboardContentModel

View file

@ -4,6 +4,7 @@ import logging
from cbpi.job.aiohttp import setup, get_scheduler_from_app from cbpi.job.aiohttp import setup, get_scheduler_from_app
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class JobController(object): class JobController(object):
def __init__(self, cbpi): def __init__(self, cbpi):

View file

@ -3,7 +3,7 @@ from cbpi.api import *
from cbpi.controller.crud_controller import CRUDController from cbpi.controller.crud_controller import CRUDController
from cbpi.database.model import KettleModel from cbpi.database.model import KettleModel
from cbpi.job.aiohttp import get_scheduler_from_app from cbpi.job.aiohttp import get_scheduler_from_app
import logging
class KettleController(CRUDController): class KettleController(CRUDController):
''' '''
@ -15,6 +15,7 @@ class KettleController(CRUDController):
super(KettleController, self).__init__(cbpi) super(KettleController, self).__init__(cbpi)
self.cbpi = cbpi self.cbpi = cbpi
self.types = {} self.types = {}
self.logger = logging.getLogger(__name__)
self.cbpi.register(self) self.cbpi.register(self)
async def init(self): async def init(self):
@ -58,7 +59,7 @@ class KettleController(CRUDController):
kettle = self.cache[int(kid)] kettle = self.cache[int(kid)]
kettle.instance = None kettle.instance = None
kettle.state = False kettle.state = False
print("FIRE")
await self.cbpi.bus.fire(topic="kettle/%s/logic/stop" % kid) await self.cbpi.bus.fire(topic="kettle/%s/logic/stop" % kid)
@on_event(topic="kettle/+/automatic") @on_event(topic="kettle/+/automatic")
@ -73,7 +74,7 @@ class KettleController(CRUDController):
''' '''
id = int(id) id = int(id)
print("K", id)
if id in self.cache: if id in self.cache:
kettle = self.cache[id] kettle = self.cache[id]
@ -84,7 +85,7 @@ class KettleController(CRUDController):
if kettle.instance is None: if kettle.instance is None:
print("start")
if kettle.logic in self.types: if kettle.logic in self.types:
clazz = self.types.get("CustomKettleLogic")["class"] clazz = self.types.get("CustomKettleLogic")["class"]
cfg = kettle.config.copy() cfg = kettle.config.copy()
@ -104,13 +105,9 @@ class KettleController(CRUDController):
def _is_logic_running(self, kettle_id): def _is_logic_running(self, kettle_id):
scheduler = get_scheduler_from_app(self.cbpi.app) scheduler = get_scheduler_from_app(self.cbpi.app)
async def heater_on(self, id): async def heater_on(self, id):
''' '''
Convenience Method to switch the heater of a kettle on Convenience Method to switch the heater of a kettle on
:param id: the kettle id :param id: the kettle id
:return: (boolean, string) :return: (boolean, string)
''' '''

View file

@ -1,3 +1,4 @@
import asyncio
import logging import logging
import os import os
from importlib import import_module from importlib import import_module
@ -5,15 +6,16 @@ from importlib import import_module
import aiohttp import aiohttp
import yaml import yaml
from aiohttp import web from aiohttp import web
from cbpi.api import *
from cbpi.api import *
from cbpi.utils.utils import load_config, json_dumps from cbpi.utils.utils import load_config, json_dumps
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import subprocess
import sys
class PluginController(): class PluginController():
modules = {} modules = {}
types = {} types = {}
@ -25,9 +27,7 @@ class PluginController():
async def load_plugin_list(self): async def load_plugin_list(self):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get('https://raw.githubusercontent.com/Manuel83/craftbeerpi-plugins/master/plugins_v4.yaml') as resp: async with session.get('https://raw.githubusercontent.com/Manuel83/craftbeerpi-plugins/master/plugins_v4.yaml') as resp:
if (resp.status == 200):
if(resp.status == 200):
data = yaml.load(await resp.text()) data = yaml.load(await resp.text())
return data return data
@ -44,7 +44,7 @@ class PluginController():
data = load_config(os.path.join(this_directory, "../extension/%s/config.yaml" % filename)) data = load_config(os.path.join(this_directory, "../extension/%s/config.yaml" % filename))
if(data.get("version") == 4): if (data.get("version") == 4):
self.modules[filename] = import_module("cbpi.extension.%s" % (filename)) self.modules[filename] = import_module("cbpi.extension.%s" % (filename))
self.modules[filename].setup(self.cbpi) self.modules[filename].setup(self.cbpi)
@ -55,6 +55,7 @@ class PluginController():
except Exception as e: except Exception as e:
print(e)
logger.error(e) logger.error(e)
def load_plugins_from_evn(self): def load_plugins_from_evn(self):
@ -63,7 +64,6 @@ class PluginController():
this_directory = os.path.dirname(__file__) this_directory = os.path.dirname(__file__)
with open("./config/plugin_list.txt") as f: with open("./config/plugin_list.txt") as f:
plugins = f.read().splitlines() plugins = f.read().splitlines()
plugins = list(set(plugins)) plugins = list(set(plugins))
@ -74,15 +74,42 @@ class PluginController():
self.modules[p] = import_module(p) self.modules[p] = import_module(p)
self.modules[p].setup(self.cbpi) self.modules[p].setup(self.cbpi)
logger.info("Plugin %s loaded successfully" % p) logger.info("Plugin %s loaded successfully" % p)
except Exception as e: except Exception as e:
logger.error("FAILED to load plugin %s " % p) logger.error("FAILED to load plugin %s " % p)
logger.error(e) logger.error(e)
@on_event("job/plugins_install/done")
async def done(self, **kwargs):
self.cbpi.notify(key="p", message="Plugin installed ", type="success")
print("DONE INSTALL PLUGIN", kwargs)
@request_mapping(path="/plugins", method="GET", auth_required=False) @request_mapping(path="/install", method="GET", auth_required=False)
async def install_plugin(self, request):
"""
---
description: Install Plugin
tags:
- Plugin
produces:
- application/json
responses:
"204":
description: successful operation. Return "pong" text
"405":
description: invalid HTTP Method
"""
async def install(name):
await asyncio.sleep(5)
subprocess.call([sys.executable, "-m", "pip", "install", name])
print("OK")
await self.cbpi.job.start_job(install('requests'), "requests", "plugins_install")
return web.Response(status=204)
@request_mapping(path="/list", method="GET", auth_required=False)
async def get_plugins(self, request): async def get_plugins(self, request):
""" """
--- ---
@ -99,8 +126,6 @@ class PluginController():
""" """
return web.json_response(await self.load_plugin_list(), dumps=json_dumps) return web.json_response(await self.load_plugin_list(), dumps=json_dumps)
def register(self, name, clazz) -> None: def register(self, name, clazz) -> None:
''' '''
Register a new actor type Register a new actor type
@ -110,7 +135,7 @@ class PluginController():
''' '''
logger.info("Register %s Class %s" % (name, clazz.__name__)) logger.info("Register %s Class %s" % (name, clazz.__name__))
if issubclass(clazz, CBPiActor): if issubclass(clazz, CBPiActor):
#self.cbpi.actor.types[name] = {"class": clazz, "config": self._parse_props(clazz)} # self.cbpi.actor.types[name] = {"class": clazz, "config": self._parse_props(clazz)}
self.cbpi.actor.types[name] = self._parse_props(clazz) self.cbpi.actor.types[name] = self._parse_props(clazz)
if issubclass(clazz, CBPiSensor): if issubclass(clazz, CBPiSensor):
@ -131,7 +156,7 @@ class PluginController():
result = {"name": name, "class": cls, "properties": [], "actions": []} result = {"name": name, "class": cls, "properties": [], "actions": []}
tmpObj = cls() tmpObj = cls(cbpi=None, managed_fields=None)
members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")] members = [attr for attr in dir(tmpObj) if not callable(getattr(tmpObj, attr)) and not attr.startswith("__")]
for m in members: for m in members:
if isinstance(tmpObj.__getattribute__(m), Property.Number): if isinstance(tmpObj.__getattribute__(m), Property.Number):

View file

@ -41,7 +41,9 @@ class SensorController(CRUDController):
self.cache[sensor.id].instance = clazz(**cfg) self.cache[sensor.id].instance = clazz(**cfg)
self.cache[sensor.id].instance.init() self.cache[sensor.id].instance.init()
scheduler = get_scheduler_from_app(self.cbpi.app) scheduler = get_scheduler_from_app(self.cbpi.app)
self.cache[sensor.id].instance.job = await scheduler.spawn(self.cache[sensor.id].instance.run(self.cbpi), sensor.name, "sensor")
self.cache[sensor.id].instance.job = await self.cbpi.job.start_job(self.cache[sensor.id].instance.run(self.cbpi), sensor.name, "sensor")
await self.cbpi.bus.fire(topic="sensor/%s/initialized" % sensor.id, id=sensor.id) await self.cbpi.bus.fire(topic="sensor/%s/initialized" % sensor.id, id=sensor.id)
else: else:
self.logger.error("Sensor type '%s' not found (Available Sensor Types: %s)" % (sensor.type, ', '.join(self.types.keys()))) self.logger.error("Sensor type '%s' not found (Available Sensor Types: %s)" % (sensor.type, ', '.join(self.types.keys())))

View file

@ -7,16 +7,13 @@ from cbpi.database.model import StepModel
class StepController(CRUDController): class StepController(CRUDController):
'''
The Step Controller. This controller is responsible to start and stop the brewing steps.
'''
model = StepModel
def __init__(self, cbpi): def __init__(self, cbpi):
super(StepController, self).__init__(cbpi) self.model = StepModel
self.caching = False
self.caching = True
self.is_stopping = False self.is_stopping = False
self.cbpi = cbpi self.cbpi = cbpi
self.current_task = None self.current_task = None
@ -28,36 +25,11 @@ class StepController(CRUDController):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.starttime = None self.starttime = None
async def init(self): def is_running(self):
''' if self.current_step is not None:
Initializer of the the Step Controller. return True
:return:
'''
await super(StepController, self).init()
step = await self.model.get_by_state('A')
# We have an active step
if step is not None:
# get the type
step_type = self.types.get(step.type)
if step_type is None:
# step type not found. cant restart step
return
if step.stepstate is not None:
cfg = step.stepstate.copy()
else: else:
cfg = {} return False
cfg.update(dict(cbpi=self.cbpi, id=step.id, managed_fields=self._get_manged_fields_as_array(step_type)))
self.current_step = step_type["class"](**cfg)
self.current_job = await self.cbpi.job.start_job(self.current_step.run(), step.name, "step")
async def get_state(self):
return dict(items=await self.get_all(),types=self.types,is_running=self.is_running(),current_step=self.current_step)
@on_event("step/action") @on_event("step/action")
async def handle_action(self, action, **kwargs): async def handle_action(self, action, **kwargs):
@ -74,162 +46,154 @@ class StepController(CRUDController):
if self.current_step is not None: if self.current_step is not None:
self.current_step.__getattribute__(action)() self.current_step.__getattribute__(action)()
@on_event("step/next")
async def next(self, **kwargs):
'''
Event Handler for "step/next".
It start the next step
:param kwargs:
:return: None
'''
self.starttime = time.time()
if self.current_step is not None and self.is_next is False:
self.logger.info("Request Next Step to start. Stopping current step")
self.is_next = True
self.current_step.stop()
else:
self.logger.info("Can Start Next")
@on_event("job/step/done")
async def _step_done(self, topic, **kwargs):
'''
Event Handler for "step/+/done".
Starts the next step
:param topic:
:param kwargs:
:return:
'''
# SHUTDONW DO NOTHING
self.logger.info("HANDLE DONE IS SHUTDONW %s IS STOPPING %s IS NEXT %s" % ( self.cbpi.shutdown, self.is_stopping, self.is_next))
if self.cbpi.shutdown:
return
if self.is_stopping:
self.is_stopping = False
return
self.is_next = False
if self.current_step is not None:
await self.model.update_state(self.current_step.id, "D", int(time.time()))
self.current_step = None
await self.start()
def _get_manged_fields_as_array(self, type_cfg): def _get_manged_fields_as_array(self, type_cfg):
result = [] result = []
for f in type_cfg.get("properties"): for f in type_cfg.get("properties"):
result.append(f.get("name")) result.append(f.get("name"))
return result return result
def is_running(self): async def init(self):
if self.current_step is not None:
return True # load all steps into cache
self.cache = await self.model.get_all()
for key, value in self.cache.items():
# get step type as string
step_type = self.types.get(value.type)
# if step has state
if value.stepstate is not None:
cfg = value.stepstate.copy()
else: else:
return False cfg = {}
# set managed fields
cfg.update(dict(cbpi=self.cbpi, id=value.id, managed_fields=self._get_manged_fields_as_array(step_type)))
if value.config is not None:
# set config values
cfg.update(**value.config)
# create step instance
value.instance = step_type["class"](**cfg)
async def get_all(self, force_db_update: bool = True):
return self.cache
def find_next_step(self):
# filter
inactive_steps = {k: v for k, v in self.cache.items() if v.state == 'I'}
if len(inactive_steps) == 0:
return None
return min(inactive_steps, key=lambda x: inactive_steps[x].order)
@on_event("step/start") @on_event("step/start")
async def start(self, **kwargs): async def start(self, **kwargs):
'''
Start the first step
:return:None
'''
if self.is_running() is False: if self.is_running() is False:
next_step = await self.model.get_by_state("I") next_step_id = self.find_next_step()
if next_step_id:
if next_step is not None: next_step = self.cache[next_step_id]
step_type = self.types[next_step.type]
print(step_type)
managed_fields = self._get_manged_fields_as_array(step_type)
config = dict(cbpi=self.cbpi, id=next_step.id, name=next_step.name, managed_fields=managed_fields)
config.update(**next_step.config)
self._set_default(step_type, config, managed_fields)
self.current_step = step_type["class"](**config)
next_step.state = 'A' next_step.state = 'A'
next_step.stepstate = next_step.config next_step.stepstate = next_step.config
next_step.start = int(time.time()) next_step.start = int(time.time())
await self.model.update(**next_step.__dict__) await self.model.update(**next_step.__dict__)
if self.starttime is not None: self.current_step = next_step
end = time.time() # start the step job
d = end - self.starttime self.current_job = await self.cbpi.job.start_job(self.current_step.instance.run(), next_step.name, "step")
print("DURATION", d)
else:
print("NORMAL START")
self.current_job = await self.cbpi.job.start_job(self.current_step.run(), next_step.name, "step")
await self.cbpi.bus.fire("step/%s/started" % self.current_step.id) await self.cbpi.bus.fire("step/%s/started" % self.current_step.id)
else: else:
await self.cbpi.bus.fire("step/brewing/finished") await self.cbpi.bus.fire("step/brewing/finished")
else: else:
self.logger.error("Process Already Running") self.logger.error("Process Already Running")
print("----------- END")
def _set_default(self, step_type, config, managed_fields): async def next(self, **kwargs):
for key in managed_fields:
if key not in config:
config[key] = None
if self.current_step is not None:
self.is_next = True
self.current_step.instance.stop()
@on_event("job/step/done")
async def step_done(self, **kwargs):
if self.cbpi.shutdown:
return
if self.is_stopping:
self.is_stopping = False
return
if self.current_step is not None:
self.current_step.state = "D"
await self.model.update_state(self.current_step.id, "D", int(time.time()))
self.current_step = None
# start the next step
await self.start()
@on_event("step/stop") @on_event("step/stop")
async def stop(self, **kwargs): async def stop_all(self, **kwargs):
# RESET DB
if self.current_step is not None:
self.current_step.stop()
self.is_stopping = True
self.current_step = None
await self.model.reset_all_steps() await self.model.reset_all_steps()
# RELOAD all Steps from DB into cache and initialize Instances
await self.init()
await self.cbpi.bus.fire("step/brewing/stopped") await self.cbpi.bus.fire("step/brewing/stopped")
@on_event("step/reset")
async def handle_reset(self, **kwargs):
'''
Event Handler for "step/reset".
Resets the current step
:param kwargs:
:return: None
'''
if self.current_step is not None:
await self.stop()
self.current_step = None
self.is_stopping = True
await self.model.reset_all_steps()
@on_event("step/clear") @on_event("step/clear")
async def clear_all(self, **kwargs): async def clear_all(self, **kwargs):
await self.model.delete_all() await self.model.delete_all()
self.cbpi.notify(key="Steps Cleared", message="Steps cleared successfully", type="success") self.cbpi.notify(key="Steps Cleared", message="Steps cleared successfully", type="success")
@on_event("step/sort")
async def sort(self, topic, data, **kwargs):
await self.model.sort(data)
async def _pre_add_callback(self, data): async def _pre_add_callback(self, data):
order = await self.model.get_max_order() order = await self.model.get_max_order()
data["order"] = 1 if order is None else order + 1 data["order"] = 1 if order is None else order + 1
data["state"] = "I" data["state"] = "I"
data["stepstate"] = {}
return await super()._pre_add_callback(data) return await super()._pre_add_callback(data)
async def init_step(self, value: StepModel):
step_type = self.types.get(value.type)
# if step has state
if value.stepstate is not None:
cfg = value.stepstate.copy()
else:
cfg = {}
# set managed fields
cfg.update(dict(cbpi=self.cbpi, id=value.id, managed_fields=self._get_manged_fields_as_array(step_type)))
# set config values
cfg.update(**value.config)
# create step instance
value.instance = step_type["class"](**cfg)
return value
async def _post_add_callback(self, m: StepModel) -> None:
self.cache[m.id] = await self.init_step(m)
async def _post_update_callback(self, m: StepModel) -> None:
'''
:param m: step model
:return: None
'''
self.cache[m.id] = await self.init_step(m)
@on_event("step/sort")
async def sort(self, topic: 'str', data: 'dict', **kwargs):
# update order in cache
for id, order in data.items():
self.cache[int(id)].order = order
# update oder in database
await self.model.sort(data)
async def get_state(self):
return dict(items=await self.get_all(),types=self.types,is_running=self.is_running(),current_step=self.current_step)

View file

@ -52,6 +52,9 @@ async def error_middleware(request, handler):
return web.json_response(status=500, data={'error': message}) return web.json_response(status=500, data={'error': message})
except MultipleInvalid as ex: except MultipleInvalid as ex:
return web.json_response(status=500, data={'error': str(ex)}) return web.json_response(status=500, data={'error': str(ex)})
except Exception as ex:
return web.json_response(status=500, data={'error': str(ex)})
return web.json_response(status=500, data={'error': message}) return web.json_response(status=500, data={'error': message})
class CraftBeerPi(): class CraftBeerPi():
@ -187,7 +190,7 @@ class CraftBeerPi():
api_version=self.static_config.get("version", ""), api_version=self.static_config.get("version", ""),
contact="info@craftbeerpi.com") contact="info@craftbeerpi.com")
def notify(self, key, message, type="info"): def notify(self, key: str, message: str, type: str = "info") -> None:
''' '''
This is a convinience method to send notification to the client This is a convinience method to send notification to the client
@ -216,7 +219,7 @@ class CraftBeerPi():
if url is not None: if url is not None:
raise web.HTTPFound(url) raise web.HTTPFound(url)
else: else:
return web.Response(text="Hello, world") return web.Response(text="Hello from CraftbeerPi!")
self.app.add_routes([web.get('/', http_index)]) self.app.add_routes([web.get('/', http_index)])

View file

@ -20,6 +20,13 @@ class ActorModel(DBModel):
'config': dict 'config': dict
} }
def to_json(self):
data = dict(**self.__dict__)
if hasattr(self,"instance"):
data["state"] = self.instance.get_state()
del data["instance"]
return data
class SensorModel(DBModel): class SensorModel(DBModel):
__fields__ = ["name", "type", "config"] __fields__ = ["name", "type", "config"]
@ -32,6 +39,15 @@ class SensorModel(DBModel):
'config': dict 'config': dict
} }
def to_json(self):
data = dict(**self.__dict__)
if hasattr(self,"instance"):
data["value"] = self.instance.get_value()
data["unit"] = self.instance.get_unit()
data["state"] = self.instance.get_state()
del data["instance"]
return data
class ConfigModel(DBModel): class ConfigModel(DBModel):
__fields__ = ["type", "value", "description", "options"] __fields__ = ["type", "value", "description", "options"]
@ -54,7 +70,7 @@ class StepModel(DBModel):
@classmethod @classmethod
async def update_step_state(cls, step_id, state): async def update_step_state(cls, step_id, state):
print("NOW UPDATE", state)
async with aiosqlite.connect(DATABASE_FILE) as db: async with aiosqlite.connect(DATABASE_FILE) as db:
cursor = await db.execute("UPDATE %s SET stepstate = ? WHERE id = ?" % cls.__table_name__, (json.dumps(state), step_id)) cursor = await db.execute("UPDATE %s SET stepstate = ? WHERE id = ?" % cls.__table_name__, (json.dumps(state), step_id))
await db.commit() await db.commit()
@ -93,7 +109,7 @@ class StepModel(DBModel):
async with aiosqlite.connect(DATABASE_FILE) as db: async with aiosqlite.connect(DATABASE_FILE) as db:
for key, value in new_order.items(): for key, value in new_order.items():
print("ORDER", key, value)
await db.execute("UPDATE %s SET '%s' = ? WHERE id = ?" % (cls.__table_name__, "order"), (value, key)) await db.execute("UPDATE %s SET '%s' = ? WHERE id = ?" % (cls.__table_name__, "order"), (value, key))
await db.commit() await db.commit()
@ -111,6 +127,19 @@ class StepModel(DBModel):
else: else:
return 0 return 0
def to_json(self):
data = dict(**self.__dict__)
if hasattr(self,"instance"):
data["state_msg"] = self.instance.get_status()
del data["instance"]
return data
def __str__(self):
return "%s, %s, %s, %s, %s" % (self.name, self.start, self.end, self.state, self.order)
def __repr__(self) -> str:
return "Steps(%s, %s, %s, %s, %s)" % (self.name, self.start, self.end, self.state, self.order)
class DashboardModel(DBModel): class DashboardModel(DBModel):
__fields__ = ["name"] __fields__ = ["name"]

View file

@ -167,3 +167,7 @@ class DBModel(object):
for idx, col in enumerate(cursor.description): for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx] d[col[0]] = row[idx]
return d return d
def to_json(self):
return self.__dict__

View file

@ -24,7 +24,7 @@ class CustomActor(CBPiActor):
# Custom property which can be configured by the user # Custom property which can be configured by the user
@action("test", parameters={}) @action("test", parameters={})
def action1(self): def action1(self):
print("EOOOHOOO")
pass pass

View file

@ -3,6 +3,7 @@ import asyncio
from aiohttp import web from aiohttp import web
from cbpi.api import * from cbpi.api import *
import re
class CustomSensor(CBPiSensor): class CustomSensor(CBPiSensor):
@ -61,6 +62,7 @@ class HTTPSensor(CBPiSensor):
def init(self): def init(self):
super().init() super().init()
self.state = True self.state = True
def get_state(self): def get_state(self):
@ -80,10 +82,12 @@ class HTTPSensor(CBPiSensor):
try: try:
value = cache.pop(self.key, None) value = cache.pop(self.key, None)
print("HTTP SENSOR READ", value)
if value is not None: if value is not None:
self.log_data(value) self.log_data(value)
await cbpi.bus.fire("sensor/%s" % self.id, value=value) await cbpi.bus.fire("sensor/%s/data" % self.id, value=value)
except: except Exception as e:
print(e)
pass pass
class HTTPSensorEndpoint(CBPiExtension): class HTTPSensorEndpoint(CBPiExtension):
@ -95,6 +99,7 @@ class HTTPSensorEndpoint(CBPiExtension):
:param cbpi: :param cbpi:
''' '''
self.pattern_check = re.compile("^[a-zA-Z0-9,.]{0,10}$")
self.cbpi = cbpi self.cbpi = cbpi
# register component for http, events # register component for http, events
@ -125,10 +130,17 @@ class HTTPSensorEndpoint(CBPiExtension):
"204": "204":
description: successful operation description: successful operation
""" """
print("HALLO")
global cache global cache
key = request.match_info['key'] key = request.match_info['key']
value = request.match_info['value'] value = request.match_info['value']
if self.pattern_check.match(key) is None:
return web.json_response(status=422, data={'error': "Key not matching pattern ^[a-zA-Z0-9,.]{0,10}$"})
if self.pattern_check.match(value) is None:
return web.json_response(status=422, data={'error': "Data not matching pattern ^[a-zA-Z0-9,.]{0,10}$"})
print("HTTP SENSOR ", key, value)
cache[key] = value cache[key] = value
return web.Response(status=204) return web.Response(status=204)

View file

@ -9,15 +9,23 @@ class CustomStepCBPi(CBPiSimpleStep):
name1 = Property.Number(label="Test", configurable=True) name1 = Property.Number(label="Test", configurable=True)
timer_end = Property.Number(label="Test", default_value=None) timer_end = Property.Number(label="Test", default_value=None)
temp = Property.Number(label="Temperature", default_value=50, configurable=True) temp = Property.Number(label="Temperature", default_value=50, configurable=True)
i = 0 i = 0
@action(key="name", parameters=None) @action(key="name", parameters=None)
def test(self, **kwargs): def test(self, **kwargs):
self.name="WOOHOO" self.name="WOOHOO"
def get_status(self):
return "Status: %s Temp" % self.temp
async def run_cycle(self): async def run_cycle(self):
self.next()
'''
print("RUN", self.name1, self.managed_fields, self.timer_end) print("RUN", self.name1, self.managed_fields, self.timer_end)
self.i = self.i + 1 self.i = self.i + 1
@ -26,7 +34,7 @@ class CustomStepCBPi(CBPiSimpleStep):
if self.i == 10: if self.i == 10:
self.next() self.next()
'''
#self.cbpi.notify(key="step", message="HELLO FROM STEP") #self.cbpi.notify(key="step", message="HELLO FROM STEP")

View file

@ -30,7 +30,7 @@ class ConfigHttpEndpoints(HttpCrudEndpoints):
"204": "204":
description: successful operation description: successful operation
""" """
print("HALLO PARA")
name = request.match_info['name'] name = request.match_info['name']
data = await request.json() data = await request.json()
await self.controller.set(name=name, value=data.get("value")) await self.controller.set(name=name, value=data.get("value"))

View file

@ -22,7 +22,7 @@ class HttpCrudEndpoints():
@request_mapping(path="/", auth_required=False) @request_mapping(path="/", auth_required=False)
async def http_get_all(self, request): async def http_get_all(self, request):
return web.json_response(await self.controller.get_all(force_db_update=True), dumps=json_dumps) return web.json_response(await self.controller.get_all(), dumps=json_dumps)
@request_mapping(path="/{id:\d+}", auth_required=False) @request_mapping(path="/{id:\d+}", auth_required=False)
async def http_get_one(self, request): async def http_get_one(self, request):
@ -39,6 +39,8 @@ class HttpCrudEndpoints():
async def http_update(self, request): async def http_update(self, request):
id = int(request.match_info['id']) id = int(request.match_info['id'])
data = await request.json() data = await request.json()
obj = await self.controller.update(id, data) obj = await self.controller.update(id, data)
return web.json_response(obj, dumps=json_dumps) return web.json_response(obj, dumps=json_dumps)

View file

@ -257,6 +257,6 @@ class DashBoardHttpEndpoints(HttpCrudEndpoints):
schema = Schema({"id": int, "x": int, "y": int}) schema = Schema({"id": int, "x": int, "y": int})
schema(data) schema(data)
content_id = int(request.match_info['content_id']) content_id = int(request.match_info['content_id'])
print("MOVE",content_id)
return web.json_response(await self.cbpi.dashboard.move_content(content_id,data["x"], data["y"]), dumps=json_dumps) return web.json_response(await self.cbpi.dashboard.move_content(content_id,data["x"], data["y"]), dumps=json_dumps)

View file

@ -28,24 +28,16 @@ class StepHttpEndpoints(HttpCrudEndpoints):
@request_mapping(path="/", auth_required=False) @request_mapping(path="/", auth_required=False)
async def http_get_all(self, request): async def http_get_all(self, request):
""" """
--- ---
description: Switch step on description: Get all steps
tags: tags:
- Step - Step
parameters:
- name: "id"
in: "path"
description: "step ID"
required: true
type: "integer"
format: "int64"
responses: responses:
"204": "204":
description: successful operation description: successful operation
"405":
description: invalid HTTP Met
""" """
return await super().http_get_all(request) return await super().http_get_all(request)

View file

@ -114,6 +114,7 @@ class Job:
self._started.set_result(None) self._started.set_result(None)
def _done_callback(self, task): def _done_callback(self, task):
scheduler = self._scheduler scheduler = self._scheduler
scheduler._done(self) scheduler._done(self)
try: try:

View file

@ -1,38 +1,13 @@
from json import JSONEncoder from json import JSONEncoder
from cbpi.database.model import ActorModel, SensorModel
class ComplexEncoder(JSONEncoder): class ComplexEncoder(JSONEncoder):
def default(self, obj): def default(self, obj):
from cbpi.database.orm_framework import DBModel
try: try:
if isinstance(obj, ActorModel): if hasattr(obj, "to_json") and callable(getattr(obj, "to_json")):
return obj.to_json()
data = dict(**obj.__dict__)
data["state"] = obj.instance.get_state()
del data["instance"]
return data
elif isinstance(obj, SensorModel):
data = dict(**obj.__dict__)
data["value"] = value=obj.instance.get_value()
data["unit"] = value = obj.instance.get_unit()
data["state"] = obj.instance.get_state()
del data["instance"]
return data
#elif callable(getattr(obj, "reprJSON")):
# return obj.reprJSON()
elif isinstance(obj, DBModel):
return obj.__dict__
#elif hasattr(obj, "callback"):
# return obj()
else: else:
raise TypeError() raise TypeError()
except Exception as e: except Exception as e:

11
cheat_sheet.txt Normal file
View file

@ -0,0 +1,11 @@
#clean
python3.7 setup.py clean --all
#build
python setup.py sdist
#Upload
twine upload dist/*

Binary file not shown.

View file

@ -1,13 +1,8 @@
import re
pattern = "(actor)\/([\d])\/(on|toggle|off)$"
p = re.compile(pattern)
result = p.match("actor/1/toggle")
print(result, result.group(3))
def test123(name: str) -> str:
print(name)
test123("HALLO")

View file

@ -1,8 +1,8 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
setup(name='cbpi', setup(name='cbpi',
version='4.0.0.1', version='4.0.0.4',
description='CraftBeerPi API', description='CraftBeerPi',
author='Manuel Fritsch', author='Manuel Fritsch',
author_email='manuel@craftbeerpi.com', author_email='manuel@craftbeerpi.com',
url='http://web.craftbeerpi.com', url='http://web.craftbeerpi.com',
@ -23,6 +23,7 @@ setup(name='cbpi',
"aiojobs==0.2.2", "aiojobs==0.2.2",
"aiosqlite==0.7.0", "aiosqlite==0.7.0",
"cryptography==2.3.1", "cryptography==2.3.1",
"requests==2.22.0",
"voluptuous==0.11.5", "voluptuous==0.11.5",
"pyfiglet==0.7.6" "pyfiglet==0.7.6"
], ],

View file

@ -68,9 +68,9 @@ class KettleTestCase(AioHTTPTestCase):
assert resp.status == 200 assert resp.status == 200
m = await resp.json() m = await resp.json()
print(m)
sensor_id = m["id"]
sensor_id = m["id"]
print("KETTLE", m["id"], m)
# Get sensor # Get sensor
resp = await self.client.get(path="/kettle/%s" % sensor_id) resp = await self.client.get(path="/kettle/%s" % sensor_id)
assert resp.status == 200 assert resp.status == 200

View file

@ -17,9 +17,11 @@ class StepTestCase(AioHTTPTestCase):
async def test_get(self): async def test_get(self):
resp = await self.client.request("GET", "/step") resp = await self.client.request("GET", "/step")
print(resp)
assert resp.status == 200 assert resp.status == 200
resp = await self.client.request("GET", "/step/types") resp = await self.client.request("GET", "/step/types")
print(resp)
assert resp.status == 200 assert resp.status == 200
@ -28,14 +30,15 @@ class StepTestCase(AioHTTPTestCase):
data = { data = {
"name": "Test", "name": "Test",
"type": "CustomStepCBPi", "type": "CustomStepCBPi",
"config": {}
} }
# Add new sensor # Add new step
resp = await self.client.post(path="/step/", json=data) resp = await self.client.post(path="/step/", json=data)
assert resp.status == 200 assert resp.status == 200
m = await resp.json() m = await resp.json()
print(m) print("Step", m)
sensor_id = m["id"] sensor_id = m["id"]
# Get sensor # Get sensor
@ -68,31 +71,37 @@ class StepTestCase(AioHTTPTestCase):
if future in done: if future in done:
pass pass
@unittest_run_loop @unittest_run_loop
async def test_process(self): async def test_process(self):
await self.cbpi.step.stop()
with mock.patch.object(self.cbpi.step, 'start', wraps=self.cbpi.step.start) as mock_obj: step_ctlr = self.cbpi.step
await step_ctlr.clear_all()
await step_ctlr.add(**{"name": "Kettle1", "type": "CustomStepCBPi", "config": {"name1": "1", "temp": 99}})
await step_ctlr.add(**{"name": "Kettle1", "type": "CustomStepCBPi", "config": {"name1": "1", "temp": 99}})
await step_ctlr.add(**{"name": "Kettle1", "type": "CustomStepCBPi", "config": {"name1": "1", "temp": 99}})
await step_ctlr.stop_all()
future = self.create_wait_callback("step/+/started") future = self.create_wait_callback("step/+/started")
await self.cbpi.step.start() await step_ctlr.start()
await self.wait(future) await self.wait(future)
for i in range(len(step_ctlr.cache)-1):
future = self.create_wait_callback("step/+/started") future = self.create_wait_callback("step/+/started")
await self.cbpi.step.next() await step_ctlr.next()
await self.wait(future) await self.wait(future)
future = self.create_wait_callback("step/+/started") await self.print_steps()
await self.cbpi.step.next()
await self.wait(future)
future = self.create_wait_callback("step/+/started")
await self.cbpi.step.next()
await self.wait(future)
future = self.create_wait_callback("job/step/done")
await self.cbpi.step.stop()
await self.wait(future)
print("COUNT", mock_obj.call_count)
print("ARGS", mock_obj.call_args_list)
async def print_steps(self):
s = await self.cbpi.step.get_all()
print(s)
for k, v in s.items():
print(k, v.to_json())