diff --git a/.gitignore b/.gitignore index b6e4761..f01a1e5 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,12 @@ dmypy.json # Pyre type checker .pyre/ + +# Log files of RasperryPie +*.log + +# File containing secrets (credentials) and config +*.cfg + + +AwningV2.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4529f72 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "cSpell.enabled": false, + "sqltools.connections": [ + { + "previewLimit": 50, + "driver": "SQLite", + "name": "database", + "database": "${workspaceFolder:RaspberryPie}/RaspberryPie/database.db" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 26d9696..d692512 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # RaspberryPie A python application for Raspberry Pi controlling GPIO via gmail and many more features. + +## Features +With this application you can control GPIO pins via Email, view a live image taken via the Raspi-Cam and temperature measurements in a Dropbox. +But wherefore do you need it? For examle if you have an awning, which shouldn't get wet thus you want to control from far away... And many more, this is just what I use it for. + +## Installation +This section will be following. \ No newline at end of file diff --git a/RaspberryPie/config.py b/RaspberryPie/config.py new file mode 100644 index 0000000..c6e51cc --- /dev/null +++ b/RaspberryPie/config.py @@ -0,0 +1,4 @@ +import configparser + +config = configparser.RawConfigParser() +config.read("config.cfg") # edit this file for your specific configuration \ No newline at end of file diff --git a/RaspberryPie/dataHandling.py b/RaspberryPie/dataHandling.py new file mode 100644 index 0000000..d6c60c6 --- /dev/null +++ b/RaspberryPie/dataHandling.py @@ -0,0 +1,104 @@ +#general imports +import datetime +import sqlite3 +import base64 + +# imports from Project +import RaspberryPie.config as config + +captScribe = None + +def _set_captScribe(_captScribe): + global captScribe + captScribe = _captScribe + +class BGenSecretary: + def __init__(self, database_path, image_path): + self.database = database_path + self.image_path = image_path + + + def _record(self, data: dict): + connection = None + try: + connection = sqlite3.connect(self.database) + cursor = connection.cursor() + for typ in data: + time = datetime.datetime.now() + data[typ]["time_string"] = str(time) + data[typ]["unix_time"] = int(time.timestamp()) + command = "INSERT INTO {} ({}) VALUES ({})".format(typ, str([i for i in data[typ]])[1:-1], str([data[typ][i] for i in data[typ]])[1:-1]) + cursor.execute(command) + captScribe.info("Inserted data into table '{}' of database".format(typ), "BGenSecretary._record") + connection.commit() + + with open(config.config["FileLocations"]["lastChangeFile"], 'w') as f: + f.write(str(int(datetime.datetime.now().timestamp()))) + except sqlite3.Error as e: + captScribe.error(str(e), "BGenSecretary._record") + finally: + if connection is sqlite3.Connection: + connection.close() + + def recordPicture(self, picture: bytes): + with open("{}/{}.jpg".format(self.image_path, str(datetime.datetime.now())), "w") as f: + f.write(picture) + + self._record({"picture": {"picture": base64.encodebytes(picture).decode('ascii').replace("\n", "")}}) + + def recordAir(self, data): + self._record({"air": {"temperature": data[0], "humidity": data[1]}}) + + def _fetchSingle(self, table, time: datetime.datetime): + unix_time = int(time.timestamp()) + command = "SELECT * FROM {} ORDER BY ABS(unix_time - {}) ASC LIMIT 1".format(table, unix_time) + connection, entry = None + try: + connection = sqlite3.connect(self.database) + cursor = connection.cursor() + cursor.execute(command) + entry = cursor.fetchone() + captScribe.info("Fetched entry for {} from {}".format(str(unix_time), table), "BGenSecretary._fetchSingle") + except sqlite3.Error as e: + captScribe.error(str(e), "BGenSecretary._fetchSingle") + finally: + if connection is sqlite3.Connection: + connection.close() + return tuple(entry) if entry is sqlite3.Row else None + + def _fetchMultiple(self, table, end:datetime.datetime, start: datetime.datetime): + unix_time_end = int(end.timestamp()) + unix_time_start = int(start.timestamp()) + command = "SELECT * FROM {} WHERE unix_time BETWEEN {} AND {}".format(unix_time_start, unix_time_end) + connection, entries = None + try: + connection = sqlite3.connect(self.database) + cursor = connection.cursor() + cursor.execute(command) + entries = cursor.fetchall() + captScribe.info("Fetched entries for {} from {}".format(str((unix_time_start, unix_time_end)), table), "BGenSecretary._fetchSingle") + except sqlite3.Error as e: + captScribe.error(str(e), "BGenSecretary._fetchSingle") + finally: + if connection is sqlite3.Connection: + connection.close() + return [tuple(entry) for entry in entries] if entries is not None else None + + def fetchAir(self, time=datetime.datetime.now(), start_time=None): # time is either one point or the end point if start is given + if not start_time: + entry = self._fetchSingle("air", time) + return {"unix_time": entry[0], "time_string": entry[1], "temperature": entry[2], "humidity": entry[3]} + else: + entries = self._fetchMultiple("air", time, start_time) + return {"unix_time": [entry[0] for entry in entries], "time_string": [entry[1] for entry in entries], "temperature": [entry[2] for entry in entries], "humidity": [entry[3] for entry in entries]} + + def fetchPicture(self, time=datetime.datetime.now(), start_time=None): + if not start_time: + entry = self._fetchSingle("picture", time) + return {"unix_time": entry[0], "time_string": entry[1], "picture": entry[2]} + else: + entries = self._fetchMultiple("picture", time, start_time) + return {"unix_time": [entry[0] for entry in entries], "time_string": [entry[1] for entry in entries], "picture": [entry[2] for entry in entries]} + + +bGenSecretary = BGenSecretary(config.config["File Locations"]["database_file"], config.config["File Locations"],["image_path"]) \ No newline at end of file diff --git a/RaspberryPie/database.db b/RaspberryPie/database.db new file mode 100644 index 0000000..c964cd1 Binary files /dev/null and b/RaspberryPie/database.db differ diff --git a/RaspberryPie/flaskAddons.py b/RaspberryPie/flaskAddons.py new file mode 100644 index 0000000..31c4f38 --- /dev/null +++ b/RaspberryPie/flaskAddons.py @@ -0,0 +1,23 @@ +import datetime +import RaspberryPie.config as config +import RaspberryPie.dataHandling as dataHandling + +def time(): return "" + +def weatherUrls(): return [config.config["WeatherUrls"][i] for i in config.config["WeatherUrls"]] + +def getLatestAirData(): return dataHandling.bGenSecretary.fetchAir() + +def getLatestPicture(): return dataHandling.bGenSecretary.fetchAir() + +def getDataRangeAir(start: int, end: int): return dataHandling.bGenSecretary.fetchAir(datetime.datetime.utcfromtimestamp(end), datetime.datetime.utcfromtimestamp(start)) + +def getDataRangePictures(start: int, end: int): return dataHandling.bGenSecretary.fetchPicture(datetime.datetime.utcfromtimestamp(end), datetime.datetime.utcfromtimestamp(start)) + +def getSingleAirData(time: int): return dataHandling.bGenSecretary.fetchAir(datetime.datetime.utcfromtimestamp(time)) + +def getSinglePicture(time: int): return dataHandling.bGenSecretary.fetchPicture(datetime.datetime.utcfromtimestamp(time)) + +def lastChange(): + with open(config.config["FileLocations"]["lastChangeFile"], 'r') as f: + return int(f.read()) \ No newline at end of file diff --git a/RaspberryPie/flask_backend.py b/RaspberryPie/flask_backend.py new file mode 100644 index 0000000..c86dcd2 --- /dev/null +++ b/RaspberryPie/flask_backend.py @@ -0,0 +1,61 @@ +from flask import Flask, render_template, make_response +import json +import signal + +import RaspberryPie.flaskAddons as fa + +app = Flask(__name__) +@app.route("/") +def index(): + latestAirData = fa.getLatestAirData() + latestPicture = fa.getLatestPicture() + return render_template("index.html", + WeatherTime = latestAirData["time_string"], + temp = latestAirData["temperature"], + humi = latestAirData["humidity"], + weather_urls=fa.weatherUrls(), + PictureTime = latestPicture["time_string"], + picture = latestPicture["picture"]) + + +@app.route("/data///") +def airData(start, end): + data = fa.getDataRangeAir(int(start), int(end)) + return json.dumps(data) + +@app.route("/picture/