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 <QMetaEnum>
#include <QJsonDocument>
#include "logging.h"
NYMEA_LOGGING_CATEGORY(dcEnergyLogs, "EnergyLogs")
@ -59,9 +60,9 @@ void EnergyLogs::setEngine(Engine *engine)
if (m_engine->jsonRpcClient()->experiences().value("Energy").toString() >= "1.0") {
m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceivedInternal");
if (m_ready && !m_loadingInhibited) {
fetchLogs();
}
// if (m_ready && !m_loadingInhibited) {
// fetchLogs();
// }
}
}
}
@ -76,8 +77,7 @@ void EnergyLogs::setSampleRate(SampleRate sampleRate)
if (m_sampleRate != sampleRate) {
m_sampleRate = sampleRate;
emit sampleRateChanged();
fetchLogs();
clear();
}
}
@ -136,9 +136,9 @@ void EnergyLogs::setLoadingInhibited(bool loadingInhibited)
m_loadingInhibited = loadingInhibited;
emit loadingInhibitedChanged();
if (!m_loadingInhibited) {
fetchLogs();
}
// if (!m_loadingInhibited) {
// fetchLogs();
// }
}
}
@ -150,7 +150,7 @@ void EnergyLogs::classBegin()
void EnergyLogs::componentComplete()
{
m_ready = true;
fetchLogs();
// fetchLogs();
}
int EnergyLogs::rowCount(const QModelIndex &parent) const
@ -166,6 +166,16 @@ QVariant EnergyLogs::data(const QModelIndex &index, int role) const
return QVariant();
}
double EnergyLogs::minValue() const
{
return m_minValue;
}
double EnergyLogs::maxValue() const
{
return m_maxValue;
}
EnergyLogEntry *EnergyLogs::get(int index) const
{
if (index < 0 || index >= m_list.count()) {
@ -174,27 +184,54 @@ EnergyLogEntry *EnergyLogs::get(int index) const
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);
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
int index = m_list.count();
beginInsertRows(QModelIndex(), index, index);
m_list.append(entry);
endInsertRows();
emit countChanged();
emit entryAdded(entry);
emit entriesAdded({entry});
emit entryAdded(index, 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)
{
beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + entries.count());
foreach (EnergyLogEntry* entry, entries) {
int index = m_list.count();
beginInsertRows(QModelIndex(), index, index + entries.count());
for (int i = 0; i < entries.count(); i++) {
EnergyLogEntry* entry = entries.at(i);
entry->setParent(this);
m_list.append(entry);
emit entryAdded(entry);
emit entryAdded(index + i, entry);
}
endInsertRows();
emit entriesAdded(entries);
emit entriesAdded(index, entries);
emit countChanged();
}
@ -206,18 +243,92 @@ QVariantMap EnergyLogs::fetchParams() const
void EnergyLogs::getLogsResponse(int commandId, const QVariantMap &params)
{
Q_UNUSED(commandId)
if (!m_list.isEmpty()) {
beginResetModel();
qDeleteAll(m_list);
m_list.clear();
endResetModel();
double minValue = 0, maxValue = 0;
// qCDebug(dcEnergyLogs()) << "Logs response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
QList<EnergyLogEntry*> entries = unpackEntries(params, &minValue, &maxValue);
if (!entries.isEmpty()) {
if (m_list.isEmpty()) {
qCDebug(dcEnergyLogs()) << "Energy logs received";
beginInsertRows(QModelIndex(), 0, entries.count());
m_list.append(entries);
endInsertRows();
emit entriesAdded(0, entries);
m_minValue = minValue;
emit minValueChanged();
m_maxValue = maxValue;
emit maxValueChanged();
} else if (entries.first()->timestamp() < m_list.first()->timestamp()) {
if (entries.last()->timestamp().addSecs(m_sampleRate * 60) == m_list.first()->timestamp()) {
beginInsertRows(QModelIndex(), 0, entries.count());
m_list = entries + m_list;
endInsertRows();
emit entriesAdded(0, entries);
if (minValue < m_minValue) {
m_minValue = minValue;
emit minValueChanged();
}
if (maxValue > m_maxValue) {
m_maxValue = maxValue;
emit maxValueChanged();
}
} else {
// End of fetched entries does not line up with start of existing entries. Discarding existing entries
qCDebug(dcEnergyLogs()) << "End of fetched entries does not line up with start of existing entries. Discarding existing entries" << entries.last()->timestamp().addSecs(m_sampleRate * 60).toString() << " - " << m_list.first()->timestamp().toString();
clear();
beginInsertRows(QModelIndex(), 0, entries.count());
m_list.append(entries);
endInsertRows();
emit entriesAdded(0, entries);
m_minValue = minValue;
emit minValueChanged();
m_maxValue = maxValue;
emit maxValueChanged();
}
} else if (entries.first()->timestamp().addSecs(-m_sampleRate * 60) == m_list.last()->timestamp()) {
int index = m_list.count();
beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + entries.count());
m_list.append(entries);
endInsertRows();
emit entriesAdded(index, entries);
if (minValue < m_minValue) {
m_minValue = minValue;
emit minValueChanged();
}
if (maxValue > m_maxValue) {
m_maxValue = maxValue;
emit maxValueChanged();
}
} else {
// Start of fetched entries does not line up with end of existing entries. Discarding existing entries
clear();
beginInsertRows(QModelIndex(), 0, entries.count());
m_list.append(entries);
endInsertRows();
emit entriesAdded(0, entries);
m_minValue = minValue;
emit minValueChanged();
m_maxValue = maxValue;
emit maxValueChanged();
}
} else {
qCDebug(dcEnergyLogs()) << "Received empty log entries set.";
}
qCDebug(dcEnergyLogs()) << "Energy logs received";
// qCDebug(dcEnergyLogs()) << "Energy logs:" << params;
logEntriesReceived(params);
m_fetchingData = false;
emit fetchingDataChanged();
if (m_fetchAgain) {
qCDebug(dcEnergyLogs()) << "Fetching again...";
m_fetchAgain = false;
fetchLogs();
} else {
emit fetchingDataChanged();
}
}
void EnergyLogs::notificationReceivedInternal(const QVariantMap &data)
@ -234,32 +345,72 @@ void EnergyLogs::notificationReceivedInternal(const QVariantMap &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()
{
if (m_loadingInhibited || !m_ready || !m_engine || m_engine->jsonRpcClient()->experiences().value("Energy").toString() < "1.0") {
return;
}
if (!m_list.isEmpty()) {
beginResetModel();
qDeleteAll(m_list);
m_list.clear();
endResetModel();
if (m_fetchingData) {
qCDebug(dcEnergyLogs()) << "Already busy.. queing up call";
m_fetchAgain = true;
return;
}
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;
fetchingDataChanged();
QVariantMap params = fetchParams();
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;
qCDebug(dcEnergyLogs()) << "Fetching" << m_startTime << m_endTime;
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 fetchingData READ fetchingData NOTIFY fetchingDataChanged)
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:
enum SampleRate {
@ -77,13 +81,19 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) 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* find(const QDateTime &timestamp);
public slots:
void clear();
void fetchLogs();
signals:
void engineChanged();
void sampleRateChanged();
void fetchPowerBalanceChanged();
void thingIdsChanged();
void startTimeChanged();
void endTimeChanged();
void liveChanged();
@ -91,34 +101,40 @@ signals:
void loadingInhibitedChanged();
void countChanged();
void entryAdded(EnergyLogEntry *entry);
void entriesAdded(const QList<EnergyLogEntry*> entries);
void entryAdded(int index, EnergyLogEntry *entry);
void entriesAdded(int index, const QList<EnergyLogEntry*> entries);
void entriesRemoved(int index, int count);
void minValueChanged();
void maxValueChanged();
protected:
virtual QString logsName() const = 0;
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;
void appendEntry(EnergyLogEntry *entry);
void appendEntry(EnergyLogEntry *entry, double minValue, double maxValue);
void appendEntries(const QList<EnergyLogEntry *> &entries);
private slots:
protected slots:
void getLogsResponse(int commandId, const QVariantMap &params);
void notificationReceivedInternal(const QVariantMap &data);
void fetchLogs();
private:
Engine *m_engine = nullptr;
SampleRate m_sampleRate = SampleRate15Mins;
bool m_fetchPowerBalance = true;
QList<QUuid> m_thingIds;
QDateTime m_startTime;
QDateTime m_endTime;
bool m_live = true;
bool m_fetchingData = false;
bool m_loadingInhibited = false;
bool m_ready = false;
bool m_fetchAgain = false;
double m_minValue = 0;
double m_maxValue = 0;
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
{
return "PowerBalanceLogs";
}
void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry)
{
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 *> PowerBalanceLogs::unpackEntries(const QVariantMap &params, double *minValue, double *maxValue)
{
QList<EnergyLogEntry*> ret;
foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) {
QVariantMap map = variant.toMap();
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 totalReturn = map.value("totalReturn").toDouble();
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)
@ -202,7 +119,9 @@ void PowerBalanceLogs::notificationReceived(const QVariantMap &data)
double totalAcquisition = map.value("totalAcquisition").toDouble();
double totalReturn = map.value("totalReturn").toDouble();
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
{
Q_OBJECT
Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged)
Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged)
public:
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:
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;
private:
void addEntry(PowerBalanceLogEntry *entry);
double m_minValue = 0;
double m_maxValue = 0;
};

View File

@ -2,6 +2,9 @@
#include <QMetaEnum>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcEnergyLogs)
ThingPowerLogEntry::ThingPowerLogEntry(QObject *parent):
EnergyLogEntry(parent)
{
@ -39,67 +42,46 @@ double ThingPowerLogEntry::totalProduction() const
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);
}
});
}
}
QList<QUuid> ThingPowerLogs::thingIds() const
ThingPowerLogsLoader *ThingPowerLogs::loader() const
{
return m_thingIds;
return m_loader;
}
void ThingPowerLogs::setThingIds(const QList<QUuid> &thingIds)
void ThingPowerLogs::setLoader(ThingPowerLogsLoader *loader)
{
if (m_thingIds != thingIds) {
m_thingIds = thingIds;
emit thingIdsChanged();
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);
});
}
}
double ThingPowerLogs::minValue() const
ThingPowerLogEntry *ThingPowerLogs::liveEntry()
{
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);
return m_liveEntry;
}
void ThingPowerLogs::addEntries(const QList<ThingPowerLogEntry *> &entries)
@ -128,32 +110,33 @@ QString ThingPowerLogs::logsName() const
QVariantMap ThingPowerLogs::fetchParams() const
{
QVariantList thingIdsStrings;
foreach (const QUuid &id, m_thingIds) {
thingIdsStrings.append(id.toString());
}
QVariantMap ret;
ret.insert("thingIds", thingIdsStrings);
ret.insert("thingIds", QVariantList{m_thingId});
ret.insert("includeCurrent", true);
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()) {
QVariantMap map = variant.toMap();
ThingPowerLogEntry *entry = unpack(map);
if (m_liveEntries.contains(entry->thingId())) {
m_liveEntries[entry->thingId()]->deleteLater();
if (map.value("thingId").toUuid() != m_thingId) {
continue;
}
m_liveEntries[entry->thingId()] = entry;
emit liveEntryChanged(entry);
if (m_liveEntry) {
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<ThingPowerLogEntry*> groupForTimestamp;
QList<EnergyLogEntry*> ret;
foreach (const QVariant &variant, params.value("thingPowerLogEntries").toList()) {
QVariantMap map = variant.toMap();
if (map.value("thingId").toUuid() != m_thingId) {
continue;
}
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
QUuid thingId = map.value("thingId").toUuid();
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);
// qWarning() << "Adding entry:" << entry->thingId() << entry->timestamp().toString() << entry->totalConsumption();
if (groupForTimestamp.isEmpty()) {
groupForTimestamp.append(entry);
} else if (groupForTimestamp.first()->timestamp() == timestamp) {
groupForTimestamp.append(entry);
} else {
// Finalize previous group and start a new one
addEntries(groupForTimestamp);
groupForTimestamp.clear();
groupForTimestamp.append(entry);
}
*minValue = qMin(*minValue, currentPower);
*maxValue = qMax(*maxValue, currentPower);
ret.append(entry);
}
if (!groupForTimestamp.isEmpty()) {
addEntries(groupForTimestamp);
}
return ret;
}
void ThingPowerLogs::notificationReceived(const QVariantMap &data)
@ -189,52 +164,193 @@ void ThingPowerLogs::notificationReceived(const QVariantMap &data)
QVariantMap entryMap = params.value("thingPowerLogEntry").toMap();
QUuid thingId = entryMap.value("thingId").toUuid();
if (!m_thingIds.isEmpty() && !m_thingIds.contains(thingId)) {
if (m_thingId != thingId) {
// Not watching this thing...
return;
}
if (sampleRate != this->sampleRate()) {
return;
}
// We'll use 1 Min samples in any case for the live value
if (sampleRate == EnergyLogs::SampleRate1Min) {
ThingPowerLogEntry *liveEntry = unpack(params.value("thingPowerLogEntry").toMap());
if (m_liveEntries.contains(thingId)) {
m_liveEntries.value(thingId)->deleteLater();
if (m_liveEntry) {
m_liveEntry->deleteLater();
}
m_liveEntries[thingId] = liveEntry;
m_liveEntry = liveEntry;
emit liveEntryChanged(liveEntry);
}
// And append the sample rate we're interested in
if (sampleRate != this->sampleRate()) {
return;
}
if (notification == "Energy.ThingPowerLogEntryAdded") {
QVariantMap map = params.value("thingPowerLogEntry").toMap();
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
QUuid thingId = map.value("thingId").toUuid();
if (!m_thingIds.isEmpty() && !m_thingIds.contains(thingId)) {
return;
}
double currentPower = map.value("currentPower").toDouble();
double totalConsumption = map.value("totalConsumption").toDouble();
double totalProduction = map.value("totalProduction").toDouble();
ThingPowerLogEntry *entry = new ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction, this);
// In order to be easier on resources, we'll batch notifications by grouping them by timestamp
// While the timestamp is the same, just cache the changes. Once the timestamp changes, we'll finalize the
// batch and actually append them.
// Also if we're not getting any more notification for a while and still have cached entries, we'll process the batch
if (m_cachedEntries.isEmpty()) {
m_cachedEntries.append(entry);
} else if (entry->timestamp() == m_cachedEntries.first()->timestamp()) {
m_cachedEntries.append(entry);
} else {
addEntries(m_cachedEntries);
m_cachedEntries.clear();
m_cachedEntries.append(entry);
}
m_cacheTimer.start();
appendEntry(entry, currentPower, currentPower);
}
}
ThingPowerLogsLoader::ThingPowerLogsLoader(QObject *parent):
QObject(parent)
{
}
Engine *ThingPowerLogsLoader::engine() const
{
return m_engine;
}
void ThingPowerLogsLoader::setEngine(Engine *engine)
{
if (m_engine != engine) {
m_engine = engine;
emit engineChanged();
if (!m_engine) {
return;
}
connect(engine, &Engine::destroyed, this, [=](){
if (engine == m_engine) {
m_engine = nullptr;
emit engineChanged();
}
});
}
}
EnergyLogs::SampleRate ThingPowerLogsLoader::sampleRate() const
{
return m_sampleRate;
}
void ThingPowerLogsLoader::setSampleRate(EnergyLogs::SampleRate sampleRate)
{
if (m_sampleRate != sampleRate) {
m_sampleRate = sampleRate;
emit sampleRateChanged();
m_lastStartTime = QDateTime();
m_lastEndTime = QDateTime();
}
}
QDateTime ThingPowerLogsLoader::startTime() const
{
return m_startTime;
}
void ThingPowerLogsLoader::setStartTime(const QDateTime &startTime)
{
if (m_startTime != startTime) {
m_startTime = startTime;
emit startTimeChanged();
}
}
QDateTime ThingPowerLogsLoader::endTime() const
{
return m_endTime;
}
void ThingPowerLogsLoader::setEndTime(const QDateTime &endTime)
{
if (m_endTime != endTime) {
m_endTime = endTime;
emit endTimeChanged();
}
}
bool ThingPowerLogsLoader::fetchingData() const
{
return m_fetchingData;
}
void ThingPowerLogsLoader::addThingId(const QUuid &thingId)
{
if (!m_thingIds.contains(thingId)) {
m_thingIds.append(thingId);
}
}
void ThingPowerLogsLoader::fetchLogs()
{
if (!m_engine || m_engine->jsonRpcClient()->experiences().value("Energy").toString() < "1.0") {
return;
}
if (m_fetchingData) {
qCDebug(dcEnergyLogs()) << "Already busy.. queing up call";
m_fetchAgain = true;
return;
}
QVariantMap params;
QVariantList thingIds;
foreach (const QUuid &thingId, m_thingIds) {
thingIds.append(thingId);
}
params.insert("thingIds", thingIds);
params.insert("includeCurrent", true);
QMetaEnum metaEnum = QMetaEnum::fromType<EnergyLogs::SampleRate>();
params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate));
if (!m_startTime.isNull() && !m_endTime.isNull()) {
QDateTime startTime;
QDateTime endTime;
if (m_lastStartTime.isNull() || m_lastEndTime.isNull()) {
startTime = m_startTime;
endTime = m_endTime;
m_lastStartTime = m_startTime;
m_lastEndTime = m_endTime;
} else {
if (m_startTime < m_lastStartTime) {
startTime = m_startTime;
endTime = m_lastStartTime;
m_lastStartTime = m_startTime;
} else if (m_lastEndTime < m_endTime) {
startTime = m_lastEndTime;
endTime = m_endTime;
m_lastEndTime = m_endTime;
} else {
// Nothing to do...
m_fetchingData = false;
emit fetchingDataChanged();
return;
}
}
params.insert("from", startTime.toSecsSinceEpoch());
params.insert("to", endTime.addSecs(-1).toSecsSinceEpoch());
qCDebug(dcEnergyLogs()) << "Fetching from" << startTime.toString() << "to" << endTime.toString() << "with sample rate" << m_sampleRate;
}
m_fetchingData = true;
fetchingDataChanged();
m_engine->jsonRpcClient()->sendCommand("Energy.GetThingPowerLogs", params, this, "getLogsResponse");
}
void ThingPowerLogsLoader::getLogsResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcEnergyLogs()) << "Logs loader response!";
emit fetched(commandId, params);
m_fetchingData = false;
if (m_fetchAgain) {
m_fetchAgain = false;
fetchLogs();
} else {
emit fetchingDataChanged();
}
}

