Bus schedules from opendata on webpage

Let’s start with the limitation, the opendata is for dutch busses only. So for other countries this tutorial might be only an inspiration.

The use case, instead of using an app/ website to search, just display all departing busses on a stop on my (wall mounted) dashboard. Lets start with the end result so you know time spent on reading is worthwhile. A tile (for a dashboard) with bus departures with the name of the stop, the bus number, destination and time.


Things I’ll use:

  • Open data/API for dutch busses (ovapi.nl)
  • HTTP Binding
  • Javascript transformation
  • JSONPATH transformation
  • Widget to display array string as list (with oh-repeater)

All bus lines, schedules, departure/ arrival times and bus stops can be found on open data. Query by the url, response is in JSON format. Best documentation I could find was on https://github.com/koch-t/KV78Turbo-OVAPI/wiki

First is finding the codes for the bus stops you want to display. Open https://v0.ovapi.nl/stopareacode and it will show them all. With a simple text search in the browser you’ll find the name of a bus stop. Next to it is the StopAreaCode, copy this code. If you want more than one stop, repeat the proces. Let’s keep it a bit private and say it’s “Erasmusplein”.

“TimingPointName”:“Den Haag, Erasmusplein”,“StopAreaCode”:“GvEras”

So now you can get all departures of this stop by using https://v0.ovapi.nl/stopareacode/GvEras/departures

(by using a comma separated list in the url – never seen that – you get the departures of more stops in one request).

HTTP binding
Add a thing based on the HTTP binding with this url. Considering the etiquette on the documentation I use a refresh time of 900 (15 minutes) because this info isn’t that dynamic. Delays and cancelled busses will appear by the way, so the schedule is updated to the actual circumstances.

Javascript transformation
The resulting JSON isn’t usable to display. Depending on codes of the bus it contains multiple blocks, some of the information is not on the same ‘level’ and more important it’s not sorted on departure time. Probably some can be solved by a JSONPATH but the sorting definitely not. So I made a little javascript to do this. It will simplify and transform the source JSON to another JSON with one flat list with all relevant information on one level. Including a new merged property good for displaying.

ovinfo.txt (2.1 KB)

