Parcourir la source

Initial Commit

Malte Veerman il y a 10 ans
commit
5a7af299b9

+ 56 - 0
CMakeLists.txt

@@ -0,0 +1,56 @@
+cmake_minimum_required(VERSION 3.0.0)
+
+project(fancontroller)
+
+include(FeatureSummary)
+
+option(NO_KF5_AUTH "Compile without KF5-Auth. Program will be unable to execute commands as root!")
+option(NO_SYSTEMD "Compile without Systemd support. Reduces functionality significantly!")
+
+#Find Qt5
+find_package(Qt5 COMPONENTS Widgets Qml Quick REQUIRED)
+include_directories(${Qt5Widgets_INCLUDE_DIRS} ${Qt5Qml_INCLUDE_DIRS} ${Qt5Quick_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
+add_definitions(${Qt5Widgets_DEFINITIONS})
+
+set(CMAKE_AUTOMOC ON)
+
+# Silence a warning
+cmake_policy(SET CMP0037 OLD)
+
+#Find KF5
+find_package(ECM)
+set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR})
+
+find_package(KF5Auth)
+find_package(Qt5DBus)
+
+feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
+
+#KF5_Auth
+if(ECM_FOUND AND KF5Auth_FOUND AND NOT NO_KF5_AUTH)
+    message(STATUS "Compiling with KF5_AUTH")
+
+    include(KDEInstallDirs)
+    include(KDECMakeSettings)
+    include(KDECompilerSettings)
+
+    add_subdirectory(helper)
+else(ECM_FOUND AND KF5Auth_FOUND AND NOT NO_KF5_AUTH)
+    message(STATUS "Compiling without KF5_AUTH")
+    set(NO_KF5_AUTH true)
+endif(ECM_FOUND AND KF5Auth_FOUND AND NOT NO_KF5_AUTH)
+
+#Systemd
+find_library(SYSTEMD_FOUND NAMES systemd)
+
+if(NOT NO_SYSTEMD AND SYSTEMD_FOUND)
+    message(STATUS "Compiling for Systemd")
+
+    include_directories(${Qt5DBus_INCLUDE_DIRS})
+else(NOT NO_SYSTEMD AND SYSTEMD_FOUND)
+    message(STATUS "Compiling without Systemd")
+    set(NO_SYSTEMD true)
+endif(NOT NO_SYSTEMD AND SYSTEMD_FOUND)
+
+add_subdirectory(share)
+add_subdirectory(fancontrol-gui)

+ 0 - 0
LICENSE


+ 25 - 0
fancontrol-gui/CMakeLists.txt

@@ -0,0 +1,25 @@
+set(Fancontrol-GUI_SRCS src/main.cpp src/loader.cpp src/hwmon.cpp src/sensors.cpp)
+
+set(LIBRARIES ${Qt5Widgets_LIBRARIES} ${Qt5Qml_Libraries} ${Qt5Quick_LIBRARIES})
+
+if(NO_KF5_AUTH)
+    add_definitions(-DNO_KF5_AUTH)
+else(NO_KF5_AUTH)
+    set(LIBRARIES ${LIBRARIES} KF5::Auth)
+endif(NO_KF5_AUTH)
+
+if(NO_SYSTEMD)
+    add_definitions(-DNO_SYSTEMD)
+else(NO_SYSTEMD)
+    set(Fancontrol-GUI_SRCS ${Fancontrol-GUI_SRCS} src/systemdcommunicator.cpp)
+    set(LIBRARIES ${LIBRARIES} Qt5::DBus)
+endif(NO_SYSTEMD)
+
+add_executable(fancontrol-gui ${Fancontrol-GUI_SRCS} ${RESOURCES})
+target_link_libraries(fancontrol-gui ${LIBRARIES})
+set_property(TARGET fancontrol-gui PROPERTY CXX_STANDARD 11)
+
+install(TARGETS fancontrol-gui RUNTIME DESTINATION bin)
+install(FILES other/fancontrol-gui.desktop DESTINATION ${CMAKE_INSTALL_PREFIX}/share/applications)
+install(FILES other/fancontrol-gui.svg DESTINATION ${ICON_INSTALL_DIR}/hicolor/scalable/apps)
+install(FILES qml/fancontrol-gui.qml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/fancontrol-gui/qml)

+ 5 - 0
fancontrol-gui/private.qrc

@@ -0,0 +1,5 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/main.qml</file>
+    </qresource>
+</RCC>

+ 84 - 0
fancontrol-gui/src/hwmon.cpp

@@ -0,0 +1,84 @@
+#include "hwmon.h"
+#include <QDir>
+#include <QTextStream>
+#include <QDebug>
+
+Hwmon::Hwmon(const QString &path) : QObject()
+{
+    m_path = path;
+    m_index = path.split('/').last().remove("hwmon").toInt();
+    QFile nameFile(path + "/name");
+    if (nameFile.open(QFile::ReadOnly))
+        m_name = QTextStream(&nameFile).readLine();
+    else
+        m_name = "Nameless hwmon";
+    emit pathChanged();
+
+    QDir dir(m_path);
+    QStringList entrys = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
+    foreach (QString entry, entrys)
+    {
+        QString str = entry;
+        int index = str.remove(QRegExp("\\D+")).toInt();
+        if (entry.contains("fan") && entry.contains("input"))
+        {
+            if (QFile::exists(m_path + "/pwm" + QString::number(index)))
+            {
+                PwmFan *newPwmFan = new PwmFan(this, index);
+                newPwmFan->setName(m_name + "/pwm" + QString::number(index));
+                connect(this, SIGNAL(sensorsUpdateNeeded()), newPwmFan, SLOT(update()));
+                m_pwmFans << newPwmFan;
+                emit pwmFansChanged();
+                m_fans << qobject_cast<Fan *>(newPwmFan);
+                emit fansChanged();
+            }
+            else
+            {
+                Fan *newFan = new Fan(this, index);
+                connect(this, SIGNAL(sensorsUpdateNeeded()), newFan, SLOT(update()));
+                m_fans << newFan;
+                emit fansChanged();
+            }
+        }
+
+        if (entry.contains("temp") && entry.contains("input"))
+        {
+            Temp *newTemp = new Temp(this, index);
+            newTemp->setName(m_name + "/" + newTemp->label());
+            connect(this, SIGNAL(sensorsUpdateNeeded()), newTemp, SLOT(update()));
+            m_temps << newTemp;
+            emit tempsChanged();
+        }
+    }
+//    qDebug() << "New Hwmon" << m_temps.size() << m_pwmFans.size();
+}
+
+QList<QObject *> Hwmon::fans() const
+{
+    QList<QObject *> list;
+    foreach (Fan *fan, m_fans)
+    {
+        list << qobject_cast<QObject *>(fan);
+    }
+    return list;
+}
+
+QList<QObject *> Hwmon::pwmFans() const
+{
+    QList<QObject *> list;
+    foreach (PwmFan *pwmFan, m_pwmFans)
+    {
+        list << qobject_cast<QObject *>(pwmFan);
+    }
+    return list;
+}
+
+QList<QObject *> Hwmon::temps() const
+{
+    QList<QObject *> list;
+    foreach (Temp *temp, m_temps)
+    {
+        list << qobject_cast<QObject *>(temp);
+    }
+    return list;
+}

+ 64 - 0
fancontrol-gui/src/hwmon.h

@@ -0,0 +1,64 @@
+#ifndef HWMON_H
+#define HWMON_H
+
+#include <QObject>
+
+#include "sensors.h"
+
+class Fan;
+class PwmFan;
+class Temp;
+
+class Hwmon : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString name READ name NOTIFY pathChanged)
+    Q_PROPERTY(QString path READ path NOTIFY pathChanged)
+    Q_PROPERTY(int index READ index NOTIFY pathChanged)
+    Q_PROPERTY(QList<QObject *> fans READ fans NOTIFY fansChanged)
+    Q_PROPERTY(QList<QObject *> pwmFans READ pwmFans NOTIFY pwmFansChanged)
+    Q_PROPERTY(QList<QObject *> temps READ temps NOTIFY tempsChanged)
+
+
+public:
+
+    explicit Hwmon(const QString &);
+
+    QString name() const { return m_name; }
+    QString path() const { return m_path; }
+    int index() const { return m_index; }
+    QList<QObject *> fans() const;
+    QList<QObject *> pwmFans() const;
+    QList<QObject *> temps() const;
+    Fan * fan(int i) const { return m_fans.value(i, nullptr); }
+    PwmFan * pwmFan(int i) const { return m_pwmFans.value(i, nullptr); }
+    Temp * temp(int i) const { return m_temps.value(i, nullptr); }
+
+
+public slots:
+
+    void updateConfig() { emit configUpdateNeeded(); }
+    void updateSensors() { emit sensorsUpdateNeeded(); }
+
+
+signals:
+
+    void pathChanged();
+    void fansChanged();
+    void pwmFansChanged();
+    void tempsChanged();
+    void configUpdateNeeded();
+    void sensorsUpdateNeeded();
+
+
+protected:
+
+    QString m_name;
+    QString m_path;
+    int m_index;
+    QList<Fan *> m_fans;
+    QList<PwmFan *> m_pwmFans;
+    QList<Temp *> m_temps;
+};
+
+#endif // HWMON_H

