GPSTracker + Google Map

By default the Mapview element in OH only supports showing one location at a time. If you are tracking multiple people you need more than one Mapview. This tutorial is based on Google Map and most of the credit goes to @patrik_gfeller . All I’ve done is update it to use the GPSTracker binding instead of MQTT and Scripted Automation using Python for the Rules. I’ve also not implemented the geocoding rules as Google now charges $5 per 1k requests a month. It would not be difficult to implement if you want to reproduce that feature.

This tutorial will result in Google Maps in a custom webview that shows the current location and all the past locations for the past 24 hours for all your configured trackers.

Preconditions

  • You have GPSTracker installed and configured correctly. See the binding read me for details.
  • You have OwnTracks or GPSLogger installed on your phones and reporting to OH.
  • You have Things created for each of your reporting phones and the Channels linked to Items.

Get your Google Maps API Key

Navigate to https://developers.google.com/maps/documentation/embed/start and select “Get an API key” on the left and follow the instructions. You will need to create a billing account with Google but the API.

Be sure to select the Maps JS API. Copy the API key, you will need it later.

Items

The map will show your most recently reported locations, all the past reported locations, and static locations. But all the information it displays comes from Items. The following Items are needed for each user:

  • Location: last reported location
  • Location_History: a String listing each lat,lon reported since midnight of the format <lat>,<lon>;<lat>,<lon>;
  • An Item for all static locations (e.g. home)
Group:String LocationHistories // All the Location_History Items 
Group:Location Locations       // All the Location Items
Location Home "Home Location"  // Lat,Lon for your home

Location Person1Location (Locations) { channel="gpstracker:tracker:P1:lastLocation" } 
String Person1LocationHistory (LocationHistories)

Location Person2Location (Locations) { channel="gpstracker:tracker:P2:lastLocation" }
String Person2LocationHistory (LocationHistories)

Use appropriate Item names and the correct Thing IDs in the Channel Links.

Rules

Because the GPSTracker binding does so much for you, the Rules are very simple. Please read the comments for details.

@rule("Initialize Maps Items",
      description="Initialize Items for Google Maps Webview",
      tags=["location"])
@when("System started")
def loc_init(event):
    """
    Initialize the static location Items. If you use restoreOnStartup on these Items,
    this Rule can be removed after the first time it runs. Alternatively you can set
    the state of the static Items using the REST API instead.
    """
    events.postUpdate("Home", "<HOME LAT>,<HOME LON>")

@rule("Clear Histories",
      description="Clears location histories at midnight",
      tags=["location"])
@when("Time cron 0 0 0 ? * * *")
def clear_hist(event):
    """
    Clears the history Items at midnight, resetting them to the current location.
    The LocationHistories Items must have at least one value so if the current
    state is NULL or UNDEF than initialize the history with the Home location.
    """
    for loc in ir.getItem("LocationHistories").members:
        clear_hist.log.info("Resetting {}:{}".format(loc.name, items[loc.name]))
        locItem = loc.name[:-8]
        init = items[locItem]
        if isinstance(init, UnDefType):
            init = items["vHome"]
            events.postUpdate(locItem, str(items["Home"]))
        events.postUpdate(loc.name, "{}".format(init))

@rule("Location Changed",
      description="Updates location history",
      tags=["location"])
@when("Member of Locations changed")
def loc_changed(event):
    """
    Updates the location history Item with the newly reported location.
    """
    if not isinstance(event.itemState, UnDefType):
        hist_item = "{}_History".format(event.itemName)
        events.postUpdate(hist_item, "{}{};".format(items[hist_item], event.itemState))

See the original posting if you want to implement geocoding to get the current address of the user which would be another simple Rule to request the address using an sendHttpGetRequest call and updating an Item with the result.

Webview

Download, change the name and unzip map.zip.txt from Google Map. Unzip this folder to $OH_CONF/html. Make the following edits:

