diff --git a/include/libcamera/internal/camera_sensor.h b/include/libcamera/internal/camera_sensor.h
index 2a2f1f2c936d..17080ba32d37 100644
--- a/include/libcamera/internal/camera_sensor.h
+++ b/include/libcamera/internal/camera_sensor.h
@@ -92,6 +92,7 @@ public:
 	virtual ~CameraSensorFactoryBase() = default;
 
 	static std::unique_ptr<CameraSensor> create(MediaEntity *entity);
+	static std::string generateId(const MediaEntity *entity);
 
 	const std::string &name() const { return name_; }
 	int priority() const { return priority_; }
diff --git a/src/libcamera/sensor/camera_sensor.cpp b/src/libcamera/sensor/camera_sensor.cpp
index 580d3626216c..05654354ea1b 100644
--- a/src/libcamera/sensor/camera_sensor.cpp
+++ b/src/libcamera/sensor/camera_sensor.cpp
@@ -13,6 +13,7 @@
 #include <libcamera/base/log.h>
 
 #include "libcamera/internal/media_object.h"
+#include "libcamera/internal/sysfs.h"
 
 /**
  * \file camera_sensor.h
@@ -474,6 +475,32 @@ std::unique_ptr<CameraSensor> CameraSensorFactoryBase::create(MediaEntity *entit
 	return nullptr;
 }
 
+/**
+ * \brief Generate a unique ID for a camera sensor
+ * \param[in] entity The media entity that corresponds to the camera sensor
+ *
+ * The ID is derived from the firmware description (device tree or ACPI) of the
+ * device associated with the sensor's media \a entity. It is unique and stable
+ * as long as the system firmware is not modified. Sensors that the firmware
+ * does not describe have no ID.
+ *
+ * The ID is computed from sysfs only, using the device numbers stored in the
+ * media graph, without opening or otherwise accessing the sensor device.
+ *
+ * \sa sysfs::firmwareNodePath()
+ *
+ * \return The sensor ID on success or an empty string on failure
+ */
+std::string CameraSensorFactoryBase::generateId(const MediaEntity *entity)
+{
+	const std::string devPath = sysfs::devicePath(entity->deviceMajor(),
+						      entity->deviceMinor());
+	if (devPath.empty())
+		return {};
+
+	return sysfs::firmwareNodePath(devPath);
+}
+
 /**
  * \fn CameraSensorFactoryBase::name()
  * \brief Retrieve the camera sensor factory name
diff --git a/src/libcamera/sensor/camera_sensor_legacy.cpp b/src/libcamera/sensor/camera_sensor_legacy.cpp
index 83e2c2593215..99661f6fd02e 100644
--- a/src/libcamera/sensor/camera_sensor_legacy.cpp
+++ b/src/libcamera/sensor/camera_sensor_legacy.cpp
@@ -302,10 +302,8 @@ int CameraSensorLegacy::init()
 
 int CameraSensorLegacy::generateId()
 {
-	const std::string devPath = subdev_->devicePath();
-
 	/* Try to get ID from firmware description. */
-	id_ = sysfs::firmwareNodePath(devPath);
+	id_ = CameraSensorFactoryBase::generateId(entity_);
 	if (!id_.empty())
 		return 0;
 
@@ -315,6 +313,8 @@ int CameraSensorLegacy::generateId()
 	 * Verify it's a platform device and construct ID from the device path
 	 * and model of sensor.
 	 */
+	const std::string devPath = sysfs::devicePath(entity_->deviceMajor(),
+						      entity_->deviceMinor());
 	if (devPath.find("/sys/devices/platform/", 0) == 0) {
 		id_ = devPath.substr(strlen("/sys/devices/")) + " " + model();
 		return 0;
diff --git a/src/libcamera/sensor/camera_sensor_raw.cpp b/src/libcamera/sensor/camera_sensor_raw.cpp
index 6344a34fc4c5..423b14af2890 100644
--- a/src/libcamera/sensor/camera_sensor_raw.cpp
+++ b/src/libcamera/sensor/camera_sensor_raw.cpp
@@ -37,7 +37,6 @@
 #include "libcamera/internal/camera_sensor_properties.h"
 #include "libcamera/internal/formats.h"
 #include "libcamera/internal/media_device.h"
-#include "libcamera/internal/sysfs.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 
 namespace libcamera {
@@ -571,7 +570,7 @@ int CameraSensorRaw::initProperties()
 	properties_.set(properties::Model, utils::toAscii(model_));
 
 	/* Generate a unique ID for the sensor. */
-	id_ = sysfs::firmwareNodePath(subdev_->devicePath());
+	id_ = CameraSensorFactoryBase::generateId(entity_);
 	if (id_.empty()) {
 		LOG(CameraSensor, Error) << "Can't generate sensor ID";
 		return -EINVAL;
