Merge cros/upstream to cros/master

Contains the following commits:
 17934810 iface-modem-3gpp-profile-manager: fix copy-paste error on tags for quarks (Maxim Anisimov)
 f00cb9f6 introspection: add Modem Simple (Dylan Van Assche)
 1e08f9ca mm-log: hiding personal info while logging (som)
 4207ee61 huawei: disable +CPOL based features in Huawei E226 (Aleksander Morgado)
 96fb4259 cinterion: add PLS63 port type hints (Konrad Zapałowicz)
 0645b70a plugins,telit: SWPKGV parsing needs more permissive regex (Carlo Lobrano)
 818b539d test,modem-helpers-telit: fix test inputs (Carlo Lobrano)
 ac243f94 sms: prevent crash if date is out of range (Carlo Lobrano)
 5c8c1136 modem-mbim: update default error when network error is out of range (som)
 e175ada2 plugins,telit: remove unnecessary after sim unlock 1s delay (Carlo Lobrano)
 7960b365 data: add example connection dispatcher (Aleksander Morgado)
 99232154 build: rename directory where fcc unlocks are kept (Aleksander Morgado)
 2dafb32d base-bearer: integrate connection status dispatcher scripts (Aleksander Morgado)
 e8612671 dispatcher-connection: new dispatcher for connection status scripts (Aleksander Morgado)
 e188a77e dispatcher-fcc-unlock: inherit from the base dispatcher object (Aleksander Morgado)
 01c8edf6 dispatcher: new generic object to handle script dispatchers (Aleksander Morgado)
 1fec1cd3 profile-manager: profile-id and apn-type check not required for user settings (som)
 19f38994 port-serial: ensure the port object is valid after BUFFER_FULL handling (Aleksander Morgado)
 210fa8bf qmi: Fall back to NAS SSP/NAS TP capabilities (Sven Schwermer)
 0fbab3c8 broadband-modem-mbim: Set InitialEPSBearer's authentication to CHAP when is UNKNOWN (Andrew Lassalle)
 97933788 bearer-properties: Match UNKNOWN auth to CHAP for loose comparison (Andrew Lassalle)
 d5805002 core: remove "all rights reserved" from copyright lines (Aleksander Morgado)
 cf7b58ce telit: reorganize common_parse_bnd_response for readability (Carlo Lobrano)
 c208d33e telit: fix AT#BND parsing for LE910C1-EUX (Carlo Lobrano)
 30a35e6d telit: detect modem model from revision (Carlo Lobrano)
 3df96a42 telit: override load revision (Carlo Lobrano)
 afd3ce86 mm-netlink: use unaligned netlink attribute length (Bjørn Mork)
 7bbf6c8a mm-netlink: only change IFF_UP flag (Bjørn Mork)
 b5a0a500 po add Dutch translation (Nathan Follens)
 ea247f7e foxconn: consolidate checks for T99W175 and T99W265 (Aleksander Morgado)
 cd2dd20b foxconn: consolidate logic deciding required update methods (Aleksander Morgado)
 b16b49f9 foxconn: consolidate logic deciding requested firmware version type (Aleksander Morgado)
 4efb5f61 foxconn: switch to use autoptr() in firmware info loading (Aleksander Morgado)
 b81680a3 foxconn: remove unneeded input bundle unref (Aleksander Morgado)
 a56f96f0 foxconn: use new qmi service(fox) to get firmware version (Freedom Liu)
 8d0d90a6 build: require libqmi 1.31.4 for FOX service support (Aleksander Morgado)
 0bc0831b build,meson: Fix udev rules and keyfiles tests (Iñigo Martínez)
 1fdc3ac7 linktop: new port type hints (Aleksander Morgado)
 e8bb90e0 cinterion: Add support for PLSx3w modems (Theodore A. Roth)
 8d95c82f shared-qmi: ignore slot status indications until initial status is known (Stephan Gerhold)
 8ab31cf0 broadband-modem-mbim: consolidate initialized SIM creation method (Aleksander Morgado)
 035879da broadband-modem-mbim: update sim-type and esim-status of inactive slot (som)
 c447785f telit: fix mode 'any' setting with AT commands (Daniele Palmas)
 598d99b5 test,modem-helpers: add test for the EM9191 COPS=? response (Aleksander Morgado)
 2c8cfb47 modem-helpers: improve and fix COPS=? numeric fields parsing (Aleksander Morgado)
 5ba67aff modem-helpers: act given in COPS=? may have more than one digit (Aleksander Morgado)

BUG=None
TEST=None

Change-Id: I35bd0d5bbdda0ff3e270a93b3d5d652b11d7bb26
diff --git a/configure.ac b/configure.ac
index 2ac41c5..a05c90c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -386,7 +386,7 @@
 dnl MBIM support (enabled by default)
 dnl
 
-LIBMBIM_VERSION=1.27.5
+LIBMBIM_VERSION=1.27.6
 
 AC_ARG_WITH(mbim, AS_HELP_STRING([--without-mbim], [Build without MBIM support]), [], [with_mbim=yes])
 AM_CONDITIONAL(WITH_MBIM, test "x$with_mbim" = "xyes")
@@ -410,7 +410,7 @@
 dnl QMI support (enabled by default)
 dnl
 
-LIBQMI_VERSION=1.31.3
+LIBQMI_VERSION=1.31.4
 
 AC_ARG_WITH(qmi, AS_HELP_STRING([--without-qmi], [Build without QMI support]), [], [with_qmi=yes])
 AM_CONDITIONAL(WITH_QMI, test "x$with_qmi" = "xyes")
@@ -577,7 +577,8 @@
 data/Makefile
 data/ModemManager.pc
 data/mm-glib.pc
-data/fcc-unlock/Makefile
+data/dispatcher-connection/Makefile
+data/dispatcher-fcc-unlock/Makefile
 data/tests/Makefile
 data/tests/org.freedesktop.ModemManager1.service
 include/Makefile
diff --git a/data/Makefile.am b/data/Makefile.am
index b42b70f..12330e2 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -1,5 +1,5 @@
 
-SUBDIRS = . fcc-unlock tests
+SUBDIRS = . dispatcher-connection dispatcher-fcc-unlock tests
 
 edit = @sed \
        -e 's|@sbindir[@]|$(sbindir)|g' \
diff --git a/data/dispatcher-connection/99-log-event b/data/dispatcher-connection/99-log-event
new file mode 100644
index 0000000..31188ef
--- /dev/null
+++ b/data/dispatcher-connection/99-log-event
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+# SPDX-License-Identifier: CC0-1.0
+# 2022 Aleksander Morgado <aleksander@aleksander.es>
+#
+# Example connection info dispatcher script
+#
+
+# require program name and at least 4 arguments
+[ $# -lt 4 ] && exit 1
+
+MODEM_PATH="$1"
+BEARER_PATH="$2"
+INTERFACE="$3"
+STATE="$4"
+
+MODEM_ID=$(basename ${MODEM_PATH})
+BEARER_ID=$(basename ${BEARER_PATH})
+
+# report in syslog the event
+logger -t "connection-dispatch" "modem${MODEM_ID}: bearer${BEARER_ID}: interface ${INTERFACE} ${STATE}"
+exit $?
diff --git a/data/dispatcher-connection/Makefile.am b/data/dispatcher-connection/Makefile.am
new file mode 100644
index 0000000..3b86e7f
--- /dev/null
+++ b/data/dispatcher-connection/Makefile.am
@@ -0,0 +1,21 @@
+
+# Directory for user-enabled tools
+connectionuser = $(pkgsysconfdir)/connection.d
+
+# Directory for package-enabled tools
+connectionpackage = $(pkglibdir)/connection.d
+
+# Shipped but disabled FCC unlock tools
+connectionavailabledir = $(pkgdatadir)/connection.available.d
+connectionavailable_SCRIPTS = \
+	99-log-event \
+	$(NULL)
+
+EXTRA_DIST = $(connectionavailable_SCRIPTS)
+
+install-data-hook:
+	$(MKDIR_P) $(DESTDIR)$(connectionuser); \
+	$(MKDIR_P) $(DESTDIR)$(connectionpackage); \
+	cd $(DESTDIR)$(connectionavailabledir); \
+	chmod go-rwx *; \
+	$(NULL)
diff --git a/data/dispatcher-connection/meson.build b/data/dispatcher-connection/meson.build
new file mode 100644
index 0000000..2e7ef8b
--- /dev/null
+++ b/data/dispatcher-connection/meson.build
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2021 Aleksander Morgado <aleksander@aleksander.es>
+
+# Shipped example connection info dispatcher
+mm_connectiondiravailable = mm_pkgdatadir / 'connection.available.d'
+
+# Directory for user-enabled scripts
+mm_connectiondiruser = mm_pkgsysconfdir / 'connection.d'
+
+# Directory for package-enabled tools
+mm_connectiondirpackage = mm_pkglibdir / 'connection.d'
+
+examples = files(
+  '99-log-event',
+)
+
+install_data(
+  examples,
+  install_mode: 'rwx------',
+  install_dir: mm_connectiondiravailable,
+)
+
+mkdir_cmd = 'mkdir -p ${DESTDIR}@0@'
+meson.add_install_script('sh', '-c', mkdir_cmd.format(mm_prefix / mm_connectiondiruser))
+meson.add_install_script('sh', '-c', mkdir_cmd.format(mm_prefix / mm_connectiondirpackage))
diff --git a/data/fcc-unlock/105b b/data/dispatcher-fcc-unlock/105b
similarity index 100%
rename from data/fcc-unlock/105b
rename to data/dispatcher-fcc-unlock/105b
diff --git a/data/fcc-unlock/1199 b/data/dispatcher-fcc-unlock/1199
similarity index 100%
rename from data/fcc-unlock/1199
rename to data/dispatcher-fcc-unlock/1199
diff --git a/data/fcc-unlock/1eac b/data/dispatcher-fcc-unlock/1eac
similarity index 100%
rename from data/fcc-unlock/1eac
rename to data/dispatcher-fcc-unlock/1eac
diff --git a/data/fcc-unlock/Makefile.am b/data/dispatcher-fcc-unlock/Makefile.am
similarity index 100%
rename from data/fcc-unlock/Makefile.am
rename to data/dispatcher-fcc-unlock/Makefile.am
diff --git a/data/fcc-unlock/meson.build b/data/dispatcher-fcc-unlock/meson.build
similarity index 100%
rename from data/fcc-unlock/meson.build
rename to data/dispatcher-fcc-unlock/meson.build
diff --git a/include/ModemManager-enums.h b/include/ModemManager-enums.h
index 46f9143..dd5601c 100644
--- a/include/ModemManager-enums.h
+++ b/include/ModemManager-enums.h
@@ -12,7 +12,7 @@
  *
  * Copyright (C) 2011 Red Hat, Inc.
  * Copyright (C) 2011 Google, Inc.
- * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021 Qualcomm Innovation Center, Inc.
  */
 
 #ifndef _MODEMMANAGER_ENUMS_H_
diff --git a/introspection/all.xml b/introspection/all.xml
index dda64dd..1ca5128 100644
--- a/introspection/all.xml
+++ b/introspection/all.xml
@@ -20,6 +20,7 @@
   <xi:include href="org.freedesktop.ModemManager1.Modem.Sar.xml"/>
   <xi:include href="org.freedesktop.ModemManager1.Modem.Signal.xml"/>
   <xi:include href="org.freedesktop.ModemManager1.Modem.Oma.xml"/>
+  <xi:include href="org.freedesktop.ModemManager1.Modem.Simple.xml"/>
 
   <!--xi:include href="wip-org.freedesktop.ModemManager1.Modem.Contacts.xml"/-->
 
diff --git a/libmm-glib/mm-bearer-properties.c b/libmm-glib/mm-bearer-properties.c
index dad93de..0d2d1a7 100644
--- a/libmm-glib/mm-bearer-properties.c
+++ b/libmm-glib/mm-bearer-properties.c
@@ -871,9 +871,13 @@
     if (a == b)
         return TRUE;
     /* Additional loose match UNKNOWN == NONE */
+    /* MBIM and QMI fallback to CHAP when a username or password is present,
+       but no authentication type was provided */
     if (flags & MM_BEARER_PROPERTIES_CMP_FLAGS_LOOSE) {
         if ((a == MM_BEARER_ALLOWED_AUTH_UNKNOWN && b == MM_BEARER_ALLOWED_AUTH_NONE) ||
-            (b == MM_BEARER_ALLOWED_AUTH_UNKNOWN && a == MM_BEARER_ALLOWED_AUTH_NONE))
+            (b == MM_BEARER_ALLOWED_AUTH_UNKNOWN && a == MM_BEARER_ALLOWED_AUTH_NONE) ||
+            (a == MM_BEARER_ALLOWED_AUTH_UNKNOWN && b == MM_BEARER_ALLOWED_AUTH_CHAP) ||
+            (b == MM_BEARER_ALLOWED_AUTH_UNKNOWN && a == MM_BEARER_ALLOWED_AUTH_CHAP) )
             return TRUE;
     }
     return FALSE;
diff --git a/libmm-glib/mm-common-helpers.c b/libmm-glib/mm-common-helpers.c
index b7883b2..085177b 100644
--- a/libmm-glib/mm-common-helpers.c
+++ b/libmm-glib/mm-common-helpers.c
@@ -1769,7 +1769,8 @@
                      guint    minute,
                      guint    second,
                      gboolean have_offset,
-                     gint     offset_minutes)
+                     gint     offset_minutes,
+                     GError **error)
 {
     g_autoptr(GDateTime) dt = NULL;
 
@@ -1781,6 +1782,14 @@
     } else
         dt = g_date_time_new_utc (year, month, day, hour, minute, second);
 
+    if (dt == NULL) {
+        g_set_error (error,
+                     MM_CORE_ERROR,
+                     MM_CORE_ERROR_INVALID_ARGS,
+                     "Invalid input for date: got year:%u, month:%u, day:%u, hour:%u, minute:%u, second:%u",
+                     year, month, day, hour, minute, second);
+        return NULL;
+    }
     return date_time_format_iso8601 (dt);
 }
 
diff --git a/libmm-glib/mm-common-helpers.h b/libmm-glib/mm-common-helpers.h
index d444012..490f882 100644
--- a/libmm-glib/mm-common-helpers.h
+++ b/libmm-glib/mm-common-helpers.h
@@ -216,7 +216,8 @@
                                                   guint    minute,
                                                   guint    second,
                                                   gboolean have_offset,
-                                                  gint     offset_minutes);
+                                                  gint     offset_minutes,
+                                                  GError **error);
 
 /******************************************************************************/
 /* Type checkers and conversion utilities */
diff --git a/libmm-glib/tests/test-common-helpers.c b/libmm-glib/tests/test-common-helpers.c
index 69b95cb..a1a2e61 100644
--- a/libmm-glib/tests/test-common-helpers.c
+++ b/libmm-glib/tests/test-common-helpers.c
@@ -602,18 +602,40 @@
 date_time_iso8601 (void)
 {
     gchar *date = NULL;
+    GError *error = NULL;
 
     date = mm_new_iso8601_time_from_unix_time (1634307342);
     g_assert_cmpstr (date, ==, "2021-10-15T14:15:42Z");
     g_free (date);
 
-    date = mm_new_iso8601_time (2021, 10, 15, 16, 15, 42, FALSE, 0);
+    date = mm_new_iso8601_time (2021, 10, 15, 16, 15, 42, FALSE, 0, &error);
+    g_assert_no_error (error);
     g_assert_cmpstr (date, ==, "2021-10-15T16:15:42Z");
     g_free (date);
 
-    date = mm_new_iso8601_time (2021, 10, 15, 16, 15, 42, TRUE, 120);
+    date = mm_new_iso8601_time (2021, 10, 15, 16, 15, 42, TRUE, 120, &error);
+    g_assert_no_error (error);
     g_assert_cmpstr (date, ==, "2021-10-15T16:15:42+02");
     g_free (date);
+
+    /* Valid args:
+     * - Year:[1-9999]
+     * - Month:[1-12]
+     * - Day:[1-28|29|30|31] according to year and month
+     * - Hour: [0-23]
+     * - Minute: [0-59]
+     * - Seconds: [0.0-60.0)
+     * */
+    date = mm_new_iso8601_time (2021, 13, 15, 16, 15, 42, TRUE, 120, &error);
+    g_assert_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS);
+    g_assert_null (date);
+    g_clear_error (&error);
+
+    /* No February 29 in 2021 */
+    date = mm_new_iso8601_time (2021, 2, 29, 16, 15, 42, TRUE, 120, &error);
+    g_assert_error (error, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS);
+    g_assert_null (date);
+    g_clear_error (&error);
 }
 
 /**************************************************************/
diff --git a/meson.build b/meson.build
index 991f649..354afa6 100644
--- a/meson.build
+++ b/meson.build
@@ -237,14 +237,14 @@
 # MBIM support (enabled by default)
 enable_mbim = get_option('mbim')
 if enable_mbim
-  mbim_glib_dep = dependency('mbim-glib', version: '>= 1.27.3')
+  mbim_glib_dep = dependency('mbim-glib', version: '>= 1.27.6')
 endif
 config_h.set('WITH_MBIM', enable_mbim)
 
 # QMI support (enabled by default)
 enable_qmi = get_option('qmi')
 if enable_qmi
-  qmi_glib_dep = dependency('qmi-glib', version: '>= 1.31.3')
+  qmi_glib_dep = dependency('qmi-glib', version: '>= 1.31.4')
 endif
 config_h.set('WITH_QMI', enable_qmi)
 
@@ -353,6 +353,7 @@
         break
       endif
     endforeach
+    config_h.set('ENABLE_PLUGIN_' + plugin_name.underscorify().to_upper(), true)
   endif
   plugins_options += {plugin_name: plugin_enabled}
 endforeach
@@ -366,7 +367,8 @@
 
 subdir('po')
 subdir('data')
-subdir('data/fcc-unlock')
+subdir('data/dispatcher-connection')
+subdir('data/dispatcher-fcc-unlock')
 subdir('introspection')
 subdir('include')
 
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index d5e3442..53d09f3 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -1002,6 +1002,10 @@
 libmm_plugin_linktop_la_LDFLAGS = $(PLUGIN_COMMON_LINKER_FLAGS)
 libmm_plugin_linktop_la_LIBADD = $(builddir)/libhelpers-linktop.la
 
+dist_udevrules_DATA += linktop/77-mm-linktop-port-types.rules
+
+AM_CFLAGS += -DTESTUDEVRULESDIR_LINKTOP=\"${srcdir}/linktop\"
+
 endif
 
 ################################################################################
diff --git a/plugins/altair/mm-broadband-modem-altair-lte.c b/plugins/altair/mm-broadband-modem-altair-lte.c
index 551a7ca..837d57c 100644
--- a/plugins/altair/mm-broadband-modem-altair-lte.c
+++ b/plugins/altair/mm-broadband-modem-altair-lte.c
@@ -1018,6 +1018,7 @@
                                            NULL, /* format */
                                            &operator_code,
                                            NULL, /* act */
+                                           self,
                                            error))
         return NULL;
 
@@ -1064,6 +1065,7 @@
                                            NULL, /* format */
                                            &operator_name,
                                            NULL, /* act */
+                                           self,
                                            error))
         return NULL;
 
diff --git a/plugins/cinterion/77-mm-cinterion-port-types.rules b/plugins/cinterion/77-mm-cinterion-port-types.rules
index 5ad47ce..c1a9bc4 100644
--- a/plugins/cinterion/77-mm-cinterion-port-types.rules
+++ b/plugins/cinterion/77-mm-cinterion-port-types.rules
@@ -48,6 +48,16 @@
 ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005d", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_PORT_IGNORE}="1"
 ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005d", ENV{.MM_USBIFNUM}=="08", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1"
 
+# PLS63
+#  ttyACM0 (if #0): AT port
+#  ttyACM1 (if #2): AT port
+#  ttyACM2 (if #4): GPS data port
+#  ttyACM3 (if #6): DIAG/QCDM
+ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="00", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1"
+ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1"
+ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="06", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1"
+
 # PLS83
 #  ttyACM0 (if #0): AT port
 #  ttyACM1 (if #2): AT port
diff --git a/plugins/cinterion/mm-broadband-bearer-cinterion.c b/plugins/cinterion/mm-broadband-bearer-cinterion.c
index dcc79a9..2802a75 100644
--- a/plugins/cinterion/mm-broadband-bearer-cinterion.c
+++ b/plugins/cinterion/mm-broadband-bearer-cinterion.c
@@ -44,6 +44,7 @@
  * The expected USB interface mapping is:
  *   INTERFACE=usb0 -> ID_USB_INTERFACE_NUM=0a
  *   INTERFACE=usb1 -> ID_USB_INTERFACE_NUM=0c
+ *   INTERFACE=usb0 -> ID_USB_INTERFACE_NUM=08  (PLSx3w)
  */
 static const UsbInterfaceConfig usb_interface_configs[] = {
     {
@@ -54,6 +55,10 @@
         .swwan_index   = 2,
         .usb_iface_num = 0x0c,
     },
+    {
+        .swwan_index   = 1,
+        .usb_iface_num = 0x08,
+    },
 };
 
 static gint
diff --git a/plugins/cinterion/mm-modem-helpers-cinterion.c b/plugins/cinterion/mm-modem-helpers-cinterion.c
index f8cec82..a75eb53 100644
--- a/plugins/cinterion/mm-modem-helpers-cinterion.c
+++ b/plugins/cinterion/mm-modem-helpers-cinterion.c
@@ -1201,6 +1201,7 @@
                              MMNetworkTimezone **tzp,
                              GError            **error)
 {
+    gboolean ret = TRUE;
     guint year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0, dst = 0;
     gint tz = 0;
 
@@ -1229,7 +1230,9 @@
         /* Return ISO-8601 format date/time string */
         *iso8601p = mm_new_iso8601_time (year, month, day, hour,
                                          minute, second,
-                                         TRUE, tz * 15);
+                                         TRUE, tz * 15,
+                                         error);
+        ret = (*iso8601p != NULL);
     }
 
     if (tzp) {
@@ -1245,7 +1248,7 @@
     if (tzp && mm_get_uint_from_match_info (match_info, 8, &dst))
         mm_network_timezone_set_dst_offset (*tzp, dst * 60);
 
-    return TRUE;
+    return ret;
 }
 
 /*****************************************************************************/
diff --git a/plugins/foxconn/mm-broadband-modem-mbim-foxconn.c b/plugins/foxconn/mm-broadband-modem-mbim-foxconn.c
index 22ab0be..1473dca 100644
--- a/plugins/foxconn/mm-broadband-modem-mbim-foxconn.c
+++ b/plugins/foxconn/mm-broadband-modem-mbim-foxconn.c
@@ -81,52 +81,86 @@
     return g_task_propagate_pointer (G_TASK (res), error);
 }
 
