plugin installer added

This commit is contained in:
manuel83 2019-08-16 21:36:55 +02:00
parent 85b6549640
commit eb2ba8fdfe
34 changed files with 990 additions and 1107 deletions

View file

@ -32,5 +32,12 @@
<auth-required>false</auth-required>
<introspection-schemas>*:@</introspection-schemas>
</data-source>
<data-source name="craftbeerpi.db test" uuid="2513ed7e-a63e-406f-b4b6-435eaa982255">
<database-info product="SQLite" version="3.16.1" jdbc-version="2.1" driver-name="SQLiteJDBC" driver-version="native" dbms="SQLITE" exact-version="3.16.1" />
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<secret-storage>master_key</secret-storage>
<auth-required>false</auth-required>
<introspection-schemas>*:@</introspection-schemas>
</data-source>
</component>
</project>

View file

@ -78,5 +78,14 @@
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="craftbeerpi.db test" uuid="2513ed7e-a63e-406f-b4b6-435eaa982255">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/cbpi4_test/2019-08-08-001/craftbeerpi.db</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</data-source>
</component>
</project>

View file

@ -4,4 +4,7 @@
<option name="languageLevel" value="JSX" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7.1 virtualenv at ~/cbp42" project-jdk-type="Python SDK" />
<component name="PyPackaging">
<option name="earlyReleasesAsUpgrades" value="true" />
</component>
</project>

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,6 @@ __all__ = ["CBPiActor",
"CBPiExtension",
"Property",
"PropertyType",
"on_websocket_message",
"on_mqtt_message",
"on_event",
"on_startup",
"request_mapping",

View file

@ -2,7 +2,7 @@ from functools import wraps
from voluptuous import Schema
__all__ = ["request_mapping", "on_startup", "on_event", "on_mqtt_message", "on_websocket_message", "action", "background_task"]
__all__ = ["request_mapping", "on_startup", "on_event", "action", "background_task"]
from aiohttp_auth import auth
@ -55,15 +55,6 @@ def request_mapping(path, name=None, method="GET", auth_required=True, json_sche
validate_json_body
)
def on_websocket_message(path, name=None):
def real_decorator(func):
func.ws = True
func.key = path
func.name = name
return func
return real_decorator
def on_event(topic):
def real_decorator(func):
func.eventbus = True
@ -76,29 +67,18 @@ def on_event(topic):
def action(key, parameters):
def real_decorator(func):
func.action = True
func.key = key
func.parameters = parameters
return func
return real_decorator
def on_mqtt_message(topic):
def real_decorator(func):
func.mqtt = True
func.topic = topic
return func
return real_decorator
def background_task(name, interval):
def real_decorator(func):
func.background_task = True
func.name = name
func.interval = interval
return func
return real_decorator
@ -108,7 +88,6 @@ def on_startup(name, order=0):
func.name = name
func.order = order
return func
return real_decorator

View file

@ -11,4 +11,4 @@ class SensorException(CBPiException):
pass
class ActorException(CBPiException):
pass
pass

View file

@ -1,11 +1,10 @@
from logging.handlers import RotatingFileHandler
from time import localtime, strftime
import logging
from abc import ABCMeta
from cbpi.api.extension import CBPiExtension
import logging
class CBPiSensor(CBPiExtension):
class CBPiSensor(CBPiExtension, metaclass=ABCMeta):
def __init__(self, *args, **kwds):
CBPiExtension.__init__(self, *args, **kwds)
self.logger = logging.getLogger(__file__)

View file

@ -2,7 +2,7 @@ import json
import time
import asyncio
import logging
from abc import abstractmethod,ABCMeta
from abc import abstractmethod, ABCMeta
class CBPiSimpleStep(metaclass=ABCMeta):

View file

@ -1,7 +1,12 @@
import argparse
import datetime
import logging
import subprocess
import sys
import re
import requests
import yaml
from cbpi.utils.utils import load_config
from cbpi.craftbeerpi import CraftBeerPi
import os
@ -59,22 +64,87 @@ def list_plugins():
print("***************************************************")
print("CraftBeerPi 4.x Plugin List")
print("***************************************************")
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)
print("")
print("***************************************************")
def add(package_name):
if package_name is None:
print("Missing Plugin Name: cbpi add --name=")
return
data = subprocess.check_output([sys.executable, "-m", "pip", "install", package_name])
data = data.decode('UTF-8')
patter_already_installed = "Requirement already satisfied: %s" % package_name
pattern = "Successfully installed %s-([-0-9a-zA-Z._]*)" % package_name
match_already_installed = re.search(patter_already_installed, data)
match_installed = re.search(pattern, data)
if match_already_installed is not None:
print("Plugin already installed")
return False
if match_installed is None:
print(data)
print("Faild to install plugin")
return False
version = match_installed.groups()[0]
plugins = load_config("./config/plugin_list.txt")
if plugins is None:
plugins = {}
now = datetime.datetime.now()
plugins[package_name] = dict(version=version, installation_date=now.strftime("%Y-%m-%d %H:%M:%S"))
with open('./config/plugin_list.txt', 'w') as outfile:
yaml.dump(plugins, outfile, default_flow_style=False)
print("Plugin %s added" % package_name)
return True
def remove(package_name):
if package_name is None:
print("Missing Plugin Name: cbpi add --name=")
return
data = subprocess.check_output([sys.executable, "-m", "pip", "uninstall", "-y", package_name])
data = data.decode('UTF-8')
pattern = "Successfully uninstalled %s-([-0-9a-zA-Z._]*)" % package_name
match_uninstalled = re.search(pattern, data)
if match_uninstalled is None:
print(data)
print("Faild to uninstall plugin")
return False
plugins = load_config("./config/plugin_list.txt")
if plugins is None:
plugins = {}
if package_name not in plugins:
return False
del plugins[package_name]
with open('./config/plugin_list.txt', 'w') as outfile:
yaml.dump(plugins, outfile, default_flow_style=False)
print("Plugin %s removed" % package_name)
return True
def main():
parser = argparse.ArgumentParser(description='Welcome to CraftBeerPi 4')
parser.add_argument("action", type=str, help="start,stop,restart,setup,plugins")
parser.add_argument("--name", type=str, help="Plugin name")
args = parser.parse_args()
#logging.basicConfig(level=logging.INFO, filename='./logs/app.log', filemode='a', format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
@ -94,6 +164,16 @@ def main():
list_plugins()
return
if args.action == "add":
add(args.name)
return
if args.action == "remove":
remove(args.name)
return
if args.action == "start":
if check_for_setup() is False:
return

