Rework energy views

This commit is contained in:
Michael Zanetti 2021-09-28 13:53:10 +02:00
parent 36ac85d00c
commit e7af535914
30 changed files with 2627 additions and 621 deletions

View File

@ -1,5 +1,4 @@
#include "energylogs.h" #include "energylogs.h"
#include "powerbalancelogs.h"
#include <QMetaEnum> #include <QMetaEnum>
@ -7,9 +6,32 @@
NYMEA_LOGGING_CATEGORY(dcEnergyLogs, "EnergyLogs") 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 &timestamp, 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 Engine *EnergyLogs::engine() const
@ -26,16 +48,19 @@ void EnergyLogs::setEngine(Engine *engine)
if (!m_engine) { if (!m_engine) {
return; return;
} }
qCDebug(dcEnergyLogs()) << "************* getting energylogs" << m_engine->jsonRpcClient()->experiences();
if (m_engine->jsonRpcClient()->experiences().value("Energy").toString() >= "1.0") { if (m_engine->jsonRpcClient()->experiences().value("Energy").toString() >= "1.0") {
m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceivedInternal");
QVariantMap params; connect(engine, &Engine::destroyed, this, [=](){
QMetaEnum metaEnum = QMetaEnum::fromType<SampleRate>(); if (engine == m_engine) {
params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate)); m_engine = nullptr;
m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceived"); emit engineChanged();
m_engine->jsonRpcClient()->sendCommand("Energy.GetPowerBalanceLogs", params, this, "powerBalanceLogsReceived"); }
// m_engine->jsonRpcClient()->sendCommand("Energy.GetThingPowerLogs", params, this, "thingPowerLogsReceived"); });
if (m_ready && !m_loadingInhibited) {
fetchLogs();
}
} }
} }
} }
@ -79,45 +104,178 @@ void EnergyLogs::setThingIds(const QList<QUuid> &thingIds)
} }
} }
PowerBalanceLogs *EnergyLogs::powerBalanceLogs() const QDateTime EnergyLogs::startTime() const
{ {
return m_powerBalanceLogs; return m_startTime;
} }
void EnergyLogs::powerBalanceLogsReceived(int commandId, const QVariantMap &params) void EnergyLogs::setStartTime(const QDateTime &startTime)
{ {
Q_UNUSED(commandId) if (m_startTime != startTime) {
foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) { qCDebug(dcEnergyLogs()) << "Setting startTime";
QVariantMap map = variant.toMap(); m_startTime = startTime;
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); emit startTimeChanged();
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);
} }
} }
void EnergyLogs::thingPowerLogsReceived(int commandId, const QVariantMap &params) QDateTime EnergyLogs::endTime() const
{ {
Q_UNUSED(commandId) return m_endTime;
qCDebug(dcEnergyLogs) << "got energy logs";
} }
void EnergyLogs::notificationReceived(const QVariantMap &data) void EnergyLogs::setEndTime(const QDateTime &endTime)
{ {
QString notification = data.value("notification").toString(); if (m_endTime != endTime) {
QVariantMap params = data.value("params").toMap(); m_endTime = endTime;
emit endTimeChanged();
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);
} }
} }
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<EnergyLogEntry *> &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 &params)
{
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 sampleRate = static_cast<SampleRate>(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<SampleRate>();
params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate));
if (!m_startTime.isNull()) {
params.insert("from", m_startTime.toSecsSinceEpoch());
}
if (!m_endTime.isNull()) {
params.insert("to", m_endTime.toSecsSinceEpoch());
}
qCDebug(dcEnergyLogs()) << "Fetching power balance logs" << params;
m_engine->jsonRpcClient()->sendCommand("Energy.Get" + logsName(), params, this, "getLogsResponse");
}

View File