-static void
-foxconn_get_firmware_version_ready (QmiClientDms *client,
-                                    GAsyncResult *res,
-                                    GTask        *task)
+static gboolean
+needs_qdu_and_mcfg_apps_version (MMIfaceModemFirmware *self)
 {
-    QmiMessageDmsFoxconnGetFirmwareVersionOutput *output;
-    GError                                       *error = NULL;
-    MMFirmwareUpdateSettings                     *update_settings = NULL;
-    const gchar                                  *str;
-    MMIfaceModemFirmware                         *self;
-    guint                                         vendor_id;
-    guint                                         product_id;
+    guint vendor_id;
+    guint product_id;
 
-    output = qmi_client_dms_foxconn_get_firmware_version_finish (client, res, &error);
-    if (!output || !qmi_message_dms_foxconn_get_firmware_version_output_get_result (output, &error))
-        goto out;
-
-    /* Create update settings now:
-     * 0x105b is the T99W175 module, T99W175 supports QDU,
-     * T99W265(0x0489:0xe0da ; 0x0489:0xe0db): supports QDU
-     * else support FASTBOOT and QMI PDC.
+    /* 0x105b is the T99W175 module, T99W175 supports QDU and requires MCFG+APPS version.
+     * T99W265(0x0489:0xe0da ; 0x0489:0xe0db): supports QDU and requires MCFG+APPS version.
+     * else support FASTBOOT and QMI PDC, and require only MCFG version.
      */
-    self = g_task_get_source_object (task);
     vendor_id = mm_base_modem_get_vendor_id (MM_BASE_MODEM (self));
     product_id = mm_base_modem_get_product_id (MM_BASE_MODEM (self));
-    if (vendor_id == 0x105b || (vendor_id == 0x0489 && (product_id  == 0xe0da || product_id == 0xe0db)))
-        update_settings = mm_firmware_update_settings_new (MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU);
-    else {
-        update_settings = mm_firmware_update_settings_new (MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT |
-                                                           MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC);
+    return (vendor_id == 0x105b || (vendor_id == 0x0489 && (product_id  == 0xe0da || product_id == 0xe0db)));
+}
+
+static MMFirmwareUpdateSettings *
+create_update_settings (MMIfaceModemFirmware *self,
+                        const gchar          *version_str)
+{
+    MMModemFirmwareUpdateMethod  methods = MM_MODEM_FIRMWARE_UPDATE_METHOD_NONE;
+    MMFirmwareUpdateSettings    *update_settings = NULL;
+
+    if (needs_qdu_and_mcfg_apps_version (self))
+        methods = MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU;
+    else
+        methods = MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT | MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC;
+
+    update_settings = mm_firmware_update_settings_new (methods);
+    if (methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT)
         mm_firmware_update_settings_set_fastboot_at (update_settings, "AT^FASTBOOT");
+    mm_firmware_update_settings_set_version (update_settings, version_str);
+    return update_settings;
+}
+
+static void
+dms_foxconn_get_firmware_version_ready (QmiClientDms *client,
+                                        GAsyncResult *res,
+                                        GTask        *task)
+{
+    g_autoptr(QmiMessageDmsFoxconnGetFirmwareVersionOutput)  output = NULL;
+    GError                                                  *error = NULL;
+    const gchar                                             *str;
+
+    output = qmi_client_dms_foxconn_get_firmware_version_finish (client, res, &error);
+    if (!output || !qmi_message_dms_foxconn_get_firmware_version_output_get_result (output, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
     }
 
     qmi_message_dms_foxconn_get_firmware_version_output_get_version (output, &str, NULL);
-    mm_firmware_update_settings_set_version (update_settings, str);
 
- out:
-    if (error)
-        g_task_return_error (task, error);
-    else {
-        g_assert (update_settings);
-        g_task_return_pointer (task, update_settings, g_object_unref);
-    }
+    g_task_return_pointer (task,
+                           create_update_settings (g_task_get_source_object (task), str),
+                           g_object_unref);
     g_object_unref (task);
-    if (output)
-        qmi_message_dms_foxconn_get_firmware_version_output_unref (output);
+}
+
+static void
+fox_get_firmware_version_ready (QmiClientFox *client,
+                                GAsyncResult *res,
+                                GTask        *task)
+{
+    g_autoptr(QmiMessageFoxGetFirmwareVersionOutput)  output = NULL;
+    GError                                           *error = NULL;
+    const gchar                                      *str;
+
+    output = qmi_client_fox_get_firmware_version_finish (client, res, &error);
+    if (!output || !qmi_message_fox_get_firmware_version_output_get_result (output, &error)) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    qmi_message_fox_get_firmware_version_output_get_version (output, &str, NULL);
+
+    g_task_return_pointer (task,
+                           create_update_settings (g_task_get_source_object (task), str),
+                           g_object_unref);
+    g_object_unref (task);
 }
 
 static void
@@ -134,47 +168,61 @@
                                GAsyncReadyCallback   callback,
                                gpointer              user_data)
 {
-    GTask                                       *task;
-    QmiMessageDmsFoxconnGetFirmwareVersionInput *input = NULL;
-    QmiClient                                   *client = NULL;
-    guint                                        vendor_id;
-    guint                                        product_id;
+    GTask     *task;
+    QmiClient *fox_client = NULL;
+    QmiClient *dms_client = NULL;
 
     task = g_task_new (self, NULL, callback, user_data);
 
-    client = mm_shared_qmi_peek_client (MM_SHARED_QMI (self),
-                                        QMI_SERVICE_DMS,
-                                        MM_PORT_QMI_FLAG_DEFAULT,
-                                        NULL);
-    if (!client) {
-        g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                                 "Unable to load version info: no QMI DMS client available");
-        g_object_unref (task);
+    /* Try to get firmware version over fox service, if it failed to peek client, try dms service. */
+    fox_client = mm_shared_qmi_peek_client (MM_SHARED_QMI (self), QMI_SERVICE_FOX, MM_PORT_QMI_FLAG_DEFAULT, NULL);
+    if (!fox_client) {
+        dms_client = mm_shared_qmi_peek_client (MM_SHARED_QMI (self), QMI_SERVICE_DMS, MM_PORT_QMI_FLAG_DEFAULT, NULL);
+        if (!dms_client) {
+            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                     "Unable to load version info: no FOX or DMS client available");
+            g_object_unref (task);
+            return;
+        }
+    }
+
+    if (fox_client) {
+        g_autoptr(QmiMessageFoxGetFirmwareVersionInput) input = NULL;
+
+        input = qmi_message_fox_get_firmware_version_input_new ();
+        qmi_message_fox_get_firmware_version_input_set_version_type (input,
+                                                                     (needs_qdu_and_mcfg_apps_version (self) ?
+                                                                      QMI_FOX_FIRMWARE_VERSION_TYPE_FIRMWARE_MCFG_APPS :
+                                                                      QMI_FOX_FIRMWARE_VERSION_TYPE_FIRMWARE_MCFG),
+                                                                     NULL);
+        qmi_client_fox_get_firmware_version (QMI_CLIENT_FOX (fox_client),
+                                             input,
+                                             10,
+                                             NULL,
+                                             (GAsyncReadyCallback)fox_get_firmware_version_ready,
+                                             task);
         return;
     }
 
-    vendor_id = mm_base_modem_get_vendor_id (MM_BASE_MODEM (self));
-    product_id = mm_base_modem_get_product_id (MM_BASE_MODEM (self));
-    input = qmi_message_dms_foxconn_get_firmware_version_input_new ();
-    /* 0x105b is the T99W175 module, T99W175/T99W265 need to compare the apps version. */
-    if (vendor_id == 0x105b || (vendor_id == 0x0489 && (product_id  == 0xe0da || product_id == 0xe0db)))
-        qmi_message_dms_foxconn_get_firmware_version_input_set_version_type (
-            input,
-            QMI_DMS_FOXCONN_FIRMWARE_VERSION_TYPE_FIRMWARE_MCFG_APPS,
-            NULL);
-    else
-        qmi_message_dms_foxconn_get_firmware_version_input_set_version_type (
-            input,
-            QMI_DMS_FOXCONN_FIRMWARE_VERSION_TYPE_FIRMWARE_MCFG,
-            NULL);
-    qmi_client_dms_foxconn_get_firmware_version (
-        QMI_CLIENT_DMS (client),
-        input,
-        10,
-        NULL,
-        (GAsyncReadyCallback)foxconn_get_firmware_version_ready,
-        task);
-    qmi_message_dms_foxconn_get_firmware_version_input_unref (input);
+    if (dms_client) {
+        g_autoptr(QmiMessageDmsFoxconnGetFirmwareVersionInput) input = NULL;
+
+        input = qmi_message_dms_foxconn_get_firmware_version_input_new ();
+        qmi_message_dms_foxconn_get_firmware_version_input_set_version_type (input,
+                                                                             (needs_qdu_and_mcfg_apps_version (self) ?
+                                                                              QMI_DMS_FOXCONN_FIRMWARE_VERSION_TYPE_FIRMWARE_MCFG_APPS:
+                                                                              QMI_DMS_FOXCONN_FIRMWARE_VERSION_TYPE_FIRMWARE_MCFG),
+                                                                             NULL);
+        qmi_client_dms_foxconn_get_firmware_version (QMI_CLIENT_DMS (dms_client),
+                                                     input,
+                                                     10,
+                                                     NULL,
+                                                     (GAsyncReadyCallback)dms_foxconn_get_firmware_version_ready,
+                                                     task);
+        return;
+    }
+
+    g_assert_not_reached ();
 }
 
 #endif
diff --git a/plugins/huawei/77-mm-huawei-net-port-types.rules b/plugins/huawei/77-mm-huawei-net-port-types.rules
index 214d7bc..fed7da0 100644
--- a/plugins/huawei/77-mm-huawei-net-port-types.rules
+++ b/plugins/huawei/77-mm-huawei-net-port-types.rules
@@ -31,4 +31,7 @@
 # R215, Disable CPOL based features
 ATTRS{idVendor}=="12d1", ATTRS{idProduct}=="1588", ENV{ID_MM_PREFERRED_NETWORKS_CPOL_DISABLED}="1"
 
+# E226, Disable CPOL based features
+ATTRS{idVendor}=="12d1", ATTRS{idProduct}=="1003", ENV{ID_MM_PREFERRED_NETWORKS_CPOL_DISABLED}="1"
+
 LABEL="mm_huawei_port_types_end"
diff --git a/plugins/huawei/mm-modem-helpers-huawei.c b/plugins/huawei/mm-modem-helpers-huawei.c
index b854841..3ded623 100644
--- a/plugins/huawei/mm-modem-helpers-huawei.c
+++ b/plugins/huawei/mm-modem-helpers-huawei.c
@@ -1228,6 +1228,9 @@
             mm_get_uint_from_match_info (match_info, 6, &second) &&
             mm_get_int_from_match_info  (match_info, 7, &tz) &&
             mm_get_uint_from_match_info (match_info, 8, &dt)) {
+
+            ret = TRUE;
+
             /* adjust year */
             if (year < 100)
                 year += 2000;
@@ -1240,7 +1243,9 @@
                 /* Return ISO-8601 format date/time string */
                 *iso8601p = mm_new_iso8601_time (year, month, day, hour,
                                                  minute, second,
-                                                 TRUE, (tz * 15) + (dt * 60));
+                                                 TRUE, (tz * 15) + (dt * 60),
+                                                 error);
+                ret = (*iso8601p != NULL);
             }
             if (tzp) {
                 *tzp = mm_network_timezone_new ();
@@ -1248,7 +1253,6 @@
                 mm_network_timezone_set_dst_offset (*tzp, dt * 60);
             }
 
-            ret = TRUE;
         } else {
             g_set_error_literal (error,
                                  MM_CORE_ERROR,
@@ -1312,14 +1316,19 @@
             mm_get_uint_from_match_info (match_info, 4, &hour) &&
             mm_get_uint_from_match_info (match_info, 5, &minute) &&
             mm_get_uint_from_match_info (match_info, 6, &second)) {
+            ret = TRUE;
+
             /* adjust year */
             if (year < 100)
                 year += 2000;
+
             /* Return ISO-8601 format date/time string */
-            if (iso8601p)
+            if (iso8601p) {
                 *iso8601p = mm_new_iso8601_time (year, month, day, hour,
-                                                 minute, second, FALSE, 0);
-            ret = TRUE;
+                                                 minute, second, FALSE, 0,
+                                                 error);
+                ret = (*iso8601p != NULL);
+            }
         } else {
             g_set_error_literal (error,
                                  MM_CORE_ERROR,
diff --git a/plugins/icera/mm-broadband-modem-icera.c b/plugins/icera/mm-broadband-modem-icera.c
index 26016c4..96cb45f 100644
--- a/plugins/icera/mm-broadband-modem-icera.c
+++ b/plugins/icera/mm-broadband-modem-icera.c
@@ -1569,6 +1569,7 @@
                         MMNetworkTimezone **tz,
                         GError **error)
 {
+    gboolean ret = TRUE;
     gint year;
     gint month;
     gint day;
@@ -1649,11 +1650,13 @@
                                         g_date_time_get_minute (adjusted),
                                         g_date_time_get_second (adjusted),
                                         TRUE,
-                                        offset);
+                                        offset,
+                                        error);
+        ret = (*iso8601 != NULL);
     }
 
     g_date_time_unref (adjusted);
-    return TRUE;
+    return ret;
 }
 
 static MMNetworkTimezone *
diff --git a/plugins/linktop/77-mm-linktop-port-types.rules b/plugins/linktop/77-mm-linktop-port-types.rules
new file mode 100644
index 0000000..dc2ef0d
--- /dev/null
+++ b/plugins/linktop/77-mm-linktop-port-types.rules
@@ -0,0 +1,16 @@
+# do not edit this file, it will be overwritten on update
+
+ACTION!="add|change|move|bind", GOTO="mm_linktop_end"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="230d", GOTO="mm_linktop_generic"
+GOTO="mm_linktop_end"
+
+LABEL="mm_linktop_generic"
+SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}"
+
+# Linktop HSPADataCard
+#  ttyACM0 (if #1): Data port
+#  ttyACM1 (if #3): Primary AT port
+ATTRS{idVendor}=="230d", ATTRS{idProduct}=="0001", ENV{.MM_USBIFNUM}=="01", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PPP}="1"
+ATTRS{idVendor}=="230d", ATTRS{idProduct}=="0001", ENV{.MM_USBIFNUM}=="03", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1"
+
+LABEL="mm_linktop_end"
diff --git a/plugins/meson.build b/plugins/meson.build
index d19ec08..5169548 100644
--- a/plugins/meson.build
+++ b/plugins/meson.build
@@ -34,6 +34,8 @@
 plugins = {}
 plugins_data = []
 plugins_udev_rules = []
+plugins_test_udev_rules_dir_c_args = []
+plugins_test_keyfile_c_args = []
 
 # never include static libs as deps when building
 # plugins or shared utils modules
@@ -264,14 +266,12 @@
 
 # plugin: broadmobi
 if plugins_options['broadmobi']
-  c_args = [
-    '-DMM_MODULE_NAME="broadmobi"',
-    '-DTESTUDEVRULESDIR_BROADMOBI="@0@"'.format(plugins_dir / 'broadmobi'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_BROADMOBI="@0@"'.format(plugins_dir / 'broadmobi')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   plugins += {'plugin-broadmobi': {
     'plugin': true,
-    'module': {'sources': files('broadmobi/mm-plugin-broadmobi.c'), 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': files('broadmobi/mm-plugin-broadmobi.c'), 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="broadmobi"']},
   }}
 
   plugins_udev_rules += files('broadmobi/77-mm-broadmobi-port-types.rules')
@@ -279,10 +279,10 @@
 
 # plugin: cinterion (previously siemens)
 if plugins_options['cinterion']
