Make the energy charts scrollable
This commit is contained in:
parent
635e04cf21
commit
1d21e88e67
@ -1,6 +1,7 @@
|
|||||||
#include "energylogs.h"
|
#include "energylogs.h"
|
||||||
|
|
||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
NYMEA_LOGGING_CATEGORY(dcEnergyLogs, "EnergyLogs")
|
NYMEA_LOGGING_CATEGORY(dcEnergyLogs, "EnergyLogs")
|
||||||
@ -59,9 +60,9 @@ void EnergyLogs::setEngine(Engine *engine)
|
|||||||
if (m_engine->jsonRpcClient()->experiences().value("Energy").toString() >= "1.0") {
|
if (m_engine->jsonRpcClient()->experiences().value("Energy").toString() >= "1.0") {
|
||||||
m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceivedInternal");
|
m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceivedInternal");
|
||||||
|
|
||||||
if (m_ready && !m_loadingInhibited) {
|
// if (m_ready && !m_loadingInhibited) {
|
||||||
fetchLogs();
|
// fetchLogs();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,8 +77,7 @@ void EnergyLogs::setSampleRate(SampleRate sampleRate)
|
|||||||
if (m_sampleRate != sampleRate) {
|
if (m_sampleRate != sampleRate) {
|
||||||
m_sampleRate = sampleRate;
|
m_sampleRate = sampleRate;
|
||||||
emit sampleRateChanged();
|
emit sampleRateChanged();
|
||||||
|
clear();
|
||||||
fetchLogs();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +136,9 @@ void EnergyLogs::setLoadingInhibited(bool loadingInhibited)
|
|||||||
m_loadingInhibited = loadingInhibited;
|
m_loadingInhibited = loadingInhibited;
|
||||||
emit loadingInhibitedChanged();
|
emit loadingInhibitedChanged();
|
||||||
|
|
||||||
if (!m_loadingInhibited) {
|
// if (!m_loadingInhibited) {
|
||||||
fetchLogs();
|
// fetchLogs();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ void EnergyLogs::classBegin()
|
|||||||
void EnergyLogs::componentComplete()
|
void EnergyLogs::componentComplete()
|
||||||
{
|
{
|
||||||
m_ready = true;
|
m_ready = true;
|
||||||
fetchLogs();
|
// fetchLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
int EnergyLogs::rowCount(const QModelIndex &parent) const
|
int EnergyLogs::rowCount(const QModelIndex &parent) const
|
||||||
@ -166,6 +166,16 @@ QVariant EnergyLogs::data(const QModelIndex &index, int role) const
|
|||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double EnergyLogs::minValue() const
|
||||||
|
{
|
||||||
|
return m_minValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double EnergyLogs::maxValue() const
|
||||||
|
{
|
||||||
|
return m_maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
EnergyLogEntry *EnergyLogs::get(int index) const
|
EnergyLogEntry *EnergyLogs::get(int index) const
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= m_list.count()) {
|
if (index < 0 || index >= m_list.count()) {
|
||||||
@ -174,27 +184,54 @@ EnergyLogEntry *EnergyLogs::get(int index) const
|
|||||||
return m_list.at(index);
|
return m_list.at(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnergyLogs::appendEntry(EnergyLogEntry *entry)
|
EnergyLogEntry *EnergyLogs::find(const QDateTime ×tamp)
|
||||||
|
{
|
||||||
|
if (m_list.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
QDateTime first = m_list.first()->timestamp();
|
||||||
|
if (timestamp < first || timestamp > m_list.last()->timestamp()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
int index = qRound(1.0 * first.secsTo(timestamp) / (m_sampleRate * 60));
|
||||||
|
if (index < 0 || index >= m_list.count()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return m_list.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnergyLogs::appendEntry(EnergyLogEntry *entry, double minValue, double maxValue)
|
||||||
{
|
{
|
||||||
entry->setParent(this);
|
entry->setParent(this);
|
||||||
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
|
int index = m_list.count();
|
||||||
|
beginInsertRows(QModelIndex(), index, index);
|
||||||
m_list.append(entry);
|
m_list.append(entry);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
emit countChanged();
|
emit countChanged();
|
||||||
emit entryAdded(entry);
|
emit entryAdded(index, entry);
|
||||||
emit entriesAdded({entry});
|
emit entriesAdded(index, {entry});
|
||||||
|
if (minValue < m_minValue) {
|
||||||
|
m_minValue = minValue;
|
||||||
|
emit minValueChanged();
|
||||||
|
}
|
||||||
|
if (maxValue > m_maxValue) {
|
||||||
|
m_maxValue = maxValue;
|
||||||
|
emit maxValueChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnergyLogs::appendEntries(const QList<EnergyLogEntry *> &entries)
|
void EnergyLogs::appendEntries(const QList<EnergyLogEntry *> &entries)
|
||||||
{
|
{
|
||||||
beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + entries.count());
|
int index = m_list.count();
|
||||||
foreach (EnergyLogEntry* entry, entries) {
|
beginInsertRows(QModelIndex(), index, index + entries.count());
|
||||||
|
for (int i = 0; i < entries.count(); i++) {
|
||||||
|
EnergyLogEntry* entry = entries.at(i);
|
||||||
entry->setParent(this);
|
entry->setParent(this);
|
||||||
m_list.append(entry);
|
m_list.append(entry);
|
||||||
emit entryAdded(entry);
|
emit entryAdded(index + i, entry);
|
||||||
}
|
}
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
emit entriesAdded(entries);
|
emit entriesAdded(index, entries);
|
||||||
emit countChanged();
|
emit countChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,18 +243,92 @@ QVariantMap EnergyLogs::fetchParams() const
|
|||||||
void EnergyLogs::getLogsResponse(int commandId, const QVariantMap ¶ms)
|
void EnergyLogs::getLogsResponse(int commandId, const QVariantMap ¶ms)
|
||||||
{
|
{
|
||||||
Q_UNUSED(commandId)
|
Q_UNUSED(commandId)
|
||||||
if (!m_list.isEmpty()) {
|
|
||||||
beginResetModel();
|
double minValue = 0, maxValue = 0;
|
||||||
qDeleteAll(m_list);
|
// qCDebug(dcEnergyLogs()) << "Logs response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
|
||||||
m_list.clear();
|
QList<EnergyLogEntry*> entries = unpackEntries(params, &minValue, &maxValue);
|
||||||
endResetModel();
|
|
||||||
|
if (!entries.isEmpty()) {
|
||||||
|
if (m_list.isEmpty()) {
|
||||||
|
qCDebug(dcEnergyLogs()) << "Energy logs received";
|
||||||
|
beginInsertRows(QModelIndex(), 0, entries.count());
|
||||||
|
m_list.append(entries);
|
||||||
|
endInsertRows();
|
||||||
|
emit entriesAdded(0, entries);
|
||||||
|
m_minValue = minValue;
|
||||||
|
emit minValueChanged();
|
||||||
|
m_maxValue = maxValue;
|
||||||
|
emit maxValueChanged();
|
||||||
|
|
||||||
|
} else if (entries.first()->timestamp() < m_list.first()->timestamp()) {
|
||||||
|
|
||||||
|
if (entries.last()->timestamp().addSecs(m_sampleRate * 60) == m_list.first()->timestamp()) {
|
||||||
|
beginInsertRows(QModelIndex(), 0, entries.count());
|
||||||
|
m_list = entries + m_list;
|
||||||
|
endInsertRows();
|
||||||
|
emit entriesAdded(0, entries);
|
||||||
|
if (minValue < m_minValue) {
|
||||||
|
m_minValue = minValue;
|
||||||
|
emit minValueChanged();
|
||||||
|
}
|
||||||
|
if (maxValue > m_maxValue) {
|
||||||
|
m_maxValue = maxValue;
|
||||||
|
emit maxValueChanged();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// End of fetched entries does not line up with start of existing entries. Discarding existing entries
|
||||||
|
qCDebug(dcEnergyLogs()) << "End of fetched entries does not line up with start of existing entries. Discarding existing entries" << entries.last()->timestamp().addSecs(m_sampleRate * 60).toString() << " - " << m_list.first()->timestamp().toString();
|
||||||
|
clear();
|
||||||
|
beginInsertRows(QModelIndex(), 0, entries.count());
|
||||||
|
m_list.append(entries);
|
||||||
|
endInsertRows();
|
||||||
|
emit entriesAdded(0, entries);
|
||||||
|
m_minValue = minValue;
|
||||||
|
emit minValueChanged();
|
||||||
|
m_maxValue = maxValue;
|
||||||
|
emit maxValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (entries.first()->timestamp().addSecs(-m_sampleRate * 60) == m_list.last()->timestamp()) {
|
||||||
|
int index = m_list.count();
|
||||||
|
beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + entries.count());
|
||||||
|
m_list.append(entries);
|
||||||
|
endInsertRows();
|
||||||
|
emit entriesAdded(index, entries);
|
||||||
|
if (minValue < m_minValue) {
|
||||||
|
m_minValue = minValue;
|
||||||
|
emit minValueChanged();
|
||||||
|
}
|
||||||
|
if (maxValue > m_maxValue) {
|
||||||
|
m_maxValue = maxValue;
|
||||||
|
emit maxValueChanged();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start of fetched entries does not line up with end of existing entries. Discarding existing entries
|
||||||
|
clear();
|
||||||
|
beginInsertRows(QModelIndex(), 0, entries.count());
|
||||||
|
m_list.append(entries);
|
||||||
|
endInsertRows();
|
||||||
|
emit entriesAdded(0, entries);
|
||||||
|
m_minValue = minValue;
|
||||||
|
emit minValueChanged();
|
||||||
|
m_maxValue = maxValue;
|
||||||
|
emit maxValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
qCDebug(dcEnergyLogs()) << "Received empty log entries set.";
|
||||||
}
|
}
|
||||||
qCDebug(dcEnergyLogs()) << "Energy logs received";
|
|
||||||
// qCDebug(dcEnergyLogs()) << "Energy logs:" << params;
|
|
||||||
logEntriesReceived(params);
|
|
||||||
|
|
||||||
m_fetchingData = false;
|
m_fetchingData = false;
|
||||||
emit fetchingDataChanged();
|
|
||||||
|
if (m_fetchAgain) {
|
||||||
|
qCDebug(dcEnergyLogs()) << "Fetching again...";
|
||||||
|
m_fetchAgain = false;
|
||||||
|
fetchLogs();
|
||||||
|
} else {
|
||||||
|
emit fetchingDataChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnergyLogs::notificationReceivedInternal(const QVariantMap &data)
|
void EnergyLogs::notificationReceivedInternal(const QVariantMap &data)
|
||||||
@ -234,32 +345,72 @@ void EnergyLogs::notificationReceivedInternal(const QVariantMap &data)
|
|||||||
notificationReceived(data);
|
notificationReceived(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EnergyLogs::clear()
|
||||||
|
{
|
||||||
|
int count = m_list.count();
|
||||||
|
beginResetModel();
|
||||||
|
qDeleteAll(m_list);
|
||||||
|
m_list.clear();
|
||||||
|
endResetModel();
|
||||||
|
emit countChanged();
|
||||||
|
emit entriesRemoved(0, count);
|
||||||
|
m_minValue = 0;
|
||||||
|
emit minValueChanged();
|
||||||
|
m_maxValue = 0;
|
||||||
|
emit maxValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
void EnergyLogs::fetchLogs()
|
void EnergyLogs::fetchLogs()
|
||||||
{
|
{
|
||||||
if (m_loadingInhibited || !m_ready || !m_engine || m_engine->jsonRpcClient()->experiences().value("Energy").toString() < "1.0") {
|
if (m_loadingInhibited || !m_ready || !m_engine || m_engine->jsonRpcClient()->experiences().value("Energy").toString() < "1.0") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_list.isEmpty()) {
|
if (m_fetchingData) {
|
||||||
beginResetModel();
|
qCDebug(dcEnergyLogs()) << "Already busy.. queing up call";
|
||||||
qDeleteAll(m_list);
|
m_fetchAgain = true;
|
||||||
m_list.clear();
|
return;
|
||||||
endResetModel();
|
}
|
||||||
|
|
||||||
|
QVariantMap params = fetchParams();
|
||||||
|
QMetaEnum metaEnum = QMetaEnum::fromType<SampleRate>();
|
||||||
|
params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate));
|
||||||
|
|
||||||
|
if (!m_startTime.isNull() && !m_endTime.isNull()) {
|
||||||
|
QDateTime startTime;
|
||||||
|
QDateTime endTime;
|
||||||
|
|
||||||
|
QDateTime oldestExisting = m_list.count() > 0 ? m_list.first()->timestamp() : QDateTime();
|
||||||
|
QDateTime newestExisting = m_list.count() > 0 ? m_list.last()->timestamp() : QDateTime();
|
||||||
|
qCDebug(dcEnergyLogs()) << "request timeframe: " << m_startTime.toString() << " - " << m_endTime.toString();
|
||||||
|
qCDebug(dcEnergyLogs()) << "existing timeframe:" << oldestExisting.toString() << "- " << newestExisting.toString();
|
||||||
|
|
||||||
|
if (oldestExisting.isNull() || newestExisting.isNull()) {
|
||||||
|
startTime = m_startTime;
|
||||||
|
endTime = m_endTime;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (m_startTime < oldestExisting) {
|
||||||
|
startTime = m_startTime;
|
||||||
|
endTime = qMin(m_endTime, oldestExisting.addSecs(-m_sampleRate * 60));
|
||||||
|
} else if (newestExisting < m_endTime) {
|
||||||
|
startTime = qMax(m_startTime, newestExisting.addSecs(m_sampleRate * 60));
|
||||||
|
endTime = m_endTime;
|
||||||
|
} else {
|
||||||
|
// Nothing to do...
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params.insert("from", startTime.toSecsSinceEpoch());
|
||||||
|
params.insert("to", endTime.toSecsSinceEpoch());
|
||||||
|
qCDebug(dcEnergyLogs()) << "Fetching from" << startTime.toString() << "to" << endTime.toString() << "with sample rate" << m_sampleRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_fetchingData = true;
|
m_fetchingData = true;
|
||||||
fetchingDataChanged();
|
fetchingDataChanged();
|
||||||
|
|
||||||
QVariantMap params = fetchParams();
|
qCDebug(dcEnergyLogs()) << "Fetching" << m_startTime << m_endTime;
|
||||||
QMetaEnum metaEnum = QMetaEnum::fromType<SampleRate>();
|
|
||||||
params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate));
|
|
||||||
if (!m_startTime.isNull()) {
|
|
||||||
params.insert("from", m_startTime.toSecsSinceEpoch());
|
|
||||||
}
|
|
||||||
if (!m_endTime.isNull()) {
|
|
||||||
params.insert("to", m_endTime.toSecsSinceEpoch());
|
|
||||||
}
|
|
||||||
qCDebug(dcEnergyLogs()) << this << "Fetching energy logs" << params;
|
|
||||||
m_engine->jsonRpcClient()->sendCommand("Energy.Get" + logsName(), params, this, "getLogsResponse");
|
m_engine->jsonRpcClient()->sendCommand("Energy.Get" + logsName(), params, this, "getLogsResponse");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,10 @@ class EnergyLogs : public QAbstractListModel, public QQmlParserStatus
|
|||||||
Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged)
|
Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged)
|
||||||
Q_PROPERTY(bool fetchingData READ fetchingData NOTIFY fetchingDataChanged)
|
Q_PROPERTY(bool fetchingData READ fetchingData NOTIFY fetchingDataChanged)
|
||||||
Q_PROPERTY(bool loadingInhibited READ loadingInhibited WRITE setLoadingInhibited NOTIFY loadingInhibitedChanged)
|
Q_PROPERTY(bool loadingInhibited READ loadingInhibited WRITE setLoadingInhibited NOTIFY loadingInhibitedChanged)
|
||||||
|
Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged)
|
||||||
|
Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged)
|
||||||
|
|
||||||
|
friend class ThingPowerLogs;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum SampleRate {
|
enum SampleRate {
|
||||||
@ -77,13 +81,19 @@ public:
|
|||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
|
double minValue() const;
|
||||||
|
double maxValue() const;
|
||||||
|
|
||||||
Q_INVOKABLE EnergyLogEntry* get(int index) const;
|
Q_INVOKABLE EnergyLogEntry* get(int index) const;
|
||||||
|
Q_INVOKABLE EnergyLogEntry* find(const QDateTime ×tamp);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void clear();
|
||||||
|
void fetchLogs();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void engineChanged();
|
void engineChanged();
|
||||||
void sampleRateChanged();
|
void sampleRateChanged();
|
||||||
void fetchPowerBalanceChanged();
|
|
||||||
void thingIdsChanged();
|
|
||||||
void startTimeChanged();
|
void startTimeChanged();
|
||||||
void endTimeChanged();
|
void endTimeChanged();
|
||||||
void liveChanged();
|
void liveChanged();
|
||||||
@ -91,34 +101,40 @@ signals:
|
|||||||
void loadingInhibitedChanged();
|
void loadingInhibitedChanged();
|
||||||
|
|
||||||
void countChanged();
|
void countChanged();
|
||||||
void entryAdded(EnergyLogEntry *entry);
|
void entryAdded(int index, EnergyLogEntry *entry);
|
||||||
void entriesAdded(const QList<EnergyLogEntry*> entries);
|
void entriesAdded(int index, const QList<EnergyLogEntry*> entries);
|
||||||
|
void entriesRemoved(int index, int count);
|
||||||
|
|
||||||
|
void minValueChanged();
|
||||||
|
void maxValueChanged();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual QString logsName() const = 0;
|
virtual QString logsName() const = 0;
|
||||||
virtual QVariantMap fetchParams() const;
|
virtual QVariantMap fetchParams() const;
|
||||||
virtual void logEntriesReceived(const QVariantMap ¶ms) = 0;
|
virtual QList<EnergyLogEntry*> unpackEntries(const QVariantMap ¶ms, double *minValue, double *maxValue) = 0;
|
||||||
virtual void notificationReceived(const QVariantMap &data) = 0;
|
virtual void notificationReceived(const QVariantMap &data) = 0;
|
||||||
|
|
||||||
void appendEntry(EnergyLogEntry *entry);
|
void appendEntry(EnergyLogEntry *entry, double minValue, double maxValue);
|
||||||
void appendEntries(const QList<EnergyLogEntry *> &entries);
|
void appendEntries(const QList<EnergyLogEntry *> &entries);
|
||||||
|
|
||||||
private slots:
|
protected slots:
|
||||||
void getLogsResponse(int commandId, const QVariantMap ¶ms);
|
void getLogsResponse(int commandId, const QVariantMap ¶ms);
|
||||||
void notificationReceivedInternal(const QVariantMap &data);
|
void notificationReceivedInternal(const QVariantMap &data);
|
||||||
|
|
||||||
void fetchLogs();
|
|
||||||
private:
|
private:
|
||||||
Engine *m_engine = nullptr;
|
Engine *m_engine = nullptr;
|
||||||
SampleRate m_sampleRate = SampleRate15Mins;
|
SampleRate m_sampleRate = SampleRate15Mins;
|
||||||
bool m_fetchPowerBalance = true;
|
bool m_fetchPowerBalance = true;
|
||||||
QList<QUuid> m_thingIds;
|
|
||||||
QDateTime m_startTime;
|
QDateTime m_startTime;
|
||||||
QDateTime m_endTime;
|
QDateTime m_endTime;
|
||||||
bool m_live = true;
|
bool m_live = true;
|
||||||
bool m_fetchingData = false;
|
bool m_fetchingData = false;
|
||||||
bool m_loadingInhibited = false;
|
bool m_loadingInhibited = false;
|
||||||
bool m_ready = false;
|
bool m_ready = false;
|
||||||
|
bool m_fetchAgain = false;
|
||||||
|
|
||||||
|
double m_minValue = 0;
|
||||||
|
double m_maxValue = 0;
|
||||||
|
|
||||||
QList<EnergyLogEntry*> m_list;
|
QList<EnergyLogEntry*> m_list;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -66,100 +66,14 @@ PowerBalanceLogs::PowerBalanceLogs(QObject *parent) : EnergyLogs(parent)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double PowerBalanceLogs::minValue() const
|
|
||||||
{
|
|
||||||
return m_minValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
double PowerBalanceLogs::maxValue() const
|
|
||||||
{
|
|
||||||
return m_maxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PowerBalanceLogs::logsName() const
|
QString PowerBalanceLogs::logsName() const
|
||||||
{
|
{
|
||||||
return "PowerBalanceLogs";
|
return "PowerBalanceLogs";
|
||||||
}
|
}
|
||||||
|
|
||||||
void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry)
|
QList<EnergyLogEntry *> PowerBalanceLogs::unpackEntries(const QVariantMap ¶ms, double *minValue, double *maxValue)
|
||||||
{
|
|
||||||
if (entry->consumption() < m_minValue) {
|
|
||||||
m_minValue = entry->consumption();
|
|
||||||
emit minValueChanged();
|
|
||||||
}
|
|
||||||
if (entry->consumption() > m_maxValue) {
|
|
||||||
m_maxValue = entry->consumption();
|
|
||||||
emit maxValueChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry->production() < m_minValue) {
|
|
||||||
m_minValue = entry->production();
|
|
||||||
emit minValueChanged();
|
|
||||||
}
|
|
||||||
if (entry->production() > m_maxValue) {
|
|
||||||
m_maxValue = entry->production();
|
|
||||||
emit maxValueChanged();
|
|
||||||
}
|
|
||||||
if (entry->acquisition() < m_minValue) {
|
|
||||||
m_minValue = entry->acquisition();
|
|
||||||
emit minValueChanged();
|
|
||||||
}
|
|
||||||
if (entry->acquisition() > m_maxValue) {
|
|
||||||
m_maxValue = entry->acquisition();
|
|
||||||
emit maxValueChanged();
|
|
||||||
}
|
|
||||||
if (entry->storage() < m_minValue) {
|
|
||||||
m_minValue = entry->storage();
|
|
||||||
emit minValueChanged();
|
|
||||||
}
|
|
||||||
if (entry->storage() > m_maxValue) {
|
|
||||||
m_maxValue = entry->storage();
|
|
||||||
emit maxValueChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
appendEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnergyLogEntry *PowerBalanceLogs::find(const QDateTime ×tamp) const
|
|
||||||
{
|
|
||||||
qWarning() << "Finding log entry for timestamp:" << timestamp;
|
|
||||||
int oldest = 0;
|
|
||||||
int newest = rowCount() - 1;
|
|
||||||
EnergyLogEntry *entry = nullptr;
|
|
||||||
int step = 0;
|
|
||||||
while (oldest <= newest && step < rowCount()) {
|
|
||||||
EnergyLogEntry *oldestEntry = get(oldest);
|
|
||||||
EnergyLogEntry *newestEntry = get(newest);
|
|
||||||
int middle = (newest - oldest) / 2 + oldest;
|
|
||||||
EnergyLogEntry *middleEntry = get(middle);
|
|
||||||
qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest);
|
|
||||||
if (timestamp <= oldestEntry->timestamp()) {
|
|
||||||
return oldestEntry;
|
|
||||||
}
|
|
||||||
if (timestamp >= newestEntry->timestamp()) {
|
|
||||||
return newestEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp == middleEntry->timestamp()) {
|
|
||||||
return middleEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamp < middleEntry->timestamp()) {
|
|
||||||
newest = middle;
|
|
||||||
} else {
|
|
||||||
oldest = middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((newest - oldest) <= 1) {
|
|
||||||
return newestEntry;
|
|
||||||
}
|
|
||||||
step++;
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PowerBalanceLogs::logEntriesReceived(const QVariantMap ¶ms)
|
|
||||||
{
|
{
|
||||||
|
QList<EnergyLogEntry*> ret;
|
||||||
foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) {
|
foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) {
|
||||||
QVariantMap map = variant.toMap();
|
QVariantMap map = variant.toMap();
|
||||||
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
||||||
@ -172,10 +86,13 @@ void PowerBalanceLogs::logEntriesReceived(const QVariantMap ¶ms)
|
|||||||
double totalAcquisition = map.value("totalAcquisition").toDouble();
|
double totalAcquisition = map.value("totalAcquisition").toDouble();
|
||||||
double totalReturn = map.value("totalReturn").toDouble();
|
double totalReturn = map.value("totalReturn").toDouble();
|
||||||
PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn, this);
|
PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn, this);
|
||||||
// qCritical() << "Adding entry:" << entry->timestamp() << entry->totalConsumption();
|
|
||||||
|
|
||||||
addEntry(entry);
|
*minValue = qMin(qMin(qMin(qMin(*minValue, consumption), production), acquisition), storage);
|
||||||
|
*maxValue = qMax(qMax(qMax(qMax(*maxValue, consumption), production), acquisition), storage);
|
||||||
|
|
||||||
|
ret.append(entry);
|
||||||
}
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PowerBalanceLogs::notificationReceived(const QVariantMap &data)
|
void PowerBalanceLogs::notificationReceived(const QVariantMap &data)
|
||||||
@ -202,7 +119,9 @@ void PowerBalanceLogs::notificationReceived(const QVariantMap &data)
|
|||||||
double totalAcquisition = map.value("totalAcquisition").toDouble();
|
double totalAcquisition = map.value("totalAcquisition").toDouble();
|
||||||
double totalReturn = map.value("totalReturn").toDouble();
|
double totalReturn = map.value("totalReturn").toDouble();
|
||||||
PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn, this);
|
PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn, this);
|
||||||
addEntry(entry);
|
double minValue = qMin(qMin(qMin(consumption, production), acquisition), storage);
|
||||||
|
double maxValue = qMax(qMax(qMax(consumption, production), acquisition), storage);
|
||||||
|
appendEntry(entry, minValue, maxValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,30 +46,13 @@ private:
|
|||||||
class PowerBalanceLogs : public EnergyLogs
|
class PowerBalanceLogs : public EnergyLogs
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged)
|
|
||||||
Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged)
|
|
||||||
public:
|
public:
|
||||||
explicit PowerBalanceLogs(QObject *parent = nullptr);
|
explicit PowerBalanceLogs(QObject *parent = nullptr);
|
||||||
|
|
||||||
double minValue() const;
|
|
||||||
double maxValue() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE EnergyLogEntry* find(const QDateTime ×tamp) const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void minValueChanged();
|
|
||||||
void maxValueChanged();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString logsName() const override;
|
QString logsName() const override;
|
||||||
void logEntriesReceived(const QVariantMap ¶ms) override;
|
QList<EnergyLogEntry*> unpackEntries(const QVariantMap ¶ms, double *minValue, double *maxValue) override;
|
||||||
void notificationReceived(const QVariantMap &data) override;
|
void notificationReceived(const QVariantMap &data) override;
|
||||||
|
|
||||||
private:
|
|
||||||
void addEntry(PowerBalanceLogEntry *entry);
|
|
||||||
|
|
||||||
double m_minValue = 0;
|
|
||||||
double m_maxValue = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
|
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(dcEnergyLogs)
|
||||||
|
|
||||||
ThingPowerLogEntry::ThingPowerLogEntry(QObject *parent):
|
ThingPowerLogEntry::ThingPowerLogEntry(QObject *parent):
|
||||||
EnergyLogEntry(parent)
|
EnergyLogEntry(parent)
|
||||||
{
|
{
|
||||||
@ -39,67 +42,46 @@ double ThingPowerLogEntry::totalProduction() const
|
|||||||
|
|
||||||
ThingPowerLogs::ThingPowerLogs(QObject *parent) : EnergyLogs(parent)
|
ThingPowerLogs::ThingPowerLogs(QObject *parent) : EnergyLogs(parent)
|
||||||
{
|
{
|
||||||
m_cacheTimer.setInterval(2000);
|
}
|
||||||
connect(&m_cacheTimer, &QTimer::timeout, this, [=](){
|
|
||||||
if (m_cachedEntries.count() > 0) {
|
QUuid ThingPowerLogs::thingId() const
|
||||||
addEntries(m_cachedEntries);
|
{
|
||||||
m_cachedEntries.clear();
|
return m_thingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogs::setThingId(const QUuid &thingId)
|
||||||
|
{
|
||||||
|
if (m_thingId != thingId) {
|
||||||
|
m_thingId = thingId;
|
||||||
|
emit thingIdChanged();
|
||||||
|
if (m_loader) {
|
||||||
|
m_loader->addThingId(thingId);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QUuid> ThingPowerLogs::thingIds() const
|
ThingPowerLogsLoader *ThingPowerLogs::loader() const
|
||||||
{
|
{
|
||||||
return m_thingIds;
|
return m_loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThingPowerLogs::setThingIds(const QList<QUuid> &thingIds)
|
void ThingPowerLogs::setLoader(ThingPowerLogsLoader *loader)
|
||||||
{
|
{
|
||||||
if (m_thingIds != thingIds) {
|
if (m_loader != loader) {
|
||||||
m_thingIds = thingIds;
|
m_loader = loader;
|
||||||
emit thingIdsChanged();
|
emit loaderChanged();
|
||||||
|
|
||||||
|
loader->addThingId(m_thingId);
|
||||||
|
connect(loader, &ThingPowerLogsLoader::fetched, this, [=](int commandId, const QVariantMap ¶ms){
|
||||||
|
qCDebug(dcEnergyLogs()) << "Loader fetched data.";
|
||||||
|
getLogsResponse(commandId, params);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double ThingPowerLogs::minValue() const
|
ThingPowerLogEntry *ThingPowerLogs::liveEntry()
|
||||||
{
|
{
|
||||||
return m_minValue;
|
return m_liveEntry;
|
||||||
}
|
|
||||||
|
|
||||||
double ThingPowerLogs::maxValue() const
|
|
||||||
{
|
|
||||||
return m_maxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ThingPowerLogEntry *ThingPowerLogs::find(const QUuid &thingId, const QDateTime ×tamp)
|
|
||||||
{
|
|
||||||
// TODO: Can we do a binary search even if they key we're looking for is not unique (but still sorted)?
|
|
||||||
// For now, 365 * consumers items is the max we'll have here which seems on the edge for doing a stupid linear search...
|
|
||||||
// qWarning() << "Finding item for" << thingId.toString() << timestamp.toString();
|
|
||||||
for (int i = rowCount() - 1; i >= 0; i--) {
|
|
||||||
ThingPowerLogEntry *entry = static_cast<ThingPowerLogEntry*>(get(i));
|
|
||||||
if (entry->thingId() != thingId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// qWarning() << "comparing" << entry->timestamp().toString();
|
|
||||||
if (timestamp == entry->timestamp()) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
if (timestamp > entry->timestamp()) {
|
|
||||||
return nullptr; // Giving up, entry is not here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
ThingPowerLogEntry *ThingPowerLogs::liveEntry(const QUuid &thingId)
|
|
||||||
{
|
|
||||||
return m_liveEntries.value(thingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ThingPowerLogs::addEntry(ThingPowerLogEntry *entry)
|
|
||||||
{
|
|
||||||
appendEntry(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThingPowerLogs::addEntries(const QList<ThingPowerLogEntry *> &entries)
|
void ThingPowerLogs::addEntries(const QList<ThingPowerLogEntry *> &entries)
|
||||||
@ -128,32 +110,33 @@ QString ThingPowerLogs::logsName() const
|
|||||||
|
|
||||||
QVariantMap ThingPowerLogs::fetchParams() const
|
QVariantMap ThingPowerLogs::fetchParams() const
|
||||||
{
|
{
|
||||||
QVariantList thingIdsStrings;
|
|
||||||
foreach (const QUuid &id, m_thingIds) {
|
|
||||||
thingIdsStrings.append(id.toString());
|
|
||||||
}
|
|
||||||
QVariantMap ret;
|
QVariantMap ret;
|
||||||
ret.insert("thingIds", thingIdsStrings);
|
ret.insert("thingIds", QVariantList{m_thingId});
|
||||||
ret.insert("includeCurrent", true);
|
ret.insert("includeCurrent", true);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThingPowerLogs::logEntriesReceived(const QVariantMap ¶ms)
|
QList<EnergyLogEntry *> ThingPowerLogs::unpackEntries(const QVariantMap ¶ms, double *minValue, double *maxValue)
|
||||||
{
|
{
|
||||||
foreach (const QVariant &variant, params.value("currentEntries").toList()) {
|
foreach (const QVariant &variant, params.value("currentEntries").toList()) {
|
||||||
QVariantMap map = variant.toMap();
|
QVariantMap map = variant.toMap();
|
||||||
ThingPowerLogEntry *entry = unpack(map);
|
if (map.value("thingId").toUuid() != m_thingId) {
|
||||||
if (m_liveEntries.contains(entry->thingId())) {
|
continue;
|
||||||
m_liveEntries[entry->thingId()]->deleteLater();
|
|
||||||
}
|
}
|
||||||
m_liveEntries[entry->thingId()] = entry;
|
if (m_liveEntry) {
|
||||||
emit liveEntryChanged(entry);
|
m_liveEntry->deleteLater();
|
||||||
|
}
|
||||||
|
m_liveEntry = unpack(map);
|
||||||
|
emit liveEntryChanged(m_liveEntry);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grouping them so when the UI gets entriesAdded, the whole set for this timstamp will be available at once
|
QList<EnergyLogEntry*> ret;
|
||||||
QList<ThingPowerLogEntry*> groupForTimestamp;
|
|
||||||
foreach (const QVariant &variant, params.value("thingPowerLogEntries").toList()) {
|
foreach (const QVariant &variant, params.value("thingPowerLogEntries").toList()) {
|
||||||
QVariantMap map = variant.toMap();
|
QVariantMap map = variant.toMap();
|
||||||
|
if (map.value("thingId").toUuid() != m_thingId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
||||||
QUuid thingId = map.value("thingId").toUuid();
|
QUuid thingId = map.value("thingId").toUuid();
|
||||||
double currentPower = map.value("currentPower").toDouble();
|
double currentPower = map.value("currentPower").toDouble();
|
||||||
@ -162,21 +145,13 @@ void ThingPowerLogs::logEntriesReceived(const QVariantMap ¶ms)
|
|||||||
ThingPowerLogEntry *entry = new ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction, this);
|
ThingPowerLogEntry *entry = new ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction, this);
|
||||||
// qWarning() << "Adding entry:" << entry->thingId() << entry->timestamp().toString() << entry->totalConsumption();
|
// qWarning() << "Adding entry:" << entry->thingId() << entry->timestamp().toString() << entry->totalConsumption();
|
||||||
|
|
||||||
if (groupForTimestamp.isEmpty()) {
|
*minValue = qMin(*minValue, currentPower);
|
||||||
groupForTimestamp.append(entry);
|
*maxValue = qMax(*maxValue, currentPower);
|
||||||
} else if (groupForTimestamp.first()->timestamp() == timestamp) {
|
|
||||||
groupForTimestamp.append(entry);
|
ret.append(entry);
|
||||||
} else {
|
|
||||||
// Finalize previous group and start a new one
|
|
||||||
addEntries(groupForTimestamp);
|
|
||||||
groupForTimestamp.clear();
|
|
||||||
groupForTimestamp.append(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupForTimestamp.isEmpty()) {
|
return ret;
|
||||||
addEntries(groupForTimestamp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThingPowerLogs::notificationReceived(const QVariantMap &data)
|
void ThingPowerLogs::notificationReceived(const QVariantMap &data)
|
||||||
@ -189,52 +164,193 @@ void ThingPowerLogs::notificationReceived(const QVariantMap &data)
|
|||||||
QVariantMap entryMap = params.value("thingPowerLogEntry").toMap();
|
QVariantMap entryMap = params.value("thingPowerLogEntry").toMap();
|
||||||
QUuid thingId = entryMap.value("thingId").toUuid();
|
QUuid thingId = entryMap.value("thingId").toUuid();
|
||||||
|
|
||||||
if (!m_thingIds.isEmpty() && !m_thingIds.contains(thingId)) {
|
if (m_thingId != thingId) {
|
||||||
// Not watching this thing...
|
// Not watching this thing...
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sampleRate != this->sampleRate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We'll use 1 Min samples in any case for the live value
|
// We'll use 1 Min samples in any case for the live value
|
||||||
if (sampleRate == EnergyLogs::SampleRate1Min) {
|
if (sampleRate == EnergyLogs::SampleRate1Min) {
|
||||||
ThingPowerLogEntry *liveEntry = unpack(params.value("thingPowerLogEntry").toMap());
|
ThingPowerLogEntry *liveEntry = unpack(params.value("thingPowerLogEntry").toMap());
|
||||||
if (m_liveEntries.contains(thingId)) {
|
if (m_liveEntry) {
|
||||||
m_liveEntries.value(thingId)->deleteLater();
|
m_liveEntry->deleteLater();
|
||||||
}
|
}
|
||||||
m_liveEntries[thingId] = liveEntry;
|
m_liveEntry = liveEntry;
|
||||||
emit liveEntryChanged(liveEntry);
|
emit liveEntryChanged(liveEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// And append the sample rate we're interested in
|
|
||||||
if (sampleRate != this->sampleRate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification == "Energy.ThingPowerLogEntryAdded") {
|
if (notification == "Energy.ThingPowerLogEntryAdded") {
|
||||||
QVariantMap map = params.value("thingPowerLogEntry").toMap();
|
QVariantMap map = params.value("thingPowerLogEntry").toMap();
|
||||||
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
||||||
QUuid thingId = map.value("thingId").toUuid();
|
QUuid thingId = map.value("thingId").toUuid();
|
||||||
if (!m_thingIds.isEmpty() && !m_thingIds.contains(thingId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double currentPower = map.value("currentPower").toDouble();
|
double currentPower = map.value("currentPower").toDouble();
|
||||||
double totalConsumption = map.value("totalConsumption").toDouble();
|
double totalConsumption = map.value("totalConsumption").toDouble();
|
||||||
double totalProduction = map.value("totalProduction").toDouble();
|
double totalProduction = map.value("totalProduction").toDouble();
|
||||||
ThingPowerLogEntry *entry = new ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction, this);
|
ThingPowerLogEntry *entry = new ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction, this);
|
||||||
|
appendEntry(entry, currentPower, currentPower);
|
||||||
// In order to be easier on resources, we'll batch notifications by grouping them by timestamp
|
|
||||||
// While the timestamp is the same, just cache the changes. Once the timestamp changes, we'll finalize the
|
|
||||||
// batch and actually append them.
|
|
||||||
// Also if we're not getting any more notification for a while and still have cached entries, we'll process the batch
|
|
||||||
if (m_cachedEntries.isEmpty()) {
|
|
||||||
m_cachedEntries.append(entry);
|
|
||||||
} else if (entry->timestamp() == m_cachedEntries.first()->timestamp()) {
|
|
||||||
m_cachedEntries.append(entry);
|
|
||||||
} else {
|
|
||||||
addEntries(m_cachedEntries);
|
|
||||||
m_cachedEntries.clear();
|
|
||||||
m_cachedEntries.append(entry);
|
|
||||||
}
|
|
||||||
m_cacheTimer.start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThingPowerLogsLoader::ThingPowerLogsLoader(QObject *parent):
|
||||||
|
QObject(parent)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine *ThingPowerLogsLoader::engine() const
|
||||||
|
{
|
||||||
|
return m_engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogsLoader::setEngine(Engine *engine)
|
||||||
|
{
|
||||||
|
if (m_engine != engine) {
|
||||||
|
m_engine = engine;
|
||||||
|
emit engineChanged();
|
||||||
|
|
||||||
|
if (!m_engine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(engine, &Engine::destroyed, this, [=](){
|
||||||
|
if (engine == m_engine) {
|
||||||
|
m_engine = nullptr;
|
||||||
|
emit engineChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EnergyLogs::SampleRate ThingPowerLogsLoader::sampleRate() const
|
||||||
|
{
|
||||||
|
return m_sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogsLoader::setSampleRate(EnergyLogs::SampleRate sampleRate)
|
||||||
|
{
|
||||||
|
if (m_sampleRate != sampleRate) {
|
||||||
|
m_sampleRate = sampleRate;
|
||||||
|
emit sampleRateChanged();
|
||||||
|
|
||||||
|
m_lastStartTime = QDateTime();
|
||||||
|
m_lastEndTime = QDateTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime ThingPowerLogsLoader::startTime() const
|
||||||
|
{
|
||||||
|
return m_startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogsLoader::setStartTime(const QDateTime &startTime)
|
||||||
|
{
|
||||||
|
if (m_startTime != startTime) {
|
||||||
|
m_startTime = startTime;
|
||||||
|
emit startTimeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime ThingPowerLogsLoader::endTime() const
|
||||||
|
{
|
||||||
|
return m_endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogsLoader::setEndTime(const QDateTime &endTime)
|
||||||
|
{
|
||||||
|
if (m_endTime != endTime) {
|
||||||
|
m_endTime = endTime;
|
||||||
|
emit endTimeChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ThingPowerLogsLoader::fetchingData() const
|
||||||
|
{
|
||||||
|
return m_fetchingData;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogsLoader::addThingId(const QUuid &thingId)
|
||||||
|
{
|
||||||
|
if (!m_thingIds.contains(thingId)) {
|
||||||
|
m_thingIds.append(thingId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogsLoader::fetchLogs()
|
||||||
|
{
|
||||||
|
if (!m_engine || m_engine->jsonRpcClient()->experiences().value("Energy").toString() < "1.0") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_fetchingData) {
|
||||||
|
qCDebug(dcEnergyLogs()) << "Already busy.. queing up call";
|
||||||
|
m_fetchAgain = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap params;
|
||||||
|
QVariantList thingIds;
|
||||||
|
foreach (const QUuid &thingId, m_thingIds) {
|
||||||
|
thingIds.append(thingId);
|
||||||
|
}
|
||||||
|
params.insert("thingIds", thingIds);
|
||||||
|
params.insert("includeCurrent", true);
|
||||||
|
|
||||||
|
QMetaEnum metaEnum = QMetaEnum::fromType<EnergyLogs::SampleRate>();
|
||||||
|
params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate));
|
||||||
|
|
||||||
|
if (!m_startTime.isNull() && !m_endTime.isNull()) {
|
||||||
|
QDateTime startTime;
|
||||||
|
QDateTime endTime;
|
||||||
|
if (m_lastStartTime.isNull() || m_lastEndTime.isNull()) {
|
||||||
|
startTime = m_startTime;
|
||||||
|
endTime = m_endTime;
|
||||||
|
m_lastStartTime = m_startTime;
|
||||||
|
m_lastEndTime = m_endTime;
|
||||||
|
} else {
|
||||||
|
if (m_startTime < m_lastStartTime) {
|
||||||
|
startTime = m_startTime;
|
||||||
|
endTime = m_lastStartTime;
|
||||||
|
m_lastStartTime = m_startTime;
|
||||||
|
} else if (m_lastEndTime < m_endTime) {
|
||||||
|
startTime = m_lastEndTime;
|
||||||
|
endTime = m_endTime;
|
||||||
|
m_lastEndTime = m_endTime;
|
||||||
|
} else {
|
||||||
|
// Nothing to do...
|
||||||
|
m_fetchingData = false;
|
||||||
|
emit fetchingDataChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
params.insert("from", startTime.toSecsSinceEpoch());
|
||||||
|
params.insert("to", endTime.addSecs(-1).toSecsSinceEpoch());
|
||||||
|
qCDebug(dcEnergyLogs()) << "Fetching from" << startTime.toString() << "to" << endTime.toString() << "with sample rate" << m_sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_fetchingData = true;
|
||||||
|
fetchingDataChanged();
|
||||||
|
|
||||||
|
m_engine->jsonRpcClient()->sendCommand("Energy.GetThingPowerLogs", params, this, "getLogsResponse");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThingPowerLogsLoader::getLogsResponse(int commandId, const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
qCDebug(dcEnergyLogs()) << "Logs loader response!";
|
||||||
|
emit fetched(commandId, params);
|
||||||
|
|
||||||
|
m_fetchingData = false;
|
||||||
|
|
||||||
|
if (m_fetchAgain) {
|
||||||
|
m_fetchAgain = false;
|
||||||
|
fetchLogs();
|
||||||
|
} else {
|
||||||
|
emit fetchingDataChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -29,53 +29,98 @@ private:
|
|||||||
double m_totalProduction = 0;
|
double m_totalProduction = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ThingPowerLogsLoader;
|
||||||
|
|
||||||
class ThingPowerLogs : public EnergyLogs
|
class ThingPowerLogs : public EnergyLogs
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QList<QUuid> thingIds READ thingIds WRITE setThingIds NOTIFY thingIdsChanged)
|
Q_PROPERTY(QUuid thingId READ thingId WRITE setThingId NOTIFY thingIdChanged)
|
||||||
Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged)
|
Q_PROPERTY(ThingPowerLogsLoader* loader READ loader WRITE setLoader NOTIFY loaderChanged)
|
||||||
Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged)
|
|
||||||
public:
|
public:
|
||||||
explicit ThingPowerLogs(QObject *parent = nullptr);
|
explicit ThingPowerLogs(QObject *parent = nullptr);
|
||||||
|
|
||||||
QList<QUuid> thingIds() const;
|
QUuid thingId() const;
|
||||||
void setThingIds(const QList<QUuid> &thingIds);
|
void setThingId(const QUuid &thingId);
|
||||||
|
|
||||||
double minValue() const;
|
ThingPowerLogsLoader *loader() const;
|
||||||
double maxValue() const;
|
void setLoader(ThingPowerLogsLoader *loader);
|
||||||
|
|
||||||
Q_INVOKABLE ThingPowerLogEntry *find(const QUuid &thingId, const QDateTime ×tamp);
|
Q_INVOKABLE ThingPowerLogEntry *liveEntry();
|
||||||
|
|
||||||
Q_INVOKABLE ThingPowerLogEntry *liveEntry(const QUuid &thingId);
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void thingIdsChanged();
|
void thingIdChanged();
|
||||||
|
void loaderChanged();
|
||||||
void minValueChanged();
|
void liveEntryChanged(ThingPowerLogEntry *liveEntry);
|
||||||
void maxValueChanged();
|
|
||||||
|
|
||||||
void liveEntryChanged(ThingPowerLogEntry *entry);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString logsName() const override;
|
QString logsName() const override;
|
||||||
QVariantMap fetchParams() const override;
|
QVariantMap fetchParams() const override;
|
||||||
void logEntriesReceived(const QVariantMap ¶ms) override;
|
QList<EnergyLogEntry*> unpackEntries(const QVariantMap ¶ms, double *minValue, double *maxValue) override;
|
||||||
void notificationReceived(const QVariantMap &data) override;
|
void notificationReceived(const QVariantMap &data) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void addEntry(ThingPowerLogEntry *entry);
|
|
||||||
void addEntries(const QList<ThingPowerLogEntry *> &entries);
|
void addEntries(const QList<ThingPowerLogEntry *> &entries);
|
||||||
|
|
||||||
ThingPowerLogEntry *unpack(const QVariantMap &map);
|
ThingPowerLogEntry *unpack(const QVariantMap &map);
|
||||||
|
|
||||||
|
QUuid m_thingId;
|
||||||
|
ThingPowerLogEntry* m_liveEntry = nullptr;
|
||||||
|
ThingPowerLogsLoader* m_loader = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThingPowerLogsLoader: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged)
|
||||||
|
Q_PROPERTY(EnergyLogs::SampleRate sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
|
||||||
|
Q_PROPERTY(QDateTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
|
||||||
|
Q_PROPERTY(QDateTime endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged)
|
||||||
|
Q_PROPERTY(bool fetchingData READ fetchingData NOTIFY fetchingDataChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
ThingPowerLogsLoader(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
Engine *engine() const;
|
||||||
|
void setEngine(Engine *engine);
|
||||||
|
|
||||||
|
EnergyLogs::SampleRate sampleRate() const;
|
||||||
|
void setSampleRate(EnergyLogs::SampleRate sampleRate);
|
||||||
|
|
||||||
|
QDateTime startTime() const;
|
||||||
|
void setStartTime(const QDateTime &startTime);
|
||||||
|
|
||||||
|
QDateTime endTime() const;
|
||||||
|
void setEndTime(const QDateTime &endTime);
|
||||||
|
|
||||||
|
bool fetchingData() const;
|
||||||
|
|
||||||
|
void addThingId(const QUuid &thingId);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void fetchLogs();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void engineChanged();
|
||||||
|
void sampleRateChanged();
|
||||||
|
void startTimeChanged();
|
||||||
|
void endTimeChanged();
|
||||||
|
void fetchingDataChanged();
|
||||||
|
void fetched(int commandId, const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void getLogsResponse(int commandId, const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Engine *m_engine = nullptr;
|
||||||
|
EnergyLogs::SampleRate m_sampleRate = EnergyLogs::SampleRate15Mins;
|
||||||
|
QDateTime m_startTime;
|
||||||
|
QDateTime m_endTime;
|
||||||
QList<QUuid> m_thingIds;
|
QList<QUuid> m_thingIds;
|
||||||
double m_minValue = 0;
|
bool m_fetchingData = false;
|
||||||
double m_maxValue = 0;
|
bool m_fetchAgain = false;
|
||||||
|
QDateTime m_lastStartTime;
|
||||||
|
QDateTime m_lastEndTime;
|
||||||
|
|
||||||
QList<ThingPowerLogEntry*> m_cachedEntries;
|
|
||||||
QTimer m_cacheTimer;
|
|
||||||
|
|
||||||
QHash<QUuid, ThingPowerLogEntry*> m_liveEntries;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // THINGPOWERLOGS_H
|
#endif // THINGPOWERLOGS_H
|
||||||
|
|||||||
@ -383,6 +383,7 @@ void registerQmlTypes() {
|
|||||||
qmlRegisterType<PowerBalanceLogEntry>(uri, 1, 0, "PowerBalanceLogEntry");
|
qmlRegisterType<PowerBalanceLogEntry>(uri, 1, 0, "PowerBalanceLogEntry");
|
||||||
qmlRegisterType<ThingPowerLogEntry>(uri, 1, 0, "ThingPowerLogEntry");
|
qmlRegisterType<ThingPowerLogEntry>(uri, 1, 0, "ThingPowerLogEntry");
|
||||||
qmlRegisterType<ThingPowerLogs>(uri, 1, 0, "ThingPowerLogs");
|
qmlRegisterType<ThingPowerLogs>(uri, 1, 0, "ThingPowerLogs");
|
||||||
|
qmlRegisterType<ThingPowerLogsLoader>(uri, 1, 0, "ThingPowerLogsLoader");
|
||||||
|
|
||||||
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
|
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -280,5 +280,6 @@
|
|||||||
<file>ui/system/zwave/ZWaveAddNetworkPage.qml</file>
|
<file>ui/system/zwave/ZWaveAddNetworkPage.qml</file>
|
||||||
<file>ui/system/zwave/ZWaveNetworkPage.qml</file>
|
<file>ui/system/zwave/ZWaveNetworkPage.qml</file>
|
||||||
<file>ui/system/zwave/ZWaveNetworkSettingsPage.qml</file>
|
<file>ui/system/zwave/ZWaveNetworkSettingsPage.qml</file>
|
||||||
|
<file>ui/components/ActivityIndicator.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
52
nymea-app/ui/components/ActivityIndicator.qml
Normal file
52
nymea-app/ui/components/ActivityIndicator.qml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import Nymea 1.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
property color color: Style.iconColor
|
||||||
|
implicitWidth: Style.iconSize
|
||||||
|
implicitHeight: Style.iconSize
|
||||||
|
|
||||||
|
property int dotSize: width / 6
|
||||||
|
|
||||||
|
property bool running: true
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: grid
|
||||||
|
columns: 3
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: (width - columns * root.dotSize) / (columns - 1)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: dotRepeater
|
||||||
|
model: Math.pow(grid.columns, 2)
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: dot
|
||||||
|
width: root.dotSize
|
||||||
|
height: width
|
||||||
|
color: root.color
|
||||||
|
property int duration: 400
|
||||||
|
property int row: Math.floor(index / grid.columns)
|
||||||
|
property int pause: row * 200
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
running: root.running && root.visible
|
||||||
|
loops: Animation.Infinite
|
||||||
|
PauseAnimation { duration: dot.pause }
|
||||||
|
NumberAnimation {
|
||||||
|
target: dot
|
||||||
|
property: "opacity"
|
||||||
|
from: 0.2; to: 1;
|
||||||
|
duration: dot.duration
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: dot
|
||||||
|
property: "opacity"
|
||||||
|
from: 1; to: 0.2;
|
||||||
|
duration: dot.duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,8 @@ Rectangle {
|
|||||||
property alias model: repeater.model
|
property alias model: repeater.model
|
||||||
readonly property var currentValue: model.hasOwnProperty("get") ? model.get(currentIndex) : model[currentIndex]
|
readonly property var currentValue: model.hasOwnProperty("get") ? model.get(currentIndex) : model[currentIndex]
|
||||||
|
|
||||||
|
signal tabSelected(int index)
|
||||||
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: repeater.count > 0 ? repeater.itemAt(root.currentIndex).x + 1 : 0
|
x: repeater.count > 0 ? repeater.itemAt(root.currentIndex).x + 1 : 0
|
||||||
@ -45,6 +47,7 @@ Rectangle {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
print("current index:", index)
|
print("current index:", index)
|
||||||
root.currentIndex = index
|
root.currentIndex = index
|
||||||
|
root.tabSelected(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,14 +150,6 @@ MainViewBase {
|
|||||||
animationsEnabled: Qt.application.active && root.isCurrentItem
|
animationsEnabled: Qt.application.active && root.isCurrentItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConsumersBarChart {
|
|
||||||
// Layout.fillWidth: true
|
|
||||||
// Layout.preferredHeight: width
|
|
||||||
// energyManager: energyManager
|
|
||||||
// visible: consumers.count > 0
|
|
||||||
// colors: root.thingColors
|
|
||||||
// consumers: consumers
|
|
||||||
// }
|
|
||||||
ConsumersHistory {
|
ConsumersHistory {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: width
|
Layout.preferredHeight: width
|
||||||
|
|||||||
@ -13,229 +13,156 @@ StatsBase {
|
|||||||
|
|
||||||
property ThingsProxy consumers: null
|
property ThingsProxy consumers: null
|
||||||
|
|
||||||
Connections {
|
QtObject {
|
||||||
target: consumers
|
id: d
|
||||||
onCountChanged: root.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
property var config: root.configs[selectionTabs.currentValue.config]
|
||||||
target: engine.thingManager
|
|
||||||
onFetchingDataChanged: root.update()
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: engine.tagsManager
|
|
||||||
onBusyChanged: root.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
property int startOffset: 0
|
||||||
if (engine.thingManager.fetchingData || engine.tagsManager.busy || selectionTabs.currentValue === undefined) {
|
|
||||||
return
|
property date startTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset)
|
||||||
|
property date endTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset + config.count)
|
||||||
|
|
||||||
|
property bool fetchPending: false
|
||||||
|
property bool loading: d.fetchPending || wheelStopTimer.running || logsLoader.fetchingData
|
||||||
|
|
||||||
|
onConfigChanged: {
|
||||||
|
for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
consumersRepeater.itemAt(i).refreshLabels()
|
||||||
|
}
|
||||||
|
|
||||||
|
valueAxis.max = 1
|
||||||
}
|
}
|
||||||
powerLogs.loadingInhibited = true
|
|
||||||
|
|
||||||
var thingIds = []
|
onLoadingChanged: {
|
||||||
for (var i = 0; i < consumers.count; i++) {
|
if (!loading) {
|
||||||
thingIds.push(consumers.get(i).id)
|
refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
powerLogs.thingIds = thingIds
|
|
||||||
|
|
||||||
var config = root.configs[selectionTabs.currentValue.config]
|
function refresh() {
|
||||||
// print("config:", config.startTime(), config.sampleList(), config.sampleListNames())
|
for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
consumersRepeater.itemAt(i).refresh()
|
||||||
powerLogs.sampleRate = config.sampleRate
|
}
|
||||||
powerLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
|
}
|
||||||
|
|
||||||
chartView.reset();
|
|
||||||
|
|
||||||
powerLogs.loadingInhibited = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ThingPowerLogs {
|
ThingPowerLogsLoader {
|
||||||
id: powerLogs
|
id: logsLoader
|
||||||
engine: _engine
|
engine: _engine
|
||||||
loadingInhibited: true
|
startTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, -d.config.count)
|
||||||
|
endTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, d.config.count)
|
||||||
property var sampleList: null
|
sampleRate: d.config.sampleRate
|
||||||
|
|
||||||
onFetchingDataChanged: {
|
onFetchingDataChanged: {
|
||||||
if (!fetchingData) {
|
if (!fetchingData) {
|
||||||
var config = root.configs[selectionTabs.currentValue.config]
|
print("Logs fetched")
|
||||||
|
d.fetchPending = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chartView.reset()
|
Repeater {
|
||||||
|
id: consumersRepeater
|
||||||
|
model: root.consumers
|
||||||
|
onCountChanged: {
|
||||||
|
if (count == root.consumers.count) {
|
||||||
|
logsLoader.fetchLogs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// First grouping log entries by timestamp
|
delegate: Item {
|
||||||
var groupedEntries = []
|
id: consumerDelegate
|
||||||
var groupedEntry = {}
|
readonly property Thing thing: root.consumers.get(index)
|
||||||
for (var i = powerLogs.count - 1; i >= 0; i--) {
|
property BarSet barSet: null
|
||||||
var entry = powerLogs.get(i);
|
|
||||||
// print("grouping entry:", entry.timestamp, "current group entry", groupedEntry.timestamp, groupedEntry.hasOwnProperty("timestamp"))
|
|
||||||
if (!groupedEntry.hasOwnProperty("timestamp")) {
|
Connections {
|
||||||
groupedEntry.timestamp = entry.timestamp;
|
target: d
|
||||||
// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp)
|
onStartOffsetChanged: refresh()
|
||||||
}
|
}
|
||||||
if (groupedEntry.timestamp.getTime() !== entry.timestamp.getTime()) {
|
|
||||||
if (groupedEntries.length > config.count) {
|
function refreshLabels() {
|
||||||
break;
|
var values = []
|
||||||
}
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
// print("finalizing grouped entry", groupedEntry.timestamp)
|
values.push(0)
|
||||||
groupedEntries.unshift(groupedEntry);
|
|
||||||
groupedEntry = {
|
|
||||||
timestamp: entry.timestamp
|
|
||||||
}
|
|
||||||
// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp)
|
|
||||||
}
|
|
||||||
groupedEntry[entry.thingId] = entry.totalConsumption
|
|
||||||
}
|
|
||||||
if (groupedEntry.hasOwnProperty("timestamp") && groupedEntries.length <= config.count) {
|
|
||||||
// print("finalizing grouped entry", groupedEntry.timestamp)
|
|
||||||
groupedEntries.unshift(groupedEntry)
|
|
||||||
}
|
}
|
||||||
|
barSet.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
var labels = []
|
function refresh() {
|
||||||
var entries = []
|
var upcomingTimestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.config.count)
|
||||||
|
// print("refreshing", consumerDelegate.thing.name ,"config start", d.config.startTime(), "upcoming:", upcomingTimestamp, "fetchPending", d.fetchPending, d.loading)
|
||||||
var newestLogTimestamp = powerLogs.count > 0 ? powerLogs.get(powerLogs.count - 1).timestamp : new Date();
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i + 1)
|
||||||
for (var i = 0; i < config.count; i++) {
|
var previousTimestamp = root.calculateTimestamp(timestamp, d.config.sampleRate, -1)
|
||||||
var groupedEntry = groupedEntries[groupedEntries.length - i - 1]
|
// print("timestamp:", timestamp, "previous:", previousTimestamp)
|
||||||
// print("have grouped entry:", groupedEntry ? groupedEntry.timestamp : "null")
|
var entry = thingPowerLogs.find(timestamp)
|
||||||
|
var previousEntry = thingPowerLogs.find(previousTimestamp);
|
||||||
// if it's the first, let's add a generated entry which shows the total from the newest log to the current live value
|
if (entry && (previousEntry || !d.loading)) {
|
||||||
if (i == 0) {
|
// print("found entry:", entry.timestamp, previousEntry)
|
||||||
var liveEntry = {}
|
var consumption = entry.totalConsumption
|
||||||
for (var j = 0; j < consumers.count; j++) {
|
if (previousEntry) {
|
||||||
var consumer = consumers.get(j)
|
consumption -= previousEntry.totalConsumption
|
||||||
var liveLogEntry = powerLogs.liveEntry(consumer.id)
|
|
||||||
// print("Got consumer:", consumer.id, consumer.name, liveLogEntry ? liveLogEntry.timestamp : "-")
|
|
||||||
var value = liveLogEntry ? liveLogEntry.totalConsumption : 0;
|
|
||||||
if (groupedEntry) {
|
|
||||||
value -= groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0
|
|
||||||
}
|
|
||||||
liveEntry[consumer.id] = value
|
|
||||||
valueAxis.adjustMax(value)
|
|
||||||
}
|
}
|
||||||
|
barSet.replace(i, consumption)
|
||||||
|
valueAxis.adjustMax(consumption)
|
||||||
|
|
||||||
// print("Adding live entry", JSON.stringify(liveEntry))
|
} else if (timestamp.getTime() == upcomingTimestamp.getTime() && (previousEntry || !d.loading)) {
|
||||||
entries.unshift(liveEntry)
|
var consumption = thingPowerLogs.liveEntry().totalConsumption
|
||||||
}
|
// print("it's today for thing", thing.name, consumption, previousEntry)
|
||||||
|
if (previousEntry) {
|
||||||
// Add the actual entry
|
// print("previous timestamp", previousEntry.timestamp, previousEntry.totalConsumption)
|
||||||
var graphEntry = {}
|
consumption -= previousEntry.totalConsumption
|
||||||
var labelTime = new Date();
|
|
||||||
|
|
||||||
if (groupedEntry) {
|
|
||||||
var previousGroupedEntry = groupedEntries[groupedEntries.length - i - 2]
|
|
||||||
for (var j = 0; j < consumers.count; j++) {
|
|
||||||
var consumer = consumers.get(j)
|
|
||||||
var value = groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0
|
|
||||||
if (previousGroupedEntry) {
|
|
||||||
var previousValue = previousGroupedEntry.hasOwnProperty(consumer.id) ? previousGroupedEntry[consumer.id] : 0
|
|
||||||
value -= previousValue
|
|
||||||
}
|
|
||||||
graphEntry[consumer.id] = value
|
|
||||||
valueAxis.adjustMax(value)
|
|
||||||
}
|
}
|
||||||
labelTime = groupedEntry.timestamp
|
barSet.replace(i, consumption)
|
||||||
|
valueAxis.adjustMax(consumption)
|
||||||
} else {
|
} else {
|
||||||
for (var j = 0; j < consumers.count; j++) {
|
barSet.replace(i, 0)
|
||||||
var consumer = consumers.get(j)
|
|
||||||
graphEntry[consumer.id] = 0
|
|
||||||
}
|
|
||||||
labelTime = calculateSampleStart(newestLogTimestamp, config.sampleRate, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print("Adding entry:", labelTime, config.toLabel(labelTime), JSON.stringify(graphEntry))
|
|
||||||
entries.unshift(graphEntry)
|
|
||||||
labels.unshift(labelTime)
|
|
||||||
|
|
||||||
// Given we've added 2 entries for the first run but only one label, we'll add the missing label
|
|
||||||
// at the end. This will shift the labels by one entries but that's ok because the logs timestamp
|
|
||||||
// is when the sample was created, but for the user it's better to show the the consumption values
|
|
||||||
// *during* that sample, not *before* the sample
|
|
||||||
if (i == config.count - 1) {
|
|
||||||
labelTime = new Date(labelTime.getTime() - config.sampleRate * 60000)
|
|
||||||
// print("Adding oldest entry label", labelTime, config.sampleRate, config.toLabel(labelTime))
|
|
||||||
labels.unshift(labelTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// print("assigning categories:", labels)
|
|
||||||
categoryAxis.timestamps = labels
|
|
||||||
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
|
||||||
var entry = entries[i]
|
|
||||||
// print("Adding entry", JSON.stringify(entry))
|
|
||||||
for (var j = 0; j < consumers.count; j++) {
|
|
||||||
var consumer = consumers.get(j)
|
|
||||||
barSeries.thingBarSetMap[consumer.id].append(entry[consumer.id])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onEntriesAdded: {
|
readonly property ThingPowerLogs logs: ThingPowerLogs {
|
||||||
if (fetchingData) {
|
id: thingPowerLogs
|
||||||
return
|
engine: _engine
|
||||||
|
startTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, -d.config.count)
|
||||||
|
endTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, d.config.count)
|
||||||
|
thingId: consumerDelegate.thing.id
|
||||||
|
sampleRate: d.config.sampleRate
|
||||||
|
loader: logsLoader
|
||||||
|
|
||||||
|
onFetchingDataChanged: {
|
||||||
|
if (fetchingData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
consumerDelegate.refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chartView.animationOptions = ChartView.NoAnimation
|
Component.onCompleted: {
|
||||||
|
var values = []
|
||||||
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
values.push(0)
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
barSet = barSeries.append(consumerDelegate.thing.name, values)
|
||||||
var entry = entries[i]
|
barSet.color = NymeaUtils.generateColor(Style.generationBaseColor, index)
|
||||||
var thing = engine.thingManager.things.getThing(entry.thingId)
|
barSet.borderColor = barSet.color
|
||||||
// print("Adding new sample. thing:", thing.name);
|
barSet.borderWith = 0
|
||||||
// print("Timestamp:", entry.timestamp, entry.totalConsumption)
|
|
||||||
// update current last
|
|
||||||
var barSet = barSeries.thingBarSetMap[thing.id]
|
|
||||||
var lastTimestamp = categoryAxis.timestamps[categoryAxis.count - 1]
|
|
||||||
var previous = powerLogs.find(entry.thingId, lastTimestamp)
|
|
||||||
var previousValue = previous ? previous.totalConsumption : 0
|
|
||||||
// print("previousValue:", previousValue, "newValue:", entry.totalConsumption, "diff", entry.totalConsumption - previousValue)
|
|
||||||
barSet.replace(barSet.count - 1, entry.totalConsumption - previousValue)
|
|
||||||
|
|
||||||
// remove the oldest
|
|
||||||
barSet.remove(0, 1)
|
|
||||||
|
|
||||||
// and add a new one (always 0 for a start)
|
|
||||||
barSet.append(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var labels = categoryAxis.timestamps
|
|
||||||
labels.splice(0, 1)
|
|
||||||
labels.push(entries[0].timestamp)
|
|
||||||
categoryAxis.timestamps = labels
|
|
||||||
|
|
||||||
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
onLiveEntryChanged: {
|
|
||||||
if (powerLogs.fetchingData) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// print("live entry changed", entry.thingId, entry.timestamp)
|
|
||||||
var previous = powerLogs.find(entry.thingId, new Date(categoryAxis.timestamps[categoryAxis.timestamps.length - 1]))
|
|
||||||
var previousValue = previous ? previous.totalConsumption : 0
|
|
||||||
var barSet = barSeries.thingBarSetMap[entry.thingId]
|
|
||||||
|
|
||||||
if (!barSet) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
barSet.replace(barSet.count - 1, entry.totalConsumption - previousValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.margins: Style.smallMargins
|
Layout.margins: Style.smallMargins
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: qsTr("Consumers totals")
|
text: qsTr("Consumers totals")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectionTabs {
|
SelectionTabs {
|
||||||
@ -243,246 +170,347 @@ StatsBase {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: Style.smallMargins
|
Layout.leftMargin: Style.smallMargins
|
||||||
Layout.rightMargin: Style.smallMargins
|
Layout.rightMargin: Style.smallMargins
|
||||||
currentIndex: 0
|
currentIndex: 1
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
Component.onCompleted: {
|
ListElement { modelData: qsTr("Hours"); config: "hours" }
|
||||||
append({modelData: qsTr("Hours"), config: "hours" })
|
ListElement { modelData: qsTr("Days"); config: "days" }
|
||||||
append({modelData: qsTr("Days"), config: "days" })
|
ListElement { modelData: qsTr("Weeks"); config: "weeks" }
|
||||||
append({modelData: qsTr("Weeks"), config: "weeks" })
|
ListElement { modelData: qsTr("Months"); config: "months" }
|
||||||
append({modelData: qsTr("Months"), config: "months" })
|
ListElement { modelData: qsTr("Years"); config: "years" }
|
||||||
append({modelData: qsTr("Years"), config: "years" })
|
// ListElement { modelData: qsTr("Minutes"); config: "minutes" }
|
||||||
// append({modelData: qsTr("Minutes"), config: "minutes" })
|
|
||||||
|
|
||||||
selectionTabs.currentIndex = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onCurrentValueChanged: {
|
onTabSelected: {
|
||||||
root.update()
|
d.startOffset = 0
|
||||||
|
logsLoader.fetchLogs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
ChartView {
|
|
||||||
id: chartView
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
// margins.left: 0
|
Label {
|
||||||
margins.right: 0
|
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
margins.bottom: 0
|
y: chartView.y + chartView.plotArea.y + Style.smallMargins
|
||||||
margins.top: 0
|
text: d.config.toRangeLabel(d.startTime)
|
||||||
|
font: Style.smallFont
|
||||||
|
opacity: d.startOffset < -d.config.count ? .5 : 0
|
||||||
|
Behavior on opacity { NumberAnimation {} }
|
||||||
|
}
|
||||||
|
|
||||||
backgroundColor: "transparent"
|
ChartView {
|
||||||
legend.alignment: Qt.AlignBottom
|
id: chartView
|
||||||
legend.font: Style.extraSmallFont
|
anchors.fill: parent
|
||||||
legend.labelColor: Style.foregroundColor
|
|
||||||
|
|
||||||
function reset() {
|
backgroundColor: "transparent"
|
||||||
chartView.animationOptions = ChartView.NoAnimation
|
// margins.left: 0
|
||||||
barSeries.clear();
|
margins.right: 0
|
||||||
valueAxis.max = 0
|
margins.bottom: 0
|
||||||
var map = {}
|
margins.top: 0
|
||||||
for (var j = 0; j < consumers.count; j++) {
|
|
||||||
var consumer = consumers.get(j)
|
legend.alignment: Qt.AlignBottom
|
||||||
var barSet = barSeries.append(consumer.name, [])
|
legend.font: Style.extraSmallFont
|
||||||
// barSet.color = root.colors[j % root.colors.length]
|
legend.labelColor: Style.foregroundColor
|
||||||
barSet.color = NymeaUtils.generateColor(Style.generationBaseColor, j)
|
|
||||||
barSet.borderColor = barSet.color
|
ActivityIndicator {
|
||||||
barSet.borderWith = 0
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
map[consumer.id] = barSet
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
visible: logsLoader.fetchingData
|
||||||
|
opacity: .5
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
text: qsTr("No data available")
|
||||||
|
opacity: {
|
||||||
|
if (logsLoader.fetchingData || d.startOffset == 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var oldestEntry = new Date().getTime();
|
||||||
|
var haveItems = false;
|
||||||
|
for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
var logsModel = consumersRepeater.itemAt(i).logs
|
||||||
|
var firstEntry = logsModel.get(0)
|
||||||
|
if (firstEntry) {
|
||||||
|
haveItems = true;
|
||||||
|
oldestEntry = Math.min(oldestEntry, firstEntry.timestamp.getTime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("oldestEntry", new Date(oldestEntry), haveItems)
|
||||||
|
if (!haveItems || oldestEntry >= d.endTime.getTime()) {
|
||||||
|
return 0.5
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
font: Style.smallFont
|
||||||
|
Behavior on opacity { NumberAnimation {}}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: labelsLayout
|
||||||
|
x: Style.smallMargins
|
||||||
|
y: chartView.plotArea.y
|
||||||
|
height: chartView.plotArea.height
|
||||||
|
width: chartView.plotArea.x - x
|
||||||
|
Repeater {
|
||||||
|
model: valueAxis.tickCount
|
||||||
|
delegate: Label {
|
||||||
|
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
||||||
|
width: parent.width - Style.smallMargins
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
color: Style.foregroundColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BarSeries {
|
||||||
|
id: barSeries
|
||||||
|
axisX: BarCategoryAxis {
|
||||||
|
id: categoryAxis
|
||||||
|
labelsColor: Style.foregroundColor
|
||||||
|
labelsFont: Style.extraSmallFont
|
||||||
|
gridVisible: false
|
||||||
|
gridLineColor: Style.tileOverlayColor
|
||||||
|
lineVisible: false
|
||||||
|
titleVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
|
||||||
|
categories: {
|
||||||
|
var ret = []
|
||||||
|
print("Updating categories from", d.config.startTime())
|
||||||
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i);
|
||||||
|
print("*** adding", timestamp, d.startOffset, i)
|
||||||
|
ret.push(d.config.toLabel(timestamp))
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axisY: ValueAxis {
|
||||||
|
id: valueAxis
|
||||||
|
min: 0
|
||||||
|
gridLineColor: Style.tileOverlayColor
|
||||||
|
labelsVisible: false
|
||||||
|
labelsColor: Style.foregroundColor
|
||||||
|
labelsFont: Style.extraSmallFont
|
||||||
|
lineVisible: false
|
||||||
|
titleVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
|
||||||
|
function adjustMax(newValue) {
|
||||||
|
if (max < newValue) {
|
||||||
|
max = Math.ceil(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
barSeries.thingBarSetMap = map
|
|
||||||
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: labelsLayout
|
anchors.fill: parent
|
||||||
x: Style.smallMargins
|
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
||||||
y: chartView.plotArea.y
|
anchors.topMargin: chartView.y + chartView.plotArea.y
|
||||||
height: chartView.plotArea.height
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||||
width: chartView.plotArea.x - x
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||||
Repeater {
|
z: -1
|
||||||
model: valueAxis.tickCount
|
|
||||||
delegate: Label {
|
Rectangle {
|
||||||
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
height: parent.height + Style.margins * 2
|
||||||
width: parent.width - Style.smallMargins
|
y: -Style.smallMargins
|
||||||
horizontalAlignment: Text.AlignRight
|
radius: Style.smallCornerRadius
|
||||||
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
|
width: chartView.plotArea.width / categoryAxis.count
|
||||||
verticalAlignment: Text.AlignTop
|
color: Style.tileBackgroundColor
|
||||||
font: Style.extraSmallFont
|
property int idx: Math.max(0, Math.min(categoryAxis.count -1, Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)))
|
||||||
color: Style.foregroundColor
|
visible: toolTip.visible
|
||||||
}
|
|
||||||
|
x: idx * parent.width / categoryAxis.count
|
||||||
|
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BarSeries {
|
|
||||||
id: barSeries
|
|
||||||
axisX: BarCategoryAxis {
|
|
||||||
id: categoryAxis
|
|
||||||
labelsColor: Style.foregroundColor
|
|
||||||
labelsFont: Style.extraSmallFont
|
|
||||||
gridVisible: false
|
|
||||||
gridLineColor: Style.tileOverlayColor
|
|
||||||
lineVisible: false
|
|
||||||
titleVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
|
|
||||||
categories: {
|
MouseArea {
|
||||||
var ret = []
|
id: mouseArea
|
||||||
for (var i = 0; i < timestamps.length; i++) {
|
anchors.fill: parent
|
||||||
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
|
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
||||||
}
|
anchors.topMargin: chartView.y + chartView.plotArea.y
|
||||||
return ret
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||||
}
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||||
|
|
||||||
property var timestamps: []
|
hoverEnabled: true
|
||||||
}
|
preventStealing: tooltipping || dragging
|
||||||
axisY: ValueAxis {
|
|
||||||
id: valueAxis
|
|
||||||
min: 0
|
|
||||||
gridLineColor: Style.tileOverlayColor
|
|
||||||
labelsVisible: false
|
|
||||||
labelsColor: Style.foregroundColor
|
|
||||||
labelsFont: Style.extraSmallFont
|
|
||||||
lineVisible: false
|
|
||||||
titleVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
|
|
||||||
function adjustMax(newValue) {
|
property int startMouseX: 0
|
||||||
if (max < newValue) {
|
property bool dragging: false
|
||||||
max = Math.ceil(newValue)
|
property bool tooltipping: false
|
||||||
|
property int dragStartOffset: 0
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 300
|
||||||
|
running: mouseArea.pressed
|
||||||
|
onTriggered: {
|
||||||
|
if (!mouseArea.dragging) {
|
||||||
|
mouseArea.tooltipping = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var thingBarSetMap: ({})
|
onReleased: {
|
||||||
}
|
if (mouseArea.dragging) {
|
||||||
}
|
logsLoader.fetchLogs();
|
||||||
}
|
d.refresh()
|
||||||
|
mouseArea.dragging = false;
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
|
||||||
anchors.topMargin: chartView.y + chartView.plotArea.y
|
|
||||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
||||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
||||||
z: -1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: parent.height + Style.margins * 2
|
|
||||||
y: -Style.smallMargins
|
|
||||||
radius: Style.smallCornerRadius
|
|
||||||
width: chartView.plotArea.width / categoryAxis.count
|
|
||||||
color: Style.tileBackgroundColor
|
|
||||||
property int idx: Math.max(0, Math.min(categoryAxis.count -1, Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)))
|
|
||||||
visible: toolTip.visible
|
|
||||||
|
|
||||||
x: idx * parent.width / categoryAxis.count
|
|
||||||
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
|
||||||
anchors.topMargin: chartView.y + chartView.plotArea.y
|
|
||||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
||||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
||||||
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 300
|
|
||||||
running: mouseArea.pressed
|
|
||||||
onTriggered: mouseArea.preventStealing = true
|
|
||||||
}
|
|
||||||
onReleased: mouseArea.preventStealing = false
|
|
||||||
|
|
||||||
|
|
||||||
NymeaToolTip {
|
|
||||||
id: toolTip
|
|
||||||
|
|
||||||
backgroundItem: chartView
|
|
||||||
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
|
|
||||||
|
|
||||||
property int idx: Math.max(0, Math.min(categoryAxis.count -1, Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)))
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
|
|
||||||
property int chartWidth: chartView.plotArea.width
|
|
||||||
property int barWidth: chartWidth / categoryAxis.count
|
|
||||||
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
|
|
||||||
idx * barWidth + barWidth + Style.smallMargins
|
|
||||||
: idx * barWidth - Style.smallMargins - width
|
|
||||||
property double setMaxValue: {
|
|
||||||
var max = 0;
|
|
||||||
for (var i = 0; i < consumers.count; i++) {
|
|
||||||
var consumer = consumers.get(i)
|
|
||||||
max = barSeries.thingBarSetMap.hasOwnProperty(consumer.id) ? Math.max(max, barSeries.thingBarSetMap[consumer.id].at(idx)) : 0
|
|
||||||
}
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
|
||||||
|
|
||||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
|
||||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: tooltipLayout
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
top: parent.top
|
|
||||||
margins: Style.smallMargins
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
|
|
||||||
font: Style.smallFont
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ListModel {
|
|
||||||
id: toolTipModel
|
|
||||||
property var entries: {
|
|
||||||
var unsorted = []
|
|
||||||
for (var i = 0; i < consumers.count; i++) {
|
|
||||||
var consumer = consumers.get(i)
|
|
||||||
var entry = {
|
|
||||||
name: consumer.name,
|
|
||||||
value: barSeries.thingBarSetMap[consumer.id].at(toolTip.idx).toFixed(2),
|
|
||||||
indexInModel: i
|
|
||||||
}
|
|
||||||
unsorted.push(entry)
|
|
||||||
}
|
|
||||||
return unsorted
|
|
||||||
}
|
|
||||||
onEntriesChanged: {
|
|
||||||
clear();
|
|
||||||
var unsorted = entries;
|
|
||||||
for (var i = 0; i < unsorted.length; i++) {
|
|
||||||
var j = 0;
|
|
||||||
while (j < count && get(j).value > unsorted[i].value) {
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
insert(j, unsorted[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
mouseArea.tooltipping = false;
|
||||||
|
}
|
||||||
|
|
||||||
delegate: RowLayout {
|
onPressed: {
|
||||||
Rectangle {
|
startMouseX = mouseX
|
||||||
width: Style.extraSmallFont.pixelSize
|
dragStartOffset = d.startOffset
|
||||||
height: width
|
}
|
||||||
// color: root.colors[model.indexInModel % root.colors.length]
|
|
||||||
color: NymeaUtils.generateColor(Style.generationBaseColor, model.indexInModel)
|
onDoubleClicked: {
|
||||||
|
var idx = Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
|
||||||
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
|
||||||
|
selectionTabs.currentIndex--
|
||||||
|
var startTime = d.config.startTime()
|
||||||
|
d.startOffset = (timestamp.getTime() - startTime.getTime()) / (d.config.sampleRate * 60 * 1000)
|
||||||
|
logsLoader.fetchLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseXChanged: {
|
||||||
|
if (!pressed || mouseArea.tooltipping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Math.abs(startMouseX - mouseX) < 10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragging = true
|
||||||
|
|
||||||
|
var dragDelta = startMouseX - mouseX
|
||||||
|
var slotWidth = mouseArea.width / d.config.count
|
||||||
|
var offset = Math.floor(dragDelta / slotWidth);
|
||||||
|
d.startOffset = Math.min(dragStartOffset + offset, 0)
|
||||||
|
d.fetchPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
property int wheelDelta: 0
|
||||||
|
onWheel: {
|
||||||
|
wheelDelta += wheel.pixelDelta.x
|
||||||
|
var slotWidth = mouseArea.width / d.config.count
|
||||||
|
while (wheelDelta > slotWidth) {
|
||||||
|
d.startOffset--
|
||||||
|
wheelDelta -= slotWidth
|
||||||
|
}
|
||||||
|
while (wheelDelta < -slotWidth) {
|
||||||
|
d.startOffset = Math.min(d.startOffset + 1, 0)
|
||||||
|
wheelDelta += slotWidth
|
||||||
|
}
|
||||||
|
d.fetchPending = true;
|
||||||
|
wheelStopTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: wheelStopTimer
|
||||||
|
interval: 300
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
logsLoader.fetchLogs()
|
||||||
|
d.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NymeaToolTip {
|
||||||
|
id: toolTip
|
||||||
|
|
||||||
|
backgroundItem: chartView
|
||||||
|
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
|
||||||
|
|
||||||
|
property int idx: Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
|
||||||
|
property date timestamp: root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
|
||||||
|
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
|
||||||
|
property int chartWidth: chartView.plotArea.width
|
||||||
|
property int barWidth: chartWidth / categoryAxis.count
|
||||||
|
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
|
||||||
|
idx * barWidth + barWidth + Style.smallMargins
|
||||||
|
: idx * barWidth - Style.smallMargins - width
|
||||||
|
property double setMaxValue: {
|
||||||
|
var max = 0;
|
||||||
|
for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
max = Math.max(max, consumersRepeater.itemAt(i).barSet.at(idx))
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
||||||
|
|
||||||
|
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||||
|
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: tooltipLayout
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
margins: Style.smallMargins
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: "%1: %2 kWh".arg(model.name).arg(model.value)
|
text: d.config.toLongLabel(toolTip.timestamp)
|
||||||
font: Style.extraSmallFont
|
font: Style.smallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ListModel {
|
||||||
|
id: toolTipModel
|
||||||
|
property var entries: {
|
||||||
|
var unsorted = []
|
||||||
|
for (var i = 0; i < consumers.count; i++) {
|
||||||
|
var consumer = consumers.get(i)
|
||||||
|
var entry = {
|
||||||
|
name: consumer.name,
|
||||||
|
value: consumersRepeater.itemAt(i).barSet.at(toolTip.idx).toFixed(2),
|
||||||
|
indexInModel: i
|
||||||
|
}
|
||||||
|
unsorted.push(entry)
|
||||||
|
}
|
||||||
|
return unsorted
|
||||||
|
}
|
||||||
|
onEntriesChanged: {
|
||||||
|
clear();
|
||||||
|
var unsorted = entries;
|
||||||
|
for (var i = 0; i < unsorted.length; i++) {
|
||||||
|
var j = 0;
|
||||||
|
while (j < count && get(j).value > unsorted[i].value) {
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
insert(j, unsorted[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
// color: root.colors[model.indexInModel % root.colors.length]
|
||||||
|
color: NymeaUtils.generateColor(Style.generationBaseColor, model.indexInModel)
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: "%1: %2 kWh".arg(model.name).arg(model.value)
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,142 +11,62 @@ Item {
|
|||||||
property var colors: null
|
property var colors: null
|
||||||
property ThingsProxy consumers: null
|
property ThingsProxy consumers: null
|
||||||
|
|
||||||
Connections {
|
PowerBalanceLogs {
|
||||||
target: consumers
|
id: powerBalanceLogs
|
||||||
onCountChanged: d.updateConsumers()
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: engine.tagsManager
|
|
||||||
onBusyChanged: d.updateConsumers()
|
|
||||||
}
|
|
||||||
|
|
||||||
ThingPowerLogs {
|
|
||||||
id: thingPowerLogs
|
|
||||||
engine: _engine
|
engine: _engine
|
||||||
startTime: dateTimeAxis.min
|
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||||
sampleRate: EnergyLogs.SampleRate15Mins
|
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||||
thingIds: []
|
sampleRate: d.sampleRate
|
||||||
loadingInhibited: thingIds.length === 0
|
Component.onCompleted: fetchLogs()
|
||||||
|
|
||||||
onModelReset: {
|
|
||||||
for (var i = 0; i < consumers.count; i++) {
|
|
||||||
var consumer = consumers.get(i);
|
|
||||||
var series = d.thingsSeriesMap[consumer.id];
|
|
||||||
series.upperSeries.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onEntriesAdded: {
|
onEntriesAdded: {
|
||||||
var thingValues = ({})
|
print("entries added", index, entries.length)
|
||||||
var timestamp = entries[0].timestamp
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
for (var i = 0; i < entries.length; i++) {
|
||||||
var entry = entries[i]
|
var entry = entries[i]
|
||||||
var thing = engine.thingManager.things.getThing(entries[i].thingId)
|
// print("got entry", entry.timestamp)
|
||||||
thingValues[entry.thingId] = entry.currentPower
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add them in the order of the chart (same as proxy), summing it up
|
zeroSeries.ensureValue(entry.timestamp)
|
||||||
var totalValue = 0;
|
valueAxis.adjustMax(entry.consumption)
|
||||||
for (var i = 0; i < consumers.count; i++) {
|
consumptionSeries.insertEntry(index + i, entry)
|
||||||
var consumer = consumers.get(i);
|
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
|
||||||
var value = thingValues.hasOwnProperty(consumer.id) ? thingValues[consumer.id] : 0
|
d.now = entry.timestamp
|
||||||
totalValue += thingValues.hasOwnProperty(consumer.id) ? thingValues[consumer.id] : 0;
|
}
|
||||||
var series = d.thingsSeriesMap[consumer.id];
|
|
||||||
series.upperSeries.append(timestamp, totalValue)
|
|
||||||
}
|
}
|
||||||
thingPowerLogs.maxValue = Math.max(thingPowerLogs.maxValue, totalValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property double maxValue: 0
|
onEntriesRemoved: {
|
||||||
|
consumptionUpperSeries.removePoints(index, count)
|
||||||
|
zeroSeries.shrink()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property PowerBalanceLogs powerBalanceLogs: PowerBalanceLogs {
|
ThingPowerLogsLoader {
|
||||||
|
id: logsLoader
|
||||||
engine: _engine
|
engine: _engine
|
||||||
startTime: dateTimeAxis.min
|
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||||
sampleRate: EnergyLogs.SampleRate15Mins
|
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||||
|
sampleRate: d.sampleRate
|
||||||
onEntryAdded: {
|
|
||||||
consumptionSeries.addEntry(entry)
|
|
||||||
|
|
||||||
if (dateTimeAxis.now < entry.timestamp) {
|
|
||||||
dateTimeAxis.now = entry.timestamp
|
|
||||||
zeroSeries.update(entry.timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 60000
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
var now = new Date()
|
|
||||||
if (dateTimeAxis.now < now) {
|
|
||||||
dateTimeAxis.now = now
|
|
||||||
zeroSeries.update(now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: engine.thingManager
|
|
||||||
onFetchingDataChanged: d.updateConsumers()
|
|
||||||
onThingAdded: {
|
|
||||||
if (thing.thingClass.interfaces.indexOf("smartmeterconsumer") >= 0) {
|
|
||||||
d.updateConsumers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
for (var i = 0; i < powerBalanceLogs.count; i++) {
|
|
||||||
var entry = powerBalanceLogs.get(i);
|
|
||||||
consumptionSeries.addEntry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.updateConsumers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: d
|
id: d
|
||||||
property var thingsSeriesMap: ({})
|
|
||||||
|
|
||||||
function updateConsumers() {
|
property date now: new Date()
|
||||||
if (engine.thingManager.fetchingData || engine.tagsManager.busy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thingPowerLogs.loadingInhibited = true;
|
|
||||||
|
|
||||||
for (var thingId in d.thingsSeriesMap) {
|
readonly property int range: selectionTabs.currentValue.range
|
||||||
chartView.removeSeries(d.thingsSeriesMap[thingId])
|
readonly property int sampleRate: selectionTabs.currentValue.sampleRate
|
||||||
}
|
readonly property int visibleValues: range / sampleRate
|
||||||
d.thingsSeriesMap = ({})
|
|
||||||
|
|
||||||
var consumerThingIds = []
|
readonly property var startTime: {
|
||||||
for (var i = 0; i < consumers.count; i++) {
|
var date = new Date(now);
|
||||||
var thing = consumers.get(i);
|
date.setTime(date.getTime() - range * 60000 + 2000);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
var baseSeries = zeroSeries;
|
readonly property var endTime: {
|
||||||
if (i > 0) {
|
var date = new Date(now);
|
||||||
baseSeries = d.thingsSeriesMap[consumerThingIds[i-1]].upperSeries
|
date.setTime(date.getTime() + 2000)
|
||||||
// print("base for:", thing.name, "is", engine.thingManager.things.getThing(consumerThingIds[i-1]).name)
|
return date;
|
||||||
}
|
|
||||||
|
|
||||||
var series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, valueAxis)
|
|
||||||
series.lowerSeries = baseSeries
|
|
||||||
series.upperSeries = lineSeriesComponent.createObject(series)
|
|
||||||
// series.color = root.colors[i % root.colors.length]
|
|
||||||
series.color = NymeaUtils.generateColor(Style.generationBaseColor, i)
|
|
||||||
series.borderWidth = 0;
|
|
||||||
series.borderColor = series.color
|
|
||||||
|
|
||||||
var map = d.thingsSeriesMap
|
|
||||||
map[thing.id] = series
|
|
||||||
d.thingsSeriesMap = map
|
|
||||||
consumerThingIds.push(thing.id)
|
|
||||||
}
|
|
||||||
thingPowerLogs.thingIds = consumerThingIds;
|
|
||||||
thingPowerLogs.loadingInhibited = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,202 +75,497 @@ Item {
|
|||||||
LineSeries { }
|
LineSeries { }
|
||||||
}
|
}
|
||||||
|
|
||||||
ChartView {
|
ColumnLayout {
|
||||||
id: chartView
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
backgroundColor: "transparent"
|
Label {
|
||||||
margins.left: 0
|
Layout.fillWidth: true
|
||||||
margins.right: 0
|
Layout.margins: Style.smallMargins
|
||||||
margins.bottom: 0
|
horizontalAlignment: Text.AlignHCenter
|
||||||
margins.top: 0
|
text: qsTr("Consumers history")
|
||||||
|
}
|
||||||
|
|
||||||
title: qsTr("Consumers history")
|
|
||||||
titleColor: Style.foregroundColor
|
|
||||||
|
|
||||||
legend.alignment: Qt.AlignBottom
|
|
||||||
legend.labelColor: Style.foregroundColor
|
|
||||||
legend.font: Style.extraSmallFont
|
|
||||||
|
|
||||||
ValueAxis {
|
|
||||||
id: valueAxis
|
|
||||||
min: 0
|
|
||||||
max: Math.ceil(Math.max(powerBalanceLogs.maxValue, thingPowerLogs.maxValue) / 1000) * 1000
|
|
||||||
labelFormat: ""
|
|
||||||
gridLineColor: Style.tileOverlayColor
|
|
||||||
labelsVisible: false
|
|
||||||
lineVisible: false
|
|
||||||
titleVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
// visible: false
|
|
||||||
|
|
||||||
|
SelectionTabs {
|
||||||
|
id: selectionTabs
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Style.smallMargins
|
||||||
|
Layout.rightMargin: Style.smallMargins
|
||||||
|
currentIndex: 1
|
||||||
|
model: ListModel {
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Hours")
|
||||||
|
sampleRate: EnergyLogs.SampleRate1Min
|
||||||
|
range: 180 // 3 Hours: 3 * 60
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Days")
|
||||||
|
sampleRate: EnergyLogs.SampleRate15Mins
|
||||||
|
range: 1440 // 1 Day: 24 * 60
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Weeks")
|
||||||
|
sampleRate: EnergyLogs.SampleRate1Hour
|
||||||
|
range: 10080 // 7 Days: 7 * 24 * 60
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Months")
|
||||||
|
sampleRate: EnergyLogs.SampleRate3Hours
|
||||||
|
range: 43200 // 30 Days: 30 * 24 * 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTabSelected: {
|
||||||
|
d.now = new Date()
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
logsLoader.fetchLogs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: labelsLayout
|
Layout.fillWidth: true
|
||||||
x: Style.smallMargins
|
Layout.fillHeight: true
|
||||||
y: chartView.plotArea.y
|
|
||||||
height: chartView.plotArea.height
|
|
||||||
width: chartView.plotArea.x - x
|
|
||||||
Repeater {
|
|
||||||
model: valueAxis.tickCount
|
|
||||||
delegate: Label {
|
|
||||||
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
|
||||||
width: parent.width - Style.smallMargins
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
|
|
||||||
verticalAlignment: Text.AlignTop
|
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeAxis {
|
Label {
|
||||||
id: dateTimeAxis
|
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
property date now: new Date()
|
y: chartView.y + chartView.plotArea.y + Style.smallMargins
|
||||||
min: {
|
text: d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
|
||||||
var date = new Date(now);
|
font: Style.smallFont
|
||||||
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000);
|
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
|
||||||
return date;
|
Behavior on opacity { NumberAnimation {} }
|
||||||
}
|
|
||||||
max: {
|
|
||||||
var date = new Date(now);
|
|
||||||
date.setTime(date.getTime() + 2000)
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
format: "hh:mm"
|
|
||||||
labelsFont: Style.extraSmallFont
|
|
||||||
gridVisible: false
|
|
||||||
minorGridVisible: false
|
|
||||||
lineVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
labelsColor: Style.foregroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
AreaSeries {
|
|
||||||
id: consumptionSeries
|
|
||||||
axisX: dateTimeAxis
|
|
||||||
axisY: valueAxis
|
|
||||||
color: Style.gray
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
name: qsTr("Unknown")
|
|
||||||
|
|
||||||
lowerSeries: LineSeries {
|
|
||||||
id: zeroSeries
|
|
||||||
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
|
|
||||||
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
|
|
||||||
function update(timestamp) {
|
|
||||||
append(timestamp, 0);
|
|
||||||
removePoints(1,1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: consumptionUpperSeries
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEntry(entry) {
|
ChartView {
|
||||||
consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption)
|
id: chartView
|
||||||
}
|
anchors.fill: parent
|
||||||
}
|
|
||||||
|
|
||||||
}
|
backgroundColor: "transparent"
|
||||||
|
margins.left: 0
|
||||||
|
margins.right: 0
|
||||||
|
margins.bottom: 0
|
||||||
|
margins.top: 0
|
||||||
|
|
||||||
MouseArea {
|
legend.alignment: Qt.AlignBottom
|
||||||
id: mouseArea
|
legend.font: Style.extraSmallFont
|
||||||
anchors.fill: parent
|
legend.labelColor: Style.foregroundColor
|
||||||
anchors.leftMargin: chartView.plotArea.x
|
|
||||||
anchors.topMargin: chartView.plotArea.y
|
|
||||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
||||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
||||||
|
|
||||||
hoverEnabled: true
|
ActivityIndicator {
|
||||||
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
Timer {
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
interval: 300
|
visible: powerBalanceLogs.fetchingData || logsLoader.fetchingData
|
||||||
running: mouseArea.pressed
|
opacity: .5
|
||||||
onTriggered: mouseArea.preventStealing = true
|
|
||||||
}
|
|
||||||
onReleased: mouseArea.preventStealing = false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: parent.height
|
|
||||||
width: 1
|
|
||||||
color: Style.foregroundColor
|
|
||||||
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
}
|
|
||||||
|
|
||||||
NymeaToolTip {
|
|
||||||
id: toolTip
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
|
|
||||||
backgroundItem: chartView
|
|
||||||
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
|
|
||||||
|
|
||||||
property int idx: consumptionUpperSeries.count - Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width)
|
|
||||||
property int seriesIndex: Math.min(consumptionUpperSeries.count - 1, Math.max(0, consumptionUpperSeries.count - idx))
|
|
||||||
|
|
||||||
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
|
|
||||||
property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - width
|
|
||||||
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
|
||||||
property double maxValue: consumptionUpperSeries.at(seriesIndex).y
|
|
||||||
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
|
||||||
|
|
||||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
|
||||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
|
||||||
|
|
||||||
property date timestamp: new Date(consumptionUpperSeries.at(seriesIndex).x)
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: tooltipLayout
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
top: parent.top
|
|
||||||
margins: Style.smallMargins
|
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
text: qsTr("No data available")
|
||||||
|
visible: !powerBalanceLogs.fetchingData && !logsLoader.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
|
||||||
font: Style.smallFont
|
font: Style.smallFont
|
||||||
|
opacity: .5
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
Rectangle {
|
|
||||||
width: Style.extraSmallFont.pixelSize
|
ValueAxis {
|
||||||
height: width
|
id: valueAxis
|
||||||
color: consumptionSeries.color
|
min: 0
|
||||||
}
|
max: 1
|
||||||
Label {
|
labelFormat: ""
|
||||||
property double rawValue: consumptionUpperSeries.at(toolTip.seriesIndex).y
|
gridLineColor: Style.tileOverlayColor
|
||||||
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
|
labelsVisible: false
|
||||||
property string unit: rawValue >= 1000 ? "kW" : "W"
|
lineVisible: false
|
||||||
text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit)
|
titleVisible: false
|
||||||
font: Style.extraSmallFont
|
shadesVisible: false
|
||||||
|
// visible: false
|
||||||
|
|
||||||
|
function adjustMax(value) {
|
||||||
|
max = Math.max(max, Math.ceil(value / 100) * 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Item {
|
||||||
model: consumers
|
id: labelsLayout
|
||||||
delegate: RowLayout {
|
x: Style.smallMargins
|
||||||
id: consumerToolTipDelegate
|
y: chartView.plotArea.y
|
||||||
Rectangle {
|
height: chartView.plotArea.height
|
||||||
width: Style.extraSmallFont.pixelSize
|
width: chartView.plotArea.x - x
|
||||||
height: width
|
Repeater {
|
||||||
// color: index >= 0 ? root.colors[index % root.colors.length] : "white"
|
model: valueAxis.tickCount
|
||||||
color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white"
|
delegate: Label {
|
||||||
}
|
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
||||||
|
width: parent.width - Style.smallMargins
|
||||||
Label {
|
horizontalAlignment: Text.AlignRight
|
||||||
property ThingPowerLogEntry entry: thingPowerLogs.find(model.id, toolTip.timestamp)
|
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
|
||||||
property double rawValue: entry ? entry.currentPower : 0
|
verticalAlignment: Text.AlignTop
|
||||||
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
|
|
||||||
property string unit: rawValue >= 1000 ? "kW" : "W"
|
|
||||||
text: "%1: %2 %3".arg(model.name).arg(displayValue.toFixed(2)).arg(unit)
|
|
||||||
font: Style.extraSmallFont
|
font: Style.extraSmallFont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DateTimeAxis {
|
||||||
|
id: dateTimeAxis
|
||||||
|
min: d.startTime
|
||||||
|
max: d.endTime
|
||||||
|
format: {
|
||||||
|
switch (selectionTabs.currentValue.sampleRate) {
|
||||||
|
case EnergyLogs.SampleRate1Min:
|
||||||
|
case EnergyLogs.SampleRate15Mins:
|
||||||
|
return "hh:mm"
|
||||||
|
case EnergyLogs.SampleRate1Hour:
|
||||||
|
case EnergyLogs.SampleRate3Hours:
|
||||||
|
case EnergyLogs.SampleRate1Day:
|
||||||
|
return "dd.MM."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tickCount: {
|
||||||
|
switch (selectionTabs.currentValue.sampleRate) {
|
||||||
|
case EnergyLogs.SampleRate1Min:
|
||||||
|
case EnergyLogs.SampleRate15Mins:
|
||||||
|
return root.width > 500 ? 13 : 7
|
||||||
|
case EnergyLogs.SampleRate1Hour:
|
||||||
|
return 7
|
||||||
|
case EnergyLogs.SampleRate3Hours:
|
||||||
|
case EnergyLogs.SampleRate1Day:
|
||||||
|
return root.width > 500 ? 12 : 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labelsFont: Style.extraSmallFont
|
||||||
|
gridVisible: false
|
||||||
|
minorGridVisible: false
|
||||||
|
lineVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
labelsColor: Style.foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
AreaSeries {
|
||||||
|
id: consumptionSeries
|
||||||
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: Style.gray
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
name: qsTr("Unknown")
|
||||||
|
// visible: false
|
||||||
|
opacity: .2
|
||||||
|
|
||||||
|
lowerSeries: LineSeries {
|
||||||
|
id: zeroSeries
|
||||||
|
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
|
||||||
|
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
|
||||||
|
function ensureValue(timestamp) {
|
||||||
|
if (count == 0) {
|
||||||
|
append(timestamp, 0)
|
||||||
|
} else if (count == 1) {
|
||||||
|
if (timestamp.getTime() < at(0).x) {
|
||||||
|
insert(0, timestamp, 0)
|
||||||
|
} else {
|
||||||
|
append(timestamp, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (timestamp.getTime() < at(0).x) {
|
||||||
|
remove(0)
|
||||||
|
insert(0, timestamp, 0)
|
||||||
|
} else if (timestamp.getTime() > at(1).x) {
|
||||||
|
remove(1)
|
||||||
|
append(timestamp, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function shrink() {
|
||||||
|
clear();
|
||||||
|
if (powerBalanceLogs.count > 0) {
|
||||||
|
ensureValue(powerBalanceLogs.get(0).timestamp)
|
||||||
|
ensureValue(powerBalanceLogs.get(powerBalanceLogs.count - 1).timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
upperSeries: LineSeries {
|
||||||
|
id: consumptionUpperSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEntry(entry) {
|
||||||
|
consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption)
|
||||||
|
}
|
||||||
|
function insertEntry(index, entry) {
|
||||||
|
consumptionUpperSeries.insert(index, entry.timestamp.getTime(), entry.consumption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: consumersRepeater
|
||||||
|
model: consumers
|
||||||
|
|
||||||
|
onCountChanged: {
|
||||||
|
if (count == consumers.count) {
|
||||||
|
logsLoader.fetchLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: consumerDelegate
|
||||||
|
|
||||||
|
readonly property Thing thing: consumers.get(index)
|
||||||
|
property AreaSeries series: null
|
||||||
|
|
||||||
|
|
||||||
|
function getBaseValue(timestamp) {
|
||||||
|
var ret = 0
|
||||||
|
if (index > 0) {
|
||||||
|
ret = consumersRepeater.itemAt(index - 1).getBaseValue(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = logs.find(timestamp)
|
||||||
|
if (entry) {
|
||||||
|
ret += entry.currentPower;
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertEntry(idx, entry) {
|
||||||
|
var baseValue = 0;
|
||||||
|
if (index > 0) {
|
||||||
|
baseValue = consumersRepeater.itemAt(index - 1).getBaseValue(entry.timestamp)
|
||||||
|
}
|
||||||
|
series.upperSeries.insert(idx, entry.timestamp.getTime(), entry.currentPower + baseValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property ThingPowerLogs logs: ThingPowerLogs {
|
||||||
|
engine: _engine
|
||||||
|
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||||
|
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||||
|
sampleRate: d.sampleRate
|
||||||
|
thingId: consumerDelegate.thing.id
|
||||||
|
loader: logsLoader
|
||||||
|
|
||||||
|
Component.onCompleted: print("thingpowerlogs completed")
|
||||||
|
|
||||||
|
onLoadingInhibitedChanged: print("Loading...", consumerDelegate.thing.name)
|
||||||
|
|
||||||
|
onEntriesAdded: {
|
||||||
|
print("Thing entries added", consumerDelegate.thing.name, index, entries.length)
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var entry = entries[i]
|
||||||
|
// print("got thing entry", thing.name, entry.timestamp, entry.currentPower, index + i)
|
||||||
|
|
||||||
|
zeroSeries.ensureValue(entry.timestamp)
|
||||||
|
valueAxis.adjustMax(entry.currentPower)
|
||||||
|
|
||||||
|
consumerDelegate.insertEntry(index + i, entry)
|
||||||
|
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
|
||||||
|
d.now = entry.timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntriesRemoved: {
|
||||||
|
consumerDelegate.series.upperSeries.removePoints(index, count)
|
||||||
|
zeroSeries.shrink()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, valueAxis)
|
||||||
|
series.lowerSeries = index == 0 ? zeroSeries : consumersRepeater.itemAt(index - 1).series.upperSeries
|
||||||
|
series.upperSeries = lineSeriesComponent.createObject(series)
|
||||||
|
series.color = NymeaUtils.generateColor(Style.generationBaseColor, index)
|
||||||
|
series.borderWidth = 0;
|
||||||
|
series.borderColor = series.color
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: chartView.plotArea.x
|
||||||
|
anchors.topMargin: chartView.plotArea.y
|
||||||
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||||
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: tooltipping || dragging
|
||||||
|
|
||||||
|
property int startMouseX: 0
|
||||||
|
property bool dragging: false
|
||||||
|
property bool tooltipping: false
|
||||||
|
|
||||||
|
property var startDatetime: null
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 300
|
||||||
|
running: mouseArea.pressed
|
||||||
|
onTriggered: {
|
||||||
|
if (!mouseArea.dragging) {
|
||||||
|
mouseArea.tooltipping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
mouseArea.tooltipping = false;
|
||||||
|
|
||||||
|
if (mouseArea.dragging) {
|
||||||
|
mouseArea.dragging = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
if (consumersRepeater.itemAt(i).logs.fetchingData) {
|
||||||
|
wheelStopTimer.start()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
logsLoader.fetchLogs()
|
||||||
|
// for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
// consumersRepeater.itemAt(i).logs.fetchLogs()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: {
|
||||||
|
startMouseX = mouseX
|
||||||
|
startDatetime = d.now
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onDoubleClicked: {
|
||||||
|
if (selectionTabs.currentIndex == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
|
||||||
|
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
|
||||||
|
selectionTabs.currentIndex--
|
||||||
|
d.now = new Date(timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
logsLoader.fetchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseXChanged: {
|
||||||
|
if (!pressed || mouseArea.tooltipping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Math.abs(startMouseX - mouseX) < 10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragging = true
|
||||||
|
|
||||||
|
var dragDelta = startMouseX - mouseX
|
||||||
|
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||||
|
// dragDelta : timeDelta = width : totalTime
|
||||||
|
var timeDelta = dragDelta * totalTime / mouseArea.width
|
||||||
|
print("dragging", dragDelta, totalTime, mouseArea.width)
|
||||||
|
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel: {
|
||||||
|
startDatetime = d.now
|
||||||
|
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||||
|
// pixelDelta : timeDelta = width : totalTime
|
||||||
|
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
|
||||||
|
print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
|
||||||
|
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
|
||||||
|
wheelStopTimer.restart()
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: wheelStopTimer
|
||||||
|
interval: 300
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
if (consumersRepeater.itemAt(i).logs.fetchingData) {
|
||||||
|
wheelStopTimer.start()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
logsLoader.fetchLogs()
|
||||||
|
// for (var i = 0; i < consumersRepeater.count; i++) {
|
||||||
|
// consumersRepeater.itemAt(i).logs.fetchLogs()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: parent.height
|
||||||
|
width: 1
|
||||||
|
color: Style.foregroundColor
|
||||||
|
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
}
|
||||||
|
|
||||||
|
NymeaToolTip {
|
||||||
|
id: toolTip
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
|
||||||
|
backgroundItem: chartView
|
||||||
|
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
|
||||||
|
|
||||||
|
property int idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
|
||||||
|
property date timestamp: new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
|
||||||
|
property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
|
||||||
|
|
||||||
|
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
|
||||||
|
property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - width
|
||||||
|
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
||||||
|
property double maxValue: toolTip.entry ? Math.max(0, entry.consumption) : 0
|
||||||
|
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
||||||
|
|
||||||
|
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||||
|
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: tooltipLayout
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
margins: Style.smallMargins
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
font: Style.smallFont
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: consumptionSeries.color
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
property double rawValue: toolTip.entry ? toolTip.entry.consumption : 0
|
||||||
|
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
|
||||||
|
property string unit: rawValue >= 1000 ? "kW" : "W"
|
||||||
|
text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit)
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: consumers
|
||||||
|
delegate: RowLayout {
|
||||||
|
id: consumerToolTipDelegate
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
// color: index >= 0 ? root.colors[index % root.colors.length] : "white"
|
||||||
|
color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
property ThingPowerLogEntry entry: toolTip.idx >= 0 ? consumersRepeater.itemAt(index).logs.find(toolTip.timestamp) : null
|
||||||
|
property double rawValue: entry ? entry.currentPower : 0
|
||||||
|
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
|
||||||
|
property string unit: rawValue >= 1000 ? "kW" : "W"
|
||||||
|
text: "%1: %2 %3".arg(model.name).arg(displayValue.toFixed(2)).arg(unit)
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,48 +19,98 @@ StatsBase {
|
|||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: d
|
id: d
|
||||||
property BarSet consumptionSet: null
|
property var config: root.configs[selectionTabs.currentValue.config]
|
||||||
property BarSet productionSet: null
|
property int startOffset: 0
|
||||||
property BarSet acquisitionSet: null
|
|
||||||
property BarSet returnSet: null
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload() {
|
property date startTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset)
|
||||||
if (selectionTabs.currentValue === undefined) {
|
property date endTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset + config.count)
|
||||||
return
|
|
||||||
}
|
property bool fetchPending: false
|
||||||
if (engine.thingManager.fetchingData) {
|
property bool loading: fetchPending || wheelStopTimer.running || powerBalanceLogs.fetchingData
|
||||||
return;
|
onLoadingChanged: {
|
||||||
|
if (!loading) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = root.configs[selectionTabs.currentValue.config]
|
onConfigChanged: valueAxis.max = 1
|
||||||
print("Loading Power Balance Stats with config:", config.startTime(), config.sampleRate)
|
onStartOffsetChanged: {
|
||||||
|
// print("updating because of offset change. fetchingData", powerBalanceLogs.fetchingData, "fetchPending", d.fetchPending)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
function refresh() {
|
||||||
|
if (powerBalanceLogs.loadingInhibited) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
powerBalanceLogs.loadingInhibited = true
|
var upcomingTimestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.config.count)
|
||||||
powerBalanceLogs.sampleRate = config.sampleRate
|
// print("refreshing config start", d.config.startTime(), "upcoming:", upcomingTimestamp, "fetchPending", d.fetchPending)
|
||||||
powerBalanceLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
powerBalanceLogs.loadingInhibited = false
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i + 1)
|
||||||
|
var previousTimestamp = root.calculateTimestamp(timestamp, d.config.sampleRate, -1)
|
||||||
chartView.reset();
|
// print("timestamp:", timestamp)
|
||||||
}
|
var entry = powerBalanceLogs.find(timestamp)
|
||||||
|
var previousEntry = powerBalanceLogs.find(previousTimestamp);
|
||||||
Connections {
|
if (entry && (previousEntry || !d.loading)) {
|
||||||
target: engine.thingManager
|
// print("found entry:", entry.timestamp, previousEntry)
|
||||||
onFetchingDataChanged: {
|
// print("Acquisition", entry.totalAcquisition)
|
||||||
print("Thingmanager loaded", engine.thingManager.fetchingData)
|
var consumption = entry.totalConsumption
|
||||||
if (!engine.thingManager.fetchingData) root.reload()
|
var production = entry.totalProduction
|
||||||
|
var acquisition = entry.totalAcquisition
|
||||||
|
var returned = entry.totalReturn
|
||||||
|
if (previousEntry) {
|
||||||
|
consumption -= previousEntry.totalConsumption
|
||||||
|
production -= previousEntry.totalProduction
|
||||||
|
acquisition -= previousEntry.totalAcquisition
|
||||||
|
returned -= previousEntry.totalReturn
|
||||||
|
}
|
||||||
|
consumptionSet.replace(i, consumption)
|
||||||
|
productionSet.replace(i, production)
|
||||||
|
acquisitionSet.replace(i, acquisition)
|
||||||
|
returnSet.replace(i, returned)
|
||||||
|
valueAxis.adjustMax(consumption)
|
||||||
|
valueAxis.adjustMax(production)
|
||||||
|
valueAxis.adjustMax(acquisition)
|
||||||
|
valueAxis.adjustMax(returned)
|
||||||
|
} else if (timestamp.getTime() == upcomingTimestamp.getTime() && (previousEntry || !d.loading)) {
|
||||||
|
// print("it's today!")
|
||||||
|
var consumption = energyManager.totalConsumption
|
||||||
|
var production = energyManager.totalProduction
|
||||||
|
var acquisition = energyManager.totalAcquisition
|
||||||
|
var returned = energyManager.totalReturn
|
||||||
|
if (previousEntry) {
|
||||||
|
consumption -= previousEntry.totalConsumption
|
||||||
|
production -= previousEntry.totalProduction
|
||||||
|
acquisition -= previousEntry.totalAcquisition
|
||||||
|
returned -= previousEntry.totalReturn
|
||||||
|
}
|
||||||
|
consumptionSet.replace(i, consumption)
|
||||||
|
productionSet.replace(i, production)
|
||||||
|
acquisitionSet.replace(i, acquisition)
|
||||||
|
returnSet.replace(i, returned)
|
||||||
|
valueAxis.adjustMax(consumption)
|
||||||
|
valueAxis.adjustMax(production)
|
||||||
|
valueAxis.adjustMax(acquisition)
|
||||||
|
valueAxis.adjustMax(returned)
|
||||||
|
} else {
|
||||||
|
consumptionSet.replace(i, 0)
|
||||||
|
productionSet.replace(i, 0)
|
||||||
|
acquisitionSet.replace(i, 0)
|
||||||
|
returnSet.replace(i, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.margins: Style.smallMargins
|
Layout.margins: Style.smallMargins
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: qsTr("Totals")
|
text: qsTr("Totals")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectionTabs {
|
SelectionTabs {
|
||||||
@ -68,422 +118,419 @@ StatsBase {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: Style.smallMargins
|
Layout.leftMargin: Style.smallMargins
|
||||||
Layout.rightMargin: Style.smallMargins
|
Layout.rightMargin: Style.smallMargins
|
||||||
|
currentIndex: 1
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
Component.onCompleted: {
|
ListElement { modelData: qsTr("Hours"); config: "hours" }
|
||||||
append({modelData: qsTr("Hours"), config: "hours" })
|
ListElement { modelData: qsTr("Days"); config: "days" }
|
||||||
append({modelData: qsTr("Days"), config: "days" })
|
ListElement { modelData: qsTr("Weeks"); config: "weeks" }
|
||||||
append({modelData: qsTr("Weeks"), config: "weeks" })
|
ListElement { modelData: qsTr("Months"); config: "months" }
|
||||||
append({modelData: qsTr("Months"), config: "months" })
|
ListElement { modelData: qsTr("Years"); config: "years" }
|
||||||
append({modelData: qsTr("Years"), config: "years" })
|
// ListElement { modelData: qsTr("Minutes"); config: "minutes" }
|
||||||
// append({modelData: qsTr("Minutes"), config: "minutes" })
|
|
||||||
|
|
||||||
selectionTabs.currentIndex = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onCurrentValueChanged: {
|
onTabSelected: {
|
||||||
root.reload()
|
d.startOffset = 0
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: energyManager
|
target: energyManager
|
||||||
onPowerBalanceChanged: {
|
onPowerBalanceChanged: {
|
||||||
var start = powerBalanceLogs.get(powerBalanceLogs.count - 1 )
|
// print("updating because of power balance change. fetchingData", powerBalanceLogs.fetchingData, "fetchPending", d.fetchPending)
|
||||||
// print("balance changed:", d.consumptionSet, powerBalanceLogs, powerBalanceLogs.count)
|
d.refresh();
|
||||||
// print("updating", start ? start.timestamp : "", start ? start.totalConsumption : 0, root.energyManager.totalConsumption, root.energyManager.totalConsumption - (start ? start.totalConsumption : 0))
|
|
||||||
if (root.hasProducers) {
|
|
||||||
var consumption = root.energyManager.totalConsumption - (start ? start.totalConsumption : 0)
|
|
||||||
d.consumptionSet.replace(d.consumptionSet.count - 1, consumption)
|
|
||||||
valueAxis.adjustMax(consumption)
|
|
||||||
var production = root.energyManager.totalProduction - (start ? start.totalProduction : 0)
|
|
||||||
d.productionSet.replace(d.productionSet.count - 1, production)
|
|
||||||
valueAxis.adjustMax(production)
|
|
||||||
}
|
|
||||||
var acquisition = root.energyManager.totalAcquisition - (start ? start.totalAcquisition : 0)
|
|
||||||
if (d.acquisitionSet) {
|
|
||||||
d.acquisitionSet.replace(d.acquisitionSet.count - 1, acquisition)
|
|
||||||
}
|
|
||||||
valueAxis.adjustMax(acquisition)
|
|
||||||
var ret = root.energyManager.totalReturn - (start ? start.totalReturn : 0)
|
|
||||||
if (d.returnSet) {
|
|
||||||
d.returnSet.replace(d.returnSet.count - 1, ret)
|
|
||||||
}
|
|
||||||
valueAxis.adjustMax(ret)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PowerBalanceLogs {
|
PowerBalanceLogs {
|
||||||
id: powerBalanceLogs
|
id: powerBalanceLogs
|
||||||
engine: _engine
|
engine: _engine
|
||||||
loadingInhibited: true
|
startTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, -d.config.count)
|
||||||
|
endTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, d.config.count)
|
||||||
|
sampleRate: d.config.sampleRate
|
||||||
|
Component.onCompleted: fetchLogs()
|
||||||
|
|
||||||
onFetchingDataChanged: {
|
onFetchingDataChanged: {
|
||||||
if (!fetchingData) {
|
if (!fetchingData) {
|
||||||
chartView.animationOptions = ChartView.NoAnimation
|
|
||||||
|
|
||||||
chartView.reset();
|
|
||||||
|
|
||||||
print("Logs fetched")
|
print("Logs fetched")
|
||||||
var config = root.configs[selectionTabs.currentValue.config]
|
d.fetchPending = false
|
||||||
|
d.refresh()
|
||||||
var labels = []
|
|
||||||
var entries = []
|
|
||||||
|
|
||||||
var newestLogTimestamp = powerBalanceLogs.count > 0 ? powerBalanceLogs.get(powerBalanceLogs.count - 1).timestamp : new Date();
|
|
||||||
for (var i = 0; i < config.count; i++) {
|
|
||||||
var entry = powerBalanceLogs.get(powerBalanceLogs.count - i - 1)
|
|
||||||
|
|
||||||
// if it's the first, let's add a generated entry which shows the total from the newest log to the current live value
|
|
||||||
if (i == 0) {
|
|
||||||
var liveEntry = {
|
|
||||||
consumption: energyManager.totalConsumption,
|
|
||||||
production: energyManager.totalProduction,
|
|
||||||
acquisition: energyManager.totalAcquisition,
|
|
||||||
returned: energyManager.totalReturn
|
|
||||||
}
|
|
||||||
if (entry) {
|
|
||||||
liveEntry.consumption -= entry.totalConsumption
|
|
||||||
liveEntry.production -= entry.totalProduction
|
|
||||||
liveEntry.acquisition -= entry.totalAcquisition
|
|
||||||
liveEntry.returned -= entry.totalReturn
|
|
||||||
}
|
|
||||||
// print("Adding live entry:", liveEntry.consumption, root.energyManager.totalConsumption, entry ? entry.totalConsumption : 0)
|
|
||||||
entries.unshift(liveEntry)
|
|
||||||
valueAxis.adjustMax(liveEntry.consumption)
|
|
||||||
valueAxis.adjustMax(liveEntry.production)
|
|
||||||
valueAxis.adjustMax(liveEntry.acquisition)
|
|
||||||
valueAxis.adjustMax(liveEntry.returned)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the actual entry
|
|
||||||
var graphEntry = {
|
|
||||||
consumption: 0,
|
|
||||||
production: 0,
|
|
||||||
acquisition: 0,
|
|
||||||
returned: 0
|
|
||||||
}
|
|
||||||
var labelTime = new Date();
|
|
||||||
if (entry) {
|
|
||||||
// print("Have entry:", entry.timestamp, config.toLabel(entry.timestamp))
|
|
||||||
var previous = powerBalanceLogs.get(powerBalanceLogs.count - i - 2)
|
|
||||||
if (previous) {
|
|
||||||
graphEntry.consumption = entry.totalConsumption - previous.totalConsumption
|
|
||||||
graphEntry.production = entry.totalProduction - previous.totalProduction
|
|
||||||
graphEntry.acquisition = entry.totalAcquisition - previous.totalAcquisition
|
|
||||||
graphEntry.returned = entry.totalReturn - previous.totalReturn
|
|
||||||
} else {
|
|
||||||
graphEntry.consumption = entry.totalConsumption
|
|
||||||
graphEntry.production = entry.totalProduction
|
|
||||||
graphEntry.acquisition = entry.totalAcquisition
|
|
||||||
graphEntry.returned = entry.totalReturn
|
|
||||||
}
|
|
||||||
labelTime = entry.timestamp
|
|
||||||
} else {
|
|
||||||
labelTime = calculateSampleStart(newestLogTimestamp, config.sampleRate, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print("Adding entry:", labelTime, graphEntry.consumption, config.toLabel(labelTime))
|
|
||||||
entries.unshift(graphEntry)
|
|
||||||
labels.unshift(labelTime)
|
|
||||||
|
|
||||||
// Given we've added 2 entries for the first run but only one label, we'll add the missing label
|
|
||||||
// at the end. This will shift the labels by one entries but that's ok because the logs timestamp
|
|
||||||
// is when the sample was created, but for the user it's better to show the the consumption values
|
|
||||||
// *during* that sample, not *before* the sample
|
|
||||||
if (i == config.count - 1) {
|
|
||||||
labelTime = new Date(labelTime.getTime() - config.sampleRate * 60000)
|
|
||||||
// print("Adding oldest entry label", labelTime, config.sampleRate, config.toLabel(labelTime))
|
|
||||||
labels.unshift(labelTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
valueAxis.adjustMax(graphEntry.consumption)
|
|
||||||
valueAxis.adjustMax(graphEntry.production)
|
|
||||||
valueAxis.adjustMax(graphEntry.acquisition)
|
|
||||||
valueAxis.adjustMax(graphEntry.returned)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print("assigning categories:", labels)
|
|
||||||
categoryAxis.timestamps = labels
|
|
||||||
|
|
||||||
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
|
|
||||||
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
|
||||||
print("Appending to set", JSON.stringify(entries[i]))
|
|
||||||
if (root.hasProducers) {
|
|
||||||
d.consumptionSet.append(entries[i].consumption)
|
|
||||||
d.productionSet.append(entries[i].production)
|
|
||||||
}
|
|
||||||
d.acquisitionSet.append(entries[i].acquisition)
|
|
||||||
d.returnSet.append(entries[i].returned)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntryAdded: {
|
onEntriesAdded: {
|
||||||
if (fetchingData) {
|
if (fetchingData) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Update the timeline by faking a left/right scroll
|
||||||
// print("Entry added")
|
d.startOffset--
|
||||||
var config = root.configs[selectionTabs.currentValue.config]
|
d.startOffset++
|
||||||
|
//d.refresh()
|
||||||
|
|
||||||
var start = entry
|
|
||||||
var consumptionValue = root.energyManager.totalConsumption - (start ? start.totalConsumption : 0)
|
|
||||||
var productionValue = root.energyManager.totalProduction - (start ? start.totalProduction : 0)
|
|
||||||
var acquisitionValue = root.energyManager.totalAcquisition - (start ? start.totalAcquisition : 0)
|
|
||||||
var returnValue = root.energyManager.totalReturn - (start ? start.totalReturn : 0)
|
|
||||||
// print("Entry added:", entry.timestamp, entry.totalConsumption, consumptionValue)
|
|
||||||
|
|
||||||
chartView.animationOptions = ChartView.NoAnimation
|
|
||||||
|
|
||||||
var timestamps = categoryAxis.timestamps;
|
|
||||||
timestamps.push(entry.timestamp)
|
|
||||||
timestamps.splice(0, 1)
|
|
||||||
categoryAxis.timestamps = timestamps
|
|
||||||
|
|
||||||
if (root.hasProducers) {
|
|
||||||
d.consumptionSet.remove(0, 1);
|
|
||||||
d.consumptionSet.append(consumptionValue)
|
|
||||||
d.productionSet.remove(0, 1);
|
|
||||||
d.productionSet.append(productionValue)
|
|
||||||
}
|
|
||||||
d.acquisitionSet.remove(0, 1);
|
|
||||||
d.acquisitionSet.append(acquisitionValue)
|
|
||||||
d.returnSet.remove(0, 1);
|
|
||||||
d.returnSet.append(returnValue)
|
|
||||||
|
|
||||||
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
ChartView {
|
|
||||||
id: chartView
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
animationOptions: ChartView.NoAnimation
|
|
||||||
|
|
||||||
backgroundColor: "transparent"
|
Label {
|
||||||
legend.alignment: Qt.AlignBottom
|
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
legend.font: Style.extraSmallFont
|
y: chartView.y + chartView.plotArea.y + Style.smallMargins
|
||||||
legend.labelColor: Style.foregroundColor
|
text: d.config.toRangeLabel(d.startTime)
|
||||||
|
font: Style.smallFont
|
||||||
|
opacity: d.startOffset < -d.config.count ? .5 : 0
|
||||||
|
Behavior on opacity { NumberAnimation {} }
|
||||||
|
}
|
||||||
|
|
||||||
// margins.left: 0
|
ChartView {
|
||||||
margins.right: 0
|
id: chartView
|
||||||
margins.bottom: 0
|
animationOptions: ChartView.NoAnimation
|
||||||
margins.top: 0
|
anchors.fill: parent
|
||||||
|
|
||||||
function reset() {
|
backgroundColor: "transparent"
|
||||||
barSeries.clear();
|
legend.alignment: Qt.AlignBottom
|
||||||
valueAxis.max = 0
|
legend.font: Style.extraSmallFont
|
||||||
if (root.hasProducers) {
|
legend.labelColor: Style.foregroundColor
|
||||||
d.consumptionSet = barSeries.append(qsTr("Consumed"), [])
|
|
||||||
d.consumptionSet.color = Style.blue
|
// margins.left: 0
|
||||||
d.consumptionSet.borderColor = d.consumptionSet.color
|
margins.right: 0
|
||||||
d.consumptionSet.borderWidth = 0
|
margins.bottom: 0
|
||||||
d.productionSet = barSeries.append(qsTr("Produced"), [])
|
margins.top: 0
|
||||||
d.productionSet.color = Style.yellow
|
|
||||||
d.productionSet.borderColor = d.productionSet.color
|
ActivityIndicator {
|
||||||
d.productionSet.borderWidth = 0
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
visible: powerBalanceLogs.fetchingData
|
||||||
|
opacity: .5
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
text: qsTr("No data available")
|
||||||
|
visible: !powerBalanceLogs.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.endTime) && d.startOffset != 0
|
||||||
|
font: Style.smallFont
|
||||||
|
opacity: .5
|
||||||
|
Behavior on opacity { NumberAnimation {}}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: labelsLayout
|
||||||
|
x: Style.smallMargins
|
||||||
|
y: chartView.plotArea.y
|
||||||
|
height: chartView.plotArea.height
|
||||||
|
width: chartView.plotArea.x - x
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: valueAxis.tickCount
|
||||||
|
delegate: Label {
|
||||||
|
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
||||||
|
width: parent.width - Style.smallMargins
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BarSeries {
|
||||||
|
id: barSeries
|
||||||
|
axisX: BarCategoryAxis {
|
||||||
|
id: categoryAxis
|
||||||
|
labelsColor: Style.foregroundColor
|
||||||
|
labelsFont: Style.extraSmallFont
|
||||||
|
gridVisible: false
|
||||||
|
gridLineColor: Style.tileOverlayColor
|
||||||
|
lineVisible: false
|
||||||
|
titleVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
|
||||||
|
categories: {
|
||||||
|
var ret = []
|
||||||
|
print("Updating categories from", d.config.startTime())
|
||||||
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i);
|
||||||
|
print("*** adding", timestamp, d.startOffset, i)
|
||||||
|
ret.push(d.config.toLabel(timestamp))
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
axisY: ValueAxis {
|
||||||
|
id: valueAxis
|
||||||
|
min: 0
|
||||||
|
gridLineColor: Style.tileOverlayColor
|
||||||
|
labelsVisible: false
|
||||||
|
labelsColor: Style.foregroundColor
|
||||||
|
labelsFont: Style.extraSmallFont
|
||||||
|
lineVisible: false
|
||||||
|
titleVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
|
||||||
|
function adjustMax(newValue) {
|
||||||
|
if (max < newValue) {
|
||||||
|
print("adjusting to new max", newValue)
|
||||||
|
max = newValue // Math.ceil(newValue / 100) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BarSet {
|
||||||
|
id: consumptionSet
|
||||||
|
label: qsTr("Consumed")
|
||||||
|
color: Style.blue
|
||||||
|
borderColor: color
|
||||||
|
borderWidth: 0
|
||||||
|
values: {
|
||||||
|
var ret = []
|
||||||
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
ret.push(0)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BarSet {
|
||||||
|
id: productionSet
|
||||||
|
label: qsTr("Produced")
|
||||||
|
color: Style.yellow
|
||||||
|
borderColor: color
|
||||||
|
borderWidth: 0
|
||||||
|
values: {
|
||||||
|
var ret = []
|
||||||
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
ret.push(0)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BarSet {
|
||||||
|
id: acquisitionSet
|
||||||
|
label: qsTr("From grid")
|
||||||
|
color: Style.red
|
||||||
|
borderColor: color
|
||||||
|
borderWidth: 0
|
||||||
|
values: {
|
||||||
|
var ret = []
|
||||||
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
ret.push(0)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BarSet {
|
||||||
|
id: returnSet
|
||||||
|
label: qsTr("To grid")
|
||||||
|
color: Style.green
|
||||||
|
borderColor: color
|
||||||
|
borderWidth: 0
|
||||||
|
values: {
|
||||||
|
var ret = []
|
||||||
|
for (var i = 0; i < d.config.count; i++) {
|
||||||
|
ret.push(0)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
d.acquisitionSet = barSeries.append(qsTr("From grid"), [])
|
|
||||||
d.acquisitionSet.color = Style.red
|
|
||||||
d.acquisitionSet.borderColor = d.acquisitionSet.color
|
|
||||||
d.acquisitionSet.borderWidth = 0
|
|
||||||
d.returnSet = barSeries.append(qsTr("To grid"), [])
|
|
||||||
d.returnSet.color = Style.green
|
|
||||||
d.returnSet.borderColor = d.returnSet.color
|
|
||||||
d.returnSet.borderWidth = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: labelsLayout
|
anchors.fill: parent
|
||||||
x: Style.smallMargins
|
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
||||||
y: chartView.plotArea.y
|
anchors.topMargin: chartView.y + chartView.plotArea.y
|
||||||
height: chartView.plotArea.height
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||||
width: chartView.plotArea.x - x
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||||
|
z: -1
|
||||||
|
|
||||||
Repeater {
|
Rectangle {
|
||||||
model: valueAxis.tickCount
|
height: parent.height + Style.margins * 2
|
||||||
delegate: Label {
|
y: -Style.smallMargins
|
||||||
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
radius: Style.smallCornerRadius
|
||||||
width: parent.width - Style.smallMargins
|
width: chartView.plotArea.width / categoryAxis.count
|
||||||
horizontalAlignment: Text.AlignRight
|
color: Style.tileBackgroundColor
|
||||||
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
|
property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1)
|
||||||
verticalAlignment: Text.AlignTop
|
visible: toolTip.visible
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
x: idx * parent.width / categoryAxis.count
|
||||||
|
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BarSeries {
|
MouseArea {
|
||||||
id: barSeries
|
id: mouseArea
|
||||||
axisX: BarCategoryAxis {
|
anchors.fill: parent
|
||||||
id: categoryAxis
|
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
||||||
labelsColor: Style.foregroundColor
|
anchors.topMargin: chartView.y + chartView.plotArea.y
|
||||||
labelsFont: Style.extraSmallFont
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||||
gridVisible: false
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||||
gridLineColor: Style.tileOverlayColor
|
|
||||||
lineVisible: false
|
|
||||||
titleVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
|
|
||||||
categories: {
|
hoverEnabled: true
|
||||||
var ret = []
|
preventStealing: tooltipping || dragging
|
||||||
for (var i = 0; i < timestamps.length; i++) {
|
|
||||||
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
property var timestamps: []
|
property int startMouseX: 0
|
||||||
|
property bool dragging: false
|
||||||
|
property bool tooltipping: false
|
||||||
|
property int dragStartOffset: 0
|
||||||
|
|
||||||
}
|
Timer {
|
||||||
axisY: ValueAxis {
|
interval: 300
|
||||||
id: valueAxis
|
running: mouseArea.pressed
|
||||||
min: 0
|
onTriggered: {
|
||||||
gridLineColor: Style.tileOverlayColor
|
if (!mouseArea.dragging) {
|
||||||
labelsVisible: false
|
mouseArea.tooltipping = true
|
||||||
labelsColor: Style.foregroundColor
|
|
||||||
labelsFont: Style.extraSmallFont
|
|
||||||
lineVisible: false
|
|
||||||
titleVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
|
|
||||||
function adjustMax(newValue) {
|
|
||||||
if (max < newValue) {
|
|
||||||
max = newValue // Math.ceil(newValue / 100) * 100
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
onReleased: {
|
||||||
anchors.fill: parent
|
if (mouseArea.dragging) {
|
||||||
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
powerBalanceLogs.fetchLogs()
|
||||||
anchors.topMargin: chartView.y + chartView.plotArea.y
|
mouseArea.dragging = false;
|
||||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
}
|
||||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
||||||
z: -1
|
|
||||||
|
|
||||||
Rectangle {
|
mouseArea.tooltipping = false;
|
||||||
height: parent.height + Style.margins * 2
|
|
||||||
y: -Style.smallMargins
|
|
||||||
radius: Style.smallCornerRadius
|
|
||||||
width: chartView.plotArea.width / categoryAxis.count
|
|
||||||
color: Style.tileBackgroundColor
|
|
||||||
property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1)
|
|
||||||
visible: toolTip.visible
|
|
||||||
|
|
||||||
x: idx * parent.width / categoryAxis.count
|
|
||||||
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
|
||||||
anchors.topMargin: chartView.y + chartView.plotArea.y
|
|
||||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
||||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
||||||
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 300
|
|
||||||
running: mouseArea.pressed
|
|
||||||
onTriggered: mouseArea.preventStealing = true
|
|
||||||
}
|
|
||||||
onReleased: mouseArea.preventStealing = false
|
|
||||||
|
|
||||||
NymeaToolTip {
|
|
||||||
id: toolTip
|
|
||||||
|
|
||||||
backgroundItem: chartView
|
|
||||||
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
|
|
||||||
|
|
||||||
property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1)
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
|
|
||||||
property int chartWidth: chartView.plotArea.width
|
|
||||||
property int barWidth: chartWidth / categoryAxis.count
|
|
||||||
|
|
||||||
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
|
|
||||||
idx * barWidth + barWidth + Style.smallMargins
|
|
||||||
: idx * barWidth - Style.smallMargins - width
|
|
||||||
property double setMaxValue: Math.max(d.consumptionSet ? d.consumptionSet.at(idx) : 0,
|
|
||||||
d.productionSet ? d.productionSet.at(idx) : 0,
|
|
||||||
d.acquisitionSet ? d.acquisitionSet.at(idx) : 0,
|
|
||||||
d.returnSet ? d.returnSet.at(idx) : 0)
|
|
||||||
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
|
||||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
|
||||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: tooltipLayout
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
top: parent.top
|
|
||||||
margins: Style.smallMargins
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
onPressed: {
|
||||||
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
|
startMouseX = mouseX
|
||||||
font: Style.smallFont
|
dragStartOffset = d.startOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
onDoubleClicked: {
|
||||||
visible: root.hasProducers
|
var idx = Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
|
||||||
Rectangle {
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
|
||||||
width: Style.extraSmallFont.pixelSize
|
selectionTabs.currentIndex--
|
||||||
height: width
|
var startTime = d.config.startTime()
|
||||||
color: Style.blue
|
d.startOffset = (timestamp.getTime() - startTime.getTime()) / (d.config.sampleRate * 60 * 1000)
|
||||||
}
|
powerBalanceLogs.fetchLogs();
|
||||||
Label {
|
|
||||||
text: toolTip.visible && d.consumptionSet ? qsTr("Consumed: %1 kWh").arg(d.consumptionSet.at(toolTip.idx).toFixed(2)) : ""
|
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
visible: root.hasProducers
|
onMouseXChanged: {
|
||||||
Rectangle {
|
if (!pressed || mouseArea.tooltipping) {
|
||||||
width: Style.extraSmallFont.pixelSize
|
return;
|
||||||
height: width
|
|
||||||
color: Style.yellow
|
|
||||||
}
|
}
|
||||||
Label {
|
if (Math.abs(startMouseX - mouseX) < 10) {
|
||||||
text: toolTip.visible && d.productionSet ? qsTr("Produced: %1 kWh").arg(d.productionSet.at(toolTip.idx).toFixed(2)) : ""
|
return;
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
}
|
||||||
|
dragging = true
|
||||||
|
|
||||||
|
var dragDelta = startMouseX - mouseX
|
||||||
|
var slotWidth = mouseArea.width / d.config.count
|
||||||
|
var offset = Math.floor(dragDelta / slotWidth);
|
||||||
|
d.startOffset = Math.min(dragStartOffset + offset, 0)
|
||||||
|
d.fetchPending = true;
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
Rectangle {
|
property int wheelDelta: 0
|
||||||
width: Style.extraSmallFont.pixelSize
|
onWheel: {
|
||||||
height: width
|
wheelDelta += wheel.pixelDelta.x
|
||||||
color: Style.red
|
var slotWidth = mouseArea.width / d.config.count
|
||||||
|
while (wheelDelta > slotWidth) {
|
||||||
|
d.startOffset--
|
||||||
|
wheelDelta -= slotWidth
|
||||||
}
|
}
|
||||||
Label {
|
while (wheelDelta < -slotWidth) {
|
||||||
text: toolTip.visible && d.acquisitionSet ? qsTr("From grid: %1 kWh").arg(d.acquisitionSet.at(toolTip.idx).toFixed(2)) : ""
|
d.startOffset = Math.min(d.startOffset + 1, 0)
|
||||||
font: Style.extraSmallFont
|
wheelDelta += slotWidth
|
||||||
}
|
}
|
||||||
|
d.fetchPending = true;
|
||||||
|
wheelStopTimer.restart()
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
Rectangle {
|
Timer {
|
||||||
width: Style.extraSmallFont.pixelSize
|
id: wheelStopTimer
|
||||||
height: width
|
interval: 300
|
||||||
color: Style.green
|
repeat: false
|
||||||
}
|
onTriggered: powerBalanceLogs.fetchLogs()
|
||||||
Label {
|
}
|
||||||
text: toolTip.visible && d.returnSet ? qsTr("To grid: %1 kWh").arg(d.returnSet.at(toolTip.idx).toFixed(2)) : ""
|
|
||||||
font: Style.extraSmallFont
|
NymeaToolTip {
|
||||||
|
id: toolTip
|
||||||
|
|
||||||
|
backgroundItem: chartView
|
||||||
|
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
|
||||||
|
|
||||||
|
property int idx: Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
|
||||||
|
property date timestamp: root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
|
||||||
|
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
|
||||||
|
property int chartWidth: chartView.plotArea.width
|
||||||
|
property int barWidth: chartWidth / categoryAxis.count
|
||||||
|
|
||||||
|
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
|
||||||
|
idx * barWidth + barWidth + Style.smallMargins
|
||||||
|
: idx * barWidth - Style.smallMargins - width
|
||||||
|
property double setMaxValue: d.startOffset !== undefined ? Math.max(consumptionSet.at(idx),
|
||||||
|
productionSet.at(idx),
|
||||||
|
acquisitionSet.at(idx),
|
||||||
|
returnSet.at(idx)) : 0
|
||||||
|
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
||||||
|
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||||
|
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: tooltipLayout
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
margins: Style.smallMargins
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: d.config.toLongLabel(toolTip.timestamp)
|
||||||
|
font: Style.smallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
visible: root.hasProducers
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.blue
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: d.startOffset !== undefined ? qsTr("Consumed: %1 kWh").arg(consumptionSet.at(toolTip.idx).toFixed(2)) : ""
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
visible: root.hasProducers
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.yellow
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: d.startOffset !== undefined ? qsTr("Produced: %1 kWh").arg(productionSet.at(toolTip.idx).toFixed(2)) : ""
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.red
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: d.startOffset !== undefined ? qsTr("From grid: %1 kWh").arg(acquisitionSet.at(toolTip.idx).toFixed(2)) :""
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.green
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: d.startOffset !== undefined ? qsTr("To grid: %1 kWh").arg(returnSet.at(toolTip.idx).toFixed(2)) : ""
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,18 +2,19 @@ import QtQuick 2.0
|
|||||||
import QtCharts 2.2
|
import QtCharts 2.2
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtGraphicalEffects 1.0
|
|
||||||
import Nymea 1.0
|
import Nymea 1.0
|
||||||
import "qrc:/ui/components"
|
import "qrc:/ui/components"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property PowerBalanceLogs energyLogs: PowerBalanceLogs {
|
PowerBalanceLogs {
|
||||||
id: powerBalanceLogs
|
id: powerBalanceLogs
|
||||||
engine: _engine
|
engine: _engine
|
||||||
startTime: dateTimeAxis.min
|
startTime: new Date(d.startTime.getTime() - (d.range * 60 * 1000))
|
||||||
sampleRate: EnergyLogs.SampleRate15Mins
|
endTime: new Date(d.endTime.getTime() + (d.range * 60 * 1000))
|
||||||
|
sampleRate: d.sampleRate
|
||||||
|
Component.onCompleted: fetchLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
property ThingsProxy batteries: ThingsProxy {
|
property ThingsProxy batteries: ThingsProxy {
|
||||||
@ -21,350 +22,552 @@ Item {
|
|||||||
shownInterfaces: ["energystorage"]
|
shownInterfaces: ["energystorage"]
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
QtObject {
|
||||||
for (var i = 0; i < powerBalanceLogs.count; i++) {
|
id: d
|
||||||
var entry = energyLogs.powerBalanceLogs.get(i);
|
property date now: new Date()
|
||||||
consumptionSeries.addEntry(entry)
|
|
||||||
selfProductionSeries.addEntry(entry)
|
readonly property int range: selectionTabs.currentValue.range
|
||||||
storageSeries.addEntry(entry)
|
readonly property int sampleRate: selectionTabs.currentValue.sampleRate
|
||||||
acquisitionSeries.addEntry(entry)
|
readonly property int visibleValues: range / sampleRate
|
||||||
|
|
||||||
|
readonly property var startTime: {
|
||||||
|
var date = new Date(now);
|
||||||
|
date.setTime(date.getTime() - (range * 60 * 1000) + 2000);
|
||||||
|
print("setting starttime to", date, range)
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var endTime: {
|
||||||
|
var date = new Date(now);
|
||||||
|
date.setTime(date.getTime() + 2000)
|
||||||
|
print("setting endtime to", date, range)
|
||||||
|
return date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: powerBalanceLogs
|
target: powerBalanceLogs
|
||||||
onEntryAdded: {
|
|
||||||
consumptionSeries.addEntry(entry)
|
|
||||||
selfProductionSeries.addEntry(entry)
|
|
||||||
storageSeries.addEntry(entry)
|
|
||||||
acquisitionSeries.addEntry(entry)
|
|
||||||
|
|
||||||
if (dateTimeAxis.now < entry.timestamp) {
|
onEntriesAdded: {
|
||||||
dateTimeAxis.now = entry.timestamp
|
// print("entries added", index, entries.length)
|
||||||
zeroSeries.update(entry.timestamp)
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var entry = entries[i]
|
||||||
|
// print("got entry", entry.timestamp)
|
||||||
|
|
||||||
|
zeroSeries.ensureValue(entry.timestamp)
|
||||||
|
// For debugging, to see if the other maths line up with the plain production graph
|
||||||
|
// consumptionSeries.insertEntry(index + i, entry)
|
||||||
|
selfProductionSeries.insertEntry(index + i, entry)
|
||||||
|
storageSeries.insertEntry(index + i, entry)
|
||||||
|
acquisitionSeries.insertEntry(index + i, entry)
|
||||||
|
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
|
||||||
|
d.now = entry.timestamp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEntriesRemoved: {
|
||||||
|
acquisitionUpperSeries.removePoints(index, count)
|
||||||
|
storageUpperSeries.removePoints(index, count)
|
||||||
|
selfProductionUpperSeries.removePoints(index, count)
|
||||||
|
consumptionUpperSeries.removePoints(index, count)
|
||||||
|
zeroSeries.shrink()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
ColumnLayout {
|
||||||
interval: 60000
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
var now = new Date()
|
|
||||||
if (dateTimeAxis.now < now) {
|
|
||||||
dateTimeAxis.now = now
|
|
||||||
zeroSeries.update(now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartView {
|
|
||||||
id: chartView
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
backgroundColor: "transparent"
|
spacing: 0
|
||||||
margins.left: 0
|
|
||||||
margins.right: 0
|
|
||||||
margins.bottom: 0
|
|
||||||
margins.top: 0
|
|
||||||
|
|
||||||
title: qsTr("My consumption history")
|
Label {
|
||||||
titleColor: Style.foregroundColor
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: Style.smallMargins
|
||||||
legend.alignment: Qt.AlignBottom
|
horizontalAlignment: Text.AlignHCenter
|
||||||
legend.labelColor: Style.foregroundColor
|
text: qsTr("My consumption history")
|
||||||
legend.font: Style.extraSmallFont
|
}
|
||||||
|
|
||||||
|
|
||||||
ValueAxis {
|
|
||||||
id: valueAxis
|
|
||||||
min: 0
|
|
||||||
max: Math.ceil(powerBalanceLogs.maxValue / 1000) * 1000
|
|
||||||
labelFormat: ""
|
|
||||||
gridLineColor: Style.tileOverlayColor
|
|
||||||
labelsVisible: false
|
|
||||||
lineVisible: false
|
|
||||||
titleVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
// visible: false
|
|
||||||
|
|
||||||
|
SelectionTabs {
|
||||||
|
id: selectionTabs
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Style.smallMargins
|
||||||
|
Layout.rightMargin: Style.smallMargins
|
||||||
|
currentIndex: 1
|
||||||
|
model: ListModel {
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Hours")
|
||||||
|
sampleRate: EnergyLogs.SampleRate1Min
|
||||||
|
range: 180 // 3 Hours: 3 * 60
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Days")
|
||||||
|
sampleRate: EnergyLogs.SampleRate15Mins
|
||||||
|
range: 1440 // 1 Day: 24 * 60
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Weeks")
|
||||||
|
sampleRate: EnergyLogs.SampleRate1Hour
|
||||||
|
range: 10080 // 7 Days: 7 * 24 * 60
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Months")
|
||||||
|
sampleRate: EnergyLogs.SampleRate3Hours
|
||||||
|
range: 43200 // 30 Days: 30 * 24 * 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTabSelected: {
|
||||||
|
d.now = new Date()
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: labelsLayout
|
Layout.fillWidth: true
|
||||||
x: Style.smallMargins
|
Layout.fillHeight: true
|
||||||
y: chartView.plotArea.y
|
|
||||||
height: chartView.plotArea.height
|
Label {
|
||||||
width: chartView.plotArea.x - x
|
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
Repeater {
|
y: chartView.y + chartView.plotArea.y + Style.smallMargins
|
||||||
model: valueAxis.tickCount
|
text: d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
|
||||||
delegate: Label {
|
font: Style.smallFont
|
||||||
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
|
||||||
width: parent.width - Style.smallMargins
|
Behavior on opacity { NumberAnimation {} }
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
|
|
||||||
verticalAlignment: Text.AlignTop
|
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
ChartView {
|
||||||
|
id: chartView
|
||||||
|
anchors.fill: parent
|
||||||
|
backgroundColor: "transparent"
|
||||||
|
margins.left: 0
|
||||||
|
margins.right: 0
|
||||||
|
margins.bottom: 0
|
||||||
|
margins.top: 0
|
||||||
|
|
||||||
DateTimeAxis {
|
legend.alignment: Qt.AlignBottom
|
||||||
id: dateTimeAxis
|
legend.labelColor: Style.foregroundColor
|
||||||
property date now: new Date()
|
legend.font: Style.extraSmallFont
|
||||||
min: {
|
|
||||||
var date = new Date(now);
|
|
||||||
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000);
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
max: {
|
|
||||||
var date = new Date(now);
|
|
||||||
date.setTime(date.getTime() + 2000)
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
format: "hh:mm"
|
|
||||||
labelsFont: Style.extraSmallFont
|
|
||||||
gridVisible: false
|
|
||||||
minorGridVisible: false
|
|
||||||
lineVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
labelsColor: Style.foregroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
// For debugging, to see the total graph and check if the other maths line up
|
ActivityIndicator {
|
||||||
AreaSeries {
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
id: consumptionSeries
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
axisX: dateTimeAxis
|
visible: powerBalanceLogs.fetchingData
|
||||||
axisY: valueAxis
|
opacity: .5
|
||||||
color: "blue"
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
opacity: .5
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
lowerSeries: zeroSeries
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: consumptionUpperSeries
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
|
||||||
return entry.consumption
|
|
||||||
}
|
|
||||||
function addEntry(entry) {
|
|
||||||
consumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AreaSeries {
|
|
||||||
id: selfProductionSeries
|
|
||||||
axisX: dateTimeAxis
|
|
||||||
axisY: valueAxis
|
|
||||||
color: Style.green
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
name: qsTr("Self production")
|
|
||||||
// visible: false
|
|
||||||
|
|
||||||
lowerSeries: LineSeries {
|
|
||||||
id: zeroSeries
|
|
||||||
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
|
|
||||||
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
|
|
||||||
function update(timestamp) {
|
|
||||||
append(timestamp, 0);
|
|
||||||
removePoints(1,1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: selfProductionUpperSeries
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
|
||||||
var value = entry.consumption - Math.max(0, entry.acquisition);
|
|
||||||
if (entry.storage < 0) {
|
|
||||||
value += entry.storage;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEntry(entry) {
|
|
||||||
selfProductionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AreaSeries {
|
|
||||||
id: storageSeries
|
|
||||||
axisX: dateTimeAxis
|
|
||||||
axisY: valueAxis
|
|
||||||
color: Style.orange
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
name: qsTr("From battery")
|
|
||||||
visible: root.batteries.count > 0
|
|
||||||
|
|
||||||
lowerSeries: selfProductionUpperSeries
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: storageUpperSeries
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
|
||||||
return selfProductionSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.storage));
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEntry(entry) {
|
|
||||||
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AreaSeries {
|
|
||||||
id: acquisitionSeries
|
|
||||||
axisX: dateTimeAxis
|
|
||||||
axisY: valueAxis
|
|
||||||
color: Style.red
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
name: qsTr("From grid")
|
|
||||||
// visible: false
|
|
||||||
|
|
||||||
lowerSeries: storageUpperSeries
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: acquisitionUpperSeries
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
|
||||||
return storageSeries.calculateValue(entry) + Math.max(0, entry.acquisition)
|
|
||||||
}
|
|
||||||
function addEntry(entry) {
|
|
||||||
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: chartView
|
|
||||||
anchors.leftMargin: chartView.plotArea.x
|
|
||||||
anchors.topMargin: chartView.plotArea.y
|
|
||||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
||||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
||||||
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 300
|
|
||||||
running: mouseArea.pressed
|
|
||||||
onTriggered: mouseArea.preventStealing = true
|
|
||||||
}
|
|
||||||
onReleased: mouseArea.preventStealing = false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: parent.height
|
|
||||||
width: 1
|
|
||||||
color: Style.foregroundColor
|
|
||||||
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
NymeaToolTip {
|
|
||||||
id: toolTip
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
|
|
||||||
backgroundItem: chartView
|
|
||||||
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
|
|
||||||
|
|
||||||
|
|
||||||
property int idx: consumptionUpperSeries.count - (Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width))
|
|
||||||
property int seriesIndex: Math.min(consumptionUpperSeries.count - 1, Math.max(0, consumptionUpperSeries.count - idx))
|
|
||||||
|
|
||||||
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
|
|
||||||
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
|
|
||||||
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
|
||||||
property double maxValue: consumptionUpperSeries.at(seriesIndex).y
|
|
||||||
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
|
||||||
|
|
||||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
|
||||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: tooltipLayout
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
top: parent.top
|
|
||||||
margins: Style.smallMargins
|
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: new Date(consumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
text: qsTr("No data available")
|
||||||
|
visible: !powerBalanceLogs.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
|
||||||
font: Style.smallFont
|
font: Style.smallFont
|
||||||
|
opacity: .5
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
ValueAxis {
|
||||||
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y
|
id: valueAxis
|
||||||
property bool translate: value >= 1000
|
min: 0
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
max: Math.ceil(powerBalanceLogs.maxValue / 100) * 100
|
||||||
text: qsTr("Total consumption: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
labelFormat: ""
|
||||||
font: Style.extraSmallFont
|
gridLineColor: Style.tileOverlayColor
|
||||||
|
labelsVisible: false
|
||||||
|
lineVisible: false
|
||||||
|
titleVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
// visible: false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Item {
|
||||||
Rectangle {
|
id: labelsLayout
|
||||||
width: Style.extraSmallFont.pixelSize
|
x: Style.smallMargins
|
||||||
height: width
|
y: chartView.plotArea.y
|
||||||
color: Style.green
|
height: chartView.plotArea.height
|
||||||
|
width: chartView.plotArea.x - x
|
||||||
|
Repeater {
|
||||||
|
model: valueAxis.tickCount
|
||||||
|
delegate: Label {
|
||||||
|
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
||||||
|
width: parent.width - Style.smallMargins
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
}
|
||||||
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
|
||||||
Component.onCompleted: lowerSeries = selfProductionSeries.lowerSeries
|
|
||||||
property XYSeries lowerSeries: null
|
|
||||||
|
|
||||||
property double value: selfProductionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
|
DateTimeAxis {
|
||||||
property bool translate: value >= 1000
|
id: dateTimeAxis
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
min: d.startTime
|
||||||
text: qsTr("Self production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
max: d.endTime
|
||||||
font: Style.extraSmallFont
|
format: {
|
||||||
|
switch (selectionTabs.currentValue.sampleRate) {
|
||||||
|
case EnergyLogs.SampleRate1Min:
|
||||||
|
case EnergyLogs.SampleRate15Mins:
|
||||||
|
return "hh:mm"
|
||||||
|
case EnergyLogs.SampleRate1Hour:
|
||||||
|
case EnergyLogs.SampleRate3Hours:
|
||||||
|
case EnergyLogs.SampleRate1Day:
|
||||||
|
return "dd.MM."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tickCount: {
|
||||||
|
switch (selectionTabs.currentValue.sampleRate) {
|
||||||
|
case EnergyLogs.SampleRate1Min:
|
||||||
|
case EnergyLogs.SampleRate15Mins:
|
||||||
|
return root.width > 500 ? 13 : 7
|
||||||
|
case EnergyLogs.SampleRate1Hour:
|
||||||
|
return 7
|
||||||
|
case EnergyLogs.SampleRate3Hours:
|
||||||
|
case EnergyLogs.SampleRate1Day:
|
||||||
|
return root.width > 500 ? 12 : 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labelsFont: Style.extraSmallFont
|
||||||
|
gridVisible: false
|
||||||
|
minorGridVisible: false
|
||||||
|
lineVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
labelsColor: Style.foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// For debugging, to see the total graph and check if the other maths line up
|
||||||
|
AreaSeries {
|
||||||
|
id: consumptionSeries
|
||||||
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: "blue"
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
opacity: .5
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
lowerSeries: zeroSeries
|
||||||
|
upperSeries: LineSeries {
|
||||||
|
id: consumptionUpperSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateValue(entry) {
|
||||||
|
return entry.consumption
|
||||||
|
}
|
||||||
|
function addEntry(entry) {
|
||||||
|
consumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
function insertEntry(index, entry) {
|
||||||
|
consumptionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
|
|
||||||
|
AreaSeries {
|
||||||
|
id: selfProductionSeries
|
||||||
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: Style.green
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
name: qsTr("Self production")
|
||||||
|
// visible: false
|
||||||
|
|
||||||
|
lowerSeries: LineSeries {
|
||||||
|
id: zeroSeries
|
||||||
|
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
|
||||||
|
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
|
||||||
|
function ensureValue(timestamp) {
|
||||||
|
if (count == 0) {
|
||||||
|
append(timestamp, 0)
|
||||||
|
} else if (count == 1) {
|
||||||
|
if (timestamp.getTime() < at(0).x) {
|
||||||
|
insert(0, timestamp, 0)
|
||||||
|
} else {
|
||||||
|
append(timestamp, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (timestamp.getTime() < at(0).x) {
|
||||||
|
remove(0)
|
||||||
|
insert(0, timestamp, 0)
|
||||||
|
} else if (timestamp.getTime() > at(1).x) {
|
||||||
|
remove(1)
|
||||||
|
append(timestamp, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function shrink() {
|
||||||
|
clear();
|
||||||
|
if (powerBalanceLogs.count > 0) {
|
||||||
|
ensureValue(powerBalanceLogs.get(0).timestamp)
|
||||||
|
ensureValue(powerBalanceLogs.get(powerBalanceLogs.count-1).timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upperSeries: LineSeries {
|
||||||
|
id: selfProductionUpperSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateValue(entry) {
|
||||||
|
var value = entry.consumption - Math.max(0, entry.acquisition);
|
||||||
|
if (entry.storage < 0) {
|
||||||
|
value += entry.storage;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEntry(entry) {
|
||||||
|
selfProductionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
function insertEntry(index, entry) {
|
||||||
|
selfProductionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AreaSeries {
|
||||||
|
id: storageSeries
|
||||||
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: Style.orange
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
name: qsTr("From battery")
|
||||||
visible: root.batteries.count > 0
|
visible: root.batteries.count > 0
|
||||||
Rectangle {
|
|
||||||
width: Style.extraSmallFont.pixelSize
|
lowerSeries: selfProductionUpperSeries
|
||||||
height: width
|
upperSeries: LineSeries {
|
||||||
color: Style.orange
|
id: storageUpperSeries
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
function calculateValue(entry) {
|
||||||
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
return selfProductionSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.storage));
|
||||||
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
|
}
|
||||||
property XYSeries lowerSeries: null
|
|
||||||
|
|
||||||
property double value: storageUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
|
function addEntry(entry) {
|
||||||
property bool translate: value >= 1000
|
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
}
|
||||||
text: qsTr("From battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
function insertEntry(index, entry) {
|
||||||
font: Style.extraSmallFont
|
storageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
Rectangle {
|
|
||||||
width: Style.extraSmallFont.pixelSize
|
AreaSeries {
|
||||||
height: width
|
id: acquisitionSeries
|
||||||
color: Style.red
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: Style.red
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
name: qsTr("From grid")
|
||||||
|
// visible: false
|
||||||
|
|
||||||
|
lowerSeries: storageUpperSeries
|
||||||
|
upperSeries: LineSeries {
|
||||||
|
id: acquisitionUpperSeries
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
function calculateValue(entry) {
|
||||||
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
return storageSeries.calculateValue(entry) + Math.max(0, entry.acquisition)
|
||||||
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
|
}
|
||||||
property XYSeries lowerSeries: null
|
function addEntry(entry) {
|
||||||
|
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
function insertEntry(index, entry) {
|
||||||
|
acquisitionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
|
|
||||||
property bool translate: value >= 1000
|
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
MouseArea {
|
||||||
text: qsTr("From grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
id: mouseArea
|
||||||
font: Style.extraSmallFont
|
anchors.fill: chartView
|
||||||
|
anchors.leftMargin: chartView.plotArea.x
|
||||||
|
anchors.topMargin: chartView.plotArea.y
|
||||||
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||||
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: tooltipping || dragging
|
||||||
|
|
||||||
|
property int startMouseX: 0
|
||||||
|
property bool dragging: false
|
||||||
|
property bool tooltipping: false
|
||||||
|
|
||||||
|
property var startDatetime: null
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 300
|
||||||
|
running: mouseArea.pressed
|
||||||
|
onTriggered: {
|
||||||
|
if (!mouseArea.dragging) {
|
||||||
|
mouseArea.tooltipping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
if (mouseArea.dragging) {
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
mouseArea.dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseArea.tooltipping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: {
|
||||||
|
startMouseX = mouseX
|
||||||
|
startDatetime = d.now
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleClicked: {
|
||||||
|
if (selectionTabs.currentIndex == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
|
||||||
|
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
|
||||||
|
selectionTabs.currentIndex--
|
||||||
|
d.now = new Date(timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseXChanged: {
|
||||||
|
if (!pressed || mouseArea.tooltipping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Math.abs(startMouseX - mouseX) < 10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragging = true
|
||||||
|
|
||||||
|
var dragDelta = startMouseX - mouseX
|
||||||
|
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||||
|
// dragDelta : timeDelta = width : totalTime
|
||||||
|
var timeDelta = dragDelta * totalTime / mouseArea.width
|
||||||
|
print("dragging", dragDelta, totalTime, mouseArea.width)
|
||||||
|
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel: {
|
||||||
|
startDatetime = d.now
|
||||||
|
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||||
|
// pixelDelta : timeDelta = width : totalTime
|
||||||
|
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
|
||||||
|
print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
|
||||||
|
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
|
||||||
|
wheelStopTimer.restart()
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: wheelStopTimer
|
||||||
|
interval: 300
|
||||||
|
repeat: false
|
||||||
|
onTriggered: powerBalanceLogs.fetchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: parent.height
|
||||||
|
width: 1
|
||||||
|
color: Style.foregroundColor
|
||||||
|
x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
}
|
||||||
|
|
||||||
|
NymeaToolTip {
|
||||||
|
id: toolTip
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
|
||||||
|
backgroundItem: chartView
|
||||||
|
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
|
||||||
|
|
||||||
|
property int idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
|
||||||
|
property var timestamp: new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
|
||||||
|
property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
|
||||||
|
|
||||||
|
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
|
||||||
|
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
|
||||||
|
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
||||||
|
property double maxValue: toolTip.entry ? Math.max(0, entry.consumption) : 0
|
||||||
|
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
||||||
|
|
||||||
|
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||||
|
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: tooltipLayout
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
margins: Style.smallMargins
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
font: Style.smallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
property double value: toolTip.entry ? Math.max(0, toolTip.entry.consumption) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("Total consumption: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.green
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
||||||
|
Component.onCompleted: lowerSeries = selfProductionSeries.lowerSeries
|
||||||
|
property XYSeries lowerSeries: null
|
||||||
|
|
||||||
|
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.production) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("Self production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
visible: root.batteries.count > 0
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.orange
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
||||||
|
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
|
||||||
|
property XYSeries lowerSeries: null
|
||||||
|
|
||||||
|
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.storage) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("From battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.red
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
||||||
|
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
|
||||||
|
property XYSeries lowerSeries: null
|
||||||
|
|
||||||
|
property double value: toolTip.entry ? Math.max(0, toolTip.entry.acquisition) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("From grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,10 +8,13 @@ import "qrc:/ui/components"
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property PowerBalanceLogs energyLogs: PowerBalanceLogs {
|
PowerBalanceLogs {
|
||||||
id: powerBalanceLogs
|
id: powerBalanceLogs
|
||||||
engine: _engine
|
engine: _engine
|
||||||
startTime: dateTimeAxis.min
|
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||||
|
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||||
|
sampleRate: d.sampleRate
|
||||||
|
Component.onCompleted: fetchLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
property ThingsProxy batteries: ThingsProxy {
|
property ThingsProxy batteries: ThingsProxy {
|
||||||
@ -19,347 +22,550 @@ Item {
|
|||||||
shownInterfaces: ["energystorage"]
|
shownInterfaces: ["energystorage"]
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
QtObject {
|
||||||
for (var i = 0; i < powerBalanceLogs.count; i++) {
|
id: d
|
||||||
var entry = energyLogs.powerBalanceLogs.get(i);
|
property date now: new Date()
|
||||||
productionSeries.addEntry(entry)
|
|
||||||
selfConsumptionSeries.addEntry(entry)
|
readonly property int range: selectionTabs.currentValue.range
|
||||||
storageSeries.addEntry(entry)
|
readonly property int sampleRate: selectionTabs.currentValue.sampleRate
|
||||||
acquisitionSeries.addEntry(entry)
|
readonly property int visibleValues: range / sampleRate
|
||||||
|
|
||||||
|
readonly property var startTime: {
|
||||||
|
var date = new Date(now);
|
||||||
|
date.setTime(date.getTime() - range * 60000 + 2000);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var endTime: {
|
||||||
|
var date = new Date(now);
|
||||||
|
date.setTime(date.getTime() + 2000)
|
||||||
|
return date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: powerBalanceLogs
|
target: powerBalanceLogs
|
||||||
onEntryAdded: {
|
|
||||||
productionSeries.addEntry(entry)
|
|
||||||
selfConsumptionSeries.addEntry(entry)
|
|
||||||
storageSeries.addEntry(entry)
|
|
||||||
acquisitionSeries.addEntry(entry)
|
|
||||||
|
|
||||||
if (dateTimeAxis.now < entry.timestamp) {
|
onEntriesAdded: {
|
||||||
dateTimeAxis.now = entry.timestamp
|
// print("entries added", index, entries.length)
|
||||||
zeroSeries.update(entry.timestamp)
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var entry = entries[i]
|
||||||
|
// print("got entry", entry.timestamp)
|
||||||
|
|
||||||
|
zeroSeries.ensureValue(entry.timestamp)
|
||||||
|
// For debugging, to see if the other maths line up with the plain production graph
|
||||||
|
// productionSeries.insertEntry(index + i, entry)
|
||||||
|
selfConsumptionSeries.insertEntry(index + i, entry)
|
||||||
|
storageSeries.insertEntry(index + i, entry)
|
||||||
|
acquisitionSeries.insertEntry(index + i, entry)
|
||||||
|
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
|
||||||
|
d.now = entry.timestamp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onEntriesRemoved: {
|
||||||
|
acquisitionUpperSeries.removePoints(index, count)
|
||||||
|
storageUpperSeries.removePoints(index, count)
|
||||||
|
selfConsumptionUpperSeries.removePoints(index, count)
|
||||||
|
productionUpperSeries.removePoints(index, count)
|
||||||
|
zeroSeries.shrink()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
ColumnLayout {
|
||||||
interval: 60000
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
var now = new Date()
|
|
||||||
if (dateTimeAxis.now < now) {
|
|
||||||
dateTimeAxis.now = now
|
|
||||||
zeroSeries.update(now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartView {
|
|
||||||
id: chartView
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
backgroundColor: "transparent"
|
Label {
|
||||||
margins.left: 0
|
Layout.fillWidth: true
|
||||||
margins.right: 0
|
Layout.margins: Style.smallMargins
|
||||||
margins.bottom: 0
|
horizontalAlignment: Text.AlignHCenter
|
||||||
margins.top: 0
|
text: qsTr("My production history")
|
||||||
|
}
|
||||||
|
|
||||||
title: qsTr("My production history")
|
SelectionTabs {
|
||||||
titleColor: Style.foregroundColor
|
id: selectionTabs
|
||||||
|
Layout.fillWidth: true
|
||||||
legend.alignment: Qt.AlignBottom
|
Layout.leftMargin: Style.smallMargins
|
||||||
legend.labelColor: Style.foregroundColor
|
Layout.rightMargin: Style.smallMargins
|
||||||
legend.font: Style.extraSmallFont
|
currentIndex: 1
|
||||||
|
model: ListModel {
|
||||||
|
ListElement {
|
||||||
ValueAxis {
|
modelData: qsTr("Hours")
|
||||||
id: valueAxis
|
sampleRate: EnergyLogs.SampleRate1Min
|
||||||
min: 0
|
range: 180 // 3 Hours: 3 * 60
|
||||||
max: Math.ceil(-powerBalanceLogs.minValue / 1000) * 1000
|
}
|
||||||
labelFormat: ""
|
ListElement {
|
||||||
gridLineColor: Style.tileOverlayColor
|
modelData: qsTr("Days")
|
||||||
labelsVisible: false
|
sampleRate: EnergyLogs.SampleRate15Mins
|
||||||
lineVisible: false
|
range: 1440 // 1 Day: 24 * 60
|
||||||
titleVisible: false
|
}
|
||||||
shadesVisible: false
|
ListElement {
|
||||||
|
modelData: qsTr("Weeks")
|
||||||
|
sampleRate: EnergyLogs.SampleRate1Hour
|
||||||
|
range: 10080 // 7 Days: 7 * 24 * 60
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
modelData: qsTr("Months")
|
||||||
|
sampleRate: EnergyLogs.SampleRate3Hours
|
||||||
|
range: 43200 // 30 Days: 30 * 24 * 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTabSelected: {
|
||||||
|
d.now = new Date()
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: labelsLayout
|
Layout.fillWidth: true
|
||||||
x: Style.smallMargins
|
Layout.fillHeight: true
|
||||||
y: chartView.plotArea.y
|
|
||||||
height: chartView.plotArea.height
|
|
||||||
width: chartView.plotArea.x - x
|
|
||||||
Repeater {
|
|
||||||
model: valueAxis.tickCount
|
|
||||||
delegate: Label {
|
|
||||||
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
|
||||||
width: parent.width - Style.smallMargins
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
|
|
||||||
verticalAlignment: Text.AlignTop
|
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeAxis {
|
Label {
|
||||||
id: dateTimeAxis
|
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
property date now: new Date()
|
y: chartView.y + chartView.plotArea.y + Style.smallMargins
|
||||||
min: {
|
text: d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
|
||||||
var date = new Date(now);
|
font: Style.smallFont
|
||||||
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000);
|
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
|
||||||
return date;
|
Behavior on opacity { NumberAnimation {} }
|
||||||
}
|
|
||||||
max: {
|
|
||||||
var date = new Date(now);
|
|
||||||
date.setTime(date.getTime() + 2000)
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
format: "hh:mm"
|
|
||||||
labelsFont: Style.extraSmallFont
|
|
||||||
gridVisible: false
|
|
||||||
minorGridVisible: false
|
|
||||||
lineVisible: false
|
|
||||||
shadesVisible: false
|
|
||||||
labelsColor: Style.foregroundColor
|
|
||||||
}
|
|
||||||
|
|
||||||
// For debugging, to see if the other maths line up with the plain production graph
|
|
||||||
AreaSeries {
|
|
||||||
id: productionSeries
|
|
||||||
axisX: dateTimeAxis
|
|
||||||
axisY: valueAxis
|
|
||||||
color: "blue"
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
opacity: .5
|
|
||||||
name: "Total production"
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
|
||||||
return Math.abs(Math.min(0, entry.production))
|
|
||||||
}
|
|
||||||
function addEntry(entry) {
|
|
||||||
productionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lowerSeries: zeroSeries
|
ChartView {
|
||||||
upperSeries: LineSeries {
|
id: chartView
|
||||||
id: productionUpperSeries
|
anchors.fill: parent
|
||||||
}
|
backgroundColor: "transparent"
|
||||||
}
|
margins.left: 0
|
||||||
|
margins.right: 0
|
||||||
|
margins.bottom: 0
|
||||||
|
margins.top: 0
|
||||||
|
|
||||||
AreaSeries {
|
legend.alignment: Qt.AlignBottom
|
||||||
id: selfConsumptionSeries
|
legend.labelColor: Style.foregroundColor
|
||||||
axisX: dateTimeAxis
|
legend.font: Style.extraSmallFont
|
||||||
axisY: valueAxis
|
|
||||||
color: Style.red
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
name: qsTr("Consumed")
|
|
||||||
// visible: false
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
ActivityIndicator {
|
||||||
return Math.abs(Math.min(0, entry.production)) - Math.abs(Math.min(0, entry.acquisition)) - Math.max(0, entry.storage)
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
}
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
visible: powerBalanceLogs.fetchingData
|
||||||
function addEntry(entry) {
|
opacity: .5
|
||||||
selfConsumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
lowerSeries: LineSeries {
|
|
||||||
id: zeroSeries
|
|
||||||
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
|
|
||||||
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
|
|
||||||
function update(timestamp) {
|
|
||||||
append(timestamp, 0);
|
|
||||||
removePoints(1,1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: selfConsumptionUpperSeries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AreaSeries {
|
|
||||||
id: storageSeries
|
|
||||||
axisX: dateTimeAxis
|
|
||||||
axisY: valueAxis
|
|
||||||
color: Style.orange
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
visible: root.batteries.count > 0
|
|
||||||
name: qsTr("To battery")
|
|
||||||
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
|
||||||
return selfConsumptionSeries.calculateValue(entry) + Math.abs(Math.max(0, entry.storage));
|
|
||||||
}
|
|
||||||
|
|
||||||
function addEntry(entry) {
|
|
||||||
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
lowerSeries: selfConsumptionUpperSeries
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: storageUpperSeries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AreaSeries {
|
|
||||||
id: acquisitionSeries
|
|
||||||
axisX: dateTimeAxis
|
|
||||||
axisY: valueAxis
|
|
||||||
color: Style.green
|
|
||||||
borderWidth: 0
|
|
||||||
borderColor: color
|
|
||||||
name: qsTr("To grid")
|
|
||||||
// visible: false
|
|
||||||
|
|
||||||
function calculateValue(entry) {
|
|
||||||
return storageSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.acquisition))
|
|
||||||
}
|
|
||||||
function addEntry(entry) {
|
|
||||||
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
lowerSeries: storageUpperSeries
|
|
||||||
upperSeries: LineSeries {
|
|
||||||
id: acquisitionUpperSeries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: chartView.plotArea.x
|
|
||||||
anchors.topMargin: chartView.plotArea.y
|
|
||||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
||||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
||||||
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 300
|
|
||||||
running: mouseArea.pressed
|
|
||||||
onTriggered: mouseArea.preventStealing = true
|
|
||||||
}
|
|
||||||
onReleased: mouseArea.preventStealing = false
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: parent.height
|
|
||||||
width: 1
|
|
||||||
color: Style.foregroundColor
|
|
||||||
x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
}
|
|
||||||
|
|
||||||
NymeaToolTip {
|
|
||||||
id: toolTip
|
|
||||||
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
||||||
|
|
||||||
backgroundItem: chartView
|
|
||||||
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
|
|
||||||
|
|
||||||
property int idx: productionUpperSeries.count - Math.floor(mouseArea.mouseX * productionUpperSeries.count / mouseArea.width)
|
|
||||||
property int seriesIndex: Math.min(productionUpperSeries.count - 1, Math.max(0, productionUpperSeries.count - idx))
|
|
||||||
|
|
||||||
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
|
|
||||||
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
|
|
||||||
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
|
||||||
property double maxValue: productionUpperSeries.at(seriesIndex).y
|
|
||||||
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
|
||||||
|
|
||||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
|
||||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: tooltipLayout
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
top: parent.top
|
|
||||||
margins: Style.smallMargins
|
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
text: new Date(selfConsumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
|
||||||
|
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
|
||||||
|
text: qsTr("No data available")
|
||||||
|
visible: !powerBalanceLogs.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
|
||||||
font: Style.smallFont
|
font: Style.smallFont
|
||||||
|
opacity: .5
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
ValueAxis {
|
||||||
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y
|
id: valueAxis
|
||||||
property bool translate: value >= 1000
|
min: 0
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
max: Math.ceil(-powerBalanceLogs.minValue / 100) * 100
|
||||||
text: qsTr("Total production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
labelFormat: ""
|
||||||
font: Style.extraSmallFont
|
gridLineColor: Style.tileOverlayColor
|
||||||
|
labelsVisible: false
|
||||||
|
lineVisible: false
|
||||||
|
titleVisible: false
|
||||||
|
shadesVisible: false
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
RowLayout {
|
id: labelsLayout
|
||||||
Rectangle {
|
x: Style.smallMargins
|
||||||
width: Style.extraSmallFont.pixelSize
|
y: chartView.plotArea.y
|
||||||
height: width
|
height: chartView.plotArea.height
|
||||||
color: Style.red
|
width: chartView.plotArea.x - x
|
||||||
}
|
Repeater {
|
||||||
|
model: valueAxis.tickCount
|
||||||
Label {
|
delegate: Label {
|
||||||
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
||||||
Component.onCompleted: lowerSeries = selfConsumptionSeries.lowerSeries
|
width: parent.width - Style.smallMargins
|
||||||
property XYSeries lowerSeries: null
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
|
||||||
property double value: selfConsumptionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
|
verticalAlignment: Text.AlignTop
|
||||||
property bool translate: value >= 1000
|
font: Style.extraSmallFont
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
}
|
||||||
text: qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
|
DateTimeAxis {
|
||||||
|
id: dateTimeAxis
|
||||||
|
min: d.startTime
|
||||||
|
max: d.endTime
|
||||||
|
format: {
|
||||||
|
switch (selectionTabs.currentValue.sampleRate) {
|
||||||
|
case EnergyLogs.SampleRate1Min:
|
||||||
|
case EnergyLogs.SampleRate15Mins:
|
||||||
|
return "hh:mm"
|
||||||
|
case EnergyLogs.SampleRate1Hour:
|
||||||
|
case EnergyLogs.SampleRate3Hours:
|
||||||
|
case EnergyLogs.SampleRate1Day:
|
||||||
|
return "dd.MM."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tickCount: {
|
||||||
|
switch (selectionTabs.currentValue.sampleRate) {
|
||||||
|
case EnergyLogs.SampleRate1Min:
|
||||||
|
case EnergyLogs.SampleRate15Mins:
|
||||||
|
return root.width > 500 ? 13 : 7
|
||||||
|
case EnergyLogs.SampleRate1Hour:
|
||||||
|
return 7
|
||||||
|
case EnergyLogs.SampleRate3Hours:
|
||||||
|
case EnergyLogs.SampleRate1Day:
|
||||||
|
return root.width > 500 ? 12 : 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labelsFont: Style.extraSmallFont
|
||||||
|
gridVisible: false
|
||||||
|
minorGridVisible: false
|
||||||
|
lineVisible: false
|
||||||
|
shadesVisible: false
|
||||||
|
labelsColor: Style.foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// For debugging, to see if the other maths line up with the plain production graph
|
||||||
|
AreaSeries {
|
||||||
|
id: productionSeries
|
||||||
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: "blue"
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
opacity: .5
|
||||||
|
name: "Total production"
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
function calculateValue(entry) {
|
||||||
|
return Math.abs(Math.min(0, entry.production))
|
||||||
|
}
|
||||||
|
function addEntry(entry) {
|
||||||
|
productionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
function insertEntry(index, entry) {
|
||||||
|
productionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
lowerSeries: zeroSeries
|
||||||
|
upperSeries: LineSeries {
|
||||||
|
id: productionUpperSeries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AreaSeries {
|
||||||
|
id: selfConsumptionSeries
|
||||||
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: Style.red
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
name: qsTr("Consumed")
|
||||||
|
// visible: false
|
||||||
|
|
||||||
|
lowerSeries: LineSeries {
|
||||||
|
id: zeroSeries
|
||||||
|
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
|
||||||
|
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
|
||||||
|
function ensureValue(timestamp) {
|
||||||
|
if (count == 0) {
|
||||||
|
append(timestamp, 0)
|
||||||
|
} else if (count == 1) {
|
||||||
|
if (timestamp.getTime() < at(0).x) {
|
||||||
|
insert(0, timestamp, 0)
|
||||||
|
} else {
|
||||||
|
append(timestamp, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (timestamp.getTime() < at(0).x) {
|
||||||
|
remove(0)
|
||||||
|
insert(0, timestamp, 0)
|
||||||
|
} else if (timestamp.getTime() > at(1).x) {
|
||||||
|
remove(1)
|
||||||
|
append(timestamp, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function shrink() {
|
||||||
|
clear();
|
||||||
|
if (powerBalanceLogs.count > 0) {
|
||||||
|
ensureValue(powerBalanceLogs.get(0).timestamp)
|
||||||
|
ensureValue(powerBalanceLogs.get(powerBalanceLogs.count-1).timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upperSeries: LineSeries {
|
||||||
|
id: selfConsumptionUpperSeries
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function calculateValue(entry) {
|
||||||
|
return Math.max(0, -entry.production) - Math.max(0, -entry.acquisition) - Math.max(0, entry.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEntry(entry) {
|
||||||
|
selfConsumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
function insertEntry(index, entry) {
|
||||||
|
selfConsumptionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AreaSeries {
|
||||||
|
id: storageSeries
|
||||||
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: Style.orange
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
visible: root.batteries.count > 0
|
visible: root.batteries.count > 0
|
||||||
Rectangle {
|
name: qsTr("To battery")
|
||||||
width: Style.extraSmallFont.pixelSize
|
|
||||||
height: width
|
|
||||||
color: Style.orange
|
function calculateValue(entry) {
|
||||||
|
return selfConsumptionSeries.calculateValue(entry) + Math.max(0, entry.storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
function addEntry(entry) {
|
||||||
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
|
}
|
||||||
property XYSeries lowerSeries: null
|
function insertEntry(index, entry) {
|
||||||
|
storageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
|
||||||
property double value: storageUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
|
lowerSeries: selfConsumptionUpperSeries
|
||||||
property bool translate: value >= 1000
|
upperSeries: LineSeries {
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
id: storageUpperSeries
|
||||||
text: qsTr("To battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowLayout {
|
|
||||||
Rectangle {
|
|
||||||
width: Style.extraSmallFont.pixelSize
|
AreaSeries {
|
||||||
height: width
|
id: acquisitionSeries
|
||||||
color: Style.green
|
axisX: dateTimeAxis
|
||||||
|
axisY: valueAxis
|
||||||
|
color: Style.green
|
||||||
|
borderWidth: 0
|
||||||
|
borderColor: color
|
||||||
|
name: qsTr("To grid")
|
||||||
|
// visible: false
|
||||||
|
|
||||||
|
function calculateValue(entry) {
|
||||||
|
return storageSeries.calculateValue(entry) + Math.max(0, -entry.acquisition)
|
||||||
|
}
|
||||||
|
function addEntry(entry) {
|
||||||
|
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
|
||||||
|
}
|
||||||
|
function insertEntry(index, entry) {
|
||||||
|
acquisitionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
lowerSeries: storageUpperSeries
|
||||||
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
upperSeries: LineSeries {
|
||||||
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
|
id: acquisitionUpperSeries
|
||||||
property XYSeries lowerSeries: null
|
|
||||||
|
|
||||||
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
|
|
||||||
property bool translate: value >= 1000
|
|
||||||
property double translatedValue: value / (translate ? 1000 : 1)
|
|
||||||
text: qsTr("To grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
|
||||||
font: Style.extraSmallFont
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: chartView.plotArea.x
|
||||||
|
anchors.topMargin: chartView.plotArea.y
|
||||||
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||||
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: tooltipping || dragging
|
||||||
|
|
||||||
|
property int startMouseX: 0
|
||||||
|
property bool dragging: false
|
||||||
|
property bool tooltipping: false
|
||||||
|
|
||||||
|
property var startDatetime: null
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 300
|
||||||
|
running: mouseArea.pressed
|
||||||
|
onTriggered: {
|
||||||
|
if (!mouseArea.dragging) {
|
||||||
|
mouseArea.tooltipping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
if (mouseArea.dragging) {
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
mouseArea.dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseArea.tooltipping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: {
|
||||||
|
startMouseX = mouseX
|
||||||
|
startDatetime = d.now
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleClicked: {
|
||||||
|
if (selectionTabs.currentIndex == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
|
||||||
|
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
|
||||||
|
selectionTabs.currentIndex--
|
||||||
|
d.now = new Date(timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)
|
||||||
|
powerBalanceLogs.fetchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseXChanged: {
|
||||||
|
if (!pressed || mouseArea.tooltipping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Math.abs(startMouseX - mouseX) < 10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragging = true
|
||||||
|
|
||||||
|
var dragDelta = startMouseX - mouseX
|
||||||
|
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||||
|
// dragDelta : timeDelta = width : totalTime
|
||||||
|
var timeDelta = dragDelta * totalTime / mouseArea.width
|
||||||
|
print("dragging", dragDelta, totalTime, mouseArea.width)
|
||||||
|
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel: {
|
||||||
|
startDatetime = d.now
|
||||||
|
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||||
|
// pixelDelta : timeDelta = width : totalTime
|
||||||
|
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
|
||||||
|
print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
|
||||||
|
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
|
||||||
|
wheelStopTimer.restart()
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: wheelStopTimer
|
||||||
|
interval: 300
|
||||||
|
repeat: false
|
||||||
|
onTriggered: powerBalanceLogs.fetchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: parent.height
|
||||||
|
width: 1
|
||||||
|
color: Style.foregroundColor
|
||||||
|
x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
}
|
||||||
|
|
||||||
|
NymeaToolTip {
|
||||||
|
id: toolTip
|
||||||
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||||
|
|
||||||
|
backgroundItem: chartView
|
||||||
|
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
|
||||||
|
|
||||||
|
property int idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
|
||||||
|
property var timestamp: new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
|
||||||
|
property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
|
||||||
|
|
||||||
|
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
|
||||||
|
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
|
||||||
|
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
||||||
|
property double maxValue: toolTip.entry ? Math.max(0, -entry.production) : 0
|
||||||
|
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
||||||
|
|
||||||
|
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||||
|
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: tooltipLayout
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
margins: Style.smallMargins
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
font: Style.smallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.production) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("Total production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.red
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
||||||
|
Component.onCompleted: lowerSeries = selfConsumptionSeries.lowerSeries
|
||||||
|
property XYSeries lowerSeries: null
|
||||||
|
|
||||||
|
property double value: toolTip.entry ? Math.max(0, toolTip.entry.consumption) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
visible: root.batteries.count > 0
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.orange
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
||||||
|
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
|
||||||
|
property XYSeries lowerSeries: null
|
||||||
|
|
||||||
|
property double value: toolTip.entry ? Math.max(0, toolTip.entry.storage) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("To battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
Rectangle {
|
||||||
|
width: Style.extraSmallFont.pixelSize
|
||||||
|
height: width
|
||||||
|
color: Style.green
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
|
||||||
|
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
|
||||||
|
property XYSeries lowerSeries: null
|
||||||
|
|
||||||
|
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.acquisition) : 0
|
||||||
|
property bool translate: value >= 1000
|
||||||
|
property double translatedValue: value / (translate ? 1000 : 1)
|
||||||
|
text: qsTr("To grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
|
||||||
|
font: Style.extraSmallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,10 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int minutesCount: 9
|
property int minutesCount: 9
|
||||||
property int hoursCount: 11
|
property int hoursCount: 10
|
||||||
property int daysCount: 6
|
property int daysCount: 7
|
||||||
property int weeksCount: 12
|
property int weeksCount: 10
|
||||||
property int monthsCount: 11
|
property int monthsCount: 6
|
||||||
property int yearsCount: 5
|
property int yearsCount: 5
|
||||||
|
|
||||||
property var configs: ({
|
property var configs: ({
|
||||||
@ -17,58 +17,61 @@ Item {
|
|||||||
startTime: minutesStart,
|
startTime: minutesStart,
|
||||||
sampleRate: EnergyLogs.SampleRate1Min,
|
sampleRate: EnergyLogs.SampleRate1Min,
|
||||||
toLabel: minuteLabel,
|
toLabel: minuteLabel,
|
||||||
toLongLabel: minuteLongLabel
|
toLongLabel: minuteLongLabel,
|
||||||
|
toRangeLabel: minuteRangeLabel
|
||||||
},
|
},
|
||||||
hours: {
|
hours: {
|
||||||
count: hoursCount,
|
count: hoursCount,
|
||||||
startTime: hoursStart,
|
startTime: hoursStart,
|
||||||
sampleRate: EnergyLogs.SampleRate1Hour,
|
sampleRate: EnergyLogs.SampleRate1Hour,
|
||||||
toLabel: hourLabel,
|
toLabel: hourLabel,
|
||||||
toLongLabel: hourLongLabel
|
toLongLabel: hourLongLabel,
|
||||||
|
toRangeLabel: hourRangeLabel
|
||||||
},
|
},
|
||||||
days: {
|
days: {
|
||||||
count: daysCount,
|
count: daysCount,
|
||||||
startTime: daysStart,
|
startTime: daysStart,
|
||||||
sampleRate: EnergyLogs.SampleRate1Day,
|
sampleRate: EnergyLogs.SampleRate1Day,
|
||||||
toLabel: dayLabel,
|
toLabel: dayLabel,
|
||||||
toLongLabel: dayLongLabel
|
toLongLabel: dayLongLabel,
|
||||||
|
toRangeLabel: dayRangeLabel
|
||||||
},
|
},
|
||||||
weeks: {
|
weeks: {
|
||||||
count: weeksCount,
|
count: weeksCount,
|
||||||
startTime: weeksStart,
|
startTime: weeksStart,
|
||||||
sampleRate: EnergyLogs.SampleRate1Week,
|
sampleRate: EnergyLogs.SampleRate1Week,
|
||||||
toLabel: weekLabel,
|
toLabel: weekLabel,
|
||||||
toLongLabel: weekLongLabel
|
toLongLabel: weekLongLabel,
|
||||||
|
toRangeLabel: weekRangeLabel
|
||||||
},
|
},
|
||||||
months: {
|
months: {
|
||||||
count: monthsCount,
|
count: monthsCount,
|
||||||
startTime: monthsStart,
|
startTime: monthsStart,
|
||||||
sampleRate: EnergyLogs.SampleRate1Month,
|
sampleRate: EnergyLogs.SampleRate1Month,
|
||||||
toLabel: monthLabel,
|
toLabel: monthLabel,
|
||||||
toLongLabel: monthLongLabel
|
toLongLabel: monthLongLabel,
|
||||||
|
toRangeLabel: monthRangeLabel
|
||||||
},
|
},
|
||||||
years: {
|
years: {
|
||||||
count: yearsCount,
|
count: yearsCount,
|
||||||
startTime: yearStart,
|
startTime: yearStart,
|
||||||
sampleRate: EnergyLogs.SampleRate1Year,
|
sampleRate: EnergyLogs.SampleRate1Year,
|
||||||
toLabel: yearLabel,
|
toLabel: yearLabel,
|
||||||
toLongLabel: yearLabel
|
toLongLabel: yearLongLabel,
|
||||||
|
toRangeLabel: yearRangeLabel
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function calculateSampleStart(sampleEnd, sampleRate, sampleCount) {
|
function calculateTimestamp(baseTime, sampleRate, offset) {
|
||||||
if (sampleCount === undefined) {
|
var timestamp = new Date(baseTime);
|
||||||
sampleCount = 1
|
|
||||||
}
|
|
||||||
var sampleStart = new Date(sampleEnd)
|
|
||||||
if (sampleRate === EnergyLogs.SampleRate1Month) {
|
if (sampleRate === EnergyLogs.SampleRate1Month) {
|
||||||
sampleStart.setMonth(sampleEnd.getMonth() - sampleCount)
|
timestamp.setMonth(baseTime.getMonth() + offset)
|
||||||
} else if (sampleRate === EnergyLogs.SampleRate1Year) {
|
} else if (sampleRate === EnergyLogs.SampleRate1Year) {
|
||||||
sampleStart.setFullYear(sampleEnd.getFullYear() - sampleCount)
|
timestamp.setFullYear(baseTime.getFullYear() + offset)
|
||||||
} else {
|
} else {
|
||||||
sampleStart.setTime(sampleEnd.getTime() - (sampleRate * 60000 * sampleCount))
|
timestamp.setTime(baseTime.getTime() + (sampleRate * 60000 * offset))
|
||||||
}
|
}
|
||||||
return sampleStart
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function minutesStart() {
|
function minutesStart() {
|
||||||
@ -82,6 +85,9 @@ Item {
|
|||||||
function minuteLongLabel(date) {
|
function minuteLongLabel(date) {
|
||||||
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
}
|
}
|
||||||
|
function minuteRangeLabel(date) {
|
||||||
|
return date.toLocaleString(Qt.locale(), Locale.ShortFormat) + " - " + new Date(date.getTime() + root.minutesCount * 60000).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function hoursStart() {
|
function hoursStart() {
|
||||||
@ -95,6 +101,9 @@ Item {
|
|||||||
function hourLongLabel(date) {
|
function hourLongLabel(date) {
|
||||||
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
}
|
}
|
||||||
|
function hourRangeLabel(date) {
|
||||||
|
return date.toLocaleString(Qt.locale(), Locale.ShortFormat) + " - " + new Date(date.getTime() + root.hoursCount * 60 * 60000).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
}
|
||||||
|
|
||||||
function daysStart() {
|
function daysStart() {
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
@ -108,11 +117,14 @@ Item {
|
|||||||
function dayLongLabel(date) {
|
function dayLongLabel(date) {
|
||||||
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
||||||
}
|
}
|
||||||
|
function dayRangeLabel(date) {
|
||||||
|
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + new Date(date.getTime() + root.daysCount * 24 * 60 * 60000).toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
}
|
||||||
|
|
||||||
function weeksStart() {
|
function weeksStart() {
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
d.setHours(0, 0, 0, 0);
|
d.setHours(0, 0, 0, 0);
|
||||||
d.setDate(d.getDate() - d.getDay() - weeksCount * 7);
|
d.setDate(d.getDate() - d.getDay() + 1 - (weeksCount - 1) * 7);
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
function weekLabel(date) {
|
function weekLabel(date) {
|
||||||
@ -127,6 +139,11 @@ Item {
|
|||||||
endDate.setDate(endDate.getDate() + 6)
|
endDate.setDate(endDate.getDate() + 6)
|
||||||
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + endDate.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + endDate.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
||||||
}
|
}
|
||||||
|
function weekRangeLabel(date) {
|
||||||
|
var endDate = new Date(date)
|
||||||
|
endDate.setDate(endDate.getDate() + (7 * root.weeksCount))
|
||||||
|
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + endDate.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function monthsStart() {
|
function monthsStart() {
|
||||||
@ -141,6 +158,11 @@ Item {
|
|||||||
function monthLongLabel(date) {
|
function monthLongLabel(date) {
|
||||||
return date.toLocaleString(Qt.locale(), "MMMM yyyy")
|
return date.toLocaleString(Qt.locale(), "MMMM yyyy")
|
||||||
}
|
}
|
||||||
|
function monthRangeLabel(date) {
|
||||||
|
var endDate = new Date(date);
|
||||||
|
endDate.setMonth(date.getMonth() + monthsCount - 1)
|
||||||
|
return date.toLocaleString(Qt.locale(), "MMMM yyyy") + " - " + endDate.toLocaleString(Qt.locale(), "MMMM yyyy")
|
||||||
|
}
|
||||||
|
|
||||||
function yearStart() {
|
function yearStart() {
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
@ -151,5 +173,11 @@ Item {
|
|||||||
function yearLabel(date) {
|
function yearLabel(date) {
|
||||||
return date.toLocaleString(Qt.locale(), "yyyy")
|
return date.toLocaleString(Qt.locale(), "yyyy")
|
||||||
}
|
}
|
||||||
|
function yearLongLabel(date) {
|
||||||
|
return date.toLocaleString(Qt.locale(), "yyyy")
|
||||||
|
}
|
||||||
|
function yearRangeLabel(date) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user