Driving Time and Distance using Google Maps [5.2.0.0,5.9.9.9]

This rule template uses the Google Maps API to query for and calculate the time and distance to travel between two points at a specific desired arrival time. This is distinct from the Driving Time and Distance using Waze [5.2.0.0;5.9.9.9] rule template which only calculates the travel time for leaving right now.

What drove the creation of this template is that I found that the alert rule I created would have us leave too late because the traffic builds up after we leave the house. It doesn’t take into account historic data to predict what the traffic may actually look like after I leave the house. Unfortunatelty Waze doesn’t provide access to this in their API but Google Maps does.

The template has eight properties:

Property Purpose Default
Google Maps API Key Allows access to the Google Maps services (see below).
Start Full address or lat,lon coordinates of the starting point
Destination Full address or lat,lon coordinates of the destination
Target Arrival Time Time you desire to arrive at the destination
Look Ahead Window Time in minutes to look ahead. for example, if I leave in 20 minutes how long will it take to get to the destination assuming traffic conditions that have me arrive at 7:50 am? 20 minutes
Traffic Model Strategy How Google should predict the traffic based on historic data. Pessimistic
Travel Time Item Number:Time Item that the rule will update with the calculated travel time.
Travel Time Distance Number:Length Item the rule will update with the calculated travel distance.

This rule template does not have any triggers or conditions. To avoid hitting the Google Maps servers unnecessarily (you need to provide payment options to gain access to the API and have up to 5000 requests per day for free) I leave the triggers and conditions up to you. I recommend only polling up to once per minute (I use every two minutes) for the half hour window in which you normally need to leave only on the days when you commute to this location. Add conditions to skip holidays and such automatically.

Alternatively you can create a separate rule or rules that call the instantiated rule for the triggers. This makes regeneration of the template easier as you won’t have to recreate the triggers and conditions.

If more than one route is desired or more than one arrival time is desired, create one instance of the rule from the template for each.

There is a companion embedded Google Maps widget at Embedded Google Maps Widget which shows the route, traffic and current travel times. It can use the same Google Maps API Token.

Obtaining a Google Maps API Token

Step 1: Create a Google Cloud Account

  1. Open your browser and navigate to the Google Cloud Console.
  2. Sign in using your preferred Google account.
  3. If this is your first time using the platform, accept the Terms of Service.
  4. Note on Billing: Google requires a valid credit card to activate an account to prevent bot abuse. However, Google provides $200 in free credits every single month for Maps Platform APIs.

Step 2: Create a New Project

  1. Look at the top blue header bar. Next to the “Google Cloud” logo, click the Project Dropdown Menu (it will either show a default project name or a blank selector).
  2. A window will pop up. Click New Project in the upper-right corner.
  3. Name your project something recognizable, such as openHAB.
  4. Leave the “Organization” field as No organization or default, and click Create.
  5. Wait a few seconds for the notification bell icon to spin, then click the Project Dropdown again and select your newly created project.

Step 3: Enable the 4 Core APIs

You must manually enable the API engines before your key can request data from them.

  1. Click the Navigation Menu (the three horizontal lines ☰ in the top-left corner).
  2. Hover over APIs & Services and click on Library.
  3. Use the search bar in the middle of the screen to find and enable these four specific APIs one by one:
    • Directions API (Required for drawing route lines inside the companion map widget)
    • Distance Matrix API (Required for the rule to calculate the distance and travel times)
    • Geocoding API (Required for translating text addresses into coordinates)
    • Maps Embed API (Required for embedding the companion map widget)
  4. For each API, click its card in the search results, and click the blue Enable button. Use the browser’s back button to return to the library and search for the next one.

Step 4: Generate the API Key

  1. Open the left Navigation Menu (☰) again.
  2. Navigate to APIs & ServicesCredentials.
  3. Click the + Create Credentials button located at the top of the main dashboard panel.
  4. Select API Key from the dropdown options.
  5. A pop-up box will appear displaying your brand-new API key string (e.g., AIzaSy...). Copy this key to your clipboard, but do not click close yet. Click the Edit Key link in the bottom right of that pop-up window to secure it immediately.