-  common_c_args = [
-    '-DMM_MODULE_NAME="cinterion"',
-    '-DTESTUDEVRULESDIR_CINTERION="@0@"'.format(plugins_dir / 'cinterion'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_CINTERION="@0@"'.format(plugins_dir / 'cinterion')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
+  common_c_args = test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="cinterion"']
 
   sources = files(
     'cinterion/mm-broadband-bearer-cinterion.c',
@@ -311,6 +311,9 @@
 
 # plugin: dell
 if plugins_options['dell']
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_DELL="@0@"'.format(plugins_dir / 'dell')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
   incs = plugins_incs + [
     foxconn_inc,
     novatel_inc,
@@ -319,14 +322,9 @@
     xmm_inc,
   ]
 
-  c_args = [
-    '-DMM_MODULE_NAME="dell"',
-    '-DTESTUDEVRULESDIR_DELL="@0@"'.format(plugins_dir / 'dell'),
-  ]
-
   plugins += {'plugin-dell': {
     'plugin': true,
-    'module': {'sources': files('dell/mm-plugin-dell.c'), 'include_directories': incs, 'c_args': c_args}
+    'module': {'sources': files('dell/mm-plugin-dell.c'), 'include_directories': incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="dell"']},
   }}
 
   plugins_udev_rules += files('dell/77-mm-dell-port-types.rules')
@@ -334,14 +332,12 @@
 
 # plugin: dlink
 if plugins_options['dlink']
-  c_args = [
-    '-DMM_MODULE_NAME="d-link"',
-    '-DTESTUDEVRULESDIR_DLINK="@0@"'.format(plugins_dir / 'dlink'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_DLINK="@0@"'.format(plugins_dir / 'dlink')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   plugins += {'plugin-dlink': {
     'plugin': true,
-    'module': {'sources': files('dlink/mm-plugin-dlink.c'), 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': files('dlink/mm-plugin-dlink.c'), 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="d-link"']},
   }}
 
   plugins_udev_rules += files('dlink/77-mm-dlink-port-types.rules')
@@ -349,10 +345,8 @@
 
 # plugin: fibocom
 if plugins_options['fibocom']
-  c_args = [
-    '-DMM_MODULE_NAME="fibocom"',
-    '-DTESTUDEVRULESDIR_FIBOCOM="@0@"'.format(plugins_dir / 'fibocom'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_FIBOCOM="@0@"'.format(plugins_dir / 'fibocom')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   sources = files(
     'fibocom/mm-broadband-bearer-fibocom-ecm.c',
@@ -367,7 +361,7 @@
   endif
   plugins += {'plugin-fibocom': {
     'plugin': true,
-    'module': {'sources': sources, 'include_directories': plugins_incs + [xmm_inc] + [fibocom_inc], 'c_args': c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs + [xmm_inc] + [fibocom_inc], 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="fibocom"']},
   }}
 
   plugins_udev_rules += files('fibocom/77-mm-fibocom-port-types.rules')
@@ -377,15 +371,15 @@
 if plugins_options['foxconn']
   foxconn_dir = plugins_dir / 'foxconn'
 
-  c_args = [
-    '-DMM_MODULE_NAME="foxconn"',
-    '-DTESTUDEVRULESDIR_FOXCONN="@0@"'.format(foxconn_dir),
-	'-DTESTKEYFILE_FOXCONN_T77W968="@0@"'.format(foxconn_dir / 'mm-foxconn-t77w968-carrier-mapping.conf'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_FOXCONN="@0@"'.format(foxconn_dir)]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
+  test_keyfile_c_args = ['-DTESTKEYFILE_FOXCONN_T77W968="@0@"'.format(foxconn_dir / 'mm-foxconn-t77w968-carrier-mapping.conf')]
+  plugins_test_keyfile_c_args += test_keyfile_c_args
 
   plugins += {'plugin-foxconn': {
     'plugin': true,
-    'module': {'sources': files('foxconn/mm-plugin-foxconn.c'), 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': files('foxconn/mm-plugin-foxconn.c'), 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + test_keyfile_c_args + ['-DMM_MODULE_NAME="foxconn"']},
   }}
 
   plugins_data += files(
@@ -405,14 +399,12 @@
 
 # plugin: gosuncn
 if plugins_options['gosuncn']
-  c_args = [
-    '-DMM_MODULE_NAME="gosuncn"',
-    '-DTESTUDEVRULESDIR_GOSUNCN="@0@"'.format(plugins_dir / 'gosuncn'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_GOSUNCN="@0@"'.format(plugins_dir / 'gosuncn')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   plugins += {'plugin-gosuncn': {
     'plugin': true,
-    'module': {'sources': files('gosuncn/mm-plugin-gosuncn.c'), 'include_directories': plugins_incs, 'c_args': c_args}
+    'module': {'sources': files('gosuncn/mm-plugin-gosuncn.c'), 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="gosuncn"']},
   }}
 
   plugins_udev_rules += files('gosuncn/77-mm-gosuncn-port-types.rules')
@@ -420,14 +412,12 @@
 
 # plugin: haier
 if plugins_options['haier']
-  c_args = [
-    '-DMM_MODULE_NAME="haier"',
-    '-DTESTUDEVRULESDIR_HAIER="@0@"'.format(plugins_dir / 'haier'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_HAIER="@0@"'.format(plugins_dir / 'haier')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   plugins += {'plugin-haier': {
     'plugin': true,
-    'module': {'sources': files('haier/mm-plugin-haier.c'), 'include_directories': plugins_incs, 'c_args': c_args}
+    'module': {'sources': files('haier/mm-plugin-haier.c'), 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="haier"']},
   }}
 
   plugins_udev_rules += files('haier/77-mm-haier-port-types.rules')
@@ -437,7 +427,10 @@
 if plugins_options['huawei']
   huawei_inc = include_directories('huawei')
 
-  common_c_args = ['-DTESTUDEVRULESDIR_HUAWEI="@0@"'.format(plugins_dir / 'huawei')]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_HUAWEI="@0@"'.format(plugins_dir / 'huawei')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
+  common_c_args = test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="huawei"']
 
   headers = files('huawei/mm-modem-helpers-huawei.h')
 
@@ -468,8 +461,8 @@
 
   plugins += {'plugin-huawei': {
     'plugin': true,
-    'helper': {'sources': files('huawei/mm-modem-helpers-huawei.c') + daemon_enums_sources, 'include_directories': plugins_incs + [huawei_inc], 'c_args': common_c_args + ['-DMM_MODULE_NAME="huawei"']},
-    'module': {'sources': sources + enums_sources + port_enums_sources + daemon_enums_sources, 'include_directories': plugins_incs + [huawei_inc], 'c_args': common_c_args + ['-DMM_MODULE_NAME="huawei"']},
+    'helper': {'sources': files('huawei/mm-modem-helpers-huawei.c') + daemon_enums_sources, 'include_directories': plugins_incs + [huawei_inc], 'c_args': common_c_args},
+    'module': {'sources': sources + enums_sources + port_enums_sources + daemon_enums_sources, 'include_directories': plugins_incs + [huawei_inc], 'c_args': common_c_args},
     'test': {'sources': files('huawei/tests/test-modem-helpers-huawei.c') + enums_sources, 'include_directories': huawei_inc, 'dependencies': libhelpers_dep},
   }}
 
@@ -511,7 +504,10 @@
 
 # plugin: linktop
 if plugins_options['linktop']
-  common_c_args = '-DMM_MODULE_NAME="linktop"'
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_LINKTOP="@0@"'.format(plugins_dir / 'linktop')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
+  common_c_args = test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="linktop"']
 
   sources = files(
     'linktop/mm-plugin-linktop.c',
@@ -524,23 +520,23 @@
     'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': common_c_args},
     'test': {'sources': files('linktop/tests/test-modem-helpers-linktop.c'), 'include_directories': include_directories('linktop'), 'dependencies': libhelpers_dep},
   }}
+
+  plugins_udev_rules += files('linktop/77-mm-linktop-port-types.rules')
 endif
 
 # plugin: longcheer (and rebranded dongles)
 if plugins_options['longcheer']
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_LONGCHEER="@0@"'.format(plugins_dir / 'longcheer')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
   sources = files(
     'longcheer/mm-broadband-modem-longcheer.c',
     'longcheer/mm-plugin-longcheer.c',
   )
 
-  c_args = [
-    '-DMM_MODULE_NAME="longcheer"',
-    '-DTESTUDEVRULESDIR_LONGCHEER="@0@"'.format(plugins_dir / 'longcheer'),
-  ]
-
   plugins += {'plugin-longcheer': {
     'plugin': true,
-    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="longcheer"']},
   }}
 
   plugins_udev_rules += files('longcheer/77-mm-longcheer-port-types.rules')
@@ -548,7 +544,10 @@
 
 # plugin: ericsson mbm
 if plugins_options['mbm']
-  common_c_args = ['-DTESTUDEVRULESDIR_MBM="@0@"'.format(plugins_dir / 'mbm')]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_MBM="@0@"'.format(plugins_dir / 'mbm')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
+  common_c_args = test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="ericsson-mbm"']
 
   sources = files(
     'mbm/mm-broadband-bearer-mbm.c',
@@ -559,8 +558,8 @@
 
   plugins += {'plugin-ericsson-mbm': {
     'plugin': true,
-    'helper': {'sources': files('mbm/mm-modem-helpers-mbm.c'), 'include_directories': plugins_incs, 'c_args': common_c_args + ['-DMM_MODULE_NAME="ericsson-mbm"']},
-    'module': {'sources': sources + daemon_enums_sources, 'include_directories': plugins_incs, 'c_args': common_c_args + ['-DMM_MODULE_NAME="ericsson-mbm"']},
+    'helper': {'sources': files('mbm/mm-modem-helpers-mbm.c'), 'include_directories': plugins_incs, 'c_args': common_c_args},
+    'module': {'sources': sources + daemon_enums_sources, 'include_directories': plugins_incs, 'c_args': common_c_args},
     'test': {'sources': files('mbm/tests/test-modem-helpers-mbm.c'), 'include_directories': plugins_incs + [include_directories('mbm')], 'dependencies': libhelpers_dep},
   }}
 
@@ -582,19 +581,17 @@
 
 # plugin: mtk
 if plugins_options['mtk']
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_MTK="@0@"'.format(plugins_dir / 'mtk')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
   sources = files(
     'mtk/mm-broadband-modem-mtk.c',
     'mtk/mm-plugin-mtk.c',
   )
 
-  c_args = [
-    '-DMM_MODULE_NAME="motorola"',
-    '-DTESTUDEVRULESDIR_MTK="@0@"'.format(plugins_dir / 'mtk'),
-  ]
-
   plugins += {'plugin-mtk': {
     'plugin': true,
-    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="motorola"']},
   }}
 
   plugins_udev_rules += files('mtk/77-mm-mtk-port-types.rules')
@@ -616,14 +613,12 @@
 
 # plugin: nokia (icera)
 if plugins_options['nokia-icera']
-  c_args = [
-    '-DMM_MODULE_NAME="nokia-icera"',
-    '-DTESTUDEVRULESDIR_NOKIA_ICERA="@0@"'.format(plugins_dir / 'nokia'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_NOKIA_ICERA="@0@"'.format(plugins_dir / 'nokia')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   plugins += {'plugin-nokia-icera': {
     'plugin': true,
-    'module': {'sources': files('nokia/mm-plugin-nokia-icera.c'), 'include_directories': plugins_incs + [icera_inc], 'c_args': c_args},
+    'module': {'sources': files('nokia/mm-plugin-nokia-icera.c'), 'include_directories': plugins_incs + [icera_inc], 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="nokia-icera"']},
   }}
 
   plugins_udev_rules += files('nokia/77-mm-nokia-port-types.rules')
@@ -690,19 +685,17 @@
 
 # plugin: qcom-soc
 if plugins_options['qcom-soc']
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_QCOM_SOC="@0@"'.format(plugins_dir / 'qcom-soc')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
   sources = files(
     'qcom-soc/mm-broadband-modem-qmi-qcom-soc.c',
     'qcom-soc/mm-plugin-qcom-soc.c',
   )
 
-  c_args = [
-    '-DMM_MODULE_NAME="qcom-soc"',
-    '-DTESTUDEVRULESDIR_QCOM_SOC="@0@"'.format(plugins_dir / 'qcom-soc'),
-  ]
-
   plugins += {'plugin-qcom-soc': {
     'plugin': true,
-    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="qcom-soc"']},
   }}
 
   plugins_udev_rules += files('qcom-soc/77-mm-qcom-soc.rules')
@@ -710,7 +703,10 @@
 
 # plugin: quectel
 if plugins_options['quectel']
-  common_c_args = ['-DTESTUDEVRULESDIR_QUECTEL="@0@"'.format(plugins_dir / 'quectel')]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_QUECTEL="@0@"'.format(plugins_dir / 'quectel')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
+  common_c_args = test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="quectel"']
 
   sources = files(
     'quectel/mm-broadband-modem-quectel.c',
@@ -728,8 +724,8 @@
 
   plugins += {'plugin-quectel': {
     'plugin': true,
-    'helper': {'sources': files('quectel/mm-modem-helpers-quectel.c'), 'include_directories': plugins_incs, 'c_args': common_c_args + ['-DMM_MODULE_NAME="quectel"']},
-    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': common_c_args + ['-DMM_MODULE_NAME="quectel"']},
+    'helper': {'sources': files('quectel/mm-modem-helpers-quectel.c'), 'include_directories': plugins_incs, 'c_args': common_c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': common_c_args},
     'test': {'sources': files('quectel/tests/test-modem-helpers-quectel.c'), 'include_directories': include_directories('quectel'), 'dependencies': libhelpers_dep},
   }}
 
@@ -774,7 +770,10 @@
 
 # plugin: simtech
 if plugins_options['simtech']
-  common_c_args = ['-DTESTUDEVRULESDIR_SIMTECH="@0@"'.format(plugins_dir / 'simtech')]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_SIMTECH="@0@"'.format(plugins_dir / 'simtech')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
+  common_c_args = test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="simtech"']
 
   sources = files(
     'simtech/mm-broadband-modem-simtech.c',
@@ -788,8 +787,8 @@
 
   plugins += {'plugin-simtech': {
     'plugin': true,
-    'helper': {'sources': files('simtech/mm-modem-helpers-simtech.c'), 'include_directories': plugins_incs, 'c_args': common_c_args + ['-DMM_MODULE_NAME="simtech"']},
-    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': common_c_args + ['-DMM_MODULE_NAME="quectel"']},
+    'helper': {'sources': files('simtech/mm-modem-helpers-simtech.c'), 'include_directories': plugins_incs, 'c_args': common_c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': common_c_args},
     'test': {'sources': files('simtech/tests/test-modem-helpers-simtech.c'), 'include_directories': plugins_incs + [include_directories('simtech')], 'dependencies': libport_dep},
   }}
 
@@ -798,14 +797,12 @@
 
 # plugin: telit
 if plugins_options['telit']
-  c_args = [
-    '-DMM_MODULE_NAME="telit"',
-    '-DTESTUDEVRULESDIR_TELIT="@0@"'.format(plugins_dir / 'telit'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_TELIT="@0@"'.format(plugins_dir / 'telit')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   plugins += {'plugin-telit': {
     'plugin': true,
-    'module': {'sources': files('telit/mm-plugin-telit.c'), 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': files('telit/mm-plugin-telit.c'), 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="telit"']},
   }}
 
   plugins_udev_rules += files('telit/77-mm-telit-port-types.rules')
@@ -830,14 +827,12 @@
 
 # plugin: tplink
 if plugins_options['tplink']
-  c_args = [
-    '-DMM_MODULE_NAME="tp-link"',
-    '-DTESTUDEVRULESDIR_TPLINK="@0@"'.format(plugins_dir / 'tplink'),
-  ]
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_TPLINK="@0@"'.format(plugins_dir / 'tplink')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
 
   plugins += {'plugin-tplink': {
     'plugin': true,
-    'module': {'sources': files('tplink/mm-plugin-tplink.c'), 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': files('tplink/mm-plugin-tplink.c'), 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="tp-link"']},
   }}
 
   plugins_udev_rules += files('tplink/77-mm-tplink-port-types.rules')
@@ -913,19 +908,17 @@
 
 # plugin: alcatel/TCT/JRD x220D and possibly others
 if plugins_options['x22x']
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_X22X="@0@"'.format(plugins_dir / 'x22x')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
   sources = files(
     'x22x/mm-broadband-modem-x22x.c',
     'x22x/mm-plugin-x22x.c',
   )
 
-  c_args = [
-    '-DMM_MODULE_NAME="x22x"',
-    '-DTESTUDEVRULESDIR_X22X="@0@"'.format(plugins_dir / 'x22x'),
-  ]
-
   plugins += {'plugin-x22x': {
     'plugin': true,
-    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs, 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="x22x"']},
   }}
 
   plugins_udev_rules += files('x22x/77-mm-x22x-port-types.rules')
@@ -933,6 +926,9 @@
 
 # plugin: zte
 if plugins_options['zte']
+  test_udev_rules_dir_c_args = ['-DTESTUDEVRULESDIR_ZTE="@0@"'.format(plugins_dir / 'zte')]
+  plugins_test_udev_rules_dir_c_args += test_udev_rules_dir_c_args
+
   sources = files(
     'zte/mm-broadband-modem-zte.c',
     'zte/mm-broadband-modem-zte-icera.c',
@@ -940,14 +936,9 @@
     'zte/mm-plugin-zte.c',
   )
 
-  c_args = [
-    '-DMM_MODULE_NAME="zte"',
-    '-DTESTUDEVRULESDIR_ZTE="@0@"'.format(plugins_dir / 'zte'),
-  ]
-
   plugins += {'plugin-zte': {
     'plugin': true,
-    'module': {'sources': sources, 'include_directories': plugins_incs + [icera_inc], 'c_args': c_args},
+    'module': {'sources': sources, 'include_directories': plugins_incs + [icera_inc], 'c_args': test_udev_rules_dir_c_args + ['-DMM_MODULE_NAME="zte"']},
   }}
 
   plugins_udev_rules += files('zte/77-mm-zte-port-types.rules')
@@ -1005,8 +996,8 @@
 
 # udev-rules and keyfiles tests
 test_units = {
-  'udev-rules': {'include_directories': top_inc, 'dependencies': libkerneldevice_dep},
-  'keyfiles': {'include_directories': [top_inc, src_inc], 'dependencies': libmm_glib_dep},
+  'udev-rules': {'include_directories': top_inc, 'dependencies': libkerneldevice_dep, 'c_args': plugins_test_udev_rules_dir_c_args},
+  'keyfiles': {'include_directories': [top_inc, src_inc], 'dependencies': libmm_glib_dep, 'c_args': plugins_test_keyfile_c_args},
 }
 
 foreach name, data: test_units
diff --git a/plugins/novatel/mm-broadband-modem-novatel.c b/plugins/novatel/mm-broadband-modem-novatel.c
index 4eba0e1..1cc88e9 100644
--- a/plugins/novatel/mm-broadband-modem-novatel.c
+++ b/plugins/novatel/mm-broadband-modem-novatel.c
@@ -1431,13 +1431,13 @@
             mm_get_int_from_match_info (match_info, 8, &utc_offset)) {
 
             result = mm_new_iso8601_time (year, month, day, hour, minute, second,
-                                          TRUE, utc_offset * 60);
+                                          TRUE, utc_offset * 60, error);
             if (out_tz) {
                 *out_tz = mm_network_timezone_new ();
                 mm_network_timezone_set_offset (*out_tz, utc_offset * 60);
             }
 
-            success = TRUE;
+            success = (result != NULL);
         } else {
             g_set_error_literal (error,
                                  MM_CORE_ERROR,
diff --git a/plugins/sierra/mm-broadband-modem-sierra.c b/plugins/sierra/mm-broadband-modem-sierra.c
index 518f8ad..3ac2080 100644
--- a/plugins/sierra/mm-broadband-modem-sierra.c
+++ b/plugins/sierra/mm-broadband-modem-sierra.c
@@ -1656,7 +1656,7 @@
             mm_get_uint_from_match_info (match_info, 4, &hour) &&
             mm_get_uint_from_match_info (match_info, 5, &minute) &&
             mm_get_uint_from_match_info (match_info, 6, &second)) {
-            result = mm_new_iso8601_time (year, month, day, hour, minute, second, FALSE, 0);
+            result = mm_new_iso8601_time (year, month, day, hour, minute, second, FALSE, 0, error);
         } else {
             g_set_error (error,
                          MM_CORE_ERROR,
diff --git a/plugins/telit/mm-broadband-modem-mbim-telit.c b/plugins/telit/mm-broadband-modem-mbim-telit.c
index 3ac17bc..97b7575 100644
--- a/plugins/telit/mm-broadband-modem-mbim-telit.c
+++ b/plugins/telit/mm-broadband-modem-mbim-telit.c
@@ -57,13 +57,14 @@
 {
     MMModemModeCombination modes_combination;
     MMModemMode modes_mask = MM_MODEM_MODE_NONE;
-    const gchar *response;
-    GArray      *modes;
-    GArray      *all;
-    GArray      *combinations;
-    GArray      *filtered;
-    GError      *error = NULL;
-    guint        i;
+    const gchar   *response;
+    GArray        *modes;
+    GArray        *all;
+    GArray        *combinations;
+    GArray        *filtered;
+    GError        *error = NULL;
+    MMSharedTelit *shared = MM_SHARED_TELIT (self);
+    guint          i;
 
     response = mm_base_modem_at_command_finish (MM_BASE_MODEM (self), res, &error);
     if (error) {
@@ -107,6 +108,7 @@
     g_array_unref (all);
     g_array_unref (combinations);
 
+    mm_shared_telit_store_supported_modes (shared, filtered);
     g_task_return_pointer (task, filtered, (GDestroyNotify) g_array_unref);
     g_object_unref (task);
 }
diff --git a/plugins/telit/mm-broadband-modem-telit.c b/plugins/telit/mm-broadband-modem-telit.c
index 4e8a191..5a295b2 100644
--- a/plugins/telit/mm-broadband-modem-telit.c
+++ b/plugins/telit/mm-broadband-modem-telit.c
@@ -404,39 +404,6 @@
 }
 
 /*****************************************************************************/
-/* After Sim Unlock (Modem interface) */
-
-static gboolean
-modem_after_sim_unlock_finish (MMIfaceModem *self,
-                               GAsyncResult *res,
-                               GError **error)
-{
-    return g_task_propagate_boolean (G_TASK (res), error);
-}
-
-static gboolean
-after_sim_unlock_ready (GTask *task)
-{
-    g_task_return_boolean (task, TRUE);
-    g_object_unref (task);
-    return G_SOURCE_REMOVE;
-}
-
-static void
-modem_after_sim_unlock (MMIfaceModem *self,
-                        GAsyncReadyCallback callback,
-                        gpointer user_data)
-{
-    GTask *task;
-
-    task = g_task_new (self, NULL, callback, user_data);
-
-    /* A short delay is necessary with some SIMs when
-    they have just been unlocked. Using 1 second as secure margin. */
-    g_timeout_add_seconds (1, (GSourceFunc) after_sim_unlock_ready, task);
-}
-
-/*****************************************************************************/
 /* Setup SIM hot swap (Modem interface) */
 
 typedef enum {
@@ -1263,10 +1230,11 @@
                                    GAsyncResult *res,
                                    GTask *task)
 {
-    GError *error = NULL;
-    GArray *all;
-    GArray *combinations;
-    GArray *filtered;
+    GError        *error = NULL;
+    GArray        *all;
+    GArray        *combinations;
+    GArray        *filtered;
+    MMSharedTelit *shared = MM_SHARED_TELIT (self);
 
     all = iface_modem_parent->load_supported_modes_finish (self, res, &error);
     if (!all) {
@@ -1288,6 +1256,7 @@
     g_array_unref (all);
     g_array_unref (combinations);
 
+    mm_shared_telit_store_supported_modes (shared, filtered);
     g_task_return_pointer (task, filtered, (GDestroyNotify) g_array_unref);
     g_object_unref (task);
 }
@@ -1418,6 +1387,8 @@
     iface->set_current_bands_finish = mm_shared_telit_modem_set_current_bands_finish;
     iface->load_current_bands = mm_shared_telit_modem_load_current_bands;
     iface->load_current_bands_finish = mm_shared_telit_modem_load_current_bands_finish;
+    iface->load_revision = mm_shared_telit_modem_load_revision;
+    iface->load_revision_finish = mm_shared_telit_modem_load_revision_finish;
     iface->load_supported_bands = mm_shared_telit_modem_load_supported_bands;
     iface->load_supported_bands_finish = mm_shared_telit_modem_load_supported_bands_finish;
     iface->load_unlock_retries_finish = modem_load_unlock_retries_finish;
@@ -1436,8 +1407,6 @@
     iface->load_current_modes_finish = mm_shared_telit_load_current_modes_finish;
     iface->set_current_modes = mm_shared_telit_set_current_modes;
     iface->set_current_modes_finish = mm_shared_telit_set_current_modes_finish;
-    iface->modem_after_sim_unlock = modem_after_sim_unlock;
-    iface->modem_after_sim_unlock_finish = modem_after_sim_unlock_finish;
     iface->setup_sim_hot_swap = modem_setup_sim_hot_swap;
     iface->setup_sim_hot_swap_finish = modem_setup_sim_hot_swap_finish;
 }
diff --git a/plugins/telit/mm-modem-helpers-telit.c b/plugins/telit/mm-modem-helpers-telit.c
index d163bf1..767548c 100644
--- a/plugins/telit/mm-modem-helpers-telit.c
+++ b/plugins/telit/mm-modem-helpers-telit.c
@@ -563,8 +563,15 @@
     gchar        *match_str = NULL;
     guint64       value;
     gchar       **tokens = NULL;
+    gboolean      hex_format = FALSE;
+    gboolean      ok;
 
-    match_str = g_match_info_fetch_named (match_info, "Bands4G");
+    match_str = g_match_info_fetch_named (match_info, "Bands4GDec");
+    if (!match_str) {
+        match_str = g_match_info_fetch_named (match_info, "Bands4GHex");
+        hex_format = match_str != NULL;
+    }
+
     if (!match_str || match_str[0] == '\0') {
         g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                      "Could not find 4G band flags from response");
@@ -575,7 +582,10 @@
     tokens = g_strsplit (match_str, "-", -1);
 
     /* If this is a range, get upper threshold, which contains the total supported mask */
-    if (!mm_get_u64_from_str (tokens[1] ? tokens[1] : tokens[0], &value)) {
+    ok = hex_format?
+        mm_get_u64_from_hex_str (tokens[1] ? tokens[1] : tokens[0], &value):
+        mm_get_u64_from_str (tokens[1] ? tokens[1] : tokens[0], &value);
+    if (!ok) {
         g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                      "Could not parse 4G band mask from string: '%s'", match_str);
         goto out;
@@ -608,7 +618,7 @@
     gchar        *match_str_ext = NULL;
     guint64       value;
 
-    match_str = g_match_info_fetch_named (match_info, "Bands4G");
+    match_str = g_match_info_fetch_named (match_info, "Bands4GHex");
     if (!match_str || match_str[0] == '\0') {
         g_set_error (&inner_error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
                      "Could not find 4G band hex mask flag from response");
@@ -667,12 +677,25 @@
     return g_strv_length (tokens) == 4;
 }
 
+/* Regex tokens for #BND parsing */
+#define MM_SUPPORTED_BANDS_2G       "\\s*\\((?P<Bands2G>[0-9\\-,]*)\\)"
+#define MM_SUPPORTED_BANDS_3G       "(,\\s*\\((?P<Bands3G>[0-9\\-,]*)\\))?"
+#define MM_SUPPORTED_BANDS_4G_HEX   "(,\\s*\\((?P<Bands4GHex>[0-9A-F\\-,]*)\\))?"
+#define MM_SUPPORTED_BANDS_4G_DEC   "(,\\s*\\((?P<Bands4GDec>[0-9\\-,]*)\\))?"
+#define MM_SUPPORTED_BANDS_4G_EXT   "(,\\s*\\((?P<Bands4GHex>[0-9A-F]+)\\))?(,\\s*\\((?P<Bands4GExt>[0-9A-F]+)\\))?"
+#define MM_CURRENT_BANDS_2G         "\\s*(?P<Bands2G>\\d+)"
+#define MM_CURRENT_BANDS_3G         "(,\\s*(?P<Bands3G>\\d+))?"
+#define MM_CURRENT_BANDS_4G_HEX     "(,\\s*(?P<Bands4GHex>[0-9A-F]+))?"
+#define MM_CURRENT_BANDS_4G_DEC     "(,\\s*(?P<Bands4GDec>\\d+))?"
+#define MM_CURRENT_BANDS_4G_EXT     "(,\\s*(?P<Bands4GHex>[0-9A-F]+))?(,\\s*(?P<Bands4GExt>[0-9A-F]+))?"
+
 static GArray *
 common_parse_bnd_response (const gchar    *response,
                            gboolean        modem_is_2g,
                            gboolean        modem_is_3g,
                            gboolean        modem_is_4g,
                            gboolean        modem_alternate_3g_bands,
+                           gboolean        modem_has_hex_format_4g_bands,
                            gboolean        modem_ext_4g_bands,
                            LoadBandsType   load_type,
                            gpointer        log_object,
@@ -681,24 +704,31 @@
     GError     *inner_error = NULL;
     GArray     *bands = NULL;
     GMatchInfo *match_info = NULL;
-    GRegex     *r;
+    GRegex     *r = NULL;
+    const gchar *load_bands_regex = NULL;
 
-    if (!modem_ext_4g_bands) {
-        static const gchar *load_bands_regex[] = {
-            [LOAD_BANDS_TYPE_SUPPORTED] = "#BND:\\s*\\((?P<Bands2G>[0-9\\-,]*)\\)(,\\s*\\((?P<Bands3G>[0-9\\-,]*)\\))?(,\\s*\\((?P<Bands4G>[0-9\\-,]*)\\))?",
-            [LOAD_BANDS_TYPE_CURRENT]   = "#BND:\\s*(?P<Bands2G>\\d+)(,\\s*(?P<Bands3G>\\d+))?(,\\s*(?P<Bands4G>\\d+))?",
-        };
+    static const gchar *load_bands_regex_4g_hex[] = {
+        [LOAD_BANDS_TYPE_SUPPORTED] = "#BND:"MM_SUPPORTED_BANDS_2G MM_SUPPORTED_BANDS_3G MM_SUPPORTED_BANDS_4G_HEX,
+        [LOAD_BANDS_TYPE_CURRENT]   = "#BND:"MM_CURRENT_BANDS_2G MM_CURRENT_BANDS_3G MM_CURRENT_BANDS_4G_HEX,
 
-        r = g_regex_new (load_bands_regex[load_type], G_REGEX_RAW, 0, NULL);
-    } else {
-        static const gchar *load_bands_regex_hex[] = {
-            [LOAD_BANDS_TYPE_SUPPORTED] = "#BND:\\s*\\((?P<Bands2G>[0-9\\-,]*)\\)(,\\s*\\((?P<Bands3G>[0-9\\-,]*)\\))?(,\\s*\\((?P<Bands4G>[0-9A-F]+)\\))?(,\\s*\\((?P<Bands4GExt>[0-9A-F]+)\\))?",
-            [LOAD_BANDS_TYPE_CURRENT]   = "#BND:\\s*(?P<Bands2G>\\d+)(,\\s*(?P<Bands3G>\\d+))?(,\\s*(?P<Bands4G>[0-9A-F]+))?(,\\s*(?P<Bands4GExt>[0-9A-F]+))?",
-        };
+    };
+    static const gchar *load_bands_regex_4g_dec[] = {
+        [LOAD_BANDS_TYPE_SUPPORTED] = "#BND:"MM_SUPPORTED_BANDS_2G MM_SUPPORTED_BANDS_3G MM_SUPPORTED_BANDS_4G_DEC,
+        [LOAD_BANDS_TYPE_CURRENT]   = "#BND:"MM_CURRENT_BANDS_2G MM_CURRENT_BANDS_3G MM_CURRENT_BANDS_4G_DEC,
+    };
+    static const gchar *load_bands_regex_4g_ext[] = {
+        [LOAD_BANDS_TYPE_SUPPORTED] = "#BND:"MM_SUPPORTED_BANDS_2G MM_SUPPORTED_BANDS_3G MM_SUPPORTED_BANDS_4G_EXT,
+        [LOAD_BANDS_TYPE_CURRENT]   = "#BND:"MM_CURRENT_BANDS_2G MM_CURRENT_BANDS_3G MM_CURRENT_BANDS_4G_EXT,
+    };
 
-        r = g_regex_new (load_bands_regex_hex[load_type], G_REGEX_RAW, 0, NULL);
-    }
+    if (modem_ext_4g_bands)
+        load_bands_regex = load_bands_regex_4g_ext[load_type];
+    else if (modem_has_hex_format_4g_bands)
+        load_bands_regex = load_bands_regex_4g_hex[load_type];
+    else
+        load_bands_regex = load_bands_regex_4g_dec[load_type];
 
+    r = g_regex_new (load_bands_regex, G_REGEX_RAW, 0, NULL);
     g_assert (r);
 
     if (!g_regex_match (r, response, 0, &match_info)) {
@@ -721,12 +751,15 @@
     if (modem_is_3g && !telit_get_3g_mm_bands (match_info, log_object, modem_alternate_3g_bands, &bands, &inner_error))
         goto out;
 
-    if (modem_is_4g && !modem_ext_4g_bands && !telit_get_4g_mm_bands (match_info, &bands, &inner_error))
-        goto out;
+    if (modem_is_4g) {
+        gboolean ok;
 
-    if (modem_is_4g && modem_ext_4g_bands && !telit_get_ext_4g_mm_bands (match_info, &bands, &inner_error))
-        goto out;
-
+        ok = modem_ext_4g_bands?
+            telit_get_ext_4g_mm_bands (match_info, &bands, &inner_error) :
+            telit_get_4g_mm_bands (match_info, &bands, &inner_error);
+        if (!ok)
+            goto out;
+    }
 out:
     g_match_info_free (match_info);
     g_regex_unref (r);
@@ -746,6 +779,7 @@
                                    gboolean      modem_is_3g,
                                    gboolean      modem_is_4g,
                                    gboolean      modem_alternate_3g_bands,
+                                   gboolean      modem_has_hex_format_4g_bands,
                                    gboolean      modem_ext_4g_bands,
                                    gpointer      log_object,
                                    GError      **error)
@@ -753,6 +787,7 @@
     return common_parse_bnd_response (response,
                                       modem_is_2g, modem_is_3g, modem_is_4g,
                                       modem_alternate_3g_bands,
+                                      modem_has_hex_format_4g_bands,
                                       modem_ext_4g_bands,
                                       LOAD_BANDS_TYPE_CURRENT,
                                       log_object,
@@ -765,6 +800,7 @@
                                   gboolean      modem_is_3g,
                                   gboolean      modem_is_4g,
                                   gboolean      modem_alternate_3g_bands,
+                                  gboolean      modem_has_hex_format_4g_bands,
                                   gboolean     *modem_ext_4g_bands,
                                   gpointer      log_object,
                                   GError      **error)
@@ -773,6 +809,7 @@
     return common_parse_bnd_response (response,
                                       modem_is_2g, modem_is_3g, modem_is_4g,
                                       modem_alternate_3g_bands,
+                                      modem_has_hex_format_4g_bands,
                                       *modem_ext_4g_bands,
                                       LOAD_BANDS_TYPE_SUPPORTED,
                                       log_object,
@@ -852,3 +889,66 @@
 
     return combinations;
 }
+
+/*****************************************************************************/
+/* Software Package version response parser */
+
+gchar *
+mm_telit_parse_swpkgv_response (const gchar *response)
+{
+    gchar *version = NULL;
+    g_autofree gchar *base_version = NULL;
+    g_autoptr(GRegex) r = NULL;
+    g_autoptr(GMatchInfo) match_info = NULL;
+    guint matches;
+
+    /* We are interested only in the first line of the response */
+    r = g_regex_new ("(?P<Base>\\d{2}.\\d{2}.*)",
+                     G_REGEX_RAW | G_REGEX_MULTILINE | G_REGEX_NEWLINE_CRLF,
+                     G_REGEX_MATCH_NEWLINE_CR,
+                     NULL);
+    g_assert (r != NULL);
+
+    if (!g_regex_match_full (r, response, strlen (response), 0, 0, &match_info, NULL)) {
+        return NULL;
+    }
+
+    matches = g_match_info_get_match_count (match_info);
+    if (matches < 2 || matches > 4) {
+        return NULL;
+    }
+
+    base_version = g_match_info_fetch_named (match_info, "Base");
+    if (base_version)
+        version = g_strdup (base_version);
+
+    return version;
+}
+
+/*****************************************************************************/
+/* MM Telit Model from revision string */
+
+MMTelitModel
+mm_telit_model_from_revision (const gchar *revision)
+{
+    guint i;
+    static const struct {
+        const gchar *revision_prefix;
+        MMTelitModel model;
+    } revision_to_model_map [] = {
+        {"24.01", MM_TELIT_MODEL_LM940},
+        {"25.", MM_TELIT_MODEL_LE910C1},
+        {"32.", MM_TELIT_MODEL_LM960},
+        {"38.", MM_TELIT_MODEL_FN980},
+        {"40.", MM_TELIT_MODEL_LN920}
+    };
+
+    g_assert (revision);
+
+    for (i = 0; i < G_N_ELEMENTS (revision_to_model_map); ++i) {
+        if (g_str_has_prefix (revision, revision_to_model_map[i].revision_prefix))
+            return revision_to_model_map[i].model;
+    }
+
+    return MM_TELIT_MODEL_DEFAULT;
+}
diff --git a/plugins/telit/mm-modem-helpers-telit.h b/plugins/telit/mm-modem-helpers-telit.h
index 718bb60..97eaf47 100644
--- a/plugins/telit/mm-modem-helpers-telit.h
+++ b/plugins/telit/mm-modem-helpers-telit.h
@@ -19,12 +19,22 @@
 #include <glib.h>
 #include "ModemManager.h"
 
+typedef enum {
+    MM_TELIT_MODEL_DEFAULT,
+    MM_TELIT_MODEL_FN980,
+    MM_TELIT_MODEL_LE910C1,
+    MM_TELIT_MODEL_LM940,
+    MM_TELIT_MODEL_LM960,
+    MM_TELIT_MODEL_LN920,
+} MMTelitModel;
+
 /* #BND response parsers and request builder */
 GArray *mm_telit_parse_bnd_query_response (const gchar  *response,
                                            gboolean      modem_is_2g,
                                            gboolean      modem_is_3g,
                                            gboolean      modem_is_4g,
                                            gboolean      modem_alternate_3g_bands,
+                                           gboolean      modem_has_hex_format_4g_bands,
                                            gboolean      modem_ext_4g_bands,
                                            gpointer      log_object,
                                            GError      **error);
@@ -33,6 +43,7 @@
                                            gboolean      modem_is_3g,
                                            gboolean      modem_is_4g,
                                            gboolean      modem_alternate_3g_bands,
+                                           gboolean      modem_has_hex_format_4g_bands,
                                            gboolean     *modem_ext_4g_bands,
                                            gpointer      log_object,
                                            GError      **error);
@@ -65,4 +76,8 @@
 
 GArray *mm_telit_build_modes_list (void);
 
+gchar *mm_telit_parse_swpkgv_response (const gchar *response);
+
+MMTelitModel mm_telit_model_from_revision (const gchar *revision);
+
 #endif  /* MM_MODEM_HELPERS_TELIT_H */
diff --git a/plugins/telit/mm-shared-telit.c b/plugins/telit/mm-shared-telit.c
index af9dea3..1401cf6 100644
--- a/plugins/telit/mm-shared-telit.c
+++ b/plugins/telit/mm-shared-telit.c
@@ -42,6 +42,8 @@
     gboolean      alternate_3g_bands;
     gboolean      ext_4g_bands;
     GArray       *supported_bands;
+    GArray       *supported_modes;
+    gchar        *software_package_version;
 } Private;
 
 static void
@@ -49,9 +51,19 @@
 {
     if (priv->supported_bands)
         g_array_unref (priv->supported_bands);
+    if (priv->supported_modes)
+        g_array_unref (priv->supported_modes);
+    g_free (priv->software_package_version);
     g_slice_free (Private, priv);
 }
 
+static gboolean
+is_bnd_4g_format_hex (MMBaseModem *self,
+                      const gchar *revision)
+{
+    return mm_telit_model_from_revision (revision) == MM_TELIT_MODEL_LE910C1;
+}
+
 static void
 initialize_alternate_3g_band (MMSharedTelit *self,
                               Private       *priv)
@@ -93,6 +105,16 @@
     return priv;
 }
 
+void
+mm_shared_telit_store_supported_modes (MMSharedTelit *self,
+                                       GArray        *modes)
+{
+    Private *priv;
+
+    priv = get_private (MM_SHARED_TELIT (self));
+    priv->supported_modes = g_array_ref (modes);
+}
+
 /*****************************************************************************/
 /* Load current mode (Modem interface) */
 
@@ -206,6 +228,7 @@
                                                   mm_iface_modem_is_3g (MM_IFACE_MODEM (self)),
                                                   mm_iface_modem_is_4g (MM_IFACE_MODEM (self)),
                                                   priv->alternate_3g_bands,
+                                                  is_bnd_4g_format_hex (self, priv->software_package_version),
                                                   &priv->ext_4g_bands,
                                                   self,
                                                   &error);
@@ -311,6 +334,7 @@
                                                    mm_iface_modem_is_3g (MM_IFACE_MODEM (self)),
                                                    mm_iface_modem_is_4g (MM_IFACE_MODEM (self)),
                                                    priv->alternate_3g_bands,
+                                                   is_bnd_4g_format_hex (self, priv->software_package_version),
                                                    priv->ext_4g_bands,
                                                    self,
                                                    &error);
@@ -529,12 +553,29 @@
                                    GAsyncReadyCallback callback,
                                    gpointer user_data)
 {
-    GTask *task;
-    gchar *command;
-    gint ws46_mode = -1;
+    GTask   *task;
+    gchar   *command;
+    Private *priv;
+    gint     ws46_mode = -1;
 
+    priv = get_private (MM_SHARED_TELIT (self));
     task = g_task_new (self, NULL, callback, user_data);
 
+    if (allowed == MM_MODEM_MODE_ANY && priv->supported_modes) {
+        guint i;
+
+        allowed = MM_MODEM_MODE_NONE;
+        /* Process list of modes to gather supported ones */
+        for (i = 0; i < priv->supported_modes->len; i++) {
+            if (g_array_index (priv->supported_modes, MMModemMode, i) & MM_MODEM_MODE_2G)
+                allowed |= MM_MODEM_MODE_2G;
+            if (g_array_index (priv->supported_modes, MMModemMode, i) & MM_MODEM_MODE_3G)
+                allowed |= MM_MODEM_MODE_3G;
+            if (g_array_index (priv->supported_modes, MMModemMode, i) & MM_MODEM_MODE_4G)
+                allowed |= MM_MODEM_MODE_4G;
+        }
+    }
+
     if (allowed == MM_MODEM_MODE_2G)
         ws46_mode = 12;
     else if (allowed == MM_MODEM_MODE_3G)
@@ -550,8 +591,7 @@
         ws46_mode = 30;
     else if (allowed == (MM_MODEM_MODE_3G | MM_MODEM_MODE_4G))
         ws46_mode = 31;
-    else if (allowed == (MM_MODEM_MODE_2G  | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G) ||
-             allowed == MM_MODEM_MODE_ANY)
+    else if (allowed == (MM_MODEM_MODE_2G  | MM_MODEM_MODE_3G | MM_MODEM_MODE_4G))
         ws46_mode = 25;
 
     /* Telit modems do not support preferred mode selection */
@@ -587,6 +627,121 @@
 }
 
 /*****************************************************************************/
+/* Revision loading */
+
+gchar *
+mm_shared_telit_modem_load_revision_finish (MMIfaceModem *self,
+                                            GAsyncResult *res,
+                                            GError **error)
+{
+    return g_task_propagate_pointer (G_TASK (res), error);
+}
+
+static void
+load_revision_ready (MMBaseModem *self,
+                     GAsyncResult *res,
+                     GTask *task)
+{
+    GError *error;
+    GVariant *result;
+
+    result = mm_base_modem_at_sequence_finish (self, res, NULL, &error);
+    if (!result) {
+        g_task_return_error (task, error);
+        g_object_unref (task);
+    } else {
+        gchar *revision = NULL;
+        Private *priv;
+
+        priv = get_private (MM_SHARED_TELIT (self));
+        revision = g_variant_dup_string (result, NULL);
+        priv->software_package_version = g_strdup (revision);
+        g_task_return_pointer (task, revision, g_free);
+        g_object_unref (task);
+    }
+}
+
+/*
+ * parse AT#SWPKGV command
+ * Execution command returns the software package version without #SWPKGV: command echo.
+ * The response is as follows:
+ *
+ * AT#SWPKGV
+ * <Telit Software Package Version>-<Production Parameters Version>
+ * <Modem FW Version> (Usually the same value returned by AT+GMR)
+ * <Production Parameters Version>
+ * <Application FW Version>
+ */
+static MMBaseModemAtResponseProcessorResult
+software_package_version_ready (MMBaseModem   *self,
+                                gpointer       none,
+                                const gchar   *command,
+                                const gchar   *response,
+                                gboolean       last_command,
+                                const GError  *error,
+                                GVariant     **result,
+                                GError       **result_error)
+{
+    gchar *version = NULL;
+
+    if (error) {
+        *result = NULL;
+
+        /* Ignore AT errors (ie, ERROR or CMx ERROR) */
+        if (error->domain != MM_MOBILE_EQUIPMENT_ERROR || last_command) {
+            *result_error = g_error_copy (error);
+            return MM_BASE_MODEM_AT_RESPONSE_PROCESSOR_RESULT_FAILURE;
+        }
+
+        *result_error = NULL;
+        return MM_BASE_MODEM_AT_RESPONSE_PROCESSOR_RESULT_CONTINUE;
+    }
+
+    version = mm_telit_parse_swpkgv_response (response);
+    if (!version)
+        return MM_BASE_MODEM_AT_RESPONSE_PROCESSOR_RESULT_CONTINUE;
+
+    *result = g_variant_new_take_string (version);
+    return MM_BASE_MODEM_AT_RESPONSE_PROCESSOR_RESULT_SUCCESS;
+}
+
+static const MMBaseModemAtCommand revisions[] = {
+    { "#SWPKGV",  3, TRUE, software_package_version_ready },
+    { "+CGMR",   3, TRUE, mm_base_modem_response_processor_string_ignore_at_errors },
+    { "+GMR",   3, TRUE, mm_base_modem_response_processor_string_ignore_at_errors },
+    { NULL }
+};
+
+void
+mm_shared_telit_modem_load_revision (MMIfaceModem *self,
+                                     GAsyncReadyCallback callback,
+                                     gpointer user_data)
+{
+    GTask *task;
+    Private *priv;
+
+    task = g_task_new (self, NULL, callback, user_data);
+    priv = get_private (MM_SHARED_TELIT (self));
+
+    mm_obj_dbg (self, "loading revision...");
+    if (priv->software_package_version) {
+        g_task_return_pointer (task,
+                               g_strdup (priv->software_package_version),
+                               g_free);
+        g_object_unref (task);
+        return;
+    }
+
+    mm_base_modem_at_sequence (
+        MM_BASE_MODEM (self),
+        revisions,
+        NULL, /* response_processor_context */
+        NULL, /* response_processor_context_free */
+        (GAsyncReadyCallback) load_revision_ready,
+        task);
+}
+
+/*****************************************************************************/
 
 static void
 shared_telit_init (gpointer g_iface)
diff --git a/plugins/telit/mm-shared-telit.h b/plugins/telit/mm-shared-telit.h
index 4cfaeb5..56f55de 100644
--- a/plugins/telit/mm-shared-telit.h
+++ b/plugins/telit/mm-shared-telit.h
@@ -42,6 +42,9 @@
 
 GType mm_shared_telit_get_type (void);
 
+void        mm_shared_telit_store_supported_modes       (MMSharedTelit *self,
+                                                         GArray *modes);
+
 gboolean    mm_shared_telit_load_current_modes_finish   (MMIfaceModem *self,
                                                          GAsyncResult *res,
                                                          MMModemMode *allowed,
@@ -87,4 +90,12 @@
                                                          GAsyncReadyCallback callback,
                                                          gpointer user_data);
 
+void       mm_shared_telit_modem_load_revision          (MMIfaceModem *self,
+                                                         GAsyncReadyCallback callback,
+                                                         gpointer user_data);
+
+gchar *   mm_shared_telit_modem_load_revision_finish    (MMIfaceModem *self,
+                                                         GAsyncResult *res,
+                                                         GError **error);
+
 #endif  /* MM_SHARED_TELIT_H */
diff --git a/plugins/telit/tests/test-mm-modem-helpers-telit.c b/plugins/telit/tests/test-mm-modem-helpers-telit.c
index e957f7d..1b96d08 100644
--- a/plugins/telit/tests/test-mm-modem-helpers-telit.c
+++ b/plugins/telit/tests/test-mm-modem-helpers-telit.c
@@ -39,6 +39,7 @@
     gboolean     modem_is_3g;
     gboolean     modem_is_4g;
     gboolean     modem_alternate_3g_bands;
+    gboolean     modem_has_4g_bands_hex_format;
     gboolean     modem_ext_4g_bands;
     guint        mm_bands_len;
     MMModemBand  mm_bands [MAX_BANDS_LIST_LEN];
@@ -46,14 +47,14 @@
 
 static BndResponseTest supported_band_mapping_tests [] = {
     {
-        "#BND: (0-3)", TRUE, FALSE, FALSE, FALSE, FALSE, 4,
+        "#BND: (0-3)", TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, 4,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_PCS,
           MM_MODEM_BAND_G850 }
     },
     {
-        "#BND: (0-3),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, 7,
+        "#BND: (0-3),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 7,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_PCS,
@@ -63,7 +64,7 @@
           MM_MODEM_BAND_UTRAN_8 }
     },
     {
-        "#BND: (0,3),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, 7,
+        "#BND: (0,3),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 7,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_PCS,
@@ -73,7 +74,7 @@
           MM_MODEM_BAND_UTRAN_8 }
     },
     {
-        "#BND: (0,2),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, 6,
+        "#BND: (0,2),(0,2,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 6,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_G850,
@@ -82,7 +83,7 @@
           MM_MODEM_BAND_UTRAN_8 }
     },
     {
-        "#BND: (0,2),(0-4,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, 7,
+        "#BND: (0,2),(0-4,5,6)", TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 7,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_G850,
@@ -92,7 +93,7 @@
           MM_MODEM_BAND_UTRAN_8 }
     },
     {
-        "#BND: (0-3),(0,2,5,6),(1-1)", TRUE, TRUE, TRUE, FALSE, FALSE, 8,
+        "#BND: (0-3),(0,2,5,6),(1-1)", TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, 8,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_PCS,
@@ -103,7 +104,7 @@
           MM_MODEM_BAND_EUTRAN_1 }
     },
     {
-        "#BND: (0),(0),(1-3)", TRUE, TRUE, TRUE, FALSE, FALSE, 5,
+        "#BND: (0),(0),(1-3)", TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, 5,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_UTRAN_1,
@@ -111,13 +112,13 @@
           MM_MODEM_BAND_EUTRAN_2 }
     },
     {
-        "#BND: (0),(0),(1-3)", FALSE, FALSE, TRUE, FALSE, FALSE, 2,
+        "#BND: (0),(0),(1-3)", FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, 2,
         { MM_MODEM_BAND_EUTRAN_1,
           MM_MODEM_BAND_EUTRAN_2 }
     },
     /* 3G alternate band settings: default */
     {
-        "#BND: (0),(0,2,5,6,12,25)", FALSE, TRUE, FALSE, FALSE, FALSE, 5,
+        "#BND: (0),(0,2,5,6,12,25)", FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, 5,
         { MM_MODEM_BAND_UTRAN_1,
           MM_MODEM_BAND_UTRAN_5,
           MM_MODEM_BAND_UTRAN_8,
@@ -126,7 +127,7 @@
     },
     /* 3G alternate band settings: alternate */
     {
-        "#BND: (0),(0,2,5,6,12,13)", FALSE, TRUE, FALSE, TRUE, FALSE, 4,
+        "#BND: (0),(0,2,5,6,12,13)", FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, 4,
         { MM_MODEM_BAND_UTRAN_1,
           MM_MODEM_BAND_UTRAN_3,
           MM_MODEM_BAND_UTRAN_5,
@@ -136,7 +137,7 @@
      * 168695967: 0xA0E189F: 0000 1010 0000 1110 0001 1000 1001 1111
      */
     {
-        "#BND: (0-5),(0),(1-168695967)", TRUE, FALSE, TRUE, FALSE, FALSE, 17,
+        "#BND: (0-5),(0),(1-168695967)", TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, 17,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_PCS,
@@ -157,7 +158,7 @@
     },
     /* 4G ext band settings: devices such as LN920 */
     {
-        "#BND: (0),(0),(1003100185A),(42)", FALSE, TRUE, TRUE, FALSE, TRUE, 13,
+        "#BND: (0),(0),(1003100185A),(42)", FALSE, TRUE, TRUE, FALSE, TRUE, TRUE, 13,
         { MM_MODEM_BAND_UTRAN_1,
           MM_MODEM_BAND_EUTRAN_2,
           MM_MODEM_BAND_EUTRAN_4,
@@ -171,6 +172,22 @@
           MM_MODEM_BAND_EUTRAN_41,
           MM_MODEM_BAND_EUTRAN_66,
           MM_MODEM_BAND_EUTRAN_71 }
+    },
+    /* 4G band in hex format: devices such as LE910C1-EUX */
+    {
+        "#BND: (0),(0,5,6,13,15,23),(80800C5)", TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, 11,
+        {
+            MM_MODEM_BAND_EGSM,
+            MM_MODEM_BAND_DCS,
+            MM_MODEM_BAND_UTRAN_1,
+            MM_MODEM_BAND_UTRAN_3,
+            MM_MODEM_BAND_UTRAN_8,
+            MM_MODEM_BAND_EUTRAN_1,
+            MM_MODEM_BAND_EUTRAN_3,
+            MM_MODEM_BAND_EUTRAN_7,
+            MM_MODEM_BAND_EUTRAN_8,
+            MM_MODEM_BAND_EUTRAN_20,
+            MM_MODEM_BAND_EUTRAN_28 }
     }
 };
 
@@ -189,6 +206,7 @@
                                                   supported_band_mapping_tests[i].modem_is_3g,
                                                   supported_band_mapping_tests[i].modem_is_4g,
                                                   supported_band_mapping_tests[i].modem_alternate_3g_bands,
+                                                  supported_band_mapping_tests[i].modem_has_4g_bands_hex_format,
                                                   &modem_ext_4g_bands,
                                                   NULL,
                                                   &error);
@@ -205,18 +223,18 @@
 
 static BndResponseTest current_band_mapping_tests [] = {
     {
-        "#BND: 0", TRUE, FALSE, FALSE, FALSE, FALSE, 2,
+        "#BND: 0", TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, 2,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS }
     },
     {
-        "#BND: 0,5", TRUE, TRUE, FALSE, FALSE, FALSE, 3,
+        "#BND: 0,5", TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 3,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_UTRAN_8 }
     },
     {
-        "#BND: 1,3", TRUE, TRUE, FALSE, FALSE, FALSE, 5,
+        "#BND: 1,3", TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 5,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_PCS,
           MM_MODEM_BAND_UTRAN_1,
@@ -224,38 +242,38 @@
           MM_MODEM_BAND_UTRAN_5 }
     },
     {
-        "#BND: 2,7", TRUE, TRUE, FALSE, FALSE, FALSE, 3,
+        "#BND: 2,7", TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, 3,
         { MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_G850,
           MM_MODEM_BAND_UTRAN_4 }
     },
     {
-        "#BND: 3,0,1", TRUE, TRUE, TRUE, FALSE, FALSE, 4,
+        "#BND: 3,0,1", TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, 4,
         { MM_MODEM_BAND_PCS,
           MM_MODEM_BAND_G850,
           MM_MODEM_BAND_UTRAN_1,
           MM_MODEM_BAND_EUTRAN_1 }
     },
     {
-        "#BND: 0,0,3", TRUE, FALSE, TRUE, FALSE, FALSE, 4,
+        "#BND: 0,0,3", TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, 4,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_EUTRAN_1,
           MM_MODEM_BAND_EUTRAN_2 }
     },
     {
-        "#BND: 0,0,3", FALSE, FALSE, TRUE, FALSE, FALSE, 2,
+        "#BND: 0,0,3", FALSE, FALSE, TRUE, FALSE, FALSE, FALSE, 2,
         { MM_MODEM_BAND_EUTRAN_1,
           MM_MODEM_BAND_EUTRAN_2 }
     },
     /* 3G alternate band settings: default */
     {
-        "#BND: 0,12", FALSE, TRUE, FALSE, FALSE, FALSE, 1,
+        "#BND: 0,12", FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, 1,
         { MM_MODEM_BAND_UTRAN_6 }
     },
     /* 3G alternate band settings: alternate */
     {
-        "#BND: 0,12", FALSE, TRUE, FALSE, TRUE, FALSE, 4,
+        "#BND: 0,12", FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, 4,
         { MM_MODEM_BAND_UTRAN_1,
           MM_MODEM_BAND_UTRAN_3,
           MM_MODEM_BAND_UTRAN_5,
@@ -265,7 +283,7 @@
      * 168695967: 0xA0E189F: 0000 1010 0000 1110 0001 1000 1001 1111
      */
     {
-        "#BND: 5,0,168695967", TRUE, FALSE, TRUE, FALSE, FALSE, 17,
+        "#BND: 5,0,168695967", TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, 17,
         { MM_MODEM_BAND_EGSM,
           MM_MODEM_BAND_DCS,
           MM_MODEM_BAND_PCS,
@@ -286,7 +304,7 @@
     },
     /* 4G ext band settings: devices such as LN920 */
     {
-        "#BND: 0,0,1003100185A,42", FALSE, TRUE, TRUE, FALSE, TRUE, 13,
+        "#BND: 0,0,1003100185A,42", FALSE, TRUE, TRUE, FALSE, FALSE, TRUE, 13,
         { MM_MODEM_BAND_UTRAN_1,
           MM_MODEM_BAND_EUTRAN_2,
           MM_MODEM_BAND_EUTRAN_4,
@@ -317,6 +335,7 @@
                                                    current_band_mapping_tests[i].modem_is_3g,
                                                    current_band_mapping_tests[i].modem_is_4g,
                                                    current_band_mapping_tests[i].modem_alternate_3g_bands,
+                                                   supported_band_mapping_tests[i].modem_has_4g_bands_hex_format,
                                                    current_band_mapping_tests[i].modem_ext_4g_bands,
                                                    NULL,
                                                    &error);
@@ -611,6 +630,33 @@
     }
 }
 
+static void
+test_telit_parse_swpkgv_response (void)
+{
+    static struct {
+        const gchar *response;
+        const gchar *expected;
+    } tt [] = {
+        {"\r\n12.34.567\r\nM0F.223004-B001\r\nP0F.224700\r\nA0F.223004-B001\r\n\r\nOK\r\n", "12.34.567"},
+        {"\r\n13.35.568-A123\r\nM0F.223004-B001\r\nP0F.224700\r\nA0F.223004-B001\r\n\r\nOK\r\n", "13.35.568-A123"},
+        {"\r\n14.36.569-B124\r\nM0F.223004-B001\r\nP0F.224700\r\nA0F.223004-B001\r\n\r\nOK\r\n", "14.36.569-B124"},
+        {"\r\n15.37.570-T125\r\nM0F.223004-B001\r\nP0F.224700\r\nA0F.223004-B001\r\n\r\nOK\r\n", "15.37.570-T125"},
+        {"\r\n16.38.571-P0F.224700\r\nM0F.223004-B001\r\nP0F.224700\r\nA0F.223004-B001\r\n\r\nOK\r\n", "16.38.571-P0F.224700"},
+        /* real example from LE910C1-EUX */
+        {"\r\n25.30.224-B001-P0F.224700\r\nM0F.223004-B001\r\nP0F.224700\r\nA0F.223004-B001\r\n\r\nOK\r\n", "25.30.224-B001-P0F.224700"},
+    };
+    guint i;
+
+    for (i = 0; i < G_N_ELEMENTS (tt); i++) {
+        gchar *actual = NULL;
+
+        actual = mm_telit_parse_swpkgv_response(tt[i].response);
+
+        g_assert_cmpstr (tt[i].expected, ==, actual);
+        g_free (actual);
+    }
+}
+
 /******************************************************************************/
 
 int main (int argc, char **argv)
@@ -625,5 +671,6 @@
     g_test_add_func ("/MM/telit/bands/current/set_bands/3g", test_telit_get_3g_bnd_flag);
     g_test_add_func ("/MM/telit/bands/current/set_bands/4g", test_telit_get_4g_bnd_flag);
     g_test_add_func ("/MM/telit/qss/query", test_telit_parse_qss_query);
+    g_test_add_func ("/MM/telit/swpkv/parse_response", test_telit_parse_swpkgv_response);
     return g_test_run ();
 }
diff --git a/plugins/tests/test-udev-rules.c b/plugins/tests/test-udev-rules.c
index b7f06d3..fe11c78 100644
--- a/plugins/tests/test-udev-rules.c
+++ b/plugins/tests/test-udev-rules.c
@@ -184,6 +184,14 @@
 }
 #endif
 
+#if defined ENABLE_PLUGIN_LINKTOP
+static void
+test_linktop (void)
+{
+    common_test (TESTUDEVRULESDIR_LINKTOP);
+}
+#endif
+
 /************************************************************/
 
 int main (int argc, char **argv)
@@ -241,6 +249,9 @@
 #if defined ENABLE_PLUGIN_QCOM_SOC && defined WITH_QMI
     g_test_add_func ("/MM/test-udev-rules/qcom-soc", test_qcom_soc);
 #endif
+#if defined ENABLE_PLUGIN_LINKTOP
+    g_test_add_func ("/MM/test-udev-rules/linktop", test_linktop);
+#endif
 
     return g_test_run ();
 }
diff --git a/po/LINGUAS b/po/LINGUAS
index c17e461..473600f 100644
--- a/po/LINGUAS
+++ b/po/LINGUAS
@@ -10,6 +10,7 @@
 id
 it
 lt
+nl
 pl
 pt_BR
 ru
diff --git a/po/nl.po b/po/nl.po
new file mode 100644
index 0000000..c51b1ad
--- /dev/null
+++ b/po/nl.po
@@ -0,0 +1,117 @@
+# Dutch translation for ModemManager.
+# Copyright (C) 2022 ModemManager's COPYRIGHT HOLDER
+# This file is distributed under the same license as the ModemManager package.
+# Nathan Follens <nfollens@gnome.org>, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: ModemManager main\n"
+"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/mobile-broadband/"
+"ModemManager/issues\n"
+"POT-Creation-Date: 2022-03-27 15:27+0000\n"
+"PO-Revision-Date: 2022-03-27 20:42+0200\n"
+"Last-Translator: Nathan Follens <nfollens@gnome.org>\n"
+"Language-Team: Dutch <gnome-nl-list@gnome.org>\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.0.1\n"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:13
+msgid "Control the Modem Manager daemon"
+msgstr "De Modembeheer-daemon besturen"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:14
+msgid "System policy prevents controlling the Modem Manager."
+msgstr "Systeembeleid verbiedt Modembeheer te besturen."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:22
+msgid "Unlock and control a mobile broadband device"
+msgstr "Een mobiele modem ontgrendelen en besturen"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:23
+msgid ""
+"System policy prevents unlocking or controlling the mobile broadband device."
+msgstr ""
+"Systeembeleid verbiedt het ontgrendelen of besturen van de mobiele modem."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:31
+msgid "Add, modify, and delete mobile broadband contacts"
+msgstr "Mobiele contacten toevoegen, bewerken en verwijderen"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:32
+msgid ""
+"System policy prevents adding, modifying, or deleting this device's contacts."
+msgstr ""
+"Systeembeleid verbiedt het toevoegen, bewerken of verwijderen van de "
+"contacten op dit apparaat."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:40
+msgid "Send, save, modify, and delete text messages"
+msgstr "Berichten verzenden, opslaan, bewerken en verwijderen"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:41
+msgid ""
+"System policy prevents sending or manipulating this device's text messages."
+msgstr ""
+"Systeembeleid verbiedt het verzenden of manipuleren van de berichten op dit "
+"apparaat."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:49
+msgid "Accept incoming voice calls or start outgoing voice calls."
+msgstr ""
+"Inkomende audiogesprekken aanvaarden of uitgaande audiogesprekken starten."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:50
+msgid "System policy prevents voice calls."
+msgstr "Systeembeleid verbiedt audiogesprekken."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:58
+msgid "Query network time and timezone information"
+msgstr "Netwerktijd en tijdszone-informatie ophalen"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:59
+msgid "System policy prevents querying network time information."
+msgstr ""
+"Systeeminstellingen verbieden het ophalen van informatie over de netwerktijd."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:67
+msgid "Enable and view geographic location and positioning information"
+msgstr ""
+"Informatie over de geografische locatie en positiebepaling inschakelen en "
+"bekijken"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:68
+msgid ""
+"System policy prevents enabling or viewing geographic location information."
+msgstr ""
+"Systeembeleid verbiedt het inschakelen of bekijken van informatie over de "
+"geografische locatie."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:76
+msgid "Query and utilize network information and services"
+msgstr "Netwerkinformatie en -diensten ophalen en gebruiken"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:77
+msgid ""
+"System policy prevents querying or utilizing network information and "
+"services."
+msgstr ""
+"Systeembeleid verbiedt het ophalen of gebruiken van netwerkinformatie en -"
+"diensten."
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:85
+msgid "Query and manage firmware on a mobile broadband device"
+msgstr "Firmware op een mobiele modem ophalen en beheren"
+
+#: data/org.freedesktop.ModemManager1.policy.in.in:86
+msgid "System policy prevents querying or managing this device's firmware."
+msgstr ""
+"Systeembeleid verbiedt het ophalen of beheren van de firmware van dit "
+"apparaat."
+
+#: src/mm-sleep-monitor.c:125
+msgid "ModemManager needs to reset devices"
+msgstr "Modembeheer moet apparaten opnieuw instellen"
diff --git a/src/Makefile.am b/src/Makefile.am
index 38944f2..e4ca5f7 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -294,6 +294,8 @@
 	-DPLUGINDIR=\"$(pkglibdir)\" \
 	-DFCCUNLOCKDIRPACKAGE=\"${pkglibdir}/fcc-unlock.d\" \
 	-DFCCUNLOCKDIRUSER=\"${pkgsysconfdir}/fcc-unlock.d\" \
+	-DCONNECTIONDIRPACKAGE=\"${pkglibdir}/connection.d\" \
+	-DCONNECTIONDIRUSER=\"${pkgsysconfdir}/connection.d\" \
 	-DMM_COMPILATION \
 	$(NULL)
 
@@ -313,8 +315,12 @@
 	mm-private-boxed-types.c \
 	mm-auth-provider.h \
 	mm-auth-provider.c \
-	mm-fcc-unlock-dispatcher.h \
-	mm-fcc-unlock-dispatcher.c \
+	mm-dispatcher.h \
+	mm-dispatcher.c \
+	mm-dispatcher-connection.h \
+	mm-dispatcher-connection.c \
+	mm-dispatcher-fcc-unlock.h \
+	mm-dispatcher-fcc-unlock.c \
 	mm-filter.h \
 	mm-filter.c \
 	mm-base-manager.c \
diff --git a/src/main.c b/src/main.c
index a9221cd..8516d7b 100644
--- a/src/main.c
+++ b/src/main.c
@@ -165,6 +165,7 @@
                        mm_context_get_log_journal (),
                        mm_context_get_log_timestamps (),
                        mm_context_get_log_relative_timestamps (),
+                       mm_context_get_log_personal_info (),
                        &error)) {
         g_printerr ("error: failed to set up logging: %s\n", error->message);
         g_error_free (error);
diff --git a/src/meson.build b/src/meson.build
index e024070..fdf6b09 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -200,7 +200,9 @@
   'mm-call-list.c',
   'mm-context.c',
   'mm-device.c',
-  'mm-fcc-unlock-dispatcher.c',
+  'mm-dispatcher.c',
+  'mm-dispatcher-connection.c',
+  'mm-dispatcher-fcc-unlock.c',
   'mm-filter.c',
   'mm-iface-modem-3gpp.c',
   'mm-iface-modem-3gpp-profile-manager.c',
@@ -254,6 +256,8 @@
   '-DPLUGINDIR="@0@"'.format(mm_prefix / mm_pkglibdir),
   '-DFCCUNLOCKDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'fcc-unlock.d'),
   '-DFCCUNLOCKDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'fcc-unlock.d'),
+  '-DCONNECTIONDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'connection.d'),
+  '-DCONNECTIONDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'connection.d'),
 ]
 
 if enable_qrtr
