This commit is contained in:
Remy Moll 2022-01-01 14:51:13 +01:00
parent bf4b02902b
commit f468332843
52 changed files with 3272 additions and 3419 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "sql_as_rest_api"]
path = sql_as_rest_api
url = https://github.com/moll-re/sql_as_rest_api

View File

@ -6,18 +6,29 @@ Just like AIO-coolers, this little program aims to tackle many problems at once.
## What it mainly does ## What it mainly does
* chat-bot (via telegram) * chat-bot (via telegram)
* clock and basic display (via LED-Matrix (size to taste)) * clock and basic display (via LED-Matrix (size to taste))
* dashboard (via external browser) * measure ambient temperatures
* Logging of the previous actions
### Chatbot ### Chatbot
Periodically calls the telegram api and reacts to sent commands. Also handles basic calls to the hardware: it allows you to control certain aspects of the clock. Periodically calls the telegram api and reacts to sent commands. Also handles basic calls to the hardware: it allows you to control certain aspects of the clock.
TODO: advanced analytics of the chat (grafana)
## Clock ### Clock
Server/Client which send/receive the output to show on the clock. Normally this is just a combination of time + weather. But special output can be triggered by the user. Server/Client which send/receive the output to show on the clock. Normally this is just a combination of time + weather. But special output can be triggered by the user.
## Dashboard ### Ambient measurements
Shows basic info of the program and other useful things. Logs temperature, luminosity, humidity to a remote database. This information is then displayed in `moll.re`.
TODO: show advanced host stats (cpu/mem/...) TODO: Log relevant worker info such as cpu activity and network connectivity.
## Submodules
This program makes use of git submodules, namely `sql_as_rest_api`. This implies additional steps when cloning this repo:
* CLone **this** repo to your machine.
* Enter the repo
* Type `git submodule init` which creates a `.gitmodules` file
* Type `git submodule update` which fetches the newest version of these submodules
TODO Describe dev process

View File

@ -1,8 +1,5 @@
from .template import * from .template import *
import datetime
import requests
NAME, NEW, ACTION, ITEMADD, ITEMREMOVE = range(5) NAME, NEW, ACTION, ITEMADD, ITEMREMOVE = range(5)
@ -41,6 +38,7 @@ class Lists(BotFunc):
def entry_point(self, update: Update, context: CallbackContext) -> None: def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context) super().entry_point(update, context)
# TODO Change DB
lists = self.db.lists.select() lists = self.db.lists.select()
sl = [l.name for l in lists] sl = [l.name for l in lists]
keyboard = [[InlineKeyboardButton(k, callback_data="list-"+k)] for k in sl] + [[InlineKeyboardButton("New list", callback_data="new")]] keyboard = [[InlineKeyboardButton(k, callback_data="list-"+k)] for k in sl] + [[InlineKeyboardButton("New list", callback_data="new")]]

View File

@ -62,6 +62,7 @@ class Status(BotFunc):
u = str(self.get_ngrok_url()) u = str(self.get_ngrok_url())
message += "URL: [" + u + "](" + u + ")\n" message += "URL: [" + u + "](" + u + ")\n"
# TODO new DB
tot_r = self.db.chats.select().where(self.db.chats.read == True).count() tot_r = self.db.chats.select().where(self.db.chats.read == True).count()
message += "Total messages read: `{}`\n".format(tot_r) message += "Total messages read: `{}`\n".format(tot_r)

View File

