diff --git a/src/py/libcamera/py_camera_manager.cpp b/src/py/libcamera/py_camera_manager.cpp
index 3dd8668e..599a9f7e 100644
--- a/src/py/libcamera/py_camera_manager.cpp
+++ b/src/py/libcamera/py_camera_manager.cpp
@@ -58,6 +58,7 @@ py::list PyCameraManager::cameras()
 	return l;
 }
 
+/* DEPRECATED */
 std::vector<py::object> PyCameraManager::getReadyRequests()
 {
 	int ret = readFd();
@@ -70,21 +71,234 @@ std::vector<py::object> PyCameraManager::getReadyRequests()
 
 	std::vector<py::object> py_reqs;
 
-	for (Request *request : getCompletedRequests()) {
-		py::object o = py::cast(request);
-		/* Decrease the ref increased in Camera.queue_request() */
-		o.dec_ref();
-		py_reqs.push_back(o);
+	for (const auto &ev : getEvents()) {
+		switch (ev.type_) {
+		case CameraEvent::Type::RequestCompleted: {
+			py::object o = py::cast(ev.request_);
+			/* Decrease the ref increased in Camera.queue_request() */
+			o.dec_ref();
+			py_reqs.push_back(o);
+		}
+		default:
+			/* ignore */
+			break;
+		}
 	}
 
 	return py_reqs;
 }
 
 /* Note: Called from another thread */
-void PyCameraManager::handleRequestCompleted(Request *req)
+void PyCameraManager::handleBufferCompleted(std::shared_ptr<Camera> cam, Request *req, FrameBuffer *fb)
 {
-	pushRequest(req);
-	writeFd();
+	CameraEvent ev(CameraEvent::Type::BufferCompleted, cam);
+	ev.request_ = req;
+	ev.fb_ = fb;
+
+	pushEvent(ev);
+}
+
+/* Note: Called from another thread */
+void PyCameraManager::handleRequestCompleted(std::shared_ptr<Camera> cam, Request *req)
+{
+	CameraEvent ev(CameraEvent::Type::RequestCompleted, cam);
+	ev.request_ = req;
+
+	pushEvent(ev);
+}
+
+/* Note: Called from another thread */
+void PyCameraManager::handleDisconnected(std::shared_ptr<Camera> cam)
+{
+	CameraEvent ev(CameraEvent::Type::Disconnect, cam);
+
+	pushEvent(ev);
+}
+
+/* Note: Called from another thread */
+void PyCameraManager::handleCameraAdded(std::shared_ptr<Camera> cam)
+{
+	CameraEvent ev(CameraEvent::Type::CameraAdded, cam);
+
+	pushEvent(ev);
+}
+
+/* Note: Called from another thread */
+void PyCameraManager::handleCameraRemoved(std::shared_ptr<Camera> cam)
+{
+	CameraEvent ev(CameraEvent::Type::CameraRemoved, cam);
+
+	pushEvent(ev);
+}
+
+void PyCameraManager::dispatchEvents()
+{
+	int ret = readFd();
+
+	if (ret == EAGAIN) {
+		LOG(Python, Debug) << "No events to dispatch";
+		return;
+	}
+
+	if (ret != 0)
+		throw std::system_error(ret, std::generic_category());
+
+	std::vector<CameraEvent> events = getEvents();
+
+	LOG(Python, Debug) << "Dispatch " << events.size() << " events";
+
+	for (const auto &event : events) {
+		std::shared_ptr<Camera> camera = event.camera_;
+
+		switch (event.type_) {
+		case CameraEvent::Type::CameraAdded: {
+			if (cameraAddedHandler_)
+				cameraAddedHandler_(camera);
+
+			break;
+		}
+		case CameraEvent::Type::CameraRemoved: {
+			if (cameraRemovedHandler_)
+				cameraRemovedHandler_(camera);
+
+			break;
+		}
+		case CameraEvent::Type::BufferCompleted: {
+			auto cb = getBufferCompleted(camera.get());
+			if (cb)
+				cb(camera, event.request_, event.fb_);
+
+			break;
+		}
+		case CameraEvent::Type::RequestCompleted: {
+			auto cb = getRequestCompleted(camera.get());
+			if (cb)
+				cb(camera, event.request_);
+
+			/* Decrease the ref increased in Camera.queue_request() */
+			py::object o = py::cast(event.request_);
+			o.dec_ref();
+
+			break;
+		}
+		case CameraEvent::Type::Disconnect: {
+			auto cb = getDisconnected(camera.get());
+			if (cb)
+				cb(camera);
+
+			break;
+		}
+		default:
+			ASSERT(false);
+		}
+	}
+}
+
+void PyCameraManager::discardEvents()
+{
+	int ret = readFd();
+
+	if (ret == EAGAIN)
+		return;
+
+	if (ret != 0)
+		throw std::system_error(ret, std::generic_category());
+
+	std::vector<CameraEvent> v = getEvents();
+
+	LOG(Python, Debug) << "Discard " << v.size() << " events";
+
+	for (const auto &ev : v) {
+		if (ev.type_ != CameraEvent::Type::RequestCompleted)
+			continue;
+
+		/* Decrease the ref increased in Camera.queue_request() */
+		py::object o = py::cast(ev.request_);
+		o.dec_ref();
+	}
+}
+
+std::function<void(std::shared_ptr<Camera>)> PyCameraManager::getCameraAdded() const
+{
+	return cameraAddedHandler_;
+}
+
+void PyCameraManager::setCameraAdded(std::function<void(std::shared_ptr<Camera>)> func)
+{
+	if (cameraAddedHandler_)
+		cameraManager_->cameraAdded.disconnect();
+
+	cameraAddedHandler_ = func;
+
+	if (func)
+		cameraManager_->cameraAdded.connect(this, &PyCameraManager::handleCameraAdded);
+}
+
+std::function<void(std::shared_ptr<Camera>)> PyCameraManager::getCameraRemoved() const
+{
+	return cameraRemovedHandler_;
+}
+
+void PyCameraManager::setCameraRemoved(std::function<void(std::shared_ptr<Camera>)> func)
+{
+	if (cameraRemovedHandler_)
+		cameraManager_->cameraRemoved.disconnect();
+
+	cameraRemovedHandler_ = func;
+
+	if (func)
+		cameraManager_->cameraRemoved.connect(this, &PyCameraManager::handleCameraRemoved);
+}
+
+std::function<void(std::shared_ptr<Camera>, Request *)> PyCameraManager::getRequestCompleted(Camera *cam)
+{
+	if (auto it = cameraRequestCompletedHandlers_.find(cam);
+	    it != cameraRequestCompletedHandlers_.end())
+		return it->second;
+
+	return nullptr;
+}
+
+void PyCameraManager::setRequestCompleted(Camera *cam, std::function<void(std::shared_ptr<Camera>, Request *)> func)
+{
+	if (func)
+		cameraRequestCompletedHandlers_[cam] = func;
+	else
+		cameraRequestCompletedHandlers_.erase(cam);
+}
+
+std::function<void(std::shared_ptr<Camera>, Request *, FrameBuffer *)> PyCameraManager::getBufferCompleted(Camera *cam)
+{
+	if (auto it = cameraBufferCompletedHandlers_.find(cam);
+	    it != cameraBufferCompletedHandlers_.end())
+		return it->second;
+
+	return nullptr;
+}
+
+void PyCameraManager::setBufferCompleted(Camera *cam, std::function<void(std::shared_ptr<Camera>, Request *, FrameBuffer *)> func)
+{
+	if (func)
+		cameraBufferCompletedHandlers_[cam] = func;
+	else
+		cameraBufferCompletedHandlers_.erase(cam);
+}
+
+std::function<void(std::shared_ptr<Camera>)> PyCameraManager::getDisconnected(Camera *cam)
+{
+	if (auto it = cameraDisconnectHandlers_.find(cam);
+	    it != cameraDisconnectHandlers_.end())
+		return it->second;
+
+	return nullptr;
+}
+
+void PyCameraManager::setDisconnected(Camera *cam, std::function<void(std::shared_ptr<Camera>)> func)
+{
+	if (func)
+		cameraDisconnectHandlers_[cam] = func;
+	else
+		cameraDisconnectHandlers_.erase(cam);
 }
 
 void PyCameraManager::writeFd()
@@ -110,16 +324,22 @@ int PyCameraManager::readFd()
 	return 0;
 }
 
