diff --git a/cbpi/__init__.py b/cbpi/__init__.py index af01513..694f190 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.0.29" \ No newline at end of file +__version__ = "4.0.0.30" \ No newline at end of file diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index ebba864..ce7b716 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -141,4 +141,13 @@ class Config: def __str__(self): return "....name={} value={}".format(self.name, self.value) def to_dict(self): - return dict(name=self.name, value=self.value, type=self.type.value, description=self.description, options=self.options) \ No newline at end of file + return dict(name=self.name, value=self.value, type=self.type.value, description=self.description, options=self.options) + +@dataclass +class NotificationAction: + label: str + method: Any = None + id: str = None + + def to_dict(self): + return dict(id=self.id, label=self.label) \ No newline at end of file diff --git a/cbpi/controller/basic_controller2.py b/cbpi/controller/basic_controller2.py index fc057a9..5a06a13 100644 --- a/cbpi/controller/basic_controller2.py +++ b/cbpi/controller/basic_controller2.py @@ -117,7 +117,6 @@ class BasicController: return {"data": list(map(lambda x: x.to_dict(), self.data)), "types":self.get_types()} async def add(self, item): - print(item) logging.info("{} Add".format(self.name)) item.id = shortuuid.uuid() self.data.append(item) diff --git a/cbpi/controller/dashboard_controller.py b/cbpi/controller/dashboard_controller.py index f17f97a..2c130b8 100644 --- a/cbpi/controller/dashboard_controller.py +++ b/cbpi/controller/dashboard_controller.py @@ -4,6 +4,8 @@ import os from os import listdir from os.path import isfile, join +from voluptuous.schema_builder import message + class DashboardController: @@ -29,6 +31,7 @@ class DashboardController: async def add_content(self, dashboard_id, data): with open(self.path, 'w') as outfile: json.dump(data, outfile, indent=4, sort_keys=True) + self.cbpi.notify(title="Dashboard", message="Saved Successfully", type="success") return {"status": "OK"} async def delete_content(self, dashboard_id): diff --git a/cbpi/controller/notification_controller.py b/cbpi/controller/notification_controller.py new file mode 100644 index 0000000..d7950af --- /dev/null +++ b/cbpi/controller/notification_controller.py @@ -0,0 +1,42 @@ +import asyncio +import logging +import shortuuid +class NotificationController: + + def __init__(self, cbpi): + ''' + + :param cbpi: craftbeerpi object + ''' + self.cbpi = cbpi + self.logger = logging.getLogger(__name__) + self.callback_cache = {} + + def notify(self, title, message: str, type: str = "info", action=[]) -> None: + ''' + This is a convinience method to send notification to the client + + :param key: notification key + :param message: notification message + :param type: notification type (info,warning,danger,successs) + :return: + ''' + notifcation_id = shortuuid.uuid() + + def prepare_action(item): + item.id = shortuuid.uuid() + return item.to_dict() + + actions = list(map(lambda item: prepare_action(item), action)) + self.callback_cache[notifcation_id] = action + self.cbpi.ws.send(dict(id=notifcation_id, topic="notifiaction", type=type, title=title, message=message, action=actions)) + + def notify_callback(self, notification_id, action_id) -> None: + try: + action = next((item for item in self.callback_cache[notification_id] if item.id == action_id), None) + if action.method is not None: + asyncio.create_task(action.method()) + del self.callback_cache[notification_id] + except Exception as e: + self.logger.error("Faild to call notificatoin callback") + \ No newline at end of file diff --git a/cbpi/controller/recipe_controller.py b/cbpi/controller/recipe_controller.py index 4516bf2..c8d6bc9 100644 --- a/cbpi/controller/recipe_controller.py +++ b/cbpi/controller/recipe_controller.py @@ -39,7 +39,7 @@ class RecipeController: path = os.path.join(".", 'config', "recipes", "{}.yaml".format(name)) with open(path, "w") as file: yaml.dump(data, file, indent=4, sort_keys=True) - self.cbpi.notify("{} saved".format(data["basic"].get("name"))) + async def get_recipes(self): path = os.path.join(".", 'config', "recipes") @@ -65,7 +65,7 @@ class RecipeController: async def remove(self, name): path = os.path.join(".", 'config', "recipes", "{}.yaml".format(name)) os.remove(path) - self.cbpi.notify("{} delted".format(name)) + async def brew(self, name): diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index 7a0ebe7..dab3a37 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -1,4 +1,5 @@ import asyncio +import cbpi import copy import json import logging @@ -6,7 +7,7 @@ import os.path from os import listdir from os.path import isfile, join import shortuuid -from cbpi.api.dataclasses import Props, Step +from cbpi.api.dataclasses import NotificationAction, Props, Step from tabulate import tabulate from ..api.step import StepMove, StepResult, StepState @@ -129,8 +130,7 @@ class StepController: await self.start_step(step) await self.save() return - - self.cbpi.notify(message="BREWING COMPLETE") + self.cbpi.notify("Brewing Complete", "Now the yeast will take over",action=[NotificationAction("OK")]) logging.info("BREWING COMPLETE") async def previous(self): diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index 89f31da..a7d1f11 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -1,4 +1,5 @@ +from cbpi.controller.notification_controller import NotificationController import logging from os import urandom import os @@ -39,7 +40,9 @@ from cbpi.http_endpoints.http_recipe import RecipeHttpEndpoints from cbpi.http_endpoints.http_plugin import PluginHttpEndpoints from cbpi.http_endpoints.http_system import SystemHttpEndpoints from cbpi.http_endpoints.http_log import LogHttpEndpoints +from cbpi.http_endpoints.http_notification import NotificationHttpEndpoints +import shortuuid logger = logging.getLogger(__name__) @@ -98,9 +101,9 @@ class CraftBeerPi: self.kettle = KettleController(self) self.step : StepController = StepController(self) self.recipe : RecipeController = RecipeController(self) + self.notification : NotificationController = NotificationController(self) #self.satellite: SatelliteController = SatelliteController(self) self.dashboard = DashboardController(self) - self.http_step = StepHttpEndpoints(self) self.http_recipe = RecipeHttpEndpoints(self) self.http_sensor = SensorHttpEndpoints(self) @@ -111,6 +114,7 @@ class CraftBeerPi: self.http_plugin = PluginHttpEndpoints(self) self.http_system = SystemHttpEndpoints(self) self.http_log = LogHttpEndpoints(self) + self.http_notification = NotificationHttpEndpoints(self) self.login = Login(self) def _setup_shutdownhook(self): @@ -208,18 +212,10 @@ class CraftBeerPi: + def notify(self, title: str, message: str, type: str = "info", action=[]) -> None: + self.notification.notify(title, message, type, action) + - def notify(self, message: str, type: str = "info") -> None: - ''' - This is a convinience method to send notification to the client - - :param key: notification key - :param message: notification message - :param type: notification type (info,warning,danger,successs) - :return: - ''' - self.ws.send(dict(topic="notifiaction", type=type, message=message)) - async def call_initializer(self, app): self.initializer = sorted(self.initializer, key=lambda k: k['order']) for i in self.initializer: diff --git a/cbpi/extension/dummyactor/__init__.py b/cbpi/extension/dummyactor/__init__.py index 88ac340..1bb003d 100644 --- a/cbpi/extension/dummyactor/__init__.py +++ b/cbpi/extension/dummyactor/__init__.py @@ -1,21 +1,33 @@ +from socket import timeout +from typing import KeysView + +from voluptuous.schema_builder import message +from cbpi.api.dataclasses import NotificationAction import logging from unittest.mock import MagicMock, patch - +from datetime import datetime from cbpi.api import * logger = logging.getLogger(__name__) @parameters([]) class DummyActor(CBPiActor): - my_name = "" + - # Custom property which can be configured by the user - @action("test", parameters={}) - async def action1(self, **kwargs): - - self.my_name = kwargs.get("name") - pass + def __init__(self, cbpi, id, props): + super().__init__(cbpi, id, props) + + async def yes(self, **kwargs): + print("YES!") + await self.cbpi.step.next() + + + @action("HELLO WORLD", {}) + async def helloWorld(self, **kwargs): + print("HELLO WORLD") + self.cbpi.notify(title="HELLO WORLD", message="DO YOU WANT TO START THE NEXT STEP", action=[NotificationAction("YES", self.yes), NotificationAction("NO")]) + async def start(self): await super().start() diff --git a/cbpi/extension/gpioactor/__init__.py b/cbpi/extension/gpioactor/__init__.py index 4c3b3f0..18fdacc 100644 --- a/cbpi/extension/gpioactor/__init__.py +++ b/cbpi/extension/gpioactor/__init__.py @@ -30,12 +30,12 @@ class GPIOActor(CBPiActor): @action(key="Cusotm Action", parameters=[Property.Number("Value", configurable=True), Property.Kettle("Kettle")]) async def custom_action(self, **kwargs): print("ACTION", kwargs) - self.cbpi.notify("ACTION CALLED") + @action(key="Cusotm Action2", parameters=[Property.Number("Value", configurable=True)]) async def custom_action2(self, **kwargs): print("ACTION2") - self.cbpi.notify("ACTION CALLED") + def get_GPIO_state(self, state): # ON diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index 05dd9eb..10fbc05 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -21,7 +21,7 @@ class MashStep(CBPiStep): async def custom_action(self, Value, **kwargs): self.summary = "VALUE FROM ACTION {}".format(Value) await self.push_update() - self.cbpi.notify("ACTION 2 CALLED".format(Value)) + async def on_timer_done(self,timer): self.summary = "" @@ -60,12 +60,12 @@ class WaitStep(CBPiStep): @action(key="Custom Step Action", parameters=[]) async def hello(self, **kwargs): print("ACTION") - self.cbpi.notify("ACTION 1 CALLED") + @action(key="Custom Step Action 2", parameters=[]) async def hello2(self, **kwargs): print("ACTION2") - self.cbpi.notify("ACTION 2 CALLED") + async def on_timer_done(self,timer): self.summary = "" @@ -157,7 +157,7 @@ class BoilStep(CBPiStep): @action("Start Timer", []) async def star_timer(self): - self.cbpi.notify("Timer started") + self.timer.start() async def run(self): diff --git a/cbpi/http_endpoints/http_notification.py b/cbpi/http_endpoints/http_notification.py new file mode 100644 index 0000000..b61e6f2 --- /dev/null +++ b/cbpi/http_endpoints/http_notification.py @@ -0,0 +1,40 @@ + +from aiohttp import web +from cbpi.api import request_mapping +from cbpi.utils import json_dumps + +class NotificationHttpEndpoints: + + def __init__(self,cbpi): + self.cbpi = cbpi + self.cbpi.register(self, url_prefix="/notification") + + @request_mapping(path="/{id}/action/{action_id}", method="POST", auth_required=False) + async def action(self, request): + """ + --- + description: Update an actor + tags: + - Notification + parameters: + - name: "id" + in: "path" + description: "Notification Id" + required: true + type: "string" + - name: "action_id" + in: "path" + description: "Action Id" + required: true + type: "string" + + responses: + "200": + description: successful operation + """ + + notification_id = request.match_info['id'] + action_id = request.match_info['action_id'] + print(notification_id, action_id) + self.cbpi.notification.notify_callback(notification_id, action_id) + return web.Response(status=204) \ No newline at end of file