View File

@ -29,53 +29,98 @@ private:
double m_totalProduction = 0;
};
class ThingPowerLogsLoader;
class ThingPowerLogs : public EnergyLogs
{
Q_OBJECT
Q_PROPERTY(QList<QUuid> thingIds READ thingIds WRITE setThingIds NOTIFY thingIdsChanged)
Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged)
Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged)
Q_PROPERTY(QUuid thingId READ thingId WRITE setThingId NOTIFY thingIdChanged)
Q_PROPERTY(ThingPowerLogsLoader* loader READ loader WRITE setLoader NOTIFY loaderChanged)
public:
explicit ThingPowerLogs(QObject *parent = nullptr);
QList<QUuid> thingIds() const;
void setThingIds(const QList<QUuid> &thingIds);
QUuid thingId() const;
void setThingId(const QUuid &thingId);
double minValue() const;
double maxValue() const;
ThingPowerLogsLoader *loader() const;
void setLoader(ThingPowerLogsLoader *loader);
Q_INVOKABLE ThingPowerLogEntry *find(const QUuid &thingId, const QDateTime &timestamp);
Q_INVOKABLE ThingPowerLogEntry *liveEntry(const QUuid &thingId);
Q_INVOKABLE ThingPowerLogEntry *liveEntry();
signals:
void thingIdsChanged();
void minValueChanged();
void maxValueChanged();
void liveEntryChanged(ThingPowerLogEntry *entry);
void thingIdChanged();
void loaderChanged();
void liveEntryChanged(ThingPowerLogEntry *liveEntry);
protected:
QString logsName() 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;
private:
void addEntry(ThingPowerLogEntry *entry);
void addEntries(const QList<ThingPowerLogEntry *> &entries);
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;
double m_minValue = 0;
double m_maxValue = 0;
bool m_fetchingData = false;
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

View File

@ -383,6 +383,7 @@ void registerQmlTypes() {
qmlRegisterType<PowerBalanceLogEntry>(uri, 1, 0, "PowerBalanceLogEntry");
qmlRegisterType<ThingPowerLogEntry>(uri, 1, 0, "ThingPowerLogEntry");
qmlRegisterType<ThingPowerLogs>(uri, 1, 0, "ThingPowerLogs");
qmlRegisterType<ThingPowerLogsLoader>(uri, 1, 0, "ThingPowerLogsLoader");
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
}

View File

@ -280,5 +280,6 @@
<file>ui/system/zwave/ZWaveAddNetworkPage.qml</file>
<file>ui/system/zwave/ZWaveNetworkPage.qml</file>
<file>ui/system/zwave/ZWaveNetworkSettingsPage.qml</file>
<file>ui/components/ActivityIndicator.qml</file>
</qresource>
</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
readonly property var currentValue: model.hasOwnProperty("get") ? model.get(currentIndex) : model[currentIndex]
signal tabSelected(int index)
Rectangle {
x: repeater.count > 0 ? repeater.itemAt(root.currentIndex).x + 1 : 0
@ -45,6 +47,7 @@ Rectangle {
onClicked: {
print("current index:", index)
root.currentIndex = index
root.tabSelected(index)
}
}
}

View File