+ 467 - 0
fancontrol-gui/src/loader.cpp

@@ -0,0 +1,467 @@
+#include "loader.h"
+
+#include <QFile>
+#include <QDir>
+#include <QTextStream>
+#include <QDebug>
+
+#ifndef NO_KF5_AUTH
+#include <KF5/KAuth/kauthexecutejob.h>
+
+using namespace KAuth;
+#endif
+
+#define HWMON_PATH "/sys/class/hwmon"
+
+Loader::Loader(QObject *parent) : QObject(parent)
+{
+    m_configUrl = QUrl::fromLocalFile("/etc/fancontrol");
+    m_interval = 10;
+    m_error = "Success";
+    parseHwmons();
+    open(m_configUrl);
+    m_timer.setSingleShot(false);
+    m_timer.start(1);
+    connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateSensors()));
+}
+
+void Loader::parseHwmons()
+{
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        hwmon->deleteLater();
+    }
+    m_hwmons.clear();
+
+    QDir hwmonDir(HWMON_PATH);
+    QStringList list;
+    if (hwmonDir.isReadable())
+    {
+        list = hwmonDir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot);
+    }
+    else
+    {
+        qDebug() << HWMON_PATH << " is not readable!";
+        return;
+    }
+
+    foreach (QString hwmonPath, list)
+    {
+        Hwmon *hwmon = new Hwmon(QFile::symLinkTarget(hwmonDir.absoluteFilePath(hwmonPath)));
+        connect(hwmon, SIGNAL(configUpdateNeeded()), this, SLOT(createConfigFile()));
+        connect(this, SIGNAL(sensorsUpdateNeeded()), hwmon, SLOT(updateSensors()));
+        m_hwmons << hwmon;
+
+//        qDebug() << m_devnames.value(hwmon);
+    }
+    emit hwmonsChanged();
+}
+
+void Loader::open(const QUrl &url)
+{
+    QString fileName = url.toLocalFile();
+    QFile file(fileName);
+    QTextStream stream;
+    QString string;
+    QStringList lines;
+
+    if (file.open(QFile::ReadOnly | QFile::Text))
+    {
+        stream.setDevice(&file);
+        m_error = "Success";
+        emit errorChanged();
+    }
+#ifdef NO_KF5_AUTH
+    else
+    {
+        m_error = file.errorString();
+        emit errorChanged();
+        return;
+    }
+#else
+    else if (file.exists())
+    {
+        Action action("fancontrol.gui.helper.read");
+        action.setHelperId("fancontrol.gui.helper");
+        QVariantMap map;
+        map["filename"] = fileName;
+        action.setArguments(map);
+        ExecuteJob *reply = action.execute();
+        if (!reply->exec())
+        {
+            m_error = reply->errorString();
+            emit errorChanged();
+            return;
+        }
+        else
+        {
+            m_error = "Success";
+            emit errorChanged();
+            string = reply->data()["content"].toString();
+            stream.setString(&string);
+        }
+    }
+#endif
+    m_configFile = stream.readAll();
+    emit configFileChanged();
+    m_configUrl = url;
+    emit configUrlChanged();
+
+    stream.seek(0);
+
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *pwmFan, hwmon->pwmFans())
+        {
+            qobject_cast<PwmFan *>(pwmFan)->reset();
+        }
+    }
+    
+    do
+    {
+        QString line(stream.readLine());
+        if (line.startsWith('#')) continue;
+        int offset = line.indexOf('#');
+        if (offset != -1) line.truncate(offset-1);
+        line = line.simplified();
+        lines << line;
+    }
+    while(!stream.atEnd());
+
+    foreach (QString line, lines)
+    {
+        if (line.startsWith("INTERVAL="))
+        {
+            setInterval(line.remove("INTERVAL=").toInt());
+        }
+        else if (line.startsWith("FCTEMPS="))
+        {
+            QStringList fctemps = line.split(' ');
+            if (!fctemps.isEmpty())
+                fctemps.first().remove("FCTEMPS=");
+            foreach (QString fctemp, fctemps)
+            {
+                QString pwm = fctemp.split('=').at(0);
+                QString temp = fctemp.split('=').at(1);
+                int pwmSensorIndex = getSensorNumber(pwm);
+                int tempSensorIndex = getSensorNumber(temp);
+                Hwmon *pwmHwmon = m_hwmons.value(getHwmonNumber(pwm), nullptr);
+
+                if (pwmHwmon)
+                {
+                    Hwmon *tempHwmon = m_hwmons.value(getHwmonNumber(temp), nullptr);
+                    PwmFan *pwmPointer = pwmHwmon->pwmFan(pwmSensorIndex);
+                    if (tempHwmon)
+                    {
+                        Temp *tempPointer = tempHwmon->temp(tempSensorIndex);
+
+                        if (pwmPointer)
+                        {
+                            pwmPointer->setTemp(tempPointer);
+                            pwmPointer->setMinPwm(0);
+                        }
+                    }
+                    else if (pwmPointer)
+                        pwmPointer->setTemp(nullptr);
+                }
+            }
+        }
+        else if (line.startsWith("MINTEMP="))
+        {
+            QStringList mintemps = line.split(' ');
+            if (!mintemps.isEmpty())
+                mintemps.first().remove("MINTEMP=");
+            foreach (QString mintemp, mintemps)
+            {
+                QString pwm = mintemp.split('=').at(0);
+                int value = mintemp.split('=').at(1).toInt();
+                int pwmHwmon = getHwmonNumber(pwm);
+                int pwmSensor = getSensorNumber(pwm);
+                PwmFan *pwmPointer = m_hwmons.value(pwmHwmon, nullptr)->pwmFan(pwmSensor);
+                if (pwmPointer)
+                    pwmPointer->setMinTemp(value);
+            }
+        }
+        else if (line.startsWith("MAXTEMP="))
+        {
+            QStringList maxtemps = line.split(' ');
+            if (!maxtemps.isEmpty())
+                maxtemps.first().remove("MAXTEMP=");
+            foreach (QString maxtemp, maxtemps)
+            {
+                QString pwm = maxtemp.split('=').at(0);
+                int value = maxtemp.split('=').at(1).toInt();
+                int pwmHwmon = getHwmonNumber(pwm);
+                int pwmSensor = getSensorNumber(pwm);
+                PwmFan *pwmPointer = m_hwmons.value(pwmHwmon, nullptr)->pwmFan(pwmSensor);
+                if (pwmPointer)
+                    pwmPointer->setMaxTemp(value);
+            }
+        }
+        else if (line.startsWith("MINSTART="))
+        {
+            QStringList minstarts = line.split(' ');
+            if (!minstarts.isEmpty())
+                minstarts.first().remove("MINSTART=");
+            foreach (QString minstart, minstarts)
+            {
+                QString pwm = minstart.split('=').at(0);
+                int value = minstart.split('=').at(1).toInt();
+                int pwmHwmon = getHwmonNumber(pwm);
+                int pwmSensor = getSensorNumber(pwm);
+                PwmFan *pwmPointer = m_hwmons.value(pwmHwmon, nullptr)->pwmFan(pwmSensor);
+                if (pwmPointer)
+                    pwmPointer->setMinStart(value);
+            }
+        }
+        else if (line.startsWith("MINSTOP="))
+        {
+            QStringList minstops = line.split(' ');
+            if (!minstops.isEmpty())
+                minstops.first().remove("MINSTOP=");
+            foreach (QString minstop, minstops)
+            {
+                QString pwm = minstop.split('=').at(0);
+                int value = minstop.split('=').at(1).toInt();
+                int pwmHwmon = getHwmonNumber(pwm);
+                int pwmSensor = getSensorNumber(pwm);
+                PwmFan *pwmPointer = m_hwmons.value(pwmHwmon, nullptr)->pwmFan(pwmSensor);
+                if (pwmPointer)
+                    pwmPointer->setMinStop(value);
+            }
+        }
+        else if (line.startsWith("MINPWM="))
+        {
+            QStringList minpwms = line.split(' ');
+            if (!minpwms.isEmpty())
+                minpwms.first().remove("MINPWM=");
+            foreach (QString minpwm, minpwms)
+            {
+                QString pwm = minpwm.split('=').at(0);
+                int value = minpwm.split('=').at(1).toInt();
+                int pwmHwmon = getHwmonNumber(pwm);
+                int pwmSensor = getSensorNumber(pwm);
+                PwmFan *pwmPointer = m_hwmons.value(pwmHwmon, nullptr)->pwmFan(pwmSensor);
+                if (pwmPointer)
+                    pwmPointer->setMinPwm(value);
+            }
+        }
+        else if (line.startsWith("MAXPWM="))
+        {
+            QStringList maxpwms = line.split(' ');
+            if (!maxpwms.isEmpty())
+                maxpwms.first().remove("MAXPWM=");
+            foreach (QString maxpwm, maxpwms)
+            {
+                QString pwm = maxpwm.split('=').at(0);
+                int value = maxpwm.split('=').at(1).toInt();
+                int pwmHwmon = getHwmonNumber(pwm);
+                int pwmSensor = getSensorNumber(pwm);
+                PwmFan *pwmPointer = m_hwmons.value(pwmHwmon, nullptr)->pwmFan(pwmSensor);
+                if (pwmPointer)
+                    pwmPointer->setMaxPwm(value);
+            }
+        }
+    }
+}
+
+void Loader::save(const QUrl &url)
+{  
+    QString fileName = url.isEmpty() ? m_configUrl.toLocalFile() : url.toLocalFile();
+    QFile file(fileName);
+    
+    if (file.open(QFile::WriteOnly | QFile::Text))
+    {
+        QTextStream stream(&file);
+        stream << m_configFile;
+        qDebug() << m_configFile;
+    }
+#ifdef NO_KF5_AUTH
+    else
+    {
+        m_error = file.errorString();
+        emit errorChanged();
+    }
+#else
+    else
+    {
+        Action action("fancontrol.gui.helper.write");
+        action.setHelperId("fancontrol.gui.helper");
+        QVariantMap map;
+        map["content"] = m_configFile;
+
+        map["filename"] = fileName;
+        action.setArguments(map);
+        ExecuteJob *reply = action.execute();
+        if (!reply->exec())
+        {
+            m_error = reply->errorString();
+            emit errorChanged();
+        }
+    }
+#endif
+}
+
+void Loader::createConfigFile()
+{
+    m_configFile = "INTERVAL=" + QString::number(m_interval) + "\n";
+
+    m_configFile += "DEVPATH=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        QString sanitizedPath = hwmon->path();
+        sanitizedPath.remove(QRegExp("^/sys/"));
+        sanitizedPath.remove(QRegExp("/hwmon/hwmon\\d\\s*$"));
+        m_configFile += "hwmon" + QString::number(hwmon->index()) + "=" + sanitizedPath + " ";
+    }
+    m_configFile += "\n";
+
+    m_configFile += "DEVNAME=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        m_configFile += "hwmon" + QString::number(hwmon->index()) + "=" + hwmon->name().split('.').first() + " ";
+    }
+    m_configFile += "\n";
+
+    m_configFile += "FCTEMPS=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp() && pwmFan->temp())
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += "hwmon" + QString::number(pwmFan->temp()->parent()->index()) + "/";
+                m_configFile += "temp" + QString::number(pwmFan->temp()->index()) + "_input ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    m_configFile += "FCFANS=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp())
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "fan" + QString::number(pwmFan->index()) + "_input ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    m_configFile += "MINTEMP=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp())
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += QString::number(pwmFan->minTemp()) + " ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    m_configFile += "MAXTEMP=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp())
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += QString::number(pwmFan->maxTemp()) + " ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    m_configFile += "MINSTART=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp())
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += QString::number(pwmFan->minStart()) + " ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    m_configFile += "MINSTOP=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp())
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += QString::number(pwmFan->minStop()) + " ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    m_configFile += "MINPWM=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp() && pwmFan->minPwm() != 0)
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += QString::number(pwmFan->minPwm()) + " ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    m_configFile += "MAXPWM=";
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        foreach (QObject *object, hwmon->pwmFans())
+        {
+            PwmFan *pwmFan = qobject_cast<PwmFan *>(object);
+            if (pwmFan->hasTemp() && pwmFan->maxPwm() != 255)
+            {
+                m_configFile += "hwmon" + QString::number(hwmon->index()) + "/";
+                m_configFile += "pwm" + QString::number(pwmFan->index()) + "=";
+                m_configFile += QString::number(pwmFan->maxPwm()) + " ";
+            }
+        }
+    }
+    m_configFile += "\n";
+
+    emit configFileChanged();
+}
+
+QList<QObject *> Loader::hwmons() const
+{
+    QList<QObject *> list;
+    foreach (Hwmon *hwmon, m_hwmons)
+    {
+        list << qobject_cast<QObject *>(hwmon);
+    }
+    return list;
+}