-void PyCameraManager::pushRequest(Request *req)
+void PyCameraManager::pushEvent(const CameraEvent &ev)
 {
-	MutexLocker guard(completedRequestsMutex_);
-	completedRequests_.push_back(req);
+	MutexLocker guard(cameraEventsMutex_);
+	cameraEvents_.push_back(ev);
+
+	writeFd();
+
+	LOG(Python, Debug) << "Queued events: " << cameraEvents_.size();
 }
 
-std::vector<Request *> PyCameraManager::getCompletedRequests()
+std::vector<PyCameraManager::CameraEvent> PyCameraManager::getEvents()
 {
-	std::vector<Request *> v;
-	MutexLocker guard(completedRequestsMutex_);
-	swap(v, completedRequests_);
+	std::vector<CameraEvent> v;
+
+	MutexLocker guard(cameraEventsMutex_);
+	swap(v, cameraEvents_);
+
 	return v;
 }
diff --git a/src/py/libcamera/py_camera_manager.h b/src/py/libcamera/py_camera_manager.h
index 3525057d..aa51a6bc 100644
--- a/src/py/libcamera/py_camera_manager.h
+++ b/src/py/libcamera/py_camera_manager.h
@@ -30,16 +30,70 @@ public:
 
 	void handleRequestCompleted(Request *req);
 
