diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 56b176b..17a4feb 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -3,29 +3,39 @@ - + + + + + + + + - + - + + - + + - - - + + + + - - - - - - - - - - - + + + - + + + + + + + + + + + + + + + @@ -78,6 +94,12 @@ + + + + + + @@ -658,6 +501,13 @@ + + + + + + + @@ -665,13 +515,32 @@ - - - - - - - + + + - - - - - - - + + + + + + + + + + - - - + + + + - + + + + - - + + @@ -916,7 +794,15 @@ - + + + + + + + + + 1541288846149 @@ -936,39 +822,39 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - @@ -980,35 +866,35 @@ - - + - - - - - - - - - - + + + + + + + + + - - + + - + - + + + @@ -1035,9 +921,14 @@ - + - - - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - - - - - - + + - - - - - - - - - - - - - + + + + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + - - - - + - - - - - - - - - - - - - - - - + + + + + - - + + - + + + + - - + + + + + + + + + + + + @@ -1240,102 +1133,93 @@ + + + + + + + + + + - - - - - - - - - + + - + - - - - - - - - - - - - - - - + - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - - - - - - - - + + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1346,78 +1230,76 @@ - - - - - - - + - - - - - + + - + - - - - - - - - - - - - + + - - - - + + + + + + + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + - - - - - + + - - - - - - - - - + + + + + \ No newline at end of file diff --git a/cbpi/api/actor.py b/cbpi/api/actor.py index b2e6fac..a5443fc 100644 --- a/cbpi/api/actor.py +++ b/cbpi/api/actor.py @@ -45,5 +45,3 @@ class CBPiActor(CBPiExtension, metaclass=ABCMeta): pass - def reprJSON(self): - return dict(state=True) \ No newline at end of file diff --git a/cbpi/api/extension.py b/cbpi/api/extension.py index 23f0c77..e5f00c2 100644 --- a/cbpi/api/extension.py +++ b/cbpi/api/extension.py @@ -50,5 +50,3 @@ class CBPiExtension(): except: logger.warning("Faild to load config %s/config.yaml" % path) - - diff --git a/cbpi/api/step.py b/cbpi/api/step.py index a46a36f..2b4e2dc 100644 --- a/cbpi/api/step.py +++ b/cbpi/api/step.py @@ -1,3 +1,4 @@ +import json import time import asyncio import logging @@ -6,22 +7,39 @@ from abc import abstractmethod,ABCMeta class CBPiSimpleStep(metaclass=ABCMeta): - __dirty = False 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__) - print(kwargs) - for a in kwargs: - super(CBPiSimpleStep, self).__setattr__(a, kwargs.get(a)) - self.id = kwargs.get("id") + self._exception_count = 0 + self._interval = 0.1 + self._max_exceptions = 2 + 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_next = False 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): ''' Method checks if the step should continue running. @@ -39,6 +57,9 @@ class CBPiSimpleStep(metaclass=ABCMeta): 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 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) if self.is_dirty(): - print("DIRTY") # Now we have to store the managed props state = {} for field in self.managed_fields: @@ -72,6 +92,7 @@ class CBPiSimpleStep(metaclass=ABCMeta): await self.cbpi.bus.fire("step/update") self.reset_dirty() + @abstractmethod async def run_cycle(self): ''' @@ -134,4 +155,4 @@ class CBPiSimpleStep(metaclass=ABCMeta): self.__dirty = True super(CBPiSimpleStep, self).__setattr__(name, value) else: - super(CBPiSimpleStep, self).__setattr__(name, value) \ No newline at end of file + super(CBPiSimpleStep, self).__setattr__(name, value) diff --git a/cbpi/cli.py b/cbpi/cli.py index e1e3059..8d913de 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -1,5 +1,7 @@ import argparse import logging +import requests +import yaml from cbpi.craftbeerpi import CraftBeerPi import os @@ -13,6 +15,7 @@ def create_plugin_file(): srcfile = os.path.join(os.path.dirname(__file__), "config", "plugin_list.txt") destfile = os.path.join(".", 'config') shutil.copy(srcfile, destfile) + print("Plugin Folder created") def create_config_file(): import os.path @@ -20,19 +23,49 @@ def create_config_file(): srcfile = os.path.join(os.path.dirname(__file__), "config", "config.yaml") destfile = os.path.join(".", 'config') shutil.copy(srcfile, destfile) + print("Config Folder created") def create_home_folder_structure(): 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) + print("Log Folder created") def copy_splash(): srcfile = os.path.join(os.path.dirname(__file__), "config", "splash.png") destfile = os.path.join(".", 'config') 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(): 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() @@ -40,15 +73,24 @@ def main(): logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') if args.action == "setup": + print("Setting up CBPi") create_home_folder_structure() create_plugin_file() create_config_file() copy_splash() + return + + if args.action == "plugins": + list_plugins() + return if args.action == "start": + if check_for_setup() is False: + return cbpi = CraftBeerPi() cbpi.start() + return parser.print_help() diff --git a/cbpi/config/config.yaml b/cbpi/config/config.yaml index f05f964..ec4a2ec 100644 --- a/cbpi/config/config.yaml +++ b/cbpi/config/config.yaml @@ -1,8 +1,8 @@ name: CraftBeerPi -version: 4.1 +version: 4.0.1_alpha -#index_url: /myext +#: /myext port: 8080 diff --git a/cbpi/controller/actor_controller.py b/cbpi/controller/actor_controller.py index eb153c8..6d7d95d 100644 --- a/cbpi/controller/actor_controller.py +++ b/cbpi/controller/actor_controller.py @@ -1,8 +1,9 @@ import asyncio import logging -from asyncio import Future -from cbpi.api import * + from voluptuous import Schema + +from cbpi.api import * from cbpi.controller.crud_controller import CRUDController 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.init() + await self.cbpi.bus.fire(topic="actor/%s/initialized" % actor.id, id=actor.id) else: @@ -73,6 +75,7 @@ class ActorController(CRUDController): actor_id = int(actor_id) if actor_id in self.cache: + self.logger.debug("ON %s" % actor_id) actor = self.cache[actor_id].instance actor.on(power) @@ -141,7 +144,7 @@ class ActorController(CRUDController): :param m: :return: ''' - print("INIT ACTION") + await self._init_actor(m) pass diff --git a/cbpi/controller/config_controller.py b/cbpi/controller/config_controller.py index 6fc0d3d..42e1997 100644 --- a/cbpi/controller/config_controller.py +++ b/cbpi/controller/config_controller.py @@ -6,7 +6,7 @@ from cbpi.database.model import ConfigModel from cbpi.utils import load_config -class ConfigController(): +class ConfigController: ''' The main actor controller ''' @@ -24,14 +24,13 @@ class ConfigController(): async def init(self): this_directory = os.path.dirname(__file__) - self.static = load_config("./config/config.yaml") items = await self.model.get_all() for key, value in items.items(): self.cache[value.name] = value 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: return self.cache[name].value else: diff --git a/cbpi/controller/crud_controller.py b/cbpi/controller/crud_controller.py index 5d41e7e..276add4 100644 --- a/cbpi/controller/crud_controller.py +++ b/cbpi/controller/crud_controller.py @@ -70,7 +70,7 @@ class CRUDController(metaclass=ABCMeta): await self._pre_add_callback(data) - print("INSSERT ADD", data) + m = await self.model.insert(**data) @@ -96,10 +96,12 @@ class CRUDController(metaclass=ABCMeta): ''' self.logger.debug("Update Sensor %s - %s " % (id, data)) + id = int(id) - if id not in self.cache: - self.logger.debug("Sensor %s Not in Cache" % (id,)) + if self.caching is True and id not in self.cache: + + self.logger.debug("%s %s Not in Cache" % (self.name, id)) raise CBPiException("%s with id %s not found" % (self.name,id)) data["id"] = id @@ -115,14 +117,10 @@ class CRUDController(metaclass=ABCMeta): self.cache[id].__dict__.update(**data) m = self.cache[id] = await self.model.update(**self.cache[id].__dict__) await self._post_update_callback(self.cache[id]) - else: - m = await self.model.update(**data) return m - - async def _pre_delete_callback(self, m): ''' diff --git a/cbpi/controller/dashboard_controller.py b/cbpi/controller/dashboard_controller.py index e7952d9..7daff7b 100644 --- a/cbpi/controller/dashboard_controller.py +++ b/cbpi/controller/dashboard_controller.py @@ -1,7 +1,5 @@ import logging -from voluptuous import Schema, MultipleInvalid - from cbpi.controller.crud_controller import CRUDController from cbpi.database.model import DashboardModel, DashboardContentModel diff --git a/cbpi/controller/job_controller.py b/cbpi/controller/job_controller.py index 9e58fb8..a5797b4 100644 --- a/cbpi/controller/job_controller.py +++ b/cbpi/controller/job_controller.py @@ -4,6 +4,7 @@ import logging from cbpi.job.aiohttp import setup, get_scheduler_from_app logger = logging.getLogger(__name__) + class JobController(object): def __init__(self, cbpi): diff --git a/cbpi/controller/kettle_controller.py b/cbpi/controller/kettle_controller.py index 89ea966..4923f73 100644 --- a/cbpi/controller/kettle_controller.py +++ b/cbpi/controller/kettle_controller.py @@ -3,7 +3,7 @@ from cbpi.api import * from cbpi.controller.crud_controller import CRUDController from cbpi.database.model import KettleModel from cbpi.job.aiohttp import get_scheduler_from_app - +import logging class KettleController(CRUDController): ''' @@ -15,6 +15,7 @@ class KettleController(CRUDController): super(KettleController, self).__init__(cbpi) self.cbpi = cbpi self.types = {} + self.logger = logging.getLogger(__name__) self.cbpi.register(self) async def init(self): @@ -58,7 +59,7 @@ class KettleController(CRUDController): kettle = self.cache[int(kid)] kettle.instance = None kettle.state = False - print("FIRE") + await self.cbpi.bus.fire(topic="kettle/%s/logic/stop" % kid) @on_event(topic="kettle/+/automatic") @@ -73,7 +74,7 @@ class KettleController(CRUDController): ''' id = int(id) - print("K", id) + if id in self.cache: kettle = self.cache[id] @@ -84,7 +85,7 @@ class KettleController(CRUDController): if kettle.instance is None: - print("start") + if kettle.logic in self.types: clazz = self.types.get("CustomKettleLogic")["class"] cfg = kettle.config.copy() @@ -104,13 +105,9 @@ class KettleController(CRUDController): def _is_logic_running(self, kettle_id): scheduler = get_scheduler_from_app(self.cbpi.app) - - async def heater_on(self, id): ''' Convenience Method to switch the heater of a kettle on - - :param id: the kettle id :return: (boolean, string) ''' diff --git a/cbpi/controller/plugin_controller.py b/cbpi/controller/plugin_controller.py index b0ac232..6a45e2b 100644 --- a/cbpi/controller/plugin_controller.py +++ b/cbpi/controller/plugin_controller.py @@ -1,3 +1,4 @@ +import asyncio import logging import os from importlib import import_module @@ -5,15 +6,16 @@ from importlib import import_module import aiohttp import yaml from aiohttp import web -from cbpi.api import * +from cbpi.api import * from cbpi.utils.utils import load_config, json_dumps logger = logging.getLogger(__name__) +import subprocess +import sys class PluginController(): - modules = {} types = {} @@ -25,9 +27,7 @@ class PluginController(): async def load_plugin_list(self): async with aiohttp.ClientSession() as session: 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()) return data @@ -44,7 +44,7 @@ class PluginController(): 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].setup(self.cbpi) @@ -55,6 +55,7 @@ class PluginController(): except Exception as e: + print(e) logger.error(e) def load_plugins_from_evn(self): @@ -63,7 +64,6 @@ class PluginController(): this_directory = os.path.dirname(__file__) with open("./config/plugin_list.txt") as f: - plugins = f.read().splitlines() plugins = list(set(plugins)) @@ -74,15 +74,42 @@ class PluginController(): self.modules[p] = import_module(p) self.modules[p].setup(self.cbpi) - - - logger.info("Plugin %s loaded successfully" % p) + logger.info("Plugin %s loaded successfully" % p) except Exception as e: logger.error("FAILED to load plugin %s " % p) 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): """ --- @@ -99,8 +126,6 @@ class PluginController(): """ return web.json_response(await self.load_plugin_list(), dumps=json_dumps) - - def register(self, name, clazz) -> None: ''' Register a new actor type @@ -110,7 +135,7 @@ class PluginController(): ''' logger.info("Register %s Class %s" % (name, clazz.__name__)) 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) if issubclass(clazz, CBPiSensor): @@ -123,7 +148,7 @@ class PluginController(): self.cbpi.step.types[name] = self._parse_props(clazz) if issubclass(clazz, CBPiExtension): - self.c = clazz(self.cbpi) + self.c = clazz(self.cbpi) def _parse_props(self, cls): @@ -131,7 +156,7 @@ class PluginController(): 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("__")] for m in members: if isinstance(tmpObj.__getattribute__(m), Property.Number): @@ -162,4 +187,4 @@ class PluginController(): parameters = method.__getattribute__("parameters") result["actions"].append({"method": method_name, "label": key, "parameters": parameters}) - return result \ No newline at end of file + return result diff --git a/cbpi/controller/sensor_controller.py b/cbpi/controller/sensor_controller.py index ea42e18..af10044 100644 --- a/cbpi/controller/sensor_controller.py +++ b/cbpi/controller/sensor_controller.py @@ -41,7 +41,9 @@ class SensorController(CRUDController): self.cache[sensor.id].instance = clazz(**cfg) self.cache[sensor.id].instance.init() 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) else: self.logger.error("Sensor type '%s' not found (Available Sensor Types: %s)" % (sensor.type, ', '.join(self.types.keys()))) diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index ec01b05..c072c05 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -7,16 +7,13 @@ from cbpi.database.model import StepModel class StepController(CRUDController): - ''' - The Step Controller. This controller is responsible to start and stop the brewing steps. - - ''' - model = StepModel + def __init__(self, cbpi): - super(StepController, self).__init__(cbpi) - self.caching = False + self.model = StepModel + + self.caching = True self.is_stopping = False self.cbpi = cbpi self.current_task = None @@ -28,36 +25,11 @@ class StepController(CRUDController): self.logger = logging.getLogger(__name__) self.starttime = None - async def init(self): - ''' - Initializer of the the Step Controller. - :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: - cfg = {} - 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) + def is_running(self): + if self.current_step is not None: + return True + else: + return False @on_event("step/action") async def handle_action(self, action, **kwargs): @@ -65,171 +37,163 @@ class StepController(CRUDController): ''' Event Handler for "step/action". It invokes the provided method name on the current step - - + + :param action: the method name which will be invoked - :param kwargs: + :param kwargs: :return: None ''' if self.current_step is not None: 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): result = [] + for f in type_cfg.get("properties"): + result.append(f.get("name")) return result - def is_running(self): - if self.current_step is not None: - return True - else: - return False + async def init(self): + + # 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: + 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") async def start(self, **kwargs): - ''' - Start the first step - - :return:None - ''' - if self.is_running() is False: - next_step = await self.model.get_by_state("I") - - if next_step is not None: - 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_id = self.find_next_step() + if next_step_id: + next_step = self.cache[next_step_id] next_step.state = 'A' next_step.stepstate = next_step.config next_step.start = int(time.time()) await self.model.update(**next_step.__dict__) - if self.starttime is not None: - end = time.time() - d = end - self.starttime - print("DURATION", d) - else: - print("NORMAL START") - self.current_job = await self.cbpi.job.start_job(self.current_step.run(), next_step.name, "step") + self.current_step = next_step + # start the step job + self.current_job = await self.cbpi.job.start_job(self.current_step.instance.run(), next_step.name, "step") await self.cbpi.bus.fire("step/%s/started" % self.current_step.id) else: await self.cbpi.bus.fire("step/brewing/finished") else: self.logger.error("Process Already Running") - print("----------- END") - def _set_default(self, step_type, config, managed_fields): - for key in managed_fields: - if key not in config: - config[key] = None + async def next(self, **kwargs): + 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") - async def stop(self, **kwargs): - - if self.current_step is not None: - self.current_step.stop() - self.is_stopping = True - self.current_step = None - + async def stop_all(self, **kwargs): + # RESET DB 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") - @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") async def clear_all(self, **kwargs): await self.model.delete_all() 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): + order = await self.model.get_max_order() data["order"] = 1 if order is None else order + 1 data["state"] = "I" + data["stepstate"] = {} 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) diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index eff0ba9..fb8c5b2 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -52,6 +52,9 @@ async def error_middleware(request, handler): return web.json_response(status=500, data={'error': message}) except MultipleInvalid as 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}) class CraftBeerPi(): @@ -187,7 +190,7 @@ class CraftBeerPi(): api_version=self.static_config.get("version", ""), 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 @@ -216,7 +219,7 @@ class CraftBeerPi(): if url is not None: raise web.HTTPFound(url) else: - return web.Response(text="Hello, world") + return web.Response(text="Hello from CraftbeerPi!") self.app.add_routes([web.get('/', http_index)]) diff --git a/cbpi/database/model.py b/cbpi/database/model.py index 057514a..e7a62cc 100644 --- a/cbpi/database/model.py +++ b/cbpi/database/model.py @@ -20,6 +20,13 @@ class ActorModel(DBModel): '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): __fields__ = ["name", "type", "config"] @@ -32,6 +39,15 @@ class SensorModel(DBModel): '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): __fields__ = ["type", "value", "description", "options"] @@ -54,7 +70,7 @@ class StepModel(DBModel): @classmethod async def update_step_state(cls, step_id, state): - print("NOW UPDATE", state) + 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)) await db.commit() @@ -93,7 +109,7 @@ class StepModel(DBModel): async with aiosqlite.connect(DATABASE_FILE) as db: 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.commit() @@ -111,6 +127,19 @@ class StepModel(DBModel): else: 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): __fields__ = ["name"] diff --git a/cbpi/database/orm_framework.py b/cbpi/database/orm_framework.py index e38a358..ea65acf 100644 --- a/cbpi/database/orm_framework.py +++ b/cbpi/database/orm_framework.py @@ -167,3 +167,7 @@ class DBModel(object): for idx, col in enumerate(cursor.description): d[col[0]] = row[idx] return d + + def to_json(self): + + return self.__dict__ diff --git a/cbpi/extension/dummyactor/__init__.py b/cbpi/extension/dummyactor/__init__.py index bc38333..cac2f60 100644 --- a/cbpi/extension/dummyactor/__init__.py +++ b/cbpi/extension/dummyactor/__init__.py @@ -24,7 +24,7 @@ class CustomActor(CBPiActor): # Custom property which can be configured by the user @action("test", parameters={}) def action1(self): - print("EOOOHOOO") + pass diff --git a/cbpi/extension/dummysensor/__init__.py b/cbpi/extension/dummysensor/__init__.py index 3baaaa9..5dfd20a 100644 --- a/cbpi/extension/dummysensor/__init__.py +++ b/cbpi/extension/dummysensor/__init__.py @@ -3,6 +3,7 @@ import asyncio from aiohttp import web from cbpi.api import * +import re class CustomSensor(CBPiSensor): @@ -61,6 +62,7 @@ class HTTPSensor(CBPiSensor): def init(self): super().init() + self.state = True def get_state(self): @@ -80,10 +82,12 @@ class HTTPSensor(CBPiSensor): try: value = cache.pop(self.key, None) + print("HTTP SENSOR READ", value) if value is not None: self.log_data(value) - await cbpi.bus.fire("sensor/%s" % self.id, value=value) - except: + await cbpi.bus.fire("sensor/%s/data" % self.id, value=value) + except Exception as e: + print(e) pass class HTTPSensorEndpoint(CBPiExtension): @@ -95,6 +99,7 @@ class HTTPSensorEndpoint(CBPiExtension): :param cbpi: ''' + self.pattern_check = re.compile("^[a-zA-Z0-9,.]{0,10}$") self.cbpi = cbpi # register component for http, events @@ -125,10 +130,17 @@ class HTTPSensorEndpoint(CBPiExtension): "204": description: successful operation """ - print("HALLO") + global cache key = request.match_info['key'] 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 return web.Response(status=204) diff --git a/cbpi/extension/dummystep/__init__.py b/cbpi/extension/dummystep/__init__.py index 0753bc9..1162936 100644 --- a/cbpi/extension/dummystep/__init__.py +++ b/cbpi/extension/dummystep/__init__.py @@ -9,15 +9,23 @@ class CustomStepCBPi(CBPiSimpleStep): name1 = Property.Number(label="Test", configurable=True) timer_end = Property.Number(label="Test", default_value=None) temp = Property.Number(label="Temperature", default_value=50, configurable=True) + + + i = 0 @action(key="name", parameters=None) def test(self, **kwargs): self.name="WOOHOO" - + def get_status(self): + return "Status: %s Temp" % self.temp async def run_cycle(self): + + self.next() + + ''' print("RUN", self.name1, self.managed_fields, self.timer_end) self.i = self.i + 1 @@ -26,7 +34,7 @@ class CustomStepCBPi(CBPiSimpleStep): if self.i == 10: self.next() - + ''' #self.cbpi.notify(key="step", message="HELLO FROM STEP") diff --git a/cbpi/http_endpoints/http_config.py b/cbpi/http_endpoints/http_config.py index c9a2850..acf1fdb 100644 --- a/cbpi/http_endpoints/http_config.py +++ b/cbpi/http_endpoints/http_config.py @@ -30,7 +30,7 @@ class ConfigHttpEndpoints(HttpCrudEndpoints): "204": description: successful operation """ - print("HALLO PARA") + name = request.match_info['name'] data = await request.json() await self.controller.set(name=name, value=data.get("value")) diff --git a/cbpi/http_endpoints/http_curd_endpoints.py b/cbpi/http_endpoints/http_curd_endpoints.py index fe0a55b..b7fb5d0 100644 --- a/cbpi/http_endpoints/http_curd_endpoints.py +++ b/cbpi/http_endpoints/http_curd_endpoints.py @@ -22,7 +22,7 @@ class HttpCrudEndpoints(): @request_mapping(path="/", auth_required=False) 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) async def http_get_one(self, request): @@ -39,6 +39,8 @@ class HttpCrudEndpoints(): async def http_update(self, request): id = int(request.match_info['id']) data = await request.json() + + obj = await self.controller.update(id, data) return web.json_response(obj, dumps=json_dumps) diff --git a/cbpi/http_endpoints/http_dashboard.py b/cbpi/http_endpoints/http_dashboard.py index 612e9b9..03a2aca 100644 --- a/cbpi/http_endpoints/http_dashboard.py +++ b/cbpi/http_endpoints/http_dashboard.py @@ -257,6 +257,6 @@ class DashBoardHttpEndpoints(HttpCrudEndpoints): schema = Schema({"id": int, "x": int, "y": int}) schema(data) 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) diff --git a/cbpi/http_endpoints/http_step.py b/cbpi/http_endpoints/http_step.py index e2aa310..194aa9d 100644 --- a/cbpi/http_endpoints/http_step.py +++ b/cbpi/http_endpoints/http_step.py @@ -28,24 +28,16 @@ class StepHttpEndpoints(HttpCrudEndpoints): @request_mapping(path="/", auth_required=False) async def http_get_all(self, request): + """ --- - description: Switch step on + description: Get all steps tags: - Step - parameters: - - name: "id" - in: "path" - description: "step ID" - required: true - type: "integer" - format: "int64" responses: "204": description: successful operation - "405": - description: invalid HTTP Met """ return await super().http_get_all(request) diff --git a/cbpi/job/_job.py b/cbpi/job/_job.py index 3e96c4d..e4b2606 100644 --- a/cbpi/job/_job.py +++ b/cbpi/job/_job.py @@ -114,6 +114,7 @@ class Job: self._started.set_result(None) def _done_callback(self, task): + scheduler = self._scheduler scheduler._done(self) try: diff --git a/cbpi/utils/encoder.py b/cbpi/utils/encoder.py index b676206..575577d 100644 --- a/cbpi/utils/encoder.py +++ b/cbpi/utils/encoder.py @@ -1,38 +1,13 @@ from json import JSONEncoder -from cbpi.database.model import ActorModel, SensorModel - class ComplexEncoder(JSONEncoder): def default(self, obj): - - from cbpi.database.orm_framework import DBModel - try: - if isinstance(obj, ActorModel): - - - 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() + if hasattr(obj, "to_json") and callable(getattr(obj, "to_json")): + return obj.to_json() else: raise TypeError() except Exception as e: diff --git a/cheat_sheet.txt b/cheat_sheet.txt new file mode 100644 index 0000000..0be4da2 --- /dev/null +++ b/cheat_sheet.txt @@ -0,0 +1,11 @@ +#clean +python3.7 setup.py clean --all + +#build +python setup.py sdist + +#Upload +twine upload dist/* + + + diff --git a/craftbeerpi.db b/craftbeerpi.db index d4ae9a2..e5d0de5 100644 Binary files a/craftbeerpi.db and b/craftbeerpi.db differ diff --git a/sample.py b/sample.py index 1af9215..b000ad6 100644 --- a/sample.py +++ b/sample.py @@ -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") \ No newline at end of file diff --git a/setup.py b/setup.py index 86a6de5..792f731 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ from setuptools import setup, find_packages setup(name='cbpi', - version='4.0.0.1', - description='CraftBeerPi API', + version='4.0.0.4', + description='CraftBeerPi', author='Manuel Fritsch', author_email='manuel@craftbeerpi.com', url='http://web.craftbeerpi.com', @@ -23,6 +23,7 @@ setup(name='cbpi', "aiojobs==0.2.2", "aiosqlite==0.7.0", "cryptography==2.3.1", + "requests==2.22.0", "voluptuous==0.11.5", "pyfiglet==0.7.6" ], diff --git a/tests/test_kettle.py b/tests/test_kettle.py index e93e576..402af9e 100644 --- a/tests/test_kettle.py +++ b/tests/test_kettle.py @@ -68,9 +68,9 @@ class KettleTestCase(AioHTTPTestCase): assert resp.status == 200 m = await resp.json() - print(m) - sensor_id = m["id"] + sensor_id = m["id"] + print("KETTLE", m["id"], m) # Get sensor resp = await self.client.get(path="/kettle/%s" % sensor_id) assert resp.status == 200 diff --git a/tests/test_step.py b/tests/test_step.py index 77d4811..6e73825 100644 --- a/tests/test_step.py +++ b/tests/test_step.py @@ -17,9 +17,11 @@ class StepTestCase(AioHTTPTestCase): async def test_get(self): resp = await self.client.request("GET", "/step") + print(resp) assert resp.status == 200 resp = await self.client.request("GET", "/step/types") + print(resp) assert resp.status == 200 @@ -28,14 +30,15 @@ class StepTestCase(AioHTTPTestCase): data = { "name": "Test", "type": "CustomStepCBPi", + "config": {} } - # Add new sensor + # Add new step resp = await self.client.post(path="/step/", json=data) assert resp.status == 200 m = await resp.json() - print(m) + print("Step", m) sensor_id = m["id"] # Get sensor @@ -68,31 +71,37 @@ class StepTestCase(AioHTTPTestCase): if future in done: pass + + @unittest_run_loop 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") + await step_ctlr.start() + await self.wait(future) + + for i in range(len(step_ctlr.cache)-1): future = self.create_wait_callback("step/+/started") - await self.cbpi.step.start() + await step_ctlr.next() await self.wait(future) - future = self.create_wait_callback("step/+/started") - await self.cbpi.step.next() - await self.wait(future) + await self.print_steps() - future = self.create_wait_callback("step/+/started") - 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()) \ No newline at end of file