wip: separate clock and rest into separate apps.

This commit is contained in:
Remy Moll 2021-06-18 16:57:20 +02:00
parent 93fb257d2d
commit ffc903b8f2
25 changed files with 580 additions and 257 deletions

View File

@ -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/...)

View File

@ -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()

1
broadcast/__init__.py Normal file
View File

@ -0,0 +1 @@
# Placeholder

65
broadcast/b_in.py Normal file
View File

@ -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

78
broadcast/b_out.py Normal file
View File

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

28
client.py Normal file
View File

@ -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()

92
clock/c_back.py Normal file
View File

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

View File

@ -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(),

61
clock/c_out.py Normal file
View File

@ -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()

View File

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

View File

@ -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()

View File

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

View File

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

View File

@ -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)
# 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]])
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
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]

View File

@ -27,3 +27,4 @@ class RepeatedTimer(object):
def stop(self):
self._timer.cancel()
self.is_running = False

View File

View File

0
dashboard/cards/xkcd.py Normal file
View File

View File

@ -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):

View File

@ -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.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.launch_modules()
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()

12
persistence/README.md Normal file
View File

@ -0,0 +1,12 @@
## What is happening here?
This "persistence"-module aims to standardize 2 things:
* the creation of a common set of variables that survives a potential (let's face it, likely) crash
* advanced logging and analytics
### Common variables
These are saved as a json file and are handled internally as a dict. Each change in the dict triggers a write to the file.
### Logging
A chunky sqlite-db which periodically gets new entries. From all modules. Ideally this db is then visualized through grafana. WIP

29
persistence/models.py Normal file
View File

@ -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()

View File

@ -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 __init__(self, own_name, parent_name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = own_name
self.parent = parent_name
def __setitem__(self, key, value):
super().__setitem__(key, value)
self.parent.__setitem__(self.name, self)
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 __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, k, d=None):
retvalue = super().pop(k, d)
self.parent.__setitem__(self.name, self)
return retvalue
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)

32
persistence/p_out.py Normal file
View File

@ -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"
)

31
server.py Normal file
View File

@ -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()