+ 72 - 0
fancontrol-gui/src/loader.h

@@ -0,0 +1,72 @@
+#ifndef LOADER_H
+#define LOADER_H
+
+#include <QObject>
+#include <QStringList>
+#include <QUrl>
+#include <QTimer>
+
+#include "hwmon.h"
+
+
+class Loader : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QUrl configUrl READ configUrl WRITE setConfigUrl NOTIFY configUrlChanged)
+    Q_PROPERTY(QString configFile READ configFile NOTIFY configFileChanged)
+    Q_PROPERTY(QList<QObject *> hwmons READ hwmons NOTIFY hwmonsChanged)
+    Q_PROPERTY(int interval READ interval WRITE setInterval NOTIFY intervalChanged)
+    Q_PROPERTY(QString error READ error NOTIFY errorChanged)
+    
+
+public:
+
+    explicit Loader(QObject *parent = 0);
+
+    Q_INVOKABLE void parseHwmons();
+    Q_INVOKABLE void open(const QUrl & = QUrl());
+    Q_INVOKABLE void save(const QUrl & = QUrl());
+    QUrl configUrl() const { return m_configUrl; }
+    void setConfigUrl(const QUrl &url) { open(url); if (m_configUrl != url) { m_configUrl = url; emit configUrlChanged(); } }
+    QString configFile() const { return m_configFile; }
+    QList<QObject *> hwmons() const;
+    int interval() { return m_interval; }
+    void setInterval(int interval) { if (interval != m_interval) { m_interval = interval; emit intervalChanged(m_interval*1000); createConfigFile(); } }
+    Hwmon * hwmon(int i) { return m_hwmons.value(i, nullptr); }
+    QString error() const { return m_error; }
+    
+    static int getHwmonNumber(const QString &str) { return str.split('/').value(0).remove("hwmon").toInt(); }
+    static int getSensorNumber(const QString &str) { return str.split('/').value(1).remove(QRegExp("pwm|fan|temp|_input")).toInt() - 1; }
+
+public slots:
+
+    void updateSensors() { emit sensorsUpdateNeeded(); }
+
+
+protected slots:
+
+    void createConfigFile();
+
+
+protected:
+
+    bool m_parsed;
+    int m_interval;
+    QList<Hwmon *> m_hwmons;
+    QUrl m_configUrl;
+    QString m_configFile;
+    QString m_error;
+    QTimer m_timer;
+
+
+signals:
+
+    void configUrlChanged();
+    void configFileChanged();
+    void hwmonsChanged();
+    void intervalChanged(int);
+    void errorChanged();
+    void sensorsUpdateNeeded();
+};
+
+#endif // LOADER_H

+ 30 - 0
fancontrol-gui/src/main.cpp

@@ -0,0 +1,30 @@
+#include <QApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+#include <QQuickView>
+
+#include "loader.h"
+
+#ifndef NO_SYSTEMD
+#include "systemdcommunicator.h"
+#endif
+
+int main(int argc, char *argv[])
+{
+    QApplication app(argc, argv);
+
+    QQmlApplicationEngine engine;
+    Loader loader;
+    engine.rootContext()->setContextProperty("loader", &loader);
+#ifndef NO_SYSTEMD
+    SystemdCommunicator com;
+    engine.rootContext()->setContextProperty("systemdCom", &com);
+#endif
+    qmlRegisterType<Hwmon>();
+    qmlRegisterType<Fan>();
+    qmlRegisterType<PwmFan>();
+    qmlRegisterType<Temp>();
+    engine.load(QUrl(QStringLiteral("file:///usr/share/fancontrol-gui/qml/fancontrol-gui.qml")));
+
+    return app.exec();
+}

+ 140 - 0
fancontrol-gui/src/sensors.cpp