View file

@ -108,4 +108,9 @@ CREATE TABLE IF NOT EXISTS dummy
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(80)
);
);
INSERT OR IGNORE INTO config (name, value, type, description, options) VALUES ('TEMP_UNIT', 'F', 'select', 'Temperature Unit', '[{"value": "C", "label": "C"}, {"value": "F", "label": "F"}]');
INSERT OR IGNORE INTO config (name, value, type, description, options) VALUES ('NAME', 'India Pale Ale1', 'string', 'Brew Name', 'null');
INSERT OR IGNORE INTO config (name, value, type, description, options) VALUES ('BREWERY_NAME', 'CraftBeerPI', 'string', 'Brewery Name', 'null');

View file

@ -1 +0,0 @@
cbpi-actor

View file

@ -149,8 +149,7 @@ class ActorController(CRUDController):
pass
async def _pre_delete_callback(self, actor_id):
#if int(actor_id) not in self.cache:
# return
if self.cache[int(actor_id)].instance is not None:
await self._stop_actor(self.cache[int(actor_id)])

View file

@ -10,6 +10,7 @@ class DashboardController(CRUDController):
name = "Dashboard"
def __init__(self, cbpi):
self.caching = False
super(DashboardController, self).__init__(cbpi)
self.cbpi = cbpi
self.logger = logging.getLogger(__name__)
@ -29,3 +30,7 @@ class DashboardController(CRUDController):
async def move_content(self,content_id, x, y):
await DashboardContentModel.update_coordinates(content_id, x, y)
async def delete_dashboard(self, dashboard_id):
await DashboardContentModel.delete_by_dashboard_id(dashboard_id)
await self.model.delete(dashboard_id)

