Add configuration page

This commit is contained in:
Thomas Klaehn 2021-03-22 12:03:15 +01:00
parent 798b068674
commit bd7d19d88d
8 changed files with 436 additions and 31 deletions

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# Greenhouse
Greenhous control
## Installation
```shell
python setup.py install
```
## Usage
### Manual
Create wrapper script (e.g. wrapper.py):
```python
from greenhous import app
if __name__ == "__main__":
app.run()
```
Execute the wrapper script:
```shell
python3 wrapper.py
```
### gunicorn
```shell
gunicorn --bind 0.0.0.0:80 greenhouse:app
```

1
greenhouse.json Normal file
View File

@ -0,0 +1 @@
{"water": {"state": false, "times": [{"on_time": "7:00", "off_time": "7:30"}, {"on_time": "19:00", "off_time": "19:30"}]}, "heat": {"state": true, "on_temperature": "3", "off_temperature": "5"}}

View File

@ -1,25 +1,24 @@
from flask import Flask
from flask import render_template
from flask import redirect
from flask import url_for
from flask import make_response
from flask import request
import json import json
import datetime
from datetime import datetime, timedelta
from threading import Thread from threading import Thread
from time import sleep from time import sleep
from flask import Flask
from flask import render_template
from flask import make_response
from flask import request
from w1thermsensor import W1ThermSensor from w1thermsensor import W1ThermSensor
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
sensor = W1ThermSensor() sensor = W1ThermSensor()
water_pin = 26 #17/27/22 water_pin = 17 #17/27/22
heat_pin = 20 heat_pin = 26
heat_state = False heat_state = False
CONFIG_FILE = "/etc/greenhouse/greenhouse.json"
GPIO.setwarnings(False) GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BCM)
GPIO.setup(water_pin, GPIO.OUT) GPIO.setup(water_pin, GPIO.OUT)
@ -33,7 +32,7 @@ class Heat(Thread):
if GPIO.input(pin): if GPIO.input(pin):
self.__state = True self.__state = True
self.__run_condition = True self.__run_condition = True
self.__next_update = datetime.now() self.__next_update = datetime.datetime.now()
def on(self): def on(self):
self.__state = True self.__state = True
@ -44,16 +43,16 @@ class Heat(Thread):
GPIO.output(self.__pin, 0) GPIO.output(self.__pin, 0)
def run(self): def run(self):
self.__next_update = datetime.now() self.__next_update = datetime.datetime.now()
while self.__run_condition: while self.__run_condition:
now = datetime.now() now = datetime.datetime.now()
if now >= self.__next_update: if now >= self.__next_update:
if self.__state: if self.__state:
# Do a power cycle to prevent auto-poweroff # Do a power cycle to prevent auto-poweroff
GPIO.output(self.__pin, 0) GPIO.output(self.__pin, 0)
sleep(5) sleep(5)
GPIO.output(self.__pin, 1) GPIO.output(self.__pin, 1)
self.__next_update = now + timedelta(minutes=5) self.__next_update = now + datetime.timedelta(minutes=5)
sleep(1) sleep(1)
def stop(self): def stop(self):
@ -64,10 +63,67 @@ class Heat(Thread):
def state(self): def state(self):
return self.__state return self.__state
heat = Heat(heat_pin) heat = Heat(heat_pin)
heat.start() heat.start()
class GreenControl(Thread):
def __init__(self, cfg_file:str):
super(GreenControl, self).__init__()
self.cfg_file = cfg_file
self.config = {}
self.__run_condition = True
self.__water_state = False
self.__trigger_read_config = True
def reload_config(self):
self.__trigger_read_config = True
def run(self):
global sensor
global heat
next_update = datetime.datetime.now()
while self.__run_condition:
if self.__trigger_read_config:
self.__trigger_read_config = False
with open(self.cfg_file, "r") as f:
self.config = json.load(f)
now = datetime.datetime.now()
if now >= next_update:
if self.config['heat']['state']:
temperature = sensor.get_temperature()
if float(temperature) < float(self.config['heat']['on_temperature']) and not heat.state():
heat.on()
elif float(temperature) > float(self.config['heat']['off_temperature']) and heat.state():
heat.off()
elif heat.state():
# Do a power cycle to prevent auto-poweroff
heat.off()
sleep(5)
heat.on()
next_update = now + datetime.timedelta(minutes=5)
if self.config['water']['state']:
index = 0
if int(now.hour) >= 12:
index = 1
on_time_pattern = self.config['water']['times'][index]['on_time']
on_time_pattern = on_time_pattern.split(':')
on_time = now.replace(hour=int(on_time_pattern[0]), minute=int(on_time_pattern[1]), second=0, microsecond=0)
off_time_pattern = self.config['water']['times'][index]['off_time']
off_time_pattern = off_time_pattern.split(':')
off_time = now.replace(hour=int(off_time_pattern[0]), minute=int(off_time_pattern[1]), second=0, microsecond=0)
if now > on_time and now <= off_time and not self.__water_state:
GPIO.output(water_pin, 1)
self.__water_state = True
elif now > off_time and self.__water_state:
GPIO.output(water_pin, 0)
self.__water_state = False
sleep(1)
green_ctrl = GreenControl(CONFIG_FILE)
green_ctrl.start()
app = Flask(__name__) app = Flask(__name__)
@ -76,13 +132,36 @@ def index():
return render_template('index.html') return render_template('index.html')
@app.route('/config')
def config():
return render_template('config.html')
@app.route('/cfg', methods=['GET'])
def get_config():
res = {}
with open(CONFIG_FILE, "r") as f:
res = json.load(f)
return res
@app.route('/cfg', methods=['PATCH'])
def patch_config():
global green_ctrl
res = make_response("", 500)
cfg = json.loads(request.data)
with open(CONFIG_FILE, "w") as f:
json.dump(cfg, f)
res = make_response("", 204)
green_ctrl.reload_config()
return res
@app.route('/sample', methods=['GET']) @app.route('/sample', methods=['GET'])
def get_sample(): def get_sample():
global heat global heat
global sensor global sensor
try: try:
temperature = f"{float(sensor.get_temperature()):.1f}" temperature = f"{float(sensor.get_temperature()):.1f}"
except SensorNotReadyError: except Exception:
temperature = None temperature = None
water_state = False water_state = False
@ -91,7 +170,7 @@ def get_sample():
res = {} res = {}
res["id"] = str(1) res["id"] = str(1)
if(temperature): if temperature:
res["temperature"] = temperature res["temperature"] = temperature
res["water"] = water_state res["water"] = water_state
res["heat"] = heat.state() res["heat"] = heat.state()
@ -99,21 +178,22 @@ def get_sample():
@app.route('/sample', methods=['PATCH']) @app.route('/sample', methods=['PATCH'])
def patch_reroute(): def patch_sample():
global heat global heat
record = json.loads(request.data) record = json.loads(request.data)
if "water" in record: if "water" in record:
water_state = record["water"] if record["water"]:
if water_state:
GPIO.output(water_pin, 1) GPIO.output(water_pin, 1)
else: else:
GPIO.output(water_pin, 0) GPIO.output(water_pin, 0)
if "heat" in record: if "heat" in record:
heat_state = record["heat"] if record["heat"]:
if heat_state:
heat.on() heat.on()
else: else:
heat.off() heat.off()
res = make_response("", 204) res = make_response("", 204)
return res return res
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8000)

View File

@ -5,7 +5,7 @@ html, body {
background: #282929; background: #282929;
} }
h1 {text-align: center;} h1, h2, h3 {text-align: center;}
p {text-align: center;} p {text-align: center;}
div {text-align: center;} div {text-align: center;}
@ -15,6 +15,15 @@ div {text-align: center;}
text-align: center; text-align: center;
} }
input {
border: 1px #999999 solid;
background-color: #f6f6f6;
padding: 2px 4px 2px 4px;
margin: 0 0 0.5rem 0;
font-size: 1.0rem;
border-radius: 8px;
}
input[type="submit" i] { input[type="submit" i] {
color: #b6b6b6; color: #b6b6b6;
background-color: #282929; background-color: #282929;
@ -26,10 +35,39 @@ input[type="submit" i] {
font-size: 20px; font-size: 20px;
} }
input[type="text"] {
border-radius: 8px;
}
td { td {
padding: 8px; padding: 8px;
} }
.table_left { .table_left {
text-align: right; text-align: right;
} }
footer {
position: fixed;
/* left: 50%; */
bottom: 20px;
/* transform: translate(-50%, -50%); */
margin: 0 auto;
}
a {
outline: none;
color: #b6b6b6;
text-decoration-line: none;
}
select {
color: #b6b6b6;
background-color: #282929;
padding: 10px;
margin: 10px 0;
border-color: #b6b6b6;
border-width: 3px;
border-radius: 16px;
font-size: 20px;
}

View File

@ -0,0 +1,121 @@
var get_cfg = function (event) {
var config = JSON.parse(event)
if(config.heat) {
if(config.heat.state) {
document.getElementById("heat_state").value = "an"
document.getElementById('heat_config').style.visibility = 'visible';
} else {
document.getElementById("heat_state").value = "aus"
document.getElementById('heat_config').style.visibility = 'hidden';
}
if(config.heat.on_temperature) {
document.getElementById("switch_on_temperature").value = config.heat.on_temperature;
}
if(config.heat.off_temperature) {
document.getElementById("switch_off_temperature").value = config.heat.off_temperature;
}
}
if(config.water) {
if(config.water.state) {
document.getElementById("water_state").value = "an"
document.getElementById('water_times').style.visibility = 'visible';
} else {
document.getElementById("water_state").value = "aus"
document.getElementById('water_times').style.visibility = 'hidden';
}
if(config.water.times) {
document.getElementById("water_on_one").value = config.water.times[0].on_time;
document.getElementById("water_off_one").value = config.water.times[0].off_time;
document.getElementById("water_on_two").value = config.water.times[1].on_time;
document.getElementById("water_off_two").value = config.water.times[1].off_time;
}
}
}
var on_change_heat_state = function() {
if(document.getElementById("heat_state").value == "an") {
document.getElementById("heat_state").value = "aus"
document.getElementById('heat_config').style.visibility = 'hidden';
} else {
document.getElementById("heat_state").value = "an"
document.getElementById('heat_config').style.visibility = 'visible';
}
}
var on_change_water_state = function() {
if(document.getElementById("water_state").value == "an") {
document.getElementById("water_state").value = "aus";
document.getElementById('water_times').style.visibility = 'hidden';
} else {
document.getElementById("water_state").value = "an";
document.getElementById('water_times').style.visibility = 'visible';
}
}
var on_push_config = function() {
var water_state = false;
if(document.getElementById("water_state").value == 'an') {
water_state = true;
}
var on_time_one = document.getElementById("water_on_one").value;
var off_time_one = document.getElementById("water_off_one").value;
var on_time_two = document.getElementById("water_on_two").value;
var off_time_two = document.getElementById("water_off_two").value;
var heat_state = false;
if(document.getElementById("heat_state").value == 'an') {
heat_state = true;
}
var on_tmp = document.getElementById("switch_on_temperature").value;
var off_tmp = document.getElementById("switch_off_temperature").value;
var json_str = JSON.stringify(
{
"water":
{
"state":water_state,
"times":
[
{
"on_time":on_time_one,
"off_time":off_time_one
},
{
"on_time":on_time_two,
"off_time":off_time_two
}
]
},
"heat":
{
"state":heat_state,
"on_temperature":on_tmp,
"off_temperature":off_tmp
}
}
);
patch_cfg(json_str);
}
var patch_http = new XMLHttpRequest();
var patch_cfg = function(config) {
patch_http.abort();
patch_http.open("PATCH", "/cfg");
patch_http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
patch_http.send(config);
}
var http = new XMLHttpRequest();
http.onreadystatechange = function () {
if (http.readyState === 4) {
var status = http.status;
if (status === 0 || (status >= 200 && status < 400)) {
// The request has been completed successfully
get_cfg(http.responseText);
}
} else {
// request error
}
}
http.open("GET", "/cfg");
http.send();

View File

@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<title>Gewächshaus</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/css/style.css" rel="stylesheet">
<script src="/static/scripts/config.js"></script>
</head>
<body>
<h1>Gewächshaus</h1>
<h2>Configuration</h2>
<hr>
<h3>Zeitgesteuerte Heizung</h3>
<div class="input">
<input id="heat_state" type="submit" value="aus" onclick="on_change_heat_state()"></input>
</div>
<div id="heat_config" style="visibility:hidden";>
<div>Die Einschalttemperatur muss unterhalb der Ausschalttemperatur liegen.</div>
<table class="center">
<tr>
<td>Einschalttemperatur </td>
<td>
<select id="switch_on_temperature">
<option value=1>1 °C</option>
<option value=2>2 °C</option>
<option value=3>3 °C</option>
<option value=4>4 °C</option>
<option value=5>5 °C</option>
<option value=6>6 °C</option>
<option value=7>7 °C</option>
<option value=8>8 °C</option>
<option value=9>9 °C</option>
<option value=10>10 °C</option>
</select>
</td>
</tr>
<tr>
<td>Ausschalttemperatur </td>
<td>
<select id="switch_off_temperature">
<option value=1>1 °C</option>
<option value=2>2 °C</option>
<option value=3>3 °C</option>
<option value=4>4 °C</option>
<option value=5>5 °C</option>
<option value=6>6 °C</option>
<option value=7>7 °C</option>
<option value=8>8 °C</option>
<option value=9>9 °C</option>
<option value=10>10 °C</option>
</select>
</td>
</tr>
</table>
</div>
<hr>
<h3>Zeitgesteuerte Bewässerung</h3>
<div class="input">
<input id="water_state" type="submit" value="aus" onclick="on_change_water_state()"></input>
</div>
<div id="water_times" style="visibility:hidden;">
<table class="center">
<tr>
<td>Vormittag</td>
<td>
<table>
<tr>
<td>Einschaltzeit</td>
<td class="input">
<input type="text" id="water_on_one" style="width: 70px;"></input>
</td>
</tr>
<tr>
<td>Ausschaltzeit</td>
<td class="input">
<input type="text" id="water_off_one" style="width: 70px;"></input>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>Nachmittag</td>
<td>
<table>
<tr>
<td>Einschaltzeit</td>
<td class="input">
<input type="text" id="water_on_two" style="width: 70px;"></input>
</td>
</tr>
<tr>
<td>Ausschaltzeit</td>
<td class="input">
<input type="text" id="water_off_two" style="width: 70px;"></input>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<hr>
<div class="input">
<input type="submit" value="Configuration senden" onclick="on_push_config()"></input>
</div>
</body>
<footer>
<nav>
<a href="/">Home</a>&nbsp|&nbsp</a>
<a href="/config">Configuration</a>
</nav>
</footer>
</html>

View File

@ -12,8 +12,8 @@
<table class="center"> <table class="center">
<tr> <tr>
<td>Temperatur </td> <td><h2>Temperatur </h2></td>
<td id="temperature_value"></td> <td><h2 id="temperature_value"></h2></td>
</tr> </tr>
</table> </table>
<table class="center"> <table class="center">
@ -33,4 +33,10 @@
</tr> </tr>
</table> </table>
</body> </body>
<footer>
<nav>
<a href="/">Home</a>&nbsp|&nbsp</a>
<a href="/config">Configuration</a>
</nav>
</footer>
</html> </html>

View File

@ -1,7 +1,6 @@
import shutil
import sys import sys
import os import os
import shutil
import stat
from setuptools import setup from setuptools import setup
NAME = 'Greenhouse' NAME = 'Greenhouse'
@ -10,7 +9,16 @@ AUTHOR = 'Thomas Klaehn'
EMAIL = 'tkl@blackfinn.de' EMAIL = 'tkl@blackfinn.de'
PACKAGES = ['greenhouse'] PACKAGES = ['greenhouse']
REQUIRES = ['Flask', 'w1thermsensor', 'RPi.GPIO'] REQUIRES = ['Flask', 'w1thermsensor', 'RPi.GPIO']
CONFIG_FOLDER = '/etc/greenhouse'
CONFIG_FILE = 'greenhouse.json'
setup(name=NAME, version=VERSION, long_description=__doc__, author=AUTHOR, author_email=EMAIL, setup(name=NAME, version=VERSION, long_description=__doc__, author=AUTHOR, author_email=EMAIL,
packages=PACKAGES, include_package_data=True, zip_safe=False, install_requires=REQUIRES) packages=PACKAGES, include_package_data=True, zip_safe=False, install_requires=REQUIRES)
if sys.argv[1] == 'install':
try:
os.makedirs(CONFIG_FOLDER)
shutil.copyfile(CONFIG_FILE, os.path.join(CONFIG_FOLDER, CONFIG_FILE))
except FileExistsError:
#FIXME: handle overwriting the config file
pass