diff --git a/bin/input-monitor.c b/bin/input-monitor.c
index 7d9efd21c39d90d295dbc0e251cf31a95b91b72a..49f092b68072315d6ceb6401ee40fc7f95360834 100644
--- a/bin/input-monitor.c
+++ b/bin/input-monitor.c
@@ -71,6 +71,44 @@ print_json (JsonBuilder *builder)
     g_warning ("Unable to write final newline: %s", g_strerror (errno));
 }
 
+static void
+add_uevent_member (JsonBuilder *builder,
+                   const char *name,
+                   const char *value)
+{
+  const char *start = value;
+  const char *end;
+
+  /* value is conceptually a single string, but we
+   * present it as an array of lines here, because that's
+   * a lot easier to read! */
+  json_builder_set_member_name (builder, name);
+  json_builder_begin_array (builder);
+
+  while (*start != '\0')
+    {
+      g_autofree gchar *valid = NULL;
+
+      while (*start == '\n')
+        start++;
+
+      if (*start == '\0')
+        break;
+
+      end = strchrnul (start, '\n');
+
+      valid = g_utf8_make_valid (start, end - start);
+      json_builder_add_string_value (builder, valid);
+
+      if (*end == '\0')
+        break;
+
+      start = end + 1;
+    }
+
+  json_builder_end_array (builder);
+}
+
 static void
 added (SrtInputDeviceMonitor *monitor,
        SrtInputDevice *dev,
@@ -84,6 +122,7 @@ added (SrtInputDeviceMonitor *monitor,
       json_builder_begin_object (builder);
         {
           g_auto(GStrv) udev_properties = NULL;
+          const char *sys_path;
 
           json_builder_set_member_name (builder, "dev_node");
           json_builder_add_string_value (builder,
@@ -120,39 +159,74 @@ added (SrtInputDeviceMonitor *monitor,
               g_autofree gchar *uevent = srt_input_device_dup_uevent (dev);
 
               if (uevent != NULL)
-                {
-                  const char *start = uevent;
-                  const char *end;
+                add_uevent_member (builder, "uevent", uevent);
+            }
+
+          sys_path = srt_input_device_get_hid_sys_path (dev);
 
-                  /* uevent is conceptually a single string, but we
-                   * present it as an array of lines here, because that's
-                   * a lot easier to read! */
-                  json_builder_set_member_name (builder, "uevent");
-                  json_builder_begin_array (builder);
+          if (sys_path != NULL)
+            {
+              json_builder_set_member_name (builder, "hid_ancestor");
+              json_builder_begin_object (builder);
+                {
+                  json_builder_set_member_name (builder, "sys_path");
+                  json_builder_add_string_value (builder, sys_path);
 
-                  while (*start != '\0')
+                  if (opt_verbose)
                     {
-                      g_autofree gchar *valid = NULL;
+                      g_autofree gchar *uevent = srt_input_device_dup_hid_uevent (dev);
+
+                      if (uevent != NULL)
+                        add_uevent_member (builder, "uevent", uevent);
+                    }
+
+                  /* TODO: vendor, product etc. */
+                }
+              json_builder_end_object (builder);
+            }
+
+          sys_path = srt_input_device_get_input_sys_path (dev);
+
+          if (sys_path != NULL)
+            {
+              json_builder_set_member_name (builder, "input_ancestor");
+              json_builder_begin_object (builder);
+                {
+                  json_builder_set_member_name (builder, "sys_path");
+                  json_builder_add_string_value (builder, sys_path);
 
-                      while (*start == '\n')
-                        start++;
+                  if (opt_verbose)
+                    {
+                      g_autofree gchar *uevent = srt_input_device_dup_input_uevent (dev);
 
-                      if (*start == '\0')
-                        break;
+                      if (uevent != NULL)
+                        add_uevent_member (builder, "uevent", uevent);
+                    }
+                }
+              json_builder_end_object (builder);
+            }
 
-                      end = strchrnul (start, '\n');
+          sys_path = srt_input_device_get_usb_device_sys_path (dev);
 
-                      valid = g_utf8_make_valid (start, (end - start) - 1);
-                      json_builder_add_string_value (builder, valid);
+          if (sys_path != NULL)
+            {
+              json_builder_set_member_name (builder, "usb_device_ancestor");
+              json_builder_begin_object (builder);
+                {
+                  json_builder_set_member_name (builder, "sys_path");
+                  json_builder_add_string_value (builder, sys_path);
 
-                      if (*end == '\0')
-                        break;
+                  if (opt_verbose)
+                    {
+                      g_autofree gchar *uevent = srt_input_device_dup_usb_device_uevent (dev);
 
-                      start = end + 1;
+                      if (uevent != NULL)
+                        add_uevent_member (builder, "uevent", uevent);
                     }
 
-                  json_builder_end_array (builder);
+                  /* TODO: vendor, product etc. */
                 }
+              json_builder_end_object (builder);
             }
 
           /* TODO: Print more details here */
diff --git a/steam-runtime-tools/direct-input-device.c b/steam-runtime-tools/direct-input-device.c
index a1431bc727527ea576634ac4dd3d5e468dd67578..7cd31f930ecb81090827e5e98b00368afd97654e 100644
--- a/steam-runtime-tools/direct-input-device.c
+++ b/steam-runtime-tools/direct-input-device.c
@@ -43,6 +43,108 @@
 static void srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface);
 static void srt_direct_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface);
 
+static void
+remove_last_component_in_place (char *path)
+{
+  char *slash = strrchr (path, '/');
+
+  if (slash == NULL)
+    *path = '\0';
+  else
+    *slash = '\0';
+}
+
+/*
+ * @path: The path to a device directory in /sys
+ *
+ * Returns: (nullable) (transfer full): The closest ancestor of @path
+ *  that is an input device with evdev capabilities, or %NULL if not found
+ */
+static gchar *
+find_input_ancestor (const char *path)
+{
+  g_autofree char *ancestor = NULL;
+
+  for (ancestor = g_strdup (path);
+       g_str_has_prefix (ancestor, "/sys/");
+       remove_last_component_in_place (ancestor))
+    {
+      g_autofree char *subsys = g_build_filename (ancestor, "subsystem", NULL);
+      g_autofree char *caps = g_build_filename (ancestor, "capabilities", "ev", NULL);
+      g_autofree char *target = glnx_readlinkat_malloc (AT_FDCWD, subsys,
+                                                        NULL, NULL);
+      const char *slash = NULL;
+
+      if (!g_file_test (caps, G_FILE_TEST_IS_REGULAR))
+        continue;
+
+      if (target != NULL)
+        slash = strrchr (target, '/');
+
+      if (slash == NULL || strcmp (slash + 1, "input") != 0)
+        continue;
+
+      return g_steal_pointer (&ancestor);
+    }
+
+  return NULL;
+}
+
+/*
+ * @path: The path to a device directory in /sys
+ * @subsystem: A desired subsystem, such as "hid" or "usb", or %NULL to accept any
+ * @devtype: A desired device type, such as "usb_device", or %NULL to accept any
+ * @uevent_out: (out) (optional): Optionally return the text of /sys/.../uevent
+ *
+ * Returns: (nullable) (transfer full): The closest ancestor of @path
+ *  that has a subsystem of @subsystem, or %NULL if not found.
+ */
+static gchar *
+get_ancestor_with_subsystem_devtype (const char *path,
+                                     const char *subsystem,
+                                     const char *devtype,
+                                     gchar **uevent_out)
+{
+  g_autofree char *ancestor = NULL;
+
+  for (ancestor = g_strdup (path);
+       g_str_has_prefix (ancestor, "/sys/");
+       remove_last_component_in_place (ancestor))
+    {
+      g_autofree char *uevent = g_build_filename (ancestor, "uevent", NULL);
+      g_autofree char *text = NULL;
+
+      /* If it doesn't have a uevent file, it isn't a real device */
+      if (!g_file_get_contents (uevent, &text, NULL, NULL))
+        continue;
+
+      if (subsystem != NULL)
+        {
+          g_autofree char *subsys = g_build_filename (ancestor, "subsystem", NULL);
+          g_autofree char *target = glnx_readlinkat_malloc (AT_FDCWD, subsys,
+                                                            NULL, NULL);
+          const char *slash = NULL;
+
+          if (target != NULL)
+            slash = strrchr (target, '/');
+
+          if (slash == NULL || strcmp (slash + 1, subsystem) != 0)
+            continue;
+        }
+
+      if (devtype != NULL
+          && !_srt_input_device_uevent_field_equals (text, "DEVTYPE", devtype))
+        continue;
+
+      if (uevent_out != NULL)
+        *uevent_out = g_steal_pointer (&text);
+
+      return g_steal_pointer (&ancestor);
+    }
+
+  return NULL;
+}
+
 static GQuark quark_hidraw = 0;
 static GQuark quark_input = 0;
 
@@ -53,6 +155,21 @@ struct _SrtDirectInputDevice
   char *sys_path;
   gchar *dev_node;
   GQuark subsystem;
+
+  struct
+  {
+    gchar *sys_path;
+  } hid_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+  } input_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+  } usb_device_ancestor;
 };
 
 struct _SrtDirectInputDeviceClass
