diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,7 @@ add_subdirectory( lsofui ) add_subdirectory( processcore ) add_subdirectory( processui ) +add_subdirectory( sensors ) if (KF5Plasma_FOUND) add_subdirectory( signalplotter ) endif() diff --git a/formatter/Formatter.h b/formatter/Formatter.h --- a/formatter/Formatter.h +++ b/formatter/Formatter.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Vlad Zagorodniy + Copyright (C) 2019 Vlad Zahorodnii This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public diff --git a/formatter/Formatter.cpp b/formatter/Formatter.cpp --- a/formatter/Formatter.cpp +++ b/formatter/Formatter.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Vlad Zagorodniy + Copyright (C) 2019 Vlad Zahorodnii formatBootTimestamp is based on TimeUtil class: Copyright (C) 2014 Gregor Mi diff --git a/formatter/Unit.h b/formatter/Unit.h --- a/formatter/Unit.h +++ b/formatter/Unit.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Vlad Zagorodniy + Copyright (C) 2019 Vlad Zahorodnii This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public diff --git a/processcore/formatter.h b/processcore/formatter.h --- a/processcore/formatter.h +++ b/processcore/formatter.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Vlad Zagorodniy + Copyright (C) 2019 Vlad Zahorodnii This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public diff --git a/processcore/unit.h b/processcore/unit.h --- a/processcore/unit.h +++ b/processcore/unit.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Vlad Zagorodniy + Copyright (C) 2019 Vlad Zahorodnii This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public diff --git a/sensors/CMakeLists.txt b/sensors/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/sensors/CMakeLists.txt @@ -0,0 +1,65 @@ +set(KSYSGUARD_SENSORS_SOVERSION 1) + +add_subdirectory(declarative) + +add_definitions(-DTRANSLATION_DOMAIN=\"ksysguard_sensors\") + +set(sensors_LIB_SRCS + Sensor.cpp + SensorDataModel.cpp + SensorTreeModel.cpp + SensorQuery.cpp + SensorDaemonInterface.cpp +) + +set(sensors_LIB_HEADERS + Sensor.h + SensorDataModel.h + SensorTreeModel.h + SensorQuery.h + SensorInfo_p.h +) + +ecm_qt_declare_logging_category(sensors_LIB_SRCS + HEADER sensors_logging.h + IDENTIFIER LIBKSYSGUARD_SENSORS + CATEGORY_NAME org.kde.libksysguard.sensors +) + +set_source_files_properties(org.kde.KSysGuardDaemon.xml PROPERTIES INCLUDE SensorInfo_p.h) +qt5_add_dbus_interface(sensors_LIB_SRCS org.kde.KSysGuardDaemon.xml ksysguarddaemon) + +add_library(Sensors ${sensors_LIB_SRCS}) +add_library(KSysGuard::Sensors ALIAS Sensors) + +target_include_directories(Sensors + PUBLIC + "$" + "$" +) + +generate_export_header(Sensors) + +target_link_libraries(Sensors + PUBLIC + Qt5::Qml + KSysGuard::Formatter + PRIVATE + Qt5::Core + Qt5::DBus + KF5::I18n +) + +set_target_properties(Sensors PROPERTIES + LIBRARY_OUTPUT_NAME KSysGuardSensors + VERSION ${KSYSGUARD_VERSION_STRING} + SOVERSION ${KSYSGUARD_SENSORS_SOVERSION} +) + +install(TARGETS Sensors EXPORT libksysguardLibraryTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES + ${sensors_LIB_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/sensors_export.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/ksysguard/sensors + COMPONENT Devel +) diff --git a/sensors/Sensor.h b/sensors/Sensor.h new file mode 100644 --- /dev/null +++ b/sensors/Sensor.h @@ -0,0 +1,188 @@ +/* + Copyright (C) 2019 Vlad Zahorodnii + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "formatter/Unit.h" + +#include "sensors_export.h" + +namespace KSysGuard +{ +class SensorData; +class SensorInfo; +class SensorQuery; + +/** + * An object encapsulating a backend sensor. + * + * This class represents a sensor as exposed by the backend. It allows querying + * various metadata properties of the sensor as well as the current value. + */ +class SENSORS_EXPORT Sensor : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /** + * The path to the backend sensor this Sensor represents. + */ + Q_PROPERTY(QString sensorId READ sensorId WRITE setSensorId NOTIFY sensorIdChanged) + /** + * The user-visible name of this Sensor. + */ + Q_PROPERTY(QString name READ name NOTIFY metaDataChanged) + /** + * A shortened name that can be displayed when space is constrained. + * + * The value is the same as name if shortName was not provided by the backend. + */ + Q_PROPERTY(QString shortName READ shortName NOTIFY metaDataChanged) + /** + * A description of the Sensor. + */ + Q_PROPERTY(QString description READ description NOTIFY metaDataChanged) + /** + * The unit of this Sensor. + */ + Q_PROPERTY(KSysGuard::Unit unit READ unit NOTIFY metaDataChanged) + /** + * The minimum value this Sensor can have. + */ + Q_PROPERTY(qreal minimum READ minimum NOTIFY metaDataChanged) + /** + * The maximum value this Sensor can have. + */ + Q_PROPERTY(qreal maximum READ maximum NOTIFY metaDataChanged) + /** + * The QVariant type for this sensor. + * + * This is used to create proper default values. + */ + Q_PROPERTY(QVariant::Type type READ type NOTIFY metaDataChanged) + /** + * The status of the sensor. + * + * Due to the asynchronous nature of the underlying code, sensors are not + * immediately available on construction. Instead, they need to request data + * from the daemon and wait for it to arrive. This property reflects where + * in that process this sensor is. + */ + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + /** + * The current value of this sensor. + */ + Q_PROPERTY(QVariant value READ value NOTIFY valueChanged) + /** + * A formatted version of \property value. + */ + Q_PROPERTY(QString formattedValue READ formattedValue NOTIFY valueChanged) + /** + * Should this Sensor check for changes? + * + * Note that if set to true, the sensor will only be enabled when the parent + * is also enabled. + */ + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + +public: + /** + * This enum type is used to specify status of the Sensor. + */ + enum class Status { + Unknown, ///< The sensor has no ID assigned. + Loading, ///< The sensor is currently being loaded. + Ready, ///< The sensor has been loaded. + Error, ///< An error occurred or the sensor has been removed. + Removed, ///< Removed from backend + }; + Q_ENUM(Status) + + explicit Sensor(QObject *parent = nullptr); + explicit Sensor(const QString &id, QObject *parent = nullptr); + /** + * Construct a Sensor from a SensorQuery result and index. + * + * This avoids an extra lookup for the sensor metadata. + */ + Sensor(const SensorQuery &query, int index, QObject *parent = nullptr); + ~Sensor() override; + + bool event(QEvent *event) override; + + QString sensorId() const; + void setSensorId(const QString &id); + Q_SIGNAL void sensorIdChanged() const; + + Status status() const; + Q_SIGNAL void statusChanged() const; + + QString name() const; + QString shortName() const; + QString description() const; + KSysGuard::Unit unit() const; + qreal minimum() const; + qreal maximum() const; + QVariant::Type type() const; + /** + * This signal is emitted when any of the metadata properties change. + */ + Q_SIGNAL void metaDataChanged() const; + + /** + * Returns the output of the sensor. + * + * The returned value is the most recent sensor data received from the ksysguard + * daemon, it's not necessarily the actual current output value. + * + * The client can't control how often the sensor data is sampled. The ksysguard + * daemon is in charge of picking the sample rate. When the Sensor receives new + * output value, dataChanged signal will be emitted. + * + * @see dataChanged + */ + QVariant value() const; + QString formattedValue() const; + Q_SIGNAL void valueChanged() const; + + bool enabled() const; + void setEnabled(bool newEnabled); + Q_SIGNAL void enabledChanged(); + + void classBegin() override; + void componentComplete() override; + +private: + void onMetaDataChanged(const QString &sensorId, const SensorInfo &metaData); + void onValueChanged(const QString &sensorId, const QVariant &value); + void onEnabledChanged(); + + class Private; + const std::unique_ptr d; +}; + +} // namespace KSysGuard diff --git a/sensors/Sensor.cpp b/sensors/Sensor.cpp new file mode 100644 --- /dev/null +++ b/sensors/Sensor.cpp @@ -0,0 +1,259 @@ +/* + Copyright (C) 2019 Vlad Zahorodnii + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include + +#include "Sensor.h" +#include "SensorDaemonInterface_p.h" +#include "SensorInfo_p.h" +#include "formatter/Formatter.h" +#include "SensorQuery.h" + +using namespace KSysGuard; + +class Q_DECL_HIDDEN Sensor::Private +{ +public: + SensorInfo sensorInfo; + + Sensor::Status status = Sensor::Status::Unknown; + QVariant value; + + bool usedByQml = false; + bool componentComplete = false; + + QString pendingId; + QString id; + + bool enabled = true; +}; + +Sensor::Sensor(QObject *parent) + : Sensor(QString{}, parent) +{ +} + +Sensor::Sensor(const QString &id, QObject *parent) + : QObject(parent) + , d(new Private()) +{ + connect(this, &Sensor::statusChanged, this, &Sensor::valueChanged); + connect(this, &Sensor::statusChanged, this, &Sensor::metaDataChanged); + connect(this, &Sensor::enabledChanged, this, &Sensor::onEnabledChanged); + + setSensorId(id); +} + +Sensor::Sensor(const SensorQuery &query, int index, QObject *parent) + : Sensor(QString{}, parent) +{ + if (index > 0 && index < query.result().size()) { + auto result = query.result().at(index); + d->id = result.first; + onMetaDataChanged(d->id, result.second); + onEnabledChanged(); + } +} + +bool Sensor::event(QEvent *event) +{ + if (event->type() == QEvent::ParentAboutToChange && parent()) { + parent()->disconnect(this); + } else if (event->type() == QEvent::ParentChange && parent()) { + if (parent()->metaObject()->indexOfSignal("enabledChanged()") != -1) { + connect(parent(), SIGNAL(enabledChanged()), this, SIGNAL(enabledChanged())); + } + } + + return QObject::event(event); +} + +Sensor::~Sensor() +{ + SensorDaemonInterface::instance()->unsubscribe(d->id); +} + +QString Sensor::sensorId() const +{ + return d->id; +} + +void Sensor::setSensorId(const QString &id) +{ + if (id == d->id) { + return; + } + + if (d->usedByQml && !d->componentComplete) { + d->pendingId = id; + return; + } + + d->id = id; + d->status = Sensor::Status::Loading; + + if (!id.isEmpty()) { + SensorDaemonInterface::instance()->requestMetaData(id); + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::metaDataChanged, this, &Sensor::onMetaDataChanged, Qt::UniqueConnection); + } + + if (enabled()) { + SensorDaemonInterface::instance()->subscribe(id); + SensorDaemonInterface::instance()->requestValue(id); + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::valueChanged, this, &Sensor::onValueChanged, Qt::UniqueConnection); + } + + Q_EMIT sensorIdChanged(); + Q_EMIT statusChanged(); +} + +Sensor::Status Sensor::status() const +{ + return d->status; +} + +QString Sensor::name() const +{ + return d->sensorInfo.name; +} + +QString Sensor::shortName() const +{ + if (d->sensorInfo.shortName.isEmpty()) { + return d->sensorInfo.name; + } + + return d->sensorInfo.name; +} + +QString Sensor::description() const +{ + return d->sensorInfo.description; +} + +Unit Sensor::unit() const +{ + return d->sensorInfo.unit; +} + +qreal Sensor::minimum() const +{ + return d->sensorInfo.min; +} + +qreal Sensor::maximum() const +{ + return d->sensorInfo.max; +} + +QVariant::Type Sensor::type() const +{ + return d->sensorInfo.variantType; +} + +QVariant Sensor::value() const +{ + if (!d->value.isValid()) { + return QVariant{d->sensorInfo.variantType}; + } + return d->value; +} + +QString Sensor::formattedValue() const +{ + return Formatter::formatValue(value(), unit(), MetricPrefixAutoAdjust, FormatOptionShowNull); +} + +bool Sensor::enabled() const +{ + if (d->enabled && parent()) { + auto parentEnabled = parent()->property("enabled"); + if (parentEnabled.isValid()) { + return parentEnabled.toBool(); + } + } + + return d->enabled; +} + +void Sensor::setEnabled(bool newEnabled) +{ + if (newEnabled == d->enabled) { + return; + } + + d->enabled = newEnabled; + Q_EMIT enabledChanged(); +} + +void Sensor::classBegin() +{ + d->usedByQml = true; +} + +void Sensor::componentComplete() +{ + d->componentComplete = true; + + setSensorId(d->pendingId); + + if (parent() && parent()->metaObject()->indexOfSignal("enabledChanged()") != -1) { + connect(parent(), SIGNAL(enabledChanged()), this, SIGNAL(enabledChanged())); + } +} + +void Sensor::onMetaDataChanged(const QString &sensorId, const SensorInfo &metaData) +{ + if (sensorId != d->id || !enabled()) { + return; + } + + d->sensorInfo = metaData; + + if (d->status == Sensor::Status::Loading) { + d->status = Sensor::Status::Ready; + Q_EMIT statusChanged(); + } + + Q_EMIT metaDataChanged(); +} + +void Sensor::onValueChanged(const QString &sensorId, const QVariant &value) +{ + if (sensorId != d->id || !enabled()) { + return; + } + + d->value = value; + Q_EMIT valueChanged(); +} + +void Sensor::onEnabledChanged() +{ + if (enabled()) { + SensorDaemonInterface::instance()->subscribe(d->id); + // Force an update of metadata and data, since that may have changed + // while we were disabled. + SensorDaemonInterface::instance()->requestMetaData(d->id); + SensorDaemonInterface::instance()->requestValue(d->id); + } else { + SensorDaemonInterface::instance()->unsubscribe(d->id); + } +} diff --git a/sensors/SensorDaemonInterface.cpp b/sensors/SensorDaemonInterface.cpp new file mode 100644 --- /dev/null +++ b/sensors/SensorDaemonInterface.cpp @@ -0,0 +1,145 @@ +/* + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "SensorDaemonInterface_p.h" + +#include + +#include "ksysguarddaemon.h" + +using namespace KSysGuard; + +class SensorDaemonInterface::Private +{ +public: + std::unique_ptr dbusInterface; + + static const QString SensorServiceName; + static const QString SensorPath; +}; + +const QString SensorDaemonInterface::Private::SensorServiceName = QStringLiteral("org.kde.kstats"); +const QString SensorDaemonInterface::Private::SensorPath = QStringLiteral("/"); + +SensorDaemonInterface::SensorDaemonInterface(QObject *parent) + : QObject(parent) + , d(new Private) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + d->dbusInterface = std::make_unique(Private::SensorServiceName, Private::SensorPath, QDBusConnection::sessionBus()); + + connect(d->dbusInterface.get(), &org::kde::KSysGuardDaemon::sensorMetaDataChanged, this, &SensorDaemonInterface::onMetaDataChanged); + connect(d->dbusInterface.get(), &org::kde::KSysGuardDaemon::newSensorData, this, &SensorDaemonInterface::onValueChanged); + connect(d->dbusInterface.get(), &org::kde::KSysGuardDaemon::sensorAdded, this, &SensorDaemonInterface::sensorAdded); + connect(d->dbusInterface.get(), &org::kde::KSysGuardDaemon::sensorRemoved, this, &SensorDaemonInterface::sensorRemoved); +} + +SensorDaemonInterface::~SensorDaemonInterface() +{ +} + +void SensorDaemonInterface::requestMetaData(const QString &sensorId) +{ + requestMetaData(QStringList{sensorId}); +} + +void SensorDaemonInterface::requestMetaData(const QStringList &sensorIds) +{ + auto watcher = new QDBusPendingCallWatcher{d->dbusInterface->sensors(sensorIds), this}; + connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + + const QDBusPendingReply reply = *self; + if (reply.isError()) { + return; + } + + const auto infos = reply.value(); + for (auto itr = infos.begin(); itr != infos.end(); ++itr) { + Q_EMIT metaDataChanged(itr.key(), itr.value()); + } + }); +} + +void SensorDaemonInterface::requestValue(const QString &sensorId) +{ + auto watcher = new QDBusPendingCallWatcher{d->dbusInterface->sensorData({sensorId}), this}; + connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + + const QDBusPendingReply reply = *self; + if (reply.isError()) { + return; + } + + const auto allData = reply.value(); + for (auto data : allData) { + Q_EMIT valueChanged(data.attribute, data.payload); + } + }); +} + +QDBusPendingCallWatcher *SensorDaemonInterface::allSensors() const +{ + return new QDBusPendingCallWatcher{d->dbusInterface->allSensors()}; +} + +void SensorDaemonInterface::subscribe(const QString &sensorId) +{ + subscribe(QStringList{sensorId}); +} + +void KSysGuard::SensorDaemonInterface::subscribe(const QStringList &sensorIds) +{ + d->dbusInterface->subscribe(sensorIds); +} + +void SensorDaemonInterface::unsubscribe(const QString &sensorId) +{ + unsubscribe(QStringList{sensorId}); +} + +void KSysGuard::SensorDaemonInterface::unsubscribe(const QStringList &sensorIds) +{ + d->dbusInterface->unsubscribe(sensorIds); +} + +SensorDaemonInterface *SensorDaemonInterface::instance() +{ + static SensorDaemonInterface instance; + return &instance; +} + +void SensorDaemonInterface::onMetaDataChanged(const QHash &metaData) +{ + for (auto itr = metaData.begin(); itr != metaData.end(); ++itr) { + Q_EMIT metaDataChanged(itr.key(), itr.value()); + } +} + +void SensorDaemonInterface::onValueChanged(const SensorDataList &values) +{ + for (auto entry : values) { + Q_EMIT valueChanged(entry.attribute, entry.payload); + } +} diff --git a/sensors/SensorDaemonInterface_p.h b/sensors/SensorDaemonInterface_p.h new file mode 100644 --- /dev/null +++ b/sensors/SensorDaemonInterface_p.h @@ -0,0 +1,70 @@ +/* + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "SensorInfo_p.h" +#include +#include + +class QDBusPendingCallWatcher; + +namespace KSysGuard +{ +/** + * Internal helper class to communicate with the daemon. + * + * This is mostly for convenience on top of the auto-generated KSysGuardDaemon + * D-Bus interface. + */ +class SensorDaemonInterface : public QObject +{ + Q_OBJECT + +public: + SensorDaemonInterface(QObject *parent = nullptr); + ~SensorDaemonInterface() override; + + void requestMetaData(const QString &sensorId); + void requestMetaData(const QStringList &sensorIds); + Q_SIGNAL void metaDataChanged(const QString &sensorId, const SensorInfo &info); + void requestValue(const QString &sensorId); + Q_SIGNAL void valueChanged(const QString &sensorId, const QVariant &value); + + QDBusPendingCallWatcher *allSensors() const; + + void subscribe(const QString &sensorId); + void subscribe(const QStringList &sensorIds); + void unsubscribe(const QString &sensorId); + void unsubscribe(const QStringList &sensorIds); + + Q_SIGNAL void sensorAdded(const QString &sensorId); + Q_SIGNAL void sensorRemoved(const QString &sensorId); + + static SensorDaemonInterface *instance(); + +private: + void onMetaDataChanged(const QHash &metaData); + void onValueChanged(const SensorDataList &values); + + class Private; + const std::unique_ptr d; +}; + +} diff --git a/sensors/SensorDataModel.h b/sensors/SensorDataModel.h new file mode 100644 --- /dev/null +++ b/sensors/SensorDataModel.h @@ -0,0 +1,109 @@ +/* + Copyright (c) 2019 Eike Hein + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "sensors_export.h" +#include +#include +#include +#include + +namespace KSysGuard +{ +class SensorInfo; + +/** + * A model representing a table of sensors. + * + * This model will expose the metadata and values of a list of sensors as a + * table, using one column for each sensor. The metadata and values are + * represented as different roles. + */ +class SENSORS_EXPORT SensorDataModel : public QAbstractTableModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /** + * The list of sensors to watch. + */ + Q_PROPERTY(QStringList sensors READ sensors WRITE setSensors NOTIFY sensorsChanged) + /** + * The minimum value of all sensors' minimum property. + */ + Q_PROPERTY(qreal minimum READ minimum NOTIFY sensorMetaDataChanged) + /** + * The maximum value of all sensors' maximum property. + */ + Q_PROPERTY(qreal maximum READ maximum NOTIFY sensorMetaDataChanged) + +public: + enum AdditionalRoles { + SensorId = Qt::UserRole + 1, //< The backend path to the sensor. + Name, //< The name of the sensor. + ShortName, //< A shorter name for the sensor. This is equal to name if not set. + Description, //< A description for the sensor. + Unit, //< The unit of the sensor. + Minimum, //< The minimum value this sensor can have. + Maximum, //< The maximum value this sensor can have. + Type, //< The QVariant::Type of the sensor. + Value, //< The value of the sensor. + FormattedValue, //< A formatted string of the value of the sensor. + }; + Q_ENUM(AdditionalRoles) + + explicit SensorDataModel(const QStringList &sensorIds = {}, QObject *parent = nullptr); + virtual ~SensorDataModel(); + + QHash roleNames() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QStringList sensors() const; + void setSensors(const QStringList &sensorIds); + Q_SIGNAL void sensorsChanged() const; + Q_SIGNAL void sensorMetaDataChanged(); + + qreal minimum() const; + qreal maximum() const; + + Q_INVOKABLE void addSensor(const QString &sensorId); + Q_INVOKABLE void removeSensor(const QString &sensorId); + Q_INVOKABLE int column(const QString &sensorId) const; + + void classBegin() override; + void componentComplete() override; + +private: + void onSensorAdded(const QString &sensorId); + void onSensorRemoved(const QString &sensorId); + void onMetaDataChanged(const QString &sensorId, const SensorInfo &info); + void onValueChanged(const QString &sensorId, const QVariant &value); + + class Private; + const std::unique_ptr d; +}; + +} // namespace KSysGuard diff --git a/sensors/SensorDataModel.cpp b/sensors/SensorDataModel.cpp new file mode 100644 --- /dev/null +++ b/sensors/SensorDataModel.cpp @@ -0,0 +1,363 @@ +/* + Copyright (c) 2019 Eike Hein + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include + +#include "Formatter.h" +#include "SensorDaemonInterface_p.h" +#include "SensorDataModel.h" +#include "SensorInfo_p.h" +#include "sensors_logging.h" + +using namespace KSysGuard; + +class Q_DECL_HIDDEN SensorDataModel::Private +{ +public: + Private(SensorDataModel *qq) + : q(qq) + { + } + + void sensorsChanged(); + void addSensor(const QString &id); + void removeSensor(const QString &id); + + QStringList requestedSensors; + + QStringList sensors; + QStringList objects; + + QHash sensorInfos; + QHash sensorData; + + bool usedByQml = false; + bool componentComplete = false; + bool loaded = false; + +private: + SensorDataModel *q; +}; + +SensorDataModel::SensorDataModel(const QStringList &sensorIds, QObject *parent) + : QAbstractTableModel(parent) + , d(new Private(this)) +{ + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::sensorAdded, this, &SensorDataModel::onSensorAdded); + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::sensorRemoved, this, &SensorDataModel::onSensorRemoved); + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::metaDataChanged, this, &SensorDataModel::onMetaDataChanged); + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::valueChanged, this, &SensorDataModel::onValueChanged); + + // Empty string is used for entries that do not specify a wildcard object + d->objects << QStringLiteral(""); + + setSensors(sensorIds); +} + +SensorDataModel::~SensorDataModel() +{ +} + +QHash SensorDataModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + + QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles")); + + for (int i = 0; i < e.keyCount(); ++i) { + roles.insert(e.value(i), e.key(i)); + } + + return roles; +} + +QVariant SensorDataModel::data(const QModelIndex &index, int role) const +{ + const bool check = checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::DoNotUseParent); + if (!check) { + return QVariant(); + } + + auto sensor = d->sensors.at(index.column()); + auto info = d->sensorInfos.value(sensor); + auto data = d->sensorData.value(sensor); + + switch (role) { + case Qt::DisplayRole: + case FormattedValue: + return Formatter::formatValue(data, info.unit); + case Value: + return data; + case Unit: + return info.unit; + case Name: + return info.name; + case ShortName: + if (info.shortName.isEmpty()) { + return info.name; + } + return info.shortName; + case Description: + return info.description; + case Minimum: + return info.min; + case Maximum: + return info.max; + case Type: + return info.variantType; + case SensorId: + return sensor; + default: + break; + } + + return QVariant(); +} + +QVariant SensorDataModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) { + return QVariant(); + } + + if (section < 0 || section >= d->sensors.size()) { + return QVariant(); + } + + auto sensor = d->sensors.at(section); + auto info = d->sensorInfos.value(sensor); + + switch (role) { + case Qt::DisplayRole: + case ShortName: + if (info.shortName.isEmpty()) { + return info.name; + } + return info.shortName; + case Name: + return info.name; + case SensorId: + return sensor; + case Unit: + return info.unit; + case Description: + return info.description; + case Minimum: + return info.min; + case Maximum: + return info.max; + case Type: + return info.variantType; + default: + break; + } + + return QVariant(); +} + +int SensorDataModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return d->objects.count(); +} + +int SensorDataModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return d->sensors.count(); +} + +qreal SensorDataModel::minimum() const +{ + if (d->sensors.isEmpty()) { + return 0; + } + + auto result = std::min_element(d->sensorInfos.cbegin(), d->sensorInfos.cend(), [](const SensorInfo &first, const SensorInfo &second) { return first.min < second.min; }); + return (*result).min; +} + +qreal SensorDataModel::maximum() const +{ + if (d->sensors.isEmpty()) { + return 0; + } + + auto result = std::max_element(d->sensorInfos.cbegin(), d->sensorInfos.cend(), [](const SensorInfo &first, const SensorInfo &second) { return first.max < second.max; }); + return (*result).max; +} + +QStringList SensorDataModel::sensors() const +{ + return d->requestedSensors; +} + +void SensorDataModel::setSensors(const QStringList &sensorIds) +{ + if (d->requestedSensors == sensorIds) { + return; + } + + d->requestedSensors = sensorIds; + + if (!d->usedByQml || d->componentComplete) { + d->sensorsChanged(); + } + Q_EMIT sensorsChanged(); +} + +void SensorDataModel::addSensor(const QString &sensorId) +{ + d->addSensor(sensorId); +} + +void SensorDataModel::removeSensor(const QString &sensorId) +{ + d->removeSensor(sensorId); +} + +int KSysGuard::SensorDataModel::column(const QString &sensorId) const +{ + return d->sensors.indexOf(sensorId); +} + +void KSysGuard::SensorDataModel::classBegin() +{ + d->usedByQml = true; +} + +void KSysGuard::SensorDataModel::componentComplete() +{ + d->componentComplete = true; + + d->sensorsChanged(); + + emit sensorsChanged(); +} + +void SensorDataModel::Private::addSensor(const QString &id) +{ + if (requestedSensors.indexOf(id) != -1) { + return; + } + + qCDebug(LIBKSYSGUARD_SENSORS) << "Add Sensor" << id; + + sensors.append(id); + SensorDaemonInterface::instance()->subscribe(id); + SensorDaemonInterface::instance()->requestMetaData(id); +} + +void SensorDataModel::Private::removeSensor(const QString &id) +{ + const int col = sensors.indexOf(id); + if (col == -1) { + return; + } + + q->beginRemoveColumns(QModelIndex(), col, col); + + sensors.removeAt(col); + sensorInfos.remove(id); + sensorData.remove(id); + + q->endRemoveColumns(); +} + +void SensorDataModel::onSensorAdded(const QString &sensorId) +{ + d->addSensor(sensorId); +} + +void SensorDataModel::onSensorRemoved(const QString &sensorId) +{ + d->removeSensor(sensorId); +} + +void SensorDataModel::onMetaDataChanged(const QString &sensorId, const SensorInfo &info) +{ + auto column = d->sensors.indexOf(sensorId); + if (column == -1) { + return; + } + + qCDebug(LIBKSYSGUARD_SENSORS) << "Received metadata change for" << sensorId; + + // Simple case: Just an update for a sensor's metadata + if (d->sensorInfos.contains(sensorId)) { + d->sensorInfos[sensorId] = info; + Q_EMIT dataChanged(index(0, column), index(0, column), {Qt::DisplayRole, Name, ShortName, Description, Unit, Minimum, Maximum, Type, FormattedValue}); + return; + } + + // Otherwise, it's a new sensor that was added + beginInsertColumns(QModelIndex{}, column, column); + d->sensorInfos[sensorId] = info; + d->sensorData[sensorId] = QVariant{}; + endInsertColumns(); + + SensorDaemonInterface::instance()->requestValue(sensorId); + emit sensorMetaDataChanged(); +} + +void SensorDataModel::onValueChanged(const QString &sensorId, const QVariant &value) +{ + if (!d->sensorData.contains(sensorId)) { + return; + } + + auto column = d->sensors.indexOf(sensorId); + d->sensorData[sensorId] = value; + Q_EMIT dataChanged(index(0, column), index(0, column), {Qt::DisplayRole, Value, FormattedValue}); +} + +void SensorDataModel::Private::sensorsChanged() +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + auto newSet = QSet{requestedSensors.begin(), requestedSensors.end()}; + auto currentSet = QSet{sensors.begin(), sensors.end()}; +#else + auto newSet = requestedSensors.toSet(); + auto currentSet = sensors.toSet(); +#endif + + const auto addedSensors = newSet - currentSet; + const auto removedSensors = currentSet - newSet; + + sensors.append(addedSensors.values()); + + SensorDaemonInterface::instance()->subscribe(addedSensors.values()); + SensorDaemonInterface::instance()->requestMetaData(addedSensors.values()); + + bool itemsRemoved = false; + for (auto sensor : removedSensors) { + removeSensor(sensor); + itemsRemoved = true; + } + if (itemsRemoved) { + emit q->sensorMetaDataChanged(); + } +} diff --git a/sensors/SensorInfo_p.h b/sensors/SensorInfo_p.h new file mode 100644 --- /dev/null +++ b/sensors/SensorInfo_p.h @@ -0,0 +1,114 @@ +/* + Copyright (c) 2019 David Edmundson + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +#pragma once + +#include +#include +#include +#include +#include + +#include "formatter/Unit.h" + +namespace KSysGuard +{ +// Data that is static for the lifespan of the sensor +class SensorInfo +{ +public: + SensorInfo() = default; + QString name; // Translated name of the sensor. + QString shortName; // Shorter translated name of the sensor, to use when space is constrained. + QString description; // Translated description of the sensor. + QVariant::Type variantType = QVariant::Invalid; + KSysGuard::Unit unit = KSysGuard::UnitInvalid; // Both a format hint and implies data type (i.e double/string) + qreal min = 0; + qreal max = 0; +}; + +class Q_DECL_EXPORT SensorData +{ +public: + SensorData() = default; + SensorData(const QString &_attribute, const QVariant &_payload) + : attribute(_attribute) + , payload(_payload) + { + } + QString attribute; + QVariant payload; +}; + +typedef QHash SensorInfoMap; +typedef QList SensorDataList; + +inline QDBusArgument &operator<<(QDBusArgument &argument, const SensorInfo &s) +{ + argument.beginStructure(); + argument << s.name; + argument << s.shortName; + argument << s.description; + argument << s.variantType; + argument << s.unit; + argument << s.min; + argument << s.max; + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, SensorInfo &s) +{ + argument.beginStructure(); + argument >> s.name; + argument >> s.shortName; + argument >> s.description; + uint32_t t; + argument >> t; + s.variantType = static_cast(t); + argument >> t; + s.unit = static_cast(t); + argument >> s.min; + argument >> s.max; + argument.endStructure(); + return argument; +} + +inline QDBusArgument &operator<<(QDBusArgument &argument, const SensorData &s) +{ + argument.beginStructure(); + argument << s.attribute; + argument << QDBusVariant(s.payload); + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, SensorData &s) +{ + argument.beginStructure(); + argument >> s.attribute; + argument >> s.payload; + argument.endStructure(); + return argument; +} + +} // namespace KSysGuard + +Q_DECLARE_METATYPE(KSysGuard::SensorInfo); +Q_DECLARE_METATYPE(KSysGuard::SensorData); +Q_DECLARE_METATYPE(KSysGuard::SensorDataList); diff --git a/sensors/SensorQuery.h b/sensors/SensorQuery.h new file mode 100644 --- /dev/null +++ b/sensors/SensorQuery.h @@ -0,0 +1,77 @@ +/* + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "sensors_export.h" +#include +#include + +namespace KSysGuard +{ +class SensorInfo; + +/** + * An object to query the daemon for a list of sensors and their metadata. + * + * This class will request a list of sensors from the daemon, then filter them + * based on the supplied path. The path can include the wildcard "*" to get a + * list of all sensors matching the specified part of their path. In addition, + * if left empty, all sensors will be returned. + */ +class SENSORS_EXPORT SensorQuery : public QObject +{ + Q_OBJECT + +public: + SensorQuery(const QString &path = QString{}, QObject *parent = nullptr); + ~SensorQuery() override; + + QString path() const; + void setPath(const QString &path); + + /** + * A list of sensors ids that match the query. + */ + QStringList sensorIds() const; + + /** + * Start processing the query. + */ + bool execute(); + /** + * Wait for the query to finish. + * + * Mostly useful for code that needs the result to be available before + * continuing. Ideally the finished() signal should be used instead. + */ + bool waitForFinished(); + + Q_SIGNAL void finished(const SensorQuery *query); + +private: + friend class Sensor; + friend class SensorTreeModel; + QVector> result() const; + + class Private; + const std::unique_ptr d; +}; + +} // namespace KSysGuard diff --git a/sensors/SensorQuery.cpp b/sensors/SensorQuery.cpp new file mode 100644 --- /dev/null +++ b/sensors/SensorQuery.cpp @@ -0,0 +1,135 @@ +/* + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "SensorQuery.h" + +#include +#include +#include + +#include "SensorDaemonInterface_p.h" +#include "sensors_logging.h" + +using namespace KSysGuard; + +class SensorQuery::Private +{ +public: + enum class State { Initial, Running, Finished }; + + void updateResult(const QDBusPendingReply &reply); + + QString path; + State state = State::Initial; + QVector> result; + + QDBusPendingCallWatcher *watcher = nullptr; +}; + +KSysGuard::SensorQuery::SensorQuery(const QString &path, QObject *parent) + : QObject(parent) + , d(std::make_unique()) +{ + d->path = path; +} + +KSysGuard::SensorQuery::~SensorQuery() +{ +} + +QString KSysGuard::SensorQuery::path() const +{ + return d->path; +} + +void KSysGuard::SensorQuery::setPath(const QString &path) +{ + if (path == d->path) { + return; + } + + if (d->state != Private::State::Initial) { + qCWarning(LIBKSYSGUARD_SENSORS) << "Cannot modify a running or finished query"; + return; + } + + d->path = path; +} + +bool KSysGuard::SensorQuery::execute() +{ + if (d->state != Private::State::Initial) { + return false; + } + + d->state = Private::State::Running; + + auto watcher = SensorDaemonInterface::instance()->allSensors(); + d->watcher = watcher; + connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this]() { + watcher->deleteLater(); + d->watcher = nullptr; + d->state = Private::State::Finished; + d->updateResult(QDBusPendingReply(*watcher)); + Q_EMIT finished(this); + }); + + return true; +} + +bool KSysGuard::SensorQuery::waitForFinished() +{ + if (!d->watcher) { + return false; + } + + d->watcher->waitForFinished(); + return true; +} + +QStringList KSysGuard::SensorQuery::sensorIds() const +{ + QStringList ids; + std::transform(d->result.cbegin(), d->result.cend(), std::back_inserter(ids), [](auto entry) { return entry.first; }); + return ids; +} + +QVector> KSysGuard::SensorQuery::result() const +{ + return d->result; +} + +void KSysGuard::SensorQuery::Private::updateResult(const QDBusPendingReply &reply) +{ + if (path.isEmpty()) { // add everything + const SensorInfoMap response = reply.value(); + for (auto it = response.constBegin(); it != response.constEnd(); it++) { + result.append(qMakePair(it.key(), it.value())); + } + return; + } + auto regexp = QRegularExpression{QRegularExpression::wildcardToRegularExpression(path)}; + + const auto sensorIds = reply.value().keys(); + for (auto id : sensorIds) { + if (regexp.match(id).hasMatch()) { + result.append(qMakePair(id, reply.value().value(id))); + } + } +} diff --git a/sensors/SensorTreeModel.h b/sensors/SensorTreeModel.h new file mode 100644 --- /dev/null +++ b/sensors/SensorTreeModel.h @@ -0,0 +1,77 @@ +/* + Copyright (c) 2019 Eike Hein + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "sensors_export.h" +#include +#include + +namespace KSysGuard +{ +class SensorInfo; + +/** + * A model representing a tree of sensors that are available from the daemon. + * + * This model exposes the daemon's sensors as a tree, based on their path. Each + * sensor is assumed to be structured in a format similar to + * `category/object/sensor`. This model will then expose a tree, with `category` + * as top level, `object` below it and finally `sensor` itself. + */ +class SENSORS_EXPORT SensorTreeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + enum AdditionalRoles { + SensorId = Qt::UserRole + 1, + }; + Q_ENUM(AdditionalRoles) + + explicit SensorTreeModel(QObject *parent = nullptr); + virtual ~SensorTreeModel(); + + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation, int role) const override; + QStringList mimeTypes() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + +private: + void init(); + void onSensorAdded(const QString &sensor); + void onSensorRemoved(const QString &sensor); + void onMetaDataChanged(const QString &sensorId, const SensorInfo &info); + + class Private; + const std::unique_ptr d; +}; + +} diff --git a/sensors/SensorTreeModel.cpp b/sensors/SensorTreeModel.cpp new file mode 100644 --- /dev/null +++ b/sensors/SensorTreeModel.cpp @@ -0,0 +1,445 @@ +/* + Copyright (c) 2019 Eike Hein + Copyright (C) 2020 Arjen Hiemstra + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "SensorTreeModel.h" + +#include +#include +#include +#include + +#include "formatter/Formatter.h" + +#include "Sensor.h" +#include "SensorDaemonInterface_p.h" +#include "SensorInfo_p.h" +#include "SensorQuery.h" + +using namespace KSysGuard; + +struct Q_DECL_HIDDEN SensorTreeItem +{ + SensorTreeItem *parent = nullptr; + QString name; + QVector children; + + inline int indexOf(const QString &name) + { + int index = -1; + + for (int i = 0; i < children.count(); ++i) { + if (children.at(i)->name == name) { + return i; + } + } + + return index; + } + + ~SensorTreeItem() + { + qDeleteAll(children); + } +}; + +class Q_DECL_HIDDEN SensorTreeModel::Private +{ +public: + Private(SensorTreeModel *qq) + : rootItem(new SensorTreeItem) + , q(qq) + { + } + ~Private() + { + delete rootItem; + } + + SensorTreeItem *rootItem; + QHash sensorInfos; + + void addSensor(const QString &sensorId, const SensorInfo &info); + void removeSensor(const QString &sensorId); + + QString sensorId(const QModelIndex &index); + + SensorTreeItem *find(const QString &sensorId); + +private: + SensorTreeModel *q; +}; + +void SensorTreeModel::Private::addSensor(const QString &sensorId, const SensorInfo &info) +{ + const QStringList &segments = sensorId.split(QLatin1Char('/')); + + if (!segments.count() || segments.at(0).isEmpty()) { + qDebug() << "Rejecting sensor" << sensorId << "- sensor id is not well-formed."; + return; + } + + SensorTreeItem *item = rootItem; + + for (auto segment : segments) { + int index = item->indexOf(segment); + + if (index != -1) { + item = item->children.at(index); + } else { + SensorTreeItem *newItem = new SensorTreeItem(); + newItem->parent = item; + newItem->name = segment; + + const QModelIndex &parentIndex = (item == rootItem) ? QModelIndex() : q->createIndex(item->parent->children.indexOf(item), 0, item); + q->beginInsertRows(parentIndex, item->children.count(), item->children.count()); + item->children.append(newItem); + q->endInsertRows(); + + item = newItem; + } + } + + sensorInfos[item] = info; +} + +void SensorTreeModel::Private::removeSensor(const QString &sensorId) +{ + SensorTreeItem *item = rootItem; + + const auto segments = sensorId.split(QLatin1Char('/')); + for (const QString &segment : segments) { + int index = item->indexOf(segment); + Q_ASSERT(index != -1); + item = item->children.at(index); + } + + SensorTreeItem *parent = item->parent; + + if (!parent) { + return; + } + + auto remove = [this](SensorTreeItem *item, SensorTreeItem *parent) { + const int index = item->parent->children.indexOf(item); + + const QModelIndex &parentIndex = (parent == rootItem) ? QModelIndex() : q->createIndex(parent->parent->children.indexOf(parent), 0, parent); + q->beginRemoveRows(parentIndex, index, index); + delete item->parent->children.takeAt(index); + q->endRemoveRows(); + + sensorInfos.remove(item); + }; + + remove(item, parent); + + while (!parent->children.count()) { + item = parent; + parent = parent->parent; + + if (!parent) { + break; + } + + remove(item, parent); + } +} + +QString SensorTreeModel::Private::sensorId(const QModelIndex &index) +{ + QStringList segments; + + SensorTreeItem *item = static_cast(index.internalPointer()); + + segments << item->name; + + while (item->parent && item->parent != rootItem) { + item = item->parent; + segments.prepend(item->name); + } + + return segments.join(QLatin1Char('/')); +} + +SensorTreeItem *KSysGuard::SensorTreeModel::Private::find(const QString &sensorId) +{ + auto item = rootItem; + const auto segments = sensorId.split(QLatin1Char('/')); + for (const QString &segment : segments) { + int index = item->indexOf(segment); + if (index != -1) { + item = item->children.at(index); + } else { + return nullptr; + } + } + return item; +} + +SensorTreeModel::SensorTreeModel(QObject *parent) + : QAbstractItemModel(parent) + , d(new Private(this)) +{ + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::sensorAdded, this, &SensorTreeModel::onSensorAdded); + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::sensorRemoved, this, &SensorTreeModel::onSensorRemoved); + connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::metaDataChanged, this, &SensorTreeModel::onMetaDataChanged); + init(); +} + +SensorTreeModel::~SensorTreeModel() +{ +} + +QHash SensorTreeModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + + QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles")); + + for (int i = 0; i < e.keyCount(); ++i) { + roles.insert(e.value(i), e.key(i)); + } + + return roles; +} + +QVariant SensorTreeModel::headerData(int section, Qt::Orientation, int role) const +{ + if (role != Qt::DisplayRole) { + return QVariant(); + } + + if (section == 0) { + return i18n("Sensor Browser"); + } + + return QVariant(); +} + +QStringList SensorTreeModel::mimeTypes() const +{ + return QStringList() << QStringLiteral("application/x-ksysguard"); +} + +QVariant SensorTreeModel::data(const QModelIndex &index, int role) const +{ + const bool check = checkIndex(index, CheckIndexOption::IndexIsValid); + + Q_ASSERT(check); + + if (!check) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + SensorTreeItem *item = static_cast(index.internalPointer()); + + if (d->sensorInfos.contains(item)) { + auto info = d->sensorInfos.value(item); + const QString &unit = Formatter::symbol(info.unit); + + if (!unit.isEmpty()) { + return i18nc("Name (unit)", "%1 (%2)", info.name, unit); + } + + return info.name; + } + + const QString &name = item->name; + if (name.isEmpty()) { + return i18n("EMPTY"); + } else { + return name; + } + // Only leaf nodes are valid sensors + } else if (role == SensorId) { + if (rowCount(index)) { + return QString(); + } else { + return d->sensorId(index); + } + } + + return QVariant(); +} + +QMimeData *SensorTreeModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *mimeData = new QMimeData(); + + if (indexes.count() != 1) { + return mimeData; + } + + const QModelIndex &index = indexes.at(0); + + const bool check = checkIndex(index, CheckIndexOption::IndexIsValid); + + Q_ASSERT(check); + + if (!check) { + return mimeData; + } + + if (rowCount(index)) { + return mimeData; + } + + mimeData->setData(QStringLiteral("application/x-ksysguard"), d->sensorId(index).toUtf8()); + + return mimeData; +} + +Qt::ItemFlags SensorTreeModel::flags(const QModelIndex &index) const +{ + const bool check = checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::DoNotUseParent); + + Q_ASSERT(check); + + if (!check) { + return Qt::NoItemFlags; + } + + if (!rowCount(index)) { + return Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + + return Qt::ItemIsEnabled; +} + +int SensorTreeModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + const bool check = checkIndex(parent, CheckIndexOption::IndexIsValid | CheckIndexOption::DoNotUseParent); + + Q_ASSERT(check); + + if (!check) { + return 0; + } + + const SensorTreeItem *item = static_cast(parent.internalPointer()); + return item->children.count(); + } + + return d->rootItem->children.count(); +} + +int SensorTreeModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return 1; +} + +QModelIndex SensorTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + SensorTreeItem *parentItem = d->rootItem; + + if (parent.isValid()) { + Q_ASSERT(parent.model() == this); + + if (parent.model() != this) { + return QModelIndex(); + } + + parentItem = static_cast(parent.internalPointer()); + } + + if (row < 0 || row >= parentItem->children.count()) { + return QModelIndex(); + } + + if (column < 0) { + return QModelIndex(); + } + + return createIndex(row, column, parentItem->children.at(row)); +} + +QModelIndex SensorTreeModel::parent(const QModelIndex &index) const +{ + const bool check = checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::DoNotUseParent); + + Q_ASSERT(check); + + if (!check) { + return QModelIndex(); + } + + if (index.column() > 0) { + return QModelIndex(); + } + + const SensorTreeItem *item = static_cast(index.internalPointer()); + SensorTreeItem *parentItem = item->parent; + + if (parentItem == d->rootItem) { + return QModelIndex(); + } + + return createIndex(parentItem->parent->children.indexOf(parentItem), 0, parentItem); +} + +void SensorTreeModel::init() +{ + auto query = new SensorQuery{QString(), this}; + connect(query, &SensorQuery::finished, [query, this]() { + query->deleteLater(); + const auto result = query->result(); + for (auto pair : result) { + d->addSensor(pair.first, pair.second); + } + }); + query->execute(); +} + +void KSysGuard::SensorTreeModel::onSensorAdded(const QString &sensor) +{ + SensorDaemonInterface::instance()->requestMetaData(sensor); +} + +void KSysGuard::SensorTreeModel::onSensorRemoved(const QString &sensor) +{ + d->removeSensor(sensor); +} + +void KSysGuard::SensorTreeModel::onMetaDataChanged(const QString &sensorId, const SensorInfo &info) +{ + auto item = d->find(sensorId); + if (!item) { + d->addSensor(sensorId, info); + } else { + item->name = info.name; + d->sensorInfos[item] = info; + + auto parentItem = item->parent; + if (!parentItem) { + return; + } + + auto parentIndex = QModelIndex{}; + if (parentItem != d->rootItem) { + parentIndex = createIndex(parentItem->parent->children.indexOf(parentItem), 0, parentItem); + } + + auto itemIndex = index(parentItem->children.indexOf(item), 0, parentIndex); + Q_EMIT dataChanged(itemIndex, itemIndex); + } +} diff --git a/sensors/declarative/CMakeLists.txt b/sensors/declarative/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/sensors/declarative/CMakeLists.txt @@ -0,0 +1,8 @@ +include_directories(${CMAKE_CURRENT_BINARY_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/..) + +add_library(SensorsPlugin SHARED SensorsPlugin.cpp) + +target_link_libraries(SensorsPlugin Qt5::Qml KSysGuard::Sensors) + +install(TARGETS SensorsPlugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/ksysguard/sensors) +install(FILES qmldir ExtendedLegend.qml DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/ksysguard/sensors) diff --git a/sensors/declarative/ExtendedLegend.qml b/sensors/declarative/ExtendedLegend.qml new file mode 100644 --- /dev/null +++ b/sensors/declarative/ExtendedLegend.qml @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Marco Martin + * Copyright 2019 David Edmundson + * Copyright 2019 Arjen Hiemstra + * Copyright 2019 Kai Uwe Broulik + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Library General Public License as + * published by the Free Software Foundation; either version 2, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details + * + * You should have received a copy of the GNU Library 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.9 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.8 as Kirigami + +import org.kde.ksysguard.formatter 1.0 +import org.kde.ksysguard.sensors 1.0 + +import org.kde.quickcharts 1.0 as Charts +import org.kde.quickcharts.controls 1.0 as ChartsControls + +ChartsControls.Legend { + id: legend + + property alias textOnlySensorIds: textOnlySensorsRepeater.model + property var sourceModel + property var colorSource + + flow: GridLayout.TopToBottom + + Layout.maximumHeight: implicitHeight + Layout.maximumWidth: parent.width + + spacing: Kirigami.Units.smallSpacing + + valueVisible: true + valueWidth: units.gridUnit * 2 + formatValue: function(input, index) { + return Formatter.formatValueShowNull(input, sourceModel.data(sourceModel.index(0, index), SensorDataModel.Unit)) + } + + Repeater { + id: textOnlySensorsRepeater + delegate: ChartsControls.LegendDelegate { + name: sensor.shortName + value: sensor.formattedValue || "" + colorVisible: false + + layoutWidth: legend.width + valueWidth: units.gridUnit * 2 + + Sensor { + id: sensor + sensorId: modelData + } + } + } +} diff --git a/processcore/formatter.h b/sensors/declarative/SensorsPlugin.h copy from processcore/formatter.h copy to sensors/declarative/SensorsPlugin.h --- a/processcore/formatter.h +++ b/sensors/declarative/SensorsPlugin.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Vlad Zagorodniy + Copyright (C) 2020 Arjen Hiemstra This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -17,4 +17,15 @@ Boston, MA 02110-1301, USA. */ -#include "formatter/Formatter.h" +#pragma once + +#include + +class SensorsPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/processcore/formatter.h b/sensors/declarative/SensorsPlugin.cpp copy from processcore/formatter.h copy to sensors/declarative/SensorsPlugin.cpp --- a/processcore/formatter.h +++ b/sensors/declarative/SensorsPlugin.cpp @@ -1,5 +1,5 @@ /* - Copyright (C) 2019 Vlad Zagorodniy + Copyright (C) 2020 Arjen Hiemstra This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -17,4 +17,21 @@ Boston, MA 02110-1301, USA. */ -#include "formatter/Formatter.h" +#include "SensorsPlugin.h" + +#include "Sensor.h" +#include "SensorDataModel.h" +#include "SensorTreeModel.h" + +#include + +using namespace KSysGuard; + +void SensorsPlugin::registerTypes(const char *uri) +{ + Q_ASSERT(QLatin1String(uri) == QLatin1String("org.kde.ksysguard.sensors")); + + qmlRegisterType(uri, 1, 0, "SensorDataModel"); + qmlRegisterType(uri, 1, 0, "SensorTreeModel"); + qmlRegisterType(uri, 1, 0, "Sensor"); +} diff --git a/sensors/declarative/qmldir b/sensors/declarative/qmldir new file mode 100644 --- /dev/null +++ b/sensors/declarative/qmldir @@ -0,0 +1,3 @@ +module org.kde.ksysguard.sensors +plugin SensorsPlugin +ExtendedLegend 1.0 ExtendedLegend.qml diff --git a/sensors/org.kde.KSysGuardDaemon.xml b/sensors/org.kde.KSysGuardDaemon.xml new file mode 100644 --- /dev/null +++ b/sensors/org.kde.KSysGuardDaemon.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +