From d9ee163ad6d3b445a5237d921a94d25b402137d5 Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Thu, 18 Feb 2021 10:28:23 +0100 Subject: [PATCH] Readded clock commands --- bot2/commands/clock.py | 156 ++++++++++++++++++++++++++++++++++++++-- bot2/commands/status.py | 2 +- bot2/main.py | 28 ++++---- clock/main.py | 142 ++++++++++++++++++------------------ dashboard/main.py | 7 +- launcher.py | 33 +++------ wrapper.py | 118 ------------------------------ 7 files changed, 250 insertions(+), 236 deletions(-) delete mode 100644 wrapper.py diff --git a/bot2/commands/clock.py b/bot2/commands/clock.py index 6b54824..6e98d47 100644 --- a/bot2/commands/clock.py +++ b/bot2/commands/clock.py @@ -1,23 +1,165 @@ from .template import * +import time +import numpy -CHOOSE, ARGS = range(2) +CHOOSE, ADDARG = range(2) +MESSAGE, WAKE, ALARM, IMAGE, ART = range(3,8) class Clock(BotFunc): """pass on commands to clock-module""" - def __init__(self, prst, hw_commands): + def __init__(self, prst, clock_module): super().__init__(prst) - self.hw_commands = hw_commands + self.clock = clock_module def create_handler(self): handler = ConversationHandler( entry_points=[CommandHandler("clock", self.entry_point)], states={ - CHOOSE : [], - ARGS : [] + CHOOSE : [ + CallbackQueryHandler(self.wake_light, pattern="^wake$"), + CallbackQueryHandler(self.alarm_blink, pattern="^alarm$"), + CallbackQueryHandler(self.show_message, pattern="^message$"), + CallbackQueryHandler(self.show_image, pattern="^image$"), + CallbackQueryHandler(self.art_gallery, pattern="^gallery$"), + ], + ADDARG : [MessageHandler(Filters.text, callback=self.get_arg1)], + MESSAGE: [MessageHandler(Filters.text, callback=self.exec_show_message)], + WAKE : [MessageHandler(Filters.text, callback=self.exec_wake_light)], + ALARM : [MessageHandler(Filters.text, callback=self.exec_alarm_blink)], + IMAGE : [MessageHandler(Filters.photo, callback=self.exec_show_image)], + ART : [MessageHandler(Filters.text, callback=self.exec_art_gallery)], }, fallbacks=[CommandHandler('clock', self.entry_point)], ) return handler - def entry_point(self): - super().entry_point() \ No newline at end of file + def entry_point(self, update: Update, context: CallbackContext) -> None: + super().entry_point() + keyboard = [ + [InlineKeyboardButton("Make a wake-light", callback_data="wake")], + [InlineKeyboardButton("Blink as alarm", callback_data="alarm")], + [InlineKeyboardButton("Show a message", callback_data="message")], + [InlineKeyboardButton("Show an image", callback_data="image")], + [InlineKeyboardButton("Art gallery!", callback_data="gallery")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text("What exactly do you want?", reply_markup=reply_markup) + return CHOOSE + + + def wake_light(self, update: Update, context: CallbackContext) -> None: + query = update.callback_query + query.answer() + + query.edit_message_text("Ok. How long should the color cycle last? (In seconds)") + return WAKE + + def alarm_blink(self, update: Update, context: CallbackContext) -> None: + query = update.callback_query + query.answer() + + query.edit_message_text("Ok. How long should it blink? (In seconds)") + self.next_state = {"ALARM" : "What frequency (Hertz)"} + return ADDARG + + def show_message(self, update: Update, context: CallbackContext) -> None: + query = update.callback_query + query.answer() + + query.edit_message_text("Ok. What message will I show?") + return MESSAGE + + def show_image(self, update: Update, context: CallbackContext) -> None: + query = update.callback_query + query.answer() + + query.edit_message_text("How long (in minutes) should the image be displayed?") + self.next_state = {"IMAGE" : "Please send me the photo to display."} + return ADDARG + + def art_gallery(self, update: Update, context: CallbackContext) -> None: + query = update.callback_query + query.answer() + + query.edit_message_text("Ok. How long should we display art? (in hours") + return ART + + def get_arg1(self, update: Update, context: CallbackContext) -> None: + a = update.message.text + self.additional_argument = a + update.message.reply_text("Furthermore: "+ list(self.next_state.values())[0]) + return list(self.next_state.keys())[0] + + + + +###### actually running clock actions + def exec_wake_light(self, update: Update, context: CallbackContext) -> None: + duration = update.message.text + + def output(duration): + self.clock.set_brightness(value=0.1) + start_color = numpy.array([153, 0, 51]) + end_color = numpy.array([255, 255, 0]) + empty = numpy.zeros((16,32)) + ones = empty + ones[ones == 0] = 1 + gradient = end_color - start_color + # 20 steps should be fine => sleep_time = duration / 20 + for i in range(20): + ct = i/20 * gradient + col = [int(x) for x in ct+start_color] + self.clock.IO.set_matrix(ones,colors=[col]) + time.sleep(int(duration) / 20) + + self.clock.run(output,(duration,)) + return ConversationHandler.END + + + def exec_alarm_blink(self, update: Update, context: CallbackContext) -> None: + duration = self.additional_argument + frequency = update.message.text + + def output(duration, frequency): + self.set_brightness(value=1) + duration = int(duration) + frequency = int(frequency) + n = duration * frequency / 2 + empty = numpy.zeros((16,32)) + red = empty.copy() + red[red == 0] = 3 + for i in range(int(n)): + self.IO.set_matrix(red) + time.sleep(1/frequency) + self.IO.set_matrix(empty) + time.sleep(1/frequency) + + if not(duration == 0 or frequency == 0): + update.message.reply_text("Now blinking") + self.clock.run(output,(duration, frequency)) + print("DOOONE") + return ConversationHandler.END + + + + def exec_show_image(self, update: Update, context: CallbackContext) -> None: + duration = self.additional_argument + img = update.message.photo + + def output(image, duration): + self.clock.IO.set_matrix_rgb([100,0,0]) + + self.clock.run(output,("image", duration)) + return ConversationHandler.END + + + def exec_show_message(self, update: Update, context: CallbackContext) -> None: + message_str = update.message.text + update.message.reply_text("Now showing: " + message_str) + self.clock.run(self.clock.IO.text_scroll,(message_str, self.clock.tspeed, [200,200,200])) + return ConversationHandler.END + + + def exec_art_gallery(self, update: Update, context: CallbackContext) -> None: + update.message.reply_text("Puuh, thats tough, I'm not ready for that.") + return ConversationHandler.END \ No newline at end of file diff --git a/bot2/commands/status.py b/bot2/commands/status.py index 4ca20ba..03ac9bd 100644 --- a/bot2/commands/status.py +++ b/bot2/commands/status.py @@ -61,7 +61,7 @@ class Status(BotFunc): message += "IP (public): `" + ip + "`\n" message += "IP (private): `" + str(local_ips) + "`\n" u = str(self.get_ngrok_url()) - message += "URL: [" + u + "](" + u + "]\n" + message += "URL: [" + u + "](" + u + ")\n" tot_r = np.array(self.persistence["bot"]["receive_activity"]["count"]).sum() message += "Total messages read: `" + str(tot_r) + "`\n" diff --git a/bot2/main.py b/bot2/main.py index 2b534ab..932d958 100644 --- a/bot2/main.py +++ b/bot2/main.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) class ChatBot(): """better framwork - unites all functions""" - def __init__(self, name, version, hw_commands, prst): + 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 @@ -16,7 +16,9 @@ class ChatBot(): -> 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.reddit_api = api.reddit.RedditFetch() @@ -24,18 +26,19 @@ class ChatBot(): 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 = commands.help.Help(prst) + self.help_module = self.commands.help.Help(prst) self.sub_modules = { - "weather": commands.weather.Weather(self.api_weather, prst), + "weather": self.commands.weather.Weather(self.api_weather, prst), "help" : self.help_module, - "status" : commands.status.Status(name, version, prst), - "zvv" : commands.zvv.Zvv(prst), - "list" : commands.lists.Lists(prst), + "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), - "clock" : commands.clock.Clock(prst, hw_commands), - "plaintext" : commands.plaintext.Plain(prst) # for handling non-command messages that should simply contribute to statistics + "plaintext" : self.commands.plaintext.Plain(prst) # for handling non-command messages that should simply contribute to statistics } # "events" : self.bot_print_events, @@ -46,9 +49,6 @@ class ChatBot(): # "news" : self.bot_send_news, # } # must be a class that has a method create_handler - - self.add_commands() - def add_commands(self): for k in self.sub_modules: @@ -56,6 +56,8 @@ class ChatBot(): self.help_module.add_commands(self.sub_modules) - def START(self): + 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() \ No newline at end of file + # self.telegram.idle() diff --git a/clock/main.py b/clock/main.py index bae504e..a0c5cea 100644 --- a/clock/main.py +++ b/clock/main.py @@ -1,19 +1,19 @@ import datetime import time import json -from threading import Thread +from threading import Thread, Timer import numpy - from clock.api import led -################################################################################ -#start of actual programm. + class ClockFace(object): """Actual functions one might need for a clock""" def __init__(self, text_speed=18, prst=""): """""" + # added by the launcher, we have self.modules (dict) + self.persistence = prst self.IO = led.OutputHandler(32,16) self.tspeed = text_speed @@ -23,16 +23,47 @@ class ClockFace(object): self.output_queue = [] # Threads to execute next - self.commands = { - "blink" : self.alarm_blink, - "wakeup" : self.wake_light, - "showmessage" : self.show_message, - } - - self.weather = "" + self.weather = {"weather":"", "high":"", "low":"", "show":"temps"} + self.weather_raw = {} + # different? self.brightness_overwrite = {"value" : 1, "duration" : 0} + def start(self): + while datetime.datetime.now().strftime("%H%M%S")[-2:] != "00": + pass + RepeatedTimer(60, self.clock_loop) + + + def clock_loop(self): + t = int(datetime.datetime.now().strftime("%H%M")) + + if t % 5 == 0: + # switch secondary face every 5 minutes + weather = self.modules["bot"].api_weather.show_weather([47.3769, 8.5417]) # zürich + + if weather != self.weather_raw and len(weather) != 0: + td = weather[1] + low = td["temps"][0] + high = td["temps"][1] + self.weather["weather"] = td["short"] + self.weather["high"] = high + self.weather["low"] = low + elif len(weather) == 0: + self.weather["weather"] = "error" + self.weather["high"] = "error" + self.weather["low"] = "error" + # if weather == self.weather.raw do nothing + + if self.weather["show"] == "weather": + next = "temps" + else: + next = "weather" + self.weather["show"] = next + + self.set_face() + + def run(self, command, kw=()): """Checks for running threads and executes the ones in queue""" def enhanced_run(command, kw): @@ -56,15 +87,9 @@ class ClockFace(object): ############################################################################ ### basic clock commands - def set_face(self, weather): + def set_face(self): """""" - self.weather = weather - self.run(self.IO.clock_face,(weather,)) - - - def text_scroll(self, text, color=""): - """""" - self.run(self.IO.text_scroll,(text, self.tspeed, color)) + self.run(self.IO.clock_face,(self.weather,)) def set_brightness(self, overwrite=[],value=-1): @@ -88,63 +113,34 @@ class ClockFace(object): self.IO.output.set_brightness(brightness) - ############################################################################ - ### Higher level commands, accessible from the chat-bot - def external_action(self, command, params): - """""" - self.commands[command](*params) + - def wake_light(self, duration=600): - """Simulates a sunris, takes one optional parameter: the duration""" - def output(duration): - self.set_brightness(value=0.1) - start_color = numpy.array([153, 0, 51]) - end_color = numpy.array([255, 255, 0]) - empty = numpy.zeros((16,32)) - ones = empty - ones[ones == 0] = 1 - gradient = end_color - start_color - # 20 steps should be fine => sleep_time = duration / 20 - for i in range(20): - ct = i/20 * gradient - col = [int(x) for x in ct+start_color] - self.IO.set_matrix(ones,colors=[col]) - time.sleep(int(duration) / 20) - self.run(output,(duration,)) +####################################################### +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 alarm_blink(self, duration=0, frequency=0): - """Blinks the whole screen (red-black). Duration in seconds, frequency in Hertz""" - def output(duration, frequency): - self.set_brightness(value=1) - duration = int(duration) - frequency = int(frequency) - n = duration * frequency / 2 - empty = numpy.zeros((16,32)) - red = empty.copy() - red[red == 0] = 3 - for i in range(int(n)): - self.IO.set_matrix(red) - time.sleep(1/frequency) - self.IO.set_matrix(empty) - time.sleep(1/frequency) - if not(duration == 0 or frequency == 0): - self.run(output,(duration, frequency)) + 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 image_show(self, image, duration): - """Shows a 16x32 image for duration seconds""" - def output(image, duration): - self.IO.set_matrix_rgb(red) - - self.run(output,(image, duration)) - - - def show_message(self, *args): - """Runs a text message over the screen. Obviously needs the text""" - # keep in mind, in this case args is a tuple of all words - message_str = " ".join(args) - print("SHOWING (CLOCK): " + message_str) - self.text_scroll(message_str) + def stop(self): + self._timer.cancel() + self.is_running = False diff --git a/dashboard/main.py b/dashboard/main.py index c227b43..0067208 100644 --- a/dashboard/main.py +++ b/dashboard/main.py @@ -18,6 +18,8 @@ import requests class DashBoard(): """""" + # added by the launcher, we have self.modules (dict) + def __init__(self, host_ip, prst): ## pre-sets @@ -51,7 +53,7 @@ class DashBoard(): return kids - def START(self): + def start(self): self.app.run_server(host=self.host_ip, port=80)#, debug=True) @@ -173,7 +175,8 @@ class DashBoard(): body = [] try: - content = self.bot.api_weather.show_weather([47.3769, 8.5417]) # still zürich + bot = self.modules["bot"] + content = bot.api_weather.show_weather([47.3769, 8.5417]) # still zürich wt = content.pop(0) body.append(weather_item("Jetzt", wt["short"], wt["temps"])) diff --git a/launcher.py b/launcher.py index 17f61fc..d55c1e4 100644 --- a/launcher.py +++ b/launcher.py @@ -1,10 +1,8 @@ # functionality -import bot.main import bot2.main import clock.main import dashboard.main -# wrapper -import wrapper + import persistence.main # various @@ -12,6 +10,7 @@ import logging from threading import Thread import os + if os.name == "nt": logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO @@ -38,28 +37,18 @@ class Launcher(): 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, hw_commands=self.clock_module.commands) + 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.threads = [] - #self.threads.append(Thread(target=self.chatbot)) - - self.threads.append(Thread(target=self.clock)) - self.threads.append(Thread(target=self.dashboard)) - - for i in self.threads: - i.start() - self.chatbot() - + self.modules = { + "bot" : self.bot_module, + "dashboard" : self.dashboard_module, + "clock" : self.clock_module, + } - def clock(self): - self.clock = wrapper.ClockWrapper(self.clock_module, self.bot_module) - - def chatbot(self): - self.bot = wrapper.BotWrapper(self.bot_module, self.clock_module) - - def dashboard(self): - self.dashboard = wrapper.DashBoardWrapper(self.dashboard_module, self.bot_module) + for module in self.modules.values(): + module.modules = self.modules + module.start() def init_persistence(self): diff --git a/wrapper.py b/wrapper.py deleted file mode 100644 index c4dac41..0000000 --- a/wrapper.py +++ /dev/null @@ -1,118 +0,0 @@ -import time -import datetime -import logging -import threading - -logger = logging.getLogger(__name__) - -class Wrapper(): - """Wrapper skeleton for the modules (bot, clock, dashboard ... maybe more to come?)""" - - def __init__(self, own_module, *other_modules): - self.own = own_module - self.others = other_modules - - logger.debug("Starting " + self.own.__class__.__name__ + " through wrapper.") - - - def external_action(self, func, *args, **kwargs): - """do a special action initiated by other modules""" - logger.info("External request to " + self.own.__class__.__name__ + ".") - - - - -class ClockWrapper(Wrapper): - """Wrapper for the CLOCK-functionality""" - def __init__(self, own_module, *other_modules): - """""" - super().__init__(own_module, *other_modules) - self.weather = {"weather":"", "high":"", "low":"", "show":"temps"} - self.weather_raw = {} - self.START() - - - - def START(self): # I prefer the name tick_tack - """Runs the showing of the clock-face periodically: update every minute""" - def perform_loop(): - t = int(datetime.datetime.now().strftime("%H%M")) - - if t % 5 == 0: - # switch secondary face every 5 minutes - weather = self.others[0].api_weather.show_weather([47.3769, 8.5417]) # zürich - - if weather != self.weather_raw and len(weather) != 0: - td = weather[1] - low = td["temps"][0] - high = td["temps"][1] - self.weather["weather"] = td["short"] - self.weather["high"] = high - self.weather["low"] = low - elif len(weather) == 0: - self.weather["weather"] = "error" - self.weather["high"] = "error" - self.weather["low"] = "error" - # if weather == self.weather.raw do nothing - - if self.weather["show"] == "weather": - next = "temps" - else: - next = "weather" - self.weather["show"] = next - - self.own.set_face(self.weather) - - perform_loop() - while datetime.datetime.now().strftime("%H%M%S")[-2:] != "00": - pass - RepeatedTimer(60, perform_loop) - - - -class BotWrapper(Wrapper): - """Wrapper for the BOT-functionality""" - def __init__(self, own_module, *other_modules): - """""" - super().__init__(own_module, *other_modules) - self.own.START() - - - -class DashBoardWrapper(Wrapper): - def __init__(self, own_module, *other_modules): - """Wrapper for the dashboard functionality""" - super().__init__(own_module, other_modules) - # self.mainloop(1 * 3600) # 1 hour refresh-cycle - # cannot get called through mainloop, will use the included callback-functionality of Dash - self.own.bot = other_modules[0] - self.own.START() - - - -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 = threading.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