map/index.html

  • Change the lang attribute to the top if “de” isn’t appropriate for you.

  • Change the map20.js to map21.js or alternatively change the name of the file in map/lib.

  • Change the error message “Positionsdaten sind nicht geladen.” to what’s appropriate in your language. English is “Position data is not loaded.”

  • Insert your API Key where indicated at the bottom of the file and change the end of the URL to initialize instead of map.initialize.

lib/map21.js

Populate the openHABLocations with your Items. The “Home” section shows how to display a static location. The “Patrik” and “Karin” sections need to be updated with the names and Items defined above.

If you don’t want to show the history for one or more people, you can turn that off by setting the “historyItem” and “historyIcon” to “undefined”. In that case you can eliminate the Rules above as well.

images

You will find the example images in map/img. The default sets are pretty generic and usable but if you have more than two people to track or more static locations to define you will want to add and customize. One idea includes using the person’s face for the location icon.

Experience

The resulting webview is quite nice and easy to use on the web page. However, it’s all but impossible to scroll around the map when viewing the webview in the Android app when showing a sitemap. I’ve not yet experimented with HABPanel views, perhaps they work better.

Good luck!

5 Likes

Thanks Rich, another bookmark, :grin: will have to give this a try once I get the Scripted Automation stuff installed on my system.

You can go to the original posting and see Patrik’s original Rules DSL version.

The rules in python do not run on OH3.2M2
Can someone share the ECMAScript equivalent of below Python rules?
Some help would be much appreciated.

@rule("Clear Histories",
      description="Clears location histories at midnight",
      tags=["location"])
@when("Time cron 0 0 0 ? * * *")
def clear_hist(event):
    """
    Clears the history Items at midnight, resetting them to the current location.
    The LocationHistories Items must have at least one value so if the current
    state is NULL or UNDEF than initialize the history with the Home location.
    """
    for loc in ir.getItem("LocationHistories").members:
        clear_hist.log.info("Resetting {}:{}".format(loc.name, items[loc.name]))
        locItem = loc.name[:-8]
        init = items[locItem]
        if isinstance(init, UnDefType):
            init = items["vHome"]
            events.postUpdate(locItem, str(items["Home"]))
        events.postUpdate(loc.name, "{}".format(init))

@rule("Location Changed",
      description="Updates location history",
      tags=["location"])
@when("Member of Locations changed")
def loc_changed(event):
    """
    Updates the location history Item with the newly reported location.
    """
    if not isinstance(event.itemState, UnDefType):
        hist_item = "{}_History".format(event.itemName)
        events.postUpdate(hist_item, "{}{};".format(items[hist_item], event.itemState))

@rlkoshak

Hi
I have tried this and it works perfect :slight_smile:

I have one question - the markers, are they suppose to be updated without refreshing?

Best
Nanna

No, the way this works they only get updated when the map first loads. There must be a refresh to get new markers.

Ok - then it works the way its supposed to, thanks @rlkoshak

Anyone know if this works for openHAB 3.3? I just get “Position data is not loaded.” I set openHAB to debug and I don’t see any rest/items API hits even trying to pull my location. I do see it pull map21.js and openHAB.js.

Is there a better way to debug this, or is debug in openhab the only way?

It’s been quite some time since I’ve used this. Since COVID the family is basically home all the time so I never rewrote this for OH 3 as it was pointless for us.

That error means either the raw Location Items are not changing or the rules are not running to populate the PersonXLocationHistory Items.

Pay attention to the prerequisites. You have to set up GPSTracker as well as a GPS tracking app on your phone. It should work with the iCloud binding too if Apple is your thing. All the rules and map page care about are the Location Items changing, regardless of how. But the PersonXLocation Items have to be changing. It’s the changes that drive the rules.

The position Channels from the GPSTracker Things must be updating with your location. Setting that up is all a prerequisite and outside the scope of this tutorial. Nothing here is going to work until you have that part working. If you see your Location Items updating in events.log, we can go from there.

