diff --git a/bot/api/__init__.py b/bot/api/__init__.py index dcf2c80..afd5954 100644 --- a/bot/api/__init__.py +++ b/bot/api/__init__.py @@ -1 +1,6 @@ -# Placeholder +from . import keys +from . import reddit +from . import weather +from . import reddit +from . import search +from . import metmuseum \ No newline at end of file diff --git a/bot/api/google.py b/bot/api/google.py deleted file mode 100644 index 2ff701c..0000000 --- a/bot/api/google.py +++ /dev/null @@ -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 " + param_string.replace("+"," ") + ":\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 diff --git a/bot/api/metmuseum.py b/bot/api/metmuseum.py new file mode 100644 index 0000000..c2807e3 --- /dev/null +++ b/bot/api/metmuseum.py @@ -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 diff --git a/bot/api/reddit.py b/bot/api/reddit.py index 49ee0d3..2cd5112 100644 --- a/bot/api/reddit.py +++ b/bot/api/reddit.py @@ -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 += "" + submission.title + "" + "\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 += "" + submission.title + "" + "\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 [] diff --git a/bot2/api/search.py b/bot/api/search.py similarity index 100% rename from bot2/api/search.py rename to bot/api/search.py diff --git a/bot/api/telegram.py b/bot/api/telegram.py deleted file mode 100644 index c046396..0000000 --- a/bot/api/telegram.py +++ /dev/null @@ -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("<","<").replace(">", ">") - # 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 \ No newline at end of file diff --git a/bot/api/weather.py b/bot/api/weather.py index 4570177..7e173b5 100644 --- a/bot/api/weather.py +++ b/bot/api/weather.py @@ -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 + diff --git a/bot2/commands/__init__.py b/bot/commands/__init__.py similarity index 100% rename from bot2/commands/__init__.py rename to bot/commands/__init__.py diff --git a/bot2/commands/alias.py b/bot/commands/alias.py similarity index 100% rename from bot2/commands/alias.py rename to bot/commands/alias.py diff --git a/bot2/commands/clock.py b/bot/commands/clock.py similarity index 83% rename from bot2/commands/clock.py rename to bot/commands/clock.py index 9044b2e..9e33bdd 100644 --- a/bot2/commands/clock.py +++ b/bot/commands/clock.py @@ -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 \ No newline at end of file diff --git a/bot2/commands/help.py b/bot/commands/help.py similarity index 100% rename from bot2/commands/help.py rename to bot/commands/help.py diff --git a/bot2/commands/lists.py b/bot/commands/lists.py similarity index 100% rename from bot2/commands/lists.py rename to bot/commands/lists.py diff --git a/bot2/commands/plaintext.py b/bot/commands/plaintext.py similarity index 100% rename from bot2/commands/plaintext.py rename to bot/commands/plaintext.py diff --git a/bot2/commands/reddit.py b/bot/commands/reddit.py similarity index 100% rename from bot2/commands/reddit.py rename to bot/commands/reddit.py diff --git a/bot2/commands/search.py b/bot/commands/search.py similarity index 100% rename from bot2/commands/search.py rename to bot/commands/search.py diff --git a/bot2/commands/status.py b/bot/commands/status.py similarity index 100% rename from bot2/commands/status.py rename to bot/commands/status.py diff --git a/bot2/commands/template.py b/bot/commands/template.py similarity index 100% rename from bot2/commands/template.py rename to bot/commands/template.py diff --git a/bot2/commands/weather.py b/bot/commands/weather.py similarity index 100% rename from bot2/commands/weather.py rename to bot/commands/weather.py diff --git a/bot2/commands/zvv.py b/bot/commands/zvv.py similarity index 100% rename from bot2/commands/zvv.py rename to bot/commands/zvv.py diff --git a/bot/framework.py b/bot/framework.py deleted file mode 100644 index b4fd3a2..0000000 --- a/bot/framework.py +++ /dev/null @@ -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 " + cmd + " to " + self.persistence["bot"]["aliases"][cmd] + " and got:\n\n" - result += call_command(new_cmd, params) - else: - result = "Command " + tmp[0] + " 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 " + command_candidate + "?") - 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] - - \ No newline at end of file diff --git a/bot/main.py b/bot/main.py index 36ff752..3fccc3a 100644 --- a/bot/main.py +++ b/bot/main.py @@ -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 += "
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) + "
" - - 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 = "Now: " + categories[now["short"]] + "\n" - message += ":thermometer: " + str(now["temps"][0]) + "°\n\n" - - for i, day in enumerate(weather): - if i == 0: - message += "" + "Today" + ": " + categories[day["short"]] + "\n" - else: - message += "" + days[(today + i + 1) % 7] + ": " + 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 = "" + args[0] + "\n" - send_text += "" + self.commands[args[0]].__doc__ + "" - 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 += "" + entry + "" - if description: - send_text += " - " + self.commands[entry].__doc__ + "\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 <language> <term> - 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 <start> 'to' <finish> - 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 <name> <action> {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 <alias-name> {<alias-name> <command>} - 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 + " -> " + 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() diff --git a/bot2/__init__.py b/bot2/__init__.py deleted file mode 100644 index dcf2c80..0000000 --- a/bot2/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Placeholder diff --git a/bot2/api/__init__.py b/bot2/api/__init__.py deleted file mode 100644 index d5f9e9b..0000000 --- a/bot2/api/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import keys -from . import reddit -from . import weather -from . import reddit -from . import search \ No newline at end of file diff --git a/bot2/api/reddit.py b/bot2/api/reddit.py deleted file mode 100644 index 2cd5112..0000000 --- a/bot2/api/reddit.py +++ /dev/null @@ -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 [] diff --git a/bot2/api/weather.py b/bot2/api/weather.py deleted file mode 100644 index 7e173b5..0000000 --- a/bot2/api/weather.py +++ /dev/null @@ -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 = "Now: " + 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 += "" + "Today" + ": " + categories[day["weather"][0]["main"]] + "\n" - # else: - # message += "" + days[(today + i + 1) % 7] + ": " + 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 - diff --git a/bot2/main.py b/bot2/main.py deleted file mode 100644 index 95c1812..0000000 --- a/bot2/main.py +++ /dev/null @@ -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() diff --git a/clock/api/__init__.py b/clock/api/__init__.py deleted file mode 100644 index 568a664..0000000 --- a/clock/api/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Placeholder -from . import led \ No newline at end of file diff --git a/clock/api/led.py b/clock/api/led.py deleted file mode 100644 index b57e965..0000000 --- a/clock/api/led.py +++ /dev/null @@ -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 - - - - - diff --git a/clock/api/sim.py b/clock/api/sim.py deleted file mode 100644 index 6792050..0000000 --- a/clock/api/sim.py +++ /dev/null @@ -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() diff --git a/clock/cin.py b/clock/cin.py new file mode 100644 index 0000000..b7db91a --- /dev/null +++ b/clock/cin.py @@ -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] + + diff --git a/clock/main.py b/clock/cout.py similarity index 77% rename from clock/main.py rename to clock/cout.py index 53e80ac..0c74220 100644 --- a/clock/main.py +++ b/clock/cout.py @@ -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 diff --git a/clock/hardware/__init__.py b/clock/hardware/__init__.py new file mode 100644 index 0000000..221ab03 --- /dev/null +++ b/clock/hardware/__init__.py @@ -0,0 +1,2 @@ +# Placeholder +from . import led, sensors \ No newline at end of file diff --git a/clock/hardware/led.py b/clock/hardware/led.py new file mode 100644 index 0000000..f4a5244 --- /dev/null +++ b/clock/hardware/led.py @@ -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 + + diff --git a/clock/hardware/neopixel.py b/clock/hardware/neopixel.py new file mode 100644 index 0000000..67707e2 --- /dev/null +++ b/clock/hardware/neopixel.py @@ -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) + diff --git a/clock/hardware/sensors.py b/clock/hardware/sensors.py new file mode 100644 index 0000000..e9fe8f4 --- /dev/null +++ b/clock/hardware/sensors.py @@ -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 diff --git a/clock/hardware/sim.py b/clock/hardware/sim.py new file mode 100644 index 0000000..3c4e22f --- /dev/null +++ b/clock/hardware/sim.py @@ -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) diff --git a/clock/api/unicorn.py b/clock/hardware/unicorn.py similarity index 83% rename from clock/api/unicorn.py rename to clock/hardware/unicorn.py index a105b55..356bb1a 100644 --- a/clock/api/unicorn.py +++ b/clock/hardware/unicorn.py @@ -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) diff --git a/clock/helpers/__init__.py b/clock/helpers/__init__.py index 34fdc42..7e5ed98 100644 --- a/clock/helpers/__init__.py +++ b/clock/helpers/__init__.py @@ -1 +1 @@ -from . import helper \ No newline at end of file +from . import helper, timer \ No newline at end of file diff --git a/clock/helpers/helper.py b/clock/helpers/helper.py index 56f6ad7..ce19dd1 100644 --- a/clock/helpers/helper.py +++ b/clock/helpers/helper.py @@ -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)) diff --git a/clock/helpers/shapes.py b/clock/helpers/shapes.py new file mode 100644 index 0000000..9e8c108 --- /dev/null +++ b/clock/helpers/shapes.py @@ -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" +} \ No newline at end of file diff --git a/clock/helpers/timer.py b/clock/helpers/timer.py new file mode 100644 index 0000000..6530aa4 --- /dev/null +++ b/clock/helpers/timer.py @@ -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 \ No newline at end of file diff --git a/dashboard/main.py b/dashboard/dout.py similarity index 80% rename from dashboard/main.py rename to dashboard/dout.py index 92484e8..fece604 100644 --- a/dashboard/main.py +++ b/dashboard/dout.py @@ -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))) diff --git a/dashboard/helpers.py b/dashboard/helpers.py new file mode 100644 index 0000000..7a834a7 --- /dev/null +++ b/dashboard/helpers.py @@ -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 \ No newline at end of file diff --git a/launcher.py b/launcher.py index b5de8bc..9f57628 100644 --- a/launcher.py +++ b/launcher.py @@ -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" : {}, diff --git a/persistence/main.py b/persistence/main.py index 5a4d083..8db6f02 100644 --- a/persistence/main.py +++ b/persistence/main.py @@ -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):