@@ -0,0 +1,140 @@
+#include "sensors.h"
+#include <QFile>
+#include <QDir>
+
+Sensor::Sensor(Hwmon *parent, uint index) : QObject(parent)
+{
+    m_parent = parent;
+    m_index = index;
+    emit indexChanged();
+}
+
+Fan::Fan(Hwmon *parent, uint index) : Sensor(parent, index)
+{
+    if (QDir(parent->path()).isReadable())
+    {
+        QFile *rpmFile = new QFile(parent->path() + "/fan" + QString::number(index) + "_input", this);
+
+        if (rpmFile->open(QFile::ReadOnly))
+        {
+            m_rpmStream.setDevice(rpmFile);
+            m_rpmStream >> m_rpm;
+            emit rpmChanged();
+
+            setName("fan"+ QString::number(index));
+        }
+        else
+        {
+            qDebug() << "Can't open rpmFile " << parent->path() + "/fan" + QString::number(index) + "_input";
+        }
+    }
+}
+
+void Fan::update()
+{
+    m_rpmStream.seek(0);
+    m_rpmStream >> m_rpm;
+    emit rpmChanged();
+}
+
+
+
+PwmFan::PwmFan(Hwmon *parent, uint index) : Fan(parent, index)
+{
+    m_temp = nullptr;
+    m_hasTemp = false;
+    m_minTemp = 0;
+    m_maxTemp = 100;
+    m_minPwm = 255;
+    m_maxPwm = 255;
+    m_minStart = 255;
+    m_minStop = 255;
+
+    connect(this, SIGNAL(tempChanged()), parent, SLOT(updateConfig()));
+    connect(this, SIGNAL(hasTempChanged()), parent, SLOT(updateConfig()));
+    connect(this, SIGNAL(minTempChanged()), parent, SLOT(updateConfig()));
+    connect(this, SIGNAL(maxTempChanged()), parent, SLOT(updateConfig()));
+    connect(this, SIGNAL(minPwmChanged()), parent, SLOT(updateConfig()));
+    connect(this, SIGNAL(maxPwmChanged()), parent, SLOT(updateConfig()));
+    connect(this, SIGNAL(minStartChanged()), parent, SLOT(updateConfig()));
+    connect(this, SIGNAL(minStopChanged()), parent, SLOT(updateConfig()));
+
+    if (QDir(parent->path()).isReadable())
+    {
+        QFile *pwmFile = new QFile(parent->path() + "/pwm" + QString::number(index), this);
+
+        if (pwmFile->open(QFile::ReadOnly)) //TODO ReadWrite with su rights
+        {
+            m_pwmStream.setDevice(pwmFile);
+            m_pwmStream >> m_pwm;
+            emit pwmChanged();
+        }
+        else
+        {
+            qDebug() << "Can't open pwmFile " << parent->path() + "/pwm" + QString::number(index);
+        }
+    }
+}
+
+void PwmFan::update()
+{
+    Fan::update();
+    m_pwmStream.seek(0);
+    m_pwmStream >> m_pwm;
+    emit pwmChanged();
+}
+
+void PwmFan::writePwm()
+{
+    m_pwmStream << m_pwm;
+}
+
+void PwmFan::reset()
+{
+    setTemp(nullptr);
+    setMinTemp(0);
+    setMaxTemp(100);
+    setMinPwm(255);
+    setMaxPwm(255);
+    setMinStart(255);
+    setMinStop(255);
+}
+
+
+Temp::Temp(Hwmon *parent, uint index) : Sensor(parent, index)
+{
+    if (QDir(parent->path()).isReadable())
+    {
+        QFile *valueFile = new QFile(parent->path() + "/temp" + QString::number(index) + "_input", this);
+        QFile labelFile(parent->path() + "/temp" + QString::number(index) + "_label");
+
+        if (valueFile->open(QFile::ReadOnly))
+        {
+            m_valueStream.setDevice(valueFile);
+            m_valueStream >> m_value;
+            m_value /= 1000;
+            emit valueChanged();
+        }
+        else
+            qDebug() << "Can't open valueFile " << parent->path() + "/temp" + QString::number(index) + "_input";
+
+        if (labelFile.open(QFile::ReadOnly))
+        {
+            m_label = QTextStream(&labelFile).readLine();
+        }
+        else
+        {
+            m_label = "temp" + QString::number(index);
+            qDebug() << "Can't open labelFile " << parent->path() + "/temp" + QString::number(index) + "_label";
+        }
+        emit labelChanged();
+    }
+}
+
+void Temp::update()
+{
+    m_valueStream.seek(0);
+    m_valueStream >> m_value;
+    m_value /= 1000;
+    emit valueChanged();
+}

+ 184 - 0
fancontrol-gui/src/sensors.h

@@ -0,0 +1,184 @@
+#ifndef SENSORS_H
+#define SENSORS_H
+
+#include <QObject>
+#include <QTextStream>
+#include <QDebug>
+
+#include "hwmon.h"
+
+class Hwmon;
+
+class Sensor : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(uint index READ index NOTIFY indexChanged)
+    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
+    Q_PROPERTY(Hwmon * parent READ parent CONSTANT)
+
+public:
+
+    explicit Sensor(Hwmon *parent, uint index);
+
+    QString name() const { return m_name; }
+    void setName(const QString &name) { if (name != m_name) { m_name = name; emit nameChanged(); } }
+    Hwmon * parent() const { return m_parent; }
+    uint index() const { return m_index; }
+
+
+public slots:
+
+    virtual void update() = 0;
+
+
+signals:
+
+    void nameChanged();
+    void indexChanged();
+
+
+protected:
+
+    QString m_name;
+    Hwmon *m_parent;
+    uint m_index;
+};
+
+
+class Temp : public Sensor
+{
+    Q_OBJECT
+    Q_PROPERTY(QString label READ label NOTIFY labelChanged)
+    Q_PROPERTY(int value READ value NOTIFY valueChanged)
+
+public:
+
+    explicit Temp(Hwmon *parent, uint index);
+
+    QString label() const { return m_label; }
+    int value() const { return m_value; }
+
+
+public slots:
+
+    void update();
+
+
+signals:
+
+    void labelChanged();
+    void valueChanged();
+
+
+protected:
+
+    QString m_label;
+    int m_value;
+    QTextStream m_valueStream;
+};
+
+
+class Fan : public Sensor
+{
+    Q_OBJECT
+    Q_PROPERTY(int rpm READ rpm NOTIFY rpmChanged)
+
+public:
+
+    explicit Fan(Hwmon *parent, uint index);
+
+    int rpm() const { return m_rpm; }
+
+    virtual int pwm() const { return 0; }
+    virtual void setPwm(int) { qDebug() << "setPwm(int) is not implemented in standard Fan"; }
+
+
+signals:
+
+    void rpmChanged();
+
+
+public slots:
+
+    void update();
+
+
+protected:
+
+    int m_rpm;
+    QTextStream m_rpmStream;
+};
+
+
+class PwmFan : public Fan
+{
+    Q_OBJECT
+    Q_PROPERTY(int pwm READ pwm WRITE setPwm NOTIFY pwmChanged)
+    Q_PROPERTY(Temp * temp READ temp WRITE setTemp NOTIFY tempChanged)
+    Q_PROPERTY(bool hasTemp READ hasTemp WRITE setHasTemp NOTIFY hasTempChanged)
+    Q_PROPERTY(int minTemp READ minTemp WRITE setMinTemp NOTIFY minTempChanged)
+    Q_PROPERTY(int maxTemp READ maxTemp WRITE setMaxTemp NOTIFY maxTempChanged)
+    Q_PROPERTY(int minPwm READ minPwm WRITE setMinPwm NOTIFY minPwmChanged)
+    Q_PROPERTY(int maxPwm READ maxPwm WRITE setMaxPwm NOTIFY maxPwmChanged)
+    Q_PROPERTY(int minStart READ minStart WRITE setMinStart NOTIFY minStartChanged)
+    Q_PROPERTY(int minStop READ minStop WRITE setMinStop NOTIFY minStopChanged)
+
+public:
+
+    explicit PwmFan(Hwmon *parent, uint index);
+
+    int pwm() const { return m_pwm; }
+    void setPwm(int pwm) { if (pwm != m_pwm) { m_pwm = pwm; emit pwmChanged(); writePwm(); } }
+    Temp * temp() const { return m_temp; }
+    bool hasTemp() const { return m_hasTemp; }
+    int minTemp() const { return m_minTemp; }
+    int maxTemp() const { return m_maxTemp; }
+    int minPwm() const { return m_minPwm; }
+    int maxPwm() const { return m_maxPwm; }
+    int minStart() const { return m_minStart; }
+    int minStop() const { return m_minStop; }
+    void setTemp(Temp *temp) { setHasTemp(temp != nullptr); if (temp != m_temp) { m_temp = temp; emit tempChanged(); } }
+    void setHasTemp(bool hasTemp) { if (hasTemp != m_hasTemp) { m_hasTemp = hasTemp; emit hasTempChanged(); } }
+    void setMinTemp(int minTemp) { if (minTemp != m_minTemp) { m_minTemp = minTemp; emit minTempChanged(); } }
+    void setMaxTemp(int maxTemp) { if (maxTemp != m_maxTemp) { m_maxTemp = maxTemp; emit maxTempChanged(); } }
+    void setMinPwm(int minPwm) { if (minPwm != m_minPwm) { m_minPwm = minPwm; emit minPwmChanged(); } }
+    void setMaxPwm(int maxPwm) { if (maxPwm != m_maxPwm) { m_maxPwm = maxPwm; emit maxPwmChanged(); } }
+    void setMinStart(int minStart) { if (minStart != m_minStart) { m_minStart = minStart; emit minStartChanged(); } }
+    void setMinStop(int minStop) { if (minStop != m_minStop) { m_minStop = minStop; emit minStopChanged(); } }
+    void reset();
+
+
+signals:
+
+    void pwmChanged();
+    void tempChanged();
+    void hasTempChanged();
+    void minTempChanged();
+    void maxTempChanged();
+    void minPwmChanged();
+    void maxPwmChanged();
+    void minStartChanged();
+    void minStopChanged();
+
+
+protected slots:
+
+    void update();
+    void writePwm();
+
+
+protected:
+
+    int m_pwm;
+    QTextStream m_pwmStream;
+    Temp *m_temp;
+    bool m_hasTemp;
+    int m_minTemp;
+    int m_maxTemp;
+    int m_minPwm;
+    int m_maxPwm;
+    int m_minStart;
+    int m_minStop;
+};
+
+#endif // SENSORS_H

+ 155 - 0
fancontrol-gui/src/systemdcommunicator.cpp

