8000 feat: add UPower Service by linkfrg · Pull Request #10 · linkfrg/ignis · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add UPower Service #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/api/services/upower.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
UPower
========

.. autoclass:: ignis.services.upower.UPowerService
:members:

.. autoclass:: ignis.services.upower.UPowerDevice
:members:
58 changes: 50 additions & 8 deletions ignis/dbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from ignis.utils import Utils
from ignis.gobject import IgnisGObject
from ignis.exceptions import DBusMethodNotFoundError, DBusPropertyNotFoundError
from typing import Literal

BUS_TYPE = {"session": Gio.BusType.SESSION, "system": Gio.BusType.SYSTEM}


class DBusService(IgnisGObject):
Expand Down Expand Up @@ -219,30 +222,40 @@ class DBusProxy(IgnisGObject):
- **object_path** (``str``, required, read-only): An object path.
- **interface_name** (``str``, required, read-only): A D-Bus interface name.
- **info** (`Gio.DBusInterfaceInfo <https://lazka.github.io/pgi-docs/Gio-2.0/classes/DBusInterfaceInfo.html>`_, required, read-only): A ``Gio.DBusInterfaceInfo`` instance. You can get it from XML using :class:`~ignis.utils.Utils.load_interface_xml`.
- **bus_type** (``Literal["session", "system"]``): The type of the bus. Default: ``"session"``.
- **proxy** (`Gio.DBusProxy <https://lazka.github.io/pgi-docs/index.html#Gio-2.0/classes/DBusProxy.html>`_, not argument, read-only): The ``Gio.DBusProxy`` instance.
- **methods** (``list[str]``, not argument, read-only): A list of methods exposed by D-Bus service.
- **properties** (``list[str]``, not argument, read-only): A list of properties exposed by D-Bus service.
- **has_owner** (``bool``, not argument, read-only): Whether the ``name`` has an owner.

To call a D-Bus method, use the standart pythonic way.
The first argument always needs to be the DBus signature tuple of the method call.
Subsequent arguments must match the provided D-Bus signature.
If the D-Bus method does not accept any arguments, do not pass arguments.
Next arguments must match the provided D-Bus signature.
If the D-Bus method does not accept any arguments, do not pass them.

.. code-block:: python

from ignis.dbus import DBusProxy
proxy = DBusProxy(...)
result = proxy.MyMethod("(is)", 42, "hello")
print(result)

To get a D-Bus property:

.. code-block:: python

from ignis.dbus import DBusProxy
proxy = DBusProxy(...)
proxy.MyMethod("(is)", 42, "hello")
print(proxy.MyValue)

To get a D-Bus property, also use the standart pythonic way.
To set a D-Bus property:

.. code-block:: python