@ -5,18 +5,36 @@
#include <QObject> #include <QObject>
#include <QUuid> #include <QUuid>
#include <QQmlParserStatus>
class PowerBalanceLogs;
class EnergyLogs : public QObject class EnergyLogEntry: public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT)
public:
EnergyLogEntry(QObject *parent = nullptr);
EnergyLogEntry(const QDateTime &timestamp, 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(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged)
Q_PROPERTY(SampleRate sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged) Q_PROPERTY(SampleRate sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
Q_PROPERTY(bool fetchPowerBalance READ fetchPowerBalance WRITE setFetchPowerBalance NOTIFY fetchPowerBalanceChanged) Q_PROPERTY(QDateTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
Q_PROPERTY(QList<QUuid> thingIds READ thingIds WRITE setThingIds NOTIFY thingIdsChanged) 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: public:
enum SampleRate { enum SampleRate {
SampleRate1Min = 1, SampleRate1Min = 1,
@ -31,6 +49,7 @@ public:
Q_ENUM(SampleRate) Q_ENUM(SampleRate)
explicit EnergyLogs(QObject *parent = nullptr); explicit EnergyLogs(QObject *parent = nullptr);
~EnergyLogs();
Engine *engine() const; Engine *engine() const;
void setEngine(Engine *engine); void setEngine(Engine *engine);
@ -44,26 +63,70 @@ public:
QList<QUuid> thingIds() const; QList<QUuid> thingIds() const;
void setThingIds(const QList<QUuid> &thingIds); void setThingIds(const QList<QUuid> &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: signals:
void engineChanged(); void engineChanged();
void sampleRateChanged(); void sampleRateChanged();
void fetchPowerBalanceChanged(); void fetchPowerBalanceChanged();
void thingIdsChanged(); void thingIdsChanged();
void startTimeChanged();
void endTimeChanged();
void liveChanged();
void fetchingDataChanged();
void loadingInhibitedChanged();
void countChanged();
void entryAdded(EnergyLogEntry *entry);
void entriesAdded(const QList<EnergyLogEntry*> entries);
protected:
virtual QString logsName() const = 0;
virtual QVariantMap fetchParams() const;
virtual void logEntriesReceived(const QVariantMap &params) = 0;
virtual void notificationReceived(const QVariantMap &data) = 0;
void appendEntry(EnergyLogEntry *entry);
void appendEntries(const QList<EnergyLogEntry *> &entries);
private slots: private slots:
void powerBalanceLogsReceived(int commandId, const QVariantMap &params); void getLogsResponse(int commandId, const QVariantMap &params);
void thingPowerLogsReceived(int commandId, const QVariantMap &params); void notificationReceivedInternal(const QVariantMap &data);
void notificationReceived(const QVariantMap &data);
void fetchLogs();
private: private:
Engine *m_engine = nullptr; Engine *m_engine = nullptr;
SampleRate m_sampleRate = SampleRate15Mins; SampleRate m_sampleRate = SampleRate15Mins;
bool m_fetchPowerBalance = true; bool m_fetchPowerBalance = true;
QList<QUuid> m_thingIds; QList<QUuid> m_thingIds;
QDateTime m_startTime;
QDateTime m_endTime;
bool m_live = true;
bool m_fetchingData = false;
bool m_loadingInhibited = false;
bool m_ready = false;
PowerBalanceLogs *m_powerBalanceLogs = nullptr; QList<EnergyLogEntry*> m_list;
}; };
#endif // ENERGYLOGS_H #endif // ENERGYLOGS_H

View File

@ -34,6 +34,7 @@ void EnergyManager::setEngine(Engine *engine)
emit engineChanged(); emit engineChanged();
if (m_engine) { 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()->registerNotificationHandler(this, "Energy", "notificationReceived");
m_engine->jsonRpcClient()->sendCommand("Energy.GetRootMeter", QVariantMap(), this, "getRootMeterResponse"); m_engine->jsonRpcClient()->sendCommand("Energy.GetRootMeter", QVariantMap(), this, "getRootMeterResponse");
m_engine->jsonRpcClient()->sendCommand("Energy.GetPowerBalance", QVariantMap(), this, "getPowerBalanceResponse"); m_engine->jsonRpcClient()->sendCommand("Energy.GetPowerBalance", QVariantMap(), this, "getPowerBalanceResponse");
@ -71,6 +72,31 @@ double EnergyManager::currentPowerAcquisition() const
return m_currentPowerAcquisition; 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) void EnergyManager::notificationReceived(const QVariantMap &data)
{ {
QString notification = data.value("notification").toString(); QString notification = data.value("notification").toString();
@ -83,6 +109,11 @@ void EnergyManager::notificationReceived(const QVariantMap &data)
m_currentPowerConsumption = params.value("currentPowerConsumption").toDouble(); m_currentPowerConsumption = params.value("currentPowerConsumption").toDouble();
m_currentPowerProduction = params.value("currentPowerProduction").toDouble(); m_currentPowerProduction = params.value("currentPowerProduction").toDouble();
m_currentPowerAcquisition = params.value("currentPowerAcquisition").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(); emit powerBalanceChanged();
} else if (notification == "Energy.PowerBalanceLogEntryAdded") { } else if (notification == "Energy.PowerBalanceLogEntryAdded") {
@ -110,6 +141,11 @@ void EnergyManager::getPowerBalanceResponse(int commandId, const QVariantMap &pa
m_currentPowerConsumption = params.value("currentPowerConsumption").toDouble(); m_currentPowerConsumption = params.value("currentPowerConsumption").toDouble();
m_currentPowerProduction = params.value("currentPowerProduction").toDouble(); m_currentPowerProduction = params.value("currentPowerProduction").toDouble();
m_currentPowerAcquisition = params.value("currentPowerAcquisition").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(); emit powerBalanceChanged();
} }

View File

@ -15,6 +15,11 @@ class EnergyManager : public QObject
Q_PROPERTY(double currentPowerConsumption READ currentPowerConsumption NOTIFY powerBalanceChanged) Q_PROPERTY(double currentPowerConsumption READ currentPowerConsumption NOTIFY powerBalanceChanged)
Q_PROPERTY(double currentPowerProduction READ currentPowerProduction NOTIFY powerBalanceChanged) Q_PROPERTY(double currentPowerProduction READ currentPowerProduction NOTIFY powerBalanceChanged)
Q_PROPERTY(double currentPowerAcquisition READ currentPowerAcquisition 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: public:
explicit EnergyManager(QObject *parent = nullptr); explicit EnergyManager(QObject *parent = nullptr);
@ -29,6 +34,11 @@ public:
double currentPowerConsumption() const; double currentPowerConsumption() const;
double currentPowerProduction() const; double currentPowerProduction() const;
double currentPowerAcquisition() const; double currentPowerAcquisition() const;
double currentPowerStorage() const;
double totalConsumption() const;
double totalProduction() const;
double totalAcquisition() const;
double totalReturn() const;
signals: signals:
void engineChanged(); void engineChanged();
@ -47,7 +57,11 @@ private:
double m_currentPowerConsumption = 0; double m_currentPowerConsumption = 0;
double m_currentPowerProduction = 0; double m_currentPowerProduction = 0;
double m_currentPowerAcquisition = 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 #endif // ENERGYMANAGER_H

View File

@ -1,19 +1,22 @@
#include "powerbalancelogs.h" #include "powerbalancelogs.h"
PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime &timestamp, double consumption, double production, double acquisition, double storage, QObject *parent): PowerBalanceLogEntry::PowerBalanceLogEntry(QObject *parent): EnergyLogEntry(parent)
QObject(parent),
m_timestamp(timestamp),
m_consumption(consumption),
m_production(production),
m_acquisition(acquisition),
m_storage(storage)
{ {
} }
QDateTime PowerBalanceLogEntry::timestamp() const PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime &timestamp, 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 double PowerBalanceLogEntry::consumption() const
@ -36,27 +39,29 @@ double PowerBalanceLogEntry::storage() const
return m_storage; return m_storage;
} }
double PowerBalanceLogEntry::totalConsumption() const
PowerBalanceLogs::PowerBalanceLogs(QObject *parent) : QAbstractListModel(parent)
{ {
return m_totalConsumption;
} }
int PowerBalanceLogs::rowCount(const QModelIndex &parent) const double PowerBalanceLogEntry::totalProduction() const
{ {
Q_UNUSED(parent) return m_totalProduction;
return m_list.count();
} }
QVariant PowerBalanceLogs::data(const QModelIndex &index, int role) const double PowerBalanceLogEntry::totalAcquisition() const
{ {
return QVariant(); return m_totalAcquisition;
} }
QHash<int, QByteArray> PowerBalanceLogs::roleNames() const double PowerBalanceLogEntry::totalReturn() const
{ {
QHash<int, QByteArray> roles; return m_totalReturn;
return roles; }
PowerBalanceLogs::PowerBalanceLogs(QObject *parent) : EnergyLogs(parent)
{
} }
double PowerBalanceLogs::minValue() const double PowerBalanceLogs::minValue() const
@ -69,14 +74,14 @@ double PowerBalanceLogs::maxValue() const
return m_maxValue; return m_maxValue;
} }
QString PowerBalanceLogs::logsName() const
{
return "PowerBalanceLogs";
}
void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry) void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry)
{ {
entry->setParent(this); appendEntry(entry);
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
m_list.append(entry);
endInsertRows();
emit entryAdded(entry);
emit countChanged();
if (entry->consumption() < m_minValue) { if (entry->consumption() < m_minValue) {
m_minValue = entry->consumption(); m_minValue = entry->consumption();
@ -114,6 +119,83 @@ void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry)
} }
EnergyLogEntry *PowerBalanceLogs::find(const QDateTime &timestamp) const
{
// qWarning() << "Finding log entry for timestamp:" << timestamp;
int oldest = 0;
int newest = rowCount() - 1;
EnergyLogEntry *entry = nullptr;
int step = 0;
while (oldest < newest && step < rowCount()) {
EnergyLogEntry *oldestEntry = get(oldest);
EnergyLogEntry *newestEntry = get(newest);
int middle = (newest - oldest) / 2 + oldest;
EnergyLogEntry *middleEntry = get(middle);
// qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest);
if (timestamp <= oldestEntry->timestamp()) {
return oldestEntry;
}
if (timestamp >= newestEntry->timestamp()) {
return newestEntry;
}
if (timestamp == middleEntry->timestamp()) {
return middleEntry;
}
if (timestamp < middleEntry->timestamp()) {
oldest = middle;
} else {
newest = middle;
}
if ((newest - oldest) <= 1) {
return newestEntry;
}
step++;
}
return entry;
}
void PowerBalanceLogs::logEntriesReceived(const QVariantMap &params)
{
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 PowerBalanceLogs *PowerBalanceLogsProxy::powerBalanceLogs() const
{ {
return m_powerBalanceLogs; return m_powerBalanceLogs;

View File

@ -6,53 +6,68 @@
#include <QDateTime> #include <QDateTime>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
class PowerBalanceLogEntry: public QObject #include "energylogs.h"
class PowerBalanceLogEntry: public EnergyLogEntry
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT)
Q_PROPERTY(double consumption READ consumption CONSTANT) Q_PROPERTY(double consumption READ consumption CONSTANT)
Q_PROPERTY(double production READ production CONSTANT) Q_PROPERTY(double production READ production CONSTANT)
Q_PROPERTY(double acquisition READ acquisition CONSTANT) Q_PROPERTY(double acquisition READ acquisition CONSTANT)
Q_PROPERTY(double storage READ storage 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: public:
PowerBalanceLogEntry() = default; PowerBalanceLogEntry(QObject *parent = nullptr);
PowerBalanceLogEntry(const QDateTime &timestamp, double consumption, double production, double acquisition, double storage, QObject *parent); PowerBalanceLogEntry(const QDateTime &timestamp, 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 consumption() const;
double production() const; double production() const;
double acquisition() const; double acquisition() const;
double storage() const; double storage() const;
double totalConsumption() const;
double totalProduction() const;
double totalAcquisition() const;
double totalReturn() const;
private: private:
QDateTime m_timestamp; QDateTime m_timestamp;
double m_consumption = 0; double m_consumption = 0;
double m_production = 0; double m_production = 0;
double m_acquisition = 0; double m_acquisition = 0;
double m_storage = 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_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged) Q_PROPERTY(double minValue READ minValue NOTIFY minValueChanged)
Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged) Q_PROPERTY(double maxValue READ maxValue NOTIFY maxValueChanged)
public: public:
explicit PowerBalanceLogs(QObject *parent = nullptr); explicit PowerBalanceLogs(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
double minValue() const; double minValue() const;
double maxValue() const; double maxValue() const;
void addEntry(PowerBalanceLogEntry *entry); Q_INVOKABLE EnergyLogEntry* find(const QDateTime &timestamp) const;
signals: signals:
void countChanged();
void entryAdded(PowerBalanceLogEntry *entry);
void minValueChanged(); void minValueChanged();
void maxValueChanged(); void maxValueChanged();
protected:
QString logsName() const override;
void logEntriesReceived(const QVariantMap &params) override;
void notificationReceived(const QVariantMap &data) override;
private: private:
QList<PowerBalanceLogEntry*> m_list; void addEntry(PowerBalanceLogEntry *entry);
double m_minValue = 0; double m_minValue = 0;
double m_maxValue = 0; double m_maxValue = 0;
}; };

View File

@ -0,0 +1,175 @@
#include "thingpowerlogs.h"
ThingPowerLogEntry::ThingPowerLogEntry(QObject *parent):
EnergyLogEntry(parent)
{
}
ThingPowerLogEntry::ThingPowerLogEntry(const QDateTime &timestamp, 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<QUuid> ThingPowerLogs::thingIds() const
{
return m_thingIds;
}
void ThingPowerLogs::setThingIds(const QList<QUuid> &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 &timestamp)
{
// TODO: Can we do a binary search even if they key we're looking for is not unique (but still sorted)?
// For now, 365 * consumers items is the max we'll have here which seems on the edge for doing a stupid linear search...
for (int i = rowCount() - 1; i >= 0; i--) {
ThingPowerLogEntry *entry = static_cast<ThingPowerLogEntry*>(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<ThingPowerLogEntry *> &entries)
{
QList<EnergyLogEntry*> 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 &params)
{
// Grouping them so when the UI gets entriesAdded, the whole set for this timstamp will be available at once
QList<ThingPowerLogEntry*> 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();
}
}

View File

@ -0,0 +1,73 @@
#ifndef THINGPOWERLOGS_H
#define THINGPOWERLOGS_H
#include <QObject>
#include <QAbstractListModel>
#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 &timestamp, 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<QUuid> 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<QUuid> thingIds() const;
void setThingIds(const QList<QUuid> &thingIds);
double minValue() const;
double maxValue() const;
Q_INVOKABLE EnergyLogEntry *find(const QUuid &thingId, const QDateTime &timestamp);
signals:
void thingIdsChanged();
void minValueChanged();
void maxValueChanged();
protected:
QString logsName() const override;
QVariantMap fetchParams() const override;
void logEntriesReceived(const QVariantMap &params) override;
void notificationReceived(const QVariantMap &data) override;
private:
void addEntry(ThingPowerLogEntry *entry);
void addEntries(const QList<ThingPowerLogEntry *> &entries);
QList<QUuid> m_thingIds;
double m_minValue = 0;
double m_maxValue = 0;
QList<ThingPowerLogEntry*> m_cachedEntries;
QTimer m_cacheTimer;
};
#endif // THINGPOWERLOGS_H

View File

@ -138,6 +138,7 @@
#include "energy/energymanager.h" #include "energy/energymanager.h"
#include "energy/energylogs.h" #include "energy/energylogs.h"
#include "energy/powerbalancelogs.h" #include "energy/powerbalancelogs.h"
#include "energy/thingpowerlogs.h"
#include <QtQml/qqml.h> #include <QtQml/qqml.h>
@ -361,9 +362,12 @@ void registerQmlTypes() {
qmlRegisterType<AppData>(uri, 1, 0, "AppData"); qmlRegisterType<AppData>(uri, 1, 0, "AppData");
qmlRegisterType<EnergyManager>(uri, 1, 0, "EnergyManager"); qmlRegisterType<EnergyManager>(uri, 1, 0, "EnergyManager");
qmlRegisterType<EnergyLogs>(uri, 1, 0, "EnergyLogs"); qmlRegisterUncreatableType<EnergyLogEntry>(uri, 1, 0, "EnergyLogEntry", "EnergyLogentry is an abstract class");
qmlRegisterUncreatableType<EnergyLogs>(uri, 1, 0, "EnergyLogs", "EnergyLogs is an abstract class");
qmlRegisterType<PowerBalanceLogs>(uri, 1, 0, "PowerBalanceLogs"); qmlRegisterType<PowerBalanceLogs>(uri, 1, 0, "PowerBalanceLogs");
qmlRegisterType<PowerBalanceLogEntry>(uri, 1, 0, "PowerBalanceLogEntry"); qmlRegisterType<PowerBalanceLogEntry>(uri, 1, 0, "PowerBalanceLogEntry");
qmlRegisterType<ThingPowerLogEntry>(uri, 1, 0, "ThingPowerLogEntry");
qmlRegisterType<ThingPowerLogs>(uri, 1, 0, "ThingPowerLogs");
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel"); qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
} }

View File

@ -24,6 +24,7 @@ SOURCES += \
$$PWD/energy/energylogs.cpp \ $$PWD/energy/energylogs.cpp \
$$PWD/energy/energymanager.cpp \ $$PWD/energy/energymanager.cpp \
$$PWD/energy/powerbalancelogs.cpp \ $$PWD/energy/powerbalancelogs.cpp \
$$PWD/energy/thingpowerlogs.cpp \
$$PWD/models/scriptsproxymodel.cpp \ $$PWD/models/scriptsproxymodel.cpp \
$$PWD/tagwatcher.cpp \ $$PWD/tagwatcher.cpp \
$$PWD/zigbee/zigbeenode.cpp \ $$PWD/zigbee/zigbeenode.cpp \
@ -183,6 +184,7 @@ HEADERS += \
$$PWD/energy/energylogs.h \ $$PWD/energy/energylogs.h \
$$PWD/energy/energymanager.h \ $$PWD/energy/energymanager.h \
$$PWD/energy/powerbalancelogs.h \ $$PWD/energy/powerbalancelogs.h \
$$PWD/energy/thingpowerlogs.h \
$$PWD/models/scriptsproxymodel.h \ $$PWD/models/scriptsproxymodel.h \
$$PWD/tagwatcher.h \ $$PWD/tagwatcher.h \
$$PWD/zigbee/zigbeenode.h \ $$PWD/zigbee/zigbeenode.h \

View File

@ -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 Package *PackagesFilterModel::get(int index) const
{ {
return m_packages->get(mapToSource(this->index(index, 0)).row()); 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; return false;
} }
} }
if (!m_nameFilter.isEmpty()) {
if (!m_packages->get(source_row)->displayName().contains(m_nameFilter)) {
return false;
}
}
return true; return true;
} }