@ -1,5 +1,4 @@
import logging import logging
import datetime
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, ParseMode from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, ParseMode
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext, MessageHandler, Filters from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext, MessageHandler, Filters
from telegram.ext import ( from telegram.ext import (
@ -21,20 +20,20 @@ class BotFunc():
self.db = db self.db = db
def log_activity(self, **kwargs): # def log_activity(self, **kwargs):
# mark that a new command has been executed # # mark that a new command has been executed
try: # try:
data = self.db.chats( # data = self.db.chats(
time=datetime.datetime.now(), # time=datetime.datetime.now(),
**kwargs # **kwargs
) # )
# kwargs can look like # # kwargs can look like
# receive=True, # # receive=True,
# execute=True, # # execute=True,
# send=False, # # send=False,
data.save() # data.save()
except Exception as e: # except Exception as e:
self.logger.error("sql error: {}".format(e)) # self.logger.error("sql error: {}".format(e))
def entry_point(self, update: Update, context: CallbackContext) -> None: def entry_point(self, update: Update, context: CallbackContext) -> None:
if update.message.text: if update.message.text:

View File

@ -1,44 +1,26 @@
from datetime import datetime
from peewee import * from peewee import *
# from playhouse.pool import PooledMySQLDatabase
from playhouse.shortcuts import ReconnectMixin
import logging import logging
import json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def create_tables(db):
db.create_tables([SensorMetric, ChatMetric, ErrorMetric, List])
db = DatabaseProxy() db = DatabaseProxy()
# set the nature of the db at runtime # set the nature of the db at runtime
class ReconnectDataBase(ReconnectMixin, MySQLDatabase):
pass
class DBModel(Model): class DBModel(Model):
# specific to the above DB # specific to the above DB
class Meta: class Meta:
database = db database = db
def save(self):
# fail-safe writing of the db-object. Usually threaded because the caller is threaded
try:
# db.connect()
super().save()
# db.close()
except Exception as e:
logger.error("Could not write to db. Dropping content of {}".format(self.__class__.__name__))
logger.error(e)
# db.atomic().rollback()
# def get(self, *query, **filters):
# try:
# return super().get(*query, **filters)
# except Exception as e:
# logger.error("Error while executing get: {}".format(e))
# print(query, filters)
class Metric(DBModel): class Metric(DBModel):
time = DateTimeField() time = DateTimeField(default = datetime.now())
### Actual metrics: ### Actual metrics:
@ -48,24 +30,26 @@ class SensorMetric(Metric):
temperature = IntegerField() temperature = IntegerField()
humidity = IntegerField() humidity = IntegerField()
luminosity = IntegerField() luminosity = IntegerField()
default = {"temperature": 100, "humidity": 100, "luminosity": 100}
class ChatMetric(Metric): class ChatMetric(Metric):
read = BooleanField() read = BooleanField()
send = BooleanField() send = BooleanField()
execute = BooleanField() execute = BooleanField()
default = {"read": False, "send": False, "execute": False}
class ErrorMetric(Metric): class ErrorMetric(Metric):
# same as above # same as above
error = TextField() error = TextField()
default = {"error": "SQL connection broke off"}
class List(DBModel): class List(DBModel):
name = CharField(unique=True) name = CharField(unique=True)
content = TextField() # unlimited length, use to serialise list into content = TextField() # unlimited length, use to serialise list into
default = {"content": "SQL connection broke off"}
@property
def content_as_list(self):
return json.loads(self.content)
def set_content(self, list_content):
self.content = json.dumps(list_content)

View File

@ -1,116 +0,0 @@
from . import models
from peewee import *
# from playhouse.pool import PooledMySQLDatabase
from playhouse.shortcuts import ReconnectMixin
import inspect
from . import keys
dbk = keys.db_keys
class ReconnectDataBase(ReconnectMixin, MySQLDatabase):
pass
class DBConnector:
"""Create a connection to a remote database and log some quantities that will be visualized otherwhere"""
def __init__(self):
self.db = models.db
self.sensors = models.SensorMetric
self.chats = models.ChatMetric
self.errors = models.ErrorMetric
self.lists = models.List
self.create_tables()
def create_tables(self):
self.db.create_tables([self.sensors, self.chats, self.errors, self.lists])
class DataBaseConnector:
def __init__(self) -> None:
self.db_object = models.ReconnectDataBase(
dbk["name"],
user=dbk["username"],
password=dbk["password"],
host=dbk["url"],
port=dbk["port"],
autorollback=True
)
models.db.initialize(self.db_object)
# self.sensors = models.SensorMetric
# self.chats = models.ChatMetric
# self.errors = models.ErrorMetric
# self.lists = models.List
## Set as property methods instead
self.db_object.create_tables([self.sensors, self.chats, self.errors, self.lists])
@property
def sensors(self):
self.connect_first()
return models.SensorMetric
@property
def chats(self):
self.connect_first()
return models.ChatMetric
@property
def errors(self):
self.connect_first()
return models.ErrorMetric
@property
def lists(self):
self.connect_first()
return models.List
def connect_first(self):
# if self.db_object.is_closed():
# self.db_object.connect()
self.db_object.connect(reuse_if_open=True)
# def auto_reconnect(func, *args, **kwargs):
# return func
# def classwide_decorator(decorator):
# def decorate(cls):
# for attr in inspect.getmembers(cls, inspect.ismethod): # there's propably a better way to do this
# # TODO: filter init
# print(attr)
# if callable(getattr(cls, attr)):
# setattr(cls, attr, decorator(getattr(cls, attr)))
# return cls
# return decorate
# # apply auto_reconnect to every method so that every method first checks the db connection and reconnects if necessary
# @classwide_decorator(auto_reconnect)
# class DataBaseConnector(ReconnectMixin, MySQLDatabase):
# def __init__(self, *args, **kwargs):
# super().__init__(
# dbk["name"],
# user=dbk["username"],
# password=dbk["password"],
# host=dbk["url"],
# port=dbk["port"],
# autorollback=True,
# *args, **kwargs)
# models.db.initialize(self)
# self.sensors = models.SensorMetric
# self.chats = models.ChatMetric
# self.errors = models.ErrorMetric
# self.lists = models.List
# self.create_tables([self.sensors, self.chats, self.errors, self.lists])
# def m1(self): pass
# def m2(self, x): pass

Binary file not shown.

@ -1 +0,0 @@
Subproject commit 1e24c1491d2e80ce9d915b20f434c1e10fab3156

20
test.py
View File

@ -1,20 +0,0 @@
class MyResource:
def __enter__(self):
print('Entering context.')
return self
def __exit__(self, *exc):
print('EXITING context.')
def try_me(self):
print("I work")
def fun():
with MyResource() as a:
print('Returning inside with-statement.')
return a
print('Returning outside with-statement.')
t =fun()
t.try_me()