Additionally, the rules as written use the Jython add-on working in OH 3 and have the helper library installed and configured. But with all that the rule should work as written. They are pretty simple rules that mainly do some string manipulation on a String Item.

I’ve not tried to use this embedded map on MainUI. I see no reason for it not to work but I’ve never tried it.

It probably would be better to rework this whole tutorial to use the map screen built into OH anyway and the rules could be rewritten as a rule template. But I have bigger fish to fry at the moment. If someone decides to take that on, I’ll happily update the OP with an updated version.

Were I to write this for OH 3 from scratch I’d use a collection of Location Items with Expire configured to time them out after an hour or so. These would all be put on the MainUI Map page. When the Item linked to the. GPSTracker changes there’d be a rule to update all the history Items. The Items themselves could be created by the rule even.

From a performance perspective this will work a whole lot better than the above. I found that if I took even a short road trip it would take the map a long time to render all the little points since it keeps them for a whole day before refreshing. So instead it’d keep just the most recent hour’s worth of locations or, if I’m moving a lot, the most recent ten (or what ever makes sense) locations.

It also gets rid of the .js files and the custom web page that needs to be loaded into a webview. It’s all straight forward Items and Groups.

I am back to trying to get this to work, yes I have locations for my iPhones, and if I look in the Chrome console, I see map21.js pulling valid locations for the 3 locations. I see no errors in console, just the browser “Position data is not loaded.” error.

I would be happy to $$ sponsor a rewrite like @rlkoshak suggested if anyone is interested.

OK, so the above tutorial is probably not viable any longer. Besides the fact that it’s overly complicated I think the Google Maps API has changed and it won’t work as written. I have it on my list to create a full tutorial on how to do this in OH 3/4 in a more streamlined manner. In the mean time this outline should be enough for most to figure it out.

  1. In MainUI create a Map Page. Configure it as desired with a name, id, zoom level and center point desired. Remember the id as you’ll need it later.
  2. Create markers for any static locations and for the PersonXLocation Items.
  3. Save the page.
  4. If you are fine with keeping it as a separate Map page in MainUI you are done. If you want to include this on a HABPanel, MainUI Layout Page, or a sitemap, use a Webview element/widget using /page/<id> where <id> is the ID for the page you created in step 1. It’s a relative URL so it will work no matter how you get to the UI.
  5. When you load the page/sitemap, click the little pin icon to collapse the side bar menu on the left.

The above doesn’t capture history though. Unfortunately there isn’t a good way right now to add historical points to the map. One approach would be to predefine a fixed number of Items for the history (e.g. 10) and create a rule to rotate the states of those Items out. Put those Items into a Group and make their names sortable (e.g. by appending _01, _02, and so on to the Item name). It would look something like this in a JS Scripting Script Action.

items.Person1History.members
                    // Get the Item names
                    .map((i) => i.name)
                    // Sort the names alphabetically
                    .sort()
                    // Reverse the order so the Item holding the oldest value is first
                    .reverse()
                    // rotate the values
                    .forEach((name, i, members) => {
                      if(i == members.length-1) items[name].postUpdate(event.oldItemState.toString()); // set the 01 Item to the previous state
                      else items[name].postUpdate(items[members[i-1]].state);
                    });

As long as the Item names can be sorted alphabetically with the Item holding the oldest states being higher numbers the above will work with any number of Items. If you exceed 10, use leading zeros. For example

Person1Location
Person1Location_01
Person1Location_02
...
Person1Location_09
Person1Location_10

Person1Location has the current position and Person1Location_10 has the oldest position. Person1Location is not in the Group with the history Items.

Note, I just typed in the above so there might be a typo. Treat it as untested code. But trigger a rule every time Person1Location changes with that and it will rotate out the values

Trigger the rule based on updates to the Person1Location Item. You don’t need any of the other rules defined above, nor the javascript and html file. We are just using stuff built into OH now.