diff --git a/src/mm-base-bearer.c b/src/mm-base-bearer.c
index a6b5c65..f8d8e73 100644
--- a/src/mm-base-bearer.c
+++ b/src/mm-base-bearer.c
@@ -39,6 +39,7 @@
 #include "mm-modem-helpers.h"
 #include "mm-error-helpers.h"
 #include "mm-bearer-stats.h"
+#include "mm-dispatcher-connection.h"
 
 /* We require up to 20s to get a proper IP when using PPP */
 #define BEARER_IP_TIMEOUT_DEFAULT 20
@@ -476,6 +477,43 @@
 /*****************************************************************************/
 
 static void
+dispatcher_connection_run_ready (MMDispatcherConnection *dispatcher,
+                                 GAsyncResult           *res,
+                                 MMBaseBearer           *self)
+{
+    g_autoptr(GError) error = NULL;
+
+    if (!mm_dispatcher_connection_run_finish (dispatcher, res, &error))
+        mm_obj_warn (self, "errors detected in dispatcher: %s", error->message);
+
+    g_object_unref (self);
+}
+
+static void
+bearer_run_dispatcher_scripts (MMBaseBearer *self,
+                               gboolean      connected)
+{
+    MMDispatcherConnection *dispatcher;
+    const gchar *interface;
+
+    interface = mm_gdbus_bearer_get_interface (MM_GDBUS_BEARER (self));
+    if (!self->priv->modem || !self->priv->path || !interface)
+        return;
+
+    dispatcher = mm_dispatcher_connection_get ();
+    mm_dispatcher_connection_run (dispatcher,
+                                  g_dbus_object_get_object_path (G_DBUS_OBJECT (self->priv->modem)),
+                                  self->priv->path,
+                                  interface,
+                                  connected,
+                                  NULL, /* cancellable */
+                                  (GAsyncReadyCallback)dispatcher_connection_run_ready,
+                                  g_object_ref (self));
+}
+
+/*****************************************************************************/
+
+static void
 bearer_reset_interface_status (MMBaseBearer *self)
 {
     mm_gdbus_bearer_set_profile_id (MM_GDBUS_BEARER (self), MM_3GPP_PROFILE_ID_UNKNOWN);
@@ -511,6 +549,9 @@
     if (self->priv->status == MM_BEARER_STATUS_DISCONNECTED) {
         g_autoptr(GString) report = NULL;
 
+        /* Report disconnection via dispatcher scripts, before reseting the interface */
+        bearer_run_dispatcher_scripts (self, FALSE);
+
         bearer_reset_interface_status (self);
         /* Cleanup flag to ignore disconnection reports */
         self->priv->ignore_disconnection_reports = FALSE;
@@ -575,6 +616,9 @@
 
     /* Start connection monitor, if supported */
     connection_monitor_start (self);
+
+    /* Run dispatcher scripts */
+    bearer_run_dispatcher_scripts (self, TRUE);
 }
 
 /*****************************************************************************/
diff --git a/src/mm-broadband-modem-mbim.c b/src/mm-broadband-modem-mbim.c
index 53ca852..25df8ab 100644
--- a/src/mm-broadband-modem-mbim.c
+++ b/src/mm-broadband-modem-mbim.c
@@ -2909,6 +2909,7 @@
     QMI_SERVICE_PDS,
     QMI_SERVICE_LOC,
     QMI_SERVICE_PDC,
+    QMI_SERVICE_FOX,
 };
 
 static void allocate_next_qmi_client (GTask *task);
