Make the energy charts scrollable

This commit is contained in:
Michael Zanetti 2022-08-22 13:59:04 +02:00
parent 635e04cf21
commit 1d21e88e67
17 changed files with 2989 additions and 1983 deletions

View File

@ -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 &timestamp)
{
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 &params) void EnergyLogs::getLogsResponse(int commandId, const QVariantMap &params)
{ {
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");
} }

View File

@ -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 &timestamp);
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 &params) = 0; virtual QList<EnergyLogEntry*> unpackEntries(const QVariantMap &params, 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 &params); void getLogsResponse(int commandId, const QVariantMap &params);
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;
}; };

View File

@ -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 &params, 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 &timestamp) 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 &params)
{ {
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 &params)
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);
} }
} }

View File

@ -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 &timestamp) const;
signals:
void minValueChanged();
void maxValueChanged();
protected: protected:
QString logsName() const override; QString logsName() const override;
void logEntriesReceived(const QVariantMap &params) override; QList<EnergyLogEntry*> unpackEntries(const QVariantMap &params, 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;
}; };

View File

@ -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 &params){
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 &timestamp)
{
// 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 &params) QList<EnergyLogEntry *> ThingPowerLogs::unpackEntries(const QVariantMap &params, 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 &params)
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 &params)
{
qCDebug(dcEnergyLogs()) << "Logs loader response!";
emit fetched(commandId, params);
m_fetchingData = false;
if (m_fetchAgain) {
m_fetchAgain = false;
fetchLogs();
} else {
emit fetchingDataChanged();
}
}

View File

@ -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 &timestamp); 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 &params) override; QList<EnergyLogEntry*> unpackEntries(const QVariantMap &params, 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 &params);
private slots:
void getLogsResponse(int commandId, const QVariantMap &params);
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

View File

@ -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");
} }

View File

@ -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>

View 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
}
}
}
}
}
}

View File

@ -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)
} }
} }
} }

View File

@ -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

View File

@ -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
}
}
} }
} }
} }
} }
} }
} }
} }

View File

@ -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
}
}
}
}
}
} }
} }
} }
} }

View File

@ -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
}
}
} }
} }
} }
} }
} }
} }

View File

@ -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
}
}
} }
} }
} }

View File

@ -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
}
}
}
}
}
} }
} }
} }

View File

@ -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 ""
}
} }