Step 5: Configure Key Restrictions

Because this key will be hardcoded into your frontend openHAB widget code (making it visible in web browser page sources), you must restrict it so it cannot be stolen and used on other expensive Google cloud services.

  1. On the Edit Key page, scroll down to the Application restrictions section.
  2. Leave this set to None. (Setting this to ‘Websites’ will break your backend openHAB rules because server-side Java requests do not send browser HTTP referrers).
  3. Scroll down to the API restrictions section and select the Restrict key radio button.
  4. Click the Select APIs dropdown menu that appears.
  5. Scroll through the checklist and check the boxes next to the four core APIs you enabled in Step 3:
    • Directions API
    • Distance Matrix API
    • Geocoding API
    • Maps Embed API
  6. Click OK to close the checklist dropdown.
  7. Click the blue Save button at the bottom of the page.

Your key is now live and fully locked down. You can copy-paste this single key directly into your new ohrt-futureDriveTime openHAB rule template configuration properties and your dashboard map widget parameters!

Language: JS Scripting

Dependencies:

  • JS Scripting openhab-js version 5.21.0+
  • Future versions may depend on OHRT

Changelog

Version 0.1

  • initial release

Resources

https://raw.githubusercontent.com/rkoshak/openhab-rules-tools/refs/heads/main/marketplace-templates/ohrt-futureDriveTime.yaml

Here is how I use this rule template for an example.

I have two instances of this rule, one for the morning and one for the afternoon. I use the following triggers and conditions for the morning instance:

    triggers:
      - id: "1"
        config:
          cronExpression: "0 45/2 6 ? 1,2,3,4,5,8,9,10,11,12 MON,TUE,WED,THU,FRI *"
        type: Cron
      - id: "2"
        config:
          cronExpression: "0 0-30/2 7 ? 1,2,3,4,5,8,9,10,11,12 MON,TUE,WED,THU,FRI *"
        type: Cron
    conditions:
      - id: "3"
        type: NotHoliday

I could have used an additional condition for weekday but added them to the cron expression instead. Essentially this runs on weekdays during the school months starting at 06:45 and every two minutes until 07:30. I could probably stop it at 07:15 really as I look ahead 20 minutes.

I use the following triggers and conditions for the afternoon instance:

    triggers:
      - id: "1"
        config:
          cronExpression: "0 0-30/2 14 ? 1,2,3,4,5,8,9,10,11,12 MON,TUE,WED,THU,FRI *"
        type: Cron
    conditions:
      - id: "2"
        type: NotHoliday

Again, I could probably stop at 14:15 but have a wider window for now while I’m proving this all out.

Finally, I have a rule that gets triggered on updates to the Travel Time Item. I use the same Item for both of the google maps rules.

In the rule I determine if this is a morning commute or an afternoon commute. I update the Item I use to populate the footer of the companion widget. Then I determine if an announcement needs to be scheduled. If so, I schedule an announcement for two minutes before the departure time. I use the OpenAI add-on to generate a unique message every time and announce it to the speakers and send a notification to the phones.

const {LoopingTimer, TimerMgr} = require('openhab_rules_tools');

function getTime(inst) {
  let hour = inst.hour();
  let meridian = "AM"
  if(hour == 12) meridian = 'PM';
  if(hour > 12) {
    hour -= 12;
    meridian = 'PM'
  }
  
  return hour + ":" + String(inst.minute()).padStart(2, '0') + " " + meridian;
}

const timer = LoopingTimer('lt', cache.private);
const tm = TimerMgr('tm', cache.private);
const notificationId = "aaclDriveTime";
const lastReportId = "lastReport";

// LookaheadMinutes must exactly match what you configured in your template instance
const lookaheadMinutes = Quantity('20 min');
items.CheckTime.postUpdate(time.toZDT());