@@ -3978,18 +3979,6 @@
             }
         }
 
-        auth = mm_bearer_properties_get_allowed_auth (config);
-        if (auth == MM_BEARER_ALLOWED_AUTH_UNKNOWN)
-            configurations[i]->auth_protocol = MBIM_AUTH_PROTOCOL_NONE;
-        else {
-            configurations[i]->auth_protocol = mm_bearer_allowed_auth_to_mbim_auth_protocol (auth, self, &error);
-            if (error) {
-                configurations[i]->auth_protocol = MBIM_AUTH_PROTOCOL_NONE;
-                mm_obj_warn (self, "unexpected auth settings requested: %s", error->message);
-                g_clear_error (&error);
-            }
-        }
-
         g_clear_pointer (&(configurations[i]->access_string), g_free);
         configurations[i]->access_string = g_strdup (mm_bearer_properties_get_apn (config));
 
@@ -3999,6 +3988,18 @@
         g_clear_pointer (&(configurations[i]->password), g_free);
         configurations[i]->password = g_strdup (mm_bearer_properties_get_password (config));
 
+        auth = mm_bearer_properties_get_allowed_auth (config);
+        if ((auth != MM_BEARER_ALLOWED_AUTH_UNKNOWN) || configurations[i]->user_name || configurations[i]->password) {
+            configurations[i]->auth_protocol = mm_bearer_allowed_auth_to_mbim_auth_protocol (auth, self, &error);
+            if (error) {
+                configurations[i]->auth_protocol = MBIM_AUTH_PROTOCOL_NONE;
+                mm_obj_warn (self, "unexpected auth settings requested: %s", error->message);
+                g_clear_error (&error);
+            }
+        } else {
+            configurations[i]->auth_protocol = MBIM_AUTH_PROTOCOL_NONE;
+        }
+
         configurations[i]->source = MBIM_CONTEXT_SOURCE_USER;
         configurations[i]->compression = MBIM_COMPRESSION_NONE;
         break;
@@ -5142,34 +5143,51 @@
     }
 }
 
-static void
-update_sim_from_slot_status (MMBroadbandModemMbim *self,
-                             MbimUiccSlotState     slot_status,
-                             guint                 slot_index)
+static MMBaseSim *
+create_sim_from_slot_state (MMBroadbandModemMbim *self,
+                            gboolean              active,
+                            guint                 slot_index,
+                            MbimUiccSlotState     slot_state)
 {
-    g_autoptr(MMBaseSim) sim = NULL;
+    MMSimType       sim_type    = MM_SIM_TYPE_UNKNOWN;
+    MMSimEsimStatus esim_status = MM_SIM_ESIM_STATUS_UNKNOWN;
 
-    mm_obj_dbg (self, "Updating sim at slot %d", slot_index + 1);
-
-    /* Not fully ready (NOT_READY) or unusable (ERROR) SIM cards should also be
-     * reported as being available in the non-active slot. */
-    if (slot_status == MBIM_UICC_SLOT_STATE_ACTIVE ||
-        slot_status == MBIM_UICC_SLOT_STATE_ACTIVE_ESIM ||
-        slot_status == MBIM_UICC_SLOT_STATE_ACTIVE_ESIM_NO_PROFILES ||
-        slot_status == MBIM_UICC_SLOT_STATE_NOT_READY ||
-        slot_status == MBIM_UICC_SLOT_STATE_ERROR) {
-        sim = mm_sim_mbim_new_initialized (MM_BASE_MODEM (self),
-                                           slot_index,
-                                           FALSE,
-                                           NULL,
-                                           NULL,
-                                           NULL,
-                                           NULL,
-                                           NULL,
-                                           NULL);
+    switch (slot_state) {
+    case MBIM_UICC_SLOT_STATE_ACTIVE:
+        sim_type = MM_SIM_TYPE_PHYSICAL;
+        break;
+    case MBIM_UICC_SLOT_STATE_ACTIVE_ESIM:
+        sim_type = MM_SIM_TYPE_ESIM;
+        esim_status = MM_SIM_ESIM_STATUS_WITH_PROFILES;
+        break;
+    case MBIM_UICC_SLOT_STATE_ACTIVE_ESIM_NO_PROFILES:
+        sim_type = MM_SIM_TYPE_ESIM;
+        esim_status = MM_SIM_ESIM_STATUS_NO_PROFILES;
+        break;
+    case MBIM_UICC_SLOT_STATE_NOT_READY:
+    case MBIM_UICC_SLOT_STATE_ERROR:
+        /* Not fully ready (NOT_READY) or unusable (ERROR) SIM cards should also be
+         * reported as being available in the non-active slot. */
+        break;
+    case MBIM_UICC_SLOT_STATE_UNKNOWN:
+    case MBIM_UICC_SLOT_SATE_OFF_EMPTY:
+    case MBIM_UICC_SLOT_STATE_OFF:
+    case MBIM_UICC_SLOT_STATE_EMPTY:
+    default:
+        return NULL;
     }
 
-    mm_iface_modem_modify_sim (MM_IFACE_MODEM (self), slot_index, sim);
+    return MM_BASE_SIM (mm_sim_mbim_new_initialized (MM_BASE_MODEM (self),
+                                                     slot_index,
+                                                     active,
+                                                     sim_type,
+                                                     esim_status,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL,
+                                                     NULL));
 }
 
 static void
@@ -5198,7 +5216,11 @@
     } else {
         /* Modifies SIM object at the given slot based on the reported state,
          * when the slot is not the active one. */
-        update_sim_from_slot_status (self, slot_state, slot_index);
+        g_autoptr(MMBaseSim) sim = NULL;
+
+        mm_obj_dbg (self, "Updating inactive sim at slot %d", slot_index + 1);
+        sim = create_sim_from_slot_state (self, FALSE, slot_index, slot_state);
+        mm_iface_modem_modify_sim (MM_IFACE_MODEM (self), slot_index, sim);
     }
 }
 
@@ -8364,7 +8386,7 @@
                                      GAsyncResult *res,
                                      GTask        *task)
 {
-    MMIfaceModem          *self;
+    MMBroadbandModemMbim  *self;
     g_autoptr(MbimMessage) response = NULL;
     GError                *error = NULL;
     guint32                slot_index;
@@ -8393,25 +8415,8 @@
     if ((slot_index + 1) == ctx->active_slot_index)
         sim_active = TRUE;
 
-    /* Not fully ready (NOT_READY) or unusable (ERROR) SIM cards should also be
-     * reported as being available. */
-    if (slot_state == MBIM_UICC_SLOT_STATE_ACTIVE ||
-        slot_state == MBIM_UICC_SLOT_STATE_ACTIVE_ESIM ||
-        slot_state == MBIM_UICC_SLOT_STATE_ACTIVE_ESIM_NO_PROFILES ||
-        slot_state == MBIM_UICC_SLOT_STATE_NOT_READY ||
-        slot_state == MBIM_UICC_SLOT_STATE_ERROR) {
-        sim = mm_sim_mbim_new_initialized (MM_BASE_MODEM (self),
-                                           slot_index,
-                                           sim_active,
-                                           NULL,
-                                           NULL,
-                                           NULL,
-                                           NULL,
-                                           NULL,
-                                           NULL);
-        g_ptr_array_add (ctx->sim_slots, sim);
-    } else
-        g_ptr_array_add (ctx->sim_slots, NULL);
+    sim = create_sim_from_slot_state (self, sim_active, slot_index, slot_state);
+    g_ptr_array_add (ctx->sim_slots, sim);
 
     ctx->query_slot_index++;
     if (ctx->query_slot_index < ctx->number_slots) {
diff --git a/src/mm-broadband-modem-qmi.c b/src/mm-broadband-modem-qmi.c
index 15f0e7a..d4341ca 100644
--- a/src/mm-broadband-modem-qmi.c
+++ b/src/mm-broadband-modem-qmi.c
@@ -12,7 +12,7 @@
  *
  * Copyright (C) 2012 Google Inc.
  * Copyright (C) 2014 Aleksander Morgado <aleksander@aleksander.es>
- * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc.
  */
 
 #include <config.h>
diff --git a/src/mm-broadband-modem.c b/src/mm-broadband-modem.c
index cf7bc83..f711e50 100644
--- a/src/mm-broadband-modem.c
+++ b/src/mm-broadband-modem.c
@@ -4496,6 +4496,7 @@
                                            NULL, /* format */
                                            &operator_code,
                                            NULL, /* act */
+                                           self,
                                            error))
         return NULL;
 
@@ -4535,6 +4536,7 @@
                                            NULL, /* format */
                                            &operator_name,
                                            NULL, /* act */
+                                           self,
                                            error))
         return NULL;
 
diff --git a/src/mm-context.c b/src/mm-context.c
index ee9b372..37793e5 100644
--- a/src/mm-context.c
+++ b/src/mm-context.c
@@ -134,6 +134,7 @@
 static gboolean     log_journal;
 static gboolean     log_show_ts;
 static gboolean     log_rel_ts;
+static gboolean     log_personal_info;
 
 static const GOptionEntry log_entries[] = {
     {
@@ -163,6 +164,11 @@
         "Use relative timestamps (from MM start)",
         NULL
     },
+    {
+        "log-personal-info", 0, 0, G_OPTION_ARG_NONE, &log_personal_info,
+        "Show personal info in logs",
+        NULL
+    },
     { NULL }
 };
 
@@ -210,6 +216,12 @@
     return log_rel_ts;
 }
 
+gboolean
+mm_context_get_log_personal_info (void)
+{
+    return log_personal_info;
+}
+
 /*****************************************************************************/
 /* Test context */
 
diff --git a/src/mm-context.h b/src/mm-context.h
index 2a69e11..8a5c045 100644
--- a/src/mm-context.h
+++ b/src/mm-context.h
@@ -41,6 +41,7 @@
 gboolean     mm_context_get_log_journal             (void);
 gboolean     mm_context_get_log_timestamps          (void);
 gboolean     mm_context_get_log_relative_timestamps (void);
+gboolean     mm_context_get_log_personal_info       (void);
 
 /* Testing support */
 gboolean     mm_context_get_test_session           (void);
