diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..0ac9cc3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -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 \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/actor.json b/.devcontainer/cbpi-dev-config/actor.json new file mode 100644 index 0000000..ce96464 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/actor.json @@ -0,0 +1,3 @@ +{ + "data": [] +} \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/chromium.desktop b/.devcontainer/cbpi-dev-config/chromium.desktop new file mode 100644 index 0000000..a112515 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/chromium.desktop @@ -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" diff --git a/.devcontainer/cbpi-dev-config/config.json b/.devcontainer/cbpi-dev-config/config.json new file mode 100644 index 0000000..f6899f3 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/config.json @@ -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" + } +} \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/config.yaml b/.devcontainer/cbpi-dev-config/config.yaml new file mode 100644 index 0000000..da4fa18 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/config.yaml @@ -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 + diff --git a/.devcontainer/cbpi-dev-config/craftbeerpi.service b/.devcontainer/cbpi-dev-config/craftbeerpi.service new file mode 100644 index 0000000..cd02dce --- /dev/null +++ b/.devcontainer/cbpi-dev-config/craftbeerpi.service @@ -0,0 +1,9 @@ +[Unit] +Description=Craftbeer Pi + +[Service] +WorkingDirectory=/home/pi +ExecStart=/usr/local/bin/cbpi start + +[Install] +WantedBy=multi-user.target diff --git a/.devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json b/.devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json new file mode 100644 index 0000000..92079a0 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json @@ -0,0 +1,3 @@ +{ + "elements": [] +} \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/dashboard/widgets/_widgets_are_placed_here b/.devcontainer/cbpi-dev-config/dashboard/widgets/_widgets_are_placed_here new file mode 100644 index 0000000..e69de29 diff --git a/.devcontainer/cbpi-dev-config/fermenter_data.json b/.devcontainer/cbpi-dev-config/fermenter_data.json new file mode 100644 index 0000000..ce96464 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/fermenter_data.json @@ -0,0 +1,3 @@ +{ + "data": [] +} \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/kettle.json b/.devcontainer/cbpi-dev-config/kettle.json new file mode 100644 index 0000000..ce96464 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/kettle.json @@ -0,0 +1,3 @@ +{ + "data": [] +} \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/sensor.json b/.devcontainer/cbpi-dev-config/sensor.json new file mode 100644 index 0000000..ce96464 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/sensor.json @@ -0,0 +1,3 @@ +{ + "data": [] +} \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/step_data.json b/.devcontainer/cbpi-dev-config/step_data.json new file mode 100644 index 0000000..fa93cc6 --- /dev/null +++ b/.devcontainer/cbpi-dev-config/step_data.json @@ -0,0 +1,6 @@ +{ + "basic": { + "name": "" + }, + "steps": [] +} \ No newline at end of file diff --git a/.devcontainer/cbpi-dev-config/upload/_uploads_are_placed_here b/.devcontainer/cbpi-dev-config/upload/_uploads_are_placed_here new file mode 100644 index 0000000..e69de29 diff --git a/.devcontainer/createMqttUser.sh b/.devcontainer/createMqttUser.sh new file mode 100644 index 0000000..0498e8d --- /dev/null +++ b/.devcontainer/createMqttUser.sh @@ -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 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..08f216c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} diff --git a/.devcontainer/docker-compose.dev.yml b/.devcontainer/docker-compose.dev.yml new file mode 100644 index 0000000..fa0ca39 --- /dev/null +++ b/.devcontainer/docker-compose.dev.yml @@ -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 diff --git a/.devcontainer/mosquitto/config/mosquitto.conf b/.devcontainer/mosquitto/config/mosquitto.conf new file mode 100644 index 0000000..ea00761 --- /dev/null +++ b/.devcontainer/mosquitto/config/mosquitto.conf @@ -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 diff --git a/.devcontainer/mosquitto/config/mosquitto.passwd b/.devcontainer/mosquitto/config/mosquitto.passwd new file mode 100644 index 0000000..3799476 --- /dev/null +++ b/.devcontainer/mosquitto/config/mosquitto.passwd @@ -0,0 +1,2 @@ +craftbeerpi:$7$101$cRIEIwJ9L/+TAFF1$lxT+v9SisokWaRBgB/Scut7DaotH4RMgzHttYHhwuy6m5yatSoac7bwrkztoQ7raNehBhKt/A4VVejnzozdxXA== +mqtt-explorer:$7$101$SFFKvbIBVXFFAIBp$Pgue6DaAfcuhegjEqtTjf+WWgNZ8geiv1/3fXqmJ0APmd0L80wNTSrEhnFdJmHvi0/vW6V9bVKPJfVRDIjPxCw== diff --git a/.devcontainer/mqtt-explorer/config/settings.json b/.devcontainer/mqtt-explorer/config/settings.json new file mode 100644 index 0000000..826101c --- /dev/null +++ b/.devcontainer/mqtt-explorer/config/settings.json @@ -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" + } + } +} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39d9296..6585770 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,8 +27,11 @@ jobs: - name: Clean run: python setup.py clean --all -# - name: Run tests -# run: python -m unittest tests + - name: Install Requirements + run: pip3 install -r requirements.txt + + - name: Run tests + run: coverage run --source cbpi -m pytest tests - name: Build source distribution package for CraftBeerPi run: python setup.py sdist @@ -61,7 +64,7 @@ jobs: if [[ $GITHUB_REF_NAME == master ]] || [[ $GITHUB_REF_NAME == main ]]; then # when building master/main use :latest tag and the version number # 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 BUILD_CACHE_IMAGE_NAME=${LATEST_IMAGE} TAGS="${LATEST_IMAGE},${{ env.image-name }}:v${VERSION}" diff --git a/.gitignore b/.gitignore index c9519c8..6349f01 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ node_modules .vscode .venv* .DS_Store -.vscode/ config/* -logs/ \ No newline at end of file +logs/ +.coverage \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c2f9637 --- /dev/null +++ b/.vscode/launch.json @@ -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"] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7ec7342 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.pythonPath": "/bin/python3", + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 23fa44e..620b16b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM alpine:latest as download RUN apk --no-cache add curl && mkdir /downloads # 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 diff --git a/README.md b/README.md index cf2789f..5dfc777 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # CraftBeerPi 4 -[![Build](https://github.com/avollkopf/craftbeerpi4/actions/workflows/build.yml/badge.svg)](https://github.com/avollkopf/craftbeerpi4/actions/workflows/build.yml) -[![GitHub license](https://img.shields.io/github/license/avollkopf/craftbeerpi4)](https://github.com/avollkopf/craftbeerpi4/blob/master/LICENSE) -![GitHub issues](https://img.shields.io/github/issues-raw/Manuel83/craftbeerpi4) +[![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/craftbeerpi/craftbeerpi4)](https://github.com/craftbeerpi/craftbeerpi4/blob/master/LICENSE) +![GitHub issues](https://img.shields.io/github/issues-raw/craftbeerpi/craftbeerpi4) ![PyPI](https://img.shields.io/pypi/v/cbpi) ![Happy Brewing](https://img.shields.io/badge/CraftBeerPi%204-Happy%20Brewing-%23FBB117)

