Better folder structure, added a few ambient sensors

This commit is contained in:
Remy Moll 2021-04-15 19:50:37 +02:00
parent c61ee3ea72
commit 03d419f8f7
45 changed files with 711 additions and 1285 deletions

View File

@ -1 +1,6 @@
# Placeholder
from . import keys
from . import reddit
from . import weather
from . import reddit
from . import search
from . import metmuseum

View File

@ -1,20 +0,0 @@
import googlesearch
def query(params):
param_string = ""
for word in params:
param_string += word + "+"
param_string = param_string[:-1]
search_url = "https://google.com/search?q=" + param_string
try:
res = googlesearch.search(param_string.replace("+"," ") ,num=5,start=0,stop=5)
send_string = "Results for <b>" + param_string.replace("+"," ") + "</b>:\n\n"
for url in res:
send_string += url + "\n\n"
send_string += "Search url:\n" + search_url
except:
send_string = "Search url:\n" + search_url
return send_string

39
bot/api/metmuseum.py Normal file
View 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

View File

@ -1,50 +1,56 @@
import praw
try:
import bot.api.keys as keys
except:
import keys
stream = praw.Reddit(client_id = keys.reddit_id, client_secret = keys.reddit_secret, user_agent=keys.reddit_user_agent)
def get_top(subreddit, number, return_type="text"):
if return_type == "text":
message = ""
try:
for submission in stream.subreddit(subreddit).top(limit=number):
if not submission.stickied:
message += "<b>" + submission.title + "</b>" + "\n" + submission.selftext + "\n\n\n"
return message
except:
return "Api call failed, sorry"
else:
images = []
try:
for submission in stream.subreddit(subreddit).top(limit=number):
if not submission.stickied:
t = {"image": submission.url, "caption": submission.title}
images.append(t)
return images
except:
return ["Api call failed, sorry"]
def get_random_rising(subreddit, number, return_type="text"):
if return_type == "text":
message = ""
try:
for submission in stream.subreddit(subreddit).random_rising(limit=number):
if not submission.stickied:
message += "<b>" + submission.title + "</b>" + "\n" + submission.selftext + "\n\n\n"
return message
except:
return "Api call failed, sorry"
else:
images = []
try:
for submission in 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 ["Api call failed, sorry"]
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 []

View File

