Dockerized and fixed errors
This commit is contained in:
1
app/bot/__init__.py
Normal file
1
app/bot/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder
|
12
app/bot/api/__init__.py
Normal file
12
app/bot/api/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from . import reddit
|
||||
from . import weather
|
||||
from . import reddit
|
||||
from . import search
|
||||
from . import metmuseum
|
||||
import os
|
||||
if os.getenv("dockerized", "") == "true":
|
||||
import sys
|
||||
sys.path.append("/keys")
|
||||
import api_keys as keys
|
||||
else:
|
||||
from . import keys
|
39
app/bot/api/metmuseum.py
Normal file
39
app/bot/api/metmuseum.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import requests
|
||||
import random
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
class ArtFetch:
|
||||
def __init__(self):
|
||||
self.base_url = "https://collectionapi.metmuseum.org/"
|
||||
self.objects = self.fetch_objects() # chosen set of images to select randomly
|
||||
|
||||
|
||||
def fetch_objects(self):
|
||||
"""We restrict ourselves to a few domains."""
|
||||
# fetch all departements
|
||||
t = requests.get(self.base_url + "public/collection/v1/departments").json()
|
||||
deps = t["departments"]
|
||||
keep_id = []
|
||||
for d in deps:
|
||||
name = d["displayName"]
|
||||
if name == "American Decorative Arts" or name == "Arts of Africa, Oceania, and the Americas" or name == "Asian Art" or name == "European Paintings":
|
||||
keep_id.append(str(d["departmentId"]))
|
||||
# fetch artworks listed under these departments
|
||||
data = {"departmentIds" : "|".join(keep_id)}
|
||||
t = requests.get(self.base_url + "public/collection/v1/objects",params=data).json()
|
||||
# num = t["total"]
|
||||
ids = t["objectIDs"]
|
||||
return ids
|
||||
|
||||
def get_random_art(self):
|
||||
"""Returns an image object of a randomly selected artwork"""
|
||||
# fetch the artwork's url
|
||||
r_id = self.objects[random.randint(0,len(self.objects))]
|
||||
t = requests.get(self.base_url + "public/collection/v1/objects/" + str(r_id)).json()
|
||||
im_url = t["primaryImageSmall"]
|
||||
# download the image
|
||||
resp = requests.get(im_url)
|
||||
img = Image.open(io.BytesIO(resp.content))
|
||||
|
||||
return img
|
56
app/bot/api/reddit.py
Normal file
56
app/bot/api/reddit.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import praw
|
||||
|
||||
|
||||
|
||||
class RedditFetch():
|
||||
def __init__(self, key):
|
||||
self.stream = praw.Reddit(client_id = key["id"], client_secret = key["secret"], user_agent=key["user_agent"])
|
||||
|
||||
def get_top(self, subreddit, number, return_type="text"):
|
||||
if return_type == "text":
|
||||
posts = []
|
||||
try:
|
||||
for submission in self.stream.subreddit(subreddit).top(limit=number):
|
||||
p = {}
|
||||
if not submission.stickied:
|
||||
p["title"] = submission.title
|
||||
p["content"] = submission.selftext
|
||||
posts.append(p)
|
||||
return posts
|
||||
except:
|
||||
return []
|
||||
else:
|
||||
images = []
|
||||
try:
|
||||
for submission in self.stream.subreddit(subreddit).top(limit=number):
|
||||
if not submission.stickied:
|
||||
t = {"image": submission.url, "caption": submission.title}
|
||||
images.append(t)
|
||||
return images
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
def get_random_rising(self, subreddit, number, return_type="text"):
|
||||
if return_type == "text":
|
||||
posts = []
|
||||
try:
|
||||
for submission in self.stream.subreddit(subreddit).random_rising(limit=number):
|
||||
p = {}
|
||||
if not submission.stickied:
|
||||
p["title"] = submission.title
|
||||
p["content"] = submission.selftext
|
||||
posts.append(p)
|
||||
return posts
|
||||
except:
|
||||
return []
|
||||
else:
|
||||
images = []
|
||||
try:
|
||||
for submission in self.stream.subreddit(subreddit).random_rising(limit=number):
|
||||
if not submission.stickied:
|
||||
t = {"image": submission.url, "caption": submission.title}
|
||||
images.append(t)
|
||||
return images
|
||||
except:
|
||||
return []
|
21
app/bot/api/search.py
Normal file
21
app/bot/api/search.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import duckduckpy
|
||||
|
||||
class WebSearch():
|
||||
def __init__(self):
|
||||
self.search = duckduckpy.query
|
||||
|
||||
def get_result(self, query):
|
||||
try:
|
||||
res = []
|
||||
response = self.search(query, container = "dict")["related_topics"]
|
||||
for r in response:
|
||||
if "text" in r:
|
||||
res.append({
|
||||
"text" : r["text"],
|
||||
"url": r["first_url"]
|
||||
})
|
||||
except:
|
||||
res = ["Connection error"]
|
||||
return res
|
||||
|
||||
# TODO: this api has more potential. Extract images or quick facts!
|
50
app/bot/api/weather.py
Normal file
50
app/bot/api/weather.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import requests
|
||||
import datetime
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
class WeatherFetch():
|
||||
def __init__(self, key):
|
||||
self.last_fetch = datetime.datetime.fromtimestamp(0)
|
||||
self.last_fetch_location = []
|
||||
self.last_weather = []
|
||||
self.calls = 0
|
||||
|
||||
self.url = "https://api.openweathermap.org/data/2.5/onecall?"
|
||||
self.key = key
|
||||
|
||||
def show_weather(self, location):
|
||||
delta = datetime.datetime.now() - self.last_fetch
|
||||
# 1 hour passed, error, or location change
|
||||
if delta.total_seconds() > 3600 \
|
||||
or len(self.last_weather) == 0\
|
||||
or self.last_fetch_location != location:
|
||||
|
||||
data = {"lat" : location[0], "lon" : location[1], "exclude" : "minutely,hourly", "appid" : self.key, "units" : "metric"}
|
||||
self.calls += 1
|
||||
logger.info("Just fetched weather. ({}th time)".format(self.calls))
|
||||
|
||||
try:
|
||||
weather = requests.get(self.url,params=data).json()
|
||||
now = weather["current"]
|
||||
ret_weather = []
|
||||
ret_weather.append({
|
||||
"short" : now["weather"][0]["main"],
|
||||
"temps" : [int(now["temp"])]
|
||||
})
|
||||
weather_days = weather["daily"]
|
||||
for i, day in enumerate(weather_days):
|
||||
ret_weather.append({
|
||||
"short" : day["weather"][0]["main"],
|
||||
"temps" : [int(day["temp"]["min"]),int(day["temp"]["max"])]
|
||||
})
|
||||
|
||||
self.last_fetch_location = location
|
||||
self.last_weather = ret_weather
|
||||
self.last_fetch = datetime.datetime.now()
|
||||
except:
|
||||
ret_weather = []
|
||||
else:
|
||||
ret_weather = self.last_weather
|
||||
|
||||
return ret_weather
|
2
app/bot/commands/__init__.py
Normal file
2
app/bot/commands/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Placeholder
|
||||
from . import clock, help, weather, status, zvv, lists, alias, plaintext, reddit, search
|
64
app/bot/commands/alias.py
Normal file
64
app/bot/commands/alias.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from .template import *
|
||||
|
||||
FIRST = range(1)
|
||||
class Alias(BotFunc):
|
||||
"""create a new command for command-paths you often use"""
|
||||
|
||||
def __init__(self, dispatcher, db):
|
||||
super().__init__(db)
|
||||
self.dispatcher = dispatcher
|
||||
# do not interact with him yet!
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('alias', self.entry_point)],
|
||||
states={
|
||||
FIRST: [
|
||||
CallbackQueryHandler(self.print_all, pattern="^all$"),
|
||||
CallbackQueryHandler(self.create_alias, pattern="^new$"),
|
||||
CallbackQueryHandler(self.delete_alias, pattern='^delete$'),
|
||||
]
|
||||
},
|
||||
fallbacks=[CommandHandler('alias', self.entry_point)],
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
test = self.dispatcher
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("All aliases", callback_data="all")],
|
||||
[InlineKeyboardButton("Create new alias", callback_data="new")],
|
||||
[InlineKeyboardButton("Delete alias", callback_data="delete")],
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
super().log_activity(receive=True, execute=False, send=True)
|
||||
update.message.reply_text("What exactly do you want?", reply_markup=reply_markup)
|
||||
return FIRST
|
||||
|
||||
|
||||
def print_all(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
all_alias = ""
|
||||
for k in self.persistence["bot"]["aliases"]:
|
||||
all_alias += k + " - " + self.persistence["bot"]["aliases"] +"\n"
|
||||
|
||||
query.edit_message_text(text="List of all commands:\n" + all_alias)
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def create_alias(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
all_alias = ""
|
||||
for k in self.persistence["bot"]["aliases"]:
|
||||
all_alias += k + " - " + self.persistence["bot"]["aliases"] +"\n"
|
||||
|
||||
query.edit_message_text(text="List of all commands:\n" + all_alias)
|
||||
return ConversationHandler.END
|
||||
|
||||
def delete_alias(self, update: Update, context: CallbackContext) -> None:
|
||||
return ConversationHandler.END
|
206
app/bot/commands/clock.py
Normal file
206
app/bot/commands/clock.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from .template import *
|
||||
import time
|
||||
import numpy
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
CHOOSE, ADDARG = range(2)
|
||||
MESSAGE, WAKE, ALARM, IMAGE, ART = range(3,8)
|
||||
|
||||
class Clock(BotFunc):
|
||||
"""pass on commands to clock-module"""
|
||||
def __init__(self, db_utils, clock_module, art_api):
|
||||
super().__init__(db_utils)
|
||||
self.clock = clock_module
|
||||
self.art_api = art_api
|
||||
|
||||
def create_handler(self):
|
||||
handler = ConversationHandler(
|
||||
entry_points=[CommandHandler("clock", self.entry_point)],
|
||||
states={
|
||||
CHOOSE : [
|
||||
CallbackQueryHandler(self.wake_light, pattern="^wake$"),
|
||||
CallbackQueryHandler(self.alarm_blink, pattern="^alarm$"),
|
||||
CallbackQueryHandler(self.show_message, pattern="^message$"),
|
||||
CallbackQueryHandler(self.show_image, pattern="^image$"),
|
||||
CallbackQueryHandler(self.art_gallery, pattern="^gallery$"),
|
||||
],
|
||||
ADDARG : [MessageHandler(Filters.text, callback=self.get_arg1)],
|
||||
MESSAGE: [MessageHandler(Filters.text, callback=self.exec_show_message)],
|
||||
WAKE : [MessageHandler(Filters.text, callback=self.exec_wake_light)],
|
||||
ALARM : [MessageHandler(Filters.text, callback=self.exec_alarm_blink)],
|
||||
IMAGE : [MessageHandler(Filters.photo, callback=self.exec_show_image)],
|
||||
ART : [MessageHandler(Filters.text, callback=self.exec_art_gallery)],
|
||||
},
|
||||
fallbacks=[CommandHandler('clock', self.entry_point)],
|
||||
)
|
||||
return handler
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("Make a wake-light", callback_data="wake")],
|
||||
[InlineKeyboardButton("Blink as alarm", callback_data="alarm")],
|
||||
[InlineKeyboardButton("Show a message", callback_data="message")],
|
||||
[InlineKeyboardButton("Show an image", callback_data="image")],
|
||||
[InlineKeyboardButton("Art gallery!", callback_data="gallery")],
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
update.message.reply_text("What exactly do you want?", reply_markup=reply_markup)
|
||||
return CHOOSE
|
||||
|
||||
|
||||
def wake_light(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
query.edit_message_text("Ok. How long should the color cycle last? (In seconds)")
|
||||
return WAKE
|
||||
|
||||
def alarm_blink(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
query.edit_message_text("Ok. How long should it blink? (In seconds)")
|
||||
self.next_state = {ALARM : "What frequency (Hertz)"}
|
||||
return ADDARG
|
||||
|
||||
def show_message(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
query.edit_message_text("Ok. What message will I show?")
|
||||
return MESSAGE
|
||||
|
||||
def show_image(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
query.edit_message_text("How long (in minutes) should the image be displayed?")
|
||||
self.next_state = {IMAGE : "Please send me the photo to display."}
|
||||
return ADDARG
|
||||
|
||||
def art_gallery(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
query.edit_message_text("Ok. How long should we display art? (in hours")
|
||||
self.next_state = {ART : "And how many artworks would you like to see during that time?"}
|
||||
return ADDARG
|
||||
|
||||
def get_arg1(self, update: Update, context: CallbackContext) -> None:
|
||||
a = update.message.text
|
||||
self.additional_argument = a
|
||||
update.message.reply_text("Furthermore: "+ list(self.next_state.values())[0])
|
||||
return list(self.next_state.keys())[0]
|
||||
|
||||
|
||||
|
||||
|
||||
###### actually running clock actions
|
||||
def exec_wake_light(self, update: Update, context: CallbackContext) -> None:
|
||||
duration = update.message.text
|
||||
|
||||
matrices = []
|
||||
start_color = numpy.array([153, 0, 51])
|
||||
end_color = numpy.array([255, 255, 0])
|
||||
col_show = numpy.zeros((*self.clock.MOP.shape, 3))
|
||||
col_show[:,:,...] = start_color
|
||||
|
||||
gradient = end_color - start_color
|
||||
# steps are shown at a frequency of ?? frames / second =>
|
||||
for i in range(duration * 2): # / 0.5
|
||||
ct = i/20 * gradient
|
||||
col_show[:,:,...] = [int(x) for x in ct+start_color]
|
||||
matrices.append(col_show)
|
||||
|
||||
self.clock.out.queue.append({"matrices" : matrices})
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def exec_alarm_blink(self, update: Update, context: CallbackContext) -> None:
|
||||
duration = self.additional_argument
|
||||
|
||||
matrices = []
|
||||
duration = int(duration * 2)
|
||||
empty = numpy.zeros((*self.clock.MOP.shape,3))
|
||||
red = numpy.ones_like(empty) * 255
|
||||
|
||||
for _ in range(int(duration / 2)):
|
||||
matrices.append(red)
|
||||
matrices.append(empty)
|
||||
|
||||
self.clock.out.queue.append({"matrices": matrices})
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
|
||||
def exec_show_image(self, update: Update, context: CallbackContext) -> None:
|
||||
duration = self.additional_argument
|
||||
i = update.message.photo
|
||||
img = update.message.photo[0]
|
||||
bot = img.bot
|
||||
id = img.file_id
|
||||
|
||||
file = bot.getFile(id).download_as_bytearray()
|
||||
width = self.clock.shape[1]
|
||||
height = self.clock.shape[0]
|
||||
|
||||
img = Image.open(io.BytesIO(file))
|
||||
im_height = img.height
|
||||
im_width = img.width
|
||||
|
||||
scalex = im_width // width
|
||||
scaley = im_height // height
|
||||
scale = min(scalex, scaley)
|
||||
|
||||
t = img.resize((width, height),box=(0,0,width*scale,height*scale))
|
||||
a = numpy.asarray(t)
|
||||
|
||||
matrices = [a for _ in range(2*60*duration)]
|
||||
|
||||
self.clock.out.queue.append({"matrices": matrices})
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def exec_show_message(self, update: Update, context: CallbackContext) -> None:
|
||||
message_str = update.message.text
|
||||
update.message.reply_text("Now showing: " + message_str)
|
||||
self.clock.run(self.clock.text_scroll,(message_str,))
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def exec_art_gallery(self, update: Update, context: CallbackContext) -> None:
|
||||
duration = float(self.additional_argument)
|
||||
number = int(update.message.text)
|
||||
|
||||
def output(number, duration):
|
||||
for i in range(number):
|
||||
img = self.art_api.get_random_art() # returns an PIL.Image object
|
||||
im_height = img.height
|
||||
im_width = img.width
|
||||
|
||||
width = self.clock.shape[1]
|
||||
height = self.clock.shape[0]
|
||||
|
||||
scalex = im_width // width
|
||||
scaley = im_height // height
|
||||
scale = min(scalex, scaley)
|
||||
|
||||
t = img.resize((width, height),box=(0,0,width*scale,height*scale))
|
||||
a = numpy.asarray(t)
|
||||
self.clock.IO.put(a)
|
||||
|
||||
time.sleep(duration*3600 / number)
|
||||
|
||||
|
||||
update.message.reply_text("Ok. Showing art for the next "+ str(duration) + " hours.")
|
||||
self.clock.run(output,(number, duration))
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# TODO FIx this to work with the new backend
|
128
app/bot/commands/help.py
Normal file
128
app/bot/commands/help.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from .template import *
|
||||
|
||||
FIRST, EXECUTE = range(2)
|
||||
|
||||
|
||||
class Help(BotFunc):
|
||||
"""Shows the functions and their usage"""
|
||||
|
||||
def __init__(self, db):
|
||||
super().__init__(db)
|
||||
self.available_commands = {}
|
||||
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('help', self.entry_point)],
|
||||
states={
|
||||
FIRST: [
|
||||
CallbackQueryHandler(self.print_all, pattern="^all$"),
|
||||
CallbackQueryHandler(self.choose_specific, pattern="^specific$"),
|
||||
CallbackQueryHandler(self.print_one, pattern='func-'),
|
||||
],
|
||||
EXECUTE :[CallbackQueryHandler(self.execute_now)],
|
||||
# ConversationHandler.TIMEOUT : [
|
||||
# CallbackQueryHandler(self.timeout)
|
||||
# ]
|
||||
},
|
||||
fallbacks=[CommandHandler('help', self.entry_point)],
|
||||
conversation_timeout=15,
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
def add_commands(self, commands):
|
||||
# commands is a dict {"name": class}
|
||||
for k in commands:
|
||||
if k != "plaintext":
|
||||
self.available_commands[k] = commands[k].__doc__
|
||||
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("All commands", callback_data="all"),
|
||||
InlineKeyboardButton("Just one", callback_data="specific"),
|
||||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
super().log_activity(read=True, execute=True, send=True) # at this point every step has been fulfilled
|
||||
if update.message:
|
||||
update.message.reply_text("What exactly do you want?", reply_markup=reply_markup)
|
||||
else:
|
||||
update._effective_chat.send_message("What exactly do you want?", reply_markup=reply_markup)
|
||||
return FIRST
|
||||
|
||||
|
||||
def print_all(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
all_cmd = ""
|
||||
for h in self.available_commands:
|
||||
all_cmd += "{} - `{}`\n".format(h, self.available_commands[h])
|
||||
|
||||
query.edit_message_text(text="List of all commands:\n" + all_cmd, parse_mode = ParseMode.MARKDOWN)
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def choose_specific(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
|
||||
keyboard = [[InlineKeyboardButton(k, callback_data="func-" + k)] for k in self.available_commands]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
query.edit_message_text(
|
||||
text="What command should be printed?", reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
|
||||
|
||||
def print_one(self, update: Update, context: CallbackContext) -> None:
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
name = query.data.replace("func-", "")
|
||||
query.answer()
|
||||
|
||||
message = name + ": `" + self.available_commands[name] + "`"
|
||||
|
||||
keyboard = [[InlineKeyboardButton("Call " + name + " now", callback_data=name),]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
query.edit_message_text(
|
||||
text= message,
|
||||
reply_markup = reply_markup,
|
||||
parse_mode = ParseMode.MARKDOWN_V2
|
||||
)
|
||||
return EXECUTE
|
||||
|
||||
|
||||
def execute_now(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
name = query.data
|
||||
query.answer()
|
||||
funcs = context.dispatcher.handlers[0]
|
||||
for func in funcs:
|
||||
if name == func.entry_points[0].command[0]:
|
||||
break
|
||||
callback = func.entry_points[0].handle_update
|
||||
callback(update, context.dispatcher, check_result=True, context=context)
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def timeout(self, update: Update, context: CallbackContext) -> None:
|
||||
"""For dying conversation. Currently unused."""
|
||||
|
||||
query = update.callback_query
|
||||
name = query.data.replace("func-", "")
|
||||
query.answer()
|
||||
|
||||
message = name + ": `" + self.available_commands[name] + "`"
|
||||
query.edit_message_text(
|
||||
text= "Timed out...",
|
||||
parse_mode = ParseMode.MARKDOWN_V2
|
||||
)
|
||||
return ConversationHandler.END
|
184
app/bot/commands/lists.py
Normal file
184
app/bot/commands/lists.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from .template import *
|
||||
|
||||
NAME, NEW, ACTION, ITEMADD, ITEMREMOVE = range(5)
|
||||
|
||||
|
||||
class Lists(BotFunc):
|
||||
"""Create and edit lists"""
|
||||
|
||||
def __init__(self, db_utils):
|
||||
super().__init__(db_utils)
|
||||
self.current_name = ""
|
||||
# self.db_utils set through super()
|
||||
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('list', self.entry_point)],
|
||||
states={
|
||||
NAME: [
|
||||
CallbackQueryHandler(self.choose_list, pattern="^list-"),
|
||||
CallbackQueryHandler(self.new_list, pattern="^new$"),
|
||||
],
|
||||
NEW : [MessageHandler(Filters.text, callback=self.new_listname)],
|
||||
ACTION: [
|
||||
CallbackQueryHandler(self.list_add, pattern="^add$"),
|
||||
CallbackQueryHandler(self.list_remove, pattern="^remove$"),
|
||||
CallbackQueryHandler(self.list_clear, pattern="^clear$"),
|
||||
CallbackQueryHandler(self.list_delete, pattern="^delete$"),
|
||||
CallbackQueryHandler(self.list_print, pattern="^print$"),
|
||||
CallbackQueryHandler(self.list_menu, pattern="^overview$"),
|
||||
],
|
||||
ITEMADD : [MessageHandler(Filters.text, callback=self.list_add_item)],
|
||||
ITEMREMOVE : [CallbackQueryHandler(self.list_remove_index)]
|
||||
},
|
||||
fallbacks=[CommandHandler('list', self.entry_point)],
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
lists = self.db_utils.list_get()
|
||||
keyboard = [[InlineKeyboardButton(k, callback_data="list-"+k)] for k in lists] + [[InlineKeyboardButton("New list", callback_data="new")]]
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
super().log_activity(read=True, execute=False, send=True)
|
||||
update.message.reply_text(text="Here are the existing lists. You can also create a new one:", reply_markup=reply_markup)
|
||||
return NAME
|
||||
|
||||
|
||||
def choose_list(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
name = data.replace("list-","")
|
||||
query.answer()
|
||||
self.current_name = name
|
||||
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("Add item", callback_data="add")],
|
||||
[InlineKeyboardButton("Remove item", callback_data="remove")],
|
||||
[InlineKeyboardButton("Clear list", callback_data="clear")],
|
||||
[InlineKeyboardButton("Print list", callback_data="print")],
|
||||
[InlineKeyboardButton("Delete list", callback_data="delete")],
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
query.edit_message_text("Very well. For " + name + " the following actions are available:", reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
def list_menu(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("Add item", callback_data="add")],
|
||||
[InlineKeyboardButton("Remove item", callback_data="remove")],
|
||||
[InlineKeyboardButton("Clear list", callback_data="clear")],
|
||||
[InlineKeyboardButton("Print list", callback_data="print")],
|
||||
[InlineKeyboardButton("Delete list", callback_data="delete")],
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
query.edit_message_text("Very well. For " + self.current_name + " the following actions are available:", reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
def new_list(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
query.edit_message_text("What's the name of the new list?")
|
||||
return NEW
|
||||
|
||||
|
||||
def new_listname(self, update: Update, context: CallbackContext) -> None:
|
||||
name = update.message.text
|
||||
try:
|
||||
self.db_utils.list_create(name)
|
||||
keyboard = [[InlineKeyboardButton("Add an item", callback_data="add"), InlineKeyboardButton("To the menu!", callback_data="overview")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
self.current_name = name
|
||||
update.message.reply_text("Thanks. List " + name + " was successfully created.", reply_markup=reply_markup)
|
||||
super().log_activity(read=False, execute=True, send=True)
|
||||
return ACTION
|
||||
except Exception as e:
|
||||
update.message.reply_text("Oh no! Encountered exception: {}".format(e))
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def list_add(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
query.edit_message_text("What would you like to add?")
|
||||
return ITEMADD
|
||||
|
||||
|
||||
def list_remove(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
sl = self.db_utils.list_get(self.current_name)
|
||||
|
||||
keyboard = [[InlineKeyboardButton(k, callback_data=i)] for i,k in enumerate(sl)]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
query.edit_message_text("Which item would you like to remove?", reply_markup = reply_markup)
|
||||
return ITEMREMOVE
|
||||
|
||||
|
||||
def list_clear(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
self.db_utils.list_update(self.current_name, replace=[])
|
||||
keyboard = [[InlineKeyboardButton("Add an item", callback_data="add"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text("List " + self.current_name + " cleared", reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
def list_delete(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
self.db_utils.list_delete(self.current_name)
|
||||
query.edit_message_text("List " + self.current_name + " deleted")
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def list_print(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
it = self.db_utils.list_get(self.current_name)
|
||||
if it:
|
||||
content = "\n".join(it)
|
||||
else:
|
||||
content = "List empty"
|
||||
|
||||
keyboard = [[InlineKeyboardButton("Add an item", callback_data="add"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text("Content of " + self.current_name + ":\n" + content, reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
def list_add_item(self, update: Update, context: CallbackContext) -> None:
|
||||
item = update.message.text
|
||||
self.db_utils.list_update(self.current_name, append=item)
|
||||
keyboard = [[InlineKeyboardButton("Add some more", callback_data="add"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
update.message.reply_text("Added " + item, reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
def list_remove_index(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
ind = int(query.data)
|
||||
query.answer()
|
||||
|
||||
old = self.db_utils.list_get(self.current_name)
|
||||
name = old.pop(ind)
|
||||
self.db_utils.list_update(self.current_name, replace=old)
|
||||
|
||||
keyboard = [[InlineKeyboardButton("Remove another", callback_data="remove"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
query.edit_message_text("Removed " + name, reply_markup=reply_markup)
|
||||
return ACTION
|
19
app/bot/commands/plaintext.py
Normal file
19
app/bot/commands/plaintext.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from .template import *
|
||||
|
||||
|
||||
class Plain(BotFunc):
|
||||
"""Not a command: just keeps logs and usage_data"""
|
||||
def __init__(self, db_utils):
|
||||
super().__init__(db_utils)
|
||||
|
||||
def create_handler(self):
|
||||
h = MessageHandler(Filters.text, callback=self.add_to_log)
|
||||
return h
|
||||
|
||||
def add_to_log(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
super().log_activity(
|
||||
read = True,
|
||||
send = False,
|
||||
execute = False
|
||||
)
|
179
app/bot/commands/reddit.py
Normal file
179
app/bot/commands/reddit.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from re import U
|
||||
from .template import *
|
||||
|
||||
|
||||
CHOOSE_NUM = 1
|
||||
class Joke(BotFunc):
|
||||
"""Tells a joke from reddit."""
|
||||
|
||||
def __init__(self, api, db):
|
||||
super().__init__(db)
|
||||
self.available_commands = {}
|
||||
self.api = api
|
||||
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('joke', self.entry_point)],
|
||||
states={
|
||||
CHOOSE_NUM: [CallbackQueryHandler(self.get_jokes),],
|
||||
},
|
||||
fallbacks=[CommandHandler('joke', self.entry_point)],
|
||||
# conversation_timeout=5,
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
keyboard = [[InlineKeyboardButton(str(i), callback_data=str(i)) for i in range(1,11)]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
super().log_activity(read=True, execute=True, send=True) # at this point every step has been fulfilled
|
||||
update.message.reply_text("How many jokes?", reply_markup=reply_markup)
|
||||
return CHOOSE_NUM
|
||||
|
||||
|
||||
def get_jokes(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
number = int(query.data)
|
||||
query.answer()
|
||||
jokes = self.api.get_random_rising("jokes", number, "text")
|
||||
# formating
|
||||
message = ""
|
||||
for j in jokes:
|
||||
message += "<b>" + j["title"] + "</b> \n" + j["content"] + "\n\n"
|
||||
if message == "":
|
||||
message += "Could not fetch jokes."
|
||||
query.edit_message_text(text = message, parse_mode = ParseMode.HTML)
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
|
||||
|
||||
CHOOSE_TOPIC = 0
|
||||
class Meme(BotFunc):
|
||||
"""Gets the latest memes from reddit"""
|
||||
|
||||
def __init__(self, api, db):
|
||||
super().__init__(db)
|
||||
self.available_commands = {}
|
||||
self.api = api
|
||||
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('meme', self.entry_point)],
|
||||
states={
|
||||
CHOOSE_TOPIC: [CallbackQueryHandler(self.choose_topic)],
|
||||
CHOOSE_NUM :[CallbackQueryHandler(self.get_memes)],
|
||||
},
|
||||
fallbacks=[CommandHandler('meme', self.entry_point)],
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("General", callback_data="memes"),],
|
||||
[InlineKeyboardButton("Dank memes", callback_data="dankmemes"),],
|
||||
[InlineKeyboardButton("Maths", callback_data="mathmemes"),],
|
||||
[InlineKeyboardButton("Physics", callback_data="physicsmemes"),],
|
||||
[InlineKeyboardButton("Biology", callback_data="biologymemes"),],
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
super().log_activity(read=True, execute=True, send=True) # at this point every step has been fulfilled
|
||||
update.message.reply_text("What kind of memes?", reply_markup=reply_markup)
|
||||
return CHOOSE_TOPIC
|
||||
|
||||
|
||||
def choose_topic(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
d = query.data
|
||||
query.answer()
|
||||
|
||||
keyboard = [[InlineKeyboardButton(str(i), callback_data=d + "-" + str(i)) for i in range(1,11)]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text("How many memes?", reply_markup=reply_markup)
|
||||
return CHOOSE_NUM
|
||||
|
||||
|
||||
def get_memes(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
data = query.data.split("-")
|
||||
query.answer()
|
||||
|
||||
memes = self.api.get_random_rising(data[0], int(data[1]), "photo")
|
||||
if len(memes) != 0:
|
||||
for m in memes:
|
||||
super().log_activity(read=False, execute=False, send=True) # we just sent an additional message
|
||||
update.effective_chat.send_photo(photo = m["image"],caption = m["caption"])
|
||||
else:
|
||||
update.effective_chat.send_message("Sorry, the meme won't yeet.")
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
|
||||
|
||||
# class News(BotFunc):
|
||||
# """Gets the latest news from reddit"""
|
||||
|
||||
# def __init__(self, api, prst):
|
||||
# super().__init__(prst)
|
||||
# self.available_commands = {}
|
||||
# self.api = api
|
||||
|
||||
|
||||
# def create_handler(self):
|
||||
# conv_handler = ConversationHandler(
|
||||
# entry_points=[CommandHandler('news', self.entry_point)],
|
||||
# states={
|
||||
# CHOOSE_TOPIC: [CallbackQueryHandler(self.choose_topic)],
|
||||
# CHOOSE_NUM :[CallbackQueryHandler(self.get_news)],
|
||||
# },
|
||||
# fallbacks=[CommandHandler('news', self.entry_point)],
|
||||
# )
|
||||
# return conv_handler
|
||||
|
||||
|
||||
# def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
# super().entry_point()
|
||||
|
||||
# keyboard = [
|
||||
# [InlineKeyboardButton("World", callback_data="worldnews"),],
|
||||
# [InlineKeyboardButton("Germany", callback_data="germannews"),],
|
||||
# [InlineKeyboardButton("France", callback_data="francenews"),],
|
||||
# [InlineKeyboardButton("Europe", callback_data="eunews"),],
|
||||
# [InlineKeyboardButton("USA", callback_data="usanews"),],
|
||||
# ]
|
||||
# reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
# update.message.reply_text("What kind of news?", reply_markup=reply_markup)
|
||||
# return CHOOSE_TOPIC
|
||||
|
||||
|
||||
# def choose_topic(self, update: Update, context: CallbackContext) -> None:
|
||||
# super().entry_point()
|
||||
# query = update.callback_query
|
||||
# d = query.data
|
||||
# query.answer()
|
||||
|
||||
# keyboard = [[InlineKeyboardButton(str(i), callback_data=d + "-" + str(i)) for i in range(1,11)]]
|
||||
# reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
# query.edit_message_text("How many entries?", reply_markup=reply_markup)
|
||||
# return CHOOSE_NUM
|
||||
|
||||
|
||||
# def get_news(self, update: Update, context: CallbackContext) -> None:
|
||||
# query = update.callback_query
|
||||
# data = query.data.split("-")
|
||||
# query.answer()
|
||||
# #try:
|
||||
# news = self.api.get_top(data[0], data[1], "text")
|
||||
# # formating
|
||||
# message = ""
|
||||
# for j in news:
|
||||
# message += "<b>" + j["title"] + "</b> \n" + j["content"] + "\n\n"
|
||||
# if message == "":
|
||||
# message += "Could not fetch news."
|
||||
# query.edit_message_text(news, paresemode=ParseMode.HTML)
|
||||
# return ConversationHandler.END
|
61
app/bot/commands/search.py
Normal file
61
app/bot/commands/search.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from .template import *
|
||||
|
||||
|
||||
SEARCH, MORE = range(2)
|
||||
class Search(BotFunc):
|
||||
"""Browse the web for a topic."""
|
||||
|
||||
def __init__(self, api, db):
|
||||
super().__init__(db)
|
||||
self.available_commands = {}
|
||||
self.api = api
|
||||
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('search', self.entry_point)],
|
||||
states={
|
||||
SEARCH: [MessageHandler(Filters.text, self.get_results),],
|
||||
MORE: [CallbackQueryHandler(self.show_more, pattern="^more$"),],
|
||||
},
|
||||
fallbacks=[CommandHandler('search', self.entry_point)],
|
||||
conversation_timeout=20,
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
update.message.reply_text("What are we searching?")
|
||||
return SEARCH
|
||||
|
||||
|
||||
def get_results(self, update: Update, context: CallbackContext) -> None:
|
||||
search = update.message.text
|
||||
results = self.api.get_result(search)
|
||||
keyboard = [[InlineKeyboardButton("More!", callback_data="more")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
# formating
|
||||
self.results = results
|
||||
if results:
|
||||
first = results[0]
|
||||
message = first["text"] + "\n(" + first["url"] + ")\n\n"
|
||||
else:
|
||||
message = "No results for search query."
|
||||
update.message.reply_text(text = message, reply_markup=reply_markup)
|
||||
super().log_activity(read = True, execute = True, send = True)
|
||||
return MORE
|
||||
|
||||
|
||||
def show_more(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
|
||||
message = ""
|
||||
for r in self.results:
|
||||
message += r["text"] + "\n(" + r["url"] + ")\n\n"
|
||||
|
||||
query.edit_message_text(message)
|
||||
super().log_activity(read = False, execute = False, send = True)
|
||||
return ConversationHandler.END
|
103
app/bot/commands/status.py
Normal file
103
app/bot/commands/status.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from .template import *
|
||||
|
||||
import datetime
|
||||
import requests
|
||||
import socket
|
||||
import json
|
||||
|
||||
|
||||
FIRST = 1
|
||||
|
||||
class Status(BotFunc):
|
||||
"""Shows a short status of the program."""
|
||||
|
||||
def __init__(self, name, version, db_utils):
|
||||
super().__init__(db_utils)
|
||||
self.start_time = datetime.datetime.now()
|
||||
self.name = name
|
||||
self.version = version
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('status', self.entry_point)],
|
||||
states={
|
||||
FIRST: [
|
||||
CallbackQueryHandler(self.send_log, pattern="^full$"),
|
||||
]
|
||||
},
|
||||
fallbacks=[CommandHandler('status', self.entry_point)],
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("And the log?", callback_data="full"),
|
||||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
delta = str(datetime.datetime.now() - self.start_time)
|
||||
message = "BeebBop, this is " + self.name + " (V." + self.version + ")\n"
|
||||
|
||||
try:
|
||||
ip = requests.get('https://api.ipify.org').text
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(('8.8.8.8', 80))
|
||||
(addr, port) = s.getsockname()
|
||||
local_ips = addr
|
||||
except:
|
||||
ip = "not fetchable"
|
||||
local_ips = "not fetchable"
|
||||
|
||||
message += "Status: Running 🟢\n"
|
||||
message += "Uptime: `" + delta[:delta.rfind(".")] + "`\n"
|
||||
# message += "Reboots: `" + str(self.persistence["global"]["reboots"]) + "`\n"
|
||||
message += "IP (public): `" + ip + "`\n"
|
||||
message += "IP (private): `" + str(local_ips) + "`\n"
|
||||
u = str(self.get_ngrok_url())
|
||||
message += "URL: [" + u + "](" + u + ")\n"
|
||||
|
||||
# TODO new DB
|
||||
tot_r = self.db_utils.chat_count("read")
|
||||
message += "Total messages read: `{}`\n".format(tot_r)
|
||||
|
||||
tot_s = self.db_utils.chat_count("send")
|
||||
message += "Total messages sent: `{}`\n".format(tot_s)
|
||||
|
||||
tot_e = self.db_utils.chat_count("execute")
|
||||
message += "Total commands executed: `{}`\n".format(tot_e)
|
||||
|
||||
if update.message:
|
||||
update.message.reply_text(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
|
||||
else:
|
||||
update._effective_chat.send_message(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
super().log_activity(read = True, execute = True, send = True)
|
||||
return FIRST
|
||||
|
||||
|
||||
def send_log(self, update: Update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
wanted = query.data.replace("status-","")
|
||||
query.answer()
|
||||
with open("persistence/server.log") as l:
|
||||
query.message.reply_document(l)
|
||||
|
||||
super().log_activity(read = False, execute = False, send = True)
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def get_ngrok_url(self):
|
||||
try:
|
||||
url = "http://localhost:4040/api/tunnels/"
|
||||
res = requests.get(url)
|
||||
res_unicode = res.content.decode("utf-8")
|
||||
res_json = json.loads(res_unicode)
|
||||
for i in res_json["tunnels"]:
|
||||
if i['name'] == 'command_line':
|
||||
return i['public_url']
|
||||
except:
|
||||
return "Not available"
|
31
app/bot/commands/template.py
Normal file
31
app/bot/commands/template.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, ParseMode
|
||||
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext, MessageHandler, Filters
|
||||
from telegram.ext import (
|
||||
Updater,
|
||||
CommandHandler,
|
||||
CallbackQueryHandler,
|
||||
ConversationHandler,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
class BotFunc():
|
||||
"""Base class for a specific bot-functionality"""
|
||||
def __init__(self, db_utils):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.db_utils = db_utils
|
||||
|
||||
def log_activity(self, **kwargs):
|
||||
# mark that a new command has been executed
|
||||
self.db_utils.sensor_log(**kwargs)
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
if update.message.text:
|
||||
self.logger.info("Chat said: {}".format(update.message.text))
|
||||
else:
|
||||
self.logger.info("Chat said: {}".format(update.message))
|
||||
|
117
app/bot/commands/weather.py
Normal file
117
app/bot/commands/weather.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from .template import *
|
||||
|
||||
import datetime
|
||||
|
||||
FIRST = 1
|
||||
|
||||
class Weather(BotFunc):
|
||||
"""Shows a weatherforecast for a given location"""
|
||||
def __init__(self, api, db):
|
||||
"""initialize api and persistence"""
|
||||
super().__init__(db)
|
||||
self.api = api
|
||||
self.city = ""
|
||||
|
||||
|
||||
def create_handler(self):
|
||||
"""returns the handlers with button-logic"""
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('weather', self.entry_point)],
|
||||
states={
|
||||
FIRST: [
|
||||
CallbackQueryHandler(self.choose_city, pattern="^city-"),
|
||||
CallbackQueryHandler(self.choose_time, pattern="^time-"),
|
||||
]
|
||||
},
|
||||
fallbacks=[CommandHandler('weather', self.entry_point)],
|
||||
)
|
||||
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
"""Reacts the call of the command. Prints the first buttons"""
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("Zürich", callback_data="city-zurich"),
|
||||
InlineKeyboardButton("Freiburg", callback_data="city-freiburg"),
|
||||
InlineKeyboardButton("Mulhouse", callback_data="city-mulhouse"),
|
||||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
if update.message:
|
||||
update.message.reply_text("Which city?", reply_markup=reply_markup)
|
||||
else:
|
||||
update.callback_query.edit_message_text("Which city", reply_markup=reply_markup)
|
||||
return FIRST
|
||||
|
||||
|
||||
def choose_city(self, update: Update, context: CallbackContext) -> None:
|
||||
"""Prompt same text & keyboard as `start` does but not as new message"""
|
||||
# Get CallbackQuery from Update
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
self.city = data.replace("city-","")
|
||||
query.answer()
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("Now", callback_data="time-now"),
|
||||
InlineKeyboardButton("Tomorrow", callback_data="time-tomorrow"),
|
||||
InlineKeyboardButton("7 days", callback_data="time-7"),
|
||||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text(
|
||||
text = "Which time?", reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
|
||||
|
||||
def choose_time(self, update: Update, context: CallbackContext) -> None:
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
forecast_time = query.data.replace("time-","")
|
||||
weather = self.get_weather(self.city, forecast_time)
|
||||
query.edit_message_text(
|
||||
text = "Broadcast for {}: \n\n{}".format(self.city, weather),
|
||||
parse_mode = ParseMode.HTML
|
||||
)
|
||||
super().log_activity(read = True, execute = True, send = True)
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def get_weather(self, city, forecast_time) -> None:
|
||||
"""get the weather that matches the given params"""
|
||||
locations = {"freiburg": [47.9990, 7.8421], "zurich": [47.3769, 8.5417], "mulhouse": [47.7508, 7.3359]}
|
||||
|
||||
city = locations[city]
|
||||
|
||||
categories = {"Clouds": "☁", "Rain": "🌧", "Thunderstorm": "🌩", "Drizzle": ":droplet:", "Snow": "❄", "Clear": "☀", "Mist": "🌫", "Smoke": "Smoke", "Haze": "Haze", "Dust": "Dust", "Fog": "Fog", "Sand": "Sand", "Dust": "Dust", "Ash": "Ash", "Squall": "Squall", "Tornado": "Tornado",}
|
||||
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
today = datetime.datetime.today().weekday()
|
||||
weather = self.api.show_weather(city)
|
||||
message = ""
|
||||
if forecast_time == "now" or forecast_time == "7":
|
||||
now = weather.pop(0)
|
||||
message += "<b>Now:</b> " + categories[now["short"]] + "\n"
|
||||
message += "🌡" + str(now["temps"][0]) + "°\n\n"
|
||||
tod = weather.pop(0)
|
||||
message += "<b>" + "Today" + ":</b> " + categories[tod["short"]] + "\n"
|
||||
message += "🌡 ❄ " + str(tod["temps"][0]) + "° , 🌡 🔥 " + str(tod["temps"][1]) + "°\n\n"
|
||||
|
||||
if forecast_time == "tomorrow" or forecast_time == "7":
|
||||
if forecast_time == "tomorrow": # previous statement was not executed: tomorrow is at weather[2]
|
||||
tom = weather.pop(2)
|
||||
else:
|
||||
tom = weather.pop(0)
|
||||
message += "<b>" + "Tomorrow" + ":</b> " + categories[tom["short"]] + "\n"
|
||||
message += "🌡 ❄ " + str(tom["temps"][0]) + "° , 🌡 🔥 " + str(tom["temps"][1]) + "°\n\n"
|
||||
|
||||
if forecast_time == "7":
|
||||
for i, day in enumerate(weather):
|
||||
message += "<b>" + days[(today + i + 2) % 7] + ":</b> " + categories[day["short"]] + "\n"
|
||||
message += "🌡 ❄ " + str(day["temps"][0]) + "° , 🌡 🔥 " + str(day["temps"][1]) + "°\n\n"
|
||||
|
||||
return message
|
87
app/bot/commands/zvv.py
Normal file
87
app/bot/commands/zvv.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from .template import *
|
||||
|
||||
import datetime
|
||||
import requests
|
||||
|
||||
START, DEST = range(2)
|
||||
|
||||
class Zvv(BotFunc):
|
||||
"""Connects to the swiss travel-api to get public transport routes"""
|
||||
|
||||
def __init__(self, db):
|
||||
super().__init__(db)
|
||||
self.start = ""
|
||||
self.dest = ""
|
||||
pass
|
||||
|
||||
def create_handler(self):
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('zvv', self.entry_point)],
|
||||
states={
|
||||
START: [MessageHandler(Filters.text, callback=self.get_start)],
|
||||
DEST: [MessageHandler(Filters.text, callback=self.get_dest)]
|
||||
},
|
||||
fallbacks=[CommandHandler('zvv', self.entry_point)],
|
||||
)
|
||||
return conv_handler
|
||||
|
||||
|
||||
def entry_point(self, update: Update, context: CallbackContext) -> None:
|
||||
super().entry_point(update, context)
|
||||
update.message.reply_text("What is the start point?")
|
||||
return START
|
||||
|
||||
|
||||
def get_start(self, update: Update, context: CallbackContext) -> None:
|
||||
loc = update.message.text
|
||||
self.start = loc
|
||||
update.message.reply_text("Ok. Going from " + loc + ", what is the destination?")
|
||||
return DEST
|
||||
|
||||
|
||||
def get_dest(self, update: Update, context: CallbackContext) -> None:
|
||||
loc = update.message.text
|
||||
self.dest = loc
|
||||
route = self.get_result()
|
||||
update.message.reply_text("Here are the routes I've got:\n" + route)
|
||||
super().log_activity(read=True, execute=True, send=True)
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def get_result(self):
|
||||
url = "http://transport.opendata.ch/v1/connections"
|
||||
|
||||
start = self.start
|
||||
dest = self.dest
|
||||
|
||||
data = {"from" : start, "to" : dest, "limit" : 2}
|
||||
try:
|
||||
routes = requests.get(url, params=data).json()
|
||||
result = routes["connections"]
|
||||
text = result[0]["from"]["station"]["name"] + " ⏩ " + result[0]["to"]["station"]["name"] + "\n\n"
|
||||
for con in result:
|
||||
text += "Start: " + datetime.datetime.fromtimestamp(int(con["from"]["departureTimestamp"])).strftime("%d/%m - %H:%M") + "\n"
|
||||
text += "🏁 " + datetime.datetime.fromtimestamp(int(con["to"]["arrivalTimestamp"])).strftime("%d/%m - %H:%M") + "\n"
|
||||
text += "⏳ " + con["duration"] + "\n"
|
||||
text += "🗺️ Route:\n"
|
||||
|
||||
for step in con["sections"]:
|
||||
if step["journey"] != None:
|
||||
text += step["journey"]["passList"][0]["station"]["name"] + " (" + datetime.datetime.fromtimestamp(int(step["journey"]["passList"][0]["departureTimestamp"])).strftime("%H:%M") + ")\n"
|
||||
|
||||
text += "➡️ Linie " + self.number_to_emoji(step["journey"]["number"]) + "\n"
|
||||
|
||||
text += step["journey"]["passList"][-1]["station"]["name"] + " (" + datetime.datetime.fromtimestamp(int(step["journey"]["passList"][-1]["arrivalTimestamp"])).strftime("%H:%M") +")\n"
|
||||
else:
|
||||
text += "Walk."
|
||||
text += "\n"
|
||||
return text
|
||||
except:
|
||||
return "Invalid api call."
|
||||
|
||||
def number_to_emoji(self, number):
|
||||
out = ""
|
||||
numbers = ["0️⃣","1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣"]
|
||||
for i in str(number):
|
||||
out += numbers[int(i)]
|
||||
return str(out)
|
69
app/bot/main.py
Normal file
69
app/bot/main.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext
|
||||
|
||||
from . import api, commands
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ChatBot():
|
||||
"""better framwork - unites all functions"""
|
||||
|
||||
def __init__(self, name, version):
|
||||
"""Inits the Bot with a few conf. vars
|
||||
Args: -> name:str - Name of the bot
|
||||
-> version:str - Version number
|
||||
-> hw_commands - dict with commands executable by the clock module
|
||||
-> prst:dict - persistence (overloaded dict that writes to json file)
|
||||
-> logger - logging object to unify log messages
|
||||
"""
|
||||
# added by the launcher, we have self.modules (dict) and persistence and db_utils
|
||||
|
||||
self.name = name
|
||||
self.version = version
|
||||
|
||||
# Import apis
|
||||
self.api_weather = api.weather.WeatherFetch(api.keys.weather_api)
|
||||
self.api_reddit = api.reddit.RedditFetch(api.keys.reddit_api)
|
||||
self.api_search = api.search.WebSearch()
|
||||
self.api_art = api.metmuseum.ArtFetch()
|
||||
# and so on
|
||||
|
||||
self.telegram = Updater(api.keys.telegram_api, use_context=True)
|
||||
self.dispatcher = self.telegram.dispatcher
|
||||
|
||||
|
||||
|
||||
def add_commands(self):
|
||||
# Mark modules as available
|
||||
db = self.db_utils
|
||||
# self.help_module = commands.help.Help(db)
|
||||
self.sub_modules = {
|
||||
"weather": commands.weather.Weather(self.api_weather, db),
|
||||
"help" : commands.help.Help(db),
|
||||
"status" : commands.status.Status(self.name, self.version, db),
|
||||
"zvv" : commands.zvv.Zvv(db),
|
||||
"list" : commands.lists.Lists(db),
|
||||
# "alias" : commands.alias.Alias(self.dispatcher, db),
|
||||
"joke" : commands.reddit.Joke(self.api_reddit, db),
|
||||
"meme" : commands.reddit.Meme(self.api_reddit, db),
|
||||
"search" : commands.search.Search(self.api_search, db),
|
||||
"clock" : commands.clock.Clock(self.api_art, self.modules["clock"], db),
|
||||
# ...
|
||||
"plaintext" : commands.plaintext.Plain(db)
|
||||
# for handling non-command messages that should simply contribute to statistics
|
||||
}
|
||||
# must be a class that has a method create_handler
|
||||
|
||||
for k in self.sub_modules:
|
||||
self.dispatcher.add_handler(self.sub_modules[k].create_handler())
|
||||
|
||||
# self.help_module.add_commands(self.sub_modules)
|
||||
self.sub_modules["help"].add_commands(self.sub_modules)
|
||||
|
||||
|
||||
def start(self):
|
||||
self.add_commands()
|
||||
self.telegram.start_polling(
|
||||
poll_interval=0.2,
|
||||
)
|
||||
self.telegram.idle()
|
1
app/broadcast/__init__.py
Normal file
1
app/broadcast/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder
|
71
app/broadcast/b_in.py
Normal file
71
app/broadcast/b_in.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import requests
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FetchUpdates:
|
||||
"""Fetches updates from the main server and relays them to the clock"""
|
||||
|
||||
def __init__(self, server_ip, port):
|
||||
"""Both methods return a list as python-object. This should be then converted to a numpy array."""
|
||||
# self.server_ip = server_ip
|
||||
self.base_url = server_ip + ":" + port
|
||||
# self.modules gets added through the caller
|
||||
self.update_calls = 0
|
||||
self.last_fetch = {}
|
||||
|
||||
|
||||
def start(self):
|
||||
# dummy for errorless launching
|
||||
pass
|
||||
|
||||
|
||||
def get_updates(self):
|
||||
update_url = "http://" + self.base_url + "/getupdates"
|
||||
result = self.call_api(update_url)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_last(self):
|
||||
update_url = "http://" + self.base_url + "/getlast"
|
||||
result = self.call_api(update_url)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fetch_data(self):
|
||||
try:
|
||||
if self.update_calls == 0:
|
||||
fetch = self.get_last()
|
||||
else:
|
||||
fetch = self.get_updates()
|
||||
if not fetch["is_new"]:
|
||||
fetch = self.last_fetch
|
||||
else:
|
||||
self.last_fetch = fetch
|
||||
|
||||
data = fetch["data"]
|
||||
has_queue = fetch["has_queue"]
|
||||
except:
|
||||
data = {}
|
||||
has_queue = False
|
||||
|
||||
self.update_calls += 1
|
||||
|
||||
return has_queue, data
|
||||
|
||||
|
||||
|
||||
def call_api(self, url):
|
||||
ret = {}
|
||||
try:
|
||||
result = requests.get(url)
|
||||
result = result.json()
|
||||
|
||||
if result.pop("status") == "ok":
|
||||
ret = result
|
||||
except:
|
||||
logger.error("Bad api call for method {}.".format(url[url.rfind("/"):]))
|
||||
|
||||
return ret
|
86
app/broadcast/b_out.py
Normal file
86
app/broadcast/b_out.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import flask
|
||||
from flask import request, jsonify
|
||||
import numpy as np
|
||||
from threading import Thread
|
||||
|
||||
|
||||
import logging
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
# hide the info-messages of each GET-request
|
||||
|
||||
|
||||
|
||||
|
||||
class BroadcastUpdates:
|
||||
"""Broadcasts (out) updates for the hw-handler to be fetched periodically"""
|
||||
|
||||
def __init__(self, port):
|
||||
""""""
|
||||
self.last_show = ""
|
||||
|
||||
self.queue = [] #[{"matrices" : [np.full((16,16,3), 10).tolist(), np.full((16,16,3), 100).tolist(), np.full((16,16,3), 200).tolist()]} for _ in range(4)]
|
||||
self.port = port
|
||||
|
||||
|
||||
def start(self):
|
||||
t = Thread(target=self.run)
|
||||
t.start()
|
||||
|
||||
|
||||
def run(self):
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
@app.route('/getupdates', methods=['GET'])
|
||||
def get_updates():
|
||||
return self.get_updates()
|
||||
@app.route('/getlast', methods=['GET'])
|
||||
def get_last():
|
||||
return self.get_last()
|
||||
|
||||
app.run('0.0.0.0', port=self.port)
|
||||
|
||||
|
||||
def get_updates(self):
|
||||
try:
|
||||
data = self.queue.pop(0)
|
||||
self.last_show = data
|
||||
is_new = True
|
||||
has_queue = len(self.queue) > 0
|
||||
except:
|
||||
data = ""
|
||||
is_new = False
|
||||
has_queue = False
|
||||
|
||||
return self.generate_response(
|
||||
is_new = is_new,
|
||||
data = data,
|
||||
has_queue = has_queue
|
||||
)
|
||||
|
||||
|
||||
|
||||
def get_last(self):
|
||||
try:
|
||||
try:
|
||||
data = self.queue[-1]
|
||||
self.queue = []
|
||||
except: # list empty
|
||||
data = self.last_show,
|
||||
except:
|
||||
data = ""
|
||||
return self.generate_response(
|
||||
data = data,
|
||||
has_queue = False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def generate_response(self, **kwargs):
|
||||
ret = {
|
||||
"status" : "ok",
|
||||
**kwargs
|
||||
}
|
||||
|
||||
return jsonify(ret)
|
||||
|
42
app/client.py
Normal file
42
app/client.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# functionality
|
||||
from clock import c_in, c_out
|
||||
from broadcast import b_in
|
||||
import launcher
|
||||
|
||||
import logging
|
||||
import platform
|
||||
|
||||
|
||||
if platform.uname().node == "ArchSpectre":
|
||||
# development
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
else:
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO,
|
||||
filename='persistence/client.log',
|
||||
)
|
||||
|
||||
|
||||
class ReceiverLauncher(launcher.Launcher):
|
||||
"""Launcher for all server-side modules. The hard-computations"""
|
||||
def __init__(self):
|
||||
|
||||
self.clock_sensor_module = c_in.SensorReadout()
|
||||
# active: periodically takes readouts
|
||||
self.clock_hardware_module = c_out.ClockFace()
|
||||
# active: periodically calls fetcher
|
||||
self.receive_module = b_in.FetchUpdates(server_ip="localhost", port="1111")
|
||||
# passive: fetches data on demand
|
||||
|
||||
super().__init__(
|
||||
sensors = self.clock_sensor_module,
|
||||
clock = self.clock_hardware_module,
|
||||
receive = self.receive_module
|
||||
)
|
||||
|
||||
|
||||
|
||||
ReceiverLauncher()
|
1
app/clock/__init__.py
Normal file
1
app/clock/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder
|
73
app/clock/c_back.py
Normal file
73
app/clock/c_back.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import numpy as np
|
||||
import datetime
|
||||
|
||||
from . import helpers
|
||||
|
||||
class ClockBackend:
|
||||
"""Heavy lifting of matrix operations"""
|
||||
def __init__(self):
|
||||
self.MOP = helpers.computations.MatrixOperations()
|
||||
|
||||
self.weather = {"weather":"", "high":"", "low":""}
|
||||
self.weather_raw = {}
|
||||
self.weather_face_swap = False
|
||||
|
||||
|
||||
def start(self):
|
||||
self.out = self.modules["broadcast"]
|
||||
helpers.timer.RepeatedTimer(15, self.clock_loop)
|
||||
|
||||
|
||||
def clock_loop(self):
|
||||
t = int(datetime.datetime.now().strftime("%H%M"))
|
||||
|
||||
if t % 5 == 0:
|
||||
# switch secondary face every 5 minutes
|
||||
weather = self.modules["bot"].api_weather.show_weather([47.3769, 8.5417]) # zürich
|
||||
|
||||
if weather != self.weather_raw and len(weather) != 0:
|
||||
td = weather[1]
|
||||
low = td["temps"][0]
|
||||
high = td["temps"][1]
|
||||
self.weather["weather"] = td["short"]
|
||||
self.weather["high"] = high
|
||||
self.weather["low"] = low
|
||||
elif len(weather) == 0:
|
||||
self.weather["weather"] = "error"
|
||||
self.weather["high"] = "error"
|
||||
self.weather["low"] = "error"
|
||||
|
||||
self.weather_face_swap = not self.weather_face_swap
|
||||
|
||||
self.send_face()
|
||||
|
||||
|
||||
|
||||
|
||||
def send_face(self):
|
||||
"""Set the clock face (time + weather) by getting updated info - gets called every minute"""
|
||||
matrices = self.MOP.clock_face(self.weather)
|
||||
if self.weather_face_swap:
|
||||
matrices = [matrices[0], matrices[2], matrices[1]]
|
||||
|
||||
matrices = [m.tolist() for m in matrices]
|
||||
self.out.queue.append({"matrices" : matrices})
|
||||
|
||||
|
||||
|
||||
|
||||
# def text_scroll(self, text, color=[[200,200,200]]):
|
||||
# pixels = self.MOP.text_converter(text, 12, color)
|
||||
# sleep_time = 1 / self.tspeed
|
||||
# width = self.shape[1]
|
||||
# frames = pixels.shape[1] - width
|
||||
# if frames <= 0:
|
||||
# frames = 1
|
||||
|
||||
# for i in range(frames):
|
||||
# visible = pixels[:,i:width+i]
|
||||
# self.IO.put(visible*self.brightness)
|
||||
# time.sleep(sleep_time)
|
||||
# time.sleep(10 * sleep_time)
|
||||
|
||||
|
38
app/clock/c_in.py
Normal file
38
app/clock/c_in.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import time
|
||||
|
||||
from . import hardware, helpers
|
||||
|
||||
|
||||
class SensorReadout:
|
||||
"""Overview class for (actual and potential) sensor sources"""
|
||||
|
||||
def __init__(self):
|
||||
""""""
|
||||
self.sensor_modules = { # we already call them, they are objects and not classes anymore
|
||||
"temperature" : hardware.sensors.TemperatureModule(),
|
||||
"humidity" : hardware.sensors.HumidityModule(),
|
||||
"luminosity" : hardware.sensors.BrightnessModule(),
|
||||
# more to come?
|
||||
}
|
||||
# self db_utils set externally
|
||||
|
||||
def start(self):
|
||||
helpers.timer.RepeatedTimer(120, self.spread_measure)
|
||||
|
||||
def spread_measure(self):
|
||||
measurements = dict((el,[]) for el in self.sensor_modules.keys())
|
||||
# create an empty dict with a list for each readout-type
|
||||
|
||||
for _ in range(5): # number of measures to average out
|
||||
for name in self.sensor_modules.keys():
|
||||
measure = self.sensor_modules[name].readout()
|
||||
measurements[name].append(measure)
|
||||
time.sleep(3)
|
||||
|
||||
results = {}
|
||||
for e in measurements.keys():
|
||||
lst = measurements[e]
|
||||
results[e] = sum(lst) / len(lst)
|
||||
|
||||
self.db_utils.sensor_log(**results)
|
||||
|
65
app/clock/c_out.py
Normal file
65
app/clock/c_out.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import datetime
|
||||
import time
|
||||
from threading import Thread
|
||||
import numpy as np
|
||||
|
||||
from . import hardware, helpers
|
||||
|
||||
|
||||
class ClockFace:
|
||||
"""Actual functions one might need for a clock"""
|
||||
|
||||
def __init__(self):
|
||||
""""""
|
||||
# added by the launcher, we have self.modules (dict)
|
||||
|
||||
self.IO = hardware.led.get_handler()
|
||||
self.shape = self.IO.shape # (16,32) for now
|
||||
# TODO handle differently!
|
||||
self.MOP = helpers.computations.MatrixOperations()
|
||||
|
||||
self.kill_output = False
|
||||
|
||||
def start(self):
|
||||
Thread(target = self.clock_loop).start()
|
||||
|
||||
|
||||
def clock_loop(self):
|
||||
while True: # TODO: allow this to be exited gracefully
|
||||
|
||||
t_start = datetime.datetime.now()
|
||||
|
||||
self.set_brightness()
|
||||
|
||||
has_queue, data = self.modules["receive"].fetch_data()
|
||||
tnext = 1 if has_queue else 30
|
||||
|
||||
if data == {}:
|
||||
matrices = self.MOP.get_fallback()
|
||||
matrices[0][0,0] = [255, 0, 0] # red dot on the top left
|
||||
else:
|
||||
matrices = [np.asarray(d).astype(int) for d in data["matrices"]]
|
||||
matrices[0][0,0] = [0, 255, 0] # green dot on the top left
|
||||
|
||||
if not self.kill_output:
|
||||
self.IO.put(matrices)
|
||||
else:
|
||||
z = np.zeros((16,16,3))
|
||||
self.IO.put([z,z,z])
|
||||
|
||||
|
||||
t_end = datetime.datetime.now()
|
||||
delta_planned = datetime.timedelta(seconds = tnext)
|
||||
delta = delta_planned - (t_end - t_start)
|
||||
|
||||
time.sleep(max(delta.total_seconds(), 0))
|
||||
|
||||
|
||||
def set_brightness(self):
|
||||
"""Kill the brightness at night"""
|
||||
|
||||
is_WE = datetime.datetime.now().weekday() > 4
|
||||
now = int(datetime.datetime.now().strftime("%H%M"))
|
||||
|
||||
self.kill_output = (now < 1000 or now > 2200) if is_WE else (now < 830 or now > 2130)
|
||||
|
2
app/clock/hardware/__init__.py
Normal file
2
app/clock/hardware/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Placeholder
|
||||
from . import led, sensors
|
17
app/clock/hardware/led.py
Normal file
17
app/clock/hardware/led.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from . import unicorn as led
|
||||
# or neopixel soon:
|
||||
# from . import neopixel as led
|
||||
|
||||
def get_handler():
|
||||
OUT = led.ClockOut()
|
||||
shape = OUT.shape
|
||||
|
||||
if led.SETUP_FAIL:
|
||||
# we use the sim
|
||||
del OUT
|
||||
from . import sim
|
||||
OUT = sim.ClockOut(shape)
|
||||
|
||||
return OUT
|
||||
|
||||
|
51
app/clock/hardware/neopixel.py
Normal file
51
app/clock/hardware/neopixel.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import colorsys
|
||||
import random
|
||||
|
||||
try:
|
||||
import rpi_ws281x as ws
|
||||
except ImportError:
|
||||
from unittest.mock import Mock
|
||||
ws = Mock()
|
||||
SETUP_FAIL = True
|
||||
|
||||
|
||||
class ClockOut:
|
||||
def __init__(self):
|
||||
self.shape = (45, 20) # H x W
|
||||
num = self.shape[0] * self.shape[1]
|
||||
pin = 18
|
||||
freq = 800000 # maybe higher
|
||||
dma = 5
|
||||
invert = False
|
||||
brightness = 100
|
||||
channel = 0
|
||||
led_type = None # ??
|
||||
self.strip = ws.PixelStrip(num, pin, freq, dma, invert, brightness, channel, led_type)
|
||||
self.strip.begin()
|
||||
|
||||
|
||||
def put(self, matrix):
|
||||
self.render(matrix)
|
||||
|
||||
|
||||
def render(self, matrix):
|
||||
p = 0
|
||||
for i in range(matrix.shape[0]):
|
||||
for j in range(matrix.shape[1]):
|
||||
col = int(ws.Color(*matrix[i,j]))
|
||||
|
||||
self.strip.setPixelColor(p, col)
|
||||
p += 1
|
||||
self.strip.show()
|
||||
|
||||
|
||||
# test = ClockOut()
|
||||
# z = np.zeros((30,30, 3), dtype=int)
|
||||
# for i in range(30):
|
||||
# for j in range(30):
|
||||
# z[i, j, ...] = [random.randint(0,255), random.randint(0,255), random.randint(0,255)]
|
||||
# test.put(z)
|
||||
# #time.sleep(0.1)
|
||||
|
87
app/clock/hardware/sensors.py
Normal file
87
app/clock/hardware/sensors.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import time
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TempSim:
|
||||
"""Simulates a temperature for running on windows"""
|
||||
temperature = 23.23 # return a celsius value
|
||||
humidity = 30.4
|
||||
|
||||
|
||||
class LightSim:
|
||||
def input(self, *args):
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
|
||||
class SensorModule:
|
||||
def __init__(self):
|
||||
logger.info("Using module " + self.__class__.__name__)
|
||||
|
||||
|
||||
|
||||
## Real sensors!
|
||||
try:
|
||||
import board
|
||||
import adafruit_dht
|
||||
dht11 = adafruit_dht.DHT11(board.D18)
|
||||
import RPi.GPIO as GPIO
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(4, GPIO.IN)
|
||||
except ImportError:
|
||||
logger.warn("Simulating sensor modules")
|
||||
dht11 = TempSim()
|
||||
GPIO = LightSim()
|
||||
|
||||
|
||||
class TemperatureModule(SensorModule):
|
||||
"""Takes readouts from the DHT 11
|
||||
Returns: temperature"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.device = dht11
|
||||
|
||||
def readout(self):
|
||||
try:
|
||||
temp = self.device.temperature
|
||||
except:
|
||||
time.sleep(1)
|
||||
try:
|
||||
temp = self.device.temperature
|
||||
except:
|
||||
temp = -1
|
||||
|
||||
return temp
|
||||
|
||||
class HumidityModule(SensorModule):
|
||||
"""Takes readouts from the DHT 11
|
||||
Returns: humidity"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.device = dht11
|
||||
|
||||
def readout(self):
|
||||
try:
|
||||
hum = self.device.humidity
|
||||
except:
|
||||
time.sleep(1)
|
||||
try:
|
||||
hum = self.device.humidity
|
||||
except:
|
||||
hum = -1
|
||||
|
||||
return hum
|
||||
|
||||
class BrightnessModule(SensorModule):
|
||||
"""Returns one for HIGH and zero for LOW"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def readout(self):
|
||||
# The sensor is reversed: 0 when bright and 1 if dark
|
||||
light = GPIO.input(4)
|
||||
if light == 0:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
55
app/clock/hardware/sim.py
Normal file
55
app/clock/hardware/sim.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import sys
|
||||
import colorsys
|
||||
import pygame.gfxdraw
|
||||
import time
|
||||
import pygame
|
||||
import numpy as np
|
||||
|
||||
class ClockOut:
|
||||
"""Creates a drawable window in case the real hardware is not accessible. For development"""
|
||||
def __init__(self, shape):
|
||||
self.pixel_size = 20
|
||||
|
||||
self.shape = shape
|
||||
self.pixels = np.zeros((*shape,3), dtype=int)
|
||||
self.WIDTH = shape[1]
|
||||
self.HEIGHT = shape[0]
|
||||
self.window_width = self.WIDTH * self.pixel_size
|
||||
self.window_height = self.HEIGHT * self.pixel_size
|
||||
|
||||
pygame.init()
|
||||
pygame.display.set_caption("Unicorn HAT simulator")
|
||||
self.screen = pygame.display.set_mode([self.window_width, self.window_height])
|
||||
|
||||
|
||||
def put(self, matrices):
|
||||
self.screen.fill((0, 0, 0))
|
||||
for event in pygame.event.get(): # User did something
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
|
||||
if self.shape == (16, 32):
|
||||
matrix = np.concatenate((matrices[0], matrices[1]), axis=1)
|
||||
|
||||
self.pixels = matrix
|
||||
self.draw_pixels()
|
||||
|
||||
pygame.display.flip()
|
||||
pygame.event.pump()
|
||||
|
||||
|
||||
def draw_pixels(self):
|
||||
p = self.pixel_size
|
||||
|
||||
r = int(p / 4)
|
||||
for i in range(self.HEIGHT):
|
||||
for j in range(self.WIDTH):
|
||||
w_x = int(j * p + p / 2)
|
||||
#w_y = int((self.HEIGHT - 1 - y) * p + p / 2)
|
||||
w_y = int(i * p + p / 2)
|
||||
color = self.pixels[i,j,:]
|
||||
color = color.astype("int")
|
||||
|
||||
pygame.gfxdraw.aacircle(self.screen, w_x, w_y, r, color)
|
||||
pygame.gfxdraw.filled_circle(self.screen, w_x, w_y, r, color)
|
96
app/clock/hardware/unicorn.py
Normal file
96
app/clock/hardware/unicorn.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import colorsys
|
||||
import time
|
||||
import numpy as np
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
SETUP_FAIL = False
|
||||
except ImportError:
|
||||
from unittest.mock import Mock
|
||||
GPIO = Mock()
|
||||
SETUP_FAIL = True
|
||||
|
||||
|
||||
class ClockOut:
|
||||
def __init__(self):
|
||||
self.PIN_CLK = 11
|
||||
##################################
|
||||
# Hardcoded vaules:
|
||||
# GPIO Pins for the actual signal. The other ones are for signal clocks and resets.
|
||||
self.PINS_DAT = [10, 22]
|
||||
self.PIN_CS = 8
|
||||
# for data transmission
|
||||
self.SOF = 0x72
|
||||
self.DELAY = 1.0/120
|
||||
# shape of 2 unicorn hats
|
||||
self.shape = (16, 32)
|
||||
##################################
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(self.PIN_CS, GPIO.OUT, initial=GPIO.HIGH)
|
||||
GPIO.setup(self.PIN_CLK, GPIO.OUT, initial=GPIO.LOW)
|
||||
GPIO.setup(self.PINS_DAT, GPIO.OUT, initial=GPIO.LOW)
|
||||
|
||||
self.HEIGHT = self.shape[0] #16
|
||||
self.WIDTH = self.shape[1] #32
|
||||
|
||||
self.reset_clock()
|
||||
|
||||
|
||||
def reset_clock(self):
|
||||
GPIO.output(self.PIN_CS, GPIO.LOW)
|
||||
time.sleep(0.00001)
|
||||
GPIO.output(self.PIN_CS, GPIO.HIGH)
|
||||
|
||||
|
||||
def spi_write(self, buf1, buf2):
|
||||
GPIO.output(self.PIN_CS, GPIO.LOW)
|
||||
|
||||
self.spi_write_byte(self.SOF, self.SOF)
|
||||
|
||||
for x in range(len(buf1)):
|
||||
b1, b2= buf1[x], buf2[x]
|
||||
self.spi_write_byte(b1, b2)
|
||||
|
||||
|
||||
GPIO.output(self.PIN_CS, GPIO.HIGH)
|
||||
|
||||
|
||||
def spi_write_byte(self, b1, b2):
|
||||
for x in range(8):
|
||||
GPIO.output(self.PINS_DAT[0], b1 & 0b10000000)
|
||||
GPIO.output(self.PINS_DAT[1], b2 & 0b10000000)
|
||||
GPIO.output(self.PIN_CLK, GPIO.HIGH)
|
||||
|
||||
b1 <<= 1
|
||||
b2 <<= 1
|
||||
#time.sleep(0.00000001)
|
||||
GPIO.output(self.PIN_CLK, GPIO.LOW)
|
||||
|
||||
|
||||
def put(self, matrices):
|
||||
"""Sets a height x width matrix directly"""
|
||||
self.reset_clock()
|
||||
matrix = np.concatenate((matrices[0], matrices[1]), axis=1) # or 1??
|
||||
self.show(matrix)
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Clear the buffer."""
|
||||
zero = np.zero((self.HEIGHT, self. WIDTH))
|
||||
self.put(zero)
|
||||
|
||||
|
||||
def show(self, matrix):
|
||||
"""Output the contents of the buffer to Unicorn HAT HD."""
|
||||
##########################################################
|
||||
## Change to desire
|
||||
buff2 = np.rot90(matrix[:self.HEIGHT,:16],3)
|
||||
buff1 = np.rot90(matrix[:self.HEIGHT,16:32],1)
|
||||
##########################################################
|
||||
# separated these are: 16x16x3 arrays
|
||||
buff1, buff2 = [(x.reshape(768)).astype(np.uint8).tolist() for x in (buff1, buff2)]
|
||||
|
||||
self.spi_write(buff1, buff2)
|
||||
|
||||
time.sleep(self.DELAY)
|
1
app/clock/helpers/__init__.py
Normal file
1
app/clock/helpers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import computations, timer
|
178
app/clock/helpers/computations.py
Normal file
178
app/clock/helpers/computations.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import numpy as np
|
||||
import datetime
|
||||
import time
|
||||
|
||||
# bulky hard-coded values:
|
||||
from . import shapes
|
||||
digits = shapes.digits
|
||||
weather_categories = shapes.weather_categories
|
||||
digit_position = [[2,4], [2,10], [9,4], [9,10]]
|
||||
|
||||
|
||||
days = np.append(np.zeros((15,16)), np.array([0,1,0,1,0,1,0,1,0,1,0,1,1,0,1,1])).reshape((16,16))
|
||||
|
||||
|
||||
|
||||
|
||||
class MatrixOperations():
|
||||
"""Helper functions to generate frequently-used images"""
|
||||
def __init__(self, shape=[16,16]):
|
||||
self.shape = shape
|
||||
# shape is going to be (16,32) for the moment
|
||||
self.primary = [200, 200, 200]
|
||||
self.secondary = [10, 200, 10]
|
||||
self.error = [200, 10, 10]
|
||||
|
||||
|
||||
def time_converter(self, top="", bottom=""):
|
||||
"""Converts 4-digit time to a 16x16 pixel-matrix
|
||||
returns: np.array"""
|
||||
# nshape = (self.shape[0], int(self.shape[1]/2))
|
||||
nshape = (16, 16)
|
||||
pixels = np.zeros(nshape, dtype=np.uint8)
|
||||
|
||||
if bottom == "" or top == "":
|
||||
top = datetime.datetime.now().strftime("%H")
|
||||
bottom = datetime.datetime.now().strftime("%M")
|
||||
|
||||
if len(top) < 2:
|
||||
top = "0" * (2 - len(top)) + top
|
||||
if len(bottom) < 2:
|
||||
bottom = "0" * (2 - len(bottom)) + bottom
|
||||
|
||||
if ("-" in top and len(top) > 2) or ("-" in bottom and len(bottom) > 2):
|
||||
time_split = 4*["-"]
|
||||
elif "error" in top and "error" in bottom:
|
||||
time_split = 4*["error"]
|
||||
else:
|
||||
time_split = [i for i in top] + [i for i in bottom]
|
||||
|
||||
if "-1" in top and len(top) != 2:
|
||||
time_split = ["-1", top[-1]] + [i for i in bottom]
|
||||
if "-1" in bottom and len(bottom) != 2:
|
||||
time_split = [i for i in top] + ["-1", bottom[-1]]
|
||||
|
||||
for i in range(4):
|
||||
x = digit_position[i][0]
|
||||
y = digit_position[i][1]
|
||||
number = digits[time_split[i]]
|
||||
pixels[x: x + 5, y: y + 3] = np.array(number)
|
||||
|
||||
return pixels
|
||||
|
||||
|
||||
def date_converter(self):
|
||||
# nshape = (self.shape[0], int(self.shape[1]/2))
|
||||
nshape = (16, 16)
|
||||
today = datetime.datetime.today()
|
||||
weekday = datetime.datetime.weekday(today)
|
||||
# size of the reduced array according to weekday
|
||||
size = [2,4,6,8,10,13,16]
|
||||
|
||||
pixels = days.copy() #base color background
|
||||
lrow = np.append(pixels[15,:size[weekday]], [0 for i in range(16 - size[weekday])])
|
||||
lrow = np.append(np.zeros((15,16)), lrow).reshape(nshape)
|
||||
pixels += lrow
|
||||
return pixels
|
||||
|
||||
|
||||
def weather_converter(self, name):
|
||||
"""Fills one half of the screen with weather info."""
|
||||
# nshape = (self.shape[0], int(self.shape[1]/2))
|
||||
nshape = (16, 16)
|
||||
result = np.zeros(nshape)
|
||||
cwd = __file__.replace("\\","/") # for windows
|
||||
cwd = cwd.rsplit("/", 1)[0] # the current working directory (where this file is)
|
||||
if len(cwd) == 0:
|
||||
cwd = "."
|
||||
icon_spritesheet = cwd + "/weather-icons.bmp"
|
||||
|
||||
icons = Image.open(icon_spritesheet)
|
||||
icons_full = np.array(icons)
|
||||
|
||||
icon_loc = ["sun","moon","sun and clouds", "moon and clouds", "cloud","fog and clouds","2 clouds", "3 clouds", "rain and cloud", "rain and clouds", "rain and cloud and sun", "rain and cloud and moon", "thunder and cloud", "thunder and cloud and moon", "snow and cloud", "snow and cloud and moon", "fog","fog night"]
|
||||
#ordered 1 2 \n 3 4 \ 5 5 ...
|
||||
if name == "":
|
||||
name = "error"
|
||||
name = weather_categories[name]
|
||||
try:
|
||||
iy, ix = int(icon_loc.index(name)/2), icon_loc.index(name)%2
|
||||
# x and y coords
|
||||
except:
|
||||
return np.zeros((*nshape,3))
|
||||
|
||||
icon_single = icons_full[16*iy:16*(iy + 1),16*ix:16*(ix + 1),...]
|
||||
return icon_single
|
||||
|
||||
|
||||
def matrix_add_depth(self, matrix, colors = []):
|
||||
"""transforms a 2d-array with 0,1,2 to a 3d-array with the rgb values for primary and secondary color"""
|
||||
|
||||
c1 = self.primary
|
||||
c2 = self.secondary
|
||||
c3 = self.error
|
||||
if len(colors) > 0:
|
||||
c1 = colors[0]
|
||||
if len(colors) > 1:
|
||||
c2 = colors[1]
|
||||
if len(colors) > 2:
|
||||
c3 = colors[2]
|
||||
if len(colors) > 3:
|
||||
print("Too many colors")
|
||||
|
||||
r3 = np.zeros((matrix.shape[0],matrix.shape[1],3),dtype=int)
|
||||
for i in range(matrix.shape[0]):
|
||||
for j in range(matrix.shape[1]):
|
||||
t = int(matrix[i, j])
|
||||
if t == 0:
|
||||
r3[i, j, :] = [0,0,0]
|
||||
elif t == 1:
|
||||
r3[i, j, :] = c1
|
||||
elif t == 2:
|
||||
r3[i, j, :] = c2
|
||||
else:
|
||||
r3[i, j, :] = c3
|
||||
return r3
|
||||
|
||||
|
||||
|
||||
def clock_face(self, weather):
|
||||
"""weather as a dict"""
|
||||
hour = self.time_converter()
|
||||
day = self.date_converter()
|
||||
face1 = hour + day
|
||||
# time + date:
|
||||
face1_3d = self.matrix_add_depth(face1)
|
||||
# weather icons
|
||||
face2_3d = self.weather_converter(weather["weather"])
|
||||
# weather temps
|
||||
face3 = self.time_converter(top=str(weather["low"]), bottom=str(weather["high"]))
|
||||
face3 = np.concatenate((face3[:8,...],2*face3[8:,...]))
|
||||
face3_3d = self.matrix_add_depth(face3,[[0, 102, 255],[255, 102, 0]])
|
||||
|
||||
return [face1_3d, face2_3d, face3_3d]
|
||||
|
||||
|
||||
def text_converter(self, text, height, color):
|
||||
"""Converts a text to a pixel-matrix
|
||||
returns: np.array((16, x, 3))"""
|
||||
|
||||
font = ImageFont.truetype("verdanab.ttf", height)
|
||||
size = font.getsize(text)
|
||||
img = Image.new("1",size,"black")
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.text((0, 0), text, "white", font=font)
|
||||
pixels = np.array(img, dtype=np.uint8)
|
||||
pixels3d = self.matrix_add_depth(pixels, color)
|
||||
return pixels3d
|
||||
|
||||
|
||||
def get_fallback(self):
|
||||
hour = self.time_converter()
|
||||
day = self.date_converter()
|
||||
face1 = hour + day
|
||||
face1_3d = self.matrix_add_depth(face1)
|
||||
|
||||
face2_3d = face3_3d = np.zeros((16,16,3))
|
||||
return [face1_3d, face2_3d, face3_3d]
|
103
app/clock/helpers/shapes.py
Normal file
103
app/clock/helpers/shapes.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import numpy as np
|
||||
|
||||
digits = {
|
||||
"1" : np.array([
|
||||
[0,0,1],
|
||||
[0,0,1],
|
||||
[0,0,1],
|
||||
[0,0,1],
|
||||
[0,0,1]]),
|
||||
"2" : np.array([
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[1,1,1],
|
||||
[1,0,0],
|
||||
[1,1,1]]),
|
||||
"3" : np.array([
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[1,1,1]]),
|
||||
"4" : np.array([
|
||||
[1,0,1],
|
||||
[1,0,1],
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[0,0,1]]),
|
||||
"5" : np.array([
|
||||
[1,1,1],
|
||||
[1,0,0],
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[1,1,1]]),
|
||||
"6" : np.array([
|
||||
[1,1,1],
|
||||
[1,0,0],
|
||||
[1,1,1],
|
||||
[1,0,1],
|
||||
[1,1,1]]),
|
||||
"7" : np.array([
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[0,0,1],
|
||||
[0,0,1],
|
||||
[0,0,1]]),
|
||||
"8" : np.array([
|
||||
[1,1,1],
|
||||
[1,0,1],
|
||||
[1,1,1],
|
||||
[1,0,1],
|
||||
[1,1,1]]),
|
||||
"9" : np.array([
|
||||
[1,1,1],
|
||||
[1,0,1],
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[1,1,1]]),
|
||||
"0" : np.array([
|
||||
[1,1,1],
|
||||
[1,0,1],
|
||||
[1,0,1],
|
||||
[1,0,1],
|
||||
[1,1,1]]),
|
||||
"-" : np.array([
|
||||
[0,0,0],
|
||||
[0,0,0],
|
||||
[1,1,1],
|
||||
[0,0,0],
|
||||
[0,0,0]]),
|
||||
"-1" : np.array([
|
||||
[0,0,1],
|
||||
[0,0,1],
|
||||
[1,1,1],
|
||||
[0,0,1],
|
||||
[0,0,1]]),
|
||||
"error" : np.array([
|
||||
[1,0,1],
|
||||
[1,0,1],
|
||||
[0,1,0],
|
||||
[1,0,1],
|
||||
[1,0,1]]),
|
||||
}
|
||||
|
||||
|
||||
weather_categories = {
|
||||
"Clouds": "cloud",
|
||||
"Rain": "rain and cloud",
|
||||
"Thunderstorm": "thunder and cloud",
|
||||
"Drizzle": "rain and cloud",
|
||||
"Snow": "snow and cloud",
|
||||
"Clear": "sun",
|
||||
"Mist": "fog and clouds",
|
||||
"Smoke": "Smoke",
|
||||
"Haze": "Haze",
|
||||
"Dust": "Dust",
|
||||
"Fog": "fog",
|
||||
"Sand": "Sand",
|
||||
"Dust": "Dust",
|
||||
"Ash": "Ash",
|
||||
"Squal": "Squal",
|
||||
"Tornado": "Tornado",
|
||||
"error" : "moon"
|
||||
}
|
30
app/clock/helpers/timer.py
Normal file
30
app/clock/helpers/timer.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from threading import Timer
|
||||
import time
|
||||
|
||||
class RepeatedTimer(object):
|
||||
def __init__(self, interval, function, *args, **kwargs):
|
||||
self._timer = None
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.is_running = False
|
||||
self.next_call = time.time()
|
||||
self.start()
|
||||
|
||||
def _run(self):
|
||||
self.is_running = False
|
||||
self.start()
|
||||
self.function(*self.args, **self.kwargs)
|
||||
|
||||
def start(self):
|
||||
if not self.is_running:
|
||||
self.next_call += self.interval
|
||||
self._timer = Timer(self.next_call - time.time(), self._run)
|
||||
self._timer.start()
|
||||
self.is_running = True
|
||||
|
||||
def stop(self):
|
||||
self._timer.cancel()
|
||||
self.is_running = False
|
||||
|
BIN
app/clock/helpers/weather-icons.bmp
Normal file
BIN
app/clock/helpers/weather-icons.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
68
app/launcher.py
Normal file
68
app/launcher.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import logging
|
||||
|
||||
from persistence import local_io, database
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Launcher:
|
||||
"""Template for launching collections of modules"""
|
||||
|
||||
def __init__(self, **modules):
|
||||
""""""
|
||||
self.persistence = local_io.PersistentDict("persistence/prst.json")
|
||||
self.db_utils = database.DatabaseUtils
|
||||
self.modules = modules
|
||||
|
||||
logger.info(self.__class__.__name__ + " initialized")
|
||||
|
||||
if len(self.persistence) == 0:
|
||||
self.init_persistence()
|
||||
self.persistence["global"]["reboots"] += 1
|
||||
|
||||
self.launch_modules()
|
||||
|
||||
|
||||
|
||||
def launch_modules(self):
|
||||
for module in self.modules.values():
|
||||
logger.info("Starting module {}".format(module.__class__.__name__))
|
||||
module.modules = self.modules
|
||||
module.persistence = self.persistence
|
||||
module.db_utils = self.db_utils()
|
||||
module.start()
|
||||
|
||||
|
||||
def init_persistence(self):
|
||||
logger.warning("No persistence found, created a new one")
|
||||
|
||||
self.persistence["global"] ={
|
||||
"lists" : {},
|
||||
"reboots": 0
|
||||
}
|
||||
|
||||
for m_name in self.modules.keys():
|
||||
data = {}
|
||||
if m_name == "bot":
|
||||
data = {
|
||||
"send_activity" : {"hour":[], "count":[]},
|
||||
"receive_activity" : {"hour":[], "count":[]},
|
||||
"execute_activity" : {"hour":[], "count":[]},
|
||||
"log": [],
|
||||
"chat_members": {},
|
||||
"aliases" : {}
|
||||
}
|
||||
if m_name == "clock":
|
||||
data = {
|
||||
"sensors" : {
|
||||
"time" : [],
|
||||
"temperature":[],
|
||||
"humidity":[],
|
||||
"brightness" : [],
|
||||
}
|
||||
}
|
||||
|
||||
self.persistence[m_name] = data
|
||||
|
||||
|
12
app/persistence/README.md
Normal file
12
app/persistence/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## What is happening here?
|
||||
|
||||
This "persistence"-module aims to standardize 2 things:
|
||||
* the creation of a common set of variables that survives a potential (let's face it, likely) crash
|
||||
* advanced logging and analytics
|
||||
|
||||
### Common variables
|
||||
These are saved as a json file and are handled internally as a dict. Each change in the dict triggers a write to the file.
|
||||
|
||||
|
||||
### Logging
|
||||
A chunky sqlite-db which periodically gets new entries. From all modules. Ideally this db is then visualized through grafana. WIP
|
1
app/persistence/__init__.py
Normal file
1
app/persistence/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#placeholder
|
98
app/persistence/database.py
Normal file
98
app/persistence/database.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from . import models
|
||||
from peewee import *
|
||||
from playhouse.pool import PooledMySQLDatabase
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
import os
|
||||
if os.getenv("dockerized", "") == "true":
|
||||
import sys
|
||||
sys.path.append("/keys")
|
||||
import db_keys as keys
|
||||
else:
|
||||
from . import keys
|
||||
dbk = keys.db_access
|
||||
|
||||
|
||||
|
||||
db_connection = PooledMySQLDatabase(
|
||||
dbk["name"],
|
||||
user=dbk["username"],
|
||||
password=dbk["password"],
|
||||
host=dbk["url"],
|
||||
port=dbk["port"],
|
||||
autorollback=True
|
||||
)
|
||||
|
||||
|
||||
def auto_connect_db(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
#before:
|
||||
db_connection.connect()
|
||||
ret = func(*args, **kwargs)
|
||||
#after:
|
||||
db_connection.close()
|
||||
# also, action is in scope now
|
||||
return ret
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
class DatabaseUtils:
|
||||
"""This object is the only database-related stuff getting exposed to the other modules. It must explicitly handle the connections!"""
|
||||
def __init__(self) -> None:
|
||||
# TODO specify arguments!
|
||||
models.db.initialize(db_connection)
|
||||
models.create_tables(db_connection)
|
||||
|
||||
|
||||
@auto_connect_db
|
||||
def chat_count(self, attribute):
|
||||
if attribute == "read":
|
||||
return models.ChatMetric.select().where(models.ChatMetric.read == True).count()
|
||||
elif attribute == "send":
|
||||
return models.ChatMetric.select().where(models.ChatMetric.send == True).count()
|
||||
elif attribute == "execute":
|
||||
return models.ChatMetric.select().where(models.ChatMetric.execute == True).count()
|
||||
else: # does not exist
|
||||
return -1
|
||||
|
||||
@auto_connect_db
|
||||
def chat_log(self, **kwargs):
|
||||
models.ChatMetric(**kwargs)
|
||||
|
||||
@auto_connect_db
|
||||
def list_get(self, list_name=""):
|
||||
if not list_name: # return all
|
||||
cursor = models.List.select(models.List.name).execute()
|
||||
return [k.name for k in cursor]
|
||||
else:
|
||||
return models.List.get(models.List.name == list_name).content_as_list
|
||||
|
||||
@auto_connect_db
|
||||
def list_update(self, list_name, append="", replace=None):
|
||||
if replace != None:
|
||||
models.List.get(models.List.name == list_name).set_content(replace)
|
||||
elif append:
|
||||
l_obj = models.List.get(models.List.name == list_name)
|
||||
l = l_obj.content_as_list
|
||||
l.append(append)
|
||||
l_obj.set_content(l)
|
||||
else:
|
||||
logger.warning("Empty update_list() query was made. Ignoring")
|
||||
|
||||
@auto_connect_db
|
||||
def list_create(self, list_name):
|
||||
models.List(name=list_name).save()
|
||||
|
||||
@auto_connect_db
|
||||
def list_delete(self, list_name):
|
||||
models.List.delete().where(models.List.name == list_name).execute()
|
||||
|
||||
@auto_connect_db
|
||||
def sensor_log(self, **kwargs):
|
||||
models.SensorMetric(**kwargs).save()
|
87
app/persistence/local_io.py
Normal file
87
app/persistence/local_io.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class PersistentDict(dict):
|
||||
"""Extended dict that writes its content to a file every time a value is changed"""
|
||||
|
||||
def __init__(self, file_name, *args, **kwargs):
|
||||
"""initialization of the dict and of the required files"""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.path = file_name
|
||||
self.last_action = ""
|
||||
try:
|
||||
self.read_dict()
|
||||
except:
|
||||
with open(self.path, "a") as f:
|
||||
f.write("{}")
|
||||
|
||||
|
||||
## helper - functions
|
||||
def write_dict(self):
|
||||
with open(self.path, "w") as f:
|
||||
json.dump(self, f)
|
||||
self.last_action = "w"
|
||||
|
||||
def read_dict(self):
|
||||
with open(self.path) as f:
|
||||
tmp = dict(json.load(f))
|
||||
for key in tmp:
|
||||
super().__setitem__(key, tmp[key])
|
||||
self.last_action = "r"
|
||||
|
||||
|
||||
## extended dictionary - logic
|
||||
def __setitem__(self, key, value):
|
||||
if self.last_action != "r":
|
||||
self.read_dict()
|
||||
# not sure if the step to read is necessary, but I'll keep it for safety
|
||||
super().__setitem__(key,value)
|
||||
self.write_dict() # writes 'self' to a json file.
|
||||
|
||||
def __getitem__(self, key):
|
||||
if self.last_action != "r":
|
||||
self.read_dict()
|
||||
|
||||
ret_val = super().__getitem__(key)
|
||||
if type(ret_val) != int and type(ret_val) != str:
|
||||
ret_val = create_struct(type(ret_val), key, self, ret_val)
|
||||
|
||||
return ret_val
|
||||
|
||||
def clear(self):
|
||||
super().clear()
|
||||
self.write_dict()
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
def create_struct(struct_type, own_name, parent_name, *args, **kwargs):
|
||||
class HookedStruct(struct_type):
|
||||
|
||||
def __init__(self, own_name, parent_name, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.name = own_name
|
||||
self.parent = parent_name
|
||||
|
||||
def __setitem__(self, *args, **kwargs):
|
||||
super().__setitem__(*args, **kwargs)
|
||||
self.parent.__setitem__(self.name, self)
|
||||
|
||||
def __getitem__(self, *args, **kwargs):
|
||||
ret_val = super().__getitem__(*args, **kwargs)
|
||||
if type(ret_val) != int and type(ret_val) != str:
|
||||
ret_val = create_struct(type(ret_val), args[0], self, ret_val)
|
||||
return ret_val
|
||||
|
||||
def pop(self, *args):
|
||||
retvalue = super().pop(*args)
|
||||
self.parent.__setitem__(self.name, self)
|
||||
return retvalue
|
||||
|
||||
def append(self, *args):
|
||||
super().append(*args)
|
||||
self.parent.__setitem__(self.name, self)
|
||||
|
||||
return HookedStruct(own_name, parent_name, *args, **kwargs)
|
56
app/persistence/models.py
Normal file
56
app/persistence/models.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from datetime import datetime
|
||||
from peewee import *
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_tables(db):
|
||||
db.create_tables([SensorMetric, ChatMetric, ErrorMetric, List])
|
||||
|
||||
db = DatabaseProxy()
|
||||
# set the nature of the db at runtime
|
||||
|
||||
|
||||
class DBModel(Model):
|
||||
# specific to the above DB
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
class Metric(DBModel):
|
||||
time = DateTimeField(default = datetime.now())
|
||||
|
||||
|
||||
|
||||
### Actual metrics:
|
||||
|
||||
class SensorMetric(Metric):
|
||||
# this is a continuous metric
|
||||
temperature = FloatField()
|
||||
humidity = FloatField()
|
||||
luminosity = FloatField()
|
||||
|
||||
|
||||
class ChatMetric(Metric):
|
||||
read = BooleanField()
|
||||
send = BooleanField()
|
||||
execute = BooleanField()
|
||||
|
||||
|
||||
class ErrorMetric(Metric):
|
||||
# same as above
|
||||
error = TextField()
|
||||
|
||||
|
||||
class List(DBModel):
|
||||
name = CharField(unique=True)
|
||||
content = TextField(default="") # unlimited length, use to serialise list into
|
||||
|
||||
@property
|
||||
def content_as_list(self):
|
||||
return json.loads(self.content or '[]')
|
||||
|
||||
def set_content(self, list_content):
|
||||
self.content = json.dumps(list_content)
|
||||
self.save()
|
39
app/server.py
Normal file
39
app/server.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# functionality
|
||||
from bot import main
|
||||
from clock import c_back
|
||||
from broadcast import b_out
|
||||
import launcher
|
||||
|
||||
import logging
|
||||
import platform
|
||||
|
||||
|
||||
import os
|
||||
if os.getenv("dockerized", "") == "true" or platform.uname().node == "ArchSpectre":
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
else:
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO,
|
||||
filename='persistence/server.log',
|
||||
)
|
||||
|
||||
|
||||
|
||||
class BroadcastLauncher(launcher.Launcher):
|
||||
"""Launcher for all server-side modules. The hard-computations"""
|
||||
def __init__(self):
|
||||
|
||||
self.bot_module = main.ChatBot(name="Norbit", version="4.1a")
|
||||
self.clock_backend_module = c_back.ClockBackend() # threaded through threading.Timer
|
||||
self.broadcast_module = b_out.BroadcastUpdates(port="1111") # Thread
|
||||
|
||||
super().__init__(
|
||||
bot = self.bot_module,
|
||||
clock = self.clock_backend_module,
|
||||
broadcast = self.broadcast_module
|
||||
)
|
||||
|
||||
BroadcastLauncher()
|
Reference in New Issue
Block a user