Adding to the 'devices' cli command an output that displays the local connectivity of a device.
For example, "id=of:0000000000000203, available=true, local-status=connected 18m7s ago, role=STANDBY, ..."
Also increasing the resolution of the TimeAgo utility.

Change-Id: Ie1b89bd193552e0edd38a9ca28c5ce99b1d27c19
diff --git a/apps/optical-model/src/main/java/org/onosproject/net/optical/device/OpticalDeviceServiceView.java b/apps/optical-model/src/main/java/org/onosproject/net/optical/device/OpticalDeviceServiceView.java
index 88ef950..1e70ff6 100644
--- a/apps/optical-model/src/main/java/org/onosproject/net/optical/device/OpticalDeviceServiceView.java
+++ b/apps/optical-model/src/main/java/org/onosproject/net/optical/device/OpticalDeviceServiceView.java
@@ -202,4 +202,10 @@
         }
     }
 
+
+    @Override
+    public String localStatus(DeviceId deviceId) {
+        return null;
+    }
+
 }
diff --git a/cli/src/main/java/org/onosproject/cli/net/DevicesListCommand.java b/cli/src/main/java/org/onosproject/cli/net/DevicesListCommand.java
index 6ac06ba..d47e019 100644
--- a/cli/src/main/java/org/onosproject/cli/net/DevicesListCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/net/DevicesListCommand.java
@@ -41,7 +41,7 @@
 public class DevicesListCommand extends AbstractShellCommand {
 
     private static final String FMT =
-            "id=%s, available=%s, role=%s, type=%s, mfr=%s, hw=%s, sw=%s, serial=%s, driver=%s%s";
+            "id=%s, available=%s, local-status=%s, role=%s, type=%s, mfr=%s, hw=%s, sw=%s, serial=%s, driver=%s%s";
 
     private static final String FMT_SHORT =
             "id=%s, available=%s, role=%s, type=%s, driver=%s";
@@ -103,6 +103,7 @@
                       deviceService.getRole(device.id()), device.type(), driver);
             } else {
                 print(FMT, device.id(), deviceService.isAvailable(device.id()),
+                      deviceService.localStatus(device.id()),
                       deviceService.getRole(device.id()), device.type(),
                       device.manufacturer(), device.hwVersion(), device.swVersion(),
                       device.serialNumber(), driver,
diff --git a/core/api/src/main/java/org/onosproject/net/device/DeviceService.java b/core/api/src/main/java/org/onosproject/net/device/DeviceService.java
index 290f7d7..1cb90e8 100644
--- a/core/api/src/main/java/org/onosproject/net/device/DeviceService.java
+++ b/core/api/src/main/java/org/onosproject/net/device/DeviceService.java
@@ -166,4 +166,14 @@
      */
     boolean isAvailable(DeviceId deviceId);
 
+    /**
+     * Indicates how long ago the device connected or disconnected from this
+     * controller instance.
+     *
+     * @param deviceId device identifier
+     * @return a human readable string indicating the time since the device
+     *          connected-to or disconnected-from this controller instance.
+     */
+    String localStatus(DeviceId deviceId);
+
 }
diff --git a/core/api/src/test/java/org/onosproject/net/device/DeviceServiceAdapter.java b/core/api/src/test/java/org/onosproject/net/device/DeviceServiceAdapter.java
index 7d31d0f..9c6887c 100644
--- a/core/api/src/test/java/org/onosproject/net/device/DeviceServiceAdapter.java
+++ b/core/api/src/test/java/org/onosproject/net/device/DeviceServiceAdapter.java
@@ -132,4 +132,9 @@
         return Collections.emptyList();
     }
 
+    @Override
+    public String localStatus(DeviceId deviceId) {
+        return null;
+    }
+
 }
diff --git a/core/net/src/main/java/org/onosproject/net/device/impl/DeviceManager.java b/core/net/src/main/java/org/onosproject/net/device/impl/DeviceManager.java
index 7a98a5f..a86b50d 100644
--- a/core/net/src/main/java/org/onosproject/net/device/impl/DeviceManager.java
+++ b/core/net/src/main/java/org/onosproject/net/device/impl/DeviceManager.java
@@ -18,6 +18,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -34,6 +35,7 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
+import org.joda.time.DateTime;
 import org.onlab.util.Tools;
 import org.onosproject.cluster.ClusterService;
 import org.onosproject.cluster.NodeId;
@@ -76,6 +78,7 @@
 import org.slf4j.Logger;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.util.concurrent.Futures;
 
@@ -145,6 +148,20 @@
         = synchronizedListMultimap(
            newListMultimap(new ConcurrentHashMap<>(), CopyOnWriteArrayList::new));
 
