Rework energy views
This commit is contained in:
parent
36ac85d00c
commit
e7af535914
@ -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 ×tamp, QObject *parent):
|
||||||
|
QObject(parent),
|
||||||
|
m_timestamp(timestamp)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime EnergyLogEntry::timestamp() const
|
||||||
|
{
|
||||||
|
return m_timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnergyLogs::EnergyLogs(QObject *parent) : QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
EnergyLogs::~EnergyLogs()
|
||||||
|
{
|
||||||
|
if (m_engine) {
|
||||||
|
m_engine->jsonRpcClient()->unregisterNotificationHandler(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Engine *EnergyLogs::engine() const
|
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 ¶ms)
|
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 ¶ms)
|
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 ¶ms)
|
||||||
|
{
|
||||||
|
Q_UNUSED(commandId)
|
||||||
|
// qCDebug(dcEnergyLogs()) << "Energy logs response:" << params;
|
||||||
|
logEntriesReceived(params);
|
||||||
|
|
||||||
|
m_fetchingData = false;
|
||||||
|
emit fetchingDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnergyLogs::notificationReceivedInternal(const QVariantMap &data)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!m_live) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.value("notification").toString().contains("Log")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMetaEnum sampleRateEnum = QMetaEnum::fromType<SampleRate>();
|
||||||
|
SampleRate 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 ×tamp, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QDateTime timestamp() const;
|
||||||
|
private:
|
||||||
|
QDateTime m_timestamp;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class EnergyLogs : public QAbstractListModel, public QQmlParserStatus
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_INTERFACES(QQmlParserStatus)
|
||||||
|
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||||
Q_PROPERTY(Engine *engine READ engine WRITE setEngine NOTIFY engineChanged)
|
Q_PROPERTY(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 ¶ms) = 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 ¶ms);
|
void getLogsResponse(int commandId, const QVariantMap ¶ms);
|
||||||
void thingPowerLogsReceived(int commandId, const QVariantMap ¶ms);
|
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
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
#include "powerbalancelogs.h"
|
#include "powerbalancelogs.h"
|
||||||
|
|
||||||
PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime ×tamp, 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 ×tamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn, QObject *parent):
|
||||||
|
EnergyLogEntry(timestamp, parent),
|
||||||
|
m_consumption(consumption),
|
||||||
|
m_production(production),
|
||||||
|
m_acquisition(acquisition),
|
||||||
|
m_storage(storage),
|
||||||
|
m_totalConsumption(totalConsumption),
|
||||||
|
m_totalProduction(totalProduction),
|
||||||
|
m_totalAcquisition(totalAcquisition),
|
||||||
|
m_totalReturn(totalReturn)
|
||||||
{
|
{
|
||||||
return m_timestamp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double PowerBalanceLogEntry::consumption() const
|
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 ×tamp) const
|
||||||
|
{
|
||||||
|
// qWarning() << "Finding log entry for timestamp:" << timestamp;
|
||||||
|
int oldest = 0;
|
||||||
|
int newest = rowCount() - 1;
|
||||||
|
EnergyLogEntry *entry = nullptr;
|
||||||
|
int step = 0;
|
||||||
|
while (oldest < newest && step < rowCount()) {
|
||||||
|
EnergyLogEntry *oldestEntry = get(oldest);
|
||||||
|
EnergyLogEntry *newestEntry = get(newest);
|
||||||
|
int middle = (newest - oldest) / 2 + oldest;
|
||||||
|
EnergyLogEntry *middleEntry = get(middle);
|
||||||
|
// qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest);
|
||||||
|
if (timestamp <= oldestEntry->timestamp()) {
|
||||||
|
return oldestEntry;
|
||||||
|
}
|
||||||
|
if (timestamp >= newestEntry->timestamp()) {
|
||||||
|
return newestEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamp == middleEntry->timestamp()) {
|
||||||
|
return middleEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamp < middleEntry->timestamp()) {
|
||||||
|
oldest = middle;
|
||||||
|
} else {
|
||||||
|
newest = middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((newest - oldest) <= 1) {
|
||||||
|
return newestEntry;
|
||||||
|
}
|
||||||
|
step++;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerBalanceLogs::logEntriesReceived(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) {
|
||||||
|
QVariantMap map = variant.toMap();
|
||||||
|
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
||||||
|
double consumption = map.value("consumption").toDouble();
|
||||||
|
double production = map.value("production").toDouble();
|
||||||
|
double acquisition = map.value("acquisition").toDouble();
|
||||||
|
double storage = map.value("storage").toDouble();
|
||||||
|
double totalConsumption = map.value("totalConsumption").toDouble();
|
||||||
|
double totalProduction = map.value("totalProduction").toDouble();
|
||||||
|
double totalAcquisition = map.value("totalAcquisition").toDouble();
|
||||||
|
double totalReturn = map.value("totalReturn").toDouble();
|
||||||
|
PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn, this);
|
||||||
|
addEntry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerBalanceLogs::notificationReceived(const QVariantMap &data)
|
||||||
|
{
|
||||||
|
QString notification = data.value("notification").toString();
|
||||||
|
QVariantMap params = data.value("params").toMap();
|
||||||
|
if (notification == "Energy.PowerBalanceLogEntryAdded") {
|
||||||
|
QVariantMap map = params.value("powerBalanceLogEntry").toMap();
|
||||||
|
QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong());
|
||||||
|
double consumption = map.value("consumption").toDouble();
|
||||||
|
double production = map.value("production").toDouble();
|
||||||
|
double acquisition = map.value("acquisition").toDouble();
|
||||||
|
double storage = map.value("storage").toDouble();
|
||||||
|
double totalConsumption = map.value("totalConsumption").toDouble();
|
||||||
|
double totalProduction = map.value("totalProduction").toDouble();
|
||||||
|
double totalAcquisition = map.value("totalAcquisition").toDouble();
|
||||||
|
double totalReturn = map.value("totalReturn").toDouble();
|
||||||
|
PowerBalanceLogEntry *entry = new PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn, this);
|
||||||
|
addEntry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PowerBalanceLogs *PowerBalanceLogsProxy::powerBalanceLogs() const
|
PowerBalanceLogs *PowerBalanceLogsProxy::powerBalanceLogs() const
|
||||||
{
|
{
|
||||||
return m_powerBalanceLogs;
|
return m_powerBalanceLogs;
|
||||||
|
|||||||
@ -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 ×tamp, double consumption, double production, double acquisition, double storage, QObject *parent);
|
PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn, QObject *parent);
|
||||||
|
|
||||||
QDateTime timestamp() const;
|
|
||||||
double consumption() const;
|
double 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 ×tamp) 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 ¶ms) 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;
|
||||||
};
|
};
|
||||||
|
|||||||
175
libnymea-app/energy/thingpowerlogs.cpp
Normal file
175
libnymea-app/energy/thingpowerlogs.cpp
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
#include "thingpowerlogs.h"
|
||||||
|
|
||||||
|
ThingPowerLogEntry::ThingPowerLogEntry(QObject *parent):
|
||||||
|
EnergyLogEntry(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ThingPowerLogEntry::ThingPowerLogEntry(const QDateTime ×tamp, const QUuid &thingId, double currentPower, double totalConsumption, double totalProduction, QObject *parent):
|
||||||
|
EnergyLogEntry(timestamp, parent),
|
||||||
|
m_thingId(thingId),
|
||||||
|
m_currentPower(currentPower),
|
||||||
|
m_totalConsumption(totalConsumption),
|
||||||
|
m_totalProduction(totalProduction)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid ThingPowerLogEntry::thingId() const
|
||||||
|
{
|
||||||
|
return m_thingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ThingPowerLogEntry::currentPower() const
|
||||||
|
{
|
||||||
|
return m_currentPower;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ThingPowerLogEntry::totalConsumption() const
|
||||||
|
{
|
||||||
|
return m_totalConsumption;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ThingPowerLogEntry::totalProduction() const
|
||||||
|
{
|
||||||
|
return m_totalProduction;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThingPowerLogs::ThingPowerLogs(QObject *parent) : EnergyLogs(parent)
|
||||||
|
{
|
||||||
|
m_cacheTimer.setInterval(2000);
|
||||||
|
connect(&m_cacheTimer, &QTimer::timeout, this, [=](){
|
||||||
|
if (m_cachedEntries.count() > 0) {
|
||||||
|
addEntries(m_cachedEntries);
|
||||||
|
m_cachedEntries.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<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 ×tamp)
|
||||||
|
{
|
||||||
|
// TODO: Can we do a binary search even if they key we're looking for is not unique (but still sorted)?
|
||||||
|
// For now, 365 * consumers items is the max we'll have here which seems on the edge for doing a stupid linear search...
|
||||||
|
for (int i = rowCount() - 1; i >= 0; i--) {
|
||||||
|
ThingPowerLogEntry *entry = static_cast<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 ¶ms)
|
||||||
|
{
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
73
libnymea-app/energy/thingpowerlogs.h
Normal file
73
libnymea-app/energy/thingpowerlogs.h
Normal 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 ×tamp, const QUuid &thingId, double currentPower, double totalConsumption, double totalProduction, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QUuid thingId() const;
|
||||||
|
double currentPower() const;
|
||||||
|
double totalConsumption() const;
|
||||||
|
double totalProduction() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QUuid m_thingId;
|
||||||
|
double m_currentPower = 0;
|
||||||
|
double m_totalConsumption = 0;
|
||||||
|
double m_totalProduction = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThingPowerLogs : public EnergyLogs
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QList<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 ×tamp);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void thingIdsChanged();
|
||||||
|
|
||||||
|
void minValueChanged();
|
||||||
|
void maxValueChanged();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QString logsName() const override;
|
||||||
|
QVariantMap fetchParams() const override;
|
||||||
|
void logEntriesReceived(const QVariantMap ¶ms) override;
|
||||||
|
void notificationReceived(const QVariantMap &data) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void addEntry(ThingPowerLogEntry *entry);
|
||||||
|
void addEntries(const QList<ThingPowerLogEntry *> &entries);
|
||||||
|
|
||||||
|
QList<QUuid> m_thingIds;
|
||||||
|
double m_minValue = 0;
|
||||||
|
double m_maxValue = 0;
|
||||||
|
|
||||||
|
QList<ThingPowerLogEntry*> m_cachedEntries;
|
||||||
|
QTimer m_cacheTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // THINGPOWERLOGS_H
|
||||||
@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 \
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
36
nymea-app/ui/mainviews/EnergyPieChartDelegate.qml
Normal file
36
nymea-app/ui/mainviews/EnergyPieChartDelegate.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
168
nymea-app/ui/mainviews/energy/ConsumerStats.qml
Normal file
168
nymea-app/ui/mainviews/energy/ConsumerStats.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
nymea-app/ui/mainviews/energy/ConsumersBarChart.qml
Normal file
160
nymea-app/ui/mainviews/energy/ConsumersBarChart.qml
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
223
nymea-app/ui/mainviews/energy/ConsumersHistory.qml
Normal file
223
nymea-app/ui/mainviews/energy/ConsumersHistory.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
177
nymea-app/ui/mainviews/energy/PowerBalanceStats.qml
Normal file
177
nymea-app/ui/mainviews/energy/PowerBalanceStats.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
232
nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml
Normal file
232
nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
227
nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml
Normal file
227
nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
227
nymea-app/ui/system/PackageListPage.qml
Normal file
227
nymea-app/ui/system/PackageListPage.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user