@@ -0,0 +1,155 @@
+#include "systemdcommunicator.h"
+
+#ifndef NO_KF5_AUTH
+#include <KF5/KAuth/kauthexecutejob.h>
+
+using namespace KAuth;
+#endif
+
+#include <QDebug>
+#include <QVariant>
+#include <QScopedPointer>
+
+
+SystemdCommunicator::SystemdCommunicator(QObject *parent) : QObject(parent)
+{
+    m_serviceName = "fancontrol";
+    m_error = "Success";
+
+    m_managerInterface = new QDBusInterface("org.freedesktop.systemd1",
+                                     "/org/freedesktop/systemd1",
+                                     "org.freedesktop.systemd1.Manager",
+                                     QDBusConnection::systemBus(),
+                                     this);
+    serviceExists();
+}
+
+void SystemdCommunicator::setServiceName(const QString &name)
+{
+    if (name != m_serviceName)
+    {
+        m_serviceName = name;
+        emit serviceNameChanged();
+    }
+}
+
+bool SystemdCommunicator::serviceExists()
+{
+    QDBusMessage dbusreply;
+
+    if (m_managerInterface->isValid())
+        dbusreply = m_managerInterface->call(QDBus::AutoDetect, "ListUnitFiles");
+
+    if (dbusreply.type() == QDBusMessage::ErrorMessage)
+    {
+        m_error = dbusreply.errorMessage();
+        emit errorChanged();
+        return false;
+    }
+    SystemdUnitFileList list = qdbus_cast<SystemdUnitFileList>(dbusreply.arguments().first());
+
+    foreach (const SystemdUnitFile &unitFile, list)
+    {
+        if (unitFile.path.contains(m_serviceName + ".service"))
+        {
+            QList<QVariant> arguments;
+            arguments << QVariant(m_serviceName + ".service");
+            dbusreply = m_managerInterface->callWithArgumentList(QDBus::AutoDetect, "LoadUnit", arguments);
+            if (dbusreply.type() == QDBusMessage::ErrorMessage)
+            {
+                m_error = dbusreply.errorMessage();
+                emit errorChanged();
+                return false;
+            }
+            m_servicePath = qdbus_cast<QDBusObjectPath>(dbusreply.arguments().first()).path();
+
+            if (m_serviceInterface)
+                m_serviceInterface->deleteLater();
+
+            m_serviceInterface = new QDBusInterface("org.freedesktop.systemd1",
+                                                    m_servicePath,
+                                                    "org.freedesktop.systemd1.Unit",
+                                                    QDBusConnection::systemBus(),
+                                                    this);
+            m_error = "Success";
+            emit errorChanged();
+            return true;
+        }
+    }
+
+    m_error = "Service " + m_serviceName + " doesn't exist";
+    emit errorChanged();
+    return false;
+}
+
+bool SystemdCommunicator::serviceActive()
+{
+    if (m_serviceInterface->isValid())
+    {
+        if (m_serviceInterface->property("ActiveState").toString() == "active")
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+void SystemdCommunicator::dbusAction(const QString &method, const QList<QVariant> &arguments)
+{
+    QDBusMessage dbusreply;
+
+    if (m_managerInterface->isValid())
+        dbusreply = m_managerInterface->callWithArgumentList(QDBus::AutoDetect, method, arguments);
+
+    if (dbusreply.type() == QDBusMessage::ErrorMessage)
+    {
+#ifndef NO_KF5_AUTH
+        if (dbusreply.errorMessage() == "Interactive authentication required.")
+        {
+            Action action("fancontrol.gui.helper.dbusaction");
+            action.setHelperId("fancontrol.gui.helper");
+            QVariantMap map;
+            map["method"] = method;
+            map["arguments"] = arguments;
+            action.setArguments(map);
+
+            ExecuteJob *reply = action.execute();
+
+            if (!reply->exec())
+            {
+                m_error = reply->errorString();
+                emit errorChanged();
+            }
+            else
+            {
+                m_error = "Success";
+                emit errorChanged();
+            }
+            return;
+        }
+#endif
+        m_error = dbusreply.errorMessage();
+        emit errorChanged();
+    }
+    else
+    {
+        m_error = "Success";
+        emit errorChanged();
+    }
+}
+
+QDBusArgument& operator <<(QDBusArgument &argument, const SystemdUnitFile &unitFile)
+{
+    argument.beginStructure();
+    argument << unitFile.path << unitFile.state;
+    argument.endStructure();
+    return argument;
+}
+
+const QDBusArgument& operator >>(const QDBusArgument &argument, SystemdUnitFile &unitFile)
+{
+    argument.beginStructure();
+    argument >> unitFile.path >> unitFile.state;
+    argument.endStructure();
+    return argument;
+}

+ 55 - 0
fancontrol-gui/src/systemdcommunicator.h

@@ -0,0 +1,55 @@
+#ifndef SYSTEMDCOMMUNICATOR_H
+#define SYSTEMDCOMMUNICATOR_H
+
+#include <QObject>
+#include <QtDBus/QtDBus>
+
+class SystemdCommunicator : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString serviceName READ serviceName WRITE setServiceName NOTIFY serviceNameChanged)
+    Q_PROPERTY(QString error READ error NOTIFY errorChanged)
+    Q_PROPERTY(bool serviceExists READ serviceExists NOTIFY serviceNameChanged)
+
+public:
+
+    explicit SystemdCommunicator(QObject *parent = 0);
+
+    QString serviceName() const { return m_serviceName; }
+    void setServiceName(const QString &name);
+    bool serviceExists();
+    Q_INVOKABLE bool serviceActive();
+    void setServiceActive(bool active);
+    QString error() const { return m_error; }
+    Q_INVOKABLE void dbusAction(const QString &method, const QList<QVariant> &arguments);
+
+
+signals:
+
+    void serviceNameChanged();
+    void errorChanged();
+
+
+protected:
+
+    QString m_serviceName;
+    QString m_servicePath;
+    QString m_error;
+    QDBusInterface *m_managerInterface;
+    QDBusInterface *m_serviceInterface;
+};
+
+typedef struct
+{
+    QString path;
+    QString state;
+} SystemdUnitFile;
+
+Q_DECLARE_METATYPE(SystemdUnitFile)
+typedef QList<SystemdUnitFile> SystemdUnitFileList;
+Q_DECLARE_METATYPE(SystemdUnitFileList)
+
+QDBusArgument &operator<<(QDBusArgument &argument, const SystemdUnitFile &unitFile);
+const QDBusArgument &operator>>(const QDBusArgument &argument, SystemdUnitFile &unitFile);
+
+#endif // SYSTEMDCOMMUNICATOR_H

+ 12 - 0
helper/CMakeLists.txt

@@ -0,0 +1,12 @@
+kauth_install_helper_files(Fancontrol-GUIHelper fancontrol.gui.helper root)
+kauth_install_actions(fancontrol.gui.helper fancontrol-gui.actions)
+
+set(LIBRARIES KF5::Auth)
+
+if(NOT NO_SYSTEMD)
+    set(LIBRARIES ${LIBRARIES} Qt5::DBus)
+endif(NOT NO_SYSTEMD)
+
+add_executable(Fancontrol-GUIHelper src/helper.cpp)
+target_link_libraries(Fancontrol-GUIHelper ${LIBRARIES})
+install(TARGETS Fancontrol-GUIHelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})

+ 21 - 0
helper/fancontrol-gui.actions

@@ -0,0 +1,21 @@
+[Domain]
+Name=Fancontrol-GUI
+URL=http://github.com/maldela/fancontrol-gui
+
+[fancontrol.gui.helper.write]
+Name=Save your configfile
+Description=Save the fancontrol configuration file
+Policy=auth_admin
+Persistence=session
+
+[fancontrol.gui.helper.read]
+Name=Read configfile
+Description=Read the fancontrol configuration file
+Policy=auth_admin
+Persistence=session
+
+[fancontrol.gui.helper.dbusaction]
+Name=Manipulate systemd over DBus
+Description=Manipulate systemd to start, stop or restart the fancontrol service
+Policy=auth_admin
+Persistence=session

+ 86 - 0
helper/src/helper.cpp

@@ -0,0 +1,86 @@
+#include "helper.h"
+
+#include <QFile>
+#include <QTextStream>
+
+#ifndef NO_SYSTEMD
+#include <QtDBus>
+
+ActionReply Helper::dbusaction(const QVariantMap &arguments)
+{
+    QString method = arguments["method"].toString();
+    QList<QVariant> argsForCall = arguments["arguments"].toList();
+
+    ActionReply reply;
+
+    QDBusConnection systembus = QDBusConnection::systemBus();
+
+    QDBusInterface *iface = new QDBusInterface ("org.freedesktop.systemd1",
+                                                "/org/freedesktop/systemd1",
+                                                "org.freedesktop.systemd1.Manager",
+                                                systembus,
+                                                this);
+
+    QDBusMessage dbusreply;
+
+    if (iface->isValid())
+        dbusreply = iface->callWithArgumentList(QDBus::AutoDetect, method, argsForCall);
+    delete iface;
+
+    if (method != "Reexecute")
+    {
+        if (dbusreply.type() == QDBusMessage::ErrorMessage)
+        {
+            reply.setErrorCode(ActionReply::DBusError);
+            reply.setErrorDescription(dbusreply.errorMessage());
+        }
+    }
+
+    return reply;
+}
+#endif
+
+
+ActionReply Helper::read(const QVariantMap &args)
+{
+    ActionReply reply;
+    QString filename = args["filename"].toString();
+    QFile file(filename);
+
+    if (!file.open(QIODevice::ReadOnly)) {
+       reply = ActionReply::HelperErrorType;
+       reply.setErrorCode(ActionReply::AuthorizationDeniedError);
+
+       return reply;
+    }
+
+    QTextStream stream(&file);
+    QString content = stream.readAll();
+
+    QVariantMap retdata;
+    retdata["content"] = content;
+
+    reply.setData(retdata);
+    return reply;
+}
+
+ActionReply Helper::write(const QVariantMap &args)
+{
+    ActionReply reply;
+    QString filename = args["filename"].toString();
+    QFile file(filename);
+
+    if (!file.open(QIODevice::WriteOnly)) {
+       reply = ActionReply::HelperErrorType;
+       reply.addData("errorDescription", file.errorString());
+
+       return reply;
+    }
+
+    QTextStream stream(&file);
+    stream << args["content"].toString();
+
+    return reply;
+}
+
+KAUTH_HELPER_MAIN("fancontrol.gui.helper", Helper)