@@ -109,6 +226,9 @@ srt_direct_input_device_finalize (GObject *object)
 
   g_clear_pointer (&self->sys_path, free);
   g_clear_pointer (&self->dev_node, g_free);
+  g_clear_pointer (&self->hid_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->input_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->usb_device_ancestor.sys_path, g_free);
 
   G_OBJECT_CLASS (_srt_direct_input_device_parent_class)->finalize (object);
 }
@@ -145,6 +265,30 @@ srt_direct_input_device_get_sys_path (SrtInputDevice *device)
   return self->sys_path;
 }
 
+static const char *
+srt_direct_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->hid_ancestor.sys_path;
+}
+
+static const char *
+srt_direct_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->input_ancestor.sys_path;
+}
+
+static const char *
+srt_direct_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->usb_device_ancestor.sys_path;
+}
+
 static void
 srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface)
 {
@@ -154,6 +298,10 @@ srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface)
   IMPLEMENT (get_sys_path);
   IMPLEMENT (get_subsystem);
 
+  IMPLEMENT (get_hid_sys_path);
+  IMPLEMENT (get_input_sys_path);
+  IMPLEMENT (get_usb_device_sys_path);
+
 #undef IMPLEMENT
 }
 
@@ -311,6 +459,15 @@ add_device (SrtDirectInputDeviceMonitor *self,
       return;
     }
 
