From fe58b5a8747343a115efef18ee7fe0ecd31f27c1 Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Fri, 18 Dec 2020 21:55:00 +0100 Subject: [PATCH] initial commit --- .gitignore | 6 + hosted.js | 138 ++++++++++++ hosted.lua | 231 +++++++++++++++++++++ hosted.py | 553 +++++++++++++++++++++++++++++++++++++++++++++++++ node.json | 45 ++++ node.lua | 69 ++++++ package.json | 5 + package.png | Bin 0 -> 1915 bytes service | 102 +++++++++ silkscreen.ttf | Bin 0 -> 18336 bytes 10 files changed, 1149 insertions(+) create mode 100644 .gitignore create mode 100644 hosted.js create mode 100644 hosted.lua create mode 100644 hosted.py create mode 100644 node.json create mode 100644 node.lua create mode 100644 package.json create mode 100644 package.png create mode 100755 service create mode 100644 silkscreen.ttf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9573b7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +services.json +config.json +*.xml +*.pyc +.venv +*.swp diff --git a/hosted.js b/hosted.js new file mode 100644 index 0000000..87a9f38 --- /dev/null +++ b/hosted.js @@ -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 + * 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; +})(); diff --git a/hosted.lua b/hosted.lua new file mode 100644 index 0000000..2bad686 --- /dev/null +++ b/hosted.lua @@ -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 + 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; +} diff --git a/hosted.py b/hosted.py new file mode 100644 index 0000000..c4f9669 --- /dev/null +++ b/hosted.py @@ -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 +# 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', ''), + )) + 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!") diff --git a/node.json b/node.json new file mode 100644 index 0000000..fb64c62 --- /dev/null +++ b/node.json @@ -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!" + }] +} diff --git a/node.lua b/node.lua new file mode 100644 index 0000000..370f269 --- /dev/null +++ b/node.lua @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..578908e --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "name": "icinga2beamer", + "author": "hi@kunsmann.eu", + "desc": "icinga2 information radiator" +} diff --git a/package.png b/package.png new file mode 100644 index 0000000000000000000000000000000000000000..3f0dea6ca76599404f8e712723d9a9c3f874b7f5 GIT binary patch literal 1915 zcmX|B2{@eB8lJ^6C}ZFEDk6g+k|M-%DH?mGiP~acCL+Xyk}yUrSJFgKWGq9F>8&NU zCZe>K$dI~0g%q7i~Rrv6PS~*!E5Di&EY=2&ekumOe)`(~Qic1ItYTeb5U<>5&@v(`C zNlHozFYjj!jKjmjgM)+7(NVxC6beEBY&LskWd#6;fjJpKf&j#D2yB;-kN^Rf%LN9& zi3R{@U=|P%*x%oGad82xqoY$*RRwxD4Z>^Ju4!s&f(-UTcrxzZy}Plov9q%SyA~H0 z;mqyr?YOwO&CN|X8J2)KN%4Ph62c6nR#a5L6+%Kn&}g)*tnB3Eq>+)4zrQ~pAD@bf z3Z#I74Gj&owYAI2%9NFrV`F1^cz7VJt*w=ml!Q84T3SF>US1CKz`y`h2$cZt=;)Z3 zm>`qMQ&UqAGMP+sbMuCVhW`G391bTUBEn!WfQO`j6A}^tr>3T6WMugG_y7(F2uM#) zkB^Us6ch>tss-}IGzOCpg7E%Wg3 zfHn;e4{K;>jE#-a>GbyY_FdII4tPx=xjSD%9EsCKMTr*dL0k}&r*xAG4v24*nGgFT0ZPH-`rzW2v@?Y_~9{eee1@W}68(&%(L8zFHrT5-2 z#vu@BnuCoM;Rj^#TL-}|JR<$C8(Q}Est%)Tj4zD`nv_@CuZV<-o$7R&XcICsJu_CS z$)Po$Ca|ktel5Qm-I?7~99*7m_`DD;*JB|2K>T?x_xWg;|Gz?)zEiE)r{0!w@VeON zocm|D($zZ8w4sT=a@?M{oWI4IvXNs%vvu|Cv=$!wj(N`PD5|0Skw~uKUgGrUXIekk zq0s1ED#;Z8`OMzZt#Uq954o)$tp6w^BbdMJ>Z)?P z2^qPj9bEZKzx(DYV8~MDz4*a5y6yb4|0J4KqIFuTm@A3V=7}woI*QskR3aPg*}i#2 z$+a`V4^`JEypN>v$Wu?z(N(g^`31Dej=6WQ-r!LF%9f_DFsGk<_x3lo<=Kq$J+sGG zS!;&gv&=k-B}eCQ3%%bN#KR@hnX<*G};=1rD<3i%2T>?h9BAH zhte4FOl_T;xj?F}WqkGRxgd(u9U05J0?GKjDzxms*Ft2;ym%=;EVhx~+%XEf3wXj7sGqcZlAFkPyD{)V!AuG&=)vmeA16UTX^A|TlSnkA{=pn{?ypdt2CUgKHTVu0Azw)@EscpQTg(xNRU(;$hBd+<+@- z?ZKI}DB3pd^_RUW-nSlzS~r%S6}s%+Z^k7!gqi5C|9Ug4k;@qm4p8@c^K>^-?p;mA zZr(xYd7CN|*2NcEdEsI=`zY9fcYM9Sb|kfIEYqcJ^M5YuZA9&@_uY~4)yRy9`F7#$ z)OteHf6gkeuJtBLf4}rmm{AefJT-VOj>VfZ7#!nEUg>&$edEWhw!GEr(TibTPNcB8 z#m*P6g%-!_NSMH!?QB~YU9aefzc1r%uRh`bvywV{!=!S4-fr 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() diff --git a/silkscreen.ttf b/silkscreen.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e2dd974b1e31e1e50e0fa939fd0123bee9f95d80 GIT binary patch literal 18336 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 IJ^ax8FU~ke7ytkO literal 0 HcmV?d00001