+ 17 - 0
helper/src/helper.h

@@ -0,0 +1,17 @@
+#include <KAuth>
+
+using namespace KAuth;
+
+class Helper : public QObject
+{
+    Q_OBJECT
+
+    public Q_SLOTS:
+
+#ifndef NO_SYSTEMD
+        ActionReply dbusaction(const QVariantMap &args);
+#endif
+
+        ActionReply read(const QVariantMap &args);
+        ActionReply write(const QVariantMap &args);
+};

+ 2 - 0
share/CMakeLists.txt

@@ -0,0 +1,2 @@
+install(DIRECTORY qml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/fancontrol-gui)
+install(DIRECTORY javascript DESTINATION ${CMAKE_INSTALL_PREFIX}/share/fancontrol-gui)

+ 60 - 0
share/javascript/arrayfunctions.js

@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+function names(array) {
+    var names = [];
+    for (var i=0; i<array.length; i++) {
+        names[i] = array[i].name;
+    }
+    return names;
+}
+
+function labels(array) {
+    var labels = [];
+    for (var i=0; i<array.length; i++) {
+        labels[i] = array[i].label;
+    }
+    return labels;
+}
+
+function allPwmFans(hwmons) {
+    var pwmFans = [];
+    for (var i=0; i<hwmons.length; i++) {
+        for (var j=0; j<hwmons[i].pwmFans.length; j++) {
+            pwmFans[pwmFans.length] = hwmons[i].pwmFans[j];
+        }
+    }
+    return pwmFans;
+}
+
+function maxProperty(array, prop) {
+    var max = 0;
+    for (var i=0; i<array.length; i++) {
+        max = Math.max(max, array[i][prop]);
+    }
+    return max;
+}
+
+function minProperty(array, prop) {
+    var min = 0;
+    for (var i=0; i<array.length; i++) {
+        min = Math.min(min, array[i][prop]);
+    }
+    return min;
+}

+ 38 - 0
share/javascript/coordinates.js

@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+function absoluteCoordinatesOf(item) {
+    if (typeof item === "undefined") {
+        item = this;
+    }
+    var offset = Qt.point(0, 0);
+    while (item.parent) {
+        offset.x += item.x;
+        offset.y += item.y;
+        item = item.parent
+    }
+    return offset;
+}
+
+function centerOf(item) {
+    var p = Qt.point(0, 0);
+    p.x = item.x + item.width / 2;
+    p.y = item.y + item.height / 2;
+    return p;
+}

+ 27 - 0
share/javascript/math.js

@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+function bound(floor, value, ceiling) {
+    if (value >= floor) {
+        if (value <= ceiling) return value;
+        else return ceiling;
+    }
+    return floor;
+}
+

+ 37 - 0
share/javascript/units.js

@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+function round(number, dec) {
+    if (!dec) dec = 5;
+    return Math.round(number*10*dec) / (10*dec);
+}
+
+function toCelsius(degrees, currentUnit) {
+    var float = parseFloat(degrees);
+    if (currentUnit === "Kelvin") { return float + 273.15; }
+    if (currentUnit === "Fahrenheit") { return (float - 32) * 5 / 9; }
+    return float;
+}
+
+function fromCelsius(degrees, newUnit) {
+    var float = parseFloat(degrees);
+    if (newUnit === "Kelvin") { return round(float - 273.15); }
+    if (newUnit === "Fahrenheit") { return round(float * 9 / 5 + 32); }
+    return round(float);
+}

+ 52 - 0
share/qml/ConfigfileTab.qml

@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import "../javascript/arrayfunctions.js" as ArrayFunctions
+
+ColumnLayout {
+    anchors.fill: parent
+    anchors.topMargin: 5
+
+    Text {
+        anchors.top: parent.top
+        text: decodeURIComponent(loader.configUrl)
+    }
+
+    Rectangle {
+        Layout.fillHeight: true
+        Layout.fillWidth: true
+
+        border.width: 1
+        radius: 5
+
+        ScrollView {
+            id: scrollView
+            anchors.fill: parent
+            anchors.margins: 5
+
+            TextEdit {
+                text: loader.configFile
+                readOnly: true
+            }
+        }
+    }
+}

+ 407 - 0
share/qml/PwmFan.qml

