Location stuff - GeoCode address, Distance etc

It took me ages to figure this out by digging through the forums.
So I thought I’d leave this here for anyone else who might need it.

This does a few location type things in one go:

Sets an Address item to HOME, or to a Google GeoCode address. I’ve made it so there are only API requests when away from home and moving at least 450m since the last update.

Sets Address to OFFLINE when phone is offline, to avoid thinking data is upto date when the phone might be dead.

Sets a Distance to Home item. Useful for other rules and automations.

I still need some help with adding a GPS Speed calulation.

I have written the code below into a single .rules file in the rules folder.
You will also need to create a few items. I put them in my iPhone group.
You will need the Network binding to get the Network Presence bit to work.
You will need to setup a Google GeoCode API to make the street address bit work.
Use a change in iCloud Location to trigger the rule.

//Run GeoCode API and other geo calculations when Away from home
rule "location_dad-GEO"
when
    Item MartinsiPhone_Location changed  
then
    logInfo("Location - Dad", "Location has changed. Checking iCloud Thing Status and Home Network Presence")
    var iPhoneThingStatus = getThingStatusInfo("icloud:device:bc3dd7b8fb:3dfa568e").toString
    if (iPhoneThingStatus == "ONLINE" && MartinsiPhone_NetworkPresence_Online.state == OFF) {
        logInfo("Location - Dad", "iPhone is " + iPhoneThingStatus + " and Home Network Presence is " + MartinsiPhone_NetworkPresence_Online.state + ". Running GeoCode API script")
        val currentLocation = newState as PointType
        val lastLocation = previousState as PointType
        val distanceTraveled = currentLocation.distanceFrom(lastLocation) as Number
        logInfo("Location - Dad", "Distance from Last Location: " + distanceTraveled + " m")

        //Set Address to GeoCode if location has changed by x meters
        if(distanceTraveled > 450) { 
            val String GoogleAPIKey = "fjdkDSFG567FGhydfRbH8mfgkdhl98sS1U-N8" 
            val geocodeURL = "https://maps.googleapis.com/maps/api/geocode/json?latlng=" + currentLocation.latitude + "," + currentLocation.longitude + "&sensor=true&key=" + GoogleAPIKey
            val String gecodeResponse = sendHttpGetRequest(geocodeURL)
            val String formattedAddress = transform("JSONPATH", "$.results[3].formatted_address", gecodeResponse)
            MartinsiPhone_Address.postUpdate(formattedAddress)
            logInfo("Location - Dad", "GeoCode Address: " + formattedAddress.toString())  
        } else {
            logInfo("Location - Dad", "GeoCode didn't run. Too close to last location.")
        }
        
    //Distance from Home
    val homeLocation = new PointType(
        new DecimalType(53.5648957206535),
        new DecimalType(-6.4856214463804)
        )
    val distanceFromHome = (currentLocation.distanceFrom(homeLocation) * 0.001) as Number
    MartinsiPhone_DistanceFromHome.postUpdate(distanceFromHome)
    logInfo("Location - Dad", "Distance from home: " + distanceFromHome + " km")

    } else {
        logInfo("Location - Dad", "iPhone is " + getThingStatusInfo("icloud:device:bc3dd7b8fb:3dfa568e").toString + " and Home Network Presence is " + MartinsiPhone_NetworkPresence_Online.state + ", so GEO rule didn't run.")
    }
end

//Set to OFFLINE when device Thing is OFFLINE
rule "location_dad-Set OFFLINE"
when
    Thing "icloud:device:bc3dd7b8fb:3dfa568e" changed from ONLINE
then
    if (MartinsiPhone_NetworkPresence_Online.state == OFF) {
        MartinsiPhone_Address.postUpdate("OFFLINE")
        logInfo("Location - Dad", "iPhone is " + getThingStatusInfo("icloud:device:bc3dd7b8fb:3dfa568e").toString + ". Address set to OFFLINE")
    }                                               
end

//Set to ON when device is connected to home network
rule "location_dad-Set to HOME"
when
    Item MartinsiPhone_NetworkPresence_Online changed to ON
