A weekday, start to finish
Good Morning Dashboard
My partner rides two Regional Rail lines to work: one into Center City, and a different one back out. This is what the house tells them along the way: a glance at the kitchen display in the morning, and a push on the phone before heading home.
6:40 AM
Tuesday, March 3
6:40
No notifications
Morning trip plan
Why I built it
A trip the SEPTA app can't plan
Every Regional Rail line runs through Center City, so you can ride any line in and a different line back out. The SEPTA app plans one line at a time. My partner's commute needs two, both ways.
More
It's a small FastAPI service on the same Raspberry Pi as the theater. Home Assistant asks it for the latest every 30 seconds and shows the answer on a wall tablet in the kitchen. A scheduler inside the service sends the afternoon pushes through ntfy.
Chapter 1 of 5
Two lines, one trip
It plans each leg on its own, then pairs them with the earliest connection you can still make once live delays are counted. You choose where to change trains: try it under the screens.
Try it
Now it gets in 3 minutes after the connection is due out. That can still work, so the card warns instead of giving up.
Seven minutes too late. It moves to the next connection and shows the new arrival time.
More
If SEPTA lists a direct train, it uses that first. The transfer station is a setting, so it works for any two lines that meet in Center City. If no connection works there, it tries the other Center City stations and says where to change instead. The display, the push and the follow-ups all read the same plan, so they never disagree. See the code
Chapter 2 of 5
Leave by, not departs
The display shows when to walk out the door. Leave-by counts the walk to the station, and it moves when the train does.
Try it
Five more minutes at home. It still makes the same connection, so the arrival time doesn't change.
Within three minutes of leave-by, the card turns red and says so.
More
The train's live position comes from SEPTA's TrainView feed, so a train sitting at the same stop for too long shows up as stalled before SEPTA's delay number catches up. The card only shows on weekdays during commute hours; the rest of the time it's the weather.
Chapter 3 of 5
The ride home comes to you
Nobody's in the kitchen at the end of the workday, so the afternoon answer goes to the phone: one push at 4:00 with the train home.
Try it
The train to catch, when it gets home, the next trains that still connect, and rain on the way, before anyone has to go looking.
More
The evening trip is the morning one in reverse, on the other line first. Without GPS it can't know which train my partner actually makes it to, so the push lists up to two later trains too, but only ones whose connection clearly works. See the code
Chapter 4 of 5
Only buzz when it's worse
After the 4:00 push, it checks again every five minutes, even after my partner boards, until the connecting train leaves. It only pushes again if the news got worse.
Try it
Late is worse than "looks good", so it buzzes.
Same news, two minutes more: the display updates, and the phone stays quiet.
Twenty minutes late misses the connection. That's worse, so it buzzes with the next connection and the new arrival time.
More
Every kind of push has a rank, from "looks good" up to cancelled and service alerts. A follow-up goes out when the rank goes up, or when the same kind of delay grows by another 10 minutes. Once the first train leaves, SEPTA's trip planner stops listing it, so the follow-ups read its live delay from SEPTA's train positions instead. A late first train that still makes its connection doesn't buzz at all. See the code
Chapter 5 of 5
Silence is never "all good"
A push that never arrives looks exactly like a push saying everything's fine. So when something breaks, it still says something.
Try it
It uses the last good answer from a few minutes ago, and says so.
Even a crash ends in a push: "Check SEPTA manually."
More
SEPTA, the live train positions, the service alerts and the weather each keep their last good answer. If a check fails, the old answer is used and flagged, on the display and in the push. And SEPTA's delay text is free-form ("On time", "5 min", "Suspended"), so text it can't read is never treated as on time. The full story
Summary
What I learned
- Connections
- Pairing two lines is the hard part: the right connection depends on the schedule, the station and live delays.
- Real APIs
- SEPTA's has quirks you only find by using it: the result count is
req3, and the parameter that looks right,top, is ignored. - Glanceable
- One line that answers "when do I leave?" beats a screen of train times.
- Quiet
- A push that buzzes for every minute gets ignored, so each one has to be worth it.
- Polite
- Answers under a minute old are reused, and nothing calls SEPTA outside commute hours: about 1,500 calls a weekday instead of 4,300.
- Fail loud
- When it can't tell, it says so instead of staying quiet.
- Tests
- Over 140 tests run on every commit, including one for each bug that made it to a real commute.
Most mornings nobody thinks about it, and that's the point. The code is on GitHub.
The lines and stations are real SEPTA ones, but not the ones my partner rides. Train numbers and times are made up. The cards, pushes and connection rules follow the real code in Good_Morning_Dashboard.