diff --git a/README.md b/README.md index 8987215..455bbd5 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,23 @@ # AIO -An all-in-one pod that acts as a home server. -Main functionality: -* chat-bot (telegram) -* clock (via a 32x16 rgb-matrix) +Just like AIO-coolers, this little program aims to tackle many problems at once. + + +## What it mainly does +* chat-bot (via telegram) +* clock and basic display (via LED-Matrix (size to taste)) * dashboard (via external browser) -## Chatbot -Periodically calls the telegram api and reacts to sent commands. Also handles calls to the hw-output. -Might add the functionality to monitor chats and make advanced statistics. +### Chatbot +Periodically calls the telegram api and reacts to sent commands. Also handles basic calls to the hardware: it allows you to control certain aspects of the clock. + +TODO: advanced analytics of the chat (grafana) ## Clock -Refreshes the matrix with useful information: time, date (weekdays), weather. I'm running out of ideas... +Server/Client which send/receive the output to show on the clock. Normally this is just a combination of time + weather. But special output can be triggered by the user. ## Dashboard -TODO +Shows basic info of the program and other useful things. + +TODO: show advanced host stats (cpu/mem/...) diff --git a/bot/main.py b/bot/main.py index 3fccc3a..13738c1 100644 --- a/bot/main.py +++ b/bot/main.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) class ChatBot(): """better framwork - unites all functions""" - def __init__(self, name, version, prst): + def __init__(self, name, version): """Inits the Bot with a few conf. vars Args: -> name:str - Name of the bot -> version:str - Version number @@ -16,9 +16,11 @@ 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) + # added by the launcher, we have self.modules (dict) and persistence + + self.name = name + self.version = version - 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) @@ -31,12 +33,15 @@ class ChatBot(): self.commands = commands - # Mark them as available + + def add_commands(self): + # Mark modules as available + prst = self.persistence 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), + "status" : self.commands.status.Status(self.name, self.version, prst), "zvv" : self.commands.zvv.Zvv(prst), "list" : self.commands.lists.Lists(prst), # "alias" : commands.alias.Alias(self.dispatcher, prst), @@ -44,20 +49,19 @@ class ChatBot(): "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.sub_modules = {"clock" : self.commands.clock.Clock(self.persistence, self.modules["clock"], self.api_art)} self.add_commands() self.telegram.start_polling() # self.telegram.idle() diff --git a/broadcast/__init__.py b/broadcast/__init__.py new file mode 100644 index 0000000..dcf2c80 --- /dev/null +++ b/broadcast/__init__.py @@ -0,0 +1 @@ +# Placeholder diff --git a/broadcast/b_in.py b/broadcast/b_in.py new file mode 100644 index 0000000..363a344 --- /dev/null +++ b/broadcast/b_in.py @@ -0,0 +1,65 @@ +import requests + +import logging +logger = logging.getLogger(__name__) + +class FetchUpdates: + """Fetches updates from the main server and relays them to the clock""" + + def __init__(self, server_ip, port): + """Both methods return a list as python-object. This should be then converted to a numpy array.""" + # self.server_ip = server_ip + self.base_url = server_ip + ":" + port + # self.modules gets added through the caller + self.update_calls = 0 + self.last_fetch = {} + + + def get_updates(self): + update_url = "http://" + self.base_url + "/getupdates" + result = self.call_api(update_url) + + return result + + + def get_last(self): + update_url = "http://" + self.base_url + "/getlast" + result = self.call_api(update_url) + + return result + + + def fetch_data(self): + if self.update_calls == 0: + fetch = self.get_last() + else: + fetch = self.get_updates() + if not fetch["is_new"]: + fetch = self.last_fetch + else: + self.last_fetch = fetch + try: + data = fetch["data"] + has_queue = fetch["has_queue"] + except: + data = {} + has_queue = False + + self.update_calls += 1 + + return has_queue, data + + + + def call_api(self, url): + ret = {} + try: + result = requests.get(url) + result = result.json() + + if result.pop("status") == "ok": + ret = result + except: + logger.error("Bad api call for method {}.".format(url[:url.rfind("/")])) + + return ret diff --git a/broadcast/b_out.py b/broadcast/b_out.py new file mode 100644 index 0000000..0569653 --- /dev/null +++ b/broadcast/b_out.py @@ -0,0 +1,78 @@ +import flask +from flask import request, jsonify +import numpy as np +from threading import Thread + + +class BroadcastUpdates: + """Broadcasts (out) updates for the hw-handler to be fetched periodically""" + + def __init__(self, port): + """""" + self.last_show = "" + + self.queue = [] #[{"matrices" : [np.full((16,16,3), 10).tolist(), np.full((16,16,3), 100).tolist(), np.full((16,16,3), 200).tolist()]} for _ in range(4)] + self.port = port + + + def start(self): + t = Thread(target=self.run) + t.start() + + + def run(self): + app = flask.Flask(__name__) + + @app.route('/getupdates', methods=['GET']) + def get_updates(): + return self.get_updates() + @app.route('/getlast', methods=['GET']) + def get_last(): + return self.get_last() + + app.run('0.0.0.0', port=self.port) + + + def get_updates(self): + try: + data = self.queue.pop(0) + self.last_show = data + is_new = True + has_queue = len(self.queue) > 0 + except: + data = "" + is_new = False + has_queue = False + + return self.generate_response( + is_new = is_new, + data = data, + has_queue = has_queue + ) + + + + def get_last(self): + try: + try: + data = self.queue[-1] + self.queue = [] + except: # list empty + data = self.last_show, + except: + data = "" + return self.generate_response( + data = data, + has_queue = False, + ) + + + + def generate_response(self, **kwargs): + ret = { + "status" : "ok", + **kwargs + } + print(ret) + return jsonify(ret) + diff --git a/client.py b/client.py new file mode 100644 index 0000000..b40ac6a --- /dev/null +++ b/client.py @@ -0,0 +1,28 @@ +# functionality +from clock import c_in, c_out +from broadcast import b_in + +import launcher + + + +class ReceiverLauncher(launcher.Launcher): + """Launcher for all server-side modules. The hard-computations""" + def __init__(self): + + self.clock_sensor_module = c_in.SensorReadout() + # active: periodically takes readouts + self.clock_hardware_module = c_out.ClockFace() + # active: periodically calls fetcher + self.receive_module = b_in.FetchUpdates(server_ip="192.168.1.110", port="1111") + # passive: fetches data on demand + + super().__init__( + sensors = self.clock_sensor_module, + clock = self.clock_hardware_module, + receive = self.receive_module + ) + + + +ReceiverLauncher() \ No newline at end of file diff --git a/clock/c_back.py b/clock/c_back.py new file mode 100644 index 0000000..508d30c --- /dev/null +++ b/clock/c_back.py @@ -0,0 +1,92 @@ +import numpy as np +import datetime + +from . import helpers + +class ClockBackend: + """Heavy lifting of matrix operations""" + def __init__(self): + self.MOP = helpers.computations.MatrixOperations() + + self.weather = {"weather":"", "high":"", "low":""} + self.weather_raw = {} + self.weather_face_swap = False + + self.brightness = 1 + self.brightness_overwrite = {"value" : 1, "duration" : 0} + + + def start(self): + self.out = self.modules["broadcast"] + helpers.timer.RepeatedTimer(15, self.clock_loop) + + + + def clock_loop(self): + print("looping") + + 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 + self.weather_face_swap = not self.weather_face_swap + + self.send_face() + + + + + def send_face(self): + """Set the clock face (time + weather) by getting updated info - gets called every minute""" + matrices = self.MOP.clock_face(self.weather) + if self.weather_face_swap: + matrices = [matrices[0], matrices[2], matrices[1]] + + # apply brightness + b = self.get_brightness() + matrices = [(b * m).tolist() for m in matrices] + self.out.queue.append({"matrices" : matrices}) + + + def get_brightness(self): + """Checks, what brightness rules to apply""" + + is_WE = datetime.datetime.now().weekday() > 4 + now = int(datetime.datetime.now().strftime("%H%M")) + if (is_WE and (now > 1000 and now < 2200)) or ((not is_WE) and (now > 800 and now < 2130)): + brightness = 0.8 + else: + brightness = 0.01 + + return brightness + + + # def text_scroll(self, text, color=[[200,200,200]]): + # pixels = self.MOP.text_converter(text, 12, color) + # sleep_time = 1 / self.tspeed + # width = self.shape[1] + # frames = pixels.shape[1] - width + # if frames <= 0: + # frames = 1 + + # for i in range(frames): + # visible = pixels[:,i:width+i] + # self.IO.put(visible*self.brightness) + # time.sleep(sleep_time) + # time.sleep(10 * sleep_time) + + diff --git a/clock/cin.py b/clock/c_in.py similarity index 95% rename from clock/cin.py rename to clock/c_in.py index b7db91a..f7bb2dd 100644 --- a/clock/cin.py +++ b/clock/c_in.py @@ -8,9 +8,8 @@ from . import hardware, helpers class SensorReadout: """Overview class for (actual and potential) sensor sources""" - def __init__(self, prst=object): + def __init__(self): """""" - 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(), diff --git a/clock/c_out.py b/clock/c_out.py new file mode 100644 index 0000000..2788acb --- /dev/null +++ b/clock/c_out.py @@ -0,0 +1,61 @@ +import datetime +import time +from threading import Thread +import numpy as np + +from . import hardware, helpers + + +class ClockFace: + """Actual functions one might need for a clock""" + + def __init__(self): + """""" + # added by the launcher, we have self.modules (dict) + + + + self.IO = hardware.led.get_handler() + self.shape = self.IO.shape # (16,32) for now + # TODO handle differently! + self.MOP = helpers.computations.MatrixOperations() + + + + def start(self): + helpers.timer.RepeatedTimer(60, self.clock_loop) + # schedule for in 60 seconds + self.clock_loop() + # run once now + # TODO start as a thread + + +# TODO Turn off when button pressed? + + def clock_loop(self): + t_start = datetime.datetime.now() + + t_minutes = int(datetime.datetime.now().strftime("%H%M")) + + has_queue, data = self.modules["receive"].fetch_data() + + if data == {}: + matrices = self.MOP.get_fallback() + else: + matrices = [np.asarray(d).astype(int) for d in data["matrices"]] + + self.IO.put(matrices) + + if has_queue: + tnext = 1 + else: + tnext = 30 + + + t_end = datetime.datetime.now() + delta_planned = datetime.timedelta(seconds = tnext) + delta = delta_planned - (t_end - t_start) + + time.sleep(max(delta.total_seconds(), 0)) + self.clock_loop() + diff --git a/clock/cout.py b/clock/cout.py deleted file mode 100644 index 0c74220..0000000 --- a/clock/cout.py +++ /dev/null @@ -1,138 +0,0 @@ -import datetime -import time -from threading import Thread -import numpy - -from . import hardware, helpers - - -class ClockFace: - """Actual functions one might need for a clock""" - - def __init__(self, text_speed=18, prst=object): - """""" - # added by the launcher, we have self.modules (dict) - - # hard coded, but can be changed to taste - self.tspeed = text_speed - self.primary = [200, 200, 200] - self.secondary = [10, 200, 10] - self.error = [200, 10, 10] - - self.persistence = prst - 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 = "" - # Action the thread is currently performing - self.output_queue = [] - # Threads to execute next - - self.weather = {"weather":"", "high":"", "low":"", "show":"temps"} - self.weather_raw = {} - - self.brightness = 1 - self.brightness_overwrite = {"value" : 1, "duration" : 0} - - - def start(self): - self.clock_loop() - while datetime.datetime.now().strftime("%H%M%S")[-2:] != "00": - pass - helpers.timer.RepeatedTimer(60, self.clock_loop) - 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.run(self.set_face,()) - - - def run(self, command, kw=()): - """Checks for running threads and executes the ones in queue""" - def enhanced_run(command, kw): - """""" - self.output_thread = "Running " + str(command) - command(*kw) - self.set_brightness() - self.output_thread = "" - if len(self.output_queue) != 0: - n = self.output_queue.pop(0) - enhanced_run(n[0],n[1]) - else: - self.set_face() - - if len(self.output_thread) == 0: - t = Thread(target=enhanced_run, args=(command, kw)) - t.start() - else: - self.output_queue.append([command,kw]) - - - ############################################################################ - ### basic clock commands - 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.put(face * self.brightness) - - - def set_brightness(self, value=-1, overwrite=[]): - """Checks, what brightness rules to apply""" - - if value != -1: - self.brightness = value - return - - if len(overwrite) != 0: - self.brightness_overwrite = overwrite - - is_WE = datetime.datetime.now().weekday() > 4 - now = int(datetime.datetime.now().strftime("%H%M")) - if (is_WE and (now > 1000 and now < 2200)) or ((not is_WE) and (now > 800 and now < 2130)): - brightness = 0.8 - else: - brightness = 0.01 - - 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 - width = self.shape[1] - frames = pixels.shape[1] - width - if frames <= 0: - frames = 1 - - for i in range(frames): - visible = pixels[:,i:width+i] - self.IO.put(visible*self.brightness) - time.sleep(sleep_time) - time.sleep(10 * sleep_time) - diff --git a/clock/hardware/sim.py b/clock/hardware/sim.py index 3c4e22f..7743f0e 100644 --- a/clock/hardware/sim.py +++ b/clock/hardware/sim.py @@ -3,7 +3,7 @@ import colorsys import pygame.gfxdraw import time import pygame -import numpy +import numpy as np class ClockOut: """Creates a drawable window in case the real hardware is not accessible. For development""" @@ -11,7 +11,7 @@ class ClockOut: self.pixel_size = 20 self.shape = shape - self.pixels = numpy.zeros((*shape,3), dtype=int) + self.pixels = np.zeros((*shape,3), dtype=int) self.WIDTH = shape[1] self.HEIGHT = shape[0] self.window_width = self.WIDTH * self.pixel_size @@ -22,13 +22,17 @@ class ClockOut: self.screen = pygame.display.set_mode([self.window_width, self.window_height]) - def put(self, matrix): + def put(self, matrices): 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() + + if self.shape == (16, 32): + matrix = np.concatenate((matrices[0], matrices[1]), axis=1) + self.pixels = matrix self.draw_pixels() diff --git a/clock/hardware/unicorn.py b/clock/hardware/unicorn.py index c31e850..8a6d34b 100644 --- a/clock/hardware/unicorn.py +++ b/clock/hardware/unicorn.py @@ -1,6 +1,6 @@ import colorsys import time -import numpy +import numpy as np try: import RPi.GPIO as GPIO SETUP_FAIL = False @@ -68,9 +68,10 @@ class ClockOut: GPIO.output(self.PIN_CLK, GPIO.LOW) - def put(self, matrix): + def put(self, matrices): """Sets a height x width matrix directly""" self.reset_clock() + matrix = np.concatenate((matrices[0], matrices[1]), axis=0) # or 1?? self.show(matrix) diff --git a/clock/helpers/__init__.py b/clock/helpers/__init__.py index 7e5ed98..c525186 100644 --- a/clock/helpers/__init__.py +++ b/clock/helpers/__init__.py @@ -1 +1 @@ -from . import helper, timer \ No newline at end of file +from . import computations, timer \ No newline at end of file diff --git a/clock/helpers/helper.py b/clock/helpers/computations.py similarity index 79% rename from clock/helpers/helper.py rename to clock/helpers/computations.py index ce19dd1..7484b6e 100644 --- a/clock/helpers/helper.py +++ b/clock/helpers/computations.py @@ -17,19 +17,20 @@ 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])) class MatrixOperations(): """Helper functions to generate frequently-used images""" - def __init__(self, shape, default_colors): + def __init__(self, shape=[16,16]): self.shape = shape # shape is going to be (16,32) for the moment - self.primary = default_colors["primary"] - self.secondary = default_colors["secondary"] - self.error = default_colors["error"] + self.primary = [200, 200, 200] + self.secondary = [10, 200, 10] + self.error = [200, 10, 10] def time_converter(self, top="", bottom=""): - """Converts 4-digit time to a pixel-matrix - returns: np.array wich fills one half of self.shape (horizontally)""" - nshape = (self.shape[0], int(self.shape[1]/2)) - pixels = np.zeros(nshape,dtype=np.uint8) + """Converts 4-digit time to a 16x16 pixel-matrix + returns: np.array""" + # nshape = (self.shape[0], int(self.shape[1]/2)) + nshape = (16, 16) + pixels = np.zeros(nshape, dtype=np.uint8) if bottom == "" or top == "": top = datetime.datetime.now().strftime("%H") @@ -62,7 +63,8 @@ class MatrixOperations(): def date_converter(self): - nshape = (self.shape[0], int(self.shape[1]/2)) + # nshape = (self.shape[0], int(self.shape[1]/2)) + nshape = (16, 16) today = datetime.datetime.today() weekday = datetime.datetime.weekday(today) # size of the reduced array according to weekday @@ -77,7 +79,8 @@ class MatrixOperations(): def weather_converter(self, name): """Fills one half of the screen with weather info.""" - nshape = (self.shape[0], int(self.shape[1]/2)) + # nshape = (self.shape[0], int(self.shape[1]/2)) + nshape = (16, 16) result = np.zeros(nshape) cwd = __file__.replace("\\","/") # for windows cwd = cwd.rsplit("/", 1)[0] # the current working directory (where this file is) @@ -90,6 +93,8 @@ class MatrixOperations(): icon_loc = ["sun","moon","sun and clouds", "moon and clouds", "cloud","fog and clouds","2 clouds", "3 clouds", "rain and cloud", "rain and clouds", "rain and cloud and sun", "rain and cloud and moon", "thunder and cloud", "thunder and cloud and moon", "snow and cloud", "snow and cloud and moon", "fog","fog night"] #ordered 1 2 \n 3 4 \ 5 5 ... + if name == "": + name = "error" name = weather_categories[name] try: iy, ix = int(icon_loc.index(name)/2), icon_loc.index(name)%2 @@ -137,21 +142,16 @@ class MatrixOperations(): hour = self.time_converter() day = self.date_converter() face1 = hour + day + # time + date: face1_3d = self.matrix_add_depth(face1) - - if weather["show"] == "weather": - face2_3d = self.weather_converter(weather["weather"]) - else: - face2 = self.time_converter(top=str(weather["low"]), bottom=str(weather["high"])) - face2 = np.concatenate((face2[:8,...],2*face2[8:,...])) - face2_3d = self.matrix_add_depth(face2,[[0, 102, 255],[255, 102, 0]]) - - face = np.zeros((max(face1_3d.shape[0],face2_3d.shape[0]),face1_3d.shape[1]+face2_3d.shape[1],3)) - - face[:face1_3d.shape[0],:face1_3d.shape[1],...] = face1_3d - face[:face2_3d.shape[0],face1_3d.shape[1]:,...] = face2_3d + # weather icons + face2_3d = self.weather_converter(weather["weather"]) + # weather temps + face3 = self.time_converter(top=str(weather["low"]), bottom=str(weather["high"])) + face3 = np.concatenate((face3[:8,...],2*face3[8:,...])) + face3_3d = self.matrix_add_depth(face3,[[0, 102, 255],[255, 102, 0]]) - return face + return [face1_3d, face2_3d, face3_3d] def text_converter(self, text, height, color): @@ -166,3 +166,13 @@ class MatrixOperations(): pixels = np.array(img, dtype=np.uint8) pixels3d = self.matrix_add_depth(pixels, color) return pixels3d + + + def get_fallback(self): + hour = self.time_converter() + day = self.date_converter() + face1 = hour + day + face1_3d = self.matrix_add_depth(face1) + + face2_3d = face3_3d = np.zeros((16,16,3)) + return [face1_3d, face2_3d, face3_3d] \ No newline at end of file diff --git a/clock/helpers/timer.py b/clock/helpers/timer.py index 6530aa4..8be6e7c 100644 --- a/clock/helpers/timer.py +++ b/clock/helpers/timer.py @@ -26,4 +26,5 @@ class RepeatedTimer(object): def stop(self): self._timer.cancel() - self.is_running = False \ No newline at end of file + self.is_running = False + \ No newline at end of file diff --git a/dashboard/cards/__init__.py b/dashboard/cards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/cards/weather.py b/dashboard/cards/weather.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/cards/xkcd.py b/dashboard/cards/xkcd.py new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/dout.py b/dashboard/d_out.py similarity index 97% rename from dashboard/dout.py rename to dashboard/d_out.py index 161f6b7..1a24124 100644 --- a/dashboard/dout.py +++ b/dashboard/d_out.py @@ -14,6 +14,8 @@ import time import xmltodict import requests +from threading import Thread + from . import helpers @@ -21,12 +23,13 @@ class DashBoard(): """""" # added by the launcher, we have self.modules (dict) - def __init__(self, host_ip, prst): + def __init__(self, port): ## pre-sets self.inter_margin = "1em" - self.persistence = prst - self.host_ip = host_ip + self.host_ip = "0.0.0.0" + self.port = port + ex_css = [dbc.themes.BOOTSTRAP] self.app = dash.Dash(__name__, external_stylesheets=ex_css) self.app.layout = html.Div([ @@ -57,7 +60,8 @@ class DashBoard(): def start(self): - self.app.run_server(host=self.host_ip, port=80)#, debug=True) + flaskThread = Thread(target=app.run_server, kwargs={"host": self.host_ip, "port": self.port}).start() + #self.app.run_server()#, debug=True) def card_header(self): diff --git a/launcher.py b/launcher.py index 9f57628..01c88cc 100644 --- a/launcher.py +++ b/launcher.py @@ -1,17 +1,12 @@ -# functionality -from bot import main -from clock import cin, cout -from dashboard import dout +from persistence import p_io -import persistence.main - -# various import logging -from threading import Thread import os + if os.name == "nt": + # development logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) @@ -24,62 +19,65 @@ else: class Launcher: - """Launches all other submodules""" + """base launcher that launches other submodules""" - def __init__(self): + def __init__(self, **modules): """""" - self.persistence = persistence.main.PersistentDict("persistence/prst.json") + self.persistence = p_io.PersistentDict("persistence/prst.json") self.logger = logging.getLogger(__name__) - self.logger.info("Launcher initialized") + self.logger.info(self.__class__.__name__ + " initialized") + self.modules = modules if len(self.persistence) == 0: self.init_persistence() self.persistence["global"]["reboots"] += 1 + + self.launch_modules() - 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, - } + + + def launch_modules(self): for module in self.modules.values(): self.logger.info("Starting module "+ module.__class__.__name__) module.modules = self.modules + module.persistence = self.persistence module.start() - + def init_persistence(self): self.logger.warning("No persistence found, created a new one") - self.persistence["bot"] = { - "send_activity" : {"hour":[], "count":[]}, - "receive_activity" : {"hour":[], "count":[]}, - "execute_activity" : {"hour":[], "count":[]}, - "log": [], - "chat_members": {}, - "aliases" : {} - } - self.persistence["clock"] = { - "sensors" : { - "time" : [], - "temperature":[], - "humidity":[], - "brightness" : [], - } - } - self.persistence["dashboard"] = {} - self.persistence["global"] = { + self.persistence["global"] ={ "lists" : {}, "reboots": 0 } + for m_name in self.modules.keys(): + data = {} + if m_name == "bot": + data = { + "send_activity" : {"hour":[], "count":[]}, + "receive_activity" : {"hour":[], "count":[]}, + "execute_activity" : {"hour":[], "count":[]}, + "log": [], + "chat_members": {}, + "aliases" : {} + } + if m_name == "clock": + data = { + "sensors" : { + "time" : [], + "temperature":[], + "humidity":[], + "brightness" : [], + } + } + + self.persistence[m_name] = data + + ######################################################################## ## Aand liftoff! -Launcher() +# Launcher() diff --git a/persistence/README.md b/persistence/README.md new file mode 100644 index 0000000..1ac5653 --- /dev/null +++ b/persistence/README.md @@ -0,0 +1,12 @@ +## What is happening here? + +This "persistence"-module aims to standardize 2 things: +* the creation of a common set of variables that survives a potential (let's face it, likely) crash +* advanced logging and analytics + +### Common variables +These are saved as a json file and are handled internally as a dict. Each change in the dict triggers a write to the file. + + +### Logging +A chunky sqlite-db which periodically gets new entries. From all modules. Ideally this db is then visualized through grafana. WIP \ No newline at end of file diff --git a/persistence/models.py b/persistence/models.py new file mode 100644 index 0000000..48da2e4 --- /dev/null +++ b/persistence/models.py @@ -0,0 +1,29 @@ +from peewee import * + +#db = SqliteDatabase('data.db') +db = MySQLDatabase("AIO_sensors", host="192.168.1.101", port=3306, user="pi", passwd="supersecret") +# whyyy? + +class Metric(Model): + time = DateTimeField() + + + class Meta: + database = db + +class SensorMetric(Metric): + # this is a continuous metric + temperature = IntegerField() + humidity = IntegerField() + luminosity = IntegerField() + + +class ChatMetric(Metric): + # this gets cumulated over one hour (or one day, or...) + activity = CharField() + + +class ErrorMetric(Metric): + # same as above + error = CharField() + diff --git a/persistence/main.py b/persistence/p_io.py similarity index 54% rename from persistence/main.py rename to persistence/p_io.py index 8db6f02..bb6727c 100644 --- a/persistence/main.py +++ b/persistence/p_io.py @@ -2,7 +2,6 @@ import json import os - class PersistentDict(dict): """Extended dict that writes its content to a file every time a value is changed""" @@ -32,6 +31,7 @@ class PersistentDict(dict): super().__setitem__(key, tmp[key]) self.last_action = "r" + ## extended dictionary - logic def __setitem__(self, key, value): if self.last_action != "r": @@ -45,8 +45,8 @@ class PersistentDict(dict): self.read_dict() ret_val = super().__getitem__(key) - if type(ret_val) == dict: - ret_val = HookedDict(key, self, ret_val) + if type(ret_val) != int and type(ret_val) != str: + ret_val = create_struct(type(ret_val), key, self, ret_val) return ret_val @@ -57,25 +57,31 @@ class PersistentDict(dict): -class HookedDict(dict): - """helper class to detect writes to a child-dictionary and triger a write in PersistentDict""" +def create_struct(struct_type, own_name, parent_name, *args, **kwargs): + class HookedStruct(struct_type): - def __init__(self, own_name, parent_dict, *args, **kwargs): - super().__init__(*args, **kwargs) - self.name = own_name - self.parent = parent_dict - - def __setitem__(self, key, value): - super().__setitem__(key, value) - self.parent.__setitem__(self.name, self) + def __init__(self, own_name, parent_name, *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = own_name + self.parent = parent_name + + def __setitem__(self, *args, **kwargs): + super().__setitem__(*args, **kwargs) + self.parent.__setitem__(self.name, self) - def __getitem__(self, key): - ret_val = super().__getitem__(key) - if type(ret_val) == dict: - ret_val = HookedDict(key, self, ret_val) - return ret_val - - def pop(self, k, d=None): - retvalue = super().pop(k, d) - self.parent.__setitem__(self.name, self) - return retvalue + def __getitem__(self, *args, **kwargs): + ret_val = super().__getitem__(*args, **kwargs) + if type(ret_val) != int and type(ret_val) != str: + ret_val = create_struct(type(ret_val), args[0], self, ret_val) + return ret_val + + def pop(self, *args): + retvalue = super().pop(*args) + self.parent.__setitem__(self.name, self) + return retvalue + + def append(self, *args): + super().append(*args) + self.parent.__setitem__(self.name, self) + print(*args) + return HookedStruct(own_name, parent_name, *args, **kwargs) diff --git a/persistence/p_out.py b/persistence/p_out.py new file mode 100644 index 0000000..1957d98 --- /dev/null +++ b/persistence/p_out.py @@ -0,0 +1,32 @@ +from models import db +from models import * +import datetime as dt +from random import randint + + +def create_tables(): + with db: + db.create_tables([SensorMetric, ChatMetric, ErrorMetric]) + + +create_tables() +# read from json, excel, txt ... whatever +now = dt.datetime.timestamp(dt.datetime.now()) + + +for i in range(1000): + with db: + sensor_data = SensorMetric.create( + time = now + i, + temperature = 23, + humidity = 30 + randint(0,20), + luminosity = 1 + ) + chat = ChatMetric( + time = now + i, + activity = "Hello world" + ) + errors = ErrorMetric( + time = now + i, + error = "Could not load module" + ) \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..11a2c5b --- /dev/null +++ b/server.py @@ -0,0 +1,31 @@ +# functionality +from bot import main +from clock import c_back +from broadcast import b_out +from dashboard import d_out + +import launcher + + + +class BroadcastLauncher(launcher.Launcher): + """Launcher for all server-side modules. The hard-computations""" + def __init__(self): + + self.bot_module = main.ChatBot(name="Norbit", version="3.0") # ??? + self.clock_backend_module = c_back.ClockBackend() # threaded through threading.Timer + self.broadcast_module = b_out.BroadcastUpdates(port="1111") # threaded as Thread + # self.dashboard_module = d_out.DashBoard(port="80") # ??? threaded as Thread + + # "sensors" : self.sensors, + # "bot" : self.bot_module, + # "clock" : self.clock_module, + # "dashboard" : self.dashboard_module, + super().__init__( + bot = self.bot_module, + clock = self.clock_backend_module, + # dashboard = self.dashboard_module, + broadcast = self.broadcast_module + ) + +BroadcastLauncher() \ No newline at end of file