This commit is contained in:
parent
3ad796ffb2
commit
2321d7d552
9 changed files with 194 additions and 18 deletions
|
@ -1,11 +1,12 @@
|
|||
import logging
|
||||
|
||||
from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view
|
||||
from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view, request, HTTPResponse
|
||||
from bottle_log import LoggingPlugin
|
||||
from bottle_websocket import websocket, GeventWebSocketServer
|
||||
from geventwebsocket.websocket import WebSocket
|
||||
|
||||
from buba.animationconfig import setup_animations
|
||||
from buba.animations.webqueue import WebQueue
|
||||
from buba.appconfig import AppConfig
|
||||
from buba.bubaanimator import BubaAnimator
|
||||
from buba.bubacmd import BubaCmd
|
||||
|
@ -25,7 +26,8 @@ TEMPLATE_PATH.insert(0, config.templatepath)
|
|||
websocket_clients = WebSocketClients()
|
||||
buba = BubaCmd(config.serial, websocket_clients.send)
|
||||
animator = BubaAnimator(buba)
|
||||
setup_animations(config, animator)
|
||||
webqueue = WebQueue(buba)
|
||||
setup_animations(config, animator, webqueue)
|
||||
|
||||
|
||||
@app.route("/static/<filepath>")
|
||||
|
@ -36,7 +38,9 @@ def server_static(filepath):
|
|||
@app.get("/")
|
||||
@jinja2_view("home.html.j2")
|
||||
def root():
|
||||
return {}
|
||||
return {
|
||||
"user_forms": range(webqueue.maxrows),
|
||||
}
|
||||
|
||||
|
||||
@app.get('/ws', apply=[websocket])
|
||||
|
@ -53,6 +57,49 @@ def websocket_endpoint(ws: WebSocket):
|
|||
websocket_clients.remove(ws)
|
||||
|
||||
|
||||
def json_error(status: str, body: dict) -> HTTPResponse:
|
||||
return HTTPResponse(
|
||||
body=body,
|
||||
status=status,
|
||||
headers={
|
||||
"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
|
||||
def json_error_400(error: str) -> HTTPResponse:
|
||||
return json_error("400 Bad Request", {
|
||||
"status": "error",
|
||||
"error": error,
|
||||
})
|
||||
|
||||
|
||||
@app.post("/user-entry")
|
||||
def user_entry():
|
||||
log.debug(f"json: {request.json}")
|
||||
title = request.json.get("title")
|
||||
if not isinstance(title, str):
|
||||
raise json_error_400("title must be a string")
|
||||
title = title.strip()
|
||||
if title == "":
|
||||
raise json_error_400("title must not be empty")
|
||||
if not isinstance(request.json.get("rows"), list):
|
||||
raise json_error_400("rows must be a list")
|
||||
rows = []
|
||||
for row in request.json.get("rows"):
|
||||
if not isinstance(row[0], str) or not isinstance(row[0], str):
|
||||
raise json_error_400("rows elements must be strings")
|
||||
row = [v.strip() for v in row]
|
||||
if row[0] != "" or row[1] != "":
|
||||
rows.append(row)
|
||||
rows = rows[:webqueue.maxrows]
|
||||
if webqueue.add(title, rows):
|
||||
return {
|
||||
"status": "success",
|
||||
}
|
||||
else:
|
||||
raise json_error_400("queue is full")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=config.listen_host, port=config.listen_port, server=GeventWebSocketServer, debug=config.debug,
|
||||
quiet=not config.debug)
|
||||
|
|
|
@ -8,25 +8,21 @@ from buba.animations.icalevents import IcalEvents
|
|||
from buba.animations.snake import SnakeAnimation
|
||||
from buba.animations.spaceapi import Spaceapi
|
||||
from buba.animations.time import BubaTime
|
||||
from buba.animations.webqueue import WebQueue
|
||||
from buba.bubaanimator import BubaAnimator
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_animations(config, animator: BubaAnimator):
|
||||
def setup_animations(config, animator: BubaAnimator, webqueue: WebQueue):
|
||||
cs = BubaCharset(animator.buba)
|
||||
# animator.add(cs)
|
||||
|
||||
bt = BubaTime(animator.buba)
|
||||
animator.add(bt)
|
||||
|
||||
snake = SnakeAnimation(animator.buba)
|
||||
animator.add(snake)
|
||||
|
||||
dbf_ahst = DBF(animator.buba, ds100="AHST", station="Holstenstraße")
|
||||
dbf_ahs = DBF(animator.buba, ds100="AHS", station="Altona", count=9)
|
||||
animator.add(dbf_ahst)
|
||||
animator.add(dbf_ahs)
|
||||
|
||||
ccchh_events = IcalEvents(animator.buba,
|
||||
url="https://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g?export",
|
||||
|
@ -34,15 +30,24 @@ def setup_animations(config, animator: BubaAnimator):
|
|||
ccc_events = IcalEvents(animator.buba,
|
||||
url="https://events.ccc.de/calendar/events.ics",
|
||||
title="CCC Events", range=datetime.timedelta(weeks=8))
|
||||
animator.add(ccchh_events)
|
||||
animator.add(ccc_events)
|
||||
|
||||
ccchh_spaceapi = Spaceapi(animator.buba, "https://spaceapi.hamburg.ccc.de", "CCCHH")
|
||||
animator.add(ccchh_spaceapi)
|
||||
|
||||
ca = None
|
||||
if config.clubassistant_token is not None:
|
||||
ca = HomeAssistant(animator.buba, "https://club-assistant.ccchh.net", "Club Assistant",
|
||||
config.clubassistant_token)
|
||||
animator.add(ca)
|
||||
else:
|
||||
LOG.warning("Club Assistant token not set, not activating animation")
|
||||
|
||||
# animator.add(cs)
|
||||
animator.add(bt)
|
||||
animator.add(snake)
|
||||
animator.add(dbf_ahst)
|
||||
animator.add(dbf_ahs)
|
||||
animator.add(ccchh_events)
|
||||
animator.add(ccc_events)
|
||||
animator.add(ccchh_spaceapi)
|
||||
if ca is not None:
|
||||
animator.add(ca)
|
||||
animator.add(webqueue)
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import os
|
||||
from datetime import timedelta, datetime
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
import icalevents.icalevents
|
||||
from pytz import timezone
|
||||
|
||||
from buba.bubaanimation import BubaAnimation
|
||||
from buba.bubacmd import BubaCmd
|
||||
|
||||
|
||||
class IcalEvents(BubaAnimation):
|
||||
|
|
48
buba/animations/webqueue.py
Normal file
48
buba/animations/webqueue.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from datetime import timedelta
|
||||
from queue import Queue, Empty
|
||||
from time import sleep
|
||||
|
||||
from buba.bubaanimation import BubaAnimation
|
||||
from buba.bubaanimator import LineLayoutColumn
|
||||
from buba.bubacmd import BubaCmd
|
||||
|
||||
|
||||
class WebQueue(BubaAnimation):
|
||||
"""
|
||||
|
||||
"""
|
||||
def __init__(self, buba, maxlen:int=3, maxrows:int=6):
|
||||
"""
|
||||
A queue of pages to show. If
|
||||
:param buba:
|
||||
:param maxlen:
|
||||
"""
|
||||
super().__init__(buba)
|
||||
self.title = "(waiting for input)"
|
||||
self.queue = Queue()
|
||||
self.maxlen = maxlen
|
||||
self.maxrows = maxrows
|
||||
|
||||
def show(self) -> None:
|
||||
try:
|
||||
(self.title, self.rows) = self.queue.get(block=False)
|
||||
self.layout = self.default_layout
|
||||
self.show_pages()
|
||||
except Empty:
|
||||
self.buba.text(page=0, row=0, col_start=0, col_end=119, text="\u0010\u0010 Your Data Here \u0011\u0011", align=BubaCmd.ALIGN_CENTER)
|
||||
self.buba.text(page=0, row=1, col_start=0, col_end=119, text="Submit your own data to", align=BubaCmd.ALIGN_CENTER)
|
||||
self.buba.text(page=0, row=2, col_start=0, col_end=119, text="this display! Go to", align=BubaCmd.ALIGN_CENTER)
|
||||
self.buba.text(page=0, row=3, col_start=0, col_end=119, text="https://buba.ccchh.net", align=BubaCmd.ALIGN_CENTER)
|
||||
sleep(7)
|
||||
|
||||
def add(self, title, rows) -> bool:
|
||||
"""
|
||||
Add an entry to the queue. Returns False if the maximum number of entries has been reached.
|
||||
:param title:
|
||||
:param rows:
|
||||
:return:
|
||||
"""
|
||||
if self.queue.qsize() >= self.maxlen:
|
||||
return False
|
||||
self.queue.put((title, rows))
|
||||
return True
|
|
@ -64,6 +64,7 @@ class BubaCmd:
|
|||
:param align: alignment options, see MIS1TextDisplay.ALIGN_*
|
||||
:return:
|
||||
"""
|
||||
text = text.replace("\u00ad", "") # remove soft hyphen
|
||||
text = text.encode("CP437", "replace").decode("CP437") # force text to be CP437 compliant
|
||||
if self.display is not None:
|
||||
self.display.text(page, row, col_start, col_end, text, align)
|
||||
|
|
|
@ -30,3 +30,15 @@ svg.geavision__row {
|
|||
.gvson {
|
||||
fill: #4f0;
|
||||
}
|
||||
|
||||
.info-form__row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.info-form__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.info-form__field * {
|
||||
display: block;
|
||||
}
|
|
@ -32,5 +32,44 @@ if (container) {
|
|||
ws.close();
|
||||
});
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
let info_send = document.getElementById("info-send");
|
||||
if (info_send) {
|
||||
document.getElementById("info-form__form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
})
|
||||
info_send.addEventListener("click", (e) => {
|
||||
let info = {
|
||||
"title": document.getElementById("info-form__title").value,
|
||||
"rows": []
|
||||
}
|
||||
for (let e of document.querySelectorAll(".info-form__row-data")) {
|
||||
let t = e.querySelector(".info-row__field-text").value;
|
||||
let v = e.querySelector(".info-row__field-value").value;
|
||||
info.rows.push([t, v]);
|
||||
}
|
||||
fetch("/user-entry", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(info),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(res => {
|
||||
if (res.status === 200) {
|
||||
alert("Your entry has been sent!")
|
||||
} else {
|
||||
return res.json()
|
||||
}
|
||||
}).then(json => {
|
||||
alert("Your entry could not be accepted: " + json.error)
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
})
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
})
|
||||
}
|
|
@ -8,7 +8,6 @@
|
|||
<script src="static/main.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ self.page_title() }}</h1>
|
||||
{% block page_body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -2,4 +2,32 @@
|
|||
{% block page_title %}CCCHH Buba{% endblock %}
|
||||
{% block page_body %}
|
||||
<div id="geavision-display">...</div>
|
||||
<h3>Submit Your Own Info</h3>
|
||||
<div class="info-form">
|
||||
<form id="info-form__form">
|
||||
<div class="info-form__row">
|
||||
<div class="info-form__field">
|
||||
<label for="info-form__title">Title</label>
|
||||
<input id="info-form__title" type="text" name="title" size="32">
|
||||
</div>
|
||||
</div>
|
||||
{% for r in user_forms %}
|
||||
<div class="info-form__row info-form__row-data">
|
||||
<div class="info-form__field">
|
||||
<label for="info-form__row-{{ r }}-text">Description</label><br>
|
||||
<input id="info-form__row-{{ r }}-text" class="info-row__field-text" type="text" name="row-{{ r }}-text" size="27"><br>
|
||||
</div>
|
||||
<div class="info-form__field">
|
||||
<label for="info-form__row-{{ r }}-value">Value</label><br>
|
||||
<input id="info-form__row-{{ r }}-value" class="info-row__field-value" type="text" name="row-{{ r }}-value" size="5">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="info-form__row">
|
||||
<div class="info-form__field">
|
||||
<button id="info-send">Send Info</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue