Timeline for Basic UI

Hi all,

another small inspiration for how to use timeline diagrams in basic UI:

Again this needs my common openhab utility .js:

// ***
// * Required definitions:
// *  - var baseURL = "../";
// *
// ***
// * Required API
// * - Google jQuery: https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
// * - jQuery CSV:    https://github.com/evanplaice/jquery-csv
// * - date.js:       http://www.datejs.com/
// ***

function GetGroupItemNames(groupItem) {
    var groupUrl = baseURL.concat("rest/items/").concat(groupItem);
    var groupData = GetData(groupUrl);
    var items = [];

    groupData.members.forEach(function (item) {
        items.push(item.name);
    });

    return items;
}

function GetOpenHABHistory(item, days, serviceId) {
    var csv = GetOpenItemHABHistoryCSV(item, days, serviceId);
    var arrayData = $.csv.toArrays(csv, {
        onParseValue: $.csv.hooks.castToScalar
    });

    return arrayData;
}

function GetData(itemUrl) {
    var itemValue = null;
    $.ajax({
        contentType: 'text/plain',
        url: itemUrl,
        data: {},
        async: false,
        success: function (data) {
            if (data != "NULL") {
                itemValue = data;
            }
        }
    });

    return itemValue;
}

// JSON to CSV Converter
function ConvertToCSV(objArray) {
    var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
    var str = '';

    for (var i = 0; i < array.data.length; i++) {
        var line = '';
        for (var index in array.data[i]) {
            if (line !== '') line += ',';

            line += array.data[i][index];
        }

        str += line + '\r\n';
    }

    return str;
}

function GetOpenHABItemState(item) {
    var itemUrl = baseURL.concat("rest/items/").concat(item).concat("/state/");
    return GetData(itemUrl);
}

function GetOpenHABItem(item) {
    var itemUrl = baseURL.concat("rest/items/").concat(item);
    return GetData(itemUrl);
}

function GetOpenHABItemIntValue(item) {
    return Math.round(GetOpenHabItemState(item));
}

function GetOpenHABItemHistoryJSON(item, days = 1, service = "rrd4j") {
    var jsonData = null;
    // var numberOfMilliseconds = 86400 * 1000 * days;

    var startTimeObject = (new Date()).addDays(days * -1);
    // var endTimeObject = new Date();
    var _month = startTimeObject.getMonth() + 1;
    var _starttime = startTimeObject.getFullYear() + "-" + _month + "-" + startTimeObject.getDate() + "T" + startTimeObject.getHours() + ":" + startTimeObject.getMinutes() + ":" + startTimeObject.getSeconds();
    // var _endtime = endTimeObject.getFullYear() + "-" + endTimeObject.getMonth() + "-" + endTimeObject.getDate() + "T" + endTimeObject.getHours() + ":" + endTimeObject.getMinutes() + ":" + endTimeObject.getSeconds();

    var itemUrl = baseURL.concat("rest/persistence/items/").concat(item);
    itemUrl = itemUrl.concat("?serviceId=" + service);

    if (days != 1) {
        itemUrl = itemUrl.concat("&starttime=" + _starttime);
    }
    // itemUrl = itemUrl.concat("&endtime="   + _endtime);

    jsonData = GetData(encodeURI(itemUrl));
    return jsonData;
}

function GetOpenItemHABHistoryCSV(item, days = 1, service = "rrd4j") {
    var jsonData = GetOpenHABItemHistoryJSON(item, days, service);
    return ConvertToCSV(jsonData);
}

function GetParameter(parameterName) {
    var result = null,
        tmp = [];
    location.search.substr(1).split("&")
        .forEach(function (item) {
            tmp = item.split("=");
            if (tmp[0] === parameterName) {
                result = decodeURIComponent(tmp[1]);
            }
        });
    return result;
}

function DOMSetElementHeight(elementId, height) {
    var pageElement = document.getElementById(elementId);
    pageElement.style.height = height + "px";
}

function DOMSetElementWidth(elementId, width) {
    var pageElement = document.getElementById(elementId);
    pageElement.style.width = width + "px";
}

The .css:
visualizationStyle.css (163 Bytes)

The html page to create a timeline diagram from a switch group:

<!DOCTYPE html>
<html>

<head>
    <Title>Timeline</Title>

    <meta http-equiv="X-UA-Compatible" content="IE=Edge">

    <link rel="stylesheet" type="text/css" href="../css/visualizationStyle.css">

    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

    <!-- https://github.com/evanplaice/jquery-csv  -->
    <script type="text/javascript" src="../javascript/jquery.csv.min.js"></script>
    <script type="text/javascript" src="../javascript/date.js"></script>
    <script type="text/javascript" src="../javascript/openHAB.js"></script>

    <script type="application/javascript">
        var baseURL = "../../../";
        var serviceId = "rrd4j";
        var openHABItemNames = [];
        
        var groupParameter = GetParameter("Group");
        var itemParameter  = GetParameter("Item");
        
        if (groupParameter != null) {
            openHABItemNames = GetGroupItemNames(GetParameter("Group"));
        }
        if (itemParameter != null) {
            openHABItemNames.push(itemParameter);
        }
           
        var numberOfDays = 1;

        function filterStateChanges(data) {
            var filteredData = [];
            for (var i = 0; i < data.length; i++) {
                // always keep the 1st entry ...
                if (i == 0) {
                    if (data[i][1] == "ON")
                        filteredData.push(data[i]);
                } else {
                    // only keep transitions ...
                    if (data[i][1] != data[i - 1][1]) {
                        filteredData.push(data[i]);
                    }
                }
            }

            return filteredData;
        }

        function addItemToChart(itemName, numberOfDays, serviceId, chartData) {
            var openHABItem = GetOpenHABItem(itemName);
            var openHABHistoryData = GetOpenHABHistory(itemName, numberOfDays, serviceId);
            var stateChanges = filterStateChanges(openHABHistoryData);

            for (var i = 0; i < stateChanges.length; i++) {
                var entry = [];
                entry[0] = openHABItem.label;
                entry[1] = new Date(parseInt(stateChanges[i][0]));
                if (i + 1 < stateChanges.length) {
                    entry[2] = new Date(parseInt(stateChanges[++i][0]));
                } else {
                    entry[2] = new Date();
                }

                chartData.addRow(entry);
            }

            return chartData;
        }

        function drawChart() {
            var container = document.getElementById("chart");
            var chart = new google.visualization.Timeline(container);
            var chartData = new google.visualization.DataTable();

            chartData.addColumn({
                type: 'string',
                id: "Label"
            });
            chartData.addColumn({
                type: 'date',
                id: 'Start'
            });
            chartData.addColumn({
                type: 'date',
                id: 'End'
            });

            openHABItemNames.forEach(function(itemName) {
                chartData = addItemToChart(itemName, numberOfDays, serviceId, chartData);
            });

            var xAxisFormat = "HH:mm"; // "EEE, HH:mm";
            var chartOptions = {
                height : window.innerHeight,
                /* A theme is a set of predefined option values that work together to achieve 
                 * a specific chart behavior or visual effect. Currently only one theme is available:
                 *  'maximized' - Maximizes the area of the chart, and draws the legend and 
                 *                all of the labels inside the chart area.
                 */
                theme: 'maximized',
                hAxis: {
                    textPosition: 'in',
                    format: xAxisFormat
                }
            }

            chart.draw(chartData, chartOptions);
        };

        // on document ready ...
        $(document).ready(function() {
            // load google charts ...
            google.charts.load("current", {
                packages: ["timeline"]
            });
            // set callback ...
            google.charts.setOnLoadCallback(drawChart);

        });

    </script>
</head>

<body>
    <center>
        <div id="chart"></div>
    </center>
</body>

</html>

And the entry in the sitemap:

Webview url="/static/Visualization/Timeline.html?Group=GroupStatus" height=9

As usual not a step-by-step guide; but some hints how to achieve this (I call them inspirations :slight_smile: ) … hope it is helpful.

with kind regards,
Patrik

22 Likes

Wonderful inspiration, great work, thx.
By the way, you are sitting too long on your sofa :grinning:

3 Likes

I just added a timeline to my sitemap and it’s wonderful!
Thanks @patrik_gfeller!
Some small questions though…

I had to comment out

<link rel="stylesheet" type="text/css" href="../css/visualizationStyle.css">

because visualizationStyle.css is of course missing. Would that be the reason for the scrollbars that appear? Never user Webview until now… If visualizationStyle.css is the culprit would you mind sharing it?

How can I adapt it to display a timeline for a single item instead of a group? Of course, one solution would be to create a group for that single item, but it seems a little forced :smile:
I have to admit a have ZERO knowledge about the CSS styles and very little regarding JS… But I can manage :laughing:

Also, what about adding start and end optional parameters to visualize a specific period? Or something like the default chart element?

Happy to hear that it works :slight_smile: … I´ve added the .css to the original post. Let me know if it helps with the scrollbar issue.

You could try:

        var groupParameter = GetParameter("Group");
        var itemParameter  = GetParameter("Item");
        
        if (groupParameter != null) {
            openHABItemNames = GetGroupItemNames(GetParameter("Group"));
        }
        if (itemParameter != null) {
            openHABItemNames.push(itemParameter);
        }

call:

Timeline.html?Item=ItemPatrikAtHome

Do you use rrd4j as persistence; if yes more than the last day won´t work for timeline charts. If not I can try to create something with that option …

I can post some more of my .html for charting if desired - but it is not documented and I´m not a web-developer; thus not a reference. But if it helps and there´s interest to play with this as a basis let me know.

E.g. multiple Y-Axis:

with kind regards,
Patrik

4 Likes

Yep, that was it! No scrollbars now, thanks.

Well, what was need to work was adding:

        var openHABItemNames = [];

prior to your piece of code. It works beautiful! Very, very useful for switches and contacts!

As a matter of fact, I’m using mysql and I modified the parameter form the beginning to:

var serviceId = "mysql";

Seeing how you modified the code to add the Item option I might be able to “compile” myself something to archive the start and end parameters… I already have something in mind: add start and end the same way you added Item, then modify GetOpenHABHistory functions from openHAB.js to use start and end instead of numberOfDays…
It should be fun because, as I said, I have limited knowledge of JS, but Google is my best fried :stuck_out_tongue_closed_eyes:
Many thanks for your work!

I just wanted to say thank you for making and sharing this script.
It is the only thing I saw other systems (home-assistant) have that I would like openhab to have.
And it turns out that we have it too!
It works great, I had to give name to each device that was in the group I was monitoring, but after 15min of fiddling I managed to make it run.

It would be awesome if this could be somehow “easily” installed for the people who cannot web development (or development at all). Like a binding or plugin for habpanel perhaps, not sure. I find it very useful for monitoring the rules and relevant items/events. Much much easier to see what went wrong then having to debug log files.

I will now check your “multiple Y-Axis” chart, I like a lot of data on the same screen :slight_smile:
Thanks again!

P.S. I see now that it already exists on habpanel. I did use 4-5 days googling about possible options, and I didn’t find much. Most likely bad googling skils (or bad karma)

Hey,

I really love the look of the timeline unfortunately I can’t use it because I don’t know where I have to put all the files and what I have to config.
Am I right that I have to put everything (css, html, utility.js) into the html folder?

If yes how will be the structure? Will it be like this?

HMTL
|- .css
|- .html
|- .js

Do I have to config something? Like BaseUrl? In the example all items of the group “GroupStatus” would be displayed, is this correct?
Example from first post.

Webview url="/static/Visualization/Timeline.html?Group=GroupStatus" height=9

Thanks for your help. :smiley:

Okay, I think I’ve got it. :slight_smile:
If someone has also problems I can try to explain what I did.

1 Like

Here is the a german tutorial. https://youtu.be/ATCqvEdePns
I’m going to do an english one as well in a few weeks. :smiley:

3 Likes

I would be interested in the sricpt for the graph. It looks pretty cool. Especially that you can put axis labels on both sides left and right. :smiley:

Deal all,

I would be really interested what went wrong when I followed the tutorial. My sitemap looks the following:

As you can see, timeline chart is blank.

Many thanks for helping me out.

Cheers
Matthias

same problem here!

Thanks
Kai

Does no one have any idea or hint?

Matthias

Hi everyone!

I managed to extend and (I think) improve the Timeline, modifying @patrik_gfeller’s excellent scripts!
Additions/modifications:

  • added Item parameter to display timeline for a single item;
  • added Hours parameter to specify the timeframe to display – much like period parameter of Chart element – if not present, default to 24h;
  • added the ability to plot empty time bar for items which do not have state changes in the interval;
  • added serviceId to specify persistence service to use;
  • added support for Contact items;
  • pad with leading 0 the day, month, minute, etc. of the date when accessing rest/persistence/items/;

Examples:

Webview url="/static/Visualization/Timeline.html?Item=Gas_Boiler_Actuator_Switch" height=4

Webview url="/static/Visualization/Timeline.html?Group=gPresence&Hours=12" height=6

Webview url="/static/Visualization/Timeline.html?Group=Lights&serviceId=jdbc&Hours=24" height=12

As I cannot post a zip archive to this forum, here’s a link to download html.zip. The contents of this archive needs to be placed in conf/html folder like this:

html
|- css
|    |- visualizationStyle.css
|-  javascript
|    |- date.js
|    |- jquery.csv.js
|    |- jquery.csv.min.js
|    |- openHAB.js
|- Visualization
     |- Timeline.html

Once more, many, many thanks to @patrik_gfeller!!!

7 Likes

I really don’t get what’s wrong with my setup… maybe someone can have a closer look :face_with_monocle:

This is my file structure:
image

My group is called timeline:

My sitemap contains two webviews: one with group, one with item

My rrd4j.persist contains the items:

But the webviews are still empty:

Many many thanks!

Regards
Matthias

@Mihai_Badea
I want to post the same effect.

It is different if you use edge or internet exlorer and it is different in ClassicUI and BasicUI

sitemap:

Webview url="/static/Visualization/Timeline.html?Item=swSwitch02&serviceId=jdbc&Hours=24"
Webview url="/static/Visualization/Timeline.html?gSwitchStatusSW&serviceId=jdbc&Hours=24"

ClassicUI and Edge

BasicUI and Edge:

and heavy load on the machine with a lot of eclipse errors.

With Internet Explorer:

And a lot of errors in logfile

2018-03-07 15:01:38.810 [ERROR] [ersey.server.ServerRuntime$Responder] - An I/O error has occurred while writing a response message entity to the container output stream.
org.glassfish.jersey.server.internal.process.MappableException: org.eclipse.jetty.io.EofException
        at org.glassfish.jersey.server.internal.MappableExceptionWrapperInterceptor.aroundWriteTo(MappableExceptionWrapperInterceptor.java:92) [179:org.glassfish.jersey.core.jersey-server:2.22.2]
        at org.glassfish.jersey.message.internal.WriterInterceptorExecutor.proceed(WriterInterceptorExecutor.java:162) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.message.internal.MessageBodyFactory.writeTo(MessageBodyFactory.java:1130) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:711) [179:org.glassfish.jersey.core.jersey-server:2.22.2]
        at org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:444) [179:org.glassfish.jersey.core.jersey-server:2.22.2]
        at org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:434) [179:org.glassfish.jersey.core.jersey-server:2.22.2]
        at org.glassfish.jersey.server.ServerRuntime$2.run(ServerRuntime.java:329) [179:org.glassfish.jersey.core.jersey-server:2.22.2]
        at org.glassfish.jersey.internal.Errors$1.call(Errors.java:271) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.internal.Errors$1.call(Errors.java:267) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.internal.Errors.process(Errors.java:315) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.internal.Errors.process(Errors.java:297) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.internal.Errors.process(Errors.java:267) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:317) [178:org.glassfish.jersey.core.jersey-common:2.22.2]
        at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:305) [179:org.glassfish.jersey.core.jersey-server:2.22.2]
        at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:1154) [179:org.glassfish.jersey.core.jersey-server:2.22.2]
        at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:473) [176:org.glassfish.jersey.containers.jersey-container-servlet-core:2.22.2]
        at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:427) [176:org.glassfish.jersey.containers.jersey-container-servlet-core:2.22.2]
        at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:388) [176:org.glassfish.jersey.containers.jersey-container-servlet-core:2.22.2]
        at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:341) [176:org.glassfish.jersey.containers.jersey-container-servlet-core:2.22.2]
        at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:228) [176:org.glassfish.jersey.contain

And 90% cpu load from internet explorer if page is shown.
grafik

Maybe the problem are the external links in the timeline.html. I block my OH from accessing internet.

    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

@Matt_Hias
If you’re using my version, I “think” (can’t look now) I hardcoded the
perisstence to jdbc.
So, you might want to use the serviceId parameter:

Webview url="/static/.......?Item=timeline&serviceId=rrd4j" height=...

It’s possible, I’m no web-developer…
I merely modified @patrik_gfeller scripts…

If I comment the two lines the heavy load is gone but then I cannot see the timeline at all.

    <!--
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
    -->