+  device->hid_ancestor.sys_path = get_ancestor_with_subsystem_devtype (device->sys_path,
+                                                              "hid", NULL,
+                                                              NULL);
+  device->input_ancestor.sys_path = find_input_ancestor (device->sys_path);
+  device->usb_device_ancestor.sys_path = get_ancestor_with_subsystem_devtype (device->sys_path,
+                                                                     "usb",
+                                                                     "usb_device",
+                                                                     NULL);
+
   g_hash_table_replace (self->devices, device->dev_node, device);
   _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self),
                                         SRT_INPUT_DEVICE (device));
diff --git a/steam-runtime-tools/input-device-internal.h b/steam-runtime-tools/input-device-internal.h
index 02acf67cc57b2aac3e9c74be720b5e0158fdfb52..4b5589ff17fa3a952e4f619a7b7aad3e9ba401e2 100644
--- a/steam-runtime-tools/input-device-internal.h
+++ b/steam-runtime-tools/input-device-internal.h
@@ -37,6 +37,15 @@ struct _SrtInputDeviceInterface
   const char * (*get_subsystem) (SrtInputDevice *device);
   gchar ** (*dup_udev_properties) (SrtInputDevice *device);
   gchar * (*dup_uevent) (SrtInputDevice *device);
+
+  const char * (*get_hid_sys_path) (SrtInputDevice *device);
+  gchar * (*dup_hid_uevent) (SrtInputDevice *device);
+
+  const char * (*get_input_sys_path) (SrtInputDevice *device);
+  gchar * (*dup_input_uevent) (SrtInputDevice *device);
+
+  const char * (*get_usb_device_sys_path) (SrtInputDevice *device);
+  gchar * (*dup_usb_device_uevent) (SrtInputDevice *device);
 };
 
 struct _SrtInputDeviceMonitorInterface
@@ -63,3 +72,9 @@ void _srt_input_device_monitor_emit_added (SrtInputDeviceMonitor *monitor,
 void _srt_input_device_monitor_emit_removed (SrtInputDeviceMonitor *monitor,
                                              SrtInputDevice *device);
 void _srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor);
+
+gchar *_srt_input_device_uevent_field (const char *text,
+                                       const char *key);
+gboolean _srt_input_device_uevent_field_equals (const char *text,
+                                                const char *key,
+                                                const char *want_value);
diff --git a/steam-runtime-tools/input-device.c b/steam-runtime-tools/input-device.c
index 7188bbf30cfdc143baeae719ef44c3381111bede..3054cc2ffcc2435f2b24b8d6433b4fc528d849a4 100644
--- a/steam-runtime-tools/input-device.c
+++ b/steam-runtime-tools/input-device.c
@@ -154,18 +154,11 @@ srt_input_device_dup_uevent (SrtInputDevice *device)
   return iface->dup_uevent (device);
 }
 
-/*
- * Default implementation: read the file in /sys. The kernel provides this,
- * so it should always be present, except in containers.
- */
 static gchar *
-srt_input_device_default_dup_uevent (SrtInputDevice *device)
+read_uevent (const char *sys_path)
 {
   g_autofree gchar *uevent_path = NULL;
   g_autofree gchar *contents = NULL;
-  const char *sys_path;
-
-  sys_path = srt_input_device_get_sys_path (device);
 
   if (sys_path == NULL)
     return NULL;
@@ -178,6 +171,34 @@ srt_input_device_default_dup_uevent (SrtInputDevice *device)
   return NULL;
 }
 
