Merge branch 'craftbeerpi:development' into development

This commit is contained in:
Alexander Vollkopf 2022-04-25 07:18:07 +02:00 committed by GitHub
commit 043e75303a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 1945 additions and 1067 deletions

20
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM mcr.microsoft.com/vscode/devcontainers/python:3.9-bullseye
RUN apt-get update \
&& apt-get upgrade -y
RUN apt-get install --no-install-recommends -y \
libatlas-base-dev \
libffi-dev \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel
# Install craftbeerpi requirements for better caching
COPY ./requirements.txt /workspace/requirements.txt
RUN pip3 install --no-cache-dir -r /workspace/requirements.txt
# Install current version of cbpi-ui
RUN mkdir /opt/downloads \
&& curl https://github.com/craftbeerpi/craftbeerpi4-ui/archive/development.zip -L -o /opt/downloads/cbpi-ui.zip \
&& pip3 install --no-cache-dir /opt/downloads/cbpi-ui.zip \
&& rm -rf /opt/downloads

View file

@ -0,0 +1,3 @@
{
"data": []
}

View file

@ -0,0 +1,6 @@
[Desktop Entry]
Type=Application
Name=Chromium
Comment=Chromium Webbrowser
NoDisplay=false
Exec=chromium-browser --noerrordialogs --disable-session-crashed-bubble --disable-infobars --force-device-scale-factor=1.00 --start-fullscreen "http://localhost:8000"

View file

@ -0,0 +1,335 @@
{
"AUTHOR": {
"description": "Author",
"name": "AUTHOR",
"options": null,
"type": "string",
"value": "John Doe"
},
"AddMashInStep": {
"description": "Add MashIn Step automatically if not defined in recipe",
"name": "AddMashInStep",
"options": [
{
"label": "Yes",
"value": "Yes"
},
{
"label": "No",
"value": "No"
}
],
"type": "select",
"value": "Yes"
},
"AutoMode": {
"description": "Use AutoMode in steps",
"name": "AutoMode",
"options": [
{
"label": "Yes",
"value": "Yes"
},
{
"label": "No",
"value": "No"
}
],
"type": "select",
"value": "Yes"
},
"BREWERY_NAME": {
"description": "Brewery Name",
"name": "BREWERY_NAME",
"options": null,
"type": "string",
"value": "CraftBeerPi Brewery"
},
"BoilKettle": {
"description": "Define Kettle that is used for Boil, Whirlpool and Cooldown. If not selected, MASH_TUN will be used",
"name": "BoilKettle",
"options": null,
"type": "kettle",
"value": ""
},
"CBPI_TEST_3": {
"description": "test",
"name": "CBPI_TEST_3",
"options": null,
"type": "string",
"value": "1"
},
"CSVLOGFILES": {
"description": "Write sensor data to csv logfiles",
"name": "CSVLOGFILES",
"options": [
{
"label": "Yes",
"value": "Yes"
},
{
"label": "No",
"value": "No"
}
],
"type": "select",
"value": "Yes"
},
"INFLUXDB": {
"description": "Write sensor data to influxdb",
"name": "INFLUXDB",
"options": [
{
"label": "Yes",
"value": "Yes"
},
{
"label": "No",
"value": "No"
}
],
"type": "select",
"value": "No"
},
"INFLUXDBADDR": {
"description": "IP Address of your influxdb server (If INFLUXDBCLOUD set to Yes use URL Address of your influxdb cloud server)",
"name": "INFLUXDBADDR",
"options": null,
"type": "string",
"value": "localhost"
},
"INFLUXDBCLOUD": {
"description": "Write sensor data to influxdb cloud (INFLUXDB must set to Yes)",
"name": "INFLUXDBCLOUD",
"options": [
{
"label": "Yes",
"value": "Yes"
},
{
"label": "No",
"value": "No"
}
],
"type": "select",
"value": "No"
},
"INFLUXDBNAME": {
"description": "Name of your influxdb database name (If INFLUXDBCLOUD set to Yes use bucket of your influxdb cloud database)",
"name": "INFLUXDBNAME",
"options": null,
"type": "string",
"value": "cbpi4"
},
"INFLUXDBPORT": {
"description": "Port of your influxdb server",
"name": "INFLUXDBPORT",
"options": null,
"type": "string",
"value": "8086"
},
"INFLUXDBPWD": {
"description": "Password for your influxdb database (only if required)(If INFLUXDBCLOUD set to Yes use token of your influxdb cloud database)",
"name": "INFLUXDBPWD",
"options": null,
"type": "string",
"value": " "
},
"INFLUXDBUSER": {
"description": "User name for your influxdb database (only if required)(If INFLUXDBCLOUD set to Yes use organisation of your influxdb cloud database)",
"name": "INFLUXDBUSER",
"options": null,
"type": "string",
"value": " "
},
"MASH_TUN": {
"description": "Default Mash Tun",
"name": "MASH_TUN",
"options": null,
"type": "kettle",
"value": ""
},
"MQTTUpdate": {
"description": "Forced MQTT Update frequency in s for Kettle and Fermenter (no changes in payload required). Restart required after change",
"name": "MQTTUpdate",
"options": [
{
"label": "30",
"value": 30
},
{
"label": "60",
"value": 60
},
{
"label": "120",
"value": 120
},
{
"label": "300",
"value": 300
},
{
"label": "Never",
"value": 0
}
],
"type": "select",
"value": 0
},
"RECIPE_CREATION_PATH": {
"description": "API path to creation plugin. Default: upload . CHANGE ONLY IF USING A RECIPE CREATION PLUGIN",
"name": "RECIPE_CREATION_PATH",
"options": null,
"type": "string",
"value": "upload"
},
"TEMP_UNIT": {
"description": "Temperature Unit",
"name": "TEMP_UNIT",
"options": [
{
"label": "C",
"value": "C"
},
{
"label": "F",
"value": "F"
}
],
"type": "select",
"value": "C"
},
"brewfather_api_key": {
"description": "Brewfather API Key",
"name": "brewfather_api_key",
"options": null,
"type": "string",
"value": ""
},
"brewfather_user_id": {
"description": "Brewfather User ID",
"name": "brewfather_user_id",
"options": null,
"type": "string",
"value": ""
},
"current_dashboard_number": {
"description": "Number of current Dashboard",
"name": "current_dashboard_number",
"options": null,
"type": "number",
"value": 1
},
"max_dashboard_number": {
"description": "Max Number of Dashboards",
"name": "max_dashboard_number",
"options": [
{
"label": "1",
"value": 1
},
{
"label": "2",
"value": 2
},
{
"label": "3",
"value": 3
},
{
"label": "4",
"value": 4
},
{
"label": "5",
"value": 5
},
{
"label": "6",
"value": 6
},
{
"label": "7",
"value": 7
},
{
"label": "8",
"value": 8
},
{
"label": "9",
"value": 9
},
{
"label": "10",
"value": 10
}
],
"type": "select",
"value": 4
},
"steps_boil": {
"description": "Boil step type",
"name": "steps_boil",
"options": null,
"type": "step",
"value": "BoilStep"
},
"steps_boil_temp": {
"description": "Default Boil Temperature for Recipe Creation",
"name": "steps_boil_temp",
"options": null,
"type": "number",
"value": "99"
},
"steps_cooldown": {
"description": "Cooldown step type",
"name": "steps_cooldown",
"options": null,
"type": "step",
"value": "CooldownStep"
},
"steps_cooldown_actor": {
"description": "Actor to trigger cooldown water on and off (default: None)",
"name": "steps_cooldown_actor",
"options": null,
"type": "actor",
"value": ""
},
"steps_cooldown_sensor": {
"description": "Alternative Sensor to monitor temperature durring cooldown (if not selected, Kettle Sensor will be used)",
"name": "steps_cooldown_sensor",
"options": null,
"type": "sensor",
"value": ""
},
"steps_cooldown_temp": {
"description": "Cooldown temp will send notification when this temeprature is reached",
"name": "steps_cooldown_temp",
"options": null,
"type": "number",
"value": "20"
},
"steps_mash": {
"description": "Mash step type",
"name": "steps_mash",
"options": null,
"type": "step",
"value": "MashStep"
},
"steps_mashin": {
"description": "MashIn step type",
"name": "steps_mashin",
"options": null,
"type": "step",
"value": "MashInStep"
},
"steps_mashout": {
"description": "MashOut step type",
"name": "steps_mashout",
"options": null,
"type": "step",
"value": "NotificationStep"
}
}

View file

@ -0,0 +1,20 @@
name: CraftBeerPi
version: 4.0.8
index_url: /cbpi_ui/static/index.html
port: 8000
mqtt: true
mqtt_host: mqtt
mqtt_port: 1883
mqtt_username: craftbeerpi
mqtt_password: cbpiSuperSecMq77!
username: cbpi
password: 123
plugins:
- cbpi4ui

View file

@ -0,0 +1,9 @@
[Unit]
Description=Craftbeer Pi
[Service]
WorkingDirectory=/home/pi
ExecStart=/usr/local/bin/cbpi start
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,3 @@
{
"elements": []
}

View file

@ -0,0 +1,3 @@
{
"data": []
}

View file

@ -0,0 +1,3 @@
{
"data": []
}

View file

@ -0,0 +1,3 @@
{
"data": []
}

View file

@ -0,0 +1,6 @@
{
"basic": {
"name": ""
},
"steps": []
}

View file

@ -0,0 +1,5 @@
#!/bin/bash
USER=craftbeerpi
PASSWORD=craftbeerpi
docker run -it --rm -v "$(pwd)/mosquitto/config/mosquitto.passwd:/opt/passwdfile" eclipse-mosquitto:2 mosquitto_passwd -b /opt/passwdfile $USER $PASSWORD

View file

@ -0,0 +1,51 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/docker-existing-docker-compose
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
{
"name": "CraftBeerPi4",
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
"dockerComposeFile": [
"docker-compose.dev.yml"
],
// The 'service' property is the name of the service for the container that VS Code should
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
"service": "craftbeerpi4-development",
// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/workspace",
// Set *default* container specific settings.json values on container create.
"settings": {
//"terminal.integrated.shell.linux": null
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"ms-azuretools.vscode-docker",
"editorconfig.editorconfig",
"eamodio.gitlens"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
"craftbeerpi4-development:8000",
"mqtt-explorer:4000"
],
// Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [],
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
"shutdownAction": "stopCompose",
// Uncomment the next line to run commands after the container is created - for example installing curl.
//"postCreateCommand": "pip3 install -r ./requirements.txt",
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}

View file

@ -0,0 +1,30 @@
version: '3.4'
services:
mqtt:
image: eclipse-mosquitto:2
volumes:
- "./mosquitto/config:/mosquitto/config"
restart: unless-stopped
craftbeerpi4-development:
build:
context: ../
dockerfile: .devcontainer/Dockerfile
command: /bin/sh -c "while sleep 1000; do :; done"
user: vscode
depends_on:
- mqtt
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- ../:/workspace:cached
mqtt-explorer:
image: smeagolworms4/mqtt-explorer
environment:
HTTP_PORT: 4000
CONFIG_PATH: /mqtt-explorer/config
volumes:
- "./mqtt-explorer/config:/mqtt-explorer/config"
depends_on:
- mqtt
restart: unless-stopped

View file

@ -0,0 +1,10 @@
persistence true
persistence_location /mosquitto/data
log_dest file /mosquitto/log/mosquitto.log
log_dest stdout
password_file /mosquitto/config/mosquitto.passwd
allow_anonymous false
port 1883

View file

@ -0,0 +1,2 @@
craftbeerpi:$7$101$cRIEIwJ9L/+TAFF1$lxT+v9SisokWaRBgB/Scut7DaotH4RMgzHttYHhwuy6m5yatSoac7bwrkztoQ7raNehBhKt/A4VVejnzozdxXA==
mqtt-explorer:$7$101$SFFKvbIBVXFFAIBp$Pgue6DaAfcuhegjEqtTjf+WWgNZ8geiv1/3fXqmJ0APmd0L80wNTSrEhnFdJmHvi0/vW6V9bVKPJfVRDIjPxCw==

View file

@ -0,0 +1,31 @@
{
"ConnectionManager_connections": {
"mqtt.eclipse.org": {
"configVersion": 1,
"certValidation": true,
"clientId": "mqtt-explorer-8eb042b9",
"id": "mqtt.eclipse.org",
"name": "CraftBeerPi MQTT Explorer",
"encryption": false,
"subscriptions": [
{
"topic": "#",
"qos": 0
},
{
"topic": "$SYS/#",
"qos": 0
}
],
"type": "mqtt",
"host": "mqtt",
"port": 1883,
"protocol": "mqtt",
"changeSet": {
"password": "mqtt-explorer"
},
"username": "mqtt-explorer",
"password": "mqtt-explorer"
}
}
}

View file

@ -27,8 +27,11 @@ jobs:
- name: Clean - name: Clean
run: python setup.py clean --all run: python setup.py clean --all
# - name: Run tests - name: Install Requirements
# run: python -m unittest tests run: pip3 install -r requirements.txt
- name: Run tests
run: coverage run --source cbpi -m pytest tests
- name: Build source distribution package for CraftBeerPi - name: Build source distribution package for CraftBeerPi
run: python setup.py sdist run: python setup.py sdist
@ -61,7 +64,7 @@ jobs:
if [[ $GITHUB_REF_NAME == master ]] || [[ $GITHUB_REF_NAME == main ]]; then if [[ $GITHUB_REF_NAME == master ]] || [[ $GITHUB_REF_NAME == main ]]; then
# when building master/main use :latest tag and the version number # when building master/main use :latest tag and the version number
# from the cbpi/__init__.py file # from the cbpi/__init__.py file
VERSION=$(grep -o -E "(([0-9]{1,2}[.]?){3}[0-9]+)" cbpi/__init__.py) VERSION=$(grep -o -E "(([0-9]{1,2}[.]?){2,3}[0-9]+)" cbpi/__init__.py)
LATEST_IMAGE=${{ env.image-name }}:latest LATEST_IMAGE=${{ env.image-name }}:latest
BUILD_CACHE_IMAGE_NAME=${LATEST_IMAGE} BUILD_CACHE_IMAGE_NAME=${LATEST_IMAGE}
TAGS="${LATEST_IMAGE},${{ env.image-name }}:v${VERSION}" TAGS="${LATEST_IMAGE},${{ env.image-name }}:v${VERSION}"

4
.gitignore vendored
View file

