How to display public transport timetable in your nspanel

I recently installed NSPanel Blueprint and one of the must-have features for me was to display public transportation timetables in nspanel.

Unfortunately, there is no ready-to-use page with transport timetables available.
The source of data in my case is hslhrt-hass-custom integration with HSL/HRT - Helsinki Regional Transport Authority. But the same approach can be used with other sources of transport data.

Step 1: bring the data into Home Assistant

In case of HSL/HRT this is simply about installing and configuring hslhrt-hass-custom integration. You also need to register in the digitransit service and receive an API key (free of charge).

This step is pretty trivial - just follow instructions from the hslhrt-hass-custom github.

Validate that you have the sensors with the name of the stop. E.g., sensor.kauniainen_ka0101_all and that these sensors have attributes with the data you need to display in the nspanel (formatted in a totally different way of course).

Step 2: activate python integration in Home Assistant

Add the line

python_script:

to configuration.yaml and restart Home Assistant.

Create directory python_scripts in the folder where configuration.yaml is located.

Step 3: develop python script which formats the data for nspanel

NSPanel Blueprint has an entity page and it can be used for displaying public transport timetable.

This is how my timetable looks in nspanel after the configuration is done:

Every line represents a stop. But in order to display this data we need to make a small hack: we need to create a sensor with friendly name containing the long text (departure times) and to put the shorter text (destination) into value.

E.g., in the image above we have three elements in every transport table row:

  1. Icon (mdi:bus or mdi:train), set in the NSPanel Blueprint entity page configuration.
  2. Long string with departure times. This is the friendly name of the sensor we’ll create later using python script.
  3. Short string with the destination name. This is the sensor’s value!

So, now we need to really format this data - create sensors which we can later configure to b e displayed in the entity page.

python_scripts/create_hsl_nspanel_timeteable.py:

# Parse key/value in a format (example): "DESTINATION: Jorvi via Kauniainen"
def parse_kv(kv):
    lst = kv.split(":")
    key = lst[0].strip()
    value = ":".join(lst[1:]).strip("' ")

    return [key, value]


# Creates a route description in the format
# "HH:MM route" (if cut_hours is False)
# OR
# ":MM route" (if cut_hours is True)
def fmt_route(route, tm, cut_hours=False):
    # tm: HH:MM:SS
    hh_mm_lst = tm.split(":")[:2]
    tm_str = ""
    if not cut_hours:
        tm_str = hh_mm_lst[0]
    tm_str = tm_str + ":" + hh_mm_lst[1]
    return f"{tm_str} {route}"


# Returns a string: time_route ("," time_route){1,n} where time_route is "HH:MM ROUTE"
def fmt_n_departures(route, arrival_tm, routes, n=3):
    # Maximal length of the output string - otherwise it doesn't fit to EU nspanel
    max_chars_qty = 28

    res = fmt_route(route, arrival_tm)

    prev_hour = arrival_tm.split(":")[0]
    sep = ", "
    for r in routes[: n - 1]:
        curr_hour = r["ARRIVAL TIME"].split(":")[0]
        cut_hour = curr_hour == prev_hour
        next_route = fmt_route(r["ROUTE"], r["ARRIVAL TIME"], cut_hour)
        if len(res) + len(sep) + len(next_route) <= max_chars_qty:
            res += sep + next_route
        else:
            break
        prev_hour = curr_hour

    return res


# names of the HSL/HRT integration sensors (without prefix "sensor.")
entity_ids = [
    "replace_with_yours",
    "replace_with_yoursl",
    "replace_with_yoursl",
    # Example!
    "kauniainen_ka0101_all",
]

for entity_id in entity_ids:
    nm = "sensor." + entity_id
    state = hass.states.get(nm)

    if state:
        route = state.attributes.get("ROUTE")
        dest = state.attributes.get("DESTINATION")
        # We limit the max. length of a destination field to fit into EU nspanel
        max_dest_len = 15
        if len(dest) > max_dest_len:
            if " " in dest:
                dest = dest.split(" ")[0]

        arr_tm_first = state.attributes.get("ARRIVAL TIME")
        routes = state.attributes.get("ROUTES", "")

        next_departures = fmt_n_departures(route, arr_tm_first, routes)
        hass.states.set(
            nm + "_next_departures", dest, {"friendly_name": next_departures}
        )

If you want to display HSL/HRT timetable, then you can just use the same script and modify the entity_ids values to match those you have configured in hslhrt-hass-custom integration.

If your data provider is different, you need to modify the script to parse your data format.

The idea of the script is pretty obvious - for every hslhrt sensor XYZ with the raw schedule data, it creates a sensor with the name XYZ_next_departures. This sensor’s friendly name has a real timetable for the stop and the value is set to the destination.

Useful docs about python scripts in Home Assistant: https://www.home-assistant.io/integrations/python_script/

Step 4: update our custom sensors when the raw data is updated

Now you can call this script from Home Assistant by calling an action: python_script.create_hsl_nspanel_timeteable

Setup an automation to call the python_scripts/create_hsl_nspanel_timeteable.py when the raw data is changed:

alias: HSLHRT nspanel page updater
description: "Update HSL/HRT page in the nspanel "
trigger:
  - platform: state
    entity_id:
      # List all your hslhrt sensors here!!!
      - sensor.kauniainen_ka0151_all
condition: []
action:
  - action: python_script.create_hsl_nspanel_timeteable
    metadata: {}
    data: {}
mode: single

Step 5: configure NSPanel Blueprint

Now the easiest part: configure these new sensors to be displayed in your entity page.

That’s it!

Have fun!

Awesome!!

Thanks for sharing.