diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp
index 6ff1e22b15d4..5210a1f38efb 100644
--- a/src/ipa/rpi/common/ipa_base.cpp
+++ b/src/ipa/rpi/common/ipa_base.cpp
@@ -55,8 +55,15 @@ constexpr Duration controllerMinFrameDuration = 1.0s / 30.0;
 
 /* List of controls handled by the Raspberry Pi IPA */
 const ControlInfoMap::Map ipaControls{
-	{ &controls::AeEnable, ControlInfo(false, true) },
+	{ &controls::ExposureTimeMode,
+	  ControlInfo(static_cast<int32_t>(controls::ExposureTimeModeAuto),
+		      static_cast<int32_t>(controls::ExposureTimeModeManual),
+		      static_cast<int32_t>(controls::ExposureTimeModeAuto)) },
 	{ &controls::ExposureTime, ControlInfo(0, 66666) },
+	{ &controls::AnalogueGainMode,
+	  ControlInfo(static_cast<int32_t>(controls::AnalogueGainModeAuto),
+		      static_cast<int32_t>(controls::AnalogueGainModeManual),
+		      static_cast<int32_t>(controls::AnalogueGainModeAuto)) },
 	{ &controls::AnalogueGain, ControlInfo(1.0f, 16.0f) },
 	{ &controls::AeMeteringMode, ControlInfo(controls::AeMeteringModeValues) },
 	{ &controls::AeConstraintMode, ControlInfo(controls::AeConstraintModeValues) },
@@ -749,6 +756,42 @@ void IpaBase::applyControls(const ControlList &controls)
 			af->setMode(mode->second);
 	}
 
+	/*
+	 * Because some AE controls are mode-specific, handle the AE-related
+	 * mode changes first.
+	 */
+	const auto analogueGainMode = controls.get(controls::AnalogueGainMode);
+	const auto exposureTimeMode = controls.get(controls::ExposureTimeMode);
+
+	if (analogueGainMode || exposureTimeMode) {
+		RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+			controller_.getAlgorithm("agc"));
+		if (agc) {
+			if (analogueGainMode) {
+				if (*analogueGainMode == controls::AnalogueGainModeManual)
+					agc->disableAutoGain();
+				else
+					agc->enableAutoGain();
+
+				libcameraMetadata_.set(controls::AnalogueGainMode,
+						       *analogueGainMode);
+			}
+
+			if (exposureTimeMode) {
+				if (*exposureTimeMode == controls::ExposureTimeModeManual)
+					agc->disableAutoExposure();
+				else
+					agc->enableAutoExposure();
+
+				libcameraMetadata_.set(controls::ExposureTimeMode,
+						       *exposureTimeMode);
+			}
+		} else {
+			LOG(IPARPI, Warning)
+				<< "Could not set AnalogueGainMode or ExposureTimeMode - no AGC algorithm";
+		}
+	}
+
 	/* Iterate over controls */
 	for (auto const &ctrl : controls) {
 		LOG(IPARPI, Debug) << "Request ctrl: "
@@ -756,23 +799,8 @@ void IpaBase::applyControls(const ControlList &controls)
 				   << " = " << ctrl.second.toString();
 
 		switch (ctrl.first) {
-		case controls::AE_ENABLE: {
-			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
-				controller_.getAlgorithm("agc"));
-			if (!agc) {
-				LOG(IPARPI, Warning)
-					<< "Could not set AE_ENABLE - no AGC algorithm";
-				break;
-			}
-
-			if (ctrl.second.get<bool>() == false)
-				agc->disableAuto();
-			else
-				agc->enableAuto();
-
-			libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>());
-			break;
-		}
+		case controls::EXPOSURE_TIME_MODE:
+			break; /* We already handled this one above */
 
 		case controls::EXPOSURE_TIME: {
 			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
@@ -783,6 +811,13 @@ void IpaBase::applyControls(const ControlList &controls)
 				break;
 			}
 
+			/*
+			 * Ignore manual exposure time when the auto exposure
+			 * algorithm is running.
+			 */
+			if (agc->autoExposureEnabled())
+				break;
+
 			/* The control provides units of microseconds. */
 			agc->setFixedExposureTime(0, ctrl.second.get<int32_t>() * 1.0us);
 
@@ -790,6 +825,9 @@ void IpaBase::applyControls(const ControlList &controls)
 			break;
 		}
 
+		case controls::ANALOGUE_GAIN_MODE:
+			break; /* We already handled this one above */
+
 		case controls::ANALOGUE_GAIN: {
 			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
 				controller_.getAlgorithm("agc"));