It’s quite basic, loops in loops in loops that read all levels of the original JSON and while doing that create the new flat JSON. Most interesting is the part that concatenates several properties to make human readable text.

            var info="";
            var p=nb.TimingPointName.indexOf(",");

            info+=" - ["+nb.LinePublicNumber+"] ";
            if (nb.LineDirection==1){
            } else {
            info+=" om "+nb.ExpectedDepartureTime.substring(p+1);

Most stupid part is an old school bubble sort, I think (read it somewhere on the forum) that the inline functions (used in one line sorts) are not allowed.

  var done;
  do {
    for (i=0;i<result.busses.length-1;i++){
      if (result.busses[i].ExpectedDepartureTime>result.busses[i+1].ExpectedDepartureTime){
	var tmp=result.busses[i];
  } while (!done);

To use it download the file and rename it to “ovinfo.js” and put in in your openHAB configuration in the “transform” folder. And if you didn’t already to it … add the Add-on Javascript Transformation Service to your openHAB.

I added more properties than just the human readable line (you never know what you want in the future). For now I discard it by adding (using the pipe operator ∩ ) a second JSONPATH filter “$.busses[*].info” that only gets the array with the “info” property.

If you didn’t already do it, install the JSONPATH addon in your openHAB else it won’t work.

So now we can merge it all to create a channel on the http-thing we created. Here’s the code.

UID: http:url:Bus
label: Bus
thingTypeUID: http:url
  authMode: BASIC
  ignoreSSLErrors: false
  baseURL: https://v0.ovapi.nl/stopareacode/GvEras/departures
  delay: 0
  stateMethod: GET
  refresh: 900
  commandMethod: GET
  timeout: 3000
  bufferSize: 2048
  - id: erasmusplein
    channelTypeUID: http:string
    label: erasmusplein
    description: null
      mode: READONLY
      stateTransformation: JS:ovinfo.js∩JSONPATH:$.busses[*].info

Now we’re close. Add a standard Text item to this channel and it will contain the array with the departures.

["Erasmusplein - [51] Den Haag Grote markt om 13:27:46", "Erasmusplein - [51] Rijswijk om 13:28:37", "Erasmusplein - [51] Den Haag Grote markt om 13:44:00", "Erasmusplein - [51] Rijswijk om 13:47:00", "Erasmusplein - [51] Den Haag Grote markt om 13:59:00", "Erasmusplein - [51] Rijswijk om 14:02:00", "Erasmusplein - [51] Den Haag Grote markt om 14:14:00", "Erasmusplein - [51] Rijswijk om 14:17:00", "Erasmusplein - [51] Den Haag Grote markt om 14:29:00", "Erasmusplein - [51] Rijswijk om 14:32:00", "Erasmusplein - [51] Den Haag Grote markt om 14:44:00", "Erasmusplein - [51] Rijswijk om 14:47:00"]

Next challenge it to display it on my wall mounted dashboard in a nice list. I created a widget for it using the oh-repeater . This can split text in parts and use that parts in a repeating component. Biggest challenge (for me) was getting rid of all the whitespace that makes it to big to display in the tiles I uses. And getting the colors in my theme. So there’s some CSS configuration to take care of this. Probably my knowledge of openHAB but the widgets ignores some of the styles (CSS+f7-styles) that are already on the page as well.

uid: bus_list
tags: []
    - context: item
      description: Item with list/array
      label: Item
      name: item
      required: true
      type: TEXT
  parameterGroups: []
timestamp: Jul 9, 2022, 11:25:56 AM
component: oh-list-card
    --f7-list-item-min-height: 20px
    --f7-card-header-min-height: 10px
  stylesheet: >
    div {
      color: gray;
      justify-content: center;
      align-items: center;

    .display-block {

    li {
      height: 18px;

    .card-content {
      padding: 10px 5px 25px 5px;

    .card-header {
      min-height: 20px;
      padding: 0px;

    .size-22 { font-size: 10px }

    .icon {
      font-size: 5px;
  title: Departure times
    - component: oh-repeater
        for: bus
        sourceType: array
        in: =items[props.item].state.split(",")
        fragment: true
        style: f7-icon-size:10px
          - component: oh-list-item
              visible: =loop.bus_idx<6
              title: = loop.bus.replaceAll('"','').replaceAll('[','').replaceAll(']','')

Most interesteting part is a trick to limit the amount list items to six by using the index of the repeater and the visible property.

visible: =loop.bus_idx<6

Most stupid part is this piped replaceAll. But I read on the forum regexp’s aren’t allowed in widgets.


To use it, copy the above code in a new widget. It had only one property, the item you made for the departure times. Now you can use this widget on your pages.

And that’s it. My first tutorial and my discovery how things work and connect in openHAB. At least, the first basics :wink: Somehow it was more complicated than I expected when I started this. But it’s also quite flexible, with some tweaks it’s probably usable with other open data. Let’s see where this leads me :disguised_face:

Have fun, if you have questions just let me know. And maybe you can answer some of my questions:

  • Did I read right inline methods can’t be used in the javascript transforms (for the sorting trick)?
  • Did I read right regexp expressions can’t be used in widgets?
  • I wanted to use an icon in the list. Adding it was easy with the default icon property but they are really big and I couldn’t make them smaller. Looking at HTML code they have hard coded sizes. Or is there a trick?
  • Is there a right way to define layout/colors at the page which the widgets on it will also use? Now I added the font color/colors at more places, seems stupid?

No, they are allowed but the syntax is different and it depends on whether you have a Java Collection or a JavaScript Array or dict.

No, there are equivalents to sort, filter, map, reduce, etc. available. But it depends on what sort of Object you have (Java or JavaScript). If you have a Java Collection (usually the result from getting the members of a Group) you’ll use the Java Streaming API.

For JavaScript you’d use the core JavaScript functions.

I can’t answer that. But I do know that expressions in widgets are limited so I wouldn’t be surprised if REGEX isn’t supported.

Can’t help with that.

Can’t help with that either.

Dealing with complex data structures can be a pain. This is a good tutorial showing one way to do it. I faced a similar problem years ago when I tried to use the weather data from NOAA. It too wasn’t sorted and had an indeterminate number of elements, but it’s XML not JSON.

Thanx!! It’s a javascript transformation. W3schools, haven’t visited that a long time anymore. There’s a point where you think you know it all :wink: But I got to fancy in my short code, occupational hazard. You’re right, the basic core syntax works! So I replaced the sort with:

  result.busses.sort(function(a, b){
	return (a.ExpectedDepartureTime==b.ExpectedDepartureTime ? 0 : (a.ExpectedDepartureTime>b.ExpectedDepartureTime ? 1 : -1 ));

A bit shorter and I assume the build-in sort is more efficient than my very basic code. Won’t make that much difference for 10 items but maybe someone goes berserk and has more items. And I learned something for the future…

Ok, solved the smaller icons as well. It took some fiddling but using the good old “important” trick and some CSS on the repeater did the trick. Now I can add an icon and re-style it to a smaller size (to match the text size). Here’s the code.

    - component: oh-repeater
        for: bus
        fragment: true
        in: =items[props.item].state.split(",")
        sourceType: array
          - component: oh-list-item
              icon: f7:arrowtriangle_right_circle_fill
              title: = loop.bus.replaceAll('"','').replaceAll('[','').replaceAll(']','')
              visible: =loop.bus_idx<6
              stylesheet: >
                .f7-icons {
                  font-size:16px !important;
                  width: 16px !important;
                  height: 16px !important;