From 59d4e1d50da38323803ddf05794db28d52746439 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 15 Nov 2021 01:20:17 +0100 Subject: [PATCH] Add logging support --- libnymea-energy/energylogs.cpp | 108 +++++ libnymea-energy/energylogs.h | 125 ++++++ libnymea-energy/energymanager.h | 7 + libnymea-energy/libnymea-energy.pro | 2 + plugin/energyjsonhandler.cpp | 80 +++- plugin/energyjsonhandler.h | 6 +- plugin/energylogger.cpp | 637 ++++++++++++++++++++++++++++ plugin/energylogger.h | 68 +++ plugin/energymanagerimpl.cpp | 49 ++- plugin/energymanagerimpl.h | 8 + plugin/plugin.pro | 4 +- 11 files changed, 1081 insertions(+), 13 deletions(-) create mode 100644 libnymea-energy/energylogs.cpp create mode 100644 libnymea-energy/energylogs.h create mode 100644 plugin/energylogger.cpp create mode 100644 plugin/energylogger.h diff --git a/libnymea-energy/energylogs.cpp b/libnymea-energy/energylogs.cpp new file mode 100644 index 0000000..4483945 --- /dev/null +++ b/libnymea-energy/energylogs.cpp @@ -0,0 +1,108 @@ +#include "energylogs.h" + +#include + +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()); +} + +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()); +} diff --git a/libnymea-energy/energylogs.h b/libnymea-energy/energylogs.h new file mode 100644 index 0000000..9f92ae9 --- /dev/null +++ b/libnymea-energy/energylogs.h @@ -0,0 +1,125 @@ +#ifndef ENERGYLOGS_H +#define ENERGYLOGS_H + +#include +#include +#include + +#include + +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 &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 +{ + Q_GADGET + Q_PROPERTY(int count READ count) +public: + PowerBalanceLogEntries() = default; + PowerBalanceLogEntries(const QList &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 +{ + Q_GADGET + Q_PROPERTY(int count READ count) +public: + ThingPowerLogEntries() = default; + ThingPowerLogEntries(const QList &other): QList(other) {} + Q_INVOKABLE QVariant get(int index) const; + Q_INVOKABLE void put(const QVariant &variant); +}; +Q_DECLARE_METATYPE(ThingPowerLogEntries) + +#endif // ENERGYLOGS_H diff --git a/libnymea-energy/energymanager.h b/libnymea-energy/energymanager.h index 83ce535..8a560cd 100644 --- a/libnymea-energy/energymanager.h +++ b/libnymea-energy/energymanager.h @@ -32,10 +32,13 @@ #ifndef ENERGYMANAGER_H #define ENERGYMANAGER_H +#include "energylogs.h" + #include #include + 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(); diff --git a/libnymea-energy/libnymea-energy.pro b/libnymea-energy/libnymea-energy.pro index fb31ade..91e8370 100644 --- a/libnymea-energy/libnymea-energy.pro +++ b/libnymea-energy/libnymea-energy.pro @@ -9,10 +9,12 @@ PKGCONFIG += nymea HEADERS += \ + energylogs.h \ energymanager.h \ energyplugin.h SOURCES += \ + energylogs.cpp \ energymanager.cpp \ energyplugin.cpp diff --git a/plugin/energyjsonhandler.cpp b/plugin/energyjsonhandler.cpp index c6ee7d9..22e402a 100644 --- a/plugin/energyjsonhandler.cpp +++ b/plugin/energyjsonhandler.cpp @@ -8,6 +8,10 @@ EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *pare m_energyManager(energyManager) { registerEnum(); + registerEnum(); + + registerObject(); + registerObject(); 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()); + params.insert("o:from", enumValueName(Uint)); + params.insert("o:to", enumValueName(Uint)); + returns.insert("powerBalanceLogEntries", objectRef()); + 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()); + params.insert("o:thingIds", QVariantList() << enumValueName(Uuid)); + params.insert("o:from", enumValueName(Uint)); + params.insert("o:to", enumValueName(Uint)); + returns.insert("thingPowerLogEntries", objectRef()); + 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()); + params.insert("powerBalanceLogEntry", objectRef()); + registerNotification("PowerBalanceLogEntryAdded", description, params); + + params.clear(); + description = "Emitted whenever a entry is added to the thing power log."; + params.insert("sampleRate", enumRef()); + params.insert("thingPowerLogEntry", objectRef()); + 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(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(params.value("sampleRate").toString()); + QList 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); +} diff --git a/plugin/energyjsonhandler.h b/plugin/energyjsonhandler.h index 4f7fed6..4d42b35 100644 --- a/plugin/energyjsonhandler.h +++ b/plugin/energyjsonhandler.h @@ -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 diff --git a/plugin/energylogger.cpp b/plugin/energylogger.cpp new file mode 100644 index 0000000..6ff2ef2 --- /dev/null +++ b/plugin/energylogger.cpp @@ -0,0 +1,637 @@ +#include "energylogger.h" + +#include + +#include +#include +#include +#include +#include + +#include +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 &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() << ")"; + } +} diff --git a/plugin/energylogger.h b/plugin/energylogger.h new file mode 100644 index 0000000..8832b15 --- /dev/null +++ b/plugin/energylogger.h @@ -0,0 +1,68 @@ +#ifndef ENERGYLOGGER_H +#define ENERGYLOGGER_H + +#include "energylogs.h" + +#include + +#include +#include +#include +#include +#include + +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 &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 m_thingsPowerLiveLogs; + + QTimer m_sampleTimer; + QHash m_nextSamples; + + QSqlDatabase m_db; + + int m_maxMinuteSamples = 0; + QMap m_configs; +}; + +#endif // ENERGYLOGGER_H diff --git a/plugin/energymanagerimpl.cpp b/plugin/energymanagerimpl.cpp index ac450fb..7b87807 100644 --- a/plugin/energymanagerimpl.cpp +++ b/plugin/energymanagerimpl.cpp @@ -1,5 +1,7 @@ #include "energymanagerimpl.h" -#include "nymeasettings.h" +#include "energylogger.h" + +#include #include @@ -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); } } diff --git a/plugin/energymanagerimpl.h b/plugin/energymanagerimpl.h index 1a70d90..617afa2 100644 --- a/plugin/energymanagerimpl.h +++ b/plugin/energymanagerimpl.h @@ -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 diff --git a/plugin/plugin.pro b/plugin/plugin.pro index 68acd49..8370364 100644 --- a/plugin/plugin.pro +++ b/plugin/plugin.pro @@ -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/