...
 
Commits (11)
*.swp
npm-debug.log
node_modules
modules
__pycache__
env
require('./modules/main.js');
import json
import threading
from queue import Queue
from flask import Flask, render_template
from flask_socketio import SocketIO
app = Flask(__name__, static_url_path='', static_folder='public')
app.config['SECRET_KEY'] = 'Een3ooYiimePood5aeCe8ae0Eihohsh1'
socketio = SocketIO(app, async_mode='threading')
latest_updates = dict()
@socketio.on('ident')
def on_ident(json):
#TODO better dependency resolution for outlets
[socketio.emit('update', upd) for key,upd in latest_updates.items() if key[1] == '.']
[socketio.emit('update', upd) for key,upd in latest_updates.items() if key[1] != '.']
queue = Queue()
def consumer():
print('consumer')
while True:
event, data = queue.get()
socketio.emit(event, data)
latest_updates[(data['module'],data['outlet'])] = data
threading.Thread(target=consumer).start()
import modules
#TODO do not pollute namespace
from modules import *
def emit_factory(mod):
def emit(content, outlet='.'):
queue.put(('update', dict(outlet=outlet, module=mod, content=content)))
return emit
def log_factory(mod):
def log(err):
print('[{}] ERR: {}'.format(mod, err))
return log
for mod_name in modules.__all__:
mod = getattr(modules, mod_name)
print('starting module {}'.format(mod_name))
threading.Thread(target=mod.run, args=(emit_factory(mod_name),log_factory(mod_name))).start()
@app.route('/')
def index():
return app.send_static_file('index.html')
if __name__ == '__main__':
socketio.run(app)
from os.path import dirname, basename, isfile
import glob
modules = glob.glob(dirname(__file__)+"/*.py")
__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
import json
import requests
from datetime import datetime
from pystache import render
from time import sleep
URL = 'https://stratum0.org/status/status.json';
def run(emit, log_err):
lastchange = -1
templates = dict()
for name in ['status','template']:
with open('modules/brand/{}.mustache'.format(name), 'r') as f:
templates[name] = f.read()
emit(render(templates['template'], {}))
while True:
try:
r = requests.get(URL)
status = json.loads(r.text)
state = status['state']
if state['lastchange'] != lastchange:
lastchange = state['lastchange']
state['since'] = datetime.fromtimestamp(lastchange).strftime('%a, %H:%M')
emit(render(templates['status'], state), outlet='status')
except Exception as e:
log_err(e)
sleep(20)
{{#lastchange}}
{{#open}}
<img src="/modules/brand/stratum0_logo_open.svg" alt="Stratum 0 Logo – space is open"><br>
<h3>Space is <span class="open">open</span> on <span class="person">{{trigger_person}}</span>
{{/open}}
{{^open}}
<img src="/modules/brand/stratum0_logo_closed.svg" alt="Stratum 0 Logo – space is closed"><br>
<h3>Space is <span class="closed">closed</span>
{{/open}}
<br>since {{since}}</h3>
<br>
{{/lastchange}}
{{^lastchange}}
<img src="/modules/brand/stratum0_logo.svg" alt="Stratum 0 Logo – no SpaceAPI connection :-(" ><br>
{{! -- prevent flickering -- }}
<h3>{{! status }}&nbsp;<br>{{! since }}&nbsp;</h3><br>
{{/lastchange}}
<div style="text-align:center;">
<img style="max-width: 470px; max-height: 310px" alt="insert cat picture here" src="http://maurudor.de/?rasd={{random}}" />
</div>
import re
import requests
import xml.etree.ElementTree as ET
from datetime import datetime
from pystache import render
from time import sleep
CITY = 'Braunschweig';
STOPS = ['Ludwigstraße', 'Hamburger Straße'];
ENTRIES = 6;
def run(emit, log_err):
templates = dict()
for name in ['inner','outer']:
with open('modules/bus/{}.mustache'.format(name), 'r') as f:
templates[name] = f.read()
emit(render(templates['outer'], [normalize_stop(s) for s in STOPS]))
while True:
for stop in STOPS:
try:
ns = normalize_stop(stop)
deps = get_data(stop, ENTRIES)
ctx = {'stop': stop, 'normalizedStop': ns, 'deps': deps}
emit(render(templates['inner'], ctx), outlet=ns)
except Exception as e:
log_err(e)
sleep(60)
def fetch_data(stop):
url = "http://62.154.206.87/efaws2/default/XML_DM_REQUEST?sessionID=0&requestID=0&language=de&useRealtime=1&coordOutputFormat=WGS84[DD.ddddd]&locationServerActive=1&mode=direct&dmLineSelectionAll=1&depType=STOPEVENTS&useAllStops=1&command=null&type_dm=stop&name_dm={} {}&mId=efa_rc2".format(CITY, stop);
r = requests.get(url)
return r.text
def get_data(stop, count):
t = ET.fromstring(fetch_data(stop))
deps = t.find('itdDepartureMonitorRequest').find('itdDepartureList')
out = []
for dep in list(deps)[:count]:
line = dep.find('itdServingLine')
time = dep.find('itdDateTime').find('itdTime')
out.append({
'line': line.get('symbol'),
'renderedLine': render_line(line.get('symbol')),
'dir': line.get('direction').replace(CITY, '').strip(),
'platform': dep.get('platform'),
'hour': time.get('hour').zfill(2),
'minute': time.get('minute').zfill(2)
})
return out
def render_line(line):
image = ''
if len(line) < 3:
image = 'tram.svg'
else:
image = 'bus.svg'
return '<img src="/modules/bus/{}"> {}'.format(image, line)
def normalize_stop(stop):
return re.sub('[^a-zA-Z0-9_]', '', stop)
......@@ -8,6 +8,6 @@
<col>
</colgroup>
{{#.}}
<tbody data-infodisplay-outlet="{{normalizedStop}}"></tbody>
<tbody data-infodisplay-outlet="{{.}}"></tbody>
{{/.}}
</table>
import re
import requests
from datetime import date, datetime, timedelta
from pystache import render
from time import sleep
from arrow import Arrow
import ics
import dateutil.rrule as rrule
URL = 'https://stratum0.org/kalender/termine.ics';
def run(emit, log_err):
template = None
with open('modules/calendar/template.mustache', 'r') as f:
template = f.read()
while True:
try:
r = requests.get(URL)
cal = _fix_and_parse(r.text)
one_day = timedelta(days=1)
day = date.today()
soon = []
for _ in range(8*7):
soon.extend(cal.events.on(Arrow.fromdate(day)))
day += one_day
emit(render(template, [_process_event(ev) for ev in soon][:8]))
except Exception as e:
log_err(e)
sleep(60)
def _process_event(ev):
now = datetime.now()
begin = ev.begin.datetime.replace(tzinfo=None)
end = ev.end.datetime.replace(tzinfo=None)
e = dict(title=ev.name)
if end < now:
e['past'] = True
elif begin <= now <= end:
e['now'] = True
if begin.date() == date.today():
e['startRendered'] = begin.strftime('%H:%M')
else:
e['startRendered'] = begin.strftime('%A, %d.%m. %H:%M')
if ev.duration < timedelta(days=1):
e['endRendered'] = end.strftime('%H:%M')
else:
e['endRendered'] = end.strftime('%A %H:%M')
e['duration'] = ':'.join(str(ev.duration).split(':')[:2])
return e
def _fix_and_parse(ical):
ical = ical.replace(';VALUE=DATE-TIME', '')
cal = ics.Calendar(ical)
# add recurring events since the stupid lib does not handle that
start_day = datetime.now() - timedelta(hours=5)
limit_day = datetime.today() + timedelta(days=8*7)
add = []
for ev in cal.events:
m = re.search('\nRRULE:([^\n]+)\n', str(ev))
if m:
dtstart = ev.begin.datetime.replace(tzinfo=None)
rule = rrule.rrulestr(m.group(1), dtstart=dtstart)
duration = ev.duration
for occ in rule.xafter(start_day):
if occ > limit_day: break
nev = ev.clone()
nev.end = occ+duration
nev.begin = occ
add.append(nev)
[cal.events.append(e) for e in add]
return cal
import time
from datetime import datetime
DATE_FORMAT = "%A %d.%m.%Y %H:%M:%S"
def run(emit, log_err):
while True:
try:
start = time.time()
out = datetime.now().strftime(DATE_FORMAT)
out = '<div align="right"><h2>{}</h2></div>'.format(out)
emit(out)
except Exception as e:
log_err(e)
time.sleep(1-(time.time()-start))
import json
import requests
from datetime import datetime
from pystache import render
from time import sleep
URL = 'http://shiny.tinyhost.de/php/getdata.php?time=1&id[]={}'
def _fetch_data(id):
url = URL.format(id)
r = requests.get(url)
data = r.text.split('\n')
out = []
for line in data[1:]:
l = line.split(',')
if len(l) == 2:
date = datetime.strptime(l[0], '%Y/%m/%d %H:%M:%S')
out.append((int(date.timestamp()*1000), float(l[1])))
return json.dumps(out)
def run(emit, log_err):
lastchange = -1
templates = dict()
for name in ['data','main']:
with open('modules/diagrams/{}.mustache'.format(name), 'r') as f:
templates[name] = f.read()
emit(render(templates['main'], {}))
while True:
try:
power = _fetch_data(1)
devices = _fetch_data(4)
emit(render(templates['data'], dict(power=power, devices=devices)), outlet='data')
except Exception as e:
log_err(e)
sleep(30)
import ssl
import irc.bot
LIMIT = 35
class InfodisplayClient(irc.bot.SingleServerIRCBot):
def __init__(self, emit, channel, nickname, server, port=6667):
pw = 'Fagee9ie'
ssl_factory = irc.connection.Factory(wrapper=ssl.wrap_socket)
irc.bot.SingleServerIRCBot.__init__(self, [(server, port, pw)], nickname, nickname, connect_factory=ssl_factory, username='infodisplay/Freenode')
self.channel = channel
self.emit = emit
self.cache = []
def on_nicknameinuse(self, c, e):
c.nick(c.get_nickname() + "_")
def on_welcome(self, c, e):
c.join(self.channel)
def on_pubmsg(self, c, e):
user = self._clean_str(e.source.nick)
if user == '***':
return
msg = self._clean_str(e.arguments[0])
line = '<p><span>{}</span> {}</p>'.format(user, msg)
self.cache.append(line)
if len(self.cache) > LIMIT:
self.cache.pop(0)
self.emit(''.join(self.cache), outlet='inner')
def _clean_str(self, s):
return s.replace('<','&lt;').replace('>','&gt;')
def run(emit, log_err):
#TODO log errors
emit('<h3>&nbsp;IRC #stratum0</h3><div class="chat" data-infodisplay-outlet="inner"></div>')
bot = InfodisplayClient(emit, '#stratum0', 'infodisplay', 'bouncer.ksal.de', port=28921)
bot.start()
import json
import requests
from datetime import datetime
from pystache import render
from time import sleep
APPID = 'fdc3690e6f9a7572128fe4012b4a2500';
CITYID = '2945024';
DIRECTIONS = { 'NNE': 11.25, 'NE': 33.75, 'ENE': 56.25, 'E': 78.75, 'ESE': 101.25, 'SE': 123.75, 'SSE': 146.25, 'S': 168.75, 'SSW': 191.25, 'SW': 213.75, 'WSW': 236.25, 'W': 258.75, 'WNW': 281.25, 'NW': 303.75, 'NNW': 326.25, 'N': 348.75 };
ICON_BASE_URL = 'http://openweathermap.org/img/w/';
WEATHER_URL = 'http://api.openweathermap.org/data/2.5/weather?units=metric&id={}&appid={}'
FORECAST_URL = 'http://api.openweathermap.org/data/2.5/forecast?units=metric&id={}&appid={}'
def run(emit, log_err):
template = None
with open('modules/weather/template.mustache', 'r') as f:
template = f.read()
while True:
try:
current = fetch_weather()
forecast = fetch_forecast(6)
emit(render(template, dict(current=current, forecast=forecast)))
except Exception as e:
log_err(e)
sleep(60*10)
def fetch_weather():
r = requests.get(WEATHER_URL.format(CITYID, APPID))
dat = json.loads(r.text)
return {
'temp': dat['main']['temp'],
'wind': {'speed': dat['wind']['speed'], dir: deg_to_direction(dat['wind']['deg'])},
'pressure': dat['main']['pressure'],
'humidity': dat['main']['humidity'],
'main': dat['weather'][0]['main'],
'desc': dat['weather'][0]['description'],
'icon': '{}{}.png'.format(ICON_BASE_URL, dat['weather'][0]['icon'])
}
def fetch_forecast(count):
r = requests.get(FORECAST_URL.format(CITYID, APPID))
dat = json.loads(r.text)
out = []
for i in range(count):
d = dat['list'][i]
out.append({
'time': datetime.fromtimestamp(d['dt']).strftime('%H:%M'),
'temp': d['main']['temp'],
'wind': {'speed': d['wind']['speed'], dir: deg_to_direction(d['wind']['deg'])},
'main': d['weather'][0]['main'],
'desc': d['weather'][0]['description'],
'icon': '{}{}.png'.format(ICON_BASE_URL, d['weather'][0]['icon'])
})
return out
# const date = new Date(d.dt * 1000);
# return {
# time: `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}`,
# temp: d.main.temp,
# wind: { speed: d.wind.speed, dir: degToDirection(d.wind.deg) },
# main: d.weather[0].main,
# desc: d.weather[0].description,
# icon: `${iconBaseURL + d.weather[0].icon }.png`,
# };
def deg_to_direction(deg):
dir = 'N'
for k,v in DIRECTIONS.items():
if deg > v:
dir = k
return dir
{
"name": "s0infodisplay",
"version": "1.0.0",
"description": "",
"main": "main.js",
"dependencies": {
"babel-polyfill": "^6.23.0",
"ee-first": "^1.1.1",
"express": "^4.13.3",
"httpreq": "^0.4.13",
"ical.js": "^1.1.2",
"irc": "^0.5.2",
"ms": "^0.7.1",
"mustache": "^2.2.0",
"pixl-xml": "^1.0.4",
"socket.io": "^1.3.7",
"time": "^0.11.4"
},
"devDependencies": {
"babel": "^6.23.0",
"babel-cli": "^6.23.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-node6": "^11.0.0",
"eslint": "^3.15.0",
"eslint-config-marudor": "^4.1.2",
"nodemon": "^1.11.0"
},
"scripts": {
"build": "babel src --out-dir modules --copy-files --source-maps",
"dev": "nodemon --watch modules --exec 'node main.js'",
"watch": "npm run build -- --watch",
"start": "node main.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Kasalehlia",
"license": "MIT"
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 400;
src: url(/fonts/Inconsolata-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'PT Sans';
font-style: normal;
font-weight: 400;
src: url(/fonts/PT_Sans-Web-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'PT Sans Narrow';
font-style: normal;
font-weight: 400;
src: url(/fonts/PT_Sans-Narrow-Web-Regular.ttf) format('truetype');
}
@font-face {
font-family: 'PT Sans Narrow';
font-style: normal;
font-weight: 700;
src: url(/fonts/PT_Sans-Narrow-Web-Bold.ttf) format('truetype');
}
......@@ -51,6 +51,17 @@ h3 {
content: ">";
color: #839496;
}
irc-date {
color: #2aa198;
}
irc-date::before {
content: "[";
color: #839496;
}
irc-date::after {
content: "]";
color: #839496;
}
#weather {
font-size: 120%;
}
......@@ -93,3 +104,7 @@ h3 {
#brand .closed {
color: #dc322f;
}
.flot-x-axis, .flot-y-axis {
color: #839496;
}
@import url(http://fonts.googleapis.com/css?family=Inconsolata);@import url(http://fonts.googleapis.com/css?family=PT+Sans);@import url(http://fonts.googleapis.com/css?family=PT+Sans+Narrow:400,700);article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap;word-wrap:break-word}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}html{font-family:'PT Sans',sans-serif}pre,code{font-family:'Inconsolata',sans-serif}h1,h2,h3,h4,h5,h6{font-family:'PT Sans Narrow',sans-serif;font-weight:700}html{background-color:#073642;color:#839496;margin:1em}body{background-color:#002b36;margin:0 auto;max-width:23cm;border:1pt solid #586e75;padding:1em}code{background-color:#073642;padding:2px}a{color:#b58900}a:visited{color:#cb4b16}a:hover{color:#cb4b16}h1{color:#d33682}h2,h3,h4,h5,h6{color:#859900}pre{background-color:#002b36;color:#839496;border:1pt solid #586e75;padding:1em;box-shadow:5pt 5pt 8pt #073642}pre code{background-color:#002b36}h1{font-size:2.8em}h2{font-size:2.4em}h3{font-size:1.8em}h4{font-size:1.4em}h5{font-size:1.3em}h6{font-size:1.15em}.tag{background-color:#073642;color:#d33682;padding:0 .2em}.todo,.next,.done{color:#002b36;background-color:#dc322f;padding:0 .2em}.tag{-webkit-border-radius:.35em;-moz-border-radius:.35em;border-radius:.35em}.TODO{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#2aa198}.NEXT{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#268bd2}.ACTIVE{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#268bd2}.DONE{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#859900}.WAITING{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#cb4b16}.HOLD{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#d33682}.NOTE{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#d33682}.CANCELLED{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#859900}
\ No newline at end of file
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap;word-wrap:break-word}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}html{font-family:'PT Sans',sans-serif}pre,code{font-family:'Inconsolata',sans-serif}h1,h2,h3,h4,h5,h6{font-family:'PT Sans Narrow',sans-serif;font-weight:700}html{background-color:#073642;color:#839496;margin:1em}body{background-color:#002b36;margin:0 auto;max-width:23cm;border:1pt solid #586e75;padding:1em}code{background-color:#073642;padding:2px}a{color:#b58900}a:visited{color:#cb4b16}a:hover{color:#cb4b16}h1{color:#d33682}h2,h3,h4,h5,h6{color:#859900}pre{background-color:#002b36;color:#839496;border:1pt solid #586e75;padding:1em;box-shadow:5pt 5pt 8pt #073642}pre code{background-color:#002b36}h1{font-size:2.8em}h2{font-size:2.4em}h3{font-size:1.8em}h4{font-size:1.4em}h5{font-size:1.3em}h6{font-size:1.15em}.tag{background-color:#073642;color:#d33682;padding:0 .2em}.todo,.next,.done{color:#002b36;background-color:#dc322f;padding:0 .2em}.tag{-webkit-border-radius:.35em;-moz-border-radius:.35em;border-radius:.35em}.TODO{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#2aa198}.NEXT{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#268bd2}.ACTIVE{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#268bd2}.DONE{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#859900}.WAITING{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#cb4b16}.HOLD{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#d33682}.NOTE{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#d33682}.CANCELLED{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;background-color:#859900}
Copyright 2006 The Inconsolata Project Authors
Copyright (c) 2010, ParaType Ltd. (http://www.paratype.com/public),
with Reserved Font Names "PT Sans" and "ParaType".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stratum 0 Infodisplay</title>
<link rel="stylesheet" href="/css/fonts.css">
<link rel="stylesheet" href="/css/solarized-dark.min.css">
<link rel="stylesheet" href="/css/infodisplay.css">
<script src="/socket.io/socket.io.js"></script>
<script src="/js/socket.io.js"></script>
<script src="/js/jquery-2.1.4.min.js"></script>
<script src="/js/jquery.flot.min.js"></script>
<script src="/js/jquery.flot.time.min.js"></script>
......
......@@ -25,21 +25,17 @@ $(function () {
});
setIntervals = [];
});
$('.component').each(function () {
var e = $(this);
var outlets = new Set();
socket.on(e.attr('id'), function (cnt) {
e.html(cnt);
e.find('[data-infodisplay-outlet]').each(function () {
var name = $(this).attr('data-infodisplay-outlet');
if (!outlets.has(name)) {
socket.on(e.attr('id')+'.'+name, function (content) {
$('[data-infodisplay-outlet="'+name+'"]').html(content);
e.trigger('content', name);
});
outlets.add(name);
socket.on('update', function (data) {
//console.log(data);
var target = $('#'+data.module);
if (data.outlet !== '.') {
target = target.find('[data-infodisplay-outlet="'+data.outlet+'"]');
}
});
});
//console.log(target);
if (target.length === 0) {
console.warn('could not find outlet');
}
target.html(data.content);
target.trigger('content', data.outlet);
});
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
(function a () {
var lastMinute = null;
setInterval(function () {
var update = function () {
var d = new Date();
var minute = d.getMinutes()
if (minute != lastMinute) {
......@@ -21,8 +21,10 @@
lastMinute = null;
}
}
}, 1000);
};
setInterval(update, 1000);
$('#bus').on('content', function (ev, name) {
lastMinute = null;
update();
});
})();
arrow==0.4.2
beautifulsoup4==4.4.1
catfish==1.4.2
chardet==2.3.0
click==6.7
command-not-found==0.3
decorator==4.0.6
defer==1.0.6
eventlet==0.22.1
Flask==0.12.2
Flask-SocketIO==2.9.4
greenlet==0.4.13
html5lib==0.999
ics==0.3.1
inflect==0.2.5
ipython==2.4.1
irc==16.2
itsdangerous==0.24
jaraco.classes==1.4.3
jaraco.collections==1.5.2
jaraco.functools==1.17
jaraco.itertools==2.1
jaraco.logging==1.5.1
jaraco.stream==1.1.2
jaraco.text==1.9.2
Jinja2==2.10
language-selector==0.1
lightdm-gtk-greeter-settings==1.2.1
lxml==3.5.0
MarkupSafe==1.0
menulibre==2.1.3
more-itertools==4.1.0
mugshot==0.3.1
netaddr==0.7.18
onboard==1.2.0
pexpect==4.0.1
Pillow==3.1.2
pkg-resources==0.0.0
psutil==3.4.2
ptyprocess==0.5
pycups==1.9.73
pycurl==7.43.0
pygobject==3.20.0
pystache==0.5.4
python-apt==1.1.0b1+ubuntu0.16.4.1
python-dateutil==2.6.1
python-debian==0.1.27
python-engineio==2.0.2
python-socketio==1.8.4
python-systemd==231
pytz==2018.3
pyxdg==0.25
reportlab==3.3.0
requests==2.9.1
sessioninstaller==0.0.0
simplegeneric==0.8.1
six==1.10.0
tempora==1.10
ubuntu-drivers-common==0.0.0
ufw==0.35
unattended-upgrades==0.1
urllib3==1.13.1
usb-creator==0.3.0
virtualenv==15.0.1
Werkzeug==0.14.1
xkit==0.0.0
......@@ -4,7 +4,7 @@ Requires=network.target
Before=infopoint-html.service
[Service]
ExecStart=/bin/sh -c 'cd /home/pi/s0infodisplay; node main.js'
ExecStart=/bin/bash -c 'cd /home/pi/s0infodisplay; source env/bin/activate; python3 main.py'
Restart=always
RestartSec=5
User=1000
......
import fs from 'fs';
import httpreq from 'httpreq';
import Mustache from 'Mustache';
const URL = 'https://stratum0.org/status/status.json';
const TEMPLATES = {
template: fs.readFileSync('modules/brand/template.mustache', 'utf-8'),
status: fs.readFileSync('modules/brand/status.mustache', 'utf-8'),
};
let status = {};
function renderStatus(sock, everything) {
const sendInner = function() {
status.random = `${Math.random()}`;
sock.emit('brand.status', Mustache.render(TEMPLATES.status, status));
};
if (everything) {
sock.emit('brand', Mustache.render(TEMPLATES.template, {}));
setTimeout(sendInner, 3000);
} else {
sendInner();
}
}
function fetchStatus(cb) {
httpreq.get(URL, (err, res) => {
const state = JSON.parse(res.body).state;
cb(state);
});
}
module.exports = function(io) {
let firstTime = true;
function update() {
fetchStatus((state) => {
if (status.lastchange !== state.lastchange) {
const d = new Date(state.lastchange * 1000);
state.since = `${DOW[d.getDay()]}, ${pad(d.getHours(), 2)}:${
pad(d.getMinutes(), 2)}`;
status = state;
renderStatus(io);
}
if (firstTime && status) {
io.on('connection', (sock) => {
renderStatus(sock, true);
});
renderStatus(io, true);
firstTime = false;
} else {
renderStatus(io, false);
}
});
}
setInterval(update, 2 * 60 * 1000); //every 2 minutes
update();
};
Space is {{#open}}<span class="open">open</span> on <span class="person">{{trigger_person}}</span>{{/open}}
{{^open}}<span class="closed">closed</span>{{/open}}
<br>since {{since}}
<br>
<style>
#pa {
margin: auto;
display: flex;
height: 310px;
width: 470px;
}
#ch {
margin: auto; /* Magic! */
max-width: 100%;
max-height: 100%;
}
</style>
<div id="pa">
<img id="ch" src="http://maurudor.de/?rasd={{random}}"></img>
</div>
</body>
</html>
<img src="/modules/brand/stratum0_logo.svg"><br>
<h3 data-infodisplay-outlet="status"></h3>
/* eslint no-mixed-operators: 0 */
import fs from 'fs';
import httpreq from 'httpreq';
import Mustache from 'mustache';
import XML from 'pixl-xml';
/// CONFIG
const CITY = 'Braunschweig';
const STOPS = ['Ludwigstraße', 'Hamburger Straße'];
const ENTRIES = 6;
/// VARS
let CACHED = [];
const TEMPLATES = {
outer: fs.readFileSync('modules/bus/outer.mustache', 'utf-8'),
inner: fs.readFileSync('modules/bus/inner.mustache', 'utf-8'),
};
function fetchData(stop, cb) {
const url = `http://62.154.206.87/efaws2/default/XML_DM_REQUEST?sessionID=0&requestID=0&language=de&useRealtime=1&coordOutputFormat=WGS84[DD.ddddd]&locationServerActive=1&mode=direct&dmLineSelectionAll=1&depType=STOPEVENTS&useAllStops=1&command=null&type_dm=stop&name_dm=${ CITY } ${ stop }&mId=efa_rc2`;
httpreq.get(url, { binary: true }, (err, res) => {
try {
cb(XML.parse(res.body));
} catch (e) {
// Swallow
}
});
}
function renderLine(line) {
let image = '';
if (line.charAt(0) === 'M') {
image = 'metro.svg';
line = line.substr(1); // eslint-disable-line
} else if (line.length < 3) {
image = 'tram.svg';
} else {
image = 'bus.svg';
}
return `<img src="/modules/bus/${image}"> ${line}`;
}
function getData(stop, count, cb) {
fetchData(stop, (data) => {
const deps = data.itdDepartureMonitorRequest.itdDepartureList.itdDeparture;
cb(deps.slice(0, count).map((dep) => ({
line: dep.itdServingLine.symbol,
renderedLine: renderLine(dep.itdServingLine.symbol),
dir: dep.itdServingLine.direction.replace(CITY, '').trim(),
platform: dep.platform,
mean: dep.itdServingLine.itdNoTrain.name,
day: pad(dep.itdDateTime.itdDate.day, 2),
month: pad(dep.itdDateTime.itdDate.month, 2),
hour: pad(dep.itdDateTime.itdTime.hour, 2),
minute: pad(dep.itdDateTime.itdTime.minute, 2),
})));
});
}
function update(io, allStopsDoneCb) {
const done = [];
const context = [];
const innerGetData = function(stop, i) {
const ns = normalizeStop(stop);
getData(stop, ENTRIES, (deps) => {
try {
context[i] = { stop, normalizedStop: ns, deps };
io.emit(`bus.${ns}`, Mustache.render(TEMPLATES.inner, context[i]));
if (done.indexOf(stop) === -1) {
done.push(stop);
if (done.length === STOPS.length) {
allStopsDoneCb();
}
}
CACHED = context;
} catch (e) {
console.log(e); // eslint-disable-line
}
// calculate when to update next
const d = new Date();
const hour = d.getHours();
const minute = d.getMinutes();
const depHour = deps[0].hour < hour ? deps[0].hour + 24 : deps[0].hour;
const diff = (depHour - hour) * 60 + (deps[0].minute - minute);
setTimeout(() => {
innerGetData(stop, i);
}, (diff * 60 + (60 - d.getSeconds()) % 60) * 1000);
});
};
STOPS.forEach((stop, i) => {
innerGetData(stop, i);
});
}
function normalizeStop(stop) {
return stop.replace(/[^a-zA-Z0-9_]/g, '');
}
module.exports = function(io) {
update(io, () => {
const pushToClient = function(sock) {
sock.emit('bus', Mustache.render(TEMPLATES.outer, CACHED));
setTimeout(() => {
for (const i in CACHED) { // eslint-disable-line
sock.emit(`bus.${CACHED[i].normalizedStop}`,
Mustache.render(TEMPLATES.inner, CACHED[i]));
}
}, 3000); //wait a second to let the client process the outlets
};
io.on('connect', pushToClient);
pushToClient(io);
});
};
/* eslint no-mixed-operators: 0 */
import fs from 'fs';
import httpreq from 'httpreq';
import ical from 'ical.js';
import Mustache from 'mustache';
import time from 'time';
const URL = 'https://stratum0.org/kalender/termine.ics';
const TEMPLATE = fs.readFileSync('modules/calendar/template.mustache', 'utf-8');
let CALENDAR;
const TZOFFSET = new time.Date().getTimezoneOffset() * 60;
function getData(count, cb) {
httpreq.get(URL, (err, res) => {
const ics = res.body.replace(/[0-9]{8}T[0-9]{6}/g, '$&Z');
const cal = new ical.Component(ical.parse(ics));
const rawEvents = cal.getAllSubcomponents('vevent').map((raw) => new ical.Event(raw));
const events = [];
const p = function(p) {
return ev.component.getFirstPropertyValue(p);
};
let ev = p;
for (const i in rawEvents) { // eslint-disable-line
ev = rawEvents[i];
if (ev.isRecurring()) {
const iter = ev.iterator();
const duration = p('dtend').toUnixTime() - p('dtstart').toUnixTime();
for (let i = 0; i < 100; i++) {
const next = iter.next();
if (next === undefined) {
break;
}
const start = next.toUnixTime() + TZOFFSET;
events.push({
title: p('summary'),
start,
end: start + duration,
});
}
} else {
events.push({
title: p('summary'),
start: p('dtstart').toUnixTime() + TZOFFSET,
end: p('dtend').toUnixTime() + TZOFFSET,
});
}
}
events.sort((a, b) => a.start - b.start);
const threshold = Date.now() / 1000 - 5 * 60 * 60; //show 5 hours ago
let i;
for (i in events) {
if (events[i].end > threshold) {
i = parseInt(i, 10);
break;
}
}
const now = new Date();
cb(events.slice(i, i + count).map((ev) => {
const start = new Date(ev.start * 1000);
if (start.getMonth() === now.getMonth() && start.getDate() === now.getDate()) {
ev.startRendered = `${pad(start.getHours(), 2)}:${pad(start.getMinutes(), 2)}`;
} else {
ev.startRendered = `${DOW[start.getDay()]}, ${pad(start.getDate(), 2)
}.${pad(start.getMonth() + 1, 2)}. ${pad(start.getHours(), 2)
}:${pad(start.getMinutes(), 2)}`;
}
const end = new Date(ev.end * 1000);
if (ev.end - ev.start >= 60 * 60 * 24) {
ev.endRendered = `${DOW[end.getDay()]} ${pad(end.getHours(), 2)
}:${pad(end.getMinutes(), 2)}`;
} else {
ev.endRendered = `${pad(end.getHours(), 2)}:${pad(end.getMinutes(), 2)}`;
}
const dur = Math.floor((ev.end - ev.start) / 60);
ev.duration = `${pad(Math.floor(dur / 60), 2) }:${ pad((dur % 60), 2)}`;
if (start.getTime() < now.getTime()) {
if (end.getTime() < now.getTime()) {
ev.past = true;
} else {
ev.now = true;
}
}
return ev;
}));
});
}
module.exports = function(io) {
function update(firstRunCallback) {
getData(8, (data) => {
CALENDAR = data;
io.emit('calendar', Mustache.render(TEMPLATE, CALENDAR));
if (firstRunCallback) {
firstRunCallback();
firstRunCallback = null; // eslint-disable-line
}
});
}
update(() => {
const pushToClients = function(sock) {
sock.emit('calendar', Mustache.render(TEMPLATE, CALENDAR));
};
io.on('connect', pushToClients);
pushToClients(io);
});
setInterval(update, 600000);
};
function date() {
const d = new Date();
return `${DOW[d.getDay()]}, ${pad(d.getDate(), 2)}.${pad(d.getMonth() + 1, 2)}.${d.getFullYear()} ${pad(d.getHours(), 2)}:${pad(d.getMinutes(), 2)}:${pad(d.getSeconds(), 2)}`;
}
module.exports = function(io) {
setInterval(() => {
io.emit('date', `<div align="right"><h2>${date()}&nbsp;</h2></div>`);
}, 1000);
};
import fs from 'fs';
import httpreq from 'httpreq';
import Mustache from 'mustache';
const TEMPLATES = {
main: fs.readFileSync('modules/diagrams/main.mustache', 'utf-8'),
data: fs.readFileSync('modules/diagrams/data.mustache', 'utf-8'),
};
function fetchData(id, cb) {
const url = `http://shiny.tinyhost.de/php/getdata.php?time=1&id[]=${id}`;
httpreq.get(url, { binary: true }, (err, res) => {
try {
cb(res.body.toString().split('\n'));
} catch (e) {
// Swallow Error
}
});
}
const DATA = { power: '[]', devices: '[]' };
function handleData(prop, data) {
let line = [];
const out = [];
for (let i = 1; i < data.length - 1; i++) {
line = data[i].split(',');
if (line.length === 2) {
out.push([(new Date(line[0])).getTime(), parseFloat(line[1])]);
}
}
DATA[prop] = JSON.stringify(out);
}
function update(cb) {
let counter = 0;
const next = function() {
counter += 1;
if (counter === 2) {
cb();
}
};
fetchData(1, (data) => {
handleData('power', data);
next();
}); //power
fetchData(4, (data) => {
handleData('devices', data);
next();
}); //devices
}
module.exports = function(io) {
setInterval(() => {
update(() => {
io.emit('diagrams.data', Mustache.render(TEMPLATES.data, DATA));
});
}, 5000);
io.on('connect', (sock) => {
sock.emit('diagrams', Mustache.render(TEMPLATES.main, {}));
});
};
import irc from 'irc';
const CHANNEL = '#stratum0';
const PW = 'Fagee9ie';
module.exports = function(io) {
const client = new irc.Client('bouncer.ksal.de', 'infodisplay', {
channels: [CHANNEL],
port: 28921,
secure: true,
selfSigned: true,
userName: 'infodisplay/Freenode',
password: PW,
});
const content = [];
client.addListener('message', (from, to, message) => {
if (to !== CHANNEL || from === undefined) {
return;
}
// eslint-disable-next-line
message = message.replace(/</g, '&lt;').replace(/>/g, '&gt;');
content.push(`<p><span>${from}</span> ${message}</p>`);
if (content.length > 25) {
content.shift();
}
io.emit('irc.inner', content.join(''));
});
io.on('connect', sock => {
sock.emit(
'irc',
'<h3>&nbsp;IRC #stratum0</h3><div class="chat" data-infodisplay-outlet="inner"></div>'
);
setTimeout(
() => {
sock.emit('irc.inner', content.join(''));
},
3000
);
});
};
/* eslint no-console: 0 */
require('babel-polyfill')
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(8000);
app.use(express.static(`${__dirname }/../public`));
const instanceClients = [];
io.on('connection', (socket) => {
socket.on('ident', (client) => {
if (instanceClients.indexOf(client) === -1) {
instanceClients.push(client);
socket.emit('meta', 'reload');
}
});
});
// for reasons
global.DOW = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
global.pad = function(n, width, z = '0') {
// eslint-disable-next-line
n = String(n);
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
};
const normalizedPath = require('path').join(__dirname);
require('fs').readdirSync(normalizedPath).forEach((file) => {
if (file.endsWith('.js') && !file.includes('main.js')) {
try {
require(`./${ file}`)(io);
console.log(`${file} loaded`);
} catch (e) {
console.log(`Error loading ${file}`);
console.log(e);
}
}
});
import fs from 'fs';
import httpreq from 'httpreq';
import Mustache from 'mustache';
///CONFIG
const APPID = 'fdc3690e6f9a7572128fe4012b4a2500';
const CITYID = '2945024';
/// STATICS
const directions = { NNE: 11.25, NE: 33.75, ENE: 56.25, E: 78.75, ESE: 101.25, SE: 123.75, SSE: 146.25, S: 168.75, SSW: 191.25, SW: 213.75, WSW: 236.25, W: 258.75, WNW: 281.25, NW: 303.75, NNW: 326.25, N: 348.75 };
const iconBaseURL = 'http://openweathermap.org/img/w/';
const TEMPLATE = fs.readFileSync('modules/weather/template.mustache', 'utf-8');
function fetchCurrent(cityid, cb) {
const url = `http://api.openweathermap.org/data/2.5/weather?units=metric&id=${cityid}&appid=${APPID}`;
httpreq.get(url, (err, res) => {
const dat = JSON.parse(res.body);
if (dat.cod) {
cb({
temp: dat.main.temp,
wind: { speed: dat.wind.speed, dir: degToDirection(dat.wind.deg) },
pressure: dat.main.pressure,
humidity: dat.main.humidity,
main: dat.weather[0].main,
desc: dat.weather[0].description,
icon: `${iconBaseURL + dat.weather[0].icon }.png`,
});
}
});
}
function fetchForecast(cityid, count, cb) {
const url = `http://api.openweathermap.org/data/2.5/forecast?units=metric&id=${cityid}&appid=${APPID}`;
httpreq.get(url, (err, res) => {
const raw = JSON.parse(res.body);
if (raw.list) {
const dat = raw.list.slice(0, count);
cb(dat.map((d) => {
const date = new Date(d.dt * 1000);
return {
time: `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}`,
temp: d.main.temp,
wind: { speed: d.wind.speed, dir: degToDirection(d.wind.deg) },
main: d.weather[0].main,
desc: d.weather[0].description,
icon: `${iconBaseURL + d.weather[0].icon }.png`,
};
}));
}
});
}
function degToDirection(deg) {
let dir = 'N';
for (const i in directions) {
if (deg > directions[i]) {
dir = i;
}
}
return dir;
}
module.exports = function(io) {
const context = {};
let firstTime = true;
const update =