Commit f6a1362e authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇
Browse files

Add setting for charge threshold

On supported hardware, when using the device mostly on line power, this allows to stop charging
the battery when it reaches a certain level as to not leave the battery fully charge for extended
periods of time. It also allows to configure below which level the battery will then be charged again.

Unfortunately, UPower doesn't seem to be aware of this, so the device will just sit around the
configured percentage, still claiming "charging".

The battery stop charging threshold is exposed on DBus so a hint coul be displayed in Battery
monitor when a limit is configured.
parent d0802516
......@@ -120,6 +120,13 @@ qt5_add_dbus_adaptor(powerdevil_SRCS org.kde.Solid.PowerManagement.PolicyAgent.x
qt5_add_dbus_adaptor(powerdevil_SRCS org.freedesktop.PowerManagement.xml powerdevilfdoconnector.h PowerDevil::FdoConnector powermanagementfdoadaptor PowerManagementFdoAdaptor)
qt5_add_dbus_adaptor(powerdevil_SRCS org.freedesktop.PowerManagement.Inhibit.xml powerdevilfdoconnector.h PowerDevil::FdoConnector powermanagementinhibitadaptor PowerManagementInhibitAdaptor)
# KAuth charge threshold helper
add_executable(chargethresholdhelper chargethresholdhelper.cpp 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 chargethreshold_helper_actions.actions)
# Backends
add_subdirectory(backends)
......
[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
PolicyInactive=yes
[org.kde.powerdevil.chargethresholdhelper.setthreshold]
Name=Set battery charge limit
Description=System policies prevent you from setting a battery charge limit.
Policy=auth_admin
Persistence=session
/*
* Copyright 2020 Kai Uwe Broulik <kde@broulik.de>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "chargethresholdhelper.h"
#include <powerdevil_debug.h>
#include <algorithm>
#include <QDir>
#include <QFile>
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<int> getThresholds(const QString &which)
{
QVector<int> 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)
/*
* Copyright 2020 Kai Uwe Broulik <kde@broulik.de>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <KAuth>
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);
};
......@@ -28,6 +28,12 @@
<method name="hasDualGpu">
<arg type="b" direction="out" />
</method>
<method name="chargeStartThreshold">
<arg type="i" direction="out" />
</method>
<method name="chargeStopThreshold">
<arg type="i" direction="out" />
</method>
<!-- schedule system wakeup in future -->
<!--
......@@ -93,5 +99,11 @@
<signal name="lidClosedChanged">
<arg type="b" direction="out" />
</signal>
<signal name="chargeStartThresholdChanged">
<arg type="i" direction="out" />
</signal>
<signal name="chargeStopThresholdChanged">
<arg type="i" direction="out" />
</signal>
</interface>
</node>
......@@ -79,6 +79,8 @@ Core::Core(QObject* parent)
m_hasDualGpu = discreteGpuJob->data()[QStringLiteral("hasdualgpu")].toBool();
});
discreteGpuJob->start();
readChargeThreshold();
}
Core::~Core()
......@@ -268,6 +270,8 @@ void Core::reparseConfiguration()
if (m_lowBatteryNotification && currentChargePercent() > PowerDevilSettings::batteryLowLevel()) {
m_lowBatteryNotification->close();
}
readChargeThreshold();
}
QString Core::currentProfile() const
......@@ -933,6 +937,36 @@ void Core::onServiceRegistered(const QString &service)
}
}
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();
const int chargeStartThreshold = data.value(QStringLiteral("chargeStartThreshold")).toInt();
if (m_chargeStartThreshold != chargeStartThreshold) {
m_chargeStartThreshold = chargeStartThreshold;
Q_EMIT chargeStartThresholdChanged(chargeStartThreshold);
}
const int chargeStopThreshold = data.value(QStringLiteral("chargeStopThreshold")).toInt();
if (m_chargeStopThreshold != chargeStopThreshold) {
m_chargeStopThreshold = chargeStopThreshold;
Q_EMIT chargeStopThresholdChanged(chargeStopThreshold);
}
qCDebug(POWERDEVIL) << "Charge thresholds: start at" << chargeStartThreshold << "- stop at" << chargeStopThreshold;
});
job->start();
}
BackendInterface* Core::backend()
{
return m_backend;
......@@ -953,6 +987,16 @@ bool Core::hasDualGpu() const
return m_hasDualGpu;
}
int Core::chargeStartThreshold() const
{
return m_chargeStartThreshold;
}
int Core::chargeStopThreshold() const
{
return m_chargeStopThreshold;
}
uint Core::scheduleWakeup(const QString &service, const QDBusObjectPath &path, qint64 timeout)
{
++m_lastWakeupCookie;
......
......@@ -100,6 +100,8 @@ public Q_SLOTS:
bool isLidPresent() const;
bool isActionSupported(const QString &actionName);
bool hasDualGpu() const;
int chargeStartThreshold() const;
int chargeStopThreshold() const;
// service - dbus interface to ping when wakeup is done
// path - dbus path on service
......@@ -115,6 +117,8 @@ Q_SIGNALS:
void configurationReloaded();
void batteryRemainingTimeChanged(qulonglong time);
void lidClosedChanged(bool closed);
void chargeStartThresholdChanged(int threshold);
void chargeStopThresholdChanged(int threshold);
private:
void registerActionTimeout(Action *action, int timeout);
......@@ -122,6 +126,8 @@ private:
void handleLowBattery(int percent);
void handleCriticalBattery(int percent);
void readChargeThreshold();
/**
* Computes the current global charge percentage.
* Sum of all battery charges.
......@@ -131,6 +137,8 @@ private:
friend class Action;
bool m_hasDualGpu;
int m_chargeStartThreshold = 0;
int m_chargeStopThreshold = 100;
BackendInterface *m_backend;
......
......@@ -12,6 +12,7 @@ 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
......
......@@ -43,6 +43,9 @@
#include <KAboutData>
#include <KLocalizedString>
#include <KAuthAction>
#include <KAuthExecuteJob>
K_PLUGIN_FACTORY(PowerDevilGeneralKCMFactory,
registerPlugin<GeneralPage>();
)
......@@ -124,6 +127,17 @@ void GeneralPage::fillUi()
connect(BatteryCriticalCombo, SIGNAL(currentIndexChanged(int)), SLOT(changed()));
connect(chargeStartThresholdSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GeneralPage::markAsChanged);
connect(chargeStopThresholdSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GeneralPage::markAsChanged);
connect(chargeStopThresholdSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this] {
if (chargeStopThresholdSpin->value() > m_chargeStopThreshold) {
chargeStopThresholdMessage->animatedShow();
} else {
chargeStopThresholdMessage->animatedHide();
}
});
chargeStopThresholdMessage->hide();
connect(pausePlayersCheckBox, SIGNAL(stateChanged(int)), SLOT(changed()));
if (!hasPowerSupplyBattery) {
......@@ -154,6 +168,26 @@ void GeneralPage::load()
BatteryCriticalCombo->setCurrentIndex(BatteryCriticalCombo->findData(PowerDevilSettings::batteryCriticalAction()));
pausePlayersCheckBox->setChecked(PowerDevilSettings::pausePlayersOnSuspend());
KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.getthreshold"));
action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper"));
KAuth::ExecuteJob *job = action.execute();
job->exec();
if (!job->error()) {
const auto data = job->data();
m_chargeStartThreshold = data.value(QStringLiteral("chargeStartThreshold")).toInt();
chargeStartThresholdSpin->setValue(m_chargeStartThreshold);
m_chargeStopThreshold = data.value(QStringLiteral("chargeStopThreshold")).toInt();
chargeStopThresholdSpin->setValue(m_chargeStopThreshold);
setChargeThresholdSupported(true);
} else {
qDebug() << "org.kde.powerdevil.chargethresholdhelper.getthreshold failed" << job->errorText();
setChargeThresholdSupported(false);
}
Q_EMIT changed(false);
}
void GeneralPage::configureNotifications()
......@@ -173,6 +207,22 @@ void GeneralPage::save()
PowerDevilSettings::self()->save();
if (chargeStartThresholdSpin->value() != m_chargeStartThreshold
|| chargeStopThresholdSpin->value() != m_chargeStopThreshold) {
KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.setthreshold"));
action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper"));
action.setArguments({
{QStringLiteral("chargeStartThreshold"), chargeStartThresholdSpin->value()},
{QStringLiteral("chargeStopThreshold"), chargeStopThresholdSpin->value()}
});
KAuth::ExecuteJob *job = action.execute();
job->exec();
if (!job->error()) {
m_chargeStartThreshold = chargeStartThresholdSpin->value();
m_chargeStopThreshold = chargeStopThresholdSpin->value();
}
}
// Notify Daemon
QDBusMessage call = QDBusMessage::createMethodCall("org.kde.Solid.PowerManagement", "/org/kde/Solid/PowerManagement",
"org.kde.Solid.PowerManagement", "refreshStatus");
......@@ -189,6 +239,17 @@ 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);
......
......@@ -44,7 +44,12 @@ private Q_SLOTS:
void onServiceUnregistered(const QString &service);
private:
void setChargeThresholdSupported(bool supported);
ErrorOverlay *m_errorOverlay = nullptr;
int m_chargeStartThreshold = 0;
int m_chargeStopThreshold = 100;
};
#endif /* GENERALPAGE_H */
......@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>510</width>
<height>276</height>
<width>926</width>
<height>524</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
......@@ -113,20 +113,91 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lowPeripheralLabel">
<property name="text">
<string>Low level for peripheral devices:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="lowPeripheralSpin">
<property name="suffix">
<string>%</string>
</property>
<property name="maximum">
<number>100</number>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<widget class="QLabel" name="batteryThresholdLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>16</height>
</size>
<property name="text">
<string>Charge Limit</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QLabel" name="batteryThresholdExplanation">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</spacer>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="9" column="1">
<item row="8" column="0" colspan="2">
<widget class="KMessageWidget" name="chargeStopThresholdMessage">
<property name="text">
<string>You might have to disconnect and re-connect the power source to start charging the battery again.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="chargeStartThresholdLabel">
<property name="text">
<string>Start charging only once below:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QSpinBox" name="chargeStartThresholdSpin">
<property name="specialValueText">
<string>Always charge when plugged in</string>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="maximum">
<number>99</number>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="pausePlayersLabel">
<property name="text">
<string>Pause media players when suspending:</string>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QCheckBox" name="pausePlayersCheckBox">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QPushButton" name="notificationsButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Maximum">
......@@ -145,34 +216,39 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="lowPeripheralLabel">
<item row="12" column="0">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Low level for peripheral devices:</string>
<string>Other Settings</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="lowPeripheralSpin">
<item row="9" column="0">
<widget class="QLabel" name="chargeStopThresholdLabel">
<property name="text">
<string>Stop charging at:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QSpinBox" name="chargeStopThresholdSpin">
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">