then
    MartinsiPhone_Address.postUpdate("HOME")
    MartinsiPhone_DistanceFromHome.postUpdate(0)
    logInfo("Location - Dad", "Home Network Presence " + newState + ". Address set to HOME")
end

I have made a UI that looks like this
image

type: script.ScriptAction

component: oh-list-item
config:
  icon: oh:man_2
  item: iPhone_Location
  title: Dad
  footer: '=(items.iPhone_Address.state != "OFFLINE" &&
    items.iPhone_Address.state != "HOME") ?
    items.iPhone_Address.state: " "'
  badge: '=(items.iPhone_NetworkPresence_Online.state == "ON") ? "HOME":
    (items.iPhone_DistanceFromHome.state < 1 &&
    items.iPhone_NetworkPresence_Online.state == "OFF" &&
    items.iPhone_Address.state != "OFFLINE") ? "< 1km AWAY":
    (items.iPhone_Address.state == "OFFLINE") ? "OFFLINE":
    items.iPhone_DistanceFromHome.displayState + " AWAY"'
  badge-color: '=(items.iPhone_NetworkPresence_Online.state == "ON") ?
    "green": (items.iPhone_Address.state == "OFFLINE") ? "black": "red"'
  action: analyzer
  actionAnalyzerItems:
    - iPhone_DistanceFromHome
    - iPhone_NetworkPresence_Online
  actionAnalyzerCoordSystem: time
3 Likes

Thanks for wrapping up.
Did you also find out how to access the system location that you define if you start OH3 from scratch?

No. Can you tell me?

No, am looking for that, too

1 Like

Thanks for posting. I’m certain this will be useful to many.

If that’s your real GoogleAPIKey I strongly recommend going and generating a new key. Even if you edit your post at this point it will be in the drafts.

Since this is a UI created rule could you click on the Code tab and post the full rule, showing the triggers as well as the Script Action? It will make the example more complete and easier for users to copy/paste into their own setup.

Looking at the code I have a few comments which might make the rule a little simpler.

  • Why pull the state as a PointType only to create a new PointType to store it. You should be able to use phoneLocation everywhere and eliminate the need for phone_location

  • I don’t have any iCloud stuff and haven’t done much work with geolocation stuff since moving to OH 3, but is there a reason you didn’t use previousState instead of the separate iPhone_LastLocation Item? That might simplify the rule over all.

  • You might have a timing issue when iPhone_LastLocation is NULL. It likely will not have changed to the current location when the next line of code runs and pulls the state from that Item. You should have a sleep there or even better, rework the if statement so you use the value posted.

var lastLocation = iPhone_LastLocation.state as PointType
  • Make sure to test for UNDEF as well as NULL.

  • Same comment for lastLocation and last_location as above. You already have the state as a PointType, there is no reason you need to create a copy in another PointType.

  • Just in case there is something I’m not seeing that does require you to create a copy of the PointTypes, use clone instead to make a copy.

    val last_location = (iPhone_LastLocation.state as PointType).clone
  • Avoid the use of primitives unless absolutely necessary in Rules DSL code. I don’t see the need for the call to intValue
1 Like

Thanks for all that.

The reason I made a new PointType is because the iPhone data has a third ‘altitude’ point which seems to screw things up. It’s been easier having just a two point location PointType.

I’ve never heard of previousState. I’ll go look it up.

the LastLocation item is only ever NULL on bootup. This is just to fill it with data really. If there were an error with timing I assume it will sort itself out on the next pass?

I don’t know what a primative is.

Sorry, I have almost no coding experience. I did my website in Wix is about the extent of it. I do this instead of Grand Theft Auto now.

1 Like

Especially when learning to code, never just copy stuff and hope it works. Go through it line by line and make sure you understand what each part of each line does and why it’s there. If you can’t figure that out, remove it and see what happens. In this case, calling intValue does absolutely nothing for you except to significantly increase the amount of time it takes to parse the code.

It’s probably better to figure out why the altitude screws things up. Maybe it’s not the altitude but something else. Perhaps there’s a bug that needs to be fixed. Maybe your approach needs to be adjusted.

