diff --git a/api/insert_data.py b/api/insert_data.py
index 053217a..aef43ef 100644
--- a/api/insert_data.py
+++ b/api/insert_data.py
@@ -1,7 +1,10 @@
-from flask.globals import request
-from database import Database
+import datetime
+
from flask import request
+
import config
+import notifications
+from database import Database
def insert_data():
@@ -11,6 +14,21 @@ def insert_data():
print("INSERT >> Received valid data: ", r_data)
database = Database()
+
+ # TODO: Implement proper check system
+ # Check for weight differences > 500g
+ try:
+ current = database.get_data(datetime.date.strftime(datetime.date.today()-datetime.timedelta(days=5), "%Y-%m-%d"),
+ datetime.date.strftime(datetime.date.today(), "%Y-%m-%d"))[-1]
+ if current["weight"] - float(r_data["w"]) > 0.5:
+ notifications.Feed().push_notification("warning",
+ "Gewichtsabfall!",
+ "Das Gewicht ist bei der aktuellen Messung um %skg abgefallen!"
+ % str(round(float(r_data["w"]) - current["weight"], 2)))
+ except Exception as e:
+ print("Error while performing data checks!\n"
+ "ignoring to still have data inserted\n%s" % e)
+
database.insert_data(r_data["t"], r_data["w"], r_data["h"])
return "data inserted"
diff --git a/app.py b/app.py
index 0b95eb6..e340889 100644
--- a/app.py
+++ b/app.py
@@ -1,10 +1,17 @@
-import config
import os
+import sys
+import threading
+
+from flask import Flask
+
+import config
from blueprints.api import api
+from blueprints.rss import rss
from blueprints.views import views
-from flask import Flask
from database import Database
+from notifications import Feed
from utils.jsonencoder import CustomJSONEncoder
+from telegram import bot as telegram_bot
print("waiting until db is ready")
os.popen(f"/bin/bash ./docker/wait-for-it.sh {config.MySql.host}:{str(config.MySql.port)}").read()
@@ -43,6 +50,9 @@
# Initialize all routes of the REST API
app.register_blueprint(api, url_prefix='/api')
+# Initialize all routes for the RSS feeds
+app.register_blueprint(rss, url_prefix='/rss')
+
# Append headers
@app.after_request
def add_header(r):
@@ -60,4 +70,17 @@ def add_header(r):
# Start the app
if __name__ == "__main__":
- app.run(host='0.0.0.0', port=config.web_port)
+ if not config.telegram_bot_token == "":
+ telegram_bot_thread = threading.Thread(target=telegram_bot.infinity_polling)
+ telegram_bot_thread.daemon = True
+ telegram_bot_thread.start()
+ else:
+ print(">>> Not starting telegram bot because there is no token")
+
+ try:
+ Feed().push_notification("admin", "Beelogger startup event", "Beelogger has been started and is now running...")
+ app.run(host='0.0.0.0', port=config.web_port)
+ except (KeyboardInterrupt, SystemExit):
+ print(">>> Stopping BeeLogger...")
+ database.connection_pool.close()
+ sys.exit()
diff --git a/backup.py b/backup.py
index 4a15421..5568596 100644
--- a/backup.py
+++ b/backup.py
@@ -1,52 +1,61 @@
import os
import time
-from shutil import copyfile, copytree, make_archive
+from shutil import copyfile, make_archive
+import notifications
from config import FileBackup, MySql
-if not os.path.isfile("backup.py"):
- print("You need to start this script from the directory it's contained in. Please cd into that folder.")
- exit()
-print("checking backup directory")
+def run_backup():
+ if not os.path.isfile("backup.py"):
+ print("You need to start this script from the directory it's contained in. Please cd into that folder.")
+ exit()
-if not os.path.exists("backup/"):
- print("create backup directory")
- os.mkdir("backup/")
+ print("checking backup directory")
-print("parsing backup name")
-dir_name = time.asctime()
-dest = "backup/" + dir_name + "/"
-dest = dest.replace(" ", "-")
+ if not os.path.exists("backup/"):
+ print("create backup directory")
+ os.mkdir("backup/")
-if os.path.exists(dest):
- os.removedirs(dest)
+ print("parsing backup name")
+ dir_name = time.asctime()
+ dest = "backup/" + dir_name + "/"
+ dest = dest.replace(" ", "-")
-os.mkdir(dest)
+ if os.path.exists(dest):
+ os.removedirs(dest)
-print("downloading MySql database")
-os.popen("mysqldump -h %s -u %s -p%s %s > %sdb_backup.sql" % (MySql.host, MySql.user, MySql.password, MySql.db, dest)).readlines()
+ os.mkdir(dest)
-try:
- print("copying files")
- copyfile("logs/insert.log", dest + "insert.log")
- # copytree("stats", dest + "stats/")
-except FileNotFoundError:
- print("no insert.log file, ignoring")
+ print("downloading MySql database")
+ os.popen("mysqldump -h %s -u %s -p%s %s > %sdb_backup.sql" % (MySql.host, MySql.user, MySql.password, MySql.db, dest)).readlines()
+
+ try:
+ print("copying files")
+ copyfile("logs/insert.log", dest + "insert.log")
+ # copytree("stats", dest + "stats/")
+ except FileNotFoundError:
+ print("no insert.log file, ignoring")
+
+ print("packing files")
+ make_archive(dest, "zip", dest)
-print("packing files")
-make_archive(dest, "zip", dest)
+ print("cleaning up")
+ os.popen("rm -r " + dest).readlines()
-print("cleaning up")
-os.popen("rm -r " + dest).readlines()
+ print("saving on remote")
+ if FileBackup.key != "":
+ cmd = f"scp -o StrictHostKeyChecking=no -i 'secrets/{FileBackup.key}' -P {FileBackup.port} '{dest[:-1]}.zip' '{FileBackup.user}@{FileBackup.host}:{FileBackup.directory}'"
+ else:
+ cmd = f"sshpass -p {FileBackup.password} scp -o StrictHostKeyChecking=no -P {FileBackup.port} '{dest[:-1]}.zip' '{FileBackup.user}@{FileBackup.host}:{FileBackup.directory}'"
-print("saving on remote")
-if FileBackup.key != "":
- cmd = f"scp -o StrictHostKeyChecking=no -i 'secrets/{FileBackup.key}' -P {FileBackup.port} '{dest[:-1]}.zip' '{FileBackup.user}@{FileBackup.host}:{FileBackup.directory}'"
-else:
- cmd = f"sshpass -p {FileBackup.password} scp -o StrictHostKeyChecking=no -P {FileBackup.port} '{dest[:-1]}.zip' '{FileBackup.user}@{FileBackup.host}:{FileBackup.directory}'"
+ # cmd = "sshpass -p '%s' scp -P %s '%s.zip' '%s@%s:%s'" % (FileBackup.password, FileBackup.port, dest[:-1], FileBackup.user, FileBackup.host, FileBackup.directory)
-# cmd = "sshpass -p '%s' scp -P %s '%s.zip' '%s@%s:%s'" % (FileBackup.password, FileBackup.port, dest[:-1], FileBackup.user, FileBackup.host, FileBackup.directory)
+ print(cmd)
+ print(os.popen(cmd).read())
-print(cmd)
-print(os.popen(cmd).read())
+
+try:
+ run_backup()
+except Exception as e:
+ notifications.Feed().push_notification("admin", "Backup Fehler", "Beim Backupvorgang ist es zu einem Fehler gekommen!\n" + e)
diff --git a/blueprints/rss.py b/blueprints/rss.py
new file mode 100644
index 0000000..d9133a1
--- /dev/null
+++ b/blueprints/rss.py
@@ -0,0 +1,47 @@
+from flask import Blueprint, Response, request, jsonify
+from flask.templating import render_template
+import notifications
+
+Notifications = notifications.Feed()
+
+rss = Blueprint("rss", __name__)
+
+@rss.route("/feeds/", methods=["GET"])
+def show_feeds():
+ return render_template('feeds.html')
+
+@rss.route("//", methods=["GET"])
+def show_feed(feed):
+ # Get the HTTP request's GET params
+ args = dict(request.args)
+
+ # Return pretty, HTML-based version of the feed if ?pretty is passed
+ if "pretty" in args.keys():
+ feed_data = Notifications.get_feed(feed, rss_format=False)
+ return render_template("feed.html", feed_name=feed, records=feed_data)
+ # Return feed as valid JSON if ?json is passed
+ elif "json" in args.keys():
+ feed_data = jsonify(Notifications.get_feed(feed, rss_format=False))
+ return feed_data
+ # Return feed as valid XML (for instance for RSS readers)
+ else:
+ feed_data = Notifications.get_feed(feed, rss_format=True)
+ return Response(feed_data, mimetype="text/xml")
+
+@rss.route("///", methods=["GET"])
+def show_article(feed, feed_id):
+ # Get the HTTP request's GET params
+ args = dict(request.args)
+
+ feed_data = Notifications.get_feed(feed, rss_format=False)
+ article = [x for x in feed_data if str(x["id"]) == feed_id][0]
+
+ # Return just the text if ?raw is passed
+ if "raw" in args.keys():
+ return article["text"]
+ # Return feed as valid JSON if ?json is passed
+ elif "json" in args.keys():
+ return article
+ # Return pretty, HTML-based version of the feed
+ else:
+ return render_template("feed-article.html", feed_name=feed, article=article)
diff --git a/database.py b/database.py
index 980d1b3..bfb2390 100644
--- a/database.py
+++ b/database.py
@@ -1,20 +1,27 @@
-import pymysql
+import ast
+import json
+
+import mysql.connector.cursor
+import mysql.connector.errors
+from dbutils.pooled_db import PooledDB
import config
import time
class Database:
def __init__(self):
- self.mysql_args = {
- "host": config.MySql.host,
- "port": config.MySql.port,
- "user": config.MySql.user,
- "password": config.MySql.password,
- "db": config.MySql.db,
- "charset": "utf8mb4",
- "cursorclass": pymysql.cursors.DictCursor
- }
-
- def get_sql_from_file(self):
+ self.connection_pool = PooledDB(mysql.connector, 5,
+ host=config.MySql.host,
+ port=config.MySql.port,
+ user=config.MySql.user,
+ password=config.MySql.password,
+ db=config.MySql.db,
+ buffered=True
+ )
+
+ self.connection_pool.connection().cursor().execute("SET NAMES UTF8")
+
+ @staticmethod
+ def get_sql_from_file():
"""
Reads and parses SQL queries from provided .sql file.
"""
@@ -37,88 +44,235 @@ def prepare_database(self) -> None:
Utilizes SQL from 'database.sql' to create all needed
tables automatically.
"""
- connection = pymysql.connect(
- host=self.mysql_args["host"],
- port=self.mysql_args["port"],
- user=self.mysql_args["user"],
- password=self.mysql_args["password"],
- charset="utf8mb4"
+ connection = mysql.connector.connect(
+ host=config.MySql.host,
+ port=config.MySql.port,
+ user=config.MySql.user,
+ password=config.MySql.password,
+ buffered=True
)
with connection.cursor() as cursor:
try:
- cursor.execute(f"CREATE DATABASE IF NOT EXISTS {self.mysql_args['db']}")
- except pymysql.err.Error as e:
+ cursor.execute(f"CREATE DATABASE IF NOT EXISTS {config.MySql.db}")
+ except mysql.connector.Error as e:
raise e
connection.commit()
connection.close()
- connection = pymysql.connect(**self.mysql_args)
- cursor = connection.cursor()
-
- # SQL queries as a list
- queries = self.get_sql_from_file()
- for query in queries:
- cursor.execute(query)
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ # SQL queries as a list
+ queries = self.get_sql_from_file()
+ for query in queries:
+ cursor.execute(query)
- # Commit changes and close connection
- connection.commit()
- connection.close()
- return
-
+ # Commit changes and close connection
+ con.commit()
+ con.close()
+ return
def get_data(self, from_date, to_date):
- connection = pymysql.connect(**self.mysql_args)
- cursor = connection.cursor()
- cursor.execute("SELECT * FROM `data` WHERE DATE(`measured`) BETWEEN '%s' AND '%s'" % (from_date, to_date))
- result = cursor.fetchall()
- connection.close()
- return result
+ """
+ Reads the data records of specified time range.
+
+ Parameters
+ ----------
+ from_date : str
+ datetime
+ Specifies starting point of query
+ to_date : str
+ datetime
+ Specifies end point of query
+ """
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ cursor.execute("SELECT * FROM `data` WHERE DATE(`measured`) BETWEEN '%s' AND '%s'" % (from_date, to_date))
+ result = cursor.fetchall()
+ con.close()
+ return result
def insert_data(self, temperature, weight, humidity):
- connection = pymysql.connect(**self.mysql_args)
- date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
- cursor = connection.cursor()
+ """
+ Inserts a new dataset into the database.
- cursor.execute("SELECT * FROM data ORDER BY number DESC LIMIT 1")
- res = cursor.fetchone()
+ Parameters
+ ----------
+ temperature : str
+ float
+ Current temperature
+ weight : str
+ float
+ Current weight
+ humidity : str
+ float
+ Current humidity
+ """
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
- if res is not None and res["weight"] is None:
- sql = "UPDATE data SET `temperature` = '%s', `weight` = '%s', `humidity` = '%s' WHERE number = '%s'" % (float(temperature), float(weight) * config.correction[0] - config.real_tare[0], float(humidity), res["number"])
- else:
- sql = "INSERT INTO `data` (`number`, `temperature`, `weight`, `humidity`, `measured`) VALUES (0, %s, %s, %s, '%s')" % (float(temperature), float(weight) * config.correction[0] - config.real_tare[0], float(humidity), date)
+ cursor.execute("SELECT * FROM data ORDER BY number DESC LIMIT 1")
+ res = cursor.fetchone()
- log = open("logs/insert.log", mode="a")
- log.write("\n[%s] - %s" % (time.asctime(), sql))
- log.close()
+ if res is not None and res["weight"] is None:
+ sql = "UPDATE data SET `temperature` = '%s', `weight` = '%s', `humidity` = '%s' WHERE number = '%s'" % (float(temperature), float(weight) * config.correction[0] - config.real_tare[0], float(humidity), res["number"])
+ else:
+ sql = "INSERT INTO `data` (`number`, `temperature`, `weight`, `humidity`, `measured`) VALUES (0, %s, %s, %s, '%s')" % (float(temperature), float(weight) * config.correction[0] - config.real_tare[0], float(humidity), date)
- cursor.execute(sql)
- connection.commit()
- connection.close()
- return
+ log = open("logs/insert.log", mode="a")
+ log.write("\n[%s] - %s" % (time.asctime(), sql))
+ log.close()
+
+ cursor.execute(sql)
+ con.commit()
+ con.close()
+ return
def scales(self, number, weight):
- connection = pymysql.connect(**self.mysql_args)
- date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
- cursor = connection.cursor()
- cursor.execute("SELECT * FROM data ORDER BY number DESC LIMIT 1")
- res = cursor.fetchone()
- cursor.execute("DESCRIBE data")
- fields = cursor.fetchall()
- fields = [str(x["Field"]) for x in fields]
- if not number in fields:
- return "No column in database for this scale"
- if res is not None and res[number] is None:
- sql = "UPDATE data SET `%s` = '%s' WHERE number = '%s'" % (number, weight, res['number'])
- else:
- sql = "INSERT INTO `data` (`%s`, `measured`) VALUES (%s, '%s')" % (number, weight, date)
+ """
+ Inserts a new dataset for a specific scale into the database.
+ A collumn with the name of "number" must exist!
+
+ Parameters
+ ----------
+ number : str
+ The unique number of the scale
+ weight : str
+ float
+ Current weight
+ """
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+ cursor.execute("SELECT * FROM data ORDER BY number DESC LIMIT 1")
+ res = cursor.fetchone()
+ cursor.execute("DESCRIBE data")
+ fields = cursor.fetchall()
+ fields = [str(x["Field"]) for x in fields]
+ if number not in fields:
+ return "No column in database for this scale"
+ if res is not None and res[number] is None:
+ sql = "UPDATE data SET `%s` = '%s' WHERE number = '%s'" % (number, weight, res['number'])
+ else:
+ sql = "INSERT INTO `data` (`%s`, `measured`) VALUES (%s, '%s')" % (number, weight, date)
+
+ print(sql)
+
+ cursor.execute(str(sql))
+ con.commit()
+ con.close()
+
+ log = open("logs/insert.log", mode="a")
+ log.write("\n[%s] - %s" % (time.asctime(), sql))
+ log.close()
+ return
+
+ def insert_feed(self, feed_name, data):
+ """
+ Inserts a new feed item into the database.
+
+ Parameters
+ ----------
+ feed_name : str
+ Name of the feed.
+ Currently only "data", "admin", or "warning".
+ data : dict
+ The data that should get inserted.
+ """
+
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ insert = json.dumps(data, ensure_ascii=False).encode('utf8')
+ cursor.execute(f"INSERT INTO `notifications` (`feed`, `data`) VALUES (%s, %s)", [feed_name, insert])
+
+ con.commit()
+ con.close()
+
+ return True
- print(sql)
+ def get_feed(self, feed_name):
+ """
+ Gets a feed from the database.
+
+ Parameters
+ ----------
+ feed_name : str
+ Name of the feed.
+ Currently only "data", "admin", or "warning".
+ """
+
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ cursor.execute(f"SELECT `id`, `data` FROM `notifications` WHERE `feed`='{feed_name}'")
+
+ feed = []
+ for item in cursor.fetchall():
+ item["data"] = json.loads(item["data"])
+ feed.append({
+ "time": item["data"]["time"],
+ "title": bytes(item["data"]["title"], "utf8").decode("utf8"),
+ "text": bytes(item["data"]["text"], "utf8").decode("utf8"),
+ "id": item["id"]
+ })
+
+ con.close()
+
+ feed.reverse()
+
+ return feed
+
+ def set_telegram_subscription(self, chat_id, feed_name, subscribe):
+ """
+ Changes the feed subscriptions of a telegram chat.
+
+ Parameters
+ ----------
+ chat_id : str
+ Chat ID of telegram chat. message.chat.id
+ feed_name : str
+ Name of the feed to change subscription. "data", "admin", "warning"
+ subscribe : bool
+ Specify whether to recieve updates on that feed.
+ """
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ cursor.execute(f"SELECT * FROM `subscriptions` WHERE `telegram_id`='{chat_id}'")
+ if cursor.fetchone() is None:
+ cursor.execute(f"INSERT INTO `subscriptions` (`telegram_id`) VALUES ({chat_id})")
+
+ cursor.execute(f"UPDATE `subscriptions` SET `{feed_name}_feed`='{1 if subscribe else 0}' WHERE `telegram_id`='{chat_id}'")
+
+ con.commit()
+ con.close()
+
+ return True
+
+ def check_telegram_subscription(self, chat_id, feed_name):
+ """
+ Checks the feed subscriptions of a telegram chat.
+
+ Parameters
+ ----------
+ chat_id : str
+ Chat ID of telegram chat. message.chat.id
+ feed_name : str
+ Name of the feed to check subscription for. "data", "admin", "warning"
+ """
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ cursor.execute(f"SELECT * FROM `subscriptions` WHERE `telegram_id`='{chat_id}'")
+ if cursor.fetchone()[f"{feed_name}_feed"] == 1:
+ con.close()
+ return True
+ else:
+ con.close()
+ return False
+
+ def get_telegram_subscriptions(self, feed_name):
+ """
+ Gets all chats who have subscribed to a feed.
+
+ Parameters
+ ----------
+ feed_name : str
+ Name of the feed to check subscription for. "data", "admin", "warning"
+ """
+ with self.connection_pool.connection() as con, con.cursor(dictionary=True) as cursor:
+ cursor.execute(f"SELECT * FROM `subscriptions` WHERE `{feed_name}_feed`='1'")
+ res = cursor.fetchall()
- cursor.execute(str(sql))
- connection.commit()
- connection.close()
+ con.close()
- log = open("logs/insert.log", mode="a")
- log.write("\n[%s] - %s" % (time.asctime(), sql))
- log.close()
- return
\ No newline at end of file
+ return [x["telegram_id"] for x in res]
diff --git a/database.sql b/database.sql
index f55f076..e1ed416 100644
--- a/database.sql
+++ b/database.sql
@@ -1,13 +1,13 @@
--- MySQL dump 10.16 Distrib 10.1.44-MariaDB, for debian-linux-gnu (x86_64)
+-- MySQL dump 10.13 Distrib 5.5.62, for Win64 (AMD64)
--
-- Host: localhost Database: beelogger
-- ------------------------------------------------------
--- Server version 10.1.44-MariaDB-0+deb9u1
+-- Server version 5.5.5-10.3.29-MariaDB-0+deb10u1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
-/*!40101 SET NAMES utf8mb4 */;
+/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
@@ -28,17 +28,38 @@ CREATE TABLE IF NOT EXISTS `data` (
`humidity` double DEFAULT NULL,
`measured` datetime DEFAULT NULL,
PRIMARY KEY (`number`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Table structure for table `notifications`
+--
+
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE IF NOT EXISTS `notifications` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `feed` varchar(100) CHARACTER SET utf8mb4 NOT NULL,
+ `data` text CHARACTER SET utf8mb4 NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
--- Dumping data for table `data`
+-- Table structure for table `subscriptions`
--
-LOCK TABLES `data` WRITE;
-/*!40000 ALTER TABLE `data` DISABLE KEYS */;
-/*!40000 ALTER TABLE `data` ENABLE KEYS */;
-UNLOCK TABLES;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE IF NOT EXISTS `subscriptions` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `telegram_id` varchar(100) DEFAULT NULL,
+ `admin_feed` tinyint(1) DEFAULT 0,
+ `warning_feed` tinyint(1) DEFAULT 0,
+ `data_feed` tinyint(1) DEFAULT 0,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping routines for database 'beelogger'
@@ -53,4 +74,4 @@ UNLOCK TABLES;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
--- Dump completed on 2020-05-29 20:22:08
+-- Dump completed on 2021-10-31 0:44:18
diff --git a/docker-compose.yml b/docker-compose.yml
index 206c492..b65af71 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -26,6 +26,8 @@ services:
- "./docker_volumes/logs:/app/logs"
- "./docker_volumes/secrets:/app/secrets"
environment:
+ telegram_bot_token: ""
+
insert_token: "changeme!2"
display_token: "changeme!3"
diff --git a/docker/docker-requirements.txt b/docker/docker-requirements.txt
index cce7f77..1614084 100644
--- a/docker/docker-requirements.txt
+++ b/docker/docker-requirements.txt
@@ -1,4 +1,7 @@
+mod_wsgi~=4.9.0
Flask~=2.0.2
Werkzeug~=2.0.2
-PyMySQL~=1.0.2
-mod_wsgi~=4.9.0
\ No newline at end of file
+rfeed~=1.1.1
+mysql-connector-python~=8.0.27
+DBUtils~=2.0.2
+pyTelegramBotAPI~=4.1.1
\ No newline at end of file
diff --git a/example_config.py b/example_config.py
index b29309b..48b6b3a 100644
--- a/example_config.py
+++ b/example_config.py
@@ -13,6 +13,8 @@ def jsonkeys2int(x):
if not use_env:
+ telegram_bot_token = ""
+
insert_token = ""
display_token = ""
@@ -21,7 +23,6 @@ def jsonkeys2int(x):
real_tare = {0: 0} # tare value before insertion into database
class MySql:
- host = ""
host = ""
port = 3306
user = "beelogger"
@@ -57,6 +58,8 @@ class Mail:
else:
+ telegram_bot_token = os.environ["telegram_bot_token"]
+
insert_token = os.environ["insert_token"]
display_token = os.environ["display_token"]
diff --git a/notifications.py b/notifications.py
new file mode 100644
index 0000000..bf987be
--- /dev/null
+++ b/notifications.py
@@ -0,0 +1,102 @@
+import datetime
+import time
+
+import rfeed
+from flask import request
+
+import config
+import database
+from telegram import bot
+
+Database = database.Database()
+
+
+class Feed:
+ def __init__(self):
+ self.supported_feeds = ["data", "admin", "warning"]
+ self.feed_descriptions = {
+ "data": "Jeder Datensatz ist hier verfügbar.",
+ "admin": "Wichtige Informationen, die nur für Administratoren bestimmt sind.",
+ "warning": "Allgemeine Warnungen, die das Bienenvolk betreffen."
+ }
+ self.feed_names = {
+ "admin": "BeeLogger Admin Alarm!",
+ "data": "BeeLogger Datensätze",
+ "warning": "BeeLogger Bienen Alarm!"
+ }
+
+ def push_notification(self, feed, title, text):
+ """
+ Creates new item on the warning feed. E.g. temperature critical.
+
+ Parameters
+ ----------
+ feed : str
+ The name of the feed. "data", "admin", "warning"
+ title : str
+ The title of the Item.
+ text : str
+ The text to push on the feed.
+
+ Raises
+ ------
+ TypeError
+ You did not provide a valid feed name.
+ """
+
+ if feed not in self.supported_feeds:
+ raise TypeError("This feed name is unsupported")
+
+ item = {
+ "time": time.strftime("%Y-%m-%d %H:%M:%S"),
+ "title": title,
+ "text": text
+ }
+
+ Database.insert_feed(feed, item)
+
+ if not config.telegram_bot_token == "":
+ ########### Telegram Bot ###########
+ message = f">>>> {self.feed_names[feed]} <<<<\n" \
+ f">>> {title} <<<\n" \
+ f"{text}\n\n" \
+ f"Automatisch generierte Nachricht!"
+ for chat_id in Database.get_telegram_subscriptions(feed):
+ bot.send_message(chat_id, message)
+
+ return True
+
+ def get_feed(self, feed_name, rss_format=True):
+ """
+ Gets the data feed. See self.push_data
+ feed_name : str
+ Name of the feed to get. "data", "admin", "warning"
+ rss_format : bool
+ Specify whether the feed should be returned in a RSS XML format.
+ """
+ if feed_name not in self.supported_feeds:
+ raise TypeError("This feed name is unsupported")
+
+ feed = Database.get_feed(feed_name)
+
+ if rss_format:
+ items = []
+ for item in feed:
+ # item = json.loads(item)
+ items.append(rfeed.Item(
+ title=item["title"],
+ description=item["text"],
+ author="Automatic BeeLogger Notification",
+ pubDate=datetime.datetime.strptime(item["time"], "%Y-%m-%d %H:%M:%S"),
+ link=f"{request.host_url}rss/{feed_name}/{item['id']}/",
+ ))
+
+ return rfeed.Feed(
+ title=self.feed_names[feed_name],
+ description=self.feed_descriptions[feed_name],
+ link=request.url,
+ language="de-DE",
+ items=items
+ ).rss()
+
+ return feed
diff --git a/pages/components/sidenav.html b/pages/components/sidenav.html
index 0bba586..74c62fc 100644
--- a/pages/components/sidenav.html
+++ b/pages/components/sidenav.html
@@ -22,6 +22,9 @@
{% endfor %}
{% endif %}
+
+
+ feedFeeds
diff --git a/pages/content/about.html b/pages/content/about.html
index bd14909..63049b2 100644
--- a/pages/content/about.html
+++ b/pages/content/about.html
@@ -10,6 +10,15 @@ Daten
Seite zur Verfügung stehen.
+
+
Feeds
+
Die komplette Aktivität des Projekts kann über unsere Echtzeit-Feeds eingesehen werden (auch im RSS-Format!)
+
+
+
+
+
+
Credits
Das Web-Dashboard, das Display und die Daten- und Statistiken-API (Schnittstelle)
diff --git a/pages/feed-article.html b/pages/feed-article.html
new file mode 100644
index 0000000..9572036
--- /dev/null
+++ b/pages/feed-article.html
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% block custom_header %}{% endblock %}
+
+{% block content %}
+
+
+
Zurück zum Feed
+
{{ article["title"] }}
+
{{ article["time"] }}
+
{{ article["text"] }}
+
+
+{% endblock %}
+
+{% block custom_footer %}{% endblock %}
\ No newline at end of file
diff --git a/pages/feed.html b/pages/feed.html
new file mode 100644
index 0000000..cc93fde
--- /dev/null
+++ b/pages/feed.html
@@ -0,0 +1,49 @@
+{% extends "base.html" %}
+
+{% block custom_header %}{% endblock %}
+
+{% block content %}
+
+
+
+
{{ feed_name.upper() }} Feed
+
+
+
QR Code zum {{ feed_name.upper() }} RSS Feed:
+
+
+
+ {% for record in records %}
+
+
+
{{ record["title"] }}
+
{{ record["time"] }}
+
{{ record["text"] }}
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
+
+{% block custom_footer %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/pages/feeds.html b/pages/feeds.html
new file mode 100644
index 0000000..aaaa3bf
--- /dev/null
+++ b/pages/feeds.html
@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+
+{% block custom_header %}{% endblock %}
+
+{% block content %}
+
+
+
+
BeeLogger Feeds
+
+
+ BeeLogger verfügt über sogenannte Feeds, in denen automatisierte
+ System-Benachrichtigungen zu finden sind.
+
+
+
+
+
+
Data Feed
+
Feed mit Benachrichtigungen über das Sammeln von Daten.
+
+
+
+
+
+
+
+
Warning Feed
+
Feed mit Warnungen über mögliche Systemabstürze, Störungen, etc.
+
+
+
+
+
+
+
+
Admin Feed
+
Feed mit Benachrichtigungen rund um die Administration von BeeLogger.
+
+
+
+
+
+
+{% endblock %}
+
+{% block custom_footer %}{% endblock %}
\ No newline at end of file
diff --git a/public/css/display.css b/public/css/display.css
index 02c41c9..ece8b79 100644
--- a/public/css/display.css
+++ b/public/css/display.css
@@ -42,6 +42,20 @@ h1 p#date {
padding: 0;
}
+.feeds-frame-wrapper {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+.feeds-frame-wrapper iframe {
+ display: block;
+ border: none;
+ height: 100vh;
+ width: 100%;
+}
+
#pages-tab, #stundenplan-tab {
margin: 0 !important;
padding: 0 !important;
diff --git a/public/js/app.js b/public/js/app.js
index 87a1139..9eafb56 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -233,6 +233,15 @@ function getWeightDelta(data) {
return weightDeltaString;
}
+/**
+ * When the 'show feeds' button has been clicked on the dashboard
+ * (not the display!), redirect to the feed page instead of showing
+ * the iframe.
+ */
+ function showFeeds() {
+ window.location.href += '/rss/feeds';
+}
+
/**
* Handler function for when the API returns an error.
* This function will catch the error and display an error
diff --git a/public/js/display.js b/public/js/display.js
index c470015..b75e791 100644
--- a/public/js/display.js
+++ b/public/js/display.js
@@ -146,8 +146,19 @@ function navigatePages(url) {
M.Sidenav.getInstance(document.querySelector('#slide-out')).close();
}
+/**
+ * Make 'feeds' iframe on about page visible and navigate it
+ * to the feeds index page.
+ */
+function showFeeds() {
+ let feedsFrame = document.getElementById('feeds-frame');
+ feedsFrame.classList.toggle('hide');
+ feedsFrame.setAttribute('src', '/rss/feeds');
+}
+
// Register timers and tabs when document is fully loaded
document.addEventListener('DOMContentLoaded', async () => {
M.Tabs.init(document.querySelectorAll('.tabs'), {});
+ M.Collapsible.init(document.querySelectorAll('.collapsible'), {});
setupTimers();
});
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 351dd8d..7336811 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,7 @@ pip~=21.3
Flask~=2.0.2
Werkzeug~=2.0.2
numpy~=1.21.3
-PyMySQL~=1.0.2
-mod_wsgi~=4.9.0
\ No newline at end of file
+rfeed~=1.1.1
+mysql-connector-python~=8.0.27
+DBUtils~=2.0.2
+pyTelegramBotAPI~=4.1.1
diff --git a/telegram.py b/telegram.py
new file mode 100644
index 0000000..6c3dea1
--- /dev/null
+++ b/telegram.py
@@ -0,0 +1,47 @@
+import telebot
+
+import config
+import database
+
+bot = telebot.TeleBot(config.telegram_bot_token, parse_mode=None)
+
+Database = database.Database()
+
+@bot.message_handler(commands=['start'])
+def send_welcome(message):
+ print(message.chat.id)
+ bot.send_message(message.chat.id, "Hi!\nIch bin der BeeLogger Bot, der dir automatisch Informationen zum Bienenschwarm schickt!\n\n"
+ "Starte mit:\n"
+ "/aboniere_warnungen um Warnungen zum Bienenvolk zu erhalten.\n"
+ "/deaboniere_warnungen um Warnungen abzubestellen.")
+
+@bot.message_handler(commands=['aboniere_warnungen'])
+def sub_warnings(message):
+ Database.set_telegram_subscription(message.chat.id, "warning", True)
+ bot.reply_to(message, "Du erhälst nun Warnungen zum Bienenvolk!")
+@bot.message_handler(commands=['deaboniere_warnungen'])
+def unsub_warnings(message):
+ Database.set_telegram_subscription(message.chat.id, "warning", False)
+ bot.reply_to(message, "Du erhälst nun keine Warnungen mehr!")
+
+@bot.message_handler(commands=['aboniere_admin'])
+def sub_admin(message):
+ Database.set_telegram_subscription(message.chat.id, "admin", True)
+ bot.reply_to(message, "Du erhälst nun Admin Nachrichten der Software!")
+@bot.message_handler(commands=['deaboniere_admin'])
+def unsub_admin(message):
+ Database.set_telegram_subscription(message.chat.id, "admin", False)
+ bot.reply_to(message, "Du erhälst nun keine Admin Nachrichten mehr!")
+
+@bot.message_handler(commands=['aboniere_data'])
+def sub_data(message):
+ Database.set_telegram_subscription(message.chat.id, "data", True)
+ bot.reply_to(message, "Du erhälst nun Datensätze via Telegram!")
+@bot.message_handler(commands=['deaboniere_data'])
+def unsub_data(message):
+ Database.set_telegram_subscription(message.chat.id, "data", False)
+ bot.reply_to(message, "Du erhälst nun keine Datensätze mehr via Telegram./st")
+
+
+if __name__ == "__main__":
+ bot.infinity_polling()