diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..569386d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pro.user +.crossbuilder/* diff --git a/config.pri b/config.pri new file mode 100644 index 0000000..faa15ab --- /dev/null +++ b/config.pri @@ -0,0 +1,10 @@ +gcc { + COMPILER_VERSION = $$system($$QMAKE_CXX " -dumpversion") + COMPILER_MAJOR_VERSION = $$str_member($$COMPILER_VERSION) + greaterThan(COMPILER_MAJOR_VERSION, 7): QMAKE_CXXFLAGS += -Wno-deprecated-copy +} + +top_srcdir = $$PWD +top_builddir = $$shadowed($$PWD) + +CONFIG += c++11 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..fbd66ef --- /dev/null +++ b/debian/changelog @@ -0,0 +1,3 @@ +nymea-experience-plugin-energy (0.3) xenial; urgency=medium + + -- Jenkins Tue, 05 Oct 2021 08:33:51 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..af488e6 --- /dev/null +++ b/debian/control @@ -0,0 +1,42 @@ +Source: nymea-experience-plugin-energy +Section: utils +Priority: optional +Maintainer: Michael Zanetti +Standards-Version: 3.9.7 +Homepage: https://nymea.io +Build-Depends: debhelper (>= 9.0.0), + dpkg-dev (>= 1.16.1~), + libnymea-dev (>= 0.17), + nymea-dev-tools:native, + qt5-qmake, + qtbase5-dev, + +Package: nymea-experience-plugin-energy +Section: libs +Architecture: any +Multi-Arch: same +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea experiece plugin for energy related use cases + This nymea experience adds the support energy related use + caes to nymea. + +Package: libnymea-energy +Section: libs +Architecture: any +Multi-Arch: same +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: Library for nymea energy experience plugins + This library is used by plugins loaded by the nymea + energy experience. + +Package: libnymea-energy-dev +Section: libdevel +Architecture: any +Multi-Arch: same +Depends: ${shlibs:Depends}, + ${misc:Depends}, + libnymea-energy (= ${binary:Version}), +Description: Library for nymea expergy experience plugins - Development files + diff --git a/debian/libnymea-energy-dev.install.in b/debian/libnymea-energy-dev.install.in new file mode 100644 index 0000000..2760d48 --- /dev/null +++ b/debian/libnymea-energy-dev.install.in @@ -0,0 +1,3 @@ +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-energy.so +usr/include/nymea-energy/*.h +usr/lib/@DEB_HOST_MULTIARCH@/pkgconfig/nymea-energy.pc diff --git a/debian/libnymea-energy.install.in b/debian/libnymea-energy.install.in new file mode 100644 index 0000000..62c885e --- /dev/null +++ b/debian/libnymea-energy.install.in @@ -0,0 +1,3 @@ +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-energy.so.1 +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-energy.so.1.0 +usr/lib/@DEB_HOST_MULTIARCH@/libnymea-energy.so.1.0.0 diff --git a/debian/nymea-experience-plugin-energy.install.in b/debian/nymea-experience-plugin-energy.install.in new file mode 100644 index 0000000..e81a9ca --- /dev/null +++ b/debian/nymea-experience-plugin-energy.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/experiences/libnymea_experiencepluginenergy.so diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..f063fd2 --- /dev/null +++ b/debian/rules @@ -0,0 +1,27 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +export DH_VERBOSE=1 +export QT_QPA_PLATFORM=minimal + +DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH) + +PREPROCESS_FILES := $(wildcard debian/*.in) + +$(PREPROCESS_FILES:.in=): %: %.in + sed 's,/@DEB_HOST_MULTIARCH@,$(DEB_HOST_MULTIARCH:%=/%),g' $< > $@ + +override_dh_install: $(PREPROCESS_FILES:.in=) + dh_install + +override_dh_auto_clean: + dh_auto_clean + rm -rf $(PREPROCESS_FILES:.in=) + +override_dh_auto_test: + +override_dh_install: $(PREPROCESS_FILES:.in=) + dh_install --fail-missing + +%: + dh $@ --buildsystem=qmake --parallel diff --git a/libnymea-energy/energylogs.cpp b/libnymea-energy/energylogs.cpp new file mode 100644 index 0000000..523078c --- /dev/null +++ b/libnymea-energy/energylogs.cpp @@ -0,0 +1,132 @@ +#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, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn): + m_timestamp(timestamp), + m_consumption(consumption), + m_production(production), + m_acquisition(acquisition), + m_storage(storage), + m_totalConsumption(totalConsumption), + m_totalProduction(totalProduction), + m_totalAcquisition(totalAcquisition), + m_totalReturn(totalReturn) +{ + +} + +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; +} + +double PowerBalanceLogEntry::totalConsumption() const +{ + return m_totalConsumption; +} + +double PowerBalanceLogEntry::totalProduction() const +{ + return m_totalProduction; +} + +double PowerBalanceLogEntry::totalAcquisition() const +{ + return m_totalAcquisition; +} + +double PowerBalanceLogEntry::totalReturn() const +{ + return m_totalReturn; +} + +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..301b6e2 --- /dev/null +++ b/libnymea-energy/energylogs.h @@ -0,0 +1,139 @@ +#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 { + SampleRateAny = 0, + 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 limit 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) + Q_PROPERTY(double totalConsumption READ totalConsumption) + Q_PROPERTY(double totalProduction READ totalProduction) + Q_PROPERTY(double totalAcquisition READ totalAcquisition) + Q_PROPERTY(double totalReturn READ totalReturn) +public: + PowerBalanceLogEntry(); + PowerBalanceLogEntry(const QDateTime ×tamp, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn); + QDateTime timestamp() const; + double consumption() const; + double production() const; + double acquisition() const; + double storage() const; + double totalConsumption() const; + double totalProduction() const; + double totalAcquisition() const; + double totalReturn() const; + +private: + QDateTime m_timestamp; + double m_consumption = 0; + double m_production = 0; + double m_acquisition = 0; + double m_storage = 0; + double m_totalConsumption = 0; + double m_totalProduction = 0; + double m_totalAcquisition = 0; + double m_totalReturn = 0; +}; +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 timestamp 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.cpp b/libnymea-energy/energymanager.cpp new file mode 100644 index 0000000..01e7a2c --- /dev/null +++ b/libnymea-energy/energymanager.cpp @@ -0,0 +1,37 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + +#include "energymanager.h" + +EnergyManager::EnergyManager(QObject *parent) : QObject(parent) +{ + +} diff --git a/libnymea-energy/energymanager.h b/libnymea-energy/energymanager.h new file mode 100644 index 0000000..2d18f5e --- /dev/null +++ b/libnymea-energy/energymanager.h @@ -0,0 +1,76 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + +#ifndef ENERGYMANAGER_H +#define ENERGYMANAGER_H + +#include "energylogs.h" + +#include + +#include + + +class EnergyManager : public QObject +{ + Q_OBJECT +public: + enum EnergyError { + EnergyErrorNoError, + EnergyErrorMissingParameter, + EnergyErrorInvalidParameter, + }; + Q_ENUM(EnergyError) + + + explicit EnergyManager(QObject *parent = nullptr); + virtual ~EnergyManager() = default; + + virtual EnergyError setRootMeter(const ThingId &rootMeterId) = 0; + virtual Thing *rootMeter() const = 0; + + virtual double currentPowerConsumption() const = 0; + virtual double currentPowerProduction() const = 0; + virtual double currentPowerAcquisition() const = 0; + virtual double currentPowerStorage() const = 0; + virtual double totalConsumption() const = 0; + virtual double totalProduction() const = 0; + virtual double totalAcquisition() const = 0; + virtual double totalReturn() const = 0; + + virtual EnergyLogs* logs() const = 0; + +signals: + void rootMeterChanged(); + void powerBalanceChanged(); +}; + +#endif // ENERGYMANAGER_H diff --git a/libnymea-energy/energyplugin.cpp b/libnymea-energy/energyplugin.cpp new file mode 100644 index 0000000..75d8f99 --- /dev/null +++ b/libnymea-energy/energyplugin.cpp @@ -0,0 +1,61 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "energyplugin.h" + +EnergyPlugin::EnergyPlugin(QObject *parent) : QObject(parent) +{ + +} + +EnergyManager *EnergyPlugin::energyManager() const +{ + return m_energyManager; +} + +ThingManager *EnergyPlugin::thingManager() const +{ + return m_thingManager; +} + +JsonRPCServer *EnergyPlugin::jsonRpcServer() const +{ + return m_jsonRpcServer; +} + +void EnergyPlugin::initPlugin(EnergyManager *energyManager, ThingManager *thingManager, JsonRPCServer *jsonRPCServer) +{ + m_energyManager = energyManager; + m_thingManager = thingManager; + m_jsonRpcServer = jsonRPCServer; + + init(); +} + diff --git a/libnymea-energy/energyplugin.h b/libnymea-energy/energyplugin.h new file mode 100644 index 0000000..9202c58 --- /dev/null +++ b/libnymea-energy/energyplugin.h @@ -0,0 +1,66 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef ENERGYPLUGIN_H +#define ENERGYPLUGIN_H + +#include + +#include +#include + +#include "energymanager.h" + +class EnergyPlugin : public QObject +{ + Q_OBJECT +public: + explicit EnergyPlugin(QObject *parent = nullptr); + virtual ~EnergyPlugin() = default; + + virtual void init() = 0; + +protected: + EnergyManager* energyManager() const; + ThingManager* thingManager() const; + JsonRPCServer* jsonRpcServer() const; + +private: + friend class ExperiencePluginEnergy; + void initPlugin(EnergyManager *energyManager, ThingManager *thingManager, JsonRPCServer *jsonRPCServer); + + EnergyManager *m_energyManager = nullptr; + ThingManager *m_thingManager = nullptr; + JsonRPCServer *m_jsonRpcServer = nullptr; +}; + +Q_DECLARE_INTERFACE(EnergyPlugin, "io.nymea.EnergyPlugin") + +#endif // ENERGYPLUGIN_H diff --git a/libnymea-energy/libnymea-energy.pro b/libnymea-energy/libnymea-energy.pro new file mode 100644 index 0000000..91e8370 --- /dev/null +++ b/libnymea-energy/libnymea-energy.pro @@ -0,0 +1,40 @@ +TEMPLATE = lib +TARGET = $$qtLibraryTarget(nymea-energy) + +include(../config.pri) +NYMEA_ENERGY_VERSION_STRING = "0.0.1" + +CONFIG += link_pkgconfig +PKGCONFIG += nymea + + +HEADERS += \ + energylogs.h \ + energymanager.h \ + energyplugin.h + +SOURCES += \ + energylogs.cpp \ + energymanager.cpp \ + energyplugin.cpp + +target.path = $$[QT_INSTALL_LIBS] +INSTALLS += target + +for(header, HEADERS) { + path = $$[QT_INSTALL_PREFIX]/include/nymea-energy/$${dirname(header)} + eval(headers_$${path}.files += $${header}) + eval(headers_$${path}.path = $${path}) + eval(INSTALLS *= headers_$${path}) +} + +CONFIG += create_pc create_prl no_install_prl +QMAKE_PKGCONFIG_NAME = libnymea-energy +QMAKE_PKGCONFIG_DESCRIPTION = nymea energy library +QMAKE_PKGCONFIG_PREFIX = $$[QT_INSTALL_PREFIX] +QMAKE_PKGCONFIG_INCDIR = $$[QT_INSTALL_PREFIX]/include/nymea-energy/ +QMAKE_PKGCONFIG_LIBDIR = $$target.path +QMAKE_PKGCONFIG_VERSION = $$NYMEA_ENERGY_VERSION_STRING +QMAKE_PKGCONFIG_FILE = nymea-energy +QMAKE_PKGCONFIG_DESTDIR = pkgconfig + diff --git a/nymea-experience-plugin-energy.pro b/nymea-experience-plugin-energy.pro new file mode 100644 index 0000000..7c4fc4b --- /dev/null +++ b/nymea-experience-plugin-energy.pro @@ -0,0 +1,7 @@ +TEMPLATE = subdirs + +SUBDIRS += libnymea-energy plugin + +plugin.depends = libnymea-energy + + diff --git a/plugin/energyjsonhandler.cpp b/plugin/energyjsonhandler.cpp new file mode 100644 index 0000000..dc2a75e --- /dev/null +++ b/plugin/energyjsonhandler.cpp @@ -0,0 +1,204 @@ +#include "energyjsonhandler.h" +#include "energymanagerimpl.h" + +Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience) + +EnergyJsonHandler::EnergyJsonHandler(EnergyManager *energyManager, QObject *parent): + JsonHandler(parent), + m_energyManager(energyManager) +{ + registerEnum(); + registerEnum(); + + registerObject(); + registerObject(); + + QVariantMap params, returns; + QString description; + + params.clear(); returns.clear(); + description = "Get the root meter ID. If there is no root meter set, the params will be empty."; + returns.insert("o:rootMeterThingId", enumValueName(Uuid)); + registerMethod("GetRootMeter", description, params, returns); + + params.clear(); returns.clear(); + description = "Set the root meter."; + params.insert("rootMeterThingId", enumValueName(Uuid)); + returns.insert("energyError", enumRef()); + registerMethod("SetRootMeter", description, params, returns); + + params.clear(); returns.clear(); + description = "Get the current power balance. That is, production, consumption and acquisition."; + returns.insert("currentPowerConsumption", enumValueName(Double)); + returns.insert("currentPowerProduction", enumValueName(Double)); + returns.insert("currentPowerAcquisition", enumValueName(Double)); + returns.insert("currentPowerStorage", enumValueName(Double)); + returns.insert("totalConsumption", enumValueName(Double)); + returns.insert("totalProduction", enumValueName(Double)); + returns.insert("totalAcquisition", enumValueName(Double)); + returns.insert("totalReturn", 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. If the parameter " + "\"includeCurrent\" is set to true, the result will contain the newest log entries available, regardless " + "of the sample rate (that is, 1 minute). This may be useful to calculate the difference to the newest " + "entry of the fetched sample rate and the current values to display the live value until the current sample " + "is completed."; + params.insert("sampleRate", enumRef()); + params.insert("o:thingIds", QVariantList() << enumValueName(Uuid)); + params.insert("o:from", enumValueName(Uint)); + params.insert("o:to", enumValueName(Uint)); + params.insert("o:includeCurrent", enumValueName(Bool)); + returns.insert("o:currentEntries", objectRef()); + 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."; + params.insert("currentPowerConsumption", enumValueName(Double)); + params.insert("currentPowerProduction", enumValueName(Double)); + params.insert("currentPowerAcquisition", enumValueName(Double)); + params.insert("currentPowerStorage", enumValueName(Double)); + params.insert("totalConsumption", enumValueName(Double)); + params.insert("totalProduction", enumValueName(Double)); + params.insert("totalAcquisition", enumValueName(Double)); + params.insert("totalReturn", enumValueName(Double)); + registerNotification("PowerBalanceChanged", description, params); + + params.clear(); + description = "Emitted whenever an 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 an 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()) { + params.insert("rootMeterThingId", m_energyManager->rootMeter()->id()); + } + emit RootMeterChanged(params); + }); + + connect(m_energyManager, &EnergyManager::powerBalanceChanged, this, [=](){ + QVariantMap params; + params.insert("currentPowerConsumption", m_energyManager->currentPowerConsumption()); + params.insert("currentPowerProduction", m_energyManager->currentPowerProduction()); + params.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition()); + params.insert("currentPowerStorage", m_energyManager->currentPowerStorage()); + params.insert("totalConsumption", m_energyManager->totalConsumption()); + params.insert("totalProduction", m_energyManager->totalProduction()); + params.insert("totalAcquisition", m_energyManager->totalAcquisition()); + params.insert("totalReturn", m_energyManager->totalReturn()); + 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 +{ + return "Energy"; +} + +JsonReply *EnergyJsonHandler::GetRootMeter(const QVariantMap ¶ms) +{ + Q_UNUSED(params) + QVariantMap ret; + if (m_energyManager->rootMeter()) { + ret.insert("rootMeterThingId", m_energyManager->rootMeter()->id()); + } + return createReply(ret); +} + +JsonReply *EnergyJsonHandler::SetRootMeter(const QVariantMap ¶ms) +{ + QVariantMap returns; + + if (!params.contains("rootMeterThingId")) { + returns.insert("energyError", enumValueName(EnergyManager::EnergyErrorMissingParameter)); + return createReply(returns); + } + EnergyManager::EnergyError status = m_energyManager->setRootMeter(params.value("rootMeterThingId").toUuid()); + returns.insert("energyError", enumValueName(status)); + return createReply(returns); +} + +JsonReply *EnergyJsonHandler::GetPowerBalance(const QVariantMap ¶ms) +{ + Q_UNUSED(params) + QVariantMap ret; + ret.insert("currentPowerConsumption", m_energyManager->currentPowerConsumption()); + ret.insert("currentPowerProduction", m_energyManager->currentPowerProduction()); + ret.insert("currentPowerAcquisition", m_energyManager->currentPowerAcquisition()); + ret.insert("currentPowerStorage", m_energyManager->currentPowerStorage()); + ret.insert("totalConsumption", m_energyManager->totalConsumption()); + ret.insert("totalProduction", m_energyManager->totalProduction()); + ret.insert("totalAcquisition", m_energyManager->totalAcquisition()); + ret.insert("totalReturn", m_energyManager->totalReturn()); + return createReply(ret); +} + +JsonReply *EnergyJsonHandler::GetPowerBalanceLogs(const QVariantMap ¶ms) +{ + EnergyLogs::SampleRate sampleRate = enumNameToValue(params.value("sampleRate").toString()); + 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("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))); + + if (params.contains("includeCurrent") && params.value("includeCurrent").toBool()) { + returns.insert("currentEntries", pack(m_energyManager->logs()->thingPowerLogs(EnergyLogs::SampleRate1Min, thingIds, QDateTime::currentDateTime().addSecs(-60)))); + } + + return createReply(returns); +} diff --git a/plugin/energyjsonhandler.h b/plugin/energyjsonhandler.h new file mode 100644 index 0000000..4d42b35 --- /dev/null +++ b/plugin/energyjsonhandler.h @@ -0,0 +1,33 @@ +#ifndef ENERGYJSONHANDLER_H +#define ENERGYJSONHANDLER_H + +#include +#include "jsonrpc/jsonhandler.h" + +class EnergyManager; + +class EnergyJsonHandler : public JsonHandler +{ + Q_OBJECT +public: + explicit EnergyJsonHandler(EnergyManager *energyManager, QObject *parent = nullptr); + + QString name() const override; + + 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..6ddfff6 --- /dev/null +++ b/plugin/energylogger.cpp @@ -0,0 +1,876 @@ +#include "energylogger.h" + +#include + +#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 50 bytes for powerBalance + 60 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 * 50 bytes) + (count * things * 60 bytes) + 5% + // 10000 entries, with 5 energy things => ~4MB + // Note: use sqlite3_analyzer to see the approx. size per entry in each table. + + 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 last values from thingsPort logs so we have at least one base sample available for sampling, even if a thing might not produce any logs for a while. + foreach (const ThingId &thingId, loggedThings()) { + m_thingsPowerLiveLogs[thingId].append(latestLogEntry(SampleRate1Min, thingId)); + } + + // 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, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn) +{ + PowerBalanceLogEntry entry(QDateTime::currentDateTime(), consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn); + + // 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) +{ + 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 << 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(queryResultToBalanceLogEntry(query.record())); + } + 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; + + qCDebug(dcEnergyExperience()) << "Fetching thing power logs for" << thingIds; + + 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; + +} + +PowerBalanceLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate) +{ + if (sampleRate == SampleRateAny) { + if (m_balanceLiveLog.count() > 0) { + return m_balanceLiveLog.first(); + } + } + QSqlQuery query(m_db); + QString queryString = "SELECT MAX(timestamp), consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn FROM powerBalance"; + QVariantList bindValues; + if (sampleRate != SampleRateAny) { + queryString += " WHERE sampleRate = ?"; + bindValues.append(sampleRate); + } + queryString += ";"; + query.prepare(queryString); + foreach (const QVariant &value, bindValues) { + query.addBindValue(value); + } + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error obtaining latest log entry from DB:" << query.lastError() << query.executedQuery(); + return PowerBalanceLogEntry(); + } + if (!query.next()) { + qCDebug(dcEnergyExperience()) << "No power balance log entry in DB for sample rate:" << sampleRate; + return PowerBalanceLogEntry(); + } + return queryResultToBalanceLogEntry(query.record()); +} + +ThingPowerLogEntry EnergyLogger::latestLogEntry(SampleRate sampleRate, const ThingId &thingId) +{ + if (sampleRate == SampleRateAny) { + if (m_thingsPowerLiveLogs.value(thingId).count() > 0) { + return m_thingsPowerLiveLogs.value(thingId).first(); + } + } + + QSqlQuery query(m_db); + query.prepare("SELECT MAX(timestamp), currentPower, totalConsumption, totalProduction from thingPower WHERE sampleRate = ? AND thingId = ?;"); + query.addBindValue(sampleRate); + query.addBindValue(thingId); + if (!query.exec()) { + qCWarning(dcEnergyExperience()) << "Error fetching latest thing log entry from DB:" << query.lastError() << query.executedQuery(); + return ThingPowerLogEntry(); + } + if (!query.next()) { + qCDebug(dcEnergyExperience()) << "No thing power log entry in DB for sample rate:" << sampleRate; + return ThingPowerLogEntry(); + } + return queryResultToThingPowerLogEntry(query.record()); + +} + +void EnergyLogger::removeThingLogs(const ThingId &thingId) +{ + QSqlQuery query(m_db); + query.prepare("DELETE FROM thingPower WHERE thingId = ?;"); + query.addBindValue(thingId); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error removing thing energy logs for thing id" << thingId << query.lastError() << query.executedQuery(); + } +} + +QList EnergyLogger::loggedThings() const +{ + QList ret; + + 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()) { + ret.append(query.value("thingId").toUuid()); + } + } + return ret; +} + +void EnergyLogger::cacheThingEntry(const ThingId &thingId, double totalEnergyConsumed, double totalEnergyProduced) +{ + QSqlQuery query(m_db); + query.prepare("INSERT OR REPLACE INTO thingCache (thingId, totalEnergyConsumed, totalEnergyProduced) VALUES (?, ?, ?);"); + query.addBindValue(thingId); + query.addBindValue(totalEnergyConsumed); + query.addBindValue(totalEnergyProduced); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Failed to store thing cache entry:" << query.lastError() << query.executedQuery(); + } +} + +ThingPowerLogEntry EnergyLogger::cachedThingEntry(const ThingId &thingId) +{ + QSqlQuery query(m_db); + query.prepare("SELECT * FROM thingCache WHERE thingId = ?;"); + query.addBindValue(thingId); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Failed to retrieve thing cache entry:" << query.lastError() << query.executedQuery(); + return ThingPowerLogEntry(); + } + if (!query.next()) { + qCDebug(dcEnergyExperience()) << "No cached thing entry for" << thingId; + return ThingPowerLogEntry(); + } + return ThingPowerLogEntry(QDateTime(), thingId, 0, query.value("totalEnergyConsumed").toDouble(), query.value("totalEnergyProduced").toDouble()); +} + +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 power balance for 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); + qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.consumption() << "start" << frameStart.toString() << "end" << frameEnd.toString(); + + medianConsumption += entry.consumption() * frameDuration; + medianProduction += entry.production() * frameDuration; + medianAcquisition += entry.acquisition() * frameDuration; + medianStorage += entry.storage() * frameDuration; + if (entry.timestamp() < sampleStart) { + break; + } + } + medianConsumption /= sampleStart.msecsTo(sampleEnd); + medianProduction /= sampleStart.msecsTo(sampleEnd); + medianAcquisition /= sampleStart.msecsTo(sampleEnd); + medianStorage /= sampleStart.msecsTo(sampleEnd); + + PowerBalanceLogEntry newest = m_balanceLiveLog.count() > 0 ? m_balanceLiveLog.at(0) : PowerBalanceLogEntry(); + double totalConsumption = newest.totalConsumption(); + double totalProduction = newest.totalProduction(); + double totalAcquisition = newest.totalAcquisition(); + double totalReturn = newest.totalReturn(); + + qCDebug(dcEnergyExperience()) << "Sampled power balance:" << SampleRate1Min << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn; + insertPowerBalance(sampleEnd, SampleRate1Min, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn); + + foreach (const ThingId &thingId, m_thingsPowerLiveLogs.keys()) { + double medianPower = 0; + qCDebug(dcEnergyExperience()) << "Sampling thing power for" << thingId.toString() << SampleRate1Min << sampleEnd.toString(); + 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); + qCDebug(dcEnergyExperience()) << "Frame" << i << "duration:" << frameDuration << "value:" << entry.currentPower(); + medianPower += entry.currentPower() * frameDuration; + if (entry.timestamp() < sampleStart) { + break; + } + } + medianPower /= sampleStart.msecsTo(sampleEnd); + + ThingPowerLogEntry newest = entries.count() > 0 ? entries.first() : ThingPowerLogEntry(); + double totalConsumption = newest.totalConsumption(); + double totalProduction = newest.totalProduction(); + + qCDebug(dcEnergyExperience()) << "Sampled thing power for" << thingId << SampleRate1Min << "🔥/🌞:" << medianPower << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction; + insertThingPower(sampleEnd, SampleRate1Min, thingId, medianPower, 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(-(qint64)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(-(qint64)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("metadata")) { + qCDebug(dcEnergyExperience()) << "No \metadata\" table in database. Creating it."; + m_db.exec("CREATE TABLE metadata (version INT);"); + m_db.exec("INSERT INTO metadata (version) VALUES (1);"); + + if (m_db.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error creating metadata table in energy log database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); + 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," + "totalConsumption FLOAT," + "totalProduction FLOAT," + "totalAcquisition FLOAT," + "totalReturn 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; + } + } + + if (!m_db.tables().contains("thingCache")) { + qCDebug(dcEnergyExperience()) << "No \"thingCache\" table in database. Creating it."; + m_db.exec("CREATE TABLE thingCache " + "(" + "thingId VARCHAR(38) PRIMARY KEY," + "totalEnergyConsumed FLOAT," + "totalEnergyProduced FLOAT" + ");"); + if (m_db.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error creating thingCache 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." << m_db.databaseName(); + 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 series 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 for the base 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 SampleRateAny: + qCWarning(dcEnergyExperience()) << "Cannot calculate next sample timestamp without a sample rate"; + return QDateTime(); + 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 power balance" << sampleRate << "from" << sampleStart << "to" << sampleEnd; + + double medianConsumption = 0; + double medianProduction = 0; + double medianAcquisition = 0; + double medianStorage = 0; + double totalConsumption = 0; + double totalProduction = 0; + double totalAcquisition = 0; + double totalReturn = 0; + + 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; + } + + int resultCount = 0; + while (query.next()) { + resultCount++; + qCDebug(dcEnergyExperience()) << "Frame:" << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString() << query.value("consumption").toDouble() << query.value("production").toDouble() << query.value("acquisition").toDouble() << query.value("storage").toDouble() << query.value("totalConsumption").toDouble() << query.value("totalProduction").toDouble() << query.value("totalAcquisition").toDouble() << query.value("totalReturn").toDouble(); + medianConsumption += query.value("consumption").toDouble(); + medianProduction += query.value("production").toDouble(); + medianAcquisition += query.value("acquisition").toDouble(); + medianStorage += query.value("storage").toDouble(); + totalConsumption = query.value("totalConsumption").toDouble(); + totalProduction = query.value("totalProduction").toDouble(); + totalAcquisition = query.value("totalAcquisition").toDouble(); + totalReturn = query.value("totalReturn").toDouble(); + } + if (resultCount > 0) { + medianConsumption = medianConsumption * baseSampleRate / sampleRate; + medianProduction = medianProduction * baseSampleRate / sampleRate; + medianAcquisition = medianAcquisition * baseSampleRate / sampleRate; + medianStorage = medianStorage * baseSampleRate / sampleRate; + + } else { + // If there are no base samples for the given time frame at all, let's try to find the last existing one in the base + // to at least copy the totals from where we left off. + + query = QSqlQuery(m_db); + query.prepare("SELECT MAX(timestamp), consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn FROM powerBalance WHERE sampleRate = ?;"); + query.addBindValue(baseSampleRate); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error fetching newest power balance sample for" << baseSampleRate; + qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery(); + return false; + } + + if (query.next()) { + totalConsumption = query.value("totalConsumption").toDouble(); + totalProduction = query.value("totalProduction").toDouble(); + totalAcquisition = query.value("totalAcquisition").toDouble(); + totalReturn = query.value("totalReturn").toDouble(); + } + } + + + qCDebug(dcEnergyExperience()) << "Sampled:" << "🔥:" << medianConsumption << "🌞:" << medianProduction << "💵:" << medianAcquisition << "🔋:" << medianStorage << "Totals:" << "🔥:" << totalConsumption << "🌞:" << totalProduction << "💵↓:" << totalAcquisition << "💵↑:" << totalReturn; + return insertPowerBalance(sampleEnd, sampleRate, medianConsumption, medianProduction, medianAcquisition, medianStorage, totalConsumption, totalProduction, totalAcquisition, totalReturn); +} + +bool EnergyLogger::insertPowerBalance(const QDateTime ×tamp, SampleRate sampleRate, double consumption, double production, double acquisition, double storage, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn) +{ + QSqlQuery query = QSqlQuery(m_db); + query.prepare("INSERT INTO powerBalance (timestamp, sampleRate, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"); + query.addBindValue(timestamp.toMSecsSinceEpoch()); + query.addBindValue(sampleRate); + query.addBindValue(consumption); + query.addBindValue(production); + query.addBindValue(acquisition); + query.addBindValue(storage); + query.addBindValue(totalConsumption); + query.addBindValue(totalProduction); + query.addBindValue(totalAcquisition); + query.addBindValue(totalReturn); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error logging consumption sample:" << query.lastError() << query.executedQuery(); + return false; + } + emit powerBalanceEntryAdded(sampleRate, PowerBalanceLogEntry(timestamp, consumption, production, acquisition, storage, totalConsumption, totalProduction, totalAcquisition, totalReturn)); + 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 thing power for" << thingId.toString() << sampleRate << "from" << sampleStart.toString() << "to" << sampleEnd.toString(); + + double medianCurrentPower = 0; + double totalConsumption = 0; + double totalProduction = 0; + + 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; + } + + qCDebug(dcEnergyExperience()) << "Query:" << query.executedQuery(); + qCDebug(dcEnergyExperience()) << "Results:" << query.size(); + + int resultCount = 0; + while (query.next()) { + resultCount++; + qCDebug(dcEnergyExperience()) << "Frame:" << query.value("currentPower").toDouble() << QDateTime::fromMSecsSinceEpoch(query.value("timestamp").toLongLong()).toString(); + medianCurrentPower += query.value("currentPower").toDouble(); + totalConsumption = query.value("totalConsumption").toDouble(); + totalProduction = query.value("totalProduction").toDouble(); + } + if (resultCount > 0) { + medianCurrentPower = medianCurrentPower * baseSampleRate / sampleRate; + + } else { + // If there are no base samples for the given time frame at all, let's try to find the last existing one in the base + // to at least copy the totals from where we left off. + + query = QSqlQuery(m_db); + query.prepare("SELECT MAX(timestamp), currentPower, totalConsumption, totalProduction FROM thingPower WHERE thingId = ? AND sampleRate = ?;"); + query.addBindValue(thingId); + query.addBindValue(baseSampleRate); + query.exec(); + if (query.lastError().isValid()) { + qCWarning(dcEnergyExperience()) << "Error fetching newest thing power sample for" << thingId.toString() << baseSampleRate; + qCWarning(dcEnergyExperience()) << "SQL error was:" << query.lastError() << "executed query:" << query.executedQuery(); + return false; + } + + if (query.next()) { + totalConsumption = query.value("totalConsumption").toDouble(); + totalProduction = query.value("totalProduction").toDouble(); + } + } + + + qCDebug(dcEnergyExperience()) << "Sampled:" << thingId.toString() << sampleRate << "median currentPower:" << medianCurrentPower << "total consumption:" << totalConsumption << "total production:" << totalProduction; + 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() << ")"; + } +} + +PowerBalanceLogEntry EnergyLogger::queryResultToBalanceLogEntry(const QSqlRecord &record) const +{ + return PowerBalanceLogEntry(QDateTime::fromMSecsSinceEpoch(record.value("timestamp").toLongLong()), + record.value("consumption").toDouble(), + record.value("production").toDouble(), + record.value("acquisition").toDouble(), + record.value("storage").toDouble(), + record.value("totalConsumption").toDouble(), + record.value("totalProduction").toDouble(), + record.value("totalAcquisition").toDouble(), + record.value("totalReturn").toDouble()); + +} + +ThingPowerLogEntry EnergyLogger::queryResultToThingPowerLogEntry(const QSqlRecord &record) const +{ + return ThingPowerLogEntry(QDateTime::fromMSecsSinceEpoch(record.value("timestamp").toULongLong()), + record.value("thingId").toUuid(), + record.value("currentPower").toDouble(), + record.value("totalConsumption").toDouble(), + record.value("totalProduction").toDouble()); +} diff --git a/plugin/energylogger.h b/plugin/energylogger.h new file mode 100644 index 0000000..6c279d8 --- /dev/null +++ b/plugin/energylogger.h @@ -0,0 +1,84 @@ +#ifndef ENERGYLOGGER_H +#define ENERGYLOGGER_H + +#include "energylogs.h" + +#include + +#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, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn); + 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; + + PowerBalanceLogEntry latestLogEntry(SampleRate sampleRate); + ThingPowerLogEntry latestLogEntry(SampleRate sampleRate, const ThingId &thingId); + + void removeThingLogs(const ThingId &thingId); + QList loggedThings() const; + + // For internal use, the energymanager needs to cache some values to track things total values + // This is really only here to have a single storage and not keep a separate cache file. Shouldn't be used for anything else + // Note that the returned ThingPowerLogEntry will be incomplete. It won't have a timestamp nor a currentPower value! + void cacheThingEntry(const ThingId &thingId, double totalEnergyConsumed, double totalEnergyProduced); + ThingPowerLogEntry cachedThingEntry(const ThingId &thingId); + +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, double totalConsumption, double totalProduction, double totalAcquisition, double totalReturn); + 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); + + PowerBalanceLogEntry queryResultToBalanceLogEntry(const QSqlRecord &record) const; + ThingPowerLogEntry queryResultToThingPowerLogEntry(const QSqlRecord &record) const; + +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 new file mode 100644 index 0000000..7b20d00 --- /dev/null +++ b/plugin/energymanagerimpl.cpp @@ -0,0 +1,319 @@ +#include "energymanagerimpl.h" +#include "energylogger.h" + +#include + +#include + +Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience) + +EnergyManagerImpl::EnergyManagerImpl(ThingManager *thingManager, QObject *parent): + EnergyManager(parent), + m_thingManager(thingManager), + m_logger(new EnergyLogger(this)) +{ + // Most of the time we get a bunch of state changes (currentPower, totals, for inverter, battery, rootmeter) + // at the same time if they're implemented by the same plugin. + // In order to decrease some load on the system, we'll wait for the event loop pass to finish until we actually + // update to accumulate those changes and calculate the change in one go. + m_balanceUpdateTimer.setInterval(0); + m_balanceUpdateTimer.setSingleShot(true); + connect(&m_balanceUpdateTimer, &QTimer::timeout, this, &EnergyManagerImpl::updatePowerBalance); + + QSettings settings(NymeaSettings::settingsPath() + "/energy.conf", QSettings::IniFormat); + ThingId rootMeterThingId = settings.value("rootMeterThingId").toUuid(); + EnergyManagerImpl::setRootMeter(rootMeterThingId); + qCDebug(dcEnergyExperience()) << "Loaded root meter" << rootMeterThingId; + + PowerBalanceLogEntry latestEntry = m_logger->latestLogEntry(EnergyLogs::SampleRateAny); + m_totalConsumption = latestEntry.totalConsumption(); + m_totalProduction = latestEntry.totalProduction(); + m_totalAcquisition = latestEntry.totalAcquisition(); + m_totalReturn = latestEntry.totalReturn(); + qCDebug(dcEnergyExperience()) << "Loaded power balance totals. Consumption:" << m_totalConsumption << "Production:" << m_totalProduction << "Acquisition:" << m_totalAcquisition << "Return:" << m_totalReturn; + + foreach (Thing *thing, m_thingManager->configuredThings()) { + watchThing(thing); + } + connect(thingManager, &ThingManager::thingAdded, this, &EnergyManagerImpl::watchThing); + connect(thingManager, &ThingManager::thingRemoved, this, &EnergyManagerImpl::unwatchThing); + + // Housekeeping on the logger + foreach (const ThingId &thingId, m_logger->loggedThings()) { + if (!m_thingManager->findConfiguredThing(thingId)) { + qCDebug(dcEnergyExperience()) << "Clearing thing logs for unknown thing id" << thingId << "from energy logs."; + m_logger->removeThingLogs(thingId); + } + } +} + +Thing *EnergyManagerImpl::rootMeter() const +{ + return m_rootMeter; +} + +EnergyManager::EnergyError EnergyManagerImpl::setRootMeter(const ThingId &rootMeterId) +{ + Thing *rootMeter = m_thingManager->findConfiguredThing(rootMeterId); + if (!rootMeter || !rootMeter->thingClass().interfaces().contains("energymeter")) { + return EnergyErrorInvalidParameter; + } + + if (m_rootMeter != rootMeter) { + qCDebug(dcEnergyExperience()) << "Setting root meter to" << rootMeter->name(); + m_rootMeter = rootMeter; + + QSettings settings(NymeaSettings::settingsPath() + "/energy.conf", QSettings::IniFormat); + settings.setValue("rootMeterThingId", rootMeter->id()); + + emit rootMeterChanged(); + } + return EnergyErrorNoError; +} + +double EnergyManagerImpl::currentPowerConsumption() const +{ + return m_currentPowerConsumption; +} + +double EnergyManagerImpl::currentPowerProduction() const +{ + return m_currentPowerProduction; +} + +double EnergyManagerImpl::currentPowerAcquisition() const +{ + return m_currentPowerAcquisition; +} + +double EnergyManagerImpl::currentPowerStorage() const +{ + return m_currentPowerStorage; +} + +double EnergyManagerImpl::totalConsumption() const +{ + return m_totalConsumption; +} + +double EnergyManagerImpl::totalProduction() const +{ + return m_totalProduction; +} + +double EnergyManagerImpl::totalAcquisition() const +{ + return m_totalAcquisition; +} + +double EnergyManagerImpl::totalReturn() const +{ + return m_totalReturn; +} + +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. + // It may be changed by the user through an API call later. + if (!m_rootMeter && thing->thingClass().interfaces().contains("energymeter")) { + setRootMeter(thing->id()); + } + + qCDebug(dcEnergyExperience()) << "Watching thing:" << thing->name(); + + // React on things that require 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") { + m_balanceUpdateTimer.start(); + } + }); + } + + // 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")) { + + // Initialize caches used to calculate diffs + ThingPowerLogEntry entry = m_logger->latestLogEntry(EnergyLogs::SampleRateAny, {thing->id()}); + ThingPowerLogEntry stateEntry = m_logger->cachedThingEntry(thing->id()); + + m_powerBalanceTotalEnergyConsumedCache[thing] = stateEntry.totalConsumption(); + m_powerBalanceTotalEnergyProducedCache[thing] = stateEntry.totalProduction(); + + m_thingsTotalEnergyConsumedCache[thing] = qMakePair(stateEntry.totalConsumption(), entry.totalConsumption()); + m_thingsTotalEnergyProducedCache[thing] = qMakePair(stateEntry.totalProduction(), entry.totalProduction()); + qCDebug(dcEnergyExperience()) << "Loaded thing power totals for" << thing->name() << "Consumption:" << entry.totalConsumption() << "Production:" << entry.totalProduction() << "Last thing state consumption:" << stateEntry.totalConsumption() << "production:" << stateEntry.totalProduction(); + + connect(thing, &Thing::stateValueChanged, this, [=](const StateTypeId &stateTypeId, const QVariant &/*value*/){ + if (QStringList({"currentPower", "totalEnergyConsumed", "totalEnergyProduced"}).contains(thing->thingClass().getStateType(stateTypeId).name())) { + + // We'll be keeping our own counters, starting from 0 at the time they're added to nymea and increasing with the things counters. + // This way we'll have proper logs even if the thing counter is reset (some things may reset their counter on power loss, factory reset etc) + // and also won't start with huge values if the thing has been counting for a while and only added to nymea later on + + + // Consumption + double oldThingConsumptionState = m_thingsTotalEnergyConsumedCache.value(thing).first; + double oldThingConsumptionInternal = m_thingsTotalEnergyConsumedCache.value(thing).second; + double newThingConsumptionState = thing->stateValue("totalEnergyConsumed").toDouble(); + // For the very first cycle (oldConsumption is 0) we'll sync up on the meter, without actually adding it to our diff + if (oldThingConsumptionState == 0 && newThingConsumptionState != 0) { + qInfo(dcEnergyExperience()) << "Don't have a consumption counter for" << thing->name() << "Synching internal counters to initial value:" << newThingConsumptionState; + oldThingConsumptionState = newThingConsumptionState; + } + // If the thing's meter has been reset in the meantime (newConsumption < oldConsumption) we'll sync down, taking the whole diff from 0 to new value + if (newThingConsumptionState < oldThingConsumptionState) { + qCInfo(dcEnergyExperience()) << "Thing meter for" << thing->name() << "seems to have been reset. Re-synching internal consumption counter."; + oldThingConsumptionState = newThingConsumptionState; + } + double consumptionDiff = newThingConsumptionState - oldThingConsumptionState; + double newThingConsumptionInternal = oldThingConsumptionInternal + consumptionDiff; + m_thingsTotalEnergyConsumedCache[thing] = qMakePair(newThingConsumptionState, newThingConsumptionInternal); + + + // Production + double oldThingProductionState = m_thingsTotalEnergyProducedCache.value(thing).first; + double oldThingProductionInternal = m_thingsTotalEnergyProducedCache.value(thing).second; + double newThingProductionState = thing->stateValue("totalEnergyProduced").toDouble(); + // For the very first cycle (oldProductino is 0) we'll sync up on the meter, without actually adding it to our diff + if (oldThingProductionState == 0 && newThingProductionState != 0) { + qInfo(dcEnergyExperience()) << "Don't have a production counter for" << thing->name() << "Synching internal counter to initial value:" << newThingProductionState; + oldThingProductionState = newThingProductionState; + } + // If the thing's meter has been reset in the meantime (newProduction < oldProduction) we'll sync down, taking the whole diff from 0 to new value + if (newThingProductionState < oldThingProductionState) { + qCInfo(dcEnergyExperience()) << "Thing meter for" << thing->name() << "seems to have been reset. Re-synching internal production counter."; + oldThingProductionState = newThingProductionState; + } + double productionDiff = newThingProductionState - oldThingProductionState; + double newThingProductionInternal = oldThingProductionInternal + productionDiff; + m_thingsTotalEnergyProducedCache[thing] = qMakePair(newThingProductionState, newThingProductionInternal); + + + // Write to log + qCDebug(dcEnergyExperience()) << "Logging thing" << thing->name() << "total consumption:" << newThingConsumptionInternal << "production:" << newThingProductionInternal; + m_logger->logThingPower(thing->id(), thing->state("currentPower").value().toDouble(), newThingConsumptionInternal, newThingProductionInternal); + + // Cache the thing state values in case nymea is restarted + m_logger->cacheThingEntry(thing->id(), newThingConsumptionState, newThingProductionState); + } + }); + } +} + +void EnergyManagerImpl::unwatchThing(const ThingId &thingId) +{ + if (m_rootMeter && m_rootMeter->id() == thingId) { + m_rootMeter = nullptr; + emit rootMeterChanged(); + } + + m_logger->removeThingLogs(thingId); +} + +void EnergyManagerImpl::updatePowerBalance() +{ + double currentPowerAcquisition = 0; + if (m_rootMeter) { + currentPowerAcquisition = m_rootMeter->stateValue("currentPower").toDouble(); + + double oldAcquisition = m_powerBalanceTotalEnergyConsumedCache.value(m_rootMeter); + double newAcquisition = m_rootMeter->stateValue("totalEnergyConsumed").toDouble(); + // For the very first cycle (oldAcquisition is 0) we'll sync up on the meter values without actually adding them to our balance. + if (oldAcquisition == 0) { + oldAcquisition = newAcquisition; + } + // If the root meter has been reset in the meantime (newConsumption < oldConsumption) we'll sync down, taking the whole diff from 0 to new value + if (newAcquisition < oldAcquisition) { + qCInfo(dcEnergyExperience()) << "Root meter seems to have been reset. Re-synching internal consumption counter."; + oldAcquisition = newAcquisition; + } + qCDebug(dcEnergyExperience()) << "Root meter total consumption: Previous value:" << oldAcquisition << "New value:" << newAcquisition << "Diff:" << (newAcquisition -oldAcquisition); + m_totalAcquisition += newAcquisition - oldAcquisition; + m_powerBalanceTotalEnergyConsumedCache[m_rootMeter] = newAcquisition; + + double oldReturn = m_powerBalanceTotalEnergyProducedCache.value(m_rootMeter); + double newReturn = m_rootMeter->stateValue("totalEnergyProduced").toDouble(); + // For the very first cycle (oldReturn is 0) we'll sync up on the meter values without actually adding them to our balance. + if (oldReturn == 0) { + oldReturn = newReturn; + } + if (newReturn < oldReturn) { + qCInfo(dcEnergyExperience()) << "Root meter seems to have been reset. Re-synching internal production counter."; + oldReturn = newReturn; + } + qCDebug(dcEnergyExperience()) << "Root meter total production: Previous value:" << oldReturn << "New value:" << newReturn << "Diff:" << (newReturn - oldReturn); + m_totalReturn += newReturn - oldReturn; + m_powerBalanceTotalEnergyProducedCache[m_rootMeter] = newReturn; + } + + double currentPowerProduction = 0; + foreach (Thing* thing, m_thingManager->configuredThings().filterByInterface("smartmeterproducer")) { + currentPowerProduction += thing->stateValue("currentPower").toDouble(); + double oldProduction = m_powerBalanceTotalEnergyProducedCache.value(thing); + double newProduction = thing->stateValue("totalEnergyProduced").toDouble(); + // For the very first cycle (oldProduction is 0) we'll sync up on the producer values without actually adding them to our balance. + if (oldProduction == 0) { + oldProduction = newProduction; + } + if (newProduction < oldProduction) { + oldProduction = newProduction; + } + qCDebug(dcEnergyExperience()) << "Producer" << thing->name() << "total production: Previous value:" << oldProduction << "New value:" << newProduction << "Diff:" << (newProduction - oldProduction); + m_totalProduction += newProduction - oldProduction; + m_powerBalanceTotalEnergyProducedCache[thing] = newProduction; + } + + double currentPowerStorage = 0; + double totalFromStorage = 0; + foreach (Thing *thing, m_thingManager->configuredThings().filterByInterface("energystorage")) { + currentPowerStorage += thing->stateValue("currentPower").toDouble(); + double oldProduction = m_powerBalanceTotalEnergyProducedCache.value(thing); + double newProduction = thing->stateValue("totalEnergyProduced").toDouble(); + // For the very first cycle (oldProdction is 0) we'll sync up on the meter values without actually adding them to our balance. + if (oldProduction == 0) { + oldProduction = newProduction; + } + if (newProduction < oldProduction) { + oldProduction = newProduction; + } + qCDebug(dcEnergyExperience()) << "Storage" << thing->name() << "total storage: Previous value:" << oldProduction << "New value:" << newProduction << "Diff:" << (newProduction - oldProduction); + totalFromStorage += newProduction - oldProduction; + m_powerBalanceTotalEnergyProducedCache[thing] = newProduction; + } + + double currentPowerConsumption = currentPowerAcquisition + qAbs(qMin(0.0, currentPowerProduction)) - currentPowerStorage; + m_totalConsumption = m_totalAcquisition + m_totalProduction + totalFromStorage - m_totalReturn; + + qCDebug(dcEnergyExperience()).noquote().nospace() << "Power balance: " << "🔥: " << currentPowerConsumption << " W, 🌞: " << currentPowerProduction << " W, 💵: " << currentPowerAcquisition << " W, 🔋: " << currentPowerStorage << " W. Totals: 🔥: " << m_totalConsumption << " kWh, 🌞: " << m_totalProduction << " kWh, 💵↓: " << m_totalAcquisition << " kWh, 💵↑: " << m_totalReturn << " kWh"; + if (currentPowerAcquisition != m_currentPowerAcquisition + || currentPowerConsumption != m_currentPowerConsumption + || 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, m_totalConsumption, m_totalProduction, m_totalAcquisition, m_totalReturn); + } +} + +void EnergyManagerImpl::logDumpConsumers() +{ + foreach (Thing *consumer, m_thingManager->configuredThings().filterByInterface("smartmeterconsumer")) { + qCDebug(dcEnergyExperience()).nospace().noquote() << consumer->name() << ": " << (consumer->stateValue("currentPower").toDouble() / 230) << "A (" << consumer->stateValue("currentPower").toDouble() << "W)"; + } +} + + diff --git a/plugin/energymanagerimpl.h b/plugin/energymanagerimpl.h new file mode 100644 index 0000000..8964e9e --- /dev/null +++ b/plugin/energymanagerimpl.h @@ -0,0 +1,76 @@ +#ifndef ENERGYMANAGERIMPL_H +#define ENERGYMANAGERIMPL_H + +#include +#include +#include + +#include "integrations/thingmanager.h" + +#include "energymanager.h" + +class EnergyLogger; + +class EnergyManagerImpl : public EnergyManager +{ + Q_OBJECT +public: + explicit EnergyManagerImpl(ThingManager *thingManager, QObject *parent = nullptr); + + Thing *rootMeter() const override; + EnergyError setRootMeter(const ThingId &rootMeterId) override; + + double currentPowerConsumption() const override; + double currentPowerProduction() const override; + double currentPowerAcquisition() const override; + double currentPowerStorage() const override; + double totalConsumption() const override; + double totalProduction() const override; + double totalAcquisition() const override; + double totalReturn() const override; + + EnergyLogs* logs() const override; + +private: + void watchThing(Thing *thing); + void unwatchThing(const ThingId &thingId); + + void updatePowerBalance(); + +private slots: + void logDumpConsumers(); + +private: + ThingManager *m_thingManager = nullptr; + + Thing *m_rootMeter = nullptr; + + QTimer m_balanceUpdateTimer; + double m_currentPowerConsumption = 0; + double m_currentPowerProduction = 0; + double m_currentPowerAcquisition = 0; + double m_currentPowerStorage = 0; + double m_totalConsumption = 0; + double m_totalProduction = 0; + double m_totalAcquisition = 0; + double m_totalReturn = 0; + + EnergyLogger *m_logger = nullptr; + + // Caching some values so we don't have to look them up on the DB all the time: + // We use different caches for power balance and thing logs because they are calculated independently + // and one must not update the others cache for the diffs to be correct + + // For things totals we need to cache 2 values: + // The last thing state values we've processed + QHash m_powerBalanceTotalEnergyConsumedCache; + QHash m_powerBalanceTotalEnergyProducedCache; + + // - The last thing state value we've read and processed + // - The last entry in our internal counters we've processed and logged + // QHash> + QHash> m_thingsTotalEnergyConsumedCache; + QHash> m_thingsTotalEnergyProducedCache; +}; + +#endif // ENERGYMANAGERIMPL_H diff --git a/plugin/experiencepluginenergy.cpp b/plugin/experiencepluginenergy.cpp new file mode 100644 index 0000000..301281e --- /dev/null +++ b/plugin/experiencepluginenergy.cpp @@ -0,0 +1,119 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "experiencepluginenergy.h" + +#include "energymanagerimpl.h" +#include "energyjsonhandler.h" + +#include "energyplugin.h" + +#include +#include + +#include +#include +#include + +NYMEA_LOGGING_CATEGORY(dcEnergyExperience, "EnergyExperience") + +ExperiencePluginEnergy::ExperiencePluginEnergy() +{ + +} + +void ExperiencePluginEnergy::init() +{ + qCDebug(dcEnergyExperience()) << "Initializing energy experience"; + + m_energyManager = new EnergyManagerImpl(thingManager(), this); + jsonRpcServer()->registerExperienceHandler(new EnergyJsonHandler(m_energyManager, this), 1, 0); + + loadPlugins(); +} + +void ExperiencePluginEnergy::loadPlugins() +{ + foreach (const QString &path, pluginSearchDirs()) { + QDir dir(path); + qCDebug(dcEnergyExperience()) << "Loading energy plugins from:" << dir.absolutePath(); + foreach (const QString &entry, dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot)) { + QFileInfo fi(path + "/" + entry); + if (fi.isFile()) { + if (entry.startsWith("libnymea_energyplugin") && entry.endsWith(".so")) { + loadEnergyPlugin(path + "/" + entry); + } + } else if (fi.isDir()) { + if (QFileInfo::exists(path + "/" + entry + "/libnymea_energyplugin" + entry + ".so")) { + loadEnergyPlugin(path + "/" + entry + "/libnymea_energyplugin" + entry + ".so"); + } + } + } + } +} + +QStringList ExperiencePluginEnergy::pluginSearchDirs() const +{ + QStringList searchDirs; + QByteArray envPath = qgetenv("NYMEA_ENERGY_PLUGINS_PATH"); + if (!envPath.isEmpty()) { + searchDirs << QString(envPath).split(':'); + } + + foreach (QString libraryPath, QCoreApplication::libraryPaths()) { + searchDirs << libraryPath.replace("qt5", "nymea").replace("plugins", "energy"); + } + searchDirs << QCoreApplication::applicationDirPath() + "/../lib/nymea/energy"; + searchDirs << QCoreApplication::applicationDirPath() + "/../energy/"; + searchDirs << QCoreApplication::applicationDirPath() + "/../../../energy/"; + return searchDirs; +} + +void ExperiencePluginEnergy::loadEnergyPlugin(const QString &file) +{ + QPluginLoader loader; + loader.setFileName(file); + loader.setLoadHints(QLibrary::ResolveAllSymbolsHint); + if (!loader.load()) { + qCWarning(dcExperiences()) << loader.errorString(); + return; + } + EnergyPlugin *plugin = qobject_cast(loader.instance()); + if (!plugin) { + qCWarning(dcEnergyExperience()) << "Could not get plugin instance of" << loader.fileName(); + loader.unload(); + return; + } + qCDebug(dcEnergyExperience()) << "Loaded energy plugin:" << loader.fileName(); + m_plugins.append(plugin); + plugin->setParent(this); + plugin->initPlugin(m_energyManager, thingManager(), jsonRpcServer()); + +} diff --git a/plugin/experiencepluginenergy.h b/plugin/experiencepluginenergy.h new file mode 100644 index 0000000..cd0d911 --- /dev/null +++ b/plugin/experiencepluginenergy.h @@ -0,0 +1,64 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef EXPERIENCEPLUGINENERGY_H +#define EXPERIENCEPLUGINENERGY_H + +#include + +#include "energyplugin.h" + +#include + +Q_DECLARE_LOGGING_CATEGORY(dcEnergyExperience) + +class EnergyManagerImpl; + +class ExperiencePluginEnergy: public ExperiencePlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.nymea.ExperiencePlugin") + Q_INTERFACES(ExperiencePlugin) + +public: + ExperiencePluginEnergy(); + void init() override; + +private: + QStringList pluginSearchDirs() const; + void loadPlugins(); + void loadEnergyPlugin(const QString &file); + + QList m_plugins; + + EnergyManagerImpl *m_energyManager = nullptr; +}; + +#endif // EXPERIENCEPLUGINENERGY_H diff --git a/plugin/plugin.pro b/plugin/plugin.pro new file mode 100644 index 0000000..8370364 --- /dev/null +++ b/plugin/plugin.pro @@ -0,0 +1,44 @@ +TEMPLATE = lib +TARGET = $$qtLibraryTarget(nymea_experiencepluginenergy) + +CONFIG += plugin link_pkgconfig c++11 +PKGCONFIG += nymea + +QT -= gui +QT += network sql + +include(../config.pri) + +INCLUDEPATH += $$top_srcdir/libnymea-energy +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/ +INSTALLS += target + +# Install translation files +TRANSLATIONS *= $$files($${_PRO_FILE_PWD_}/translations/*ts, true) +lupdate.depends = FORCE +lupdate.depends += qmake_all +lupdate.commands = lupdate -recursive -no-obsolete $${_PRO_FILE_PWD_}/experience.pro +QMAKE_EXTRA_TARGETS += lupdate + +# make lrelease to build .qm from .ts +lrelease.depends = FORCE +lrelease.commands += lrelease $$files($$_PRO_FILE_PWD_/translations/*.ts, true); +QMAKE_EXTRA_TARGETS += lrelease + +translations.depends += lrelease +translations.path = /usr/share/nymea/translations +translations.files = $$[QT_SOURCE_TREE]/translations/*.qm +INSTALLS += translations +