Perhaps, but the line immediately following the postUpdate will fail with a big old exception in the logs because iPhone_LastLocation.state won’t be a PointType. It’s always better to avoid errors and exceptions, particularly when it’s so easy to avoid in the first place.

Also, for that matter, if iPhone_Location is ever NULL (when OH first starts) or UNDEF (if the binding can’t communicate with Apple’s server) there will be a big exception in the logs too.

1 Like

Thank you for all of this input. I’ve tried to implement as much as I can.

But I have to say that I strongly disagree with your statement about not copying stuff and hope it works. I think that is exactly how I’m able to progress with any of this. I don’t wan’t to start the age old discussion about how difficult OpenHab is to figure out, but it is.
The docs assume that I know lots of things that I don’t, and I keep finding wording that is vague or misleading. And to find answers in the mess of forums from the last 6+ years is definitely not easy.

Well, all I can say is if you practice cargo cult programming, it is exceptionally difficult if not impossible to help you when something doesn’t just work after you copy it. You don’t understand it enough to even ask a proper question.

Notice I didn’t say don’t copy rules from the forum or elsewhere. What I said is don’t copy without making sure you understand how it works.

If you don’t spend at least an effort to try and understand your own code, you’ll soon find no one will be around to help you when things go wrong.

I don’t know what cargo cult programming is. You have just shown a perfect example of what OpenHab is like for everyone except for the elite programmers who have been working on this for years.
I’m doing my best here. I think what I’ve done above is pretty good for someone that’s never programmed before, and just trying things to see what happens is the only way I could figure any of this out. If people ask questions in this forum before trying it for themselves, people complain that they should have tried harder…

I heard a great explaination once about how Science is often like trying to figure out how to build a piano, only by hearing the sound of one falling down the stairs. Seems relevant to my OpenHAB experience.

Still fun though!

The key quote:

The term cargo cult programmer may apply when anyone inexperienced with the problem at hand copies some program code from one place to another with little understanding of how it works or whether it is required.

Yes you did. But you seem to be missing my whole point. Let’s say someone copies some code from a random post on the forum. It doesn’t work.

  1. The cargo cult programmer will abandon the code and look for some other example of code. Failing that they will post a “do it for me” post on the forum, often without providing near enough information to do it for them. And even if we do it for them, there is never any gratitude, just complaints that OH and programming are too hard. Eventually they abandon OH, often leaving an FU type post on their way out the door.

  2. The learner will try to understand the code they copied. What does it do? What does it use a received command instead of received update as the trigger? What does that weird line with the ? mean? What happens when I change this line to something else? Let’s add some logging so we can see the steps taken by the rule. What breaks when I remove this line over here? These are the sorts of things the learner will ask and try to answer. They may not be successful. They may not be able to figure it out for themselves. And that’s OK. We don’t expect everyone to be able to figure everything out on their own. But when these users post for help, half a dozen of us will jump on the chance to help. In fact, most of use will end up doing it for them because we know that this isn’t a hole that we’ll be pouring out precious time into. We know that doing it for them becomes a teaching/learning opportunity.

We only complain about the first type of user on this forum. But if you are the second type, you have to tell us by telling us what you’ve already tried. Otherwise we assume you are a type 1.

I doubt you are the first type. But if you continue down the path of “I don’t know what that does and I don’t care as long as it works” you will become a type 1.

1 Like

I tried experiementing with previousState() but not having much luck.
This is the closest to right I can think of, but it’s not working. Can you point me in the right direction?

