wip: separate clock and rest into separate apps.
This commit is contained in:
parent
93fb257d2d
commit
ffc903b8f2
23
README.md
23
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/...)
|
||||
|
20
bot/main.py
20
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()
|
||||
|
1
broadcast/__init__.py
Normal file
1
broadcast/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Placeholder
|
65
broadcast/b_in.py
Normal file
65
broadcast/b_in.py
Normal 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
78
broadcast/b_out.py
Normal 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
28
client.py
Normal 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
92
clock/c_back.py
Normal 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)
|
||||
|
||||
|
@ -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
61
clock/c_out.py
Normal 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()
|
||||
|
138
clock/cout.py
138
clock/cout.py
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -1 +1 @@
|
||||
from . import helper, timer
|
||||
from . import computations, timer
|
@ -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]
|
@ -27,3 +27,4 @@ class RepeatedTimer(object):
|
||||
def stop(self):
|
||||
self._timer.cancel()
|
||||
self.is_running = False
|
||||
|
0
dashboard/cards/__init__.py
Normal file
0
dashboard/cards/__init__.py
Normal file
0
dashboard/cards/weather.py
Normal file
0
dashboard/cards/weather.py
Normal file
0
dashboard/cards/xkcd.py
Normal file
0
dashboard/cards/xkcd.py
Normal 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):
|
80
launcher.py
80
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.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
12
persistence/README.md
Normal 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
29
persistence/models.py
Normal 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()
|
||||
|
@ -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
32
persistence/p_out.py
Normal 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
31
server.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user