diff --git a/src/mm-dispatcher-connection.c b/src/mm-dispatcher-connection.c
new file mode 100644
index 0000000..7f370a5
--- /dev/null
+++ b/src/mm-dispatcher-connection.c
@@ -0,0 +1,231 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * 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:
+ *
+ * Copyright (C) 2022 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include <config.h>
+#include <sys/stat.h>
+
+#include <ModemManager.h>
+#include "mm-errors-types.h"
+#include "mm-utils.h"
+#include "mm-log-object.h"
+#include "mm-dispatcher-connection.h"
+
+#if !defined CONNECTIONDIRPACKAGE
+# error CONNECTIONDIRPACKAGE must be defined at build time
+#endif
+
+#if !defined CONNECTIONDIRUSER
+# error CONNECTIONDIRUSER must be defined at build time
+#endif
+
+#define OPERATION_DESCRIPTION "connection status report"
+#define CONNECTED_STRING      "connected"
+#define DISCONNECTED_STRING   "disconnected"
+
+/* Maximum time a connection dispatcher command is allowed to run before
+ * us killing it */
+#define MAX_CONNECTION_EXEC_TIME_SECS 5
+
+struct _MMDispatcherConnection {
+    MMDispatcher parent;
+};
+
+struct _MMDispatcherConnectionClass {
+    MMDispatcherClass parent;
+};
+
+G_DEFINE_TYPE (MMDispatcherConnection, mm_dispatcher_connection, MM_TYPE_DISPATCHER)
+
+/*****************************************************************************/
+
+typedef struct {
+    gchar    *modem_dbus_path;
+    gchar    *bearer_dbus_path;
+    gchar    *data_port;
+    gboolean  connected;
+    GList    *dispatcher_scripts;
+    GFile    *current;
+    guint     n_failures;
+} ConnectionRunContext;
+
+static void
+connection_run_context_free (ConnectionRunContext *ctx)
+{
+    g_assert (!ctx->current);
+    g_free (ctx->modem_dbus_path);
+    g_free (ctx->bearer_dbus_path);
+    g_free (ctx->data_port);
+    g_list_free_full (ctx->dispatcher_scripts, (GDestroyNotify)g_object_unref);
+    g_slice_free (ConnectionRunContext, ctx);
+}
+
+gboolean
+mm_dispatcher_connection_run_finish (MMDispatcherConnection  *self,
+                                     GAsyncResult           *res,
+                                     GError                **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void connection_run_next (GTask *task);
+
+static void
+dispatcher_run_ready (MMDispatcher *self,
+                      GAsyncResult *res,
+                      GTask        *task)
+{
+    ConnectionRunContext *ctx;
+    g_autoptr(GError)     error = NULL;
+
+    ctx = g_task_get_task_data (task);
+
+    if (!mm_dispatcher_run_finish (self, res, &error)) {
+        ctx->n_failures++;
+        mm_obj_warn (self, "Cannot run " OPERATION_DESCRIPTION " operation from %s: %s",
+                     g_file_peek_path (ctx->current), error->message);
+    } else
+        mm_obj_dbg (self, OPERATION_DESCRIPTION " operation successfully from %s",
+                    g_file_peek_path (ctx->current));
+
+    g_clear_object (&ctx->current);
+    connection_run_next (task);
+}
+
+static void
+connection_run_next (GTask *task)
+{
+    MMDispatcherConnection *self;
+    ConnectionRunContext   *ctx;
+    g_autofree gchar       *path = NULL;
+    GPtrArray              *aux;
+    g_auto(GStrv)           argv = NULL;
+
+    self = g_task_get_source_object (task);
+    ctx = g_task_get_task_data (task);
+
+    if (!ctx->dispatcher_scripts) {
+        if (ctx->n_failures)
+            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                     "Failed %u " OPERATION_DESCRIPTION " operations",
+                                     ctx->n_failures);
+        else
+            g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+        return;
+    }
+
+    /* store current file reference in context */
+    ctx->current = ctx->dispatcher_scripts->data;
+    ctx->dispatcher_scripts = g_list_delete_link (ctx->dispatcher_scripts, ctx->dispatcher_scripts);
+    path = g_file_get_path (ctx->current);
+
+    /* build argv */
+    aux = g_ptr_array_new ();
+    g_ptr_array_add (aux, g_steal_pointer (&path));
+    g_ptr_array_add (aux, g_strdup (ctx->modem_dbus_path));
+    g_ptr_array_add (aux, g_strdup (ctx->bearer_dbus_path));
+    g_ptr_array_add (aux, g_strdup (ctx->data_port));
+    g_ptr_array_add (aux, g_strdup (ctx->connected ? CONNECTED_STRING : DISCONNECTED_STRING));
+    g_ptr_array_add (aux, NULL);
+    argv = (GStrv) g_ptr_array_free (aux, FALSE);
+
+    /* run */
+    mm_dispatcher_run (MM_DISPATCHER (self),
+                       argv,
+                       MAX_CONNECTION_EXEC_TIME_SECS,
+                       g_task_get_cancellable (task),
+                       (GAsyncReadyCallback) dispatcher_run_ready,
+                       task);
+}
+
+static gint
+dispatcher_script_cmp (GFile *a,
+                       GFile *b)
+{
+    g_autofree gchar *a_name = NULL;
+    g_autofree gchar *b_name = NULL;
+
+    a_name = g_file_get_basename (a);
+    b_name = g_file_get_basename (b);
+
+    return g_strcmp0 (a_name, b_name);
+}
+
+void
+mm_dispatcher_connection_run (MMDispatcherConnection *self,
+                              const gchar            *modem_dbus_path,
+                              const gchar            *bearer_dbus_path,
+                              const gchar            *data_port,
+                              gboolean                connected,
+                              GCancellable           *cancellable,
+                              GAsyncReadyCallback     callback,
+                              gpointer                user_data)
+{
+    GTask                *task;
+    ConnectionRunContext *ctx;
+    guint                 i;
+    const gchar          *enabled_dirs[] = {
+        CONNECTIONDIRUSER,    /* sysconfdir */
+        CONNECTIONDIRPACKAGE, /* libdir */
+    };
+
+    task = g_task_new (self, cancellable, callback, user_data);
+
+    ctx = g_slice_new0 (ConnectionRunContext);
+    ctx->modem_dbus_path = g_strdup (modem_dbus_path);
+    ctx->bearer_dbus_path = g_strdup (bearer_dbus_path);
+    ctx->data_port = g_strdup (data_port);
+    ctx->connected = connected;
+    g_task_set_task_data (task, ctx, (GDestroyNotify)connection_run_context_free);
+
+    /* Iterate over all enabled dirs and collect all dispatcher script paths */
+    for (i = 0; i < G_N_ELEMENTS (enabled_dirs); i++) {
+        g_autoptr(GFile)            dir_file = NULL;
+        g_autoptr(GFileEnumerator)  enumerator = NULL;
+        GFile                      *child;
+
+        dir_file = g_file_new_for_path (enabled_dirs[i]);
+        enumerator = g_file_enumerate_children (dir_file,
+                                                G_FILE_ATTRIBUTE_STANDARD_NAME,
+                                                G_FILE_QUERY_INFO_NONE,
+                                                cancellable,
+                                                NULL);
+        if (!enumerator)
+            continue;
+
+        while (g_file_enumerator_iterate (enumerator, NULL, &child, cancellable, NULL) && child)
+            ctx->dispatcher_scripts = g_list_prepend (ctx->dispatcher_scripts, g_object_ref (child));
+    }
+
+    /* Sort all by filename, regardless of the directory where they're in */
+    ctx->dispatcher_scripts = g_list_sort (ctx->dispatcher_scripts, (GCompareFunc)dispatcher_script_cmp);
+
+    connection_run_next (task);
+}
+
+/*****************************************************************************/
+
+static void
+mm_dispatcher_connection_init (MMDispatcherConnection *self)
+{
+}
+
+static void
+mm_dispatcher_connection_class_init (MMDispatcherConnectionClass *class)
+{
+}
+
+MM_DEFINE_SINGLETON_GETTER (MMDispatcherConnection, mm_dispatcher_connection_get, MM_TYPE_DISPATCHER_CONNECTION,
+                            MM_DISPATCHER_OPERATION_DESCRIPTION, OPERATION_DESCRIPTION)
diff --git a/src/mm-dispatcher-connection.h b/src/mm-dispatcher-connection.h
new file mode 100644
index 0000000..dfa2170
--- /dev/null
+++ b/src/mm-dispatcher-connection.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * 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:
+ *
+ * Copyright (C) 2022 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#ifndef MM_DISPATCHER_CONNECTION_H
+#define MM_DISPATCHER_CONNECTION_H
+
+#include <config.h>
+#include <gio/gio.h>
+
+#include "mm-dispatcher.h"
+
+#define MM_TYPE_DISPATCHER_CONNECTION            (mm_dispatcher_connection_get_type ())
+#define MM_DISPATCHER_CONNECTION(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_DISPATCHER_CONNECTION, MMDispatcherConnection))
+#define MM_DISPATCHER_CONNECTION_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_DISPATCHER_CONNECTION, MMDispatcherConnectionClass))
+#define MM_IS_DISPATCHER_CONNECTION(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_DISPATCHER_CONNECTION))
+#define MM_IS_DISPATCHER_CONNECTION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_DISPATCHER_CONNECTION))
+#define MM_DISPATCHER_CONNECTION_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_DISPATCHER_CONNECTION, MMDispatcherConnectionClass))
+
+typedef struct _MMDispatcherConnection        MMDispatcherConnection;
+typedef struct _MMDispatcherConnectionClass   MMDispatcherConnectionClass;
+typedef struct _MMDispatcherConnectionPrivate MMDispatcherConnectionPrivate;
+
+GType                   mm_dispatcher_connection_get_type   (void);
+MMDispatcherConnection *mm_dispatcher_connection_get        (void);
+void                    mm_dispatcher_connection_run        (MMDispatcherConnection *self,
+                                                             const gchar            *modem_dbus_path,
+                                                             const gchar            *bearer_dbus_path,
+                                                             const gchar            *data_port,
+                                                             gboolean                connected,
+                                                             GCancellable           *cancellable,
+                                                             GAsyncReadyCallback     callback,
+                                                             gpointer                user_data);
+gboolean                mm_dispatcher_connection_run_finish (MMDispatcherConnection  *self,
+                                                             GAsyncResult           *res,
+                                                             GError                **error);
+
+#endif /* MM_DISPATCHER_CONNECTION_H */
diff --git a/src/mm-dispatcher-fcc-unlock.c b/src/mm-dispatcher-fcc-unlock.c
new file mode 100644
index 0000000..ec9aec1
--- /dev/null
+++ b/src/mm-dispatcher-fcc-unlock.c
@@ -0,0 +1,149 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * 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:
+ *
+ * Copyright (C) 2021-2022 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include <config.h>
+#include <sys/stat.h>
+
+#include <ModemManager.h>
+#include "mm-errors-types.h"
+#include "mm-utils.h"
+#include "mm-log-object.h"
+#include "mm-dispatcher-fcc-unlock.h"
+
+#if !defined FCCUNLOCKDIRPACKAGE
+# error FCCUNLOCKDIRPACKAGE must be defined at build time
+#endif
+
+#if !defined FCCUNLOCKDIRUSER
+# error FCCUNLOCKDIRUSER must be defined at build time
+#endif
+
+#define OPERATION_DESCRIPTION "fcc unlock"
+
+/* Maximum time a FCC unlock command is allowed to run before
+ * us killing it */
+#define MAX_FCC_UNLOCK_EXEC_TIME_SECS 5
+
+struct _MMDispatcherFccUnlock {
+    MMDispatcher parent;
+};
+
+struct _MMDispatcherFccUnlockClass {
+    MMDispatcherClass parent;
+};
+
+G_DEFINE_TYPE (MMDispatcherFccUnlock, mm_dispatcher_fcc_unlock, MM_TYPE_DISPATCHER)
+
+/*****************************************************************************/
+
+gboolean
+mm_dispatcher_fcc_unlock_run_finish (MMDispatcherFccUnlock  *self,
+                                     GAsyncResult           *res,
+                                     GError                **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+dispatcher_run_ready (MMDispatcher *self,
+                      GAsyncResult *res,
+                      GTask        *task)
+{
+    GError *error = NULL;
+
+    if (!mm_dispatcher_run_finish (self, res, &error))
+        g_task_return_error (task, error);
+    else
+        g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+void
+mm_dispatcher_fcc_unlock_run (MMDispatcherFccUnlock  *self,
+                              guint                   vid,
+                              guint                   pid,
+                              const gchar            *modem_dbus_path,
+                              const GStrv             modem_ports,
+                              GCancellable           *cancellable,
+                              GAsyncReadyCallback     callback,
+                              gpointer                user_data)
+{
+    GTask            *task;
+    guint             i;
+    g_autofree gchar *filename = NULL;
+    const gchar      *enabled_dirs[] = {
+        FCCUNLOCKDIRUSER,    /* sysconfdir */
+        FCCUNLOCKDIRPACKAGE, /* libdir */
+    };
+
+    task = g_task_new (self, cancellable, callback, user_data);
+
+    filename = g_strdup_printf ("%04x:%04x", vid, pid);
+
+    for (i = 0; i < G_N_ELEMENTS (enabled_dirs); i++) {
+        GPtrArray        *aux;
+        g_auto(GStrv)     argv = NULL;
+        g_autofree gchar *path = NULL;
+        g_autoptr(GFile)  file = NULL;
+        g_autoptr(GError) error = NULL;
+        guint             j;
+
+        path = g_build_path (G_DIR_SEPARATOR_S, enabled_dirs[i], filename, NULL);
+        file = g_file_new_for_path (path);
+
+        /* If file exists, we attempt to use it */
+        if (!g_file_query_exists (file, cancellable)) {
+            mm_obj_dbg (self, "Cannot run " OPERATION_DESCRIPTION " operation from %s: file doesn't exist", path);
+            continue;
+        }
+
+        /* build argv */
+        aux = g_ptr_array_new ();
+        g_ptr_array_add (aux, g_steal_pointer (&path));
+        g_ptr_array_add (aux, g_strdup (modem_dbus_path));
+        for (j = 0; modem_ports && modem_ports[j]; j++)
+            g_ptr_array_add (aux, g_strdup (modem_ports[j]));
+        g_ptr_array_add (aux, NULL);
+        argv = (GStrv) g_ptr_array_free (aux, FALSE);
+
+        /* run */
+        mm_dispatcher_run (MM_DISPATCHER (self),
+                           argv,
+                           MAX_FCC_UNLOCK_EXEC_TIME_SECS,
+                           cancellable,
+                           (GAsyncReadyCallback) dispatcher_run_ready,
+                           task);
+        return;
+    }
+
+    g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                             OPERATION_DESCRIPTION " operation launch aborted: no valid program found");
+    g_object_unref (task);
+}
+
+/*****************************************************************************/
+
+static void
+mm_dispatcher_fcc_unlock_init (MMDispatcherFccUnlock *self)
+{
+}
+
+static void
+mm_dispatcher_fcc_unlock_class_init (MMDispatcherFccUnlockClass *class)
+{
+}
+
+MM_DEFINE_SINGLETON_GETTER (MMDispatcherFccUnlock, mm_dispatcher_fcc_unlock_get, MM_TYPE_DISPATCHER_FCC_UNLOCK,
+                            MM_DISPATCHER_OPERATION_DESCRIPTION, OPERATION_DESCRIPTION)
diff --git a/src/mm-dispatcher-fcc-unlock.h b/src/mm-dispatcher-fcc-unlock.h
new file mode 100644
index 0000000..6f182fe
--- /dev/null
+++ b/src/mm-dispatcher-fcc-unlock.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * 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:
+ *
+ * Copyright (C) 2021-2022 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#ifndef MM_DISPATCHER_FCC_UNLOCK_H
+#define MM_DISPATCHER_FCC_UNLOCK_H
+
+#include <config.h>
+#include <gio/gio.h>
+
+#include "mm-dispatcher.h"
+
+#define MM_TYPE_DISPATCHER_FCC_UNLOCK            (mm_dispatcher_fcc_unlock_get_type ())
+#define MM_DISPATCHER_FCC_UNLOCK(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_DISPATCHER_FCC_UNLOCK, MMDispatcherFccUnlock))
+#define MM_DISPATCHER_FCC_UNLOCK_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_DISPATCHER_FCC_UNLOCK, MMDispatcherFccUnlockClass))
+#define MM_IS_DISPATCHER_FCC_UNLOCK(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_DISPATCHER_FCC_UNLOCK))
+#define MM_IS_DISPATCHER_FCC_UNLOCK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_DISPATCHER_FCC_UNLOCK))
+#define MM_DISPATCHER_FCC_UNLOCK_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_DISPATCHER_FCC_UNLOCK, MMDispatcherFccUnlockClass))
+
+typedef struct _MMDispatcherFccUnlock        MMDispatcherFccUnlock;
+typedef struct _MMDispatcherFccUnlockClass   MMDispatcherFccUnlockClass;
+typedef struct _MMDispatcherFccUnlockPrivate MMDispatcherFccUnlockPrivate;
+
+GType                  mm_dispatcher_fcc_unlock_get_type   (void);
+MMDispatcherFccUnlock *mm_dispatcher_fcc_unlock_get        (void);
+void                   mm_dispatcher_fcc_unlock_run        (MMDispatcherFccUnlock  *self,
+                                                            guint                   vid,
+                                                            guint                   pid,
+                                                            const gchar            *modem_dbus_path,
+                                                            const GStrv             ports,
+                                                            GCancellable           *cancellable,
+                                                            GAsyncReadyCallback     callback,
+                                                            gpointer                user_data);
+gboolean               mm_dispatcher_fcc_unlock_run_finish (MMDispatcherFccUnlock  *self,
+                                                            GAsyncResult           *res,
+                                                            GError                **error);
+
+#endif /* MM_DISPATCHER_FCC_UNLOCK_H */
diff --git a/src/mm-dispatcher.c b/src/mm-dispatcher.c
new file mode 100644
index 0000000..9b45eca
--- /dev/null
+++ b/src/mm-dispatcher.c
@@ -0,0 +1,351 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * 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:
+ *
+ * Copyright (C) 2021-2022 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#include <config.h>
+#include <sys/stat.h>
+
+#include <ModemManager.h>
+#include "mm-errors-types.h"
+#include "mm-utils.h"
+#include "mm-log-object.h"
+#include "mm-dispatcher.h"
+
+static void log_object_iface_init (MMLogObjectInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (MMDispatcher, mm_dispatcher, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init))
+
+enum {
+    PROP_0,
+    PROP_OPERATION_DESCRIPTION,
+    PROP_LAST
+};
+
+static GParamSpec *properties[PROP_LAST];
+
+struct _MMDispatcherPrivate {
+    gchar               *operation_description;
+    GSubprocessLauncher *launcher;
+};
+
+/*****************************************************************************/
+
+static gchar *
+log_object_build_id (MMLogObject *_self)
+{
+    MMDispatcher *self = MM_DISPATCHER (_self);
+
+    return g_strdup_printf ("%s dispatcher", self->priv->operation_description);
+}
+
+/*****************************************************************************/
+
+static gboolean
+validate_file (const gchar  *path,
+               GError      **error)
+{
+    g_autoptr(GFile)     file = NULL;
+    g_autoptr(GFileInfo) file_info = NULL;
+    guint32              file_mode;
+    guint32              file_uid;
+
+    file = g_file_new_for_path (path);
+    file_info = g_file_query_info (file,
+                                   (G_FILE_ATTRIBUTE_STANDARD_SIZE           ","
+                                    G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET ","
+                                    G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK     ","
+                                    G_FILE_ATTRIBUTE_UNIX_MODE               ","
+                                    G_FILE_ATTRIBUTE_UNIX_UID),
+                                   G_FILE_QUERY_INFO_NONE,
+                                   NULL,
+                                   error);
+    if (!file_info)
+        return FALSE;
+
+    if (g_file_info_get_is_symlink (file_info)) {
+        const gchar *link_target;
+
+        link_target = g_file_info_get_symlink_target (file_info);
+        if (g_strcmp0 (link_target, "/dev/null") == 0) {
+            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
+                         "Link '%s' to /dev/null is not executable", path);
+            return FALSE;
+        }
+    }
+
+    if (g_file_info_get_size (file_info) == 0) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
+                     "File '%s' is empty", path);
+        return FALSE;
+    }
+
+    file_uid = g_file_info_get_attribute_uint32 (file_info, G_FILE_ATTRIBUTE_UNIX_UID);
+    if (file_uid != 0) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
+                     "File '%s' not owned by root", path);
+        return FALSE;
+    }
+
+    file_mode = g_file_info_get_attribute_uint32 (file_info, G_FILE_ATTRIBUTE_UNIX_MODE);
+    if (!S_ISREG (file_mode)) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
+                     "File '%s' is not regular", path);
+        return FALSE;
+    }
+    if (file_mode & (S_IWGRP | S_IWOTH)) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
+                     "File '%s' is writable by group or other", path);
+        return FALSE;
+    }
+    if (file_mode & S_ISUID) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
+                     "File '%s' is set-UID", path);
+        return FALSE;
+    }
+    if (!(file_mode & S_IXUSR)) {
+        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
+                     "File '%s' is not executable by the owner", path);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GSubprocess *subprocess;
+    guint        timeout_id;
+} RunContext;
+
+static void
+run_context_free (RunContext *ctx)
+{
+    g_assert (!ctx->timeout_id);
+    g_clear_object (&ctx->subprocess);
+    g_slice_free (RunContext, ctx);
+}
+
+gboolean
+mm_dispatcher_run_finish (MMDispatcher  *self,
+                          GAsyncResult  *res,
+                          GError       **error)
+{
+    return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static gboolean
+subprocess_wait_timed_out (GTask *task)
+{
+    MMDispatcher *self;
+    RunContext   *ctx;
+
+    self = g_task_get_source_object (task);
+    ctx = g_task_get_task_data (task);
+
+    mm_obj_warn (self, "forcing exit on %s operation", self->priv->operation_description);
+    g_subprocess_force_exit (ctx->subprocess);
+
+    ctx->timeout_id = 0;
+    return G_SOURCE_REMOVE;
+}
+
+static void
+subprocess_wait_ready (GSubprocess  *subprocess,
+                       GAsyncResult *res,
+                       GTask        *task)
+{
+    GError       *error = NULL;
+    MMDispatcher *self;
+    RunContext   *ctx;
+
+    self = g_task_get_source_object (task);
+    ctx = g_task_get_task_data (task);
+
+    /* cleanup timeout before any return */
+    if (ctx->timeout_id) {
+        g_source_remove (ctx->timeout_id);
+        ctx->timeout_id = 0;
+    }
+
+    if (!g_subprocess_wait_finish (subprocess, res, &error)) {
+        g_prefix_error (&error, "%s operation wait failed: ", self->priv->operation_description);
+        g_task_return_error (task, error);
+    } else if (!g_subprocess_get_successful (subprocess)) {
+        if (g_subprocess_get_if_signaled (subprocess))
+            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                     "%s operation aborted with signal %d",
+                                     self->priv->operation_description,
+                                     g_subprocess_get_term_sig (subprocess));
+        else if (g_subprocess_get_if_exited (subprocess))
+            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                     "%s operation finished with status %d",
+                                     self->priv->operation_description,
+                                     g_subprocess_get_exit_status (subprocess));
+        else
+            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
+                                     "%s operation failed", self->priv->operation_description);
+    } else
+        g_task_return_boolean (task, TRUE);
+    g_object_unref (task);
+}
+
+void
+mm_dispatcher_run (MMDispatcher        *self,
+                   const GStrv          argv,
+                   guint                timeout_secs,
+                   GCancellable        *cancellable,
+                   GAsyncReadyCallback  callback,
+                   gpointer             user_data)
+{
+    GTask      *task;
+    RunContext *ctx;
+    GError     *error = NULL;
+
+    g_assert (g_strv_length (argv) >= 1);
+
+    task = g_task_new (self, cancellable, callback, user_data);
+    ctx = g_slice_new0 (RunContext);
+    g_task_set_task_data (task, ctx, (GDestroyNotify) run_context_free);
+
+    /* Validation checks to see if we should run it or not */
+    if (!validate_file (argv[0], &error)) {
+        g_prefix_error (&error, "Cannot run %s operation from %s: ",
+                        self->priv->operation_description, argv[0]);
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    /* create and launch subprocess */
+    ctx->subprocess = g_subprocess_launcher_spawnv (self->priv->launcher,
+                                                    (const gchar * const *)argv,
+                                                    &error);
+    if (!ctx->subprocess) {
+        g_prefix_error (&error, "%s operation launch from %s failed: ",
+                        self->priv->operation_description, argv[0]);
+        g_task_return_error (task, error);
+        g_object_unref (task);
+        return;
+    }
+
+    /* setup timeout */
+    ctx->timeout_id = g_timeout_add_seconds (timeout_secs,
+                                             (GSourceFunc)subprocess_wait_timed_out,
+                                             task);
+
+    /* wait for subprocess exit */
+    g_subprocess_wait_async (ctx->subprocess,
+                             cancellable,
+                             (GAsyncReadyCallback)subprocess_wait_ready,
+                             task);
+}
+
+/*****************************************************************************/
+
+static void
+mm_dispatcher_init (MMDispatcher *self)
+{
+    /* Initialize private data */
+    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_DISPATCHER, MMDispatcherPrivate);
+
+    /* Create launcher and inherit parent's environment */
+    self->priv->launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE | G_SUBPROCESS_FLAGS_STDERR_SILENCE);
+    g_subprocess_launcher_set_environ (self->priv->launcher, NULL);
+}
+
+static void
+set_property (GObject      *object,
+              guint         prop_id,
+              const GValue *value,
+              GParamSpec   *pspec)
+{
+    MMDispatcher *self = MM_DISPATCHER (object);
+
+    switch (prop_id) {
+    case PROP_OPERATION_DESCRIPTION:
+        /* construct only */
+        self->priv->operation_description = g_value_dup_string (value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+get_property (GObject    *object,
+              guint       prop_id,
+              GValue     *value,
+              GParamSpec *pspec)
+{
+    MMDispatcher *self = MM_DISPATCHER (object);
+
+    switch (prop_id) {
+    case PROP_OPERATION_DESCRIPTION:
+        g_value_set_string (value, self->priv->operation_description);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+dispose (GObject *object)
+{
+    MMDispatcher *self = MM_DISPATCHER (object);
+
+    g_clear_object (&self->priv->launcher);
+
+    G_OBJECT_CLASS (mm_dispatcher_parent_class)->dispose (object);
+}
+
+static void
+finalize (GObject *object)
+{
+    MMDispatcher *self = MM_DISPATCHER (object);
+
+    g_free (self->priv->operation_description);
+
+    G_OBJECT_CLASS (mm_dispatcher_parent_class)->finalize (object);
+}
+
+static void
+log_object_iface_init (MMLogObjectInterface *iface)
+{
+    iface->build_id = log_object_build_id;
+}
+
+static void
+mm_dispatcher_class_init (MMDispatcherClass *class)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+    g_type_class_add_private (object_class, sizeof (MMDispatcherPrivate));
+
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+    object_class->dispose = dispose;
+    object_class->finalize = finalize;
+
+    properties[PROP_OPERATION_DESCRIPTION] =
+        g_param_spec_string (MM_DISPATCHER_OPERATION_DESCRIPTION,
+                             "Operation description",
+                             "String describing the operation, to be used in logging",
+                             NULL,
+                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+    g_object_class_install_property (object_class, PROP_OPERATION_DESCRIPTION, properties[PROP_OPERATION_DESCRIPTION]);
+}
diff --git a/src/mm-dispatcher.h b/src/mm-dispatcher.h
new file mode 100644
index 0000000..16b9a86
--- /dev/null
+++ b/src/mm-dispatcher.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * 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:
+ *
+ * Copyright (C) 2021-2022 Aleksander Morgado <aleksander@aleksander.es>
+ */
+
+#ifndef MM_DISPATCHER_H
+#define MM_DISPATCHER_H
+
+#include <config.h>
+#include <gio/gio.h>
+
+#define MM_TYPE_DISPATCHER            (mm_dispatcher_get_type ())
+#define MM_DISPATCHER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_DISPATCHER, MMDispatcher))
+#define MM_DISPATCHER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_DISPATCHER, MMDispatcherClass))
+#define MM_IS_DISPATCHER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_DISPATCHER))
+#define MM_IS_DISPATCHER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_DISPATCHER))
+#define MM_DISPATCHER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_DISPATCHER, MMDispatcherClass))
+
+typedef struct _MMDispatcher        MMDispatcher;
+typedef struct _MMDispatcherClass   MMDispatcherClass;
+typedef struct _MMDispatcherPrivate MMDispatcherPrivate;
+
+#define MM_DISPATCHER_OPERATION_DESCRIPTION "operation-description"
+
+struct _MMDispatcher {
+    GObject parent;
+    MMDispatcherPrivate *priv;
+};
+
+struct _MMDispatcherClass {
+    GObjectClass parent;
+};
+
+GType mm_dispatcher_get_type (void);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMDispatcher, g_object_unref)
+
+void     mm_dispatcher_run        (MMDispatcher         *self,
+                                   const GStrv           argv,
+                                   guint                 timeout_secs,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+gboolean mm_dispatcher_run_finish (MMDispatcher         *self,
+                                   GAsyncResult         *res,
+                                   GError              **error);
+
+#endif /* MM_DISPATCHER_H */
diff --git a/src/mm-fcc-unlock-dispatcher.c b/src/mm-fcc-unlock-dispatcher.c
deleted file mode 100644
index a033d99..0000000
--- a/src/mm-fcc-unlock-dispatcher.c
+++ /dev/null
@@ -1,319 +0,0 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
-/*
- * 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:
- *
- * Copyright (C) 2021 Aleksander Morgado <aleksander@aleksander.es>
- */
-
-#include <config.h>
-#include <sys/stat.h>
-
-#include <ModemManager.h>
-#include "mm-errors-types.h"
-#include "mm-utils.h"
-#include "mm-log-object.h"
-#include "mm-fcc-unlock-dispatcher.h"
-
-#if !defined FCCUNLOCKDIRPACKAGE
-# error FCCUNLOCKDIRPACKAGE must be defined at build time
-#endif
-
-#if !defined FCCUNLOCKDIRUSER
-# error FCCUNLOCKDIRUSER must be defined at build time
-#endif
-
-/* Maximum time a FCC unlock command is allowed to run before
- * us killing it */
-#define MAX_FCC_UNLOCK_EXEC_TIME_SECS 5
-
-struct _MMFccUnlockDispatcher {
-    GObject              parent;
-    GSubprocessLauncher *launcher;
-};
-
-struct _MMFccUnlockDispatcherClass {
-    GObjectClass parent;
-};
-
-static void log_object_iface_init (MMLogObjectInterface *iface);
-
-G_DEFINE_TYPE_EXTENDED (MMFccUnlockDispatcher, mm_fcc_unlock_dispatcher, G_TYPE_OBJECT, 0,
-                        G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init))
-
-/*****************************************************************************/
-
-static gchar *
-log_object_build_id (MMLogObject *_self)
-{
-    return g_strdup ("fcc-unlock-dispatcher");
-}
-
-/*****************************************************************************/
-
-typedef struct {
-    gchar       *filename;
-    GSubprocess *subprocess;
-    guint        timeout_id;
-} RunContext;
-
-static void
-run_context_free (RunContext *ctx)
-{
-    g_assert (!ctx->timeout_id);
-    g_clear_object (&ctx->subprocess);
-    g_free (ctx->filename);
-    g_slice_free (RunContext, ctx);
-}
-
-gboolean
-mm_fcc_unlock_dispatcher_run_finish (MMFccUnlockDispatcher  *self,
-                                     GAsyncResult           *res,
-                                     GError                **error)
-{
-    return g_task_propagate_boolean (G_TASK (res), error);
-}
-
-static gboolean
-subprocess_wait_timed_out (GTask *task)
-{
-    MMFccUnlockDispatcher *self;
-    RunContext            *ctx;
-
-    self = g_task_get_source_object (task);
-    ctx = g_task_get_task_data (task);
-
-    mm_obj_warn (self, "forcing exit on %s FCC unlock operation", ctx->filename);
-    g_subprocess_force_exit (ctx->subprocess);
-
-    ctx->timeout_id = 0;
-    return G_SOURCE_REMOVE;
-}
-
-static void
-subprocess_wait_ready (GSubprocess  *subprocess,
-                       GAsyncResult *res,
-                       GTask        *task)
-{
-    GError     *error = NULL;
-    RunContext *ctx;
-
-    /* cleanup timeout before any return */
-    ctx = g_task_get_task_data (task);
-    if (ctx->timeout_id) {
-        g_source_remove (ctx->timeout_id);
-        ctx->timeout_id = 0;
-    }
-
-    if (!g_subprocess_wait_finish (subprocess, res, &error)) {
-        g_prefix_error (&error, "FCC unlock operation wait failed: ");
-        g_task_return_error (task, error);
-    } else if (!g_subprocess_get_successful (subprocess)) {
-        if (g_subprocess_get_if_signaled (subprocess))
-            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                                     "FCC unlock operation aborted with signal %d",
-                                     g_subprocess_get_term_sig (subprocess));
-        else if (g_subprocess_get_if_exited (subprocess))
-            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                                     "FCC unlock operation finished with status %d",
-                                     g_subprocess_get_exit_status (subprocess));
-        else
-            g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                                     "FCC unlock operation failed");
-    } else
-        g_task_return_boolean (task, TRUE);
-    g_object_unref (task);
-}
-
-static gboolean
-validate_file (const gchar  *path,
-               GError      **error)
-{
-    g_autoptr(GFile)     file = NULL;
-    g_autoptr(GFileInfo) file_info = NULL;
-    guint32              file_mode;
-    guint32              file_uid;
-
-    file = g_file_new_for_path (path);
-    file_info = g_file_query_info (file,
-                                   (G_FILE_ATTRIBUTE_STANDARD_SIZE           ","
-                                    G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET ","
-                                    G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK     ","
-                                    G_FILE_ATTRIBUTE_UNIX_MODE               ","
-                                    G_FILE_ATTRIBUTE_UNIX_UID),
-                                   G_FILE_QUERY_INFO_NONE,
-                                   NULL,
-                                   error);
-    if (!file_info)
-        return FALSE;
-
-    if (g_file_info_get_is_symlink (file_info)) {
-        const gchar *link_target;
-
-        link_target = g_file_info_get_symlink_target (file_info);
-        if (g_strcmp0 (link_target, "/dev/null") == 0) {
-            g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
-                         "Link '%s' to /dev/null is not executable", path);
-            return FALSE;
-        }
-    }
-
-    if (g_file_info_get_size (file_info) == 0) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
-                     "File '%s' is empty", path);
-        return FALSE;
-    }
-
-    file_uid = g_file_info_get_attribute_uint32 (file_info, G_FILE_ATTRIBUTE_UNIX_UID);
-    if (file_uid != 0) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
-                     "File '%s' not owned by root", path);
-        return FALSE;
-    }
-
-    file_mode = g_file_info_get_attribute_uint32 (file_info, G_FILE_ATTRIBUTE_UNIX_MODE);
-    if (!S_ISREG (file_mode)) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
-                     "File '%s' is not regular", path);
-        return FALSE;
-    }
-    if (file_mode & (S_IWGRP | S_IWOTH)) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
-                     "File '%s' is writable by group or other", path);
-        return FALSE;
-    }
-    if (file_mode & S_ISUID) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
-                     "File '%s' is set-UID", path);
-        return FALSE;
-    }
-    if (!(file_mode & S_IXUSR)) {
-        g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_ABORTED,
-                     "File '%s' is not executable by the owner", path);
-        return FALSE;
-    }
-
-    return TRUE;
-}
-
-void
-mm_fcc_unlock_dispatcher_run (MMFccUnlockDispatcher  *self,
-                              guint                   vid,
-                              guint                   pid,
-                              const gchar            *modem_dbus_path,
-                              const GStrv             modem_ports,
-                              GCancellable           *cancellable,
-                              GAsyncReadyCallback     callback,
-                              gpointer                user_data)
-{
-    GTask            *task;
-    RunContext       *ctx;
-    guint             i;
-    const gchar      *enabled_dirs[] = {
-        FCCUNLOCKDIRUSER,    /* sysconfdir */
-        FCCUNLOCKDIRPACKAGE, /* libdir */
-    };
-
-    task = g_task_new (self, cancellable, callback, user_data);
-    ctx = g_slice_new0 (RunContext);
-    g_task_set_task_data (task, ctx, (GDestroyNotify) run_context_free);
-
-    ctx->filename = g_strdup_printf ("%04x:%04x", vid, pid);
-
-    for (i = 0; i < G_N_ELEMENTS (enabled_dirs); i++) {
-        GPtrArray        *aux;
-        g_auto(GStrv)     argv = NULL;
-        g_autofree gchar *path = NULL;
-        g_autoptr(GError) error = NULL;
-        guint             j;
-
-        path = g_build_path (G_DIR_SEPARATOR_S, enabled_dirs[i], ctx->filename, NULL);
-
-        /* Validation checks to see if we should run it or not */
-        if (!validate_file (path, &error)) {
-            mm_obj_dbg (self, "Cannot run FCC unlock operation from %s: %s",
-                        path, error->message);
-            continue;
-        }
-
-        /* build argv */
-        aux = g_ptr_array_new ();
-        g_ptr_array_add (aux, g_steal_pointer (&path));
-        g_ptr_array_add (aux, g_strdup (modem_dbus_path));
-        for (j = 0; modem_ports && modem_ports[j]; j++)
-            g_ptr_array_add (aux, g_strdup (modem_ports[j]));
-        g_ptr_array_add (aux, NULL);
-        argv = (GStrv) g_ptr_array_free (aux, FALSE);
-
-        /* create and launch subprocess */
-        ctx->subprocess = g_subprocess_launcher_spawnv (self->launcher,
-                                                        (const gchar * const *)argv,
-                                                        &error);
-        if (!ctx->subprocess) {
-            g_prefix_error (&error, "FCC unlock operation launch from %s failed: ", path);
-            g_task_return_error (task, error);
-            g_object_unref (task);
-            return;
-        }
-
-        /* setup timeout */
-        ctx->timeout_id = g_timeout_add_seconds (MAX_FCC_UNLOCK_EXEC_TIME_SECS,
-                                                 (GSourceFunc)subprocess_wait_timed_out,
-                                                 task);
-
-        /* wait for subprocess exit */
-        g_subprocess_wait_async (ctx->subprocess,
-                                 cancellable,
-                                 (GAsyncReadyCallback)subprocess_wait_ready,
-                                 task);
-        return;
-    }
-
-    g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
-                             "FCC unlock operation launch aborted: no valid program found");
-    g_object_unref (task);
-}
-
-/*****************************************************************************/
-
-static void
-mm_fcc_unlock_dispatcher_init (MMFccUnlockDispatcher *self)
-{
-    self->launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE | G_SUBPROCESS_FLAGS_STDERR_SILENCE);
-
-    /* inherit parent's environment */
-    g_subprocess_launcher_set_environ (self->launcher, NULL);
-}
-
-static void
-dispose (GObject *object)
-{
-    MMFccUnlockDispatcher *self = MM_FCC_UNLOCK_DISPATCHER (object);
-
-    g_clear_object (&self->launcher);
-
-    G_OBJECT_CLASS (mm_fcc_unlock_dispatcher_parent_class)->dispose (object);
-}
-
-static void
-log_object_iface_init (MMLogObjectInterface *iface)
-{
-    iface->build_id = log_object_build_id;
-}
-
-static void
-mm_fcc_unlock_dispatcher_class_init (MMFccUnlockDispatcherClass *class)
-{
-    GObjectClass *object_class = G_OBJECT_CLASS (class);
-
-    object_class->dispose = dispose;
-}
-
-MM_DEFINE_SINGLETON_GETTER (MMFccUnlockDispatcher, mm_fcc_unlock_dispatcher_get, MM_TYPE_FCC_UNLOCK_DISPATCHER)
diff --git a/src/mm-fcc-unlock-dispatcher.h b/src/mm-fcc-unlock-dispatcher.h
deleted file mode 100644
index 53a1b4c..0000000
--- a/src/mm-fcc-unlock-dispatcher.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
-/*
- * 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:
- *
- * Copyright (C) 2021 Aleksander Morgado <aleksander@aleksander.es>
- */
-
-#ifndef MM_FCC_UNLOCK_DISPATCHER_H
-#define MM_FCC_UNLOCK_DISPATCHER_H
-
-#include <config.h>
-#include <gio/gio.h>
-
-#define MM_TYPE_FCC_UNLOCK_DISPATCHER            (mm_fcc_unlock_dispatcher_get_type ())
-#define MM_FCC_UNLOCK_DISPATCHER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_FCC_UNLOCK_DISPATCHER, MMFccUnlockDispatcher))
-#define MM_FCC_UNLOCK_DISPATCHER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  MM_TYPE_FCC_UNLOCK_DISPATCHER, MMFccUnlockDispatcherClass))
-#define MM_IS_FCC_UNLOCK_DISPATCHER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_FCC_UNLOCK_DISPATCHER))
-#define MM_IS_FCC_UNLOCK_DISPATCHER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  MM_TYPE_FCC_UNLOCK_DISPATCHER))
-#define MM_FCC_UNLOCK_DISPATCHER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  MM_TYPE_FCC_UNLOCK_DISPATCHER, MMFccUnlockDispatcherClass))
-
-typedef struct _MMFccUnlockDispatcher        MMFccUnlockDispatcher;
-typedef struct _MMFccUnlockDispatcherClass   MMFccUnlockDispatcherClass;
-typedef struct _MMFccUnlockDispatcherPrivate MMFccUnlockDispatcherPrivate;
-
-GType                  mm_fcc_unlock_dispatcher_get_type   (void);
-MMFccUnlockDispatcher *mm_fcc_unlock_dispatcher_get        (void);
-void                   mm_fcc_unlock_dispatcher_run        (MMFccUnlockDispatcher  *self,
-                                                            guint                   vid,
-                                                            guint                   pid,
-                                                            const gchar            *modem_dbus_path,
-                                                            const GStrv             ports,
-                                                            GCancellable           *cancellable,
-                                                            GAsyncReadyCallback     callback,
-                                                            gpointer                user_data);
-gboolean               mm_fcc_unlock_dispatcher_run_finish (MMFccUnlockDispatcher  *self,
-                                                            GAsyncResult           *res,
-                                                            GError                **error);
-
-#endif /* MM_FCC_UNLOCK_DISPATCHER_H */
diff --git a/src/mm-iface-modem-3gpp-profile-manager.c b/src/mm-iface-modem-3gpp-profile-manager.c
index d2b11fe..cf32e19 100644
--- a/src/mm-iface-modem-3gpp-profile-manager.c
+++ b/src/mm-iface-modem-3gpp-profile-manager.c
@@ -27,8 +27,8 @@
 #include "mm-base-modem.h"
 #include "mm-log-object.h"
 
