2025, Oct 31 08:16
Как правильно передавать a{sa{sv}} в NetworkManager по D‑Bus на Python
Почему AddAndActivateConnection в NetworkManager по D‑Bus требует a{sa{sv}}, как обернуть значения в GLib.Variant в Python и задать SSID как ay для Wi‑Fi
Работать с NetworkManager через D‑Bus довольно просто, пока не упираешься в сигнатуры типов. Один из частых камней преткновения — AddAndActivateConnection и его первый параметр с типом a{sa{sv}}. Если передать обычный Python‑словарь со строками внутри, GLib выдаст TypeError: ожидался GLib.Variant, а пришёл str. Ниже — что на самом деле происходит и как это исправить без гаданий.
Как воспроизвести проблему
Код ниже находит Wi‑Fi‑устройство, собирает профиль подключения и пытается активировать его. Структура выглядит корректной с точки зрения Python, но из‑за правил типизации D‑Bus вызов падает.
from pydbus import SystemBus
sys_bus = SystemBus()
nmgr = sys_bus.get("org.freedesktop.NetworkManager")
# Найти устройство Wi‑Fi (тип 2)
wlan_objpath = None
for obj_path in nmgr.GetDevices():
    dev_obj = sys_bus.get("org.freedesktop.NetworkManager", obj_path)
    if dev_obj.DeviceType == 2:
        wlan_objpath = obj_path
        break
if not wlan_objpath:
    raise RuntimeError("No Wi-Fi device found.")
net_ssid = "MyWiFi"
net_pass = "mypassword123"
raw_settings = {
    'connection': {
        'id': net_ssid,
        'type': '802-11-wireless',
    },
    '802-11-wireless': {
        'ssid': net_ssid,
        'mode': 'infrastructure',
    },
    '802-11-wireless-security': {
        'key-mgmt': 'wpa-psk',
        'psk': net_pass,
    },
    'ipv4': {
        'method': 'auto',
    },
    'ipv6': {
        'method': 'ignore',
    }
}
nmgr.AddAndActivateConnection(raw_settings, wlan_objpath, "/")
Во время выполнения это приводит к ошибке GLib. Ключевая строчка обычно выглядит так:
TypeError: argument value: Expected GLib.Variant, but got str
Почему это не работает
Сигнатура a{sa{sv}} имеет значение. Она описывает отображение от строк к другому отображению, значения которого — варианты. Внешний dict — это словарь групп настроек. Каждый внутренний dict — это словарь ключей, и значения во внутренних словарях должны быть типа variant. У Python‑словаря нет сведений о типе, поэтому каждое значение нужно явно оборачивать в GLib.Variant, чтобы удовлетворить сигнатуре D‑Bus. Даже если все значения — строки, простой a{sa{ss}} — это не то же самое, что a{sa{sv}}, и вызов будет отклонён, когда pydbus соберёт дерево GLib.Variant.
Хочется обернуть весь словарь настроек в один Variant, но это не решает несоответствие. Так вы получите v на верхнем уровне вместо a{sa{sv}}, а внутренние значения всё равно не станут отдельными вариантами со своими типами. Более того, даже внутри варианта содержащаяся структура должна оставаться представимой типами D‑Bus, так что такой «шорткат» не помогает.
Есть ещё одна деталь, которая часто подводит: поле 802-11-wireless.ssid должно иметь тип ay (массив байт), а не строка. Передача строкового варианта для ssid приведёт к новой ошибке. Правильное представление — bytearray, обёрнутый в Variant('ay', ...).
Решение
Оборачивайте каждое внутреннее значение в GLib.Variant с корректным типом. Для строковых полей — 's'. Для SSID — 'ay' с bytearray из UTF‑8‑байтов. Внешние и внутренние словари остаются обычными Python‑dict, но все листовые значения должны быть вариантами.
from pydbus import SystemBus
from gi.repository.GLib import Variant
sys_bus = SystemBus()
nmgr = sys_bus.get("org.freedesktop.NetworkManager")
# Найти устройство Wi‑Fi (тип 2)
wlan_objpath = None
for obj_path in nmgr.GetDevices():
    dev_obj = sys_bus.get("org.freedesktop.NetworkManager", obj_path)
    if dev_obj.DeviceType == 2:
        wlan_objpath = obj_path
        break
if not wlan_objpath:
    raise RuntimeError("No Wi-Fi device found.")
net_ssid = "MyWiFi"
net_pass = "mypassword123"
fixed_settings = {
    "connection": {
        "id": Variant('s', net_ssid),
        "type": Variant('s', "802-11-wireless"),
    },
    "802-11-wireless": {
        "ssid": Variant('ay', bytearray(net_ssid, 'utf-8')),
        "mode": Variant('s', "infrastructure"),
    },
    "802-11-wireless-security": {
        "key-mgmt": Variant('s', "wpa-psk"),
        "psk": Variant('s', net_pass),
    },
    "ipv4": {"method": Variant('s', "auto")},
    "ipv6": {"method": Variant('s', "ignore")},
}
nmgr.AddAndActivateConnection(fixed_settings, wlan_objpath, "/")
Что происходит под капотом
Словари, передаваемые по D‑Bus, строго типизированы. Поэтому API часто объявляют карты опций с вариантными значениями: варианты несут реальный тип, сохраняя при этом единообразную внешнюю структуру. GLib.Variant близко соответствует D‑Bus‑варианту, используются те же строковые сигнатуры. В этом контексте pydbus строит дерево GLib.Variant на основе сигнатуры из интроспекции и поэтому настаивает, чтобы внутренние значения уже были вариантами. Некоторые вызовы, которые читают a{sv}, могут вернуть обычный Python‑словарь, но создание a{sv} для отправки строже и требует явной обёртки. Если вы получили настройки геттером, затем изменили поле и отправляете обратно, перед обновляющим вызовом нужно заново обернуть изменённые листья в Variant — иначе получите ту же ошибку типов.
Зачем это понимать
Как только вы начинаете программно составлять настройки NetworkManager, несоответствие типов становится самым быстрым способом потерять время. Понимание того, что a{sa{sv}} означает «внутренние значения должны быть вариантами», позволяет сразу формировать данные правильно. Это также предотвращает тонкие баги при смешанном чтении и записи: геттер может вернуть обычную Python‑структуру, а сеттер для тех же полей ожидает типизированные варианты.
Выводы
Всегда опирайтесь на сигнатуру D‑Bus из интроспекции. Для AddAndActivateConnection внешние и внутренние отображения — обычные dict, но каждое реальное значение внутри внутренних словарей должно быть GLib.Variant подходящего типа. Не оборачивайте всю карту настроек в один Variant — это не соответствует a{sa{sv}}. Убедитесь, что 802-11-wireless.ssid — это Variant('ay', bytearray(...)), а не строка. И когда меняете настройки, полученные из другого вызова, перед отправкой заново применяйте обёртки Variant к изменённым полям.