Merge PR #861: Make the energy charts scrollable

This commit is contained in:
jenkins 2022-08-30 16:55:42 +02:00
commit 68c703c759
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,19 +243,93 @@ 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"; qCDebug(dcEnergyLogs()) << "Energy logs received";
// qCDebug(dcEnergyLogs()) << "Energy logs:" << params; beginInsertRows(QModelIndex(), 0, entries.count());
logEntriesReceived(params); 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.";
}
m_fetchingData = false; m_fetchingData = false;
if (m_fetchAgain) {
qCDebug(dcEnergyLogs()) << "Fetching again...";
m_fetchAgain = false;
fetchLogs();
} else {
emit fetchingDataChanged(); 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) {
addEntries(m_cachedEntries);
m_cachedEntries.clear();
} }
QUuid ThingPowerLogs::thingId() const
{
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);
}
}
}
ThingPowerLogsLoader *ThingPowerLogs::loader() const
{
return m_loader;
}
void ThingPowerLogs::setLoader(ThingPowerLogsLoader *loader)
{
if (m_loader != loader) {
m_loader = loader;
emit loaderChanged();
loader->addThingId(m_thingId);
connect(loader, &ThingPowerLogsLoader::fetched, this, [=](int commandId, const QVariantMap &params){
qCDebug(dcEnergyLogs()) << "Loader fetched data.";
getLogsResponse(commandId, params);
}); });
} }
QList<QUuid> ThingPowerLogs::thingIds() const
{
return m_thingIds;
} }
void ThingPowerLogs::setThingIds(const QList<QUuid> &thingIds) ThingPowerLogEntry *ThingPowerLogs::liveEntry()
{ {
if (m_thingIds != thingIds) { return m_liveEntry;
m_thingIds = thingIds;
emit thingIdsChanged();
}
}
double ThingPowerLogs::minValue() const
{
return m_minValue;
}
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 ThingPowerLogsLoader::ThingPowerLogsLoader(QObject *parent):
// batch and actually append them. QObject(parent)
// 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); 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 { } else {
addEntries(m_cachedEntries); if (m_startTime < m_lastStartTime) {
m_cachedEntries.clear(); startTime = m_startTime;
m_cachedEntries.append(entry); endTime = m_lastStartTime;
} m_lastStartTime = m_startTime;
m_cacheTimer.start(); } 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()
property var config: root.configs[selectionTabs.currentValue.config]
property int startOffset: 0
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()
} }
Connections { valueAxis.max = 1
target: engine.thingManager
onFetchingDataChanged: root.update()
}
Connections {
target: engine.tagsManager
onBusyChanged: root.update()
} }
function update() { onLoadingChanged: {
if (engine.thingManager.fetchingData || engine.tagsManager.busy || selectionTabs.currentValue === undefined) { if (!loading) {
return refresh()
} }
powerLogs.loadingInhibited = true
var thingIds = []
for (var i = 0; i < consumers.count; i++) {
thingIds.push(consumers.get(i).id)
}
powerLogs.thingIds = thingIds
var config = root.configs[selectionTabs.currentValue.config]
// print("config:", config.startTime(), config.sampleList(), config.sampleListNames())
powerLogs.sampleRate = config.sampleRate
powerLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
chartView.reset();
powerLogs.loadingInhibited = false
} }
ThingPowerLogs { function refresh() {
id: powerLogs for (var i = 0; i < consumersRepeater.count; i++) {
consumersRepeater.itemAt(i).refresh()
}
}
}
ThingPowerLogsLoader {
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()
// First grouping log entries by timestamp
var groupedEntries = []
var groupedEntry = {}
for (var i = powerLogs.count - 1; i >= 0; i--) {
var entry = powerLogs.get(i);
// print("grouping entry:", entry.timestamp, "current group entry", groupedEntry.timestamp, groupedEntry.hasOwnProperty("timestamp"))
if (!groupedEntry.hasOwnProperty("timestamp")) {
groupedEntry.timestamp = entry.timestamp;
// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp)
} }
if (groupedEntry.timestamp.getTime() !== entry.timestamp.getTime()) {
if (groupedEntries.length > config.count) {
break;
} }
// print("finalizing grouped entry", groupedEntry.timestamp)
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)
} }
var labels = [] Repeater {
var entries = [] id: consumersRepeater
model: root.consumers
var newestLogTimestamp = powerLogs.count > 0 ? powerLogs.get(powerLogs.count - 1).timestamp : new Date(); onCountChanged: {
if (count == root.consumers.count) {
for (var i = 0; i < config.count; i++) { logsLoader.fetchLogs();
var groupedEntry = groupedEntries[groupedEntries.length - i - 1]
// print("have grouped entry:", groupedEntry ? groupedEntry.timestamp : "null")
// 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 = {}
for (var j = 0; j < consumers.count; j++) {
var consumer = consumers.get(j)
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)
} }
// print("Adding live entry", JSON.stringify(liveEntry)) delegate: Item {
entries.unshift(liveEntry) id: consumerDelegate
readonly property Thing thing: root.consumers.get(index)
property BarSet barSet: null
Connections {
target: d
onStartOffsetChanged: refresh()
} }
// Add the actual entry function refreshLabels() {
var graphEntry = {} var values = []
var labelTime = new Date(); for (var i = 0; i < d.config.count; i++) {
values.push(0)
}
barSet.values = values;
}
if (groupedEntry) { function refresh() {
var previousGroupedEntry = groupedEntries[groupedEntries.length - i - 2] var upcomingTimestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.config.count)
for (var j = 0; j < consumers.count; j++) { // print("refreshing", consumerDelegate.thing.name ,"config start", d.config.startTime(), "upcoming:", upcomingTimestamp, "fetchPending", d.fetchPending, d.loading)
var consumer = consumers.get(j) for (var i = 0; i < d.config.count; i++) {
var value = groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0 var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i + 1)
if (previousGroupedEntry) { var previousTimestamp = root.calculateTimestamp(timestamp, d.config.sampleRate, -1)
var previousValue = previousGroupedEntry.hasOwnProperty(consumer.id) ? previousGroupedEntry[consumer.id] : 0 // print("timestamp:", timestamp, "previous:", previousTimestamp)
value -= previousValue var entry = thingPowerLogs.find(timestamp)
var previousEntry = thingPowerLogs.find(previousTimestamp);
if (entry && (previousEntry || !d.loading)) {
// print("found entry:", entry.timestamp, previousEntry)
var consumption = entry.totalConsumption
if (previousEntry) {
consumption -= previousEntry.totalConsumption
} }
graphEntry[consumer.id] = value barSet.replace(i, consumption)
valueAxis.adjustMax(value) valueAxis.adjustMax(consumption)
} else if (timestamp.getTime() == upcomingTimestamp.getTime() && (previousEntry || !d.loading)) {
var consumption = thingPowerLogs.liveEntry().totalConsumption
// print("it's today for thing", thing.name, consumption, previousEntry)
if (previousEntry) {
// print("previous timestamp", previousEntry.timestamp, previousEntry.totalConsumption)
consumption -= previousEntry.totalConsumption
} }
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 {
id: thingPowerLogs
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) { if (fetchingData) {
return return;
}
consumerDelegate.refresh()
}
} }
chartView.animationOptions = ChartView.NoAnimation Component.onCompleted: {
var values = []
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < d.config.count; i++) {
var entry = entries[i] values.push(0)
var thing = engine.thingManager.things.getThing(entry.thingId)
// print("Adding new sample. thing:", thing.name);
// 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 barSet = barSeries.append(consumerDelegate.thing.name, values)
labels.splice(0, 1) barSet.color = NymeaUtils.generateColor(Style.generationBaseColor, index)
labels.push(entries[0].timestamp) barSet.borderColor = barSet.color
categoryAxis.timestamps = labels barSet.borderWith = 0
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,56 +170,81 @@ 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
} }
} onTabSelected: {
onCurrentValueChanged: { d.startOffset = 0
root.update() logsLoader.fetchLogs();
} }
} }
Item {
ChartView {
id: chartView
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Label {
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.y + chartView.plotArea.y + Style.smallMargins
text: d.config.toRangeLabel(d.startTime)
font: Style.smallFont
opacity: d.startOffset < -d.config.count ? .5 : 0
Behavior on opacity { NumberAnimation {} }
}
ChartView {
id: chartView
anchors.fill: parent
backgroundColor: "transparent"
// margins.left: 0 // margins.left: 0
margins.right: 0 margins.right: 0
margins.bottom: 0 margins.bottom: 0
margins.top: 0 margins.top: 0
backgroundColor: "transparent"
legend.alignment: Qt.AlignBottom legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor legend.labelColor: Style.foregroundColor
function reset() { ActivityIndicator {
chartView.animationOptions = ChartView.NoAnimation x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
barSeries.clear(); y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
valueAxis.max = 0 visible: logsLoader.fetchingData
var map = {} opacity: .5
for (var j = 0; j < consumers.count; j++) {
var consumer = consumers.get(j)
var barSet = barSeries.append(consumer.name, [])
// barSet.color = root.colors[j % root.colors.length]
barSet.color = NymeaUtils.generateColor(Style.generationBaseColor, j)
barSet.borderColor = barSet.color
barSet.borderWith = 0
map[consumer.id] = barSet
} }
barSeries.thingBarSetMap = map Label {
chartView.animationOptions = NymeaUtils.chartsAnimationOptions 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 { Item {
@ -329,13 +281,14 @@ StatsBase {
categories: { categories: {
var ret = [] var ret = []
for (var i = 0; i < timestamps.length; i++) { print("Updating categories from", d.config.startTime())
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i])) 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 return ret;
} }
property var timestamps: []
} }
axisY: ValueAxis { axisY: ValueAxis {
id: valueAxis id: valueAxis
@ -354,9 +307,6 @@ StatsBase {
} }
} }
} }
property var thingBarSetMap: ({})
}
} }
} }
@ -392,14 +342,87 @@ StatsBase {
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property int dragStartOffset: 0
Timer { Timer {
interval: 300 interval: 300
running: mouseArea.pressed running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
} }
onReleased: mouseArea.preventStealing = false
onReleased: {
if (mouseArea.dragging) {
logsLoader.fetchLogs();
d.refresh()
mouseArea.dragging = false;
}
mouseArea.tooltipping = false;
}
onPressed: {
startMouseX = mouseX
dragStartOffset = d.startOffset
}
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 { NymeaToolTip {
id: toolTip id: toolTip
@ -407,8 +430,10 @@ StatsBase {
backgroundItem: chartView backgroundItem: chartView
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height) 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))) property int idx: Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
visible: mouseArea.containsMouse || mouseArea.preventStealing 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 chartWidth: chartView.plotArea.width
property int barWidth: chartWidth / categoryAxis.count property int barWidth: chartWidth / categoryAxis.count
@ -417,9 +442,8 @@ StatsBase {
: idx * barWidth - Style.smallMargins - width : idx * barWidth - Style.smallMargins - width
property double setMaxValue: { property double setMaxValue: {
var max = 0; var max = 0;
for (var i = 0; i < consumers.count; i++) { for (var i = 0; i < consumersRepeater.count; i++) {
var consumer = consumers.get(i) max = Math.max(max, consumersRepeater.itemAt(i).barSet.at(idx))
max = barSeries.thingBarSetMap.hasOwnProperty(consumer.id) ? Math.max(max, barSeries.thingBarSetMap[consumer.id].at(idx)) : 0
} }
return max return max
} }
@ -436,7 +460,7 @@ StatsBase {
margins: Style.smallMargins margins: Style.smallMargins
} }
Label { Label {
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : "" text: d.config.toLongLabel(toolTip.timestamp)
font: Style.smallFont font: Style.smallFont
} }
@ -449,7 +473,7 @@ StatsBase {
var consumer = consumers.get(i) var consumer = consumers.get(i)
var entry = { var entry = {
name: consumer.name, name: consumer.name,
value: barSeries.thingBarSetMap[consumer.id].at(toolTip.idx).toFixed(2), value: consumersRepeater.itemAt(i).barSet.at(toolTip.idx).toFixed(2),
indexInModel: i indexInModel: i
} }
unsorted.push(entry) unsorted.push(entry)
@ -486,3 +510,7 @@ StatsBase {
} }
} }
} }
}
}

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
zeroSeries.ensureValue(entry.timestamp)
valueAxis.adjustMax(entry.consumption)
consumptionSeries.insertEntry(index + i, entry)
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
d.now = entry.timestamp
}
}
} }
// Add them in the order of the chart (same as proxy), summing it up onEntriesRemoved: {
var totalValue = 0; consumptionUpperSeries.removePoints(index, count)
for (var i = 0; i < consumers.count; i++) { zeroSeries.shrink()
var consumer = consumers.get(i);
var value = thingValues.hasOwnProperty(consumer.id) ? thingValues[consumer.id] : 0
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 ThingPowerLogsLoader {
} id: logsLoader
property PowerBalanceLogs powerBalanceLogs: PowerBalanceLogs {
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;
if (i > 0) {
baseSeries = d.thingsSeriesMap[consumerThingIds[i-1]].upperSeries
// print("base for:", thing.name, "is", engine.thingManager.things.getThing(consumerThingIds[i-1]).name)
} }
var series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, valueAxis) readonly property var endTime: {
series.lowerSeries = baseSeries var date = new Date(now);
series.upperSeries = lineSeriesComponent.createObject(series) date.setTime(date.getTime() + 2000)
// series.color = root.colors[i % root.colors.length] return date;
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,6 +75,65 @@ Item {
LineSeries { } LineSeries { }
} }
ColumnLayout {
anchors.fill: parent
spacing: 0
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("Consumers history")
}
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 {
Layout.fillWidth: true
Layout.fillHeight: true
Label {
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.y + chartView.plotArea.y + Style.smallMargins
text: d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
font: Style.smallFont
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
Behavior on opacity { NumberAnimation {} }
}
ChartView { ChartView {
id: chartView id: chartView
anchors.fill: parent anchors.fill: parent
@ -165,18 +144,30 @@ Item {
margins.bottom: 0 margins.bottom: 0
margins.top: 0 margins.top: 0
title: qsTr("Consumers history")
titleColor: Style.foregroundColor
legend.alignment: Qt.AlignBottom legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor
ActivityIndicator {
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 || 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")
visible: !powerBalanceLogs.fetchingData && !logsLoader.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
font: Style.smallFont
opacity: .5
}
ValueAxis { ValueAxis {
id: valueAxis id: valueAxis
min: 0 min: 0
max: Math.ceil(Math.max(powerBalanceLogs.maxValue, thingPowerLogs.maxValue) / 1000) * 1000 max: 1
labelFormat: "" labelFormat: ""
gridLineColor: Style.tileOverlayColor gridLineColor: Style.tileOverlayColor
labelsVisible: false labelsVisible: false
@ -185,6 +176,9 @@ Item {
shadesVisible: false shadesVisible: false
// visible: false // visible: false
function adjustMax(value) {
max = Math.max(max, Math.ceil(value / 100) * 100)
}
} }
Item { Item {
@ -208,18 +202,31 @@ Item {
DateTimeAxis { DateTimeAxis {
id: dateTimeAxis id: dateTimeAxis
property date now: new Date() min: d.startTime
min: { max: d.endTime
var date = new Date(now); format: {
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000); switch (selectionTabs.currentValue.sampleRate) {
return date; 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
} }
max: {
var date = new Date(now);
date.setTime(date.getTime() + 2000)
return date;
} }
format: "hh:mm"
labelsFont: Style.extraSmallFont labelsFont: Style.extraSmallFont
gridVisible: false gridVisible: false
minorGridVisible: false minorGridVisible: false
@ -236,14 +243,38 @@ Item {
borderWidth: 0 borderWidth: 0
borderColor: color borderColor: color
name: qsTr("Unknown") name: qsTr("Unknown")
// visible: false
opacity: .2
lowerSeries: LineSeries { lowerSeries: LineSeries {
id: zeroSeries id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 } XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 } XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function update(timestamp) { function ensureValue(timestamp) {
append(timestamp, 0); if (count == 0) {
removePoints(1,1); 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 { upperSeries: LineSeries {
@ -253,8 +284,94 @@ Item {
function addEntry(entry) { function addEntry(entry) {
consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption) 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 { MouseArea {
@ -266,43 +383,137 @@ Item {
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property var startDatetime: null
Timer { Timer {
interval: 300 interval: 300
running: mouseArea.pressed running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true 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()
// }
}
} }
onReleased: mouseArea.preventStealing = false
Rectangle { Rectangle {
height: parent.height height: parent.height
width: 1 width: 1
color: Style.foregroundColor color: Style.foregroundColor
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX)) x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
visible: mouseArea.containsMouse || mouseArea.preventStealing visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
} }
NymeaToolTip { NymeaToolTip {
id: toolTip id: toolTip
visible: mouseArea.containsMouse || mouseArea.preventStealing visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
backgroundItem: chartView backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height) 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 idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
property int seriesIndex: Math.min(consumptionUpperSeries.count - 1, Math.max(0, consumptionUpperSeries.count - idx)) 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 xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - width property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: consumptionUpperSeries.at(seriesIndex).y 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) 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 width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2 height: tooltipLayout.implicitHeight + Style.smallMargins * 2
property date timestamp: new Date(consumptionUpperSeries.at(seriesIndex).x)
ColumnLayout { ColumnLayout {
id: tooltipLayout id: tooltipLayout
anchors { anchors {
@ -321,7 +532,7 @@ Item {
color: consumptionSeries.color color: consumptionSeries.color
} }
Label { Label {
property double rawValue: consumptionUpperSeries.at(toolTip.seriesIndex).y property double rawValue: toolTip.entry ? toolTip.entry.consumption : 0
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W" property string unit: rawValue >= 1000 ? "kW" : "W"
text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit) text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit)
@ -341,7 +552,7 @@ Item {
} }
Label { Label {
property ThingPowerLogEntry entry: thingPowerLogs.find(model.id, toolTip.timestamp) property ThingPowerLogEntry entry: toolTip.idx >= 0 ? consumersRepeater.itemAt(index).logs.find(toolTip.timestamp) : null
property double rawValue: entry ? entry.currentPower : 0 property double rawValue: entry ? entry.currentPower : 0
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W" property string unit: rawValue >= 1000 ? "kW" : "W"
@ -354,3 +565,7 @@ Item {
} }
} }
} }
}
}

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 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: fetchPending || wheelStopTimer.running || powerBalanceLogs.fetchingData
onLoadingChanged: {
if (!loading) {
refresh()
}
} }
function reload() { onConfigChanged: valueAxis.max = 1
if (selectionTabs.currentValue === undefined) { onStartOffsetChanged: {
return // print("updating because of offset change. fetchingData", powerBalanceLogs.fetchingData, "fetchPending", d.fetchPending)
refresh()
} }
if (engine.thingManager.fetchingData) { function refresh() {
if (powerBalanceLogs.loadingInhibited) {
return; return;
} }
var config = root.configs[selectionTabs.currentValue.config] var upcomingTimestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.config.count)
print("Loading Power Balance Stats with config:", config.startTime(), config.sampleRate) // print("refreshing config start", d.config.startTime(), "upcoming:", upcomingTimestamp, "fetchPending", d.fetchPending)
for (var i = 0; i < d.config.count; i++) {
powerBalanceLogs.loadingInhibited = true var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i + 1)
powerBalanceLogs.sampleRate = config.sampleRate var previousTimestamp = root.calculateTimestamp(timestamp, d.config.sampleRate, -1)
powerBalanceLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000) // print("timestamp:", timestamp)
powerBalanceLogs.loadingInhibited = false var entry = powerBalanceLogs.find(timestamp)
var previousEntry = powerBalanceLogs.find(previousTimestamp);
chartView.reset(); if (entry && (previousEntry || !d.loading)) {
// print("found entry:", entry.timestamp, previousEntry)
// print("Acquisition", entry.totalAcquisition)
var consumption = entry.totalConsumption
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)
}
} }
Connections {
target: engine.thingManager
onFetchingDataChanged: {
print("Thingmanager loaded", engine.thingManager.fetchingData)
if (!engine.thingManager.fetchingData) root.reload()
} }
} }
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,201 +118,73 @@ 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
} }
} onTabSelected: {
onCurrentValueChanged: { d.startOffset = 0
root.reload() 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 {
Layout.fillWidth: true
Layout.fillHeight: true
Label {
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.y + chartView.plotArea.y + Style.smallMargins
text: d.config.toRangeLabel(d.startTime)
font: Style.smallFont
opacity: d.startOffset < -d.config.count ? .5 : 0
Behavior on opacity { NumberAnimation {} }
}
ChartView { ChartView {
id: chartView id: chartView
Layout.fillWidth: true
Layout.fillHeight: true
animationOptions: ChartView.NoAnimation animationOptions: ChartView.NoAnimation
anchors.fill: parent
backgroundColor: "transparent" backgroundColor: "transparent"
legend.alignment: Qt.AlignBottom legend.alignment: Qt.AlignBottom
@ -274,27 +196,20 @@ StatsBase {
margins.bottom: 0 margins.bottom: 0
margins.top: 0 margins.top: 0
function reset() { ActivityIndicator {
barSeries.clear(); x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
valueAxis.max = 0 y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
if (root.hasProducers) { visible: powerBalanceLogs.fetchingData
d.consumptionSet = barSeries.append(qsTr("Consumed"), []) opacity: .5
d.consumptionSet.color = Style.blue
d.consumptionSet.borderColor = d.consumptionSet.color
d.consumptionSet.borderWidth = 0
d.productionSet = barSeries.append(qsTr("Produced"), [])
d.productionSet.color = Style.yellow
d.productionSet.borderColor = d.productionSet.color
d.productionSet.borderWidth = 0
} }
d.acquisitionSet = barSeries.append(qsTr("From grid"), []) Label {
d.acquisitionSet.color = Style.red x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
d.acquisitionSet.borderColor = d.acquisitionSet.color y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
d.acquisitionSet.borderWidth = 0 text: qsTr("No data available")
d.returnSet = barSeries.append(qsTr("To grid"), []) visible: !powerBalanceLogs.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.endTime) && d.startOffset != 0
d.returnSet.color = Style.green font: Style.smallFont
d.returnSet.borderColor = d.returnSet.color opacity: .5
d.returnSet.borderWidth = 0 Behavior on opacity { NumberAnimation {}}
} }
Item { Item {
@ -331,14 +246,14 @@ StatsBase {
categories: { categories: {
var ret = [] var ret = []
for (var i = 0; i < timestamps.length; i++) { print("Updating categories from", d.config.startTime())
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i])) 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 return ret;
} }
property var timestamps: []
} }
axisY: ValueAxis { axisY: ValueAxis {
id: valueAxis id: valueAxis
@ -353,10 +268,68 @@ StatsBase {
function adjustMax(newValue) { function adjustMax(newValue) {
if (max < newValue) { if (max < newValue) {
print("adjusting to new max", newValue)
max = newValue // Math.ceil(newValue / 100) * 100 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
}
} }
} }
} }
@ -392,13 +365,84 @@ StatsBase {
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property int dragStartOffset: 0
Timer { Timer {
interval: 300 interval: 300
running: mouseArea.pressed running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
}
onReleased: {
if (mouseArea.dragging) {
powerBalanceLogs.fetchLogs()
mouseArea.dragging = false;
}
mouseArea.tooltipping = false;
}
onPressed: {
startMouseX = mouseX
dragStartOffset = d.startOffset
}
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)
powerBalanceLogs.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: powerBalanceLogs.fetchLogs()
} }
onReleased: mouseArea.preventStealing = false
NymeaToolTip { NymeaToolTip {
id: toolTip id: toolTip
@ -406,8 +450,10 @@ StatsBase {
backgroundItem: chartView backgroundItem: chartView
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height) 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) property int idx: Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
visible: mouseArea.containsMouse || mouseArea.preventStealing 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 chartWidth: chartView.plotArea.width
property int barWidth: chartWidth / categoryAxis.count property int barWidth: chartWidth / categoryAxis.count
@ -415,10 +461,10 @@ StatsBase {
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ? x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
idx * barWidth + barWidth + Style.smallMargins idx * barWidth + barWidth + Style.smallMargins
: idx * barWidth - Style.smallMargins - width : idx * barWidth - Style.smallMargins - width
property double setMaxValue: Math.max(d.consumptionSet ? d.consumptionSet.at(idx) : 0, property double setMaxValue: d.startOffset !== undefined ? Math.max(consumptionSet.at(idx),
d.productionSet ? d.productionSet.at(idx) : 0, productionSet.at(idx),
d.acquisitionSet ? d.acquisitionSet.at(idx) : 0, acquisitionSet.at(idx),
d.returnSet ? d.returnSet.at(idx) : 0) returnSet.at(idx)) : 0
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height) 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 width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2 height: tooltipLayout.implicitHeight + Style.smallMargins * 2
@ -432,7 +478,7 @@ StatsBase {
} }
Label { Label {
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : "" text: d.config.toLongLabel(toolTip.timestamp)
font: Style.smallFont font: Style.smallFont
} }
@ -444,7 +490,7 @@ StatsBase {
color: Style.blue color: Style.blue
} }
Label { Label {
text: toolTip.visible && d.consumptionSet ? qsTr("Consumed: %1 kWh").arg(d.consumptionSet.at(toolTip.idx).toFixed(2)) : "" text: d.startOffset !== undefined ? qsTr("Consumed: %1 kWh").arg(consumptionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont font: Style.extraSmallFont
} }
} }
@ -456,7 +502,7 @@ StatsBase {
color: Style.yellow color: Style.yellow
} }
Label { Label {
text: toolTip.visible && d.productionSet ? qsTr("Produced: %1 kWh").arg(d.productionSet.at(toolTip.idx).toFixed(2)) : "" text: d.startOffset !== undefined ? qsTr("Produced: %1 kWh").arg(productionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont font: Style.extraSmallFont
} }
} }
@ -467,7 +513,7 @@ StatsBase {
color: Style.red color: Style.red
} }
Label { Label {
text: toolTip.visible && d.acquisitionSet ? qsTr("From grid: %1 kWh").arg(d.acquisitionSet.at(toolTip.idx).toFixed(2)) : "" text: d.startOffset !== undefined ? qsTr("From grid: %1 kWh").arg(acquisitionSet.at(toolTip.idx).toFixed(2)) :""
font: Style.extraSmallFont font: Style.extraSmallFont
} }
} }
@ -478,7 +524,7 @@ StatsBase {
color: Style.green color: Style.green
} }
Label { Label {
text: toolTip.visible && d.returnSet ? qsTr("To grid: %1 kWh").arg(d.returnSet.at(toolTip.idx).toFixed(2)) : "" text: d.startOffset !== undefined ? qsTr("To grid: %1 kWh").arg(returnSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont font: Style.extraSmallFont
} }
} }
@ -486,4 +532,5 @@ StatsBase {
} }
} }
} }
}
}

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,41 +22,115 @@ 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
} }
} }
} }
Timer { onEntriesRemoved: {
interval: 60000 acquisitionUpperSeries.removePoints(index, count)
repeat: true storageUpperSeries.removePoints(index, count)
onTriggered: { selfProductionUpperSeries.removePoints(index, count)
var now = new Date() consumptionUpperSeries.removePoints(index, count)
if (dateTimeAxis.now < now) { zeroSeries.shrink()
dateTimeAxis.now = now
zeroSeries.update(now)
} }
} }
ColumnLayout {
anchors.fill: parent
spacing: 0
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("My consumption history")
}
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 {
Layout.fillWidth: true
Layout.fillHeight: true
Label {
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.y + chartView.plotArea.y + Style.smallMargins
text: d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
font: Style.smallFont
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
Behavior on opacity { NumberAnimation {} }
} }
ChartView { ChartView {
@ -67,18 +142,29 @@ Item {
margins.bottom: 0 margins.bottom: 0
margins.top: 0 margins.top: 0
title: qsTr("My consumption history")
titleColor: Style.foregroundColor
legend.alignment: Qt.AlignBottom legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont legend.font: Style.extraSmallFont
ActivityIndicator {
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.now)
font: Style.smallFont
opacity: .5
}
ValueAxis { ValueAxis {
id: valueAxis id: valueAxis
min: 0 min: 0
max: Math.ceil(powerBalanceLogs.maxValue / 1000) * 1000 max: Math.ceil(powerBalanceLogs.maxValue / 100) * 100
labelFormat: "" labelFormat: ""
gridLineColor: Style.tileOverlayColor gridLineColor: Style.tileOverlayColor
labelsVisible: false labelsVisible: false
@ -111,18 +197,31 @@ Item {
DateTimeAxis { DateTimeAxis {
id: dateTimeAxis id: dateTimeAxis
property date now: new Date() min: d.startTime
min: { max: d.endTime
var date = new Date(now); format: {
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000); switch (selectionTabs.currentValue.sampleRate) {
return date; 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
} }
max: {
var date = new Date(now);
date.setTime(date.getTime() + 2000)
return date;
} }
format: "hh:mm"
labelsFont: Style.extraSmallFont labelsFont: Style.extraSmallFont
gridVisible: false gridVisible: false
minorGridVisible: false minorGridVisible: false
@ -153,6 +252,9 @@ Item {
function addEntry(entry) { function addEntry(entry) {
consumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) consumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
} }
function insertEntry(index, entry) {
consumptionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
} }
@ -170,9 +272,31 @@ Item {
id: zeroSeries id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 } XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 } XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function update(timestamp) { function ensureValue(timestamp) {
append(timestamp, 0); if (count == 0) {
removePoints(1,1); 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)
}
} }
} }
@ -191,6 +315,9 @@ Item {
function addEntry(entry) { function addEntry(entry) {
selfProductionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) selfProductionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
} }
function insertEntry(index, entry) {
selfProductionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
} }
AreaSeries { AreaSeries {
@ -215,6 +342,9 @@ Item {
function addEntry(entry) { function addEntry(entry) {
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
} }
function insertEntry(index, entry) {
storageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
} }
@ -239,8 +369,12 @@ Item {
function addEntry(entry) { function addEntry(entry) {
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
} }
function insertEntry(index, entry) {
acquisitionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
} }
} }
}
MouseArea { MouseArea {
@ -252,38 +386,105 @@ Item {
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property var startDatetime: null
Timer { Timer {
interval: 300 interval: 300
running: mouseArea.pressed running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true 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()
} }
onReleased: mouseArea.preventStealing = false
Rectangle { Rectangle {
height: parent.height height: parent.height
width: 1 width: 1
color: Style.foregroundColor color: Style.foregroundColor
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX)) x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
visible: mouseArea.containsMouse || mouseArea.preventStealing visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
} }
NymeaToolTip { NymeaToolTip {
id: toolTip id: toolTip
visible: mouseArea.containsMouse || mouseArea.preventStealing visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
backgroundItem: chartView backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height) 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 int idx: consumptionUpperSeries.count - (Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width)) property var timestamp: new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
property int seriesIndex: Math.min(consumptionUpperSeries.count - 1, Math.max(0, consumptionUpperSeries.count - idx)) property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: consumptionUpperSeries.at(seriesIndex).y 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) 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 width: tooltipLayout.implicitWidth + Style.smallMargins * 2
@ -297,12 +498,12 @@ Item {
margins: Style.smallMargins margins: Style.smallMargins
} }
Label { Label {
text: new Date(consumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat) text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
font: Style.smallFont font: Style.smallFont
} }
Label { Label {
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, toolTip.entry.consumption) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Total consumption: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("Total consumption: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -321,7 +522,7 @@ Item {
Component.onCompleted: lowerSeries = selfProductionSeries.lowerSeries Component.onCompleted: lowerSeries = selfProductionSeries.lowerSeries
property XYSeries lowerSeries: null property XYSeries lowerSeries: null
property double value: selfProductionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, -toolTip.entry.production) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Self production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("Self production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -341,7 +542,7 @@ Item {
Component.onCompleted: lowerSeries = storageSeries.lowerSeries Component.onCompleted: lowerSeries = storageSeries.lowerSeries
property XYSeries lowerSeries: null property XYSeries lowerSeries: null
property double value: storageUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, -toolTip.entry.storage) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("From battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("From battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -360,7 +561,7 @@ Item {
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
property XYSeries lowerSeries: null property XYSeries lowerSeries: null
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, toolTip.entry.acquisition) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("From grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("From grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -371,4 +572,6 @@ Item {
} }
} }
} }
}
}

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,65 +22,147 @@ 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
} }
} }
} }
Timer { onEntriesRemoved: {
interval: 60000 acquisitionUpperSeries.removePoints(index, count)
repeat: true storageUpperSeries.removePoints(index, count)
onTriggered: { selfConsumptionUpperSeries.removePoints(index, count)
var now = new Date() productionUpperSeries.removePoints(index, count)
if (dateTimeAxis.now < now) { zeroSeries.shrink()
dateTimeAxis.now = now
zeroSeries.update(now)
} }
} }
ColumnLayout {
anchors.fill: parent
spacing: 0
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("My production history")
}
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 {
Layout.fillWidth: true
Layout.fillHeight: true
Label {
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.y + chartView.plotArea.y + Style.smallMargins
text: d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
font: Style.smallFont
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
Behavior on opacity { NumberAnimation {} }
} }
ChartView { ChartView {
id: chartView id: chartView
anchors.fill: parent anchors.fill: parent
backgroundColor: "transparent" backgroundColor: "transparent"
margins.left: 0 margins.left: 0
margins.right: 0 margins.right: 0
margins.bottom: 0 margins.bottom: 0
margins.top: 0 margins.top: 0
title: qsTr("My production history")
titleColor: Style.foregroundColor
legend.alignment: Qt.AlignBottom legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont legend.font: Style.extraSmallFont
ActivityIndicator {
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.now)
font: Style.smallFont
opacity: .5
}
ValueAxis { ValueAxis {
id: valueAxis id: valueAxis
min: 0 min: 0
max: Math.ceil(-powerBalanceLogs.minValue / 1000) * 1000 max: Math.ceil(-powerBalanceLogs.minValue / 100) * 100
labelFormat: "" labelFormat: ""
gridLineColor: Style.tileOverlayColor gridLineColor: Style.tileOverlayColor
labelsVisible: false labelsVisible: false
@ -85,7 +170,6 @@ Item {
titleVisible: false titleVisible: false
shadesVisible: false shadesVisible: false
} }
Item { Item {
id: labelsLayout id: labelsLayout
x: Style.smallMargins x: Style.smallMargins
@ -107,18 +191,31 @@ Item {
DateTimeAxis { DateTimeAxis {
id: dateTimeAxis id: dateTimeAxis
property date now: new Date() min: d.startTime
min: { max: d.endTime
var date = new Date(now); format: {
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000); switch (selectionTabs.currentValue.sampleRate) {
return date; 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
} }
max: {
var date = new Date(now);
date.setTime(date.getTime() + 2000)
return date;
} }
format: "hh:mm"
labelsFont: Style.extraSmallFont labelsFont: Style.extraSmallFont
gridVisible: false gridVisible: false
minorGridVisible: false minorGridVisible: false
@ -145,6 +242,9 @@ Item {
function addEntry(entry) { function addEntry(entry) {
productionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) productionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
} }
function insertEntry(index, entry) {
productionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
lowerSeries: zeroSeries lowerSeries: zeroSeries
upperSeries: LineSeries { upperSeries: LineSeries {
@ -162,27 +262,53 @@ Item {
name: qsTr("Consumed") name: qsTr("Consumed")
// visible: false // visible: false
function calculateValue(entry) {
return Math.abs(Math.min(0, entry.production)) - Math.abs(Math.min(0, entry.acquisition)) - Math.max(0, entry.storage)
}
function addEntry(entry) {
selfConsumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
lowerSeries: LineSeries { lowerSeries: LineSeries {
id: zeroSeries id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 } XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 } XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function update(timestamp) { function ensureValue(timestamp) {
append(timestamp, 0); if (count == 0) {
removePoints(1,1); 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 { upperSeries: LineSeries {
id: selfConsumptionUpperSeries 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 { AreaSeries {
@ -197,12 +323,15 @@ Item {
function calculateValue(entry) { function calculateValue(entry) {
return selfConsumptionSeries.calculateValue(entry) + Math.abs(Math.max(0, entry.storage)); return selfConsumptionSeries.calculateValue(entry) + Math.max(0, entry.storage);
} }
function addEntry(entry) { function addEntry(entry) {
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
} }
function insertEntry(index, entry) {
storageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
lowerSeries: selfConsumptionUpperSeries lowerSeries: selfConsumptionUpperSeries
upperSeries: LineSeries { upperSeries: LineSeries {
@ -222,11 +351,14 @@ Item {
// visible: false // visible: false
function calculateValue(entry) { function calculateValue(entry) {
return storageSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.acquisition)) return storageSeries.calculateValue(entry) + Math.max(0, -entry.acquisition)
} }
function addEntry(entry) { function addEntry(entry) {
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
} }
function insertEntry(index, entry) {
acquisitionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
lowerSeries: storageUpperSeries lowerSeries: storageUpperSeries
upperSeries: LineSeries { upperSeries: LineSeries {
@ -244,36 +376,105 @@ Item {
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property var startDatetime: null
Timer { Timer {
interval: 300 interval: 300
running: mouseArea.pressed running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true 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()
} }
onReleased: mouseArea.preventStealing = false
Rectangle { Rectangle {
height: parent.height height: parent.height
width: 1 width: 1
color: Style.foregroundColor color: Style.foregroundColor
x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX)) x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
visible: mouseArea.containsMouse || mouseArea.preventStealing visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
} }
NymeaToolTip { NymeaToolTip {
id: toolTip id: toolTip
visible: mouseArea.containsMouse || mouseArea.preventStealing visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
backgroundItem: chartView backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height) 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 idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
property int seriesIndex: Math.min(productionUpperSeries.count - 1, Math.max(0, productionUpperSeries.count - idx)) 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 xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: productionUpperSeries.at(seriesIndex).y 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) 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 width: tooltipLayout.implicitWidth + Style.smallMargins * 2
@ -287,12 +488,12 @@ Item {
margins: Style.smallMargins margins: Style.smallMargins
} }
Label { Label {
text: new Date(selfConsumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat) text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
font: Style.smallFont font: Style.smallFont
} }
Label { Label {
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, -toolTip.entry.production) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Total production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("Total production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -311,7 +512,7 @@ Item {
Component.onCompleted: lowerSeries = selfConsumptionSeries.lowerSeries Component.onCompleted: lowerSeries = selfConsumptionSeries.lowerSeries
property XYSeries lowerSeries: null property XYSeries lowerSeries: null
property double value: selfConsumptionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, toolTip.entry.consumption) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -331,7 +532,7 @@ Item {
Component.onCompleted: lowerSeries = storageSeries.lowerSeries Component.onCompleted: lowerSeries = storageSeries.lowerSeries
property XYSeries lowerSeries: null property XYSeries lowerSeries: null
property double value: storageUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, toolTip.entry.storage) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("To battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("To battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -350,7 +551,7 @@ Item {
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
property XYSeries lowerSeries: null property XYSeries lowerSeries: null
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y property double value: toolTip.entry ? Math.max(0, -toolTip.entry.acquisition) : 0
property bool translate: value >= 1000 property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1) property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("To grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") text: qsTr("To grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
@ -360,6 +561,11 @@ Item {
} }
} }
} }
}
}
} }

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