View File

@ -41,6 +41,8 @@ class PackagesFilterModel : public QSortFilterProxyModel
Q_PROPERTY(bool updatesOnly READ updatesOnly WRITE setUpdatesOnly NOTIFY updatesOnlyChanged) Q_PROPERTY(bool updatesOnly READ updatesOnly WRITE setUpdatesOnly NOTIFY updatesOnlyChanged)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(QString nameFilter READ nameFilter WRITE setNameFilter NOTIFY nameFilterChanged)
public: public:
explicit PackagesFilterModel(QObject *parent = nullptr); explicit PackagesFilterModel(QObject *parent = nullptr);
@ -50,6 +52,9 @@ public:
bool updatesOnly() const; bool updatesOnly() const;
void setUpdatesOnly(bool updatesOnly); void setUpdatesOnly(bool updatesOnly);
QString nameFilter() const;
void setNameFilter(const QString &nameFilter);
Q_INVOKABLE Package* get(int index) const; Q_INVOKABLE Package* get(int index) const;
protected: protected:
@ -59,11 +64,14 @@ signals:
void countChanged(); void countChanged();
void packagesChanged(); void packagesChanged();
void updatesOnlyChanged(); void updatesOnlyChanged();
void nameFilterChanged();
private: private:
Packages *m_packages; Packages *m_packages;
bool m_updatesOnly = false; bool m_updatesOnly = false;
QString m_nameFilter;
}; };
#endif // PACKAGESFILTERMODEL_H #endif // PACKAGESFILTERMODEL_H

View File

@ -36,6 +36,7 @@
ThingsProxy::ThingsProxy(QObject *parent) : ThingsProxy::ThingsProxy(QObject *parent) :
QSortFilterProxyModel(parent) QSortFilterProxyModel(parent)
{ {
setSortRole(Things::RoleName);
} }
Engine *ThingsProxy::engine() const Engine *ThingsProxy::engine() const
@ -61,7 +62,7 @@ void ThingsProxy::setEngine(Engine *engine)
setSourceModel(m_engine->thingManager()->things()); setSourceModel(m_engine->thingManager()->things());
setSortRole(Things::RoleName); setSortRole(Things::RoleName);
sort(0); sort(0, sortOrder());
connect(sourceModel(), SIGNAL(countChanged()), this, SIGNAL(countChanged())); connect(sourceModel(), SIGNAL(countChanged()), this, SIGNAL(countChanged()));
connect(sourceModel(), &QAbstractItemModel::dataChanged, this, [this]() { connect(sourceModel(), &QAbstractItemModel::dataChanged, this, [this]() {
invalidateFilter(); 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 Thing *ThingsProxy::get(int index) const
{ {
return getInternal(mapToSource(this->index(index, 0)).row()); 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; 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); int comparison = QString::localeAwareCompare(leftName, rightName);
if (comparison == 0) { if (comparison == 0) {

View File

@ -80,6 +80,12 @@ class ThingsProxy : public QSortFilterProxyModel
Q_PROPERTY(bool groupByInterface READ groupByInterface WRITE setGroupByInterface NOTIFY groupByInterfaceChanged) 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: public:
explicit ThingsProxy(QObject *parent = nullptr); explicit ThingsProxy(QObject *parent = nullptr);
@ -152,6 +158,11 @@ public:
bool groupByInterface() const; bool groupByInterface() const;
void setGroupByInterface(bool groupByInterface); 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 *get(int index) const;
Q_INVOKABLE Thing *getThing(const QUuid &thingId) const; Q_INVOKABLE Thing *getThing(const QUuid &thingId) const;
Q_INVOKABLE int indexOf(Thing *thing) const; Q_INVOKABLE int indexOf(Thing *thing) const;
@ -180,6 +191,8 @@ signals:
void filterUpdatesChanged(); void filterUpdatesChanged();
void paramsFilterChanged(); void paramsFilterChanged();
void groupByInterfaceChanged(); void groupByInterfaceChanged();
void sortStateNameChanged();
void sortOrderChanged();
void countChanged(); void countChanged();
private: private:
@ -214,6 +227,8 @@ private:
bool m_groupByInterface = false; bool m_groupByInterface = false;
QString m_sortStateName;
protected: protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE;
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;

View File

@ -259,5 +259,15 @@
<file>ui/devicepages/EvChargerThingPage.qml</file> <file>ui/devicepages/EvChargerThingPage.qml</file>
<file>ui/components/BlurredLabel.qml</file> <file>ui/components/BlurredLabel.qml</file>
<file>ui/components/NymeaSpinBox.qml</file> <file>ui/components/NymeaSpinBox.qml</file>
<file>ui/mainviews/EnergyPieChartDelegate.qml</file>
<file>ui/mainviews/energy/PowerConsumptionBalanceHistory.qml</file>
<file>ui/mainviews/energy/PowerProductionBalanceHistory.qml</file>
<file>ui/mainviews/energy/ConsumersBarChart.qml</file>
<file>ui/mainviews/energy/ConsumersHistory.qml</file>
<file>ui/mainviews/energy/PowerBalanceStats.qml</file>
<file>ui/mainviews/energy/CurrentProductionBalancePieChart.qml</file>
<file>ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml</file>
<file>ui/mainviews/energy/ConsumerStats.qml</file>
<file>ui/system/PackageListPage.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -105,11 +105,11 @@ Item {
property color red: "indianred" property color red: "indianred"
property color green: "mediumseagreen" property color green: "mediumseagreen"
property color yellow: "gold" property color yellow: "gold"
property color white: "white" property color white: "white"
property color gray: "gray" property color gray: "gray"
property color darkGray: "darkGray" property color darkGray: "darkGray"
property color blue: "deepskyblue" property color blue: "deepskyblue"
property color orange: "#f6a625"
readonly property int fastAnimationDuration: 100 readonly property int fastAnimationDuration: 100
readonly property int animationDuration: 150 readonly property int animationDuration: 150

View File

@ -211,7 +211,7 @@ ThingPageBase {
property bool isCharging: root.chargingState && root.chargingState.value === "charging" property bool isCharging: root.chargingState && root.chargingState.value === "charging"
property bool isDischarging: root.chargingState && root.chargingState.value === "discharging" property bool isDischarging: root.chargingState && root.chargingState.value === "discharging"
property double availableWh: isBattery ? root.capacityState.value * 1000 * root.batteryLevelState.value / 100 : 0 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 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 date endTime: isBattery ? new Date(new Date().getTime() + remainingHours * 60 * 60 * 1000) : new Date()
property int n: Math.round(remainingHours) property int n: Math.round(remainingHours)

View File

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

View File

@ -32,16 +32,23 @@ import QtQuick 2.8
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtGraphicalEffects 1.0
import QtCharts 2.2 import QtCharts 2.2
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../delegates" import "../delegates"
import "energy"
MainViewBase { MainViewBase {
id: root id: root
contentY: flickable.contentY + topMargin contentY: flickable.contentY + topMargin
EnergyManager {
id: energyManager
engine: _engine
}
ThingsProxy { ThingsProxy {
id: energyMeters id: energyMeters
engine: _engine engine: _engine
@ -61,6 +68,11 @@ MainViewBase {
shownInterfaces: ["smartmeterproducer"] shownInterfaces: ["smartmeterproducer"]
} }
ThingsProxy {
id: batteries
engine: _engine
shownInterfaces: ["energystorage"]
}
Flickable { Flickable {
id: flickable id: flickable
@ -70,406 +82,88 @@ MainViewBase {
visible: energyMeters.count > 0 visible: energyMeters.count > 0
topMargin: root.topMargin topMargin: root.topMargin
// GridLayout directly in a flickable causes problems at initialisation
GridLayout { Item {
id: energyGrid
width: parent.width width: parent.width
columns: root.width > 600 ? 2 : 1 height: energyGrid.implicitHeight
rowSpacing: 0
columnSpacing: 0
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 { GridLayout {
Layout.fillWidth: true id: energyGrid
Layout.preferredHeight: width * .7 width: parent.width
backgroundColor: Style.tileBackgroundColor property int rawColumns: Math.floor(flickable.width / 300)
backgroundRoundness: Style.cornerRadius columns: Math.max(1, rawColumns - (rawColumns % 2))
rootMeter: root.rootMeter rowSpacing: 0
meters: producers columnSpacing: 0
title: qsTr("Total produced energy")
stateName: "totalEnergyProduced"
readonly property State totalProducedState: rootMeter ? rootMeter.stateByName("totalEnergyProduced") : null
visible: (rootMeterTotalEnergyState && rootMeterTotalEnergyState.value > 0) || producers.count > 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 CurrentConsumptionBalancePieChart {
property var endTime: xAxis.max Layout.fillWidth: true
Layout.preferredHeight: width
property int sampleRate: XYSeriesAdapter.SampleRateMinute energyManager: energyManager
visible: producers.count > 0
property int busyModels: 0 }
CurrentProductionBalancePieChart {
BusyIndicator { Layout.fillWidth: true
anchors.centerIn: parent Layout.preferredHeight: width
visible: chartView.busyModels > 0 energyManager: energyManager
running: visible visible: producers.count > 0
} }
LogsModel { PowerConsumptionBalanceHistory {
id: rootMeterLogsModel Layout.fillWidth: true
objectName: "Root meter model" Layout.preferredHeight: width
engine: rootMeter ? _engine : null // Don't start fetching before we know what we want visible: producers.count > 0
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)
} }
AreaSeries { PowerProductionBalanceHistory {
id: rootMeterAreaSeries Layout.fillWidth: true
color: Style.accentColor Layout.preferredHeight: width
borderWidth: 0 visible: producers.count > 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)
}
}
}
} }
Repeater { ConsumersBarChart {
id: consumersRepeater Layout.fillWidth: true
model: rootMeterAreaSeries.ready && !engine.thingManager.fetchingData ? consumers : null Layout.preferredHeight: width
energyManager: energyManager
delegate: Item { visible: consumers.count > 0
id: consumer }
property Thing thing: consumers.get(index) ConsumersHistory {
Layout.fillWidth: true
property var model: LogsModel { Layout.preferredHeight: width
id: logsModel visible: consumers.count > 0
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)
}
}
} }
ValueAxis { PowerBalanceStats {
id: yAxis Layout.fillWidth: true
readonly property XYSeriesAdapter highestSeriesAdapter: consumersRepeater.count > 0 ? consumersRepeater.itemAt(consumersRepeater.count - 1).adapter : null Layout.preferredHeight: width
property double rawMax: rootMeter ? rootMeterSeriesAdapter.maxValue energyManager: energyManager
: 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
} }
ConsumerStats {
DateTimeAxis { Layout.fillWidth: true
id: xAxis Layout.preferredHeight: width
gridVisible: false energyManager: energyManager
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2) visible: consumers.count > 0
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)
}
}
} }
} }
} }
} }
EmptyViewPlaceholder { EmptyViewPlaceholder {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - app.margins * 2 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.") title: qsTr("There are no energy meters installed.")
text: qsTr("To get an overview of your current energy usage, install an energy meter.") text: qsTr("To get an overview of your current energy usage, install an energy meter.")
imageSource: "../images/smartmeter.svg" imageSource: "../images/smartmeter.svg"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,6 +58,10 @@ SettingsPageBase {
subText: engine.jsonRpcClient.serverUuid subText: engine.jsonRpcClient.serverUuid
progressive: false progressive: false
prominentSubText: false prominentSubText: false
onClicked: {
PlatformHelper.toClipBoard(engine.jsonRpcClient.serverUuid)
ToolTip.show(qsTr("ID copied to clipboard"), 500);
}
} }
NymeaSwipeDelegate { NymeaSwipeDelegate {
Layout.fillWidth: true Layout.fillWidth: true

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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
}
}
}

View File

@ -209,7 +209,7 @@ Page {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Install or remove software") text: qsTr("Install or remove software")
onClicked: { 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 { UpdateRunningOverlay {
} }