@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+import QtQuick 2.4
+import QtQuick.Controls 1.3
+import QtQuick.Controls.Styles 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.2
+import "../javascript/arrayfunctions.js" as ArrayFunctions
+import "../javascript/math.js" as MoreMath
+import "../javascript/units.js" as Units
+import "../javascript/coordinates.js" as Coordinates
+
+Rectangle {
+    property var fan
+    property url config: loader.configUrl
+    property real minTemp: 30.0
+    property real maxTemp: 90.0
+    property int margin: 5
+    property int minimizeDuration: 400
+    property real hwRatio
+    property string unit: "Celsius"
+
+    id: root
+    height: width * hwRatio
+    color: "transparent"
+    border.color: "black"
+    border.width: 2
+    radius: 10
+    clip: false
+
+    function update() {
+        hasTempCheckBox.checked = fan.hasTemp;
+        fanOffCheckBox.checked = (fan.minPwm == 0);
+        minStartInput.text = fan.minStart;
+        hwmonBox.currentIndex = fan.temp ? fan.temp.parent.index : 0;
+        tempBox.currentIndex = fan.temp ? fan.temp.index - 1 : 0;
+        canvas.requestPaint();
+    }
+    
+    onFanChanged: update();
+    onConfigChanged: update();
+
+    states: [
+        State {
+            name: "minimized"
+
+            PropertyChanges {
+                target: root
+                height: header.height + 2*margin
+            }
+        },
+        State {
+            name: "maximized"
+
+//            PropertyChanges {
+//                target: root
+//                height:
+//            }
+        }
+    ]
+    transitions: Transition {
+        NumberAnimation {
+            target: root
+            property: "height"
+            easing.amplitude: 1.5
+            easing.type: Easing.InOutQuad
+            duration: minimizeDuration
+        }
+    }
+
+    SystemPalette {
+        id: palette
+    }
+    SystemPalette {
+        id: disabledPalette
+        colorGroup: SystemPalette.Disabled
+    }
+
+    RowLayout {
+        id: header
+        anchors {
+            left: parent.left
+            leftMargin: margin
+            right: parent.right
+            rightMargin: margin
+            top: parent.top
+            topMargin: margin
+        }
+        z: -1
+        clip: true
+        spacing: margin
+
+        TextEdit {
+            id: nameField
+            Layout.alignment: Qt.AlignTop
+            text: fan.name
+            onTextChanged: fan.name = text;
+            horizontalAlignment: TextEdit.AlignLeft
+            wrapMode: TextEdit.Wrap
+            selectByMouse: true
+            Layout.fillWidth: true
+
+            MouseArea {
+                anchors.fill: parent
+                cursorShape: Qt.IBeamCursor
+                acceptedButtons: Qt.NoButton
+            }
+        }
+
+        Rectangle {
+            id: collapseButton
+            height: 16
+            width: 16
+            Layout.alignment: Qt.AlignTop
+            color: collapseMouseArea.containsMouse ? "red" : "transparent"
+            radius: width / 2
+
+            Text {
+                anchors.fill: parent
+                text: root.state == "minimized" ? "-" : "X"
+                color: collapseMouseArea.containsMouse ? "black" : "red"
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+            }
+
+            MouseArea {
+                id: collapseMouseArea
+                anchors.fill: parent
+                hoverEnabled: true
+                acceptedButtons: Qt.LeftButton
+                onClicked: root.state = (root.state != "minimized") ? "minimized" : ""
+            }
+        }
+    }
+
+    Canvas {
+        property int leftPadding: 40 * Screen.devicePixelRatio
+        property int rightPadding: 20 * Screen.devicePixelRatio
+        property int topPadding: 10 * Screen.devicePixelRatio
+        property int bottomPadding: 20 * Screen.devicePixelRatio
+        property int plotWidth: width - leftPadding - rightPadding
+        property int plotHeight: height - topPadding - bottomPadding
+        property alias minTemp: root.minTemp
+        property alias maxTemp: root.maxTemp
+
+        id: canvas
+        renderTarget: Canvas.FramebufferObject
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: header.bottom
+            bottom: settingsArea.top
+        }
+        opacity: state == "minimized" ? 0 : 1
+
+        Behavior on opacity {
+            NumberAnimation { duration: minimizeDuration / 2 }
+        }
+
+        Rectangle {
+            id: currentPwm
+            x: parent.scaleX(fan.temp ? fan.temp.value : minTemp) - width/2
+            y: parent.scaleY(fan.pwm) - height/2
+            width: 7
+            height: width
+            radius: width / 2
+            color: "black"
+            visible: parent.contains(Coordinates.centerOf(this))
+        }
+        PwmPoint {
+            id: stopPoint
+            color: "blue"
+            drag.maximumX: Math.min(canvas.scaleX(canvas.scaleTemp(maxPoint.x)-1), maxPoint.x-1)
+            drag.minimumY: Math.max(canvas.scaleY(canvas.scalePwm(maxPoint.y)-1), maxPoint.y+1)
+            x: canvas.scaleX(MoreMath.bound(minTemp, fan.minTemp, maxTemp)) - width/2
+            y: canvas.scaleY(fan.minStop) - height/2
+            visible: parent.contains(Coordinates.centerOf(this))
+            drag.onActiveChanged: {
+                if (!drag.active) {
+                    fan.minStop = canvas.scalePwm(centerY);
+                    fan.minTemp = canvas.scaleTemp(centerX);
+                    if (!fanOffCheckBox.checked) fan.minPwm = fan.minStop;
+                }
+            }
+        }
+        PwmPoint {
+            id: maxPoint
+            color: "red"
+            drag.minimumX: stopPoint.x
+            drag.maximumY: stopPoint.y
+            x: canvas.scaleX(MoreMath.bound(minTemp, fan.maxTemp, maxTemp)) - width/2
+            y: canvas.scaleY(fan.maxPwm) - height/2
+            visible: parent.contains(Coordinates.centerOf(this))
+            drag.onActiveChanged: {
+                if (!drag.active) {
+                    fan.maxPwm = canvas.scalePwm(centerY);
+                    fan.maxTemp = canvas.scaleTemp(centerX);
+                }
+            }
+        }
+
+        function scaleX(temp) {
+            var scaledX = (temp - minTemp) * plotWidth / (maxTemp - minTemp);
+            return leftPadding + scaledX;
+        }
+        function scaleY(pwm) {
+            var scaledY = pwm * plotHeight / 255;
+            return height - bottomPadding - scaledY;
+        }
+        function scaleTemp(x) {
+            var scaledTemp = (x - leftPadding) / plotWidth * (maxTemp - minTemp);
+            return  minTemp + scaledTemp;
+        }
+        function scalePwm(y) {
+            var scaledPwm = (y - topPadding) / plotHeight * 255;
+            return 255 - scaledPwm;
+        }
+
+        onPaint: {
+            var c = canvas.getContext("2d");
+
+            c.clearRect(0, 0, width, height);
+            c.fillStyle = palette.base;
+            c.fillRect(leftPadding, topPadding, plotWidth, plotHeight);
+
+            var fillGradient = c.createLinearGradient(0, 0, width, 0);
+            fillGradient.addColorStop(0, "rgb(0, 0, 255)");
+            fillGradient.addColorStop(1, "rgb(255, 0, 0)");
+            var strokeGradient = c.createLinearGradient(0, 0, width, 0);
+            strokeGradient.addColorStop(0, "rgb(0, 0, 255)");
+            strokeGradient.addColorStop(1, "rgb(255, 0, 0)");
+            c.fillStyle = fillGradient;
+            c.strokeStyle = strokeGradient;
+            c.lineWidth = 2;
+            c.lineJoin = "round";
+            c.beginPath();
+            if (fanOffCheckBox.checked) {
+                c.moveTo(scaleX(minTemp), scaleY(0));
+                c.lineTo(stopPoint.centerX, scaleY(0));
+            } else {
+                c.moveTo(scaleX(minTemp), stopPoint.centerY);
+            }
+            c.lineTo(stopPoint.centerX, stopPoint.centerY);
+            c.lineTo(maxPoint.centerX, maxPoint.centerY);
+            c.lineTo(scaleX(maxTemp), maxPoint.centerY);
+            c.stroke();
+            c.lineTo(scaleX(maxTemp), height - bottomPadding);
+            c.lineTo(leftPadding, height - bottomPadding);
+            c.fill();
+            fillGradient = c.createLinearGradient(0, 0, 0, height);
+            fillGradient.addColorStop(0, "rgba(127, 127, 127, 0.6)");
+            fillGradient.addColorStop(1, "rgba(127, 127, 127, 0.9)");
+            c.fillStyle = fillGradient;
+            c.fill();
+            c.closePath();
+
+            c.textAlign = "right";
+            c.textBaseline = "middle";
+            c.strokeStyle = palette.dark;
+            c.fillStyle = palette.dark;
+            c.lineWidth = 1;
+            c.strokeRect(leftPadding-0.5, topPadding-0.5, plotWidth+0.5, plotHeight+1.5);
+            for (var i=0; i<=100; i+=20) {
+                var y = scaleY(i*2.55);
+                c.fillText(i + '%', leftPadding - 2, y);
+                if (i != 0 && i != 100) {
+                    for (var j=leftPadding; j<=width-rightPadding; j+=15) {
+                        c.moveTo(j, y);
+                        c.lineTo(Math.min(j+5, width-rightPadding), y);
+                    }
+                    c.stroke();
+                }
+            }
+            c.textAlign = "center";
+            c.textBaseline = "top";
+            var convertedMinTemp = Units.fromCelsius(minTemp, unit);
+            var convertedMaxTemp = Units.fromCelsius(maxTemp, unit);
+            for (i=convertedMinTemp; i<convertedMaxTemp; i+= 10) {
+                var x = scaleX(Units.toCelsius(i, unit));
+                c.fillText(i + '°', x, height-15);
+                if (i != convertedMinTemp) {
+                    for (var j=scaleY(255); j<=scaleY(0); j+=20) {
+                        c.moveTo(x, j);
+                        c.lineTo(x, Math.min(j+5, width-rightPadding));
+                    }
+                    c.stroke();
+                }
+            }
+            c.fillText(convertedMaxTemp + '°', scaleX(maxTemp), height-15);
+        }
+    }
+
+    ColumnLayout {
+        property int padding: 10
+
+        id: settingsArea
+        anchors {
+            left: parent.left
+            leftMargin: padding
+            right: parent.right
+            rightMargin: padding
+            bottom: parent.bottom
+            bottomMargin: padding
+        }
+        visible: root.height >= header.height + height + 2*margin
+        opacity: canvas.opacity
+        clip: true
+        spacing: 0
+
+        RowLayout {
+            spacing: 0
+            height: hwmonBox.height
+            anchors.left: parent.left
+            anchors.right: parent.right
+
+            CheckBox {
+                id: hasTempCheckBox
+                text: "Controlled by:"
+                checked: fan.hasTemp
+                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                onCheckedChanged: fan.hasTemp = checked
+            }
+            Item {
+                Layout.fillWidth: true
+            }
+            ComboBox {
+                property var hwmon: loader.hwmons[currentIndex]
+
+                id: hwmonBox
+                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+                model: ArrayFunctions.names(loader.hwmons)
+                enabled: hasTempCheckBox.checked
+            }
+            Text {
+                text: "/"
+                color: enabled ? palette.text : disabledPalette.text
+                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+                verticalAlignment: Text.AlignVCenter
+                enabled: hasTempCheckBox.checked
+                renderType: Text.NativeRendering
+            }
+            ComboBox {
+                id: tempBox
+                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+                model: ArrayFunctions.labels(hwmonBox.hwmon.temps)
+                enabled: hasTempCheckBox.checked
+                onCurrentIndexChanged: { 
+                    if (hasTempCheckBox.checked)
+                        fan.temp = hwmonBox.hwmon.temps[currentIndex];
+                }
+                onModelChanged: {
+                    if (hasTempCheckBox.checked)
+                        fan.temp = hwmonBox.hwmon.temps[currentIndex];
+                }
+            }
+        }
+
+        CheckBox {
+            id: fanOffCheckBox
+            text: "Turn Fan off if temp < MINTEMP"
+            enabled: hasTempCheckBox.checked
+            checked: fan.minPwm == 0
+            onCheckedChanged: {
+                fan.minPwm = checked ? 0 : fan.minStop;
+                canvas.requestPaint();
+            }
+        }
+
+        RowLayout {
+            anchors.left: parent.left
+            anchors.right: parent.right
+
+            Text {
+                text: "Pwm value for fan to start:"
+                Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+                enabled: fanOffCheckBox.checked && fanOffCheckBox.enabled
+                color: enabled ? palette.text : disabledPalette.text
+                renderType: Text.NativeRendering
+            }
+            NumberOption {
+                id: minStartInput
+                anchors.right: parent.right
+                width: 50
+                enabled: fanOffCheckBox.checked && fanOffCheckBox.enabled
+                text: fan.minStart
+                onTextChanged: fan.minStart = text
+            }
+        }
+    }
+}

+ 61 - 0
share/qml/PwmFansTab.qml

@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import "../javascript/arrayfunctions.js" as ArrayFunctions
+
+ScrollView {
+    property real size: 1.0
+    property real minTemp: 30.0
+    property real maxTemp: 90.0
+    property string unit: "Celsius"
+
+    id: scrollView
+    anchors.fill: parent
+    anchors.topMargin: 5
+
+    Flow {
+        spacing: 20 * size
+        width: scrollView.viewport.width
+        move: Transition {
+            NumberAnimation {
+                easing.type: Easing.OutQuad
+                properties: "x,y"
+                duration: 300
+            }
+        }
+
+        Repeater {
+            property var fans: ArrayFunctions.allPwmFans(loader.hwmons)
+
+            id: repeater
+            model: fans.length
+
+            PwmFan {
+                width: 1000 * size
+                hwRatio: 0.8
+                fan: repeater.fans[index]
+                minTemp: scrollView.minTemp
+                maxTemp: scrollView.maxTemp
+                unit: scrollView.unit
+            }
+        }
+    }
+}