View file

@ -1,5 +1,6 @@
import logging
from os import urandom
import os
from aiohttp import web
from aiohttp_auth import auth
@ -231,7 +232,8 @@ class CraftBeerPi():
else:
return web.Response(text="Hello from CraftbeerPi!")
self.app.add_routes([web.get('/', http_index)])
self.app.add_routes([web.get('/', http_index), web.static('/static', os.path.join(os.path.dirname(__file__),"static"), show_index=True)])
async def init_serivces(self):

View file

@ -1,7 +1,4 @@
import os
from aiohttp import web
from cbpi.api import *
from cbpi.controller.crud_controller import CRUDController
from cbpi.database.orm_framework import DBModel
@ -22,11 +19,8 @@ class MyComp(CBPiExtension, CRUDController, HttpCrudEndpoints):
def __init__(self, cbpi):
'''
Initializer
:param cbpi:
'''
self.cbpi = cbpi
# register component for http, events
# In addtion the sub folder static is exposed to access static content via http
@ -35,14 +29,14 @@ class MyComp(CBPiExtension, CRUDController, HttpCrudEndpoints):
@on_event(topic="actor/#")
async def listen(self, **kwargs):
# Listen for all actor events
pass
@on_event(topic="kettle/+/automatic")
async def listen2(self, **kwargs):
# listen for all kettle events which are switching the automatic logic
pass
#await self.cbpi.bus.fire(topic="actor/%s/toggle" % 1, id=1)
def setup(cbpi):
@ -54,4 +48,4 @@ def setup(cbpi):
'''
# regsiter the component to the core
cbpi.plugin.register("MyComp", MyComp)
pass
pass

View file

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
import asyncio
import threading
import time
from aiohttp import web
from cbpi.api import *
import re
import random
def getSensors():
try:
arr = []
for dirname in os.listdir('/sys/bus/w1/devices'):
if (dirname.startswith("28") or dirname.startswith("10")):
cbpi.app.logger.info("Device %s Found (Family: 28/10, Thermometer on GPIO4 (w1))" % dirname)
arr.append(dirname)
return arr
except:
return []
class myThread (threading.Thread):
value = 0
def __init__(self, sensor_name):
threading.Thread.__init__(self)
self.value = 0
self.sensor_name = sensor_name
self.runnig = True
def shutdown(self):
pass
def stop(self):
self.runnig = False
def run(self):
while self.runnig:
try:
app.logger.info("READ TEMP")
## Test Mode
if self.sensor_name is None:
return
with open('/sys/bus/w1/devices/w1_bus_master1/%s/w1_slave' % self.sensor_name, 'r') as content_file:
content = content_file.read()
if (content.split('\n')[0].split(' ')[11] == "YES"):
temp = float(content.split("=")[-1]) / 1000 # temp in Celcius
self.value = temp
except:
pass
self.value = random.randint(1,100)
time.sleep(4)
class DS18B20(CBPiSensor):
sensor_name = Property.Select("Sensor", getSensors(), description="The OneWire sensor address.")
offset = Property.Number("Offset", True, 0, description="Offset which is added to the received sensor data. Positive and negative values are both allowed.")
interval = Property.Number(label="interval", configurable=True)
# Internal runtime variable
value = 0
def init(self):
super().init()
self.state = True
self.t = myThread(self.sensor_name)
def shudown():
shudown.cb.shutdown()
shudown.cb = self.t
self.t.start()
def get_state(self):
return self.state
def get_value(self):
return self.value
def get_unit(self):
return "°%s" % self.get_parameter("TEMP_UNIT", "C")
def stop(self):
try:
self.t.stop()
except:
pass
async def run(self, cbpi):
self.value = 0
while True:
await asyncio.sleep(self.interval)
self.value = random.randint(1,101)
self.log_data(self.value)
await cbpi.bus.fire("sensor/%s/data" % self.id, value=self.value)
def setup(cbpi):
'''
This method is called by the server during startup
Here you need to register your plugins at the server
:param cbpi: the cbpi core
:return:
'''
cbpi.plugin.register("DS18B20", DS18B20)

View file

@ -0,0 +1,3 @@
name: DummySensor
version: 4
active: true

View file

@ -4,13 +4,12 @@ from aiohttp import web
from cbpi.api import *
import re
import random
class CustomSensor(CBPiSensor):
# Custom Properties which will can be configured by the user
p1 = Property.Number(label="Test")
p2 = Property.Text(label="Test")
interval = Property.Number(label="interval", configurable=True)
# Internal runtime variable
@ -45,104 +44,10 @@ class CustomSensor(CBPiSensor):
self.value = 0
while True:
await asyncio.sleep(self.interval)
self.value = self.value + 1
self.value = random.randint(1,101)
self.log_data(self.value)
await cbpi.bus.fire("sensor/%s/data" % self.id, value=self.value)
cache = {}
class HTTPSensor(CBPiSensor):
# Custom Properties which will can be configured by the user
key = Property.Text(label="Key", configurable=True)
def init(self):
super().init()
self.state = True
def get_state(self):
return self.state
def get_value(self):
return self.value
def stop(self):
pass
async def run(self, cbpi):
self.value = 0
while True:
await asyncio.sleep(1)
try:
value = cache.pop(self.key, None)
if value is not None:
self.log_data(value)
await cbpi.bus.fire("sensor/%s/data" % self.id, value=value)
except Exception as e:
print(e)
pass
class HTTPSensorEndpoint(CBPiExtension):
def __init__(self, cbpi):
'''
Initializer
:param cbpi:
'''
self.pattern_check = re.compile("^[a-zA-Z0-9,.]{0,10}$")
self.cbpi = cbpi
# register component for http, events
# In addtion the sub folder static is exposed to access static content via http
self.cbpi.register(self, "/httpsensor")
@request_mapping(path="/{key}/{value}", auth_required=False)
async def http_new_value2(self, request):
"""
---
description: Kettle Heater on
tags:
- HttpSensor
parameters:
- name: "key"
in: "path"
description: "Sensor Key"
required: true
type: "string"
- name: "value"
in: "path"
description: "Value"
required: true
type: "integer"
format: "int64"
responses:
"204":
description: successful operation
"""
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)
def setup(cbpi):
@ -153,6 +58,5 @@ def setup(cbpi):
:param cbpi: the cbpi core
:return:
'''
cbpi.plugin.register("HTTPSensor", HTTPSensor)
cbpi.plugin.register("HTTPSensorEndpoint", HTTPSensorEndpoint)
cbpi.plugin.register("CustomSensor", CustomSensor)