// 1. Identify the target arrival thresholds based on the time of day
console.info('Calculating targets and thresholds based on time of day');
let targetArrival = null;
let isSchoolRunWindow = false;
let isMorning = false;

if (time.toZDT().isBetweenTimes(time.toZDT('06:45'), time.toZDT('07:30'))) {
  // Morning Run: Target drop-off bell is 7:50 AM
  targetArrival = time.toZDT('07:50');
  isSchoolRunWindow = true;
  isMorning = true;
} else if (time.toZDT().isBetweenTimes(time.toZDT('14:00'), time.toZDT('14:30'))) {
  // Afternoon Run: Target pickup bell is 2:50 PM (14:50)
  targetArrival = time.toZDT('14:50')
  isSchoolRunWindow = true;
}


// 2. Update your dashboard log item
console.info('Updating the UI notification message');
const now = time.toZDT();
const departureTime = time.toZDT(lookaheadMinutes);
const departureTimeAnnouncement = (event.eventName == "ExecutionEvent") ? now : departureTime.minusMinutes(2);
const predictedArrivalTime = departureTime.plusSeconds(items.TravelTime.quantityState.toUnit('s').int);
items.TravelMessage.postUpdate("As of " + getTime(now )
                                          + " if you leave by " + getTime(departureTime)
                                          + ", you will arrive at " + getTime(predictedArrivalTime));

// 3. Announce?
console.info('Determining if we need to schedule the announcement');
let triggerAnnouncement = isSchoolRunWindow 
                          && targetArrival !== null 
                          && predictedArrivalTime.isClose(targetArrival, new time.Duration.parse('PT5M'));

// 4. Handle Content Generation & Alert Dispatching
const timeSinceLast = time.Duration.between(cache.private.get(lastReportId, () => time.toZDT().minusDays(1)), time.toZDT());
console.info("Time since last report is " + timeSinceLast);

console.info('Now: ' + getTime(now) 
             + " departureTime: " + getTime(departureTime) 
             + " predictedArrivalTime: " + getTime(predictedArrivalTime) 
             + " timeSinceLast: " + timeSinceLast 
             + " triggerAnnouncement: " + triggerAnnouncement );