+ 77 - 0
share/qml/PwmPoint.qml

@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+
+Rectangle {
+    property var canvas: parent
+    property point center: Qt.point(x + width / 2, y + height / 2);
+    readonly property real centerX: x + width / 2
+    readonly property real centerY: y + height / 2
+    property alias drag: pwmMouse.drag
+
+    id: root
+    width: 10
+    height: width
+    radius: width / 2
+    border.width: pwmMouse.containsMouse || drag.active ? 1 : 0
+
+    onXChanged: parent.requestPaint();
+    onYChanged: parent.requestPaint();
+
+    Drag.dragType: Drag.Automatic
+
+    MouseArea {
+        id: pwmMouse
+        anchors.fill: parent
+        hoverEnabled: canvas.minimized ? false : true
+        drag.target: root
+        drag.axis: Drag.XAndYAxis
+        drag.smoothed: false
+        drag.minimumX: canvas.scaleX(canvas.minTemp) - root.width/2
+        drag.maximumX: canvas.scaleX(canvas.maxTemp) - root.width/2
+        drag.minimumY: canvas.scaleY(255) - root.height/2
+        drag.maximumY: canvas.scaleY(0) - root.height/2
+    }
+
+    Rectangle {
+        id: tooltip
+        x: parent.width
+        y: - height
+        width: Math.max(pwm.width, temp.width)
+        height: pwm.height + temp.height
+        radius: 4
+        color: Qt.rgba(parent.color.r, parent.color.g, parent.color.b, 0.3)
+        visible: pwmMouse.containsMouse || drag.active
+
+        Column {
+            Text {
+                id: pwm
+                font.pointSize: 9
+                text: Math.round(canvas.scalePwm(root.centerY) / 2.55) + '%'
+            }
+            Text {
+                id: temp
+                font.pointSize: 9
+                text: Math.round(canvas.scaleTemp(root.centerX)) + '°'
+            }
+        }
+    }
+}

+ 98 - 0
share/qml/SensorsTab.qml

@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import "../javascript/arrayfunctions.js" as ArrayFunctions
+
+RowLayout {
+    id: root
+    anchors.fill: parent
+    anchors.topMargin: 10
+
+    Repeater {
+        model: loader.hwmons.length
+
+        Rectangle {
+            property var hwmon: loader.hwmons[index]
+            property int padding: 10
+
+            Layout.preferredWidth: root.width / loader.hwmons.length - root.spacing
+            Layout.fillHeight: true
+            border.width: 1
+            border.color: "black"
+            radius: 5
+
+            Column {
+                id: column
+                anchors.fill: parent
+
+                Text {
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    text: hwmon.name
+                    horizontalAlignment: Text.horizontalCenter
+                }
+
+                Repeater {
+                    model: hwmon.fans.length
+
+                    RowLayout {
+                        width: parent.width
+
+                        Text {
+                            anchors.left: parent.left
+                            anchors.leftMargin: padding
+                            Layout.maximumWidth: parent.width - rpmValue.width - padding*2
+                            clip: true
+                            text: "Fan " + (index+1) + " RPM : "
+                        }
+                        Text {
+                            id: rpmValue
+                            anchors.right: parent.right
+                            anchors.rightMargin: padding
+                            text: hwmon.fans[index].rpm
+                        }
+                    }
+                }
+                Repeater {
+                    model: hwmon.temps.length
+
+                    RowLayout {
+                        width: parent.width
+
+                        Text {
+                            anchors.left: parent.left
+                            anchors.leftMargin: padding
+                            text: hwmon.temps[index].label + ": "
+                            Layout.maximumWidth: parent.width - tempValue.width - padding*2
+                            clip: true
+                        }
+                        Text {
+                            id: tempValue
+                            anchors.right: parent.right
+                            anchors.rightMargin: padding
+                            text: hwmon.temps[index].value
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 164 - 0
share/qml/SettingsTab.qml

@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2015  Malte Veerman <maldela@halloarsch.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser 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.
+ *
+ */
+
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 1.2
+import "../javascript/arrayfunctions.js" as ArrayFunctions
+import "../javascript/units.js" as Units
+
+Item {
+    property real minTemp: 30.0
+    property real maxTemp: 90.0
+    property int interval: 1
+    property int padding: 10
+    property string unit: "Celsius"
+
+    id: root
+    anchors.fill: parent
+    anchors.topMargin: 5
+
+    onIntervalChanged: {
+        var fans = ArrayFunctions.allPwmFans(loader.hwmons);
+        for (var i=0; i<fans.length; i++) {
+            fans[i].interval = interval;
+        }
+    }
+
+    Rectangle {
+        width: 600
+        height: parent.height
+        border.width: 1
+        border.color: "black"
+        radius: 5
+
+        Column {
+            anchors.fill: parent
+            anchors.topMargin: padding
+            spacing: 5
+
+            RowLayout {
+                width: parent.width
+
+                Text {
+                    anchors.left: parent.left
+                    anchors.leftMargin: padding
+                    Layout.maximumWidth: parent.width - intervalValue.width - padding*2
+                    clip: true
+                    text: "Interval:"
+                }
+                OptionInput {
+                    id: intervalValue
+                    anchors.right: parent.right
+                    anchors.rightMargin: padding
+                    width: 100
+                    inputMethodHints: Qt.ImhDigitsOnly
+                    text: interval
+                    onTextChanged: if (text != "") loader.interval = parseInt(text)
+                }
+            }
+            RowLayout {
+                width: parent.width
+
+                Text {
+                    anchors.left: parent.left
+                    anchors.leftMargin: padding
+                    Layout.maximumWidth: parent.width - minTempValue.width - padding*2
+                    clip: true
+                    text: "Minimum temperature for fan graphs:"
+                }
+                OptionInput {
+                    id: minTempValue
+                    anchors.right: parent.right
+                    anchors.rightMargin: padding
+                    width: 100
+                    inputMethodHints: Qt.ImhDigitsOnly
+                    onTextChanged: if (activeFocus) minTemp = Units.toCelsius(text, unit)
+                    Component.onCompleted: text = Units.fromCelsius(minTemp, unit)
+                }
+            }
+            RowLayout {
+                width: parent.width
+
+                Text {
+                    anchors.left: parent.left
+                    anchors.leftMargin: padding
+                    Layout.maximumWidth: parent.width - maxTempValue.width - padding*2
+                    clip: true
+                    text: "Maximum temperature for fan graphs:"
+                }
+                OptionInput {
+                    id: maxTempValue
+                    anchors.right: parent.right
+                    anchors.rightMargin: padding
+                    width: 100
+                    inputMethodHints: Qt.ImhDigitsOnly
+                    text: Units.fromCelsius(maxTemp, unit.currentText)
+                    onTextChanged: if (activeFocus) maxTemp = Units.toCelsius(text, unit)
+                    Component.onCompleted: text = Units.fromCelsius(maxTemp, unit)
+                }
+            }
+            RowLayout {
+                width: parent.width
+
+                Text {
+                    anchors.left: parent.left
+                    anchors.leftMargin: padding
+                    Layout.maximumWidth: parent.width - maxTempValue.width - padding*2
+                    clip: true
+                    text: "Unit:"
+                }
+                ComboBox {
+                    id: unitBox
+                    anchors.right: parent.right
+                    anchors.rightMargin: padding
+                    model: ["Celsius", "Kelvin", "Fahrenheit"]
+                    currentIndex: find(root.unit)
+                    onCurrentIndexChanged: {
+                        unit = currentText;
+                        minTempValue.text = Units.fromCelsius(minTemp, unit);
+                        maxTempValue.text = Units.fromCelsius(maxTemp, unit);
+                    }
+                }
+            }
+            RowLayout {
+                width: parent.width
+                visible: typeof systemdCom != "undefined"
+
+                Text {
+                    anchors.left: parent.left
+                    anchors.leftMargin: padding
+                    Layout.maximumWidth: parent.width - maxTempValue.width - padding*2
+                    clip: true
+                    text: "Name of the fancontrol systemd service:"
+                }
+                OptionInput {
+                    id: serviceName
+                    anchors.right: parent.right
+                    anchors.rightMargin: padding
+                    color: systemdCom.serviceExists ? "green" : "red"
+                    width: 100
+                    text: systemdCom.serviceName
+                    onTextChanged: systemdCom.serviceName = text
+                }
+            }
+        }
+    }
+}
+

+ 14 - 0
share/share.qrc

@@ -0,0 +1,14 @@
+<RCC>
+    <qresource prefix="/">
+        <file>qml/ConfigfileTab.qml</file>
+        <file>qml/NumberOption.qml</file>
+        <file>qml/PwmFan.qml</file>
+        <file>qml/PwmFansTab.qml</file>
+        <file>qml/PwmPoint.qml</file>
+        <file>qml/SensorsTab.qml</file>
+        <file>qml/SettingsTab.qml</file>
+        <file>javascript/arrayfunctions.js</file>
+        <file>javascript/math.js</file>
+        <file>javascript/units.js</file>
+    </qresource>
+</RCC>