+	void handleBufferCompleted(std::shared_ptr<Camera> cam, Request *req, FrameBuffer *fb);
+	void handleRequestCompleted(std::shared_ptr<Camera> cam, Request *req);
+	void handleDisconnected(std::shared_ptr<Camera> cam);
+	void handleCameraAdded(std::shared_ptr<Camera> cam);
+	void handleCameraRemoved(std::shared_ptr<Camera> cam);
+
+	void dispatchEvents();
+	void discardEvents();
+
+	std::function<void(std::shared_ptr<Camera>)> getCameraAdded() const;
+	void setCameraAdded(std::function<void(std::shared_ptr<Camera>)> func);
+
+	std::function<void(std::shared_ptr<Camera>)> getCameraRemoved() const;
+	void setCameraRemoved(std::function<void(std::shared_ptr<Camera>)> func);
+
+	std::function<void(std::shared_ptr<Camera>, Request *)> getRequestCompleted(Camera *cam);
+	void setRequestCompleted(Camera *cam, std::function<void(std::shared_ptr<Camera>, Request *)> func);
+
+	std::function<void(std::shared_ptr<Camera>, Request *, FrameBuffer *)> getBufferCompleted(Camera *cam);
+	void setBufferCompleted(Camera *cam, std::function<void(std::shared_ptr<Camera>, Request *, FrameBuffer *)> func);
+
+	std::function<void(std::shared_ptr<Camera>)> getDisconnected(Camera *cam);
+	void setDisconnected(Camera *cam, std::function<void(std::shared_ptr<Camera>)> func);
+
 private:
+	struct CameraEvent {
+		enum class Type {
+			Undefined = 0,
+			CameraAdded,
+			CameraRemoved,
+			Disconnect,
+			RequestCompleted,
+			BufferCompleted,
+		};
+
+		CameraEvent(Type type, std::shared_ptr<Camera> camera)
+			: type_(type), camera_(camera)
+		{
+		}
+
+		Type type_;
+
+		std::shared_ptr<Camera> camera_;
+
+		Request *request_ = nullptr;
+		FrameBuffer *fb_ = nullptr;
+	};
+
 	std::unique_ptr<CameraManager> cameraManager_;
 
 	UniqueFD eventFd_;
-	libcamera::Mutex completedRequestsMutex_;
-	std::vector<Request *> completedRequests_
-		LIBCAMERA_TSA_GUARDED_BY(completedRequestsMutex_);
+	libcamera::Mutex cameraEventsMutex_;
+	std::vector<CameraEvent> cameraEvents_
+		LIBCAMERA_TSA_GUARDED_BY(cameraEvents_);
+
+	std::function<void(std::shared_ptr<Camera>)> cameraAddedHandler_;
+	std::function<void(std::shared_ptr<Camera>)> cameraRemovedHandler_;
+
+	std::map<Camera *, std::function<void(std::shared_ptr<Camera>, Request *, FrameBuffer *)>> cameraBufferCompletedHandlers_;
+	std::map<Camera *, std::function<void(std::shared_ptr<Camera>, Request *)>> cameraRequestCompletedHandlers_;
+	std::map<Camera *, std::function<void(std::shared_ptr<Camera>)>> cameraDisconnectHandlers_;
 
 	void writeFd();
 	int readFd();
