Commit bef4eb4d authored by loelkes's avatar loelkes
Browse files

WIP Version 1

parent f41c3aa4
Copyright 2018 Christian Lölkes
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copyright 2018 Christian Lölkes
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Infobeamer package BMJV
[![Import](https://cdn.infobeamer.com/s/img/import.png)](https://info-beamer.com/use?url=https://hertz-gitlab.zkm.de/rapid-prototyping-lab/info-beamer/freifunk-karlsruhe.git)
# Freifunk
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean vel libero nibh. Cras in ex eu nisi dapibus posuere ac in mauris. Ut finibus odio nulla, ut faucibus sapien dictum a. Morbi id tortor tristique, interdum ligula ut, consectetur mauris. Suspendisse id odio lacinia, accumsan orci a, feugiat nibh. Praesent pulvinar at justo ut mollis. Quisque scelerisque elementum libero, nec semper erat varius dapibus. Sed ullamcorper vulputate sapien, et aliquet est faucibus at. Fusce sed felis scelerisque, suscipit diam nec, euismod quam. Pellentesque est risus, luctus non gravida at, pretium vitae quam.
## Notes
* This package allows the InfoBeamer node to access the internet.
* This package is currently not responsive and only works for 1920x1080 screens.
* This plugin is written for [this API](https://api.frickelfunk.net/).
## Authors
* **info-beamer** - *info-beamer SDK* - [GitHub](https://github.com/info-beamer/package-sdk)
* **Christian Lölkes** - *Package* - [GitHub](https://github.com/loelkes)
## To Do
* Make it response (different screen sizes and resolutions)
* Allow for screen rotation (see above).
## License
This project is licensed under the MIT License - see the [COPYRIGHT](COPYRIGHT) file for details.
## Changelog
### Version 1
Initial working version of this plugin. This is based on the plugin-sdk from info-beamer.
{
"__metadata": {
"api": "",
"device_id": 0,
"instance_id": 0,
"package_id": 0,
"scratch": "",
"secrets": {
"account": "",
"setup": ""
},
"setup_id": 0
},
"bg_color": {
"a": 1,
"b": 0,
"g": 0,
"hex": "000000",
"r": 0,
"rgba": [
0,
0,
0,
1
]
},
"font": "Inconsolata-Bold.ttf",
"rotation": 0,
"text_color": {
"a": 1,
"b": 1,
"g": 1,
"hex": "ffffff",
"r": 1,
"rgba": [
1,
1,
1,
1
]
},
"api_url": "https://api.frickelfunk.net/yanic/meshviewer/nodes.json",
"node_id": "c46e1ffe5248"
}
#!/usr/bin/python
# -*- coding: utf-8 -*-
from http_API import API
from toolbox import Util
from hosted import node, device
from datetime import datetime
import time
class Freifunk(object):
def __init__(self, url, node_id):
self.util = Util()
self.api_url = API(url=url)
self.api_url.format = 'json'
self.lastRefresh = 0
self.node_id = node_id
self.node_data = False
self.human_readable = True
def find_node(self, id='', data=False):
data = data or self.api_url.response['nodes']
self.node_data = next(node for node in data if node['nodeinfo']['node_id'] == self.node_id)
# From https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
def HRB(self, num, suffix='B'):
# Convert to Humand Readable Bits or Bytes (HRB)
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(num) < 1024.0:
return '{:3.1f}{}{}'.format(num, unit, suffix)
num /= 1024.0
return '{:.1f}{}{}'.format(num, 'Yi', suffix)
@property
def firstseen(self):
return self.node_data['firstseen']
@property
def lastseen(self):
return self.node_data['lastseen']
# --------------------------------------------------------------------------
# Flags
# --------------------------------------------------------------------------
@property
def is_online(self):
return self.node_data['flags']['online']
@property
def is_gateway(self):
return self.node_data['flags']['gateway']
# --------------------------------------------------------------------------
# Statistics
# --------------------------------------------------------------------------
@property
def clients(self):
return self.node_data['statistics']['clients']
@property
def rootfs_usage(self):
return self.node_data['statistics']['rootfs_usage']
@property
def loadavg(self):
return self.node_data['statistics']['loadavg']
@property
def memory_usage(self):
return self.node_data['statistics']['memory_usage']
@property
def uptime(self):
return self.node_data['statistics']['uptime']
@property
def idletime(self):
return self.node_data['statistics']['idletime']
@property
def gateway(self):
return self.node_data['statistics']['gateway']
@property
def gateway6(self):
return self.node_data['statistics']['gateway6']
@property
def total_processes(self):
return self.node_data['statistics']['processes']['total']
@property
def running_processes(self):
return self.node_data['statistics']['processes']['running']
@property
def tx_bytes(self):
bytes = self.node_data['statistics']['traffic']['tx']['bytes']
return self.HRB(bytes) if self.human_readable else bytes
@property
def tx_packets(self):
return self.node_data['statistics']['traffic']['tx']['packets']
@property
def tx_dropped(self):
return self.node_data['statistics']['traffic']['tx']['dropped']
@property
def rx_bytes(self):
bytes = self.node_data['statistics']['traffic']['rx']['bytes']
return self.HRB(bytes) if self.human_readable else bytes
@property
def rx_packets(self):
return self.node_data['statistics']['traffic']['rx']['packets']
@property
def rx_dropped(self):
return self.node_data['statistics']['traffic']['dropped']
@property
def traffic(self):
return 'Tx: {} | Rx: {}'.format(self.tx_bytes, self.rx_bytes)
### Infobeamer package part
def refresh(self):
if time.time() - self.lastRefresh > 60:
self.api_url.query()
self.find_node()
node['/stream'](dict(line=self.util.line(self.traffic, height=20)))
self.lastRefresh = time.time()
else:
pass
def update(self):
self.util.reset()
self.refresh()
self.util.update()
#
# Part of info-beamer hosted. You can find the latest version
# of this file at:
#
# https://github.com/info-beamer/package-sdk
#
# Copyright (c) 2014,2015,2016,2017,2018 Florian Wesch <fw@info-beamer.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
VERSION = "1.3"
import os
import sys
import json
import time
import errno
import socket
import select
import pyinotify
import thread
import threading
import requests
from tempfile import NamedTemporaryFile
types = {}
def init_types():
def type(fn):
types[fn.__name__] = fn
return fn
@type
def color(value):
return value
@type
def string(value):
return value
@type
def text(value):
return value
@type
def section(value):
return value
@type
def boolean(value):
return value
@type
def select(value):
return value
@type
def duration(value):
return value
@type
def integer(value):
return value
@type
def float(value):
return value
@type
def font(value):
return value
@type
def device(value):
return value
@type
def resource(value):
return value
@type
def json(value):
return value
@type
def custom(value):
return value
@type
def date(value):
return value
init_types()
def log(msg):
print >>sys.stderr, "[hosted.py] %s" % msg
def abort_service(reason):
log("restarting service (%s)" % reason)
try:
thread.interrupt_main()
except:
pass
time.sleep(2)
os.kill(os.getpid(), 2)
time.sleep(2)
os.kill(os.getpid(), 15)
time.sleep(2)
os.kill(os.getpid(), 9)
time.sleep(100)
class Configuration(object):
def __init__(self):
self._restart = False
self._options = []
self._config = {}
self._parsed = {}
self.parse_node_json(do_update=False)
self.parse_config_json()
def restart_on_update(self):
log("going to restart when config is updated")
self._restart = True
def parse_node_json(self, do_update=True):
with open("node.json") as f:
self._options = json.load(f).get('options', [])
if do_update:
self.update_config()
def parse_config_json(self, do_update=True):
with open("config.json") as f:
self._config = json.load(f)
if do_update:
self.update_config()
def update_config(self):
if self._restart:
return abort_service("restart_on_update set")
def parse_recursive(options, config, target):
# print 'parsing', config
for option in options:
if not 'name' in option:
continue
if option['type'] == 'list':
items = []
for item in config[option['name']]:
parsed = {}
parse_recursive(option['items'], item, parsed)
items.append(parsed)
target[option['name']] = items
continue
target[option['name']] = types[option['type']](config[option['name']])
parsed = {}
parse_recursive(self._options, self._config, parsed)
log("updated config")
self._parsed = parsed
@property
def raw(self):
return self._config
@property
def metadata(self):
return self._config['__metadata']
def __getitem__(self, key):
return self._parsed[key]
def __getattr__(self, key):
return self._parsed[key]
def setup_inotify(configuration):
class EventHandler(pyinotify.ProcessEvent):
def process_default(self, event):
basename = os.path.basename(event.pathname)
if basename == 'node.json':
log("node.json changed")
configuration.parse_node_json()
elif basename == 'config.json':
log("config.json changed!")
configuration.parse_config_json()
elif basename.endswith('.py'):
abort_service("python file changed")
wm = pyinotify.WatchManager()
notifier = pyinotify.ThreadedNotifier(wm, EventHandler())
notifier.daemon = True
notifier.start()
wm.add_watch('.', pyinotify.IN_MOVED_TO)
class Node(object):
def __init__(self, node):
self._node = node
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def send_raw(self, raw):
log("sending %r" % (raw,))
self._sock.sendto(raw, ('127.0.0.1', 4444))
def send(self, data):
self.send_raw(self._node + data)
@property
def is_top_level(self):
return self._node == "root"
@property
def path(self):
return self._node
def write_file(self, filename, content):
f = NamedTemporaryFile(prefix='.hosted-py-tmp', dir=os.getcwd())
try:
f.write(content)
except:
traceback.print_exc()
f.close()
raise
else:
f.delete = False
f.close()
os.rename(f.name, filename)
def write_json(self, filename, data):
self.write_file(filename, json.dumps(
data,
ensure_ascii=False,
separators=(',',':'),
).encode('utf8'))
class Sender(object):
def __init__(self, node, path):
self._node = node
self._path = path
def __call__(self, data):
if isinstance(data, (dict, list)):
raw = "%s:%s" % (self._path, json.dumps(
data,
ensure_ascii=False,
separators=(',',':'),
).encode('utf8'))
else:
raw = "%s:%s" % (self._path, data)
self._node.send_raw(raw)
def __getitem__(self, path):
return self.Sender(self, self._node + path)
def __call__(self, data):
return self.Sender(self, self._node)(data)
def scratch_cached(self, filename, generator):
cached = os.path.join(os.environ['SCRATCH'], filename)
if not os.path.exists(cached):
f = NamedTemporaryFile(prefix='scratch-cached-tmp', dir=os.environ['SCRATCH'])
try:
generator(f)
except:
raise
else:
f.delete = False
f.close()
os.rename(f.name, cached)
if os.path.exists(filename):
try:
os.unlink(filename)
except:
pass
os.symlink(cached, filename)
class APIError(Exception):
pass
class APIProxy(object):
def __init__(self, apis, api_name):
self._apis = apis
self._api_name = api_name
@property
def url(self):
index = self._apis.get_api_index()
if not self._api_name in index:
raise APIError("api '%s' not available" % (self._api_name,))
return index[self._api_name]['url']
def unwrap(self, r):
r.raise_for_status()
if r.status_code == 304:
return None
if r.headers['content-type'] == 'application/json':
resp = r.json()
if not resp['ok']:
raise APIError(u"api call failed: %s" % (
resp.get('error', '<unknown error>'),
))
return resp.get(self._api_name)
else:
return r.content
def add_defaults(self, kwargs):
if not 'timeout' in kwargs:
kwargs['timeout'] = 10
def get(self, **kwargs):
self.add_defaults(kwargs)
try:
return self.unwrap(self._apis.session.get(
url = self.url,
**kwargs
))
except APIError:
raise
except Exception as err:
raise APIError(err)
def post(self, **kwargs):
self.add_defaults(kwargs)
try:
return self.unwrap(self._apis.session.post(
url = self.url,
**kwargs
))
except APIError:
raise
except Exception as err:
raise APIError(err)
class APIs(object):
def __init__(self, config):
self._config = config
self._index = None
self._valid_until = 0
self._lock = threading.Lock()
self._session = requests.Session()
self._session.headers.update({
'User-Agent': 'hosted.py version/%s' % (VERSION,)
})
def update_apis(self):
log("fetching api index")
r = self._session.get(
url = self._config.metadata['api'],
timeout = 5,
)
r.raise_for_status()
resp = r.json()
if not resp['ok']:
raise APIError("cannot retrieve api index")
self._index = resp['apis']
self._valid_until = resp['valid_until'] - 300
def get_api_index(self):
with self._lock:
now = time.time()
if now > self._valid_until:
self.update_apis()
return self._index
@property
def session(self):
return self._session
<