initial commit
This commit is contained in:
commit
fe58b5a874
10 changed files with 1149 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
services.json
|
||||
config.json
|
||||
*.xml
|
||||
*.pyc
|
||||
.venv
|
||||
*.swp
|
138
hosted.js
Normal file
138
hosted.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* info-beamer hosted.js Mockup for local development.
|
||||
* You can find the latest version of this file at:
|
||||
*
|
||||
* https://github.com/info-beamer/package-sdk
|
||||
*
|
||||
* BSD 2-Clause License
|
||||
*
|
||||
* Copyright (c) 2017-2019 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.
|
||||
*
|
||||
*/
|
||||
(function() {
|
||||
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
var asset_root = "https://cdn.infobeamer.com/s/mock-use-latest/";
|
||||
|
||||
function setupResources(js, css) {
|
||||
for (var idx = 0; idx < js.length; idx++) {
|
||||
var script = document.createElement('script');
|
||||
script.setAttribute("type","text/javascript");
|
||||
script.setAttribute("src", asset_root + 'js/' + js[idx]);
|
||||
head.appendChild(script);
|
||||
}
|
||||
|
||||
for (var idx = css.length-1; idx >= 0; idx--) {
|
||||
var link = document.createElement('link')
|
||||
link.setAttribute('rel', 'stylesheet')
|
||||
link.setAttribute('type', 'text/css')
|
||||
link.setAttribute('href', asset_root + 'css/' + css[idx])
|
||||
head.insertBefore(link, head.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
var style = document.createElement('style');
|
||||
var rules = document.createTextNode(
|
||||
"body { width: 750px; margin: auto; }"
|
||||
)
|
||||
style.type = 'text/css';
|
||||
style.appendChild(rules);
|
||||
head.appendChild(style);
|
||||
|
||||
if (window.MOCK_ASSETS == undefined)
|
||||
console.error("[MOCK HOSTED.JS] window.MOCK_ASSETS undefined");
|
||||
if (window.MOCK_NODE_ASSETS == undefined)
|
||||
console.error("[MOCK HOSTED.JS] window.MOCK_NODE_ASSETS undefined");
|
||||
if (window.MOCK_DEVICES == undefined)
|
||||
console.error("[MOCK HOSTED.JS] window.MOCK_DEVICES undefined");
|
||||
if (window.MOCK_CONFIG == undefined)
|
||||
console.error("[MOCK HOSTED.JS] window.MOCK_CONFIG undefined");
|
||||
|
||||
var ib = {
|
||||
assets: window.MOCK_ASSETS,
|
||||
node_assets: window.MOCK_NODE_ASSETS,
|
||||
config: window.MOCK_CONFIG,
|
||||
devices: window.MOCK_DEVICES,
|
||||
doc_link_base: 'data:text/plain,This would have opened the package documentation for ',
|
||||
apis: {
|
||||
geo: {
|
||||
get: function(params) {
|
||||
if (!params.q) {
|
||||
console.error("no q parameter for weather query");
|
||||
}
|
||||
return new Promise(function(resolve, reject) {
|
||||
setTimeout(function() { // simulate latency
|
||||
resolve({"hits":[
|
||||
{"lat":49.00937,"lon":8.40444,"name":"Karlsruhe (Baden-W\u00fcrttemberg, Germany)"},
|
||||
{"lat":48.09001,"lon":-100.62042,"name":"Karlsruhe (North Dakota, United States)"}
|
||||
]})
|
||||
}, 800);
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ib.setDefaultStyle = function() {
|
||||
setupResources([], ['reset.css', 'bootstrap.css'])
|
||||
}
|
||||
|
||||
var asset_chooser_response = window.MOCK_ASSET_CHOOSER_RESPONSE
|
||||
if (asset_chooser_response) {
|
||||
console.log("[MOCK HOSTED.JS] emulating asset chooser");
|
||||
ib.assetChooser = function() {
|
||||
console.log("[MOCK HOSTED.JS] asset chooser mockup returns", asset_chooser_response);
|
||||
return new Promise(function(resolve) {
|
||||
resolve(asset_chooser_response);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ib.setConfig = function(config) {
|
||||
var as_string = JSON.stringify(config);
|
||||
ib.config = JSON.parse(as_string);
|
||||
console.log("[MOCK HOSTED.JS] setConfig", as_string);
|
||||
}
|
||||
|
||||
ib.getConfig = function(cb) {
|
||||
console.warn("[MOCK HOSTED.JS] using .getConfig is deprecated. Use .ready.then(...) instead");
|
||||
cb(ib.config);
|
||||
}
|
||||
|
||||
ib.getDocLink = function(name) {
|
||||
return ib.doc_link_base + name;
|
||||
}
|
||||
|
||||
ib.onAssetUpdate = function(cb) {
|
||||
console.warn("[MOCK HOSTED.JS] onAssetUpdate is a no-op in the mock environment");
|
||||
}
|
||||
|
||||
ib.ready = new Promise(function(resolve) {
|
||||
console.log("[MOCK HOSTED.JS] ready");
|
||||
resolve(ib.config);
|
||||
})
|
||||
|
||||
window.infobeamer = window.ib = ib;
|
||||
})();
|
231
hosted.lua
Normal file
231
hosted.lua
Normal file
|
@ -0,0 +1,231 @@
|
|||
--[[
|
||||
|
||||
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 Florian Wesch <fw@dividuum.de>
|
||||
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.
|
||||
|
||||
]]--
|
||||
|
||||
local resource_types = {
|
||||
["image"] = function(value)
|
||||
local surface
|
||||
local image = {
|
||||
asset_name = value.asset_name,
|
||||
filename = value.filename,
|
||||
type = value.type,
|
||||
}
|
||||
|
||||
function image.ensure_loaded()
|
||||
if not surface then
|
||||
surface = resource.load_image(value.asset_name)
|
||||
end
|
||||
return surface
|
||||
end
|
||||
function image.load()
|
||||
image.ensure_loaded()
|
||||
local state = surface:state()
|
||||
return state ~= "loading"
|
||||
end
|
||||
function image.get_surface()
|
||||
return image.ensure_loaded()
|
||||
end
|
||||
function image.draw(...)
|
||||
image.ensure_loaded():draw(...)
|
||||
end
|
||||
function image.unload()
|
||||
if surface then
|
||||
surface:dispose()
|
||||
surface = nil
|
||||
end
|
||||
end
|
||||
function image.get_config()
|
||||
return image
|
||||
end
|
||||
return image
|
||||
end;
|
||||
["video"] = function(value)
|
||||
local surface
|
||||
local video = {
|
||||
asset_name = value.asset_name,
|
||||
filename = value.filename,
|
||||
type = value.type,
|
||||
}
|
||||
function video.ensure_loaded(opt)
|
||||
if not surface then
|
||||
surface = util.videoplayer(value.asset_name, opt)
|
||||
end
|
||||
return surface
|
||||
end
|
||||
function video.load(opt)
|
||||
video.ensure_loaded(opt)
|
||||
local state = surface:state()
|
||||
return state ~= "loading"
|
||||
end
|
||||
function video.get_surface(opt)
|
||||
return video.ensure_loaded(opt)
|
||||
end
|
||||
function video.draw(...)
|
||||
video.ensure_loaded():draw(...)
|
||||
end
|
||||
function video.unload()
|
||||
if surface then
|
||||
surface:dispose()
|
||||
surface = nil
|
||||
end
|
||||
end
|
||||
function video.get_config()
|
||||
return video
|
||||
end
|
||||
return video
|
||||
end;
|
||||
["child"] = function(value)
|
||||
local surface
|
||||
local child = {
|
||||
asset_name = value.asset_name,
|
||||
filename = value.filename,
|
||||
type = value.type,
|
||||
}
|
||||
function child.ensure_loaded()
|
||||
if surface then
|
||||
surface:dispose()
|
||||
end
|
||||
surface = resource.render_child(value.asset_name)
|
||||
return surface
|
||||
end
|
||||
function child.load()
|
||||
return true
|
||||
end
|
||||
function child.get_surface()
|
||||
return child.ensure_loaded()
|
||||
end
|
||||
function child.draw(...)
|
||||
child.ensure_loaded():draw(...)
|
||||
end
|
||||
function child.unload()
|
||||
if surface then
|
||||
surface:dispose()
|
||||
surface = nil
|
||||
end
|
||||
end
|
||||
function child.get_config()
|
||||
return child
|
||||
end
|
||||
return child
|
||||
end;
|
||||
["json"] = function(value)
|
||||
return require("json").decode(value)
|
||||
end;
|
||||
}
|
||||
|
||||
local types = {
|
||||
["date"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["json"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["text"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["string"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["integer"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["select"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["device"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["boolean"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["duration"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["custom"] = function(value)
|
||||
return value
|
||||
end;
|
||||
["color"] = function(value)
|
||||
local color = {}
|
||||
color.r = value.r
|
||||
color.g = value.g
|
||||
color.b = value.b
|
||||
color.a = value.a
|
||||
color.rgba_table = {color.r, color.g, color.b, color.a}
|
||||
color.rgba = function()
|
||||
return color.r, color.g, color.b, color.a
|
||||
end
|
||||
color.rgb_with_a = function(a)
|
||||
return color.r, color.g, color.b, a
|
||||
end
|
||||
color.clear = function()
|
||||
gl.clear(color.r, color.g, color.b, color.a)
|
||||
end
|
||||
return color
|
||||
end;
|
||||
["resource"] = function(value)
|
||||
return resource_types[value.type](value)
|
||||
end;
|
||||
["font"] = function(value)
|
||||
return resource.load_font(value.asset_name)
|
||||
end;
|
||||
}
|
||||
|
||||
local function parse_config(options, config)
|
||||
local function parse_recursive(options, config, target)
|
||||
for _, option in ipairs(options) do
|
||||
local name = option.name
|
||||
if name then
|
||||
if option.type == "list" then
|
||||
local list = {}
|
||||
for _, child_config in ipairs(config[name]) do
|
||||
local child = {}
|
||||
parse_recursive(option.items, child_config, child)
|
||||
list[#list + 1] = child
|
||||
end
|
||||
target[name] = list
|
||||
else
|
||||
target[name] = types[option.type](config[name])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local current_config = {}
|
||||
parse_recursive(options, config, current_config)
|
||||
return current_config
|
||||
end
|
||||
|
||||
return {
|
||||
parse_config = parse_config;
|
||||
}
|
553
hosted.py
Normal file
553
hosted.py
Normal file
|
@ -0,0 +1,553 @@
|
|||
#
|
||||
# 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
|
||||
|
||||
def list(self):
|
||||
try:
|
||||
index = self.get_api_index()
|
||||
return sorted(index.keys())
|
||||
except Exception as err:
|
||||
raise APIError(err)
|
||||
|
||||
def __getitem__(self, api_name):
|
||||
return APIProxy(self, api_name)
|
||||
|
||||
def __getattr__(self, api_name):
|
||||
return APIProxy(self, api_name)
|
||||
|
||||
class GPIO(object):
|
||||
def __init__(self):
|
||||
self._pin_fd = {}
|
||||
self._state = {}
|
||||
self._fd_2_pin = {}
|
||||
self._poll = select.poll()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def setup_pin(self, pin, direction="in", invert=False):
|
||||
if not os.path.exists("/sys/class/gpio/gpio%d" % pin):
|
||||
with open("/sys/class/gpio/export", "wb") as f:
|
||||
f.write(str(pin))
|
||||
# mdev is giving the newly create GPIO directory correct permissions.
|
||||
for i in range(10):
|
||||
try:
|
||||
with open("/sys/class/gpio/gpio%d/active_low" % pin, "wb") as f:
|
||||
f.write("1" if invert else "0")
|
||||
break
|
||||
except IOError as err:
|
||||
if err.errno != errno.EACCES:
|
||||
raise
|
||||
time.sleep(0.1)
|
||||
log("waiting for GPIO permissions")
|
||||
else:
|
||||
raise IOError(errno.EACCES, "Cannot access GPIO")
|
||||
with open("/sys/class/gpio/gpio%d/direction" % pin, "wb") as f:
|
||||
f.write(direction)
|
||||
|
||||
def set_pin_value(self, pin, high):
|
||||
with open("/sys/class/gpio/gpio%d/value" % pin, "wb") as f:
|
||||
f.write("1" if high else "0")
|
||||
|
||||
def monitor(self, pin, invert=False):
|
||||
if pin in self._pin_fd:
|
||||
return
|
||||
self.setup_pin(pin, direction="in", invert=invert)
|
||||
with open("/sys/class/gpio/gpio%d/edge" % pin, "wb") as f:
|
||||
f.write("both")
|
||||
fd = os.open("/sys/class/gpio/gpio%d/value" % pin, os.O_RDONLY)
|
||||
self._state[pin] = bool(int(os.read(fd, 5)))
|
||||
self._fd_2_pin[fd] = pin
|
||||
self._pin_fd[pin] = fd
|
||||
self._poll.register(fd, select.POLLPRI | select.POLLERR)
|
||||
|
||||
def poll(self, timeout=1000):
|
||||
changes = []
|
||||
for fd, evt in self._poll.poll(timeout):
|
||||
os.lseek(fd, 0, 0)
|
||||
state = bool(int(os.read(fd, 5)))
|
||||
pin = self._fd_2_pin[fd]
|
||||
with self._lock:
|
||||
prev_state, self._state[pin] = self._state[pin], state
|
||||
if state != prev_state:
|
||||
changes.append((pin, state))
|
||||
return changes
|
||||
|
||||
def poll_forever(self):
|
||||
while 1:
|
||||
for event in self.poll():
|
||||
yield event
|
||||
|
||||
def on(self, pin):
|
||||
with self._lock:
|
||||
return self._state.get(pin, False)
|
||||
|
||||
class Device(object):
|
||||
def __init__(self):
|
||||
self._socket = None
|
||||
self._gpio = GPIO()
|
||||
|
||||
@property
|
||||
def gpio(self):
|
||||
return self._gpio
|
||||
|
||||
@property
|
||||
def screen_resolution(self):
|
||||
with open("/sys/class/graphics/fb0/virtual_size", "rb") as f:
|
||||
return [int(val) for val in f.read().strip().split(',')]
|
||||
|
||||
def ensure_connected(self):
|
||||
if self._socket:
|
||||
return True
|
||||
try:
|
||||
log("establishing upstream connection")
|
||||
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self._socket.connect(os.getenv('SYNCER_SOCKET', "/tmp/syncer"))
|
||||
return True
|
||||
except Exception as err:
|
||||
log("cannot connect to upstream socket: %s" % (err,))
|
||||
return False
|
||||
|
||||
def send_raw(self, raw):
|
||||
try:
|
||||
if self.ensure_connected():
|
||||
self._socket.send(raw + '\n')
|
||||
except Exception as err:
|
||||
log("cannot send to upstream: %s" % (err,))
|
||||
if self._socket:
|
||||
self._socket.close()
|
||||
self._socket = None
|
||||
|
||||
def send_upstream(self, **data):
|
||||
self.send_raw(json.dumps(data))
|
||||
|
||||
def turn_screen_off(self):
|
||||
self.send_raw("tv off")
|
||||
|
||||
def turn_screen_on(self):
|
||||
self.send_raw("tv on")
|
||||
|
||||
def screen(self, on=True):
|
||||
if on:
|
||||
self.turn_screen_on()
|
||||
else:
|
||||
self.turn_screen_off()
|
||||
|
||||
def reboot(self):
|
||||
self.send_raw("system reboot")
|
||||
|
||||
def halt_until_powercycled(self):
|
||||
self.send_raw("system halt")
|
||||
|
||||
def restart_infobeamer(self):
|
||||
self.send_raw("infobeamer restart")
|
||||
|
||||
def verify_cache(self):
|
||||
self.send_raw("syncer verify_cache")
|
||||
|
||||
if __name__ == "__main__":
|
||||
device = Device()
|
||||
while 1:
|
||||
try:
|
||||
command = raw_input("syncer> ")
|
||||
device.send_raw(command)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
break
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
log("starting version %s" % (VERSION,))
|
||||
node = NODE = Node(os.environ['NODE'])
|
||||
device = DEVICE = Device()
|
||||
config = CONFIG = Configuration()
|
||||
api = API = APIs(CONFIG)
|
||||
setup_inotify(CONFIG)
|
||||
log("ready to go!")
|
45
node.json
Normal file
45
node.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "Icinga2 Statusmonitor",
|
||||
"permissions": {
|
||||
"network": "Needs to connect to icinga2 api"
|
||||
},
|
||||
"options": [{
|
||||
"title": "Background color",
|
||||
"ui_width": 4,
|
||||
"name": "background_color",
|
||||
"type": "color",
|
||||
"default": [0,0,0,1]
|
||||
}, {
|
||||
"title": "Font",
|
||||
"ui_width": 8,
|
||||
"name": "font",
|
||||
"type": "font",
|
||||
"default": "silkscreen.ttf"
|
||||
}, {
|
||||
"title": "icinga2 API user",
|
||||
"ui_width": 6,
|
||||
"name": "api_user",
|
||||
"type": "string",
|
||||
"default": "readonly"
|
||||
}, {
|
||||
"title": "icinga2 API password",
|
||||
"ui_width": 6,
|
||||
"name": "api_password",
|
||||
"type": "string",
|
||||
"default": "really_secure"
|
||||
}, {
|
||||
"title": "icinga2 API URL for hosts",
|
||||
"ui_width": 12,
|
||||
"name": "url_hosts",
|
||||
"type": "string",
|
||||
"default": "https://icinga2/api/v1/objects/hosts",
|
||||
"hint": "Full URL to the API endpoint which returns a list of monitored hosts"
|
||||
}, {
|
||||
"title": "icinga2 API URL for services",
|
||||
"ui_width": 12,
|
||||
"name": "url_services",
|
||||
"type": "string",
|
||||
"default": "https://icinga2/api/v1/objects/services?filter=service.state!=ServiceOK",
|
||||
"hint": "Full URL to the API endpoint which returns a list of monitored services. Keeping the filter is strongly recommended!"
|
||||
}]
|
||||
}
|
69
node.lua
Normal file
69
node.lua
Normal file
|
@ -0,0 +1,69 @@
|
|||
util.init_hosted()
|
||||
|
||||
local json = require "json"
|
||||
local services = {}
|
||||
local host_width = 0
|
||||
local time_width = 0
|
||||
|
||||
local c_hard = {}
|
||||
c_hard[0] = resource.create_colored_texture(0, 0.6, 0, 1)
|
||||
c_hard[1] = resource.create_colored_texture(0.8, 0.9, 0, 1)
|
||||
c_hard[2] = resource.create_colored_texture(0.8, 0, 0, 1)
|
||||
c_hard[3] = resource.create_colored_texture(0.6, 0, 0.7, 1)
|
||||
|
||||
local c_soft = {}
|
||||
c_soft[1] = resource.create_colored_texture(0.3, 0.4, 0, 1)
|
||||
c_soft[2] = resource.create_colored_texture(0.3, 0, 0, 1)
|
||||
c_soft[3] = resource.create_colored_texture(0.3, 0, 0.4, 1)
|
||||
|
||||
local c_text = {}
|
||||
c_text[0] = {1, 1, 1}
|
||||
c_text[1] = {0, 0, 0}
|
||||
c_text[2] = {1, 1, 1}
|
||||
c_text[3] = {1, 1, 1}
|
||||
|
||||
gl.setup(NATIVE_WIDTH, NATIVE_HEIGHT)
|
||||
|
||||
util.file_watch("services.json", function(content)
|
||||
services = json.decode(content)
|
||||
host_width = 0
|
||||
|
||||
for idx, service in ipairs(services.services) do
|
||||
host_width = math.max(host_width, CONFIG.font:width(service.host, 50))
|
||||
end
|
||||
|
||||
time_width = CONFIG.font:width(services.prettytime, 30)
|
||||
end)
|
||||
|
||||
local white = resource.create_colored_texture(1,1,1,1)
|
||||
local base_time = N.base_time or 0
|
||||
|
||||
function node.render()
|
||||
CONFIG.background_color.clear()
|
||||
CONFIG.font:write(NATIVE_WIDTH/2-time_width/2, 10, services.prettytime, 30, 1,1,1,1)
|
||||
|
||||
local y = 50
|
||||
for idx, serv in ipairs(services.services) do
|
||||
my_height = (#serv.output*40)+90
|
||||
|
||||
if serv.type == 0 then
|
||||
c_soft[serv.state]:draw(0, y, NATIVE_WIDTH, y+my_height)
|
||||
else
|
||||
c_hard[serv.state]:draw(0, y, NATIVE_WIDTH, y+my_height)
|
||||
end
|
||||
|
||||
y = y+20
|
||||
|
||||
CONFIG.font:write(10, y, serv.host, 50, c_text[serv.state][1],c_text[serv.state][2],c_text[serv.state][2],1)
|
||||
CONFIG.font:write(host_width+40, y, serv.service, 50, c_text[serv.state][1],c_text[serv.state][2],c_text[serv.state][3],1)
|
||||
|
||||
y = y+60
|
||||
|
||||
for idx, line in ipairs(serv.output) do
|
||||
CONFIG.font:write(host_width+40, y, line, 30, c_text[serv.state][1],c_text[serv.state][2],c_text[serv.state][3],1)
|
||||
y = y+40
|
||||
end
|
||||
|
||||
y = y+12
|
||||
end
|
||||
end
|
5
package.json
Normal file
5
package.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "icinga2beamer",
|
||||
"author": "hi@kunsmann.eu",
|
||||
"desc": "icinga2 information radiator"
|
||||
}
|
BIN
package.png
Normal file
BIN
package.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
102
service
Executable file
102
service
Executable file
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env python
|
||||
import requests
|
||||
import traceback
|
||||
import time
|
||||
import json
|
||||
import urllib2
|
||||
import pytz
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import islice
|
||||
|
||||
from hosted import CONFIG, NODE
|
||||
|
||||
CONFIG.restart_on_update()
|
||||
|
||||
def current_time():
|
||||
timezone = pytz.timezone("Europe/Berlin")
|
||||
now = datetime.utcnow()
|
||||
now = now.replace(tzinfo=pytz.utc)
|
||||
now = now.astimezone(timezone)
|
||||
now = now.replace(tzinfo=None)
|
||||
return now
|
||||
|
||||
def to_unixtimestamp(dt):
|
||||
return int(time.mktime(dt.timetuple()))
|
||||
|
||||
def regenerate():
|
||||
now = current_time()
|
||||
|
||||
services = {
|
||||
'generated': to_unixtimestamp(now),
|
||||
'prettytime': now.strftime('%d.%m.%Y %H:%M:%S'),
|
||||
'services': [],
|
||||
}
|
||||
|
||||
try:
|
||||
hosts = requests.get(CONFIG["url_hosts"], auth=(CONFIG["api_user"], CONFIG["api_password"]), verify=False).json()
|
||||
serv = requests.get(CONFIG["url_services"], auth=(CONFIG["api_user"], CONFIG["api_password"]), verify=False).json()
|
||||
|
||||
if 'results' not in hosts:
|
||||
raise KeyError('API call for hosts did not return any results')
|
||||
if 'results' not in serv:
|
||||
raise KeyError('API call for services did not return any results')
|
||||
|
||||
for host in hosts['results']:
|
||||
if host['attrs']['downtime_depth'] > 0:
|
||||
continue
|
||||
|
||||
if host['attrs']['problem']:
|
||||
services['services'].append({
|
||||
'host': host['attrs']['display_name'],
|
||||
'service': '-- HOST --',
|
||||
'state': 2,
|
||||
'type': host['attrs']['state_type'],
|
||||
'output': host['attrs']['last_check_result']['output'].splitlines(),
|
||||
})
|
||||
|
||||
for svc in serv['results']:
|
||||
if host['attrs']['downtime_depth'] > 0:
|
||||
continue
|
||||
|
||||
if svc['attrs']['problem']:
|
||||
services['services'].append({
|
||||
'host': svc['attrs']['host_name'],
|
||||
'service': svc['attrs']['display_name'],
|
||||
'state': svc['attrs']['state'],
|
||||
'type': svc['attrs']['state_type'],
|
||||
'output': svc['attrs']['last_check_result']['output'].splitlines(),
|
||||
})
|
||||
|
||||
services['services'].sort(key=lambda x: str(x['type'])+str(x['state'])+x['host']+x['service'])
|
||||
services['services'].reverse()
|
||||
except Exception as e:
|
||||
services['services'].append({
|
||||
'host': 'icinga2beamer',
|
||||
'service': 'INTERNAL',
|
||||
'state': 2,
|
||||
'output': [repr(e)],
|
||||
})
|
||||
|
||||
if len(services['services']) == 0:
|
||||
services['services'].append({
|
||||
'host': '',
|
||||
'service': 'icinga2',
|
||||
'state': 0,
|
||||
'output': ['Everything is fine. Go get some coffee.'],
|
||||
})
|
||||
|
||||
with file("services.json", "wb") as f:
|
||||
f.write(json.dumps(services, ensure_ascii=False).encode("utf8"))
|
||||
|
||||
def main():
|
||||
while 1:
|
||||
try:
|
||||
regenerate()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
time.sleep(20)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
BIN
silkscreen.ttf
Normal file
BIN
silkscreen.ttf
Normal file
Binary file not shown.
Loading…
Reference in a new issue