@@ -799,6 +837,13 @@ void IpaBase::applyControls(const ControlList &controls)
 				break;
 			}
 
+			/*
+			 * Ignore manual analogue gain value when the auto gain
+			 * algorithm is running.
+			 */
+			if (agc->autoGainEnabled())
+				break;
+
 			agc->setFixedAnalogueGain(0, ctrl.second.get<float>());
 
 			libcameraMetadata_.set(controls::AnalogueGain,
@@ -855,6 +900,13 @@ void IpaBase::applyControls(const ControlList &controls)
 				break;
 			}
 
+			/*
+			 * Ignore AE_EXPOSURE_MODE if the shutter or the gain
+			 * are in auto mode.
+			 */
+			if (agc->autoExposureEnabled() || agc->autoGainEnabled())
+				break;
+
 			int32_t idx = ctrl.second.get<int32_t>();
 			if (ExposureModeTable.count(idx)) {
 				agc->setExposureMode(ExposureModeTable.at(idx));
@@ -1334,9 +1386,19 @@ void IpaBase::reportMetadata(unsigned int ipaContext)
 	}
 
 	AgcPrepareStatus *agcPrepareStatus = rpiMetadata.getLocked<AgcPrepareStatus>("agc.prepare_status");
-	if (agcPrepareStatus) {
-		libcameraMetadata_.set(controls::AeLocked, agcPrepareStatus->locked);
+	if (agcPrepareStatus)
 		libcameraMetadata_.set(controls::DigitalGain, agcPrepareStatus->digitalGain);
+
+	RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+		controller_.getAlgorithm("agc"));
+	if (agc) {
+		if (!agc->autoExposureEnabled() && !agc->autoGainEnabled())
+			libcameraMetadata_.set(controls::AeState, controls::AeStateIdle);
+		else if (agcPrepareStatus)
+			libcameraMetadata_.set(controls::AeState,
+					       agcPrepareStatus->locked
+						       ? controls::AeStateConverged
+						       : controls::AeStateSearching);
 	}
 
 	LuxStatus *luxStatus = rpiMetadata.getLocked<LuxStatus>("lux.status");
diff --git a/src/ipa/rpi/controller/agc_algorithm.h b/src/ipa/rpi/controller/agc_algorithm.h
index c97828577602..fdaa10e6c176 100644
--- a/src/ipa/rpi/controller/agc_algorithm.h
+++ b/src/ipa/rpi/controller/agc_algorithm.h
@@ -30,8 +30,12 @@ public:
 	virtual void setMeteringMode(std::string const &meteringModeName) = 0;
 	virtual void setExposureMode(std::string const &exposureModeName) = 0;
 	virtual void setConstraintMode(std::string const &contraintModeName) = 0;
-	virtual void enableAuto() = 0;
-	virtual void disableAuto() = 0;
+	virtual void enableAutoExposure() = 0;
+	virtual void disableAutoExposure() = 0;
+	virtual bool autoExposureEnabled() const = 0;
+	virtual void enableAutoGain() = 0;
+	virtual void disableAutoGain() = 0;
+	virtual bool autoGainEnabled() const = 0;
 	virtual void setActiveChannels(const std::vector<unsigned int> &activeChannels) = 0;
 };
 
diff --git a/src/ipa/rpi/controller/rpi/agc.cpp b/src/ipa/rpi/controller/rpi/agc.cpp
index c48fdf156591..02bfdb4a5e22 100644
--- a/src/ipa/rpi/controller/rpi/agc.cpp
+++ b/src/ipa/rpi/controller/rpi/agc.cpp
@@ -74,22 +74,62 @@ int Agc::checkChannel(unsigned int channelIndex) const
 	return 0;
 }
 
-void Agc::disableAuto()
+void Agc::disableAutoExposure()
 {
-	LOG(RPiAgc, Debug) << "disableAuto";
+	LOG(RPiAgc, Debug) << "disableAutoExposure";
 
 	/* All channels are enabled/disabled together. */
 	for (auto &data : channelData_)
-		data.channel.disableAuto();
+		data.channel.disableAutoExposure();
 }
 
