Skip to content
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
5 changes: 5 additions & 0 deletions docs/api/services/power_profiles.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Power Profiles
==============

.. autoclass:: ignis.services.power_profiles.PowerProfilesService
:members:
46 changes: 46 additions & 0 deletions ignis/dbus/org.freedesktop.UPower.PowerProfiles.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<node name="/">
<interface name="org.freedesktop.UPower.PowerProfiles">
<method name="HoldProfile">
<arg type="s" name="profile" direction="in">
</arg>
<arg type="s" name="reason" direction="in">
</arg>
<arg type="s" name="application_id" direction="in">
</arg>
<arg type="u" name="cookie" direction="out">
</arg>
</method>
<method name="ReleaseProfile">
<arg type="u" name="cookie" direction="in">
</arg>
</method>
<method name="SetActionEnabled">
<arg type="s" name="action" direction="in">
</arg>
<arg type="b" name="enabled" direction="in">
</arg>
</method>
<signal name="ProfileReleased">
<arg type="u" name="cookie">
</arg>
</signal>
<property type="s" name="ActiveProfile" access="readwrite">
</property>
<property type="s" name="PerformanceInhibited" access="read">
</property>
<property type="s" name="PerformanceDegraded" access="read">
</property>
<property type="aa{sv}" name="Profiles" access="read">
</property>
<property type="as" name="Actions" access="read">
</property>
<property type="aa{sv}" name="ActionsInfo" access="read">
</property>
<property type="aa{sv}" name="ActiveProfileHolds" access="read">
</property>
<property type="s" name="Version" access="read">
</property>
<property type="b" name="BatteryAware" access="readwrite">
</property>
</interface>
</node>
12 changes: 12 additions & 0 deletions ignis/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,18 @@ def __init__(self, *args: object) -> None:
)


class PowerProfilesDaemonNotRunningError(Exception):
"""
Raised when Power Profiles daemon is not running.
"""

def __init__(self, *args: object) -> None:
super().__init__(
"Power Profiles daemon is not running. To use the Power Profile service, install and run power-profiles-daemon",
*args,
)


class GnomeBluetoothNotFoundError(Exception):
"""
Raised when GnomeBluetooth-3.0 is not found.
Expand Down
5 changes: 5 additions & 0 deletions ignis/services/power_profiles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .service import PowerProfilesService

__all__ = [
"PowerProfilesService",
]
1 change: 1 addition & 0 deletions ignis/services/power_profiles/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PP_ICON_TEMPLATE = "power-profile-{}-symbolic"
158 changes: 158 additions & 0 deletions ignis/services/power_profiles/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from __future__ import annotations
from ignis.base_service import BaseService
from ignis.dbus import DBusProxy
from ignis.exceptions import PowerProfilesDaemonNotRunningError
from ignis.gobject import IgnisProperty
from gi.repository import GLib # type: ignore
from ignis import utils
from .constants import PP_ICON_TEMPLATE


class PowerProfilesService(BaseService):
"""
A service for managing power profiles through the DBus interface power-profiles-daemon provides.

Example usage:

.. code-block:: python

from ignis.services.power_profiles import PowerProfilesService

power_profiles = PowerProfilesService.get_default()

print(power_profiles.active_profile)
power_profiles.active_profile = "performance"

for profile in power_profiles.profiles:
print(profile)

power_profiles.connect("notify::active-profile", lambda x, y: print(power_profiles.active_profile))
"""

def __init__(self) -> None:
super().__init__()

self._proxy = DBusProxy.new(
name="org.freedesktop.UPower.PowerProfiles",
object_path="/org/freedesktop/UPower/PowerProfiles",
interface_name="org.freedesktop.UPower.PowerProfiles",
info=utils.load_interface_xml("org.freedesktop.UPower.PowerProfiles"),
bus_type="system",
)

if not self.is_available:
return

self._proxy.gproxy.connect("g-properties-changed", self.__on_properties_changed)

self._active_profile: str = self._proxy.ActiveProfile
self._profiles: list[str] = [p["Profile"] for p in self._proxy.Profiles]
self._cookie = -1

@IgnisProperty
def is_available(self) -> bool:
"""
Whether power profiles capability in UPower is available and UPower is running.

If ``False``, this service will not be functional.
"""
return self._proxy.has_owner

@IgnisProperty
def active_profile( # type: ignore
self,
) -> str:
"""
Current active power profile.

Should be either of:
- `performance`
- `balanced`
- `power-saver`
"""
if not self.is_available:
raise PowerProfilesDaemonNotRunningError()
return self._active_profile

@active_profile.setter
def active_profile(
self,
profile: str,
) -> None:
if not self.is_available:
raise PowerProfilesDaemonNotRunningError()
if profile not in self.profiles:
raise ValueError(f"Profile '{profile}' is not available.")

self._cookie = -1
self._proxy.ActiveProfile = GLib.Variant("s", profile)

def hold_profile(self, profile: str) -> None:
"""
This forces the passed profile (only `performance` or `power-saver`) to be activated until ignis exits,
:func:`~release_profile` is called, or the :attr:`~active_profile` is changed manually.

Use if you need to ensure a specific profile is active for a certain amount of time or while
a specific task is being performed. This way the previous state will not have to be managed by you.
"""
if profile not in self.profiles:
raise ValueError(f"Profile '{profile}' is not available.")
if profile == "balanced":
raise ValueError(
"Cannot hold the balanced profile, only performance or power-saver."
)

if not self.is_available:
raise PowerProfilesDaemonNotRunningError()

if self._cookie != -1:
return

self._cookie = self._proxy.gproxy.HoldProfile(
"(sss)", profile, "", "com.github.linkfrg.ignis"
)

def release_profile(self) -> None:
"""
Release the hold on the profile
"""
if not self.is_available:
raise PowerProfilesDaemonNotRunningError()

if self._cookie == -1:
return

self._proxy.gproxy.ReleaseProfile("(u)", self._cookie)
self._cookie = -1

@IgnisProperty
def profiles(self) -> list[str]:
"""
List of available power profiles.

Possible values are:
- `performance`
- `balanced`
- `power-saver`
"""
if not self.is_available:
return []
return self._profiles

@IgnisProperty
def icon_name(self) -> str:
"""
Icon name representing the active power profile.
"""
return PP_ICON_TEMPLATE.format(self.active_profile)

def __on_properties_changed(self, _, properties: GLib.Variant, ignored):
prop_dict = properties.unpack()

if "ActiveProfile" in prop_dict:
self._active_profile = prop_dict["ActiveProfile"]
self.notify("active-profile")
self.notify("icon-name")
if "Profiles" in prop_dict:
self._profiles = list(prop_dict["Profiles"].keys())
self.notify("profiles")