//Distance travelled since last update
  val distance_travelled = (new PointType (iPhone_Location.state).distanceFrom(new PointType (iPhone_Location.previousState().state))
  logInfo("Location", "Distance from Last Location: " + distance_travelled.toString() + "m")

Also, I tried removing the .toInt() you mentioned, but the script fails and gives the error:

Ambiguous binary operation.

The operator declarations

operator_greaterThan(Type, Number) in NumberExtensions and

operator_greaterThan(Number, Number) in NumberExtensions

both match.; line 41, column 1919, length 1

This is refering to the next line which uses this distance:

if (distance_travelled > 400 && iPhone_NetworkPresence_Online.state == OFF){

Can you help me understand why?

What does it do that you weren’t expecting? What doesn’t it do that you did expect? You looked in your openhab.log for clues?

val distance_travelled = (new PointType (iPhone_Location.state).distanceFrom(new PointType (iPhone_Location.previousState().state))

This is rather complicated. Which bit might be going wrong? Hard to tell. Why not do one thing at a time, and see what each individual step is doing?

val current = iPhone_Location.state
logInfo("test", "current loc " + current.toString)

val old = iPhone_Location.previousState()
logInfo("test", "old loc " + old.state.toString)
logInfo("test", "from " + old.timestamp.toString)
val oldPoint = new PointType (old.state)
logInfo("test", "old point " + oldPoint.toString)

val distance_travelled = new PointType current.distanceFrom(oldPoint)

I expect that last to blow up really, trying to make a new PointType out of a distance result doesn’t make sense.

1 Like

ok, I got it functioning again. I have updated my original post with what is working.
The only change is
val PointType lastLocation = (MartinsiPhone_Location.previousState().state) as PointType

It still needs the intValue() to work though.

Markus, please post a fresh topic if you found out how to set OH3 system home location. Iam using OH3 in my RV and boat and would be very usefull for bindings like weather etc when changing location.

I’m not sure it can work like that. Astro and Weather etc. create Things derived from system location using the co-ords of the moment. Then, never look at location again. Updating system location, if there were a way without restarting, won’t affect what the bindings do.

You’d have to get all the required bindings reworked to use some special dynamic system property instead of fixed locations, and then add events to indicate if that location changed, and add binding functions about what to do when changed, and some tool to do the changing.
None of that is impossible, but it’s quite a bit of work in a number of different places.

Most of that could be simulated with rules and maintaining your own “master location” Item. You’d “just” need to able to edit/replace the preconfigured Things by rule, and have the bindings respond to such changes.

I think you misunderstood what I meant by using previousState. I was not referring to using persistence. In fact, that has a real chance of failing due to timing when used like this because it takes some time for Persistence to save the value to the database.

What I meant was to use the implicit variable that is populated for all rules that trigger with a changed trigger. See Rules | openHAB. Instead of MartinsiPhone_Location.previousState().state you can just use previousState. That variable is populated with the state of the Item that it changed from. No need to go back to a database or do anything else complicated or time consuming. It’s just there ready to be used.

OK.
I think I understand, but I think this is circling back around to the problem of the iPhone data having three points.
I can’t make any of the distance or geocoding stuff work unless I extract the two lat and long points as I have done in my example. Although I seem to be able to call MartinsiPhone_Location.previousState and get a readout in my log, I can’t seem to extract the two lat long points from this.
So I can’t do calculations using three point locations, and I can’t extract the individual points from previousState.

What you do with the Points doesn’t change. My point is that MartinsiPhone_Location.previousState().state and previousState should give you the exact same thing. However, due to timing issues with saving the value to the database and retrieving it, previousState will always work and MartinsiPhone_Location.previousState().state will fail sometimes.

Note, it’s not MartinsiPhone_Location.previousState, it’s literally just previousState.

Though I do now notice that you have two triggers for the rule so that won’t work in the case where the rule is triggered by iPhone_NetworkPresence_Online. So you would either have to check for which Item triggered the rule (which is easy enough using another implicit variable triggeringItemName) or just live with the fact that this rule will fail or, more correctly stated, come up with the wrong answer periodically based on timing.

I did some looking. distanceFrom returns a DecimalType, not a Number like I expected. Therefore a better approach would be to use as Number instead of intValue() in Rules DSL. The use of primitives can have a significant impact on the performance of Rules DSL and it’s going to try really hard to convert them back to a Number anyway. May as well leave it as a Number to begin with.

I’m not sure I understand how to write this then. Can you show me an example?

I’m watching the rule trigger in the logs and it always seems to work. Shortest iCloud update is every 5 mins. So presumably timing isn’t an issue?