@ -150,14 +150,6 @@ MainViewBase {
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 {
Layout.fillWidth: true
Layout.preferredHeight: width

View File

@ -13,229 +13,156 @@ StatsBase {
property ThingsProxy consumers: null
Connections {
target: consumers
onCountChanged: root.update()
}
QtObject {
id: d
Connections {
target: engine.thingManager
onFetchingDataChanged: root.update()
}
Connections {
target: engine.tagsManager
onBusyChanged: root.update()
}
property var config: root.configs[selectionTabs.currentValue.config]
function update() {
if (engine.thingManager.fetchingData || engine.tagsManager.busy || selectionTabs.currentValue === undefined) {
return
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()
}
valueAxis.max = 1
}
powerLogs.loadingInhibited = true
var thingIds = []
for (var i = 0; i < consumers.count; i++) {
thingIds.push(consumers.get(i).id)
onLoadingChanged: {
if (!loading) {
refresh()
}
}
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
function refresh() {
for (var i = 0; i < consumersRepeater.count; i++) {
consumersRepeater.itemAt(i).refresh()
}
}
}
ThingPowerLogs {
id: powerLogs
ThingPowerLogsLoader {
id: logsLoader
engine: _engine
loadingInhibited: true
property var sampleList: null
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
onFetchingDataChanged: {
if (!fetchingData) {
var config = root.configs[selectionTabs.currentValue.config]
print("Logs fetched")
d.fetchPending = false
}
}
}
chartView.reset()
Repeater {
id: consumersRepeater
model: root.consumers
onCountChanged: {
if (count == root.consumers.count) {
logsLoader.fetchLogs();
}
}
// First grouping log entries by timestamp
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)
delegate: Item {
id: consumerDelegate
readonly property Thing thing: root.consumers.get(index)
property BarSet barSet: null
Connections {
target: d
onStartOffsetChanged: refresh()
}
function refreshLabels() {
var values = []
for (var i = 0; i < d.config.count; i++) {
values.push(0)
}
barSet.values = values;
}
var labels = []
var entries = []
var newestLogTimestamp = powerLogs.count > 0 ? powerLogs.get(powerLogs.count - 1).timestamp : new Date();
for (var i = 0; i < config.count; i++) {
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)
function refresh() {
var upcomingTimestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.config.count)
// print("refreshing", consumerDelegate.thing.name ,"config start", d.config.startTime(), "upcoming:", upcomingTimestamp, "fetchPending", d.fetchPending, d.loading)
for (var i = 0; i < d.config.count; i++) {
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i + 1)
var previousTimestamp = root.calculateTimestamp(timestamp, d.config.sampleRate, -1)
// print("timestamp:", timestamp, "previous:", previousTimestamp)
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
}
barSet.replace(i, consumption)
valueAxis.adjustMax(consumption)
// print("Adding live entry", JSON.stringify(liveEntry))
entries.unshift(liveEntry)
}
// Add the actual entry
var graphEntry = {}
var labelTime = new Date();
if (groupedEntry) {
var previousGroupedEntry = groupedEntries[groupedEntries.length - i - 2]
for (var j = 0; j < consumers.count; j++) {
var consumer = consumers.get(j)
var value = groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0
if (previousGroupedEntry) {
var previousValue = previousGroupedEntry.hasOwnProperty(consumer.id) ? previousGroupedEntry[consumer.id] : 0
value -= previousValue
}
graphEntry[consumer.id] = value
valueAxis.adjustMax(value)
} 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 {
for (var j = 0; j < consumers.count; j++) {
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])
barSet.replace(i, 0)
}
}
}
}
onEntriesAdded: {
if (fetchingData) {
return
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) {
return;
}
consumerDelegate.refresh()
}
}
chartView.animationOptions = ChartView.NoAnimation
Component.onCompleted: {
var values = []
for (var i = 0; i < d.config.count; i++) {
values.push(0)
}
for (var i = 0; i < entries.length; i++) {
var entry = entries[i]
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)
barSet = barSeries.append(consumerDelegate.thing.name, values)
barSet.color = NymeaUtils.generateColor(Style.generationBaseColor, index)
barSet.borderColor = barSet.color
barSet.borderWith = 0
}
var labels = categoryAxis.timestamps
labels.splice(0, 1)
labels.push(entries[0].timestamp)
categoryAxis.timestamps = labels
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
}
onLiveEntryChanged: {
if (powerLogs.fetchingData) {
return
}
// print("live entry changed", entry.thingId, entry.timestamp)
var previous = powerLogs.find(entry.thingId, new Date(categoryAxis.timestamps[categoryAxis.timestamps.length - 1]))
var previousValue = previous ? previous.totalConsumption : 0
var barSet = barSeries.thingBarSetMap[entry.thingId]
if (!barSet) {
return
}
barSet.replace(barSet.count - 1, entry.totalConsumption - previousValue)
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("Consumers totals")
}
SelectionTabs {
@ -243,246 +170,347 @@ StatsBase {
Layout.fillWidth: true
Layout.leftMargin: Style.smallMargins
Layout.rightMargin: Style.smallMargins
currentIndex: 0
currentIndex: 1
model: ListModel {
Component.onCompleted: {
append({modelData: qsTr("Hours"), config: "hours" })
append({modelData: qsTr("Days"), config: "days" })
append({modelData: qsTr("Weeks"), config: "weeks" })
append({modelData: qsTr("Months"), config: "months" })
append({modelData: qsTr("Years"), config: "years" })
// append({modelData: qsTr("Minutes"), config: "minutes" })
selectionTabs.currentIndex = 1
}
ListElement { modelData: qsTr("Hours"); config: "hours" }
ListElement { modelData: qsTr("Days"); config: "days" }
ListElement { modelData: qsTr("Weeks"); config: "weeks" }
ListElement { modelData: qsTr("Months"); config: "months" }
ListElement { modelData: qsTr("Years"); config: "years" }
// ListElement { modelData: qsTr("Minutes"); config: "minutes" }
}
onCurrentValueChanged: {
root.update()
onTabSelected: {
d.startOffset = 0
logsLoader.fetchLogs();
}
}
ChartView {
id: chartView
Item {
Layout.fillWidth: true
Layout.fillHeight: true
// margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
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 {} }
}
backgroundColor: "transparent"
legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor
ChartView {
id: chartView
anchors.fill: parent
function reset() {
chartView.animationOptions = ChartView.NoAnimation
barSeries.clear();
valueAxis.max = 0
var map = {}
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
backgroundColor: "transparent"
// margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
legend.alignment: Qt.AlignBottom
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: logsLoader.fetchingData
opacity: .5
}
Label {
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
text: qsTr("No data available")
opacity: {
if (logsLoader.fetchingData || d.startOffset == 0) {
return 0
}
var oldestEntry = new Date().getTime();
var haveItems = false;
for (var i = 0; i < consumersRepeater.count; i++) {
var logsModel = consumersRepeater.itemAt(i).logs
var firstEntry = logsModel.get(0)
if (firstEntry) {
haveItems = true;
oldestEntry = Math.min(oldestEntry, firstEntry.timestamp.getTime())
}
}
print("oldestEntry", new Date(oldestEntry), haveItems)
if (!haveItems || oldestEntry >= d.endTime.getTime()) {
return 0.5
}
return 0;
}
font: Style.smallFont
Behavior on opacity { NumberAnimation {}}
}
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
color: Style.foregroundColor
}
}
}
BarSeries {
id: barSeries
axisX: BarCategoryAxis {
id: categoryAxis
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
gridVisible: false
gridLineColor: Style.tileOverlayColor
lineVisible: false
titleVisible: false
shadesVisible: false
categories: {
var ret = []
print("Updating categories from", d.config.startTime())
for (var i = 0; i < d.config.count; i++) {
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i);
print("*** adding", timestamp, d.startOffset, i)
ret.push(d.config.toLabel(timestamp))
}
return ret;
}
}
axisY: ValueAxis {
id: valueAxis
min: 0
gridLineColor: Style.tileOverlayColor
labelsVisible: false
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
lineVisible: false
titleVisible: false
shadesVisible: false
function adjustMax(newValue) {
if (max < newValue) {
max = Math.ceil(newValue)
}
}
}
}
barSeries.thingBarSetMap = map
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
}
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
color: Style.foregroundColor
}
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
z: -1
Rectangle {
height: parent.height + Style.margins * 2
y: -Style.smallMargins
radius: Style.smallCornerRadius
width: chartView.plotArea.width / categoryAxis.count
color: Style.tileBackgroundColor
property int idx: Math.max(0, Math.min(categoryAxis.count -1, Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)))
visible: toolTip.visible
x: idx * parent.width / categoryAxis.count
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
}
}
BarSeries {
id: barSeries
axisX: BarCategoryAxis {
id: categoryAxis
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
gridVisible: false
gridLineColor: Style.tileOverlayColor
lineVisible: false
titleVisible: false
shadesVisible: false
categories: {
var ret = []
for (var i = 0; i < timestamps.length; i++) {
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
}
return ret
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
property var timestamps: []
}
axisY: ValueAxis {
id: valueAxis
min: 0
gridLineColor: Style.tileOverlayColor
labelsVisible: false
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
lineVisible: false
titleVisible: false
shadesVisible: false
hoverEnabled: true
preventStealing: tooltipping || dragging
function adjustMax(newValue) {
if (max < newValue) {
max = Math.ceil(newValue)
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property int dragStartOffset: 0
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
}
property var thingBarSetMap: ({})
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
z: -1
Rectangle {
height: parent.height + Style.margins * 2
y: -Style.smallMargins
radius: Style.smallCornerRadius
width: chartView.plotArea.width / categoryAxis.count
color: Style.tileBackgroundColor
property int idx: Math.max(0, Math.min(categoryAxis.count -1, Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)))
visible: toolTip.visible
x: idx * parent.width / categoryAxis.count
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true
}
onReleased: mouseArea.preventStealing = false
NymeaToolTip {
id: toolTip
backgroundItem: chartView
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.max(0, Math.min(categoryAxis.count -1, Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)))
visible: mouseArea.containsMouse || mouseArea.preventStealing
property int chartWidth: chartView.plotArea.width
property int barWidth: chartWidth / categoryAxis.count
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
idx * barWidth + barWidth + Style.smallMargins
: idx * barWidth - Style.smallMargins - width
property double setMaxValue: {
var max = 0;
for (var i = 0; i < consumers.count; i++) {
var consumer = consumers.get(i)
max = barSeries.thingBarSetMap.hasOwnProperty(consumer.id) ? Math.max(max, barSeries.thingBarSetMap[consumer.id].at(idx)) : 0
}
return max
}
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
font: Style.smallFont
}
Repeater {
model: ListModel {
id: toolTipModel
property var entries: {
var unsorted = []
for (var i = 0; i < consumers.count; i++) {
var consumer = consumers.get(i)
var entry = {
name: consumer.name,
value: barSeries.thingBarSetMap[consumer.id].at(toolTip.idx).toFixed(2),
indexInModel: i
}
unsorted.push(entry)
}
return unsorted
}
onEntriesChanged: {
clear();
var unsorted = entries;
for (var i = 0; i < unsorted.length; i++) {
var j = 0;
while (j < count && get(j).value > unsorted[i].value) {
j++;
}
insert(j, unsorted[i])
}
}
onReleased: {
if (mouseArea.dragging) {
logsLoader.fetchLogs();
d.refresh()
mouseArea.dragging = false;
}
mouseArea.tooltipping = false;
}
delegate: RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
// color: root.colors[model.indexInModel % root.colors.length]
color: NymeaUtils.generateColor(Style.generationBaseColor, model.indexInModel)
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 {
id: toolTip
backgroundItem: chartView
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
property date timestamp: root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
property int chartWidth: chartView.plotArea.width
property int barWidth: chartWidth / categoryAxis.count
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
idx * barWidth + barWidth + Style.smallMargins
: idx * barWidth - Style.smallMargins - width
property double setMaxValue: {
var max = 0;
for (var i = 0; i < consumersRepeater.count; i++) {
max = Math.max(max, consumersRepeater.itemAt(i).barSet.at(idx))
}
return max
}
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: "%1: %2 kWh".arg(model.name).arg(model.value)
font: Style.extraSmallFont
text: d.config.toLongLabel(toolTip.timestamp)
font: Style.smallFont
}
Repeater {
model: ListModel {
id: toolTipModel
property var entries: {
var unsorted = []
for (var i = 0; i < consumers.count; i++) {
var consumer = consumers.get(i)
var entry = {
name: consumer.name,
value: consumersRepeater.itemAt(i).barSet.at(toolTip.idx).toFixed(2),
indexInModel: i
}
unsorted.push(entry)
}
return unsorted
}
onEntriesChanged: {
clear();
var unsorted = entries;
for (var i = 0; i < unsorted.length; i++) {
var j = 0;
while (j < count && get(j).value > unsorted[i].value) {
j++;
}
insert(j, unsorted[i])
}
}
}
delegate: RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
// color: root.colors[model.indexInModel % root.colors.length]
color: NymeaUtils.generateColor(Style.generationBaseColor, model.indexInModel)
}
Label {
text: "%1: %2 kWh".arg(model.name).arg(model.value)
font: Style.extraSmallFont
}
}
}
}
}
}
}
}
}

