From 47bcf8ee610b9868e0b670b0b9439b770232cbae Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 2 Jan 2025 21:54:36 -0800 Subject: [PATCH] service/upower: add power-profiles support --- src/dbus/properties.hpp | 4 +- src/services/upower/CMakeLists.txt | 1 + src/services/upower/core.hpp | 8 + src/services/upower/module.md | 1 + src/services/upower/powerprofiles.cpp | 213 +++++++++++++++++++++++ src/services/upower/powerprofiles.hpp | 237 ++++++++++++++++++++++++++ 6 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 src/services/upower/powerprofiles.cpp create mode 100644 src/services/upower/powerprofiles.hpp diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 846f70f2..f800ef3e 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -36,8 +36,8 @@ template class DBusResult { public: explicit DBusResult() = default; - explicit DBusResult(T value): value(std::move(value)) {} - explicit DBusResult(QDBusError error): error(std::move(error)) {} + DBusResult(T value): value(std::move(value)) {} + DBusResult(QDBusError error): error(std::move(error)) {} explicit DBusResult(T value, QDBusError error) : value(std::move(value)) , error(std::move(error)) {} diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt index ca87f6ae..b18ec155 100644 --- a/src/services/upower/CMakeLists.txt +++ b/src/services/upower/CMakeLists.txt @@ -21,6 +21,7 @@ qt_add_dbus_interface(DBUS_INTERFACES qt_add_library(quickshell-service-upower STATIC core.cpp device.cpp + powerprofiles.cpp ${DBUS_INTERFACES} ) diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index 9ade8121..c3878150 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -54,6 +54,14 @@ private: DBusUPowerService* service = nullptr; }; +///! Provides access to the UPower service. +/// An interface to the [UPower daemon], which can be used to +/// view battery and power statistics for your computer and +/// connected devices. +/// +/// > [!NOTE] The UPower daemon must be installed to use this service. +/// +/// [UPower daemon]: https://upower.freedesktop.org class UPowerQml: public QObject { Q_OBJECT; QML_NAMED_ELEMENT(UPower); diff --git a/src/services/upower/module.md b/src/services/upower/module.md index 99c7ece4..e1d697fe 100644 --- a/src/services/upower/module.md +++ b/src/services/upower/module.md @@ -3,5 +3,6 @@ description = "UPower Service" headers = [ "core.hpp", "device.hpp", + "powerprofiles.hpp", ] ----- diff --git a/src/services/upower/powerprofiles.cpp b/src/services/upower/powerprofiles.cpp new file mode 100644 index 00000000..b4b477b0 --- /dev/null +++ b/src/services/upower/powerprofiles.cpp @@ -0,0 +1,213 @@ +#include "powerprofiles.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/bus.hpp" +#include "../../dbus/properties.hpp" + +namespace qs::service::upower { + +namespace { +Q_LOGGING_CATEGORY(logPowerProfiles, "quickshell.service.powerprofiles", QtWarningMsg); +} + +QString PowerProfile::toString(PowerProfile::Enum profile) { + switch (profile) { + case PowerProfile::PowerSaver: return QStringLiteral("PowerSaver"); + case PowerProfile::Balanced: return QStringLiteral("Balanced"); + case PowerProfile::Performance: return QStringLiteral("Performance"); + default: return QStringLiteral("Invalid"); + } +} + +QString PerformanceDegradationReason::toString(PerformanceDegradationReason::Enum reason) { + switch (reason) { + case PerformanceDegradationReason::LapDetected: return QStringLiteral("LapDetected"); + case PerformanceDegradationReason::HighTemperature: return QStringLiteral("HighTemperature"); + default: return QStringLiteral("Invalid"); + } +} + +bool PowerProfileHold::operator==(const PowerProfileHold& other) const { + return other.profile == this->profile && other.applicationId == this->applicationId + && other.reason == this->reason; +} + +QDebug& operator<<(QDebug& debug, const PowerProfileHold& hold) { + auto saver = QDebugStateSaver(debug); + + debug.nospace(); + debug << "PowerProfileHold(profile=" << hold.profile << ", applicationId=" << hold.applicationId + << ", reason=" << hold.reason << ')'; + + return debug; +} + +PowerProfiles::PowerProfiles() { + qDBusRegisterMetaType>(); + + this->bHasPerformanceProfile.setBinding([this]() { + return this->bProfiles.value().contains(PowerProfile::Performance); + }); + + qCDebug(logPowerProfiles) << "Starting PowerProfiles Service."; + + auto bus = QDBusConnection::systemBus(); + + if (!bus.isConnected()) { + qCWarning(logPowerProfiles + ) << "Could not connect to DBus. PowerProfiles services will not work."; + } + + this->service = new QDBusInterface( + "org.freedesktop.UPower.PowerProfiles", + "/org/freedesktop/UPower/PowerProfiles", + "org.freedesktop.UPower.PowerProfiles", + bus, + this + ); + + if (!this->service->isValid()) { + qCDebug(logPowerProfiles + ) << "PowerProfilesDaemon is not currently running, attempting to start it."; + + dbus::tryLaunchService(this, bus, "org.freedesktop.UPower.PowerProfiles", [this](bool success) { + if (success) { + qCDebug(logPowerProfiles) << "Successfully launched PowerProfiles service."; + this->init(); + } else { + qCWarning(logPowerProfiles) + << "Could not start PowerProfilesDaemon. The PowerProfiles service will not work."; + } + }); + } else { + this->init(); + } +} + +void PowerProfiles::init() { + this->properties.setInterface(this->service); + this->properties.updateAllViaGetAll(); +} + +void PowerProfiles::setProfile(PowerProfile::Enum profile) { + if (profile == PowerProfile::Performance && !this->bHasPerformanceProfile) { + qCCritical(logPowerProfiles + ) << "Cannot request performance profile as it is not present for this device."; + return; + } else if (profile < PowerProfile::PowerSaver || profile > PowerProfile::Performance) { + qCCritical(logPowerProfiles) << "Tried to request invalid power profile" << profile; + return; + } + + this->bProfile = profile; + this->pProfile.write(); +} + +PowerProfiles* PowerProfiles::instance() { + static auto* instance = new PowerProfiles(); // NOLINT + return instance; +} + +PowerProfilesQml::PowerProfilesQml(QObject* parent): QObject(parent) { + auto* instance = PowerProfiles::instance(); + + this->bProfile.setBinding([instance]() { return instance->bProfile.value(); }); + + this->bHasPerformanceProfile.setBinding([instance]() { + return instance->bHasPerformanceProfile.value(); + }); + + this->bDegradationReason.setBinding([instance]() { return instance->bDegradationReason.value(); } + ); + + this->bHolds.setBinding([instance]() { return instance->bHolds.value(); }); +} + +} // namespace qs::service::upower + +namespace qs::dbus { + +using namespace qs::service::upower; + +DBusResult DBusDataTransform::fromWire(const Wire& wire) { + if (wire == QStringLiteral("power-saver")) { + return PowerProfile::PowerSaver; + } else if (wire == QStringLiteral("balanced")) { + return PowerProfile::Balanced; + } else if (wire == QStringLiteral("performance")) { + return PowerProfile::Performance; + } else { + return QDBusError(QDBusError::InvalidArgs, QString("Invalid PowerProfile: %1").arg(wire)); + } +} + +QString DBusDataTransform::toWire(Data data) { + switch (data) { + case PowerProfile::PowerSaver: return QStringLiteral("power-saver"); + case PowerProfile::Balanced: return QStringLiteral("balanced"); + case PowerProfile::Performance: return QStringLiteral("performance"); + } +} + +DBusResult> +DBusDataTransform>::fromWire(const Wire& wire) { + QList profiles; + + for (const auto& entry: wire) { + auto profile = + DBusDataTransform::fromWire(entry.value("Profile").value()); + + if (!profile.isValid()) return profile.error; + profiles.append(profile.value); + } + + return profiles; +} + +DBusResult +DBusDataTransform::fromWire(const Wire& wire) { + if (wire.isEmpty()) { + return PerformanceDegradationReason::None; + } else if (wire == QStringLiteral("lap-detected")) { + return PerformanceDegradationReason::LapDetected; + } else if (wire == QStringLiteral("high-operating-temperature")) { + return PerformanceDegradationReason::HighTemperature; + } else { + return QDBusError( + QDBusError::InvalidArgs, + QString("Invalid PerformanceDegradationReason: %1").arg(wire) + ); + } +} + +DBusResult> +DBusDataTransform>::fromWire(const Wire& wire) { + QList holds; + + for (const auto& entry: wire) { + auto profile = + DBusDataTransform::fromWire(entry.value("Profile").value()); + + if (!profile.isValid()) return profile.error; + + auto applicationId = entry.value("ApplicationId").value(); + auto reason = entry.value("Reason").value(); + + holds.append(PowerProfileHold(profile.value, applicationId, reason)); + } + + return holds; +} + +} // namespace qs::dbus diff --git a/src/services/upower/powerprofiles.hpp b/src/services/upower/powerprofiles.hpp new file mode 100644 index 00000000..b7340328 --- /dev/null +++ b/src/services/upower/powerprofiles.hpp @@ -0,0 +1,237 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" + +namespace qs::service::upower { + +///! Power profile exposed by the PowerProfiles service. +/// See @@PowerProfiles. +class PowerProfile: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// This profile will limit system performance in order to save power. + PowerSaver = 0, + /// This profile is the default, and will attempt to strike a balance + /// between performance and power consumption. + Balanced = 1, + /// This profile will maximize performance at the cost of power consumption. + Performance = 2, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(qs::service::upower::PowerProfile::Enum profile); +}; + +///! Reason for performance degradation exposed by the PowerProfiles service. +/// See @@PowerProfiles.degradationReason for more information. +class PerformanceDegradationReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// Performance has not been degraded in a way power-profiles-daemon can detect. + None = 0, + /// Performance has been reduced due to the computer's lap detection function, + /// which attempts to keep the computer from getting too hot while on your lap. + LapDetected = 1, + /// Performance has been reduced due to high system temperatures. + HighTemperature = 2, + }; + Q_ENUM(Enum); + + // clang-format off + Q_INVOKABLE static QString toString(qs::service::upower::PerformanceDegradationReason::Enum reason); + // clang-format on +}; + +class PowerProfileHold; + +} // namespace qs::service::upower + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::upower::PowerProfile::Enum; + static DBusResult fromWire(const Wire& wire); + static Wire toWire(Data data); +}; + +template <> +struct DBusDataTransform> { + using Wire = QList; + using Data = QList; + static DBusResult fromWire(const Wire& wire); +}; + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::upower::PerformanceDegradationReason::Enum; + static DBusResult fromWire(const Wire& wire); +}; + +template <> +struct DBusDataTransform> { + using Wire = QList; + using Data = QList; + static DBusResult fromWire(const Wire& wire); +}; + +} // namespace qs::dbus + +namespace qs::service::upower { + +// docgen can't hit gadgets yet +class PowerProfileHold { + Q_GADGET; + QML_VALUE_TYPE(powerProfileHold); + Q_PROPERTY(qs::service::upower::PowerProfile::Enum profile MEMBER profile CONSTANT); + Q_PROPERTY(QString applicationId MEMBER applicationId CONSTANT); + Q_PROPERTY(QString reason MEMBER reason CONSTANT); + +public: + explicit PowerProfileHold() = default; + explicit PowerProfileHold(PowerProfile::Enum profile, QString applicationId, QString reason) + : profile(profile) + , applicationId(std::move(applicationId)) + , reason(std::move(reason)) {} + + PowerProfile::Enum profile = PowerProfile::Balanced; + QString applicationId; + QString reason; + + [[nodiscard]] bool operator==(const PowerProfileHold& other) const; +}; + +QDebug& operator<<(QDebug& debug, const PowerProfileHold& hold); + +class PowerProfiles: public QObject { + Q_OBJECT; + +public: + void setProfile(PowerProfile::Enum profile); + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(PowerProfiles, PowerProfile::Enum, bProfile, PowerProfile::Balanced); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, bool, bHasPerformanceProfile); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, PerformanceDegradationReason::Enum, bDegradationReason); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, QList, bHolds); + // clang-format on + + static PowerProfiles* instance(); + +private: + explicit PowerProfiles(); + void init(); + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, QList, bProfiles); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(PowerProfiles, properties); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pProfile, bProfile, properties, "ActiveProfile"); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pProfiles, bProfiles, properties, "Profiles"); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pPerformanceDegraded, bDegradationReason, properties, "PerformanceDegraded"); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pHolds, bHolds, properties, "ActiveProfileHolds"); + // clang-format on + + QDBusInterface* service = nullptr; +}; + +///! Provides access to the Power Profiles service. +/// An interface to the UPower [power profiles daemon], which can be +/// used to view and manage power profiles. +/// +/// > [!NOTE] The power profiles daemon must be installed to use this service. +/// > Installing UPower does not necessarily install the power profiles daemon. +/// +/// [power profiles daemon]: https://gitlab.freedesktop.org/upower/power-profiles-daemon +class PowerProfilesQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(PowerProfiles); + QML_SINGLETON; + // clang-format off + /// The current power profile. + /// + /// This property may be set to change the system's power profile, however + /// it cannot be set to `Performance` unless @@hasPerformanceProfile is true. + Q_PROPERTY(qs::service::upower::PowerProfile::Enum profile READ default WRITE setProfile NOTIFY profileChanged BINDABLE bindableProfile); + /// If the system has a performance profile. + /// + /// If this property is false, your system does not have a performance + /// profile known to power-profiles-daemon. + Q_PROPERTY(bool hasPerformanceProfile READ default NOTIFY hasPerformanceProfileChanged BINDABLE bindableHasPerformanceProfile); + /// If power-profiles-daemon detects degraded system performance, the reason + /// for the degradation will be present here. + Q_PROPERTY(qs::service::upower::PerformanceDegradationReason::Enum degradationReason READ default NOTIFY degradationReasonChanged BINDABLE bindableDegradationReason); + /// Power profile holds created by other applications. + /// + /// This property returns a `powerProfileHold` object, which has the following properties. + /// - `profile` - The @@PowerProfile held by the application. + /// - `applicationId` - A string identifying the application + /// - `reason` - The reason the application has given for holding the profile. + /// + /// Applications may "hold" a power profile in place for their lifetime, such + /// as a game holding Performance mode or a system daemon holding Power Saver mode + /// when reaching a battery threshold. If the user selects a different profile explicitly + /// (e.g. by setting @@profile$) all holds will be removed. + /// + /// Multiple applications may hold a power profile, however if multiple applications request + /// profiles than `PowerSaver` will win over `Performance`. Only `Performance` and `PowerSaver` + /// profiles may be held. + Q_PROPERTY(QList holds READ default NOTIFY holdsChanged BINDABLE bindableHolds); + // clang-format on + +signals: + void profileChanged(); + void hasPerformanceProfileChanged(); + void degradationReasonChanged(); + void holdsChanged(); + +public: + explicit PowerProfilesQml(QObject* parent = nullptr); + + [[nodiscard]] QBindable bindableProfile() const { return &this->bProfile; } + + static void setProfile(PowerProfile::Enum profile) { + PowerProfiles::instance()->setProfile(profile); + } + + [[nodiscard]] QBindable bindableHasPerformanceProfile() const { + return &this->bHasPerformanceProfile; + } + + [[nodiscard]] QBindable bindableDegradationReason() const { + return &this->bDegradationReason; + } + + [[nodiscard]] QBindable> bindableHolds() const { return &this->bHolds; } + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, PowerProfile::Enum, bProfile, &PowerProfilesQml::profileChanged); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, bool, bHasPerformanceProfile, &PowerProfilesQml::hasPerformanceProfileChanged); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, PerformanceDegradationReason::Enum, bDegradationReason, &PowerProfilesQml::degradationReasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, QList, bHolds, &PowerProfilesQml::holdsChanged); + // clang-format on +}; + +} // namespace qs::service::upower