Folks,
I’ve long wanted to display the local train times departing from my station. If I had this info in oH then I could a) display it as I wanted, b) create alarms and get Alexa to tell me that I need to leave in 3,2,1 or I will miss my train.
A screenprint of the HabPanel widget
So, a quick scratch around the 'net for the UK transportation API and a couple of hours writing a rule to pull it in, I now have what I want.
There is an accompanying widget listed to display the times so, if you wanted to, you could use that too.
Basic Operation
- It uses the transportAPI.com API to get the train departure information.
- You need to signup for a free key. Free allows 1000 requests a day
- You can display 1-4 train times. Simply create items for each one, add it to the group, and the code works out the rest.
- You need the 3-letter code for your local station as well as the destination station. These can be obtained from NationalRail.co.uk
- I’ve tested this with a single train from my station to one of the main London stations. Not sure if there are any cases where the code will not work. I’ll happily help solve problems.
- It’s a cron-based rule which fires every 5 minutes. There are 1440 minutes in a day, you have 1000 hits a day limit. Don’t overshoot it.
This needs the json binding installed into oH
This runs on oH 2.4 and probably 2.3. Don’t know about any others.
I run this on a RPi 3B+. No idea if it will run on Windows install.
Your items have to follow a pattern the code expects.
Items:
//leave these alone
Group gTrainCollection
Group gTrainCollectionItems
/// stop leaving alone
//You can have up to 4 sets below. The format must be:
//TrainDept + Your Station Code + 1/2/3/4 + Whatever is below.
String TrainDeptName (gTrainCollectionItems)
Switch TrainTimesRefresh "Refresh Train Times"
String TrainDept1 (gTrainCollection)
DateTime TrainDept1TimeExpected (gTrainCollectionItems)
String TrainDept1Status (gTrainCollectionItems)
Number TrainDept1Until (gTrainCollectionItems)
String TrainDept2 (gTrainCollection)
DateTime TrainDept2TimeExpected (gTrainCollectionItems)
String TrainDept2Status (gTrainCollectionItems)
Number TrainDept2Until (gTrainCollectionItems)
String TrainDept3 (gTrainCollection)
DateTime TrainDept3TimeExpected (gTrainCollectionItems)
String TrainDept3Status (gTrainCollectionItems)
Number TrainDept3Until (gTrainCollectionItems)
String TrainDept4 (gTrainCollection)
DateTime TrainDept4TimeExpected (gTrainCollectionItems)
String TrainDept4Status (gTrainCollectionItems)
Number TrainDept4Until (gTrainCollectionItems)
If you have only one, it will only display the next train in the widget. If you have all 4 it will populate all 4. Up to you.
Rules:
val org.eclipse.xtext.xbase.lib.Functions$Function2<DateTimeType, NumberItem, Boolean> minutesUntil = [ DateTimeType dt, NumberItem item |
if (dt === null){
item.postUpdate(-1)
return true;
}
val DateTime now2 = parse(now.toString("1970-01-01'T'HH:mm:ss"))
val DateTime dtTime = parse(dt.toString())
if (dtTime.getHourOfDay < now.getHourOfDay){
dt.plusDays(1)
}
val f1 = (dt.zonedDateTime.toInstant.toEpochMilli - now2.millis) / 1000 / 60
item.postUpdate(f1.toString)
return true;
]
val String STA_ORIGIN = "PAD" //this is the station where the train starts. This is often going to be before your station.
val String STA_YOUR_DEST = "SWI" //this is the station your going to.
val String STA_YOUR = "PAD" //this is your station where you get on the train
val String API_KEY = "<API KEY>"
val String APP_ID = "<APP ID>"
//do not change
val BASE_URL = "https://transportapi.com/v3/uk/train/station/"
rule "Transport.GetTrainTimes"
when
Item TrainTimesRefresh changed to ON
then
//logDebug("Transport.GetTrainTimes", "GetTrainTimes - Starting")
val StringBuilder sURL = new StringBuilder
sURL.append(BASE_URL)
sURL.append(STA_YOUR)
sURL.append("/")
sURL.append("live.json?")
sURL.append("app_id=" + APP_ID)
sURL.append("&app_key=" + API_KEY)
sURL.append("&calling_at=")
sURL.append(STA_YOUR_DEST)
sURL.append("&origin=")
sURL.append(STA_ORIGIN)
sURL.append("&train_status=passenger")
sURL.append("&darwin=true")
var httpResponse = ""
logWarn("F", sURL.toString)
try{
httpResponse = sendHttpGetRequest(sURL.toString, 50000)
if (httpResponse === null || httpResponse == ""){
throw new Exception("Invalid HTTP response from call.")
}
}
catch(Exception t) {
for (var int j = gTrainCollection?.members.length; j > 0;j--){
val itemStatus = (gTrainCollectionItems?.members.findFirst[ t | ] as StringItem)
postUpdate("TrainDept" + j.toString + "Status", "ERROR")
}
logError("Transport.GetTrainTimes", t.getMessage())
TrainTimesRefresh.sendCommand(OFF)
return;
}
val numDept = Integer::parseInt(transform("JSONPATH", "$.departures.all.length()", httpResponse))
var stationName = "UNKNOWN"
var itemStationName = (gTrainCollectionItems?.members.findFirst[ t | t.name == "TrainDeptName" ] as StringItem)
if (itemStationName !== null){
stationName = transform("JSONPATH", "$.station_name", httpResponse)
if (stationName === null){
stationName = "Unknown"
logWarn("Transport.GetTrainTimes", "message")
}
itemStationName.postUpdate(stationName)
}
for (var int j = gTrainCollection?.members.length; j > numDept;j--){
val itemStatus = (gTrainCollectionItems?.members.findFirst[ t | t.name == "TrainDept" + j.toString + "Status" ] as StringItem)
itemStatus.sendCommand("--")
}
for (var int i = 0;i <= numDept - 1;i++){
val itemExpected = (gTrainCollectionItems?.members.findFirst[ t | t.name == "TrainDept" + (i+1).toString + "TimeExpected" ] as DateTimeItem)
val itemStatus = (gTrainCollectionItems?.members.findFirst[ t | t.name == "TrainDept" + (i+1).toString + "Status" ] as StringItem)
val itemUntil = (gTrainCollectionItems?.members.findFirst[ t | t.name == "TrainDept" + (i+1).toString + "Until" ] as NumberItem)
if (itemExpected !== null){
var String sDepart = transform( "JSONPATH", "$.departures.all[" + i.toString() + "].expected_departure_time", httpResponse)
var DateTimeType dtDepart
if (sDepart === null || sDepart == "" || sDepart == "null"){
logWarn("Transport.Trains", "Unable to determine departure time for " + " - " + (i+1).toString + ". Ignoring")
}
else {
//if null then it's probably because it's cancelled or something.
dtDepart = new DateTimeType(sDepart)
}
minutesUntil.apply(dtDepart, itemUntil)
var status = transform( "JSONPATH", "$.departures.all[" + i.toString() + "].status", httpResponse )
if (status == "STARTS HERE"){status = "ON TIME"}
itemExpected.postUpdate(dtDepart)
itemStatus.postUpdate(status)
logWarn("Transport.GetTrainTimes", stationName + " - " + itemStatus.state + " - " + dtDepart + " - " + status + " - " + itemUntil.state.toString)
}
}
// logWarn("LLL", "6")
TrainTimesRefresh.sendCommand(OFF)
end
TODO
The items are set in such a way that you can fave multiple stations. I did not code this into the rule as I don’t need it for now.
I want to try my hand at writing a binding for this instead of the rule. It will be a a while before I get the dev environment setup so this is my quick-fix.
I welcome any feedback on this. What could I have done better etc. Simpler ways to get stuff done etc.
Hope someone else can make use of this
Update:20/10/2019
- Changed the widget. This rule will not work with correctly unless you’ve updated the widget too.
- Check your original Items. You no longer need TrainDeptXXX3TimeAimed
- Notice that the Item names NO LONGER have the XXX station name in them. This is not needed and your items must be updated.
- You can now set the API too correctly follow the train route.
- There is now a Refresh button (controlled through config) which will allow you to force a refresh.
- Changed sendCommand to postUpdate
- Multiple refactoring to improve the way the code is run.
- -I have noticed this rule takes longer to compile that I seem to remember. I will refactor it a bit to try work out why. I know the primitive issue with DSL. Might be, might not be. Functionally, it works.
Update:18/11/2019
- Removed depreciated calendar code. Removes compile warnings.
C