+    /**
+     * Local storage for connectivity status of devices.
+     */
+    private class LocalStatus {
+        boolean connected;
+        DateTime dateTime;
+
+        public LocalStatus(boolean b, DateTime now) {
+            connected = b;
+            dateTime = now;
+        }
+    }
+    private final Map<DeviceId, LocalStatus> deviceLocalStatus =
+            Maps.newConcurrentMap();
 
     @Activate
     public void activate() {
@@ -262,6 +279,16 @@
         return store.isAvailable(deviceId);
     }
 
+    @Override
+    public String localStatus(DeviceId deviceId) {
+        LocalStatus ls = deviceLocalStatus.get(deviceId);
+        if (ls == null) {
+            return "No Record";
+        }
+        String timeAgo = Tools.timeAgo(ls.dateTime.getMillis());
+        return (ls.connected) ? "connected " + timeAgo : "disconnected " + timeAgo;
+    }
+
     // Check a device for control channel connectivity.
     private boolean isReachable(DeviceId deviceId) {
         if (deviceId == null) {
@@ -334,7 +361,6 @@
                     }
                 } else {
                     // check if the device has master, if not, mark it offline
-                    NodeId masterId = mastershipService.getMasterFor(deviceId);
                     // only the nodes which has mastership role can mark any device offline.
                     CompletableFuture<MastershipRole> roleFuture = mastershipService.requestRoleFor(deviceId);
                     roleFuture.thenAccept(role -> {
@@ -404,6 +430,8 @@
             checkNotNull(deviceDescription, DEVICE_DESCRIPTION_NULL);
             checkValidity();
 
+            deviceLocalStatus.put(deviceId, new LocalStatus(true, DateTime.now()));
+
             BasicDeviceConfig cfg = networkConfigService.getConfig(deviceId, BasicDeviceConfig.class);
             if (!isAllowed(cfg)) {
                 log.warn("Device {} is not allowed", deviceId);
@@ -445,7 +473,7 @@
         public void deviceDisconnected(DeviceId deviceId) {
             checkNotNull(deviceId, DEVICE_ID_NULL);
             checkValidity();
-
+            deviceLocalStatus.put(deviceId, new LocalStatus(false, DateTime.now()));
             log.info("Device {} disconnected from this node", deviceId);
 
             List<PortDescription> descs = store.getPortDescriptions(provider().id(), deviceId)
diff --git a/incubator/net/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkDeviceManager.java b/incubator/net/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkDeviceManager.java
index 81bf123..4b9221e 100644
--- a/incubator/net/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkDeviceManager.java
+++ b/incubator/net/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkDeviceManager.java
@@ -182,4 +182,10 @@
     public VirtualNetwork network() {
         return network;
     }
+
+    @Override
+    public String localStatus(DeviceId deviceId) {
+        // TODO not supported at this time
+        return null;
+    }
 }
diff --git a/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfDeviceServiceMock.java b/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfDeviceServiceMock.java
index a13636b..e008285 100644
--- a/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfDeviceServiceMock.java
+++ b/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfDeviceServiceMock.java
@@ -111,4 +111,9 @@
     public void removeListener(DeviceListener listener) {
 
     }
+
+    @Override
+    public String localStatus(DeviceId deviceId) {
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/protocols/pcep/ctl/src/test/java/org/onosproject/pcelabelstore/util/MockDeviceService.java b/protocols/pcep/ctl/src/test/java/org/onosproject/pcelabelstore/util/MockDeviceService.java
index b8c5017..0917eb2 100644
--- a/protocols/pcep/ctl/src/test/java/org/onosproject/pcelabelstore/util/MockDeviceService.java
+++ b/protocols/pcep/ctl/src/test/java/org/onosproject/pcelabelstore/util/MockDeviceService.java
@@ -145,4 +145,10 @@
         // TODO Auto-generated method stub
         return false;
     }
+
+    @Override
+    public String localStatus(DeviceId deviceId) {
+        // TODO Auto-generated method stub
+        return null;
+    }
 }
diff --git a/utils/misc/src/main/java/org/onlab/util/Tools.java b/utils/misc/src/main/java/org/onlab/util/Tools.java
index 6df74b0..cc769c4 100644
--- a/utils/misc/src/main/java/org/onlab/util/Tools.java
+++ b/utils/misc/src/main/java/org/onlab/util/Tools.java
@@ -541,11 +541,11 @@
         long hoursSince = (long) (deltaMillis / (1000.0 * 60 * 60));
         long daysSince = (long) (deltaMillis / (1000.0 * 60 * 60 * 24));
         if (daysSince > 0) {
-            return String.format("%dd ago", daysSince);
+            return String.format("%dd%dh ago", daysSince, hoursSince - daysSince * 24);
         } else if (hoursSince > 0) {
-            return String.format("%dh ago", hoursSince);
+            return String.format("%dh%dm ago", hoursSince, minsSince - hoursSince * 60);
         } else if (minsSince > 0) {
-            return String.format("%dm ago", minsSince);
+            return String.format("%dm%ds ago", minsSince, secondsSince - minsSince * 60);
         } else if (secondsSince > 0) {
             return String.format("%ds ago", secondsSince);
         } else {