Tracking the trains coming to your station using the CTA API

There is already a great CTA Bus Tracker for those of us lucky enough to live in Chicago and use public transit. I was inspired by this, and the soon-to-arrive renaissance of the after-times to make a train tracker.

I wanted something that would tell me the times to arrival for the next few trains at my nearby Red Line stop. I also made a map that shows me where these trains are, and an alert to warn me if there are any disruptions on that train line. The API supplied by CTA is rich in information, free and well documented. What I used here should work for any other line, too. Ideally someone who programs in python could turn it into an integration. Anyway, I had to learn lots of new details, which was fun. Thought I would save somebody some time by sharing what works.

Let me break down the steps.

1. Apply for an API key.
That can be done for free here. It took less than a minute to receive the key necessary to use the API. While you are waiting, look up the stpid for your platform here. Scroll down to the table labeled Individual Stop IDs Quick Reference and grab the code in the first column corresponding to your station and direction of travel. Once the key has arrived by email and you have the ID for your stop, make a call to the API from your browser (Firefox is particularly helpful):
http://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key=xxxxx&stpid=yyyyy&max=5&outputType=JSON
where you can put in your emailed key for xxxxx and your stop ID for yyyyy. You should see lots of information about the trains approaching your stop. There is no use continuing if you don’t see this JSON code.

2. Make a sensor template to download all the information from the API:

- platform: rest
  resource: http://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key=xxxxx&stpid=yyyyy&max=5&outputType=JSON
  name: Mystation South Trains
  value_template: "OK"
  json_attributes:
    - ctatt
  scan_interval: 20

A few things to note. I called this new entity Mystation South Trains, but you be you. You need that line with value_template: "OK" because the JSON code returned has more than 255 characters. The saved attribute, ctatt is actually the mother of all data, so you will be saving everything this way, meaning that you can make one call to the API and get all incoming trains (maximum 5 set here). Finally, I set the scan interval to 20seconds because the CTA is very generous with the number of calls allowed—50,000 per day! I will probably shut off this call at night so as not to spam them. It would be a good idea to check your configuration, and restart HA. Then go to the Develop Tools and see if you have sensor.mystation_south_trains. The state should just be “OK”, but the state attributes should be rich with data.

3. Make a sensor template to extract out the arrival time for a train.
I will show just the first train, but this code can easily be copied, pasted, and modified to get subsequent trains. Just change .eta[0] to .eta[1] for the second train, etc. There are two sensors here, one to get the time of arrival, and the next to calculate the number of minutes until arrival. For the second sensor to work, you need to have the time and date integration.

- platform: template
  sensors:
    first_red_south_train_time:
      friendly_name: "Red Line first southbound train arrival time"
      value_template: '{{ states.sensor.mystation_south_trains.attributes["ctatt"].eta[0]["arrT"] }}'
    first_red_south_train_time_left:
      friendly_name: "Red Line first southbound train time left to arrival"
      value_template: > 
        {% if is_state('sensor.first_red_south_train_time','unavailable')  %}
          N/A
        {% else %}
          {{ ((as_timestamp(states('sensor.first_red_south_train_time'))|float 
          - as_timestamp(states('sensor.date_time_iso'))|float) / 60) |float| round(1) }}
        {% endif %}
      unit_of_measurement: minutes

I don’t know why, but it is a bit sensitive in how exactly you extract out the information from the JSON (what type of quotes where, when you use states() vs. states., etc), this is the only one that worked for me. It gives the minutes in decimals. If you don’t like tenths of minutes you can change it to round(0), or don’t divide by 60 and make unit_of_measurement: seconds. At this point, you should check you configuration again, restart HA and see if you have these sensors using the Developer’s Tools. If it is working, you can put them in any card that you like.

4. If you want to put the trains on a map.
Then you need to create a new sensor template, some automations, and make sure that you have device_tracker:in your configuration YAML. These two new sensor templates extract the longitude and latitude for the first train incoming:

- platform: template
  sensors:
    first_red_south_train_longitude:
      friendly_name: "Red Line first southbound train longitude"
      value_template:  '{{ states.sensor.mystation_south_trains.attributes["ctatt"].eta[0]["lon"] }}'
    first_red_south_train_latitude:
      friendly_name: "Red Line first southbound train latitude"
      value_template:  '{{ states.sensor.mystation_south_trains.attributes["ctatt"].eta[0]["lat"] }}'

