Add logging support
This commit is contained in:
parent
885fe9be3a
commit
59d4e1d50d
108
libnymea-energy/energylogs.cpp
Normal file
108
libnymea-energy/energylogs.cpp
Normal file
@ -0,0 +1,108 @@
|
||||
#include "energylogs.h"
|
||||
|
||||
#include <QVariant>
|
||||
|
||||
EnergyLogs::EnergyLogs(QObject *parent): QObject(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
PowerBalanceLogEntry::PowerBalanceLogEntry()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
PowerBalanceLogEntry::PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage):
|
||||
m_timestamp(timestamp),
|
||||
m_consumption(consumption),
|
||||
m_production(production),
|
||||
m_acquisition(acquisition),
|
||||
m_storage(storage)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QDateTime PowerBalanceLogEntry::timestamp() const
|
||||
{
|
||||
return m_timestamp;
|
||||
}
|
||||
|
||||
double PowerBalanceLogEntry::consumption() const
|
||||
{
|
||||
return m_consumption;
|
||||
}
|
||||
|
||||
double PowerBalanceLogEntry::production() const
|
||||
{
|
||||
return m_production;
|
||||
}
|
||||
|
||||
double PowerBalanceLogEntry::acquisition() const
|
||||
{
|
||||
return m_acquisition;
|
||||
}
|
||||
|
||||
double PowerBalanceLogEntry::storage() const
|
||||
{
|
||||
return m_storage;
|
||||
}
|
||||
|
||||
QVariant PowerBalanceLogEntries::get(int index) const
|
||||
{
|
||||
return QVariant::fromValue(at(index));
|
||||
}
|
||||
|
||||
void PowerBalanceLogEntries::put(const QVariant &variant)
|
||||
{
|
||||
append(variant.value<PowerBalanceLogEntry>());
|
||||
}
|
||||
|
||||
ThingPowerLogEntry::ThingPowerLogEntry()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
ThingPowerLogEntry::ThingPowerLogEntry(const QDateTime ×tamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction):
|
||||
m_timestamp(timestamp),
|
||||
m_thingId(thingId),
|
||||
m_currentPower(currentPower),
|
||||
m_totalConsumption(totalConsumption),
|
||||
m_totalProduction(totalProduction)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QDateTime ThingPowerLogEntry::timestamp() const
|
||||
{
|
||||
return m_timestamp;
|
||||
}
|
||||
|
||||
ThingId 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;
|
||||
}
|
||||
|
||||
QVariant ThingPowerLogEntries::get(int index) const
|
||||
{
|
||||
return QVariant::fromValue(at(index));
|
||||
}
|
||||
|
||||
void ThingPowerLogEntries::put(const QVariant &variant)
|
||||
{
|
||||
append(variant.value<ThingPowerLogEntry>());
|
||||
}
|
||||
125
libnymea-energy/energylogs.h
Normal file
125
libnymea-energy/energylogs.h
Normal file
@ -0,0 +1,125 @@
|
||||
#ifndef ENERGYLOGS_H
|
||||
#define ENERGYLOGS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QVariant>
|
||||
|
||||
#include <typeutils.h>
|
||||
|
||||
class PowerBalanceLogEntry;
|
||||
class PowerBalanceLogEntries;
|
||||
class ThingPowerLogEntry;
|
||||
class ThingPowerLogEntries;
|
||||
|
||||
class EnergyLogs: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
EnergyLogs(QObject *parent = nullptr);
|
||||
virtual ~EnergyLogs() = default;
|
||||
|
||||
enum SampleRate {
|
||||
SampleRate1Min = 1,
|
||||
SampleRate15Mins = 15,
|
||||
SampleRate1Hour = 60,
|
||||
SampleRate3Hours = 180,
|
||||
SampleRate1Day = 1440,
|
||||
SampleRate1Week = 10080,
|
||||
SampleRate1Month = 43200,
|
||||
SampleRate1Year = 525600
|
||||
};
|
||||
Q_ENUM(SampleRate)
|
||||
|
||||
/*! Returns logs for the given sample rate for total household consumption, production, acquisition and storage balance.
|
||||
* From and to may be given to limit results to a time span.
|
||||
*/
|
||||
virtual PowerBalanceLogEntries powerBalanceLogs(SampleRate sampleRate, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const = 0;
|
||||
|
||||
/*! Returns logs for the given sample rate for currentPower, totalEnergyConsumed and totalEnergyProduced for the given things.
|
||||
* From and to may be given to limie results to a time span.
|
||||
* If thingIds is empty, all things will be returned.
|
||||
*/
|
||||
virtual ThingPowerLogEntries thingPowerLogs(SampleRate sampleRate, const QList<ThingId> &thingIds, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const = 0;
|
||||
|
||||
signals:
|
||||
void powerBalanceEntryAdded(SampleRate sampleRate, const PowerBalanceLogEntry &entry);
|
||||
void thingPowerEntryAdded(SampleRate sampleRate, const ThingPowerLogEntry &entry);
|
||||
|
||||
};
|
||||
|
||||
|
||||
class PowerBalanceLogEntry
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QDateTime timestamp READ timestamp)
|
||||
Q_PROPERTY(double consumption READ consumption)
|
||||
Q_PROPERTY(double production READ production)
|
||||
Q_PROPERTY(double acquisition READ acquisition)
|
||||
Q_PROPERTY(double storage READ storage)
|
||||
public:
|
||||
PowerBalanceLogEntry();
|
||||
PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage);
|
||||
QDateTime timestamp() const;
|
||||
double consumption() const;
|
||||
double production() const;
|
||||
double acquisition() const;
|
||||
double storage() const;
|
||||
private:
|
||||
QDateTime m_timestamp;
|
||||
double m_consumption = 0;
|
||||
double m_production = 0;
|
||||
double m_acquisition = 0;
|
||||
double m_storage = 0;
|
||||
};
|
||||
Q_DECLARE_METATYPE(PowerBalanceLogEntry)
|
||||
|
||||
class PowerBalanceLogEntries: public QList<PowerBalanceLogEntry>
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(int count READ count)
|
||||
public:
|
||||
PowerBalanceLogEntries() = default;
|
||||
PowerBalanceLogEntries(const QList<PowerBalanceLogEntry> &other);
|
||||
Q_INVOKABLE QVariant get(int index) const;
|
||||
Q_INVOKABLE void put(const QVariant &variant);
|
||||
};
|
||||
Q_DECLARE_METATYPE(PowerBalanceLogEntries)
|
||||
|
||||
class ThingPowerLogEntry {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QDateTime timstamp READ timestamp)
|
||||
Q_PROPERTY(QUuid thingId READ thingId)
|
||||
Q_PROPERTY(double currentPower READ currentPower)
|
||||
Q_PROPERTY(double totalConsumption READ totalConsumption)
|
||||
Q_PROPERTY(double totalProduction READ totalProduction)
|
||||
public:
|
||||
ThingPowerLogEntry();
|
||||
ThingPowerLogEntry(const QDateTime ×tamp, const ThingId &thingId, double currentPower, double totalConsumption, double totalProuction);
|
||||
QDateTime timestamp() const;
|
||||
ThingId thingId() const;
|
||||
double currentPower() const;
|
||||
double totalConsumption() const;
|
||||
double totalProduction() const;
|
||||
private:
|
||||
QDateTime m_timestamp;
|
||||
ThingId m_thingId;
|
||||
double m_currentPower = 0;
|
||||
double m_totalConsumption = 0;
|
||||
double m_totalProduction = 0;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ThingPowerLogEntry)
|
||||
|
||||
class ThingPowerLogEntries: public QList<ThingPowerLogEntry>
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(int count READ count)
|
||||
public:
|
||||
ThingPowerLogEntries() = default;
|
||||
ThingPowerLogEntries(const QList<ThingPowerLogEntry> &other): QList<ThingPowerLogEntry>(other) {}
|
||||
Q_INVOKABLE QVariant get(int index) const;
|
||||
Q_INVOKABLE void put(const QVariant &variant);
|
||||
};
|
||||
Q_DECLARE_METATYPE(ThingPowerLogEntries)
|
||||
|
||||
#endif // ENERGYLOGS_H
|
||||
@ -32,10 +32,13 @@
|
||||
#ifndef ENERGYMANAGER_H
|
||||
#define ENERGYMANAGER_H
|
||||
|
||||
#include "energylogs.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <integrations/thing.h>
|
||||
|
||||
|
||||
class EnergyManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -47,6 +50,7 @@ public:
|
||||
};
|
||||
Q_ENUM(EnergyError)
|
||||
|
||||
|
||||
explicit EnergyManager(QObject *parent = nullptr);
|
||||
virtual ~EnergyManager() = default;
|
||||
|
||||
@ -56,6 +60,9 @@ public:
|
||||
virtual double currentPowerConsumption() const = 0;
|
||||
virtual double currentPowerProduction() const = 0;
|
||||
virtual double currentPowerAcquisition() const = 0;
|
||||
virtual double currentPowerStorage() const = 0;
|
||||
|
||||
virtual EnergyLogs* logs() const = 0;
|
||||
|
||||
signals:
|
||||
void rootMeterChanged();
|
||||
|
||||
@ -9,10 +9,12 @@ PKGCONFIG += nymea
|
||||
|
||||
|
||||
HEADERS += \
|
||||
energylogs.h \
|
||||
energymanager.h \
|
||||
energyplugin.h
|
||||
|
||||
SOURCES += \
|
||||
energylogs.cpp \
|
||||
energymanager.cpp \
|
||||
energyplugin.cpp
|
||||
|
||||
|
||||
@ -8,6 +8,10 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare
|
||||
m_energyManager(energyManager)
|
||||
{
|
||||
registerEnum<EnergyManager::EnergyError>();
|
||||
registerEnum<EnergyLogs::SampleRate>();
|
||||
|
||||
registerObject<PowerBalanceLogEntry, PowerBalanceLogEntries>();
|
||||
registerObject<ThingPowerLogEntry, ThingPowerLogEntries>();
|
||||
|
||||
QVariantMap params, returns;
|
||||
QString description;
|
||||
@ -30,18 +34,51 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare
|
||||
returns.insert("currentPowerAcquisition", enumValueName(Double));
|
||||
registerMethod("GetPowerBalance", description, params, returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Get logs for the power balance. If from is not give, the log will start at the beginning of "
|
||||
"recording. If to is not given, the logs will and at the last sample for this sample rate before now.";
|
||||
params.insert("sampleRate", enumRef<EnergyLogs::SampleRate>());
|
||||
params.insert("o:from", enumValueName(Uint));
|
||||
params.insert("o:to", enumValueName(Uint));
|
||||
returns.insert("powerBalanceLogEntries", objectRef<PowerBalanceLogEntries>());
|
||||
registerMethod("GetPowerBalanceLogs", description, params, returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Get logs for one or more things power values. If thingIds is not given, logs for all energy related "
|
||||
"things will be returned. If from is not given, the log will start at the beginning of recording. If "
|
||||
"to is not given, the logs will and at the last sample for this sample rate before now.";
|
||||
params.insert("sampleRate", enumRef<EnergyLogs::SampleRate>());
|
||||
params.insert("o:thingIds", QVariantList() << enumValueName(Uuid));
|
||||
params.insert("o:from", enumValueName(Uint));
|
||||
params.insert("o:to", enumValueName(Uint));
|
||||
returns.insert("thingPowerLogEntries", objectRef<ThingPowerLogEntries>());
|
||||
registerMethod("GetThingPowerLogs", description, params, returns);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted whenever the root meter id changes. If the root meter has been unset, the params will be empty.";
|
||||
params.insert("o:rootMeterThingId", enumValueName(Uuid));
|
||||
registerNotification("RootMeterChanged", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted whenever the energy balance changes. That is, when the current consumption, production or acquisition changes. Typically they will all change at the same time.";
|
||||
description = "Emitted whenever the energy balance changes. That is, when the current consumption, production or "
|
||||
"acquisition changes. Typically they will all change at the same time.";
|
||||
params.insert("currentPowerConsumption", enumValueName(Double));
|
||||
params.insert("currentPowerProduction", enumValueName(Double));
|
||||
params.insert("currentPowerAcquisition", enumValueName(Double));
|
||||
registerNotification("PowerBalanceChanged", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted whenever a entry is added to the power balance log.";
|
||||
params.insert("sampleRate", enumRef<EnergyLogs::SampleRate>());
|
||||
params.insert("powerBalanceLogEntry", objectRef<PowerBalanceLogEntry>());
|
||||
registerNotification("PowerBalanceLogEntryAdded", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted whenever a entry is added to the thing power log.";
|
||||
params.insert("sampleRate", enumRef<EnergyLogs::SampleRate>());
|
||||
params.insert("thingPowerLogEntry", objectRef<ThingPowerLogEntry>());
|
||||
registerNotification("ThingPowerLogEntryAdded", description, params);
|
||||
|
||||
connect(m_energyManager, &EnergyManager::rootMeterChanged, this, [=](){
|
||||
QVariantMap params;
|
||||
if (m_energyManager->rootMeter()) {
|
||||
@ -57,6 +94,20 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare
|
||||
params.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition());
|
||||
emit PowerBalanceChanged(params);
|
||||
});
|
||||
|
||||
connect(m_energyManager->logs(), &EnergyLogs::powerBalanceEntryAdded, this, [=](EnergyLogs::SampleRate sampleRate, const PowerBalanceLogEntry &entry){
|
||||
QVariantMap params;
|
||||
params.insert("sampleRate", enumValueName(sampleRate));
|
||||
params.insert("powerBalanceLogEntry", pack(entry));
|
||||
emit PowerBalanceLogEntryAdded(params);
|
||||
});
|
||||
|
||||
connect(m_energyManager->logs(), &EnergyLogs::thingPowerEntryAdded, this, [=](EnergyLogs::SampleRate sampleRate, const ThingPowerLogEntry &entry){
|
||||
QVariantMap params;
|
||||
params.insert("sampleRate", enumValueName(sampleRate));
|
||||
params.insert("thingPowerLogEntry", pack(entry));
|
||||
emit ThingPowerLogEntryAdded(params);
|
||||
});
|
||||
}
|
||||
|
||||
QString EnergyJsonHandler::name() const
|
||||
@ -96,3 +147,30 @@ JsonReply *EnergyJsonHandler::GetPowerBalance(const QVariantMap ¶ms)
|
||||
ret.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition());
|
||||
return createReply(ret);
|
||||
}
|
||||
|
||||
JsonReply *EnergyJsonHandler::GetPowerBalanceLogs(const QVariantMap ¶ms)
|
||||
{
|
||||
qCDebug(dcEnergyExperience()) << "params" << params;
|
||||
qCDebug(dcEnergyExperience()) << "from" << params.value("from");
|
||||
EnergyLogs::SampleRate sampleRate = enumNameToValue<EnergyLogs::SampleRate>(params.value("sampleRate").toString());
|
||||
QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime();
|
||||
qCDebug(dcEnergyExperience()) << "from2" << from;
|
||||
QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime();
|
||||
QVariantMap returns;
|
||||
returns.insert("powerBalanceLogEntries", pack(m_energyManager->logs()->powerBalanceLogs(sampleRate, from, to)));
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply *EnergyJsonHandler::GetThingPowerLogs(const QVariantMap ¶ms)
|
||||
{
|
||||
EnergyLogs::SampleRate sampleRate = enumNameToValue<EnergyLogs::SampleRate>(params.value("sampleRate").toString());
|
||||
QList<ThingId> thingIds;
|
||||
foreach (const QVariant &thingId, params.value("thingIds").toList()) {
|
||||
thingIds.append(thingId.toUuid());
|
||||
}
|
||||
QDateTime from = params.contains("from") ? QDateTime::fromMSecsSinceEpoch(params.value("from").toLongLong() * 1000) : QDateTime();
|
||||
QDateTime to = params.contains("to") ? QDateTime::fromMSecsSinceEpoch(params.value("to").toLongLong() * 1000) : QDateTime();
|
||||
QVariantMap returns;
|
||||
returns.insert("thingPowerLogEntries",pack(m_energyManager->logs()->thingPowerLogs(sampleRate, thingIds, from, to)));
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
@ -17,15 +17,17 @@ public:
|
||||
Q_INVOKABLE JsonReply* GetRootMeter(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply* SetRootMeter(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply* GetPowerBalance(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply* GetPowerBalanceLogs(const QVariantMap ¶ms);
|
||||
Q_INVOKABLE JsonReply* GetThingPowerLogs(const QVariantMap ¶ms);
|
||||
|
||||
signals:
|
||||
void RootMeterChanged(const QVariantMap ¶ms);
|
||||
|
||||
void PowerBalanceChanged(const QVariantMap ¶ms);
|
||||
void PowerBalanceLogEntryAdded(const QVariantMap ¶ms);
|
||||
void ThingPowerLogEntryAdded(const QVariantMap ¶ms);
|
||||
|
||||
private:
|
||||
EnergyManager *m_energyManager = nullptr;
|
||||
|
||||
};
|
||||
|
||||
#endif // ENERGYJSONHANDLER_H
|
||||
|
||||
637
plugin/energylogger.cpp
Normal file
637
plugin/energylogger.cpp
Normal file
@ -0,0 +1,637 @@
|
||||
#include "energylogger.h"
|
||||
|
||||
#include <nymeasettings.h>
|
||||
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QSettings>
|
||||
|
||||
#include <QLoggingCategory>
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience)
|
||||
|
||||
EnergyLogger::EnergyLogger(QObject *parent) : EnergyLogs(parent)
|
||||
{
|
||||
if (!initDB()) {
|
||||
qCCritical(dcEnergyExperience()) << "Unable to open energy log. Energy logs will not be available.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Logging configuration
|
||||
// Note: SampleRate1Min is always sampled as it is the base series for others
|
||||
// Make sure your base series always has enough samples to build a full sample
|
||||
// of all series building on it.
|
||||
|
||||
// Disk space considerations;
|
||||
// Each entry takes approx 30 bytes for powerBalance + 50 bytes for thingCurrentPower per thing of disk space
|
||||
// SQLite adds metadata and overhead of about 5%
|
||||
// The resulting database size can be estimated with (count being the sum of all numbers below):
|
||||
// (count * 30 bytes) + (count * things * 50 bytes) + 5%
|
||||
// 10000 entries, with 5 energy things => ~3MB
|
||||
|
||||
m_maxMinuteSamples = 15;
|
||||
|
||||
addConfig(SampleRate15Mins, SampleRate1Min, 6720); // 10 weeks
|
||||
addConfig(SampleRate1Hour, SampleRate15Mins, 1680); // 10 weeks
|
||||
addConfig(SampleRate3Hours, SampleRate15Mins, 560); // 10 weeks
|
||||
addConfig(SampleRate1Day, SampleRate1Hour, 1095); // 3 years
|
||||
addConfig(SampleRate1Week, SampleRate1Day, 168); // 3 years
|
||||
addConfig(SampleRate1Month, SampleRate1Day, 240); // 20 years
|
||||
addConfig(SampleRate1Year, SampleRate1Month, 20); // 20 years
|
||||
|
||||
// Load thingIds from logs so we have the complete list available for sampling, even if a thing might not produce any logs for a while.
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT DISTINCT thingId FROM thingPower;");
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Failed to load existing things from logs:" << query.lastError();
|
||||
} else {
|
||||
while (query.next()) {
|
||||
m_thingsPowerLiveLogs[query.value("thingId").toUuid()] = ThingPowerLogEntries();
|
||||
}
|
||||
}
|
||||
|
||||
// Start the scheduling
|
||||
|
||||
scheduleNextSample(SampleRate1Min);
|
||||
foreach (SampleRate sampleRate, m_configs.keys()) {
|
||||
scheduleNextSample(sampleRate);
|
||||
}
|
||||
|
||||
// Now all the data is initialized. We can start with sampling.
|
||||
|
||||
// First check if we missed any samplings (e.g. because the system was offline at the time when it should have created a sample)
|
||||
foreach(SampleRate sampleRate, m_configs.keys()) {
|
||||
rectifySamples(sampleRate, m_configs.value(sampleRate).baseSampleRate);
|
||||
}
|
||||
|
||||
// And start the sampler timer
|
||||
connect(&m_sampleTimer, &QTimer::timeout, this, &EnergyLogger::sample);
|
||||
m_sampleTimer.start(1000);
|
||||
}
|
||||
|
||||
void EnergyLogger::logPowerBalance(double consumption, double production, double acquisition, double storage)
|
||||
{
|
||||
PowerBalanceLogEntry entry(QDateTime::currentDateTime(), consumption, production, acquisition, storage);
|
||||
|
||||
// Add everything to livelog, keep that for one day, in memory only
|
||||
m_balanceLiveLog.prepend(entry);
|
||||
while (m_balanceLiveLog.count() > 1 && m_balanceLiveLog.last().timestamp().addDays(1) < QDateTime::currentDateTime()) {
|
||||
qCDebug(dcEnergyExperience) << "Discarding livelog entry from" << m_balanceLiveLog.last().timestamp().toString();
|
||||
m_balanceLiveLog.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
void EnergyLogger::logThingPower(const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction)
|
||||
{
|
||||
qCDebug(dcEnergyExperience()) << "Logging thing power:" << currentPower << totalConsumption << totalProduction;
|
||||
ThingPowerLogEntry entry(QDateTime::currentDateTime(), thingId, currentPower, totalConsumption, totalProduction);
|
||||
|
||||
m_thingsPowerLiveLogs[thingId].prepend(entry);
|
||||
while (m_thingsPowerLiveLogs[thingId].count() > 1 && m_thingsPowerLiveLogs[thingId].last().timestamp().addDays(1) < QDateTime::currentDateTime()) {
|
||||
qCDebug(dcEnergyExperience()) << "Discarding thing power livelog entry for thing" << thingId << "from" << m_thingsPowerLiveLogs[thingId].last().timestamp().toString();
|
||||
m_thingsPowerLiveLogs[thingId].removeLast();
|
||||
}
|
||||
}
|
||||
PowerBalanceLogEntries EnergyLogger::powerBalanceLogs(SampleRate sampleRate, const QDateTime &from, const QDateTime &to) const
|
||||
{
|
||||
PowerBalanceLogEntries result;
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
QString queryString = "SELECT * FROM powerBalance WHERE sampleRate = ?";
|
||||
QVariantList bindValues;
|
||||
bindValues << sampleRate;
|
||||
qCDebug(dcEnergyExperience()) << "Fetching logs. Timestamp:" << from << from.isNull();
|
||||
if (!from.isNull()) {
|
||||
queryString += " AND timestamp >= ?";
|
||||
bindValues << from.toMSecsSinceEpoch();
|
||||
}
|
||||
if (!to.isNull()) {
|
||||
queryString += " AND timestamp <= ?";
|
||||
bindValues << to.toMSecsSinceEpoch();
|
||||
}
|
||||
query.prepare(queryString);
|
||||
foreach (const QVariant &bindValue, bindValues) {
|
||||
query.addBindValue(bindValue);
|
||||
}
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Executing" << queryString << query.executedQuery() << bindValues;
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error fetching power balance logs:" << query.lastError() << query.executedQuery();
|
||||
return result;
|
||||
}
|
||||
|
||||
while (query.next()) {
|
||||
qCDebug(dcEnergyExperience()) << "Adding result";
|
||||
result.append(PowerBalanceLogEntry(QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()), query.value("consumption").toDouble(), query.value("production").toDouble(), query.value("acquisition").toDouble(), query.value("storage").toDouble()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ThingPowerLogEntries EnergyLogger::thingPowerLogs(SampleRate sampleRate, const QList<ThingId> &thingIds, const QDateTime &from, const QDateTime &to) const
|
||||
{
|
||||
ThingPowerLogEntries result;
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
QString queryString = "SELECT * FROM thingPower WHERE sampleRate = ?";
|
||||
QVariantList bindValues;
|
||||
bindValues << sampleRate;
|
||||
|
||||
QStringList thingsQuery;
|
||||
foreach (const ThingId &thingId, thingIds) {
|
||||
thingsQuery.append("thingId = ?");
|
||||
bindValues << thingId;
|
||||
}
|
||||
if (!thingsQuery.isEmpty()) {
|
||||
queryString += "AND (" + thingsQuery.join(" OR ") + " )";
|
||||
}
|
||||
|
||||
if (!from.isNull()) {
|
||||
queryString += " AND timestamp >= ?";
|
||||
bindValues << from.toMSecsSinceEpoch();
|
||||
}
|
||||
if (!to.isNull()) {
|
||||
queryString += " AND timestamp <= ?";
|
||||
bindValues << to.toMSecsSinceEpoch();
|
||||
}
|
||||
query.prepare(queryString);
|
||||
foreach (const QVariant &bindValue, bindValues) {
|
||||
query.addBindValue(bindValue);
|
||||
}
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error fetching power balance logs:" << query.lastError() << query.executedQuery();
|
||||
return result;
|
||||
}
|
||||
|
||||
while (query.next()) {
|
||||
result.append(ThingPowerLogEntry(
|
||||
QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()),
|
||||
query.value("thingId").toUuid(),
|
||||
query.value("currentPower").toDouble(),
|
||||
query.value("totalConsumption").toDouble(),
|
||||
query.value("totalProduction").toDouble()));
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
void EnergyLogger::sample()
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
|
||||
if (now >= m_nextSamples.value(SampleRate1Min)) {
|
||||
QDateTime sampleEnd = m_nextSamples.value(SampleRate1Min);
|
||||
QDateTime sampleStart = sampleEnd.addMSecs(-60 * 1000);
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Sampling 1 min" << sampleEnd.toString();
|
||||
|
||||
double medianConsumption = 0;
|
||||
double medianProduction = 0;
|
||||
double medianAcquisition = 0;
|
||||
double medianStorage = 0;
|
||||
for (int i = 0; i < m_balanceLiveLog.count(); i++) {
|
||||
const PowerBalanceLogEntry &entry = m_balanceLiveLog.at(i);
|
||||
QDateTime frameStart = (entry.timestamp() < sampleStart) ? sampleStart : entry.timestamp();
|
||||
QDateTime frameEnd = i == 0 ? sampleEnd : m_balanceLiveLog.at(i-1).timestamp();
|
||||
int frameDuration = frameStart.msecsTo(frameEnd);
|
||||
medianConsumption += entry.consumption() * frameDuration;
|
||||
medianProduction += entry.production() * frameDuration;
|
||||
medianAcquisition += entry.acquisition() * frameDuration;
|
||||
medianStorage += entry.storage() * frameDuration;
|
||||
// qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.consumption << "start" << frameStart.toString() << "end" << frameEnd.toString();
|
||||
if (entry.timestamp() < sampleStart) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
medianConsumption /= sampleStart.msecsTo(sampleEnd);
|
||||
medianProduction /= sampleStart.msecsTo(sampleEnd);
|
||||
medianAcquisition /= sampleStart.msecsTo(sampleEnd);
|
||||
medianStorage /= sampleStart.msecsTo(sampleEnd);
|
||||
qCDebug(dcEnergyExperience()) << "Power balance for sample:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "duration:" << sampleStart.msecsTo(sampleEnd);
|
||||
insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage);
|
||||
|
||||
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
|
||||
medianConsumption = 0;
|
||||
ThingPowerLogEntries entries = m_thingsPowerLiveLogs.value(thingId);
|
||||
for (int i = 0; i < entries.count(); i++) {
|
||||
const ThingPowerLogEntry &entry = entries.at(i);
|
||||
QDateTime frameStart = (entry.timestamp() < sampleStart) ? sampleStart : entry.timestamp();
|
||||
QDateTime frameEnd = i == 0 ? sampleEnd : entries.at(i-1).timestamp();
|
||||
int frameDuration = frameStart.msecsTo(frameEnd);
|
||||
medianConsumption += entry.currentPower() * frameDuration;
|
||||
// qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.value;
|
||||
if (entry.timestamp() < sampleStart) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
medianConsumption /= sampleStart.msecsTo(sampleEnd);
|
||||
double totalConsumption = 0;
|
||||
double totalProduction = 0;
|
||||
if (entries.count() > 0) {
|
||||
totalConsumption = entries.last().totalConsumption();
|
||||
totalProduction = entries.last().totalProduction();
|
||||
}
|
||||
qCDebug(dcEnergyExperience()) << "Thing power of sample:" << medianConsumption << totalConsumption << totalProduction << "total duration:" << sampleStart.msecsTo(sampleEnd);
|
||||
insertThingPower(sampleEnd, SampleRate1Min, thingId, medianConsumption, totalConsumption, totalProduction);
|
||||
}
|
||||
}
|
||||
|
||||
// First sample all the configs.
|
||||
foreach (SampleRate sampleRate, m_configs.keys()) {
|
||||
if (now >= m_nextSamples.value(sampleRate)) {
|
||||
QDateTime sampleTime = m_nextSamples.value(sampleRate);
|
||||
SampleRate baseSampleRate = m_configs.value(sampleRate).baseSampleRate;
|
||||
samplePowerBalance(sampleRate, baseSampleRate, sampleTime);
|
||||
sampleThingsPower(sampleRate, baseSampleRate, sampleTime);
|
||||
}
|
||||
}
|
||||
|
||||
// and then trim them
|
||||
if (now > m_nextSamples.value(SampleRate1Min)) {
|
||||
QDateTime sampleTime = m_nextSamples.value(SampleRate1Min);
|
||||
QDateTime oldestTimestamp = sampleTime.addMSecs(-m_maxMinuteSamples * 60 * 1000);
|
||||
trimPowerBalance(SampleRate1Min, oldestTimestamp);
|
||||
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
|
||||
trimThingPower(thingId, SampleRate1Min, oldestTimestamp);
|
||||
}
|
||||
}
|
||||
foreach (SampleRate sampleRate, m_configs.keys()) {
|
||||
if (now >= m_nextSamples.value(sampleRate)) {
|
||||
QDateTime sampleTime = m_nextSamples.value(sampleRate);
|
||||
QDateTime oldestTimestamp = sampleTime.addMSecs(-m_configs.value(sampleRate).maxSamples * sampleRate * 60 * 1000);
|
||||
trimPowerBalance(sampleRate, oldestTimestamp);
|
||||
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
|
||||
trimThingPower(thingId, sampleRate, oldestTimestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly we reschedule the next sample for each config
|
||||
// Note: keep this at the end as the previous stuff uses the schedule to work
|
||||
if (now > m_nextSamples.value(SampleRate1Min)) {
|
||||
scheduleNextSample(SampleRate1Min);
|
||||
}
|
||||
foreach (SampleRate sampleRate, m_configs.keys()) {
|
||||
if (now >= m_nextSamples.value(sampleRate)) {
|
||||
scheduleNextSample(sampleRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool EnergyLogger::initDB()
|
||||
{
|
||||
m_db.close();
|
||||
|
||||
m_db = QSqlDatabase::addDatabase("QSQLITE", "energylogs");
|
||||
QDir path = QDir(NymeaSettings::storagePath());
|
||||
if (!path.exists()) {
|
||||
path.mkpath(path.path());
|
||||
}
|
||||
m_db.setDatabaseName(path.filePath("energylogs.sqlite"));
|
||||
|
||||
bool opened = m_db.open();
|
||||
if (!opened) {
|
||||
qCWarning(dcEnergyExperience()) << "Cannot open energy log DB at" << m_db.databaseName() << m_db.lastError();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_db.tables().contains("powerBalance")) {
|
||||
qCDebug(dcEnergyExperience()) << "No \"powerBalance\" table in database. Creating it.";
|
||||
m_db.exec("CREATE TABLE powerBalance "
|
||||
"("
|
||||
"timestamp BIGINT,"
|
||||
"sampleRate INT,"
|
||||
"consumption FLOAT,"
|
||||
"production FLOAT,"
|
||||
"acquisition FLOAT,"
|
||||
"storage FLOAT"
|
||||
");");
|
||||
|
||||
if (m_db.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error creating powerBalance table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_db.tables().contains("thingPower")) {
|
||||
qCDebug(dcEnergyExperience()) << "No \"thingPower\" table in database. Creating it.";
|
||||
m_db.exec("CREATE TABLE thingPower "
|
||||
"("
|
||||
"timestamp BIGINT,"
|
||||
"sampleRate INT,"
|
||||
"thingId VARCHAR(38),"
|
||||
"currentPower FLOAT,"
|
||||
"totalConsumption FLOAT,"
|
||||
"totalProduction FLOAT"
|
||||
");");
|
||||
if (m_db.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error creating thingPower table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Initialized logging DB successfully.";
|
||||
return true;
|
||||
}
|
||||
|
||||
void EnergyLogger::addConfig(SampleRate sampleRate, SampleRate baseSampleRate, int maxSamples)
|
||||
{
|
||||
SampleConfig config;
|
||||
config.baseSampleRate = baseSampleRate;
|
||||
config.maxSamples = maxSamples;
|
||||
m_configs.insert(sampleRate, config);
|
||||
}
|
||||
|
||||
QDateTime EnergyLogger::getOldestPowerBalanceSampleTimestamp(SampleRate sampleRate)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT MIN(timestamp) AS oldestTimestamp FROM powerBalance WHERE sampleRate = ?;");
|
||||
query.addBindValue(sampleRate);
|
||||
query.exec();
|
||||
if (query.next() && !query.value("oldestTimestamp").isNull()) {
|
||||
return QDateTime::fromMSecsSinceEpoch(query.value("oldestTimestamp").toLongLong());
|
||||
}
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
QDateTime EnergyLogger::getNewestPowerBalanceSampleTimestamp(SampleRate sampleRate)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT MAX(timestamp) AS latestTimestamp FROM powerBalance WHERE sampleRate = ?;");
|
||||
query.addBindValue(sampleRate);
|
||||
query.exec();
|
||||
if (query.next() && !query.value("latestTimestamp").isNull()) {
|
||||
return QDateTime::fromMSecsSinceEpoch(query.value("latestTimestamp").toLongLong());
|
||||
}
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
QDateTime EnergyLogger::getOldestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT MIN(timestamp) AS oldestTimestamp FROM thingPower WHERE thingId = ? AND sampleRate = ?;");
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(sampleRate);
|
||||
query.exec();
|
||||
if (query.next() && !query.value("oldestTimestamp").isNull()) {
|
||||
return QDateTime::fromMSecsSinceEpoch(query.value("oldestTimestamp").toLongLong());
|
||||
}
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
QDateTime EnergyLogger::getNewestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT MAX(timestamp) AS newestTimestamp FROM thingPower WHERE thingId = ? AND sampleRate = ?;");
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(sampleRate);
|
||||
query.exec();
|
||||
if (query.next() && !query.value("newestTimestamp").isNull()) {
|
||||
return QDateTime::fromMSecsSinceEpoch(query.value("newestTimestamp").toLongLong());
|
||||
}
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
void EnergyLogger::scheduleNextSample(SampleRate sampleRate)
|
||||
{
|
||||
QDateTime next = nextSampleTimestamp(sampleRate, QDateTime::currentDateTime());
|
||||
m_nextSamples.insert(sampleRate, next);
|
||||
qCDebug(dcEnergyExperience()) << "Next sample for" << sampleRate << "scheduled at" << next.toString();
|
||||
}
|
||||
|
||||
void EnergyLogger::rectifySamples(SampleRate sampleRate, SampleRate baseSampleRate)
|
||||
{
|
||||
// Normally we'd need to find the newest available sample of a serien and catch up from there.
|
||||
// However, it could happen a series does not have any samples at all yet. For example if we're logging since january,
|
||||
// and at new years the system was off, we missed the new years yearly sample and don't have any earlier. For those cases
|
||||
// we need to start resampling from the oldest timestamp we find in the DB at all (regardless of the sampleRate)
|
||||
QDateTime oldestBaseSample = getOldestPowerBalanceSampleTimestamp(baseSampleRate);
|
||||
QDateTime newestSample = getNewestPowerBalanceSampleTimestamp(sampleRate);
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Checking for missing samples for" << sampleRate;
|
||||
qCDebug(dcEnergyExperience()) << "Newest sample:" << newestSample.toString() << "Oldest base sample:" << oldestBaseSample.toString();
|
||||
if (newestSample.isNull()) {
|
||||
qCDebug(dcEnergyExperience()) << "No sample at all so far. Using base as starting point.";
|
||||
newestSample = oldestBaseSample;
|
||||
}
|
||||
qCDebug(dcEnergyExperience()) << "next sample after last in series:" << nextSampleTimestamp(sampleRate, newestSample).toString();
|
||||
qCDebug(dcEnergyExperience()) << "next scheduled sample:" << m_nextSamples.value(sampleRate).toString();
|
||||
while (!newestSample.isNull() && nextSampleTimestamp(sampleRate, newestSample) < m_nextSamples[sampleRate]) {
|
||||
QDateTime nextSample = nextSampleTimestamp(sampleRate, newestSample.addMSecs(1000));
|
||||
qCDebug(dcEnergyExperience()) << "Rectifying missed sample for" << sampleRate << "from" << nextSample.toString();
|
||||
samplePowerBalance(sampleRate, baseSampleRate, nextSample);
|
||||
newestSample = nextSample;
|
||||
}
|
||||
|
||||
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
|
||||
QDateTime oldestBaseSample = getOldestThingPowerSampleTimestamp(thingId, baseSampleRate);
|
||||
QDateTime newestSample = getNewestThingPowerSampleTimestamp(thingId, sampleRate);
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "T Checking for missing samples for" << sampleRate;
|
||||
qCDebug(dcEnergyExperience()) << "T Newest sample:" << newestSample.toString() << "Oldest base sample:" << oldestBaseSample.toString();
|
||||
if (newestSample.isNull()) {
|
||||
qCDebug(dcEnergyExperience()) << "T No sample at all so far. Using base as starting point.";
|
||||
newestSample = oldestBaseSample;
|
||||
}
|
||||
qCDebug(dcEnergyExperience()) << "T next sample after last in series:" << nextSampleTimestamp(sampleRate, newestSample).toString();
|
||||
qCDebug(dcEnergyExperience()) << "T next scheduled sample:" << m_nextSamples.value(sampleRate).toString();
|
||||
while (!newestSample.isNull() && nextSampleTimestamp(sampleRate, newestSample) < m_nextSamples[sampleRate]) {
|
||||
QDateTime nextSample = nextSampleTimestamp(sampleRate, newestSample.addMSecs(1000));
|
||||
qCDebug(dcEnergyExperience()) << "T Rectifying missed sample for" << sampleRate << "from" << nextSample.toString();
|
||||
sampleThingPower(thingId, sampleRate, baseSampleRate, nextSample);
|
||||
newestSample = nextSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QDateTime EnergyLogger::nextSampleTimestamp(SampleRate sampleRate, const QDateTime &dateTime)
|
||||
{
|
||||
QTime time = dateTime.time();
|
||||
QDate date = dateTime.date();
|
||||
QDateTime next;
|
||||
switch (sampleRate) {
|
||||
case SampleRate1Min:
|
||||
time.setHMS(time.hour(), time.minute(), 0);
|
||||
next = QDateTime(date, time).addMSecs(60 * 1000);
|
||||
break;
|
||||
case SampleRate15Mins:
|
||||
time.setHMS(time.hour(), time.minute() - (time.minute() % 15), 0);
|
||||
next = QDateTime(date, time).addMSecs(15 * 60 * 1000);
|
||||
break;
|
||||
case SampleRate1Hour:
|
||||
time.setHMS(time.hour(), 0, 0);
|
||||
next = QDateTime(date, time).addMSecs(60 * 60 * 1000);
|
||||
break;
|
||||
case SampleRate3Hours:
|
||||
time.setHMS(time.hour() - (time.hour() % 3), 0, 0);
|
||||
next = QDateTime(date, time).addMSecs(3 * 60 * 60 * 1000);
|
||||
break;
|
||||
case SampleRate1Day:
|
||||
next = QDateTime(date, QTime()).addDays(1);
|
||||
break;
|
||||
case SampleRate1Week:
|
||||
date = date.addDays(-date.dayOfWeek() + 1);
|
||||
next = QDateTime(date, QTime()).addDays(7);
|
||||
break;
|
||||
case SampleRate1Month:
|
||||
date = date.addDays(-date.day() + 1);
|
||||
next = QDateTime(date, QTime()).addMonths(1);
|
||||
break;
|
||||
case SampleRate1Year:
|
||||
date.setDate(date.year(), 1, 1);
|
||||
next = QDateTime(date, QTime()).addYears(1);
|
||||
break;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
bool EnergyLogger::samplePowerBalance(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
|
||||
{
|
||||
QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000);
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Sampling" << sampleRate << "from" << sampleStart << "to" << sampleEnd;
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT * FROM powerBalance WHERE sampleRate = ? AND timestamp >= ? AND timestamp < ?;");
|
||||
query.addBindValue(baseSampleRate);
|
||||
query.addBindValue(sampleStart.toMSecsSinceEpoch());
|
||||
query.addBindValue(sampleEnd.toMSecsSinceEpoch());
|
||||
query.exec();
|
||||
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error fetching power balance samples for" << baseSampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString();
|
||||
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
|
||||
return false;
|
||||
}
|
||||
|
||||
double medianConsumption = 0;
|
||||
double medianProduction = 0;
|
||||
double medianAcquisition = 0;
|
||||
double medianStorage = 0;
|
||||
while (query.next()) {
|
||||
qCDebug(dcEnergyExperience()) << "Frame:" << query.value("consumption").toDouble() << query.value("production").toDouble() << query.value("acquisition").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString();
|
||||
medianConsumption += query.value("consumption").toDouble();
|
||||
medianProduction += query.value("production").toDouble();
|
||||
medianAcquisition += query.value("acquisition").toDouble();
|
||||
medianStorage += query.value("storage").toDouble();
|
||||
}
|
||||
qCDebug(dcEnergyExperience()) << "Totals:" << medianConsumption << medianProduction << medianAcquisition << medianStorage << "base samplerate" << baseSampleRate << "samplerate:" << sampleRate;
|
||||
medianConsumption = medianConsumption * baseSampleRate / sampleRate;
|
||||
medianProduction = medianProduction * baseSampleRate / sampleRate;
|
||||
medianAcquisition = medianAcquisition * baseSampleRate / sampleRate;
|
||||
medianStorage = medianStorage * baseSampleRate / sampleRate;
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Sampled:" << medianConsumption << medianProduction << medianAcquisition << medianStorage;
|
||||
return insertPowerBalance(sampleEnd, sampleRate, medianConsumption, medianProduction, medianAcquisition, medianStorage);
|
||||
}
|
||||
|
||||
bool EnergyLogger::insertPowerBalance(const QDateTime ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage)
|
||||
{
|
||||
QSqlQuery query = QSqlQuery(m_db);
|
||||
query.prepare("INSERT INTO powerBalance (timestamp, sampleRate, consumption, production, acquisition, storage) values (?, ?, ?, ?, ?, ?);");
|
||||
query.addBindValue(timestamp.toMSecsSinceEpoch());
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(consumption);
|
||||
query.addBindValue(production);
|
||||
query.addBindValue(acquisition);
|
||||
query.addBindValue(storage);
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error logging consumption sample:" << query.lastError();
|
||||
return false;
|
||||
}
|
||||
emit powerBalanceEntryAdded(sampleRate, PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EnergyLogger::sampleThingsPower(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
|
||||
{
|
||||
bool ret = true;
|
||||
foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) {
|
||||
ret &= sampleThingPower(thingId, sampleRate, baseSampleRate, sampleEnd);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool EnergyLogger::sampleThingPower(const ThingId &thingId, SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd)
|
||||
{
|
||||
QDateTime sampleStart = sampleEnd.addMSecs(-sampleRate * 60 * 1000);
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Sampling" << sampleRate << "from" << sampleStart << "to" << sampleEnd;
|
||||
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("SELECT * FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp >= ? AND timestamp < ?;");
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(baseSampleRate);
|
||||
query.addBindValue(sampleStart.toMSecsSinceEpoch());
|
||||
query.addBindValue(sampleEnd.toMSecsSinceEpoch());
|
||||
query.exec();
|
||||
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error fetching thing power samples for" << baseSampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString();
|
||||
qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery();
|
||||
return false;
|
||||
}
|
||||
|
||||
double medianCurrentPower = 0;
|
||||
while (query.next()) {
|
||||
qCDebug(dcEnergyExperience()) << "Frame:" << query.value("currentPower").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString();
|
||||
medianCurrentPower += query.value("currentPower").toDouble();
|
||||
}
|
||||
qCDebug(dcEnergyExperience()) << "Total:" << medianCurrentPower << "base samplerate" << baseSampleRate << "samplerate:" << sampleRate;
|
||||
medianCurrentPower = medianCurrentPower * baseSampleRate / sampleRate;
|
||||
|
||||
double totalConsumption = query.value("totalConsumption").toDouble();
|
||||
double totalProduction = query.value("totalProduction").toDouble();
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Sampled:" << medianCurrentPower;
|
||||
return insertThingPower(sampleEnd, sampleRate, thingId, medianCurrentPower, totalConsumption, totalProduction);
|
||||
}
|
||||
|
||||
bool EnergyLogger::insertThingPower(const QDateTime ×tamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction)
|
||||
{
|
||||
QSqlQuery query = QSqlQuery(m_db);
|
||||
query.prepare("INSERT INTO thingPower (timestamp, sampleRate, thingId, currentPower, totalConsumption, totalProduction) values (?, ?, ?, ?, ?, ?);");
|
||||
query.addBindValue(timestamp.toMSecsSinceEpoch());
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(currentPower);
|
||||
query.addBindValue(totalConsumption);
|
||||
query.addBindValue(totalProduction);
|
||||
query.exec();
|
||||
if (query.lastError().isValid()) {
|
||||
qCWarning(dcEnergyExperience()) << "Error logging thing power sample:" << query.lastError() << query.executedQuery();
|
||||
return false;
|
||||
}
|
||||
emit thingPowerEntryAdded(sampleRate, ThingPowerLogEntry(timestamp, thingId, currentPower, totalConsumption, totalProduction));
|
||||
return true;
|
||||
}
|
||||
|
||||
void EnergyLogger::trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("DELETE FROM powerBalance WHERE sampleRate = ? AND timestamp < ?;");
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(beforeTime.toMSecsSinceEpoch());
|
||||
query.exec();
|
||||
if (query.numRowsAffected() > 0) {
|
||||
qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from power balance series: " << sampleRate << " (Older than: " << beforeTime.toString() << ")";
|
||||
}
|
||||
}
|
||||
|
||||
void EnergyLogger::trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime)
|
||||
{
|
||||
QSqlQuery query(m_db);
|
||||
query.prepare("DELETE FROM thingPower WHERE thingId = ? AND sampleRate = ? AND timestamp < ?;");
|
||||
query.addBindValue(thingId);
|
||||
query.addBindValue(sampleRate);
|
||||
query.addBindValue(beforeTime.toMSecsSinceEpoch());
|
||||
query.exec();
|
||||
if (query.numRowsAffected() > 0) {
|
||||
qCDebug(dcEnergyExperience()).nospace() << "Trimmed " << query.numRowsAffected() << " from thing power series for: " << thingId << sampleRate << " (Older than: " << beforeTime.toString() << ")";
|
||||
}
|
||||
}
|
||||
68
plugin/energylogger.h
Normal file
68
plugin/energylogger.h
Normal file
@ -0,0 +1,68 @@
|
||||
#ifndef ENERGYLOGGER_H
|
||||
#define ENERGYLOGGER_H
|
||||
|
||||
#include "energylogs.h"
|
||||
|
||||
#include <typeutils.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QSqlDatabase>
|
||||
#include <QTimer>
|
||||
#include <QMap>
|
||||
|
||||
class EnergyLogger : public EnergyLogs
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EnergyLogger(QObject *parent = nullptr);
|
||||
|
||||
void logPowerBalance(double consumption, double production, double acquisition, double storage);
|
||||
void logThingPower(const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction);
|
||||
|
||||
PowerBalanceLogEntries powerBalanceLogs(SampleRate sampleRate, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const override;
|
||||
ThingPowerLogEntries thingPowerLogs(SampleRate sampleRate, const QList<ThingId> &thingIds, const QDateTime &from = QDateTime(), const QDateTime &to = QDateTime()) const override;
|
||||
|
||||
private slots:
|
||||
void sample();
|
||||
|
||||
private:
|
||||
bool initDB();
|
||||
void addConfig(SampleRate sampleRate, SampleRate baseSampleRate, int maxSamples);
|
||||
QDateTime getOldestPowerBalanceSampleTimestamp(SampleRate sampleRate);
|
||||
QDateTime getNewestPowerBalanceSampleTimestamp(SampleRate sampleRate);
|
||||
QDateTime getOldestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate);
|
||||
QDateTime getNewestThingPowerSampleTimestamp(const ThingId &thingId, SampleRate sampleRate);
|
||||
|
||||
QDateTime nextSampleTimestamp(SampleRate sampleRate, const QDateTime &dateTime);
|
||||
void scheduleNextSample(SampleRate sampleRate);
|
||||
|
||||
void rectifySamples(SampleRate sampleRate, EnergyLogger::SampleRate baseSampleRate);
|
||||
|
||||
bool samplePowerBalance(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd);
|
||||
bool insertPowerBalance(const QDateTime ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage);
|
||||
bool sampleThingsPower(SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd);
|
||||
bool sampleThingPower(const ThingId &thingId, SampleRate sampleRate, SampleRate baseSampleRate, const QDateTime &sampleEnd);
|
||||
bool insertThingPower(const QDateTime ×tamp, SampleRate sampleRate, const ThingId &thingId, double currentPower, double totalConsumption, double totalProduction);
|
||||
void trimPowerBalance(SampleRate sampleRate, const QDateTime &beforeTime);
|
||||
void trimThingPower(const ThingId &thingId, SampleRate sampleRate, const QDateTime &beforeTime);
|
||||
|
||||
private:
|
||||
struct SampleConfig {
|
||||
SampleRate baseSampleRate;
|
||||
int maxSamples = 0;
|
||||
};
|
||||
|
||||
PowerBalanceLogEntries m_balanceLiveLog;
|
||||
QHash<ThingId, ThingPowerLogEntries> m_thingsPowerLiveLogs;
|
||||
|
||||
QTimer m_sampleTimer;
|
||||
QHash<SampleRate, QDateTime> m_nextSamples;
|
||||
|
||||
QSqlDatabase m_db;
|
||||
|
||||
int m_maxMinuteSamples = 0;
|
||||
QMap<SampleRate, SampleConfig> m_configs;
|
||||
};
|
||||
|
||||
#endif // ENERGYLOGGER_H
|
||||
@ -1,5 +1,7 @@
|
||||
#include "energymanagerimpl.h"
|
||||
#include "nymeasettings.h"
|
||||
#include "energylogger.h"
|
||||
|
||||
#include <nymeasettings.h>
|
||||
|
||||
#include <qmath.h>
|
||||
|
||||
@ -7,7 +9,8 @@ Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience)
|
||||
|
||||
EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent):
|
||||
EnergyManager(parent),
|
||||
m_thingManager(thingManager)
|
||||
m_thingManager(thingManager),
|
||||
m_logger(new EnergyLogger(this))
|
||||
{
|
||||
// Most of the time we get a bunch of signals at the same time (root meter, producers, consumers etc)
|
||||
// In order to decrease some load on the system, we'll wait for wee bit until we actually update to
|
||||
@ -67,6 +70,16 @@ double EnergyManagerImpl::currentPowerAcquisition() const
|
||||
return m_currentPowerAcquisition;
|
||||
}
|
||||
|
||||
double EnergyManagerImpl::currentPowerStorage() const
|
||||
{
|
||||
return m_currentPowerStorage;
|
||||
}
|
||||
|
||||
EnergyLogs *EnergyManagerImpl::logs() const
|
||||
{
|
||||
return m_logger;
|
||||
}
|
||||
|
||||
void EnergyManagerImpl::watchThing(Thing *thing)
|
||||
{
|
||||
// If we don't have a root meter yet, we'll be auto-setting the first energymeter that appears.
|
||||
@ -76,8 +89,10 @@ void EnergyManagerImpl::watchThing(Thing *thing)
|
||||
}
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Wathing thing:" << thing->name();
|
||||
if (thing->thingClass().interfaces().contains("smartmeterproducer")
|
||||
|| thing->thingClass().interfaces().contains("energymeter")
|
||||
|
||||
// React on things that requie us updating the power balance
|
||||
if (thing->thingClass().interfaces().contains("energymeter")
|
||||
|| thing->thingClass().interfaces().contains("smartmeterproducer")
|
||||
|| thing->thingClass().interfaces().contains("energystorage")) {
|
||||
connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId){
|
||||
if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") {
|
||||
@ -85,6 +100,18 @@ void EnergyManagerImpl::watchThing(Thing *thing)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// React on things that need to be logged
|
||||
if (thing->thingClass().interfaces().contains("energymeter")
|
||||
|| thing->thingClass().interfaces().contains("smartmeterconsumer")
|
||||
|| thing->thingClass().interfaces().contains("smartmeterproducer")
|
||||
|| thing->thingClass().interfaces().contains("energystorage")) {
|
||||
connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &value){
|
||||
if (thing->thingClass().getStateType(stateTypeId).name() == "currentPower") {
|
||||
m_logger->logThingPower(thing->id(), value.toDouble(), thing->state("totalEnergyConsumed").value().toDouble(), thing->state("totalEnergyProduced").value().toDouble());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void EnergyManagerImpl::unwatchThing(const ThingId &thingId)
|
||||
@ -107,21 +134,25 @@ void EnergyManagerImpl::updatePowerBalance()
|
||||
currentPowerProduction += thing->stateValue("currentPower").toDouble();
|
||||
}
|
||||
|
||||
double currentBatteryBalance = 0;
|
||||
double currentPowerStorage = 0;
|
||||
foreach (Thing *thing, m_thingManager->configuredThings().filterByInterface("energystorage")) {
|
||||
currentBatteryBalance += thing->stateValue("currentPower").toDouble();
|
||||
currentPowerStorage += thing->stateValue("currentPower").toDouble();
|
||||
}
|
||||
|
||||
double currentPowerConsumption = -currentPowerProduction + currentPowerAcquisition - currentBatteryBalance;
|
||||
double currentPowerConsumption = -currentPowerProduction + currentPowerAcquisition - currentPowerStorage;
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Consumption:" << currentPowerConsumption << "Production:" << currentPowerProduction << "Acquisition:" << currentPowerAcquisition << "Battery:" << currentBatteryBalance;
|
||||
|
||||
qCDebug(dcEnergyExperience()) << "Consumption:" << currentPowerConsumption << "Production:" << currentPowerProduction << "Acquisition:" << currentPowerAcquisition << "Storage:" << currentPowerStorage;
|
||||
if (currentPowerAcquisition != m_currentPowerAcquisition
|
||||
|| currentPowerConsumption != m_currentPowerConsumption
|
||||
|| currentPowerProduction != m_currentPowerProduction) {
|
||||
|| currentPowerProduction != m_currentPowerProduction
|
||||
|| currentPowerStorage != m_currentPowerStorage) {
|
||||
m_currentPowerAcquisition = currentPowerAcquisition;
|
||||
m_currentPowerProduction = currentPowerProduction;
|
||||
m_currentPowerConsumption = currentPowerConsumption;
|
||||
m_currentPowerStorage = currentPowerStorage;
|
||||
emit powerBalanceChanged();
|
||||
m_logger->logPowerBalance(m_currentPowerConsumption, m_currentPowerProduction, m_currentPowerAcquisition, m_currentPowerStorage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
|
||||
#include "energymanager.h"
|
||||
|
||||
class EnergyLogger;
|
||||
|
||||
class EnergyManagerImpl : public EnergyManager
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -21,6 +23,9 @@ public:
|
||||
double currentPowerConsumption() const override;
|
||||
double currentPowerProduction() const override;
|
||||
double currentPowerAcquisition() const override;
|
||||
double currentPowerStorage() const override;
|
||||
|
||||
EnergyLogs* logs() const override;
|
||||
|
||||
private:
|
||||
void watchThing(Thing *thing);
|
||||
@ -40,6 +45,9 @@ private:
|
||||
double m_currentPowerConsumption;
|
||||
double m_currentPowerProduction;
|
||||
double m_currentPowerAcquisition;
|
||||
double m_currentPowerStorage;
|
||||
|
||||
EnergyLogger *m_logger = nullptr;
|
||||
};
|
||||
|
||||
#endif // ENERGYMANAGERIMPL_H
|
||||
|
||||
@ -5,7 +5,7 @@ CONFIG += plugin link_pkgconfig c++11
|
||||
PKGCONFIG += nymea
|
||||
|
||||
QT -= gui
|
||||
QT += network
|
||||
QT += network sql
|
||||
|
||||
include(../config.pri)
|
||||
|
||||
@ -14,10 +14,12 @@ LIBS += -L$$top_builddir/libnymea-energy -lnymea-energy
|
||||
|
||||
HEADERS += experiencepluginenergy.h \
|
||||
energyjsonhandler.h \
|
||||
energylogger.h \
|
||||
energymanagerimpl.h
|
||||
|
||||
SOURCES += experiencepluginenergy.cpp \
|
||||
energyjsonhandler.cpp \
|
||||
energylogger.cpp \
|
||||
energymanagerimpl.cpp
|
||||
|
||||
target.path = $$[QT_INSTALL_LIBS]/nymea/experiences/
|
||||
|
||||
Reference in New Issue
Block a user