View file

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
import asyncio
from aiohttp import web
from cbpi.api import *
import re
import random
cache = {}
class HTTPSensor(CBPiSensor):
# Custom Properties which will can be configured by the user
key = Property.Text(label="Key", configurable=True)
def init(self):
super().init()
self.state = True
def get_state(self):
return self.state
def get_value(self):
return self.value
def stop(self):
pass
async def run(self, cbpi):
self.value = 0
while True:
await asyncio.sleep(1)
try:
value = cache.pop(self.key, None)
if value is not None:
self.log_data(value)
await cbpi.bus.fire("sensor/%s/data" % self.id, value=value)
except Exception as e:
print(e)
pass
class HTTPSensorEndpoint(CBPiExtension):
def __init__(self, cbpi):
'''
Initializer
:param cbpi:
'''
self.pattern_check = re.compile("^[a-zA-Z0-9,.]{0,10}$")
self.cbpi = cbpi
# register component for http, events
# In addtion the sub folder static is exposed to access static content via http
self.cbpi.register(self, "/httpsensor")
@request_mapping(path="/{key}/{value}", auth_required=False)
async def http_new_value2(self, request):
"""
---
description: Kettle Heater on
tags:
- HttpSensor
parameters:
- name: "key"
in: "path"
description: "Sensor Key"
required: true
type: "string"
- name: "value"
in: "path"
description: "Value"
required: true
type: "integer"
format: "int64"
responses:
"204":
description: successful operation
"""
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)
def setup(cbpi):
'''
This method is called by the server during startup
Here you need to register your plugins at the server
:param cbpi: the cbpi core
:return:
'''
cbpi.plugin.register("HTTPSensor", HTTPSensor)
cbpi.plugin.register("HTTPSensorEndpoint", HTTPSensorEndpoint)