-	void pushRequest(Request *req);
-	std::vector<Request *> getCompletedRequests();
+	void pushEvent(const CameraEvent &ev);
+	std::vector<CameraEvent> getEvents();
 };
diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp
index 1ef1384e..a07f06c4 100644
--- a/src/py/libcamera/py_main.cpp
+++ b/src/py/libcamera/py_main.cpp
@@ -107,7 +107,20 @@ PYBIND11_MODULE(_libcamera, m)
 		.def_property_readonly("cameras", &PyCameraManager::cameras)
 
 		.def_property_readonly("event_fd", &PyCameraManager::eventFd)
-		.def("get_ready_requests", &PyCameraManager::getReadyRequests);
+
+		/* DEPRECATED */
+		.def("get_ready_requests", &PyCameraManager::getReadyRequests)
+
+		.def("dispatch_events", &PyCameraManager::dispatchEvents)
+		.def("discard_events", &PyCameraManager::discardEvents)
+
+		.def_property("camera_added",
+		              &PyCameraManager::getCameraAdded,
+		              &PyCameraManager::setCameraAdded)
+
+		.def_property("camera_removed",
+		              &PyCameraManager::getCameraRemoved,
+		              &PyCameraManager::setCameraRemoved);
 
 	pyCamera
 		.def_property_readonly("id", &Camera::id)
@@ -131,7 +144,14 @@ PYBIND11_MODULE(_libcamera, m)
 			auto cm = gCameraManager.lock();
 			ASSERT(cm);
 
-			self.requestCompleted.connect(cm.get(), &PyCameraManager::handleRequestCompleted);
+			/*
+			 * Note: We always subscribe requestComplete, as the bindings
+			 * use requestComplete event to decrement the Request refcount-
+			 */
+
+			self.requestCompleted.connect(&self, [cm, camera=self.shared_from_this()](Request *req) {
+				cm->handleRequestCompleted(camera, req);
+			});
 
 			ControlList controlList(self.controls());
 
@@ -159,6 +179,71 @@ PYBIND11_MODULE(_libcamera, m)
 				                        "Failed to start camera");
 		})
 
+		.def_property("request_completed",
+		[](Camera &self) {
+			auto cm = gCameraManager.lock();
+			ASSERT(cm);
+
+			return cm->getRequestCompleted(&self);
+		},
+		[](Camera &self, std::function<void(std::shared_ptr<Camera>, Request *)> f) {
+			auto cm = gCameraManager.lock();
+			ASSERT(cm);
+
+			cm->setRequestCompleted(&self, f);
+
+			/*
+			 * Note: We do not subscribe requestComplete here, as we
+			 * do that in the start() method.
+			 */
+		})
+
+		.def_property("buffer_completed",
+		[](Camera &self) -> std::function<void(std::shared_ptr<Camera>, Request *, FrameBuffer *)> {
+			auto cm = gCameraManager.lock();
+			ASSERT(cm);
+
+			return cm->getBufferCompleted(&self);
+		},
+		[](Camera &self, std::function<void(std::shared_ptr<Camera>, Request *, FrameBuffer *)> f) {
+			auto cm = gCameraManager.lock();
+			ASSERT(cm);
+
+			cm->setBufferCompleted(&self, f);
+
+			self.bufferCompleted.disconnect();
+
+			if (!f)
+				return;
+
+			self.bufferCompleted.connect(&self, [cm, camera=self.shared_from_this()](Request *req, FrameBuffer *fb) {
+				cm->handleBufferCompleted(camera, req, fb);
+			});
+		})
+
+		.def_property("disconnected",
+		[](Camera &self) -> std::function<void(std::shared_ptr<Camera>)> {
+			auto cm = gCameraManager.lock();
+			ASSERT(cm);
+
+			return cm->getDisconnected(&self);
+		},
+		[](Camera &self, std::function<void(std::shared_ptr<Camera>)> f) {
+			auto cm = gCameraManager.lock();
+			ASSERT(cm);
+
+			cm->setDisconnected(&self, f);
+
+			self.disconnected.disconnect();
+
+			if (!f)
+				return;
+
+			self.disconnected.connect(&self, [cm, camera=self.shared_from_this()]() {
+				cm->handleDisconnected(camera);
+			});
+		})
+
 		.def("__str__", [](Camera &self) {
 			return "<libcamera.Camera '" + self.id() + "'>";
 		})