@ -1,150 +0,0 @@
import emoji
import requests
import datetime
import bot.api.keys
class TelegramIO():
def __init__(self, persistence):
"""Inits the Telegram-Interface
"""
self.base_url = "https://api.telegram.org/bot" + bot.api.keys.telegram_api + "/"
self.persistence = persistence
# Dynamic variables for answering
self.chat_id = ""
self.offset = 0
self.message_id = ""
self.message_queue = []
def update_commands(self,commands):
self.commands = commands
########################################################################
"""Helper-Functions"""
def fetch_updates(self):
""""""
update_url = self.base_url + "getUpdates"
data = {"offset":self.offset}
try:
result = requests.post(update_url,data=data)
result = result.json()["result"]
self.message_queue = result
except:
result = []
return len(result)
def process_message(self):
"""Inspects the first message from self.message_queue and reacts accordingly."""
message_data = self.message_queue.pop(0)
self.increase_counter("receive_activity")
self.offset = message_data["update_id"] + 1
if "edited_message" in message_data:
return
message = message_data["message"]
self.message_id = message["message_id"]
self.chat_id = message["chat"]["id"]
author = message["from"]
if str(author["id"]) not in self.persistence["bot"]["chat_members"]:
name = ""
if "first_name" in author:
name += author["first_name"] + " "
if "last_name" in author:
name += author["last_name"]
if len(name) == 0:
name = "anonymous"
self.persistence["bot"]["chat_members"][str(author["id"])] = name # seems like the conversion to string is handled implicitly, but it got me really confused
self.send_message("Welcome to this chat " + name + "!")
if "text" in message:
print("Chat said: ", emoji.demojize(message["text"]))
if "entities" in message:
for entry in message["entities"]:
if entry["type"] == "bot_command":
return message["text"] #self.handle_command(message["text"][1:])
elif "photo" in message:
print("Photo received, what do I do?")
return
def send_thinking_note(self):
data = {
"chat_id" : self.chat_id,
"action" : "typing",
}
send_url = self.base_url + "sendChatAction"
try:
r = requests.post(send_url, data=data)
except:
print("Could not show that I'm thinking =(")
def send_message(self, message):
if message == "" or message == None:
return
print("SENDING: " + message)
# message = message.replace("<","&lt;").replace(">", "&gt;")
# TODO: sanitize input but keep relevant tags
data = {
'chat_id': self.chat_id,
'text': emoji.emojize(message),
"parse_mode": "HTML",
"reply_to_message_id" : self.message_id,
}
send_url = self.base_url + "sendMessage"
try:
r = requests.post(send_url, data=data)
if (r.status_code != 200):
raise Exception
self.increase_counter("send_activity")
except:
out = datetime.datetime.now().strftime("%d.%m.%y - %H:%M")
out += " @ " + "telegram.send_message"
out += " --> " + "did not send:\n" + message
self.persistence["bot"]["log"] += [out]
def send_photo(self, url, caption):
print("SENDING PHOTO: " + url)
data = {
'chat_id': self.chat_id,
'photo': url,
"parse_mode": "HTML",
"reply_to_message_id" : self.message_id,
'caption' : caption,
}
send_url = self.base_url + "sendPhoto"
try:
r = requests.post(send_url, data=data)
self.increase_counter("send_activity")
except:
out = datetime.datetime.now().strftime("%d.%m.%y - %H:%M")
out += " @ " + "telegram.send_photo"
out += " --> " + "did not send:\n" + url
self.persistence["bot"]["log"] += [out]
def increase_counter(self, counter_name):
current_hour = int(datetime.datetime.now().timestamp() // 3600)
if len(self.persistence["bot"][counter_name]["hour"]) == 0 or current_hour != self.persistence["bot"][counter_name]["hour"][-1]:
self.persistence["bot"][counter_name]["hour"].append(current_hour)
self.persistence["bot"][counter_name]["count"].append(1)
else:
self.persistence["bot"][counter_name]["count"][-1] += 1

View File

@ -1,14 +1,13 @@
import requests
import bot.api.keys
import datetime
class WeatherFetch():
def __init__(self):
def __init__(self, key):
self.last_fetch = datetime.datetime.fromtimestamp(0)
self.last_weather = ""
self.url = "https://api.openweathermap.org/data/2.5/onecall?"
self.key = bot.api.keys.weather_api
self.key = key
def show_weather(self, location):
delta = datetime.datetime.now() - self.last_fetch
@ -59,3 +58,25 @@ class WeatherFetch():
ret_weather = self.last_weather
return ret_weather
# def get_weather_by_city(self, city):
# loc = get_coords_from_city(self, city)
# weather = self.show_weather(loc)
# return weather
# def get_coords_from_city(self, city):
# url = "https://devru-latitude-longitude-find-v1.p.rapidapi.com/latlon.php"
# data = {"location": city}
# headers = {
# "x-rapidapi-key" : "d4e0ab7ab3mshd5dde5a282649e0p11fd98jsnc93afd98e3aa",
# "x-rapidapi-host" : "devru-latitude-longitude-find-v1.p.rapidapi.com",
# }
# #try:
# resp = requests.request("GET", url, headers=headers, params=data)
# result = resp.text
# #except:
# # result = "???"
# return result

View File

@ -9,9 +9,10 @@ MESSAGE, WAKE, ALARM, IMAGE, ART = range(3,8)
class Clock(BotFunc):
"""pass on commands to clock-module"""
def __init__(self, prst, clock_module):
def __init__(self, prst, clock_module, art_api):
super().__init__(prst)
self.clock = clock_module
self.api_art = art_api
def create_handler(self):
handler = ConversationHandler(
@ -84,7 +85,8 @@ class Clock(BotFunc):
query.answer()
query.edit_message_text("Ok. How long should we display art? (in hours")
return ART
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
@ -111,8 +113,7 @@ class Clock(BotFunc):
for i in range(20):
ct = i/20 * gradient
col_show[:,:,...] = [int(x) for x in ct+start_color]
self.clock.IO.array = col_show
self.clock.IO.SHOW()
self.clock.IO.put(col_show)
time.sleep(int(duration) / 20)
self.clock.run(output,(duration,))
@ -132,11 +133,9 @@ class Clock(BotFunc):
red = empty.copy()
red[...,0] = 255
for i in range(int(n)):
self.clock.IO.array = red
self.clock.IO.SHOW()
self.clock.IO.put(red)
time.sleep(1/frequency)
self.clock.IO.array = empty
self.clock.IO.SHOW()
self.clock.IO.put(empty)
time.sleep(1/frequency)
if not(duration == 0 or frequency == 0):
@ -169,8 +168,7 @@ class Clock(BotFunc):
a = numpy.asarray(t)
def output(image, duration):
self.clock.IO.array = image
self.clock.IO.SHOW()
self.clock.IO.put(image)
time.sleep(int(duration) * 60)
self.clock.run(output,(a, duration))
@ -185,5 +183,29 @@ class Clock(BotFunc):
def exec_art_gallery(self, update: Update, context: CallbackContext) -> None:
update.message.reply_text("Puuh, thats tough, I'm not ready for that.")
duration = float(self.additional_argument)
number = int(update.message.text)
def output(number, duration):
for i in range(number):
img = self.api_art.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

View File

@ -1,96 +0,0 @@
import datetime
from bot.api import telegram, google, weather, reddit
import Levenshtein as lev
class BotFramework():
"""Main functionality for a bot """
def __init__(self, name, version, prst):
"""Inits the Bot with a few conf. vars
Args: -> name:str - Name of the bot
-> version:str - Version number
-> prst:shelveObj - persistence
"""
self.version = version
self.name = name
# Persistent variable
self.persistence = prst
# Uptime counter
self.start_time = datetime.datetime.now()
self.telegram = telegram.TelegramIO(self.persistence)
self.weather = weather.WeatherFetch()
def react_chats(self):
"""Checks unanswered messages and answers them"""
num = self.telegram.fetch_updates()
for i in range(num):
self.react_command()
def react_command(self):
"""Reacts if a new command is present
Returns command, params iff the command is a hardware-one (for the clock), else None"""
message = self.telegram.process_message()
if message == None:
return
message = message[1:] #remove first "/"
tmp = message.split(" ")
cmd = tmp[0]
params = tmp[1:]
def call_command(cmd, par):
result = self.commands[cmd](*par)
# *params means the list is unpacked and handed over as separate arguments.
self.telegram.increase_counter("execute_activity")
return result
if self.is_command(cmd): # first word
result = call_command(cmd, params)
elif cmd in self.persistence["bot"]["aliases"]:
dealias = self.persistence["bot"]["aliases"][cmd].split(" ") # as a list
new_cmd = dealias[0]
params = dealias[1:] + params
result = "Substituted <code>" + cmd + "</code> to <code>" + self.persistence["bot"]["aliases"][cmd] + "</code> and got:\n\n"
result += call_command(new_cmd, params)
else:
result = "Command <code>" + tmp[0] + "</code> not found."
self.telegram.send_message(result)
def is_command(self, input):
"""checks if we have a command. Returns true if yes and False if not
Also sends a mesage if close to an existing command
"""
max_match = 0
command_candidate = ""
for command in self.commands.keys():
match = lev.ratio(input.lower(),command)
if match > 0.7 and match > max_match:
max_match = match
command_candidate = command
if max_match == 1:
return True
if max_match != 0:
self.telegram.send_message("Did you mean <code>" + command_candidate + "</code>?")
return False
def write_bot_log(self, function_name, error_message):
""""""
out = datetime.datetime.now().strftime("%d.%m.%y - %H:%M")
out += " @ " + function_name
out += " --> " + error_message
self.persistence["bot"]["log"] += [out]

View File

@ -1,510 +1,63 @@
import datetime
from bot.api import telegram, google, weather, reddit
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext
import requests
import socket
import numpy as np
import time
import json
import datetime
import emoji
from . import api, commands
import bot.framework as FW
import logging
logger = logging.getLogger(__name__)
class ChatBot(FW.BotFramework):
""""""
def __init__(self, name, version, prst, hw_commands):
class ChatBot():
"""better framwork - unites all functions"""
def __init__(self, name, version, prst):
"""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
"""
super().__init__(name, version, prst)
# added by the launcher, we have self.modules (dict)
# Available commands. Must be manually updated!
self.commands = dict({
"help" : self.bot_show_help,
"status" : self.bot_print_status,
"log" : self.bot_print_log,
"lorem" : self.bot_print_lorem,
"weather" : self.bot_show_weather,
"google" : self.bot_google_search,
"events" : self.bot_print_events,
"wikipedia" : self.bot_show_wikipedia,
"zvv" : self.bot_zvv,
"cronjob" : self.bot_cronjob,
"joke" : self.bot_tell_joke,
"meme" : self.bot_send_meme,
"news" : self.bot_send_news,
"list" : self.bot_list,
"alias" : self.bot_save_alias,
}, **hw_commands)
# concat bot_commands + hw-commands
############################################################################
"""BOT-Commands: implementation"""
def bot_print_lorem(self, *args):
"""Prints a placeholder text."""
if "full" in args:
message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. At tellus at urna condimentum mattis pellentesque id nibh. Convallis aenean et tortor at risus viverra adipiscing at in. Aliquet risus feugiat in ante metus dictum. Tincidunt augue interdum velit euismod in pellentesque massa placerat duis. Tincidunt vitae semper quis lectus nulla at. Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit. Phasellus egestas tellus rutrum tellus pellentesque eu tincidunt. Gravida rutrum quisque non tellus orci. Adipiscing at in tellus integer feugiat. Integer quis auctor elit sed vulputate mi sit amet mauris. Risus pretium quam vulputate dignissim suspendisse in est. Cras fermentum odio eu feugiat pretium. Ut etiam sit amet nisl purus in mollis nunc sed. Elementum tempus egestas sed sed risus pretium quam. Massa ultricies mi quis hendrerit dolor magna eget."
else:
message = "Lorem ipsum dolor sit amet, bla bla bla..."
return message
def bot_print_status(self, *args):
"""Prints the bots current status and relevant information"""
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 += "<pre>Status: Running :green_circle:\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"
tot_r = np.array(self.persistence["bot"]["receive_activity"]["count"]).sum()
message += "Total messages read: " + str(tot_r) + "\n"
tot_s = np.array(self.persistence["bot"]["send_activity"]["count"]).sum()
message += "Total messages sent: " + str(tot_s) + "\n"
tot_e = np.array(self.persistence["bot"]["execute_activity"]["count"]).sum()
message += "Commands executed " + str(tot_e) + "</pre>"
return message
if "full" in args:
self.bot_print_log()
def bot_show_weather(self, *args):
"""Shows a weather-forecast for a given location"""
if len(args) != 1:
return "Invalid Syntax, please give one parameter, the location"
locations = {"freiburg": [47.9990, 7.8421], "zurich": [47.3769, 8.5417], "mulhouse": [47.7508, 7.3359]}
loc = args[0]
if loc.lower().replace("ü","u") in locations:
city = locations[loc.lower().replace("ü","u")]
else:
return "Couldn't find city, it might be added later though."
categories = {"Clouds": ":cloud:", "Rain": ":cloud_with_rain:", "Thunderstorm": "thunder_cloud_rain", "Drizzle": ":droplet:", "Snow": ":cloud_snow:", "Clear": ":sun:", "Mist": "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.weather.show_weather(city)
now = weather.pop(0)
message = "<b>Now:</b> " + categories[now["short"]] + "\n"
message += ":thermometer: " + str(now["temps"][0]) + "°\n\n"
for i, day in enumerate(weather):
if i == 0:
message += "<b>" + "Today" + ":</b> " + categories[day["short"]] + "\n"
else:
message += "<b>" + days[(today + i + 1) % 7] + ":</b> " + categories[day["short"]] + "\n"
message += ":thermometer: :fast_down_button: " + str(day["temps"][0]) + "° , :thermometer: :fast_up_button: " + str(day["temps"][1]) + "°\n\n"
return message
def bot_google_search(self, *args):
"""Does a google search and shows relevant links"""
if len(args) < 1:
return "Please tell me what to look for"
send_string = google.query(args)
return send_string
def bot_print_events(self, *args):
"""Shows a list of couple-related events and a countdown"""
events = {
"anniversary :red_heart:" : datetime.date(datetime.datetime.now().year,12,7),
"valentine's day :rose:": datetime.date(datetime.datetime.now().year,2,14),
"Marine's birthday :party_popper:": datetime.date(datetime.datetime.now().year,8,31),
"Remy's birthday :party_popper:": datetime.date(datetime.datetime.now().year,3,25),
"Christmas :wrapped_gift:" : datetime.date(datetime.datetime.now().year,12,24),
}
send_string = "Upcoming events: \n"
for key in events:
delta = events[key] - datetime.date.today()
if delta < datetime.timedelta(0):
delta += datetime.timedelta(days = 365)
send_string += key + ": " + str(delta.days) + " days \n"
return send_string
def bot_show_help(self, *args):
"""Show a help message.
Usage: help {keyword}
Keywords:
* no kw - list of all commands
* full - all commands and their docstring
* command-name - specific command and its docstring
"""
description = False
if len(args) > 0:
if args[0] == "full":
description = True
elif args[0] in self.commands:
send_text = "<b>" + args[0] + "</b>\n"
send_text += "<code>" + self.commands[args[0]].__doc__ + "</code>"
return send_text
send_text = "BeebBop, this is " + self.name + " (V." + self.version + ")\n"
send_text += "Here is what I can do up to now: \n"
entries = sorted(list(self.commands.keys()))
for entry in entries:
send_text += "<b>" + entry + "</b>"
if description:
send_text += " - <code>" + self.commands[entry].__doc__ + "</code>\n\n"
else:
send_text += "\n"
return send_text
def bot_print_log(self, *args):
"""Show an error-log, mostly of bad api-requests.
Usage: log {keyword}
Keywords:
* clear - clears log
* system - shows python output
"""
if "clear" in args:
self.persistence["bot"]["log"] = []
return "Log cleared"
elif "system" in args:
path="persistence/log.txt"
try:
file = open(path,"r")
content = file.read()
file.close()
return content
except:
return "could not read File"
send_text = ""
for event in self.persistence["bot"]["log"]:
send_text += event + "\n"
if send_text == "":
send_text += "No errors up to now"
return send_text
def bot_show_wikipedia(self, *args):
"""Shows the wikipedia entry for a given term
Usage: wikipedia &lt;language&gt; &lt;term&gt;
Keywords:
* language - de, fr, en ...
* term - search term, can consist of multiple words
"""
if len(args) == 0:
return "Please provide the first argument for language (de or fr or en or ...) and then your query"
args = list(args)
if len(args) >= 2:
url = "https://" + args.pop(0) + ".wikipedia.org/wiki/" + args.pop(0)
for word in args:
url += "_" + word
else:
url = "https://en.wikipedia.org/wiki/" + args[0]
print(url)
r = requests.get(url)
if r.status_code == 404:
return "No result found for query (404)"
return url
def bot_zvv(self, *args):
"""Uses the swiss travel api to return the best route between a start- and endpoint.
Usage: zvv &lt;start&gt; 'to' &lt;finish&gt;
Keywords:
* start - start point (can be more than 1 word9
* end - end point
"""
if len(args) < 3:
return "Please specify a start- and endpoint as well as a separator (the 'to')"
url = "http://transport.opendata.ch/v1/connections"
args = list(args)
goal = " ".join(args[:args.index("to")])
dest = " ".join(args[args.index("to")+1:])
data = {"from" : goal, "to" : dest, "limit" : 2}
try:
routes = requests.get(url, params=data).json()
result = routes["connections"]
text = result[0]["from"]["station"]["name"] + " :fast-forward_button: " + 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 += ":chequered_flag: " + datetime.datetime.fromtimestamp(int(con["to"]["arrivalTimestamp"])).strftime("%d/%m - %H:%M") + "\n"
text += ":hourglass_not_done: " + con["duration"] + "\n"
text += ":world_map: 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 += ":right_arrow: Linie " + 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 bot_cronjob(self, params):
"""Allows you to add a timed command, in a crontab-like syntax. Not implemented yet.
Example usage: /cronjob add 0 8 * * * weather Zürich
"""
return "I'm not functional yet. But when I am, it is gonna be legendary!"
def match_reddit_params(self, *args):
""" matches a list of two elements to a dict
returns: {"int": number, "str": name}
"""
r = {"int": 1, "str": "default"}
print(args)
if len(args) == 2:
p1, p2 = args[0], args[1]
try:
try:
r1 = int(p1)
r2 = p2
except:
r1 = int(p2)
r2 = p1
r["int"] = r1
r["str"] = r2
except:
self.write_bot_log("match_reddit_params", "could not match given params to known pattern")
elif len(args) == 1:
try:
try:
r["int"] = int(args[0])
except:
r["str"] = args[0]
except:
self.write_bot_log("match_reddit_params", "could not match given params to known pattern")
return r
def bot_tell_joke(self, *args):
"""Tells you the top joke on r/jokes
Usage: joke {number}
Keywords:
* number - number of jokes
"""
params_sorted = self.match_reddit_params(*args)
number = params_sorted["int"]
if len(params_sorted) > 1:
self.telegram.send_message("Ignored other params than number of jokes")
joke = reddit.get_random_rising("jokes", number, "text")
return joke
def bot_send_meme(self, *args):
"""Sends a meme from r/"""
subnames = {
"default" : "memes", # general case
"physics" : "physicsmemes",
"dank" : "dankmemes",
"biology" : "biologymemes",
"math" : "mathmemes"
}
params_sorted = self.match_reddit_params(*args)
number = params_sorted["int"]
if params_sorted["str"] in subnames:
subreddit_name = subnames[params_sorted["str"]]
else:
subreddit_name = subnames["default"]
urls = reddit.get_random_rising(subreddit_name, number, "photo")
for u in urls:
try:
self.telegram.send_photo(u["image"], u["caption"])
except:
self.write_bot_log("bot_send_meme", "could not send image")
return "Meme won't yeet"
return ""
def bot_send_news(self, *args):
"""Sends the first entries for new from r/"""
subnames = {
"default" : "worldnews",
"germany" : "germannews",
"france" : "francenews",
"europe" : "eunews",
"usa" : "usanews"
}
params_sorted = self.match_reddit_params(*args)
number = params_sorted["int"]
if params_sorted["str"] in subnames:
subreddit_name = subnames[params_sorted["str"]]
else:
subreddit_name = subnames["default"]
text = reddit.get_top(subreddit_name, number, "text")
return text
def bot_list(self, *args):
"""Interacts with a list (like a shopping list eg.)
Usage list &lt;name&gt; &lt;action&gt; {object}
Keyword:
* name - name of list
* action - create, delete, all, print, clear, add, remove
* object - might not be needed: index to delete, or item to add
Example usage:
list create shopping : creates list name shopping
list shopping add bread : adds bread to the list
list shopping print
list shopping clear
list all
"""
output = ""
# args = list(args)
if len(args) == 0:
return "Missing parameters"
try:
if args[0] == "all":
try:
return "Existing lists are: " + " ".join(list(self.persistence["global"]["lists"].keys()))
except:
return "No lists created."
if len(args) < 2:
return "Missing parameters"
if args[0] == "create":
lname = " ".join(args[1:])
self.persistence["global"]["lists"][lname] = []
output = "Created list " + lname
elif args[0] == "delete":
lname = " ".join(args[1:])
self.persistence["global"]["lists"].pop(lname, None) # no error if key doesnt exist
output = "Deleted list " + lname
else:
lname = args[0]
act = args[1]
if act == "print":
sl = self.persistence["global"]["lists"][lname]
output += "Content of " + lname + ":\n"
for ind,thing in enumerate(sl):
output += str(ind+1) + ". " + thing + "\n"
elif act == "clear":
self.persistence["global"]["lists"][lname] = []
output = "Cleared list " + lname
elif act == "add":
if len(args) < 3:
return "Missing paramaeter"
add = " ".join(args[2:])
self.persistence["global"]["lists"][lname] += [add]
return "Added " + add + "."
elif act == "remove":
if len(args) < 3:
return "Missing paramaeter"
try:
ind = int(args[2]) - 1
item = self.persistence["global"]["lists"][lname].pop(ind)
return "Removed " + item + " from list " + lname + "."
except:
return "Couldn't remove item."
return "Not working yet"
except:
output = "Could not handle your request. Maybe check the keys?"
return output
def bot_save_alias(self, *args):
"""Save a shortcut for special commands (+params)
Usage: alias &lt;alias-name&gt; {&lt;alias-name&gt; &lt;command&gt;}
Keywords:
* action - all, add, delete or clear (deleta all)
* alias-name - short name
* command - command to be executed, can contain arguments for the command
Example usage:
* alias sa list shopping add
* alias sp list shopping print
Means that '/sa ...' will now be treated as if typed '/list shopping add ...'
"""
# args = list(args)
if len(args) == 0:
return "Missing parameters"
try:
if args[0] == "clear":
self.persistence["bot"]["aliases"] = {}
return "All aliases cleared"
elif args[0] == "all":
try:
output = "Existing aliases are:\n"
for j, k in self.persistence["bot"]["aliases"].items():
output += j + " -&gt; " + k + "\n"
return output
except:
return "No aliases created."
if len(args) < 2:
return "Missing parameters"
if args[0] == "delete":
ak = args[1]
self.persistence["bot"]["aliases"].pop(ak, None) # no error if key doesnt exist
return "Deleted alias " + ak
if len(args) < 3:
return "Missing parameters"
if args[0] == "add":
ak = args[1]
cmd = " ".join(args[2:])
self.persistence["bot"]["aliases"][ak] = cmd
return "Created alias for " + ak
except:
return "Could not handle your request. Maybe check the keys?"
return "Bad input..."
self.persistence = prst
# Import submodules
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
self.commands = commands
# Mark them as available
self.help_module = self.commands.help.Help(prst)
self.sub_modules = {
"weather": self.commands.weather.Weather(self.api_weather, prst),
"help" : self.help_module,
"status" : self.commands.status.Status(name, version, prst),
"zvv" : self.commands.zvv.Zvv(prst),
"list" : self.commands.lists.Lists(prst),
# "alias" : commands.alias.Alias(self.dispatcher, prst),
"joke" : self.commands.reddit.Joke(self.api_reddit, prst),
"meme" : self.commands.reddit.Meme(self.api_reddit, prst),
# "news" : self.commands.reddit.News(self.api_reddit, prst),
"search" : self.commands.search.Search(self.api_search, prst),
"plaintext" : self.commands.plaintext.Plain(prst) # for handling non-command messages that should simply contribute to statistics
}
# must be a class that has a method create_handler
def add_commands(self):
for k in self.sub_modules:
self.dispatcher.add_handler(self.sub_modules[k].create_handler())
self.help_module.add_commands(self.sub_modules)
def start(self):
self.sub_modules = {**{"clock" : self.commands.clock.Clock(self.persistence, self.modules["clock"], self.api_art)}, **self.sub_modules}
self.add_commands()
self.telegram.start_polling()
# self.telegram.idle()

View File

@ -1 +0,0 @@
# Placeholder

View File

@ -1,5 +0,0 @@
from . import keys
from . import reddit
from . import weather
from . import reddit
from . import search

View File

@ -1,56 +0,0 @@
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 []

View File

@ -1,82 +0,0 @@
import requests
import datetime
class WeatherFetch():
def __init__(self, key):
self.last_fetch = datetime.datetime.fromtimestamp(0)
self.last_weather = ""
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
if delta.total_seconds()/60 > 60 or "\n" not in self.last_weather: # 1 hour passed:
data = {"lat" : location[0], "lon" : location[1], "exclude" : "minutely,hourly", "appid" : self.key, "units" : "metric"}
# today = datetime.datetime.today().weekday()
# days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
try:
weather = requests.get(self.url,params=data).json()
# categories = {"Clouds": ":cloud:", "Rain": ":cloud_with_rain:", "Thunderstorm": "thunder_cloud_rain", "Drizzle": ":droplet:", "Snow": ":cloud_snow:", "Clear": ":sun:", "Mist": "Mist", "Smoke": "Smoke", "Haze": "Haze", "Dust": "Dust", "Fog": "Fog", "Sand": "Sand", "Dust": "Dust", "Ash": "Ash", "Squall": "Squall", "Tornado": "Tornado",}
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"])]
})
except:
ret_weather = []
# now = weather["current"]
# message = "<b>Now:</b> " + categories[now["weather"][0]["main"]] + "\n"
# message += ":thermometer: " + str(int(now["temp"])) + "°\n\n"
# weather_days = weather["daily"]
# for i, day in enumerate(weather_days):
# if i == 0:
# message += "<b>" + "Today" + ":</b> " + categories[day["weather"][0]["main"]] + "\n"
# else:
# message += "<b>" + days[(today + i + 1) % 7] + ":</b> " + categories[day["weather"][0]["main"]] + "\n"
# message += ":thermometer: :fast_down_button: " + str(int(day["temp"]["min"])) + "° , :thermometer: :fast_up_button: " + str(int(day["temp"]["max"])) + "°\n\n"
# except:
# message = "Query failed, it's my fault, I'm sorry :sad:"
self.last_weather = ret_weather
self.last_fetch = datetime.datetime.now()
else:
ret_weather = self.last_weather
return ret_weather
# def get_weather_by_city(self, city):
# loc = get_coords_from_city(self, city)
# weather = self.show_weather(loc)
# return weather
# def get_coords_from_city(self, city):
# url = "https://devru-latitude-longitude-find-v1.p.rapidapi.com/latlon.php"
# data = {"location": city}
# headers = {
# "x-rapidapi-key" : "d4e0ab7ab3mshd5dde5a282649e0p11fd98jsnc93afd98e3aa",
# "x-rapidapi-host" : "devru-latitude-longitude-find-v1.p.rapidapi.com",
# }
# #try:
# resp = requests.request("GET", url, headers=headers, params=data)
# result = resp.text
# #except:
# # result = "???"
# return result

View File

@ -1,62 +0,0 @@
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, prst):
"""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)
self.persistence = prst
# Import submodules
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()
# and so on
self.telegram = Updater(api.keys.telegram_api, use_context=True)
self.dispatcher = self.telegram.dispatcher
self.commands = commands
# Mark them as available
self.help_module = self.commands.help.Help(prst)
self.sub_modules = {
"weather": self.commands.weather.Weather(self.api_weather, prst),
"help" : self.help_module,
"status" : self.commands.status.Status(name, version, prst),
"zvv" : self.commands.zvv.Zvv(prst),
"list" : self.commands.lists.Lists(prst),
# "alias" : commands.alias.Alias(self.dispatcher, prst),
"joke" : self.commands.reddit.Joke(self.api_reddit, prst),
"meme" : self.commands.reddit.Meme(self.api_reddit, prst),
# "news" : self.commands.reddit.News(self.api_reddit, prst),
"search" : self.commands.search.Search(self.api_search, prst),
"plaintext" : self.commands.plaintext.Plain(prst) # for handling non-command messages that should simply contribute to statistics
}
# must be a class that has a method create_handler
def add_commands(self):
for k in self.sub_modules:
self.dispatcher.add_handler(self.sub_modules[k].create_handler())
self.help_module.add_commands(self.sub_modules)
def start(self):
self.sub_modules = {**{"clock" : self.commands.clock.Clock(self.persistence, self.modules["clock"])}, **self.sub_modules}
self.add_commands()
self.telegram.start_polling()
# self.telegram.idle()

View File

@ -1,2 +0,0 @@
# Placeholder
from . import led

View File

@ -1,91 +0,0 @@
import time
import numpy as np
try:
from . import unicorn
output = unicorn.ClockOut
except ImportError:
from . import sim
output = sim.ClockOut
# import sim
# output = sim.ClockOut
class OutputHandler():
"""Matrix of led-points (RGB- values). It has the two given dimensions + a third which is given by the color values"""
# def __new__(subtype, shape, dtype=float, buffer=None, offset=0,
# strides=None, order=None, info=None):
# # Create the ndarray instance of our type, given the usual
# # ndarray input arguments. This will call the standard
# # ndarray constructor, but return an object of our type.
# # It also triggers a call to InfoArray.__array_finalize__
# # expand the given tuple (flat display) to a 3d array containing the colors as well
# nshape = (*shape, 3)
# obj = super(OutputHandler, subtype).__new__(subtype, nshape, "int",
# buffer, offset, strides,
# order)
# # set the new 'info' attribute to the value passed
# obj.info = info
# obj.OUT = output(shape)
# # Finally, we must return the newly created object:
# return obj
def __init__(self, shape):
nshape = (*shape, 3)
self.array = np.array(shape, dtype=np.uint8)
self.OUT = output(shape)
# def __array_finalize__(self, obj):
# self.OUT = sim.ClockOut()
# # ``self`` is a new object resulting from
# # ndarray.__new__(), therefore it only has
# # attributes that the ndarray.__new__ constructor gave it -
# # i.e. those of a standard ndarray.
# #
# # We could have got to the ndarray.__new__ call in 3 ways:
# # From an explicit constructor - e.g. InfoArray():
# # obj is None
# # (we're in the middle of the InfoArray.__new__
# # constructor, and self.info will be set when we return to
# # InfoArray.__new__)
# if obj is None: return
# # From view casting - e.g arr.view(InfoArray):
# # obj is arr
# # (type(obj) can be InfoArray)
# # From new-from-template - e.g infoarr[:3]
# # type(obj) is InfoArray
# #
# # Note that it is here, rather than in the __new__ method,
# # that we set the default value for 'info', because this
# # method sees all creation of default objects - with the
# # InfoArray.__new__ constructor, but also with
# # arr.view(InfoArray).
# self.info = getattr(obj, 'info', None)
# # We do not need to return anything
def SHOW(self):
# self.output.set_matrix(self)
self.OUT.put(self.array)
# def __init__(self, width, height, primary = [200, 200, 200], secondary = [10, 200, 10], error = [200, 10, 10]):
# """width is presumed to be larger than height"""
# self.width = width
# self.height = height
# self.output = HAT.UnicornHat(width, height)
# self.primary = primary
# self.secondary = secondary
# self.red = error

View File

@ -1,27 +0,0 @@
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
mpl.rcParams['toolbar'] = 'None'
mpl.use('WX')
class ClockOut():
"""Simulate a clock output on a computer screen"""
def __init__(self, shape):
plt.axis('off')
plt.ion()
nshape = (*shape, 3)
zero = np.zeros(nshape)
self.figure, ax = plt.subplots()
ax.set_axis_off()
i = Image.fromarray(zero, "RGB")
self.canvas = ax.imshow(i)
def put(self, matrix):
matrix_rescale = matrix / 255
self.canvas.set_array(matrix_rescale)
self.figure.canvas.draw()
self.figure.canvas.flush_events()

44
clock/cin.py Normal file
View File

@ -0,0 +1,44 @@
import datetime
import time
from threading import Thread, Timer
from . import hardware, helpers
class SensorReadout:
"""Overview class for (actual and potential) sensor sources"""
def __init__(self, prst=object):
""""""
self.persistence = prst
self.sensor_modules = { # we already call them, they are objects and not classes anymore
"temperature" : hardware.sensors.TemperatureModule(),
"humidity" : hardware.sensors.HumidityModule(),
"brightness" : hardware.sensors.BrightnessModule(),
# more to come?
}
def start(self):
helpers.timer.RepeatedTimer(300, self.spread_measure)
def spread_measure(self):
results = 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()
results[name].append(measure)
time.sleep(3)
self.save_results(results)
def save_results(self, results):
current_minute = int(datetime.datetime.now().timestamp() // 60)
self.persistence["clock"]["sensors"]["time"] += [current_minute]
for name in results.keys():
keep_value = sum(results[name]) / len(results[name])
self.persistence["clock"]["sensors"][name] += [keep_value]

View File

@ -1,13 +1,12 @@
import datetime
import time
import json
from threading import Thread, Timer
from threading import Thread
import numpy
from . import api, helpers
from . import hardware, helpers
class ClockFace(object):
class ClockFace:
"""Actual functions one might need for a clock"""
def __init__(self, text_speed=18, prst=object):
@ -19,10 +18,10 @@ class ClockFace(object):
self.primary = [200, 200, 200]
self.secondary = [10, 200, 10]
self.error = [200, 10, 10]
self.shape = (16,32)
# shape: (16,32) is hard-coded for the moment
self.persistence = prst
self.IO = api.led.OutputHandler(self.shape)
self.IO = hardware.led.get_handler()
self.shape = self.IO.shape # (16,32) for now
self.MOP = helpers.helper.MatrixOperations(self.shape, default_colors={"primary": self.primary, "secondary": self.secondary, "error": self.error})
self.output_thread = ""
@ -41,7 +40,8 @@ class ClockFace(object):
self.clock_loop()
while datetime.datetime.now().strftime("%H%M%S")[-2:] != "00":
pass
RepeatedTimer(60, self.clock_loop)
helpers.timer.RepeatedTimer(60, self.clock_loop)
self.clock_loop()
def clock_loop(self):
@ -99,8 +99,7 @@ class ClockFace(object):
def set_face(self):
"""Set the clock face (time + weather) by getting updated info - gets called every minute"""
face = self.MOP.clock_face(self.weather)
self.IO.array = face * self.brightness
self.IO.SHOW()
self.IO.put(face * self.brightness)
def set_brightness(self, value=-1, overwrite=[]):
@ -122,6 +121,7 @@ class ClockFace(object):
self.brightness = brightness
def text_scroll(self, text, color=[[200,200,200]]):
pixels = self.MOP.text_converter(text, 12, color)
sleep_time = 1 / self.tspeed
@ -132,39 +132,7 @@ class ClockFace(object):
for i in range(frames):
visible = pixels[:,i:width+i]
self.IO.array = visible*self.brightness
self.IO.SHOW()
self.IO.put(visible*self.brightness)
time.sleep(sleep_time)
time.sleep(10 * sleep_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

View File

@ -0,0 +1,2 @@
# Placeholder
from . import led, sensors

17
clock/hardware/led.py Normal file
View 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

View 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)

80
clock/hardware/sensors.py Normal file
View File

@ -0,0 +1,80 @@
import time
import logging
logger = logging.getLogger(__name__)
class TempSim:
"""Simulates a temperature for running on windows"""
temperature = 23 # return a celsius value
humidity = 0.3
class SensorModule:
def __init__(self):
logger.info("Using module " + self.__class__.__name__)
class LightSim:
def input(self, *args):
return 1
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:
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 = 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

52
clock/hardware/sim.py Normal file
View File

@ -0,0 +1,52 @@
import sys
import colorsys
import pygame.gfxdraw
import time
import pygame
import numpy
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 = numpy.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, matrix):
self.screen.fill((0, 0, 0))
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT:
print("Exiting...")
pygame.quit()
sys.exit()
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)

View File

@ -1,30 +1,37 @@
import colorsys
import time
import numpy
import RPi.GPIO as GPIO
try:
import RPi.GPIO as GPIO
except ImportError:
from unittest.mock import Mock
GPIO = Mock()
SETUP_FAIL = True
class ClockOut(object):
def __init__(self, shape):
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.SOF = 0x72
self.DELAY = 1.0/120
self.HEIGHT = shape[0] #16
self.WIDTH = shape[1] #32
self.HEIGHT = self.shape[0] #16
self.WIDTH = self.shape[1] #32
self.reset_clock()
@ -60,8 +67,6 @@ class ClockOut(object):
GPIO.output(self.PIN_CLK, GPIO.LOW)
def put(self, matrix):
"""Sets a height x width matrix directly"""
self.reset_clock()
@ -81,7 +86,7 @@ class ClockOut(object):
buff2 = numpy.rot90(matrix[:self.HEIGHT,:16],3)
buff1 = numpy.rot90(matrix[:self.HEIGHT,16:32],1)
##########################################################
# separated these are: 16x16x3 arrays
buff1, buff2 = [(x.reshape(768)).astype(numpy.uint8).tolist() for x in (buff1, buff2)]
self.spi_write(buff1, buff2)

View File

@ -1 +1 @@
from . import helper
from . import helper, timer

View File

@ -3,46 +3,12 @@ import numpy as np
import datetime
import time
####### bulky hard-coded values:
digits = {
"1" : [[0,0,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]],
"2" : [[1,1,1],[0,0,1],[1,1,1],[1,0,0],[1,1,1]],
"3" : [[1,1,1],[0,0,1],[1,1,1],[0,0,1],[1,1,1]],
"4" : [[1,0,1],[1,0,1],[1,1,1],[0,0,1],[0,0,1]],
"5" : [[1,1,1],[1,0,0],[1,1,1],[0,0,1],[1,1,1]],
"6" : [[1,1,1],[1,0,0],[1,1,1],[1,0,1],[1,1,1]],
"7" : [[1,1,1],[0,0,1],[0,0,1],[0,0,1],[0,0,1]],
"8" : [[1,1,1],[1,0,1],[1,1,1],[1,0,1],[1,1,1]],
"9" : [[1,1,1],[1,0,1],[1,1,1],[0,0,1],[1,1,1]],
"0" : [[1,1,1],[1,0,1],[1,0,1],[1,0,1],[1,1,1]],
"-" : [[0,0,0],[0,0,0],[1,1,1],[0,0,0],[0,0,0]],
"-1" : [[0,0,1],[0,0,1],[1,1,1],[0,0,1],[0,0,1]],
"error" : [[1,0,1],[1,0,1],[0,1,0],[1,0,1],[1,0,1]],
} # these are 2-d arrays as we only work with one or 2 colors, and not the whole rgb spectrum
##place of numbers, invariant (for the given shape of 16x32)
# 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]]
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"
}
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))

103
clock/helpers/shapes.py Normal file
View 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"
}

29
clock/helpers/timer.py Normal file
View File

@ -0,0 +1,29 @@
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

View File

@ -15,6 +15,7 @@ import xmltodict
import requests
from . import helpers
class DashBoard():
""""""
@ -44,11 +45,12 @@ class DashBoard():
kids = [
self.card_header(),
dbc.CardColumns([
self.card_weather(),
# self.card_weather(),
*self.cards_lists(),
self.card_bot_stats(),
self.card_news(),
self.card_xkcd(),
self.card_sensor_stats(),
])
]
return kids
@ -98,7 +100,7 @@ class DashBoard():
card = dbc.Card(
[
dbc.CardBody([
html.H4("Statistiken", className="card-title"),
html.H4("Chat-Metriken", className="card-title"),
dcc.Graph(figure=self.stat_graph, config={'displayModeBar': False})
]),
],
@ -107,6 +109,45 @@ class DashBoard():
)
return card
def card_sensor_stats(self):
fig = go.Figure()
sensors = self.persistence["clock"]["sensors"]
time = sensors["time"]
time = time - time[0] # rescale
for sensor in sensors.keys():
if sensor != "time":
fig.add_trace(go.Scatter(x=time, y=sensors[sensor], mode="lines", text=sensor, line=dict(width=4)))
fig.layout.update(
# xaxis = {
# 'showgrid': False, # thin lines in the background
# 'zeroline': False, # thick line at x=0
# 'visible': False, # numbers below
# }, # the same for yaxis
# yaxis = {
# 'showgrid': False, # thin lines in the background
# 'zeroline': False, # thick line at x=0
# 'visible': False, # numbers below
# }, # the same for yaxis
showlegend=False,
# margin=dict(l=0, r=0, t=0, b=0),
# paper_bgcolor='rgba(0,0,0,0)',
# plot_bgcolor='rgba(0,0,0,0)',
)
card = dbc.Card(
[
dbc.CardBody([
html.H4("Sensor-Metriken", className="card-title"),
dcc.Graph(figure=fig, config={'displayModeBar': False})
]),
],
color="dark",
inverse=True,
)
return card
def card_weather(self):
def weather_item(name, overview, temps):
@ -222,24 +263,9 @@ class DashBoard():
######### helper:
def set_stats(self):
def cleanse_graph(category):
x = self.persistence["bot"][category]["hour"]
y = self.persistence["bot"][category]["count"]
xn = range(x[0], x[-1]+1)
yn = []
count = 0
for x_i in xn:
if x_i in x:
yn.append(y[count])
count += 1
else:
yn.append(0)
xn = [i - int(x[0]) for i in xn]
return xn, yn
xs, ys = cleanse_graph("send_activity")
xr, yr = cleanse_graph("receive_activity")
xe, ye = cleanse_graph("execute_activity")
xs, ys = helpers.clean_axis(self.persistence["bot"]["send_activity"]["hour"], self.persistence["bot"]["send_activity"]["count"])
xr, yr = helpers.clean_axis(self.persistence["bot"]["receive_activity"]["hour"], self.persistence["bot"]["receive_activity"]["count"])
xe, ye = helpers.clean_axis(self.persistence["bot"]["execute_activity"]["hour"], self.persistence["bot"]["execute_activity"]["count"])
fig = go.Figure()
fig.add_trace(go.Scatter(x=xr, y=yr, mode="lines", text="Gelesen", line=dict(width=4)))

18
dashboard/helpers.py Normal file
View File

@ -0,0 +1,18 @@
def clean_axis(x,y):
"""x is the time the point in y was taken"""
try:
xn = range(x[0], x[-1]+1)
yn = []
count = 0
for x_i in xn:
if x_i in x:
yn.append(y[count])
count += 1
else:
yn.append(0)
xn = [i - int(x[0]) for i in xn]
except:
xn = []
yn = []
return xn, yn

View File

@ -1,7 +1,7 @@
# functionality
import bot2.main
import clock.main
import dashboard.main
from bot import main
from clock import cin, cout
from dashboard import dout
import persistence.main
@ -23,24 +23,26 @@ else:
)
class Launcher():
class Launcher:
"""Launches all other submodules"""
def __init__(self):
""""""
self.persistence = persistence.main.PersistentDict("persistence/prst.json")
self.logger = logging.getLogger(__name__)
self.logger.info("Starting")
self.logger.info("Launcher initialized")
if len(self.persistence) == 0:
self.init_persistence()
self.persistence["global"]["reboots"] += 1
self.clock_module = clock.main.ClockFace(prst=self.persistence)
self.bot_module = bot2.main.ChatBot(name="Norbit", version="3.0a", prst=self.persistence)
self.dashboard_module = dashboard.main.DashBoard(host_ip="0.0.0.0", prst=self.persistence)
self.clock_module = cout.ClockFace(prst=self.persistence)
self.bot_module = main.ChatBot(name="Norbit", version="3.0a", prst=self.persistence)
self.dashboard_module = dout.DashBoard(host_ip="0.0.0.0", prst=self.persistence)
self.sensors = cin.SensorReadout(prst=self.persistence)
self.modules = {
"sensors" : self.sensors,
"bot" : self.bot_module,
"clock" : self.clock_module,
"dashboard" : self.dashboard_module,
@ -50,10 +52,10 @@ class Launcher():
self.logger.info("Starting module "+ module.__class__.__name__)
module.modules = self.modules
module.start()
def init_persistence(self):
self.logger.warn("No persistence found, created a new one")
self.logger.warning("No persistence found, created a new one")
self.persistence["bot"] = {
"send_activity" : {"hour":[], "count":[]},
@ -63,7 +65,14 @@ class Launcher():
"chat_members": {},
"aliases" : {}
}
self.persistence["clock"] = {}
self.persistence["clock"] = {
"sensors" : {
"time" : [],
"temperature":[],
"humidity":[],
"brightness" : [],
}
}
self.persistence["dashboard"] = {}
self.persistence["global"] = {
"lists" : {},

View File

@ -12,10 +12,12 @@ class PersistentDict(dict):
self.path = file_name
self.last_action = ""
if not os.path.exists(self.path):
try:
self.read_dict()
except:
with open(self.path, "a") as f:
f.write("{}")
self.read_dict()
## helper - functions
def write_dict(self):