+/*
+ * Default implementation: read the file in /sys. The kernel provides this,
+ * so it should always be present, except in containers.
+ */
+static gchar *
+srt_input_device_default_dup_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_sys_path (device));
+}
+
+static gchar *
+srt_input_device_default_dup_hid_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_hid_sys_path (device));
+}
+
+static gchar *
+srt_input_device_default_dup_input_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_input_sys_path (device));
+}
+
+static gchar *
+srt_input_device_default_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  return read_uevent (srt_input_device_get_usb_device_sys_path (device));
+}
+
 /**
  * srt_input_device_dup_udev_properties:
  * @device: An object implementing #SrtInputDeviceInterface
@@ -199,6 +220,153 @@ srt_input_device_dup_udev_properties (SrtInputDevice *device)
   return iface->dup_udev_properties (device);
 }
 
+/**
+ * srt_input_device_get_hid_sys_path:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the path of the device directory in /sys that represents this
+ * input device's closest ancestor that is a Human Interface Device.
+ * Many, but not all, input devices have a HID ancestor. If there is no
+ * applicable HID device, return %NULL.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /sys.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the directory in /sys might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /sys that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_hid_sys_path (device);
+}
+
+/**
+ * srt_input_device_dup_hid_uevent:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the uevent data structure similar to
+ * srt_input_device_dup_uevent(), but for the ancestor device returned
+ * by srt_input_device_get_hid_sys_path().
+ *
+ * Returns: (nullable) (transfer full): A multi-line string, or %NULL.
+ *  Free with g_free().
+ */
+gchar *
+srt_input_device_dup_hid_uevent (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_hid_uevent (device);
+}
+
+/**
+ * srt_input_device_get_usb_device_sys_path:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * If the @device is associated with a USB device, return the path in
+ * /sys representing the Linux `usb_device`. If not, return %NULL.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /sys.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the directory in /sys might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /sys that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_usb_device_sys_path (device);
+}
+
+/**
+ * srt_input_device_dup_usb_device_uevent:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the uevent data structure similar to
+ * srt_input_device_dup_uevent(), but for the ancestor device returned
+ * by srt_input_device_get_usb_device_sys_path().
+ *
+ * Returns: (nullable) (transfer full): A multi-line string, or %NULL.
+ *  Free with g_free().
+ */
+gchar *
+srt_input_device_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_usb_device_uevent (device);
+}
+
+/**
+ * srt_input_device_get_input_sys_path:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * If the @device has an ancestor device that advertises evdev input
+ * capabilities, return the path in /sys for that device. Otherwise
+ * return %NULL.
+ *
+ * For processes in a container, it is not guaranteed that this path will
+ * exist in the container's /sys.
+ *
+ * The returned string will not be freed as long as @device is referenced,
+ * but the directory in /sys might be deleted, or even reused for a different
+ * device.
+ *
+ * Returns: (nullable) (transfer none): A path in /sys that will not be
+ *  freed as long as a reference to @device is held, or %NULL
+ */
+const char *
+srt_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->get_input_sys_path (device);
+}
+
+/**
+ * srt_input_device_dup_input_uevent:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return the uevent data structure similar to
+ * srt_input_device_dup_uevent(), but for the ancestor device returned
+ * by srt_input_device_get_input_sys_path().
+ *
+ * Returns: (nullable) (transfer full): A multi-line string, or %NULL.
+ *  Free with g_free().
+ */
+gchar *
+srt_input_device_dup_input_uevent (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, NULL);
+
+  return iface->dup_input_uevent (device);
+}
+
 static void
 srt_input_device_default_init (SrtInputDeviceInterface *iface)
 {
@@ -208,8 +376,17 @@ srt_input_device_default_init (SrtInputDeviceInterface *iface)
   IMPLEMENT2 (get_dev_node, get_null_string);
   IMPLEMENT2 (get_sys_path, get_null_string);
   IMPLEMENT2 (get_subsystem, get_null_string);
+
+  IMPLEMENT2 (get_hid_sys_path, get_null_string);
+  IMPLEMENT2 (get_input_sys_path, get_null_string);
+  IMPLEMENT2 (get_usb_device_sys_path, get_null_string);
+
   IMPLEMENT2 (dup_udev_properties, get_null_strv);
+
   IMPLEMENT (dup_uevent);
+  IMPLEMENT (dup_hid_uevent);
+  IMPLEMENT (dup_input_uevent);
+  IMPLEMENT (dup_usb_device_uevent);
 
 #undef IMPLEMENT
 #undef IMPLEMENT2
@@ -581,3 +758,77 @@ _srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor)
   g_debug ("All for now");
   g_signal_emit (monitor, monitor_signal_all_for_now, 0);
 }
+
+/*
+ * Returns: The field @key from @text, or %NULL if not found.
+ */
+gchar *
+_srt_input_device_uevent_field (const char *text,
+                                const char *key)
+{
+  const char *line = text;
+  size_t key_len = strlen (key);
+
+  for (line = text;
+       line != NULL && *line != '\0';
+       line = strchr (line, '\n'))
+    {
+      while (*line == '\n')
+        line++;
+
+      /* If the line starts with KEY=, we're looking at the right field */
+      if (strncmp (line, key, key_len) == 0
+          && line[key_len] == '=')
+        {
+          const char *value = line + key_len + 1;
+          const char *after = strchrnul (line, '\n');
+
+          g_assert (after != NULL);
+          g_assert (after >= value);
+          g_assert (*after == '\n' || *after == '\0');
+          return g_strndup (value, after - value);
+        }
+    }
+
+  return NULL;
+}
+
+/*
+ * Returns: %TRUE if @text contains KEY=WANT_VALUE, preceded by
+ *  beginning-of-string or a newline, and followed by a newline
+ *  or end-of-string.
+ */
+gboolean
+_srt_input_device_uevent_field_equals (const char *text,
+                                       const char *key,
+                                       const char *want_value)
+{
+  const char *line = text;
+  size_t key_len = strlen (key);
+  size_t val_len = strlen (want_value);
+
+  for (line = text;
+       line != NULL && *line != '\0';
+       line = strchr (line, '\n'))
+    {
+      while (*line == '\n')
+        line++;
+
+      /* If the line starts with KEY=, we're looking at the right field */
+      if (strncmp (line, key, key_len) == 0
+          && line[key_len] == '=')
+        {
+          const char *real_value = line + key_len + 1;
+          char after;
+
+          if (strncmp (real_value, want_value, val_len) != 0)
+            return FALSE;
+
+          after = real_value[val_len];
+
+          return (after == '\0' || after == '\n');
+        }
+    }
+
+  return FALSE;
+}
diff --git a/steam-runtime-tools/input-device.h b/steam-runtime-tools/input-device.h
index 67685b7366cbc67a6451a897fe7714a623599673..037e4c992be9a65bbf11c5dc523504733a035a79 100644
--- a/steam-runtime-tools/input-device.h
+++ b/steam-runtime-tools/input-device.h
@@ -55,6 +55,21 @@ gchar **srt_input_device_dup_udev_properties (SrtInputDevice *device);
 _SRT_PUBLIC
 gchar *srt_input_device_dup_uevent (SrtInputDevice *device);
 