- CraftBeerPi Logo + CraftBeerPi Logo

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. 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 -[![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) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index cee5728..743997c 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1,3 @@ -__version__ = "4.0.2.0.a17" +__version__ = "4.0.5.a11" +__codename__ = "Spring Break" + diff --git a/cbpi/api/base.py b/cbpi/api/base.py index d698c1e..837b4a2 100644 --- a/cbpi/api/base.py +++ b/cbpi/api/base.py @@ -38,6 +38,12 @@ class CBPiBase(metaclass=ABCMeta): async def set_fermenter_target_temp(self,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): return self.cbpi.sensor.find_by_id(id) diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index ffb6963..614b36c 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -126,21 +126,23 @@ class Fermenter: id: str = None name: str = None sensor: Sensor = None + pressure_sensor : Sensor = None heater: Actor = None cooler: Actor = None + valve: Actor = None brewname: str = None description : str = None props: Props = Props() - target_temp: int = 0 + target_temp: float = 0 + target_pressure: float = 0 type: str = None steps: List[Step]= field(default_factory=list) instance: str = None 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): if self.instance is not None: @@ -151,7 +153,7 @@ class Fermenter: state = False 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 diff --git a/cbpi/api/extension.py b/cbpi/api/extension.py index b3a0148..0c2c61a 100644 --- a/cbpi/api/extension.py +++ b/cbpi/api/extension.py @@ -48,7 +48,7 @@ class CBPiExtension(): return data except: - logger.warning("Faild to load config %s/config.yaml" % path) + logger.warning("Failed to load config %s/config.yaml" % path) diff --git a/cbpi/api/sensor.py b/cbpi/api/sensor.py index 4ed9777..a539aca 100644 --- a/cbpi/api/sensor.py +++ b/cbpi/api/sensor.py @@ -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/sensor/{}/udpate".format(self.id), dict(id=self.id, value=value), retain=True) except: - logging.error("Faild to push sensor update") + logging.error("Failed to push sensor update") async def start(self): pass diff --git a/cbpi/cli.py b/cbpi/cli.py index 850b53a..815e30e 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -1,433 +1,285 @@ -import argparse -import datetime import logging -import subprocess -import sys -import re import requests -import yaml +from cbpi.configFolder import ConfigFolder from cbpi.utils.utils import load_config from zipfile import ZipFile from cbpi.craftbeerpi import CraftBeerPi import os -import platform -import pathlib import pkgutil import shutil -import yaml import click from subprocess import call -import zipfile from colorama import Fore, Back, Style -from importlib import import_module import importlib -from jinja2 import Template -from importlib_metadata import metadata, version +from importlib_metadata import metadata from tabulate import tabulate 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: - srcfile = os.path.join(os.path.dirname(__file__), "config", "sensor.json") - destfile = os.path.join(".", 'config') - shutil.copy(srcfile, destfile) +class CraftBeerPiCli(): + def __init__(self, config) -> None: + self.config = config + pass - if os.path.exists(os.path.join(".", 'config', "kettle.json")) is False: - srcfile = os.path.join(os.path.dirname(__file__), "config", "kettle.json") - destfile = os.path.join(".", 'config') - shutil.copy(srcfile, destfile) + def setup(self): + print("Setting up CraftBeerPi") + self.config.create_home_folder_structure() + self.config.create_config_file() - if os.path.exists(os.path.join(".", 'config', "fermenter_data.json")) is False: - srcfile = os.path.join(os.path.dirname(__file__), "config", "fermenter_data.json") - destfile = os.path.join(".", 'config') - shutil.copy(srcfile, destfile) + def start(self): + if self.config.check_for_setup() is False: + return + print("START") + cbpi = CraftBeerPi(self.config) + cbpi.start() - if os.path.exists(os.path.join(".", 'config', "step_data.json")) is False: - srcfile = os.path.join(os.path.dirname(__file__), "config", "step_data.json") - destfile = os.path.join(".", 'config') - shutil.copy(srcfile, destfile) + def setup_one_wire(self): + 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") - if os.path.exists(os.path.join(".", 'config', "config.json")) is False: - srcfile = os.path.join(os.path.dirname(__file__), "config", "config.json") - destfile = os.path.join(".", 'config') - shutil.copy(srcfile, destfile) - - 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(): + def list_one_wire(self): + print("List 1Wire") + call(["modprobe", "w1-gpio"]) + call(["modprobe", "w1-therm"]) try: - meta = metadata(key) - result.append(dict(Name=meta["Name"], Version=meta["Version"], Author=meta["Author"], Homepage=meta["Home-page"], Summary=meta["Summary"])) - + 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) - print(Fore.LIGHTGREEN_EX, tabulate(result, headers="keys"), Style.RESET_ALL) - - -def plugin_create(): - - - - - print("Plugin Creation") - print("") - - questions = [ - { - 'type': 'input', - 'name': 'name', - 'message': 'Plugin Name:', + def plugins_list(self): + 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: + 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"] - if os.path.exists(os.path.join(".", name)) is True: - print("Cant create Plugin. Folder {} already exists ".format(name)) - return + def plugin_create(self): + print("Plugin Creation") + print("") - 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) + questions = [ + { + 'type': 'input', + 'name': 'name', + 'message': 'Plugin Name:', + } + ] - with ZipFile('temp.zip', 'r') as repo_zip: - repo_zip.extractall() + answers = prompt(questions) - os.rename("./craftbeerpi4-plugin-template-main", os.path.join(".", name)) - os.rename(os.path.join(".", name, "src"), os.path.join(".", name, name)) + name = "cbpi4_" + answers["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)) - templateEnv = jinja2.Environment(loader=templateLoader) - TEMPLATE_FILE = "setup.py" - template = templateEnv.get_template(TEMPLATE_FILE) - outputText = template.render(name=name) + with ZipFile('temp.zip', 'r') as repo_zip: + repo_zip.extractall() - with open(os.path.join(".", name, "setup.py"), "w") as fh: - fh.write(outputText) + os.rename("./craftbeerpi4-plugin-template-main", os.path.join(".", name)) + os.rename(os.path.join(".", name, "src"), os.path.join(".", name, name)) - TEMPLATE_FILE = "MANIFEST.in" - 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) + import jinja2 - TEMPLATE_FILE = os.path.join("/", name, "config.yaml") - template = templateEnv.get_template(TEMPLATE_FILE) - outputText = template.render(name=name) + templateLoader = jinja2.FileSystemLoader(searchpath=os.path.join(".", name)) + templateEnv = jinja2.Environment(loader=templateLoader) + 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: - fh.write(outputText) + with open(os.path.join(".", name, "setup.py"), "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("") - + TEMPLATE_FILE = "MANIFEST.in" + 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") + operatingsystem = str(platform.system()).lower() + if operatingsystem.startswith("win"): + 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() -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("Welcome to CBPi") print("---------------------") level = logging.INFO 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 - -@click.command() -def setup(): +@main.command() +@click.pass_context +def setup(context): '''Create Config folder''' - print("Setting up CraftBeerPi") - create_home_folder_structure() - create_config_file() + context.obj.setup() - -@click.command() +@main.command() +@click.pass_context @click.option('--list', is_flag=True, help="List all 1Wire Devices") @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''' if setup is True: - setup_one_wire() + context.obj.setup_one_wire() if list is True: - list_one_wire() + context.obj.list_one_wire() +@main.command() +@click.pass_context +def start(context): + context.obj.start() - -@click.command() -def start(): - '''Lets go brewing''' - if check_for_setup() is False: - return - print("Starting up CraftBeerPi ...") - cbpi = CraftBeerPi() - cbpi.start() - - -@click.command() -def plugins(): +@main.command() +@click.pass_context +def plugins(context): '''List active plugins''' - plugins_list() - return - - - + context.obj.plugins_list() @click.command() -def create(): +@click.pass_context +def create(context): '''Create New Plugin''' - plugin_create() + context.obj.plugin_create() -@click.command() +@main.command() +@click.pass_context @click.argument('name') -def autostart(name): +def autostart(context, name): '''(on|off|status) 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 = 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 - - + context.obj.autostart(name) -@click.command() +@main.command() +@click.pass_context @click.argument('name') -def chromium(name): +def chromium(context, name): '''(on|off|status) Enable or disable Kiosk mode''' - 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 = 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) + context.obj.chromium(name) diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py new file mode 100644 index 0000000..542ce73 --- /dev/null +++ b/cbpi/configFolder.py @@ -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) \ No newline at end of file diff --git a/cbpi/controller/actor_controller.py b/cbpi/controller/actor_controller.py index f31abeb..1ae2a01 100644 --- a/cbpi/controller/actor_controller.py +++ b/cbpi/controller/actor_controller.py @@ -7,6 +7,7 @@ class ActorController(BasicController): def __init__(self, cbpi): super(ActorController, self).__init__(cbpi, Actor,"actor.json") self.update_key = "actorupdate" + self.sorting=True async def on(self, id, power=None): try: @@ -20,7 +21,7 @@ class ActorController(BasicController): if item.instance.state is False: await item.instance.on(power) #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) else: await self.set_power(id, power) @@ -34,7 +35,7 @@ class ActorController(BasicController): if item.instance.state is True: await item.instance.off() #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()) except Exception as e: logging.error("Failed to switch on Actor {} {}".format(id, e), True) @@ -44,7 +45,7 @@ class ActorController(BasicController): item = self.find_by_id(id) instance = item.get("instance") 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()) except Exception as e: logging.error("Failed to toggle Actor {} {}".format(id, e)) @@ -61,7 +62,7 @@ class ActorController(BasicController): item = self.find_by_id(id) item.power = round(power) #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()) except Exception as e: logging.error("Failed to update Actor {} {}".format(id, e)) diff --git a/cbpi/controller/basic_controller2.py b/cbpi/controller/basic_controller2.py index 990f54e..7ef8b4f 100644 --- a/cbpi/controller/basic_controller2.py +++ b/cbpi/controller/basic_controller2.py @@ -14,6 +14,7 @@ class BasicController: def __init__(self, cbpi, resource, file): self.resource = resource self.update_key = "" + self.sorting = False self.name = self.__class__.__name__ self.cbpi = cbpi self.cbpi.register(self) @@ -23,7 +24,7 @@ class BasicController: self.data = [] self.autostart = True #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) async def init(self): @@ -36,6 +37,7 @@ class BasicController: logging.info("{} Load ".format(self.name)) with open(self.path) as json_file: data = json.load(json_file) + data['data'].sort(key=lambda x: x.get('name').upper()) for i in data["data"]: self.data.append(self.create(i)) @@ -54,7 +56,7 @@ class BasicController: await self.push_udpate() 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))) for item in self.data: 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) await item.instance.__getattribute__(action)(**parameter) 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)) diff --git a/cbpi/controller/config_controller.py b/cbpi/controller/config_controller.py index 65d0298..4afaa0b 100644 --- a/cbpi/controller/config_controller.py +++ b/cbpi/controller/config_controller.py @@ -14,8 +14,8 @@ class ConfigController: self.logger = logging.getLogger(__name__) self.cbpi = cbpi self.cbpi.register(self) - self.path = os.path.join(".", 'config', "config.json") - self.path_static = os.path.join(".", 'config', "config.yaml") + self.path = cbpi.config_folder.get_file_path("config.json") + self.path_static = cbpi.config_folder.get_file_path("config.yaml") def get_state(self): diff --git a/cbpi/controller/dashboard_controller.py b/cbpi/controller/dashboard_controller.py index 73bd064..228839c 100644 --- a/cbpi/controller/dashboard_controller.py +++ b/cbpi/controller/dashboard_controller.py @@ -18,14 +18,14 @@ class DashboardController: self.logger = logging.getLogger(__name__) 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): pass async def get_content(self, dashboard_id): 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) with open(self.path) as json_file: data = json.load(json_file) @@ -35,21 +35,21 @@ class DashboardController: async def add_content(self, dashboard_id, 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: json.dump(data, outfile, indent=4, sort_keys=True) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Saved Successfully", type=NotificationType.SUCCESS) return {"status": "OK"} 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): os.remove(self.path) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Deleted Successfully", type=NotificationType.SUCCESS) 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")] return onlyfiles diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index d0a5009..8fa5de2 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -22,7 +22,7 @@ class FermentationController: self.update_key = "fermenterupdate" self.cbpi = cbpi 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.types = {} self.steptypes = {} @@ -35,18 +35,18 @@ class FermentationController: pass 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") 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) - 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() if (fermenterid == None): for fermenter in self.data: @@ -54,10 +54,7 @@ class FermentationController: for step in fermenter.steps: try: self.logger.info("Stop {}".format(step.name)) - try: - step.instance.shutdown = True - except: - pass + step.instance.shutdown = True await step.instance.stop() except Exception as e: self.logger.error(e) @@ -67,10 +64,7 @@ class FermentationController: for step in fermenter.steps: try: self.logger.info("Stop {}".format(step.name)) - try: - step.instance.shutdown = True - except: - pass + step.instance.shutdown = True await step.instance.stop() except Exception as e: self.logger.error(e) @@ -109,6 +103,7 @@ class FermentationController: return step def _done(self, step_instance, result, fermenter): + logging.info(result) step_instance.step["status"] = "D" self.save() if result == StepResult.NEXT: @@ -119,14 +114,17 @@ class FermentationController: id = data.get("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","") logictype = data.get("type") temp = data.get("target_temp") + pressure = data.get("target_pressure") brewname = data.get("brewname") description = data.get("description") 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", []))) self.push_update() return fermenter @@ -135,7 +133,7 @@ class FermentationController: 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): return list(map(lambda x: x.to_dict(), self.data)) @@ -202,13 +200,16 @@ class FermentationController: def _update(old_item: Fermenter, item: Fermenter): old_item.name = item.name old_item.sensor = item.sensor + old_item.pressure_sensor = item.pressure_sensor old_item.heater = item.heater old_item.cooler = item.cooler + old_item.valve = item.valve old_item.type = item.type old_item.brewname = item.brewname old_item.description = item.description old_item.props = item.props old_item.target_temp = item.target_temp + old_item.target_pressure = item.target_pressure return old_item 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: 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 ): item = self._find_by_id(id) self.data = list(filter(lambda item: item.id != id, self.data)) @@ -234,7 +246,7 @@ class FermentationController: self.push_update() 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: json.dump(data, file, indent=4, sort_keys=True) @@ -304,6 +316,8 @@ class FermentationController: item = self._find_by_id(id) # might require later check if step is active item.steps = [] + item.brewname = "" + self.push_update() self.save() self.push_update("fermenterstepupdate") @@ -323,6 +337,17 @@ class FermentationController: def _find_step_by_id(self, data, id): 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): self.logger.info("Start {}".format(id)) try: @@ -338,7 +363,9 @@ class FermentationController: endtime = step.endtime await step.instance.start() 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.push_update() self.push_update("fermenterstepupdate") @@ -364,8 +391,8 @@ class FermentationController: try: item = self._find_by_id(id) step = self._find_by_status(item.steps, StepState.ACTIVE) - logging.info(step) - logging.info(step.status) + #logging.info(step) + #logging.info(step.status) if step != None: logging.info("CALLING STOP STEP") try: @@ -396,7 +423,6 @@ class FermentationController: await item.instance.start() item.instance.running = True item.instance.task = asyncio.get_event_loop().create_task(item.instance._run()) - logging.info("{} started {}".format(item.name, id)) @@ -516,7 +542,7 @@ class FermentationController: async def savetobook(self, fermenterid): 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) try: brewname = fermenter.brewname @@ -549,11 +575,17 @@ class FermentationController: item["props"]["Sensor"] = fermenter.sensor 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: fermenter.brewname = name else: - fermenter.brewname = data['basic']['name'] + try: + fermenter.brewname = data['basic'].get("name") + except: + fermenter.brewname = "Fermentation" await self.update(fermenter) fermenter.steps=[] for item in data.get("steps"): @@ -569,4 +601,4 @@ class FermentationController: self.save() self.push_update("fermenterstepupdate") return step - \ No newline at end of file + diff --git a/cbpi/controller/fermenter_recipe_controller.py b/cbpi/controller/fermenter_recipe_controller.py index d0de8b5..0ab6aa5 100644 --- a/cbpi/controller/fermenter_recipe_controller.py +++ b/cbpi/controller/fermenter_recipe_controller.py @@ -29,56 +29,52 @@ class FermenterRecipeController: async def create(self, name): 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=[]) with open(path, "w") as file: yaml.dump(data, file) return id 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) with open(path, "w") as file: yaml.dump(data, file, indent=4, sort_keys=True) async def get_recipes(self): - path = os.path.join(".", 'config', "fermenterrecipes") - onlyfiles = [os.path.splitext(f)[0] for f in listdir(path) if isfile(join(path, f)) and f.endswith(".yaml")] + fermenter_recipe_ids = self.cbpi.config_folder.get_all_fermenter_recipes() result = [] - for filename in onlyfiles: - recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % filename) - with open(recipe_path) as file: + for recipe_id in fermenter_recipe_ids: + + with open(self.cbpi.config_folder.get_fermenter_recipe_by_id(recipe_id)) as file: data = yaml.load(file, Loader=yaml.FullLoader) dataset = data["basic"] - dataset["file"] = filename + dataset["file"] = recipe_id result.append(dataset) logging.info(result) return result - + async def get_by_name(self, name): - - recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % name) + recipe_path = self.cbpi.config_folder.get_fermenter_recipe_by_id(name) with open(recipe_path) as file: return yaml.load(file, Loader=yaml.FullLoader) - 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) - 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) with open(recipe_path) as file: data = yaml.load(file, Loader=yaml.FullLoader) await self.cbpi.fermenter.load_recipe(data, fermenterid, 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: data = yaml.load(file, Loader=yaml.FullLoader) data["basic"]["name"] = new_name diff --git a/cbpi/controller/notification_controller.py b/cbpi/controller/notification_controller.py index 0e9fbfe..5d2b4ef 100644 --- a/cbpi/controller/notification_controller.py +++ b/cbpi/controller/notification_controller.py @@ -22,7 +22,7 @@ class NotificationController: try: del self.listener[listener_id] 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): for id, method in self.listener.items(): @@ -60,5 +60,5 @@ class NotificationController: asyncio.create_task(action.method()) del self.callback_cache[notification_id] except Exception as e: - self.logger.error("Faild to call notificatoin callback") + self.logger.error("Failed to call notificatoin callback") \ No newline at end of file diff --git a/cbpi/controller/plugin_controller.py b/cbpi/controller/plugin_controller.py index 2d8410d..26081e6 100644 --- a/cbpi/controller/plugin_controller.py +++ b/cbpi/controller/plugin_controller.py @@ -208,7 +208,7 @@ class PluginController(): result.append({row: meta[row] for row in list(metadata(key))}) except Exception as e: - logger.error("FAILED to load plugin {} ".fromat(key)) + logger.error("FAILED to load plugin {} ".format(key)) logger.error(e) except Exception as e: diff --git a/cbpi/controller/recipe_controller.py b/cbpi/controller/recipe_controller.py index d668291..c3fbe51 100644 --- a/cbpi/controller/recipe_controller.py +++ b/cbpi/controller/recipe_controller.py @@ -29,26 +29,26 @@ class RecipeController: async def create(self, name): 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=[]) with open(path, "w") as file: yaml.dump(data, file) return id 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) with open(path, "w") as file: yaml.dump(data, file, indent=4, sort_keys=True) 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")] result = [] 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: data = yaml.load(file, Loader=yaml.FullLoader) dataset = data["basic"] @@ -58,25 +58,25 @@ class RecipeController: 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: return yaml.load(file, Loader=yaml.FullLoader) 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) 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: data = yaml.load(file, Loader=yaml.FullLoader) await self.cbpi.step.load_recipe(data) 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: data = yaml.load(file, Loader=yaml.FullLoader) data["basic"]["name"] = new_name diff --git a/cbpi/controller/sensor_controller.py b/cbpi/controller/sensor_controller.py index fad7152..6c70c87 100644 --- a/cbpi/controller/sensor_controller.py +++ b/cbpi/controller/sensor_controller.py @@ -6,13 +6,14 @@ class SensorController(BasicController): def __init__(self, cbpi): super(SensorController, self).__init__(cbpi, Sensor, "sensor.json") self.update_key = "sensorupdate" + self.sorting = True def create_dict(self, data): try: instance = data.get("instance") state =instance.get_state() except Exception as e: - logging.error("Faild to create sensor dict {} ".format(e)) + logging.error("Failed to create sensor dict {} ".format(e)) state = dict() 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: return self.find_by_id(id).instance.get_state() except Exception as e: - logging.error("Faild read sensor value {} {} ".format(id, e)) + logging.error("Failed read sensor value {} {} ".format(id, e)) return None \ No newline at end of file diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index 6632982..8850fd9 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -19,7 +19,7 @@ class StepController: def __init__(self, cbpi): self.cbpi = cbpi 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.basic_data = {} self.step = None @@ -199,8 +199,8 @@ class StepController: def get_types(self): result = {} for key, value in self.types.items(): - if "ferment" not in str(value.get("class")).lower(): - result[key] = dict(name=value.get("name"), properties=value.get("properties"), actions=value.get("actions")) + #if "ferment" not in str(value.get("class")).lower(): + result[key] = dict(name=value.get("name"), properties=value.get("properties"), actions=value.get("actions")) return result def get_state(self): @@ -230,7 +230,7 @@ class StepController: await self.save() async def shutdown(self, app=None): - logging.info("Mash Profile Shutdonw") + logging.info("Mash Profile Shutdown") for p in self.profile: instance = p.instance # Stopping all running task @@ -279,7 +279,7 @@ class StepController: await step.instance.start() step.status = StepState.ACTIVE 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): logging.info("SAVE Basic Data") @@ -293,7 +293,7 @@ class StepController: item = self.find_by_id(id) await item.instance.__getattribute__(action)(**parameter) 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): try: @@ -324,7 +324,7 @@ class StepController: async def savetobook(self): 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))) with open(path, "w") as file: yaml.dump(data, file) diff --git a/cbpi/controller/system_controller.py b/cbpi/controller/system_controller.py index 33bf050..8c211fc 100644 --- a/cbpi/controller/system_controller.py +++ b/cbpi/controller/system_controller.py @@ -38,7 +38,7 @@ class SystemController: async def backupConfig(self): 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) async def downloadlog(self, logtime): @@ -153,7 +153,7 @@ class SystemController: try: content = svg_file.read().decode('utf-8','replace') 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) f=open(self.path, "w") @@ -178,6 +178,8 @@ class SystemController: mempercent = 0 eth0IP = "N/A" wlan0IP = "N/A" + eth0speed = "N/A" + wlan0speed = "N/A" TEMP_UNIT=self.cbpi.config.get("TEMP_UNIT", "C") FAHRENHEIT = False if TEMP_UNIT == "C" else True @@ -225,12 +227,31 @@ class SystemController: if str(addr.family) == "AddressFamily.AF_INET": if 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: pass if system == "Windows": try: - ethernet = psutil.net_if_addrs() + ethernet = psutil.net_if_addrs() for nic, addrs in ethernet.items(): if nic == "Ethernet": for addr in addrs: @@ -242,6 +263,23 @@ class SystemController: if str(addr.family) == "AddressFamily.AF_INET": if 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: pass @@ -258,7 +296,9 @@ class SystemController: 'temp': temp, 'temp_unit': TEMP_UNIT, 'eth0': eth0IP, - 'wlan0': wlan0IP} + 'wlan0': wlan0IP, + 'eth0speed': eth0speed, + 'wlan0speed': wlan0speed} return systeminfo diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index c7db2bb..2d21de0 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -32,7 +32,7 @@ class UploadController: async def get_kbh_recipes(self): try: - path = os.path.join(".", 'config', "upload", "kbh.db") + path = self.cbpi.config_folder.get_upload_file("kbh.db") conn = sqlite3.connect(path) c = conn.cursor() c.execute('SELECT ID, Sudname, Status FROM Sud') @@ -47,7 +47,7 @@ class UploadController: async def get_xml_recipes(self): 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() result =[] counter = 1 @@ -61,7 +61,7 @@ class UploadController: async def get_json_recipes(self): try: - path = os.path.join(".", 'config', "upload", "mmum.json") + path = self.cbpi.config_folder.get_upload_file("mmum.json") e = json.load(open(path)) result =[] result.append({'value': str(1), 'label': e['Name']}) @@ -123,7 +123,7 @@ class UploadController: try: beer_xml = recipe_file.read().decode('utf-8','replace') 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.write(beer_xml) @@ -137,7 +137,7 @@ class UploadController: try: mmum_json = recipe_file.read().decode('utf-8','replace') 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.write(mmum_json) @@ -151,7 +151,7 @@ class UploadController: try: content = recipe_file.read() 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.write(content) @@ -168,7 +168,7 @@ class UploadController: config = self.get_config_values() if self.kettle is not None: # 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: 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) 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)) for idx in range(1,20): search_string = string.replace("%%",str(idx)) @@ -328,7 +328,7 @@ class UploadController: return i 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)) return float(e['Infusion_Einmaischtemperatur']) @@ -337,7 +337,7 @@ class UploadController: if self.kettle is not None: # 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: 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: # 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: 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) steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)}) 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)) for idx in range(1,self.findMax("Infusion_Rastzeit%%")): if self.cbpi.config.get("TEMP_UNIT", "C") == "C": diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index 45e54ac..c4dad9d 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -12,7 +12,7 @@ from cbpi.controller.notification_controller import NotificationController import logging from os import urandom import os -from cbpi import __version__ +from cbpi import __version__, __codename__ from aiohttp import web from aiohttp_auth import auth from aiohttp_session import session_middleware @@ -86,7 +86,7 @@ async def error_middleware(request, handler): class CraftBeerPi: - def __init__(self): + def __init__(self, configFolder): operationsystem= sys.platform 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.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") policy = auth.SessionTktAuthentication(urandom(32), 60, include_ip=True) @@ -288,6 +289,7 @@ class CraftBeerPi: self._setup_http_index() self.plugin.load_plugins() self.plugin.load_plugins_from_evn() + await self.fermenter.init() await self.sensor.init() await self.step.init() @@ -295,8 +297,8 @@ class CraftBeerPi: await self.kettle.init() await self.call_initializer(self.app) await self.dashboard.init() - await self.fermenter.init() - + + self._swagger_setup() level = logging.INFO diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index bc24110..e5dc9ac 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -46,7 +46,7 @@ class ConfigUpdate(CBPiExtension): influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None) mqttupdate = self.cbpi.config.get("MQTTUpdate", None) - + PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", None) if boil_temp is None: @@ -276,6 +276,15 @@ class ConfigUpdate(CBPiExtension): except: 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): cbpi.plugin.register("ConfigUpdate", ConfigUpdate) diff --git a/cbpi/extension/FermentationStep/__init__.py b/cbpi/extension/FermentationStep/__init__.py index 947be94..c863f64 100644 --- a/cbpi/extension/FermentationStep/__init__.py +++ b/cbpi/extension/FermentationStep/__init__.py @@ -81,8 +81,9 @@ class FermenterTargetTempStep(CBPiFermentationStep): 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 + if self.shutdown == False: + await self.next(self.fermenter.id) + return StepResult.DONE async def on_timer_update(self,timer, seconds): @@ -92,7 +93,8 @@ class FermenterTargetTempStep(CBPiFermentationStep): self.shutdown = False self.AutoMode = True if self.props.get("AutoMode","No") == "Yes" else False 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: await self.setAutoMode(True) 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): await self.cbpi.fermenter.toggle(self.fermenter.id) 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() except Exception as e: @@ -149,8 +151,9 @@ class FermenterTargetTempStep(CBPiFermentationStep): @parameters([Property.Number(label="TimerD", description="Timer Days", configurable=True), Property.Number(label="TimerH", description="Timer Hours", configurable=True), Property.Number(label="TimerM", description="Timer Minutes", configurable=True), - Property.Number(label="Temp", configurable=True), - Property.Sensor(label="Sensor"), + Property.Number(label="Temp", configurable=True, description="Step Temperature"), + 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")]) class FermenterStep(CBPiFermentationStep): @@ -183,7 +186,7 @@ class FermenterStep(CBPiFermentationStep): if self.AutoMode == True: await self.setAutoMode(False) self.cbpi.notify(self.name, 'Step finished', NotificationType.SUCCESS) - if self.shutdown != True: + if self.shutdown == False: await self.next(self.fermenter.id) return StepResult.DONE @@ -204,12 +207,14 @@ class FermenterStep(CBPiFermentationStep): self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False 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: await self.setAutoMode(True) await self.push_update() 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.is_running = False elif self.fermenter is not None: @@ -279,6 +284,121 @@ class FermenterStep(CBPiFermentationStep): 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): try: 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("FermenterTargetTempStep", FermenterTargetTempStep) - cbpi.plugin.register("FermenterStep", FermenterStep) \ No newline at end of file + cbpi.plugin.register("FermenterRampTempStep", FermenterRampTempStep) + cbpi.plugin.register("FermenterStep", FermenterStep) diff --git a/cbpi/extension/FermenterHysteresis/__init__.py b/cbpi/extension/FermenterHysteresis/__init__.py index 5be707c..0376d01 100644 --- a/cbpi/extension/FermenterHysteresis/__init__.py +++ b/cbpi/extension/FermenterHysteresis/__init__.py @@ -100,7 +100,7 @@ class FermenterHysteresis(CBPiFermenterLogic): except asyncio.CancelledError as e: pass except Exception as e: - logging.error("CustomLogic Error {}".format(e)) + logging.error("Fermenter Hysteresis Error {}".format(e)) finally: self.running = False if self.heater: @@ -109,6 +109,111 @@ class FermenterHysteresis(CBPiFermenterLogic): 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): @@ -119,7 +224,7 @@ def setup(cbpi): :param cbpi: the cbpi core :return: ''' - + cbpi.plugin.register("Fermenter Spunding Hysteresis", FermenterSpundingHysteresis) cbpi.plugin.register("Fermenter Hysteresis", FermenterHysteresis) cbpi.plugin.register("Fermenter Autostart", FermenterAutostart) diff --git a/cbpi/extension/dummysensor/__init__.py b/cbpi/extension/dummysensor/__init__.py index 34c9c25..c935df2 100644 --- a/cbpi/extension/dummysensor/__init__.py +++ b/cbpi/extension/dummysensor/__init__.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- import asyncio import random - -from cbpi.api import parameters, CBPiSensor - +import logging +from cbpi.api import * +from cbpi.api.base import CBPiBase +from cbpi.api.dataclasses import Kettle, Props, Fermenter @parameters([]) class CustomSensor(CBPiSensor): @@ -23,6 +24,44 @@ class CustomSensor(CBPiSensor): def get_state(self): 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): ''' @@ -33,3 +72,4 @@ def setup(cbpi): :return: ''' cbpi.plugin.register("CustomSensor", CustomSensor) + cbpi.plugin.register("DummyPressure", DummyPressure) diff --git a/cbpi/extension/mqtt_actor/mqtt_actor.py b/cbpi/extension/mqtt_actor/mqtt_actor.py index 5788c12..28dd198 100644 --- a/cbpi/extension/mqtt_actor/mqtt_actor.py +++ b/cbpi/extension/mqtt_actor/mqtt_actor.py @@ -22,6 +22,8 @@ class MQTTActor(CBPiActor): async def on_start(self): self.topic = self.props.get("Topic", None) self.power = 100 + await self.off() + self.state = False async def on(self, power=None): if power is not None: diff --git a/cbpi/http_endpoints/http_actor.py b/cbpi/http_endpoints/http_actor.py index 0572206..8a6ea3b 100644 --- a/cbpi/http_endpoints/http_actor.py +++ b/cbpi/http_endpoints/http_actor.py @@ -23,6 +23,32 @@ class ActorHttpEndpoints(): description: successful operation """ 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) diff --git a/cbpi/http_endpoints/http_dashboard.py b/cbpi/http_endpoints/http_dashboard.py index 2865191..53d5d8a 100644 --- a/cbpi/http_endpoints/http_dashboard.py +++ b/cbpi/http_endpoints/http_dashboard.py @@ -12,7 +12,7 @@ class DashBoardHttpEndpoints: def __init__(self, cbpi): self.cbpi = cbpi 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) diff --git a/cbpi/http_endpoints/http_fermentation.py b/cbpi/http_endpoints/http_fermentation.py index 81c4d89..2a97530 100644 --- a/cbpi/http_endpoints/http_fermentation.py +++ b/cbpi/http_endpoints/http_fermentation.py @@ -77,7 +77,9 @@ class FermentationHttpEndpoints(): description: successful operation """ 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) return web.json_response(data=response_data.to_dict()) @@ -115,7 +117,9 @@ class FermentationHttpEndpoints(): """ id = request.match_info['id'] 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()) @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")) 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) async def http_add_step(self, request): diff --git a/cbpi/http_endpoints/http_system.py b/cbpi/http_endpoints/http_system.py index 9830cc0..c21c4cf 100644 --- a/cbpi/http_endpoints/http_system.py +++ b/cbpi/http_endpoints/http_system.py @@ -5,7 +5,7 @@ from cbpi.job.aiohttp import get_scheduler_from_app import logging from cbpi.api import request_mapping from cbpi.utils import json_dumps -from cbpi import __version__ +from cbpi import __version__, __codename__ import pathlib import os from cbpi.controller.system_controller import SystemController @@ -36,7 +36,8 @@ class SystemHttpEndpoints: step=self.cbpi.step.get_state(), fermentersteps=self.cbpi.fermenter.get_fermenter_steps(), config=self.cbpi.config.get_state(), - version=__version__) + version=__version__, + codename=__codename__) , dumps=json_dumps) @request_mapping(path="/logs", auth_required=False) diff --git a/cbpi/websocket.py b/cbpi/websocket.py index fa6bc5c..970ccc4 100644 --- a/cbpi/websocket.py +++ b/cbpi/websocket.py @@ -28,11 +28,16 @@ class CBPiWebSocket: self.send(data) - def send(self, data): + def send(self, data, sorting=False): self.logger.debug("broadcast to ws clients. Data: %s" % data) for ws in self._clients: async def send_data(ws, data): 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) except Exception as e: self.logger.error("Error with client %s: %s" % (ws, str(e))) diff --git a/craftbeerpi.db b/craftbeerpi.db deleted file mode 100644 index 4df71f1..0000000 Binary files a/craftbeerpi.db and /dev/null differ diff --git a/release.py b/release.py index 4417727..60514b9 100644 --- a/release.py +++ b/release.py @@ -1,3 +1,4 @@ +import code import subprocess import click import re @@ -9,17 +10,23 @@ def main(): @click.command() @click.option('-m', prompt='Commit Message') def commit(m): - + + new_content = [] file = "./cbpi/__init__.py" with open(file) as reader: match = re.search('.*\"(.*)\"', reader.readline()) - major, minor, patch, build = match.group(1).split(".") - build = int(build) - build += 1 - new_version = "__version__ = \"{}.{}.{}.{}\"".format(major,minor,patch, build) + codename = reader.readline() + try: + major, minor, patch, build = match.group(1).split(".") + 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: - print("New Version {}.{}.{}.{}".format(major,minor,patch, build)) - file.write(new_version) + print("New Version {}.{}.{}".format(major,minor,patch)) + file.writelines("%s\n" % i for i in new_content) subprocess.run(["git", "add", "-A"]) subprocess.run(["git", "commit", "-m", "\"{}\"".format(m)]) diff --git a/requirements.txt b/requirements.txt index ca5da1d..088cdc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,6 @@ asyncio-mqtt psutil==5.9.0 zipp>=0.5 PyInquirer==1.0.3 -colorama==0.4.4 \ No newline at end of file +colorama==0.4.4 +pytest-aiohttp +coverage==6.3.1 diff --git a/run.py b/run.py index db3291c..940b73e 100644 --- a/run.py +++ b/run.py @@ -1,3 +1,3 @@ from cbpi.cli import main -main() \ No newline at end of file +main(auto_envvar_prefix='CBPI') \ No newline at end of file diff --git a/tests/cbpi-test-config/actor.json b/tests/cbpi-test-config/actor.json new file mode 100644 index 0000000..9e413ee --- /dev/null +++ b/tests/cbpi-test-config/actor.json @@ -0,0 +1,12 @@ +{ + "data": [ + { + "id": "3CUJte4bkxDMFCtLX8eqsX", + "name": "SomeActor", + "power": 100, + "props": {}, + "state": false, + "type": "DummyActor" + } + ] +} \ No newline at end of file diff --git a/tests/cbpi-test-config/chromium.desktop b/tests/cbpi-test-config/chromium.desktop new file mode 100644 index 0000000..a112515 --- /dev/null +++ b/tests/cbpi-test-config/chromium.desktop @@ -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" diff --git a/tests/cbpi-test-config/config.json b/tests/cbpi-test-config/config.json new file mode 100644 index 0000000..56f98c5 --- /dev/null +++ b/tests/cbpi-test-config/config.json @@ -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" + } +} diff --git a/tests/cbpi-test-config/config.yaml b/tests/cbpi-test-config/config.yaml new file mode 100644 index 0000000..f7a5abe --- /dev/null +++ b/tests/cbpi-test-config/config.yaml @@ -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 + diff --git a/tests/cbpi-test-config/craftbeerpi.service b/tests/cbpi-test-config/craftbeerpi.service new file mode 100644 index 0000000..cd02dce --- /dev/null +++ b/tests/cbpi-test-config/craftbeerpi.service @@ -0,0 +1,9 @@ +[Unit] +Description=Craftbeer Pi + +[Service] +WorkingDirectory=/home/pi +ExecStart=/usr/local/bin/cbpi start + +[Install] +WantedBy=multi-user.target diff --git a/tests/cbpi-test-config/craftbeerpiboot b/tests/cbpi-test-config/craftbeerpiboot new file mode 100644 index 0000000..9c4ca4b --- /dev/null +++ b/tests/cbpi-test-config/craftbeerpiboot @@ -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 \ No newline at end of file diff --git a/tests/cbpi-test-config/dashboard/cbpi_dashboard_1.json b/tests/cbpi-test-config/dashboard/cbpi_dashboard_1.json new file mode 100644 index 0000000..92079a0 --- /dev/null +++ b/tests/cbpi-test-config/dashboard/cbpi_dashboard_1.json @@ -0,0 +1,3 @@ +{ + "elements": [] +} \ No newline at end of file diff --git a/tests/cbpi-test-config/dashboard/widgets/.empty b/tests/cbpi-test-config/dashboard/widgets/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/cbpi-test-config/fermenter_data.json b/tests/cbpi-test-config/fermenter_data.json new file mode 100644 index 0000000..f788313 --- /dev/null +++ b/tests/cbpi-test-config/fermenter_data.json @@ -0,0 +1,5 @@ +{ + "data": [ + + ] +} \ No newline at end of file diff --git a/tests/cbpi-test-config/kettle.json b/tests/cbpi-test-config/kettle.json new file mode 100644 index 0000000..f788313 --- /dev/null +++ b/tests/cbpi-test-config/kettle.json @@ -0,0 +1,5 @@ +{ + "data": [ + + ] +} \ No newline at end of file diff --git a/tests/cbpi-test-config/plugin_list.txt b/tests/cbpi-test-config/plugin_list.txt new file mode 100644 index 0000000..da44c91 --- /dev/null +++ b/tests/cbpi-test-config/plugin_list.txt @@ -0,0 +1,3 @@ +cbpi4-ui: + installation_date: '2021-01-06 16:03:31' + version: '0.0.1' \ No newline at end of file diff --git a/tests/cbpi-test-config/recipes/.empty b/tests/cbpi-test-config/recipes/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/cbpi-test-config/sensor.json b/tests/cbpi-test-config/sensor.json new file mode 100644 index 0000000..6346c5f --- /dev/null +++ b/tests/cbpi-test-config/sensor.json @@ -0,0 +1,5 @@ +{ + "data": [ + + ] +} \ No newline at end of file diff --git a/tests/cbpi-test-config/splash.png b/tests/cbpi-test-config/splash.png new file mode 100644 index 0000000..68086b5 Binary files /dev/null and b/tests/cbpi-test-config/splash.png differ diff --git a/tests/cbpi-test-config/step_data.json b/tests/cbpi-test-config/step_data.json new file mode 100644 index 0000000..60a4e28 --- /dev/null +++ b/tests/cbpi-test-config/step_data.json @@ -0,0 +1,8 @@ +{ + "basic": { + "name": "" + }, + "steps": [ + + ] +} \ No newline at end of file diff --git a/tests/cbpi-test-config/upload/.empty b/tests/cbpi-test-config/upload/.empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/cbpi_config_fixture.py b/tests/cbpi_config_fixture.py new file mode 100644 index 0000000..b9824ba --- /dev/null +++ b/tests/cbpi_config_fixture.py @@ -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 diff --git a/tests/craftbeerpi.db b/tests/craftbeerpi.db deleted file mode 100644 index eec4e68..0000000 Binary files a/tests/craftbeerpi.db and /dev/null differ diff --git a/tests/test_actor.py b/tests/test_actor.py index d808fe4..69ecc82 100644 --- a/tests/test_actor.py +++ b/tests/test_actor.py @@ -1,66 +1,39 @@ import logging from unittest import mock -from unittest.mock import MagicMock, Mock -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop -from cbpi.craftbeerpi import CraftBeerPi +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') -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_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 - +class ActorTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_actor_switch(self): 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") - assert resp.status == 204 - i = await self.cbpi.actor.get_one(1) + resp = await self.client.request("POST", "/actor/3CUJte4bkxDMFCtLX8eqsX/on") + assert resp.status == 204, "switching actor on should work" + i = self.cbpi.actor.find_by_id("3CUJte4bkxDMFCtLX8eqsX") 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 - i = await self.cbpi.actor.get_one(1) - 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) + i = self.cbpi.actor.find_by_id("3CUJte4bkxDMFCtLX8eqsX") assert i.instance.state is False @unittest_run_loop async def test_crud(self): data = { - "name": "CustomActor", - "type": "CustomActor", - "config": { - "interval": 5 - } + "name": "SomeActor", + "power": 100, + "props": { + }, + "state": False, + "type": "DummyActor" } # Add new sensor diff --git a/tests/test_cli.py b/tests/test_cli.py index 1e41833..0a9f705 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,20 +1,16 @@ import logging 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') - class CLITest(unittest.TestCase): - def test_install(self): - assert add("cbpi4-ui-plugin") == True - assert add("cbpi4-ui-plugin") == False - assert remove("cbpi4-ui-plugin") == True - def test_list(self): - list_plugins() + cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config")) + cli.plugins_list() if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index 277b0c5..ffe7cd0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,54 +1,31 @@ import time -import aiosqlite -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop -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 +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase +class ConfigTestCase(CraftBeerPiTestCase): @unittest_run_loop 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 async def test_set_get(self): - value = str(time.time()) + value = 35 - await self.cbpi.config.set("CBPI_TEST_2", 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") + await self.cbpi.config.set("steps_cooldown_temp", value) + assert self.cbpi.config.get("steps_cooldown_temp", 1) == value @unittest_run_loop async def test_http_set(self): - value = str(time.time()) - key = "CBPI_TEST_3" - await self.cbpi.config.set(key, value) - assert self.cbpi.config.get(key, 1) == value + value = "Some New Brewery Name" + key = "BREWERY_NAME" - 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 self.cbpi.config.get(key, -1) == "1" + + assert self.cbpi.config.get(key, -1) == value @unittest_run_loop async def test_http_get(self): @@ -57,5 +34,5 @@ class ConfigTestCase(AioHTTPTestCase): @unittest_run_loop async def test_get_default(self): - value = self.cbpi.config.get("HELLO_WORLD", None) - assert value == None \ No newline at end of file + value = self.cbpi.config.get("HELLO_WORLD", "DefaultValue") + assert value == "DefaultValue" \ No newline at end of file diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 6066130..ea7bbee 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -1,18 +1,10 @@ -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 DashboardTestCase(AioHTTPTestCase): - - - - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +class DashboardTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_crud(self): @@ -28,56 +20,12 @@ class DashboardTestCase(AioHTTPTestCase): "config": {} } - resp = await self.client.get(path="/dashboard") + resp = await self.client.get(path="/dashboard/current") assert resp.status == 200 - # Add new dashboard - resp = await self.client.post(path="/dashboard/", json=data) - assert resp.status == 200 + dashboard_id = await resp.json() - m = await resp.json() - 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 + # Add dashboard content dashboard_content["dbid"] = dashboard_id resp = await self.client.post(path="/dashboard/%s/content" % dashboard_id, json=dashboard_content) - assert resp.status == 200 - 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) \ No newline at end of file + assert resp.status == 204 \ No newline at end of file diff --git a/tests/test_fermenter.py b/tests/test_fermenter.py deleted file mode 100644 index 96ccfc0..0000000 --- a/tests/test_fermenter.py +++ /dev/null @@ -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() diff --git a/tests/test_gpio.py b/tests/test_gpio.py index d5279c0..c5cba85 100644 --- a/tests/test_gpio.py +++ b/tests/test_gpio.py @@ -15,13 +15,6 @@ except Exception: patcher.start() import RPi.GPIO as GPIO - -class HelloWorld(object): - - def test(self, a): - return a - - class TestSwitch(unittest.TestCase): GPIO_NUM = 22 @@ -35,15 +28,4 @@ class TestSwitch(unittest.TestCase): @patch("RPi.GPIO.output") def test_switch_without_scheduler_starts_disabled(self, patched_output): GPIO.output(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) - + patched_output.assert_called_once_with(self.GPIO_NUM, GPIO.LOW) \ No newline at end of file diff --git a/tests/test_index.py b/tests/test_index.py index 06d8b20..c548113 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,14 +1,8 @@ -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop - -from cbpi.craftbeerpi import CraftBeerPi +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase -class IndexTestCase(AioHTTPTestCase): - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +class IndexTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_index(self): diff --git a/tests/test_kettle.py b/tests/test_kettle.py index 402af9e..b481d6a 100644 --- a/tests/test_kettle.py +++ b/tests/test_kettle.py @@ -1,52 +1,16 @@ -import asyncio -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop -from cbpi.craftbeerpi import CraftBeerPi +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase -class KettleTestCase(AioHTTPTestCase): - - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +class KettleTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_get(self): resp = await self.client.request("GET", "/kettle") assert resp.status == 200 - print(await resp.json()) - - @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 - + kettle = resp.json() + assert kettle != None @unittest_run_loop async def test_crud(self): @@ -69,21 +33,21 @@ class KettleTestCase(AioHTTPTestCase): m = await resp.json() - sensor_id = m["id"] + kettle_id = m["id"] 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 - m2 = await resp.json() - sensor_id = m2["id"] - - # Update Sensor - resp = await self.client.put(path="/kettle/%s" % sensor_id, json=m) - assert resp.status == 200 + # Set Kettle target temp + resp = await self.client.post(path="/kettle/%s/target_temp" % kettle_id, json={"temp":75}) + assert resp.status == 204 # # 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 + + diff --git a/tests/test_logger.py b/tests/test_logger.py index cc9e7f3..d626ab0 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,21 +1,16 @@ import asyncio 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 UtilsTestCase(AioHTTPTestCase): - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +class LoggerTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_log_data(self): + os.makedirs("./logs", exist_ok=True) log_name = "test" #clear all logs self.cbpi.log.clear_log(log_name) diff --git a/tests/test_notification_controller.py b/tests/test_notification_controller.py index b69bfc0..9de65ff 100644 --- a/tests/test_notification_controller.py +++ b/tests/test_notification_controller.py @@ -1,16 +1,8 @@ -import aiohttp -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop - -from cbpi.craftbeerpi import CraftBeerPi +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase -class NotificationTestCase(AioHTTPTestCase): - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app - +class NotificationTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_actor_switch(self): diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 7473469..250c4b9 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,14 +1,8 @@ -import asyncio -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop -from cbpi.craftbeerpi import CraftBeerPi +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase -class SensorTestCase(AioHTTPTestCase): - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +class SensorTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_crud(self): @@ -28,12 +22,11 @@ class SensorTestCase(AioHTTPTestCase): m = await resp.json() sensor_id = m["id"] - # Get sensor + # Get sensor value resp = await self.client.get(path="/sensor/%s"% sensor_id) assert resp.status == 200 m2 = await resp.json() - sensor_id = m2["id"] # Update Sensor resp = await self.client.put(path="/sensor/%s" % sensor_id, json=m) diff --git a/tests/test_step.py b/tests/test_step.py index e2ccb9d..a4ce794 100644 --- a/tests/test_step.py +++ b/tests/test_step.py @@ -1,23 +1,13 @@ import asyncio -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop -from cbpi.craftbeerpi import CraftBeerPi +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase - -class StepTestCase(AioHTTPTestCase): - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +class StepTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_get(self): - resp = await self.client.request("GET", "/step") - print(resp) - assert resp.status == 200 - - resp = await self.client.request("GET", "/step/types") + resp = await self.client.request("GET", "/step2") print(resp) assert resp.status == 200 @@ -31,26 +21,19 @@ class StepTestCase(AioHTTPTestCase): } # 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 m = await resp.json() print("Step", m) sensor_id = m["id"] - # Get sensor - resp = await self.client.get(path="/step/%s" % sensor_id) + # Update step + resp = await self.client.put(path="/step2/%s" % sensor_id, json=m) assert resp.status == 200 - m2 = await resp.json() - sensor_id = m2["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) + # # Delete step + resp = await self.client.delete(path="/step2/%s" % sensor_id) assert resp.status == 204 def create_wait_callback(self, topic): @@ -66,34 +49,4 @@ class StepTestCase(AioHTTPTestCase): done, pending = await asyncio.wait({future}) if future in done: - 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()) + pass \ No newline at end of file diff --git a/tests/test_step_ng.py b/tests/test_step_ng.py deleted file mode 100644 index 7bc8383..0000000 --- a/tests/test_step_ng.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/tests/test_system.py b/tests/test_system.py index dd4de41..3cbbc7b 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -1,19 +1,11 @@ -from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop - -from cbpi.craftbeerpi import CraftBeerPi +from aiohttp.test_utils import unittest_run_loop +from tests.cbpi_config_fixture import CraftBeerPiTestCase -class IndexTestCase(AioHTTPTestCase): - - async def get_application(self): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +class IndexTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_endpoints(self): - - # Test Index Page resp = await self.client.post(path="/system/restart") assert resp.status == 200 @@ -21,9 +13,3 @@ class IndexTestCase(AioHTTPTestCase): resp = await self.client.post(path="/system/shutdown") 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 - diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index fdf4ba8..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -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 - diff --git a/tests/test_ws.py b/tests/test_ws.py index c0ebce8..862cfaf 100644 --- a/tests/test_ws.py +++ b/tests/test_ws.py @@ -1,56 +1,46 @@ -import asyncio - 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): - self.cbpi = CraftBeerPi() - await self.cbpi.init_serivces() - return self.cbpi.app +# assert count_step_done == 4 +# @unittest_run_loop +# async def test_wrong_format(self): - @unittest_run_loop - async def test_brewing_process(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() - 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 - - 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() +# else: +# raise Exception()