Libksysguard

SensorDataModel.cpp
1/*
2 SPDX-FileCopyrightText: 2019 Eike Hein <hein@kde.org>
3 SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "SensorDataModel.h"
9
10#include <chrono>
11#include <optional>
12
13#include <QMetaEnum>
14
15#include "formatter/Formatter.h"
16#include "systemstats/SensorInfo.h"
17
18#include "SensorDaemonInterface_p.h"
19#include "sensors_logging.h"
20
21using namespace KSysGuard;
22
23namespace chrono = std::chrono;
24
25class Q_DECL_HIDDEN SensorDataModel::Private
26{
27public:
28 Private(SensorDataModel *qq)
29 : q(qq)
30 {
31 }
32
33 void sensorsChanged();
34 void addSensor(const QString &id);
35 void removeSensor(const QString &id);
36
37 QStringList requestedSensors;
38
39 QStringList sensors;
40 QStringList objects;
41
43 QHash<QString, QVariant> sensorData;
44 QVariantMap sensorColors;
45 QVariantMap sensorLabels;
46
47 bool usedByQml = false;
48 bool componentComplete = false;
49 bool loaded = false;
50 bool enabled = true;
51
52 std::optional<qreal> minimum;
53 std::optional<qreal> maximum;
54
55 std::optional<int> updateRateLimit;
57
58private:
60};
61
62SensorDataModel::SensorDataModel(const QStringList &sensorIds, QObject *parent)
63 : QAbstractTableModel(parent)
64 , d(new Private(this))
65{
66 connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::sensorAdded, this, &SensorDataModel::onSensorAdded);
67 connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::sensorRemoved, this, &SensorDataModel::onSensorRemoved);
68 connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::metaDataChanged, this, &SensorDataModel::onMetaDataChanged);
69 connect(SensorDaemonInterface::instance(), &SensorDaemonInterface::valueChanged, this, &SensorDataModel::onValueChanged);
70
71 // Empty string is used for entries that do not specify a wildcard object
72 d->objects << QStringLiteral("");
73
74 setSensors(sensorIds);
75}
76
77SensorDataModel::~SensorDataModel()
78{
79}
80
81QHash<int, QByteArray> SensorDataModel::roleNames() const
82{
84
85 QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles"));
86
87 for (int i = 0; i < e.keyCount(); ++i) {
88 roles.insert(e.value(i), e.key(i));
89 }
90
91 return roles;
92}
93
94QVariant SensorDataModel::data(const QModelIndex &index, int role) const
95{
96 const bool check = checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::DoNotUseParent);
97 if (!check) {
98 return QVariant();
99 }
100
101 auto sensor = d->sensors.at(index.column());
102 auto info = d->sensorInfos.value(sensor);
103 auto data = d->sensorData.value(sensor);
104
105 switch (role) {
106 case Qt::DisplayRole:
107 case FormattedValue:
108 return Formatter::formatValue(data, info.unit);
109 case Value:
110 return data;
111 case Unit:
112 return info.unit;
113 case Name:
114 return d->sensorLabels.value(sensor, info.name);
115 case ShortName: {
116 auto it = d->sensorLabels.constFind(sensor);
117 if (it != d->sensorLabels.constEnd()) {
118 return *it;
119 }
120 if (info.shortName.isEmpty()) {
121 return info.name;
122 }
123 return info.shortName;
124 }
125 case Description:
126 return info.description;
127 case Minimum:
128 return info.min;
129 case Maximum:
130 return info.max;
131 case Type:
132 return info.variantType;
133 case SensorId:
134 return sensor;
135 case Color:
136 if (!d->sensorColors.empty()) {
137 return d->sensorColors.value(sensor);
138 }
139 break;
140 case UpdateInterval:
141 // TODO: Make this dynamic once the backend supports it.
142 return BackendUpdateInterval;
143 default:
144 break;
145 }
146
147 return QVariant();
148}
149
150QVariant SensorDataModel::headerData(int section, Qt::Orientation orientation, int role) const
151{
152 if (orientation == Qt::Vertical) {
153 return QVariant();
154 }
155
156 if (section < 0 || section >= d->sensors.size()) {
157 return QVariant();
158 }
159
160 auto sensor = d->sensors.at(section);
161 auto info = d->sensorInfos.value(sensor);
162
163 switch (role) {
164 case Qt::DisplayRole:
165 case ShortName:
166 if (info.shortName.isEmpty()) {
167 return info.name;
168 }
169 return info.shortName;
170 case Name:
171 return info.name;
172 case SensorId:
173 return sensor;
174 case Unit:
175 return info.unit;
176 case Description:
177 return info.description;
178 case Minimum:
179 return info.min;
180 case Maximum:
181 return info.max;
182 case Type:
183 return info.variantType;
184 case UpdateInterval:
185 // TODO: Make this dynamic once the backend supports it.
186 return BackendUpdateInterval;
187 default:
188 break;
189 }
190
191 return QVariant();
192}
193
194int SensorDataModel::rowCount(const QModelIndex &parent) const
195{
196 if (parent.isValid()) {
197 return 0;
198 }
199
200 return d->objects.count();
201}
202
203int SensorDataModel::columnCount(const QModelIndex &parent) const
204{
205 if (parent.isValid()) {
206 return 0;
207 }
208
209 return d->sensorInfos.count();
210}
211
212qreal SensorDataModel::minimum() const
213{
214 if (d->sensors.isEmpty()) {
215 return 0;
216 }
217
218 if (d->minimum.has_value()) {
219 return d->minimum.value();
220 }
221
222 auto result = std::min_element(d->sensorInfos.cbegin(), d->sensorInfos.cend(), [](const SensorInfo &first, const SensorInfo &second) {
223 return first.min < second.min;
224 });
225 if (result == d->sensorInfos.cend()) {
226 d->minimum = 0.0;
227 } else {
228 d->minimum = (*result).min;
229 }
230 return d->minimum.value();
231}
232
233qreal SensorDataModel::maximum() const
234{
235 if (d->sensors.isEmpty()) {
236 return 0;
237 }
238
239 if (d->maximum.has_value()) {
240 return d->maximum.value();
241 }
242
243 auto result = std::max_element(d->sensorInfos.cbegin(), d->sensorInfos.cend(), [](const SensorInfo &first, const SensorInfo &second) {
244 return first.max < second.max;
245 });
246 if (result == d->sensorInfos.cend()) {
247 d->maximum = 0.0;
248 } else {
249 d->maximum = (*result).max;
250 }
251 return d->maximum.value();
252}
253
255{
256 return d->requestedSensors;
257}
258
259void SensorDataModel::setSensors(const QStringList &sensorIds)
260{
261 if (d->requestedSensors == sensorIds) {
262 return;
263 }
264
265 d->requestedSensors = sensorIds;
266
267 if (!d->usedByQml || d->componentComplete) {
268 d->sensorsChanged();
269 }
270 Q_EMIT readyChanged();
271 Q_EMIT sensorsChanged();
272}
273
274bool SensorDataModel::enabled() const
275{
276 return d->enabled;
277}
278
279void SensorDataModel::setEnabled(bool newEnabled)
280{
281 if (newEnabled == d->enabled) {
282 return;
283 }
284
285 d->enabled = newEnabled;
286 if (d->enabled) {
287 SensorDaemonInterface::instance()->subscribe(d->sensorInfos.keys());
288 SensorDaemonInterface::instance()->requestMetaData(d->sensorInfos.keys());
289 } else {
290 SensorDaemonInterface::instance()->unsubscribe(d->sensorInfos.keys());
291 }
292
293 Q_EMIT enabledChanged();
294}
295
296QVariantMap SensorDataModel::sensorColors() const
297{
298 return d->sensorColors;
299}
300
301void SensorDataModel::setSensorColors(const QVariantMap &sensorColors)
302{
303 if (sensorColors == d->sensorColors) {
304 return;
305 }
306 d->sensorColors = sensorColors;
307 Q_EMIT sensorColorsChanged();
308 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1), {Color});
309}
310
311QVariantMap SensorDataModel::sensorLabels() const
312{
313 return d->sensorLabels;
314}
315
316void SensorDataModel::setSensorLabels(const QVariantMap &sensorLabels)
317{
318 if (sensorLabels == d->sensorLabels) {
319 return;
320 }
321 d->sensorLabels = sensorLabels;
322 Q_EMIT sensorLabelsChanged();
323 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1), {Name, ShortName});
324}
325
327{
328 return d->updateRateLimit.value_or(-1);
329}
330
331void SensorDataModel::setUpdateRateLimit(int newUpdateRateLimit)
332{
333 // An update rate limit of 0 or less makes no sense, so treat it as clearing
334 // the limit.
335 if (newUpdateRateLimit <= 0) {
336 if (!d->updateRateLimit) {
337 return;
338 }
339
340 d->updateRateLimit.reset();
341 } else {
342 if (d->updateRateLimit && d->updateRateLimit.value() == newUpdateRateLimit) {
343 return;
344 }
345
346 d->updateRateLimit = newUpdateRateLimit;
347 }
348 d->lastUpdateTimes.clear();
349 Q_EMIT updateRateLimitChanged();
350}
351
352void KSysGuard::SensorDataModel::resetUpdateRateLimit()
353{
354 setUpdateRateLimit(-1);
355}
356
357bool KSysGuard::SensorDataModel::isReady() const
358{
359 return d->sensors.size() == d->sensorInfos.size();
360}
361
362void SensorDataModel::addSensor(const QString &sensorId)
363{
364 d->addSensor(sensorId);
365}
366
367void SensorDataModel::removeSensor(const QString &sensorId)
368{
369 d->removeSensor(sensorId);
370}
371
372int KSysGuard::SensorDataModel::column(const QString &sensorId) const
373{
374 return d->sensors.indexOf(sensorId);
375}
376
377void KSysGuard::SensorDataModel::classBegin()
378{
379 d->usedByQml = true;
380}
381
382void KSysGuard::SensorDataModel::componentComplete()
383{
384 d->componentComplete = true;
385
386 d->sensorsChanged();
387
388 Q_EMIT sensorsChanged();
389}
390
391void SensorDataModel::Private::addSensor(const QString &id)
392{
393 if (!requestedSensors.contains(id)) {
394 return;
395 }
396
397 qCDebug(LIBKSYSGUARD_SENSORS) << "Add Sensor" << id;
398
399 sensors.append(id);
400 SensorDaemonInterface::instance()->subscribe(id);
401 SensorDaemonInterface::instance()->requestMetaData(id);
402}
403
404void SensorDataModel::Private::removeSensor(const QString &id)
405{
406 const int col = sensors.indexOf(id);
407 if (col == -1) {
408 return;
409 }
410
411 q->beginRemoveColumns(QModelIndex(), col, col);
412
413 sensors.removeAt(col);
414 sensorInfos.remove(id);
415 sensorData.remove(id);
416
417 q->endRemoveColumns();
418}
419
420void SensorDataModel::onSensorAdded(const QString &sensorId)
421{
422 if (!d->enabled) {
423 return;
424 }
425
426 d->addSensor(sensorId);
427}
428
429void SensorDataModel::onSensorRemoved(const QString &sensorId)
430{
431 if (!d->enabled) {
432 return;
433 }
434
435 d->removeSensor(sensorId);
436}
437
438void SensorDataModel::onMetaDataChanged(const QString &sensorId, const SensorInfo &info)
439{
440 if (!d->enabled) {
441 return;
442 }
443
444 auto column = d->sensors.indexOf(sensorId);
445 if (column == -1) {
446 return;
447 }
448
449 qCDebug(LIBKSYSGUARD_SENSORS) << "Received metadata change for" << sensorId;
450
451 d->minimum.reset();
452 d->maximum.reset();
453
454 // Simple case: Just an update for a sensor's metadata
455 if (d->sensorInfos.contains(sensorId)) {
456 d->sensorInfos[sensorId] = info;
458 Q_EMIT sensorMetaDataChanged();
459 return;
460 }
461
462 // Otherwise, it's a new sensor that was added
463
464 // Ensure we do not insert columns that are out of range.
465 while (d->sensorInfos.count() + 1 <= column && column > 0) {
466 column--;
467 }
468
469 beginInsertColumns(QModelIndex{}, column, column);
470 d->sensorInfos[sensorId] = info;
471 d->sensorData[sensorId] = QVariant{};
473
474 SensorDaemonInterface::instance()->requestValue(sensorId);
475 Q_EMIT sensorMetaDataChanged();
476
477 if (isReady()) {
478 Q_EMIT readyChanged();
479 }
480}
481
482void SensorDataModel::onValueChanged(const QString &sensorId, const QVariant &value)
483{
484 const auto column = d->sensors.indexOf(sensorId);
485 if (column == -1 || !d->enabled) {
486 return;
487 }
488
489 if (d->updateRateLimit && d->sensorData[sensorId].isValid()) {
490 auto updateRateLimit = chrono::steady_clock::duration(chrono::milliseconds(d->updateRateLimit.value()));
491 auto now = chrono::steady_clock::now();
492 if (d->lastUpdateTimes.contains(column) && now - d->lastUpdateTimes.value(column) < updateRateLimit) {
493 return;
494 } else {
495 d->lastUpdateTimes[column] = now;
496 }
497 }
498
499 d->sensorData[sensorId] = value;
500 Q_EMIT dataChanged(index(0, column), index(0, column), {Qt::DisplayRole, Value, FormattedValue});
501}
502
503void SensorDataModel::Private::sensorsChanged()
504{
505 q->beginResetModel();
506
507 SensorDaemonInterface::instance()->unsubscribe(sensors);
508
509 sensors.clear();
510 sensorData.clear();
511 sensorInfos.clear();
512 lastUpdateTimes.clear();
513
514 sensors = requestedSensors;
515
516 SensorDaemonInterface::instance()->subscribe(requestedSensors);
517 SensorDaemonInterface::instance()->requestMetaData(requestedSensors);
518
519 q->endResetModel();
520}
static QString formatValue(const QVariant &value, Unit unit, MetricPrefix targetPrefix=MetricPrefixAutoAdjust, FormatOptions options=FormatOptionNone)
Converts value to the appropriate displayable string.
A model representing a table of sensors.
@ Value
The value of the sensor.
@ Description
A description for the sensor.
@ Name
The name of the sensor.
@ ShortName
A shorter name for the sensor. This is equal to name if not set.
@ UpdateInterval
The time in milliseconds between each update of the sensor.
@ Color
A color of the sensor, if sensorColors is set.
@ FormattedValue
A formatted string of the value of the sensor.
@ Minimum
The minimum value this sensor can have.
@ SensorId
The backend path to the sensor.
@ Maximum
The maximum value this sensor can have.
@ Unit
The unit of the sensor.
@ Type
The QVariant::Type of the sensor.
int updateRateLimit
The minimum time between updates, in milliseconds.
qreal maximum
The maximum value of all sensors' maximum property.
bool enabled
Should this model be updated or not.
QVariantMap sensorLabels
Provides custom labels for Sensors that are used instead of the name and short name of the sensors.
qreal minimum
The minimum value of all sensors' minimum property.
QVariantMap sensorColors
Used by the model to provide data for the Color role if set.
QStringList sensors
The list of sensors to watch.
void beginInsertColumns(const QModelIndex &parent, int first, int last)
bool checkIndex(const QModelIndex &index, CheckIndexOptions options) const const
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual QHash< int, QByteArray > roleNames() const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
iterator insert(const Key &key, const T &value)
void append(QList< T > &&value)
void clear()
void removeAt(qsizetype i)
const char * key(int index) const const
int keyCount() const const
int value(int index) const const
QMetaEnum enumerator(int index) const const
int column() const const
Q_EMITQ_EMIT
virtual const QMetaObject * metaObject() const const
QObject * parent() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
qsizetype indexOf(const QRegularExpression &re, qsizetype from) const const
DisplayRole
Orientation
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:21:23 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.