diff --git a/PowerDevilSettings.kcfg b/PowerDevilSettings.kcfg index 0b82211d..76324f06 100644 --- a/PowerDevilSettings.kcfg +++ b/PowerDevilSettings.kcfg @@ -1,41 +1,48 @@ true true 250 100 10 5 2 10 + + + 0 + + + 100 + diff --git a/daemon/backends/CMakeLists.txt b/daemon/backends/CMakeLists.txt index 05c4263e..802c3ad0 100644 --- a/daemon/backends/CMakeLists.txt +++ b/daemon/backends/CMakeLists.txt @@ -1,69 +1,76 @@ ########################## UPower Backend ##################################### include_directories(${CMAKE_CURRENT_SOURCE_DIR}/upower ${X11_INCLUDE_DIR} ${X11_Xrandr_INCLUDE_PATH}) set(powerdevilupowerbackend_SRCS ${CMAKE_CURRENT_BINARY_DIR}/../powerdevil_debug.cpp upower/upowersuspendjob.cpp upower/login1suspendjob.cpp upower/powerdevilupowerbackend.cpp upower/xrandrbrightness.cpp upower/xrandrxcbhelper.cpp upower/udevqtclient.cpp upower/udevqtdevice.cpp upower/ddcutilbrightness.cpp ) set_source_files_properties( ${CMAKE_CURRENT_SOURCE_DIR}/upower/dbus/org.freedesktop.UPower.xml ${CMAKE_CURRENT_SOURCE_DIR}/upower/dbus/org.freedesktop.UPower.Device.xml PROPERTIES NO_NAMESPACE TRUE) qt5_add_dbus_interface(powerdevilupowerbackend_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/upower/dbus/org.freedesktop.UPower.xml upower_interface) qt5_add_dbus_interface(powerdevilupowerbackend_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/upower/dbus/org.freedesktop.UPower.Device.xml upower_device_interface) qt5_add_dbus_interface(powerdevilupowerbackend_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/upower/dbus/org.freedesktop.UPower.KbdBacklight.xml upower_kbdbacklight_interface) ## backlight helper executable add_executable(backlighthelper upower/backlighthelper.cpp ${CMAKE_CURRENT_BINARY_DIR}/../powerdevil_debug.cpp ${backlighthelper_mocs}) target_link_libraries(backlighthelper Qt5::Core KF5::AuthCore KF5::I18n) install(TARGETS backlighthelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) kauth_install_helper_files(backlighthelper org.kde.powerdevil.backlighthelper root) kauth_install_actions(org.kde.powerdevil.backlighthelper ${CMAKE_CURRENT_SOURCE_DIR}/upower/backlight_helper_actions.actions) ## discrete gpu helper executable add_executable(discretegpuhelper upower/discretegpuhelper.cpp ${CMAKE_CURRENT_BINARY_DIR}/../powerdevil_debug.cpp ${discretegpuhelper_mocs}) target_link_libraries(discretegpuhelper Qt5::Core KF5::AuthCore) install(TARGETS discretegpuhelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) kauth_install_helper_files(discretegpuhelper org.kde.powerdevil.discretegpuhelper root) kauth_install_actions(org.kde.powerdevil.discretegpuhelper ${CMAKE_CURRENT_SOURCE_DIR}/upower/discretegpu_helper_actions.actions) +## charge threshold helper executable +add_executable(chargethresholdhelper upower/chargethresholdhelper.cpp ${CMAKE_CURRENT_BINARY_DIR}/../powerdevil_debug.cpp ${chargethresholdhelper_mocs}) +target_link_libraries(chargethresholdhelper Qt5::Core KF5::AuthCore) +install(TARGETS chargethresholdhelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) +kauth_install_helper_files(chargethresholdhelper org.kde.powerdevil.chargethresholdhelper root) +kauth_install_actions(org.kde.powerdevil.chargethresholdhelper ${CMAKE_CURRENT_SOURCE_DIR}/upower/chargethreshold_helper_actions.actions) + add_library(powerdevilupowerbackend ${powerdevilupowerbackend_SRCS}) set_target_properties(powerdevilupowerbackend PROPERTIES PREFIX "") target_link_libraries(powerdevilupowerbackend Qt5::Widgets KF5::AuthCore KF5::ConfigCore KF5::CoreAddons KF5::DBusAddons KF5::I18n ${UDEV_LIBS} ${X11_LIBRARIES} ${X11_Xrandr_LIB} ${XCB_XCB_LIBRARY} ${XCB_RANDR_LIBRARY} powerdevilcore ) if(DDCUTIL_FOUND) target_link_libraries(powerdevilupowerbackend ${LIBDDCUTIL_LIBRARY}) endif() install(TARGETS powerdevilupowerbackend DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/powerdevil) diff --git a/daemon/backends/upower/chargethreshold_helper_actions.actions b/daemon/backends/upower/chargethreshold_helper_actions.actions new file mode 100644 index 00000000..c9aa1257 --- /dev/null +++ b/daemon/backends/upower/chargethreshold_helper_actions.actions @@ -0,0 +1,16 @@ +[Domain] +Name=KDE +Icon=kde + +[org.kde.powerdevil.chargethresholdhelper.getthreshold] +Name=Get current battery charge limit +Description=System policies prevent you from getting the current battery charge limit. +Policy=yes +# FIXME whatever this means? +PolicyInactive=yes + +[org.kde.powerdevil.chargethresholdhelper.setthreshold] +Name=Set battery charge limit +Description=System policies prevent you from setting a battery charge limit. +Policy=yes +PolicyInactive=yes diff --git a/daemon/backends/upower/chargethresholdhelper.cpp b/daemon/backends/upower/chargethresholdhelper.cpp new file mode 100644 index 00000000..374635bb --- /dev/null +++ b/daemon/backends/upower/chargethresholdhelper.cpp @@ -0,0 +1,148 @@ +/* + * Copyright 2020 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "chargethresholdhelper.h" + +#include + +#include + +#include +#include + +static const QString s_powerSupplySysFsPath = QStringLiteral("/sys/class/power_supply"); + +static const QString s_chargeStartThreshold = QStringLiteral("charge_start_threshold"); +static const QString s_chargeStopThreshold = QStringLiteral("charge_stop_threshold"); + +ChargeThresholdHelper::ChargeThresholdHelper(QObject *parent) + : QObject(parent) +{ +} + +static QStringList getBatteries() +{ + return QDir(s_powerSupplySysFsPath).entryList({QStringLiteral("BAT*")}, QDir::Dirs); +} + +static QVector getThresholds(const QString &which) +{ + QVector thresholds; + + const QStringList batteries = getBatteries(); + for (const QString &battery : batteries) { + QFile file(s_powerSupplySysFsPath + QLatin1Char('/') + battery + QLatin1Char('/') + which); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open" << file.fileName() << "for reading"; + continue; + } + + int threshold = -1; + QTextStream stream(&file); + stream >> threshold; + + if (threshold < 0 || threshold > 100) { + qWarning() << file.fileName() << "contains invalid threshold" << threshold; + continue; + } + + thresholds.append(threshold); + } + + return thresholds; +} + +static bool setThresholds(const QString &which, int threshold) +{ + const QStringList batteries = getBatteries(); + for (const QString &battery : batteries) { + QFile file(s_powerSupplySysFsPath + QLatin1Char('/') + battery + QLatin1Char('/') + which); + // TODO should we check the current value before writing the new one or is it clever + // enough not to shred some chip by writing the same thing again? + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "Failed to open" << file.fileName() << "for writing"; + return false; + } + + if (file.write(QByteArray::number(threshold)) == -1) { + qWarning() << "Failed to write threshold into" << file.fileName(); + return false; + } + } + + return true; +} + +ActionReply ChargeThresholdHelper::getthreshold(const QVariantMap &args) +{ + Q_UNUSED(args); + + const auto startThresholds = getThresholds(s_chargeStartThreshold); + const auto stopThresholds = getThresholds(s_chargeStopThreshold); + + if (startThresholds.isEmpty() || stopThresholds.isEmpty() || startThresholds.count() != stopThresholds.count()) { + auto reply = ActionReply::HelperErrorReply(); + reply.setErrorDescription(QStringLiteral("Charge thresholds not supported")); + return reply; + } + + // In the rare case there are multiple batteries with varying charge thresholds, try to use something sensible + const int startThreshold = *std::max_element(startThresholds.begin(), startThresholds.end()); + const int stopThreshold = *std::min_element(stopThresholds.begin(), stopThresholds.end()); + + ActionReply reply; + reply.setData({ + {QStringLiteral("chargeStartThreshold"), startThreshold}, + {QStringLiteral("chargeStopThreshold"), stopThreshold} + }); + return reply; +} + +ActionReply ChargeThresholdHelper::setthreshold(const QVariantMap &args) +{ + const int startThreshold = args.value(QStringLiteral("chargeStartThreshold")).toInt(); + const int stopThreshold = args.value(QStringLiteral("chargeStopThreshold")).toInt(); + + if (startThreshold < 0 + || startThreshold > 100 + || stopThreshold < 0 + || stopThreshold > 100 + || startThreshold > stopThreshold) { + auto reply = ActionReply::HelperErrorReply(); // is there an "invalid arguments" error? + reply.setErrorDescription(QStringLiteral("Invalid thresholds provided")); + return reply; + } + + if (!setThresholds(s_chargeStartThreshold, startThreshold)) { + auto reply = ActionReply::HelperErrorReply(); + reply.setErrorDescription(QStringLiteral("Failed to write start charge threshold")); + return reply; + } + + if (!setThresholds(s_chargeStopThreshold, stopThreshold)) { + auto reply = ActionReply::HelperErrorReply(); + reply.setErrorDescription(QStringLiteral("Failed to write stop charge threshold")); + return reply; + } + + return ActionReply(); +} + +KAUTH_HELPER_MAIN("org.kde.powerdevil.chargethresholdhelper", ChargeThresholdHelper) diff --git a/daemon/backends/upower/chargethresholdhelper.h b/daemon/backends/upower/chargethresholdhelper.h new file mode 100644 index 00000000..f505f008 --- /dev/null +++ b/daemon/backends/upower/chargethresholdhelper.h @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include + +using namespace KAuth; + +class ChargeThresholdHelper : public QObject +{ + Q_OBJECT +public: + explicit ChargeThresholdHelper(QObject *parent = nullptr); + +public Q_SLOTS: + ActionReply getthreshold(const QVariantMap &args); + ActionReply setthreshold(const QVariantMap &args); + +}; diff --git a/daemon/org.kde.Solid.PowerManagement.xml b/daemon/org.kde.Solid.PowerManagement.xml index af584080..dc514823 100644 --- a/daemon/org.kde.Solid.PowerManagement.xml +++ b/daemon/org.kde.Solid.PowerManagement.xml @@ -1,52 +1,58 @@ + + + + + + diff --git a/daemon/powerdevilcore.cpp b/daemon/powerdevilcore.cpp index 26a16aa6..fa80853c 100644 --- a/daemon/powerdevilcore.cpp +++ b/daemon/powerdevilcore.cpp @@ -1,938 +1,1004 @@ /*************************************************************************** * Copyright (C) 2010 by Dario Freddi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "powerdevilcore.h" #include "PowerDevilSettings.h" #include "powerdevilaction.h" #include "powerdevilactionpool.h" #include "powerdevilbackendinterface.h" #include "powerdevilpolicyagent.h" #include "powerdevilprofilegenerator.h" #include "powerdevil_debug.h" #include "actions/bundled/suspendsession.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace PowerDevil { Core::Core(QObject* parent) : QObject(parent) , m_hasDualGpu(false) , m_backend(nullptr) , m_notificationsWatcher(nullptr) , m_criticalBatteryTimer(new QTimer(this)) , m_activityConsumer(new KActivities::Consumer(this)) , m_pendingWakeupEvent(true) { KAuth::Action discreteGpuAction(QStringLiteral("org.kde.powerdevil.discretegpuhelper.hasdualgpu")); discreteGpuAction.setHelperId(QStringLiteral("org.kde.powerdevil.discretegpuhelper")); KAuth::ExecuteJob *discreteGpuJob = discreteGpuAction.execute(); connect(discreteGpuJob, &KJob::result, this, [this, discreteGpuJob] { if (discreteGpuJob->error()) { qCWarning(POWERDEVIL) << "org.kde.powerdevil.discretegpuhelper.hasdualgpu failed"; qCDebug(POWERDEVIL) << discreteGpuJob->errorText(); return; } m_hasDualGpu = discreteGpuJob->data()[QStringLiteral("hasdualgpu")].toBool(); }); discreteGpuJob->start(); + + readChargeThreshold(); } Core::~Core() { qCDebug(POWERDEVIL) << "Core unloading"; // Unload all actions before exiting, and clear the cache ActionPool::instance()->unloadAllActiveActions(); ActionPool::instance()->clearCache(); } void Core::loadCore(BackendInterface* backend) { if (!backend) { return; } m_backend = backend; // Async backend init - so that KDED gets a bit of a speed up qCDebug(POWERDEVIL) << "Core loaded, initializing backend"; connect(m_backend, SIGNAL(backendReady()), this, SLOT(onBackendReady())); m_backend->init(); } void Core::onBackendReady() { qCDebug(POWERDEVIL) << "Backend ready, KDE Power Management system initialized"; m_profilesConfig = KSharedConfig::openConfig(QStringLiteral("powermanagementprofilesrc"), KConfig::CascadeConfig); QStringList groups = m_profilesConfig->groupList(); // the "migration" key is for shortcuts migration in added by migratePre512KeyboardShortcuts // and as such our configuration would never be considered empty, ignore it! groups.removeOne(QStringLiteral("migration")); // Is it brand new? if (groups.isEmpty()) { // Generate defaults qCDebug(POWERDEVIL) << "Generating a default configuration"; bool toRam = m_backend->supportedSuspendMethods() & PowerDevil::BackendInterface::ToRam; bool toDisk = m_backend->supportedSuspendMethods() & PowerDevil::BackendInterface::ToDisk; ProfileGenerator::generateProfiles(toRam, toDisk); m_profilesConfig->reparseConfiguration(); } // Get the battery devices ready { using namespace Solid; connect(DeviceNotifier::instance(), SIGNAL(deviceAdded(QString)), this, SLOT(onDeviceAdded(QString))); connect(DeviceNotifier::instance(), SIGNAL(deviceRemoved(QString)), this, SLOT(onDeviceRemoved(QString))); // Force the addition of already existent batteries const auto devices = Device::listFromType(DeviceInterface::Battery, QString()); for (const Device &device : devices) { onDeviceAdded(device.udi()); } } connect(m_backend, SIGNAL(acAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState)), this, SLOT(onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState))); connect(m_backend, SIGNAL(batteryRemainingTimeChanged(qulonglong)), this, SLOT(onBatteryRemainingTimeChanged(qulonglong))); connect(m_backend, SIGNAL(lidClosedChanged(bool)), this, SLOT(onLidClosedChanged(bool))); connect(m_backend, &BackendInterface::aboutToSuspend, this, &Core::onAboutToSuspend); connect(KIdleTime::instance(), SIGNAL(timeoutReached(int,int)), this, SLOT(onKIdleTimeoutReached(int,int))); connect(KIdleTime::instance(), SIGNAL(resumingFromIdle()), this, SLOT(onResumingFromIdle())); connect(m_activityConsumer, &KActivities::Consumer::currentActivityChanged, this, [this]() { loadProfile(); }); // Set up the policy agent PowerDevil::PolicyAgent::instance()->init(); // When inhibitions change, simulate user activity, see Bug 315438 connect(PowerDevil::PolicyAgent::instance(), &PowerDevil::PolicyAgent::unavailablePoliciesChanged, this, [](PowerDevil::PolicyAgent::RequiredPolicies newPolicies) { Q_UNUSED(newPolicies); KIdleTime::instance()->simulateUserActivity(); }); // Bug 354250: Simulate user activity when session becomes inactive, // this keeps us from sending the computer to sleep when switching to an idle session. // (this is just being lazy as it will result in us clearing everything connect(PowerDevil::PolicyAgent::instance(), &PowerDevil::PolicyAgent::sessionActiveChanged, this, [this](bool active) { if (active) { // force reload profile so all actions re-register their idle timeouts loadProfile(true /*force*/); } else { // Bug 354250: Keep us from sending the computer to sleep when switching // to an idle session by removing all idle timeouts KIdleTime::instance()->removeAllIdleTimeouts(); m_registeredActionTimeouts.clear(); } }); // Initialize the action pool, which will also load the needed startup actions. PowerDevil::ActionPool::instance()->init(this); // Set up the critical battery timer m_criticalBatteryTimer->setSingleShot(true); m_criticalBatteryTimer->setInterval(60000); connect(m_criticalBatteryTimer, SIGNAL(timeout()), this, SLOT(onCriticalBatteryTimerExpired())); // wait until the notification system is set up before firing notifications // to avoid them showing ontop of ksplash... if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.Notifications"))) { onServiceRegistered(QString()); } else { m_notificationsWatcher = new QDBusServiceWatcher(QStringLiteral("org.freedesktop.Notifications"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this); connect(m_notificationsWatcher, SIGNAL(serviceRegistered(QString)), this, SLOT(onServiceRegistered(QString))); // ...but fire them after 30s nonetheless to ensure they've been shown QTimer::singleShot(30000, this, SLOT(onNotificationTimeout())); } // All systems up Houston, let's go! Q_EMIT coreReady(); refreshStatus(); } bool Core::isActionSupported(const QString& actionName) { Action *action = ActionPool::instance()->loadAction(actionName, KConfigGroup(), this); if (!action) { return false; } else { return action->isSupported(); } } void Core::refreshStatus() { /* The configuration could have changed if this function was called, so * let's resync it. */ reparseConfiguration(); loadProfile(true); } void Core::reparseConfiguration() { PowerDevilSettings::self()->load(); m_profilesConfig->reparseConfiguration(); // Config reloaded Q_EMIT configurationReloaded(); // Check if critical threshold might have changed and cancel the timer if necessary. if (m_criticalBatteryTimer->isActive() && currentChargePercent() > PowerDevilSettings::batteryCriticalLevel()) { m_criticalBatteryTimer->stop(); if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } } if (m_lowBatteryNotification && currentChargePercent() > PowerDevilSettings::batteryLowLevel()) { m_lowBatteryNotification->close(); } + + if (m_chargeStartThreshold != PowerDevilSettings::chargeStartThreshold() + || m_chargeStopThreshold != PowerDevilSettings::chargeStopThreshold()) { + writeChargeThreshold(); + } } QString Core::currentProfile() const { return m_currentProfile; } void Core::loadProfile(bool force) { QString profileId; // Policy check if (PolicyAgent::instance()->requirePolicyCheck(PolicyAgent::ChangeProfile) != PolicyAgent::None) { qCDebug(POWERDEVIL) << "Policy Agent prevention: on"; return; } KConfigGroup config; // Check the activity in which we are in QString activity = m_activityConsumer->currentActivity(); qCDebug(POWERDEVIL) << "Currently using activity " << activity; KConfigGroup activitiesConfig(m_profilesConfig, "Activities"); qCDebug(POWERDEVIL) << activitiesConfig.groupList() << activitiesConfig.keyList(); // Are we mirroring an activity? if (activitiesConfig.group(activity).readEntry("mode", "None") == QStringLiteral("ActLike") && activitiesConfig.group(activity).readEntry("actLike", QString()) != QStringLiteral("AC") && activitiesConfig.group(activity).readEntry("actLike", QString()) != QStringLiteral("Battery") && activitiesConfig.group(activity).readEntry("actLike", QString()) != QStringLiteral("LowBattery")) { // Yes, let's use that then activity = activitiesConfig.group(activity).readEntry("actLike", QString()); qCDebug(POWERDEVIL) << "Activity is a mirror"; } KConfigGroup activityConfig = activitiesConfig.group(activity); qCDebug(POWERDEVIL) << activityConfig.groupList() << activityConfig.keyList(); // See if this activity has priority if (activityConfig.readEntry("mode", "None") == QStringLiteral("SeparateSettings")) { // Prioritize this profile over anything config = activityConfig.group("SeparateSettings"); qCDebug(POWERDEVIL) << "Activity is enforcing a different profile"; profileId = activity; } else { // It doesn't, let's load the current state's profile if (m_batteriesPercent.isEmpty()) { qCDebug(POWERDEVIL) << "No batteries found, loading AC"; profileId = QStringLiteral("AC"); } else if (activityConfig.readEntry("mode", "None") == QStringLiteral("ActLike")) { if (activityConfig.readEntry("actLike", QString()) == QStringLiteral("AC") || activityConfig.readEntry("actLike", QString()) == QStringLiteral("Battery") || activityConfig.readEntry("actLike", QString()) == QStringLiteral("LowBattery")) { // Same as above, but with an existing profile config = m_profilesConfig.data()->group(activityConfig.readEntry("actLike", QString())); profileId = activityConfig.readEntry("actLike", QString()); qCDebug(POWERDEVIL) << "Activity is mirroring a different profile"; } } else { // Compute the previous and current global percentage const int percent = currentChargePercent(); if (backend()->acAdapterState() == BackendInterface::Plugged) { profileId = QStringLiteral("AC"); qCDebug(POWERDEVIL) << "Loading profile for plugged AC"; } else if (percent <= PowerDevilSettings::batteryLowLevel()) { profileId = QStringLiteral("LowBattery"); qCDebug(POWERDEVIL) << "Loading profile for low battery"; } else { profileId = QStringLiteral("Battery"); qCDebug(POWERDEVIL) << "Loading profile for unplugged AC"; } } config = m_profilesConfig.data()->group(profileId); qCDebug(POWERDEVIL) << "Activity is not forcing a profile"; } // Release any special inhibitions { QHash::iterator i = m_sessionActivityInhibit.begin(); while (i != m_sessionActivityInhibit.end()) { PolicyAgent::instance()->ReleaseInhibition(i.value()); i = m_sessionActivityInhibit.erase(i); } i = m_screenActivityInhibit.begin(); while (i != m_screenActivityInhibit.end()) { PolicyAgent::instance()->ReleaseInhibition(i.value()); i = m_screenActivityInhibit.erase(i); } } if (!config.isValid()) { qCWarning(POWERDEVIL) << "Profile " << profileId << "has been selected but does not exist."; return; } // Check: do we need to change profile at all? if (m_currentProfile == profileId && !force) { // No, let's leave things as they are qCDebug(POWERDEVIL) << "Skipping action reload routine as profile has not changed"; // Do we need to force a wakeup? if (m_pendingWakeupEvent) { // Fake activity at this stage, when no timeouts are registered onResumingFromIdle(); m_pendingWakeupEvent = false; } } else { // First of all, let's clean the old actions. This will also call the onProfileUnload callback ActionPool::instance()->unloadAllActiveActions(); // Do we need to force a wakeup? if (m_pendingWakeupEvent) { // Fake activity at this stage, when no timeouts are registered onResumingFromIdle(); m_pendingWakeupEvent = false; } // Cool, now let's load the needed actions const auto groupList = config.groupList(); for (const QString &actionName : groupList) { Action *action = ActionPool::instance()->loadAction(actionName, config.group(actionName), this); if (action) { action->onProfileLoad(); } else { // Ouch, error. But let's just warn and move on anyway //TODO Maybe Remove from the configuration if unsupported qCWarning(POWERDEVIL) << "The profile " << profileId << "tried to activate" << actionName << "a non-existent action. This is usually due to an installation problem," " a configuration problem, or because the action is not supported"; } } // We are now on a different profile m_currentProfile = profileId; Q_EMIT profileChanged(m_currentProfile); } // Now... any special behaviors we'd like to consider? if (activityConfig.readEntry("mode", "None") == QStringLiteral("SpecialBehavior")) { qCDebug(POWERDEVIL) << "Activity has special behaviors"; KConfigGroup behaviorGroup = activityConfig.group("SpecialBehavior"); if (behaviorGroup.readEntry("performAction", false)) { // Let's override the configuration for this action at all times ActionPool::instance()->loadAction(QStringLiteral("SuspendSession"), behaviorGroup.group("ActionConfig"), this); qCDebug(POWERDEVIL) << "Activity overrides suspend session action"; // debug hence not sleep } if (behaviorGroup.readEntry("noSuspend", false)) { qCDebug(POWERDEVIL) << "Activity triggers a suspend inhibition"; // debug hence not sleep // Trigger a special inhibition - if we don't have one yet if (!m_sessionActivityInhibit.contains(activity)) { int cookie = PolicyAgent::instance()->AddInhibition(PolicyAgent::InterruptSession, i18n("Activity Manager"), i18n("This activity's policies prevent the system from going to sleep")); m_sessionActivityInhibit.insert(activity, cookie); } } if (behaviorGroup.readEntry("noScreenManagement", false)) { qCDebug(POWERDEVIL) << "Activity triggers a screen management inhibition"; // Trigger a special inhibition - if we don't have one yet if (!m_screenActivityInhibit.contains(activity)) { int cookie = PolicyAgent::instance()->AddInhibition(PolicyAgent::ChangeScreenSettings, i18n("Activity Manager"), i18n("This activity's policies prevent screen power management")); m_screenActivityInhibit.insert(activity, cookie); } } } // If the lid is closed, retrigger the lid close signal // so that "switching profile then closing the lid" has the same result as // "closing lid then switching profile". if (m_backend->isLidClosed()) { Q_EMIT m_backend->buttonPressed(PowerDevil::BackendInterface::LidClose); } } void Core::onDeviceAdded(const QString &udi) { if (m_batteriesPercent.contains(udi) || m_peripheralBatteriesPercent.contains(udi)) { // We already know about this device return; } using namespace Solid; Device device(udi); Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); if (!b) { return; } connect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged); connect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged); qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "was detected"; if (b->isPowerSupply()) { m_batteriesPercent[udi] = b->chargePercent(); m_batteriesCharged[udi] = (b->chargeState() == Solid::Battery::FullyCharged); + + // Apply charge limit to the new battery, too + writeChargeThreshold(); } else { // non-power supply batteries are treated separately m_peripheralBatteriesPercent[udi] = b->chargePercent(); // notify the user about the empty mouse/keyboard when plugging it in; don't when // notifications aren't ready yet so we avoid showing them ontop of ksplash; // also we'll notify about all devices when notifications are ready anyway if (m_notificationsReady) { emitBatteryChargePercentNotification(b->chargePercent(), 1000 /* so current is always lower than previous */, udi); } } // If a new battery has been added, let's clear some pending suspend actions if the new global batteries percentage is // higher than the battery critical level. (See bug 329537) if (m_lowBatteryNotification && currentChargePercent() > PowerDevilSettings::batteryLowLevel()) { m_lowBatteryNotification->close(); } if (m_criticalBatteryTimer->isActive() && currentChargePercent() > PowerDevilSettings::batteryCriticalLevel()) { m_criticalBatteryTimer->stop(); if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } emitRichNotification(QStringLiteral("pluggedin"), i18n("Extra Battery Added"), i18n("The computer will no longer go to sleep.")); } } void Core::onDeviceRemoved(const QString &udi) { if (!m_batteriesPercent.contains(udi) && !m_peripheralBatteriesPercent.contains(udi)) { // We don't know about this device return; } using namespace Solid; Device device(udi); Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); disconnect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged); disconnect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged); qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "has been removed"; m_batteriesPercent.remove(udi); m_peripheralBatteriesPercent.remove(udi); m_batteriesCharged.remove(udi); } void Core::emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName) { KNotification::event(eventId, title, message, iconName, nullptr, KNotification::CloseOnTimeout, QStringLiteral("powerdevil")); } void Core::emitRichNotification(const QString &evid, const QString &title, const QString &message) { KNotification::event(evid, title, message, QPixmap(), nullptr, KNotification::CloseOnTimeout, QStringLiteral("powerdevil")); } bool Core::emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi) { using namespace Solid; Device device(udi); Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); if (b && !b->isPowerSupply()) { // if you leave the device out of reach or it has not been initialized yet // it won't be "there" and report 0%, don't show anything in this case if (!b->isPresent() || b->chargePercent() == 0) { return false; } // Bluetooth devices don't report charge state, so it's "NoCharge" in all cases for them if (b->chargeState() != Battery::Discharging && b->chargeState() != Battery::NoCharge) { return false; } if (currentPercent <= PowerDevilSettings::peripheralBatteryLowLevel() && previousPercent > PowerDevilSettings::peripheralBatteryLowLevel()) { QString name = device.product(); if (!device.vendor().isEmpty()) { name = i18nc("%1 is vendor name, %2 is product name", "%1 %2", device.vendor(), device.product()); } QString title; QString msg; QString icon; switch(b->type()) { case Battery::MouseBattery: title = i18n("Mouse Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in (\"%1\") is running low, and the device may turn off at any time. " "Please recharge or replace the battery.", name); icon = QStringLiteral("input-mouse"); break; case Battery::KeyboardBattery: title = i18n("Keyboard Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in (\"%1\") is running low, and the device may turn off at any time. " "Please recharge or replace the battery.", name); icon = QStringLiteral("input-keyboard"); break; case Battery::BluetoothBattery: title = i18n("Bluetooth Device Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in Bluetooth device \"%1\" is running low, and the device may turn off at any time. " "Please recharge or replace the battery.", name); icon = QStringLiteral("preferences-system-bluetooth"); break; default: title = i18nc("The battery in an external device", "Device Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in (\"%1\") is running low, and the device may turn off at any time. " "Please recharge or replace the battery.", name); icon = QStringLiteral("battery-caution"); break; } emitNotification(QStringLiteral("lowperipheralbattery"), title, msg, icon); return true; } return false; } if (m_backend->acAdapterState() == BackendInterface::Plugged) { return false; } if (currentPercent <= PowerDevilSettings::batteryCriticalLevel() && previousPercent > PowerDevilSettings::batteryCriticalLevel()) { handleCriticalBattery(currentPercent); return true; } else if (currentPercent <= PowerDevilSettings::batteryLowLevel() && previousPercent > PowerDevilSettings::batteryLowLevel()) { handleLowBattery(currentPercent); return true; } return false; } void Core::handleLowBattery(int percent) { if (m_lowBatteryNotification) { return; } m_lowBatteryNotification = new KNotification(QStringLiteral("lowbattery"), KNotification::Persistent, nullptr); m_lowBatteryNotification->setComponentName(QStringLiteral("powerdevil")); m_lowBatteryNotification->setTitle(i18n("Battery Low (%1% Remaining)", percent)); m_lowBatteryNotification->setText(i18n("Battery running low - to continue using your computer, plug it in or shut it down and change the battery.")); m_lowBatteryNotification->setUrgency(KNotification::CriticalUrgency); m_lowBatteryNotification->sendEvent(); } void Core::handleCriticalBattery(int percent) { if (m_lowBatteryNotification) { m_lowBatteryNotification->close(); } // no parent, but it won't leak, since it will be closed both in case of timeout or direct action m_criticalBatteryNotification = new KNotification(QStringLiteral("criticalbattery"), KNotification::Persistent, nullptr); m_criticalBatteryNotification->setComponentName(QStringLiteral("powerdevil")); m_criticalBatteryNotification->setTitle(i18n("Battery Critical (%1% Remaining)", percent)); const QStringList actions = {i18nc("Cancel timeout that will automatically put system to sleep because of low battery", "Cancel")}; connect(m_criticalBatteryNotification.data(), &KNotification::action1Activated, this, [this] { m_criticalBatteryTimer->stop(); m_criticalBatteryNotification->close(); }); switch (PowerDevilSettings::batteryCriticalAction()) { case PowerDevil::BundledActions::SuspendSession::ShutdownMode: m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will shut down in 60 seconds.")); m_criticalBatteryNotification->setActions(actions); m_criticalBatteryTimer->start(); break; case PowerDevil::BundledActions::SuspendSession::ToDiskMode: m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will enter hibernation mode in 60 seconds.")); m_criticalBatteryNotification->setActions(actions); m_criticalBatteryTimer->start(); break; case PowerDevil::BundledActions::SuspendSession::ToRamMode: m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will go to sleep in 60 seconds.")); m_criticalBatteryNotification->setActions(actions); m_criticalBatteryTimer->start(); break; default: m_criticalBatteryNotification->setText(i18n("Battery level critical. Please save your work.")); // no timer, no actions break; } m_criticalBatteryNotification->sendEvent(); } void Core::onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState state) { qCDebug(POWERDEVIL); // Post request for faking an activity event - usually adapters don't plug themselves out :) m_pendingWakeupEvent = true; loadProfile(); if (state == BackendInterface::Plugged) { // If the AC Adaptor has been plugged in, let's clear some pending suspend actions if (m_lowBatteryNotification) { m_lowBatteryNotification->close(); } if (m_criticalBatteryTimer->isActive()) { m_criticalBatteryTimer->stop(); if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } emitRichNotification(QStringLiteral("pluggedin"), i18n("AC Adapter Plugged In"), i18n("The computer will no longer go to sleep.")); } else { emitRichNotification(QStringLiteral("pluggedin"), i18n("Running on AC power"), i18n("The power adapter has been plugged in.")); } } else if (state == BackendInterface::Unplugged) { emitRichNotification(QStringLiteral("unplugged"), i18n("Running on Battery Power"), i18n("The power adapter has been unplugged.")); } } void Core::onBatteryChargePercentChanged(int percent, const QString &udi) { if (m_peripheralBatteriesPercent.contains(udi)) { const int previousPercent = m_peripheralBatteriesPercent.value(udi); m_peripheralBatteriesPercent[udi] = percent; if (percent < previousPercent) { emitBatteryChargePercentNotification(percent, previousPercent, udi); } return; } // Compute the previous and current global percentage const int previousPercent = currentChargePercent(); const int currentPercent = previousPercent - (m_batteriesPercent[udi] - percent); // Update the battery percentage m_batteriesPercent[udi] = percent; if (currentPercent < previousPercent) { if (emitBatteryChargePercentNotification(currentPercent, previousPercent, udi)) { // Only refresh status if a notification has actually been emitted loadProfile(); } } } void Core::onBatteryChargeStateChanged(int state, const QString &udi) { if (!m_batteriesCharged.contains(udi)) { return; } bool previousCharged = true; for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) { if (!i.value()) { previousCharged = false; break; } } m_batteriesCharged[udi] = (state == Solid::Battery::FullyCharged); if (m_backend->acAdapterState() != BackendInterface::Plugged) { return; } bool currentCharged = true; for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) { if (!i.value()) { currentCharged = false; break; } } if (!previousCharged && currentCharged) { emitRichNotification(QStringLiteral("fullbattery"), i18n("Charging Complete"), i18n("Battery now fully charged.")); loadProfile(); } } void Core::onCriticalBatteryTimerExpired() { if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } // Do that only if we're not on AC if (m_backend->acAdapterState() == BackendInterface::Unplugged) { // We consider this as a very special button PowerDevil::Action *helperAction = ActionPool::instance()->loadAction(QStringLiteral("HandleButtonEvents"), KConfigGroup(), this); if (helperAction) { QVariantMap args; args[QStringLiteral("Button")] = 32; args[QStringLiteral("Type")] = QVariant::fromValue(PowerDevilSettings::batteryCriticalAction()); args[QStringLiteral("Explicit")] = true; helperAction->trigger(args); } } } void Core::onBatteryRemainingTimeChanged(qulonglong time) { Q_EMIT batteryRemainingTimeChanged(time); } void Core::onKIdleTimeoutReached(int identifier, int msec) { // Find which action(s) requested this idle timeout for (auto i = m_registeredActionTimeouts.constBegin(), end = m_registeredActionTimeouts.constEnd(); i != end; ++i) { if (i.value().contains(identifier)) { i.key()->onIdleTimeout(msec); // And it will need to be awaken m_pendingResumeFromIdleActions.insert(i.key()); break; } } // Catch the next resume event if some actions require it if (!m_pendingResumeFromIdleActions.isEmpty()) { KIdleTime::instance()->catchNextResumeEvent(); } } void Core::onLidClosedChanged(bool closed) { Q_EMIT lidClosedChanged(closed); } void Core::onAboutToSuspend() { if (PowerDevilSettings::pausePlayersOnSuspend()) { qCDebug(POWERDEVIL) << "Pausing all media players before sleep"; QDBusPendingCall listNamesCall = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames")); QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(listNamesCall, this); connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { QDBusPendingReply reply = *watcher; watcher->deleteLater(); if (reply.isError()) { qCWarning(POWERDEVIL) << "Failed to fetch list of DBus service names for pausing players on entering sleep" << reply.error().message(); return; } const QStringList &services = reply.value(); for (const QString &serviceName : services) { if (!serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) { continue; } qCDebug(POWERDEVIL) << "Pausing media player with service name" << serviceName; QDBusMessage pauseMsg = QDBusMessage::createMethodCall(serviceName, QStringLiteral("/org/mpris/MediaPlayer2"), QStringLiteral("org.mpris.MediaPlayer2.Player"), QStringLiteral("Pause")); QDBusConnection::sessionBus().asyncCall(pauseMsg); } }); } } void Core::registerActionTimeout(Action* action, int timeout) { // Register the timeout with KIdleTime int identifier = KIdleTime::instance()->addIdleTimeout(timeout); // Add the identifier to the action hash QList< int > timeouts = m_registeredActionTimeouts[action]; timeouts.append(identifier); m_registeredActionTimeouts[action] = timeouts; } void Core::unregisterActionTimeouts(Action* action) { // Clear all timeouts from the action const QList< int > timeoutsToClean = m_registeredActionTimeouts[action]; for (int id : timeoutsToClean) { KIdleTime::instance()->removeIdleTimeout(id); } m_registeredActionTimeouts.remove(action); } int Core::currentChargePercent() const { int chargePercent = 0; for (auto it = m_batteriesPercent.constBegin(); it != m_batteriesPercent.constEnd(); ++it) { chargePercent += it.value(); } return chargePercent; } void Core::onResumingFromIdle() { KIdleTime::instance()->simulateUserActivity(); // Wake up the actions in which an idle action was triggered std::for_each(m_pendingResumeFromIdleActions.cbegin(), m_pendingResumeFromIdleActions.cend(), std::mem_fn(&PowerDevil::Action::onWakeupFromIdle)); m_pendingResumeFromIdleActions.clear(); } void Core::onNotificationTimeout() { // cannot connect QTimer::singleShot directly to the other method onServiceRegistered(QString()); } void Core::onServiceRegistered(const QString &service) { Q_UNUSED(service); if (m_notificationsReady) { return; } bool needsRefresh = false; // show warning about low batteries right on session startup, force it to show // by making sure the "old" percentage (that magic number) is always higher than the current one if (emitBatteryChargePercentNotification(currentChargePercent(), 1000)) { needsRefresh = true; } // now also emit notifications for all peripheral batteries for (auto it = m_peripheralBatteriesPercent.constBegin(), end = m_peripheralBatteriesPercent.constEnd(); it != end; ++it) { if (emitBatteryChargePercentNotification(it.value() /*currentPercent*/, 1000, it.key() /*udi*/)) { needsRefresh = true; } } // need to refresh status to prevent the notification from showing again when charge percentage changes if (needsRefresh) { refreshStatus(); } m_notificationsReady = true; if (m_notificationsWatcher) { delete m_notificationsWatcher; m_notificationsWatcher = nullptr; } } +void Core::onChargeThresholdChanged(int start, int stop) +{ + m_chargeStartThreshold = start; + + if (m_chargeStopThreshold != stop) { + m_chargeStopThreshold = stop; + Q_EMIT chargeStopThresholdChanged(stop); + } +} + +void Core::readChargeThreshold() +{ + KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.getthreshold")); + action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper")); + KAuth::ExecuteJob *job = action.execute(); + connect(job, &KJob::result, this, [this, job] { + if (job->error()) { + qCWarning(POWERDEVIL) << "org.kde.powerdevil.chargethresholdhelper.getthreshold failed" << job->errorText(); + return; + } + + const auto data = job->data(); + onChargeThresholdChanged(data.value(QStringLiteral("chargeStartThreshold")).toInt(), + data.value(QStringLiteral("chargeStopThreshold")).toInt()); + }); + job->start(); +} + +void Core::writeChargeThreshold() +{ + const int newStartThreshold = PowerDevilSettings::chargeStartThreshold(); + const int newStopThreshold = PowerDevilSettings::chargeStopThreshold(); + + KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.setthreshold")); + action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper")); + action.setArguments({ + {QStringLiteral("chargeStartThreshold"), newStartThreshold}, + {QStringLiteral("chargeStopThreshold"), newStopThreshold} + }); + KAuth::ExecuteJob *job = action.execute(); + connect(job, &KJob::result, this, [this, job, newStartThreshold, newStopThreshold] { + if (job->error()) { + qCWarning(POWERDEVIL) << "org.kde.powerdevil.chargethresholdhelper.setthreshold failed" << job->errorText(); + return; + } + + onChargeThresholdChanged(newStartThreshold, newStopThreshold); + }); + job->start(); +} + BackendInterface* Core::backend() { return m_backend; } bool Core::isLidClosed() const { return m_backend->isLidClosed(); } bool Core::isLidPresent() const { return m_backend->isLidPresent(); } bool Core::hasDualGpu() const { return m_hasDualGpu; } +int Core::chargeStopThreshold() const +{ + return m_chargeStopThreshold; +} + qulonglong Core::batteryRemainingTime() const { return m_backend->batteryRemainingTime(); } uint Core::backendCapabilities() { return m_backend->capabilities(); } } diff --git a/daemon/powerdevilcore.h b/daemon/powerdevilcore.h index 8f36bca3..80d31464 100644 --- a/daemon/powerdevilcore.h +++ b/daemon/powerdevilcore.h @@ -1,160 +1,168 @@ /*************************************************************************** * Copyright (C) 2010 by Dario Freddi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #ifndef POWERDEVILCORE_H #define POWERDEVILCORE_H #include "powerdevilbackendinterface.h" #include #include #include #include namespace KActivities { class Consumer; } // namespace KActivities class QDBusServiceWatcher; class QTimer; class KNotification; namespace Solid { class Battery; } namespace PowerDevil { class BackendInterface; class Action; class Q_DECL_EXPORT Core : public QObject { Q_OBJECT Q_DISABLE_COPY(Core) Q_CLASSINFO("D-Bus Interface", "org.kde.Solid.PowerManagement") public: explicit Core(QObject* parent); ~Core() override; void reloadProfile(int state); void emitRichNotification(const QString &evid, const QString &title, const QString &message = QString()); void emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName); bool emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi = QString()); BackendInterface *backend(); // More... public Q_SLOTS: void loadCore(PowerDevil::BackendInterface *backend); // Set of common action - useful for the DBus interface uint backendCapabilities(); void refreshStatus(); void reparseConfiguration(); QString currentProfile() const; void loadProfile(bool force = false); qulonglong batteryRemainingTime() const; bool isLidClosed() const; bool isLidPresent() const; bool isActionSupported(const QString &actionName); bool hasDualGpu() const; + int chargeStopThreshold() const; Q_SIGNALS: void coreReady(); void profileChanged(const QString &newProfile); void configurationReloaded(); void batteryRemainingTimeChanged(qulonglong time); void lidClosedChanged(bool closed); + void chargeStopThresholdChanged(int threshold); private: void registerActionTimeout(Action *action, int timeout); void unregisterActionTimeouts(Action *action); void handleLowBattery(int percent); void handleCriticalBattery(int percent); + void onChargeThresholdChanged(int start, int stop); + void readChargeThreshold(); + void writeChargeThreshold(); + /** * Computes the current global charge percentage. * Sum of all battery charges. */ int currentChargePercent() const; friend class Action; bool m_hasDualGpu; + int m_chargeStartThreshold = 0; + int m_chargeStopThreshold = 100; BackendInterface *m_backend; QDBusServiceWatcher *m_notificationsWatcher; bool m_notificationsReady = false; KSharedConfigPtr m_profilesConfig; QString m_currentProfile; QHash m_batteriesPercent; QHash m_peripheralBatteriesPercent; QHash m_batteriesCharged; QPointer m_lowBatteryNotification; QTimer *m_criticalBatteryTimer; QPointer m_criticalBatteryNotification; KActivities::Consumer *m_activityConsumer; // Idle time management QHash< Action*, QList< int > > m_registeredActionTimeouts; QSet m_pendingResumeFromIdleActions; bool m_pendingWakeupEvent; // Activity inhibition management QHash< QString, int > m_sessionActivityInhibit; QHash< QString, int > m_screenActivityInhibit; private Q_SLOTS: void onBackendReady(); void onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState); void onBatteryChargePercentChanged(int,const QString&); void onBatteryChargeStateChanged(int,const QString&); void onBatteryRemainingTimeChanged(qulonglong); void onKIdleTimeoutReached(int,int); void onResumingFromIdle(); void onDeviceAdded(const QString &udi); void onDeviceRemoved(const QString &udi); void onCriticalBatteryTimerExpired(); void onNotificationTimeout(); void onServiceRegistered(const QString &service); void onLidClosedChanged(bool closed); void onAboutToSuspend(); }; } #endif // POWERDEVILCORE_H diff --git a/kcmodule/global/CMakeLists.txt b/kcmodule/global/CMakeLists.txt index a125fccb..c9b27850 100644 --- a/kcmodule/global/CMakeLists.txt +++ b/kcmodule/global/CMakeLists.txt @@ -1,22 +1,23 @@ add_definitions(-DTRANSLATION_DOMAIN=\"powerdevilglobalconfig\") set( kcm_powerdevil_global_SRCS GeneralPage.cpp ) ki18n_wrap_ui(kcm_powerdevil_global_SRCS generalPage.ui) kconfig_add_kcfg_files(kcm_powerdevil_global_SRCS ../../PowerDevilSettings.kcfgc) add_library(kcm_powerdevilglobalconfig MODULE ${kcm_powerdevil_global_SRCS}) target_link_libraries(kcm_powerdevilglobalconfig + KF5::AuthCore KF5::ConfigWidgets KF5::KIOWidgets KF5::NotifyConfig powerdevilconfigcommonprivate ) install(TARGETS kcm_powerdevilglobalconfig DESTINATION ${KDE_INSTALL_PLUGINDIR} ) install( FILES powerdevilglobalconfig.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) diff --git a/kcmodule/global/GeneralPage.cpp b/kcmodule/global/GeneralPage.cpp index e3746ce2..a650b696 100644 --- a/kcmodule/global/GeneralPage.cpp +++ b/kcmodule/global/GeneralPage.cpp @@ -1,215 +1,254 @@ /*************************************************************************** * Copyright (C) 2008 by Dario Freddi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "GeneralPage.h" #include "ErrorOverlay.h" #include "PowerDevilSettings.h" #include "actions/bundled/suspendsession.h" #include "powerdevilpowermanagement.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include +#include + K_PLUGIN_FACTORY(PowerDevilGeneralKCMFactory, registerPlugin(); ) GeneralPage::GeneralPage(QWidget *parent, const QVariantList &args) : KCModule(nullptr, parent, args) { setButtons(Apply | Help); // KAboutData *about = // new KAboutData("powerdevilglobalconfig", "powerdevilglobalconfig", ki18n("Global Power Management Configuration"), // "", ki18n("A global power management configurator for KDE Power Management System"), // KAboutData::License_GPL, ki18n("(c), 2010 Dario Freddi"), // ki18n("From this module, you can configure the main Power Management daemon, assign profiles to " // "states, and do some advanced fine tuning on battery handling")); // // about->addAuthor(ki18n("Dario Freddi"), ki18n("Maintainer") , "drf@kde.org", // "http://drfav.wordpress.com"); // // setAboutData(about); setupUi(this); fillUi(); QDBusServiceWatcher *watcher = new QDBusServiceWatcher("org.kde.Solid.PowerManagement", QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this); connect(watcher, SIGNAL(serviceRegistered(QString)), this, SLOT(onServiceRegistered(QString))); connect(watcher, SIGNAL(serviceUnregistered(QString)), this, SLOT(onServiceUnregistered(QString))); if (QDBusConnection::sessionBus().interface()->isServiceRegistered("org.kde.Solid.PowerManagement")) { onServiceRegistered("org.kde.Solid.PowerManagement"); } else { onServiceUnregistered("org.kde.Solid.PowerManagement"); } } GeneralPage::~GeneralPage() { } void GeneralPage::fillUi() { bool hasPowerSupplyBattery = false; bool hasPeripheralBattery = false; const auto devices = Solid::Device::listFromType(Solid::DeviceInterface::Battery, QString()); for (const Solid::Device &device : devices) { const Solid::Battery *b = qobject_cast (device.asDeviceInterface(Solid::DeviceInterface::Battery)); if (b->isPowerSupply()) { hasPowerSupplyBattery = true; } else { hasPeripheralBattery = true; } } BatteryCriticalCombo->addItem(QIcon::fromTheme("dialog-cancel"), i18n("Do nothing"), PowerDevil::BundledActions::SuspendSession::None); if (PowerDevil::PowerManagement::instance()->canSuspend()) { BatteryCriticalCombo->addItem(QIcon::fromTheme("system-suspend"), i18nc("Suspend to RAM", "Sleep"), PowerDevil::BundledActions::SuspendSession::ToRamMode); } if (PowerDevil::PowerManagement::instance()->canHibernate()) { BatteryCriticalCombo->addItem(QIcon::fromTheme("system-suspend-hibernate"), i18n("Hibernate"), PowerDevil::BundledActions::SuspendSession::ToDiskMode); } BatteryCriticalCombo->addItem(QIcon::fromTheme("system-shutdown"), i18n("Shut down"), PowerDevil::BundledActions::SuspendSession::ShutdownMode); notificationsButton->setIcon(QIcon::fromTheme("preferences-desktop-notification")); // modified fields... connect(notificationsButton, SIGNAL(clicked()), SLOT(configureNotifications())); connect(lowSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(criticalSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(lowPeripheralSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(BatteryCriticalCombo, SIGNAL(currentIndexChanged(int)), SLOT(changed())); + connect(chargeStartThresholdSpin, QOverload::of(&QSpinBox::valueChanged), this, &GeneralPage::markAsChanged); + connect(chargeStopThresholdSpin, QOverload::of(&QSpinBox::valueChanged), this, &GeneralPage::markAsChanged); + connect(pausePlayersCheckBox, SIGNAL(stateChanged(int)), SLOT(changed())); if (!hasPowerSupplyBattery) { BatteryCriticalLabel->hide(); BatteryCriticalCombo->hide(); lowLabel->hide(); lowSpin->hide(); criticalLabel->hide(); criticalSpin->hide(); } if (!hasPeripheralBattery) { lowPeripheralLabel->hide(); lowPeripheralSpin->hide(); } if (!hasPowerSupplyBattery && !hasPeripheralBattery) { batteryLevelsLabel->hide(); } + + // We're basically just interested whehter we can read the values + // Displayed SpinBox is from config which PowerDevil will apply rather than the actual current threshold + KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.getthreshold")); + action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper")); + KAuth::ExecuteJob *job = action.execute(); + connect(job, &KJob::result, this, [this, job] { + if (job->error()) { + qDebug() << "org.kde.powerdevil.chargethresholdhelper.getthreshold failed" << job->errorText(); + setChargeThresholdSupported(false); + return; + } + + setChargeThresholdSupported(true); + }); + job->start(); } void GeneralPage::load() { lowSpin->setValue(PowerDevilSettings::batteryLowLevel()); criticalSpin->setValue(PowerDevilSettings::batteryCriticalLevel()); lowPeripheralSpin->setValue(PowerDevilSettings::peripheralBatteryLowLevel()); + chargeStartThresholdSpin->setValue(PowerDevilSettings::chargeStartThreshold()); + chargeStopThresholdSpin->setValue(PowerDevilSettings::chargeStopThreshold()); + BatteryCriticalCombo->setCurrentIndex(BatteryCriticalCombo->findData(PowerDevilSettings::batteryCriticalAction())); pausePlayersCheckBox->setChecked(PowerDevilSettings::pausePlayersOnSuspend()); } void GeneralPage::configureNotifications() { KNotifyConfigWidget::configure(this, "powerdevil"); } void GeneralPage::save() { PowerDevilSettings::setBatteryLowLevel(lowSpin->value()); PowerDevilSettings::setBatteryCriticalLevel(criticalSpin->value()); PowerDevilSettings::setPeripheralBatteryLowLevel(lowPeripheralSpin->value()); PowerDevilSettings::setBatteryCriticalAction(BatteryCriticalCombo->itemData(BatteryCriticalCombo->currentIndex()).toInt()); + PowerDevilSettings::setChargeStartThreshold(chargeStartThresholdSpin->value()); + PowerDevilSettings::setChargeStopThreshold(chargeStopThresholdSpin->value()); + PowerDevilSettings::setPausePlayersOnSuspend(pausePlayersCheckBox->checkState() == Qt::Checked); PowerDevilSettings::self()->save(); // Notify Daemon QDBusMessage call = QDBusMessage::createMethodCall("org.kde.Solid.PowerManagement", "/org/kde/Solid/PowerManagement", "org.kde.Solid.PowerManagement", "refreshStatus"); // Perform call QDBusConnection::sessionBus().asyncCall(call); // And now we are set with no change Q_EMIT changed(false); } void GeneralPage::defaults() { KCModule::defaults(); } +void GeneralPage::setChargeThresholdSupported(bool supported) +{ + batteryThresholdLabel->setVisible(supported); + batteryThresholdExplanation->setVisible(supported); + + chargeStartThresholdLabel->setVisible(supported); + chargeStartThresholdSpin->setVisible(supported); + chargeStopThresholdLabel->setVisible(supported); + chargeStopThresholdSpin->setVisible(supported); +} + void GeneralPage::onServiceRegistered(const QString& service) { Q_UNUSED(service); if (m_errorOverlay) { m_errorOverlay->deleteLater(); m_errorOverlay = nullptr; } } void GeneralPage::onServiceUnregistered(const QString& service) { Q_UNUSED(service); if (m_errorOverlay) { m_errorOverlay->deleteLater(); } m_errorOverlay = new ErrorOverlay(this, i18n("The Power Management Service appears not to be running.\n" "This can be solved by starting or scheduling it inside \"Startup and Shutdown\""), this); } #include "GeneralPage.moc" diff --git a/kcmodule/global/GeneralPage.h b/kcmodule/global/GeneralPage.h index 6b690e92..00da3514 100644 --- a/kcmodule/global/GeneralPage.h +++ b/kcmodule/global/GeneralPage.h @@ -1,50 +1,52 @@ /*************************************************************************** * Copyright (C) 2008 by Dario Freddi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #ifndef GENERALPAGE_H #define GENERALPAGE_H #include #include "ui_generalPage.h" class ErrorOverlay; class GeneralPage : public KCModule, private Ui_generalPage { Q_OBJECT public: GeneralPage(QWidget *parent, const QVariantList &args); ~GeneralPage() override; void fillUi(); void load() override; void save() override; void defaults() override; private Q_SLOTS: void configureNotifications(); void onServiceRegistered(const QString &service); void onServiceUnregistered(const QString &service); private: + void setChargeThresholdSupported(bool supported); + ErrorOverlay *m_errorOverlay = nullptr; }; #endif /* GENERALPAGE_H */ diff --git a/kcmodule/global/generalPage.ui b/kcmodule/global/generalPage.ui index 8d6b466a..97aa4951 100644 --- a/kcmodule/global/generalPage.ui +++ b/kcmodule/global/generalPage.ui @@ -1,208 +1,274 @@ generalPage 0 0 510 - 276 + 406 QFormLayout::ExpandingFieldsGrow <b>Battery Levels </b> &Low level: lowSpin 100 16777215 Low battery level Battery will be considered low when it reaches this level % 100 &Critical level: criticalSpin 100 16777215 Critical battery level Battery will be considered critical when it reaches this level % 100 A&t critical level: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false BatteryCriticalCombo 300 16777215 + + + + Low level for peripheral devices: + + + + + + + % + + + 100 + + + - - - Qt::Vertical + + + + 75 + true + - - - 20 - 16 - + + Charge Limit - + - + + + + <html><head/><body><p>Keeping the battery charged 100% over a prolonged period of time may accelerate deterioration of battery health. By limiting the maximum battery charge you can help extend the battery lifespan.</p></body></html> + + + true + + + + + + + Start charging only once below: + + + + + + + Always charge when plugged in + + + % + + + 99 + + + + + + + Pause media players when suspending: + + + + + + + Enabled + + + + 0 0 16777215 16777215 Configure Notifications... - - + + + + + 75 + true + + - Low level for peripheral devices: + Other Settings - - + + + + Stop charging at: + + + + + % + + 50 + 100 - - - - - - Enabled - - - - - - - Pause media players when suspending: + + 100 Qt::Horizontal QSizePolicy::Fixed 8 20 KComboBox QComboBox
kcombobox.h