-#define SUPPORT_CHECKED_TAG "3gpp-ussd-support-checked-tag"
-#define SUPPORTED_TAG       "3gpp-ussd-supported-tag"
+#define SUPPORT_CHECKED_TAG "3gpp-profile-manager-support-checked-tag"
+#define SUPPORTED_TAG       "3gpp-profile-manager-supported-tag"
 
 static GQuark support_checked_quark;
 static GQuark supported_quark;
@@ -1016,8 +1016,6 @@
     const gchar              *index_field;
     GError                   *error = NULL;
     g_autoptr(MM3gppProfile)  profile_requested = NULL;
-    gint                      profile_id = MM_3GPP_PROFILE_ID_UNKNOWN;
-    MMBearerApnType           apn_type = MM_BEARER_APN_TYPE_NONE;
 
     if (!mm_base_modem_authorize_finish (self, res, &error)) {
         g_dbus_method_invocation_take_error (ctx->invocation, error);
@@ -1047,24 +1045,6 @@
     }
 
     index_field = mm_gdbus_modem3gpp_profile_manager_get_index_field (ctx->skeleton);
-    if (g_strcmp0 (index_field, "profile-id") == 0) {
-        profile_id = mm_3gpp_profile_get_profile_id (profile_requested);
-        if (profile_id == MM_3GPP_PROFILE_ID_UNKNOWN) {
-            g_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
-                                                           "Missing index field ('profile-id') in profile settings");
-            handle_set_context_free (ctx);
-            return;
-        }
-    } else if (g_strcmp0 (index_field, "apn-type") == 0) {
-        apn_type = mm_3gpp_profile_get_apn_type (profile_requested);
-        if (apn_type == MM_BEARER_APN_TYPE_NONE) {
-            g_dbus_method_invocation_return_error_literal (ctx->invocation, MM_CORE_ERROR, MM_CORE_ERROR_INVALID_ARGS,
-                                                           "Missing index field ('apn-type') in profile settings");
-            handle_set_context_free (ctx);
-            return;
-        }
-    } else
-        g_assert_not_reached ();
 
     /* Don't call the class callback directly, use the common helper method
      * that is also used by other internal operations. */
diff --git a/src/mm-iface-modem.c b/src/mm-iface-modem.c
index 140af48..0129930 100644
--- a/src/mm-iface-modem.c
+++ b/src/mm-iface-modem.c
@@ -28,7 +28,7 @@
 #include "mm-private-boxed-types.h"
 #include "mm-log-object.h"
 #include "mm-context.h"
-#include "mm-fcc-unlock-dispatcher.h"
+#include "mm-dispatcher-fcc-unlock.h"
 #if defined WITH_QMI
 # include "mm-broadband-modem-qmi.h"
 #endif
@@ -3954,7 +3954,7 @@
 }
 
 static void