if( triggerAnnouncement && timeSinceLast.toMinutes() > 10 ) {
  console.info('Scheduling announcement for ' + getTime(departureTimeAnnouncement));
  tm.check('scheduleAnnouncement', departureTimeAnnouncement.toToday(), 
        () => {
            // Compose prompt
            console.info('Composing prompt');
            let prompt = '';
         
            // Morning specific enhancements (Weather integration)
            if( isMorning ) {
              console.info('Morning prompt');
              prompt = 'You are a traffic reporter. Generate an announcement saying it is time to leave for school right now. State that if you leave right now you will arrive at school at ' 
                         + getTime(predictedArrivalTime) + '.';
              prompt = prompt + 'Include a note about the weather forecast which the weather app says "' 
                       + items.Weather_Equipment_ForecastDaily_Condition.state + '".';
                
              if( items.OWM_One_Call_API_weather_and_forecast_One_Call_API_Daily_Forecast_Day_Temperature.quantityState.lessThan("65 °F")
                 || items.Weather_Equipment_ForecastDaily_Precipprobability.quantityState.greaterThan("50 %") ) {
                prompt = prompt + 'The afternoon temperature will be ' + items.OWM_One_Call_API_weather_and_forecast_One_Call_API_Daily_Forecast_Day_Temperature.state
                         + ' and the chance of precipitation is ' + items.Weather_Equipment_ForecastDaily_Precipprobability.state + ' so also include a reminder to take a jacket or coat.';
              }
            }
            // Afternoon 
            else {
              console.info('Afternoon prompt');
              prompt = 'You are a traffic reporter. Generate an announcement saying it is time to leave to pick up the kid from school right now. State that if you leave right now you will arrive at school at ' 
                       + getTime(predictedArrivalTime) + '.';
            }

            // Different route
            console.info('Traffic jam?');
            if(items.TravelDistance.quantityState.greaterThan("18 mi") ) {
              prompt = prompt + ' Include a warning that an accident has occurred and an alternative detour route is advised today.';
            }

            // Clear and seed the OpenAI prompt proxy channel
            console.info('Sending prompt to LLM: ' + prompt);
            items.OpenAI.postUpdate('');
            items.OpenAI.sendCommand(prompt);

            // Wait for LLM to respond to generate the message
            timer.loop(() => {
              console.info('Waiting for LLM response...');
              // sleep for a second if not yet ready TODO: add timeout
              if(items.OpenAI.state == prompt || items.OpenAI.state == '') {
                console.info('No response yet');
                return 'PT1S';
              }

              console.info('Generating announcements');
              const msg = items.OpenAI.state;
              console.info(msg);
              cache.private.put(lastReportId, time.toZDT());
            
              // Send the push notification and schedule cancellation
              actions.notificationBuilder(msg).addUserId('rlkoshak@gmail.com')
                                              .withTitle('Time to leave!')
                                              .withIcon('f7:car-fill')
                                              .withOnClickAction('app:android=com.waze')
                                              .withReferenceId(notificationId)
                                              .send();
              tm.check('cancelNotification', 'PT20M', () => actions.notificationBuilder('cancel notification').withReferenceId(notificationId).hide().send());
            
              // Query online speaker zones and announce using TTS
              const speaker = ['chromecast:audiogroup:46e7bfc6-d7b5-425c-876d-dada046541d2',  // all speakers
                               'chromecast:audiogroup:37af1c61-cab4-44fd-b8ab-5ba75ab950a3',  // main floor
                               'chromecast:chromecast:8ac883645140bb10a498af280595dd5c']     // kitchen
                                 .find( tid => things.getThing(tid).status == "ONLINE");
              actions.Voice.say(msg, null, speaker);
                
            }, time.toZDT());
          },
          true, null, null);
}
else {
  console.info('Not scheduling an announcement. If we were, it would be scheduled for ' + getTime(departureTimeAnnouncement));
}

It’s a first draft and there are still some debugging and troubleshooting artifacts left over. I’ve not looked for ways to make it cleaner and less complicated yet. But hopefully it illustrates at least one way this rule template could be used.

I am trying to install through the UI on version 5.2.0 and it fails.
Is there any hint on this log entry?
I will try to directly copy the yaml into the template rules directory.