View File

@ -11,142 +11,62 @@ Item {
property var colors: null
property ThingsProxy consumers: null
Connections {
target: consumers
onCountChanged: d.updateConsumers()
}
Connections {
target: engine.tagsManager
onBusyChanged: d.updateConsumers()
}
ThingPowerLogs {
id: thingPowerLogs
PowerBalanceLogs {
id: powerBalanceLogs
engine: _engine
startTime: dateTimeAxis.min
sampleRate: EnergyLogs.SampleRate15Mins
thingIds: []
loadingInhibited: thingIds.length === 0
onModelReset: {
for (var i = 0; i < consumers.count; i++) {
var consumer = consumers.get(i);
var series = d.thingsSeriesMap[consumer.id];
series.upperSeries.clear()
}
}
startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate
Component.onCompleted: fetchLogs()
onEntriesAdded: {
var thingValues = ({})
var timestamp = entries[0].timestamp
print("entries added", index, entries.length)
for (var i = 0; i < entries.length; i++) {
var entry = entries[i]
var thing = engine.thingManager.things.getThing(entries[i].thingId)
thingValues[entry.thingId] = entry.currentPower
}
// print("got entry", entry.timestamp)
// Add them in the order of the chart (same as proxy), summing it up
var totalValue = 0;
for (var i = 0; i < consumers.count; i++) {
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)
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
}
}
thingPowerLogs.maxValue = Math.max(thingPowerLogs.maxValue, totalValue)
}
property double maxValue: 0
onEntriesRemoved: {
consumptionUpperSeries.removePoints(index, count)
zeroSeries.shrink()
}
}
property PowerBalanceLogs powerBalanceLogs: PowerBalanceLogs {
ThingPowerLogsLoader {
id: logsLoader
engine: _engine
startTime: dateTimeAxis.min
sampleRate: EnergyLogs.SampleRate15Mins
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();
startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate
}
QtObject {
id: d
property var thingsSeriesMap: ({})
function updateConsumers() {
if (engine.thingManager.fetchingData || engine.tagsManager.busy) {
return;
}
thingPowerLogs.loadingInhibited = true;
property date now: new Date()
for (var thingId in d.thingsSeriesMap) {
chartView.removeSeries(d.thingsSeriesMap[thingId])
}
d.thingsSeriesMap = ({})
readonly property int range: selectionTabs.currentValue.range
readonly property int sampleRate: selectionTabs.currentValue.sampleRate
readonly property int visibleValues: range / sampleRate
var consumerThingIds = []
for (var i = 0; i < consumers.count; i++) {
var thing = consumers.get(i);
readonly property var startTime: {
var date = new Date(now);
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)
series.lowerSeries = baseSeries
series.upperSeries = lineSeriesComponent.createObject(series)
// series.color = root.colors[i % root.colors.length]
series.color = NymeaUtils.generateColor(Style.generationBaseColor, i)
series.borderWidth = 0;
series.borderColor = series.color
var map = d.thingsSeriesMap
map[thing.id] = series
d.thingsSeriesMap = map
consumerThingIds.push(thing.id)
}
thingPowerLogs.thingIds = consumerThingIds;
thingPowerLogs.loadingInhibited = false;
readonly property var endTime: {
var date = new Date(now);
date.setTime(date.getTime() + 2000)
return date;
}
}
@ -155,202 +75,497 @@ Item {
LineSeries { }
}
ChartView {
id: chartView
ColumnLayout {
anchors.fill: parent
spacing: 0
backgroundColor: "transparent"
margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
title: qsTr("Consumers history")
titleColor: Style.foregroundColor
legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont
ValueAxis {
id: valueAxis
min: 0
max: Math.ceil(Math.max(powerBalanceLogs.maxValue, thingPowerLogs.maxValue) / 1000) * 1000
labelFormat: ""
gridLineColor: Style.tileOverlayColor
labelsVisible: false
lineVisible: false
titleVisible: false
shadesVisible: false
// visible: false
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 {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
}
Layout.fillWidth: true
Layout.fillHeight: true
DateTimeAxis {
id: dateTimeAxis
property date now: new Date()
min: {
var date = new Date(now);
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000);
return date;
}
max: {
var date = new Date(now);
date.setTime(date.getTime() + 2000)
return date;
}
format: "hh:mm"
labelsFont: Style.extraSmallFont
gridVisible: false
minorGridVisible: false
lineVisible: false
shadesVisible: false
labelsColor: Style.foregroundColor
}
AreaSeries {
id: consumptionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.gray
borderWidth: 0
borderColor: color
name: qsTr("Unknown")
lowerSeries: LineSeries {
id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function update(timestamp) {
append(timestamp, 0);
removePoints(1,1);
}
}
upperSeries: LineSeries {
id: consumptionUpperSeries
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 {} }
}
function addEntry(entry) {
consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption)
}
}
ChartView {
id: chartView
anchors.fill: parent
}
backgroundColor: "transparent"
margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.plotArea.x
anchors.topMargin: chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor
hoverEnabled: true
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true
}
onReleased: mouseArea.preventStealing = false
Rectangle {
height: parent.height
width: 1
color: Style.foregroundColor
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
visible: mouseArea.containsMouse || mouseArea.preventStealing
}
NymeaToolTip {
id: toolTip
visible: mouseArea.containsMouse || mouseArea.preventStealing
backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: consumptionUpperSeries.count - Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width)
property int seriesIndex: Math.min(consumptionUpperSeries.count - 1, Math.max(0, consumptionUpperSeries.count - idx))
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: consumptionUpperSeries.at(seriesIndex).y
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
property date timestamp: new Date(consumptionUpperSeries.at(seriesIndex).x)
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
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 {
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
text: qsTr("No data available")
visible: !powerBalanceLogs.fetchingData && !logsLoader.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
font: Style.smallFont
opacity: .5
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: consumptionSeries.color
}
Label {
property double rawValue: consumptionUpperSeries.at(toolTip.seriesIndex).y
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W"
text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit)
font: Style.extraSmallFont
ValueAxis {
id: valueAxis
min: 0
max: 1
labelFormat: ""
gridLineColor: Style.tileOverlayColor
labelsVisible: false
lineVisible: false
titleVisible: false
shadesVisible: false
// visible: false
function adjustMax(value) {
max = Math.max(max, Math.ceil(value / 100) * 100)
}
}
Repeater {
model: consumers
delegate: RowLayout {
id: consumerToolTipDelegate
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
// color: index >= 0 ? root.colors[index % root.colors.length] : "white"
color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white"
}
Label {
property ThingPowerLogEntry entry: thingPowerLogs.find(model.id, toolTip.timestamp)
property double rawValue: entry ? entry.currentPower : 0
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W"
text: "%1: %2 %3".arg(model.name).arg(displayValue.toFixed(2)).arg(unit)
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
}
DateTimeAxis {
id: dateTimeAxis
min: d.startTime
max: d.endTime
format: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return "hh:mm"
case EnergyLogs.SampleRate1Hour:
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return "dd.MM."
}
}
tickCount: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return root.width > 500 ? 13 : 7
case EnergyLogs.SampleRate1Hour:
return 7
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return root.width > 500 ? 12 : 6
}
}
labelsFont: Style.extraSmallFont
gridVisible: false
minorGridVisible: false
lineVisible: false
shadesVisible: false
labelsColor: Style.foregroundColor
}
AreaSeries {
id: consumptionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.gray
borderWidth: 0
borderColor: color
name: qsTr("Unknown")
// visible: false
opacity: .2
lowerSeries: LineSeries {
id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function ensureValue(timestamp) {
if (count == 0) {
append(timestamp, 0)
} else if (count == 1) {
if (timestamp.getTime() < at(0).x) {
insert(0, timestamp, 0)
} else {
append(timestamp, 0)
}
} else {
if (timestamp.getTime() < at(0).x) {
remove(0)
insert(0, timestamp, 0)
} else if (timestamp.getTime() > at(1).x) {
remove(1)
append(timestamp, 0)
}
}
}
function shrink() {
clear();
if (powerBalanceLogs.count > 0) {
ensureValue(powerBalanceLogs.get(0).timestamp)
ensureValue(powerBalanceLogs.get(powerBalanceLogs.count - 1).timestamp)
}
}
}
upperSeries: LineSeries {
id: consumptionUpperSeries
}
function addEntry(entry) {
consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption)
}
function insertEntry(index, entry) {
consumptionUpperSeries.insert(index, entry.timestamp.getTime(), entry.consumption)
}
}
Repeater {
id: consumersRepeater
model: consumers
onCountChanged: {
if (count == consumers.count) {
logsLoader.fetchLogs()
}
}
delegate: Item {
id: consumerDelegate
readonly property Thing thing: consumers.get(index)
property AreaSeries series: null
function getBaseValue(timestamp) {
var ret = 0
if (index > 0) {
ret = consumersRepeater.itemAt(index - 1).getBaseValue(timestamp)
}
var entry = logs.find(timestamp)
if (entry) {
ret += entry.currentPower;
}
return ret
}
function insertEntry(idx, entry) {
var baseValue = 0;
if (index > 0) {
baseValue = consumersRepeater.itemAt(index - 1).getBaseValue(entry.timestamp)
}
series.upperSeries.insert(idx, entry.timestamp.getTime(), entry.currentPower + baseValue)
}
readonly property ThingPowerLogs logs: ThingPowerLogs {
engine: _engine
startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate
thingId: consumerDelegate.thing.id
loader: logsLoader
Component.onCompleted: print("thingpowerlogs completed")
onLoadingInhibitedChanged: print("Loading...", consumerDelegate.thing.name)
onEntriesAdded: {
print("Thing entries added", consumerDelegate.thing.name, index, entries.length)
for (var i = 0; i < entries.length; i++) {
var entry = entries[i]
// print("got thing entry", thing.name, entry.timestamp, entry.currentPower, index + i)
zeroSeries.ensureValue(entry.timestamp)
valueAxis.adjustMax(entry.currentPower)
consumerDelegate.insertEntry(index + i, entry)
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
d.now = entry.timestamp
}
}
}
onEntriesRemoved: {
consumerDelegate.series.upperSeries.removePoints(index, count)
zeroSeries.shrink()
}
}
Component.onCompleted: {
series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, valueAxis)
series.lowerSeries = index == 0 ? zeroSeries : consumersRepeater.itemAt(index - 1).series.upperSeries
series.upperSeries = lineSeriesComponent.createObject(series)
series.color = NymeaUtils.generateColor(Style.generationBaseColor, index)
series.borderWidth = 0;
series.borderColor = series.color
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.plotArea.x
anchors.topMargin: chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property var startDatetime: null
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
}
onReleased: {
mouseArea.tooltipping = false;
if (mouseArea.dragging) {
mouseArea.dragging = false;
for (var i = 0; i < consumersRepeater.count; i++) {
if (consumersRepeater.itemAt(i).logs.fetchingData) {
wheelStopTimer.start()
return;
}
}
powerBalanceLogs.fetchLogs()
logsLoader.fetchLogs()
// for (var i = 0; i < consumersRepeater.count; i++) {
// consumersRepeater.itemAt(i).logs.fetchLogs()
// }
}
}
onPressed: {
startMouseX = mouseX
startDatetime = d.now
}
onDoubleClicked: {
if (selectionTabs.currentIndex == 0) {
return;
}
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
selectionTabs.currentIndex--
d.now = new Date(timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)
powerBalanceLogs.fetchLogs()
logsLoader.fetchLogs()
}
onMouseXChanged: {
if (!pressed || mouseArea.tooltipping) {
return;
}
if (Math.abs(startMouseX - mouseX) < 10) {
return;
}
dragging = true
var dragDelta = startMouseX - mouseX
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// dragDelta : timeDelta = width : totalTime
var timeDelta = dragDelta * totalTime / mouseArea.width
print("dragging", dragDelta, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
}
onWheel: {
startDatetime = d.now
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// pixelDelta : timeDelta = width : totalTime
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
wheelStopTimer.restart()
}
Timer {
id: wheelStopTimer
interval: 300
repeat: false
onTriggered: {
for (var i = 0; i < consumersRepeater.count; i++) {
if (consumersRepeater.itemAt(i).logs.fetchingData) {
wheelStopTimer.start()
return;
}
}
powerBalanceLogs.fetchLogs()
logsLoader.fetchLogs()
// for (var i = 0; i < consumersRepeater.count; i++) {
// consumersRepeater.itemAt(i).logs.fetchLogs()
// }
}
}
Rectangle {
height: parent.height
width: 1
color: Style.foregroundColor
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
}
NymeaToolTip {
id: toolTip
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
property date timestamp: new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: toolTip.entry ? Math.max(0, entry.consumption) : 0
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
font: Style.smallFont
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: consumptionSeries.color
}
Label {
property double rawValue: toolTip.entry ? toolTip.entry.consumption : 0
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W"
text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit)
font: Style.extraSmallFont
}
}
Repeater {
model: consumers
delegate: RowLayout {
id: consumerToolTipDelegate
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
// color: index >= 0 ? root.colors[index % root.colors.length] : "white"
color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white"
}
Label {
property ThingPowerLogEntry entry: toolTip.idx >= 0 ? consumersRepeater.itemAt(index).logs.find(toolTip.timestamp) : null
property double rawValue: entry ? entry.currentPower : 0
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W"
text: "%1: %2 %3".arg(model.name).arg(displayValue.toFixed(2)).arg(unit)
font: Style.extraSmallFont
}
}
}
}
}
}
}
}
}

