JavaScript utilities and examples for using Grafana with Eclipse SmartHome
Switch branches/tags
Nothing to show
Clone or download
wborn Update ignores and minified version (#8)
Signed-off-by: Wouter Born <eclipse@maindrain.net>
Latest commit 270f45a Feb 24, 2018

README.md

smarthome-grafana

This project provides a JavaScript library with examples to simplify embedding Grafana panels in Eclipse SmartHome (ESH) and its derivatives like openHAB 2 (OH2).

The library provides the following functionality:

  • Generation of Grafana embedded panel URL parameters based on the state of ESH items (using REST and subscribing to Server-sent events (SSE))
  • Smart parameter resolution (more on that later)
  • Debug mode for debugging URL generation

This functionality solves the issue of having to create a Webview with a visibility=[..] configuration for user customizable Grafana panel parameters. The resulting sitemaps have less duplication and page load times decrease because a only single Webview has to be loaded.

Requirements

Library objects, their parameters and how they are resolved

The library consists of two kinds of objects:

  • SmartHomeSubscriber: resolves initial ESH item values and listens for item value changes
  • GrafanaPanel: keeps track of the properties of a Grafana panel properties, the enclosing <iframe> tag and updates the <iframe> source URL when ESH item values change.

A SmartHomeSubscriber instance is always created when the JavaScript of the library loaded. GrafanaPanel objects are created using JavaScript. The utility method addGrafanaPanel(uniqueId, grafanaPanelParameters) can be used to create and add a Grafana panel to the <body> of the HTML document.

Library objects require some parameters for their functionality. These parameters are resolved in the following order:

  1. JavaScript object constructor arguments
  2. query parameters of the HTML document, the URL of the document that is loaded in a ESH Webview
  3. query parameters of the parent HTML document, the URL of ESH itself
  4. the SMARTHOME_GRAFANA_DEFAULTS defined in smarthome-grafana.js

SmartHomeSubscriber parameters

The SmartHomeSubscriber typically resolves its parameters from the ESH URL. These parameters can be manually added to the browser URL when showing the page outside of ESH.

Parameter name Comment
w the subscription page, e.g. "0002"
sitemap the subscription sitemap, e.g. "default"

GrafanaPanel parameters

Parameter name Comment
frame the unique identifier of the <iframe> tag in which the panel is shown, e.g. "panel-123-frame" when the panel is created with addGrafanaPanel("123")
urlPrefix the prefix to use for generating Grafana panel URLs, e.g. "http://grafana:3000"
panelPath the panel path in Grafana panel URLs, e.g. "/dashboard-solo/db/"
renderPanelPath the rendered path in Grafana panel URLs, e.g. "/render/dashboard-solo/db/"
debug shows the generated URL instead of the panel when set to "true"
debugItem the name of the ESH item used for resolving the debug value, e.g. "Combo2_Debug"
debugItemFunction the function for mapping the debugItem value to "true" or "false" (JavaScript only)
dashboard static definition of the Grafana dashboard name, e.g. "motion"
dashboardItem the name of the ESH item used for resolving the dashboard Grafana URL value, e.g. "Combo2_Dashboard"
dashboardItemFunction the function for mapping the dashboardItem value to a Grafana URL value (JavaScript only)
from static definition of the from= Grafana URL value, e.g. "now-1w"
fromItem the name of the ESH item used for resolving the from= value, e.g. "Combo2_From"
fromItemFunction the function for mapping the fromItem value to a Grafana URL value (JavaScript only)
to static definition of the to= Grafana URL value, e.g. "now"
toItem the name of the ESH item used for resolving the to= value, e.g. "Combo2_To"
toItemFunction the function for mapping the toItem value to a Grafana URL value (JavaScript only)
panel static definition of the panelId= Grafana URL value, e.g. "5"
panelItem the name of the ESH item used for resolving the panelId= value, e.g. "Combo2_Panel"
panelItemFunction the function for mapping the panelItem value to a Grafana URL value (JavaScript only)
theme static definition of the theme= Grafana URL value, e.g. "light"
themeItem the name of the ESH item used for resolving the theme= value, e.g. "Combo2_Theme"
themeItemFunction the function for mapping the themeItem value to a Grafana URL value (JavaScript only)
render makes Grafana render an image of the panel, use "true" or "false", when set to "true" the width, height and refresh parameters are used
renderItem the name of the ESH item used for resolving the render value, e.g. "Render2_Render"
renderItemFunction the function for mapping the renderItem value to "true" or "false" (JavaScript only)
width static definition of the width= Grafana URL value, e.g. "1000" (only used when render is "true", use width=auto to use the width of the frame
widthItem the name of the ESH item used for resolving the width= value, e.g. "Render2_Width"
widthItemFunction the function for mapping the widthItem value to a number or "auto" (JavaScript only)
height static definition of the height= Grafana URL value, e.g. "300" (only used when render is "true", use height=auto to use the height of the frame
heightItem the name of the ESH item used for resolving the height= value, e.g. "Render2_Height"
heightItemFunction the function for mapping the heightItem value to a number or "auto" (JavaScript only)
refresh static definition of the refresh interval for a rendered panel in milliseconds, e.g. "60000" for refreshing every minute, use "0" to disable
refreshItem the name of the ESH item used for resolving the refresh value, e.g. "Render2_Refresh"
refreshItemFunction the function for mapping the refreshItem value to a number (JavaScript only)

Grafana time units

The Grafana documentation has an overview with the time units that can be used with the from= and to= URL values.

Building

Use Grunt to check the library code with eslint and minify it.

Demo

A demo is included that has some examples on how to use the library. A Grafana installation is not required for the demo, because it uses the library in debug mode. In this mode it does not load Grafana panels. Instead it shows the generated panel URLs.

To run the demo:

  1. add the files in the example directory to the /conf/ directory of an existing ESH/openHAB 2 installation
  2. start the demo by opening the grafana sitemap, e.g. navigate to: http://localhost:8080/basicui/app?sitemap=grafana

Demo screenshots

Click on a thumbnail below to zoom in on a demo page.

Main menu
Main
Static panels Combining panels
Static Combo
Multiple panels Rendered panels
Multi Render

Static panels

These three panels are created using the same demo.html file:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-type" CONTENT="text/html; charset=utf-8">
    <link rel="stylesheet" type="text/css" href="panel-300px.css" />
    <script src="smarthome-grafana.js"></script>
    <script src="smarthome-grafana-user-defaults.js"></script>
</head>

<body>
</body>

<script>
    addGrafanaPanel();
</script>

</html>

This allows for reusing the same CSS and some Grafana defaults defined in smarthome-grafana-user-defaults.js.

Because of these defaults, the urlPrefix is automatically added and the light Grafana theme is used even though they are not defined in the Webview URLs in grafana.sitemap. The URL of the Static 3 Webview in the sitemap also specifies theme=dark which overrides the light default.

By using the debug mode, the library only shows the generated URLs and not the actual pages. This mode can disabled by commenting the debug line in smarthome-grafana-user-defaults.js. In debug mode clicking on a generated link loads the actual Grafana panel. When debug mode is disabled, it can be reenabled for a particular Webview by adding &debug=true its respective URL. It can also be reenabled by adding &debug=true to your browser URL, e.g.: http://localhost:8080/basicui/app?w=0000&sitemap=grafana&debug=true

Combining panels

On this page the Grafana URL panel parameters are generated by the library using the state of ESH items. There are only two Webviews on this page. Without the library, for each parameter combination a Webview with a visibility=[..] expression needs to be added to the sitemap. For Combo 2 this would have resulted in 2*4*5*6*2 = 480 Webviews being added to the sitemap. It would also take a lot of time to load such a page.

Using ESH items as Grafana panel URL parameters is done by adding their names to the URL of a Webview. For instance Combo 2 uses the following Webview definition:

Webview url="/static/demo-combo2.html?dashboardItem=Combo2_Dashboard&fromItem=Combo2_From&toItem=Combo2_To&panelItem=Combo2_Panel&themeItem=Combo2_Theme" height=9

The library retrieves the initial values of these items using a REST call. It also subscribes to sitemap events so it can update them when they are changed. A JavaScript function can be defined to map ESH item values to Grafana panel parameter values.

This is straightforward and for instance done in demo-combo2.html using:

var fromToItemFunction = function(value) {
    switch (value) {
        case "NOW": 
            return "now";
        case "HOUR": 
            return "now-1h";
        case "DAY":
            return "now-1d";
        default: 
        case "WEEK":
            return "now-1w";
        case "MONTH":
            return "now-1M";
        case "YEAR":
            return "now-1y";
    }
}

var panelItemFunction = function(value) {
    switch (value) {
        case "HUMIDITY": 
            return "1";
        default: 
        case "TEMPERATURE":
            return "2";
    }
}

addGrafanaPanel("combination", {
    fromItemFunction: fromToItemFunction,
    toItemFunction: fromToItemFunction,
    panelItemFunction: panelItemFunction,
});

Combo 3 shows how to completely work without JavaScript functions for mapping item values to URL values. This can be done by using the Grafana URL values with double quotes " in the grafana.sitemap mappings. This also allows for reusing the same HTML page (demo.html) for a lot of Webviews.

Multiple panels

This page shows how a JavaScript for loop is used to show several panels of the same dashboard in the same Webview. The panel numbers are calculated and added in demo-multi1.html as follows:

for (var i = 1; i <= 10; i++) {
    addGrafanaPanel(i, {
        panel: i
    });
}

Generated panel tags all have a unique identifier that can be used for CSS styling. The identifiers are based on the first parameter of the addGrafanaPanel function. In this example the library generates:

  • panel-1-frame, panel-2-frame, ... for Grafana panel <iframe> tags
  • panel-1-container, panel-2-container, ... for the <div> tags enclosing the corresponding <iframe> tags

Rendered panels

This page shows how the library is used to generate URLs for rendered Grafana panels. Render 1 again uses demo.html and a static parameter definition for the Webview URL in grafana.sitemap:

Webview url="/static/demo.html?dashboard=wifireception&from=now-1w&to=now&panel=10&width=600&height=300&render=true&refresh=5000" height=9

A rendered panel image is used when the URL contains render=true. To let the library calculate the width and height of a panel use width=auto and height=auto. It will then use the width and height of the frame for the Grafana URL values. The refresh=5000 URL parameter makes the library reload the Grafana panel image every 5 seconds. Refresh can be disabled by omitting the value or by using refresh=0.

Render 2 shows how ESH item values can be used for customizing the render, width, height and refresh parameters.

Reverse proxy configuration

When the Grafana panels do not properly update this is most likely caused by Same-origin policy violations. These can be resolved by for instance configuring a reverse proxy on the same host/port that proxies traffic to ESH/openHAB and Grafana.

With the Apache2 and Nginx examples below Grafana will be accessible on /grafana, htpasswd is used for access control and SSL certificates for encrypting the data. See also the openHAB Security Documentation for information on this subject.

Because Grafana runs on another port/URL with this reverse proxy setup don't forget to update the URL in smarthome-grafana-user-defaults.js to something like the line below (where hostname is the IP/host you use in your browser):

SMARTHOME_GRAFANA_DEFAULTS["urlPrefix"] = "https://hostname/grafana";

Grafana will also need to be reconfigured to use /grafana as root URL. This is done by updating the root_url in grafana.ini (in /etc/grafana) to the configuration below and then restarting Grafana.

# The full public facing url you use in browser, used for redirects and emails
# If you use reverse proxy and sub path specify full url (with sub path)
root_url = %(protocol)s://%(domain)s/grafana/

The following sections contain configuration examples for Apache2 and Nginx. After editing your configuration make sure to restart the Apache2 or Nginx to reload the configuration. You should then be able to access both ESH/openHAB and Grafana via HTTPS on the same port in your browser using the reverse proxy (port 443 instead of 8080).

If you still run into any issues with this you can:

  • Check your browser debug console (F12) for any errors
  • Check the Grafana log file for any errors (/var/log/grafana/grafana.log)
  • Restart ESH/openHAB, in the past there have been issues where newly added sitemap items were not properly updated in UIs
  • Read the comments in this issue which also contains some example error messages that are caused by Same-origin policy violations

Apache2

With Apache2 you can create an enabled site with the configuration below. In Debian based distributions (Ubuntu, openHABian, Raspbian) the enabled sites are usually stored in /etc/apache2/sites-enabled.

SSLStaplingCache shmcb:/tmp/stapling_cache(128000)

<VirtualHost _default_:443>
        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html

    RequestHeader set X-Forwarded-Proto "https"

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    SSLEngine on
    SSLCertificateFile      /etc/ssl-certs/domain.com/cert.pem
    SSLCertificateKeyFile   /etc/ssl-certs/domain.com/privkey.pem
    SSLCertificateChainFile /etc/ssl-certs/domain.com/chain.pem
    SSLUseStapling on

    RewriteEngine On

    <FilesMatch "\.(cgi|shtml|phtml|php)$">
            SSLOptions +StdEnvVars
    </FilesMatch>
    <Directory /usr/lib/cgi-bin>
            SSLOptions +StdEnvVars
    </Directory>

    <Location "/">
        AuthType Basic
        AuthName "Authentication"
        AuthUserFile "/etc/auth-basic/htpasswd"
        Require valid-user

        Order allow,deny
        Allow from all

        ProxyPass        http://localhost:8080/ retry=0 timeout=3600
        ProxyPassReverse http://localhost:8080/
    </Location>

    <Location "/grafana">
        AuthType Basic
        AuthName "Authentication"
        AuthUserFile "/etc/auth-basic/htpasswd"
        Require valid-user

        Order allow,deny
        Allow from all

        ProxyPass        http://localhost:3000 retry=0 timeout=3600
        ProxyPassReverse http://localhost:3000
    </Location>

    BrowserMatch "MSIE [2-6]" \
            nokeepalive ssl-unclean-shutdown \
            downgrade-1.0 force-response-1.0
    BrowserMatch "MSIE [7-9]" ssl-unclean-shutdown

</VirtualHost>

Nginx

With Nginx you can add a server to your server configuration file. In Debian based distributions (Ubuntu, openHABian, Raspbian) the enabled sites are usually stored in /etc/nginx/sites-enabled.

server {
    listen                          443 ssl;
    server_name                     _;

    ssl_certificate                 /etc/ssl-certs/domain.com/fullchain.pem;
    ssl_certificate_key             /etc/ssl-certs/domain.com/privkey.pem;

    auth_basic                      "Username and Password Required";
    auth_basic_user_file            /etc/auth-basic/htpasswd;

    location / {
        proxy_pass                            http://localhost:8080/;
        proxy_set_header Host                 $http_host;
        proxy_set_header X-Real-IP            $remote_addr;
        proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto    $scheme;
    }

    location /grafana/ {
        proxy_pass http://localhost:3000/;
    }
}