1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-20 02:18:59 +00:00
Files
supervisor/supervisor/utils/dbus.py
Stefan Agner 7a6663ba80 Use Python dbus-next D-Bus library (#3234)
* Use the correct interface name to get properties of systemd

It seems that gdbus (or systemd) automatically pick the correct
interface and return the properties. However, dbussy requires the
correct interface name to get all properties.

* Don't expect array from Strength property

The property returns a type "y" which equates to "guchar":
https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.AccessPoint.html#gdbus-property-org-freedesktop-NetworkManager-AccessPoint.Strength

It seems that the old D-Bus implementation returned an array. With
dbus-next a integer is returned, so no list indexing required.

* Support signals and remove no longer used tests and code

* Pass rauc update file path as string

That is what the interface is expecting, otherwise the new lib chocks on
the Pathlib type.

* Support Network configuration with dbus-next

Assemble Python native objects and pass them to dbus-next. Use dbus-next
specific Variant class where necessary.

* Use org.freedesktop.NetworkManager.Connection.Active.StateChanged

org.freedesktop.NetworkManager.Connection.Active.PropertyChanged is
depricated. Also it seems that StateChanged leads to fewer and more
accurate signals.

* Pass correct data type to RequestScan.

RequestScan expects an option dictionary. Pass an empty option
dictionary to it.

* Update unit tests

Replace gdbus specific fixtures with json files representing the return
values. Those can be easily converted into native Python objects.

* Rename D-Bus utils module gdbus to dbus
2021-10-18 23:06:44 +02:00

250 lines
8.0 KiB
Python

"""DBus implementation with glib."""
from __future__ import annotations
import asyncio
import logging
from typing import Any
from dbus_next import BusType, InvalidIntrospectionError, Message, MessageType
from dbus_next.aio import MessageBus
from dbus_next.signature import Variant
from ..exceptions import (
DBusFatalError,
DBusInterfaceError,
DBusInterfaceMethodError,
DBusNotConnectedError,
DBusParseError,
)
def _remove_dbus_signature(data: Any) -> Any:
if isinstance(data, Variant):
return _remove_dbus_signature(data.value)
elif isinstance(data, dict):
for k in data:
data[k] = _remove_dbus_signature(data[k])
return data
elif isinstance(data, list):
new_list = []
for item in data:
new_list.append(_remove_dbus_signature(item))
return new_list
else:
return data
_LOGGER: logging.Logger = logging.getLogger(__name__)
DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll"
DBUS_METHOD_SET: str = "org.freedesktop.DBus.Properties.Set"
class DBus:
"""DBus handler."""
def __init__(self, bus_name: str, object_path: str) -> None:
"""Initialize dbus object."""
self.bus_name: str = bus_name
self.object_path: str = object_path
self.methods: set[str] = set()
self.signals: set[str] = set()
self._bus: MessageBus = None
@staticmethod
async def connect(bus_name: str, object_path: str) -> DBus:
"""Read object data."""
self = DBus(bus_name, object_path)
# pylint: disable=protected-access
await self._init_proxy()
_LOGGER.debug("Connect to D-Bus: %s - %s", bus_name, object_path)
return self
def _add_interfaces(self, introspection: Any):
# Read available methods
for interface in introspection.interfaces:
interface_name = interface.name
# Methods
for method in interface.methods:
method_name = method.name
self.methods.add(f"{interface_name}.{method_name}")
# Signals
for signal in interface.signals:
signal_name = signal.name
self.signals.add(f"{interface_name}.{signal_name}")
async def _init_proxy(self) -> None:
"""Read interface data."""
# Wait for dbus object to be available after restart
try:
self._bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
except Exception as err:
raise DBusFatalError() from err
try:
introspection = await self._bus.introspect(
self.bus_name, self.object_path, timeout=10
)
except InvalidIntrospectionError as err:
_LOGGER.error("Can't parse introspect data: %s", err)
raise DBusParseError() from err
self._add_interfaces(introspection)
def _prepare_args(self, *args: list[Any]) -> tuple[str, list[Any]]:
signature = ""
arg_list = []
for arg in args:
_LOGGER.debug("...arg %s (type %s)", str(arg), type(arg))
if isinstance(arg, bool):
signature += "b"
arg_list.append(arg)
elif isinstance(arg, int):
signature += "i"
arg_list.append(arg)
elif isinstance(arg, float):
signature += "d"
arg_list.append(arg)
elif isinstance(arg, str):
signature += "s"
arg_list.append(arg)
elif isinstance(arg, tuple):
signature += arg[0]
arg_list.append(arg[1])
else:
raise DBusFatalError(f"Type {type(arg)} not supported")
return signature, arg_list
async def call_dbus(self, method: str, *args: list[Any]) -> str:
"""Call a dbus method."""
method_parts = method.split(".")
signature, arg_list = self._prepare_args(*args)
_LOGGER.debug("Call %s on %s", method, self.object_path)
reply = await self._bus.call(
Message(
destination=self.bus_name,
path=self.object_path,
interface=".".join(method_parts[:-1]),
member=method_parts[-1],
signature=signature,
body=arg_list,
)
)
if reply.message_type == MessageType.ERROR:
if reply.error_name in (
"org.freedesktop.DBus.Error.ServiceUnknown",
"org.freedesktop.DBus.Error.UnknownMethod",
):
raise DBusInterfaceError(reply.body[0])
if reply.error_name == "org.freedesktop.DBus.Error.Disconnected":
raise DBusNotConnectedError()
if reply.body and len(reply.body) > 0:
raise DBusFatalError(reply.body[0])
raise DBusFatalError()
return _remove_dbus_signature(reply.body)
async def get_properties(self, interface: str) -> dict[str, Any]:
"""Read all properties from interface."""
try:
return (await self.call_dbus(DBUS_METHOD_GETALL, interface))[0]
except IndexError as err:
_LOGGER.error("No attributes returned for %s", interface)
raise DBusFatalError() from err
async def set_property(
self, interface: str, name: str, value: Any
) -> dict[str, Any]:
"""Set a property from interface."""
try:
return (await self.call_dbus(DBUS_METHOD_SET, interface, name, value))[0]
except IndexError as err:
_LOGGER.error("No Set attribute %s for %s", name, interface)
raise DBusFatalError() from err
async def wait_signal(self, signal):
"""Wait for single event."""
signal_parts = signal.split(".")
interface = ".".join(signal_parts[:-1])
member = signal_parts[-1]
_LOGGER.debug("Wait for signal %s", signal)
await self._bus.call(
Message(
destination="org.freedesktop.DBus",
interface="org.freedesktop.DBus",
path="/org/freedesktop/DBus",
member="AddMatch",
signature="s",
body=[f"type='signal',interface={interface},member={member}"],
)
)
loop = asyncio.get_event_loop()
future = loop.create_future()
def message_handler(msg: Message):
if msg.message_type != MessageType.SIGNAL:
return
_LOGGER.debug(
"Signal message received %s, %s %s", msg, msg.interface, msg.member
)
if msg.interface != interface or msg.member != member:
return
# Avoid race condition: We already received signal but handler not yet removed.
if future.done():
return
future.set_result(_remove_dbus_signature(msg.body))
self._bus.add_message_handler(message_handler)
result = await future
self._bus.remove_message_handler(message_handler)
return result
def __getattr__(self, name: str) -> DBusCallWrapper:
"""Map to dbus method."""
return getattr(DBusCallWrapper(self, self.bus_name), name)
class DBusCallWrapper:
"""Wrapper a DBus interface for a call."""
def __init__(self, dbus: DBus, interface: str) -> None:
"""Initialize wrapper."""
self.dbus: DBus = dbus
self.interface: str = interface
def __call__(self) -> None:
"""Catch this method from being called."""
_LOGGER.error("D-Bus method %s not exists!", self.interface)
raise DBusInterfaceMethodError()
def __getattr__(self, name: str):
"""Map to dbus method."""
interface = f"{self.interface}.{name}"
if interface not in self.dbus.methods:
return DBusCallWrapper(self.dbus, interface)
def _method_wrapper(*args):
"""Wrap method.
Return a coroutine
"""
return self.dbus.call_dbus(interface, *args)
return _method_wrapper