forked from feedc0de/evcharger-app
600 lines
21 KiB
C++
600 lines
21 KiB
C++
#include "homeassistantclient.h"
|
|
|
|
#include "appsettings.h"
|
|
|
|
#include <QDateTime>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonParseError>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QUrlQuery>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace
|
|
{
|
|
constexpr auto urlSetting = "homeAssistant/url";
|
|
constexpr auto tokenSetting = "homeAssistant/accessToken";
|
|
constexpr auto chartsSetting = "homeAssistant/charts";
|
|
|
|
QString normalizedBaseUrl(const QString &input)
|
|
{
|
|
QString text = input.trimmed();
|
|
while (text.endsWith('/'))
|
|
text.chop(1);
|
|
return text;
|
|
}
|
|
|
|
QDateTime beginningOfPeriod(const QString &range, int offset)
|
|
{
|
|
const QDate today = QDate::currentDate();
|
|
QDate start;
|
|
if (range == QStringLiteral("week"))
|
|
start = today.addDays(1 - today.dayOfWeek()).addDays(offset * 7);
|
|
else if (range == QStringLiteral("month"))
|
|
start = QDate(today.year(), today.month(), 1).addMonths(offset);
|
|
else if (range == QStringLiteral("year"))
|
|
start = QDate(today.year(), 1, 1).addYears(offset);
|
|
else
|
|
start = today.addDays(offset);
|
|
return start.startOfDay();
|
|
}
|
|
|
|
QDateTime endOfPeriod(const QString &range, const QDateTime &start)
|
|
{
|
|
if (range == QStringLiteral("week"))
|
|
return start.addDays(7);
|
|
if (range == QStringLiteral("month"))
|
|
return QDateTime(start.date().addMonths(1), QTime(0, 0));
|
|
if (range == QStringLiteral("year"))
|
|
return QDateTime(start.date().addYears(1), QTime(0, 0));
|
|
return start.addDays(1);
|
|
}
|
|
|
|
QString defaultColor(int index)
|
|
{
|
|
static const QStringList colors{
|
|
QStringLiteral("#3274d9"), QStringLiteral("#f2c037"),
|
|
QStringLiteral("#2ca02c"), QStringLiteral("#d64f4f"),
|
|
QStringLiteral("#8b5cf6"), QStringLiteral("#00a6a6"),
|
|
QStringLiteral("#e8792e"), QStringLiteral("#64748b")};
|
|
return colors.at(index % colors.size());
|
|
}
|
|
}
|
|
|
|
HomeAssistantEntityModel::HomeAssistantEntityModel(QObject *parent)
|
|
: QAbstractListModel(parent)
|
|
{
|
|
}
|
|
|
|
int HomeAssistantEntityModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
return parent.isValid() ? 0 : m_visibleRows.size();
|
|
}
|
|
|
|
QVariant HomeAssistantEntityModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_visibleRows.size())
|
|
return {};
|
|
const Entity &entity = m_entities.at(m_visibleRows.at(index.row()));
|
|
switch (role)
|
|
{
|
|
case EntityIdRole: return entity.entityId;
|
|
case NameRole: return entity.name;
|
|
case UnitRole: return entity.unit;
|
|
case DeviceClassRole: return entity.deviceClass;
|
|
case StateRole: return entity.state;
|
|
case SearchTextRole: return entity.searchText;
|
|
default: return {};
|
|
}
|
|
}
|
|
|
|
QHash<int, QByteArray> HomeAssistantEntityModel::roleNames() const
|
|
{
|
|
return {{EntityIdRole, "entityId"}, {NameRole, "name"}, {UnitRole, "unit"},
|
|
{DeviceClassRole, "deviceClass"}, {StateRole, "currentValue"},
|
|
{SearchTextRole, "searchText"}};
|
|
}
|
|
|
|
void HomeAssistantEntityModel::setEntities(QList<Entity> entities)
|
|
{
|
|
std::sort(entities.begin(), entities.end(), [](const Entity &left, const Entity &right) {
|
|
const int nameOrder = QString::localeAwareCompare(left.name, right.name);
|
|
return nameOrder == 0 ? left.entityId < right.entityId : nameOrder < 0;
|
|
});
|
|
|
|
beginResetModel();
|
|
m_entities = std::move(entities);
|
|
m_entityRows.clear();
|
|
for (int i = 0; i < m_entities.size(); ++i)
|
|
m_entityRows.insert(m_entities.at(i).entityId, i);
|
|
m_visibleRows.clear();
|
|
const QStringList words = m_filter.split(' ', Qt::SkipEmptyParts);
|
|
for (int i = 0; i < m_entities.size(); ++i)
|
|
{
|
|
bool matches = true;
|
|
for (const QString &word : words)
|
|
matches = matches && m_entities.at(i).searchText.contains(word, Qt::CaseInsensitive);
|
|
if (matches)
|
|
m_visibleRows.append(i);
|
|
}
|
|
endResetModel();
|
|
}
|
|
|
|
void HomeAssistantEntityModel::setFilter(const QString &filter)
|
|
{
|
|
const QString simplified = filter.simplified();
|
|
if (m_filter == simplified)
|
|
return;
|
|
m_filter = simplified;
|
|
rebuildVisibleRows();
|
|
}
|
|
|
|
const HomeAssistantEntityModel::Entity *HomeAssistantEntityModel::entity(
|
|
const QString &entityId) const
|
|
{
|
|
const auto row = m_entityRows.constFind(entityId);
|
|
return row == m_entityRows.cend() ? nullptr : &m_entities.at(*row);
|
|
}
|
|
|
|
void HomeAssistantEntityModel::rebuildVisibleRows()
|
|
{
|
|
beginResetModel();
|
|
m_visibleRows.clear();
|
|
const QStringList words = m_filter.split(' ', Qt::SkipEmptyParts);
|
|
for (int i = 0; i < m_entities.size(); ++i)
|
|
{
|
|
bool matches = true;
|
|
for (const QString &word : words)
|
|
matches = matches && m_entities.at(i).searchText.contains(word, Qt::CaseInsensitive);
|
|
if (matches)
|
|
m_visibleRows.append(i);
|
|
}
|
|
endResetModel();
|
|
}
|
|
|
|
HomeAssistantClient::HomeAssistantClient(QObject *parent)
|
|
: QObject(parent), m_settings(nullptr), m_entities(this)
|
|
{
|
|
}
|
|
|
|
void HomeAssistantClient::setSettings(AppSettings *settings)
|
|
{
|
|
if (m_settings == settings)
|
|
return;
|
|
if (m_settings)
|
|
disconnect(m_settings, &AppSettings::homeAssistantConfigurationChanged,
|
|
this, &HomeAssistantClient::loadConfiguration);
|
|
m_settings = settings;
|
|
if (m_settings)
|
|
connect(m_settings, &AppSettings::homeAssistantConfigurationChanged,
|
|
this, &HomeAssistantClient::loadConfiguration);
|
|
loadConfiguration();
|
|
emit settingsChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::setEntitySearch(const QString &search)
|
|
{
|
|
if (m_entitySearch == search)
|
|
return;
|
|
m_entitySearch = search;
|
|
m_entities.setFilter(search);
|
|
emit entitySearchChanged();
|
|
}
|
|
|
|
QVariantMap HomeAssistantClient::entityDetails(const QString &entityId) const
|
|
{
|
|
const auto *entity = m_entities.entity(entityId);
|
|
if (!entity)
|
|
return {{QStringLiteral("entityId"), entityId},
|
|
{QStringLiteral("name"), entityId}};
|
|
return {{QStringLiteral("entityId"), entity->entityId},
|
|
{QStringLiteral("name"), entity->name},
|
|
{QStringLiteral("unit"), entity->unit},
|
|
{QStringLiteral("deviceClass"), entity->deviceClass},
|
|
{QStringLiteral("currentValue"), entity->state}};
|
|
}
|
|
|
|
QString HomeAssistantClient::defaultSeriesColor(int index) const
|
|
{
|
|
return defaultColor(index);
|
|
}
|
|
|
|
void HomeAssistantClient::testAndSaveConnection(const QString &baseUrl,
|
|
const QString &accessToken)
|
|
{
|
|
if (m_busy)
|
|
return;
|
|
|
|
m_pendingBaseUrl = normalizedBaseUrl(baseUrl);
|
|
m_pendingAccessToken = accessToken.trimmed();
|
|
const QUrl parsedUrl(m_pendingBaseUrl);
|
|
if (!parsedUrl.isValid()
|
|
|| (parsedUrl.scheme() != QStringLiteral("http")
|
|
&& parsedUrl.scheme() != QStringLiteral("https"))
|
|
|| parsedUrl.host().isEmpty())
|
|
{
|
|
setErrorText(tr("Enter a valid HTTP or HTTPS Home Assistant address."));
|
|
emit connectionTestFinished(false);
|
|
return;
|
|
}
|
|
if (m_pendingAccessToken.isEmpty())
|
|
{
|
|
setErrorText(tr("Enter a Home Assistant access token."));
|
|
emit connectionTestFinished(false);
|
|
return;
|
|
}
|
|
|
|
setErrorText({});
|
|
setStatusText(tr("Connecting to Home Assistant…"));
|
|
setBusy(true);
|
|
|
|
QUrl url = parsedUrl;
|
|
QString path = url.path();
|
|
if (path.endsWith('/'))
|
|
path.chop(1);
|
|
url.setPath(path + QStringLiteral("/api/"));
|
|
QNetworkReply *reply = m_network.get(authenticatedRequest(
|
|
url, m_pendingAccessToken.toUtf8()));
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
|
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
const QByteArray body = reply->readAll();
|
|
const bool success = reply->error() == QNetworkReply::NoError
|
|
&& status >= 200 && status < 300;
|
|
if (success)
|
|
{
|
|
m_baseUrl = m_pendingBaseUrl;
|
|
m_accessToken = m_pendingAccessToken;
|
|
persistConfiguration();
|
|
emit configurationChanged();
|
|
setErrorText({});
|
|
setStatusText(tr("Connected to Home Assistant."));
|
|
}
|
|
else
|
|
{
|
|
QString detail = reply->errorString();
|
|
const QJsonDocument errorDocument = QJsonDocument::fromJson(body);
|
|
if (errorDocument.isObject()
|
|
&& !errorDocument.object().value(QStringLiteral("message")).toString().isEmpty())
|
|
detail = errorDocument.object().value(QStringLiteral("message")).toString();
|
|
setErrorText(status == 401
|
|
? tr("Home Assistant rejected the access token.")
|
|
: tr("Could not connect to Home Assistant: %1").arg(detail));
|
|
setStatusText({});
|
|
}
|
|
setBusy(false);
|
|
reply->deleteLater();
|
|
emit connectionTestFinished(success);
|
|
if (success)
|
|
loadEntities();
|
|
});
|
|
}
|
|
|
|
void HomeAssistantClient::loadEntities()
|
|
{
|
|
if (!configured() || m_entitiesLoading)
|
|
return;
|
|
setErrorText({});
|
|
setEntitiesLoading(true);
|
|
setStatusText(tr("Loading Home Assistant entities…"));
|
|
QNetworkReply *reply = m_network.get(authenticatedRequest(apiUrl(QStringLiteral("api/states"))));
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
|
const QByteArray body = reply->readAll();
|
|
QJsonParseError parseError;
|
|
const QJsonDocument document = QJsonDocument::fromJson(body, &parseError);
|
|
if (reply->error() != QNetworkReply::NoError || !document.isArray())
|
|
{
|
|
setErrorText(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401
|
|
? tr("The Home Assistant credential is no longer valid.")
|
|
: tr("Could not load Home Assistant entities: %1")
|
|
.arg(reply->errorString()));
|
|
}
|
|
else
|
|
{
|
|
QList<HomeAssistantEntityModel::Entity> entities;
|
|
entities.reserve(document.array().size());
|
|
for (const QJsonValue &value : document.array())
|
|
{
|
|
const QJsonObject object = value.toObject();
|
|
const QString entityId = object.value(QStringLiteral("entity_id")).toString();
|
|
if (entityId.isEmpty())
|
|
continue;
|
|
const QJsonObject attributes = object.value(QStringLiteral("attributes")).toObject();
|
|
HomeAssistantEntityModel::Entity entity;
|
|
entity.entityId = entityId;
|
|
entity.name = attributes.value(QStringLiteral("friendly_name")).toString();
|
|
if (entity.name.isEmpty())
|
|
entity.name = entityId;
|
|
entity.unit = attributes.value(QStringLiteral("unit_of_measurement")).toString();
|
|
entity.deviceClass = attributes.value(QStringLiteral("device_class")).toString();
|
|
entity.state = object.value(QStringLiteral("state")).toString();
|
|
entity.searchText = entity.name + QLatin1Char(' ') + entity.entityId
|
|
+ QLatin1Char(' ') + entity.unit + QLatin1Char(' ')
|
|
+ entity.deviceClass;
|
|
entities.append(std::move(entity));
|
|
}
|
|
m_entities.setEntities(std::move(entities));
|
|
setErrorText({});
|
|
setStatusText(tr("Loaded %1 Home Assistant entities.")
|
|
.arg(m_entities.rowCount()));
|
|
}
|
|
setEntitiesLoading(false);
|
|
reply->deleteLater();
|
|
});
|
|
}
|
|
|
|
void HomeAssistantClient::saveCharts(const QVariantList &charts)
|
|
{
|
|
m_charts = charts;
|
|
persistConfiguration();
|
|
emit chartsChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::clearConfiguration()
|
|
{
|
|
if (m_historyReply)
|
|
{
|
|
disconnect(m_historyReply, nullptr, this, nullptr);
|
|
m_historyReply->abort();
|
|
m_historyReply->deleteLater();
|
|
m_historyReply = nullptr;
|
|
setHistoryLoading(false);
|
|
}
|
|
m_baseUrl.clear();
|
|
m_accessToken.clear();
|
|
m_charts.clear();
|
|
m_historyPanels.clear();
|
|
m_entities.setEntities({});
|
|
if (m_settings)
|
|
{
|
|
m_settings->remove(QStringLiteral("homeAssistant"));
|
|
m_settings->sync();
|
|
emit m_settings->homeAssistantConfigurationChanged();
|
|
}
|
|
setStatusText({});
|
|
setErrorText({});
|
|
emit configurationChanged();
|
|
emit chartsChanged();
|
|
emit historyPanelsChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::requestHistory(const QString &range, int offset)
|
|
{
|
|
if (!configured() || m_charts.isEmpty())
|
|
return;
|
|
|
|
if (m_historyReply)
|
|
{
|
|
disconnect(m_historyReply, nullptr, this, nullptr);
|
|
m_historyReply->abort();
|
|
m_historyReply->deleteLater();
|
|
m_historyReply = nullptr;
|
|
setHistoryLoading(false);
|
|
}
|
|
|
|
QStringList entityIds;
|
|
for (const QVariant &chartValue : m_charts)
|
|
{
|
|
const QVariantList series = chartValue.toMap().value(QStringLiteral("entities")).toList();
|
|
for (const QVariant &seriesValue : series)
|
|
{
|
|
const QString entityId = seriesValue.toMap().value(QStringLiteral("entityId")).toString();
|
|
if (!entityId.isEmpty() && !entityIds.contains(entityId))
|
|
entityIds.append(entityId);
|
|
}
|
|
}
|
|
if (entityIds.isEmpty())
|
|
return;
|
|
|
|
const QDateTime start = beginningOfPeriod(range, offset);
|
|
QDateTime end = endOfPeriod(range, start);
|
|
const QDateTime now = QDateTime::currentDateTime();
|
|
if (end > now)
|
|
end = now;
|
|
if (start > end)
|
|
return;
|
|
|
|
QUrl url = apiUrl(QStringLiteral("api/history/period/")
|
|
+ start.toUTC().toString(Qt::ISODate));
|
|
QUrlQuery query;
|
|
query.addQueryItem(QStringLiteral("end_time"), end.toUTC().toString(Qt::ISODate));
|
|
query.addQueryItem(QStringLiteral("filter_entity_id"), entityIds.join(','));
|
|
query.addQueryItem(QStringLiteral("minimal_response"), QString());
|
|
query.addQueryItem(QStringLiteral("no_attributes"), QString());
|
|
url.setQuery(query);
|
|
|
|
setErrorText({});
|
|
setHistoryLoading(true);
|
|
setStatusText(tr("Loading Home Assistant history…"));
|
|
QNetworkReply *reply = m_network.get(authenticatedRequest(url));
|
|
m_historyReply = reply;
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
|
if (m_historyReply != reply)
|
|
{
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
m_historyReply = nullptr;
|
|
const QByteArray body = reply->readAll();
|
|
QJsonParseError parseError;
|
|
const QJsonDocument document = QJsonDocument::fromJson(body, &parseError);
|
|
if (reply->error() != QNetworkReply::NoError || !document.isArray())
|
|
{
|
|
setErrorText(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401
|
|
? tr("The Home Assistant credential is no longer valid.")
|
|
: tr("Could not load Home Assistant history: %1")
|
|
.arg(reply->errorString()));
|
|
}
|
|
else
|
|
{
|
|
rebuildHistoryPanels(document.array());
|
|
setErrorText({});
|
|
setStatusText(tr("Home Assistant history is up to date."));
|
|
}
|
|
setHistoryLoading(false);
|
|
reply->deleteLater();
|
|
});
|
|
}
|
|
|
|
QUrl HomeAssistantClient::apiUrl(const QString &relativePath) const
|
|
{
|
|
QUrl url(m_baseUrl);
|
|
QString path = url.path();
|
|
if (!path.endsWith('/'))
|
|
path += '/';
|
|
url.setPath(path + relativePath);
|
|
return url;
|
|
}
|
|
|
|
QNetworkRequest HomeAssistantClient::authenticatedRequest(const QUrl &url,
|
|
const QByteArray &token) const
|
|
{
|
|
QNetworkRequest request(url);
|
|
request.setRawHeader("Authorization", "Bearer " + (token.isEmpty()
|
|
? m_accessToken.toUtf8()
|
|
: token));
|
|
request.setRawHeader("Accept", "application/json");
|
|
request.setHeader(QNetworkRequest::UserAgentHeader,
|
|
QStringLiteral("EVChargerApp HomeAssistantClient"));
|
|
request.setTransferTimeout(20000);
|
|
return request;
|
|
}
|
|
|
|
void HomeAssistantClient::setBusy(bool busy)
|
|
{
|
|
if (m_busy == busy)
|
|
return;
|
|
m_busy = busy;
|
|
emit busyChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::setEntitiesLoading(bool loading)
|
|
{
|
|
if (m_entitiesLoading == loading)
|
|
return;
|
|
m_entitiesLoading = loading;
|
|
emit entitiesLoadingChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::setHistoryLoading(bool loading)
|
|
{
|
|
if (m_historyLoading == loading)
|
|
return;
|
|
m_historyLoading = loading;
|
|
emit historyLoadingChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::setStatusText(const QString &text)
|
|
{
|
|
if (m_statusText == text)
|
|
return;
|
|
m_statusText = text;
|
|
emit statusTextChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::setErrorText(const QString &text)
|
|
{
|
|
if (m_errorText == text)
|
|
return;
|
|
m_errorText = text;
|
|
emit errorTextChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::persistConfiguration()
|
|
{
|
|
if (!m_settings)
|
|
return;
|
|
m_settings->setValue(urlSetting, m_baseUrl);
|
|
m_settings->setValue(tokenSetting, m_accessToken);
|
|
m_settings->setValue(chartsSetting,
|
|
QJsonDocument::fromVariant(m_charts).toJson(QJsonDocument::Compact));
|
|
m_settings->sync();
|
|
emit m_settings->homeAssistantConfigurationChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::loadConfiguration()
|
|
{
|
|
const QString previousBaseUrl = m_baseUrl;
|
|
const QString previousAccessToken = m_accessToken;
|
|
const QVariantList previousCharts = m_charts;
|
|
m_baseUrl.clear();
|
|
m_accessToken.clear();
|
|
m_charts.clear();
|
|
if (m_settings)
|
|
{
|
|
m_baseUrl = normalizedBaseUrl(m_settings->value(urlSetting).toString());
|
|
m_accessToken = m_settings->value(tokenSetting).toString();
|
|
|
|
QJsonParseError error;
|
|
const QJsonDocument document = QJsonDocument::fromJson(
|
|
m_settings->value(chartsSetting).toByteArray(), &error);
|
|
if (error.error == QJsonParseError::NoError && document.isArray())
|
|
m_charts = document.array().toVariantList();
|
|
}
|
|
if (previousBaseUrl != m_baseUrl || previousAccessToken != m_accessToken)
|
|
emit configurationChanged();
|
|
if (previousCharts != m_charts)
|
|
emit chartsChanged();
|
|
}
|
|
|
|
void HomeAssistantClient::rebuildHistoryPanels(const QJsonArray &history)
|
|
{
|
|
QHash<QString, QVariantList> pointsByEntity;
|
|
for (const QJsonValue &seriesValue : history)
|
|
{
|
|
const QJsonArray states = seriesValue.toArray();
|
|
QString entityId;
|
|
QVariantList points;
|
|
points.reserve(states.size());
|
|
for (const QJsonValue &stateValue : states)
|
|
{
|
|
const QJsonObject state = stateValue.toObject();
|
|
const QString receivedId = state.value(QStringLiteral("entity_id")).toString();
|
|
if (!receivedId.isEmpty())
|
|
entityId = receivedId;
|
|
bool numeric = false;
|
|
const double value = state.value(QStringLiteral("state")).toString().toDouble(&numeric);
|
|
const QString timestampText = state.value(QStringLiteral("last_changed")).toString(
|
|
state.value(QStringLiteral("last_updated")).toString());
|
|
const QDateTime timestamp = QDateTime::fromString(timestampText, Qt::ISODate);
|
|
if (numeric && timestamp.isValid())
|
|
points.append(QVariantMap{{QStringLiteral("t"), timestamp.toMSecsSinceEpoch() / 1000.0},
|
|
{QStringLiteral("value"), value}});
|
|
}
|
|
if (!entityId.isEmpty())
|
|
pointsByEntity.insert(entityId, points);
|
|
}
|
|
|
|
QVariantList panels;
|
|
for (const QVariant &chartValue : m_charts)
|
|
{
|
|
const QVariantMap chart = chartValue.toMap();
|
|
QVariantList dataSeries;
|
|
const QVariantList configuredSeries = chart.value(QStringLiteral("entities")).toList();
|
|
for (int i = 0; i < configuredSeries.size(); ++i)
|
|
{
|
|
const QVariantMap configuration = configuredSeries.at(i).toMap();
|
|
const QString entityId = configuration.value(QStringLiteral("entityId")).toString();
|
|
const auto *entity = m_entities.entity(entityId);
|
|
const QString label = configuration.value(QStringLiteral("label")).toString();
|
|
dataSeries.append(QVariantMap{
|
|
{QStringLiteral("name"), entityId},
|
|
{QStringLiteral("friendlyName"), !label.isEmpty() ? label
|
|
: entity ? entity->name : entityId},
|
|
{QStringLiteral("unit"), entity ? entity->unit : QString()},
|
|
{QStringLiteral("color"), configuration.value(QStringLiteral("color"),
|
|
defaultColor(i))},
|
|
{QStringLiteral("showLegend"), true},
|
|
{QStringLiteral("dataSet"), pointsByEntity.value(entityId)}});
|
|
}
|
|
panels.append(QVariantMap{{QStringLiteral("title"), chart.value(QStringLiteral("title"))},
|
|
{QStringLiteral("dataseries"), dataSeries}});
|
|
}
|
|
m_historyPanels = panels;
|
|
m_historyCurrentTime = QDateTime::currentSecsSinceEpoch();
|
|
emit historyPanelsChanged();
|
|
}
|