From e7af535914b1b9e82e6cde46025be0763e37b31a Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 28 Sep 2021 13:53:10 +0200 Subject: [PATCH] Rework energy views --- libnymea-app/energy/energylogs.cpp | 236 +++++++-- libnymea-app/energy/energylogs.h | 83 +++- libnymea-app/energy/energymanager.cpp | 36 ++ libnymea-app/energy/energymanager.h | 16 +- libnymea-app/energy/powerbalancelogs.cpp | 134 +++++- libnymea-app/energy/powerbalancelogs.h | 43 +- libnymea-app/energy/thingpowerlogs.cpp | 175 +++++++ libnymea-app/energy/thingpowerlogs.h | 73 +++ libnymea-app/libnymea-app-core.h | 6 +- libnymea-app/libnymea-app.pri | 2 + libnymea-app/models/packagesfiltermodel.cpp | 20 + libnymea-app/models/packagesfiltermodel.h | 8 + libnymea-app/thingsproxy.cpp | 45 +- libnymea-app/thingsproxy.h | 15 + nymea-app/resources.qrc | 10 + nymea-app/ui/StyleBase.qml | 2 +- .../ui/devicepages/SmartMeterDevicePage.qml | 2 +- .../ui/mainviews/EnergyPieChartDelegate.qml | 36 ++ nymea-app/ui/mainviews/EnergyView.qml | 450 +++--------------- .../ui/mainviews/energy/ConsumerStats.qml | 168 +++++++ .../ui/mainviews/energy/ConsumersBarChart.qml | 160 +++++++ .../ui/mainviews/energy/ConsumersHistory.qml | 223 +++++++++ .../CurrentConsumptionBalancePieChart.qml | 145 ++++++ .../CurrentProductionBalancePieChart.qml | 145 ++++++ .../ui/mainviews/energy/PowerBalanceStats.qml | 177 +++++++ .../energy/PowerConsumptionBalanceHistory.qml | 232 +++++++++ .../energy/PowerProductionBalanceHistory.qml | 227 +++++++++ nymea-app/ui/system/AboutNymeaPage.qml | 4 + nymea-app/ui/system/PackageListPage.qml | 227 +++++++++ nymea-app/ui/system/SystemUpdatePage.qml | 148 +----- 30 files changed, 2627 insertions(+), 621 deletions(-) create mode 100644 libnymea-app/energy/thingpowerlogs.cpp create mode 100644 libnymea-app/energy/thingpowerlogs.h create mode 100644 nymea-app/ui/mainviews/EnergyPieChartDelegate.qml create mode 100644 nymea-app/ui/mainviews/energy/ConsumerStats.qml create mode 100644 nymea-app/ui/mainviews/energy/ConsumersBarChart.qml create mode 100644 nymea-app/ui/mainviews/energy/ConsumersHistory.qml create mode 100644 nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml create mode 100644 nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml create mode 100644 nymea-app/ui/mainviews/energy/PowerBalanceStats.qml create mode 100644 nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml create mode 100644 nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml create mode 100644 nymea-app/ui/system/PackageListPage.qml diff --git a/libnymea-app/energy/energylogs.cpp b/libnymea-app/energy/energylogs.cpp index 8e98aa2c..ded15a56 100644 --- a/libnymea-app/energy/energylogs.cpp +++ b/libnymea-app/energy/energylogs.cpp @@ -1,5 +1,4 @@ #include "energylogs.h" -#include "powerbalancelogs.h" #include @@ -7,9 +6,32 @@ NYMEA_LOGGING_CATEGORY(dcEnergyLogs, "EnergyLogs") -EnergyLogs::EnergyLogs(QObject *parent) : QObject(parent) +EnergyLogEntry::EnergyLogEntry(QObject *parent): QObject(parent) { - m_powerBalanceLogs = new PowerBalanceLogs(this); + +} + +EnergyLogEntry::EnergyLogEntry(const QDateTime ×tamp, QObject *parent): + QObject(parent), + m_timestamp(timestamp) +{ + +} + +QDateTime EnergyLogEntry::timestamp() const +{ + return m_timestamp; +} + +EnergyLogs::EnergyLogs(QObject *parent) : QAbstractListModel(parent) +{ +} + +EnergyLogs::~EnergyLogs() +{ + if (m_engine) { + m_engine->jsonRpcClient()->unregisterNotificationHandler(this); + } } Engine *EnergyLogs::engine() const @@ -26,16 +48,19 @@ void EnergyLogs::setEngine(Engine *engine) if (!m_engine) { return; } - qCDebug(dcEnergyLogs()) << "************* getting energylogs" << m_engine->jsonRpcClient()->experiences(); if (m_engine->jsonRpcClient()->experiences().value("Energy").toString() >= "1.0") { + m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceivedInternal"); - QVariantMap params; - QMetaEnum metaEnum = QMetaEnum::fromType(); - params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate)); - m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceived"); - m_engine->jsonRpcClient()->sendCommand("Energy.GetPowerBalanceLogs", params, this, "powerBalanceLogsReceived"); -// m_engine->jsonRpcClient()->sendCommand("Energy.GetThingPowerLogs", params, this, "thingPowerLogsReceived"); + connect(engine, &Engine::destroyed, this, [=](){ + if (engine == m_engine) { + m_engine = nullptr; + emit engineChanged(); + } + }); + if (m_ready && !m_loadingInhibited) { + fetchLogs(); + } } } } @@ -79,45 +104,178 @@ void EnergyLogs::setThingIds(const QList &thingIds) } } -PowerBalanceLogs *EnergyLogs::powerBalanceLogs() const +QDateTime EnergyLogs::startTime() const { - return m_powerBalanceLogs; + return m_startTime; } -void EnergyLogs::powerBalanceLogsReceived(int commandId, const QVariantMap ¶ms) +void EnergyLogs::setStartTime(const QDateTime &startTime) { - Q_UNUSED(commandId) - foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) { - QVariantMap map = variant.toMap(); - QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); - double consumption = map.value("consumption").toDouble(); - double production = map.value("production").toDouble(); - double acquisition = map.value("acquisition").toDouble(); - double storage = map.value("storage").toDouble(); - PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, this); - m_powerBalanceLogs->addEntry(entry); + if (m_startTime != startTime) { + qCDebug(dcEnergyLogs()) << "Setting startTime"; + m_startTime = startTime; + emit startTimeChanged(); } } -void EnergyLogs::thingPowerLogsReceived(int commandId, const QVariantMap ¶ms) +QDateTime EnergyLogs::endTime() const { - Q_UNUSED(commandId) - qCDebug(dcEnergyLogs) << "got energy logs"; + return m_endTime; } -void EnergyLogs::notificationReceived(const QVariantMap &data) +void EnergyLogs::setEndTime(const QDateTime &endTime) { - QString notification = data.value("notification").toString(); - QVariantMap params = data.value("params").toMap(); - - if (notification == "Energy.PowerBalanceLogEntryAdded") { - QVariantMap map = data.value("powerBalanceLogEntry").toMap(); - QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); - double consumption = map.value("consumption").toDouble(); - double production = map.value("production").toDouble(); - double acquisition = map.value("acquisition").toDouble(); - double storage = map.value("storage").toDouble(); - PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, this); - m_powerBalanceLogs->addEntry(entry); + if (m_endTime != endTime) { + m_endTime = endTime; + emit endTimeChanged(); } } + +bool EnergyLogs::live() const +{ + return m_live; +} + +void EnergyLogs::setLive(bool live) +{ + if (m_live != live) { + m_live = live; + emit liveChanged(); + } +} + +bool EnergyLogs::fetchingData() const +{ + return m_fetchingData; +} + +bool EnergyLogs::loadingInhibited() const +{ + return m_loadingInhibited; +} + +void EnergyLogs::setLoadingInhibited(bool loadingInhibited) +{ + if (m_loadingInhibited != loadingInhibited) { + m_loadingInhibited = loadingInhibited; + emit loadingInhibitedChanged(); + + if (!m_loadingInhibited) { + fetchLogs(); + } + } +} + +void EnergyLogs::classBegin() +{ + +} + +void EnergyLogs::componentComplete() +{ + m_ready = true; + fetchLogs(); +} + +int EnergyLogs::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +QVariant EnergyLogs::data(const QModelIndex &index, int role) const +{ + Q_UNUSED(index) + Q_UNUSED(role) + return QVariant(); +} + +EnergyLogEntry *EnergyLogs::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +void EnergyLogs::appendEntry(EnergyLogEntry *entry) +{ + entry->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(entry); + endInsertRows(); + emit entryAdded(entry); + emit entriesAdded({entry}); + emit countChanged(); +} + +void EnergyLogs::appendEntries(const QList &entries) +{ + beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + entries.count()); + foreach (EnergyLogEntry* entry, entries) { + entry->setParent(this); + m_list.append(entry); + emit entryAdded(entry); + } + endInsertRows(); + emit entriesAdded(entries); + emit countChanged(); +} + +QVariantMap EnergyLogs::fetchParams() const +{ + return QVariantMap(); +} + +void EnergyLogs::getLogsResponse(int commandId, const QVariantMap ¶ms) +{ + Q_UNUSED(commandId) +// qCDebug(dcEnergyLogs()) << "Energy logs response:" << params; + logEntriesReceived(params); + + m_fetchingData = false; + emit fetchingDataChanged(); +} + +void EnergyLogs::notificationReceivedInternal(const QVariantMap &data) +{ + + if (!m_live) { + return; + } + + if (!data.value("notification").toString().contains("Log")) { + return; + } + + QMetaEnum sampleRateEnum = QMetaEnum::fromType(); + SampleRate sampleRate = static_cast(sampleRateEnum.keyToValue(data.value("params").toMap().value("sampleRate").toByteArray())); + if (sampleRate != m_sampleRate) { + return; + } + + notificationReceived(data); +} + +void EnergyLogs::fetchLogs() +{ + if (m_loadingInhibited || !m_ready || !m_engine || m_engine->jsonRpcClient()->experiences().value("Energy").toString() < "1.0") { + return; + } + + m_fetchingData = true; + fetchingDataChanged(); + + QVariantMap params = fetchParams(); + QMetaEnum metaEnum = QMetaEnum::fromType(); + 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()) << "Fetching power balance logs" << params; + m_engine->jsonRpcClient()->sendCommand("Energy.Get" + logsName(), params, this, "getLogsResponse"); +} + diff --git a/libnymea-app/energy/energylogs.h b/libnymea-app/energy/energylogs.h index 51b2c2d4..11209dbf 100644 --- a/libnymea-app/energy/energylogs.h +++ b/libnymea-app/energy/energylogs.h @@ -5,18 +5,36 @@ #include #include +#include -class PowerBalanceLogs; -class EnergyLogs : public QObject +class EnergyLogEntry: public QObject { Q_OBJECT + Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) +public: + EnergyLogEntry(QObject *parent = nullptr); + EnergyLogEntry(const QDateTime ×tamp, QObject *parent = nullptr); + + QDateTime timestamp() const; +private: + QDateTime m_timestamp; + +}; + +class EnergyLogs : public QAbstractListModel, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged) Q_PROPERTY(SampleRate sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged) - Q_PROPERTY(bool fetchPowerBalance READ fetchPowerBalance WRITE setFetchPowerBalance NOTIFY fetchPowerBalanceChanged) - Q_PROPERTY(QList thingIds READ thingIds WRITE setThingIds NOTIFY thingIdsChanged) + Q_PROPERTY(QDateTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged) + Q_PROPERTY(QDateTime endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged) + 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(PowerBalanceLogs *powerBalanceLogs READ powerBalanceLogs CONSTANT) public: enum SampleRate { SampleRate1Min = 1, @@ -31,6 +49,7 @@ public: Q_ENUM(SampleRate) explicit EnergyLogs(QObject *parent = nullptr); + ~EnergyLogs(); Engine *engine() const; void setEngine(Engine *engine); @@ -44,26 +63,70 @@ public: QList thingIds() const; void setThingIds(const QList &thingIds); - PowerBalanceLogs *powerBalanceLogs() const; + QDateTime startTime() const; + void setStartTime(const QDateTime &startTime); + + QDateTime endTime() const; + void setEndTime(const QDateTime &endTime); + + bool live() const; + void setLive(bool live); + + bool fetchingData() const; + + bool loadingInhibited() const; + void setLoadingInhibited(bool loadingInhibited); + + void classBegin() override; + void componentComplete() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + Q_INVOKABLE EnergyLogEntry* get(int index) const; signals: void engineChanged(); void sampleRateChanged(); void fetchPowerBalanceChanged(); void thingIdsChanged(); + void startTimeChanged(); + void endTimeChanged(); + void liveChanged(); + void fetchingDataChanged(); + void loadingInhibitedChanged(); + + void countChanged(); + void entryAdded(EnergyLogEntry *entry); + void entriesAdded(const QList entries); + +protected: + virtual QString logsName() const = 0; + virtual QVariantMap fetchParams() const; + virtual void logEntriesReceived(const QVariantMap ¶ms) = 0; + virtual void notificationReceived(const QVariantMap &data) = 0; + + void appendEntry(EnergyLogEntry *entry); + void appendEntries(const QList &entries); private slots: - void powerBalanceLogsReceived(int commandId, const QVariantMap ¶ms); - void thingPowerLogsReceived(int commandId, const QVariantMap ¶ms); - void notificationReceived(const QVariantMap &data); + void getLogsResponse(int commandId, const QVariantMap ¶ms); + void notificationReceivedInternal(const QVariantMap &data); + void fetchLogs(); private: Engine *m_engine = nullptr; SampleRate m_sampleRate = SampleRate15Mins; bool m_fetchPowerBalance = true; QList m_thingIds; + QDateTime m_startTime; + QDateTime m_endTime; + bool m_live = true; + bool m_fetchingData = false; + bool m_loadingInhibited = false; + bool m_ready = false; - PowerBalanceLogs *m_powerBalanceLogs = nullptr; + QList m_list; }; #endif // ENERGYLOGS_H diff --git a/libnymea-app/energy/energymanager.cpp b/libnymea-app/energy/energymanager.cpp index 5aaf49a8..a2f877c0 100644 --- a/libnymea-app/energy/energymanager.cpp +++ b/libnymea-app/energy/energymanager.cpp @@ -34,6 +34,7 @@ void EnergyManager::setEngine(Engine *engine) emit engineChanged(); if (m_engine) { + connect(engine, &Engine::destroyed, this, [engine, this]{ if (m_engine == engine) m_engine = nullptr; }); m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceived"); m_engine->jsonRpcClient()->sendCommand("Energy.GetRootMeter", QVariantMap(), this, "getRootMeterResponse"); m_engine->jsonRpcClient()->sendCommand("Energy.GetPowerBalance", QVariantMap(), this, "getPowerBalanceResponse"); @@ -71,6 +72,31 @@ double EnergyManager::currentPowerAcquisition() const return m_currentPowerAcquisition; } +double EnergyManager::currentPowerStorage() const +{ + return m_currentPowerStorage; +} + +double EnergyManager::totalConsumption() const +{ + return m_totalConsumption; +} + +double EnergyManager::totalProduction() const +{ + return m_totalProduction; +} + +double EnergyManager::totalAcquisition() const +{ + return m_totalAcquisition; +} + +double EnergyManager::totalReturn() const +{ + return m_totalReturn; +} + void EnergyManager::notificationReceived(const QVariantMap &data) { QString notification = data.value("notification").toString(); @@ -83,6 +109,11 @@ void EnergyManager::notificationReceived(const QVariantMap &data) m_currentPowerConsumption = params.value("currentPowerConsumption").toDouble(); m_currentPowerProduction = params.value("currentPowerProduction").toDouble(); m_currentPowerAcquisition = params.value("currentPowerAcquisition").toDouble(); + m_currentPowerStorage = params.value("currentPowerStorage").toDouble(); + m_totalConsumption = params.value("totalConsumption").toDouble(); + m_totalProduction = params.value("totalProduction").toDouble(); + m_totalAcquisition = params.value("totalAcquisition").toDouble(); + m_totalReturn = params.value("totalReturn").toDouble(); emit powerBalanceChanged(); } else if (notification == "Energy.PowerBalanceLogEntryAdded") { @@ -110,6 +141,11 @@ void EnergyManager::getPowerBalanceResponse(int commandId, const QVariantMap &pa m_currentPowerConsumption = params.value("currentPowerConsumption").toDouble(); m_currentPowerProduction = params.value("currentPowerProduction").toDouble(); m_currentPowerAcquisition = params.value("currentPowerAcquisition").toDouble(); + m_currentPowerStorage = params.value("currentPowerStorage").toDouble(); + m_totalConsumption = params.value("totalConsumption").toDouble(); + m_totalProduction = params.value("totalProduction").toDouble(); + m_totalAcquisition = params.value("totalAcquisition").toDouble(); + m_totalReturn = params.value("totalReturn").toDouble(); emit powerBalanceChanged(); } diff --git a/libnymea-app/energy/energymanager.h b/libnymea-app/energy/energymanager.h index 23c98e21..4d5f0da2 100644 --- a/libnymea-app/energy/energymanager.h +++ b/libnymea-app/energy/energymanager.h @@ -15,6 +15,11 @@ class EnergyManager : public QObject Q_PROPERTY(double currentPowerConsumption READ currentPowerConsumption NOTIFY powerBalanceChanged) Q_PROPERTY(double currentPowerProduction READ currentPowerProduction NOTIFY powerBalanceChanged) Q_PROPERTY(double currentPowerAcquisition READ currentPowerAcquisition NOTIFY powerBalanceChanged) + Q_PROPERTY(double currentPowerStorage READ currentPowerStorage NOTIFY powerBalanceChanged) + Q_PROPERTY(double totalConsumption READ totalConsumption NOTIFY powerBalanceChanged) + Q_PROPERTY(double totalProduction READ totalProduction NOTIFY powerBalanceChanged) + Q_PROPERTY(double totalAcquisition READ totalAcquisition NOTIFY powerBalanceChanged) + Q_PROPERTY(double totalReturn READ totalReturn NOTIFY powerBalanceChanged) public: explicit EnergyManager(QObject *parent = nullptr); @@ -29,6 +34,11 @@ public: double currentPowerConsumption() const; double currentPowerProduction() const; double currentPowerAcquisition() const; + double currentPowerStorage() const; + double totalConsumption() const; + double totalProduction() const; + double totalAcquisition() const; + double totalReturn() const; signals: void engineChanged(); @@ -47,7 +57,11 @@ private: double m_currentPowerConsumption = 0; double m_currentPowerProduction = 0; double m_currentPowerAcquisition = 0; - + double m_currentPowerStorage = 0; + double m_totalConsumption = 0; + double m_totalProduction = 0; + double m_totalAcquisition = 0; + double m_totalReturn = 0; }; #endif // ENERGYMANAGER_H diff --git a/libnymea-app/energy/powerbalancelogs.cpp b/libnymea-app/energy/powerbalancelogs.cpp index 926edf93..dc6d4d12 100644 --- a/libnymea-app/energy/powerbalancelogs.cpp +++ b/libnymea-app/energy/powerbalancelogs.cpp @@ -1,19 +1,22 @@ #include "powerbalancelogs.h" -PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage, QObject *parent): - QObject(parent), - m_timestamp(timestamp), - m_consumption(consumption), - m_production(production), - m_acquisition(acquisition), - m_storage(storage) +PowerBalanceLogEntry::PowerBalanceLogEntry(QObject *parent): EnergyLogEntry(parent) { } -QDateTime PowerBalanceLogEntry::timestamp() const +PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn, QObject *parent): + EnergyLogEntry(timestamp, parent), + m_consumption(consumption), + m_production(production), + m_acquisition(acquisition), + m_storage(storage), + m_totalConsumption(totalConsumption), + m_totalProduction(totalProduction), + m_totalAcquisition(totalAcquisition), + m_totalReturn(totalReturn) { - return m_timestamp; + } double PowerBalanceLogEntry::consumption() const @@ -36,27 +39,29 @@ double PowerBalanceLogEntry::storage() const return m_storage; } - -PowerBalanceLogs::PowerBalanceLogs(QObject *parent) : QAbstractListModel(parent) +double PowerBalanceLogEntry::totalConsumption() const { - + return m_totalConsumption; } -int PowerBalanceLogs::rowCount(const QModelIndex &parent) const +double PowerBalanceLogEntry::totalProduction() const { - Q_UNUSED(parent) - return m_list.count(); + return m_totalProduction; } -QVariant PowerBalanceLogs::data(const QModelIndex &index, int role) const +double PowerBalanceLogEntry::totalAcquisition() const { - return QVariant(); + return m_totalAcquisition; } -QHash PowerBalanceLogs::roleNames() const +double PowerBalanceLogEntry::totalReturn() const { - QHash roles; - return roles; + return m_totalReturn; +} + +PowerBalanceLogs::PowerBalanceLogs(QObject *parent) : EnergyLogs(parent) +{ + } double PowerBalanceLogs::minValue() const @@ -69,14 +74,14 @@ double PowerBalanceLogs::maxValue() const return m_maxValue; } +QString PowerBalanceLogs::logsName() const +{ + return "PowerBalanceLogs"; +} + void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry) { - entry->setParent(this); - beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); - m_list.append(entry); - endInsertRows(); - emit entryAdded(entry); - emit countChanged(); + appendEntry(entry); if (entry->consumption() < m_minValue) { m_minValue = entry->consumption(); @@ -114,6 +119,83 @@ void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry) } +EnergyLogEntry *PowerBalanceLogs::find(const QDateTime ×tamp) const +{ +// qWarning() << "Finding log entry for timestamp:" << timestamp; + int oldest = 0; + int newest = rowCount() - 1; + EnergyLogEntry *entry = nullptr; + int step = 0; + while (oldest < newest && step < rowCount()) { + EnergyLogEntry *oldestEntry = get(oldest); + EnergyLogEntry *newestEntry = get(newest); + int middle = (newest - oldest) / 2 + oldest; + EnergyLogEntry *middleEntry = get(middle); +// qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest); + if (timestamp <= oldestEntry->timestamp()) { + return oldestEntry; + } + if (timestamp >= newestEntry->timestamp()) { + return newestEntry; + } + + if (timestamp == middleEntry->timestamp()) { + return middleEntry; + } + + if (timestamp < middleEntry->timestamp()) { + oldest = middle; + } else { + newest = middle; + } + + if ((newest - oldest) <= 1) { + return newestEntry; + } + step++; + } + return entry; + +} + +void PowerBalanceLogs::logEntriesReceived(const QVariantMap ¶ms) +{ + foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) { + QVariantMap map = variant.toMap(); + QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); + double consumption = map.value("consumption").toDouble(); + double production = map.value("production").toDouble(); + double acquisition = map.value("acquisition").toDouble(); + double storage = map.value("storage").toDouble(); + double totalConsumption = map.value("totalConsumption").toDouble(); + double totalProduction = map.value("totalProduction").toDouble(); + 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); + } +} + +void PowerBalanceLogs::notificationReceived(const QVariantMap &data) +{ + QString notification = data.value("notification").toString(); + QVariantMap params = data.value("params").toMap(); + if (notification == "Energy.PowerBalanceLogEntryAdded") { + QVariantMap map = params.value("powerBalanceLogEntry").toMap(); + QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); + double consumption = map.value("consumption").toDouble(); + double production = map.value("production").toDouble(); + double acquisition = map.value("acquisition").toDouble(); + double storage = map.value("storage").toDouble(); + double totalConsumption = map.value("totalConsumption").toDouble(); + double totalProduction = map.value("totalProduction").toDouble(); + 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); + } +} + PowerBalanceLogs *PowerBalanceLogsProxy::powerBalanceLogs() const { return m_powerBalanceLogs; diff --git a/libnymea-app/energy/powerbalancelogs.h b/libnymea-app/energy/powerbalancelogs.h index 8407303f..82826bf4 100644 --- a/libnymea-app/energy/powerbalancelogs.h +++ b/libnymea-app/energy/powerbalancelogs.h @@ -6,53 +6,68 @@ #include #include -class PowerBalanceLogEntry: public QObject +#include "energylogs.h" + +class PowerBalanceLogEntry: public EnergyLogEntry { Q_OBJECT - Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) Q_PROPERTY(double consumption READ consumption CONSTANT) Q_PROPERTY(double production READ production CONSTANT) Q_PROPERTY(double acquisition READ acquisition CONSTANT) Q_PROPERTY(double storage READ storage CONSTANT) + Q_PROPERTY(double totalConsumption READ totalConsumption CONSTANT) + Q_PROPERTY(double totalProduction READ totalProduction CONSTANT) + Q_PROPERTY(double totalAcquisition READ totalAcquisition CONSTANT) + Q_PROPERTY(double totalReturn READ totalReturn CONSTANT) public: - PowerBalanceLogEntry() = default; - PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage, QObject *parent); + PowerBalanceLogEntry(QObject *parent = nullptr); + PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn, QObject *parent); - QDateTime timestamp() const; double consumption() const; double production() const; double acquisition() const; double storage() const; + double totalConsumption() const; + double totalProduction() const; + double totalAcquisition() const; + double totalReturn() const; private: QDateTime m_timestamp; double m_consumption = 0; double m_production = 0; double m_acquisition = 0; double m_storage = 0; + double m_totalConsumption = 0; + double m_totalProduction = 0; + double m_totalAcquisition = 0; + double m_totalReturn = 0; }; -class PowerBalanceLogs : public QAbstractListModel +class PowerBalanceLogs : public EnergyLogs { Q_OBJECT - Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged) Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged) public: explicit PowerBalanceLogs(QObject *parent = nullptr); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - QHash roleNames() const override; + double minValue() const; double maxValue() const; - void addEntry(PowerBalanceLogEntry *entry); + Q_INVOKABLE EnergyLogEntry* find(const QDateTime ×tamp) const; + signals: - void countChanged(); - void entryAdded(PowerBalanceLogEntry *entry); void minValueChanged(); void maxValueChanged(); + +protected: + QString logsName() const override; + void logEntriesReceived(const QVariantMap ¶ms) override; + void notificationReceived(const QVariantMap &data) override; + private: - QList m_list; + void addEntry(PowerBalanceLogEntry *entry); + double m_minValue = 0; double m_maxValue = 0; }; diff --git a/libnymea-app/energy/thingpowerlogs.cpp b/libnymea-app/energy/thingpowerlogs.cpp new file mode 100644 index 00000000..e16aa58e --- /dev/null +++ b/libnymea-app/energy/thingpowerlogs.cpp @@ -0,0 +1,175 @@ +#include "thingpowerlogs.h" + +ThingPowerLogEntry::ThingPowerLogEntry(QObject *parent): + EnergyLogEntry(parent) +{ +} + +ThingPowerLogEntry::ThingPowerLogEntry(const QDateTime ×tamp, const QUuid &thingId, double currentPower, double totalConsumption, double totalProduction, QObject *parent): + EnergyLogEntry(timestamp, parent), + m_thingId(thingId), + m_currentPower(currentPower), + m_totalConsumption(totalConsumption), + m_totalProduction(totalProduction) +{ + +} + +QUuid ThingPowerLogEntry::thingId() const +{ + return m_thingId; +} + +double ThingPowerLogEntry::currentPower() const +{ + return m_currentPower; +} + +double ThingPowerLogEntry::totalConsumption() const +{ + return m_totalConsumption; +} + +double ThingPowerLogEntry::totalProduction() const +{ + return m_totalProduction; +} + +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(); + } + }); +} + +QList ThingPowerLogs::thingIds() const +{ + return m_thingIds; +} + +void ThingPowerLogs::setThingIds(const QList &thingIds) +{ + if (m_thingIds != thingIds) { + m_thingIds = thingIds; + emit thingIdsChanged(); + } +} + +double ThingPowerLogs::minValue() const +{ + return m_minValue; +} + +double ThingPowerLogs::maxValue() const +{ + return m_maxValue; +} + +EnergyLogEntry *ThingPowerLogs::find(const QUuid &thingId, const QDateTime ×tamp) +{ + // TODO: Can we do a binary search even if they key we're looking for is not unique (but still sorted)? + // For now, 365 * consumers items is the max we'll have here which seems on the edge for doing a stupid linear search... + for (int i = rowCount() - 1; i >= 0; i--) { + ThingPowerLogEntry *entry = static_cast(get(i)); + if (entry->thingId() != thingId) { + continue; + } + if (timestamp == entry->timestamp()) { + return entry; + } + if (timestamp < entry->timestamp()) { + return nullptr; // Giving up, entry is not here + } + } + return nullptr; +} + +void ThingPowerLogs::addEntry(ThingPowerLogEntry *entry) +{ + appendEntry(entry); +} + +void ThingPowerLogs::addEntries(const QList &entries) +{ + QList energyLogEntries; + foreach (ThingPowerLogEntry* entry, entries) { + energyLogEntries.append(entry); + } + appendEntries(energyLogEntries); +} + +QString ThingPowerLogs::logsName() const +{ + return "ThingPowerLogs"; +} + +QVariantMap ThingPowerLogs::fetchParams() const +{ + QVariantList thingIdsStrings; + foreach (const QUuid &id, m_thingIds) { + thingIdsStrings.append(id.toString()); + } + QVariantMap ret; + ret.insert("thingIds", thingIdsStrings); + return ret; +} + +void ThingPowerLogs::logEntriesReceived(const QVariantMap ¶ms) +{ + // Grouping them so when the UI gets entriesAdded, the whole set for this timstamp will be available at once + QList groupForTimestamp; + foreach (const QVariant &variant, params.value("thingPowerLogEntries").toList()) { + QVariantMap map = variant.toMap(); + QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); + QUuid thingId = map.value("thingId").toUuid(); + 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); + + 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); + } + } + + if (!groupForTimestamp.isEmpty()) { + addEntries(groupForTimestamp); + } +} + +void ThingPowerLogs::notificationReceived(const QVariantMap &data) +{ + QString notification = data.value("notification").toString(); + QVariantMap params = data.value("params").toMap(); + if (notification == "Energy.ThingPowerLogEntryAdded") { + QVariantMap map = params.value("thingPowerLogEntry").toMap(); + QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); + QUuid thingId = map.value("thingId").toUuid(); + 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); + 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(); + } +} + diff --git a/libnymea-app/energy/thingpowerlogs.h b/libnymea-app/energy/thingpowerlogs.h new file mode 100644 index 00000000..e262f589 --- /dev/null +++ b/libnymea-app/energy/thingpowerlogs.h @@ -0,0 +1,73 @@ +#ifndef THINGPOWERLOGS_H +#define THINGPOWERLOGS_H + +#include +#include + +#include "energylogs.h" + +class ThingPowerLogEntry: public EnergyLogEntry +{ + Q_OBJECT + Q_PROPERTY(QUuid thingId READ thingId CONSTANT) + Q_PROPERTY(double currentPower READ currentPower CONSTANT) + Q_PROPERTY(double totalConsumption READ totalConsumption CONSTANT) + Q_PROPERTY(double totalProduction READ totalProduction CONSTANT) +public: + ThingPowerLogEntry(QObject *parent = nullptr); + ThingPowerLogEntry(const QDateTime ×tamp, const QUuid &thingId, double currentPower, double totalConsumption, double totalProduction, QObject *parent = nullptr); + + QUuid thingId() const; + double currentPower() const; + double totalConsumption() const; + double totalProduction() const; + +private: + QUuid m_thingId; + double m_currentPower = 0; + double m_totalConsumption = 0; + double m_totalProduction = 0; +}; + +class ThingPowerLogs : public EnergyLogs +{ + Q_OBJECT + Q_PROPERTY(QList thingIds READ thingIds WRITE setThingIds NOTIFY thingIdsChanged) + Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged) + Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged) +public: + explicit ThingPowerLogs(QObject *parent = nullptr); + + QList thingIds() const; + void setThingIds(const QList &thingIds); + + double minValue() const; + double maxValue() const; + + Q_INVOKABLE EnergyLogEntry *find(const QUuid &thingId, const QDateTime ×tamp); + +signals: + void thingIdsChanged(); + + void minValueChanged(); + void maxValueChanged(); + +protected: + QString logsName() const override; + QVariantMap fetchParams() const override; + void logEntriesReceived(const QVariantMap ¶ms) override; + void notificationReceived(const QVariantMap &data) override; + +private: + void addEntry(ThingPowerLogEntry *entry); + void addEntries(const QList &entries); + + QList m_thingIds; + double m_minValue = 0; + double m_maxValue = 0; + + QList m_cachedEntries; + QTimer m_cacheTimer; +}; + +#endif // THINGPOWERLOGS_H diff --git a/libnymea-app/libnymea-app-core.h b/libnymea-app/libnymea-app-core.h index b078ee8d..4ff13c5b 100644 --- a/libnymea-app/libnymea-app-core.h +++ b/libnymea-app/libnymea-app-core.h @@ -138,6 +138,7 @@ #include "energy/energymanager.h" #include "energy/energylogs.h" #include "energy/powerbalancelogs.h" +#include "energy/thingpowerlogs.h" #include @@ -361,9 +362,12 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "AppData"); qmlRegisterType(uri, 1, 0, "EnergyManager"); - qmlRegisterType(uri, 1, 0, "EnergyLogs"); + qmlRegisterUncreatableType(uri, 1, 0, "EnergyLogEntry", "EnergyLogentry is an abstract class"); + qmlRegisterUncreatableType(uri, 1, 0, "EnergyLogs", "EnergyLogs is an abstract class"); qmlRegisterType(uri, 1, 0, "PowerBalanceLogs"); qmlRegisterType(uri, 1, 0, "PowerBalanceLogEntry"); + qmlRegisterType(uri, 1, 0, "ThingPowerLogEntry"); + qmlRegisterType(uri, 1, 0, "ThingPowerLogs"); qmlRegisterType(uri, 1, 0, "SortFilterProxyModel"); } diff --git a/libnymea-app/libnymea-app.pri b/libnymea-app/libnymea-app.pri index ba3736b5..94567d94 100644 --- a/libnymea-app/libnymea-app.pri +++ b/libnymea-app/libnymea-app.pri @@ -24,6 +24,7 @@ SOURCES += \ $$PWD/energy/energylogs.cpp \ $$PWD/energy/energymanager.cpp \ $$PWD/energy/powerbalancelogs.cpp \ + $$PWD/energy/thingpowerlogs.cpp \ $$PWD/models/scriptsproxymodel.cpp \ $$PWD/tagwatcher.cpp \ $$PWD/zigbee/zigbeenode.cpp \ @@ -183,6 +184,7 @@ HEADERS += \ $$PWD/energy/energylogs.h \ $$PWD/energy/energymanager.h \ $$PWD/energy/powerbalancelogs.h \ + $$PWD/energy/thingpowerlogs.h \ $$PWD/models/scriptsproxymodel.h \ $$PWD/tagwatcher.h \ $$PWD/zigbee/zigbeenode.h \ diff --git a/libnymea-app/models/packagesfiltermodel.cpp b/libnymea-app/models/packagesfiltermodel.cpp index 186b0457..82a834c1 100644 --- a/libnymea-app/models/packagesfiltermodel.cpp +++ b/libnymea-app/models/packagesfiltermodel.cpp @@ -69,6 +69,21 @@ void PackagesFilterModel::setUpdatesOnly(bool updatesOnly) } } +QString PackagesFilterModel::nameFilter() const +{ + return m_nameFilter; +} + +void PackagesFilterModel::setNameFilter(const QString &nameFilter) +{ + if (nameFilter != m_nameFilter) { + m_nameFilter = nameFilter; + emit nameFilterChanged(); + invalidateFilter(); + emit countChanged(); + } +} + Package *PackagesFilterModel::get(int index) const { return m_packages->get(mapToSource(this->index(index, 0)).row()); @@ -82,5 +97,10 @@ bool PackagesFilterModel::filterAcceptsRow(int source_row, const QModelIndex &so return false; } } + if (!m_nameFilter.isEmpty()) { + if (!m_packages->get(source_row)->displayName().contains(m_nameFilter)) { + return false; + } + } return true; } diff --git a/libnymea-app/models/packagesfiltermodel.h b/libnymea-app/models/packagesfiltermodel.h index 240e66ff..80a4006b 100644 --- a/libnymea-app/models/packagesfiltermodel.h +++ b/libnymea-app/models/packagesfiltermodel.h @@ -41,6 +41,8 @@ class PackagesFilterModel : public QSortFilterProxyModel Q_PROPERTY(bool updatesOnly READ updatesOnly WRITE setUpdatesOnly NOTIFY updatesOnlyChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(QString nameFilter READ nameFilter WRITE setNameFilter NOTIFY nameFilterChanged) + public: explicit PackagesFilterModel(QObject *parent = nullptr); @@ -50,6 +52,9 @@ public: bool updatesOnly() const; void setUpdatesOnly(bool updatesOnly); + QString nameFilter() const; + void setNameFilter(const QString &nameFilter); + Q_INVOKABLE Package* get(int index) const; protected: @@ -59,11 +64,14 @@ signals: void countChanged(); void packagesChanged(); void updatesOnlyChanged(); + void nameFilterChanged(); private: Packages *m_packages; bool m_updatesOnly = false; + + QString m_nameFilter; }; #endif // PACKAGESFILTERMODEL_H diff --git a/libnymea-app/thingsproxy.cpp b/libnymea-app/thingsproxy.cpp index a047645d..838aa397 100644 --- a/libnymea-app/thingsproxy.cpp +++ b/libnymea-app/thingsproxy.cpp @@ -36,6 +36,7 @@ ThingsProxy::ThingsProxy(QObject *parent) : QSortFilterProxyModel(parent) { + setSortRole(Things::RoleName); } Engine *ThingsProxy::engine() const @@ -61,7 +62,7 @@ void ThingsProxy::setEngine(Engine *engine) setSourceModel(m_engine->thingManager()->things()); setSortRole(Things::RoleName); - sort(0); + sort(0, sortOrder()); connect(sourceModel(), SIGNAL(countChanged()), this, SIGNAL(countChanged())); connect(sourceModel(), &QAbstractItemModel::dataChanged, this, [this]() { invalidateFilter(); @@ -422,6 +423,26 @@ void ThingsProxy::setGroupByInterface(bool groupByInterface) } } +QString ThingsProxy::sortStateName() const +{ + return m_sortStateName; +} + +void ThingsProxy::setSortStateName(const QString &sortStateName) +{ + if (m_sortStateName != sortStateName) { + m_sortStateName = sortStateName; + emit sortStateNameChanged(); + invalidate(); + } +} + +void ThingsProxy::setSortOrder(Qt::SortOrder sortOrder) +{ + sort(0, sortOrder); + emit sortOrderChanged(); +} + Thing *ThingsProxy::get(int index) const { return getInternal(mapToSource(this->index(index, 0)).row()); @@ -478,8 +499,26 @@ bool ThingsProxy::lessThan(const QModelIndex &left, const QModelIndex &right) co return QString::localeAwareCompare(leftBaseInterface, rightBaseInterface) < 0; } } - QString leftName = sourceModel()->data(left, Things::RoleName).toString(); - QString rightName = sourceModel()->data(right, Things::RoleName).toString(); + + if (!m_sortStateName.isEmpty()) { + Thing *leftThing = nullptr; + Thing *rightThing = nullptr; + if (m_parentProxy) { + leftThing = m_parentProxy->get(left.row()); + rightThing = m_parentProxy->get(right.row()); + } else { + leftThing = m_engine->thingManager()->things()->get(left.row()); + rightThing = m_engine->thingManager()->things()->get(right.row()); + } + State *leftState = leftThing->stateByName(m_sortStateName); + State *rightState = rightThing->stateByName(m_sortStateName); + QVariant leftStateValue = leftState ? leftState->value() : 0; + QVariant rightStateValue = rightState ? rightState->value() : 0; + return leftStateValue < rightStateValue; + } + + QString leftName = sourceModel()->data(left, sortRole()).toString(); + QString rightName = sourceModel()->data(right, sortRole()).toString(); int comparison = QString::localeAwareCompare(leftName, rightName); if (comparison == 0) { diff --git a/libnymea-app/thingsproxy.h b/libnymea-app/thingsproxy.h index 591d8411..2ae4718f 100644 --- a/libnymea-app/thingsproxy.h +++ b/libnymea-app/thingsproxy.h @@ -80,6 +80,12 @@ class ThingsProxy : public QSortFilterProxyModel Q_PROPERTY(bool groupByInterface READ groupByInterface WRITE setGroupByInterface NOTIFY groupByInterfaceChanged) + // If set, sorting will happen for the value of the given state. Make sure the filter is set to contain only things that have the given state + // Does not work in combination with groupByInterface + Q_PROPERTY(QString sortStateName READ sortStateName WRITE setSortStateName NOTIFY sortStateNameChanged) + + Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) + public: explicit ThingsProxy(QObject *parent = nullptr); @@ -152,6 +158,11 @@ public: bool groupByInterface() const; void setGroupByInterface(bool groupByInterface); + QString sortStateName() const; + void setSortStateName(const QString &sortStateName); + + void setSortOrder(Qt::SortOrder sortOrder); + Q_INVOKABLE Thing *get(int index) const; Q_INVOKABLE Thing *getThing(const QUuid &thingId) const; Q_INVOKABLE int indexOf(Thing *thing) const; @@ -180,6 +191,8 @@ signals: void filterUpdatesChanged(); void paramsFilterChanged(); void groupByInterfaceChanged(); + void sortStateNameChanged(); + void sortOrderChanged(); void countChanged(); private: @@ -214,6 +227,8 @@ private: bool m_groupByInterface = false; + QString m_sortStateName; + protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 98485e44..78355e9c 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -259,5 +259,15 @@ ui/devicepages/EvChargerThingPage.qml ui/components/BlurredLabel.qml ui/components/NymeaSpinBox.qml + ui/mainviews/EnergyPieChartDelegate.qml + ui/mainviews/energy/PowerConsumptionBalanceHistory.qml + ui/mainviews/energy/PowerProductionBalanceHistory.qml + ui/mainviews/energy/ConsumersBarChart.qml + ui/mainviews/energy/ConsumersHistory.qml + ui/mainviews/energy/PowerBalanceStats.qml + ui/mainviews/energy/CurrentProductionBalancePieChart.qml + ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml + ui/mainviews/energy/ConsumerStats.qml + ui/system/PackageListPage.qml diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index 1d7abf66..752d3865 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -105,11 +105,11 @@ Item { property color red: "indianred" property color green: "mediumseagreen" property color yellow: "gold" - property color white: "white" property color gray: "gray" property color darkGray: "darkGray" property color blue: "deepskyblue" + property color orange: "#f6a625" readonly property int fastAnimationDuration: 100 readonly property int animationDuration: 150 diff --git a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml index 4c9cdc30..ab8b4449 100644 --- a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml +++ b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml @@ -211,7 +211,7 @@ ThingPageBase { property bool isCharging: root.chargingState && root.chargingState.value === "charging" property bool isDischarging: root.chargingState && root.chargingState.value === "discharging" property double availableWh: isBattery ? root.capacityState.value * 1000 * root.batteryLevelState.value / 100 : 0 - property double remainingWh: isCharging ? root.capacityState.value - availableWh : availableWh + property double remainingWh: isCharging ? root.capacityState.value * 1000 - availableWh : availableWh property double remainingHours: isBattery ? remainingWh / Math.abs(root.currentPower) : 0 property date endTime: isBattery ? new Date(new Date().getTime() + remainingHours * 60 * 60 * 1000) : new Date() property int n: Math.round(remainingHours) diff --git a/nymea-app/ui/mainviews/EnergyPieChartDelegate.qml b/nymea-app/ui/mainviews/EnergyPieChartDelegate.qml new file mode 100644 index 00000000..e53819f9 --- /dev/null +++ b/nymea-app/ui/mainviews/EnergyPieChartDelegate.qml @@ -0,0 +1,36 @@ +import QtQuick 2.3 +import QtCharts 2.2 + +Item { + id: sliceItem + property PieSeries series: null + property Thing thing: model.get(index) + property State currentPowerState: thing ? thing.stateByName("currentPower") : null + property PieSlice consumerSlice: null + property PieSlice producerSlice: null + Component.onCompleted: { + if (currentPowerState.value >= 0) { + consumerSlice = consumersSeries.append(thing.name, currentPowerState.value) + prodcuersSlice = producerSeries.append(thing.name, 0) + } else { + consumerSlice = consumersSeries.append(thing.name, 0) + prodcuersSlice = producerSeries.append(thing.name, Math.abs(currentPowerState.value)) + } + } + Connections { + target: currentPowerState + onValueChanged: { + if (currentPowerState.value >= 0) { + consumerSlice.value = currentPowerState.value + producerSlice.value = 0 + } else { + consumerSlice.value = 0 + producerSlice.value = Math.abs(currentPowerState.value) + } + } + } + + Component.onDestruction: { + consumersSeries.remove(slice) + } +} diff --git a/nymea-app/ui/mainviews/EnergyView.qml b/nymea-app/ui/mainviews/EnergyView.qml index 4bdabb38..6fdfd931 100644 --- a/nymea-app/ui/mainviews/EnergyView.qml +++ b/nymea-app/ui/mainviews/EnergyView.qml @@ -32,16 +32,23 @@ import QtQuick 2.8 import QtQuick.Controls 2.1 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 import QtCharts 2.2 import Nymea 1.0 import "../components" import "../delegates" +import "energy" MainViewBase { id: root contentY: flickable.contentY + topMargin + EnergyManager { + id: energyManager + engine: _engine + } + ThingsProxy { id: energyMeters engine: _engine @@ -61,6 +68,11 @@ MainViewBase { shownInterfaces: ["smartmeterproducer"] } + ThingsProxy { + id: batteries + engine: _engine + shownInterfaces: ["energystorage"] + } Flickable { id: flickable @@ -70,406 +82,88 @@ MainViewBase { visible: energyMeters.count > 0 topMargin: root.topMargin - - GridLayout { - id: energyGrid + // GridLayout directly in a flickable causes problems at initialisation + Item { width: parent.width - columns: root.width > 600 ? 2 : 1 - rowSpacing: 0 - columnSpacing: 0 + height: energyGrid.implicitHeight - SmartMeterChart { - Layout.fillWidth: true -// Layout.preferredWidth: energyGrid.width / energyGrid.columns - Layout.preferredHeight: (energyGrid.width / energyGrid.columns) * .7 - // FIXME: multiple root meters... Not exactly a use case, still possible tho - rootMeter: root.rootMeter - meters: consumers - title: qsTr("Total consumed energy") - visible: rootMeterTotalEnergyState || consumers.count > 0 - } - SmartMeterChart { - Layout.fillWidth: true - Layout.preferredHeight: width * .7 - backgroundColor: Style.tileBackgroundColor - backgroundRoundness: Style.cornerRadius - rootMeter: root.rootMeter - meters: producers - title: qsTr("Total produced energy") - stateName: "totalEnergyProduced" - readonly property State totalProducedState: rootMeter ? rootMeter.stateByName("totalEnergyProduced") : null - visible: (rootMeterTotalEnergyState && rootMeterTotalEnergyState.value > 0) || producers.count > 0 - } + GridLayout { + id: energyGrid + width: parent.width + property int rawColumns: Math.floor(flickable.width / 300) + columns: Math.max(1, rawColumns - (rawColumns % 2)) + rowSpacing: 0 + columnSpacing: 0 - ChartView { - id: chartView - Layout.fillWidth: true -// Layout.preferredWidth: energyGrid.width / energyGrid.columns - Layout.columnSpan: energyGrid.columns - Layout.preferredHeight: width * .7 - legend.alignment: Qt.AlignBottom - legend.font: Style.extraSmallFont -// legend.visible: false - legend.labelColor: Style.foregroundColor - backgroundColor: Style.tileBackgroundColor - backgroundRoundness: Style.cornerRadius - theme: ChartView.ChartThemeLight - titleColor: Style.foregroundColor - title: qsTr("Power usage history") - property var startTime: xAxis.min - property var endTime: xAxis.max - - property int sampleRate: XYSeriesAdapter.SampleRateMinute - - property int busyModels: 0 - - BusyIndicator { - anchors.centerIn: parent - visible: chartView.busyModels > 0 - running: visible + CurrentConsumptionBalancePieChart { + Layout.fillWidth: true + Layout.preferredHeight: width + energyManager: energyManager + visible: producers.count > 0 + } + CurrentProductionBalancePieChart { + Layout.fillWidth: true + Layout.preferredHeight: width + energyManager: energyManager + visible: producers.count > 0 } - LogsModel { - id: rootMeterLogsModel - objectName: "Root meter model" - engine: rootMeter ? _engine : null // Don't start fetching before we know what we want - thingId: rootMeter ? rootMeter.id : "" - typeIds: rootMeter ? [rootMeter.thingClass.stateTypes.findByName("currentPower").id] : [] - viewStartTime: xAxis.min - live: true - } - XYSeriesAdapter { - id: rootMeterSeriesAdapter - objectName: "Root meter adapter" - logsModel: rootMeterLogsModel - sampleRate: chartView.sampleRate - xySeries: rootMeterSeries - Component.onCompleted: ensureSamples(xAxis.min, xAxis.max) - } - Connections { - target: xAxis - onMinChanged: rootMeterSeriesAdapter.ensureSamples(xAxis.min, xAxis.max) - onMaxChanged: rootMeterSeriesAdapter.ensureSamples(xAxis.min, xAxis.max) + PowerConsumptionBalanceHistory { + Layout.fillWidth: true + Layout.preferredHeight: width + visible: producers.count > 0 } - AreaSeries { - id: rootMeterAreaSeries - color: Style.accentColor - borderWidth: 0 - axisX: xAxis - axisY: yAxis - name: qsTr("Unknown") - useOpenGL: true - lowerSeries: LineSeries { - id: rootMeterLowerSeries - XYPoint { x: xAxis.max.getTime(); y: 0 } - XYPoint { x: xAxis.min.getTime(); y: 0 } - } - // HACK: We want this to be created (added to the chart) *before* the repeater Series below... - // That might not be the case for a reason I don't understand. Most likely due to a mix of the declarative - // approach here and the imperative approach using chartView.createSeries() below. - // So hacking around by blocking the repeater from loading until this one is done - property bool ready: false - Component.onCompleted: ready = true - - upperSeries: LineSeries { - id: rootMeterSeries - - onPointAdded: { - var newPoint = rootMeterSeries.at(index) - - if (newPoint.x > rootMeterLowerSeries.at(0).x) { - rootMeterLowerSeries.replace(0, newPoint.x, 0) - } - if (newPoint.x < rootMeterLowerSeries.at(1).x) { - rootMeterLowerSeries.replace(1, newPoint.x, 0) - } - } - } + PowerProductionBalanceHistory { + Layout.fillWidth: true + Layout.preferredHeight: width + visible: producers.count > 0 } - Repeater { - id: consumersRepeater - model: rootMeterAreaSeries.ready && !engine.thingManager.fetchingData ? consumers : null - - delegate: Item { - id: consumer - property Thing thing: consumers.get(index) - - property var model: LogsModel { - id: logsModel - objectName: consumer.thing.name - engine: _engine - thingId: consumer.thing.id - typeIds: [consumer.thing.thingClass.stateTypes.findByName("currentPower").id] - viewStartTime: xAxis.min - live: true - onBusyChanged: { - if (busy) { - chartView.busyModels++ - } else { - chartView.busyModels-- - } - } - } - property XYSeriesAdapter adapter: XYSeriesAdapter { - id: seriesAdapter - objectName: consumer.thing.name + " adapter" - logsModel: logsModel - sampleRate: chartView.sampleRate - xySeries: upperSeries - } - Connections { - target: xAxis - onMinChanged: seriesAdapter.ensureSamples(xAxis.min, xAxis.max) - onMaxChanged: seriesAdapter.ensureSamples(xAxis.min, xAxis.max) - } - property XYSeries lineSeries: LineSeries { - id: upperSeries - onPointAdded: { - var newPoint = upperSeries.at(index) - - if (newPoint.x > lowerSeries.at(0).x) { - lowerSeries.replace(0, newPoint.x, 0) - } - if (newPoint.x < lowerSeries.at(1).x) { - lowerSeries.replace(1, newPoint.x, 0) - } - } - } - LineSeries { - id: lowerSeries - XYPoint { x: xAxis.max.getTime(); y: 0 } - XYPoint { x: xAxis.min.getTime(); y: 0 } - } - - property AreaSeries areaSeries: null - Component.onCompleted: { - var indexInModel = consumers.indexOf(consumer.thing) - print("creating series", consumer.thing.name, index, indexInModel) - seriesAdapter.ensureSamples(xAxis.min, xAxis.max) - areaSeries = chartView.createSeries(ChartView.SeriesTypeArea, consumer.thing.name, xAxis, yAxis) - areaSeries.useOpenGL = true - areaSeries.upperSeries = upperSeries; - seriesAdapter.baseSeries = Qt.binding(function() { - if (index > 0) { - return consumersRepeater.itemAt(index - 1).lineSeries - } else { - return null; - } - }) - areaSeries.lowerSeries = Qt.binding(function() { - if (index > 0) { - return consumersRepeater.itemAt(index - 1).lineSeries - } else { - return lowerSeries; - } - }) - - var color = Style.accentColor - for (var j = 0; j <= indexInModel; j+=2) { - if (indexInModel % 2 == 0) { - color = Qt.lighter(color, 1.2); - } else { - color = Qt.darker(color, 1.2) - } - } - areaSeries.color = color; - areaSeries.borderColor = color; - areaSeries.borderWidth = 0; - } - Component.onDestruction: { - chartView.removeSeries(areaSeries) - } - } + ConsumersBarChart { + Layout.fillWidth: true + Layout.preferredHeight: width + energyManager: energyManager + visible: consumers.count > 0 + } + ConsumersHistory { + Layout.fillWidth: true + Layout.preferredHeight: width + visible: consumers.count > 0 } - ValueAxis { - id: yAxis - readonly property XYSeriesAdapter highestSeriesAdapter: consumersRepeater.count > 0 ? consumersRepeater.itemAt(consumersRepeater.count - 1).adapter : null - property double rawMax: rootMeter ? rootMeterSeriesAdapter.maxValue - : highestSeriesAdapter ? highestSeriesAdapter.maxValue : 1 - property double rawMin: rootMeter ? rootMeterSeriesAdapter.minValue - : highestSeriesAdapter ? highestSeriesAdapter.minValue : 0 - max: Math.ceil(Math.max(rawMax * 0.9, rawMax * 1.1)) - min: Math.floor(Math.min(rawMin * 0.9, rawMin * 1.1)) - // This seems to crash occationally -// onMinChanged: applyNiceNumbers(); -// onMaxChanged: applyNiceNumbers(); - labelsFont: Style.extraSmallFont - labelFormat: "%d" - labelsColor: Style.foregroundColor - color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) - gridLineColor: color + PowerBalanceStats { + Layout.fillWidth: true + Layout.preferredHeight: width + energyManager: energyManager } - - DateTimeAxis { - id: xAxis - gridVisible: false - color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) - tickCount: chartView.width / 70 - labelsFont: Style.extraSmallFont - labelsColor: Style.foregroundColor - property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 - - function getTimeSpanString() { - var td = Math.round(timeDiff) - if (td < 60) { - return qsTr("%n seconds", "", td); - } - td = Math.round(td / 60) - if (td < 60) { - return qsTr("%n minutes", "", td); - } - td = Math.round(td / 60) - if (td < 48) { - return qsTr("%n hours", "", td); - } - td = Math.round(td / 24); - if (td < 14) { - return qsTr("%n days", "", td); - } - td = Math.round(td / 7) - if (td < 9) { - return qsTr("%n weeks", "", td); - } - td = Math.round(td * 7 / 30) - if (td < 24) { - return qsTr("%n months", "", td); - } - td = Math.round(td * 30 / 356) - return qsTr("%n years", "", td) - } - - titleText: { - if (xAxis.min.getYear() === xAxis.max.getYear() - && xAxis.min.getMonth() === xAxis.max.getMonth() - && xAxis.min.getDate() === xAxis.max.getDate()) { - return Qt.formatDate(xAxis.min) + " (" + getTimeSpanString() + ")" - } - return Qt.formatDate(xAxis.min) + " - " + Qt.formatDate(xAxis.max) + " (" + getTimeSpanString() + ")" - } - titleBrush: Style.foregroundColor - format: { - if (timeDiff < 60) { // one minute - return "mm:ss" - } - if (timeDiff < 60 * 60) { // one hour - return "hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 2) { // two day - return "hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 7) { // one week - return "ddd hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month - return "dd.MM." - } - return "MMM yy" - } - - min: { - var date = new Date(); - date.setTime(date.getTime() - (1000 * 60 * 60 * 6) + 2000); - return date; - } - max: { - var date = new Date(); - date.setTime(date.getTime() + 2000) - return date; - } - } - - MouseArea { - id: scrollMouseArea - x: chartView.plotArea.x - y: chartView.plotArea.y - width: chartView.plotArea.width - height: chartView.plotArea.height - property int lastX: 0 - property int startX: 0 - preventStealing: false - - property bool autoScroll: true - - function scrollRightLimited(dx) { - chartView.animationOptions = ChartView.NoAnimation - var now = new Date() - // if we're already at the limit, don't even start scrolling - if (dx < 0 || xAxis.max < now) { - chartView.scrollRight(dx) - } - // figure out if we scrolled too far - var overshoot = xAxis.max.getTime() - now.getTime() - // print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot)) - if (overshoot > 0) { - var range = xAxis.max - xAxis.min - xAxis.max = now - xAxis.min = new Date(xAxis.max.getTime() - range) - } - // If the user scrolled closer than 5 pixels to the right edge, enable autoscroll - autoScroll = overshoot > -5; - - chartView.animationOptions = ChartView.SeriesAnimations - } - - function zoomInLimited(dy) { - chartView.animationOptions = ChartView.NoAnimation - var oldMax = xAxis.max; - chartView.scrollRight(dy); - xAxis.min = new Date(xAxis.min.getTime() - xAxis.timeDiff * 1000 * 2) - chartView.animationOptions = ChartView.SeriesAnimations - } - - onPressed: { - lastX = mouse.x - startX = mouse.x - preventStealing = true - } - onClicked: { - // var pt = chartView.mapToValue(Qt.point(mouse.x + chartView.plotArea.x, mouse.y + chartView.plotArea.y), mainSeries) - // mainSeries.markClosestPoint(pt) - } - - onWheel: { - scrollRightLimited(-wheel.pixelDelta.x) - // zoomInLimited(wheel.pixelDelta.y) - } - - onPositionChanged: { - if (lastX !== mouse.x) { - scrollRightLimited(lastX - mouseX) - lastX = mouse.x - } - - if (Math.abs(startX - mouse.x) > 10) { - preventStealing = true; - } - } - - onReleased: preventStealing = false; - - - Timer { - running: scrollMouseArea.autoScroll - interval: 1000 - repeat: true - onTriggered: { - scrollMouseArea.scrollRightLimited(10) - } - } + ConsumerStats { + Layout.fillWidth: true + Layout.preferredHeight: width + energyManager: energyManager + visible: consumers.count > 0 } } } + } EmptyViewPlaceholder { anchors.centerIn: parent width: parent.width - app.margins * 2 - visible: !engine.thingManager.fetchingData && energyMeters.count == 0 + visible: !engine.jsonRpcClient.experiences.hasOwnProperty("Energy") + title: qsTr("Energy plugin not installed installed.") + text: qsTr("This %1 system does not have the energy extensions installed.").arg(Configuration.systemName) + imageSource: "../images/smartmeter.svg" + buttonText: qsTr("Install energy plugin") + onButtonClicked: pageStack.push(Qt.resolvedUrl("../system/PackageListPage.qml"), {filter: "nymea-experience-plugin-energy"}) + } + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + visible: engine.jsonRpcClient.experiences.hasOwnProperty("Energy") && !engine.thingManager.fetchingData && energyMeters.count == 0 title: qsTr("There are no energy meters installed.") text: qsTr("To get an overview of your current energy usage, install an energy meter.") imageSource: "../images/smartmeter.svg" diff --git a/nymea-app/ui/mainviews/energy/ConsumerStats.qml b/nymea-app/ui/mainviews/energy/ConsumerStats.qml new file mode 100644 index 00000000..e7d0a38b --- /dev/null +++ b/nymea-app/ui/mainviews/energy/ConsumerStats.qml @@ -0,0 +1,168 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.3 +import QtCharts 2.3 +import Nymea 1.0 + +ChartView { + id: root + backgroundColor: "transparent" + legend.alignment: Qt.AlignBottom + legend.labelColor: Style.foregroundColor + legend.font: Style.extraSmallFont + + margins.left: 0 + margins.right: 0 + margins.bottom: 0 + margins.top: 0 + + title: qsTr("Consumer statistics") + titleColor: Style.foregroundColor + + property EnergyManager energyManager: null + + readonly property date dayStart: { + var d = new Date(); + d.setHours(0,0,0,0); + return d; + } + + readonly property var daysList: { + var ret = [] + for (var i = 6; i >= 0; i--) { + var last = new Date(dayStart) + ret.push(last.setDate(last.getDate() - i)) + } + return ret; + } + + readonly property var daysListNames: { + var ret = [] + for (var i = 0; i < daysList.length; i++) { + ret.push(new Date(daysList[i]).toLocaleString(Qt.locale(), "ddd")) + } + return ret; + } + + readonly property date weekStart: { + var d = new Date(); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() - d.getDay()); + return d + } + readonly property date monthStart: { + var d = new Date(); + d.setHours(0,0,0,0); + d.setDate(1); + return d; + } + readonly property date yearStart: { + var d = new Date(); + d.setHours(0,0,0,0); + d.setDate(1); + d.setMonth(0); + return d; + } + + + ThingsProxy { + id: consumers + engine: _engine + shownInterfaces: ["smartmeterconsumer"] + sortStateName: "totalEnergyConsumed" + sortOrder: Qt.DescendingOrder + + } + Connections { + target: engine.thingManager + onFetchingDataChanged: { + var thingIds = [] + for (var i = 0; i < consumers.count; i++) { + thingIds.push(consumers.get(i).id) + } + powerLogs.thingIds = thingIds + } + } + + ThingPowerLogs { + id: powerLogs + engine: _engine + sampleRate: EnergyLogs.SampleRate1Day + startTime: root.yearStart + loadingInhibited: thingIds.length === 0 + + onFetchingDataChanged: { + if (!fetchingData) { + barSeries.clear(); + for (var j = 0; j < consumers.count; j++) { + var consumer = consumers.get(j) + var consumptionValues = [] + for (var i = 0; i < daysList.length; i++) { + var start = powerLogs.find(consumer.id, new Date(daysList[i])) + var startValue = start !== null ? start.totalConsumption : 0 + var end = i < daysList.length -1 ? powerLogs.find(consumer.id, new Date(daysList[i+1])) : null + var endValue = end !== null ? end.totalConsumption : start !== null ? consumer.stateByName("totalEnergyConsumed").value : 0 + var consumptionValue = endValue - startValue + consumptionValues.push(consumptionValue) + valueAxis.adjustMax(consumptionValue) + } + var barSet = barSeries.append(consumer.name, consumptionValues) + barSet.borderWidth = 0 + barSet.borderColor = barSet.color + } + } + } + } + + Item { + id: labelsLayout + x: Style.smallMargins + y: root.plotArea.y + height: root.plotArea.height + width: 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(0) + "kWh" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + } + } + } + + BarSeries { + id: barSeries + axisX: BarCategoryAxis { + id: categoryAxis + categories: daysListNames + labelsColor: Style.foregroundColor + labelsFont: Style.extraSmallFont + gridVisible: false + gridLineColor: Style.tileOverlayColor + lineVisible: false + titleVisible: false + shadesVisible: false + + } + 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) + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/energy/ConsumersBarChart.qml b/nymea-app/ui/mainviews/energy/ConsumersBarChart.qml new file mode 100644 index 00000000..58be1390 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/ConsumersBarChart.qml @@ -0,0 +1,160 @@ +import QtQuick 2.0 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.3 +import QtGraphicalEffects 1.0 +import Nymea 1.0 +import "qrc:/ui/components" + +Item { + id: root + + property EnergyManager energyManager: null + + property int tickCount: 5 + + property int labelsWidth: 40 + + + QtObject { + id: d + property int topMargin: Style.margins + property int bottomMargin: Style.margins + property int leftMargin: Style.margins + property int rightMargin: Style.margins + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.smallMargins + Label { + text: qsTr("Consumers") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + + Item { + id: valueAxis + Layout.fillWidth: true + Layout.fillHeight: true + + property double max: Math.ceil(root.energyManager.currentPowerConsumption / 100) * 100 + Repeater { + model: root.tickCount + delegate: RowLayout { + width: parent.width - d.leftMargin - d.rightMargin + y: index * ((parent.height - d.topMargin - d.bottomMargin - Style.iconSize - Style.margins) / (root.tickCount - 1)) - height / 2 + d.topMargin + x: d.leftMargin + Label { + property double value: (valueAxis.max - index * (valueAxis.max / (root.tickCount - 1))) + text: (value >= 1000 ? (value / 1000).toFixed(2) : value.toFixed(1)) + (value >= 1000 ? "kW" : "W") + font: Style.extraSmallFont + Layout.preferredWidth: root.labelsWidth + } + Rectangle { + Layout.preferredHeight: 1 + Layout.fillWidth: true + color: Style.tileOverlayColor + } + } + } + + RowLayout { + anchors.fill: parent + anchors.topMargin: d.topMargin + anchors.leftMargin: root.labelsWidth + d.leftMargin + anchors.bottomMargin: d.bottomMargin + anchors.rightMargin: d.rightMargin + + Repeater { + model: consumers.count + 1 + + delegate: ColumnLayout { + id: consumerDelegate + Layout.fillHeight: true + Layout.preferredWidth: root.width / consumers.count + spacing: Style.margins + property Thing thing: consumers.get(index) + property State currentPowerState: thing ? thing.stateByName("currentPower") : null + + property double consumption: { + var consumption = 0 + if (thing) { + consumption = currentPowerState.value + } else { + consumption = energyManager.currentPowerConsumption + for (var i = 0; i < consumers.count; i++) { + consumption -= consumers.get(i).stateByName("currentPower").value + } + } + return consumption; + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + Rectangle { + id: bar + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + top: parent.top + } + gradient: Gradient { + GradientStop { position: 1; color: Style.green } + GradientStop { position: 0.5; color: Style.orange } + GradientStop { position: 0; color: Style.red } + } + width: 20 + visible: false + } + + Item { + id: barMask + anchors.fill: bar + Rectangle { + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + width: 20 + Behavior on height { NumberAnimation { duration: Style.slowAnimationDuration; easing.type: Easing.InOutQuad } } + height: Math.max(1, parent.height * consumerDelegate.consumption / valueAxis.max) + // visible: false + } + } + + + OpacityMask { + anchors.fill: bar + source: bar + maskSource: barMask + } + + Label { + anchors.bottom: bar.bottom + anchors.left: bar.left + text: consumerDelegate.thing ? consumerDelegate.thing.name : qsTr("Unknown") + transform: Rotation { + angle: -90 + } + } + + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.iconSize + + ColorIcon { + anchors.centerIn: parent + name: consumerDelegate.thing ? app.interfacesToIcon(consumerDelegate.thing.thingClass.interfaces) : "energy" + } + } + + } + } + } + } + } + +} diff --git a/nymea-app/ui/mainviews/energy/ConsumersHistory.qml b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml new file mode 100644 index 00000000..6761cfe2 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml @@ -0,0 +1,223 @@ +import QtQuick 2.0 +import QtCharts 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.3 +import Nymea 1.0 + +ChartView { + id: root + 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 + + ThingPowerLogs { + id: thingPowerLogs + engine: _engine + startTime: dateTimeAxis.min + sampleRate: EnergyLogs.SampleRate15Mins + thingIds: [] + loadingInhibited: thingIds.length === 0 + + onEntriesAdded: { + var thingValues = ({}) + var timestamp = entries[0].timestamp + 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 + } + + // 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) + } + } + } + + property PowerBalanceLogs powerBalanceLogs: PowerBalanceLogs { + 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) + } + } + } + + ThingsProxy { + id: consumers + engine: _engine + shownInterfaces: ["smartmeterconsumer"] + } + Connections { + target: engine.thingManager + onFetchingDataChanged: d.updateConsumers() + } + + + Component.onCompleted: { + for (var i = 0; i < powerBalanceLogs.count; i++) { + var entry = powerBalanceLogs.get(i); + consumptionSeries.addEntry(entry) + } + + d.updateConsumers(); + } + + QtObject { + id: d + property var thingsSeriesMap: ({}) + + function updateConsumers() { + if (engine.thingManager.fetchingData) { + return; + } + + for (var thingId in d.thingsSeriesMap) { + root.removeSeries(d.thingsSeriesMap[thingId]) + } + d.thingsSeriesMap = ({}) + + var consumerThingIds = [] + for (var i = 0; i < consumers.count; i++) { + var thing = consumers.get(i); + + 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 = root.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, valueAxis) + series.lowerSeries = baseSeries + series.upperSeries = lineSeriesComponent.createObject(series) + series.borderWidth = 0; + series.borderColor = series.color + + print("Adding thingId series", thing.id, thing.name) + d.thingsSeriesMap[thing.id] = series + consumerThingIds.push(thing.id) + } + thingPowerLogs.thingIds = consumerThingIds; + } + } + + Component { + id: lineSeriesComponent + LineSeries { } + } + + 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 + + } + + Item { + id: labelsLayout + x: Style.smallMargins + y: root.plotArea.y + height: root.plotArea.height + width: 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 + 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.accentColor + borderWidth: 0 + borderColor: color + name: qsTr("Unknown") + + lowerSeries: LineSeries { + id: zeroSeries + XYPoint { x: dateTimeAxis.min.getTime(); y: 0 } + XYPoint { x: dateTimeAxis.max.getTime(); y: 0 } + function update(timestamp) { + append(timestamp, 0); + removePoints(1,1); + } + } + upperSeries: LineSeries { + id: consumptionUpperSeries + } + + function addEntry(entry) { + consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption) + } + } +} diff --git a/nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml b/nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml new file mode 100644 index 00000000..2c230112 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml @@ -0,0 +1,145 @@ +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtCharts 2.2 +import Nymea 1.0 + +ChartView { + id: consumptionPieChart + backgroundColor: "transparent" + animationOptions: ChartView.SeriesAnimations + title: qsTr("Current power consumption balance") + titleColor: Style.foregroundColor + legend.visible: false + + property EnergyManager energyManager: null + + + PieSeries { + id: consumptionBalanceSeries + size: 0.9 + holeSize: 0.7 + + property double fromGrid: Math.max(0, energyManager.currentPowerAcquisition) + property double fromStorage: -Math.min(0, energyManager.currentPowerStorage) + property double fromProduction: energyManager.currentPowerConsumption - fromGrid - fromStorage + + PieSlice { + color: Style.red + borderColor: Style.foregroundColor + value: consumptionBalanceSeries.fromGrid + } + PieSlice { + color: Style.green + borderColor: Style.foregroundColor + value: consumptionBalanceSeries.fromProduction + } + PieSlice { + color: Style.orange + borderColor: Style.foregroundColor + value: consumptionBalanceSeries.fromStorage + } + PieSlice { + color: Style.backgroundColor + borderColor: Style.foregroundColor + value: consumptionBalanceSeries.fromGrid == 0 && consumptionBalanceSeries.fromProduction == 0 && consumptionBalanceSeries.fromStorage == 0 ? 1 : 0 + } + } + + + Column { + id: centerLayout + x: consumptionPieChart.plotArea.x + (consumptionPieChart.plotArea.width - width) / 2 + y: consumptionPieChart.plotArea.y + (consumptionPieChart.plotArea.height - height) / 2 + width: consumptionPieChart.plotArea.width * 0.65 +// height: consumptionPieChart.plotArea.height * 0.65 + height: childrenRect.height + spacing: Style.smallMargins + + ColumnLayout { + width: parent.width + spacing: 0 + Label { + text: qsTr("Total") + font: Style.smallFont + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + + Label { + text: "%1 %2" + .arg((energyManager.currentPowerConsumption / (energyManager.currentPowerConsumption > 1000 ? 1000 : 1)).toFixed(1)) + .arg(energyManager.currentPowerConsumption > 1000 ? "kW" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.bigFont + + } + } + + + ColumnLayout { + width: parent.width + spacing: 0 + Label { + text: qsTr("From grid") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.extraSmallFont + } + Label { + property double absValue: consumptionBalanceSeries.fromGrid + color: Style.red + text: "%1 %2" + .arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) + .arg(absValue > 1000 ? "kWh" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + } + } + + + ColumnLayout { + width: parent.width + spacing: 0 + Label { + text: qsTr("From self production") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.extraSmallFont + } + Label { + color: Style.green + property double absValue: consumptionBalanceSeries.fromProduction + text: "%1 %2".arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) + .arg(absValue > 1000 ? "kW" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + } + } + ColumnLayout { + width: parent.width + spacing: 0 + visible: batteries.count > 0 + Label { + text: qsTr("From battery") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.extraSmallFont + } + Label { + color: Style.orange + property double absValue: consumptionBalanceSeries.fromStorage + text: "%1 %2".arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) + .arg(absValue > 1000 ? "kW" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + } + } + } +} diff --git a/nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml b/nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml new file mode 100644 index 00000000..70db7831 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml @@ -0,0 +1,145 @@ +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtCharts 2.2 +import Nymea 1.0 + +ChartView { + id: productionPieChart + backgroundColor: "transparent" + animationOptions: ChartView.SeriesAnimations + title: qsTr("Current power production balance") + titleColor: Style.foregroundColor + legend.visible: false + + property EnergyManager energyManager: null + + PieSeries { + id: productionBalanceSeries + size: 0.9 + holeSize: 0.7 + + property double toGrid: Math.abs(Math.min(0, energyManager.currentPowerAcquisition)) + property double toStorage: Math.max(0, energyManager.currentPowerStorage) + property double toConsumers: -energyManager.currentPowerProduction - toGrid - toStorage + + PieSlice { + color: Style.red + borderColor: Style.foregroundColor + value: productionBalanceSeries.toConsumers + } + PieSlice { + color: Style.green + borderColor: Style.foregroundColor + value: productionBalanceSeries.toGrid + } + PieSlice { + color: Style.orange + borderColor: Style.foregroundColor + value: productionBalanceSeries.toStorage + } + PieSlice { + color: Style.backgroundColor + borderColor: Style.foregroundColor + value: productionBalanceSeries.toConsumers == 0 && productionBalanceSeries.toGrid == 0 && productionBalanceSeries.toStorage == 0 ? 1 : 0 + } + } + + + Column { + id: productionCenterLayout + x: productionPieChart.plotArea.x + (productionPieChart.plotArea.width - width) / 2 + y: productionPieChart.plotArea.y + (productionPieChart.plotArea.height - height) / 2 + width: productionPieChart.plotArea.width * 0.65 +// height: productionPieChart.plotArea.height * 0.65 + height: childrenRect.height + spacing: Style.smallMargins + + ColumnLayout { + spacing: 0 + width: parent.width + Label { + text: qsTr("Total") + font: Style.smallFont + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + + Label { + property double absValue: Math.abs(Math.min(0, energyManager.currentPowerProduction)) + text: "%1 %2" + .arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) + .arg(absValue > 1000 ? "kW" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.bigFont + + } + } + + + ColumnLayout { + spacing: 0 + width: parent.width + Label { + text: qsTr("Consumed") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.extraSmallFont + } + Label { + property double absValue: productionBalanceSeries.toConsumers + color: Style.red + text: "%1 %2" + .arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) + .arg(absValue > 1000 ? "kWh" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + } + } + + + ColumnLayout { + spacing: 0 + width: parent.width + Label { + text: qsTr("To grid") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.extraSmallFont + } + Label { + color: Style.green + property double absValue: productionBalanceSeries.toGrid + text: "%1 %2".arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) + .arg(absValue > 1000 ? "kW" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + } + } + ColumnLayout { + spacing: 0 + width: parent.width + visible: batteries.count > 0 + Label { + text: qsTr("To battery") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.extraSmallFont + } + Label { + color: Style.orange + property double absValue: productionBalanceSeries.toStorage + text: "%1 %2".arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) + .arg(absValue > 1000 ? "kW" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + } + } + } +} diff --git a/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml new file mode 100644 index 00000000..21d55bfa --- /dev/null +++ b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml @@ -0,0 +1,177 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.3 +import QtCharts 2.3 +import Nymea 1.0 + +ChartView { + id: root + 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 + + title: qsTr("Energy consumption statistics") + titleColor: Style.foregroundColor + + property EnergyManager energyManager: null + + readonly property date dayStart: { + var d = new Date(); + d.setHours(0,0,0,0); + return d; + } + + readonly property var daysList: { + var ret = [] + for (var i = 6; i >= 0; i--) { + var last = new Date(dayStart) + ret.push(last.setDate(last.getDate() - i)) + } + return ret; + } + + readonly property var daysListNames: { + var ret = [] + for (var i = 0; i < daysList.length; i++) { + ret.push(new Date(daysList[i]).toLocaleString(Qt.locale(), "ddd")) + } + return ret; + } + + + readonly property date weekStart: { + var d = new Date(); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() - d.getDay()); + return d + } + readonly property date monthStart: { + var d = new Date(); + d.setHours(0,0,0,0); + d.setDate(1); + return d; + } + readonly property date yearStart: { + var d = new Date(); + d.setHours(0,0,0,0); + d.setDate(1); + d.setMonth(0); + return d; + } + + PowerBalanceLogs { + id: yearLogs + engine: _engine + sampleRate: EnergyLogs.SampleRate1Day + startTime: root.yearStart; + + onFetchingDataChanged: { + if (!fetchingData) { + for (var i = 0; i < daysList.length; i++) { + var start = yearLogs.find(new Date(daysList[i])) + var end = null; + if (i+1 < daysList.length) { + end = yearLogs.find(new Date(daysList[i+1])) + } + var consumptionValue = (end != null ? end.totalConsumption : root.energyManager.totalConsumption) - start.totalConsumption + var productionValue = (end != null ? end.totalProduction : root.energyManager.totalProduction) - start.totalProduction + var acquisitionValue = (end != null ? end.totalAcquisition : root.energyManager.totalAcquisition) - start.totalAcquisition + var returnValue = (end != null ? end.totalReturn : root.energyManager.totalReturn) - start.totalReturn + consumptionSeries.append(consumptionValue) + productionSeries.append(productionValue) + acquisitionSeries.append(acquisitionValue) + returnSeries.append(returnValue) + + valueAxis.adjustMax(consumptionValue) + valueAxis.adjustMax(productionValue) + valueAxis.adjustMax(acquisitionValue) + valueAxis.adjustMax(returnValue) + } + } + } + } + + Item { + id: labelsLayout + x: Style.smallMargins + y: root.plotArea.y + height: root.plotArea.height + width: 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(0) + "kWh" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + } + } + } + + BarSeries { + axisX: BarCategoryAxis { + id: categoryAxis + categories: daysListNames + labelsColor: Style.foregroundColor + labelsFont: Style.extraSmallFont + gridVisible: false + gridLineColor: Style.tileOverlayColor + lineVisible: false + titleVisible: false + shadesVisible: false + + } + 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 / 100) * 100 + } + } + } + + BarSet { + id: consumptionSeries + label: qsTr("Consumed") + borderWidth: 0 + } + BarSet { + id: productionSeries + label: qsTr("Produced") + color: Style.green + borderWidth: 0 + borderColor: color + } + BarSet { + id: acquisitionSeries + label: qsTr("From grid") + color: Style.red + borderWidth: 0 + borderColor: color + } + BarSet { + id: returnSeries + label: qsTr("To grid") + color: Style.orange + borderWidth: 0 + borderColor: color + } + } +} diff --git a/nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml b/nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml new file mode 100644 index 00000000..fed938bb --- /dev/null +++ b/nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml @@ -0,0 +1,232 @@ +import QtQuick 2.0 +import QtCharts 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.3 +import Nymea 1.0 + +ChartView { + id: root + backgroundColor: "transparent" + margins.left: 0 + margins.right: 0 + margins.bottom: 0 + margins.top: 0 + + title: qsTr("Power consumption balance history") + titleColor: Style.foregroundColor + + legend.alignment: Qt.AlignBottom + legend.labelColor: Style.foregroundColor + legend.font: Style.extraSmallFont + + property PowerBalanceLogs energyLogs: PowerBalanceLogs { + id: powerBalanceLogs + engine: _engine + startTime: dateTimeAxis.min + sampleRate: EnergyLogs.SampleRate15Mins + } + + 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) + } + } + + 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) + } + } + } + + Timer { + interval: 60000 + repeat: true + onTriggered: { + var now = new Date() + if (dateTimeAxis.now < now) { + dateTimeAxis.now = now + zeroSeries.update(now) + } + } + } + + 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 + + } + + Item { + id: labelsLayout + x: Style.smallMargins + y: root.plotArea.y + height: root.plotArea.height + width: 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 + 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 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: false + + 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)) + } + } + +} diff --git a/nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml b/nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml new file mode 100644 index 00000000..f684f193 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml @@ -0,0 +1,227 @@ +import QtQuick 2.0 +import QtCharts 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.3 +import Nymea 1.0 + +ChartView { + id: root + backgroundColor: "transparent" + margins.left: 0 + margins.right: 0 + margins.bottom: 0 + margins.top: 0 + + title: qsTr("Power production balance history") + titleColor: Style.foregroundColor + + legend.alignment: Qt.AlignBottom + legend.labelColor: Style.foregroundColor + legend.font: Style.extraSmallFont + + property PowerBalanceLogs energyLogs: PowerBalanceLogs { + id: powerBalanceLogs + engine: _engine + startTime: dateTimeAxis.min + } + + 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) + } + } + + 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) + } + } + } + + Timer { + interval: 60000 + repeat: true + onTriggered: { + var now = new Date() + if (dateTimeAxis.now < now) { + dateTimeAxis.now = now + zeroSeries.update(now) + } + } + } + + ValueAxis { + id: valueAxis + min: 0 + max: -Math.ceil(powerBalanceLogs.minValue / 1000) * 1000 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false +// visible: false + + } + + Item { + id: labelsLayout + x: Style.smallMargins + y: root.plotArea.y + height: root.plotArea.height + width: 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 + 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)) + } + + 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 + + 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: false + 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 + } + } + +} diff --git a/nymea-app/ui/system/AboutNymeaPage.qml b/nymea-app/ui/system/AboutNymeaPage.qml index 0d60117f..bdd0357d 100644 --- a/nymea-app/ui/system/AboutNymeaPage.qml +++ b/nymea-app/ui/system/AboutNymeaPage.qml @@ -58,6 +58,10 @@ SettingsPageBase { subText: engine.jsonRpcClient.serverUuid progressive: false prominentSubText: false + onClicked: { + PlatformHelper.toClipBoard(engine.jsonRpcClient.serverUuid) + ToolTip.show(qsTr("ID copied to clipboard"), 500); + } } NymeaSwipeDelegate { Layout.fillWidth: true diff --git a/nymea-app/ui/system/PackageListPage.qml b/nymea-app/ui/system/PackageListPage.qml new file mode 100644 index 00000000..eea80863 --- /dev/null +++ b/nymea-app/ui/system/PackageListPage.qml @@ -0,0 +1,227 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.3 +import "../components" +import Nymea 1.0 + +SettingsPageBase { + id: packageListPage + title: qsTr("All packages") + + property Packages packages: engine.systemController.packages + property string filter: "" + + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Layout.margins: Style.margins + spacing: Style.margins + ColorIcon { + name: "find" + } + TextField { + id: filterTextField + Layout.fillWidth: true + text: packageListPage.filter + onTextChanged: packageListPage.filter = text + } + ColorIcon { + name: "close" + visible: filterTextField.text.length > 0 + MouseArea { + anchors.fill: parent + onClicked: filterTextField.text = "" + } + } + } + } + + ListView { + id: listView + Layout.fillWidth: true + Layout.preferredHeight: packageListPage.height - y + clip: true + + ScrollBar.vertical: ScrollBar {} + + model: PackagesFilterModel { + id: filterModel + packages: packageListPage.packages + nameFilter: packageListPage.filter + } + + delegate: NymeaSwipeDelegate { + width: parent.width + text: model.displayName + subText: model.candidateVersion + prominentSubText: false + iconName: model.updateAvailable + ? Qt.resolvedUrl("../images/system-update.svg") + : Qt.resolvedUrl("../images/view-" + (model.installedVersion.length > 0 ? "expand" : "collapse") + ".svg") + iconColor: model.updateAvailable + ? "green" + : model.installedVersion.length > 0 ? "blue" : Style.iconColor + onClicked: { + pageStack.push(packageDetailsComponent, {pkg: filterModel.get(index)}) + } + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - Style.margins * 2 + visible: filterModel.count == 0 + title: qsTr("No package found") + text: qsTr("We're sorry. We couldn't find any package matching the search term %1.").arg(packageListPage.filter) + imageSource: "/ui/images/dialog-error-symbolic.svg" + buttonVisible: false + } + + UpdateRunningOverlay { + } + } + Component { + id: packageDetailsComponent + SettingsPageBase { + id: packageDetailsPage + + title: qsTr("Package information") + property Package pkg: null + + GridLayout { + Layout.fillWidth: true + columns: app.landscape ? 2 : 1 + RowLayout { + Layout.margins: app.margins + spacing: app.margins + ColorIcon { + Layout.preferredHeight: Style.iconSize * 2 + Layout.preferredWidth: Style.iconSize * 2 + name: "../images/plugin.svg" + color: Style.accentColor + } + Label { + Layout.fillWidth: true + text: pkg.displayName + font.pixelSize: app.largeFont + elide: Text.ElideRight + } + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + text: packageDetailsPage.pkg.summary + wrapMode: Text.WordWrap + } + + NymeaSwipeDelegate { + Layout.fillWidth: true + text: qsTr("Installed version:") + subText: packageDetailsPage.pkg.installedVersion.length > 0 ? packageDetailsPage.pkg.installedVersion : qsTr("Not installed") + progressive: false + } + + NymeaSwipeDelegate { + Layout.fillWidth: true + text: qsTr("Candidate version:") + subText: packageDetailsPage.pkg.candidateVersion + visible: packageDetailsPage.pkg.updateAvailable || packageDetailsPage.pkg.installedVersion.length === 0 + progressive: false + } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + visible: packageDetailsPage.pkg.updateAvailable || packageDetailsPage.pkg.installedVersion.length === 0 + text: packageDetailsPage.pkg.updateAvailable ? qsTr("Update") : qsTr("Install") + onClicked: { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("This will start a system update. Note that the update might take several minutes and your %1 might not be functioning properly or restart during this time.").arg(Configuration.systemName) + + "\n\n" + + qsTr("\nDo you want to proceed?") + var popup = dialog.createObject(app, + { + headerIcon: "../images/system-update.svg", + title: qsTr("Start update"), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + engine.systemController.updatePackages(packageDetailsPage.pkg.id) + }) + + } + } + Button { + Layout.fillWidth: true + Layout.margins: app.margins + text: qsTr("Remove") + visible: packageDetailsPage.pkg.canRemove + onClicked: { + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var text = qsTr("This will start a system update. Note that the update might take several minutes and your %1 system might not be functioning properly during this time and restart during the process.\nDo you want to proceed?").arg(Configuration.systemName) + var popup = dialog.createObject(app, + { + headerIcon: "../images/system-update.svg", + title: qsTr("Remove package"), + text: text, + standardButtons: Dialog.Ok | Dialog.Cancel + }); + popup.open(); + popup.accepted.connect(function() { + engine.systemController.removePackages(packageDetailsPage.pkg.id) + }) + } + } + + } + UpdateRunningOverlay { + } + } + } + + Component { + id: errorDialogComponent + + ErrorDialog { + id: errorDialog + } + } +} + + + + + diff --git a/nymea-app/ui/system/SystemUpdatePage.qml b/nymea-app/ui/system/SystemUpdatePage.qml index 619cd314..0b3c5727 100644 --- a/nymea-app/ui/system/SystemUpdatePage.qml +++ b/nymea-app/ui/system/SystemUpdatePage.qml @@ -209,7 +209,7 @@ Page { Layout.fillWidth: true text: qsTr("Install or remove software") onClicked: { - pageStack.push(packageListComponent, {packages: engine.systemController.packages}) + pageStack.push("PackageListPage.qml") } } } @@ -258,152 +258,6 @@ Page { } } - Component { - id: packageListComponent - Page { - id: packageListPage - - property var packages: null - - header: NymeaHeader { - text: qsTr("All packages") - onBackPressed: pageStack.pop() - } - - ListView { - anchors.fill: parent - model: PackagesFilterModel { - id: filterModel - packages: packageListPage.packages - } - delegate: NymeaSwipeDelegate { - width: parent.width - text: model.displayName - subText: model.candidateVersion - prominentSubText: false - iconName: model.updateAvailable - ? Qt.resolvedUrl("../images/system-update.svg") - : Qt.resolvedUrl("../images/view-" + (model.installedVersion.length > 0 ? "expand" : "collapse") + ".svg") - iconColor: model.updateAvailable - ? "green" - : model.installedVersion.length > 0 ? "blue" : Style.iconColor - onClicked: { - pageStack.push(packageDetailsComponent, {pkg: filterModel.get(index)}) - } - } - } - UpdateRunningOverlay { - } - } - } - - Component { - id: packageDetailsComponent - Page { - id: packageDetailsPage - - property Package pkg: null - - header: NymeaHeader { - text: qsTr("Package information") - onBackPressed: pageStack.pop() - } - - GridLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } - columns: app.landscape ? 2 : 1 - RowLayout { - Layout.margins: app.margins - spacing: app.margins - ColorIcon { - Layout.preferredHeight: Style.iconSize * 2 - Layout.preferredWidth: Style.iconSize * 2 - name: "../images/plugin.svg" - color: Style.accentColor - } - Label { - Layout.fillWidth: true - text: pkg.displayName - font.pixelSize: app.largeFont - elide: Text.ElideRight - } - } - - Label { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - text: packageDetailsPage.pkg.summary - wrapMode: Text.WordWrap - } - - NymeaSwipeDelegate { - Layout.fillWidth: true - text: qsTr("Installed version:") - subText: packageDetailsPage.pkg.installedVersion.length > 0 ? packageDetailsPage.pkg.installedVersion : qsTr("Not installed") - progressive: false - } - - NymeaSwipeDelegate { - Layout.fillWidth: true - text: qsTr("Candidate version:") - subText: packageDetailsPage.pkg.candidateVersion - visible: packageDetailsPage.pkg.updateAvailable || packageDetailsPage.pkg.installedVersion.length === 0 - progressive: false - } - Button { - Layout.fillWidth: true - Layout.margins: app.margins - visible: packageDetailsPage.pkg.updateAvailable || packageDetailsPage.pkg.installedVersion.length === 0 - text: packageDetailsPage.pkg.updateAvailable ? qsTr("Update") : qsTr("Install") - onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("This will start a system update. Note that the update might take several minutes and your %1 might not be functioning properly or restart during this time.").arg(Configuration.systemName) - + "\n\n" - + qsTr("\nDo you want to proceed?") - var popup = dialog.createObject(app, - { - headerIcon: "../images/system-update.svg", - title: qsTr("Start update"), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - engine.systemController.updatePackages(packageDetailsPage.pkg.id) - }) - - } - } - Button { - Layout.fillWidth: true - Layout.margins: app.margins - text: qsTr("Remove") - visible: packageDetailsPage.pkg.canRemove - onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); - var text = qsTr("This will start a system update. Note that the update might take several minutes and your %1 system might not be functioning properly during this time and restart during the process.\nDo you want to proceed?").arg(Configuration.systemName) - var popup = dialog.createObject(app, - { - headerIcon: "../images/system-update.svg", - title: qsTr("Remove package"), - text: text, - standardButtons: Dialog.Ok | Dialog.Cancel - }); - popup.open(); - popup.accepted.connect(function() { - engine.systemController.removePackages(packageDetailsPage.pkg.id) - }) - } - } - - } - UpdateRunningOverlay { - } - } - } - - UpdateRunningOverlay { }