@ -14,6 +14,6 @@ node_modules
.vscode .vscode
.venv* .venv*
.DS_Store .DS_Store
.vscode/
config/* config/*
logs/ logs/
.coverage

16
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Run CraftBeerPi4",
"type": "python",
"request": "launch",
"module": "run",
"args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "start"]
}
]
}

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"python.pythonPath": "/bin/python3",
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

View file

@ -1,7 +1,7 @@
FROM alpine:latest as download FROM alpine:latest as download
RUN apk --no-cache add curl && mkdir /downloads RUN apk --no-cache add curl && mkdir /downloads
# Download installation files # Download installation files
RUN curl https://github.com/avollkopf/craftbeerpi4-ui/archive/main.zip -L -o ./downloads/cbpi-ui.zip RUN curl https://github.com/craftbeerpi/craftbeerpi4-ui/archive/main.zip -L -o ./downloads/cbpi-ui.zip
FROM python:3.9 as base FROM python:3.9 as base

View file

@ -1,13 +1,13 @@
# CraftBeerPi 4 # CraftBeerPi 4
[![Build](https://github.com/avollkopf/craftbeerpi4/actions/workflows/build.yml/badge.svg)](https://github.com/avollkopf/craftbeerpi4/actions/workflows/build.yml) [![Build](https://github.com/craftbeerpi/craftbeerpi4/actions/workflows/build.yml/badge.svg)](https://github.com/craftbeerpi/craftbeerpi4/actions/workflows/build.yml)
[![GitHub license](https://img.shields.io/github/license/avollkopf/craftbeerpi4)](https://github.com/avollkopf/craftbeerpi4/blob/master/LICENSE) [![GitHub license](https://img.shields.io/github/license/craftbeerpi/craftbeerpi4)](https://github.com/craftbeerpi/craftbeerpi4/blob/master/LICENSE)
![GitHub issues](https://img.shields.io/github/issues-raw/Manuel83/craftbeerpi4) ![GitHub issues](https://img.shields.io/github/issues-raw/craftbeerpi/craftbeerpi4)
![PyPI](https://img.shields.io/pypi/v/cbpi) ![PyPI](https://img.shields.io/pypi/v/cbpi)
![Happy Brewing](https://img.shields.io/badge/CraftBeerPi%204-Happy%20Brewing-%23FBB117) ![Happy Brewing](https://img.shields.io/badge/CraftBeerPi%204-Happy%20Brewing-%23FBB117)
<p align="center"> <p align="center">
<img src="https://github.com/Manuel83/craftbeerpi4-ui/blob/main/cbpi4ui/public/logo192.png?raw=true" alt="CraftBeerPi Logo"/> <img src="https://github.com/craftbeerpi/craftbeerpi4-ui/blob/main/cbpi4ui/public/logo192.png?raw=true" alt="CraftBeerPi Logo"/>
</p> </p>
CraftBeerPi 4 is an open source software solution to control the brewing and CraftBeerPi 4 is an open source software solution to control the brewing and
@ -21,7 +21,19 @@ in the documentation, that can be found here: [gitbook.io](https://openbrewing.g
Plugins extend the base functionality of CraftBeerPi 4. Plugins extend the base functionality of CraftBeerPi 4.
You can find a list of available plugins [here](https://openbrewing.gitbook.io/craftbeerpi4_support/master/plugin-installation#plugin-list). You can find a list of available plugins [here](https://openbrewing.gitbook.io/craftbeerpi4_support/master/plugin-installation#plugin-list).
## 🧑‍🤝‍🧑 Contributers ## 🧑‍🤝‍🧑 Contribute
You want to help develop CraftBeerPi4? To get you quickly stated, this repository comes with a preconfigured
development environment. To be able to use this environment you need 2 things installed on your computer:
- docker
- visual studio code (vscode)
To start developing clone this repository, open the folder in vscode and use the _development container_ feature. The command is called _Reopen in container_. Please note that this quick start setup does not work if you want to develop directly on a 32bit raspberry pi os because docker is only available for 64bit arm plattform. Please use the regular development setup for that.
For a more detailed description of a development setup without the _development container_ feature see the documentation page:
[gitbook.io](https://openbrewing.gitbook.io/craftbeerpi4_support/)
### Contributors
Thanks to all the people who have contributed Thanks to all the people who have contributed
[![contributors](https://contributors-img.web.app/image?repo=avollkopf/craftbeerpi4)](https://github.com/avollkopf/craftbeerpi4/graphs/contributors) [![contributors](https://contributors-img.web.app/image?repo=craftbeerpi/craftbeerpi4)](https://github.com/craftbeerpi/craftbeerpi4/graphs/contributors)

View file

@ -1 +1,3 @@
__version__ = "4.0.2.0.a17" __version__ = "4.0.5.a11"
__codename__ = "Spring Break"

View file

@ -38,6 +38,12 @@ class CBPiBase(metaclass=ABCMeta):
async def set_fermenter_target_temp(self,id, temp): async def set_fermenter_target_temp(self,id, temp):
await self.cbpi.fermenter.set_target_temp(id, temp) await self.cbpi.fermenter.set_target_temp(id, temp)
def get_fermenter_target_pressure(self,id):
return self.cbpi.fermenter._find_by_id(id).target_pressure
async def set_fermenter_target_pressure(self,id, temp):
await self.cbpi.fermenter.set_target_pressure(id, temp)
def get_sensor(self,id): def get_sensor(self,id):
return self.cbpi.sensor.find_by_id(id) return self.cbpi.sensor.find_by_id(id)

View file

@ -126,21 +126,23 @@ class Fermenter:
id: str = None id: str = None
name: str = None name: str = None
sensor: Sensor = None sensor: Sensor = None
pressure_sensor : Sensor = None
heater: Actor = None heater: Actor = None
cooler: Actor = None cooler: Actor = None
valve: Actor = None
brewname: str = None brewname: str = None
description : str = None description : str = None
props: Props = Props() props: Props = Props()
target_temp: int = 0 target_temp: float = 0
target_pressure: float = 0
type: str = None type: str = None
steps: List[Step]= field(default_factory=list) steps: List[Step]= field(default_factory=list)
instance: str = None instance: str = None
def __str__(self): def __str__(self):
return "name={} props={} temp={}".format(self.name, self.props, self.target_temp) return "name={} props={} temp={}".format(self.name, self.props, self.target_temp, self.target_pressure)
# return "id={} name={} sensor={} heater={} cooler={} brewname={} props={} temp={} type={} steps={}".format(self.id, self.name, self.sensor, self.heater, self.cooler, self.brewname, self.props, self.target_temp, self.type, self.steps)
def to_dict(self): def to_dict(self):
if self.instance is not None: if self.instance is not None:
@ -151,7 +153,7 @@ class Fermenter:
state = False state = False
steps = list(map(lambda item: item.to_dict(), self.steps)) steps = list(map(lambda item: item.to_dict(), self.steps))
return dict(id=self.id, name=self.name, state=state, sensor=self.sensor, heater=self.heater, cooler=self.cooler, brewname=self.brewname, description=self.description, props=self.props.to_dict() if self.props is not None else None, target_temp=self.target_temp, type=self.type, steps=steps) return dict(id=self.id, name=self.name, state=state, sensor=self.sensor, pressure_sensor=self.pressure_sensor, heater=self.heater, cooler=self.cooler, valve=self.valve, brewname=self.brewname, description=self.description, props=self.props.to_dict() if self.props is not None else None, target_temp=self.target_temp, target_pressure=self.target_pressure, type=self.type, steps=steps)
@dataclass @dataclass

View file

@ -48,7 +48,7 @@ class CBPiExtension():
return data return data
except: except:
logger.warning("Faild to load config %s/config.yaml" % path) logger.warning("Failed to load config %s/config.yaml" % path)

View file

@ -39,7 +39,7 @@ class CBPiSensor(CBPiBase, metaclass=ABCMeta):
self.cbpi.push_update("cbpi/sensordata/{}".format(self.id), dict(id=self.id, value=value), retain=True) self.cbpi.push_update("cbpi/sensordata/{}".format(self.id), dict(id=self.id, value=value), retain=True)
# self.cbpi.push_update("cbpi/sensor/{}/udpate".format(self.id), dict(id=self.id, value=value), retain=True) # self.cbpi.push_update("cbpi/sensor/{}/udpate".format(self.id), dict(id=self.id, value=value), retain=True)
except: except:
logging.error("Faild to push sensor update") logging.error("Failed to push sensor update")
async def start(self): async def start(self):
pass pass

View file

@ -1,433 +1,285 @@
import argparse
import datetime
import logging import logging
import subprocess
import sys
import re
import requests import requests
import yaml from cbpi.configFolder import ConfigFolder
from cbpi.utils.utils import load_config from cbpi.utils.utils import load_config
from zipfile import ZipFile from zipfile import ZipFile
from cbpi.craftbeerpi import CraftBeerPi from cbpi.craftbeerpi import CraftBeerPi
import os import os
import platform
import pathlib
import pkgutil import pkgutil
import shutil import shutil
import yaml
import click import click
from subprocess import call from subprocess import call
import zipfile
from colorama import Fore, Back, Style from colorama import Fore, Back, Style
from importlib import import_module
import importlib import importlib
from jinja2 import Template from importlib_metadata import metadata
from importlib_metadata import metadata, version
from tabulate import tabulate from tabulate import tabulate
from PyInquirer import prompt, print_json from PyInquirer import prompt, print_json
def create_config_file():
if os.path.exists(os.path.join(".", 'config', "config.yaml")) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", "config.yaml")
destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile)
if os.path.exists(os.path.join(".", 'config', "actor.json")) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", "actor.json")
destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile)
if os.path.exists(os.path.join(".", 'config', "sensor.json")) is False: class CraftBeerPiCli():
srcfile = os.path.join(os.path.dirname(__file__), "config", "sensor.json") def __init__(self, config) -> None:
destfile = os.path.join(".", 'config') self.config = config
shutil.copy(srcfile, destfile) pass
if os.path.exists(os.path.join(".", 'config', "kettle.json")) is False: def setup(self):
srcfile = os.path.join(os.path.dirname(__file__), "config", "kettle.json") print("Setting up CraftBeerPi")
destfile = os.path.join(".", 'config') self.config.create_home_folder_structure()
shutil.copy(srcfile, destfile) self.config.create_config_file()
if os.path.exists(os.path.join(".", 'config', "fermenter_data.json")) is False: def start(self):
srcfile = os.path.join(os.path.dirname(__file__), "config", "fermenter_data.json") if self.config.check_for_setup() is False:
destfile = os.path.join(".", 'config') return
shutil.copy(srcfile, destfile) print("START")
cbpi = CraftBeerPi(self.config)
cbpi.start()
if os.path.exists(os.path.join(".", 'config', "step_data.json")) is False: def setup_one_wire(self):
srcfile = os.path.join(os.path.dirname(__file__), "config", "step_data.json") print("Setting up 1Wire")
destfile = os.path.join(".", 'config') with open('/boot/config.txt', 'w') as f:
shutil.copy(srcfile, destfile) f.write("dtoverlay=w1-gpio,gpiopin=4,pullup=on")
print("/boot/config.txt created")
if os.path.exists(os.path.join(".", 'config', "config.json")) is False: def list_one_wire(self):
srcfile = os.path.join(os.path.dirname(__file__), "config", "config.json") print("List 1Wire")
destfile = os.path.join(".", 'config') call(["modprobe", "w1-gpio"])
shutil.copy(srcfile, destfile) call(["modprobe", "w1-therm"])
if os.path.exists(os.path.join(".", 'config', "dashboard", "cbpi_dashboard_1.json")) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", "dashboard", "cbpi_dashboard_1.json")
destfile = os.path.join(".", "config", "dashboard")
shutil.copy(srcfile, destfile)
if os.path.exists(os.path.join(".", 'config', "carftbeerpi.service")) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", "craftbeerpi.service")
destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile)
if os.path.exists(os.path.join(".", 'config', "chromium.desktop")) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", "chromium.desktop")
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)
pathlib.Path(os.path.join(".", 'config/dashboard')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(".", 'config/dashboard/widgets')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(".", 'config/recipes')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(".", 'config/fermenterrecipes')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(".", 'config/upload')).mkdir(parents=True, exist_ok=True)
print("Folder created")
def setup_one_wire():
print("Setting up 1Wire")
with open('/boot/config.txt', 'w') as f:
f.write("dtoverlay=w1-gpio,gpiopin=4,pullup=on")
print("/boot/config.txt created")
def list_one_wire():
print("List 1Wire")
call(["modprobe", "w1-gpio"])
call(["modprobe", "w1-therm"])
try:
for dirname in os.listdir('/sys/bus/w1/devices'):
if (dirname.startswith("28") or dirname.startswith("10")):
print(dirname)
except Exception as e:
print(e)
def copy_splash():
srcfile = os.path.join(".", "config", "splash.png")
destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile)
print("Splash Srceen created")
def clear_db():
import os.path
if os.path.exists(os.path.join(".", "craftbeerpi.db")) is True:
os.remove(os.path.join(".", "craftbeerpi.db"))
print("database cleared")
def recursive_chown(path, owner, group):
for dirpath, dirnames, filenames in os.walk(path):
shutil.chown(dirpath, owner, group)
for filename in filenames:
shutil.chown(os.path.join(dirpath, filename), owner, group)
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
if os.path.exists(os.path.join(".", "config", "upload")) is False:
print("***************************************************")
print("CraftBeerPi upload folder not found: %s" % os.path.join(".", "config/upload"))
print("Please run 'cbpi setup' before starting the server ")
print("***************************************************")
return False
# if os.path.exists(os.path.join(".", "config", "fermenterrecipes")) is False:
# print("***************************************************")
# print("CraftBeerPi fermenterrecipes folder not found: %s" % os.path.join(".", "config/fermenterrecipes"))
# print("Please run 'cbpi setup' before starting the server ")
# print("***************************************************")
# return False
backupfile = os.path.join(".", "restored_config.zip")
if os.path.exists(os.path.join(backupfile)) is True:
print("***************************************************")
print("Found backup of config. Starting restore")
required_content=['dashboard/', 'recipes/', 'upload/', 'config.json', 'config.yaml']
zip=zipfile.ZipFile(backupfile)
zip_content_list = zip.namelist()
zip_content = True
print("Checking content of zip file")
for content in required_content:
try:
check = zip_content_list.index(content)
except:
zip_content = False
if zip_content == True:
print("Found correct content. Starting Restore process")
output_path = pathlib.Path(os.path.join(".", 'config'))
system = platform.system()
print(system)
if system != "Windows":
owner = output_path.owner()
group = output_path.group()
print("Removing old config folder")
shutil.rmtree(output_path, ignore_errors=True)
print("Extracting zip file to config folder")
zip.extractall(output_path)
zip.close()
if system != "Windows":
print("Changing owner and group of config folder recursively to {}:{}".format(owner,group))
recursive_chown(output_path, owner, group)
print("Removing backup file")
os.remove(backupfile)
else:
print("Wrong Content in zip file. No restore possible")
print("Removing zip file")
os.remove(backupfile)
print("***************************************************")
return True
else:
return True
def plugins_list():
result = []
print("")
print(Fore.LIGHTYELLOW_EX,"List of active plugins", Style.RESET_ALL)
print("")
discovered_plugins = {
name: importlib.import_module(name)
for finder, name, ispkg
in pkgutil.iter_modules()
if name.startswith('cbpi') and len(name) > 4
}
for key, module in discovered_plugins.items():
try: try:
meta = metadata(key) for dirname in os.listdir('/sys/bus/w1/devices'):
result.append(dict(Name=meta["Name"], Version=meta["Version"], Author=meta["Author"], Homepage=meta["Home-page"], Summary=meta["Summary"])) if (dirname.startswith("28") or dirname.startswith("10")):
print(dirname)
except Exception as e: except Exception as e:
print(e) print(e)
print(Fore.LIGHTGREEN_EX, tabulate(result, headers="keys"), Style.RESET_ALL)
def plugins_list(self):
def plugin_create(): result = []
print("")
print(Fore.LIGHTYELLOW_EX,"List of active plugins", Style.RESET_ALL)
print("")
discovered_plugins = {
print("Plugin Creation") name: importlib.import_module(name)
print("") for finder, name, ispkg
in pkgutil.iter_modules()
questions = [ if name.startswith('cbpi') and len(name) > 4
{
'type': 'input',
'name': 'name',
'message': 'Plugin Name:',
} }
] for key, module in discovered_plugins.items():
try:
meta = metadata(key)
result.append(dict(Name=meta["Name"], Version=meta["Version"], Author=meta["Author"], Homepage=meta["Home-page"], Summary=meta["Summary"]))
except Exception as e:
print(e)
print(Fore.LIGHTGREEN_EX, tabulate(result, headers="keys"), Style.RESET_ALL)
answers = prompt(questions)
name = "cbpi4_" + answers["name"] def plugin_create(self):
if os.path.exists(os.path.join(".", name)) is True: print("Plugin Creation")
print("Cant create Plugin. Folder {} already exists ".format(name)) print("")
return
url = 'https://github.com/Manuel83/craftbeerpi4-plugin-template/archive/main.zip' questions = [
r = requests.get(url) {
with open('temp.zip', 'wb') as f: 'type': 'input',
f.write(r.content) 'name': 'name',
'message': 'Plugin Name:',
}
]
with ZipFile('temp.zip', 'r') as repo_zip: answers = prompt(questions)
repo_zip.extractall()
os.rename("./craftbeerpi4-plugin-template-main", os.path.join(".", name)) name = "cbpi4_" + answers["name"]
os.rename(os.path.join(".", name, "src"), os.path.join(".", name, name)) if os.path.exists(os.path.join(".", name)) is True:
print("Cant create Plugin. Folder {} already exists ".format(name))
return
import jinja2 url = 'https://github.com/Manuel83/craftbeerpi4-plugin-template/archive/main.zip'
r = requests.get(url)
with open('temp.zip', 'wb') as f:
f.write(r.content)
templateLoader = jinja2.FileSystemLoader(searchpath=os.path.join(".", name)) with ZipFile('temp.zip', 'r') as repo_zip:
templateEnv = jinja2.Environment(loader=templateLoader) repo_zip.extractall()
TEMPLATE_FILE = "setup.py"
template = templateEnv.get_template(TEMPLATE_FILE)
outputText = template.render(name=name)
with open(os.path.join(".", name, "setup.py"), "w") as fh: os.rename("./craftbeerpi4-plugin-template-main", os.path.join(".", name))
fh.write(outputText) os.rename(os.path.join(".", name, "src"), os.path.join(".", name, name))
TEMPLATE_FILE = "MANIFEST.in" import jinja2
template = templateEnv.get_template(TEMPLATE_FILE)
outputText = template.render(name=name)
with open(os.path.join(".", name, "MANIFEST.in"), "w") as fh:
fh.write(outputText)
TEMPLATE_FILE = os.path.join("/", name, "config.yaml") templateLoader = jinja2.FileSystemLoader(searchpath=os.path.join(".", name))
template = templateEnv.get_template(TEMPLATE_FILE) templateEnv = jinja2.Environment(loader=templateLoader)
outputText = template.render(name=name) TEMPLATE_FILE = "setup.py"
template = templateEnv.get_template(TEMPLATE_FILE)
outputText = template.render(name=name)
with open(os.path.join(".", name, name, "config.yaml"), "w") as fh: with open(os.path.join(".", name, "setup.py"), "w") as fh:
fh.write(outputText) fh.write(outputText)
TEMPLATE_FILE = "MANIFEST.in"
print("") template = templateEnv.get_template(TEMPLATE_FILE)
print("") outputText = template.render(name=name)
print("Plugin {}{}{} created! ".format(Fore.LIGHTGREEN_EX, name, Style.RESET_ALL) ) with open(os.path.join(".", name, "MANIFEST.in"), "w") as fh:
print("") fh.write(outputText)
print("Developer Documentation: https://openbrewing.gitbook.io/craftbeerpi4_support/readme/development")
print("") TEMPLATE_FILE = os.path.join("/", name, "config.yaml")
print("Happy developing! Cheers") operatingsystem = str(platform.system()).lower()
print("") if operatingsystem.startswith("win"):
print("") TEMPLATE_FILE=str(TEMPLATE_FILE).replace('\\','/')
template = templateEnv.get_template(TEMPLATE_FILE)
outputText = template.render(name=name)
with open(os.path.join(".", name, name, "config.yaml"), "w") as fh:
fh.write(outputText)
print("")
print("")
print("Plugin {}{}{} created! ".format(Fore.LIGHTGREEN_EX, name, Style.RESET_ALL) )
print("")
print("Developer Documentation: https://openbrewing.gitbook.io/craftbeerpi4_support/readme/development")
print("")
print("Happy developing! Cheers")
print("")
print("")
def autostart(self, name):
'''Enable or disable autostart'''
if(name == "status"):
if os.path.exists(os.path.join("/etc/systemd/system","craftbeerpi.service")) is True:
print("CraftBeerPi Autostart is {}ON{}".format(Fore.LIGHTGREEN_EX,Style.RESET_ALL))
else:
print("CraftBeerPi Autostart is {}OFF{}".format(Fore.RED,Style.RESET_ALL))
elif(name == "on"):
print("Add craftbeerpi.service to systemd")
try:
if os.path.exists(os.path.join("/etc/systemd/system","craftbeerpi.service")) is False:
srcfile = self.config.get_file_path("craftbeerpi.service")
destfile = os.path.join("/etc/systemd/system")
shutil.copy(srcfile, destfile)
print("Copied craftbeerpi.service to /etc/systemd/system")
os.system('systemctl enable craftbeerpi.service')
print('Enabled craftbeerpi service')
os.system('systemctl start craftbeerpi.service')
print('Started craftbeerpi.service')
else:
print("craftbeerpi.service is already located in /etc/systemd/system")
except Exception as e:
print(e)
return
return
elif(name == "off"):
print("Remove craftbeerpi.service from systemd")
try:
status = os.popen('systemctl list-units --type=service --state=running | grep craftbeerpi.service').read()
if status.find("craftbeerpi.service") != -1:
os.system('systemctl stop craftbeerpi.service')
print('Stopped craftbeerpi service')
os.system('systemctl disable craftbeerpi.service')
print('Removed craftbeerpi.service as service')
else:
print('craftbeerpi.service service is not running')
if os.path.exists(os.path.join("/etc/systemd/system","craftbeerpi.service")) is True:
os.remove(os.path.join("/etc/systemd/system","craftbeerpi.service"))
print("Deleted craftbeerpi.service from /etc/systemd/system")
else:
print("craftbeerpi.service is not located in /etc/systemd/system")
except Exception as e:
print(e)
return
return
def chromium(self, name):
'''Enable or disable autostart'''
if(name == "status"):
if os.path.exists(os.path.join("/etc/xdg/autostart/","chromium.desktop")) is True:
print("CraftBeerPi Chromium Desktop is {}ON{}".format(Fore.LIGHTGREEN_EX,Style.RESET_ALL))
else:
print("CraftBeerPi Chromium Desktop is {}OFF{}".format(Fore.RED,Style.RESET_ALL))
elif(name == "on"):
print("Add chromium.desktop to /etc/xdg/autostart/")
try:
if os.path.exists(os.path.join("/etc/xdg/autostart/","chromium.desktop")) is False:
srcfile = self.config.get_file_path("chromium.desktop")
destfile = os.path.join("/etc/xdg/autostart/")
shutil.copy(srcfile, destfile)
print("Copied chromium.desktop to /etc/xdg/autostart/")
else:
print("chromium.desktop is already located in /etc/xdg/autostart/")
except Exception as e:
print(e)
return
return
elif(name == "off"):
print("Remove chromium.desktop from /etc/xdg/autostart/")
try:
if os.path.exists(os.path.join("/etc/xdg/autostart/","chromium.desktop")) is True:
os.remove(os.path.join("/etc/xdg/autostart/","chromium.desktop"))
print("Deleted chromium.desktop from /etc/xdg/autostart/")
else:
print("chromium.desktop is not located in /etc/xdg/autostart/")
except Exception as e:
print(e)
return
return
@click.group() @click.group()
def main(): @click.pass_context
@click.option('--config-folder-path', '-c', default="./config", type=click.Path(), help="Specify where the config folder is located. Defaults to './config'.")
def main(context, config_folder_path):
print("---------------------") print("---------------------")
print("Welcome to CBPi") print("Welcome to CBPi")
print("---------------------") print("---------------------")
level = logging.INFO level = logging.INFO
logging.basicConfig(level=level, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') logging.basicConfig(level=level, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path))
context.obj = cbpi_cli
pass pass
@main.command()
@click.command() @click.pass_context
def setup(): def setup(context):
'''Create Config folder''' '''Create Config folder'''
print("Setting up CraftBeerPi") context.obj.setup()
create_home_folder_structure()
create_config_file()
@main.command()
@click.command() @click.pass_context
@click.option('--list', is_flag=True, help="List all 1Wire Devices") @click.option('--list', is_flag=True, help="List all 1Wire Devices")
@click.option('--setup', is_flag=True, help="Setup 1Wire on Raspberry Pi") @click.option('--setup', is_flag=True, help="Setup 1Wire on Raspberry Pi")
def onewire(list, setup): def onewire(context, list, setup):
'''Setup 1wire on Raspberry Pi''' '''Setup 1wire on Raspberry Pi'''
if setup is True: if setup is True:
setup_one_wire() context.obj.setup_one_wire()
if list is True: if list is True:
list_one_wire() context.obj.list_one_wire()
@main.command()
@click.pass_context
def start(context):
context.obj.start()
@main.command()
@click.command() @click.pass_context
def start(): def plugins(context):
'''Lets go brewing'''
if check_for_setup() is False:
return
print("Starting up CraftBeerPi ...")
cbpi = CraftBeerPi()
cbpi.start()
@click.command()
def plugins():
'''List active plugins''' '''List active plugins'''
plugins_list() context.obj.plugins_list()
return
@click.command() @click.command()
def create(): @click.pass_context
def create(context):
'''Create New Plugin''' '''Create New Plugin'''
plugin_create() context.obj.plugin_create()
@click.command() @main.command()
@click.pass_context
@click.argument('name') @click.argument('name')
def autostart(name): def autostart(context, name):
'''(on|off|status) Enable or disable autostart''' '''(on|off|status) Enable or disable autostart'''
if(name == "status"): context.obj.autostart(name)
if os.path.exists(os.path.join("/etc/systemd/system","craftbeerpi.service")) is True:
print("CraftBeerPi Autostart is {}ON{}".format(Fore.LIGHTGREEN_EX,Style.RESET_ALL))
else:
print("CraftBeerPi Autostart is {}OFF{}".format(Fore.RED,Style.RESET_ALL))
elif(name == "on"):
print("Add craftbeerpi.service to systemd")
try:
if os.path.exists(os.path.join("/etc/systemd/system","craftbeerpi.service")) is False:
srcfile = os.path.join(".", "config", "craftbeerpi.service")
destfile = os.path.join("/etc/systemd/system")
shutil.copy(srcfile, destfile)
print("Copied craftbeerpi.service to /etc/systemd/system")
os.system('systemctl enable craftbeerpi.service')
print('Enabled craftbeerpi service')
os.system('systemctl start craftbeerpi.service')
print('Started craftbeerpi.service')
else:
print("craftbeerpi.service is already located in /etc/systemd/system")
except Exception as e:
print(e)
return
return
elif(name == "off"):
print("Remove craftbeerpi.service from systemd")
try:
status = os.popen('systemctl list-units --type=service --state=running | grep craftbeerpi.service').read()
if status.find("craftbeerpi.service") != -1:
os.system('systemctl stop craftbeerpi.service')
print('Stopped craftbeerpi service')
os.system('systemctl disable craftbeerpi.service')
print('Removed craftbeerpi.service as service')
else:
print('craftbeerpi.service service is not running')
if os.path.exists(os.path.join("/etc/systemd/system","craftbeerpi.service")) is True:
os.remove(os.path.join("/etc/systemd/system","craftbeerpi.service"))
print("Deleted craftbeerpi.service from /etc/systemd/system")
else:
print("craftbeerpi.service is not located in /etc/systemd/system")
except Exception as e:
print(e)
return
return
@click.command() @main.command()
@click.pass_context
@click.argument('name') @click.argument('name')
def chromium(name): def chromium(context, name):
'''(on|off|status) Enable or disable Kiosk mode''' '''(on|off|status) Enable or disable Kiosk mode'''
if(name == "status"): context.obj.chromium(name)
if os.path.exists(os.path.join("/etc/xdg/autostart/","chromium.desktop")) is True:
print("CraftBeerPi Chromium Desktop is {}ON{}".format(Fore.LIGHTGREEN_EX,Style.RESET_ALL))
else:
print("CraftBeerPi Chromium Desktop is {}OFF{}".format(Fore.RED,Style.RESET_ALL))
elif(name == "on"):
print("Add chromium.desktop to /etc/xdg/autostart/")
try:
if os.path.exists(os.path.join("/etc/xdg/autostart/","chromium.desktop")) is False:
srcfile = os.path.join(".", "config", "chromium.desktop")
destfile = os.path.join("/etc/xdg/autostart/")
shutil.copy(srcfile, destfile)
print("Copied chromium.desktop to /etc/xdg/autostart/")
else:
print("chromium.desktop is already located in /etc/xdg/autostart/")
except Exception as e:
print(e)
return
return
elif(name == "off"):
print("Remove chromium.desktop from /etc/xdg/autostart/")
try:
if os.path.exists(os.path.join("/etc/xdg/autostart/","chromium.desktop")) is True:
os.remove(os.path.join("/etc/xdg/autostart/","chromium.desktop"))
print("Deleted chromium.desktop from /etc/xdg/autostart/")
else:
print("chromium.desktop is not located in /etc/xdg/autostart/")
except Exception as e:
print(e)
return
return
main.add_command(setup)
main.add_command(start)
main.add_command(autostart)
main.add_command(chromium)
main.add_command(plugins)
main.add_command(onewire)
main.add_command(create)

133
cbpi/configFolder.py Normal file
View file

@ -0,0 +1,133 @@
import os
from os import listdir
from os.path import isfile, join
import pathlib
import platform
import shutil
import zipfile
import glob
class ConfigFolder:
def __init__(self, configFolderPath):
self._rawPath = configFolderPath
def config_file_exists(self, path):
return os.path.exists(self.get_file_path(path))
def get_file_path(self, file):
return os.path.join(self._rawPath, file)
def get_upload_file(self, file):
return os.path.join(self._rawPath, 'upload', file)
def get_recipe_file_by_id(self, recipe_id):
return os.path.join(self._rawPath, 'recipes', "{}.yaml".format(recipe_id))
def get_fermenter_recipe_by_id(self, recipe_id):
return os.path.join(self._rawPath, 'fermenterrecipes', "{}.yaml".format(recipe_id))
def get_all_fermenter_recipes(self):
fermenter_recipes_folder = os.path.join(self._rawPath, 'fermenterrecipes')
fermenter_recipe_ids = [os.path.splitext(f)[0] for f in listdir(fermenter_recipes_folder) if isfile(join(fermenter_recipes_folder, f)) and f.endswith(".yaml")]
return fermenter_recipe_ids
def check_for_setup(self):
if self.config_file_exists("config.yaml") is False:
print("***************************************************")
print("CraftBeerPi Config File not found: %s" % self.get_file_path("config.yaml"))
print("Please run 'cbpi setup' before starting the server ")
print("***************************************************")
return False
if self.config_file_exists("upload") is False:
print("***************************************************")
print("CraftBeerPi upload folder not found: %s" % self.get_file_path("upload"))
print("Please run 'cbpi setup' before starting the server ")
print("***************************************************")
return False
# if os.path.exists(os.path.join(".", "config", "fermenterrecipes")) is False:
# print("***************************************************")
# print("CraftBeerPi fermenterrecipes folder not found: %s" % os.path.join(".", "config/fermenterrecipes"))
# print("Please run 'cbpi setup' before starting the server ")
# print("***************************************************")
# return False
backupfile = os.path.join(".", "restored_config.zip")
if os.path.exists(os.path.join(backupfile)) is True:
print("***************************************************")
print("Found backup of config. Starting restore")
required_content=['dashboard/', 'recipes/', 'upload/', 'config.json', 'config.yaml']
zip=zipfile.ZipFile(backupfile)
zip_content_list = zip.namelist()
zip_content = True
print("Checking content of zip file")
for content in required_content:
try:
check = zip_content_list.index(content)
except:
zip_content = False
if zip_content == True:
print("Found correct content. Starting Restore process")
output_path = pathlib.Path(self._rawPath)
system = platform.system()
print(system)
if system != "Windows":
owner = output_path.owner()
group = output_path.group()
print("Removing old config folder")
shutil.rmtree(output_path, ignore_errors=True)
print("Extracting zip file to config folder")
zip.extractall(output_path)
zip.close()
if system != "Windows":
print(f"Changing owner and group of config folder recursively to {owner}:{group}")
self.recursive_chown(output_path, owner, group)
print("Removing backup file")
os.remove(backupfile)
else:
print("Wrong Content in zip file. No restore possible")
print("Removing zip file")
os.remove(backupfile)
print("***************************************************")
def copyDefaultFileIfNotExists(self, file):
if self.config_file_exists(file) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", file)
destfile = os.path.join(self._rawPath, file)
shutil.copy(srcfile, destfile)
def create_config_file(self):
self.copyDefaultFileIfNotExists("config.yaml")
self.copyDefaultFileIfNotExists("actor.json")
self.copyDefaultFileIfNotExists("sensor.json")
self.copyDefaultFileIfNotExists("kettle.json")
self.copyDefaultFileIfNotExists("fermenter_data.json")
self.copyDefaultFileIfNotExists("step_data.json")
self.copyDefaultFileIfNotExists("config.json")
self.copyDefaultFileIfNotExists("craftbeerpi.service")
self.copyDefaultFileIfNotExists("chromium.desktop")
if os.path.exists(os.path.join(self._rawPath, "dashboard", "cbpi_dashboard_1.json")) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", "dashboard", "cbpi_dashboard_1.json")
destfile = os.path.join(self._rawPath, "dashboard")
shutil.copy(srcfile, destfile)
print("Config Folder created")
def create_home_folder_structure(configFolder):
pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True)
configFolder.create_folders()
print("Folder created")
def create_folders(self):
pathlib.Path(self._rawPath).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(self._rawPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(self._rawPath, 'recipes')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(self._rawPath, 'upload')).mkdir(parents=True, exist_ok=True)
def recursive_chown(path, owner, group):
for dirpath, dirnames, filenames in os.walk(path):
shutil.chown(dirpath, owner, group)
for filename in filenames:
shutil.chown(os.path.join(dirpath, filename), owner, group)

View file

@ -7,6 +7,7 @@ class ActorController(BasicController):
def __init__(self, cbpi): def __init__(self, cbpi):
super(ActorController, self).__init__(cbpi, Actor,"actor.json") super(ActorController, self).__init__(cbpi, Actor,"actor.json")
self.update_key = "actorupdate" self.update_key = "actorupdate"
self.sorting=True
async def on(self, id, power=None): async def on(self, id, power=None):
try: try:
@ -20,7 +21,7 @@ class ActorController(BasicController):
if item.instance.state is False: if item.instance.state is False:
await item.instance.on(power) await item.instance.on(power)
#await self.push_udpate() #await self.push_udpate()
self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data))),self.sorting)
self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict(), True) self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict(), True)
else: else:
await self.set_power(id, power) await self.set_power(id, power)
@ -34,7 +35,7 @@ class ActorController(BasicController):
if item.instance.state is True: if item.instance.state is True:
await item.instance.off() await item.instance.off()
#await self.push_udpate() #await self.push_udpate()
self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data))),self.sorting)
self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict())
except Exception as e: except Exception as e:
logging.error("Failed to switch on Actor {} {}".format(id, e), True) logging.error("Failed to switch on Actor {} {}".format(id, e), True)
@ -44,7 +45,7 @@ class ActorController(BasicController):
item = self.find_by_id(id) item = self.find_by_id(id)
instance = item.get("instance") instance = item.get("instance")
await instance.toggle() await instance.toggle()
self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data))),self.sorting)
self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict())
except Exception as e: except Exception as e:
logging.error("Failed to toggle Actor {} {}".format(id, e)) logging.error("Failed to toggle Actor {} {}".format(id, e))
@ -61,7 +62,7 @@ class ActorController(BasicController):
item = self.find_by_id(id) item = self.find_by_id(id)
item.power = round(power) item.power = round(power)
#await self.push_udpate() #await self.push_udpate()
self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data))),self.sorting)
self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict())
except Exception as e: except Exception as e:
logging.error("Failed to update Actor {} {}".format(id, e)) logging.error("Failed to update Actor {} {}".format(id, e))

View file

@ -14,6 +14,7 @@ class BasicController:
def __init__(self, cbpi, resource, file): def __init__(self, cbpi, resource, file):
self.resource = resource self.resource = resource
self.update_key = "" self.update_key = ""
self.sorting = False
self.name = self.__class__.__name__ self.name = self.__class__.__name__
self.cbpi = cbpi self.cbpi = cbpi
self.cbpi.register(self) self.cbpi.register(self)
@ -23,7 +24,7 @@ class BasicController:
self.data = [] self.data = []
self.autostart = True self.autostart = True
#self._loop = asyncio.get_event_loop() #self._loop = asyncio.get_event_loop()
self.path = os.path.join(".", 'config', file) self.path = self.cbpi.config_folder.get_file_path(file)
self.cbpi.app.on_cleanup.append(self.shutdown) self.cbpi.app.on_cleanup.append(self.shutdown)
async def init(self): async def init(self):
@ -36,6 +37,7 @@ class BasicController:
logging.info("{} Load ".format(self.name)) logging.info("{} Load ".format(self.name))
with open(self.path) as json_file: with open(self.path) as json_file:
data = json.load(json_file) data = json.load(json_file)
data['data'].sort(key=lambda x: x.get('name').upper())
for i in data["data"]: for i in data["data"]:
self.data.append(self.create(i)) self.data.append(self.create(i))
@ -54,7 +56,7 @@ class BasicController:
await self.push_udpate() await self.push_udpate()
async def push_udpate(self): async def push_udpate(self):
self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data))),self.sorting)
#self.cbpi.push_update("cbpi/{}".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) #self.cbpi.push_update("cbpi/{}".format(self.update_key), list(map(lambda item: item.to_dict(), self.data)))
for item in self.data: for item in self.data:
self.cbpi.push_update("cbpi/{}/{}".format(self.update_key,item.id), item.to_dict()) self.cbpi.push_update("cbpi/{}/{}".format(self.update_key,item.id), item.to_dict())
@ -151,4 +153,4 @@ class BasicController:
item = self.find_by_id(id) item = self.find_by_id(id)
await item.instance.__getattribute__(action)(**parameter) await item.instance.__getattribute__(action)(**parameter)
except Exception as e: except Exception as e:
logging.error("{} Faild to call action on {} {} {}".format(self.name, id, action, e)) logging.error("{} Failed to call action on {} {} {}".format(self.name, id, action, e))

View file

@ -14,8 +14,8 @@ class ConfigController:
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.cbpi = cbpi self.cbpi = cbpi
self.cbpi.register(self) self.cbpi.register(self)
self.path = os.path.join(".", 'config', "config.json") self.path = cbpi.config_folder.get_file_path("config.json")
self.path_static = os.path.join(".", 'config', "config.yaml") self.path_static = cbpi.config_folder.get_file_path("config.yaml")
def get_state(self): def get_state(self):

View file

@ -18,14 +18,14 @@ class DashboardController:
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.cbpi.register(self) self.cbpi.register(self)
self.path = os.path.join(".", 'config', "cbpi_dashboard_1.json") self.path = cbpi.config_folder.get_file_path("cbpi_dashboard_1.json")
async def init(self): async def init(self):
pass pass
async def get_content(self, dashboard_id): async def get_content(self, dashboard_id):
try: try:
self.path = os.path.join(".", 'config', "cbpi_dashboard_"+ str(dashboard_id) +".json") self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_"+ str(dashboard_id) +".json")
logging.info(self.path) logging.info(self.path)
with open(self.path) as json_file: with open(self.path) as json_file:
data = json.load(json_file) data = json.load(json_file)
@ -35,21 +35,21 @@ class DashboardController:
async def add_content(self, dashboard_id, data): async def add_content(self, dashboard_id, data):
print(data) print(data)
self.path = os.path.join(".", 'config', "cbpi_dashboard_" + str(dashboard_id)+ ".json") self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_" + str(dashboard_id)+ ".json")
with open(self.path, 'w') as outfile: with open(self.path, 'w') as outfile:
json.dump(data, outfile, indent=4, sort_keys=True) json.dump(data, outfile, indent=4, sort_keys=True)
self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Saved Successfully", type=NotificationType.SUCCESS) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Saved Successfully", type=NotificationType.SUCCESS)
return {"status": "OK"} return {"status": "OK"}
async def delete_content(self, dashboard_id): async def delete_content(self, dashboard_id):
self.path = os.path.join(".", 'config', "cbpi_dashboard_"+ str(dashboard_id)+ ".json") self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_"+ str(dashboard_id)+ ".json")
if os.path.exists(self.path): if os.path.exists(self.path):
os.remove(self.path) os.remove(self.path)
self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Deleted Successfully", type=NotificationType.SUCCESS) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Deleted Successfully", type=NotificationType.SUCCESS)
async def get_custom_widgets(self): async def get_custom_widgets(self):
path = os.path.join(".", 'config', "dashboard", "widgets") path = os.path.join(self.cbpi.config_folder.get_file_path("dashboard"), "widgets")
onlyfiles = [os.path.splitext(f)[0] for f in sorted(listdir(path)) if isfile(join(path, f)) and f.endswith(".svg")] onlyfiles = [os.path.splitext(f)[0] for f in sorted(listdir(path)) if isfile(join(path, f)) and f.endswith(".svg")]
return onlyfiles return onlyfiles

View file

@ -22,7 +22,7 @@ class FermentationController:
self.update_key = "fermenterupdate" self.update_key = "fermenterupdate"
self.cbpi = cbpi self.cbpi = cbpi
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.path = os.path.join(".", 'config', "fermenter_data.json") self.path = self.cbpi.config_folder.get_file_path("fermenter_data.json")
self.data = [] self.data = []
self.types = {} self.types = {}
self.steptypes = {} self.steptypes = {}
@ -35,18 +35,18 @@ class FermentationController:
pass pass
def check_fermenter_file(self): def check_fermenter_file(self):
if os.path.exists(os.path.join(".", 'config', "fermenter_data.json")) is False: if os.path.exists(self.cbpi.config_folder.get_file_path("fermenter_data.json")) is False:
logging.info("INIT fermenter_data.json file") logging.info("INIT fermenter_data.json file")
data = { data = {
"data": [ "data": [
] ]
} }
destfile = os.path.join(".", 'config', "fermenter_data.json") destfile = self.cbpi.config_folder.get_file_path("fermenter_data.json")
json.dump(data,open(destfile,'w'),indent=4, sort_keys=True) json.dump(data,open(destfile,'w'),indent=4, sort_keys=True)
pathlib.Path(os.path.join(".", 'config/fermenterrecipes')).mkdir(parents=True, exist_ok=True) pathlib.Path(self.cbpi.config_folder.get_file_path("fermenterrecipes")).mkdir(parents=True, exist_ok=True)
async def shutdown(self, app=None, fermenterid=None): async def shutdown(self, app=None, fermenterid=None):
self.save() self.save()
if (fermenterid == None): if (fermenterid == None):
for fermenter in self.data: for fermenter in self.data:
@ -54,10 +54,7 @@ class FermentationController:
for step in fermenter.steps: for step in fermenter.steps:
try: try:
self.logger.info("Stop {}".format(step.name)) self.logger.info("Stop {}".format(step.name))
try: step.instance.shutdown = True
step.instance.shutdown = True
except:
pass
await step.instance.stop() await step.instance.stop()
except Exception as e: except Exception as e:
self.logger.error(e) self.logger.error(e)
@ -67,10 +64,7 @@ class FermentationController:
for step in fermenter.steps: for step in fermenter.steps:
try: try:
self.logger.info("Stop {}".format(step.name)) self.logger.info("Stop {}".format(step.name))
try: step.instance.shutdown = True
step.instance.shutdown = True
except:
pass
await step.instance.stop() await step.instance.stop()
except Exception as e: except Exception as e:
self.logger.error(e) self.logger.error(e)
@ -109,6 +103,7 @@ class FermentationController:
return step return step
def _done(self, step_instance, result, fermenter): def _done(self, step_instance, result, fermenter):
logging.info(result)
step_instance.step["status"] = "D" step_instance.step["status"] = "D"
self.save() self.save()
if result == StepResult.NEXT: if result == StepResult.NEXT:
@ -119,14 +114,17 @@ class FermentationController:
id = data.get("id") id = data.get("id")
name = data.get("name") name = data.get("name")
sensor = data.get("sensor") sensor = data.get("sensor")
pressure_sensor = data.get("pressure_sensor")
heater = data.get("heater") heater = data.get("heater")
cooler = data.get("cooler") cooler = data.get("cooler")
valve = data.get("valve","")
logictype = data.get("type") logictype = data.get("type")
temp = data.get("target_temp") temp = data.get("target_temp")
pressure = data.get("target_pressure")
brewname = data.get("brewname") brewname = data.get("brewname")
description = data.get("description") description = data.get("description")
props = Props(data.get("props", {})) props = Props(data.get("props", {}))
fermenter = Fermenter(id, name, sensor, heater, cooler, brewname, description, props, temp, logictype) fermenter = Fermenter(id, name, sensor, pressure_sensor, heater, cooler, valve, brewname, description, props, temp, pressure, logictype)
fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", []))) fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", [])))
self.push_update() self.push_update()
return fermenter return fermenter
@ -135,7 +133,7 @@ class FermentationController:
def _find_by_id(self, id): def _find_by_id(self, id):
return next((item for item in self.data if item.id == id), None) return next((item for item in self.data if item.id == id), None)
async def get_all(self): async def get_all(self):
return list(map(lambda x: x.to_dict(), self.data)) return list(map(lambda x: x.to_dict(), self.data))
@ -202,13 +200,16 @@ class FermentationController:
def _update(old_item: Fermenter, item: Fermenter): def _update(old_item: Fermenter, item: Fermenter):
old_item.name = item.name old_item.name = item.name
old_item.sensor = item.sensor old_item.sensor = item.sensor
old_item.pressure_sensor = item.pressure_sensor
old_item.heater = item.heater old_item.heater = item.heater
old_item.cooler = item.cooler old_item.cooler = item.cooler
old_item.valve = item.valve
old_item.type = item.type old_item.type = item.type
old_item.brewname = item.brewname old_item.brewname = item.brewname
old_item.description = item.description old_item.description = item.description
old_item.props = item.props old_item.props = item.props
old_item.target_temp = item.target_temp old_item.target_temp = item.target_temp
old_item.target_pressure = item.target_pressure
return old_item return old_item
self.data = list(map(lambda old: _update(old, item) if old.id == item.id else old, self.data)) self.data = list(map(lambda old: _update(old, item) if old.id == item.id else old, self.data))
@ -227,6 +228,17 @@ class FermentationController:
except Exception as e: except Exception as e:
logging.error("Failed to set Target Temp {} {}".format(id, e)) logging.error("Failed to set Target Temp {} {}".format(id, e))
async def set_target_pressure(self, id: str, target_pressure):
try:
item = self._find_by_id(id)
logging.info(item.target_pressure)
if item:
item.target_pressure = target_pressure
self.save()
self.push_update()
except Exception as e:
logging.error("Failed to set Target Pressure {} {}".format(id, e))
async def delete(self, id: str ): async def delete(self, id: str ):
item = self._find_by_id(id) item = self._find_by_id(id)
self.data = list(filter(lambda item: item.id != id, self.data)) self.data = list(filter(lambda item: item.id != id, self.data))
@ -234,7 +246,7 @@ class FermentationController:
self.push_update() self.push_update()
def save(self): def save(self):
data = dict(data=list(map(lambda item: item.to_dict(), self.data))) data = dict(data=list(map(lambda item: item.to_dict(), self.data)))
with open(self.path, "w") as file: with open(self.path, "w") as file:
json.dump(data, file, indent=4, sort_keys=True) json.dump(data, file, indent=4, sort_keys=True)
@ -304,6 +316,8 @@ class FermentationController:
item = self._find_by_id(id) item = self._find_by_id(id)
# might require later check if step is active # might require later check if step is active
item.steps = [] item.steps = []
item.brewname = ""
self.push_update()
self.save() self.save()
self.push_update("fermenterstepupdate") self.push_update("fermenterstepupdate")
@ -323,6 +337,17 @@ class FermentationController:
def _find_step_by_id(self, data, id): def _find_step_by_id(self, data, id):
return next((item for item in data if item.id == id), None) return next((item for item in data if item.id == id), None)
async def update_endtime(self, id, stepid, endtime):
try:
item = self._find_by_id(id)
step = self._find_step_by_id(item.steps, stepid)
step.endtime = int(endtime)
self.save()
self.push_update("fermenterstepupdate")
except Exception as e:
self.logger.error(e)
async def start(self, id): async def start(self, id):
self.logger.info("Start {}".format(id)) self.logger.info("Start {}".format(id))
try: try:
@ -338,7 +363,9 @@ class FermentationController:
endtime = step.endtime endtime = step.endtime
await step.instance.start() await step.instance.start()
logging.info("Restarting step {}".format(step.name)) logging.info("Restarting step {}".format(step.name))
step.status = StepState.ACTIVE if endtime != 0:
logging.info("Need to change timer")
step.status = StepState.ACTIVE
self.save() self.save()
self.push_update() self.push_update()
self.push_update("fermenterstepupdate") self.push_update("fermenterstepupdate")
@ -364,8 +391,8 @@ class FermentationController:
try: try:
item = self._find_by_id(id) item = self._find_by_id(id)
step = self._find_by_status(item.steps, StepState.ACTIVE) step = self._find_by_status(item.steps, StepState.ACTIVE)
logging.info(step) #logging.info(step)
logging.info(step.status) #logging.info(step.status)
if step != None: if step != None:
logging.info("CALLING STOP STEP") logging.info("CALLING STOP STEP")
try: try:
@ -396,7 +423,6 @@ class FermentationController:
await item.instance.start() await item.instance.start()
item.instance.running = True item.instance.running = True
item.instance.task = asyncio.get_event_loop().create_task(item.instance._run()) item.instance.task = asyncio.get_event_loop().create_task(item.instance._run())
logging.info("{} started {}".format(item.name, id)) logging.info("{} started {}".format(item.name, id))
@ -516,7 +542,7 @@ class FermentationController:
async def savetobook(self, fermenterid): async def savetobook(self, fermenterid):
name = shortuuid.uuid() name = shortuuid.uuid()
path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(name)) path = self.cbpi.config_folder.get_fermenter_recipe_by_id(name)
fermenter=self._find_by_id(fermenterid) fermenter=self._find_by_id(fermenterid)
try: try:
brewname = fermenter.brewname brewname = fermenter.brewname
@ -549,11 +575,17 @@ class FermentationController:
item["props"]["Sensor"] = fermenter.sensor item["props"]["Sensor"] = fermenter.sensor
list(map(lambda item: add_runtime_data(item), data.get("steps"))) list(map(lambda item: add_runtime_data(item), data.get("steps")))
fermenter.description = data['basic']['desc'] try:
fermenter.description = data['basic'].get("desc")
except:
fermenter.description = "No Description"
if name is not None: if name is not None:
fermenter.brewname = name fermenter.brewname = name
else: else:
fermenter.brewname = data['basic']['name'] try:
fermenter.brewname = data['basic'].get("name")
except:
fermenter.brewname = "Fermentation"
await self.update(fermenter) await self.update(fermenter)
fermenter.steps=[] fermenter.steps=[]
for item in data.get("steps"): for item in data.get("steps"):
@ -569,4 +601,4 @@ class FermentationController:
self.save() self.save()
self.push_update("fermenterstepupdate") self.push_update("fermenterstepupdate")
return step return step

View file

@ -29,56 +29,52 @@ class FermenterRecipeController:
async def create(self, name): async def create(self, name):
id = shortuuid.uuid() id = shortuuid.uuid()
path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(id)) path = self.cbpi.config_folder.get_fermenter_recipe_by_id(id)
data = dict(basic=dict(name=name), steps=[]) data = dict(basic=dict(name=name), steps=[])
with open(path, "w") as file: with open(path, "w") as file:
yaml.dump(data, file) yaml.dump(data, file)
return id return id
async def save(self, name, data): async def save(self, name, data):
path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(name)) path = self.cbpi.config_folder.get_fermenter_recipe_by_id(name)
logging.info(data) logging.info(data)
with open(path, "w") as file: with open(path, "w") as file:
yaml.dump(data, file, indent=4, sort_keys=True) yaml.dump(data, file, indent=4, sort_keys=True)
async def get_recipes(self): async def get_recipes(self):
path = os.path.join(".", 'config', "fermenterrecipes") fermenter_recipe_ids = self.cbpi.config_folder.get_all_fermenter_recipes()
onlyfiles = [os.path.splitext(f)[0] for f in listdir(path) if isfile(join(path, f)) and f.endswith(".yaml")]
result = [] result = []
for filename in onlyfiles: for recipe_id in fermenter_recipe_ids:
recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % filename)
with open(recipe_path) as file: with open(self.cbpi.config_folder.get_fermenter_recipe_by_id(recipe_id)) as file:
data = yaml.load(file, Loader=yaml.FullLoader) data = yaml.load(file, Loader=yaml.FullLoader)
dataset = data["basic"] dataset = data["basic"]
dataset["file"] = filename dataset["file"] = recipe_id
result.append(dataset) result.append(dataset)
logging.info(result) logging.info(result)
return result return result
async def get_by_name(self, name): async def get_by_name(self, name):
recipe_path = self.cbpi.config_folder.get_fermenter_recipe_by_id(name)
recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % name)
with open(recipe_path) as file: with open(recipe_path) as file:
return yaml.load(file, Loader=yaml.FullLoader) return yaml.load(file, Loader=yaml.FullLoader)
async def remove(self, name): async def remove(self, name):
path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(name)) path = self.cbpi.config_folder.get_fermenter_recipe_by_id(name)
os.remove(path) os.remove(path)
async def brew(self, recipeid, fermenterid, name): async def brew(self, recipeid, fermenterid, name):
recipe_path = self.cbpi.config_folder.get_fermenter_recipe_by_id(recipeid)
recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % recipeid)
logging.info(recipe_path) logging.info(recipe_path)
with open(recipe_path) as file: with open(recipe_path) as file:
data = yaml.load(file, Loader=yaml.FullLoader) data = yaml.load(file, Loader=yaml.FullLoader)
await self.cbpi.fermenter.load_recipe(data, fermenterid, name) await self.cbpi.fermenter.load_recipe(data, fermenterid, name)
async def clone(self, id, new_name): async def clone(self, id, new_name):
recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % id) recipe_path = self.cbpi.config_folder.get_fermenter_recipe_by_id(id)
with open(recipe_path) as file: with open(recipe_path) as file:
data = yaml.load(file, Loader=yaml.FullLoader) data = yaml.load(file, Loader=yaml.FullLoader)
data["basic"]["name"] = new_name data["basic"]["name"] = new_name

View file

@ -22,7 +22,7 @@ class NotificationController:
try: try:
del self.listener[listener_id] del self.listener[listener_id]
except: except:
self.logger.error("Faild to remove listener {}".format(listener_id)) self.logger.error("Failed to remove listener {}".format(listener_id))
async def _call_listener(self, title, message, type, action): async def _call_listener(self, title, message, type, action):
for id, method in self.listener.items(): for id, method in self.listener.items():
@ -60,5 +60,5 @@ class NotificationController:
asyncio.create_task(action.method()) asyncio.create_task(action.method())
del self.callback_cache[notification_id] del self.callback_cache[notification_id]
except Exception as e: except Exception as e:
self.logger.error("Faild to call notificatoin callback") self.logger.error("Failed to call notificatoin callback")

View file

@ -208,7 +208,7 @@ class PluginController():
result.append({row: meta[row] result.append({row: meta[row]
for row in list(metadata(key))}) for row in list(metadata(key))})
except Exception as e: except Exception as e:
logger.error("FAILED to load plugin {} ".fromat(key)) logger.error("FAILED to load plugin {} ".format(key))
logger.error(e) logger.error(e)
except Exception as e: except Exception as e:

View file

@ -29,26 +29,26 @@ class RecipeController:
async def create(self, name): async def create(self, name):
id = shortuuid.uuid() id = shortuuid.uuid()
path = os.path.join(".", 'config', "recipes", "{}.yaml".format(id)) path = self.cbpi.config_folder.get_recipe_file_by_id(id)
data = dict(basic=dict(name=name, author=self.cbpi.config.get("AUTHOR", "John Doe")), steps=[]) data = dict(basic=dict(name=name, author=self.cbpi.config.get("AUTHOR", "John Doe")), steps=[])
with open(path, "w") as file: with open(path, "w") as file:
yaml.dump(data, file) yaml.dump(data, file)
return id return id
async def save(self, name, data): async def save(self, name, data):
path = os.path.join(".", 'config', "recipes", "{}.yaml".format(name)) path = self.cbpi.config_folder.get_recipe_file_by_id(name)
logging.info(data) logging.info(data)
with open(path, "w") as file: with open(path, "w") as file:
yaml.dump(data, file, indent=4, sort_keys=True) yaml.dump(data, file, indent=4, sort_keys=True)
async def get_recipes(self): async def get_recipes(self):
path = os.path.join(".", 'config', "recipes") path = self.cbpi.config_folder.get_file_path("recipes")
onlyfiles = [os.path.splitext(f)[0] for f in listdir(path) if isfile(join(path, f)) and f.endswith(".yaml")] onlyfiles = [os.path.splitext(f)[0] for f in listdir(path) if isfile(join(path, f)) and f.endswith(".yaml")]
result = [] result = []
for filename in onlyfiles: for filename in onlyfiles:
recipe_path = os.path.join(".", 'config', "recipes", "%s.yaml" % filename) recipe_path = self.cbpi.config_folder.get_recipe_file_by_id(filename)
with open(recipe_path) as file: with open(recipe_path) as file:
data = yaml.load(file, Loader=yaml.FullLoader) data = yaml.load(file, Loader=yaml.FullLoader)
dataset = data["basic"] dataset = data["basic"]
@ -58,25 +58,25 @@ class RecipeController:
async def get_by_name(self, name): async def get_by_name(self, name):
recipe_path = os.path.join(".", 'config', "recipes", "%s.yaml" % name) recipe_path = self.cbpi.config_folder.get_recipe_file_by_id(name)
with open(recipe_path) as file: with open(recipe_path) as file:
return yaml.load(file, Loader=yaml.FullLoader) return yaml.load(file, Loader=yaml.FullLoader)
async def remove(self, name): async def remove(self, name):
path = os.path.join(".", 'config', "recipes", "{}.yaml".format(name)) path = self.cbpi.config_folder.get_recipe_file_by_id(name)
os.remove(path) os.remove(path)
async def brew(self, name): async def brew(self, name):
recipe_path = os.path.join(".", 'config', "recipes", "%s.yaml" % name) recipe_path = self.cbpi.config_folder.get_recipe_file_by_id(name)
with open(recipe_path) as file: with open(recipe_path) as file:
data = yaml.load(file, Loader=yaml.FullLoader) data = yaml.load(file, Loader=yaml.FullLoader)
await self.cbpi.step.load_recipe(data) await self.cbpi.step.load_recipe(data)
async def clone(self, id, new_name): async def clone(self, id, new_name):
recipe_path = os.path.join(".", 'config', "recipes", "%s.yaml" % id) recipe_path = self.cbpi.config_folder.get_recipe_file_by_id(id)
with open(recipe_path) as file: with open(recipe_path) as file:
data = yaml.load(file, Loader=yaml.FullLoader) data = yaml.load(file, Loader=yaml.FullLoader)
data["basic"]["name"] = new_name data["basic"]["name"] = new_name

View file

@ -6,13 +6,14 @@ class SensorController(BasicController):
def __init__(self, cbpi): def __init__(self, cbpi):
super(SensorController, self).__init__(cbpi, Sensor, "sensor.json") super(SensorController, self).__init__(cbpi, Sensor, "sensor.json")
self.update_key = "sensorupdate" self.update_key = "sensorupdate"
self.sorting = True
def create_dict(self, data): def create_dict(self, data):
try: try:
instance = data.get("instance") instance = data.get("instance")
state =instance.get_state() state =instance.get_state()
except Exception as e: except Exception as e:
logging.error("Faild to create sensor dict {} ".format(e)) logging.error("Failed to create sensor dict {} ".format(e))
state = dict() state = dict()
return dict(name=data.get("name"), id=data.get("id"), type=data.get("type"), state=state,props=data.get("props", [])) return dict(name=data.get("name"), id=data.get("id"), type=data.get("type"), state=state,props=data.get("props", []))
@ -21,5 +22,5 @@ class SensorController(BasicController):
try: try:
return self.find_by_id(id).instance.get_state() return self.find_by_id(id).instance.get_state()
except Exception as e: except Exception as e:
logging.error("Faild read sensor value {} {} ".format(id, e)) logging.error("Failed read sensor value {} {} ".format(id, e))
return None return None

View file

@ -19,7 +19,7 @@ class StepController:
def __init__(self, cbpi): def __init__(self, cbpi):
self.cbpi = cbpi self.cbpi = cbpi
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.path = os.path.join(".", 'config', "step_data.json") self.path = self.cbpi.config_folder.get_file_path("step_data.json")
#self._loop = asyncio.get_event_loop() #self._loop = asyncio.get_event_loop()
self.basic_data = {} self.basic_data = {}
self.step = None self.step = None
@ -199,8 +199,8 @@ class StepController:
def get_types(self): def get_types(self):
result = {} result = {}
for key, value in self.types.items(): for key, value in self.types.items():
if "ferment" not in str(value.get("class")).lower(): #if "ferment" not in str(value.get("class")).lower():
result[key] = dict(name=value.get("name"), properties=value.get("properties"), actions=value.get("actions")) result[key] = dict(name=value.get("name"), properties=value.get("properties"), actions=value.get("actions"))
return result return result
def get_state(self): def get_state(self):
@ -230,7 +230,7 @@ class StepController:
await self.save() await self.save()
async def shutdown(self, app=None): async def shutdown(self, app=None):
logging.info("Mash Profile Shutdonw") logging.info("Mash Profile Shutdown")
for p in self.profile: for p in self.profile:
instance = p.instance instance = p.instance
# Stopping all running task # Stopping all running task
@ -279,7 +279,7 @@ class StepController:
await step.instance.start() await step.instance.start()
step.status = StepState.ACTIVE step.status = StepState.ACTIVE
except Exception as e: except Exception as e:
logging.error("Faild to start step %s" % step) logging.error("Failed to start step %s" % step)
async def save_basic(self, data): async def save_basic(self, data):
logging.info("SAVE Basic Data") logging.info("SAVE Basic Data")
@ -293,7 +293,7 @@ class StepController:
item = self.find_by_id(id) item = self.find_by_id(id)
await item.instance.__getattribute__(action)(**parameter) await item.instance.__getattribute__(action)(**parameter)
except Exception as e: except Exception as e:
logging.error("Step Controller -Faild to call action on {} {} {}".format(id, action, e)) logging.error("Step Controller -Failed to call action on {} {} {}".format(id, action, e))
async def load_recipe(self, data): async def load_recipe(self, data):
try: try:
@ -324,7 +324,7 @@ class StepController:
async def savetobook(self): async def savetobook(self):
name = shortuuid.uuid() name = shortuuid.uuid()
path = os.path.join(".", 'config', "recipes", "{}.yaml".format(name)) path = os.path.join(self.cbpi.config_folder.get_file_path("recipes"), "{}.yaml".format(name))
data = dict(basic=self.basic_data, steps=list(map(lambda item: item.to_dict(), self.profile))) data = dict(basic=self.basic_data, steps=list(map(lambda item: item.to_dict(), self.profile)))
with open(path, "w") as file: with open(path, "w") as file:
yaml.dump(data, file) yaml.dump(data, file)

View file

@ -38,7 +38,7 @@ class SystemController:
async def backupConfig(self): async def backupConfig(self):
output_filename = "cbpi4_config" output_filename = "cbpi4_config"
dir_name = pathlib.Path(os.path.join(".", 'config')) dir_name = pathlib.Path(self.cbpi.config_folder.get_file_path(''))
shutil.make_archive(output_filename, 'zip', dir_name) shutil.make_archive(output_filename, 'zip', dir_name)
async def downloadlog(self, logtime): async def downloadlog(self, logtime):
@ -153,7 +153,7 @@ class SystemController:
try: try:
content = svg_file.read().decode('utf-8','replace') content = svg_file.read().decode('utf-8','replace')
if svg_file and self.allowed_file(filename, 'svg'): if svg_file and self.allowed_file(filename, 'svg'):
self.path = os.path.join(".","config","dashboard","widgets", filename) self.path = os.path.join(self.cbpi.config_folder.get_file_path("dashboard"),"widgets", filename)
logging.info(self.path) logging.info(self.path)
f=open(self.path, "w") f=open(self.path, "w")
@ -178,6 +178,8 @@ class SystemController:
mempercent = 0 mempercent = 0
eth0IP = "N/A" eth0IP = "N/A"
wlan0IP = "N/A" wlan0IP = "N/A"
eth0speed = "N/A"
wlan0speed = "N/A"
TEMP_UNIT=self.cbpi.config.get("TEMP_UNIT", "C") TEMP_UNIT=self.cbpi.config.get("TEMP_UNIT", "C")
FAHRENHEIT = False if TEMP_UNIT == "C" else True FAHRENHEIT = False if TEMP_UNIT == "C" else True
@ -225,12 +227,31 @@ class SystemController:
if str(addr.family) == "AddressFamily.AF_INET": if str(addr.family) == "AddressFamily.AF_INET":
if addr.address: if addr.address:
wlan0IP = addr.address wlan0IP = addr.address
info = psutil.net_if_stats()
try:
for nic in info:
if nic == 'eth0':
if info[nic].isup == True:
if info[nic].speed:
eth0speed = info[nic].speed
else:
eth0speed = "down"
if nic == 'wlan0':
if info[nic].isup == True:
ratestring = os.popen('iwlist wlan0 rate | grep Rate').read()
start = ratestring.find("=") + 1
end = ratestring.find(" Mb/s")
wlan0speed = ratestring[start:end]
else:
wlan0speed = "down"
except Exception as e:
logging.info(e)
except: except:
pass pass
if system == "Windows": if system == "Windows":
try: try:
ethernet = psutil.net_if_addrs() ethernet = psutil.net_if_addrs()
for nic, addrs in ethernet.items(): for nic, addrs in ethernet.items():
if nic == "Ethernet": if nic == "Ethernet":
for addr in addrs: for addr in addrs:
@ -242,6 +263,23 @@ class SystemController:
if str(addr.family) == "AddressFamily.AF_INET": if str(addr.family) == "AddressFamily.AF_INET":
if addr.address: if addr.address:
wlan0IP = addr.address wlan0IP = addr.address
info = psutil.net_if_stats()
try:
for nic in info:
if nic == 'Ethernet':
if info[nic].isup == True:
if info[nic].speed:
eth0speed = info[nic].speed
else:
eth0speed = "down"
if nic == 'WLAN':
if info[nic].isup == True:
if info[nic].speed:
wlan0speed = info[nic].speed
else:
wlan0speed = "down"
except Exception as e:
logging.info(e)
except: except:
pass pass
@ -258,7 +296,9 @@ class SystemController:
'temp': temp, 'temp': temp,
'temp_unit': TEMP_UNIT, 'temp_unit': TEMP_UNIT,
'eth0': eth0IP, 'eth0': eth0IP,
'wlan0': wlan0IP} 'wlan0': wlan0IP,
'eth0speed': eth0speed,
'wlan0speed': wlan0speed}
return systeminfo return systeminfo

View file

@ -32,7 +32,7 @@ class UploadController:
async def get_kbh_recipes(self): async def get_kbh_recipes(self):
try: try:
path = os.path.join(".", 'config', "upload", "kbh.db") path = self.cbpi.config_folder.get_upload_file("kbh.db")
conn = sqlite3.connect(path) conn = sqlite3.connect(path)
c = conn.cursor() c = conn.cursor()
c.execute('SELECT ID, Sudname, Status FROM Sud') c.execute('SELECT ID, Sudname, Status FROM Sud')
@ -47,7 +47,7 @@ class UploadController:
async def get_xml_recipes(self): async def get_xml_recipes(self):
try: try:
path = os.path.join(".", 'config', "upload", "beer.xml") path = self.cbpi.config_folder.get_upload_file("beer.xml")
e = xml.etree.ElementTree.parse(path).getroot() e = xml.etree.ElementTree.parse(path).getroot()
result =[] result =[]
counter = 1 counter = 1
@ -61,7 +61,7 @@ class UploadController:
async def get_json_recipes(self): async def get_json_recipes(self):
try: try:
path = os.path.join(".", 'config', "upload", "mmum.json") path = self.cbpi.config_folder.get_upload_file("mmum.json")
e = json.load(open(path)) e = json.load(open(path))
result =[] result =[]
result.append({'value': str(1), 'label': e['Name']}) result.append({'value': str(1), 'label': e['Name']})
@ -123,7 +123,7 @@ class UploadController:
try: try:
beer_xml = recipe_file.read().decode('utf-8','replace') beer_xml = recipe_file.read().decode('utf-8','replace')
if recipe_file and self.allowed_file(filename, 'xml'): if recipe_file and self.allowed_file(filename, 'xml'):
self.path = os.path.join(".", 'config', "upload", "beer.xml") self.path = self.cbpi.config_folder.get_upload_file("beer.xml")
f = open(self.path, "w") f = open(self.path, "w")
f.write(beer_xml) f.write(beer_xml)
@ -137,7 +137,7 @@ class UploadController:
try: try:
mmum_json = recipe_file.read().decode('utf-8','replace') mmum_json = recipe_file.read().decode('utf-8','replace')
if recipe_file and self.allowed_file(filename, 'json'): if recipe_file and self.allowed_file(filename, 'json'):
self.path = os.path.join(".", 'config', "upload", "mmum.json") self.path = self.cbpi.config_folder.get_upload_file("mmum.json")
f = open(self.path, "w") f = open(self.path, "w")
f.write(mmum_json) f.write(mmum_json)
@ -151,7 +151,7 @@ class UploadController:
try: try:
content = recipe_file.read() content = recipe_file.read()
if recipe_file and self.allowed_file(filename, 'sqlite'): if recipe_file and self.allowed_file(filename, 'sqlite'):
self.path = os.path.join(".", 'config', "upload", "kbh.db") self.path = self.cbpi.config_folder.get_upload_file("kbh.db")
f=open(self.path, "wb") f=open(self.path, "wb")
f.write(content) f.write(content)
@ -168,7 +168,7 @@ class UploadController:
config = self.get_config_values() config = self.get_config_values()
if self.kettle is not None: if self.kettle is not None:
# load beerxml file located in upload folder # load beerxml file located in upload folder
self.path = os.path.join(".", 'config', "upload", "kbh.db") self.path = self.cbpi.config_folder.get_upload_file("kbh.db")
if os.path.exists(self.path) is False: if os.path.exists(self.path) is False:
self.cbpi.notify("File Not Found", "Please upload a kbh V2 databsel file", NotificationType.ERROR) self.cbpi.notify("File Not Found", "Please upload a kbh V2 databsel file", NotificationType.ERROR)
@ -318,7 +318,7 @@ class UploadController:
self.cbpi.notify('Recipe Upload', 'No default Kettle defined. Please specify default Kettle in settings', NotificationType.ERROR) self.cbpi.notify('Recipe Upload', 'No default Kettle defined. Please specify default Kettle in settings', NotificationType.ERROR)
def findMax(self, string): def findMax(self, string):
self.path = os.path.join(".", 'config', "upload", "mmum.json") self.path = self.cbpi.config_folder.get_upload_file("mmum.json")
e = json.load(open(self.path)) e = json.load(open(self.path))
for idx in range(1,20): for idx in range(1,20):
search_string = string.replace("%%",str(idx)) search_string = string.replace("%%",str(idx))
@ -328,7 +328,7 @@ class UploadController:
return i return i
def getJsonMashin(self, id): def getJsonMashin(self, id):
self.path = os.path.join(".", 'config', "upload", "mmum.json") self.path = self.cbpi.config_folder.get_upload_file("mmum.json")
e = json.load(open(self.path)) e = json.load(open(self.path))
return float(e['Infusion_Einmaischtemperatur']) return float(e['Infusion_Einmaischtemperatur'])
@ -337,7 +337,7 @@ class UploadController:
if self.kettle is not None: if self.kettle is not None:
# load mmum-json file located in upload folder # load mmum-json file located in upload folder
self.path = os.path.join(".", 'config', "upload", "mmum.json") self.path = self.cbpi.config_folder.get_upload_file("mmum.json")
if os.path.exists(self.path) is False: if os.path.exists(self.path) is False:
self.cbpi.notify("File Not Found", "Please upload a MMuM-JSON File", NotificationType.ERROR) self.cbpi.notify("File Not Found", "Please upload a MMuM-JSON File", NotificationType.ERROR)
@ -551,7 +551,7 @@ class UploadController:
if self.kettle is not None: if self.kettle is not None:
# load beerxml file located in upload folder # load beerxml file located in upload folder
self.path = os.path.join(".", 'config', "upload", "beer.xml") self.path = self.cbpi.config_folder.get_upload_file("beer.xml")
if os.path.exists(self.path) is False: if os.path.exists(self.path) is False:
self.cbpi.notify("File Not Found", "Please upload a Beer.xml File", NotificationType.ERROR) self.cbpi.notify("File Not Found", "Please upload a Beer.xml File", NotificationType.ERROR)
@ -692,7 +692,7 @@ class UploadController:
temp = round(9.0 / 5.0 * float(e.find("STEP_TEMP").text) + 32, 2) temp = round(9.0 / 5.0 * float(e.find("STEP_TEMP").text) + 32, 2)
steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)}) steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)})
elif recipe_type == "json": elif recipe_type == "json":
self.path = os.path.join(".", 'config', "upload", "mmum.json") self.path = self.cbpi.config_folder.get_upload_file("mmum.json")
e = json.load(open(self.path)) e = json.load(open(self.path))
for idx in range(1,self.findMax("Infusion_Rastzeit%%")): for idx in range(1,self.findMax("Infusion_Rastzeit%%")):
if self.cbpi.config.get("TEMP_UNIT", "C") == "C": if self.cbpi.config.get("TEMP_UNIT", "C") == "C":

View file

@ -12,7 +12,7 @@ from cbpi.controller.notification_controller import NotificationController
import logging import logging
from os import urandom from os import urandom
import os import os
from cbpi import __version__ from cbpi import __version__, __codename__
from aiohttp import web from aiohttp import web
from aiohttp_auth import auth from aiohttp_auth import auth
from aiohttp_session import session_middleware from aiohttp_session import session_middleware
@ -86,7 +86,7 @@ async def error_middleware(request, handler):
class CraftBeerPi: class CraftBeerPi:
def __init__(self): def __init__(self, configFolder):
operationsystem= sys.platform operationsystem= sys.platform
if operationsystem.startswith('win'): if operationsystem.startswith('win'):
@ -95,10 +95,11 @@ class CraftBeerPi:
self.path = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-1]) # The path to the package dir self.path = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-1]) # The path to the package dir
self.version = __version__ self.version = __version__
self.codename = __codename__
self.static_config = load_config(os.path.join(".", 'config', "config.yaml")) self.config_folder = configFolder
self.static_config = load_config(configFolder.get_file_path("config.yaml"))
self.database_file = os.path.join(".", 'config', "craftbeerpi.db")
logger.info("Init CraftBeerPI") logger.info("Init CraftBeerPI")
policy = auth.SessionTktAuthentication(urandom(32), 60, include_ip=True) policy = auth.SessionTktAuthentication(urandom(32), 60, include_ip=True)
@ -288,6 +289,7 @@ class CraftBeerPi:
self._setup_http_index() self._setup_http_index()
self.plugin.load_plugins() self.plugin.load_plugins()
self.plugin.load_plugins_from_evn() self.plugin.load_plugins_from_evn()
await self.fermenter.init()
await self.sensor.init() await self.sensor.init()
await self.step.init() await self.step.init()
@ -295,8 +297,8 @@ class CraftBeerPi:
await self.kettle.init() await self.kettle.init()
await self.call_initializer(self.app) await self.call_initializer(self.app)
await self.dashboard.init() await self.dashboard.init()
await self.fermenter.init()
self._swagger_setup() self._swagger_setup()
level = logging.INFO level = logging.INFO

View file

@ -46,7 +46,7 @@ class ConfigUpdate(CBPiExtension):
influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None)
influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None) influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None)
mqttupdate = self.cbpi.config.get("MQTTUpdate", None) mqttupdate = self.cbpi.config.get("MQTTUpdate", None)
PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", None)
if boil_temp is None: if boil_temp is None:
@ -276,6 +276,15 @@ class ConfigUpdate(CBPiExtension):
except: except:
logger.warning('Unable to update database') logger.warning('Unable to update database')
## Check if PRESSURE_UNIT is in config
if PRESSURE_UNIT is None:
logger.info("INIT PRESSURE_UNIT")
try:
await self.cbpi.config.add("PRESSURE_UNIT", "kPa", ConfigType.SELECT, "Set unit for pressure",
[{"label": "kPa", "value": "kPa"},
{"label": "PSI", "value": "PSI"}])
except:
logger.warning('Unable to update config')
def setup(cbpi): def setup(cbpi):
cbpi.plugin.register("ConfigUpdate", ConfigUpdate) cbpi.plugin.register("ConfigUpdate", ConfigUpdate)

View file

@ -81,8 +81,9 @@ class FermenterTargetTempStep(CBPiFermentationStep):
if self.AutoMode == True: if self.AutoMode == True:
await self.setAutoMode(False) await self.setAutoMode(False)
self.cbpi.notify(self.name, self.props.get("Notification","Target Temp reached. Please add malt and klick next to move on.")) self.cbpi.notify(self.name, self.props.get("Notification","Target Temp reached. Please add malt and klick next to move on."))
await self.next(self.fermenter.id) if self.shutdown == False:
return StepResult.DONE await self.next(self.fermenter.id)
return StepResult.DONE
async def on_timer_update(self,timer, seconds): async def on_timer_update(self,timer, seconds):
@ -92,7 +93,8 @@ class FermenterTargetTempStep(CBPiFermentationStep):
self.shutdown = False self.shutdown = False
self.AutoMode = True if self.props.get("AutoMode","No") == "Yes" else False self.AutoMode = True if self.props.get("AutoMode","No") == "Yes" else False
if self.fermenter is not None: if self.fermenter is not None:
self.fermenter.target_temp = int(self.props.get("Temp", 0)) self.fermenter.target_temp = float(self.props.get("Temp", 0))
self.fermenter.target_pressure = 0
if self.AutoMode == True: if self.AutoMode == True:
await self.setAutoMode(True) await self.setAutoMode(True)
self.summary = "Waiting for Target Temp" self.summary = "Waiting for Target Temp"
@ -139,7 +141,7 @@ class FermenterTargetTempStep(CBPiFermentationStep):
if (self.fermenter.instance is None or self.fermenter.instance.state == False) and (auto_state is True): if (self.fermenter.instance is None or self.fermenter.instance.state == False) and (auto_state is True):
await self.cbpi.fermenter.toggle(self.fermenter.id) await self.cbpi.fermenter.toggle(self.fermenter.id)
elif (self.fermenter.instance.state == True) and (auto_state is False): elif (self.fermenter.instance.state == True) and (auto_state is False):
await self.fermenter.instance.stop() await self.cbpi.fermenter.toggle(self.fermenter.id)
await self.push_update() await self.push_update()
except Exception as e: except Exception as e:
@ -149,8 +151,9 @@ class FermenterTargetTempStep(CBPiFermentationStep):
@parameters([Property.Number(label="TimerD", description="Timer Days", configurable=True), @parameters([Property.Number(label="TimerD", description="Timer Days", configurable=True),
Property.Number(label="TimerH", description="Timer Hours", configurable=True), Property.Number(label="TimerH", description="Timer Hours", configurable=True),
Property.Number(label="TimerM", description="Timer Minutes", configurable=True), Property.Number(label="TimerM", description="Timer Minutes", configurable=True),
Property.Number(label="Temp", configurable=True), Property.Number(label="Temp", configurable=True, description="Step Temperature"),
Property.Sensor(label="Sensor"), Property.Number(label="Pressure", configurable=True, description="Step Pressure"),
Property.Sensor(label="Sensor", description="Temperature Sensor"),
Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Fermenterlogic automatically on and off -> Yes")]) Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Fermenterlogic automatically on and off -> Yes")])
class FermenterStep(CBPiFermentationStep): class FermenterStep(CBPiFermentationStep):
@ -183,7 +186,7 @@ class FermenterStep(CBPiFermentationStep):
if self.AutoMode == True: if self.AutoMode == True:
await self.setAutoMode(False) await self.setAutoMode(False)
self.cbpi.notify(self.name, 'Step finished', NotificationType.SUCCESS) self.cbpi.notify(self.name, 'Step finished', NotificationType.SUCCESS)
if self.shutdown != True: if self.shutdown == False:
await self.next(self.fermenter.id) await self.next(self.fermenter.id)
return StepResult.DONE return StepResult.DONE
@ -204,12 +207,14 @@ class FermenterStep(CBPiFermentationStep):
self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False
if self.fermenter is not None: if self.fermenter is not None:
self.fermenter.target_temp = int(self.props.get("Temp", 0)) self.fermenter.target_temp = float(self.props.get("Temp", 0))
self.fermenter.target_pressure = float(self.props.get("Pressure", 0))
if self.AutoMode == True: if self.AutoMode == True:
await self.setAutoMode(True) await self.setAutoMode(True)
await self.push_update() await self.push_update()
if self.fermenter is not None and self.timer is None: if self.fermenter is not None and self.timer is None:
logging.info("Set Timer")
self.timer = Timer(self.fermentationtime ,on_update=self.on_timer_update, on_done=self.on_timer_done) self.timer = Timer(self.fermentationtime ,on_update=self.on_timer_update, on_done=self.on_timer_done)
self.timer.is_running = False self.timer.is_running = False
elif self.fermenter is not None: elif self.fermenter is not None:
@ -279,6 +284,121 @@ class FermenterStep(CBPiFermentationStep):
return StepResult.DONE return StepResult.DONE
async def setAutoMode(self, auto_state):
try:
if (self.fermenter.instance is None or self.fermenter.instance.state == False) and (auto_state is True):
await self.cbpi.fermenter.toggle(self.fermenter.id)
elif (self.fermenter.instance.state == True) and (auto_state is False):
await self.cbpi.fermenter.toggle(self.fermenter.id)
await self.push_update()
except Exception as e:
logging.error("Failed to switch on FermenterLogic {} {}".format(self.fermenter.id, e))
@parameters([Property.Number(label="Temp", configurable=True, description = "Ramp to this temp"),
Property.Number(label="Pressure", configurable=True, description="Step Pressure"),
Property.Number(label="RampRate", configurable=True, description = "Ramp x °C/F per day. Default: 1"),
Property.Sensor(label="Sensor", description="Temperature Sensor"),
Property.Text(label="Notification",configurable = True, description = "Text for notification when Temp is reached"),
Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Fermenterlogic automatically on and off -> Yes")])
class FermenterRampTempStep(CBPiFermentationStep):
async def NextStep(self, **kwargs):
if self.shutdown == False:
await self.next(self.fermenter.id)
return StepResult.DONE
async def on_timer_done(self,timer):
self.summary = ""
await self.push_update()
if self.AutoMode == True:
await self.setAutoMode(False)
self.cbpi.notify(self.name, self.props.get("Notification","Target Temp reached. Please add malt and klick next to move on."))
await self.next(self.fermenter.id)
return StepResult.DONE
async def on_timer_update(self,timer, seconds):
await self.push_update()
async def on_start(self):
self.shutdown = False
self.AutoMode = True if self.props.get("AutoMode","No") == "Yes" else False
self.rate=float(self.props.get("RampRate",1))
logging.info(self.rate)
self.target_temp = round(float(self.props.get("Temp", 0))*10)/10
logging.info(self.target_temp)
self.fermenter.target_pressure = float(self.props.get("Pressure", 0))
while self.get_sensor_value(self.props.get("Sensor", None)).get("value") > 900:
await asyncio.sleep(1)
self.starttemp = self.get_sensor_value(self.props.get("Sensor", None)).get("value")
self.current_target_temp = self.starttemp
if self.fermenter is not None:
await self.set_fermenter_target_temp(self.fermenter.id, self.current_target_temp)
if self.AutoMode == True:
await self.setAutoMode(True)
self.summary = "Ramping to {}° with {}° per day".format(self.target_temp,self.rate)
if self.fermenter is not None and self.timer is None:
self.timer = Timer(1 ,on_update=self.on_timer_update, on_done=self.on_timer_done)
await self.push_update()
async def on_stop(self):
await self.timer.stop()
self.summary = ""
if self.AutoMode == True:
await self.setAutoMode(False)
await self.push_update()
async def calc_target_temp(self):
delta_time = time.time() - self.starttime
current_target_temp = round((self.starttemp + delta_time * self.ratesecond)*10)/10
# logging.info(current_target_temp)
if current_target_temp != self.current_target_temp:
self.current_target_temp = current_target_temp
await self.set_fermenter_target_temp(self.fermenter.id, self.current_target_temp)
#self.fermenter.target_temp = self.current_target_temp
await self.push_update()
pass
async def run(self):
self.delta_temp = self.target_temp-self.starttemp
try:
self.deltadays = abs(self.delta_temp / self.rate)
self.deltaseconds = self.deltadays * 24 * 60 * 60
self.ratesecond = self.delta_temp/self.deltaseconds
except Exception as e:
logging.info(e)
self.starttime=time.time()
if self.target_temp >= self.starttemp:
logging.info("warmup")
while self.running == True:
if self.current_target_temp != self.target_temp:
await self.calc_target_temp()
sensor_value = self.get_sensor_value(self.props.get("Sensor", None)).get("value")
if sensor_value >= self.target_temp and self.timer.is_running is not True:
self.timer.start()
self.timer.is_running = True
await asyncio.sleep(1)
elif self.target_temp <= self.starttemp:
logging.info("Cooldown")
while self.running == True:
if self.current_target_temp != self.target_temp:
await self.calc_target_temp()
sensor_value = self.get_sensor_value(self.props.get("Sensor", None)).get("value")
if sensor_value <= self.target_temp and self.timer.is_running is not True:
self.timer.start()
self.timer.is_running = True
await asyncio.sleep(1)
await self.push_update()
return StepResult.DONE
async def reset(self):
self.timer = Timer(1 ,on_update=self.on_timer_update, on_done=self.on_timer_done)
self.timer.is_running == False
async def setAutoMode(self, auto_state): async def setAutoMode(self, auto_state):
try: try:
if (self.fermenter.instance is None or self.fermenter.instance.state == False) and (auto_state is True): if (self.fermenter.instance is None or self.fermenter.instance.state == False) and (auto_state is True):
@ -302,4 +422,5 @@ def setup(cbpi):
cbpi.plugin.register("FermenterNotificationStep", FermenterNotificationStep) cbpi.plugin.register("FermenterNotificationStep", FermenterNotificationStep)
cbpi.plugin.register("FermenterTargetTempStep", FermenterTargetTempStep) cbpi.plugin.register("FermenterTargetTempStep", FermenterTargetTempStep)
cbpi.plugin.register("FermenterStep", FermenterStep) cbpi.plugin.register("FermenterRampTempStep", FermenterRampTempStep)
cbpi.plugin.register("FermenterStep", FermenterStep)

View file

@ -100,7 +100,7 @@ class FermenterHysteresis(CBPiFermenterLogic):
except asyncio.CancelledError as e: except asyncio.CancelledError as e:
pass pass
except Exception as e: except Exception as e:
logging.error("CustomLogic Error {}".format(e)) logging.error("Fermenter Hysteresis Error {}".format(e))
finally: finally:
self.running = False self.running = False
if self.heater: if self.heater:
@ -109,6 +109,111 @@ class FermenterHysteresis(CBPiFermenterLogic):
await self.actor_off(self.cooler) await self.actor_off(self.cooler)
@parameters([Property.Number(label="HeaterOffsetOn", configurable=True, description="Offset as decimal number when the heater is switched on. Should be greater then 'HeaterOffsetOff'. For example a value of 2 switches on the heater if the current temperature is 2 degrees below the target temperature"),
Property.Number(label="HeaterOffsetOff", configurable=True, description="Offset as decimal number when the heater is switched off. Should be smaller then 'HeaterOffsetOn'. For example a value of 1 switches off the heater if the current temperature is 1 degree below the target temperature"),
Property.Number(label="CoolerOffsetOn", configurable=True, description="Offset as decimal number when the cooler is switched on. Should be greater then 'CoolerOffsetOff'. For example a value of 2 switches on the cooler if the current temperature is 2 degrees below the target temperature"),
Property.Number(label="CoolerOffsetOff", configurable=True, description="Offset as decimal number when the cooler is switched off. Should be smaller then 'CoolerOffsetOn'. For example a value of 1 switches off the cooler if the current temperature is 1 degree below the target temperature"),
Property.Number(label="SpundingOffsetOpen", configurable=True, description="Offset above target pressure as decimal number when the valve is opened"),
Property.Select(label="ValveRelease", options=[1,2,3,4,5],description="Valve Release time in seconds"),
Property.Select(label="Pause", options=[1,2,3,4,5],description="Pause time in seconds between valve release"),
Property.Select(label="AutoStart", options=["Yes","No"],description="Autostart Fermenter on cbpi start"),
Property.Sensor(label="sensor2",description="Optional Sensor for LCDisplay(e.g. iSpindle)")])
class FermenterSpundingHysteresis(CBPiFermenterLogic):
# subroutine that controls pressure
async def pressure_control(self):
self.spunding_offset=float(self.props.get("SpundingOffsetOpen",0))
self.valverelease=int(self.props.get("ValveRelease",1))
self.pause=int(self.props.get("Pause",2))
if self.valve and self.fermenter.pressure_sensor:
#valve = self.cbpi.actor.find_by_id(self.valve)
await self.actor_off(self.valve)
#logging.info("Closing Spunding Valve")
while self.running:
target_pressure=float(self.fermenter.target_pressure)
current_pressure = float(self.get_sensor_value(self.fermenter.pressure_sensor).get("value"))
#logging.info(f'Target: {target_pressure} | Current: {current_pressure}')
if current_pressure >= (target_pressure + self.spunding_offset) and target_pressure !=0:
while current_pressure >= target_pressure:
await self.actor_on(self.valve)
await asyncio.sleep(self.valverelease)
await self.actor_off(self.valve)
await asyncio.sleep(self.pause)
current_pressure = float(self.get_sensor_value(self.fermenter.pressure_sensor).get("value"))
#logging.info("Value higher than target: Spunding loop is running")
await asyncio.sleep(1)
else:
logging.info("No valve or pressure sensor defined")
async def temperature_control(self):
self.heater_offset_min = float(self.props.get("HeaterOffsetOn", 0))
self.heater_offset_max = float(self.props.get("HeaterOffsetOff", 0))
self.cooler_offset_min = float(self.props.get("CoolerOffsetOn", 0))
self.cooler_offset_max = float(self.props.get("CoolerOffsetOff", 0))
heater = self.cbpi.actor.find_by_id(self.heater)
cooler = self.cbpi.actor.find_by_id(self.cooler)
while self.running == True:
sensor_value = float(self.get_sensor_value(self.fermenter.sensor).get("value"))
target_temp = float(self.get_fermenter_target_temp(self.id))
try:
heater_state = heater.instance.state
except:
heater_state= False
try:
cooler_state = cooler.instance.state
except:
cooler_state= False
if sensor_value + self.heater_offset_min <= target_temp:
if self.heater and (heater_state == False):
await self.actor_on(self.heater)
if sensor_value + self.heater_offset_max >= target_temp:
if self.heater and (heater_state == True):
await self.actor_off(self.heater)
if sensor_value >= self.cooler_offset_min + target_temp:
if self.cooler and (cooler_state == False):
await self.actor_on(self.cooler)
if sensor_value <= self.cooler_offset_max + target_temp:
if self.cooler and (cooler_state == True):
await self.actor_off(self.cooler)
await asyncio.sleep(1)
async def run(self):
try:
self.fermenter = self.get_fermenter(self.id)
self.heater = self.fermenter.heater
self.cooler = self.fermenter.cooler
self.valve = self.fermenter.valve
pressure_controller = asyncio.create_task(self.pressure_control())
temperature_controller = asyncio.create_task(self.temperature_control())
await pressure_controller
await temperature_controller
except asyncio.CancelledError as e:
pass
except Exception as e:
logging.error("Fermenter Spunding Hysteresis Error {}".format(e))
finally:
self.running = False
if self.heater:
await self.actor_off(self.heater)
if self.cooler:
await self.actor_off(self.cooler)
if self.valve:
await self.actor_off(self.valve)
def setup(cbpi): def setup(cbpi):
@ -119,7 +224,7 @@ def setup(cbpi):
:param cbpi: the cbpi core :param cbpi: the cbpi core
:return: :return:
''' '''
cbpi.plugin.register("Fermenter Spunding Hysteresis", FermenterSpundingHysteresis)
cbpi.plugin.register("Fermenter Hysteresis", FermenterHysteresis) cbpi.plugin.register("Fermenter Hysteresis", FermenterHysteresis)
cbpi.plugin.register("Fermenter Autostart", FermenterAutostart) cbpi.plugin.register("Fermenter Autostart", FermenterAutostart)

View file

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio import asyncio
import random import random
import logging
from cbpi.api import parameters, CBPiSensor from cbpi.api import *
from cbpi.api.base import CBPiBase
from cbpi.api.dataclasses import Kettle, Props, Fermenter
@parameters([]) @parameters([])
class CustomSensor(CBPiSensor): class CustomSensor(CBPiSensor):
@ -23,6 +24,44 @@ class CustomSensor(CBPiSensor):
def get_state(self): def get_state(self):
return dict(value=self.value) return dict(value=self.value)
@parameters([Property.Number(label="Pressure", configurable=True, description="Start Pressure"),
Property.Number(label="PressureIncrease", configurable=True, description="Pressure increase per hour"),
Property.Number(label="PressureDecrease", configurable=True, description="Pressure decrease per second on openm valve"),
Property.Fermenter(label="Fermenter",description="Fermenter")])
class DummyPressure(CBPiSensor):
def __init__(self, cbpi, id, props):
super(DummyPressure, self).__init__(cbpi, id, props)
self.value = float(self.props.get("Pressure",0))
fermenter=self.props.get("Fermenter",None)
self.fermenter=self.get_fermenter(fermenter)
self.valve=self.fermenter.valve
async def run(self):
self.uprate=float(self.props.get("PressureIncrease",0))/3600
self.decrease=float(self.props.get("PressureDecrease",0))
logging.info(self.uprate)
logging.info(self.decrease)
while self.running:
valve_state=self.get_actor_state(self.valve)
fermenter_instance=self.fermenter.instance
if fermenter_instance:
fermenter_state=fermenter_instance.state
else:
fermenter_state = False
if valve_state == False and fermenter_state:
self.value = self.value + self.uprate
elif valve_state and fermenter_state:
self.value=self.value-self.decrease
self.log_data(self.value)
self.push_update(round(self.value,2))
await asyncio.sleep(1)
def get_state(self):
return dict(value=self.value)
def setup(cbpi): def setup(cbpi):
''' '''
@ -33,3 +72,4 @@ def setup(cbpi):
:return: :return:
''' '''
cbpi.plugin.register("CustomSensor", CustomSensor) cbpi.plugin.register("CustomSensor", CustomSensor)
cbpi.plugin.register("DummyPressure", DummyPressure)

View file

@ -22,6 +22,8 @@ class MQTTActor(CBPiActor):
async def on_start(self): async def on_start(self):
self.topic = self.props.get("Topic", None) self.topic = self.props.get("Topic", None)
self.power = 100 self.power = 100
await self.off()
self.state = False
async def on(self, power=None): async def on(self, power=None):
if power is not None: if power is not None:

View file

@ -23,6 +23,32 @@ class ActorHttpEndpoints():
description: successful operation description: successful operation
""" """
return web.json_response(data=self.controller.get_state()) return web.json_response(data=self.controller.get_state())
@request_mapping(path="/{id:\w+}", auth_required=False)
async def http_get_one(self, request):
"""
---
description: Get one Actor
tags:
- Actor
parameters:
- name: "id"
in: "path"
description: "Actor ID"
required: true
type: "integer"
format: "int64"
responses:
"200":
description: successful operation
"404":
description: Actor not found
"""
actor = self.controller.find_by_id(request.match_info['id'])
if (actor is None):
return web.json_response(status=404)
return web.json_response(data=actor.to_dict(), status=200)
@request_mapping(path="/", method="POST", auth_required=False) @request_mapping(path="/", method="POST", auth_required=False)

View file

@ -12,7 +12,7 @@ class DashBoardHttpEndpoints:
def __init__(self, cbpi): def __init__(self, cbpi):
self.cbpi = cbpi self.cbpi = cbpi
self.controller = cbpi.dashboard self.controller = cbpi.dashboard
self.cbpi.register(self, "/dashboard", os.path.join(".","config", "dashboard", "widgets")) self.cbpi.register(self, "/dashboard", os.path.join(cbpi.config_folder.get_file_path("dashboard"), "widgets"))
@request_mapping(path="/{id:\d+}/content", auth_required=False) @request_mapping(path="/{id:\d+}/content", auth_required=False)

View file

@ -77,7 +77,9 @@ class FermentationHttpEndpoints():
description: successful operation description: successful operation
""" """
data = await request.json() data = await request.json()
fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), heater=data.get("heater"), cooler=data.get("cooler"), brewname=data.get("brewname"), description=data.get("description"), target_temp=data.get("target_temp"), props=Props(data.get("props", {})), type=data.get("type")) fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), pressure_sensor=data.get("pressure_sensor"), heater=data.get("heater"),
cooler=data.get("cooler"), valve=data.get("valve"), brewname=data.get("brewname"), description=data.get("description"),
target_temp=data.get("target_temp"), target_pressure=data.get("target_pressure"), props=Props(data.get("props", {})), type=data.get("type"))
response_data = await self.controller.create(fermenter) response_data = await self.controller.create(fermenter)
return web.json_response(data=response_data.to_dict()) return web.json_response(data=response_data.to_dict())
@ -115,7 +117,9 @@ class FermentationHttpEndpoints():
""" """
id = request.match_info['id'] id = request.match_info['id']
data = await request.json() data = await request.json()
fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), heater=data.get("heater"), cooler=data.get("cooler"), brewname=data.get("brewname"), description=data.get("description"), target_temp=data.get("target_temp"), props=Props(data.get("props", {})), type=data.get("type")) fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), pressure_sensor=data.get("pressure_sensor"), heater=data.get("heater"),
cooler=data.get("cooler"), valve=data.get("valve"), brewname=data.get("brewname"), description=data.get("description"),
target_temp=data.get("target_temp"), target_pressure=data.get("target_pressure"), props=Props(data.get("props", {})), type=data.get("type"))
return web.json_response(data=(await self.controller.update(fermenter)).to_dict()) return web.json_response(data=(await self.controller.update(fermenter)).to_dict())
@request_mapping(path="/{id}", method="DELETE", auth_required=False) @request_mapping(path="/{id}", method="DELETE", auth_required=False)
@ -284,6 +288,40 @@ class FermentationHttpEndpoints():
await self.controller.set_target_temp(id,data.get("temp")) await self.controller.set_target_temp(id,data.get("temp"))
return web.Response(status=204) return web.Response(status=204)
@request_mapping(path="/{id}/target_pressure", method="POST", auth_required=auth)
async def http_target_pressure(self, request) -> web.Response:
"""
---
description: Set Target pressure for Fermenter
tags:
- Fermenter
parameters:
- name: "id"
in: "path"
description: "Fermenter ID"
required: true
type: "integer"
format: "int64"
- in: body
name: body
description: Update Pressure
required: true
schema:
type: object
properties:
temp:
type: integer
responses:
"204":
description: successful operation
"""
id = request.match_info['id']
data = await request.json()
await self.controller.set_target_pressure(id,data.get("pressure"))
return web.Response(status=204)
@request_mapping(path="/{id}/addstep", method="POST", auth_required=False) @request_mapping(path="/{id}/addstep", method="POST", auth_required=False)
async def http_add_step(self, request): async def http_add_step(self, request):

View file

@ -5,7 +5,7 @@ from cbpi.job.aiohttp import get_scheduler_from_app
import logging import logging
from cbpi.api import request_mapping from cbpi.api import request_mapping
from cbpi.utils import json_dumps from cbpi.utils import json_dumps
from cbpi import __version__ from cbpi import __version__, __codename__
import pathlib import pathlib
import os import os
from cbpi.controller.system_controller import SystemController from cbpi.controller.system_controller import SystemController
@ -36,7 +36,8 @@ class SystemHttpEndpoints:
step=self.cbpi.step.get_state(), step=self.cbpi.step.get_state(),
fermentersteps=self.cbpi.fermenter.get_fermenter_steps(), fermentersteps=self.cbpi.fermenter.get_fermenter_steps(),
config=self.cbpi.config.get_state(), config=self.cbpi.config.get_state(),
version=__version__) version=__version__,
codename=__codename__)
, dumps=json_dumps) , dumps=json_dumps)
@request_mapping(path="/logs", auth_required=False) @request_mapping(path="/logs", auth_required=False)

View file

@ -28,11 +28,16 @@ class CBPiWebSocket:
self.send(data) self.send(data)
def send(self, data): def send(self, data, sorting=False):
self.logger.debug("broadcast to ws clients. Data: %s" % data) self.logger.debug("broadcast to ws clients. Data: %s" % data)
for ws in self._clients: for ws in self._clients:
async def send_data(ws, data): async def send_data(ws, data):
try: try:
if sorting:
try:
data['data'].sort(key=lambda x: x.get('name').upper())
except:
pass
await ws.send_json(data=data, dumps=json_dumps) await ws.send_json(data=data, dumps=json_dumps)
except Exception as e: except Exception as e:
self.logger.error("Error with client %s: %s" % (ws, str(e))) self.logger.error("Error with client %s: %s" % (ws, str(e)))

Binary file not shown.

View file

@ -1,3 +1,4 @@
import code
import subprocess import subprocess
import click import click
import re import re
@ -9,17 +10,23 @@ def main():
@click.command() @click.command()
@click.option('-m', prompt='Commit Message') @click.option('-m', prompt='Commit Message')
def commit(m): def commit(m):
new_content = []
file = "./cbpi/__init__.py" file = "./cbpi/__init__.py"
with open(file) as reader: with open(file) as reader:
match = re.search('.*\"(.*)\"', reader.readline()) match = re.search('.*\"(.*)\"', reader.readline())
major, minor, patch, build = match.group(1).split(".") codename = reader.readline()
build = int(build) try:
build += 1 major, minor, patch, build = match.group(1).split(".")
new_version = "__version__ = \"{}.{}.{}.{}\"".format(major,minor,patch, build) except:
major, minor, patch = match.group(1).split(".")
patch = int(patch)
patch += 1
new_content.append("__version__ = \"{}.{}.{}\"".format(major,minor,patch))
new_content.append(codename)
with open(file,'w',encoding = 'utf-8') as file: with open(file,'w',encoding = 'utf-8') as file:
print("New Version {}.{}.{}.{}".format(major,minor,patch, build)) print("New Version {}.{}.{}".format(major,minor,patch))
file.write(new_version) file.writelines("%s\n" % i for i in new_content)
subprocess.run(["git", "add", "-A"]) subprocess.run(["git", "add", "-A"])
subprocess.run(["git", "commit", "-m", "\"{}\"".format(m)]) subprocess.run(["git", "commit", "-m", "\"{}\"".format(m)])

View file

@ -21,4 +21,6 @@ asyncio-mqtt
psutil==5.9.0 psutil==5.9.0
zipp>=0.5 zipp>=0.5
PyInquirer==1.0.3 PyInquirer==1.0.3
colorama==0.4.4 colorama==0.4.4
pytest-aiohttp
coverage==6.3.1

2
run.py
View file

@ -1,3 +1,3 @@
from cbpi.cli import main from cbpi.cli import main
main() main(auto_envvar_prefix='CBPI')

View file

@ -0,0 +1,12 @@
{
"data": [
{
"id": "3CUJte4bkxDMFCtLX8eqsX",
"name": "SomeActor",
"power": 100,
"props": {},
"state": false,
"type": "DummyActor"
}
]
}

View file

@ -0,0 +1,6 @@
[Desktop Entry]
Type=Application
Name=Chromium
Comment=Chromium Webbrowser
NoDisplay=false
Exec=chromium-browser --noerrordialogs --disable-session-crashed-bubble --disable-infobars --force-device-scale-factor=1.00 --start-fullscreen "http://localhost:8000"

View file

@ -0,0 +1,148 @@
{
"AUTHOR": {
"description": "Author",
"name": "AUTHOR",
"options": null,
"type": "string",
"value": "John Doe"
},
"BREWERY_NAME": {
"description": "Brewery Name",
"name": "BREWERY_NAME",
"options": null,
"type": "string",
"value": "CraftBeerPi Brewery"
},
"MASH_TUN": {
"description": "Default Mash Tun",
"name": "MASH_TUN",
"options": null,
"type": "kettle",
"value": ""
},
"AddMashInStep": {
"description": "Add MashIn Step automatically if not defined in recipe",
"name": "AddMashInStep",
"options": [
{
"label": "Yes",
"value": "Yes"
},
{
"label": "No",
"value": "No"
}
],
"type": "select",
"value": "Yes"
},
"RECIPE_CREATION_PATH": {
"description": "API path to creation plugin. Default: empty",
"name": "RECIPE_CREATION_PATH",
"options": null,
"type": "string",
"value": ""
},
"brewfather_api_key": {
"description": "Brewfather API Kay",
"name": "brewfather_api_key",
"options": null,
"type": "string",
"value": ""
},
"brewfather_user_id": {
"description": "Brewfather User ID",
"name": "brewfather_user_id",
"options": null,
"type": "string",
"value": ""
},
"TEMP_UNIT": {
"description": "Temperature Unit",
"name": "TEMP_UNIT",
"options": [
{
"label": "C",
"value": "C"
},
{
"label": "F",
"value": "F"
}
],
"type": "select",
"value": "C"
},
"AutoMode": {
"description": "Use AutoMode in steps",
"name": "AutoMode",
"options": [
{
"label": "Yes",
"value": "Yes"
},
{
"label": "No",
"value": "No"
}
],
"type": "select",
"value": "Yes"
},
"steps_boil": {
"description": "Boil step type",
"name": "steps_boil",
"options": null,
"type": "step",
"value": "BoilStep"
},
"steps_boil_temp": {
"description": "Default Boil Temperature for Recipe Creation",
"name": "steps_boil_temp",
"options": null,
"type": "number",
"value": "99"
},
"steps_cooldown": {
"description": "Cooldown step type",
"name": "steps_cooldown",
"options": null,
"type": "step",
"value": "CooldownStep"
},
"steps_cooldown_sensor": {
"description": "Alternative Sensor to monitor temperature durring cooldown (if not selected, Kettle Sensor will be used)",
"name": "steps_cooldown_sensor",
"options": null,
"type": "sensor",
"value": ""
},
"steps_cooldown_temp": {
"description": "Cooldown temp will send notification when this temeprature is reached",
"name": "steps_cooldown_temp",
"options": null,
"type": "number",
"value": "20"
},
"steps_mash": {
"description": "Mash step type",
"name": "steps_mash",
"options": null,
"type": "step",
"value": "MashStep"
},
"steps_mashin": {
"description": "MashIn step type",
"name": "steps_mashin",
"options": null,
"type": "step",
"value": "MashInStep"
},
"steps_mashout": {
"description": "MashOut step type",
"name": "steps_mashout",
"options": null,
"type": "step",
"value": "NotificationStep"
}
}

View file

@ -0,0 +1,20 @@
name: CraftBeerPi
version: 4.0.8
index_url: /cbpi_ui/static/index.html
port: 8000
mqtt: false
mqtt_host: localhost
mqtt_port: 1883
mqtt_username: ""
mqtt_password: ""
username: cbpi
password: 123
plugins:
- cbpi4ui

View file

@ -0,0 +1,9 @@
[Unit]
Description=Craftbeer Pi
[Service]
WorkingDirectory=/home/pi
ExecStart=/usr/local/bin/cbpi start
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,62 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: craftbeerpi
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Put a short description of the service here
# Description: Put a long description of the service here
### END INIT INFO
# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=#DIR#
DAEMON=$DIR/cbpi
DAEMON_NAME=CraftBeerPI
# Add any command line options for your daemon here
DAEMON_OPTS=""
# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=root
# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid
. /lib/lsb/init-functions
do_start () {
log_daemon_msg "Starting system $DAEMON_NAME daemon"
start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --chdir $DIR --startas $DAEMON -- $DAEMON_OPTS
log_end_msg $?
}
do_stop () {
log_daemon_msg "Stopping system $DAEMON_NAME daemon"
start-stop-daemon --stop --pidfile $PIDFILE --retry 10
log_end_msg $?
}
case "$1" in
start|stop)
do_${1}
;;
restart|reload|force-reload)
do_stop
do_start
;;
status)
status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
;;
*)
echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
exit 1
;;
esac
exit 0

View file

@ -0,0 +1,3 @@
{
"elements": []
}

View file

@ -0,0 +1,5 @@
{
"data": [
]
}

View file

@ -0,0 +1,5 @@
{
"data": [
]
}

View file

@ -0,0 +1,3 @@
cbpi4-ui:
installation_date: '2021-01-06 16:03:31'
version: '0.0.1'

View file

View file

@ -0,0 +1,5 @@
{
"data": [
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -0,0 +1,8 @@
{
"basic": {
"name": ""
},
"steps": [
]
}

View file

View file

@ -0,0 +1,23 @@
# content of conftest.py
from codecs import ignore_errors
from distutils.command.config import config
import os
from cbpi.configFolder import ConfigFolder
from cbpi.craftbeerpi import CraftBeerPi
from aiohttp.test_utils import AioHTTPTestCase
from distutils.dir_util import copy_tree
class CraftBeerPiTestCase(AioHTTPTestCase):
async def get_application(self):
self.config_folder = self.configuration()
self.cbpi = CraftBeerPi(self.config_folder)
await self.cbpi.init_serivces()
return self.cbpi.app
def configuration(self):
test_directory = os.path.dirname(__file__)
test_config_directory = os.path.join(test_directory, 'cbpi-test-config')
configFolder = ConfigFolder(test_config_directory)
return configFolder

Binary file not shown.

View file

@ -1,66 +1,39 @@
import logging import logging
from unittest import mock from unittest import mock
from unittest.mock import MagicMock, Mock from aiohttp.test_utils import unittest_run_loop
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi
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')
class ActorTestCase(AioHTTPTestCase): class ActorTestCase(CraftBeerPiTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop
async def test_actor_mock(self):
with mock.patch.object(self.cbpi.bus, 'fire', wraps=self.cbpi.bus.fire) as mock_obj:
# Send HTTP POST
resp = await self.client.request("POST", "/actor/1/on")
# Check Result
assert resp.status == 204
# Check if Event are fired
assert mock_obj.call_count == 2
@unittest_run_loop @unittest_run_loop
async def test_actor_switch(self): async def test_actor_switch(self):
resp = await self.client.post(path="/login", data={"username": "cbpi", "password": "123"}) resp = await self.client.post(path="/login", data={"username": "cbpi", "password": "123"})
assert resp.status == 200 assert resp.status == 200, "login should be successful"
resp = await self.client.request("POST", "/actor/1/on") resp = await self.client.request("POST", "/actor/3CUJte4bkxDMFCtLX8eqsX/on")
assert resp.status == 204 assert resp.status == 204, "switching actor on should work"
i = await self.cbpi.actor.get_one(1) i = self.cbpi.actor.find_by_id("3CUJte4bkxDMFCtLX8eqsX")
assert i.instance.state is True assert i.instance.state is True
resp = await self.client.request("POST", "/actor/1/off") resp = await self.client.request("POST", "/actor/3CUJte4bkxDMFCtLX8eqsX/off")
assert resp.status == 204 assert resp.status == 204
i = await self.cbpi.actor.get_one(1) i = self.cbpi.actor.find_by_id("3CUJte4bkxDMFCtLX8eqsX")
assert i.instance.state is False
resp = await self.client.request("POST", "/actor/1/toggle")
assert resp.status == 204
i = await self.cbpi.actor.get_one(1)
assert i.instance.state is True
resp = await self.client.request("POST", "/actor/1/toggle")
assert resp.status == 204
i = await self.cbpi.actor.get_one(1)
assert i.instance.state is False assert i.instance.state is False
@unittest_run_loop @unittest_run_loop
async def test_crud(self): async def test_crud(self):
data = { data = {
"name": "CustomActor", "name": "SomeActor",
"type": "CustomActor", "power": 100,
"config": { "props": {
"interval": 5 },
} "state": False,
"type": "DummyActor"
} }
# Add new sensor # Add new sensor

View file

@ -1,20 +1,16 @@
import logging import logging
import unittest import unittest
from cbpi.cli import CraftBeerPiCli
from cli import add, remove, list_plugins from cbpi.configFolder import ConfigFolder
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')
class CLITest(unittest.TestCase): 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): def test_list(self):
list_plugins() cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config"))
cli.plugins_list()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -1,54 +1,31 @@
import time import time
import aiosqlite from aiohttp.test_utils import unittest_run_loop
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.api.config import ConfigType
from cbpi.craftbeerpi import CraftBeerPi
class ConfigTestCase(AioHTTPTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
class ConfigTestCase(CraftBeerPiTestCase):
@unittest_run_loop @unittest_run_loop
async def test_get(self): async def test_get(self):
assert self.cbpi.config.get("CBPI_TEST_1", 1) == "22" assert self.cbpi.config.get("steps_boil_temp", 1) == "99"
@unittest_run_loop @unittest_run_loop
async def test_set_get(self): async def test_set_get(self):
value = str(time.time()) value = 35
await self.cbpi.config.set("CBPI_TEST_2", value) await self.cbpi.config.set("steps_cooldown_temp", value)
assert self.cbpi.config.get("steps_cooldown_temp", 1) == value
assert self.cbpi.config.get("CBPI_TEST_2", 1) == value
@unittest_run_loop
async def test_add(self):
value = str(time.time())
key = "CBPI_TEST_3"
async with aiosqlite.connect("./craftbeerpi.db") as db:
await db.execute("DELETE FROM config WHERE name = ? ", (key,))
await db.commit()
await self.cbpi.config.add(key, value, type=ConfigType.STRING, description="test")
@unittest_run_loop @unittest_run_loop
async def test_http_set(self): async def test_http_set(self):
value = str(time.time()) value = "Some New Brewery Name"
key = "CBPI_TEST_3" key = "BREWERY_NAME"
await self.cbpi.config.set(key, value)
assert self.cbpi.config.get(key, 1) == value
resp = await self.client.request("PUT", "/config/%s/" % key, json={'value': '1'}) resp = await self.client.request("PUT", "/config/%s/" % key, json={'value': value})
assert resp.status == 204 assert resp.status == 204
assert self.cbpi.config.get(key, -1) == "1"
assert self.cbpi.config.get(key, -1) == value
@unittest_run_loop @unittest_run_loop
async def test_http_get(self): async def test_http_get(self):
@ -57,5 +34,5 @@ class ConfigTestCase(AioHTTPTestCase):
@unittest_run_loop @unittest_run_loop
async def test_get_default(self): async def test_get_default(self):
value = self.cbpi.config.get("HELLO_WORLD", None) value = self.cbpi.config.get("HELLO_WORLD", "DefaultValue")
assert value == None assert value == "DefaultValue"

View file

@ -1,18 +1,10 @@
import aiohttp from aiohttp.test_utils import unittest_run_loop
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi from cbpi.craftbeerpi import CraftBeerPi
class DashboardTestCase(AioHTTPTestCase): class DashboardTestCase(CraftBeerPiTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_crud(self): async def test_crud(self):
@ -28,56 +20,12 @@ class DashboardTestCase(AioHTTPTestCase):
"config": {} "config": {}
} }
resp = await self.client.get(path="/dashboard") resp = await self.client.get(path="/dashboard/current")
assert resp.status == 200 assert resp.status == 200
# Add new dashboard dashboard_id = await resp.json()
resp = await self.client.post(path="/dashboard/", json=data)
assert resp.status == 200
m = await resp.json() # Add dashboard content
dashboard_id = m["id"]
# Get dashboard
resp = await self.client.get(path="/dashboard/%s" % dashboard_id)
assert resp.status == 200
m2 = await resp.json()
dashboard_id = m2["id"]
# Update dashboard
resp = await self.client.put(path="/dashboard/%s" % dashboard_id, json=m)
assert resp.status == 200
# Add dashboard content
dashboard_content["dbid"] = dashboard_id dashboard_content["dbid"] = dashboard_id
resp = await self.client.post(path="/dashboard/%s/content" % dashboard_id, json=dashboard_content) resp = await self.client.post(path="/dashboard/%s/content" % dashboard_id, json=dashboard_content)
assert resp.status == 200 assert resp.status == 204
m_content = await resp.json()
print("CONTENT", m_content)
content_id = m_content["id"]
# Get dashboard
resp = await self.client.get(path="/dashboard/%s/content" % (dashboard_id))
assert resp.status == 200
resp = await self.client.post(path="/dashboard/%s/content/%s/move" % (dashboard_id, content_id), json=dict(x=1,y=1))
assert resp.status == 200
resp = await self.client.delete(path="/dashboard/%s/content/%s" % (dashboard_id, content_id))
assert resp.status == 204
# Delete dashboard
resp = await self.client.delete(path="/dashboard/%s" % dashboard_id)
assert resp.status == 204
@unittest_run_loop
async def test_dashboard_controller(self):
result = await self.cbpi.dashboard.get_all()
print(result)
await self.cbpi.dashboard.add(**{"name":"Tewst"})
print(await self.cbpi.dashboard.get_one(1))
await self.cbpi.dashboard.add_content(dict(dbid=1,element_id=1,type="test",config={"name":"Manue"}))
await self.cbpi.dashboard.move_content(1,2,3)

View file

@ -1,116 +0,0 @@
import asyncio
from cbpi.api.dataclasses import Fermenter, FermenterStep, Props, Step
import logging
from unittest import mock
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from cbpi.craftbeerpi import CraftBeerPi
from cbpi.controller.fermentation_controller import FermenationController
import unittest
import json
from aiohttp import web
from unittest.mock import MagicMock, Mock
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
class FermenterTest(AioHTTPTestCase):
async def get_application(self):
app = web.Application()
return app
def create_file(self):
data = [
{
"id": "f1",
"name": "Fermenter1",
"props": {},
"steps": [
{
"id": "f1s1",
"name": "Step1",
"props": {},
"state_text": "",
"status": "I",
"type": "T2"
},
{
"id": "f1s2",
"name": "Step2",
"props": {},
"state_text": "",
"status": "I",
"type": "T1"
},
],
"target_temp": 0
},
{
"id": "f2",
"name": "Fermenter2",
"props": {},
"steps": [
{
"id": "f2s1",
"name": "Step1",
"props": {},
"state_text": "",
"status": "I",
"type": "T1"
},
{
"id": "f2s2",
"name": "Step2",
"props": {},
"state_text": "",
"status": "I",
"type": "T2"
},
],
"target_temp": 0
}
]
with open("./config/fermenter_data.json", "w") as file:
json.dump(data, file, indent=4, sort_keys=True)
@unittest_run_loop
async def test_actor_mock(self):
self.create_file()
mock = Mock()
f = FermenationController(mock)
f.types = {
"T1": {"name": "T2", "class": FermenterStep, "properties": [], "actions": []},
"T2": {"name": "T2", "class": FermenterStep, "properties": [], "actions": []}
}
await f.load()
#ferm = Fermenter(name="Maneul")
# item = await f.create(ferm)
# await f.create_step(item.id, Step(name="Manuel"))
# await f.delete(item.id)
item = await f.get("f1")
await f.start("f1")
await f.start("f2")
await asyncio.sleep(3)
# await f.create_step(item.id, Step(name="MANUEL", props=Props()))
#await f.start(item.id)
#await asyncio.sleep(1)
#await f.next(item.id)
#await asyncio.sleep(1)
#await f.next(item.id)
#await asyncio.sleep(1)
#await f.next(item.id)
#await asyncio.sleep(1)
#await f.move_step("f1", "f1s1", 1)
# await f.reset(item.id)
await f.shutdown()
if __name__ == '__main__':
unittest.main()

View file

@ -15,13 +15,6 @@ except Exception:
patcher.start() patcher.start()
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
class HelloWorld(object):
def test(self, a):
return a
class TestSwitch(unittest.TestCase): class TestSwitch(unittest.TestCase):
GPIO_NUM = 22 GPIO_NUM = 22
@ -35,15 +28,4 @@ class TestSwitch(unittest.TestCase):
@patch("RPi.GPIO.output") @patch("RPi.GPIO.output")
def test_switch_without_scheduler_starts_disabled(self, patched_output): def test_switch_without_scheduler_starts_disabled(self, patched_output):
GPIO.output(self.GPIO_NUM, GPIO.LOW) GPIO.output(self.GPIO_NUM, GPIO.LOW)
patched_output.assert_called_once_with(self.GPIO_NUM, GPIO.LOW) patched_output.assert_called_once_with(self.GPIO_NUM, GPIO.LOW)
def test_hello_world(self):
h = HelloWorld()
with mock.patch.object(HelloWorld, 'test', wraps=h.test) as fake_increment:
#print(h.test("HALLO"))
print(h.test("ABC"))
print(fake_increment.call_args)
print(h.test("HALLO"))
print(fake_increment.call_args_list)

View file

@ -1,14 +1,8 @@
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from aiohttp.test_utils import unittest_run_loop
from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi
class IndexTestCase(AioHTTPTestCase): class IndexTestCase(CraftBeerPiTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_index(self): async def test_index(self):

View file

@ -1,52 +1,16 @@
import asyncio from aiohttp.test_utils import unittest_run_loop
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi
class KettleTestCase(AioHTTPTestCase): class KettleTestCase(CraftBeerPiTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_get(self): async def test_get(self):
resp = await self.client.request("GET", "/kettle") resp = await self.client.request("GET", "/kettle")
assert resp.status == 200 assert resp.status == 200
print(await resp.json()) kettle = resp.json()
assert kettle != None
@unittest_run_loop
async def test_heater(self):
resp = await self.client.get("/kettle/1/heater/on")
assert resp.status == 204
resp = await self.client.get("/kettle/1/heater/off")
assert resp.status == 204
@unittest_run_loop
async def test_agitator(self):
resp = await self.client.get("/kettle/1/agitator/on")
assert resp.status == 204
resp = await self.client.get("/kettle/1/agitator/off")
assert resp.status == 204
@unittest_run_loop
async def test_temp(self):
resp = await self.client.get("/kettle/1/temp")
assert resp.status == 204
resp = await self.client.get("/kettle/1/targettemp")
assert resp.status == 200
@unittest_run_loop
async def test_automatic(self):
resp = await self.client.post("/kettle/1/automatic")
assert resp.status == 204
@unittest_run_loop @unittest_run_loop
async def test_crud(self): async def test_crud(self):
@ -69,21 +33,21 @@ class KettleTestCase(AioHTTPTestCase):
m = await resp.json() m = await resp.json()
sensor_id = m["id"] kettle_id = m["id"]
print("KETTLE", m["id"], m) print("KETTLE", m["id"], m)
# Get sensor
resp = await self.client.get(path="/kettle/%s" % sensor_id) # Update Kettle
resp = await self.client.put(path="/kettle/%s" % kettle_id, json=m)
assert resp.status == 200 assert resp.status == 200
m2 = await resp.json() # Set Kettle target temp
sensor_id = m2["id"] resp = await self.client.post(path="/kettle/%s/target_temp" % kettle_id, json={"temp":75})
assert resp.status == 204
# Update Sensor
resp = await self.client.put(path="/kettle/%s" % sensor_id, json=m)
assert resp.status == 200
# # Delete Sensor # # Delete Sensor
resp = await self.client.delete(path="/kettle/%s" % sensor_id) resp = await self.client.delete(path="/kettle/%s" % kettle_id)
assert resp.status == 204 assert resp.status == 204

View file

@ -1,21 +1,16 @@
import asyncio import asyncio
import glob import glob
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from aiohttp.test_utils import unittest_run_loop
from tests.cbpi_config_fixture import CraftBeerPiTestCase
import os
from cbpi.craftbeerpi import CraftBeerPi, load_config class LoggerTestCase(CraftBeerPiTestCase):
class UtilsTestCase(AioHTTPTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_log_data(self): async def test_log_data(self):
os.makedirs("./logs", exist_ok=True)
log_name = "test" log_name = "test"
#clear all logs #clear all logs
self.cbpi.log.clear_log(log_name) self.cbpi.log.clear_log(log_name)

View file

@ -1,16 +1,8 @@
import aiohttp from aiohttp.test_utils import unittest_run_loop
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi
class NotificationTestCase(AioHTTPTestCase): class NotificationTestCase(CraftBeerPiTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_actor_switch(self): async def test_actor_switch(self):

View file

@ -1,14 +1,8 @@
import asyncio from aiohttp.test_utils import unittest_run_loop
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi
class SensorTestCase(AioHTTPTestCase): class SensorTestCase(CraftBeerPiTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_crud(self): async def test_crud(self):
@ -28,12 +22,11 @@ class SensorTestCase(AioHTTPTestCase):
m = await resp.json() m = await resp.json()
sensor_id = m["id"] sensor_id = m["id"]
# Get sensor # Get sensor value
resp = await self.client.get(path="/sensor/%s"% sensor_id) resp = await self.client.get(path="/sensor/%s"% sensor_id)
assert resp.status == 200 assert resp.status == 200
m2 = await resp.json() m2 = await resp.json()
sensor_id = m2["id"]
# Update Sensor # Update Sensor
resp = await self.client.put(path="/sensor/%s" % sensor_id, json=m) resp = await self.client.put(path="/sensor/%s" % sensor_id, json=m)

View file

@ -1,23 +1,13 @@
import asyncio import asyncio
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from aiohttp.test_utils import unittest_run_loop
from cbpi.craftbeerpi import CraftBeerPi from tests.cbpi_config_fixture import CraftBeerPiTestCase
class StepTestCase(CraftBeerPiTestCase):
class StepTestCase(AioHTTPTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_get(self): async def test_get(self):
resp = await self.client.request("GET", "/step") resp = await self.client.request("GET", "/step2")
print(resp)
assert resp.status == 200
resp = await self.client.request("GET", "/step/types")
print(resp) print(resp)
assert resp.status == 200 assert resp.status == 200
@ -31,26 +21,19 @@ class StepTestCase(AioHTTPTestCase):
} }
# Add new step # Add new step
resp = await self.client.post(path="/step/", json=data) resp = await self.client.post(path="/step2/", json=data)
assert resp.status == 200 assert resp.status == 200
m = await resp.json() m = await resp.json()
print("Step", m) print("Step", m)
sensor_id = m["id"] sensor_id = m["id"]
# Get sensor # Update step
resp = await self.client.get(path="/step/%s" % sensor_id) resp = await self.client.put(path="/step2/%s" % sensor_id, json=m)
assert resp.status == 200 assert resp.status == 200
m2 = await resp.json() # # Delete step
sensor_id = m2["id"] resp = await self.client.delete(path="/step2/%s" % sensor_id)
# Update Sensor
resp = await self.client.put(path="/step/%s" % sensor_id, json=m)
assert resp.status == 200
# # Delete Sensor
resp = await self.client.delete(path="/step/%s" % sensor_id)
assert resp.status == 204 assert resp.status == 204
def create_wait_callback(self, topic): def create_wait_callback(self, topic):
@ -66,34 +49,4 @@ class StepTestCase(AioHTTPTestCase):
done, pending = await asyncio.wait({future}) done, pending = await asyncio.wait({future})
if future in done: if future in done:
pass pass
@unittest_run_loop
async def test_process(self):
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 step_ctlr.next()
await self.wait(future)
await self.print_steps()
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())

View file

@ -1,46 +0,0 @@
import logging
from unittest import mock
import unittest
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from cbpi.craftbeerpi import CraftBeerPi
import pprint
import asyncio
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
pp = pprint.PrettyPrinter(indent=4)
class ActorTestCase(AioHTTPTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
'''
@unittest_run_loop
async def test_get_all(self):
resp = await self.client.get(path="/step2")
assert resp.status == 200
@unittest_run_loop
async def test_add_step(self):
resp = await self.client.post(path="/step2", json=dict(name="Manuel"))
data = await resp.json()
assert resp.status == 200
@unittest_run_loop
async def test_delete(self):
resp = await self.client.post(path="/step2", json=dict(name="Manuel"))
data = await resp.json()
assert resp.status == 200
resp = await self.client.delete(path="/step2/%s" % data["id"])
assert resp.status == 204
'''
@unittest_run_loop
async def test_move(self):
await self.cbpi.step2.resume()
if __name__ == '__main__':
unittest.main()

View file

@ -1,19 +1,11 @@
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from aiohttp.test_utils import unittest_run_loop
from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi
class IndexTestCase(AioHTTPTestCase): class IndexTestCase(CraftBeerPiTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop @unittest_run_loop
async def test_endpoints(self): async def test_endpoints(self):
# Test Index Page # Test Index Page
resp = await self.client.post(path="/system/restart") resp = await self.client.post(path="/system/restart")
assert resp.status == 200 assert resp.status == 200
@ -21,9 +13,3 @@ class IndexTestCase(AioHTTPTestCase):
resp = await self.client.post(path="/system/shutdown") resp = await self.client.post(path="/system/shutdown")
assert resp.status == 200 assert resp.status == 200
resp = await self.client.get(path="/system/jobs")
assert resp.status == 200
resp = await self.client.get(path="/system/events")
assert resp.status == 200

View file

@ -1,16 +0,0 @@
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from cbpi.craftbeerpi import CraftBeerPi, load_config
class UtilsTestCase(AioHTTPTestCase):
async def get_application(self):
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
@unittest_run_loop
async def test_load_file(self):
assert load_config("") is None

View file

@ -1,56 +1,46 @@
import asyncio
import aiohttp import aiohttp
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from aiohttp.test_utils import unittest_run_loop
from tests.cbpi_config_fixture import CraftBeerPiTestCase
from cbpi.craftbeerpi import CraftBeerPi # class WebSocketTestCase(CraftBeerPiTestCase):
# @unittest_run_loop
# async def test_brewing_process(self):
class WebSocketTestCase(AioHTTPTestCase): # count_step_done = 0
# async with self.client.ws_connect('/ws') as ws:
# await ws.send_json(data=dict(topic="step/stop"))
# await ws.send_json(data=dict(topic="step/start"))
# async for msg in ws:
# if msg.type == aiohttp.WSMsgType.TEXT:
# try:
# msg_obj = msg.json()
# topic = msg_obj.get("topic")
# if topic == "job/step/done":
# count_step_done = count_step_done + 1
# if topic == "step/brewing/finished":
# await ws.send_json(data=dict(topic="close"))
# except Exception as e:
# print(e)
# break
# elif msg.type == aiohttp.WSMsgType.ERROR:
# break
async def get_application(self): # assert count_step_done == 4
self.cbpi = CraftBeerPi()
await self.cbpi.init_serivces()
return self.cbpi.app
# @unittest_run_loop
# async def test_wrong_format(self):
@unittest_run_loop # async with self.client.ws_connect('/ws') as ws:
async def test_brewing_process(self): # await ws.send_json(data=dict(a="close"))
# async for msg in ws:
# print("MSG TYP", msg.type, msg.data)
# if msg.type == aiohttp.WSMsgType.TEXT:
# msg_obj = msg.json()
# if msg_obj["topic"] != "connection/success":
# print(msg.data)
# raise Exception()
count_step_done = 0 # else:
async with self.client.ws_connect('/ws') as ws: # raise Exception()
await ws.send_json(data=dict(topic="step/stop"))
await ws.send_json(data=dict(topic="step/start"))
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
try:
msg_obj = msg.json()
topic = msg_obj.get("topic")
if topic == "job/step/done":
count_step_done = count_step_done + 1
if topic == "step/brewing/finished":
await ws.send_json(data=dict(topic="close"))
except Exception as e:
print(e)
break
elif msg.type == aiohttp.WSMsgType.ERROR:
break
assert count_step_done == 4
@unittest_run_loop
async def test_wrong_format(self):
async with self.client.ws_connect('/ws') as ws:
await ws.send_json(data=dict(a="close"))
async for msg in ws:
print("MSG TYP", msg.type, msg.data)
if msg.type == aiohttp.WSMsgType.TEXT:
msg_obj = msg.json()
if msg_obj["topic"] != "connection/success":
print(msg.data)
raise Exception()
else:
raise Exception()