View File

@ -19,48 +19,98 @@ StatsBase {
QtObject {
id: d
property BarSet consumptionSet: null
property BarSet productionSet: null
property BarSet acquisitionSet: null
property BarSet returnSet: null
}
property var config: root.configs[selectionTabs.currentValue.config]
property int startOffset: 0
function reload() {
if (selectionTabs.currentValue === undefined) {
return
}
if (engine.thingManager.fetchingData) {
return;
property date startTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset)
property date endTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset + config.count)
property bool fetchPending: false
property bool loading: fetchPending || wheelStopTimer.running || powerBalanceLogs.fetchingData
onLoadingChanged: {
if (!loading) {
refresh()
}
}
var config = root.configs[selectionTabs.currentValue.config]
print("Loading Power Balance Stats with config:", config.startTime(), config.sampleRate)
onConfigChanged: valueAxis.max = 1
onStartOffsetChanged: {
// print("updating because of offset change. fetchingData", powerBalanceLogs.fetchingData, "fetchPending", d.fetchPending)
refresh()
}
function refresh() {
if (powerBalanceLogs.loadingInhibited) {
return;
}
powerBalanceLogs.loadingInhibited = true
powerBalanceLogs.sampleRate = config.sampleRate
powerBalanceLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
powerBalanceLogs.loadingInhibited = false
chartView.reset();
}
Connections {
target: engine.thingManager
onFetchingDataChanged: {
print("Thingmanager loaded", engine.thingManager.fetchingData)
if (!engine.thingManager.fetchingData) root.reload()
var upcomingTimestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.config.count)
// print("refreshing config start", d.config.startTime(), "upcoming:", upcomingTimestamp, "fetchPending", d.fetchPending)
for (var i = 0; i < d.config.count; i++) {
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i + 1)
var previousTimestamp = root.calculateTimestamp(timestamp, d.config.sampleRate, -1)
// print("timestamp:", timestamp)
var entry = powerBalanceLogs.find(timestamp)
var previousEntry = powerBalanceLogs.find(previousTimestamp);
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)
}
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("Totals")
}
SelectionTabs {
@ -68,422 +118,419 @@ StatsBase {
Layout.fillWidth: true
Layout.leftMargin: Style.smallMargins
Layout.rightMargin: Style.smallMargins
currentIndex: 1
model: ListModel {
Component.onCompleted: {
append({modelData: qsTr("Hours"), config: "hours" })
append({modelData: qsTr("Days"), config: "days" })
append({modelData: qsTr("Weeks"), config: "weeks" })
append({modelData: qsTr("Months"), config: "months" })
append({modelData: qsTr("Years"), config: "years" })
// append({modelData: qsTr("Minutes"), config: "minutes" })
selectionTabs.currentIndex = 1
}
ListElement { modelData: qsTr("Hours"); config: "hours" }
ListElement { modelData: qsTr("Days"); config: "days" }
ListElement { modelData: qsTr("Weeks"); config: "weeks" }
ListElement { modelData: qsTr("Months"); config: "months" }
ListElement { modelData: qsTr("Years"); config: "years" }
// ListElement { modelData: qsTr("Minutes"); config: "minutes" }
}
onCurrentValueChanged: {
root.reload()
onTabSelected: {
d.startOffset = 0
powerBalanceLogs.fetchLogs()
}
}
Connections {
target: energyManager
onPowerBalanceChanged: {
var start = powerBalanceLogs.get(powerBalanceLogs.count - 1 )
// print("balance changed:", d.consumptionSet, powerBalanceLogs, powerBalanceLogs.count)
// 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)
// print("updating because of power balance change. fetchingData", powerBalanceLogs.fetchingData, "fetchPending", d.fetchPending)
d.refresh();
}
}
PowerBalanceLogs {
id: powerBalanceLogs
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: {
if (!fetchingData) {
chartView.animationOptions = ChartView.NoAnimation
chartView.reset();
print("Logs fetched")
var config = root.configs[selectionTabs.currentValue.config]
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)
}
d.fetchPending = false
d.refresh()
}
}
onEntryAdded: {
onEntriesAdded: {
if (fetchingData) {
return
}
// print("Entry added")
var config = root.configs[selectionTabs.currentValue.config]
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
// Update the timeline by faking a left/right scroll
d.startOffset--
d.startOffset++
//d.refresh()
}
}
ChartView {
id: chartView
Item {
Layout.fillWidth: true
Layout.fillHeight: true
animationOptions: ChartView.NoAnimation
backgroundColor: "transparent"
legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor
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 {} }
}
// margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
ChartView {
id: chartView
animationOptions: ChartView.NoAnimation
anchors.fill: parent
function reset() {
barSeries.clear();
valueAxis.max = 0
if (root.hasProducers) {
d.consumptionSet = barSeries.append(qsTr("Consumed"), [])
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
backgroundColor: "transparent"
legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor
// margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
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.endTime) && d.startOffset != 0
font: Style.smallFont
opacity: .5
Behavior on opacity { NumberAnimation {}}
}
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
}
BarSeries {
id: barSeries
axisX: BarCategoryAxis {
id: categoryAxis
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
gridVisible: false
gridLineColor: Style.tileOverlayColor
lineVisible: false
titleVisible: false
shadesVisible: false
categories: {
var ret = []
print("Updating categories from", d.config.startTime())
for (var i = 0; i < d.config.count; i++) {
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i);
print("*** adding", timestamp, d.startOffset, i)
ret.push(d.config.toLabel(timestamp))
}
return ret;
}
}
axisY: ValueAxis {
id: valueAxis
min: 0
gridLineColor: Style.tileOverlayColor
labelsVisible: false
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
lineVisible: false
titleVisible: false
shadesVisible: false
function adjustMax(newValue) {
if (max < newValue) {
print("adjusting to new max", newValue)
max = newValue // Math.ceil(newValue / 100) * 100
}
}
}
BarSet {
id: consumptionSet
label: qsTr("Consumed")
color: Style.blue
borderColor: color
borderWidth: 0
values: {
var ret = []
for (var i = 0; i < d.config.count; i++) {
ret.push(0)
}
return ret
}
}
BarSet {
id: productionSet
label: qsTr("Produced")
color: Style.yellow
borderColor: color
borderWidth: 0
values: {
var ret = []
for (var i = 0; i < d.config.count; i++) {
ret.push(0)
}
return ret
}
}
BarSet {
id: acquisitionSet
label: qsTr("From grid")
color: Style.red
borderColor: color
borderWidth: 0
values: {
var ret = []
for (var i = 0; i < d.config.count; i++) {
ret.push(0)
}
return ret
}
}
BarSet {
id: returnSet
label: qsTr("To grid")
color: Style.green
borderColor: color
borderWidth: 0
values: {
var ret = []
for (var i = 0; i < d.config.count; i++) {
ret.push(0)
}
return ret
}
}
}
d.acquisitionSet = barSeries.append(qsTr("From grid"), [])
d.acquisitionSet.color = Style.red
d.acquisitionSet.borderColor = d.acquisitionSet.color
d.acquisitionSet.borderWidth = 0
d.returnSet = barSeries.append(qsTr("To grid"), [])
d.returnSet.color = Style.green
d.returnSet.borderColor = d.returnSet.color
d.returnSet.borderWidth = 0
}
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
z: -1
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
Rectangle {
height: parent.height + Style.margins * 2
y: -Style.smallMargins
radius: Style.smallCornerRadius
width: chartView.plotArea.width / categoryAxis.count
color: Style.tileBackgroundColor
property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1)
visible: toolTip.visible
x: idx * parent.width / categoryAxis.count
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
}
}
BarSeries {
id: barSeries
axisX: BarCategoryAxis {
id: categoryAxis
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
gridVisible: false
gridLineColor: Style.tileOverlayColor
lineVisible: false
titleVisible: false
shadesVisible: false
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
categories: {
var ret = []
for (var i = 0; i < timestamps.length; i++) {
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
}
return ret
}
hoverEnabled: true
preventStealing: tooltipping || dragging
property var timestamps: []
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property int dragStartOffset: 0
}
axisY: ValueAxis {
id: valueAxis
min: 0
gridLineColor: Style.tileOverlayColor
labelsVisible: false
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
lineVisible: false
titleVisible: false
shadesVisible: false
function adjustMax(newValue) {
if (max < newValue) {
max = newValue // Math.ceil(newValue / 100) * 100
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
}
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
z: -1
onReleased: {
if (mouseArea.dragging) {
powerBalanceLogs.fetchLogs()
mouseArea.dragging = false;
}
Rectangle {
height: parent.height + Style.margins * 2
y: -Style.smallMargins
radius: Style.smallCornerRadius
width: chartView.plotArea.width / categoryAxis.count
color: Style.tileBackgroundColor
property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1)
visible: toolTip.visible
x: idx * parent.width / categoryAxis.count
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.x + chartView.plotArea.x
anchors.topMargin: chartView.y + chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true
}
onReleased: mouseArea.preventStealing = false
NymeaToolTip {
id: toolTip
backgroundItem: chartView
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1)
visible: mouseArea.containsMouse || mouseArea.preventStealing
property int chartWidth: chartView.plotArea.width
property int barWidth: chartWidth / categoryAxis.count
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
idx * barWidth + barWidth + Style.smallMargins
: idx * barWidth - Style.smallMargins - width
property double setMaxValue: Math.max(d.consumptionSet ? d.consumptionSet.at(idx) : 0,
d.productionSet ? d.productionSet.at(idx) : 0,
d.acquisitionSet ? d.acquisitionSet.at(idx) : 0,
d.returnSet ? d.returnSet.at(idx) : 0)
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
mouseArea.tooltipping = false;
}
Label {
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
font: Style.smallFont
onPressed: {
startMouseX = mouseX
dragStartOffset = d.startOffset
}
RowLayout {
visible: root.hasProducers
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.blue
}
Label {
text: toolTip.visible && d.consumptionSet ? qsTr("Consumed: %1 kWh").arg(d.consumptionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
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();
}
RowLayout {
visible: root.hasProducers
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.yellow
onMouseXChanged: {
if (!pressed || mouseArea.tooltipping) {
return;
}
Label {
text: toolTip.visible && d.productionSet ? qsTr("Produced: %1 kWh").arg(d.productionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
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;
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.red
property int wheelDelta: 0
onWheel: {
wheelDelta += wheel.pixelDelta.x
var slotWidth = mouseArea.width / d.config.count
while (wheelDelta > slotWidth) {
d.startOffset--
wheelDelta -= slotWidth
}
Label {
text: toolTip.visible && d.acquisitionSet ? qsTr("From grid: %1 kWh").arg(d.acquisitionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
while (wheelDelta < -slotWidth) {
d.startOffset = Math.min(d.startOffset + 1, 0)
wheelDelta += slotWidth
}
d.fetchPending = true;
wheelStopTimer.restart()
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.green
}
Label {
text: toolTip.visible && d.returnSet ? qsTr("To grid: %1 kWh").arg(d.returnSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
Timer {
id: wheelStopTimer
interval: 300
repeat: false
onTriggered: powerBalanceLogs.fetchLogs()
}
NymeaToolTip {
id: toolTip
backgroundItem: chartView
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
property date timestamp: root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
property int chartWidth: chartView.plotArea.width
property int barWidth: chartWidth / categoryAxis.count
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
idx * barWidth + barWidth + Style.smallMargins
: idx * barWidth - Style.smallMargins - width
property double setMaxValue: d.startOffset !== undefined ? Math.max(consumptionSet.at(idx),
productionSet.at(idx),
acquisitionSet.at(idx),
returnSet.at(idx)) : 0
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: d.config.toLongLabel(toolTip.timestamp)
font: Style.smallFont
}
RowLayout {
visible: root.hasProducers
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.blue
}
Label {
text: d.startOffset !== undefined ? qsTr("Consumed: %1 kWh").arg(consumptionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
}
RowLayout {
visible: root.hasProducers
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.yellow
}
Label {
text: d.startOffset !== undefined ? qsTr("Produced: %1 kWh").arg(productionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.red
}
Label {
text: d.startOffset !== undefined ? qsTr("From grid: %1 kWh").arg(acquisitionSet.at(toolTip.idx).toFixed(2)) :""
font: Style.extraSmallFont
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.green
}
Label {
text: d.startOffset !== undefined ? qsTr("To grid: %1 kWh").arg(returnSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
}
}
}
}
}
}
}

View File

@ -2,18 +2,19 @@ import QtQuick 2.0
import QtCharts 2.2
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0
import Nymea 1.0
import "qrc:/ui/components"
Item {
id: root
property PowerBalanceLogs energyLogs: PowerBalanceLogs {
PowerBalanceLogs {
id: powerBalanceLogs
engine: _engine
startTime: dateTimeAxis.min
sampleRate: EnergyLogs.SampleRate15Mins
startTime: new Date(d.startTime.getTime() - (d.range * 60 * 1000))
endTime: new Date(d.endTime.getTime() + (d.range * 60 * 1000))
sampleRate: d.sampleRate
Component.onCompleted: fetchLogs()
}
property ThingsProxy batteries: ThingsProxy {
@ -21,350 +22,552 @@ Item {
shownInterfaces: ["energystorage"]
}
Component.onCompleted: {
for (var i = 0; i < powerBalanceLogs.count; i++) {
var entry = energyLogs.powerBalanceLogs.get(i);
consumptionSeries.addEntry(entry)
selfProductionSeries.addEntry(entry)
storageSeries.addEntry(entry)
acquisitionSeries.addEntry(entry)
QtObject {
id: d
property date now: new Date()
readonly property int range: selectionTabs.currentValue.range
readonly property int sampleRate: selectionTabs.currentValue.sampleRate
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 {
target: powerBalanceLogs
onEntryAdded: {
consumptionSeries.addEntry(entry)
selfProductionSeries.addEntry(entry)
storageSeries.addEntry(entry)
acquisitionSeries.addEntry(entry)
if (dateTimeAxis.now < entry.timestamp) {
dateTimeAxis.now = entry.timestamp
zeroSeries.update(entry.timestamp)
onEntriesAdded: {
// print("entries added", index, entries.length)
for (var i = 0; i < entries.length; i++) {
var entry = entries[i]
// print("got entry", entry.timestamp)
zeroSeries.ensureValue(entry.timestamp)
// For debugging, to see if the other maths line up with the plain production graph
// consumptionSeries.insertEntry(index + i, entry)
selfProductionSeries.insertEntry(index + i, entry)
storageSeries.insertEntry(index + i, entry)
acquisitionSeries.insertEntry(index + i, entry)
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
d.now = entry.timestamp
}
}
}
onEntriesRemoved: {
acquisitionUpperSeries.removePoints(index, count)
storageUpperSeries.removePoints(index, count)
selfProductionUpperSeries.removePoints(index, count)
consumptionUpperSeries.removePoints(index, count)
zeroSeries.shrink()
}
}
Timer {
interval: 60000
repeat: true
onTriggered: {
var now = new Date()
if (dateTimeAxis.now < now) {
dateTimeAxis.now = now
zeroSeries.update(now)
}
}
}
ChartView {
id: chartView
ColumnLayout {
anchors.fill: parent
backgroundColor: "transparent"
margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
spacing: 0
title: qsTr("My consumption history")
titleColor: Style.foregroundColor
legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont
ValueAxis {
id: valueAxis
min: 0
max: Math.ceil(powerBalanceLogs.maxValue / 1000) * 1000
labelFormat: ""
gridLineColor: Style.tileOverlayColor
labelsVisible: false
lineVisible: false
titleVisible: false
shadesVisible: false
// visible: false
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 {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
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 {
id: chartView
anchors.fill: parent
backgroundColor: "transparent"
margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
DateTimeAxis {
id: dateTimeAxis
property date now: new Date()
min: {
var date = new Date(now);
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000);
return date;
}
max: {
var date = new Date(now);
date.setTime(date.getTime() + 2000)
return date;
}
format: "hh:mm"
labelsFont: Style.extraSmallFont
gridVisible: false
minorGridVisible: false
lineVisible: false
shadesVisible: false
labelsColor: Style.foregroundColor
}
legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont
// For debugging, to see the total graph and check if the other maths line up
AreaSeries {
id: consumptionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: "blue"
borderWidth: 0
borderColor: color
opacity: .5
visible: false
lowerSeries: zeroSeries
upperSeries: LineSeries {
id: consumptionUpperSeries
}
function calculateValue(entry) {
return entry.consumption
}
function addEntry(entry) {
consumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
}
AreaSeries {
id: selfProductionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.green
borderWidth: 0
borderColor: color
name: qsTr("Self production")
// visible: false
lowerSeries: LineSeries {
id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function update(timestamp) {
append(timestamp, 0);
removePoints(1,1);
}
}
upperSeries: LineSeries {
id: selfProductionUpperSeries
}
function calculateValue(entry) {
var value = entry.consumption - Math.max(0, entry.acquisition);
if (entry.storage < 0) {
value += entry.storage;
}
return value;
}
function addEntry(entry) {
selfProductionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
}
AreaSeries {
id: storageSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.orange
borderWidth: 0
borderColor: color
name: qsTr("From battery")
visible: root.batteries.count > 0
lowerSeries: selfProductionUpperSeries
upperSeries: LineSeries {
id: storageUpperSeries
}
function calculateValue(entry) {
return selfProductionSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.storage));
}
function addEntry(entry) {
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
}
AreaSeries {
id: acquisitionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.red
borderWidth: 0
borderColor: color
name: qsTr("From grid")
// visible: false
lowerSeries: storageUpperSeries
upperSeries: LineSeries {
id: acquisitionUpperSeries
}
function calculateValue(entry) {
return storageSeries.calculateValue(entry) + Math.max(0, entry.acquisition)
}
function addEntry(entry) {
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
}
}
MouseArea {
id: mouseArea
anchors.fill: chartView
anchors.leftMargin: chartView.plotArea.x
anchors.topMargin: chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true
}
onReleased: mouseArea.preventStealing = false
Rectangle {
height: parent.height
width: 1
color: Style.foregroundColor
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
visible: mouseArea.containsMouse || mouseArea.preventStealing
}
NymeaToolTip {
id: toolTip
visible: mouseArea.containsMouse || mouseArea.preventStealing
backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: consumptionUpperSeries.count - (Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width))
property int seriesIndex: Math.min(consumptionUpperSeries.count - 1, Math.max(0, consumptionUpperSeries.count - idx))
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: consumptionUpperSeries.at(seriesIndex).y
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
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 {
text: new Date(consumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat)
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
text: qsTr("No data available")
visible: !powerBalanceLogs.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
font: Style.smallFont
opacity: .5
}
Label {
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Total consumption: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
ValueAxis {
id: valueAxis
min: 0
max: Math.ceil(powerBalanceLogs.maxValue / 100) * 100
labelFormat: ""
gridLineColor: Style.tileOverlayColor
labelsVisible: false
lineVisible: false
titleVisible: false
shadesVisible: false
// visible: false
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.green
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = selfProductionSeries.lowerSeries
property XYSeries lowerSeries: null
}
property double value: selfProductionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Self production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
DateTimeAxis {
id: dateTimeAxis
min: d.startTime
max: d.endTime
format: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return "hh:mm"
case EnergyLogs.SampleRate1Hour:
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return "dd.MM."
}
}
tickCount: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return root.width > 500 ? 13 : 7
case EnergyLogs.SampleRate1Hour:
return 7
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return root.width > 500 ? 12 : 6
}
}
labelsFont: Style.extraSmallFont
gridVisible: false
minorGridVisible: false
lineVisible: false
shadesVisible: false
labelsColor: Style.foregroundColor
}
// For debugging, to see the total graph and check if the other maths line up
AreaSeries {
id: consumptionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: "blue"
borderWidth: 0
borderColor: color
opacity: .5
visible: false
lowerSeries: zeroSeries
upperSeries: LineSeries {
id: consumptionUpperSeries
}
function calculateValue(entry) {
return entry.consumption
}
function addEntry(entry) {
consumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
consumptionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
}
RowLayout {
AreaSeries {
id: selfProductionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.green
borderWidth: 0
borderColor: color
name: qsTr("Self production")
// visible: false
lowerSeries: LineSeries {
id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function ensureValue(timestamp) {
if (count == 0) {
append(timestamp, 0)
} else if (count == 1) {
if (timestamp.getTime() < at(0).x) {
insert(0, timestamp, 0)
} else {
append(timestamp, 0)
}
} else {
if (timestamp.getTime() < at(0).x) {
remove(0)
insert(0, timestamp, 0)
} else if (timestamp.getTime() > at(1).x) {
remove(1)
append(timestamp, 0)
}
}
}
function shrink() {
clear();
if (powerBalanceLogs.count > 0) {
ensureValue(powerBalanceLogs.get(0).timestamp)
ensureValue(powerBalanceLogs.get(powerBalanceLogs.count-1).timestamp)
}
}
}
upperSeries: LineSeries {
id: selfProductionUpperSeries
}
function calculateValue(entry) {
var value = entry.consumption - Math.max(0, entry.acquisition);
if (entry.storage < 0) {
value += entry.storage;
}
return value;
}
function addEntry(entry) {
selfProductionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
selfProductionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
}
AreaSeries {
id: storageSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.orange
borderWidth: 0
borderColor: color
name: qsTr("From battery")
visible: root.batteries.count > 0
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.orange
lowerSeries: selfProductionUpperSeries
upperSeries: LineSeries {
id: storageUpperSeries
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
property XYSeries lowerSeries: null
function calculateValue(entry) {
return selfProductionSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.storage));
}
property double value: storageUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("From battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
function addEntry(entry) {
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
storageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.red
AreaSeries {
id: acquisitionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.red
borderWidth: 0
borderColor: color
name: qsTr("From grid")
// visible: false
lowerSeries: storageUpperSeries
upperSeries: LineSeries {
id: acquisitionUpperSeries
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
property XYSeries lowerSeries: null
function calculateValue(entry) {
return storageSeries.calculateValue(entry) + Math.max(0, entry.acquisition)
}
function addEntry(entry) {
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
acquisitionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
}
}
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("From grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
MouseArea {
id: mouseArea
anchors.fill: chartView
anchors.leftMargin: chartView.plotArea.x
anchors.topMargin: chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property var startDatetime: null
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
}
onReleased: {
if (mouseArea.dragging) {
powerBalanceLogs.fetchLogs()
mouseArea.dragging = false;
}
mouseArea.tooltipping = false;
}
onPressed: {
startMouseX = mouseX
startDatetime = d.now
}
onDoubleClicked: {
if (selectionTabs.currentIndex == 0) {
return;
}
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
selectionTabs.currentIndex--
d.now = new Date(timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)
powerBalanceLogs.fetchLogs()
}
onMouseXChanged: {
if (!pressed || mouseArea.tooltipping) {
return;
}
if (Math.abs(startMouseX - mouseX) < 10) {
return;
}
dragging = true
var dragDelta = startMouseX - mouseX
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// dragDelta : timeDelta = width : totalTime
var timeDelta = dragDelta * totalTime / mouseArea.width
print("dragging", dragDelta, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
}
onWheel: {
startDatetime = d.now
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// pixelDelta : timeDelta = width : totalTime
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
wheelStopTimer.restart()
}
Timer {
id: wheelStopTimer
interval: 300
repeat: false
onTriggered: powerBalanceLogs.fetchLogs()
}
Rectangle {
height: parent.height
width: 1
color: Style.foregroundColor
x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
}
NymeaToolTip {
id: toolTip
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
property var timestamp: new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: toolTip.entry ? Math.max(0, entry.consumption) : 0
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
font: Style.smallFont
}
Label {
property double value: toolTip.entry ? Math.max(0, toolTip.entry.consumption) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Total consumption: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.green
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = selfProductionSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.production) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Self production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
}
RowLayout {
visible: root.batteries.count > 0
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.orange
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.storage) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("From battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.red
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: toolTip.entry ? Math.max(0, toolTip.entry.acquisition) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("From grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
}
}
}
}

View File

@ -8,10 +8,13 @@ import "qrc:/ui/components"
Item {
id: root
property PowerBalanceLogs energyLogs: PowerBalanceLogs {
PowerBalanceLogs {
id: powerBalanceLogs
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 {
@ -19,347 +22,550 @@ Item {
shownInterfaces: ["energystorage"]
}
Component.onCompleted: {
for (var i = 0; i < powerBalanceLogs.count; i++) {
var entry = energyLogs.powerBalanceLogs.get(i);
productionSeries.addEntry(entry)
selfConsumptionSeries.addEntry(entry)
storageSeries.addEntry(entry)
acquisitionSeries.addEntry(entry)
QtObject {
id: d
property date now: new Date()
readonly property int range: selectionTabs.currentValue.range
readonly property int sampleRate: selectionTabs.currentValue.sampleRate
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 {
target: powerBalanceLogs
onEntryAdded: {
productionSeries.addEntry(entry)
selfConsumptionSeries.addEntry(entry)
storageSeries.addEntry(entry)
acquisitionSeries.addEntry(entry)
if (dateTimeAxis.now < entry.timestamp) {
dateTimeAxis.now = entry.timestamp
zeroSeries.update(entry.timestamp)
onEntriesAdded: {
// print("entries added", index, entries.length)
for (var i = 0; i < entries.length; i++) {
var entry = entries[i]
// print("got entry", entry.timestamp)
zeroSeries.ensureValue(entry.timestamp)
// For debugging, to see if the other maths line up with the plain production graph
// productionSeries.insertEntry(index + i, entry)
selfConsumptionSeries.insertEntry(index + i, entry)
storageSeries.insertEntry(index + i, entry)
acquisitionSeries.insertEntry(index + i, entry)
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
d.now = entry.timestamp
}
}
}
onEntriesRemoved: {
acquisitionUpperSeries.removePoints(index, count)
storageUpperSeries.removePoints(index, count)
selfConsumptionUpperSeries.removePoints(index, count)
productionUpperSeries.removePoints(index, count)
zeroSeries.shrink()
}
}
Timer {
interval: 60000
repeat: true
onTriggered: {
var now = new Date()
if (dateTimeAxis.now < now) {
dateTimeAxis.now = now
zeroSeries.update(now)
}
}
}
ChartView {
id: chartView
ColumnLayout {
anchors.fill: parent
spacing: 0
backgroundColor: "transparent"
margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("My production history")
}
title: qsTr("My production history")
titleColor: Style.foregroundColor
legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont
ValueAxis {
id: valueAxis
min: 0
max: Math.ceil(-powerBalanceLogs.minValue / 1000) * 1000
labelFormat: ""
gridLineColor: Style.tileOverlayColor
labelsVisible: false
lineVisible: false
titleVisible: false
shadesVisible: false
SelectionTabs {
id: selectionTabs
Layout.fillWidth: true
Layout.leftMargin: Style.smallMargins
Layout.rightMargin: Style.smallMargins
currentIndex: 1
model: ListModel {
ListElement {
modelData: qsTr("Hours")
sampleRate: EnergyLogs.SampleRate1Min
range: 180 // 3 Hours: 3 * 60
}
ListElement {
modelData: qsTr("Days")
sampleRate: EnergyLogs.SampleRate15Mins
range: 1440 // 1 Day: 24 * 60
}
ListElement {
modelData: qsTr("Weeks")
sampleRate: EnergyLogs.SampleRate1Hour
range: 10080 // 7 Days: 7 * 24 * 60
}
ListElement {
modelData: qsTr("Months")
sampleRate: EnergyLogs.SampleRate3Hours
range: 43200 // 30 Days: 30 * 24 * 60
}
}
onTabSelected: {
d.now = new Date()
powerBalanceLogs.fetchLogs()
}
}
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
}
Layout.fillWidth: true
Layout.fillHeight: true
DateTimeAxis {
id: dateTimeAxis
property date now: new Date()
min: {
var date = new Date(now);
date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000);
return date;
}
max: {
var date = new Date(now);
date.setTime(date.getTime() + 2000)
return date;
}
format: "hh:mm"
labelsFont: Style.extraSmallFont
gridVisible: false
minorGridVisible: false
lineVisible: false
shadesVisible: false
labelsColor: Style.foregroundColor
}
// For debugging, to see if the other maths line up with the plain production graph
AreaSeries {
id: productionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: "blue"
borderWidth: 0
borderColor: color
opacity: .5
name: "Total production"
visible: false
function calculateValue(entry) {
return Math.abs(Math.min(0, entry.production))
}
function addEntry(entry) {
productionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
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 {} }
}
lowerSeries: zeroSeries
upperSeries: LineSeries {
id: productionUpperSeries
}
}
ChartView {
id: chartView
anchors.fill: parent
backgroundColor: "transparent"
margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
AreaSeries {
id: selfConsumptionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.red
borderWidth: 0
borderColor: color
name: qsTr("Consumed")
// visible: false
legend.alignment: Qt.AlignBottom
legend.labelColor: Style.foregroundColor
legend.font: Style.extraSmallFont
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 {
id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function update(timestamp) {
append(timestamp, 0);
removePoints(1,1);
}
}
upperSeries: LineSeries {
id: selfConsumptionUpperSeries
}
}
AreaSeries {
id: storageSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.orange
borderWidth: 0
borderColor: color
visible: root.batteries.count > 0
name: qsTr("To battery")
function calculateValue(entry) {
return selfConsumptionSeries.calculateValue(entry) + Math.abs(Math.max(0, entry.storage));
}
function addEntry(entry) {
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
lowerSeries: selfConsumptionUpperSeries
upperSeries: LineSeries {
id: storageUpperSeries
}
}
AreaSeries {
id: acquisitionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.green
borderWidth: 0
borderColor: color
name: qsTr("To grid")
// visible: false
function calculateValue(entry) {
return storageSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.acquisition))
}
function addEntry(entry) {
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
lowerSeries: storageUpperSeries
upperSeries: LineSeries {
id: acquisitionUpperSeries
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.plotArea.x
anchors.topMargin: chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true
}
onReleased: mouseArea.preventStealing = false
Rectangle {
height: parent.height
width: 1
color: Style.foregroundColor
x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
visible: mouseArea.containsMouse || mouseArea.preventStealing
}
NymeaToolTip {
id: toolTip
visible: mouseArea.containsMouse || mouseArea.preventStealing
backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: productionUpperSeries.count - Math.floor(mouseArea.mouseX * productionUpperSeries.count / mouseArea.width)
property int seriesIndex: Math.min(productionUpperSeries.count - 1, Math.max(0, productionUpperSeries.count - idx))
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: productionUpperSeries.at(seriesIndex).y
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
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 {
text: new Date(selfConsumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat)
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
text: qsTr("No data available")
visible: !powerBalanceLogs.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
font: Style.smallFont
opacity: .5
}
Label {
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Total production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
ValueAxis {
id: valueAxis
min: 0
max: Math.ceil(-powerBalanceLogs.minValue / 100) * 100
labelFormat: ""
gridLineColor: Style.tileOverlayColor
labelsVisible: false
lineVisible: false
titleVisible: false
shadesVisible: false
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.red
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = selfConsumptionSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: selfConsumptionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1))) / 1000).toFixed(2) + "kW"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
}
RowLayout {
DateTimeAxis {
id: dateTimeAxis
min: d.startTime
max: d.endTime
format: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return "hh:mm"
case EnergyLogs.SampleRate1Hour:
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return "dd.MM."
}
}
tickCount: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return root.width > 500 ? 13 : 7
case EnergyLogs.SampleRate1Hour:
return 7
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return root.width > 500 ? 12 : 6
}
}
labelsFont: Style.extraSmallFont
gridVisible: false
minorGridVisible: false
lineVisible: false
shadesVisible: false
labelsColor: Style.foregroundColor
}
// For debugging, to see if the other maths line up with the plain production graph
AreaSeries {
id: productionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: "blue"
borderWidth: 0
borderColor: color
opacity: .5
name: "Total production"
visible: false
function calculateValue(entry) {
return Math.abs(Math.min(0, entry.production))
}
function addEntry(entry) {
productionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
productionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
lowerSeries: zeroSeries
upperSeries: LineSeries {
id: productionUpperSeries
}
}
AreaSeries {
id: selfConsumptionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.red
borderWidth: 0
borderColor: color
name: qsTr("Consumed")
// visible: false
lowerSeries: LineSeries {
id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function ensureValue(timestamp) {
if (count == 0) {
append(timestamp, 0)
} else if (count == 1) {
if (timestamp.getTime() < at(0).x) {
insert(0, timestamp, 0)
} else {
append(timestamp, 0)
}
} else {
if (timestamp.getTime() < at(0).x) {
remove(0)
insert(0, timestamp, 0)
} else if (timestamp.getTime() > at(1).x) {
remove(1)
append(timestamp, 0)
}
}
}
function shrink() {
clear();
if (powerBalanceLogs.count > 0) {
ensureValue(powerBalanceLogs.get(0).timestamp)
ensureValue(powerBalanceLogs.get(powerBalanceLogs.count-1).timestamp)
}
}
}
upperSeries: LineSeries {
id: selfConsumptionUpperSeries
}
function calculateValue(entry) {
return Math.max(0, -entry.production) - Math.max(0, -entry.acquisition) - Math.max(0, entry.storage)
}
function addEntry(entry) {
selfConsumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
selfConsumptionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
}
AreaSeries {
id: storageSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.orange
borderWidth: 0
borderColor: color
visible: root.batteries.count > 0
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.orange
name: qsTr("To battery")
function calculateValue(entry) {
return selfConsumptionSeries.calculateValue(entry) + Math.max(0, entry.storage);
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
property XYSeries lowerSeries: null
function addEntry(entry) {
storageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
storageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
property double value: storageUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("To battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
lowerSeries: selfConsumptionUpperSeries
upperSeries: LineSeries {
id: storageUpperSeries
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.green
AreaSeries {
id: acquisitionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.green
borderWidth: 0
borderColor: color
name: qsTr("To grid")
// visible: false
function calculateValue(entry) {
return storageSeries.calculateValue(entry) + Math.max(0, -entry.acquisition)
}
function addEntry(entry) {
acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry))
}
function insertEntry(index, entry) {
acquisitionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry))
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: acquisitionUpperSeries.at(toolTip.seriesIndex).y - lowerSeries.at(toolTip.seriesIndex).y
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("To grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
lowerSeries: storageUpperSeries
upperSeries: LineSeries {
id: acquisitionUpperSeries
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.leftMargin: chartView.plotArea.x
anchors.topMargin: chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property var startDatetime: null
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
}
onReleased: {
if (mouseArea.dragging) {
powerBalanceLogs.fetchLogs()
mouseArea.dragging = false;
}
mouseArea.tooltipping = false;
}
onPressed: {
startMouseX = mouseX
startDatetime = d.now
}
onDoubleClicked: {
if (selectionTabs.currentIndex == 0) {
return;
}
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
selectionTabs.currentIndex--
d.now = new Date(timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)
powerBalanceLogs.fetchLogs()
}
onMouseXChanged: {
if (!pressed || mouseArea.tooltipping) {
return;
}
if (Math.abs(startMouseX - mouseX) < 10) {
return;
}
dragging = true
var dragDelta = startMouseX - mouseX
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// dragDelta : timeDelta = width : totalTime
var timeDelta = dragDelta * totalTime / mouseArea.width
print("dragging", dragDelta, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
}
onWheel: {
startDatetime = d.now
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// pixelDelta : timeDelta = width : totalTime
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
wheelStopTimer.restart()
}
Timer {
id: wheelStopTimer
interval: 300
repeat: false
onTriggered: powerBalanceLogs.fetchLogs()
}
Rectangle {
height: parent.height
width: 1
color: Style.foregroundColor
x: Math.min(mouseArea.width, Math.max(0, mouseArea.mouseX))
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
}
NymeaToolTip {
id: toolTip
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
property var timestamp: new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.mouseX, mouseArea.width) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: toolTip.entry ? Math.max(0, -entry.production) : 0
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
font: Style.smallFont
}
Label {
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.production) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Total production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.red
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = selfConsumptionSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: toolTip.entry ? Math.max(0, toolTip.entry.consumption) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
}
RowLayout {
visible: root.batteries.count > 0
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.orange
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = storageSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: toolTip.entry ? Math.max(0, toolTip.entry.storage) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("To battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.green
}
Label {
// Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings
Component.onCompleted: lowerSeries = acquisitionSeries.lowerSeries
property XYSeries lowerSeries: null
property double value: toolTip.entry ? Math.max(0, -toolTip.entry.acquisition) : 0
property bool translate: value >= 1000
property double translatedValue: value / (translate ? 1000 : 1)
text: qsTr("To grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W")
font: Style.extraSmallFont
}
}
}
}
}
}
}
}

View File

@ -5,10 +5,10 @@ Item {
id: root
property int minutesCount: 9
property int hoursCount: 11
property int daysCount: 6
property int weeksCount: 12
property int monthsCount: 11
property int hoursCount: 10
property int daysCount: 7
property int weeksCount: 10
property int monthsCount: 6
property int yearsCount: 5
property var configs: ({
@ -17,58 +17,61 @@ Item {
startTime: minutesStart,
sampleRate: EnergyLogs.SampleRate1Min,
toLabel: minuteLabel,
toLongLabel: minuteLongLabel
toLongLabel: minuteLongLabel,
toRangeLabel: minuteRangeLabel
},
hours: {
count: hoursCount,
startTime: hoursStart,
sampleRate: EnergyLogs.SampleRate1Hour,
toLabel: hourLabel,
toLongLabel: hourLongLabel
toLongLabel: hourLongLabel,
toRangeLabel: hourRangeLabel
},
days: {
count: daysCount,
startTime: daysStart,
sampleRate: EnergyLogs.SampleRate1Day,
toLabel: dayLabel,
toLongLabel: dayLongLabel
toLongLabel: dayLongLabel,
toRangeLabel: dayRangeLabel
},
weeks: {
count: weeksCount,
startTime: weeksStart,
sampleRate: EnergyLogs.SampleRate1Week,
toLabel: weekLabel,
toLongLabel: weekLongLabel
toLongLabel: weekLongLabel,
toRangeLabel: weekRangeLabel
},
months: {
count: monthsCount,
startTime: monthsStart,
sampleRate: EnergyLogs.SampleRate1Month,
toLabel: monthLabel,
toLongLabel: monthLongLabel
toLongLabel: monthLongLabel,
toRangeLabel: monthRangeLabel
},
years: {
count: yearsCount,
startTime: yearStart,
sampleRate: EnergyLogs.SampleRate1Year,
toLabel: yearLabel,
toLongLabel: yearLabel
toLongLabel: yearLongLabel,
toRangeLabel: yearRangeLabel
}
})
function calculateSampleStart(sampleEnd, sampleRate, sampleCount) {
if (sampleCount === undefined) {
sampleCount = 1
}
var sampleStart = new Date(sampleEnd)
function calculateTimestamp(baseTime, sampleRate, offset) {
var timestamp = new Date(baseTime);
if (sampleRate === EnergyLogs.SampleRate1Month) {
sampleStart.setMonth(sampleEnd.getMonth() - sampleCount)
timestamp.setMonth(baseTime.getMonth() + offset)
} else if (sampleRate === EnergyLogs.SampleRate1Year) {
sampleStart.setFullYear(sampleEnd.getFullYear() - sampleCount)
timestamp.setFullYear(baseTime.getFullYear() + offset)
} else {
sampleStart.setTime(sampleEnd.getTime() - (sampleRate * 60000 * sampleCount))
timestamp.setTime(baseTime.getTime() + (sampleRate * 60000 * offset))
}
return sampleStart
return timestamp;
}
function minutesStart() {
@ -82,6 +85,9 @@ Item {
function minuteLongLabel(date) {
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() {
@ -95,6 +101,9 @@ Item {
function hourLongLabel(date) {
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() {
var d = new Date();
@ -108,11 +117,14 @@ Item {
function dayLongLabel(date) {
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() {
var d = new Date();
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
}
function weekLabel(date) {
@ -127,6 +139,11 @@ Item {
endDate.setDate(endDate.getDate() + 6)
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() {
@ -141,6 +158,11 @@ Item {
function monthLongLabel(date) {
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() {
var d = new Date();
@ -151,5 +173,11 @@ Item {
function yearLabel(date) {
return date.toLocaleString(Qt.locale(), "yyyy")
}
function yearLongLabel(date) {
return date.toLocaleString(Qt.locale(), "yyyy")
}
function yearRangeLabel(date) {
return ""
}
}