View file

@ -0,0 +1,3 @@
name: DummySensor
version: 4
active: true

View file

@ -118,7 +118,9 @@ class DashBoardHttpEndpoints(HttpCrudEndpoints):
"204":
description: successful operation
"""
return await super().http_delete_one(request)
id = request.match_info['id']
await self.cbpi.dashboard.delete_dashboard(id)
return web.Response(status=204)
@request_mapping(path="/{id:\d+}/content", auth_required=False)
async def get_content(self, request):

View file

@ -180,9 +180,9 @@ class SensorHttpEndpoints(HttpCrudEndpoints):
"""
---
description: Toogle an actor on or off
description: Execute action on sensor
tags:
- Actor
- Sensor
parameters:
- name: "id"
in: "path"

BIN
cbpi/static/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

17
cbpi/static/test.html Normal file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CraftBeerPi 4.0</title>
<style>
body {
background-image: url("/static/splash.png");
}
</style>
</head>
<body>
</body>
</html>

View file

@ -7,11 +7,13 @@ import yaml
def load_config(fname):
try:
with open(fname, 'rt') as f:
data = yaml.load(f)
data = yaml.load(f, Loader=yaml.FullLoader)
return data
except Exception as e:
print(e)
pass
def json_dumps(obj):

View file

@ -1,14 +0,0 @@
listeners:
default:
type: tcp
bind: 0.0.0.0:1883
sys_interval: 20
auth:
allow-anonymous: true
plugins:
- auth_file
- auth_anonymous
topic-check:
enabled': True
plugins':
- topic_taboo

View file

@ -1,13 +0,0 @@
SampleActor:
description: A sample Actor for CraftBeerPi
api: 4.0
author: CraftBeerPi11
pip: requests
repo_url: https://github.com/craftbeerpi/sample_actor
SampleActor2:
description: A sample Actor2 for CraftBeerPi
api: 4.0
author: CraftBeerPi
pip: requests
repo_url: https://github.com/craftbeerpi/sample_actor

View file

@ -1,62 +0,0 @@
import yaml
from aiohttp import web
def load_yaml():
try:
with open('./repo/plugins.yaml', 'rt') as f:
data = yaml.load(f)
return data
except Exception as e:
print(e)
pass
data = load_yaml()
for k, v in data.items():
del v["pip"]
data2 = load_yaml()
async def check(request):
peername = request.transport.get_extra_info('peername')
if peername is not None:
host, port = peername
print(host, port)
data = await request.json()
print(data)
return web.json_response(data=dict(latestversion="4.0.0.3"))
async def reload_yaml(request):
global data, data2
file = load_yaml()
for k, v in file.items():
del v["pip"]
data = file
data2 = load_yaml()
return web.json_response(data=data2)
async def get_list(request):
print("Request List")
return web.json_response(data=data)
async def get_package_name(request):
print("Request Package")
name = request.match_info.get('plugin_name', None)
if name in data2:
package_name = data2[name]["pip"]
else:
package_name = None
return web.json_response(data=dict(package_name=package_name))
app = web.Application()
app.add_routes([
web.get('/list', get_list),
web.post('/check', check),
web.get('/reload', reload_yaml),
web.get('/get/{plugin_name}', get_package_name)])
web.run_app(app, port=2202)