from ignis.dbus import DBusProxy
proxy = DBusProxy(...)
value = proxy.MyValue
print(value)
# pass GLib.Variant as new property value
proxy.MyValue = GLib.Variant("s", "Hello world!")
"""

def __init__(
Expand All @@ -251,18 +264,20 @@ def __init__(
object_path: str,
interface_name: str,
info: Gio.DBusInterfaceInfo,
bus_type: Literal["session", "system"] = "session",
):
super().__init__()
self._name = name
self._object_path = object_path
self._interface_name = interface_name
self._info = info
self._bus_type = bus_type

self._methods: list[str] = []
self._properties: list[str] = []

self._proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
BUS_TYPE[bus_type],
Gio.DBusProxyFlags.NONE,
info,
name,
Expand Down Expand Up @@ -293,6 +308,10 @@ def interface_name(self) -> str:
def info(self) -> Gio.DBusInterfaceInfo:
return self._info

@GObject.Property
def bus_type(self) -> Literal["session", "system"]:
return self._bus_type

@GObject.Property
def connection(self) -> Gio.DBusConnection:
return self._proxy.get_connection()
Expand All @@ -316,6 +335,7 @@ def has_owner(self) -> bool:
object_path="/org/freedesktop/DBus",
interface_name="org.freedesktop.DBus",
info=Utils.load_interface_xml("org.freedesktop.DBus"),
bus_type=self._bus_type,
)
return dbus.NameHasOwner("(s)", self.name)

Expand All @@ -327,6 +347,12 @@ def __getattr__(self, name: str) -> Any:
else:
return super().__getattribute__(name)

def __setattr__(self, name: str, value: Any) -> None:
if name in self.__dict__.get("_properties", {}): # avoid recursion
self.__set_dbus_property(name, value)
else:
return super().__setattr__(name, value)

def signal_subscribe(
self,
signal_name: str,
Expand Down Expand Up @@ -379,6 +405,22 @@ def __get_dbus_property(self, property_name: str) -> Any:
except GLib.GError: # type: ignore
return None

def __set_dbus_property(self, property_name: str, value: GLib.Variant) -> None:
self.connection.call_sync(
self.name,
self.object_path,
"org.freedesktop.DBus.Properties",
"Set",
GLib.Variant(
"(ssv)",
(self.interface_name, property_name, value),
),
None,
Gio.DBusCallFlags.NONE,
-1,
None,
)

def watch_name(
self,
on_name_appeared: Callable | None = None,
Expand All @@ -392,7 +434,7 @@ def watch_name(
on_name_vanished (``Callable``, optional): A function to call when ``name`` vanished.
"""
self._watcher = Gio.bus_watch_name(
Gio.BusType.SESSION,
BUS_TYPE[self._bus_type],
self.name,
Gio.BusNameWatcherFlags.NONE,
on_name_appeared,
Expand Down
12 changes: 12 additions & 0 deletions ignis/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,15 @@ def __init__(self, name: str, *args: object) -> None:
@property
def name(self) -> str:
return self._name


class UPowerNotFoundError(Exception):
"""
Raised when UPower is not found.
"""

def __init__(self, *args: object) -> None:
super().__init__(
"UPower is not found! To use the battery service, install UPower and UPowerGLib",
*args,
)
4 changes: 4 additions & 0 deletions ignis/services/upower/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .service import UPowerService
from .device import UPowerDevice

__all__ = ["UPowerService", "UPowerDevice"]
12 changes: 12 additions & 0 deletions ignis/services/upower/_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import gi
import sys
from ignis.exceptions import UPowerNotFoundError

try:
if "sphinx" not in sys.modules:
gi.require_version("UPowerGlib", "1.0")
from gi.repository import UPowerGlib # type: ignore
except (ImportError, ValueError):
raise UPowerNotFoundError() from None

__all__ = ["UPowerGlib"]
162 changes: 162 additions & 0 deletions ignis/services/upower/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from ignis.gobject import IgnisGObject
from ._imports import UPowerGlib
from gi.repository import GObject # type: ignore


class UPowerDevice(IgnisGObject):
"""
The general class for power devices, including batteries.

Signals:
- **removed** (): Emitted when the device has been removed.

Properties:
- **device** (``UPowerGlib.Device``, read-only): The instance of ``UPowerGlib.Device``.
- **native_path** (``str``, read-only): The native path of the device.
- **kind** (``str``, read-only): The device kind, e.g. ``"battery"``.
- **available** (``bool`` read-only): Whether the device is available.
- **percent** (``float`` read-only): The current percentage of the device.
- **charging** (``bool`` read-only): Whether the device is currently charging.
- **charged** (``bool`` read-only): Whether the device is charged.
- **icon_name** (``str`` read-only): The current icon name.
- **time_remaining** (``int`` read-only): Time in seconds until fully charged (when charging) or until fully drains (when discharging).
- **energy** (``float`` read-only): The energy left in the device. Measured in mWh.
- **energy_full** (``float``, read-only): The amount of energy when the device is fully charged. Measured in mWh.
- **energy_full_design** (``float``, read-only): The amount of energy when the device was brand new. Measured in mWh.
- **energy_rate** (``float``, read-only): The rate of discharge or charge. Measured in mW.
- **charge_cycles** (``int``, read-only): The number of charge cycles for the device, or -1 if unknown or non-applicable.
- **vendor** (``str``, read-only): The vendor of the device.
- **model** (``str``, read-only): The model of the device.
- **serial** (``str``, read-only): The serial number of the device.
- **power_supply** (``bool``, read-only): Whether the device is powering the system.
- **technology** (``str``, read-only): The device technology e.g. ``"lithium-ion"``.
- **temperature** (``float``, read-only): The temperature of the device in degrees Celsius.
- **voltage** (``float``, read-only): The current voltage of the device.
"""

__gsignals__ = {
"removed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
}

def __init__(self, device: UPowerGlib.Device):
super().__init__()

self._device = device

self._charge_threshold: bool = False
self._charge_threshold_supported: bool = False
self._charge_start_threshold: int = 0
self._charge_end_threshold: int = 0

for prop_name in [
"icon-name",
"energy",
"energy-full",
"energy-rate",
"charge-cycles",
"power-supply",
"voltage",
"temperature",
]:
self.__conn_dev_notif(prop_name, prop_name)

self.__conn_dev_notif("is-present", "available")
self.__conn_dev_notif("percentage", "percent")
self.__conn_dev_notif("state", "charging", "charged")

self.__conn_dev_notif("time-to-full", "time-remaining")
self.__conn_dev_notif("state", "time-remaining")
self.__conn_dev_notif("time-to-empty", "time-remaining")

def __conn_dev_notif(self, dev_prop: str, *self_props: str) -> None:
self._device.connect(
f"notify::{dev_prop}", lambda x, y: (self.notify(i) for i in self_props)
)

@GObject.Property
def device(self) -> UPowerGlib.Device:
return self._device

@GObject.Property
def native_path(self) -> str:
return self._device.props.native_path

@GObject.Property
def kind(self) -> str:
return UPowerGlib.Device.kind_to_string(self._device.props.kind) # type: ignore

@GObject.Property
def available(self) -> bool:
return self._device.props.is_present

@GObject.Property
def percent(self) -> float:
return self._device.props.percentage

@GObject.Property
def charging(self) -> bool:
return self._device.props.state == UPowerGlib.DeviceState.CHARGING

@GObject.Property
def charged(self) -> bool:
return self._device.props.state == UPowerGlib.DeviceState.FULLY_CHARGED

@GObject.Property
def icon_name(self) -> str:
return self._device.props.icon_name

@GObject.Property
def time_remaining(self) -> int:
return (
self._device.props.time_to_full
if self.charging
else self._device.props.time_to_empty
)

@GObject.Property
def energy(self) -> float:
return self._device.props.energy

@GObject.Property
def energy_full(self) -> float:
return self._device.props.energy_full

@GObject.Property
def energy_full_design(self) -> float:
return self._device.props.energy_full_design

@GObject.Property
def energy_rate(self) -> float:
return self._device.props.energy_rate

@GObject.Property
def charge_cycles(self) -> int:
return self._device.props.charge_cycles

@GObject.Property
def vendor(self) -> str:
return self._device.props.vendor

@GObject.Property
def model(self) -> str:
return self._device.props.model

@GObject.Property
def serial(self) -> str:
return self._device.props.serial

@GObject.Property
def power_supply(self) -> bool:
return self._device.props.power_supply

@GObject.Property
def technology(self) -> str:
return UPowerGlib.Device.technology_to_string(self._device.props.technology) # type: ignore

@GObject.Property
def temperature(self) -> float:
return self._device.props.temperature

@GObject.Property
def voltage(self) -> float:
return self._device.props.voltage
Loading
Loading
0