-void Agc::enableAuto()
+void Agc::enableAutoExposure()
 {
-	LOG(RPiAgc, Debug) << "enableAuto";
+	LOG(RPiAgc, Debug) << "enableAutoExposure";
 
 	/* All channels are enabled/disabled together. */
 	for (auto &data : channelData_)
-		data.channel.enableAuto();
+		data.channel.enableAutoExposure();
+}
+
+bool Agc::autoExposureEnabled() const
+{
+	LOG(RPiAgc, Debug) << "autoExposureEnabled";
+
+	/*
+	 * We always have at least one channel, and since all channels are
+	 * enabled and disabled together we can simply check the first one.
+	 */
+	return channelData_[0].channel.autoExposureEnabled();
+}
+
+void Agc::disableAutoGain()
+{
+	LOG(RPiAgc, Debug) << "disableAutoGain";
+
+	/* All channels are enabled/disabled together. */
+	for (auto &data : channelData_)
+		data.channel.disableAutoGain();
+}
+
+void Agc::enableAutoGain()
+{
+	LOG(RPiAgc, Debug) << "enableAutoGain";
+
+	/* All channels are enabled/disabled together. */
+	for (auto &data : channelData_)
+		data.channel.enableAutoGain();
+}
+
+bool Agc::autoGainEnabled() const
+{
+	LOG(RPiAgc, Debug) << "autoGainEnabled";
+
+	/*
+	 * We always have at least one channel, and since all channels are
+	 * enabled and disabled together we can simply check the first one.
+	 */
+	return channelData_[0].channel.autoGainEnabled();
 }
 
 unsigned int Agc::getConvergenceFrames() const
diff --git a/src/ipa/rpi/controller/rpi/agc.h b/src/ipa/rpi/controller/rpi/agc.h
index 3aca000bb4c2..c3a940bf697a 100644
--- a/src/ipa/rpi/controller/rpi/agc.h
+++ b/src/ipa/rpi/controller/rpi/agc.h
@@ -40,8 +40,12 @@ public:
 	void setMeteringMode(std::string const &meteringModeName) override;
 	void setExposureMode(std::string const &exposureModeName) override;
 	void setConstraintMode(std::string const &contraintModeName) override;
-	void enableAuto() override;
-	void disableAuto() override;
+	void enableAutoExposure() override;
+	void disableAutoExposure() override;
+	bool autoExposureEnabled() const override;
+	void enableAutoGain() override;
+	void disableAutoGain() override;
+	bool autoGainEnabled() const override;
 	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
 	void prepare(Metadata *imageMetadata) override;
 	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
diff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp
index 79c459735dfd..e79184b7ac74 100644
--- a/src/ipa/rpi/controller/rpi/agc_channel.cpp
+++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp
@@ -319,18 +319,36 @@ int AgcChannel::read(const libcamera::YamlObject &params,
 	return 0;
 }
 
-void AgcChannel::disableAuto()
+void AgcChannel::disableAutoExposure()
 {
 	fixedExposureTime_ = status_.exposureTime;
+}
+
+void AgcChannel::enableAutoExposure()
+{
+	fixedExposureTime_ = 0s;
+}
+
+bool AgcChannel::autoExposureEnabled() const
+{
+	return fixedExposureTime_ == 0s;
+}
+
+void AgcChannel::disableAutoGain()
+{
 	fixedAnalogueGain_ = status_.analogueGain;
 }
 
-void AgcChannel::enableAuto()
+void AgcChannel::enableAutoGain()
 {
-	fixedExposureTime_ = 0s;
 	fixedAnalogueGain_ = 0;
 }
 
+bool AgcChannel::autoGainEnabled() const
+{
+	return fixedAnalogueGain_ == 0;
+}
+
 unsigned int AgcChannel::getConvergenceFrames() const
 {
 	/*
diff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h
index 734e5efd3266..fa697e6fa57d 100644
--- a/src/ipa/rpi/controller/rpi/agc_channel.h
+++ b/src/ipa/rpi/controller/rpi/agc_channel.h
@@ -96,8 +96,12 @@ public:
 	void setMeteringMode(std::string const &meteringModeName);
 	void setExposureMode(std::string const &exposureModeName);
 	void setConstraintMode(std::string const &contraintModeName);
-	void enableAuto();
-	void disableAuto();
+	void enableAutoExposure();
+	void disableAutoExposure();
+	bool autoExposureEnabled() const;
+	void enableAutoGain();
+	void disableAutoGain();
+	bool autoGainEnabled() const;
 	void switchMode(CameraMode const &cameraMode, Metadata *metadata);
 	void prepare(Metadata *imageMetadata);
 	void process(StatisticsPtr &stats, DeviceStatus const &deviceStatus, Metadata *imageMetadata,