+_SRT_PUBLIC
+const char *srt_input_device_get_hid_sys_path (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar *srt_input_device_dup_hid_uevent (SrtInputDevice *device);
+
+_SRT_PUBLIC
+const char *srt_input_device_get_input_sys_path (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar *srt_input_device_dup_input_uevent (SrtInputDevice *device);
+
+_SRT_PUBLIC
+const char *srt_input_device_get_usb_device_sys_path (SrtInputDevice *device);
+_SRT_PUBLIC
+gchar *srt_input_device_dup_usb_device_uevent (SrtInputDevice *device);
+
 /**
  * SrtInputDeviceMonitorFlags:
  * @SRT_INPUT_DEVICE_MONITOR_FLAGS_ONCE: Enumerate the devices that were
diff --git a/steam-runtime-tools/udev-input-device.c b/steam-runtime-tools/udev-input-device.c
index 7245a7535079218fa177f2642ffe747a4b3cd9af..ca3594cb81cc88f9704a7d130f09737b675b1995 100644
--- a/steam-runtime-tools/udev-input-device.c
+++ b/steam-runtime-tools/udev-input-device.c
@@ -65,6 +65,7 @@ static struct
   struct udev_list_entry *(*udev_device_get_properties_list_entry) (struct udev_device *);
   const char *(*udev_device_get_property_value) (struct udev_device *, const char *);
   const char *(*udev_device_get_sysattr_value) (struct udev_device *, const char *);
+  struct udev_device *(*udev_device_get_parent_with_subsystem_devtype) (struct udev_device *, const char *, const char *);
   struct udev_device *(*udev_device_ref) (struct udev_device *);
   struct udev_device *(*udev_device_unref) (struct udev_device *);
 
@@ -86,11 +87,42 @@ static struct
   struct udev_enumerate *(*udev_enumerate_unref) (struct udev_enumerate *);
 } symbols;
 
+static struct udev_device *
+find_input_ancestor (struct udev_device *dev)
+{
+  struct udev_device *ancestor;
+
+  for (ancestor = dev;
+       ancestor != NULL;
+       ancestor = symbols.udev_device_get_parent_with_subsystem_devtype (ancestor, "input", NULL))
+    {
+      if (symbols.udev_device_get_sysattr_value (ancestor, "capabilities/ev") != NULL)
+        return ancestor;
+    }
+
+  return NULL;
+}
+
 struct _SrtUdevInputDevice
 {
   GObject parent;
 
-  struct udev_device *dev;
+  struct udev_device *dev;                    /* owned */
+
+  struct
+  {
+    struct udev_device *dev;                  /* borrowed from child dev */
+  } hid_ancestor;
+
+  struct
+  {
+    struct udev_device *dev;                  /* borrowed from child dev */
+  } input_ancestor;
+
+  struct
+  {
+    struct udev_device *dev;                  /* borrowed from child dev */
+  } usb_device_ancestor;
 };
 
 struct _SrtUdevInputDeviceClass
@@ -217,6 +249,72 @@ srt_udev_input_device_dup_uevent (SrtInputDevice *device)
   return g_strdup (symbols.udev_device_get_sysattr_value (self->dev, "uevent"));
 }
 
+static const char *
+srt_udev_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->hid_ancestor.dev == NULL)
+    return NULL;
+
+  return symbols.udev_device_get_syspath (self->hid_ancestor.dev);
+}
+
+static const char *
+srt_udev_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->input_ancestor.dev == NULL)
+    return NULL;
+
+  return symbols.udev_device_get_syspath (self->input_ancestor.dev);
+}
+
+static const char *
+srt_udev_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->usb_device_ancestor.dev == NULL)
+    return NULL;
+
+  return symbols.udev_device_get_syspath (self->usb_device_ancestor.dev);
+}
+
+static gchar *
+srt_udev_input_device_dup_hid_uevent (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->hid_ancestor.dev == NULL)
+    return NULL;
+
+  return g_strdup (symbols.udev_device_get_sysattr_value (self->hid_ancestor.dev, "uevent"));
+}
+
+static gchar *
+srt_udev_input_device_dup_input_uevent (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->input_ancestor.dev == NULL)
+    return NULL;
+
+  return g_strdup (symbols.udev_device_get_sysattr_value (self->input_ancestor.dev, "uevent"));
+}
+
+static gchar *
+srt_udev_input_device_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  if (self->usb_device_ancestor.dev == NULL)
+    return NULL;
+
+  return g_strdup (symbols.udev_device_get_sysattr_value (self->usb_device_ancestor.dev, "uevent"));
+}
+
 static void
 srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface)
 {
@@ -228,6 +326,14 @@ srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface)
   IMPLEMENT (dup_udev_properties);
   IMPLEMENT (dup_uevent);
 
