My wife asked for a sensor that would inform her of the next bus here in Ottawa. Turns out they have an actual API! Someone had tried this on the forums before but said the API had changed1.

This requires an API key. Only takes a few moments. It permits 10000 requests or something reasonably high - my code is aiming for <100.

The rest integration is used in the configuration.yaml. You have to restart, and may need to bump the log level for the rest integration as otherwise it pretty much silently fails.

What you definitely can’t do is keep the whole JSON object, it’s far too large for Home Assistant. Hence most examples setting the value to OK if it managed to do a grab, and then putting various paths in attributes, as they don’t have a history tracked.

  - platform: rest
    resource: "https://api.octranspo1.com/v2.0/GetNextTripsForStop"
    scan_interval: 900
    params:
      format: JSON
      appID: !secret octranspo_appid
      apiKey: !secret octranspo_apikey
      stopNo: xxx
      routeNo: yyy
    name: octranspo_bus_info
    icon: directions_bus
    value_template: OK
    json_attributes_path: '$.GetNextTripsForStopResult.Route.RouteDirection.Trips.Trip[0]'
    json_attributes:
      - TripStartTime
      - AdjustmentAge
      - LastTripOfSchedule
      - AdjustedScheduleTime
      - TripDestination

This only requests relatively infrequently, but the Update Entity service in Home Assistant can be used to e.g. update this more frequently during the day, or as I’m experimenting with, just having a manual button when you’re actually getting ready to leave the house.

This results in JSON like below. It would be nice to keep multiple ones so we can automatically move onto the next one but I think either a proper integration is required or some really evil jinja templating.

{
    "Longitude": "",
    "Latitude": "",
    "GPSSpeed": "",
    "TripDestination": "Millennium",
    "TripStartTime": "16:02",
    "AdjustedScheduleTime": "25",
    "AdjustmentAge": "-1",
    "LastTripOfSchedule": false,
    "BusType": ""
}

I then create a template helper off this to apply the “adjustments” that OCtranspo apply that tell you when the bus is actually due. This is helpfully done by checking the AdjustmentAge is not -1, and then taking the current time and adding the AdjustedScheduleTime to it (rather than adjusting, I don’t know, the schedule).

{% if state_attr("sensor.octranspo_bus_info",
                 "AdjustmentAge") != -1 -%}
{{ states.sensor.octranspo_bus_info.last_updated + timedelta(minutes=int(state_attr("sensor.octranspo_bus_info", "AdjustedScheduleTime"))) }}
{% elif state_attr("sensor.octranspo_bus_info",
                   "TripStartTime") %}
{{ today_at(state_attr("sensor.octranspo_bus_info",
                       "TripStartTime")).isoformat() }}
{%- else -%}
unavailable
{%- endif %}

It’s still not perfect, but I think some of that is that OCtranspo is not perfect, so there’s still results of “the next bus is 5m ago” and I’m not sure if that’s updates that are too slow or because the bus is still due but the company don’t know where it is.


  1. Yes, I also added my solution to that ticket, I just wanted to keep a copy for myself. As it were. ↩︎