View file

@ -1,11 +1 @@
cbpi-actor:
api: 4.0
author: CraftBeerPi11
description: A sample Actor for CraftBeerPi
repo_url: https://github.com/craftbeerpi/sample_actor
cbpi-ui:
api: 4.0
author: CraftBeerPi
description: A sample Actor2 for CraftBeerPi
repo_url: https://github.com/craftbeerpi/sample_actor
{}

Binary file not shown.

View file

@ -1,72 +1,19 @@
import datetime
import glob
import json
import logging
from logging.handlers import RotatingFileHandler
from time import strftime, localtime
import pandas as pd
import matplotlib.pyplot as plt
sid = 2
import yaml
from cbpi.utils.utils import load_config
package_name = "test222"
with open("./config/plugin_list.txt", 'rt') as f:
print(f)
plugins = yaml.load(f)
if plugins is None:
plugins = {}
data_logger = logging.getLogger('cbpi.sensor.%s' % sid)
data_logger.propagate = False
data_logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler('./logs/sensor_%s.log' % sid, maxBytes=100_000, backupCount=10)
data_logger.addHandler(handler)
import random
now = datetime.datetime.now()
start = datetime.datetime.now()
'''
v = random.randint(50,60)
for i in range(5760):
d = start + datetime.timedelta(seconds=6*i)
formatted_time = d.strftime("%Y-%m-%d %H:%M:%S")
if i % 750 == 0:
v = random.randint(50,60)
data_logger.info("%s,%s" % (formatted_time, v))
'''
def dateparse (time_in_secs):
return datetime.datetime.strptime(time_in_secs, '%Y-%m-%d %H:%M:%S')
all_filenames = glob.glob('./logs/sensor_1.log*')
all_filenames.sort()
all_filenames2 = glob.glob('./logs/sensor_2.log*')
all_filenames2.sort()
combined_csv = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Sensor1'], header=None) for f in all_filenames])
combined_csv2 = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Sensor2'], header=None) for f in all_filenames2])
print(combined_csv)
print(combined_csv2)
m2 = pd.merge(combined_csv, combined_csv2, how='inner', left_index=True, right_index=True)
print(m2)
m2.plot()
m2.plot(y=['Sensor1','Sensor2'])
ts = combined_csv.Sensor1.resample('5000s').max()
#ts.plot(y='Sensor1')
i = 0
def myconverter(o):
if isinstance(o, datetime.datetime):
return o.__str__()
data = {"time": ts.index.tolist(), "data": ts.tolist()}
s1 = json.dumps(data, default = myconverter)
plt.show()
plugins[package_name] = dict(version="1.0", installation_date=now.strftime("%Y-%m-%d %H:%M:%S"))
with open('./config/plugin_list.txt', 'w') as outfile:
yaml.dump(plugins, outfile, default_flow_style=False)

View file

@ -1,7 +1,7 @@
from setuptools import setup, find_packages
setup(name='cbpi',
version='4.0.0.4',
version='4.0.0.5',
description='CraftBeerPi',
author='Manuel Fritsch',
author_email='manuel@craftbeerpi.com',

20
tests/test_cli.py Normal file
View file

@ -0,0 +1,20 @@
import logging
import unittest
from cli import add, remove, list_plugins
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
class CLITest(unittest.TestCase):
def test_install(self):
assert add("cbpi4-ui-plugin") == True
assert add("cbpi4-ui-plugin") == False
assert remove("cbpi4-ui-plugin") == True
def test_list(self):
list_plugins()
if __name__ == '__main__':
unittest.main()