From 5dc3e19e486de2cf4f0b78044742a61139f68718 Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Tue, 26 Oct 2021 19:43:45 +0200 Subject: [PATCH 01/19] database refactor --- database.py | 209 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 130 insertions(+), 79 deletions(-) diff --git a/database.py b/database.py index 980d1b3..cea3896 100644 --- a/database.py +++ b/database.py @@ -1,20 +1,24 @@ -import pymysql +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 +41,135 @@ 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. + + 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()) - cursor.execute("SELECT * FROM data ORDER BY number DESC LIMIT 1") - res = cursor.fetchone() + cursor.execute("SELECT * FROM data ORDER BY number DESC LIMIT 1") + res = cursor.fetchone() - 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) + 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) - log = open("logs/insert.log", mode="a") - log.write("\n[%s] - %s" % (time.asctime(), sql)) - log.close() + log = open("logs/insert.log", mode="a") + log.write("\n[%s] - %s" % (time.asctime(), sql)) + log.close() - cursor.execute(sql) - connection.commit() - connection.close() - return + 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) - - print(sql) - - cursor.execute(str(sql)) - connection.commit() - connection.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 + """ + 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_rss_feed(self, feed, data): + """ + Inserts a new feed item into the database. + + Parameters + ---------- + feed : str + Name of the feed. + Currently only "data", "admin", or "warning". + data : str + The text to insert. + """ + pass From 9c1f0e7dafb0e2251cec03e33abc7104428209d2 Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Thu, 28 Oct 2021 19:37:15 +0200 Subject: [PATCH 02/19] prepare rss feature --- database.py | 16 +++++++++++++++- requirements.txt | 4 +++- rss.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 rss.py diff --git a/database.py b/database.py index cea3896..68a432e 100644 --- a/database.py +++ b/database.py @@ -160,7 +160,8 @@ def scales(self, number, weight): log.close() return - def insert_rss_feed(self, feed, data): + @staticmethod + def insert_rss_feed(feed, data): """ Inserts a new feed item into the database. @@ -173,3 +174,16 @@ def insert_rss_feed(self, feed, data): The text to insert. """ pass + + @staticmethod + def get_rss_feed(feed): + """ + Gets a feed from the database. + + Parameters + ---------- + feed : str + Name of the feed. + Currently only "data", "admin", or "warning". + """ + pass diff --git a/requirements.txt b/requirements.txt index eaaa5b6..96ebabf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ pip~=21.3 Flask~=2.0.2 Werkzeug~=2.0.2 numpy~=1.21.3 -PyMySQL~=1.0.2 \ No newline at end of file +rfeed~=1.1.1 +mysql-connector-python~=8.0.27 +DBUtils~=2.0.2 \ No newline at end of file diff --git a/rss.py b/rss.py new file mode 100644 index 0000000..2af2eb3 --- /dev/null +++ b/rss.py @@ -0,0 +1,33 @@ +import rfeed +from database import Database + +class RssFeed: + @staticmethod + def push_warning(feed_name, title, text): + """ + Creates new item on the warning feed. E.g. temperature critical. + + Parameters + ---------- + feed_name : str + The name of the feed. + title : str + The title of the feed. + text : str + The text to push on the feed. + """ + feed = Database.get_rss_feed(feed_name) + feed.append({ + "time": + }) + + Database.insert_rss_feed(feed_name, text) + return + + @staticmethod + def get_feed(feed): + """ + Gets the data feed. See self.push_data + """ + Database.get_rss_feed(feed) + pass From 621496e188e37452731cb71fc12fd1dba82c6033 Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Sat, 30 Oct 2021 22:04:20 +0200 Subject: [PATCH 03/19] added rss feeds --- app.py | 4 +++ blueprints/rss.py | 17 +++++++++ database.py | 47 ++++++++++++++++++------ database.sql | 27 ++++++++------ notifications.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++ rss.py | 33 ----------------- 6 files changed, 167 insertions(+), 53 deletions(-) create mode 100644 blueprints/rss.py create mode 100644 notifications.py delete mode 100644 rss.py diff --git a/app.py b/app.py index c606cce..3e414a2 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,7 @@ import os from blueprints.api import api from blueprints.views import views +from blueprints.rss import rss from flask import Flask from database import Database from jsonencoder import CustomJSONEncoder @@ -45,6 +46,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): diff --git a/blueprints/rss.py b/blueprints/rss.py new file mode 100644 index 0000000..bb399eb --- /dev/null +++ b/blueprints/rss.py @@ -0,0 +1,17 @@ +from flask import Blueprint +import notifications + +Notifications = notifications.Feed() + +rss = Blueprint("rss", __name__) + +@rss.route("//", methods=["GET"]) +def show_feed(feed): + return Notifications.get_feed(feed) + +@rss.route("///", methods=["GET"]) +def show_article(feed, feed_id): + items = Notifications.get_feed(feed, rss_format=False) + article = [x for x in items if str(x["id"]) == feed_id][0] + + return article["text"] diff --git a/database.py b/database.py index 68a432e..83492e7 100644 --- a/database.py +++ b/database.py @@ -1,3 +1,6 @@ +import ast +import json + import mysql.connector.cursor import mysql.connector.errors from dbutils.pooled_db import PooledDB @@ -160,30 +163,54 @@ def scales(self, number, weight): log.close() return - @staticmethod - def insert_rss_feed(feed, data): + def insert_feed(self, feed_name, data): """ Inserts a new feed item into the database. Parameters ---------- - feed : str + feed_name : str Name of the feed. Currently only "data", "admin", or "warning". - data : str - The text to insert. + data : dict + The data that should get inserted. """ - pass - @staticmethod - def get_rss_feed(feed): + 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 + + def get_feed(self, feed_name): """ Gets a feed from the database. Parameters ---------- - feed : str + feed_name : str Name of the feed. Currently only "data", "admin", or "warning". """ - pass + + 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 diff --git a/database.sql b/database.sql index f55f076..88d932c 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 */; @@ -19,9 +19,10 @@ -- Table structure for table `data` -- +DROP TABLE IF EXISTS `data`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; -CREATE TABLE IF NOT EXISTS `data` ( +CREATE TABLE `data` ( `number` bigint(20) NOT NULL AUTO_INCREMENT, `temperature` double DEFAULT NULL, `weight` double DEFAULT NULL, @@ -32,13 +33,19 @@ CREATE TABLE IF NOT EXISTS `data` ( /*!40101 SET character_set_client = @saved_cs_client */; -- --- Dumping data for table `data` +-- Table structure for table `notifications` -- -LOCK TABLES `data` WRITE; -/*!40000 ALTER TABLE `data` DISABLE KEYS */; -/*!40000 ALTER TABLE `data` ENABLE KEYS */; -UNLOCK TABLES; +DROP TABLE IF EXISTS `notifications`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `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=11 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Dumping routines for database 'beelogger' @@ -53,4 +60,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-30 22:00:37 diff --git a/notifications.py b/notifications.py new file mode 100644 index 0000000..3468cdf --- /dev/null +++ b/notifications.py @@ -0,0 +1,92 @@ +import datetime +import json +import time + +import rfeed +from flask import jsonify, request + +import database + +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) + + 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/rss.py b/rss.py deleted file mode 100644 index 2af2eb3..0000000 --- a/rss.py +++ /dev/null @@ -1,33 +0,0 @@ -import rfeed -from database import Database - -class RssFeed: - @staticmethod - def push_warning(feed_name, title, text): - """ - Creates new item on the warning feed. E.g. temperature critical. - - Parameters - ---------- - feed_name : str - The name of the feed. - title : str - The title of the feed. - text : str - The text to push on the feed. - """ - feed = Database.get_rss_feed(feed_name) - feed.append({ - "time": - }) - - Database.insert_rss_feed(feed_name, text) - return - - @staticmethod - def get_feed(feed): - """ - Gets the data feed. See self.push_data - """ - Database.get_rss_feed(feed) - pass From aa8c4e0dbdf1abce622f30af2492b6c85b83bb66 Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Sun, 31 Oct 2021 00:55:35 +0200 Subject: [PATCH 04/19] added telegram bot --- app.py | 22 +++++++++++++--- database.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++ database.sql | 28 +++++++++++++++------ docker-compose.yml | 2 ++ example_config.py | 4 +++ notifications.py | 13 +++++++--- requirements.txt | 3 ++- telegram.py | 47 +++++++++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 telegram.py diff --git a/app.py b/app.py index 3e414a2..76ed94c 100644 --- a/app.py +++ b/app.py @@ -1,11 +1,16 @@ -import config import os +import sys +import threading + +from flask import Flask + +import config from blueprints.api import api -from blueprints.views import views from blueprints.rss import rss -from flask import Flask +from blueprints.views import views from database import Database from jsonencoder import CustomJSONEncoder +from telegram import bot as telegram_bot print("waiting until db is ready") os.popen(f"/bin/bash ./wait-for-it.sh {config.MySql.host}:{str(config.MySql.port)}").read() @@ -66,4 +71,13 @@ def add_header(r): # Start the app if __name__ == "__main__": - app.run(host='0.0.0.0', port=config.web_port) + telegram_bot_thread = threading.Thread(target=telegram_bot.infinity_polling) + telegram_bot_thread.daemon = True + telegram_bot_thread.start() + + try: + 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/database.py b/database.py index 83492e7..bfb2390 100644 --- a/database.py +++ b/database.py @@ -214,3 +214,65 @@ def get_feed(self, feed_name): 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() + + con.close() + + return [x["telegram_id"] for x in res] diff --git a/database.sql b/database.sql index 88d932c..e1ed416 100644 --- a/database.sql +++ b/database.sql @@ -19,32 +19,46 @@ -- Table structure for table `data` -- -DROP TABLE IF EXISTS `data`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; -CREATE TABLE `data` ( +CREATE TABLE IF NOT EXISTS `data` ( `number` bigint(20) NOT NULL AUTO_INCREMENT, `temperature` double DEFAULT NULL, `weight` double DEFAULT NULL, `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` -- -DROP TABLE IF EXISTS `notifications`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; -CREATE TABLE `notifications` ( +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=11 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `subscriptions` +-- + +/*!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 */; -- @@ -60,4 +74,4 @@ CREATE TABLE `notifications` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2021-10-30 22:00:37 +-- Dump completed on 2021-10-31 0:44:18 diff --git a/docker-compose.yml b/docker-compose.yml index 0a761a8..cd19920 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,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/example_config.py b/example_config.py index b29309b..7457f37 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 = "" @@ -57,6 +59,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 index 3468cdf..8b2614c 100644 --- a/notifications.py +++ b/notifications.py @@ -1,14 +1,15 @@ import datetime -import json import time import rfeed -from flask import jsonify, request +from flask import request import database +from telegram import bot Database = database.Database() + class Feed: def __init__(self): self.supported_feeds = ["data", "admin", "warning"] @@ -53,6 +54,13 @@ def push_notification(self, feed, title, text): Database.insert_feed(feed, item) + ########### Telegram Bot ########### + message = 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): @@ -89,4 +97,3 @@ def get_feed(self, feed_name, rss_format=True): ).rss() return feed - diff --git a/requirements.txt b/requirements.txt index 96ebabf..4b64601 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ Werkzeug~=2.0.2 numpy~=1.21.3 rfeed~=1.1.1 mysql-connector-python~=8.0.27 -DBUtils~=2.0.2 \ No newline at end of file +DBUtils~=2.0.2 +pyTelegramBotAPI~=4.1.1 \ No newline at end of file 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() From b93c4e19cd3e833298b8d777b4fe95f11cf56c7a Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Sun, 31 Oct 2021 01:08:48 +0200 Subject: [PATCH 05/19] update docker-requirements.txt --- docker-requirements.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-requirements.txt b/docker-requirements.txt index cce7f77..1614084 100644 --- a/docker-requirements.txt +++ b/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 From 412a0449e86e150fdec4bdcafa2c2fe62bd5557f Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Sun, 31 Oct 2021 12:21:10 +0100 Subject: [PATCH 06/19] made telegram bot optional changed message a bit (e.g. to include feed) --- app.py | 9 ++++++--- notifications.py | 15 +++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index 76ed94c..cc6a9ad 100644 --- a/app.py +++ b/app.py @@ -71,9 +71,12 @@ def add_header(r): # Start the app if __name__ == "__main__": - telegram_bot_thread = threading.Thread(target=telegram_bot.infinity_polling) - telegram_bot_thread.daemon = True - telegram_bot_thread.start() + 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: app.run(host='0.0.0.0', port=config.web_port) diff --git a/notifications.py b/notifications.py index 8b2614c..bf987be 100644 --- a/notifications.py +++ b/notifications.py @@ -4,6 +4,7 @@ import rfeed from flask import request +import config import database from telegram import bot @@ -54,12 +55,14 @@ def push_notification(self, feed, title, text): Database.insert_feed(feed, item) - ########### Telegram Bot ########### - message = 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) + 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 From aa5b1f3048dcb97021465bcdecbd09937550ccbd Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sat, 4 Dec 2021 15:07:02 +0100 Subject: [PATCH 07/19] Feeds: Push "Startup Event" to Admin Feed --- app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.py b/app.py index 5c1da39..e340889 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,7 @@ from blueprints.rss import rss from blueprints.views import views from database import Database +from notifications import Feed from utils.jsonencoder import CustomJSONEncoder from telegram import bot as telegram_bot @@ -77,6 +78,7 @@ def add_header(r): 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...") From 6ba86c4635bcb23ca09685b97deca793968c7d42 Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sat, 4 Dec 2021 15:07:37 +0100 Subject: [PATCH 08/19] Feeds: Add Feed HTML Template --- pages/rss.html | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 pages/rss.html diff --git a/pages/rss.html b/pages/rss.html new file mode 100644 index 0000000..b4758fd --- /dev/null +++ b/pages/rss.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block custom_header %} + +{% endblock %} + +{% block content %} +
+
+
+

{{ feed_name.upper() }} Feed

+ {% for record in records %} +
+
+ {{ record["title"] }} + {{ record["time"] }} +

{{ record["text"] }}

+
+
+ Ansehen +
+
+ {% endfor %} +
+
+
+{% endblock %} + +{% block custom_footer %}{% endblock %} \ No newline at end of file From 89fa6a3d2b64de4ae5163997321348e1d5348a1b Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sat, 4 Dec 2021 15:10:01 +0100 Subject: [PATCH 09/19] Feeds: Add "Feed Formats" (XML, JSON, HTML) Add ability to retrieve a feed in different formats. There's a classic RSS (XML) option, a JSON option and an HTML option utilizing the Feed Template. --- blueprints/rss.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/blueprints/rss.py b/blueprints/rss.py index bb399eb..b025b87 100644 --- a/blueprints/rss.py +++ b/blueprints/rss.py @@ -1,4 +1,5 @@ -from flask import Blueprint +from flask import Blueprint, Response, request, jsonify +from flask.templating import render_template import notifications Notifications = notifications.Feed() @@ -7,7 +8,21 @@ @rss.route("//", methods=["GET"]) def show_feed(feed): - return Notifications.get_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("rss.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): From 2aa11664e7311ab47369d1ff23cbca762903e213 Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sat, 4 Dec 2021 15:26:56 +0100 Subject: [PATCH 10/19] Feeds: Add Article HTML Template Add HTML template for a single feed article. Also, rename `rss.html` to `feed.html`. --- pages/feed-article.html | 16 ++++++++++++++++ pages/{rss.html => feed.html} | 8 ++------ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 pages/feed-article.html rename pages/{rss.html => feed.html} (91%) 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/rss.html b/pages/feed.html similarity index 91% rename from pages/rss.html rename to pages/feed.html index b4758fd..4249a4b 100644 --- a/pages/rss.html +++ b/pages/feed.html @@ -1,10 +1,6 @@ {% extends "base.html" %} -{% block custom_header %} - -{% endblock %} +{% block custom_header %}{% endblock %} {% block content %}
@@ -19,7 +15,7 @@

{{ feed_name.upper() }} Feed

{{ record["text"] }}

{% endfor %} From a3a468d1ca42683b04c69b50a0ab51f5d54b7733 Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sat, 4 Dec 2021 15:30:14 +0100 Subject: [PATCH 11/19] Feeds: Add "Article Formats" (JSON, HTML) Add ability to retrieve a feed article in different formats. There's a JSON option and an HTML option utilizing the feed article template. --- blueprints/rss.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/blueprints/rss.py b/blueprints/rss.py index b025b87..c77e177 100644 --- a/blueprints/rss.py +++ b/blueprints/rss.py @@ -14,7 +14,7 @@ def show_feed(feed): # 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("rss.html", feed_name=feed, records=feed_data) + 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)) @@ -26,7 +26,19 @@ def show_feed(feed): @rss.route("///", methods=["GET"]) def show_article(feed, feed_id): - items = Notifications.get_feed(feed, rss_format=False) - article = [x for x in items if str(x["id"]) == feed_id][0] + # 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 article["text"] + # Return pretty, HTML-based version of the feed if ?pretty is passed + if "pretty" in args.keys(): + return render_template("feed-article.html", feed_name=feed, article=article) + # Return feed as valid JSON if ?json is passed + elif "json" in args.keys(): + return article + # Return feed as valid XML (for instance for RSS readers) + else: + # TODO: Return a single article in a proper RSS XML format (not just the text)!! + return article["text"] From cb0d992a5b274d5f4227fd0603b906573619f2cd Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Sun, 5 Dec 2021 00:09:46 +0100 Subject: [PATCH 12/19] Feeds: changed single feed view to meet rss needs --- blueprints/rss.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/blueprints/rss.py b/blueprints/rss.py index c77e177..4387f21 100644 --- a/blueprints/rss.py +++ b/blueprints/rss.py @@ -32,13 +32,12 @@ def show_article(feed, feed_id): feed_data = Notifications.get_feed(feed, rss_format=False) article = [x for x in feed_data if str(x["id"]) == feed_id][0] - # Return pretty, HTML-based version of the feed if ?pretty is passed - if "pretty" in args.keys(): - return render_template("feed-article.html", feed_name=feed, article=article) + # 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 feed as valid XML (for instance for RSS readers) + # Return pretty, HTML-based version of the feed else: - # TODO: Return a single article in a proper RSS XML format (not just the text)!! - return article["text"] + return render_template("feed-article.html", feed_name=feed, article=article) From 6d9f17eb98a0a6a21e42a35d2aad198439cc66c8 Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Sun, 5 Dec 2021 00:10:23 +0100 Subject: [PATCH 13/19] Feeds: Update backup.py to push feed on error --- backup.py | 79 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 35 deletions(-) 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) From 735edd52bc4d736301ef46080528f6f308c1de85 Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sun, 5 Dec 2021 10:55:28 +0100 Subject: [PATCH 14/19] Configs: Remove 2nd `host` attribute from config --- example_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/example_config.py b/example_config.py index 7457f37..48b6b3a 100644 --- a/example_config.py +++ b/example_config.py @@ -23,7 +23,6 @@ def jsonkeys2int(x): real_tare = {0: 0} # tare value before insertion into database class MySql: - host = "" host = "" port = 3306 user = "beelogger" From 4dad106c68f19f4443e904690bd4300e833bb58d Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sun, 5 Dec 2021 12:05:41 +0100 Subject: [PATCH 15/19] Feeds: Add QR Code to RSS Feed on Feed Page --- pages/feed.html | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/pages/feed.html b/pages/feed.html index 4249a4b..cc93fde 100644 --- a/pages/feed.html +++ b/pages/feed.html @@ -6,7 +6,13 @@
-

{{ feed_name.upper() }} Feed

+

{{ feed_name.upper() }} Feed

+ +
+
QR Code zum {{ feed_name.upper() }} RSS Feed:
+ Unable to generate RSS QR code. +
+ {% for record in records %}
@@ -24,4 +30,20 @@

{{ feed_name.upper() }} Feed

{% endblock %} -{% block custom_footer %}{% endblock %} \ No newline at end of file +{% block custom_footer %} + +{% endblock %} \ No newline at end of file From f253f208dda9a6319423c8046bb3aeb6ecf101cc Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sun, 5 Dec 2021 12:19:36 +0100 Subject: [PATCH 16/19] Feeds: Add Page with Link to Each Feed Add "index" page for feeds with link to each feed alongside some info about the feed. --- blueprints/rss.py | 4 ++++ pages/feeds.html | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 pages/feeds.html diff --git a/blueprints/rss.py b/blueprints/rss.py index 4387f21..d9133a1 100644 --- a/blueprints/rss.py +++ b/blueprints/rss.py @@ -6,6 +6,10 @@ 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 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.

+
+
+ Ansehen +
+
+ + +
+
+ Warning Feed +

Feed mit Warnungen über mögliche Systemabstürze, Störungen, etc.

+
+
+ Ansehen +
+
+ + +
+
+ Admin Feed +

Feed mit Benachrichtigungen rund um die Administration von BeeLogger.

+
+
+ Ansehen +
+
+
+
+
+{% endblock %} + +{% block custom_footer %}{% endblock %} \ No newline at end of file From d3a47902f38f1879cc72471f3dbf4334a09fe885 Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sun, 5 Dec 2021 12:47:30 +0100 Subject: [PATCH 17/19] Display: Add Iframe for Feeds in 'About' Tab --- pages/content/about.html | 9 +++++++++ public/css/display.css | 14 ++++++++++++++ public/js/display.js | 11 +++++++++++ 3 files changed, 34 insertions(+) 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/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/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 From c8629185d712b494b568095b16553d0470147318 Mon Sep 17 00:00:00 2001 From: Fabian Reinders Date: Sun, 5 Dec 2021 12:48:02 +0100 Subject: [PATCH 18/19] Dashboard: Add Link(s) to Feeds --- pages/components/sidenav.html | 3 +++ public/js/app.js | 9 +++++++++ 2 files changed, 12 insertions(+) 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 %} +

  • +
  • Sonstiges
  • +
  • feedFeeds

  • 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 From 63716441cf35908358106da4240606f878707ba0 Mon Sep 17 00:00:00 2001 From: AuxiliumCDNG Date: Tue, 25 Jan 2022 13:45:33 +0100 Subject: [PATCH 19/19] Feeds: Implement weight alarm TODO: Implement proper check system --- api/insert_data.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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"