-fcc_unlock_dispatcher_ready (MMFccUnlockDispatcher *dispatcher,
+dispatcher_fcc_unlock_ready (MMDispatcherFccUnlock *dispatcher,
                              GAsyncResult          *res,
                              GTask                 *task)
 {
@@ -3965,7 +3965,7 @@
     self = g_task_get_source_object (task);
     ctx = g_task_get_task_data (task);
 
-    if (!mm_fcc_unlock_dispatcher_run_finish (dispatcher, res, &error))
+    if (!mm_dispatcher_fcc_unlock_run_finish (dispatcher, res, &error))
         mm_obj_dbg (self, "couldn't run FCC unlock: %s", error->message);
 
     /* always retry, even on reported error */
@@ -3977,7 +3977,7 @@
 fcc_unlock (GTask *task)
 {
     MMIfaceModem          *self;
-    MMFccUnlockDispatcher *dispatcher;
+    MMDispatcherFccUnlock *dispatcher;
     MMModemPortInfo       *port_infos;
     guint                  n_port_infos = 0;
     guint                  i;
@@ -3986,7 +3986,7 @@
 
     self = g_task_get_source_object (task);
 
-    dispatcher = mm_fcc_unlock_dispatcher_get ();
+    dispatcher = mm_dispatcher_fcc_unlock_get ();
 
     aux = g_ptr_array_new ();
     port_infos = mm_base_modem_get_port_infos (MM_BASE_MODEM (self), &n_port_infos);
@@ -4011,13 +4011,13 @@
     g_ptr_array_add (aux, NULL);
     modem_ports = (GStrv) g_ptr_array_free (aux, FALSE);
 
-    mm_fcc_unlock_dispatcher_run (dispatcher,
+    mm_dispatcher_fcc_unlock_run (dispatcher,
                                   mm_base_modem_get_vendor_id (MM_BASE_MODEM (self)),
                                   mm_base_modem_get_product_id (MM_BASE_MODEM (self)),
                                   g_dbus_object_get_object_path (G_DBUS_OBJECT (self)),
                                   modem_ports,
                                   g_task_get_cancellable (task),
-                                  (GAsyncReadyCallback)fcc_unlock_dispatcher_ready,
+                                  (GAsyncReadyCallback)dispatcher_fcc_unlock_ready,
                                   task);
 }
 
diff --git a/src/mm-log.c b/src/mm-log.c
index 22a3901..7b81d9f 100644
--- a/src/mm-log.c
+++ b/src/mm-log.c
@@ -310,6 +310,7 @@
               gboolean log_journal,
               gboolean show_timestamps,
               gboolean rel_timestamps,
+              gboolean show_personal_info,
               GError **error)
 {
     /* levels */
@@ -366,6 +367,7 @@
 #endif
 
 #if defined WITH_MBIM
+    mbim_utils_set_show_personal_info (show_personal_info);
     g_log_set_handler ("Mbim",
                        G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION,
                        log_handler,
diff --git a/src/mm-log.h b/src/mm-log.h
index d0b5c60..4c643e2 100644
--- a/src/mm-log.h
+++ b/src/mm-log.h
@@ -60,6 +60,7 @@
                        gboolean log_journal,
                        gboolean show_ts,
                        gboolean rel_ts,
+                       gboolean show_personal_info,
                        GError **error);
 
 void mm_log_shutdown (void);
diff --git a/src/mm-modem-helpers-mbim.c b/src/mm-modem-helpers-mbim.c
index 50e2dc8..757d3ab 100644
--- a/src/mm-modem-helpers-mbim.c
+++ b/src/mm-modem-helpers-mbim.c
@@ -432,21 +432,24 @@
 mm_mobile_equipment_error_from_mbim_nw_error (MbimNwError nw_error,
                                               gpointer    log_object)
 {
-    MMMobileEquipmentError  error_code;
     const gchar            *msg;
 
-    /* convert to mobile equipment error */
-    error_code = mbim_nw_errors[nw_error];
-    if (error_code)
-        return mm_mobile_equipment_error_for_code (error_code, log_object);
+    if (nw_error < G_N_ELEMENTS (mbim_nw_errors)) {
+        MMMobileEquipmentError  error_code;
 
-    /* provide a nicer error message on unmapped errors */
-    msg = mbim_nw_error_get_string (nw_error);
-    if (msg)
-        return g_error_new (MM_MOBILE_EQUIPMENT_ERROR,
-                            MM_MOBILE_EQUIPMENT_ERROR_UNKNOWN,
-                            "Unsupported error (%u): %s",
-                            nw_error, msg);
+        /* convert to mobile equipment error */
+        error_code = mbim_nw_errors[nw_error];
+        if (error_code)
+            return mm_mobile_equipment_error_for_code (error_code, log_object);
+
+        /* provide a nicer error message on unmapped errors */
+        msg = mbim_nw_error_get_string (nw_error);
+        if (msg)
+            return g_error_new (MM_MOBILE_EQUIPMENT_ERROR,
+                                MM_MOBILE_EQUIPMENT_ERROR_UNKNOWN,
+                                "Unsupported error (%u): %s",
+                                nw_error, msg);
+    }
 
     /* fallback */
     return g_error_new_literal (MM_MOBILE_EQUIPMENT_ERROR,
diff --git a/src/mm-modem-helpers-qmi.c b/src/mm-modem-helpers-qmi.c
index d95fd50..ce3967f 100644
--- a/src/mm-modem-helpers-qmi.c
+++ b/src/mm-modem-helpers-qmi.c
@@ -12,7 +12,7 @@
  *
  * Copyright (C) 2012-2018 Google, Inc.
  * Copyright (C) 2018 Aleksander Morgado <aleksander@aleksander.es>
- * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021 Qualcomm Innovation Center, Inc.
  */
 
 #include <string.h>
@@ -1842,9 +1842,17 @@
     g_autofree gchar *tmp_str = NULL;
 
     /* If not a multimode device, we're done */
-    if (!ctx->multimode)
-        tmp = ctx->dms_capabilities;
-    else {
+    if (!ctx->multimode) {
+        if (ctx->dms_capabilities != MM_MODEM_CAPABILITY_NONE)
+            tmp = ctx->dms_capabilities;
+        /* SSP logic to gather capabilities uses the Mode Preference TLV if available */
+        else if (ctx->nas_ssp_mode_preference_mask)
+            tmp = mm_modem_capability_from_qmi_rat_mode_preference (ctx->nas_ssp_mode_preference_mask);
+        /* If no value retrieved from SSP, check TP. We only process TP
+         * values if not 'auto' (0). */
+        else if (ctx->nas_tp_mask != QMI_NAS_RADIO_TECHNOLOGY_PREFERENCE_AUTO)
+            tmp = mm_modem_capability_from_qmi_radio_technology_preference (ctx->nas_tp_mask);
+    } else {
         /* We have a multimode CDMA/EVDO+GSM/UMTS device, check SSP and TP */
 
         /* SSP logic to gather capabilities uses the Mode Preference TLV if available */
diff --git a/src/mm-modem-helpers-qmi.h b/src/mm-modem-helpers-qmi.h
index e4eb9c7..8f3dc4f 100644
--- a/src/mm-modem-helpers-qmi.h
+++ b/src/mm-modem-helpers-qmi.h
@@ -11,7 +11,7 @@
  * GNU General Public License for more details:
  *
  * Copyright (C) 2012 Google, Inc.
- * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021 Qualcomm Innovation Center, Inc.
  */
 
 #ifndef MM_MODEM_HELPERS_QMI_H
diff --git a/src/mm-modem-helpers.c b/src/mm-modem-helpers.c
index a780b15..5e58ba9 100644
--- a/src/mm-modem-helpers.c
+++ b/src/mm-modem-helpers.c
@@ -13,7 +13,7 @@
  * Copyright (C) 2008 - 2009 Novell, Inc.
  * Copyright (C) 2009 - 2012 Red Hat, Inc.
  * Copyright (C) 2012 Google, Inc.
- * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021 Qualcomm Innovation Center, Inc.
  */
 
 #include <config.h>
@@ -1124,11 +1124,34 @@
     g_list_free_full (info_list, (GDestroyNotify) mm_3gpp_network_info_free);
 }
 
+static MMModem3gppNetworkAvailability
+get_mm_network_availability_from_3gpp_network_availability (guint    val,
+                                                            gpointer log_object)
+{
+    /* The network availability status in the MM API and in the 3GPP specs take the
+     * same numeric values, but we map them with an enum as it's much safer if new
+     * values are defined by 3GPP in the future. */
+    switch (val) {
+    case 1:
+        return MM_MODEM_3GPP_NETWORK_AVAILABILITY_AVAILABLE;
+    case 2:
+        return MM_MODEM_3GPP_NETWORK_AVAILABILITY_CURRENT;
+    case 3:
+        return MM_MODEM_3GPP_NETWORK_AVAILABILITY_FORBIDDEN;
+    default:
+        break;
+    }
+
+    mm_obj_warn (log_object, "unknown network availability value: %u", val);
+    return MM_MODEM_3GPP_NETWORK_AVAILABILITY_UNKNOWN;
+}
+
 static MMModemAccessTechnology
-get_mm_access_tech_from_etsi_access_tech (guint act)
+get_mm_access_tech_from_etsi_access_tech (guint    val,
+                                          gpointer log_object)
 {
     /* See ETSI TS 27.007 */
-    switch (act) {
+    switch (val) {
     case 0: /* GSM */
     case 8: /* EC-GSM-IoT (A/Gb mode) */
         return MM_MODEM_ACCESS_TECHNOLOGY_GSM;
@@ -1154,40 +1177,11 @@
     case 13: /* E-UTRA-NR dual connectivity */
         return (MM_MODEM_ACCESS_TECHNOLOGY_5GNR | MM_MODEM_ACCESS_TECHNOLOGY_LTE);
     default:
-        return MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN;
-    }
-}
-
-static MMModem3gppNetworkAvailability
-parse_network_status (const gchar *str,
-                      gpointer     log_object)
-{
-    /* Expecting a value between '0' and '3' inclusive */
-    if (!str ||
-        strlen (str) != 1 ||
-        str[0] < '0' ||
-        str[0] > '3') {
-        mm_obj_warn (log_object, "cannot parse network status value '%s'", str);
-        return MM_MODEM_3GPP_NETWORK_AVAILABILITY_UNKNOWN;
+        break;
     }
 
-    return (MMModem3gppNetworkAvailability) (str[0] - '0');
-}
-
-static MMModemAccessTechnology
-parse_access_tech (const gchar *str,
-                   gpointer     log_object)
-{
-    /* Recognized access technologies are between '0' and '7' inclusive... */
-    if (!str ||
-        strlen (str) != 1 ||
-        str[0] < '0' ||
-        str[0] > '7') {
-        mm_obj_warn (log_object, "cannot parse access technology value '%s'", str);
-        return MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN;
-    }
-
-    return get_mm_access_tech_from_etsi_access_tech (str[0] - '0');
+    mm_obj_warn (log_object, "unknown access technology value: %u", val);
+    return MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN;
 }
 
 GList *
@@ -1228,7 +1222,7 @@
      *       +COPS: (2,"","T-Mobile","31026",0),(1,"AT&T","AT&T","310410"),0)
      */
 
-    r = g_regex_new ("\\((\\d),\"([^\"\\)]*)\",([^,\\)]*),([^,\\)]*)[\\)]?,(\\d)\\)", G_REGEX_UNGREEDY, 0, NULL);
+    r = g_regex_new ("\\((\\d),\"([^\"\\)]*)\",([^,\\)]*),([^,\\)]*)[\\)]?,(\\d+)\\)", G_REGEX_UNGREEDY, 0, NULL);
     g_assert (r);
 
     /* If we didn't get any hits, try the pre-UMTS format match */
@@ -1260,14 +1254,14 @@
     /* Parse the results */
     while (g_match_info_matches (match_info)) {
         MM3gppNetworkInfo *info;
-        gchar *tmp;
-        gboolean valid = FALSE;
+        guint              net_value = 0;
+        gboolean           valid = FALSE;
 
         info = g_new0 (MM3gppNetworkInfo, 1);
 
-        tmp = mm_get_string_unquoted_from_match_info (match_info, 1);
-        info->status = parse_network_status (tmp, log_object);
-        g_free (tmp);
+        /* the regex makes sure this is a number, it won't fail */
+        mm_get_uint_from_match_info (match_info, 1, &net_value);
+        info->status = get_mm_network_availability_from_3gpp_network_availability (net_value, log_object);
 
         info->operator_long = mm_get_string_unquoted_from_match_info (match_info, 2);
         info->operator_short = mm_get_string_unquoted_from_match_info (match_info, 3);
@@ -1280,13 +1274,14 @@
 
         /* Only try for access technology with UMTS-format matches.
          * If none give, assume GSM */
-        tmp = (umts_format ?
-               mm_get_string_unquoted_from_match_info (match_info, 5) :
-               NULL);
-        info->access_tech = (tmp ?
-                             parse_access_tech (tmp, log_object) :
-                             MM_MODEM_ACCESS_TECHNOLOGY_GSM);
-        g_free (tmp);
+        if (umts_format) {
+            guint act_value = 0;
+
+            /* the regex makes sure this is a number, it won't fail */
+            mm_get_uint_from_match_info (match_info, 5, &act_value);
+            info->access_tech = get_mm_access_tech_from_etsi_access_tech (act_value, log_object);
+        } else
+            info->access_tech = MM_MODEM_ACCESS_TECHNOLOGY_GSM;
 
         /* If the operator number isn't valid (ie, at least 5 digits),
          * ignore the scan result; it's probably the parameter stuff at the
@@ -1294,6 +1289,8 @@
          * but there's no good way to ignore it.
          */
         if (info->operator_code && (strlen (info->operator_code) >= 5)) {
+            gchar *tmp;
+
             valid = TRUE;
             tmp = info->operator_code;
             while (*tmp) {
@@ -1337,6 +1334,7 @@
                                   guint                    *out_format,
                                   gchar                   **out_operator,
                                   MMModemAccessTechnology  *out_act,
+                                  gpointer                  log_object,
                                   GError                  **error)
 {
     GRegex *r;
@@ -1388,7 +1386,7 @@
             inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, "Error parsing AcT");
             goto out;
         }
-        act = get_mm_access_tech_from_etsi_access_tech (actval);
+        act = get_mm_access_tech_from_etsi_access_tech (actval, log_object);
     }
 
 out:
@@ -2127,7 +2125,9 @@
         /* Don't fill in lac/ci/act if the device's state is unknown */
         *out_lac = (gulong)lac;
         *out_ci  = (gulong)ci;
-        *out_act = (act >= 0 ? get_mm_access_tech_from_etsi_access_tech (act) : MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN);
+        *out_act = (act >= 0 ?
+                    get_mm_access_tech_from_etsi_access_tech (act, log_object) :
+                    MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN);
     }
     return TRUE;
 }
@@ -5112,15 +5112,17 @@
         mm_network_timezone_set_offset (*tzp, tz * 15);
     }
 
+    ret = TRUE;
+
     if (iso8601p) {
         /* Return ISO-8601 format date/time string */
         *iso8601p = mm_new_iso8601_time (year, month, day, hour,
                                          minute, second,
-                                         TRUE, (tz * 15));
+                                         TRUE, (tz * 15),
+                                         error);
+        ret = (*iso8601p != NULL);
     }
 
-    ret = TRUE;
-
  out:
     g_match_info_free (match_info);
     g_regex_unref (r);
diff --git a/src/mm-modem-helpers.h b/src/mm-modem-helpers.h
index 0d8b96a..c04d0e0 100644
--- a/src/mm-modem-helpers.h
+++ b/src/mm-modem-helpers.h
@@ -160,6 +160,7 @@
                                            guint                    *out_format,
                                            gchar                   **out_operator,
                                            MMModemAccessTechnology  *out_act,
+                                           gpointer                  log_object,
                                            GError                  **error);
 
 /* Logic to compare two APN names */
diff --git a/src/mm-netlink.c b/src/mm-netlink.c
index 5fd2ab3..c54942b 100644
--- a/src/mm-netlink.c
+++ b/src/mm-netlink.c
@@ -98,7 +98,7 @@
     memset ((char *) msg->data + old_len, 0, msg->len - old_len);
 
     new_attr.rta_type = type;
-    new_attr.rta_len = attr_len;
+    new_attr.rta_len = RTA_LENGTH (len);
     next_attr_abs_pos = (char *) msg->data + next_attr_rel_pos;
     memcpy (next_attr_abs_pos, &new_attr, sizeof (struct rtattr));
 
@@ -136,8 +136,6 @@
     hdr->msghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
     hdr->ifreq.ifi_family = AF_UNSPEC;
     hdr->ifreq.ifi_index = ifindex;
-    hdr->ifreq.ifi_flags = 0;
-    hdr->ifreq.ifi_change = 0xFFFFFFFF;
 
     return msg;
 }
@@ -154,7 +152,7 @@
     hdr = netlink_message_header (msg);
 
     hdr->ifreq.ifi_flags = up ? IFF_UP : 0;
-    hdr->ifreq.ifi_change = 0xFFFFFFFF;
+    hdr->ifreq.ifi_change = IFF_UP;
 
     if (mtu)
         append_netlink_attribute_uint32 (msg, IFLA_MTU, mtu);
diff --git a/src/mm-port-serial.c b/src/mm-port-serial.c
index 8b6e34e..3dedcab 100644
--- a/src/mm-port-serial.c
+++ b/src/mm-port-serial.c
@@ -1017,13 +1017,6 @@
         serial_debug (self, "<--", buf, bytes_read);
         g_byte_array_append (self->priv->response, (const guint8 *) buf, bytes_read);
 
-        /* Make sure the response doesn't grow too long */
-        if ((self->priv->response->len > SERIAL_BUF_SIZE) && self->priv->spew_control) {
-            /* Notify listeners and then trim the buffer */
-            g_signal_emit (self, signals[BUFFER_FULL], 0, self->priv->response);
-            g_byte_array_remove_range (self->priv->response, 0, (SERIAL_BUF_SIZE / 2));
-        }
-
         /* See if we can parse anything. The response parsing may actually
          * schedule the completion of a serial command, and that in turn may end
          * up fully disposing this serial port object. In order to cope with
@@ -1032,6 +1025,13 @@
          * we should be keeping this socket/iochannel source or not. */
         g_object_ref (self);
         {
+            /* Make sure the response doesn't grow too long */
+            if ((self->priv->response->len > SERIAL_BUF_SIZE) && self->priv->spew_control) {
+                /* Notify listeners and then trim the buffer */
+                g_signal_emit (self, signals[BUFFER_FULL], 0, self->priv->response);
+                g_byte_array_remove_range (self->priv->response, 0, (SERIAL_BUF_SIZE / 2));
+            }
+
             parse_response_buffer (self);
 
             /* If we didn't end up closing the iochannel/socket in the previous
diff --git a/src/mm-shared-qmi.c b/src/mm-shared-qmi.c
index 0326735..162c622 100644
--- a/src/mm-shared-qmi.c
+++ b/src/mm-shared-qmi.c
@@ -11,7 +11,7 @@
  * GNU General Public License for more details:
  *
  * Copyright (C) 2018 Aleksander Morgado <aleksander@aleksander.es>
- * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc.
  */
 
 #include <config.h>
@@ -3795,6 +3795,11 @@
     priv = get_private (self);
     mm_obj_dbg (self, "received slot status indication");
 
+    if (!priv->slots_status) {
+        mm_obj_dbg (self, "initial slot status is not loaded yet");
+        return;
+    }
+
     if (!qmi_indication_uim_slot_status_output_get_physical_slot_status (output,
                                                                          &new_slots_status,
                                                                          &error)) {
diff --git a/src/mm-sim-mbim.c b/src/mm-sim-mbim.c
index b0addb2..1500fd2 100644
--- a/src/mm-sim-mbim.c
+++ b/src/mm-sim-mbim.c
@@ -1186,14 +1186,16 @@
 
 MMBaseSim *
 mm_sim_mbim_new_initialized (MMBaseModem *modem,
-                             guint        slot_number,
-                             gboolean     active,
-                             const gchar *sim_identifier,
-                             const gchar *imsi,
-                             const gchar *eid,
-                             const gchar *operator_identifier,
-                             const gchar *operator_name,
-                             const GStrv  emergency_numbers)
+                             guint            slot_number,
+                             gboolean         active,
+                             MMSimType        sim_type,
+                             MMSimEsimStatus  esim_status,
+                             const gchar     *sim_identifier,
+                             const gchar     *imsi,
+                             const gchar     *eid,
+                             const gchar     *operator_identifier,
+                             const gchar     *operator_name,
+                             const GStrv      emergency_numbers)
 {
     MMBaseSim *sim;
 
@@ -1201,6 +1203,8 @@
                                      MM_BASE_SIM_MODEM,       modem,
                                      MM_BASE_SIM_SLOT_NUMBER, slot_number,
                                      "active",                active,
+                                     "sim-type",              sim_type,
+                                     "esim-status",           esim_status,
                                      "sim-identifier",        sim_identifier,
                                      "eid",                   eid,
                                      "operator-identifier",   operator_identifier,
diff --git a/src/mm-sim-mbim.h b/src/mm-sim-mbim.h
index 49aa556..709c3d6 100644
--- a/src/mm-sim-mbim.h
+++ b/src/mm-sim-mbim.h
@@ -51,12 +51,14 @@
 MMBaseSim *mm_sim_mbim_new_finish (GAsyncResult  *res,
                                    GError       **error);
 MMBaseSim *mm_sim_mbim_new_initialized (MMBaseModem *modem,
-                                        guint        slot_number,
-                                        gboolean     active,
-                                        const gchar *sim_identifier,
-                                        const gchar *imsi,
-                                        const gchar *eid,
-                                        const gchar *operator_identifier,
-                                        const gchar *operator_name,
-                                        const GStrv  emergency_numbers);
+                                        guint            slot_number,
+                                        gboolean         active,
+                                        MMSimType        sim_type,
+                                        MMSimEsimStatus  esim_status,
+                                        const gchar     *sim_identifier,
+                                        const gchar     *imsi,
+                                        const gchar     *eid,
+                                        const gchar     *operator_identifier,
+                                        const gchar     *operator_name,
+                                        const GStrv      emergency_numbers);
 #endif /* MM_SIM_MBIM_H */
diff --git a/src/mm-sms-part-3gpp.c b/src/mm-sms-part-3gpp.c
index bfae03a..aeb9dec 100644
--- a/src/mm-sms-part-3gpp.c
+++ b/src/mm-sms-part-3gpp.c
@@ -161,7 +161,8 @@
 }
 
 static gchar *
-sms_decode_timestamp (const guint8 *timestamp)
+sms_decode_timestamp (const guint8 *timestamp,
+                      GError **error)
 {
     /* ISO8601 format: YYYY-MM-DDTHH:MM:SS+HHMM */
     guint year, month, day, hour, minute, second;
@@ -179,7 +180,7 @@
         offset_minutes = -1 * offset_minutes;
 
     return mm_new_iso8601_time (year, month, day, hour,
-                                minute, second, TRUE, offset_minutes);
+                                minute, second, TRUE, offset_minutes, error);
 }
 
 static MMSmsEncoding
@@ -509,6 +510,7 @@
     /* Get timestamps and indexes for TP-PID, TP-DCS and TP-UDL/TP-UD */
 
     if (pdu_type == SMS_TP_MTI_SMS_DELIVER) {
+        gchar *str = NULL;
         PDU_SIZE_CHECK (offset + 9,
                         "cannot read PID/DCS/Timestamp"); /* 1+1+7=9 */
 
@@ -519,8 +521,13 @@
         tp_dcs_offset = offset++;
 
         /* ------ Timestamp (7 bytes) ------ */
+        str = sms_decode_timestamp (&pdu[offset], error);
+        if (!str) {
+            mm_sms_part_free (sms_part);
+            return NULL;
+        }
         mm_sms_part_take_timestamp (sms_part,
-                                    sms_decode_timestamp (&pdu[offset]));
+                                    str);
         offset += 7;
 
         tp_user_data_len_offset = offset;
@@ -564,6 +571,7 @@
         tp_user_data_len_offset = offset;
     }
     else if (pdu_type == SMS_TP_MTI_SMS_STATUS_REPORT) {
+        gchar *str = NULL;
         /* We have 2 timestamps in status report PDUs:
          *  first, the timestamp for when the PDU was received in the SMSC
          *  second, the timestamp for when the PDU was forwarded by the SMSC
@@ -571,13 +579,21 @@
         PDU_SIZE_CHECK (offset + 15, "cannot read Timestamps/TP-STATUS"); /* 7+7+1=15 */
 
         /* ------ Timestamp (7 bytes) ------ */
-        mm_sms_part_take_timestamp (sms_part,
-                                    sms_decode_timestamp (&pdu[offset]));
+        str = sms_decode_timestamp (&pdu[offset], error);
+        if (!str) {
+            mm_sms_part_free (sms_part);
+            return NULL;
+        }
+        mm_sms_part_take_timestamp (sms_part, str);
         offset += 7;
 
         /* ------ Discharge Timestamp (7 bytes) ------ */
-        mm_sms_part_take_discharge_timestamp (sms_part,
-                                              sms_decode_timestamp (&pdu[offset]));
+        str = sms_decode_timestamp (&pdu[offset], error);
+        if (!str) {
+            mm_sms_part_free (sms_part);
+            return NULL;
+        }
+        mm_sms_part_take_discharge_timestamp (sms_part, str);
         offset += 7;
 
         /* ----- TP-STATUS (1 byte) ------ */
diff --git a/src/tests/test-modem-helpers-qmi.c b/src/tests/test-modem-helpers-qmi.c
index f49e1e8..544e6bf 100644
--- a/src/tests/test-modem-helpers-qmi.c
+++ b/src/tests/test-modem-helpers-qmi.c
@@ -11,7 +11,7 @@
  * GNU General Public License for more details:
  *
  * Copyright (C) 2012 Lanedo GmbH.
- * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc.
  */
 
 #include <glib.h>
diff --git a/src/tests/test-modem-helpers.c b/src/tests/test-modem-helpers.c
index 1f040e8..489cbd4 100644
--- a/src/tests/test-modem-helpers.c
+++ b/src/tests/test-modem-helpers.c
@@ -912,6 +912,28 @@
 }
 
 static void
+test_cops_response_em9191 (void *f, gpointer d)
+{
+    const char *reply =
+        "+COPS: "
+        "(1,\"Telekom.de\",\"TDG\",\"26201\",12),"
+        "(1,\"Telekom.de\",\"Telekom.\",\"26201\",7),"
+        "(1,\"o2 - de\",\"o2 - de\",\"26203\",7),"
+        "(1,\"vodafone.de\",\"Vodafone\",\"26202\",7)"
+        /* these next ones will be ignored */
+        "(0,1,2,3,4),"
+        "(0,1,2)";
+    static MM3gppNetworkInfo expected[] = {
+        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_AVAILABLE, (gchar *) "Telekom.de",  (gchar *) "TDG",      (gchar *) "26201", MM_MODEM_ACCESS_TECHNOLOGY_5GNR },
+        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_AVAILABLE, (gchar *) "Telekom.de",  (gchar *) "Telekom.", (gchar *) "26201", MM_MODEM_ACCESS_TECHNOLOGY_LTE  },
+        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_AVAILABLE, (gchar *) "o2 - de",     (gchar *) "o2 - de",  (gchar *) "26203", MM_MODEM_ACCESS_TECHNOLOGY_LTE  },
+        { MM_MODEM_3GPP_NETWORK_AVAILABILITY_AVAILABLE, (gchar *) "vodafone.de", (gchar *) "Vodafone", (gchar *) "26202", MM_MODEM_ACCESS_TECHNOLOGY_LTE  },
+    };
+
+    test_cops_results ("EM9191", reply, MM_MODEM_CHARSET_GSM, &expected[0], G_N_ELEMENTS (expected));
+}
+
+static void
 test_cops_response_gsm_invalid (void *f, gpointer d)
 {
     const gchar *reply = "+COPS: (0,1,2,3),(1,2,3,4)";
@@ -961,6 +983,7 @@
                                                &format,
                                                &operator,
                                                &act,
+                                               NULL,
                                                &error);
     g_assert_no_error (error);
     g_assert (result);
@@ -4659,6 +4682,7 @@
     g_test_suite_add (suite, TESTCASE (test_cops_response_sek600i, NULL));
     g_test_suite_add (suite, TESTCASE (test_cops_response_samsung_z810, NULL));
     g_test_suite_add (suite, TESTCASE (test_cops_response_ublox_lara, NULL));
+    g_test_suite_add (suite, TESTCASE (test_cops_response_em9191, NULL));
 
     g_test_suite_add (suite, TESTCASE (test_cops_response_gsm_invalid, NULL));
     g_test_suite_add (suite, TESTCASE (test_cops_response_umts_invalid, NULL));