diff --git a/src/meson.build b/src/meson.build
index 09bb70a..f53ae92 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -40,6 +40,8 @@ upowerd_private = static_library('upowerd-private',
'up-enumerator.h',
'up-kbd-backlight.h',
'up-kbd-backlight.c',
+ 'up-device-kbd-backlight.c',
+ 'up-device-kbd-backlight.h',
'up-history.h',
'up-history.c',
'up-backend.h',
diff --git a/src/up-device-kbd-backlight.c b/src/up-device-kbd-backlight.c
new file mode 100644
index 0000000..b5944ab
--- /dev/null
+++ b/src/up-device-kbd-backlight.c
@@ -0,0 +1,410 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Kate Hsuan
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include "up-native.h"
+#include "up-device-kbd-backlight.h"
+#include "up-stats-item.h"
+
+typedef struct
+{
+ UpDaemon *daemon;
+ GObject *native;
+} UpDeviceKbdBacklightPrivate;
+
+static void up_device_kbd_backlight_initable_iface_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_EXTENDED (UpDeviceKbdBacklight, up_device_kbd_backlight, UP_TYPE_EXPORTED_KBD_BACKLIGHT_SKELETON, 0,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+ up_device_kbd_backlight_initable_iface_init)
+ G_ADD_PRIVATE (UpDeviceKbdBacklight))
+
+enum {
+ PROP_0,
+ PROP_DAEMON,
+ PROP_NATIVE,
+ N_PROPS
+};
+
+/* Upower keeps the legacy keyboard backlight DBus API (/org/freedesktop/UPower/KbdBacklight/)
+ * for backward compatibility.
+ * The new API path is based on the legacy one and the suffix is the name of the keyboard
+ * backlight device. For example, the path of the keyboard backlight device
+ * "tpacpiookbd_backlight" is
+ * "/org/freedesktop/UPower/KbdBacklight/tpacpiookbd_backlight".*/
+#define UP_DEVICES_KBD_BACKLIGHT_DBUS_PATH "/org/freedesktop/UPower/KbdBacklight"
+static GParamSpec *properties[N_PROPS];
+
+/**
+ * up_kbd_backlight_emit_change:
+ **/
+void
+up_device_kbd_backlight_emit_change(UpDeviceKbdBacklight *kbd_backlight, int value, const char *source)
+{
+ up_exported_kbd_backlight_emit_brightness_changed (UP_EXPORTED_KBD_BACKLIGHT (kbd_backlight), value);
+ up_exported_kbd_backlight_emit_brightness_changed_with_source (UP_EXPORTED_KBD_BACKLIGHT (kbd_backlight), value, source);
+}
+
+
+/**
+ * up_kbd_backlight_get_brightness:
+ *
+ * Gets the current brightness
+ **/
+static gboolean
+up_kbd_backlight_get_brightness (UpExportedKbdBacklight *skeleton,
+ GDBusMethodInvocation *invocation,
+ UpDeviceKbdBacklight *kbd_backlight)
+{
+ UpDeviceKbdBacklightClass *klass;
+ gint brightness = 0;
+
+ g_return_val_if_fail (UP_IS_DEVICE_KBD_BACKLIGHT (kbd_backlight), FALSE);
+
+ klass = UP_DEVICE_KBD_BACKLIGHT_GET_CLASS (kbd_backlight);
+
+ brightness = klass->get_brightness (kbd_backlight);
+
+ if (brightness >= 0) {
+ up_exported_kbd_backlight_complete_get_brightness (skeleton, invocation,
+ brightness);
+ } else {
+ g_dbus_method_invocation_return_error (invocation,
+ UP_DAEMON_ERROR, UP_DAEMON_ERROR_GENERAL,
+ "error reading brightness");
+ }
+
+ return TRUE;
+}
+
+
+/**
+ * up_kbd_backlight_get_max_brightness:
+ *
+ * Gets the max brightness
+ **/
+static gboolean
+up_kbd_backlight_get_max_brightness (UpExportedKbdBacklight *skeleton,
+ GDBusMethodInvocation *invocation,
+ UpDeviceKbdBacklight *kbd_backlight)
+{
+ UpDeviceKbdBacklightClass *klass;
+ gint brightness = -1;
+
+ g_return_val_if_fail (UP_IS_DEVICE_KBD_BACKLIGHT (kbd_backlight), FALSE);
+
+ klass = UP_DEVICE_KBD_BACKLIGHT_GET_CLASS (kbd_backlight);
+
+ if (klass->get_max_brightness != NULL)
+ brightness = klass->get_max_brightness (kbd_backlight);
+
+ if (brightness >= 0) {
+ up_exported_kbd_backlight_complete_get_max_brightness (skeleton, invocation,
+ brightness);
+ } else {
+ g_dbus_method_invocation_return_error (invocation,
+ UP_DAEMON_ERROR, UP_DAEMON_ERROR_GENERAL,
+ "error reading max brightness");
+ }
+
+ return TRUE;
+}
+
+
+/**
+ * up_kbd_backlight_set_brightness:
+ *
+ * Sets the kbd backlight LED brightness.
+ **/
+static gboolean
+up_kbd_backlight_set_brightness (UpExportedKbdBacklight *skeleton,
+ GDBusMethodInvocation *invocation,
+ gint value,
+ UpDeviceKbdBacklight *kbd_backlight)
+{
+ UpDeviceKbdBacklightClass *klass;
+ gboolean ret = FALSE;
+
+ g_return_val_if_fail (UP_IS_DEVICE_KBD_BACKLIGHT (kbd_backlight), FALSE);
+
+ klass = UP_DEVICE_KBD_BACKLIGHT_GET_CLASS (kbd_backlight);
+
+ if (klass->set_brightness == NULL) {
+ g_dbus_method_invocation_return_error (invocation,
+ UP_DAEMON_ERROR, UP_DAEMON_ERROR_GENERAL,
+ "setting brightness is unsupported");
+ return TRUE;
+ }
+ ret = klass->set_brightness (kbd_backlight, value);
+
+ if (ret) {
+ up_exported_kbd_backlight_complete_set_brightness (skeleton, invocation);
+ up_device_kbd_backlight_emit_change (kbd_backlight, value, "external");
+ } else {
+ g_dbus_method_invocation_return_error (invocation,
+ UP_DAEMON_ERROR, UP_DAEMON_ERROR_GENERAL,
+ "error writing brightness %d", value);
+ }
+
+ return TRUE;
+}
+
+GObject *
+up_device_kbd_backlight_get_native (UpDeviceKbdBacklight *device)
+{
+ UpDeviceKbdBacklightPrivate *priv = up_device_kbd_backlight_get_instance_private (device);
+ g_return_val_if_fail (UP_IS_DEVICE_KBD_BACKLIGHT (device), NULL);
+ return priv->native;
+}
+
+static gchar *
+up_device_kbd_backlight_compute_object_path (UpDeviceKbdBacklight *device)
+{
+ UpDeviceKbdBacklightPrivate *priv = up_device_kbd_backlight_get_instance_private (device);
+ g_autofree gchar *basename = NULL;
+ g_autofree gchar *id = NULL;
+ gchar *object_path;
+ const gchar *native_path;
+ guint i;
+
+ if (priv->native == NULL) {
+ return g_build_filename (UP_DEVICES_KBD_BACKLIGHT_DBUS_PATH, "KbdBacklight", NULL);
+ }
+
+ native_path = up_exported_kbd_backlight_get_native_path (UP_EXPORTED_KBD_BACKLIGHT (device));
+ basename = g_path_get_basename (native_path);
+ id = g_strjoin ("_", basename, NULL);
+
+ /* make DBUS valid path */
+ for (i=0; id[i] != '\0'; i++) {
+ if (id[i] == '-')
+ id[i] = '_';
+ if (id[i] == '.')
+ id[i] = 'x';
+ if (id[i] == ':')
+ id[i] = 'o';
+ if (id[i] == '@')
+ id[i] = '_';
+ }
+ object_path = g_build_filename (UP_DEVICES_KBD_BACKLIGHT_DBUS_PATH, id, NULL);
+
+ return object_path;
+}
+
+static void
+up_device_kbd_backlight_export_skeleton (UpDeviceKbdBacklight *device,
+ const gchar *object_path)
+{
+ UpDeviceKbdBacklightPrivate *priv = up_device_kbd_backlight_get_instance_private (device);
+ GError *error = NULL;
+
+ g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (device),
+ g_dbus_interface_skeleton_get_connection (G_DBUS_INTERFACE_SKELETON (priv->daemon)),
+ object_path,
+ &error);
+
+ if (error != NULL) {
+ g_critical ("error registering device on system bus: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+gboolean
+up_device_kbd_backlight_register (UpDeviceKbdBacklight *device)
+{
+ g_autofree char *computed_object_path = NULL;
+
+ if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (device)) != NULL)
+ return FALSE;
+ computed_object_path = up_device_kbd_backlight_compute_object_path (device);
+ g_debug ("Exported Keyboard backlight with path %s", computed_object_path);
+ up_device_kbd_backlight_export_skeleton (device, computed_object_path);
+ return TRUE;
+}
+
+void
+up_device_kbd_backlight_unregister (UpDeviceKbdBacklight *device)
+{
+ g_autofree char *object_path = NULL;
+
+ object_path = g_strdup (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (device)));
+ if (object_path != NULL) {
+ g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (device));
+ g_debug ("Unexported UpDeviceKbdBacklight with path %s", object_path);
+ }
+}
+
+const gchar *
+up_device_kbd_backlight_get_object_path (UpDeviceKbdBacklight *device)
+{
+ g_return_val_if_fail (UP_IS_DEVICE_KBD_BACKLIGHT (device), NULL);
+ return g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (device));
+}
+
+static void
+up_device_kbd_backlight_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ UpDeviceKbdBacklight *device = UP_DEVICE_KBD_BACKLIGHT (object);
+ UpDeviceKbdBacklightPrivate *priv = up_device_kbd_backlight_get_instance_private (device);
+
+ switch (prop_id)
+ {
+ case PROP_DAEMON:
+ priv->daemon = g_value_dup_object (value);
+ break;
+
+ case PROP_NATIVE:
+ priv->native = g_value_dup_object (value);
+ if (priv->native == NULL)
+ g_warning ("KBD native is NULL");
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+up_device_kbd_backlight_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (prop_id)
+ {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static gboolean
+up_device_kbd_backlight_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ UpDeviceKbdBacklight *device = UP_DEVICE_KBD_BACKLIGHT (initable);
+ UpDeviceKbdBacklightPrivate *priv = up_device_kbd_backlight_get_instance_private (device);
+ const gchar *native_path = NULL;
+ UpDeviceKbdBacklightClass *klass = UP_DEVICE_KBD_BACKLIGHT_GET_CLASS (device);
+ int ret;
+
+ g_return_val_if_fail (UP_IS_DEVICE_KBD_BACKLIGHT (device), FALSE);
+
+ if (priv->native) {
+ native_path = up_native_get_native_path (priv->native);
+ up_exported_kbd_backlight_set_native_path (UP_EXPORTED_KBD_BACKLIGHT (device), native_path);
+ }
+
+ /* coldplug source */
+ if (klass->coldplug != NULL) {
+ ret = klass->coldplug (device);
+ if (!ret) {
+ g_debug ("failed to coldplug %s", native_path);
+ g_propagate_error (error, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Failed to coldplug %s", native_path));
+
+ return FALSE;
+ }
+ }
+
+ up_device_kbd_backlight_register (device);
+
+ return TRUE;
+}
+
+static void
+up_device_kbd_backlight_initable_iface_init (GInitableIface *iface)
+{
+ iface->init = up_device_kbd_backlight_initable_init;
+}
+
+/**
+ * up_kbd_backlight_init:
+ **/
+static void
+up_device_kbd_backlight_init (UpDeviceKbdBacklight *kbd_backlight)
+{
+ g_signal_connect (kbd_backlight, "handle-get-brightness",
+ G_CALLBACK (up_kbd_backlight_get_brightness), kbd_backlight);
+ g_signal_connect (kbd_backlight, "handle-get-max-brightness",
+ G_CALLBACK (up_kbd_backlight_get_max_brightness), kbd_backlight);
+ g_signal_connect (kbd_backlight, "handle-set-brightness",
+ G_CALLBACK (up_kbd_backlight_set_brightness), kbd_backlight);
+}
+
+/**
+ * up_kbd_backlight_finalize:
+ **/
+static void
+up_device_kbd_backlight_finalize (GObject *object)
+{
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (UP_IS_DEVICE_KBD_BACKLIGHT (object));
+
+ G_OBJECT_CLASS (up_device_kbd_backlight_parent_class)->finalize (object);
+}
+
+static void
+up_device_kbd_backlight_class_init (UpDeviceKbdBacklightClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = up_device_kbd_backlight_finalize;
+
+ object_class->set_property = up_device_kbd_backlight_set_property;
+ object_class->get_property = up_device_kbd_backlight_get_property;
+
+ properties[PROP_DAEMON] =
+ g_param_spec_object ("daemon",
+ "UpDaemon",
+ "UpDaemon reference",
+ UP_TYPE_DAEMON,
+ G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+
+ properties[PROP_NATIVE] =
+ g_param_spec_object ("native",
+ "Native",
+ "Native Object",
+ G_TYPE_OBJECT,
+ G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+/**
+ * up_kbd_backlight_new:
+ **/
+UpDeviceKbdBacklight *
+up_device_kbd_backlight_new (UpDaemon *daemon, GObject *native)
+{
+ return UP_DEVICE_KBD_BACKLIGHT (g_object_new (UP_TYPE_DEVICE_KBD_BACKLIGHT,
+ "daemon", daemon,
+ "native", native,
+ NULL));
+}
diff --git a/src/up-device-kbd-backlight.h b/src/up-device-kbd-backlight.h
new file mode 100644
index 0000000..786649e
--- /dev/null
+++ b/src/up-device-kbd-backlight.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Kate Hsuan
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __UP_DEVICE_KBD_BACKLIGHT_H__
+#define __UP_DEVICE_KBD_BACKLIGHT_H__
+
+#include
+#include "up-daemon.h"
+
+G_BEGIN_DECLS
+
+#define UP_TYPE_DEVICE_KBD_BACKLIGHT (up_device_kbd_backlight_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (UpDeviceKbdBacklight, up_device_kbd_backlight, UP, DEVICE_KBD_BACKLIGHT, UpExportedKbdBacklightSkeleton)
+
+struct _UpDeviceKbdBacklightClass
+{
+ UpExportedKbdBacklightSkeletonClass parent_class;
+
+ gboolean (*coldplug) (UpDeviceKbdBacklight *device);
+
+ gint (*get_max_brightness) (UpDeviceKbdBacklight *device);
+ gint (*get_brightness) (UpDeviceKbdBacklight *device);
+ gboolean (*set_brightness) (UpDeviceKbdBacklight *device, gint brightness);
+
+};
+
+
+GType up_device_kbd_backlight_get_type (void);
+
+void up_device_kbd_backlight_emit_change (UpDeviceKbdBacklight *kbd_backlight,
+ int value,
+ const char *source);
+const gchar *up_device_kbd_backlight_get_object_path (UpDeviceKbdBacklight *device);
+GObject *up_device_kbd_backlight_get_native (UpDeviceKbdBacklight *device);
+UpDeviceKbdBacklight *up_device_kbd_backlight_new (UpDaemon *daemon,
+ GObject *native);
+gboolean up_device_kbd_backlight_register (UpDeviceKbdBacklight *device);
+void up_device_kbd_backlight_unregister (UpDeviceKbdBacklight *device);
+
+G_END_DECLS
+
+#endif /* __UP_DEVICE_KBD_BACKLIGHT_H__ */