+  IMPLEMENT (get_hid_sys_path);
+  IMPLEMENT (get_input_sys_path);
+  IMPLEMENT (get_usb_device_sys_path);
+
+  IMPLEMENT (dup_hid_uevent);
+  IMPLEMENT (dup_input_uevent);
+  IMPLEMENT (dup_usb_device_uevent);
+
 #undef IMPLEMENT
 }
 
@@ -391,6 +497,7 @@ srt_udev_input_device_monitor_initable_init (GInitable *initable,
   SYMBOL (udev_device_get_properties_list_entry);
   SYMBOL (udev_device_get_property_value);
   SYMBOL (udev_device_get_sysattr_value);
+  SYMBOL (udev_device_get_parent_with_subsystem_devtype);
   SYMBOL (udev_device_ref);
   SYMBOL (udev_device_unref);
 
@@ -480,6 +587,14 @@ add_device (SrtUdevInputDeviceMonitor *self,
   device = g_object_new (SRT_TYPE_UDEV_INPUT_DEVICE, NULL);
   device->dev = symbols.udev_device_ref (dev);
 
+  device->hid_ancestor.dev = symbols.udev_device_get_parent_with_subsystem_devtype (device->dev,
+                                                                                    "hid",
+                                                                                    NULL);
+  device->input_ancestor.dev = find_input_ancestor (device->dev);
+  device->usb_device_ancestor.dev = symbols.udev_device_get_parent_with_subsystem_devtype (device->dev,
+                                                                                           "usb",
+                                                                                           "usb_device");
+
   g_hash_table_replace (self->devices, (char *) syspath, device);
   _srt_input_device_monitor_emit_added (SRT_INPUT_DEVICE_MONITOR (self),
                                         SRT_INPUT_DEVICE (device));
diff --git a/tests/input-device.c b/tests/input-device.c
index 74335137a54d4e20527e1e6273f993c8161eb983..af84938582f1ece392a01560b2086486af465c64 100644
--- a/tests/input-device.c
+++ b/tests/input-device.c
@@ -104,6 +104,9 @@ test_input_device_usb (Fixture *f,
   SrtInputDevice *device = SRT_INPUT_DEVICE (mock_device);
   g_auto(GStrv) udev_properties = NULL;
   g_autofree gchar *uevent = NULL;
+  g_autofree gchar *hid_uevent = NULL;
+  g_autofree gchar *input_uevent = NULL;
+  g_autofree gchar *usb_uevent = NULL;
 
   mock_device->dev_node = g_strdup ("/dev/input/event0");
   mock_device->sys_path = g_strdup ("/sys/devices/mock/usb/hid/input/input0/event0");
@@ -113,6 +116,15 @@ test_input_device_usb (Fixture *f,
   mock_device->udev_properties[1] = NULL;
   mock_device->uevent = g_strdup ("A=a\nB=b\n");
 
+  mock_device->hid_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid");
+  mock_device->hid_ancestor.uevent = g_strdup ("HID=yes\n");
+
+  mock_device->input_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid/input");
+  mock_device->input_ancestor.uevent = g_strdup ("INPUT=yes\n");
+
+  mock_device->usb_device_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb");
+  mock_device->usb_device_ancestor.uevent = g_strdup ("USB=usb_device\n");
+
   /* TODO: Fill in somewhat realistic details for a USB-attached
    * Steam Controller */
 
@@ -125,6 +137,21 @@ test_input_device_usb (Fixture *f,
   uevent = srt_input_device_dup_uevent (device);
   g_assert_cmpstr (uevent, ==, "A=a\nB=b\n");
 
+  g_assert_cmpstr (srt_input_device_get_hid_sys_path (device), ==,
+                   "/sys/devices/mock/usb/hid");
+  hid_uevent = srt_input_device_dup_hid_uevent (device);
+  g_assert_cmpstr (hid_uevent, ==, "HID=yes\n");
+
+  g_assert_cmpstr (srt_input_device_get_input_sys_path (device), ==,
+                   "/sys/devices/mock/usb/hid/input");
+  input_uevent = srt_input_device_dup_input_uevent (device);
+  g_assert_cmpstr (input_uevent, ==, "INPUT=yes\n");
+
+  g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (device), ==,
+                   "/sys/devices/mock/usb");
+  usb_uevent = srt_input_device_dup_usb_device_uevent (device);
+  g_assert_cmpstr (usb_uevent, ==, "USB=usb_device\n");
+
   udev_properties = srt_input_device_dup_udev_properties (device);
   g_assert_nonnull (udev_properties);
   g_assert_cmpstr (udev_properties[0], ==, "ID_INPUT_JOYSTICK=1");
@@ -164,6 +191,9 @@ device_added_cb (SrtInputDeviceMonitor *monitor,
   if (f->config->type == MOCK)
     {
       g_autofree gchar *uevent = NULL;
+      g_autofree gchar *hid_uevent = NULL;
+      g_autofree gchar *input_uevent = NULL;
+      g_autofree gchar *usb_uevent = NULL;
       g_auto(GStrv) udev_properties = NULL;
 
       uevent = srt_input_device_dup_uevent (device);
@@ -174,6 +204,21 @@ device_added_cb (SrtInputDeviceMonitor *monitor,
       g_assert_cmpstr (udev_properties[0], ==, "ID_INPUT_JOYSTICK=1");
       g_assert_cmpstr (udev_properties[1], ==, NULL);
 
+      g_assert_cmpstr (srt_input_device_get_hid_sys_path (device), ==,
+                       "/sys/devices/mock/usb/hid");
+      hid_uevent = srt_input_device_dup_hid_uevent (device);
+      g_assert_cmpstr (hid_uevent, ==, "HID=yes\n");
+
+      g_assert_cmpstr (srt_input_device_get_input_sys_path (device), ==,
+                       "/sys/devices/mock/usb/hid/input");
+      input_uevent = srt_input_device_dup_input_uevent (device);
+      g_assert_cmpstr (input_uevent, ==, "INPUT=yes\n");
+
+      g_assert_cmpstr (srt_input_device_get_usb_device_sys_path (device), ==,
+                       "/sys/devices/mock/usb");
+      usb_uevent = srt_input_device_dup_usb_device_uevent (device);
+      g_assert_cmpstr (usb_uevent, ==, "USB=usb_device\n");
+
       g_ptr_array_add (f->log, g_steal_pointer (&message));
     }
 
diff --git a/tests/mock-input-device.c b/tests/mock-input-device.c
index cbbc05824e3f0fd8bfb639512d010c450f973ae3..3a2bda45d618db18b1c6b561787dc50c67a9591d 100644
--- a/tests/mock-input-device.c
+++ b/tests/mock-input-device.c
@@ -66,6 +66,15 @@ mock_input_device_finalize (GObject *object)
   g_clear_pointer (&self->udev_properties, g_strfreev);
   g_clear_pointer (&self->uevent, g_free);
 
+  g_clear_pointer (&self->hid_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->hid_ancestor.uevent, g_free);
+
+  g_clear_pointer (&self->input_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->input_ancestor.uevent, g_free);
+
+  g_clear_pointer (&self->usb_device_ancestor.sys_path, g_free);
+  g_clear_pointer (&self->usb_device_ancestor.uevent, g_free);
+
   G_OBJECT_CLASS (mock_input_device_parent_class)->finalize (object);
 }
 
@@ -117,6 +126,54 @@ mock_input_device_dup_uevent (SrtInputDevice *device)
   return g_strdup (self->uevent);
 }
 
+static const char *
+mock_input_device_get_hid_sys_path (SrtInputDevice *device)
+{
+  MockInputDevice *self = MOCK_INPUT_DEVICE (device);
+
+  return self->hid_ancestor.sys_path;
+}
+
+static gchar *
+mock_input_device_dup_hid_uevent (SrtInputDevice *device)
+{
+  MockInputDevice *self = MOCK_INPUT_DEVICE (device);
+
+  return g_strdup (self->hid_ancestor.uevent);
+}
+
+static const char *
+mock_input_device_get_input_sys_path (SrtInputDevice *device)
+{
+  MockInputDevice *self = MOCK_INPUT_DEVICE (device);
+
+  return self->input_ancestor.sys_path;
+}
+
+static gchar *
+mock_input_device_dup_input_uevent (SrtInputDevice *device)
+{
+  MockInputDevice *self = MOCK_INPUT_DEVICE (device);
+
+  return g_strdup (self->input_ancestor.uevent);
+}
+
+static const char *
+mock_input_device_get_usb_device_sys_path (SrtInputDevice *device)
+{
+  MockInputDevice *self = MOCK_INPUT_DEVICE (device);
+
+  return self->usb_device_ancestor.sys_path;
+}
+
+static gchar *
+mock_input_device_dup_usb_device_uevent (SrtInputDevice *device)
+{
+  MockInputDevice *self = MOCK_INPUT_DEVICE (device);
+
+  return g_strdup (self->usb_device_ancestor.uevent);
+}
+
 static void
 mock_input_device_iface_init (SrtInputDeviceInterface *iface)
 {
@@ -128,6 +185,13 @@ mock_input_device_iface_init (SrtInputDeviceInterface *iface)
   IMPLEMENT (dup_udev_properties);
   IMPLEMENT (dup_uevent);
 
+  IMPLEMENT (get_hid_sys_path);
+  IMPLEMENT (dup_hid_uevent);
+  IMPLEMENT (get_input_sys_path);
+  IMPLEMENT (dup_input_uevent);
+  IMPLEMENT (get_usb_device_sys_path);
+  IMPLEMENT (dup_usb_device_uevent);
+
 #undef IMPLEMENT
 }
 
@@ -283,6 +347,15 @@ add_steam_controller (MockInputDeviceMonitor *self,
   device->subsystem = g_strdup ("input");
   device->uevent = g_strdup ("ONE=1\nTWO=2\n");
 
+  device->hid_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid");
+  device->hid_ancestor.uevent = g_strdup ("HID=yes\n");
+
+  device->input_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb/hid/input");
+  device->input_ancestor.uevent = g_strdup ("INPUT=yes\n");
+
+  device->usb_device_ancestor.sys_path = g_strdup ("/sys/devices/mock/usb");
+  device->usb_device_ancestor.uevent = g_strdup ("USB=usb_device\n");
+
   g_ptr_array_add (arr, g_strdup ("ID_INPUT_JOYSTICK=1"));
   g_ptr_array_add (arr, NULL);
   device->udev_properties = (gchar **) g_ptr_array_free (g_steal_pointer (&arr), FALSE);
diff --git a/tests/mock-input-device.h b/tests/mock-input-device.h
index 1f73593d79ab1ee1970f233b15344112ec532265..b3255af3760896ac2b407ae916e9f5af6538940b 100644
--- a/tests/mock-input-device.h
+++ b/tests/mock-input-device.h
@@ -54,6 +54,24 @@ struct _MockInputDevice
   gchar *subsystem;
   gchar **udev_properties;
   gchar *uevent;
+
+  struct
+  {
+    gchar *sys_path;
+    gchar *uevent;
+  } hid_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+    gchar *uevent;
+  } input_ancestor;
+
+  struct
+  {
+    gchar *sys_path;
+    gchar *uevent;
+  } usb_device_ancestor;
 };
 
 struct _MockInputDeviceClass
diff --git a/tests/utils.c b/tests/utils.c
index e4e22b5bdba507894a1136d626e058693d6468de..569d274d48730835f44fe5e38fb0ce99987f0a97 100644
--- a/tests/utils.c
+++ b/tests/utils.c
@@ -29,6 +29,7 @@
 #include <glib/gstdio.h>
 #include <glib-object.h>
 
+#include "steam-runtime-tools/input-device-internal.h"
 #include "steam-runtime-tools/utils-internal.h"
 #include "test-utils.h"
 
@@ -229,6 +230,64 @@ test_str_is_integer (Fixture *f,
   g_assert_false (_srt_str_is_integer ("23a"));
 }
 
+static const char uevent[] =
+"DRIVER=lenovo\n"
+"HID_ID=0003:000017EF:00006009\n"
+"HID_NAME=Lite-On Technology Corp. ThinkPad USB Keyboard with TrackPoint\n"
+"HID_PHYS=usb-0000:00:14.0-2/input0\n"
+"HID_UNIQ=\n"
+"MODALIAS=hid:b0003g0000v000017EFp00006009\n";
+
+static struct
+{
+  const char *key;
+  const char *value;
+} uevent_parsed[] =
+{
+  { "DRIVER", "lenovo" },
+  { "HID_ID", "0003:000017EF:00006009" },
+  { "HID_NAME", "Lite-On Technology Corp. ThinkPad USB Keyboard with TrackPoint" },
+  { "HID_PHYS", "usb-0000:00:14.0-2/input0" },
+  { "HID_UNIQ", "" },
+  { "MODALIAS", "hid:b0003g0000v000017EFp00006009" }
+};
+
+static const char no_newline[] = "DRIVER=lenovo";
+
+static void
+test_uevent_field (Fixture *f,
+                   gconstpointer context)
+{
+  gsize i;
+
+  g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", ""));
+  g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenov"));
+  g_assert_true (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenovo"));
+  g_assert_false (_srt_input_device_uevent_field_equals (no_newline, "DRIVER", "lenovoo"));
+
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "DRIVER", "lenov"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "DRIVER", "lenovoo"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_ID", "0003:000017EF:0000600"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_ID", "0003:000017EF:000060099"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "HID_UNIQ", "x"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "MODALIAS", "nope"));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "NOPE", ""));
+  g_assert_false (_srt_input_device_uevent_field_equals (uevent, "NOPE", "nope"));
+
+  for (i = 0; i < G_N_ELEMENTS (uevent_parsed); i++)
+    {
+      g_autofree gchar *v = NULL;
+
+      v = _srt_input_device_uevent_field (uevent, uevent_parsed[i].key);
+      g_assert_cmpstr (v, ==, uevent_parsed[i].value);
+      g_assert_true (_srt_input_device_uevent_field_equals (uevent,
+                                                            uevent_parsed[i].key,
+                                                            uevent_parsed[i].value));
+    }
+
+  g_assert_null (_srt_input_device_uevent_field (uevent, "NOPE"));
+}
+
 int
 main (int argc,
       char **argv)
@@ -243,6 +302,8 @@ main (int argc,
               filter_gameoverlayrenderer, teardown);
   g_test_add ("/utils/str_is_integer", Fixture, NULL,
               setup, test_str_is_integer, teardown);
+  g_test_add ("/utils/uevent-field", Fixture, NULL,
+              setup, test_uevent_field, teardown);
 
   return g_test_run ();
 }