diff --git a/src/plugins/quectel/mm-broadband-modem-quectel.c b/src/plugins/quectel/mm-broadband-modem-quectel.c
index ecd7c9c17d8b3566aa52fb7c5fa5fdb64a6b1507..bdee9c985fb01b5c7a476319ca04f487ab15c4cc 100644
--- a/src/plugins/quectel/mm-broadband-modem-quectel.c
+++ b/src/plugins/quectel/mm-broadband-modem-quectel.c
@@ -21,6 +21,7 @@
 #include "mm-iface-modem-firmware.h"
 #include "mm-iface-modem-location.h"
 #include "mm-iface-modem-time.h"
+#include "mm-log-object.h"
 #include "mm-shared-quectel.h"
 #include "mm-base-modem-at.h"
 
@@ -41,6 +42,19 @@ G_DEFINE_TYPE_EXTENDED (MMBroadbandModemQuectel, mm_broadband_modem_quectel, MM_
                         G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_TIME, iface_modem_time_init)
                         G_IMPLEMENT_INTERFACE (MM_TYPE_SHARED_QUECTEL, shared_quectel_init))
 
+struct _MMBroadbandModemQuectelPrivate {
+    GRegex *powered_down_regex;
+};
+
+#define MM_BROADBAND_MODEM_QUECTEL_POWERED_DOWN "powered-down"
+
+enum {
+    POWERED_DOWN,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
 /*****************************************************************************/
 /* Power state loading (Modem interface) */
 
@@ -99,7 +113,150 @@ load_power_state (MMIfaceModem        *self,
 }
 
 /*****************************************************************************/
-/* Modem power up/down/off (Modem interface) */
+/* POWERED DOWN unsolicited event handler */
+
+static void
+powered_down_handler (MMPortSerialAt   *port,
+                      GMatchInfo       *match_info,
+                      MMBroadbandModem *self)
+{
+    /* The POWERED DOWN URC indicates the modem is ready to be powered down */
+    g_signal_emit (self, signals[POWERED_DOWN], 0);
+}
+
+/*****************************************************************************/
+/* Modem power down (Modem interface) */
+
+typedef struct {
+    MMBroadbandModemQuectel *modem;
+    guint                    urc_id;
+    guint                    timeout_id;
+} PowerDownContext;
+
+static void
+power_down_context_clear_timeout (PowerDownContext *ctx)
+{
+    if (ctx->timeout_id) {
+        g_source_remove (ctx->timeout_id);
+        ctx->timeout_id = 0;
+    }
+}
+
+static void
+power_down_context_disconnect_urc (PowerDownContext *ctx)
+{
+    if (ctx->urc_id) {
+        g_signal_handler_disconnect (ctx->modem, ctx->urc_id);
+        ctx->urc_id = 0;
+    }
+}
+
+static void
+power_down_context_free (PowerDownContext *ctx)
+{
+    g_assert (!ctx->urc_id);
+    g_assert (!ctx->timeout_id);
+    g_clear_object (&ctx->modem);
+    g_slice_free (PowerDownContext, ctx);
+}
+
+static gboolean
+modem_power_down_finish (MMIfaceModem  *self,
+                         GAsyncResult  *res,
+                         GError       **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+power_off_powered_down (MMBroadbandModemQuectel *self,
+                        GTask                   *task)
+{
+    PowerDownContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+    mm_obj_msg (self, "got POWERED DOWN URC; proceeding with power off");
+    power_down_context_clear_timeout (ctx);
+    power_down_context_disconnect_urc (ctx);
+    g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+static gboolean
+powered_down_timeout (GTask *task)
+{
+    PowerDownContext *ctx;
+
+    ctx = g_task_get_task_data (task);
+    power_down_context_clear_timeout (ctx);
+    power_down_context_disconnect_urc (ctx);
+
+    g_task_return_new_error (task,
+                             MM_CORE_ERROR,
+                             MM_CORE_ERROR_TIMEOUT,
+                             "timed out waiting for POWERED DOWN URC");
+    g_object_unref (task);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+power_off_ready (MMBroadbandModemQuectel *self,
+                 GAsyncResult            *res,
+                 GTask                   *task)
+{
+    g_autoptr(GError) error = NULL;
+
+    /* Docs for many devices state that +QPOWD's OK response comes very
+     * quickly but caller must wait for a "POWERED DOWN" URC before allowing
+     * modem power to be cut.
+     */
+
+    if (!mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    /* Wait for the POWERED DOWN URC */
+    mm_obj_msg (self, "waiting for POWERED DOWN URC...");
+}
+
+static void
+modem_power_off (MMIfaceModem        *_self,
+                 GAsyncReadyCallback  callback,
+                 gpointer             user_data)
+{
+    MMBroadbandModemQuectel *self = MM_BROADBAND_MODEM_QUECTEL (_self);
+    GTask                   *task;
+    PowerDownContext        *ctx;
+
+    task = g_task_new (self,
+                       mm_base_modem_peek_cancellable (MM_BASE_MODEM (self)),
+                       callback,
+                       user_data);
+
+    ctx = g_slice_new0 (PowerDownContext);
+    ctx->modem = g_object_ref (self);
+    ctx->urc_id = g_signal_connect (self,
+                                    MM_BROADBAND_MODEM_QUECTEL_POWERED_DOWN,
+                                    (GCallback)power_off_powered_down,
+                                    task);
+    /* Docs state caller must wait up to 60 seconds for POWERED DOWN URC */
+    ctx->timeout_id = g_timeout_add_seconds (62,
+                                             (GSourceFunc)powered_down_timeout,
+                                             task);
+    g_task_set_task_data (task, ctx, (GDestroyNotify)power_down_context_free);
+
+    mm_base_modem_at_command (MM_BASE_MODEM (self),
+                              "+QPOWD=1",
+                              5,
+                              FALSE,
+                              (GAsyncReadyCallback)power_off_ready,
+                              task);
+}
+
+/*****************************************************************************/
+/* Modem power up/off (Modem interface) */
 
 static gboolean
 common_modem_power_operation_finish (MMIfaceModem  *self,
@@ -117,7 +274,7 @@ common_modem_power_operation (MMBroadbandModemQuectel  *self,
 {
     mm_base_modem_at_command (MM_BASE_MODEM (self),
                               command,
-                              30,
+                              15,
                               FALSE,
                               callback,
                               user_data);
@@ -131,14 +288,6 @@ modem_reset (MMIfaceModem        *self,
     common_modem_power_operation (MM_BROADBAND_MODEM_QUECTEL (self), "+CFUN=1,1", callback, user_data);
 }
 
-static void
-modem_power_off (MMIfaceModem        *self,
-                 GAsyncReadyCallback  callback,
-                 gpointer             user_data)
-{
-    common_modem_power_operation (MM_BROADBAND_MODEM_QUECTEL (self), "+QPOWD=1", callback, user_data);
-}
-
 static void
 modem_power_down (MMIfaceModem        *self,
                   GAsyncReadyCallback  callback,
@@ -157,6 +306,32 @@ modem_power_up (MMIfaceModem        *self,
 
 /*****************************************************************************/
 
+static void
+setup_ports (MMBroadbandModem *_self)
+{
+    MMBroadbandModemQuectel *self = MM_BROADBAND_MODEM_QUECTEL (_self);
+    MMPortSerialAt          *ports[2];
+    guint                    i;
+
+    mm_shared_quectel_setup_ports (_self);
+
+    ports[0] = mm_base_modem_peek_port_primary   (MM_BASE_MODEM (self));
+    ports[1] = mm_base_modem_peek_port_secondary (MM_BASE_MODEM (self));
+
+    for (i = 0; i < G_N_ELEMENTS (ports); i++) {
+        if (ports[i]) {
+            mm_port_serial_at_add_unsolicited_msg_handler (
+                ports[i],
+                self->priv->powered_down_regex,
+                (MMPortSerialAtUnsolicitedMsgFn)powered_down_handler,
+                self,
+                NULL);
+        }
+    }
+}
+
+/*****************************************************************************/
+
 MMBroadbandModemQuectel *
 mm_broadband_modem_quectel_new (const gchar  *device,
                                 const gchar  *physdev,
@@ -182,6 +357,13 @@ mm_broadband_modem_quectel_new (const gchar  *device,
 static void
 mm_broadband_modem_quectel_init (MMBroadbandModemQuectel *self)
 {
+    /* Initialize opaque pointer to private data */
+    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+                                              MM_TYPE_BROADBAND_MODEM_QUECTEL,
+                                              MMBroadbandModemQuectelPrivate);
+
+    self->priv->powered_down_regex = g_regex_new ("\\r\\nPOWERED DOWN", G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL);
+    g_assert (self->priv->powered_down_regex);
 }
 
 static void
@@ -197,7 +379,7 @@ iface_modem_init (MMIfaceModemInterface *iface)
     iface->modem_power_up        = modem_power_up;
     iface->modem_power_up_finish = common_modem_power_operation_finish;
     iface->modem_power_down        = modem_power_down;
-    iface->modem_power_down_finish = common_modem_power_operation_finish;
+    iface->modem_power_down_finish = modem_power_down_finish;
     iface->modem_power_off        = modem_power_off;
     iface->modem_power_off_finish = common_modem_power_operation_finish;
     iface->reset        = modem_reset;
@@ -266,10 +448,37 @@ shared_quectel_init (MMSharedQuectelInterface *iface)
     iface->peek_parent_class                    = peek_parent_class;
 }
 
+static void
+finalize (GObject *object)
+{
+    MMBroadbandModemQuectel *self = MM_BROADBAND_MODEM_QUECTEL (object);
+
+    g_regex_unref (self->priv->powered_down_regex);
+
+    G_OBJECT_CLASS (mm_broadband_modem_quectel_parent_class)->finalize (object);
+}
+
 static void
 mm_broadband_modem_quectel_class_init (MMBroadbandModemQuectelClass *klass)
 {
+    GObjectClass          *object_class = G_OBJECT_CLASS (klass);
     MMBroadbandModemClass *broadband_modem_class = MM_BROADBAND_MODEM_CLASS (klass);
 
-    broadband_modem_class->setup_ports = mm_shared_quectel_setup_ports;
+    g_type_class_add_private (object_class, sizeof (MMBroadbandModemQuectelPrivate));
+
+    /* Virtual methods */
+    object_class->finalize = finalize;
+
+    broadband_modem_class->setup_ports = setup_ports;
+
+    signals[POWERED_DOWN] = g_signal_new (MM_BROADBAND_MODEM_QUECTEL_POWERED_DOWN,
+                                          G_OBJECT_CLASS_TYPE (object_class),
+                                          G_SIGNAL_RUN_FIRST,
+                                          0,
+                                          NULL, /* accumulator      */
+                                          NULL, /* accumulator data */
+                                          g_cclosure_marshal_generic,
+                                          G_TYPE_NONE,
+                                          0,
+                                          NULL);
 }
diff --git a/src/plugins/quectel/mm-broadband-modem-quectel.h b/src/plugins/quectel/mm-broadband-modem-quectel.h
index 2067ff1083b10f9f88b31b1905634c3d7d04d461..56f22d231f359b0e2b32f9830a07748a305592f6 100644
--- a/src/plugins/quectel/mm-broadband-modem-quectel.h
+++ b/src/plugins/quectel/mm-broadband-modem-quectel.h
@@ -27,9 +27,12 @@
 
 typedef struct _MMBroadbandModemQuectel MMBroadbandModemQuectel;
 typedef struct _MMBroadbandModemQuectelClass MMBroadbandModemQuectelClass;
+typedef struct _MMBroadbandModemQuectelPrivate MMBroadbandModemQuectelPrivate;
 
 struct _MMBroadbandModemQuectel {
     MMBroadbandModem parent;
+
+    MMBroadbandModemQuectelPrivate *priv;
 };
 
 struct _MMBroadbandModemQuectelClass{