You could also just delete the first two lines and put these just below the template sensors above. To be safe you could again check your configuration and restart HA, and go to Developer Tools to make sure that you have two new sensors sensor.first_red_south_train_longitude and sensor.first_red_south_train_latitude. The automation creates a device_tracker using the service device_tracker.see. I will show three trains being tracked. This is because they have to be added in this way, or it fails for some reason.

- alias: first_red_south_train_location
  trigger:
    platform: state
    entity_id:
      - sensor.first_red_south_train_latitude
      - sensor.first_red_south_train_longitude
      - sensor.second_red_south_train_latitude
      - sensor.second_red_south_train_longitude
      - sensor.third_red_south_train_latitude
      - sensor.third_red_south_train_longitude
  action:
    - service: device_tracker.see
      data_template:
        dev_id: first_red_south_train_x
        gps:
          - "{{ states('sensor.first_red_south_train_latitude') }}"
          - "{{ states('sensor.first_red_south_train_longitude') }}"  
    - service: device_tracker.see
      data_template:
        dev_id: second_red_south_train_x
        gps:
          - "{{ states('sensor.second_red_south_train_latitude') }}"
          - "{{ states('sensor.second_red_south_train_longitude') }}"
    - service: device_tracker.see
      data_template:
        dev_id: third_red_south_train_x
        gps:
          - "{{ states('sensor.third_red_south_train_latitude') }}"
          - "{{ states('sensor.third_red_south_train_longitude') }}"

This will give you device trackers that show up in a Map card. It is modified whenever any of the coordinates of any of the three trains changes. If you have only one train, it should be clear what to delete. It might not be useful, but it looks cool.

5. Use the API to put an alert on your dashboard.
Forgot to put this here the first time. Just need two template sensors, no API key required:

- platform: rest
  resource: https://www.transitchicago.com/api/1.0/routes.aspx?routeid=red&outputType=JSON
  name: Redline Alerts
  value_template: "OK"
  json_attributes:
    - CTARoutes
- platform: template
  sensors:
    redline_status:
      friendly_name: Red Line status
      value_template: '{{ states.sensor.redline_alerts.attributes["CTARoutes"].RouteInfo.RouteStatus }}'

I also added a custom button that flashes on my dashboard only if there are delays. When you click on it, it takes you to the CTA website that has more info. In lovelace:

  - type: conditional
    conditions:
      - entity: sensor.redline_status
        state_not: Normal Service
    card:
      type: 'custom:button-card'
      tap_action:
        action: url
        url_path: 'https://www.transitchicago.com/redline/#alerts'
      entity: sensor.redline_status
      name: RedLine
      icon: 'mdi:subway-variant'
      template: alert
      styles:
        icon:
          - animation:
              - blink 2s ease infinite
      show_state: true

where I used a template for the page:

  alert:
    show_state: true
    styles:
      card:
        - background-color: 'rgb(3, 169, 244)'
        - boxshadow: none;
        - overflow: unset
      icon:
        - color: yellow
        - animation:
            - blink 2s ease infinite
      name:
        - color: yellow
      state:
        - color: yellow

However, there is some bug here, since I shouldn’t need to add that blinking bit twice.

6. What else?
If you don’t live in Chicago, it might still be useful since CTA tries to conform to some Google standard. In case you are not aware, there is an integration with Here, to estimate your travel times between two points, like work and home, using public transit (and biking and driving). You could also use the mapid to get trains going both directions, but then the JSON will be different and you will have to figure out how to modify this. Some day this will be fed into some AI that tracks all my movement, and start telling me what I should be doing in the morning. Maybe.

This can probably all be done more efficiently, so suggestions are welcome in the comments. Also ideas about how to expand this would be great. Hope it helps someone!

4 Likes

Just added this, still works great. Much appreciated.

FWIW, if anyone ends up here, I’ve forked the CTABusTracker integration and added train support to it.

Currently available only in my forked repo.

@smcpeck Any chance you can take a look at your integration? I cannot get it to show trains and bus sensors simultaneously… in whatever order I place the sensors it will only show whichever I display second, trains or buses. If you could show a working example of both I’d appreciate it. Thanks.