23:31:37.006 [ERROR] [org.openhab.core.addon.marketplace.internal.community.CommunityRuleTemplateAddonHandler] - Failed to add rule template from the marketplace: [Template] java.lang.IllegalArgumentException: Cannot construct instance of `org.openhab.core.automation.dto.RuleTemplateDTO` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('<!DOCTYPE html> <html lang="en" class="html-auth"
data-color-mode="auto" data-light-theme="light" data-dark-theme="dark" data-a11y-animated-images="system" data-a11y-link-underlines="true"
>



<head> <meta charset="utf-8"> <link rel="dns-prefetch" href="https://github.githubassets.com"> <link rel="dns-prefetch" href="https://avatars.githubusercontent.com"> <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com"> <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/"> <link rel="preconnect" href="https://github.githubassets.com" crossorigin> <link rel="preconnect" href="https://avatars.githubusercontent.com">




<link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light-99f877e9ddfc0e51.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light_high_contrast-48fdd0811afbab3c.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark-79ad2ace604703b3.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark_high_contrast-24484a076f02295f.css" /><link data-color-theme="light" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light-99f877e9ddfc0e51.css" /><link data-color-theme="light_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_high_contrast-48fdd0811afbab3c.css" /><link data-color-theme="light_colorblind" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_colorblind-f4bf1142976e4bbf.css" /><link data-color-theme="light_colorblind_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_colorblind_high_contrast-f661b49995ba0bd8.css" /><link data-color-theme="light_tritanopia" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_tritanopia-0b38d22346321c92.css" /><link data-color-theme="light_tritanopia_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_tritanopia_high_contrast-f1c62c9e70259b9f.css" /><link data-color-theme="dark" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark-79ad2ace604703b3.css" /><link data-color-theme="dark_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_high_contrast-24484a076f02295f.css" /><link data-color-theme="dark_colorblind" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_colorblind-f50cacf0a86b9929.css" /><link data-color-theme="dark_colorblind_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_colorblind_high_contrast-e61d4f4ca17852c2.css" /><link data-color-theme="dark_tritanopia" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_tritanopia-39c10993d5603fac.css" /><link data-color-theme="dark_tritanopia_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_tritanopia_high_contrast-73236c840c0c7d90.css" /><link data-color-theme="dark_dimmed" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_dimmed-0de76f07cc035b10.css" /><link data-color-theme="dark_dimmed_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/dark_dimmed_high_contrast-fd1500c8744e40d6.css" />
<style type="text/css"> :root { --tab-size-preference')
 at [Source: UNKNOWN; byte offset: #UNKNOWN]


Try it now.

Great template.
Since I want to minimize my use of Google.
I use the APIs from
Openrouteservice: https://openrouteservice.org/
or
Open Source Routing Machine: https://project-osrm.org/

I am trying to use this rule template to address a slightly different use case. Basically I want to get a warning if the travel time is anomalous due to some unusual traffic jam. It seems to me that for this use case, I do not need to use historic data but I have to use live data.
I begin by calling this modified template one hour earlier, half an hour earlier and then more frequently.
I modified the call to API as follows

// 2. Hardcoded Absolute Google Endpoint URL (Bypassing any variable concatenation bugs)
const URL = "https://maps.googleapis.com/maps/api/distancematrix/json""?origins="
 + encodeURIComponent(FROM_COORD)"&destinations="
 + encodeURIComponent(TO_COORD)"&departure_time=now"
//            + "&departure_time=" + futureDepartureEpoch
//            + "&traffic_model=" + trafficModel"&key=" + API_KEY;

Is this modification worth to be considered, or is it better to define a different rule template?

It uses both when it calculates the travel time and travel distance. But for your use case, Driving Time and Distance using Waze [5.2.0.0;5.9.9.9] would probably be a better fit (plus it doesn’t require an API key or anything like that to work). Instead of calculating the time and distance for a given arrival time, instead it just calculates what it would take if you left right now.

That’s actually how I call even this one template too. I use the following cron expressions to trigger the rule:

  • "0 45/2 6 ? 1,2,3,4,5,8,9,10,11,12 MON,TUE,WED,THU,FRI *"
  • "0 0-30/2 7 ? 1,2,3,4,5,8,9,10,11,12 MON,TUE,WED,THU,FRI *"

Basically every two minutes starting at 06:45 through 07-30 on weekdays during months where my son has school. The rule that triggers when the arrival time or drive distance changes or is updated checks the expected arrival time and as soon as it’s within a couple minutes of when I want to arrive it sets a timer to two minutes before the departure time.

However, with the Waze version of the template, what I did was just run it every two minutes same as above and immediately as soon as the arrival time was at or near when I wanted to be there it would alert.

In both cases, if the distance changes from the normal distance I know a new route is advised and that’s included as part of the alert.

The problem with Waze’s “now” forecast of the driving time was that it was not taking into consideration how the traffic builds up after I leave. So by the time it tells you to leave it’s already too late.

I think you might face the same issue.

It might be that a combo of the two rule tempaltes might be required to get the best forewarning. This rule template calculates when you should leave based on historic data but the waze one acts as a backup. As soon as the Waze one detects that you need to leave earlier than Google says, you know there’s trouble and need to leave early.

But so far using the historic data has been enough for me to get the kid to school on time. YMMV.

Note, it’s perfectly OK to modify a rule created by a template. However, you will have to redo the edits to upgrade the template.